[
  {
    "path": ".codecov.yml",
    "content": "'coverage':\n  'status':\n    'project':\n      'default':\n        'target': '40%'\n        'threshold': null\n    'patch': false\n    'changes': false\n"
  },
  {
    "path": ".gitattributes",
    "content": "client/* linguist-vendored\n# This file contains a lot of inline SVG data, which often interferes with\n# grepping.  Technically, this file must be reviewed when new icons appear, but\n# that happens fairly rarely.\nclient/src/components/ui/Icons.js -diff\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "'body':\n  - 'attributes':\n        'description': >\n            Please make sure that the issue is not a duplicate or a question.\n            If it's a duplicate, please react to the original issue with a\n            thumbs up.  If it's a question, please post it to the GitHub\n            Discussions page.\n        'label': 'Prerequisites'\n        'options':\n          - 'label': >\n                I have checked the\n                [Wiki](https://github.com/AdguardTeam/AdGuardHome/wiki) and\n                [Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)\n                and found no answer\n            'required': true\n          - 'label': >\n                I have searched other issues and found no duplicates\n            'required': true\n          - 'label': >\n                I want to report a bug and not [ask a question or ask for\n                help](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)\n            'required': true\n          - 'label': >\n                I have set up AdGuard Home correctly and [configured clients to\n                use it](https://github.com/AdguardTeam/AdGuardHome/wiki/Clients).\n                (Use the\n                [Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)\n                for help with installing and configuring clients.)\n            'required': true\n    'id': 'prerequisites'\n    'type': 'checkboxes'\n  - 'attributes':\n        'description': 'On which Platform does the issue occur?'\n        'label': 'Platform (OS and CPU architecture)'\n        # NOTE: Keep the 386 at the bottom for each OS, because a lot of people\n        # Seem to confuse them with AMD64, which is what they actually need.\n        'options':\n          - 'Darwin (aka macOS), AMD64 (aka x86_64)'\n          - 'Darwin (aka macOS), ARM64'\n          - 'FreeBSD, AMD64 (aka x86_64)'\n          - 'FreeBSD, ARM64'\n          - 'FreeBSD, ARMv5'\n          - 'FreeBSD, ARMv6'\n          - 'FreeBSD, ARMv7'\n          - 'FreeBSD, 32-bit Intel (aka 386)'\n          - 'Linux, AMD64 (aka x86_64)'\n          - 'Linux, ARM64'\n          - 'Linux, ARMv5'\n          - 'Linux, ARMv6'\n          - 'Linux, ARMv7'\n          - 'Linux, MIPS LE'\n          - 'Linux, MIPS'\n          - 'Linux, MIPS64 LE'\n          - 'Linux, MIPS64'\n          - 'Linux, PPC64 LE'\n          - 'Linux, 32-bit Intel (aka 386)'\n          - 'OpenBSD, AMD64 (aka x86_64)'\n          - 'OpenBSD, ARM64'\n          - 'Windows, AMD64 (aka x86_64)'\n          - 'Windows, ARM64'\n          - 'Windows, 32-bit Intel (aka 386)'\n          - 'Custom (please mention in the description)'\n    'id': 'os'\n    'type': 'dropdown'\n    'validations':\n        'required': true\n  - 'attributes':\n        'description': 'How did you install AdGuard Home?'\n        'label': 'Installation'\n        'options':\n          - 'GitHub releases or script from README'\n          - 'Docker'\n          - 'Snapcraft'\n          - 'Custom package (OpenWrt, HomeAssistant, etc; please mention in the description)'\n          - 'Other (please mention in the description)'\n    'id': 'install'\n    'type': 'dropdown'\n    'validations':\n        'required': true\n  - 'attributes':\n        'description': 'How did you setup AdGuard Home?'\n        'label': 'Setup'\n        'options':\n          - 'On one machine'\n          - 'On a router, DHCP is handled by the router'\n          - 'On a router, DHCP is handled by AdGuard Home'\n          - 'Other (please mention in the description)'\n    'id': 'setup'\n    'type': 'dropdown'\n    'validations':\n        'required': true\n  - 'attributes':\n        'description': 'What version of AdGuard Home are you using?'\n        'label': 'AdGuard Home version'\n    'id': 'version'\n    'type': 'input'\n    'validations':\n        'required': true\n  - 'attributes':\n        'description': >\n            Please describe what you did.  An `nslookup` or a `dig` command is\n            the best way.  For crashes, please provide a full failure log.\n        'label': 'Action'\n        'value': |\n            Replace the following command with the one you're calling or a\n            description of the failing action:\n\n            ```sh\n            nslookup -debug -type=a 'www.example.com' '$YOUR_AGH_ADDRESS'\n            ```\n    'id': 'failing_action'\n    'type': 'textarea'\n    'validations':\n        'required': true\n  - 'attributes':\n        'description': >\n            What did you expect to see?  Please add a description and/or\n            screenshots, if applicable.\n        'label': 'Expected result'\n        'placeholder': >\n            What did you expect to see?\n    'id': 'expected'\n    'type': 'textarea'\n    'validations':\n        'required': true\n  - 'attributes':\n        'description': >\n            What happened instead?  Please add a description and/or screenshots,\n            if applicable.\n        'label': 'Actual result'\n        'placeholder': >\n            What did you see instead?\n    'id': 'result'\n    'type': 'textarea'\n    'validations':\n        'required': true\n  - 'attributes':\n        'description': >\n            Please add additional information, such as non-standard OS or port,\n            here.  You can also put screenshots here, if applicable.  For\n            example, it is better to copy and paste text from a terminal instead\n            of posting a screenshot of the terminal.\n        'label': 'Additional information and/or screenshots'\n        'placeholder': >\n            Additional OS information, screenshots of the UI, etc.\n    'id': 'additional'\n    'type': 'textarea'\n    'validations':\n        'required': false\n# NOTE: GitHub limits the description length to 200 characters.  Also, Markdown\n# doesn't work here.\n'description': |\n    For help, use the Discussions section instead.  Write the title in English\n    to make it easier for other people to search for duplicates.  (Any language\n    is fine in the body.)\n'name': 'Bug'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "'blank_issues_enabled': false\n'contact_links':\n  - 'about': >\n        Please report filtering issues, for example advertising filters\n        misfiring or safe browsing false positives, using the form on our\n        website\n    'name': 'AdGuard filters issues'\n    'url': 'https://link.adtidy.org/forward.html?action=report&app=home&from=github'\n  - 'about': >\n        Please send requests for new blocked services and vetted filtering lists\n        to the Hostlists Registry repository\n    'name': 'Blocked services and vetted filtering rule lists: AdGuard Hostlists Registry'\n    'url': 'https://github.com/AdguardTeam/HostlistsRegistry'\n  - 'about': >\n        Please use GitHub Discussions for questions\n    'name': 'Q&A Discussions'\n    'url': 'https://github.com/AdguardTeam/AdGuardHome/discussions'\n  - 'about': >\n        Please check our Wiki for configuration file description, frequently\n        asked questions, and other documentation\n    'name': 'Wiki'\n    'url': 'https://github.com/AdguardTeam/AdGuardHome/wiki'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "'body':\n  - 'attributes':\n        'description': >\n            Please make sure that the issue is not a duplicate or a question.\n            If it's a duplicate, please react to the original issue with a\n            thumbs up.  If it's a question, please post it to the GitHub\n            Discussions page.\n        'label': 'Prerequisites'\n        'options':\n          - 'label': >\n                I have checked the\n                [Wiki](https://github.com/AdguardTeam/AdGuardHome/wiki) and\n                [Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions)\n                and found no answer\n            'required': true\n          - 'label': >\n                I have searched other issues and found no duplicates\n            'required': true\n          - 'label': >\n                I want to request a feature or enhancement and not ask a\n                question\n            'required': true\n    'id': 'prerequisites'\n    'type': 'checkboxes'\n  - 'attributes':\n        'description': 'Please describe the problem you are trying to solve'\n        'label': 'The problem'\n        'placeholder': >\n            Please describe the problem you are trying to solve\n    'id': 'problem'\n    'type': 'textarea'\n    'validations':\n        'required': true\n  - 'attributes':\n        'description': 'What feature are you proposing to solve this problem?'\n        'label': 'Proposed solution'\n        'placeholder': >\n            What feature are you proposing to solve this problem?\n    'id': 'proposed_solution'\n    'type': 'textarea'\n    'validations':\n        'required': true\n  - 'attributes':\n        'label': 'Alternatives considered and additional information'\n        'placeholder': >\n            Are there any other ways to solve the problem?\n    'id': 'additional'\n    'type': 'textarea'\n    'validations':\n        'required': false\n# NOTE: GitHub limits the description length to 200 characters.  Also, Markdown\n# doesn't work here.\n'description': |\n    Write the title in English to make it easier for other people to search for\n    duplicates.  (Any language is fine in the body.)\n'labels':\n  - 'feature request'\n'name': 'Feature request or enhancement'\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE",
    "content": "Before submitting a PR please make sure that:\n\n1.  You have discussed your solution in an issue and have got an\n    approval from a maintainer.  See our\n    [contribution guide](https://github.com/AdguardTeam/AdGuardHome/blob/master/CONTRIBUTING.md).\n\n2.  This isn't a localization fix; please send those to our\n    [CrowdIn](https://crowdin.com/project/adguard-applications/en#/adguard-home)\n    page.\n\n3.  Your code follows our\n    [code guidelines](https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md).\n\nAdd a short description here.  The description should include:\n\n1.  Which issue this PR closes (`Closes #NNNN.`) or updates (`Updates\n    #NNNN.`).  Please do not open PRs without filing an issue first.\n\n2.  A short description of how the change achieves that.\n\nDo not forget to remove these instructions!\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale.\n'daysUntilStale': 90\n# Number of days of inactivity before a stale issue is closed.\n'daysUntilClose': 15\n# Issues with these labels will never be considered stale.\n'exemptLabels':\n  - 'bug'\n  - 'documentation'\n  - 'enhancement'\n  - 'feature request'\n  - 'help wanted'\n  - 'localization'\n  - 'needs investigation'\n  - 'recurrent'\n  - 'research'\n# Set to true to ignore issues in a milestone.\n'exemptMilestones': true\n# Label to use when marking an issue as stale.\n'staleLabel': 'wontfix'\n# Comment to post when marking an issue as stale. Set to `false` to disable.\n'markComment': >\n  This issue has been automatically marked as stale because it has not had\n  recent activity.  It will be closed if no further activity occurs.  Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable.\n'closeComment': false\n# Limit the number of actions per hour.\n'limitPerRun': 1\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "'name': 'build'\n\n'env':\n  'GO_VERSION': '1.26.1'\n  'NODE_VERSION': '20'\n\n'on':\n  'push':\n    'branches':\n    - '*'\n    'tags':\n    - 'v*'\n  'pull_request':\n\n'jobs':\n  'test':\n    'runs-on': '${{ matrix.os }}'\n    'env':\n      'GO111MODULE': 'on'\n      'GOPROXY': 'https://goproxy.cn'\n    'strategy':\n      'fail-fast': false\n      'matrix':\n        'os':\n        - 'ubuntu-latest'\n        - 'macOS-latest'\n        - 'windows-latest'\n    'steps':\n    - 'name': 'Checkout'\n      'uses': 'actions/checkout@v2'\n      'with':\n        'fetch-depth': 0\n    - 'name': 'Set up Go'\n      'uses': 'actions/setup-go@v3'\n      'with':\n        'go-version': '${{ env.GO_VERSION }}'\n    - 'name': 'Set up Node'\n      'uses': 'actions/setup-node@v1'\n      'with':\n        'node-version': '${{ env.NODE_VERSION }}'\n    - 'name': 'Set up Go modules cache'\n      'uses': 'actions/cache@v4'\n      'with':\n        'path': '~/go/pkg/mod'\n        'key': \"${{ runner.os }}-go-${{ hashFiles('go.sum') }}\"\n        'restore-keys': '${{ runner.os }}-go-'\n    - 'name': 'Get npm cache directory'\n      'id': 'npm-cache'\n      'run': 'echo \"::set-output name=dir::$( npm config get cache )\"'\n    - 'name': 'Set up npm cache'\n      'uses': 'actions/cache@v4'\n      'with':\n        'path': '${{ steps.npm-cache.outputs.dir }}'\n        'key': \"${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }}\"\n        'restore-keys': '${{ runner.os }}-node-'\n    - 'name': 'Run tests'\n      'shell': 'bash'\n      'run': 'make VERBOSE=1 deps test go-bench go-fuzz'\n    - 'name': 'Upload coverage'\n      'uses': 'codecov/codecov-action@v1'\n      'if': \"success() && matrix.os == 'ubuntu-latest'\"\n      'with':\n        'token': '${{ secrets.CODECOV_TOKEN }}'\n        'file': './coverage.txt'\n\n  'build-release':\n    'runs-on': 'ubuntu-latest'\n    'needs': 'test'\n    'steps':\n    - 'name': 'Checkout'\n      'uses': 'actions/checkout@v2'\n      'with':\n        'fetch-depth': 0\n    - 'name': 'Set up Go'\n      'uses': 'actions/setup-go@v3'\n      'with':\n        'go-version': '${{ env.GO_VERSION }}'\n    - 'name': 'Set up Node'\n      'uses': 'actions/setup-node@v1'\n      'with':\n        'node-version': '${{ env.NODE_VERSION }}'\n    - 'name': 'Set up Go modules cache'\n      'uses': 'actions/cache@v4'\n      'with':\n        'path': '~/go/pkg/mod'\n        'key': \"${{ runner.os }}-go-${{ hashFiles('go.sum') }}\"\n        'restore-keys': '${{ runner.os }}-go-'\n    - 'name': 'Get npm cache directory'\n      'id': 'npm-cache'\n      'run': 'echo \"::set-output name=dir::$(npm config get cache)\"'\n    - 'name': 'Set up npm cache'\n      'uses': 'actions/cache@v4'\n      'with':\n        'path': '${{ steps.npm-cache.outputs.dir }}'\n        'key': \"${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }}\"\n        'restore-keys': '${{ runner.os }}-node-'\n    - 'name': 'Set up Snapcraft'\n      'run': 'sudo snap install snapcraft --classic'\n    - 'name': 'Set up QEMU'\n      'uses': 'docker/setup-qemu-action@v3'\n    - 'name': 'Set up Docker Buildx'\n      'uses': 'docker/setup-buildx-action@v3'\n      'with':\n        'install': true\n    - 'name': 'Run snapshot build'\n      # Set a custom version string, since the checkout@v2 action does not seem\n      # to know about the master branch, while the version script uses it to\n      # count the number of commits within the branch.\n      'run': 'make SIGN=0 VERBOSE=1 VERSION=\"v0.0.0-github\" build-release build-docker'\n\n  'notify':\n    'needs':\n    - 'build-release'\n    # Secrets are not passed to workflows that are triggered by a pull request\n    # from a fork.\n    #\n    # Use always() to signal to the runner that this job must run even if the\n    # previous ones failed.\n    'if':\n      ${{\n        always() &&\n        github.repository_owner == 'AdguardTeam' &&\n        (\n          github.event_name == 'push' ||\n          github.event.pull_request.head.repo.full_name == github.repository\n        )\n      }}\n    'runs-on': 'ubuntu-latest'\n    'steps':\n    - 'name': 'Conclusion'\n      'uses': 'technote-space/workflow-conclusion-action@v1'\n    - 'name': 'Send Slack notif'\n      'uses': '8398a7/action-slack@v3'\n      'with':\n        'status': '${{ env.WORKFLOW_CONCLUSION }}'\n        'fields': 'repo, message, commit, author, workflow'\n      'env':\n        'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}'\n        'SLACK_WEBHOOK_URL': '${{ secrets.SLACK_WEBHOOK_URL }}'\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "'name': 'lint'\n\n'env':\n  'GO_VERSION': '1.26.1'\n\n'on':\n  'push':\n    'tags':\n    - 'v*'\n    'branches':\n    - '*'\n  'pull_request':\n\n'jobs':\n  'go-lint':\n    'runs-on': 'ubuntu-latest'\n    'steps':\n    - 'uses': 'actions/checkout@v2'\n    - 'name': 'Set up Go'\n      'uses': 'actions/setup-go@v3'\n      'with':\n        'go-version': '${{ env.GO_VERSION }}'\n    - 'name': 'run-lint'\n      'run': >\n        make go-deps go-lint\n\n  'eslint':\n    'runs-on': 'ubuntu-latest'\n    'steps':\n    - 'uses': 'actions/checkout@v2'\n    - 'name': 'Install modules'\n      'run': 'npm --prefix=\"./client\" ci'\n    - 'name': 'Run ESLint'\n      'run': 'npm --prefix=\"./client\" run lint'\n\n  'notify':\n    'needs':\n    - 'go-lint'\n    - 'eslint'\n    # Secrets are not passed to workflows that are triggered by a pull request\n    # from a fork.\n    #\n    # Use always() to signal to the runner that this job must run even if the\n    # previous ones failed.\n    'if':\n      ${{\n        always() &&\n        github.repository_owner == 'AdguardTeam' &&\n        (\n          github.event_name == 'push' ||\n          github.event.pull_request.head.repo.full_name == github.repository\n        )\n      }}\n    'runs-on': 'ubuntu-latest'\n    'steps':\n    - 'name': 'Conclusion'\n      'uses': 'technote-space/workflow-conclusion-action@v1'\n    - 'name': 'Send Slack notif'\n      'uses': '8398a7/action-slack@v3'\n      'with':\n        'status': '${{ env.WORKFLOW_CONCLUSION }}'\n        'fields': 'repo, message, commit, author, workflow'\n      'env':\n        'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}'\n        'SLACK_WEBHOOK_URL': '${{ secrets.SLACK_WEBHOOK_URL }}'\n"
  },
  {
    "path": ".github/workflows/potential-duplicates.yml",
    "content": "'name': 'potential-duplicates'\n'on':\n    'issues':\n        'types':\n          - 'opened'\n'jobs':\n    'run':\n        'runs-on': 'ubuntu-latest'\n        'steps':\n          - 'uses': 'wow-actions/potential-duplicates@v1'\n            'with':\n                'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}'\n                'state': 'all'\n                'threshold': 0.6\n                'comment': |\n                    Potential duplicates: {{#issues}}\n                     *  [#{{ number }}] {{ title }} ({{ accuracy }}%)\n                    {{/issues}}\n"
  },
  {
    "path": ".gitignore",
    "content": "# This comment is used to simplify checking local copies of the file.  Bump\n# this number every time a significant change is made to this file.\n#\n# AdGuard-Project-Version: 1\n\n# Please, DO NOT put your text editors' temporary files here.  The more are\n# added, the harder it gets to maintain and manage projects' gitignores.  Put\n# them into your global gitignore file instead.\n#\n# See https://stackoverflow.com/a/7335487/1892060.\n#\n# Only build, run, and test outputs here.  Sorted.  With negations at the\n# bottom to make sure they take effect.\n*.db\n*.log\n*.out\n*.snap\n*.test\n/agh-backup/\n/bin/\n/build/*\n/client/blob-report/\n/client/playwright-report/\n/client/playwright/.cache/\n/client/test-results/\n/data/\n/dist/\n/filtering/tests/filtering.TestLotsOfRules*.pprof\n/filtering/tests/top-1m.csv\n/internal/next/AdGuardHome.yaml\n/launchpad_credentials\n/querylog.json*\n/snapcraft_login\n/test-reports/\nAdGuardHome\nAdGuardHome.exe\nAdGuardHome.yaml*\ncoverage.txt\nnode_modules/\ntmp/\n\n!/build/gitkeep\n"
  },
  {
    "path": ".markdownlint.json",
    "content": "{\n  \"ul-indent\": {\n    \"indent\": 4\n  },\n  \"ul-style\": {\n    \"style\": \"dash\"\n  },\n  \"emphasis-style\": {\n    \"style\": \"asterisk\"\n  },\n  \"no-duplicate-heading\": {\n    \"siblings_only\": true\n  },\n  \"no-inline-html\": {\n    \"allowed_elements\": [\n      \"a\"\n    ]\n  },\n  \"no-trailing-spaces\": {\n    \"br_spaces\": 0\n  },\n  \"line-length\": false,\n  \"no-bare-urls\": false,\n  \"no-emphasis-as-heading\": false,\n  \"link-fragments\": false\n}\n"
  },
  {
    "path": ".twosky.json",
    "content": "[\n  {\n    \"project_id\": \"home\",\n    \"base_locale\": \"en\",\n    \"localizable_files\": [\n      \"client/src/__locales/en.json\"\n    ],\n    \"languages\": {\n      \"ar\": \"العربية\",\n      \"be\": \"Беларуская\",\n      \"bg\": \"Български\",\n      \"cs\": \"Český\",\n      \"da\": \"Dansk\",\n      \"de\": \"Deutsch\",\n      \"en\": \"English\",\n      \"es\": \"Español\",\n      \"fa\": \"فارسی\",\n      \"fi\": \"Suomi\",\n      \"fr\": \"Français\",\n      \"hr\": \"Hrvatski\",\n      \"hu\": \"Magyar\",\n      \"id\": \"Indonesian\",\n      \"it\": \"Italiano\",\n      \"ja\": \"日本語\",\n      \"ko\": \"한국어\",\n      \"nl\": \"Nederlands\",\n      \"no\": \"Norsk\",\n      \"pl\": \"Polski\",\n      \"pt-br\": \"Português (BR)\",\n      \"pt-pt\": \"Português (PT)\",\n      \"ro\": \"Română\",\n      \"ru\": \"Русский\",\n      \"si-lk\": \"සිංහල\",\n      \"sk\": \"Slovenčina\",\n      \"sl\": \"Slovenščina\",\n      \"sr-cs\": \"Srpski\",\n      \"sv\": \"Svenska\",\n      \"th\": \"ภาษาไทย\",\n      \"tr\": \"Türkçe\",\n      \"uk\": \"Українська\",\n      \"vi\": \"Tiếng Việt\",\n      \"zh-cn\": \"简体中文\",\n      \"zh-hk\": \"繁體中文（香港）\",\n      \"zh-tw\": \"正體中文（台灣）\"\n    }\n  },\n  {\n    \"project_id\": \"hostlists-registry\",\n    \"base_locale\": \"en\",\n    \"localizable_files\": [\n      \"client/src/__locales-services/services.json\"\n    ],\n    \"languages\": {\n      \"ar\": \"العربية\",\n      \"be\": \"Беларуская\",\n      \"bg\": \"Български\",\n      \"cs\": \"Český\",\n      \"da\": \"Dansk\",\n      \"de\": \"Deutsch\",\n      \"en\": \"English\",\n      \"es\": \"Español\",\n      \"fa\": \"فارسی\",\n      \"fi\": \"Suomi\",\n      \"fr\": \"Français\",\n      \"hr\": \"Hrvatski\",\n      \"hu\": \"Magyar\",\n      \"id\": \"Indonesian\",\n      \"it\": \"Italiano\",\n      \"ja\": \"日本語\",\n      \"ko\": \"한국어\",\n      \"nl\": \"Nederlands\",\n      \"no\": \"Norsk\",\n      \"pl\": \"Polski\",\n      \"pt-br\": \"Português (BR)\",\n      \"pt-pt\": \"Português (PT)\",\n      \"ro\": \"Română\",\n      \"ru\": \"Русский\",\n      \"si-lk\": \"සිංහල\",\n      \"sk\": \"Slovenčina\",\n      \"sl\": \"Slovenščina\",\n      \"sr-cs\": \"Srpski\",\n      \"sv\": \"Svenska\",\n      \"th\": \"ภาษาไทย\",\n      \"tr\": \"Türkçe\",\n      \"uk\": \"Українська\",\n      \"vi\": \"Tiếng Việt\",\n      \"zh-cn\": \"简体中文\",\n      \"zh-hk\": \"繁體中文（香港）\",\n      \"zh-tw\": \"正體中文（台灣）\"\n    }\n  }\n]\n"
  },
  {
    "path": "AGHTechDoc.md",
    "content": "# AdGuard Home Technical Document\n\nThe document describes technical details and internal algorithms of AdGuard Home.\n\nContents:\n* First startup\n* Installation wizard\n\t* \"Get install settings\" command\n\t* \"Check configuration\" command\n\t* Disable DNSStubListener\n\t* \"Apply configuration\" command\n* Updating\n\t* Get version command\n\t* Update command\n* API: Get global status\n* TLS\n\t* API: Get TLS configuration\n\t* API: Set TLS configuration\n* Device Names and Per-client Settings\n\t* Per-client settings\n\t* Get list of clients\n\t* Add client\n\t* Update client\n\t* Delete client\n\t* API: Find clients by IP\n* DHCP server\n\t* DHCP server in DNS\n\t* DHCP Custom Options\n\t* API: Show DHCP interfaces\n\t* API: Show DHCP status\n\t* API: Check DHCP\n\t* API: Enable DHCP\n\t* Static IP check/set\n\t* API: Add a static lease\n\t* API: Reset DHCP configuration\n\t* RA+SLAAC\n* DNS general settings\n\t* API: Get DNS general settings\n\t* API: Set DNS general settings\n* DNS access settings\n\t* List access settings\n\t* Set access settings\n* Rewrites\n\t* API: List rewrite entries\n\t* API: Add a rewrite entry\n\t* API: Remove a rewrite entry\n* Services Filter\n\t* API: Get blocked services list\n\t* API: Set blocked services list\n* Statistics\n\t* API: Get statistics data\n\t* API: Clear statistics data\n\t* API: Set statistics parameters\n\t* API: Get statistics parameters\n* Query logs\n\t* API: Get query log\n\t* API: Set querylog parameters\n\t* API: Get querylog parameters\n* Filtering\n\t* Filters update mechanism\n\t* API: Get filtering parameters\n\t* API: Set filtering parameters\n\t* API: Refresh filters\n\t* API: Add Filter\n\t* API: Set URL parameters\n\t* API: Delete URL\n\t* API: Domain Check\n* Log-in page\n\t* API: Log in\n\t* API: Log out\n\t* API: Get current user info\n* Safe services\n* ipset\n\n\n## Relations between subsystems\n\n![](doc/agh-arch.png)\n\n\n## First startup\n\nThe first application startup is detected when there's no .yaml configuration file.\n\nWe check if the user is root, otherwise we fail with an error.\n\nWeb server is started up on port 3000 and automatically redirects requests to `/` to Installation wizard.\n\nAfter Installation wizard steps are completed, we write configuration to a file and start normal operation.\n\n\n## Installation wizard\n\nThis is the collection of UI screens that are shown to a user on first application startup.\n\nThe screens are:\n\n1. Welcome\n2. Set up network interface and listening ports for Web and DNS servers\n3. Set up administrator username and password\n4. Configuration complete\n5. Done\n\nAlgorithm:\n\nScreen 2:\n* UI asks server for initial information and shows it\n* User edits the default settings, clicks on \"Next\" button\n* UI asks server to check new settings\n* Server searches for the known issues\n* UI shows information about the known issues and the means to fix them\n* Server applies automatic fixes of the known issues on command from UI\n\nScreen 3:\n* UI asks server to apply the configuration\n* Server restarts DNS server\n\n\n### \"Get install settings\" command\n\nRequest:\n\n\tGET /control/install/get_addresses\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\"web_port\":80,\n\t\"dns_port\":53,\n\t\"interfaces\":{\n\t\t\"enp2s0\":{\"name\":\"enp2s0\",\"mtu\":1500,\"hardware_address\":\"\",\"ip_addresses\":[\"\",\"\"],\"flags\":\"up|broadcast|multicast\"},\n\t\t\"lo\":{\"name\":\"lo\",\"mtu\":65536,\"hardware_address\":\"\",\"ip_addresses\":[\"127.0.0.1\",\"::1\"],\"flags\":\"up|loopback\"},\n\t}\n\t}\n\nIf `interfaces.flags` doesn't contain `up` flag, UI must show `(Down)` status next to its IP address in interfaces selector.\n\n\n### \"Check configuration\" command\n\nRequest:\n\n\tPOST /control/install/check_config\n\n\t{\n\t\"web\":{\"port\":80,\"ip\":\"192.168.11.33\"},\n\t\"dns\":{\"port\":53,\"ip\":\"127.0.0.1\",\"autofix\":false},\n\t\"set_static_ip\": true | false\n\t}\n\nServer should check whether a port is available only in case it itself isn't already listening on that port.\n\nIf `set_static_ip` is `true`, Server attempts to set a static IP for the network interface chosen by `dns.ip` setting.  If the operation is successful, `static_ip.static` setting will be `yes`.  If it fails, `static_ip.static` setting will be set to `error` and `static_ip.error` will contain the error message.\n\nServer replies on success:\n\n\t200 OK\n\n\t{\n\t\"web\":{\"status\":\"\"},\n\t\"dns\":{\"status\":\"\"},\n\t\"static_ip\": {\n\t\t\"static\": \"yes|no|error\",\n\t\t\"ip\": \"<Current dynamic IP address>\", // set if static=no\n\t\t\"error\": \"...\" // set if static=error\n\t}\n\t}\n\nIf `static_ip.static` is `no`, Server has detected that the system uses a dynamic address and it can  automatically set a static address if `set_static_ip` in request is `true`.  See section `Static IP check/set` for detailed process.\n\nServer replies on error:\n\n\t200 OK\n\n\t{\n\t\"web\":{\"status\":\"ERROR MESSAGE\"},\n\t\"dns\":{\"status\":\"ERROR MESSAGE\", \"can_autofix\": true|false},\n\t}\n\n\n### Disable DNSStubListener\n\nOn Linux, if 53 port is not available, server performs several additional checks to determine if the issue can be fixed automatically.\n\n#### Phase 1\n\nRequest:\n\n\tPOST /control/install/check_config\n\n\t{\n\t\"dns\":{\n\t\t\"port\":53,\n\t\t\"ip\":\"127.0.0.1\",\n\t\t\"autofix\":false\n\t}\n\t}\n\nCheck if DNSStubListener is enabled:\n\n\tsystemctl is-enabled systemd-resolved\n\nCheck if DNSStubListener is active:\n\n\tgrep -E '#?DNSStubListener=yes' /etc/systemd/resolved.conf\n\nIf the issue can be fixed automatically, server replies with `\"can_autofix\":true`\n\n\t200 OK\n\n\t{\n\t\"dns\":{\"status\":\"ERROR MESSAGE\", \"can_autofix\":true},\n\t}\n\nIn this case UI shows \"Fix\" button next to error message.\n\n#### Phase 2\n\nIf user clicks on \"Fix\" button, UI sends request to perform an automatic fix\n\n\tPOST /control/install/check_config\n\n\t{\n\t\"dns\":{\"port\":53,\"ip\":\"127.0.0.1\",\"autofix\":true},\n\t}\n\nDeactivate DNSStubListener and update DNS server address.  Create a new file: `/etc/systemd/resolved.conf.d/adguardhome.conf` (create a `/etc/systemd/resolved.conf.d` directory if necessary):\n\n\t[Resolve]\n\tDNS=127.0.0.1\n\tDNSStubListener=no\n\nSpecifying \"127.0.0.1\" as DNS server address is necessary because otherwise the nameserver will be \"127.0.0.53\" which doesn't work without DNSStubListener.\n\nActivate another resolv.conf file:\n\n\tmv /etc/resolv.conf /etc/resolv.conf.backup\n\tln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf\n\nStop DNSStubListener:\n\n\tsystemctl reload-or-restart systemd-resolved\n\nServer replies:\n\n\t200 OK\n\n\t{\n\t\"dns\":{\"status\":\"\"},\n\t}\n\n\n### \"Apply configuration\" command\n\nRequest:\n\n\tPOST /control/install/configure\n\n\t{\n\t\"web\":{\"port\":80,\"ip\":\"192.168.11.33\"},\n\t\"dns\":{\"port\":53,\"ip\":\"127.0.0.1\"},\n\t\"username\":\"u\",\n\t\"password\":\"p\",\n\t}\n\nServer checks the parameters once again, restarts DNS server, replies:\n\n\t200 OK\n\nOn error, server responds with code 400 or 500.  In this case UI should show error message and reset to the beginning.\n\n\t400 Bad Request\n\n\tERROR MESSAGE\n\n\n## Updating\n\nAlgorithm of an update by command:\n\n* UI requests the latest version information from Server\n* Server requests information from Internet;  stores the data in cache for several hours;  sends data to UI\n* If UI sees that a new version is available, it shows notification message and \"Update Now\" button\n* When user clicks on \"Update Now\" button, UI sends Update command to Server\n* UI shows \"Please wait, AGH is being updated...\" message\n* Server performs an update:\n\t* Use working directory from `--work-dir` if necessary\n\t* Download new package for the current OS and CPU\n\t* Unpack the package to a temporary directory `update-vXXX`\n\t* Copy the current configuration file to the directory we unpacked new AGH to\n\t* Check configuration compatibility by executing `./AGH --check-config`.  If this command fails, we won't be able to update.\n\t* Create `backup-vXXX` directory and copy the current configuration file there\n\t* Copy supporting files (README, LICENSE, etc.) to backup directory\n\t* Copy supporting files from the update directory to the current directory\n\t* Move the current binary file to backup directory\n\t* Note: if power fails here, AGH won't be able to start at system boot.  Administrator has to fix it manually\n\t* Move new binary file to the current directory\n\t* Send response to UI\n\t* Stop all tasks, including DNS server, DHCP server, HTTP server\n\t* If AGH is running as a service, use service control functionality to restart\n\t* If AGH is not running as a service, use the current process arguments to start a new process\n\t* Exit process\n* UI resends Get Status command until Server responds to it with the new version.  This means that Server is successfully restarted after update.\n* UI reloads itself\n\n\n### Get version command\n\nOn receiving this request server downloads version.json data from github and stores it in cache for several hours.\n\nExample of version.json data:\n\n\t{\n\t\"version\": \"v0.95-hotfix\",\n\t\"announcement\": \"AdGuard Home v0.95-hotfix is now available!\",\n\t\"announcement_url\": \"\",\n\t\"download_windows_amd64\": \"\",\n\t\"download_windows_386\": \"\",\n\t\"download_darwin_amd64\": \"\",\n\t\"download_linux_amd64\": \"\",\n\t\"download_linux_386\": \"\",\n\t\"download_linux_arm\": \"\",\n\t\"download_linux_arm64\": \"\",\n\t\"download_linux_mips\": \"\",\n\t\"download_linux_mipsle\": \"\",\n\t\"selfupdate_min_version\": \"v0.0\"\n\t}\n\nServer can only auto-update if the current version is equal or higher than `selfupdate_min_version`.\n\nRequest:\n\n\tPOST /control/version.json\n\n\t{\n\t\t\"recheck_now\": true | false // if false, server will check for a new version data only once in several hours\n\t}\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\"new_version\": \"v0.95\",\n\t\"announcement\": \"AdGuard Home v0.95 is now available!\",\n\t\"announcement_url\": \"http://...\",\n\t\"can_autoupdate\": true\n\t}\n\nIf `can_autoupdate` is true, then the server can automatically upgrade to a new version.\n\nResponse when auto-update is disabled by command-line argument:\n\n\t200 OK\n\n\t{\n\t\t\"disabled\":true\n\t}\n\nIt means that update check is disabled by user.  UI should do nothing.\n\n\n### Update command\n\nPerform an update procedure to the latest available version\n\nRequest:\n\n\tPOST /control/update\n\nResponse:\n\n\t200 OK\n\nError response:\n\n\t500\n\nUI shows error message \"Auto-update has failed\"\n\n\n## API: Get global status\n\nRequest:\n\n\tGET /control/status\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\"dns_addresses\":[\"...\"],\n\t\"dns_port\":53,\n\t\"http_port\":3000,\n\t\"language\":\"en\",\n\t\"protection_enabled\":true,\n\t\"running\":true,\n\t\"dhcp_available\":true,\n    \"protection_disabled_duration\":0\n\t\"version\":\"undefined\"\n\t}\n\n\n## DHCP server\n\nEnable DHCP server algorithm:\n\n* UI shows DHCP configuration screen with \"Enabled DHCP\" button disabled, and \"Check DHCP\" button enabled\n* User clicks on \"Check DHCP\"; UI sends request to server\n* Server may fail to detect whether there is another DHCP server working in the network.  In this case UI shows a warning.\n* Server may detect that a dynamic IP configuration is used for this interface.  In this case UI shows a warning.\n* UI enables \"Enable DHCP\" button\n* User clicks on \"Enable DHCP\"; UI sends request to server\n* Server sets a static IP (if necessary), enables DHCP server, sends the status back to UI\n* UI shows the status\n\n\n### DHCP server in DNS\n\nDHCP leases are used in several ways by DNS module.\n\n* For \"A\" DNS request we reply with an IP address leased by our DHCP server.\n\n\n\t\t< A bills-notebook.lan.\n\t\t> A bills-notebook.lan. = 192.168.1.100\n\n* For \"PTR\" DNS request we reply with a hostname from an active DHCP lease.\n\n\t\t< PTR 100.1.168.192.in-addr.arpa.\n\t\t> PTR 100.1.168.192.in-addr.arpa. = bills-notebook.\n\n\n### DHCP Custom Options\n\nOption with arbitrary hexadecimal data:\n\n\tDEC_CODE hex HEX_DATA\n\nwhere DEC_CODE is a decimal DHCPv4 option code in range [1..255]\n\nOption with IP data (only 1 IP is supported):\n\n\tDEC_CODE ip IP_ADDR\n\n\n### API: Show DHCP interfaces\n\nRequest:\n\n\tGET /control/dhcp/interfaces\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\t\"iface_name\":{\n\t\t\t\"name\":\"iface_name\",\n\t\t\t\"hardware_address\":\"...\",\n\t\t\t\"ipv4_addresses\":[\"ipv4 addr\", ...],\n\t\t\t\"ipv6_addresses\":[\"ipv6 addr\", ...],\n\t\t\t\"gateway_ip\":\"...\",\n\t\t\t\"flags\":\"up|broadcast|multicast\"\n\t\t}\n\t\t...\n\t}\n\n\n### API: Show DHCP status\n\nRequest:\n\n\tGET /control/dhcp/status\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\t\"enabled\":false,\n\t\t\"interface_name\":\"...\",\n\t\t\"v4\":{\n\t\t\t\"gateway_ip\":\"...\",\n\t\t\t\"subnet_mask\":\"...\",\n\t\t\t\"range_start\":\"...\", // if empty: DHCPv4 won't be enabled\n\t\t\t\"range_end\":\"...\",\n\t\t\t\"lease_duration\":60,\n\t\t},\n\t\t\"v6\":{\n\t\t\t\"range_start\":\"...\", // if empty: DHCPv6 won't be enabled\n\t\t\t\"lease_duration\":60,\n\t\t}\n\t\t\"leases\":[\n\t\t\t{\"ip\":\"...\",\"mac\":\"...\",\"hostname\":\"...\",\"expires\":\"...\"}\n\t\t\t...\n\t\t],\n\t\t\"static_leases\":[\n\t\t\t{\"ip\":\"...\",\"mac\":\"...\",\"hostname\":\"...\"}\n\t\t\t...\n\t\t]\n\t}\n\n\n### API: Check DHCP\n\nRequest:\n\n\tPOST /control/dhcp/find_active_dhcp\n\n\tvboxnet0\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\tv4: {\n\t\t\t\"other_server\": {\n\t\t\t\t\"found\": \"yes|no|error\",\n\t\t\t\t\"error\": \"Error message\", // set if found=error\n\t\t\t},\n\t\t\t\"static_ip\": {\n\t\t\t\t\"static\": \"yes|no|error\",\n\t\t\t\t\"ip\": \"<Current dynamic IP address>\", // set if static=no\n\t\t\t}\n\t\t}\n\t\tv6: {\n\t\t\t\"other_server\": {\n\t\t\t\t\"found\": \"yes|no|error\",\n\t\t\t\t\"error\": \"Error message\", // set if found=error\n\t\t\t},\n\t\t}\n\t}\n\nIf `other_server.found` is:\n* `no`: everything is fine - there is no other DHCP server\n* `yes`: we found another DHCP server.  UI shows a warning.\n* `error`: we failed to determine whether there's another DHCP server.  `other_server.error` contains error details.  UI shows a warning.\n\nIf `static_ip.static` is:\n* `yes`: everything is fine - server uses static IP address.\n\n* `no`: `static_ip.ip` contains the current dynamic IP address which we may set as static.  In this case UI shows a warning:\n\n\t\tYour system uses dynamic IP address configuration for interface <CURRENT INTERFACE NAME>.  In order to use DHCP server a static IP address must be set.  Your current IP address is <static_ip.ip>.  We will automatically set this IP address as static if you press Enable DHCP button.\n\n* `error`: this means that the server failed to check for a static IP.  In this case UI shows a warning:\n\n\t\tIn order to use DHCP server a static IP address must be set.  We failed to determine if this network interface is configured using static IP address.  Please set a static IP address manually.\n\n\n### API: Enable DHCP\n\nRequest:\n\n\tPOST /control/dhcp/set_config\n\n\t{\n\t\"enabled\":true,\n\t\"interface_name\":\"vboxnet0\",\n\t\"v4\":{\n\t\t\"gateway_ip\":\"192.169.56.1\",\n\t\t\"subnet_mask\":\"255.255.255.0\",\n\t\t\"range_start\":\"192.169.56.100\",\n\t\t\"range_end\":\"192.169.56.200\", // Note: first 3 octets must match \"range_start\"\n\t\t\"lease_duration\":60,\n\t},\n\t\"v6\":{\n\t\t\"range_start\":\"...\",\n\t\t\"lease_duration\":60,\n\t}\n\t}\n\nResponse:\n\n\t200 OK\n\n\tOK\n\nFor v4, if range_start = \"1.2.3.4\", the range_end must be \"1.2.3.X\" where X > 4.\n\nFor v6, if range_start = \"2001::1\", the last IP is \"2001:ff\".\n\n\n### Static IP check/set\n\nBefore enabling DHCP server we have to make sure the network interface we use has a static IP configured.\n\n#### Phase 1\n\nOn Debian systems DHCP is configured by `/etc/dhcpcd.conf`.\n\nTo detect if a static IP is used currently we search for line\n\n\tinterface eth0\n\nand then look for line\n\n\tstatic ip_address=...\n\nIf the interface already has a static IP, everything is set up, we don't have to change anything.\n\nTo get the current IP address along with netmask we execute\n\n\tip -oneline -family inet address show eth0\n\nwhich will print:\n\n\t2: eth0    inet 192.168.0.1/24 brd 192.168.0.255 scope global eth0\\       valid_lft forever preferred_lft forever\n\nTo get the current gateway address:\n\n\tip route show dev enp2s0\n\nwhich will print:\n\n\tdefault via 192.168.0.1 proto dhcp metric 100\n\n\n#### Phase 2 (Raspbian)\n\nStep 1.\n\nTo set a static IP address we add these lines to `dhcpcd.conf`:\n\n\tinterface eth0\n\tstatic ip_address=192.168.0.1/24\n\tstatic routers=192.168.0.1\n\tstatic domain_name_servers=192.168.0.1\n\n* Don't set 'routers' if we couldn't find gateway IP\n* Set 'domain_name_servers' equal to our IP\n\nStep 2.\n\nIf we would set a different IP address, we'd need to replace the IP address for the current network configuration.  But currently this step isn't necessary.\n\n\tip addr replace dev eth0 192.168.0.1/24\n\n\n#### Phase 2 (Ubuntu)\n\n`/etc/netplan/01-netcfg.yaml` or `/etc/netplan/01-network-manager-all.yaml`\n\nThis configuration example has a static IP set for `enp0s3` interface:\n\n\tnetwork:\n\t\tversion: 2\n\t\trenderer: networkd\n\t\tethernets:\n\t\t\tenp0s3:\n\t\t\t\tdhcp4: no\n\t\t\t\taddresses: [192.168.0.2/24]\n\t\t\t\tgateway: 192.168.0.1\n\t\t\t\tnameservers:\n\t\t\t\t\taddresses: [192.168.0.1,8.8.8.8]\n\nFor dynamic configuration `dhcp4: yes` is set.\n\nMake a backup copy to `/etc/netplan/01-netcfg.yaml.backup`.\n\nApply:\n\n\tnetplan apply\n\nRestart network:\n\n\tsystemctl restart networking\n\nor:\n\n\tsystemctl restart network-manager\n\nor:\n\n\tsystemctl restart system-networkd\n\n\n### API: Add a static lease\n\nRequest:\n\n\tPOST /control/dhcp/add_static_lease\n\n\t{\n\t\t\"mac\":\"...\",\n\t\t\"ip\":\"...\",\n\t\t\"hostname\":\"...\"\n\t}\n\nResponse:\n\n\t200 OK\n\n\n### Remove a static lease\n\nRequest:\n\n\tPOST /control/dhcp/remove_static_lease\n\n\t{\n\t\t\"mac\":\"...\",\n\t\t\"ip\":\"...\",\n\t\t\"hostname\":\"...\"\n\t}\n\nResponse:\n\n\t200 OK\n\n\n### API: Reset DHCP configuration\n\nClear all DHCP leases and configuration settings.\nDHCP server will be stopped if it's currently running.\n\nRequest:\n\n\tPOST /control/dhcp/reset\n\nResponse:\n\n\t200 OK\n\n\n### RA+SLAAC\n\nThere are 3 options for a client to get IPv6 address:\n\n1. via DHCPv6.\n\tClient doesn't receive any `ICMPv6.RouterAdvertisement` packets, so it tries to use DHCPv6.\n2. via SLAAC.\n\tClient receives a `ICMPv6.RouterAdvertisement` packet with `Managed=false` flag and IPv6 prefix.\n\tClient then assigns to itself an IPv6 address using this prefix and its MAC address.\n\tDHCPv6 server won't be started in this case.\n3. via DHCPv6 or SLAAC.\n\tClient receives a `ICMPv6.RouterAdvertisement` packet with `Managed=true` flag and IPv6 prefix.\n\tClient may choose to use SLAAC or DHCPv6 to obtain an IPv6 address.\n\nConfiguration:\n\n\tdhcp:\n\t\t...\n\t\tdhcpv6:\n\t\t\t...\n\t\t\tra_slaac_only: false\n\t\t\tra_allow_slaac: false\n\n* `ra_slaac_only:false; ra_allow_slaac:false`: use option #1.\n\tDon't send any `ICMPv6.RouterAdvertisement` packets.\n* `ra_slaac_only:true; ra_allow_slaac:false`: use option #2.\n\tPeriodically send `ICMPv6.RouterAdvertisement(Flags=(Managed=false,Other=false))` packets.\n* `ra_slaac_only:false; ra_allow_slaac:true`: use option #3.\n\tPeriodically send `ICMPv6.RouterAdvertisement(Flags=(Managed=true,Other=true))` packets.\n\nICMPv6.RouterAdvertisement packet description:\n\n\tICMPv6:\n\tType=RouterAdvertisement(134)\n\tFlags\n\t\tManaged=<BOOL>\n\t\tOther=<BOOL>\n\tOption=Prefix information(3)\n\t\t<IPv6 address prefix (/64) of the network interface>\n\tOption=MTU(5)\n\t\t<...>\n\tOption=Source link-layer address(1)\n\t\t<MAC address>\n\tOption=Recursive DNS Server(25)\n\t\t<IPv6 address of DNS server>\n\n\n## TLS\n\n\n### API: Get TLS configuration\n\nRequest:\n\n\tGET /control/tls/status\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\"enabled\":true,\n\t\"server_name\":\"...\",\n\t\"port_https\":443,\n\t\"port_dns_over_tls\":853,\n\t\"port_dns_over_quic\":784,\n\t\"certificate_chain\":\"...\",\n\t\"private_key\":\"...\",\n\t\"certificate_path\":\"...\",\n\t\"private_key_path\":\"...\"\n\n\t\"subject\":\"CN=...\",\n\t\"issuer\":\"CN=...\",\n\t\"not_before\":\"2019-03-19T08:23:45Z\",\n\t\"not_after\":\"2029-03-16T08:23:45Z\",\n\t\"dns_names\":null,\n\t\"key_type\":\"RSA\",\n\t\"valid_cert\":true,\n\t\"valid_key\":true,\n\t\"valid_chain\":false,\n\t\"valid_pair\":true,\n\t\"warning_validation\":\"Your certificate does not verify: x509: certificate signed by unknown authority\"\n\t}\n\n\n### API: Set TLS configuration\n\nRequest:\n\n\tPOST /control/tls/configure\n\n\t{\n\t\"enabled\":true,\n\t\"server_name\":\"hostname\",\n\t\"force_https\":false,\n\t\"port_https\":443,\n\t\"port_dns_over_tls\":853,\n\t\"port_dns_over_quic\":784,\n\t\"certificate_chain\":\"...\",\n\t\"private_key\":\"...\",\n\t\"certificate_path\":\"...\", // if set, certificate_chain must be empty\n\t\"private_key_path\":\"...\" // if set, private_key must be empty\n\t}\n\nResponse:\n\n\t200 OK\n\n### API: Validate TLS configuration\n\nRequest:\n\n\tPOST /control/tls/validate\n\n    {\n    \"enabled\":true,\n    \"port_https\":443,\n    \"port_dns_over_tls\":853,\n    \"port_dns_over_quic\":784,\n    \"allow_unencrypted_doh\":false,\n    \"certificate_chain\":\"...\",\n    \"private_key\":\"...\",\n    \"certificate_path\":\"...\",\n    \"private_key_path\":\"...\",\n    \"valid_cert\":true,\n    \"valid_chain\":false,\n    \"not_before\":\"2019-03-19T08:23:45Z\",\n    \"not_after\":\"2029-03-16T08:23:45Z\",\n    \"dns_names\":null,\n    \"valid_key\":true,\n    \"valid_pair\":true\n    }\n\n\nResponse:\n\n\t200 OK\n\n\n## Device Names and Per-client Settings\n\nWhen a client requests information from DNS server, he's identified by IP address.\nAdministrator can set a name for a client with a known IP and also override global settings for this client.  The name is used to improve readability of DNS logs: client's name is shown in UI next to its IP address.  The names are loaded from 3 sources:\n* automatically from \"/etc/hosts\" file.  It's a list of `IP<->Name` entries which is loaded once on AGH startup from \"/etc/hosts\" file.\n* automatically using rDNS.  It's a list of `IP<->Name` entries which is added in runtime using rDNS mechanism when a client first makes a DNS request.\n* manually configured via UI.  It's a list of client's names and their settings which is loaded from configuration file and stored on disk.\n\n### Per-client settings\n\nUI provides means to manage the list of known clients (List/Add/Update/Delete) and their settings.  These settings are stored in configuration file as an array of objects.\n\nNotes:\n\n* `name`, `ip` and `mac` values are unique.\n\n* If `mac` is set and DHCP server is enabled, IP is taken from DHCP lease table.\n\n* If `use_global_settings` is true, then DNS responses for this client are processed and filtered using global settings.\n\n* If `use_global_settings` is false, then the client-specific settings are used to override (enable or disable) global settings.\n\n* If `use_global_blocked_services` is false, then the client-specific settings are used to override (enable or disable) global Blocked Services settings.\n\n\n### Get list of clients\n\nRequest:\n\n\tGET /control/clients\n\nResponse:\n\n\t200 OK\n\n\t{\n\tclients: [\n\t\t{\n\t\t\tname: \"client1\"\n\t\t\tids: [\"...\", ...] // IP, CIDR or MAC\n\t\t\ttags: [\"...\", ...]\n\t\t\tuse_global_settings: true\n\t\t\tfiltering_enabled: false\n\t\t\tparental_enabled: false\n\t\t\tsafebrowsing_enabled: false\n\t\t\tsafesearch_enabled: false\n\t\t\tuse_global_blocked_services: true\n\t\t\tblocked_services: [ \"name1\", ... ]\n\t\t\twhois_info: {\n\t\t\t\tkey: \"value\"\n\t\t\t\t...\n\t\t\t}\n\t\t\tupstreams: [\"upstream1\", ...]\n\t\t}\n\t]\n\tauto_clients: [\n\t\t{\n\t\t\tname: \"host\"\n\t\t\tip: \"...\"\n\t\t\tsource: \"etc/hosts\" || \"rDNS\"\n\t\t\twhois_info: {\n\t\t\t\tkey: \"value\"\n\t\t\t\t...\n\t\t\t}\n\t\t}\n\t]\n\tsupported_tags: [\"...\", ...]\n\t}\n\nSupported keys for `whois_info`: orgname, country, city.\n\n\n### Add client\n\nRequest:\n\n\tPOST /control/clients/add\n\n\t{\n\t\tname: \"client1\"\n\t\tids: [\"...\", ...] // IP, CIDR or MAC\n\t\ttags: [\"...\", ...]\n\t\tuse_global_settings: true\n\t\tfiltering_enabled: false\n\t\tparental_enabled: false\n\t\tsafebrowsing_enabled: false\n\t\tsafesearch_enabled: false\n\t\tuse_global_blocked_services: true\n\t\tblocked_services: [ \"name1\", ... ]\n\t\tupstreams: [\"upstream1\", ...]\n\t}\n\nResponse:\n\n\t200 OK\n\nError response (Client already exists):\n\n\t400\n\n\n### Update client\n\nRequest:\n\n\tPOST /control/clients/update\n\n\t{\n\t\tname: \"client1\"\n\t\tdata: {\n\t\t\tname: \"client1\"\n\t\t\tids: [\"...\", ...] // IP, CIDR or MAC\n\t\t\ttags: [\"...\", ...]\n\t\t\tuse_global_settings: true\n\t\t\tfiltering_enabled: false\n\t\t\tparental_enabled: false\n\t\t\tsafebrowsing_enabled: false\n\t\t\tsafesearch_enabled: false\n\t\t\tuse_global_blocked_services: true\n\t\t\tblocked_services: [ \"name1\", ... ]\n\t\t\tupstreams: [\"upstream1\", ...]\n\t\t}\n\t}\n\nResponse:\n\n\t200 OK\n\nError response (Client not found):\n\n\t400\n\n\n### Delete client\n\nRequest:\n\n\tPOST /control/clients/delete\n\n\t{\n\t\tname: \"client1\"\n\t}\n\nResponse:\n\n\t200 OK\n\nError response (Client not found):\n\n\t400\n\n\n### API: Find clients by IP\n\nThis method returns the list of clients (manual and auto-clients) matching the IP list.\nFor auto-clients only `name`, `ids`, `whois_info`, `disallowed`, and `disallowed_rule` fields are set.  Other fields are empty.\n\nRequest:\n\n\tGET /control/clients/find?ip0=...&ip1=...&ip2=...\n\nResponse:\n\n\t200 OK\n\n\t[\n\t{\n\t\t\"1.2.3.4\": {\n\t\t\tname: \"client1\"\n\t\t\tids: [\"...\", ...] // IP, CIDR or MAC\n\t\t\tuse_global_settings: true\n\t\t\tfiltering_enabled: false\n\t\t\tparental_enabled: false\n\t\t\tsafebrowsing_enabled: false\n\t\t\tsafesearch_enabled: false\n\t\t\tuse_global_blocked_services: true\n\t\t\tblocked_services: [ \"name1\", ... ]\n\t\t\twhois_info: {\n\t\t\t\tkey: \"value\"\n\t\t\t\t...\n\t\t\t}\n\n\t\t\t\"disallowed\": false,\n\t\t\t\"disallowed_rule\": \"...\"\n\t\t}\n\t}\n\t...\n\t]\n\n* `disallowed` - whether the client's IP is blocked or not.\n* `disallowed_rule` - the rule due to which the client is disallowed. If `disallowed` is `true`, and this string is empty - it means that the client IP is disallowed by the \"allowed IP list\", i.e. it is not included in allowed.\n\n## DNS general settings\n\n### API: Get DNS general settings\n\nRequest:\n\n\tGET /control/dns_info\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\t\"upstream_dns\": [\"tls://...\", ...],\n\t\t\"upstream_dns_file\": \"\",\n\t\t\"bootstrap_dns\": [\"1.2.3.4\", ...],\n\n\t\t\"protection_enabled\": true | false,\n\t\t\"ratelimit\": 1234,\n\t\t\"blocking_mode\": \"default\" | \"refused\" | \"nxdomain\" | \"null_ip\" | \"custom_ip\",\n\t\t\"blocking_ipv4\": \"1.2.3.4\",\n\t\t\"blocking_ipv6\": \"1:2:3::4\",\n\t\t\"edns_cs_enabled\": true | false,\n\t\t\"dnssec_enabled\": true | false\n\t\t\"disable_ipv6\": true | false,\n\t\t\"upstream_mode\": \"\" | \"parallel\" | \"fastest_addr\"\n\t\t\"cache_size\": 1234, // in bytes\n\t\t\"cache_ttl_min\": 1234, // in seconds\n\t\t\"cache_ttl_max\": 1234, // in seconds\n\t}\n\n\n### API: Set DNS general settings\n\nRequest:\n\n\tPOST /control/dns_config\n\n\t{\n\t\t\"upstream_dns\": [\"tls://...\", ...],\n\t\t\"upstream_dns_file\": \"\",\n\t\t\"bootstrap_dns\": [\"1.2.3.4\", ...],\n\n\t\t\"protection_enabled\": true | false,\n\t\t\"ratelimit\": 1234,\n\t\t\"blocking_mode\": \"default\" | \"refused\" | \"nxdomain\" | \"null_ip\" | \"custom_ip\",\n\t\t\"blocking_ipv4\": \"1.2.3.4\",\n\t\t\"blocking_ipv6\": \"1:2:3::4\",\n\t\t\"edns_cs_enabled\": true | false,\n\t\t\"dnssec_enabled\": true | false\n\t\t\"disable_ipv6\": true | false,\n\t\t\"upstream_mode\": \"\" | \"parallel\" | \"fastest_addr\"\n\t\t\"cache_size\": 1234, // in bytes\n\t\t\"cache_ttl_min\": 1234, // in seconds\n\t\t\"cache_ttl_max\": 1234, // in seconds\n\t}\n\nResponse:\n\n\t200 OK\n\n`blocking_mode`:\n* default: Respond with NXDOMAIN when blocked by Adblock-style rule;  respond with the IP address specified in the rule when blocked by /etc/hosts-style rule\n* NXDOMAIN: Respond with NXDOMAIN code\n* Null IP: Respond with zero IP address (0.0.0.0 for A; :: for AAAA)\n* Custom IP: Respond with a manually set IP address\n\n`blocking_ipv4` and `blocking_ipv6` values are active when `blocking_mode` is set to `custom_ip`.\n\n\n## DNS access settings\n\nThere are low-level settings that can block undesired DNS requests.  \"Blocking\" means not responding to request.\n\nThere are 3 types of access settings:\n* allowed_clients: Only these clients are allowed to make DNS requests.\n* disallowed_clients: These clients are not allowed to make DNS requests.\n* blocked_hosts: These hosts are not allowed to be resolved by a DNS request.\n\n\n### List access settings\n\nRequest:\n\n\tGET /control/access/list\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\tallowed_clients: [\"127.0.0.1\", ...]\n\t\tdisallowed_clients: [\"127.0.0.1\", ...]\n\t\tblocked_hosts: [\"host.com\", ...] // host name or a wildcard\n\t}\n\n\n### Set access settings\n\nRequest:\n\n\tPOST /control/access/set\n\n\t{\n\t\tallowed_clients: [\"127.0.0.1\", ...]\n\t\tdisallowed_clients: [\"127.0.0.1\", ...]\n\t\tblocked_hosts: [\"host.com\", ...]\n\t}\n\nResponse:\n\n\t200 OK\n\n\n## Rewrites\n\nThis section allows the administrator to easily configure custom DNS response for a specific domain name.\nA, AAAA and CNAME records are supported.\n\nSyntax:\n\n\tkey -> value\n\nwhere `key` is a host name or a wild card that matches Question in DNS request\nand `value` is either:\n* IPv4 address: use this IP in A response\n* IPv6 address: use this IP in AAAA response\n* canonical name: add CNAME record\n* \"`key`\": CNAME exception - pass request to upstream\n* \"A\": A exception - pass A request to upstream\n* \"AAAA\": AAAA exception - pass AAAA request to upstream\n\n\n#### Example: A record\n\n\thost.com -> 1.2.3.4\n\nResponse:\n\n\tA:\n\t\tA = 1.2.3.4\n\tAAAA:\n\t\t<empty>\n\n#### Example: AAAA record\n\n\thost.com -> ::1\n\nResponse:\n\n\tA:\n\t\t<empty>\n\tAAAA:\n\t\tAAAA = ::1\n\n#### Example: CNAME record\n\n\tsub.host.com -> host.com\n\nResponse:\n\n\tA:\n\t\tCNAME = host.com\n\t\tA = <IPv4 address of host.com>\n\tAAAA:\n\t\tCNAME = host.com\n\t\tAAAA = <IPv6 address of host.com>\n\n#### Example: CNAME+A records\n\n\tsub.host.com -> host.com\n\thost.com -> 1.2.3.4\n\nResponse:\n\n\tA:\n\t\tCNAME = host.com\n\t\tA = 1.2.3.4\n\tAAAA:\n\t\tCNAME = host.com\n\n#### Example: Wildcard CNAME+A record with CNAME exception\n\n\t*.host.com -> 1.2.3.4\n\tpass.host.com -> pass.host.com\n\nResponse to `my.host.com`:\n\n\tA:\n\t\tA = 1.2.3.4\n\tAAAA:\n\t\t<empty>\n\nResponse to `pass.host.com`:\n\n\tA:\n\t\tA = <IPv4 address of pass.host.com>\n\tAAAA:\n\t\tAAAA = <IPv6 address of pass.host.com>\n\n#### Example: A record with AAAA exception\n\n\thost.com -> 1.2.3.4\n\thost.com -> AAAA\n\nResponse:\n\n\tA:\n\t\tA = 1.2.3.4\n\tAAAA:\n\t\tAAAA = <IPv6 address of host.com>\n\n#### Example: pass A only\n\n\thost.com -> A\n\nResponse:\n\n\tA:\n\t\tA = <IPv4 address of host.com>\n\tAAAA:\n\t\t<empty>\n\n\n### API: List rewrite entries\n\nRequest:\n\n\tGET /control/rewrite/list\n\nResponse:\n\n\t200 OK\n\n\t[\n\t{\n\t\tdomain: \"...\"\n\t\tanswer: \"...\"\n\t}\n\t...\n\t]\n\n`domain` can be an exact host name (`www.host.com`) or a wildcard (`*.host.com`).\n\n\n### API: Add a rewrite entry\n\nRequest:\n\n\tPOST /control/rewrite/add\n\n\t{\n\t\tdomain: \"...\"\n\t\tanswer: \"...\" // \"1.2.3.4\" (A) || \"::1\" (AAAA) || \"hostname\" (CNAME)\n\t}\n\nResponse:\n\n\t200 OK\n\n\n### API: Remove a rewrite entry\n\nRequest:\n\n\tPOST /control/rewrite/delete\n\n\t{\n\t\tdomain: \"...\"\n\t\tanswer: \"...\"\n\t}\n\nResponse:\n\n\t200 OK\n\n\n## Services Filter\n\nAllows to quickly block popular sites globally or for specific client only.\nUI manages these settings via global or per-client API.\nUI and server have the same list of the services supported and this list must always be in synchronization.\nUI code also contains icons for each service: `client/src/components/ui/Icons.js`.\n\nHow it works:\n* UI presents the list of services which user may want to block\n* Admin clicks on the checkboxes in front of the services to block and presses Save\n* UI sends `Set blocked services list` or `Update client` message\n* Server updates the internal configuration\n* When a user sends a DNS request for a host which is blocked by these settings, he won't receive its IP address\n* Query log will show that this request was blocked by \"Blocked services\"\n\nInternally, all supported services are stored as a map:\n\n\tservice name -> list of rules\n\n\n### API: Get blocked services list of available services\n\nRequest:\n\n\tGET /control/blocked_services/services\n\nResponse:\n\n\t200 OK\n\n\t[ \"name1\", ... ]\n\n\n### API: Get blocked services list\n\nRequest:\n\n\tGET /control/blocked_services/list\n\nResponse:\n\n\t200 OK\n\n\t[ \"name1\", ... ]\n\n\n### API: Set blocked services list\n\nRequest:\n\n\tPOST /control/blocked_services/set\n\n\t[ \"name1\", ... ]\n\nResponse:\n\n\t200 OK\n\n\n## Statistics\n\nLoad (main thread):\n. Load data from the last bucket from DB for the current hour\n\nRuntime (DNS worker threads):\n. Update current unit\n\nRuntime (goroutine):\n. Periodically check that current unit should be flushed to file (when the current hour changes)\n . If so, flush it, allocate a new empty unit\n\nRuntime (HTTP worker threads):\n. To respond to \"Get statistics\" API request we:\n . load all units from file\n . load current unit\n . process data from all loaded units:\n  . sum up data for \"total counters\" output values\n  . add value into \"per time unit counters\" output arrays\n  . aggregate data for \"top_\" output arrays;  sort in descending order\n\nUnload (main thread):\n. Flush current unit to file\n\n\n### API: Get statistics data\n\nRequest:\n\n\tGET /control/stats\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\ttime_units: hours | days\n\n\t\t// total counters:\n\t\tnum_dns_queries: 123\n\t\tnum_blocked_filtering: 123\n\t\tnum_replaced_safebrowsing: 123\n\t\tnum_replaced_safesearch: 123\n\t\tnum_replaced_parental: 123\n\t\tavg_processing_time: 123.123\n\n\t\t// per time unit counters\n\t\tdns_queries: [123, ...]\n\t\tblocked_filtering: [123, ...]\n\t\treplaced_parental: [123, ...]\n\t\treplaced_safebrowsing: [123, ...]\n\n\t\ttop_queried_domains: [\n\t\t\t{host: 123},\n\t\t\t...\n\t\t]\n\t\ttop_blocked_domains: [\n\t\t\t{host: 123},\n\t\t\t...\n\t\t]\n\t\ttop_clients: [\n\t\t\t{IP: 123},\n\t\t\t...\n\t\t]\n\t}\n\n\n### API: Clear statistics data\n\nRequest:\n\n\tPOST /control/stats_reset\n\nResponse:\n\n\t200 OK\n\n\n### API: Set statistics parameters\n\nRequest:\n\n\tPOST /control/stats_config\n\n\t{\n\t\t\"interval\": 1 | 7 | 30 | 90\n\t}\n\nResponse:\n\n\t200 OK\n\n\n### API: Get statistics parameters\n\nRequest:\n\n\tGET /control/stats_info\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\t\"interval\": 1 | 7 | 30 | 90\n\t}\n\n\n## Query logs\n\nWhen a new DNS request is received and processed, we store information about this event in \"query log\".  It is a file on disk in JSON format:\n\n\t{\n\t\"IP\":\"127.0.0.1\", // client IP\n\t\"T\":\"...\", // response time\n\t\"QH\":\"...\", // target host name without the last dot\n\t\"QT\":\"...\", // question type\n\t\"QC\":\"...\", // question class\n\t\"CP\":\"\" | \"doh\", // client connection protocol\n\t\"Answer\":\"base64 data\",\n\t\"OrigAnswer\":\"base64 data\",\n\t\"Result\":{\n\t\t\"IsFiltered\":true,\n\t\t\"Reason\":3,\n\t\t\"Rule\":\"...\",\n\t\t\"FilterID\":1,\n\t\t\"ServiceName\":\"...\"\n\t\t},\n\t\"Elapsed\":12345,\n\t\"Upstream\":\"...\",\n\t}\n\n\n### Adding new data\n\nFirst, new data is stored in a memory region.  When this array is filled to a particular amount of entries (e.g. 5000), we flush this data to a file and clear the array.\n\n\n### Getting data\n\nWhen UI asks for data from query log (see \"API: Get query log\"), server reads the newest entries from memory array and the file.  The maximum number of items returned per one request is limited by configuration.\n\n\n### Removing old data\n\nWe store data for a limited amount of time - the log file is automatically rotated.\n\n* On AGH startup read the first line from query logs and store its time value\n* If there's no log file yet, set the time value of the first log event when the file is created\n* If this time value is older than our time limit, perform file rotate procedure\n* While AGH is running, check the previous condition every 24 hours\n\n\n### API: Get query log\n\nRequest:\n\n\tGET /control/querylog\n\t?older_than=2006-01-02T15:04:05.999999999Z07:00\n\t&search=...\n\t&response_status=\"...\"\n\n`older_than` setting is used for paging.  UI uses an empty value for `older_than` on the first request and gets the latest log entries. To get the older entries, UI sets `older_than` to the `oldest` value from the server's response.\n\nIf search settings are set, server returns only entries that match the specified request.\n\n`search`:\nmatch by domain name or client IP address.\nThe server matches substrings by default: e.g. `adguard.com` matches `www.adguard.com`.\nStrict matching can be enabled by enclosing the value in double quotes: e.g. `\"adguard.com\"` matches `adguard.com` but doesn't match `www.adguard.com`.\n\n`response_status`:\n* all\n* filtered             - all kinds of filtering\n* blocked              - blocked or blocked services\n* blocked_services     - blocked services\n* blocked_safebrowsing - blocked by safebrowsing\n* blocked_parental     - blocked by parental control\n* whitelisted          - whitelisted\n* rewritten            - all kinds of rewrites\n* safe_search          - enforced safe search\n* processed            - not blocked, not white-listed entries\n\nResponse:\n\n\t{\n\t\"oldest\":\"2006-01-02T15:04:05.999999999Z07:00\"\n\t\"data\":[\n\t{\n\t\t\"answer\":[\n\t\t\t{\n\t\t\t\"ttl\":10,\n\t\t\t\"type\":\"AAAA\",\n\t\t\t\"value\":\"::\"\n\t\t\t}\n\t\t\t...\n\t\t],\n\t\t\"original_answer\":[ // Answer from upstream server (optional)\n\t\t\t{\n\t\t\t\"type\":\"AAAA\",\n\t\t\t\"value\":\"::\"\n\t\t\t}\n\t\t\t...\n\t\t],\n\t\t\"upstream\":\"...\", // Upstream URL starting with tcp://, tls://, https://, or with an IP address\n\t\t\"answer_dnssec\": true,\n\t\t\"client\":\"127.0.0.1\",\n\t\t\"client_proto\": \"\" (plain) | \"doh\" | \"dot\" | \"doq\",\n\t\t\"elapsedMs\":\"0.098403\",\n\t\t\"filterId\":1,\n\t\t\"question\":{\n\t\t\t\"class\":\"IN\",\n\t\t\t\"host\":\"doubleclick.net\",\n\t\t\t\"type\":\"AAAA\"\n\t\t},\n\t\t\"reason\":\"FilteredBlackList\",\n\t\t\"rule\":\"||doubleclick.net^\",\n\t\t\"service_name\": \"...\", // set if reason=FilteredBlockedService\n\t\t\"status\":\"NOERROR\",\n\t\t\"time\":\"2006-01-02T15:04:05.999999999Z07:00\"\n\t}\n\t...\n\t]\n\t}\n\nThe most recent entries are at the top of list.\n\nIf there are no more older entries, `\"oldest\":\"\"` is returned.\n\n\n### API: Set querylog parameters\n\nRequest:\n\n\tPOST /control/querylog_config\n\n\t{\n\t\t\"enabled\": true | false\n\t\t\"interval\": 1 | 7 | 30 | 90\n\t\t\"anonymize_client_ip\": true | false // anonymize clients' IP addresses\n\t}\n\nResponse:\n\n\t200 OK\n\n`anonymize_client_ip`:\n1. New log entries written to a log file will contain modified client IP addresses.  Note that there's no way to obtain the full IP address later for these entries.\n2. `GET /control/querylog` response data will contain modified client IP addresses (masked /24 or /112).\n3. Searching by client IP won't work for the previously stored entries.\n\nHow `anonymize_client_ip` affects Stats:\n1. After AGH restart, new stats entries will contain modified client IP addresses.\n2. Existing entries are not affected.\n\n\n### API: Get querylog parameters\n\nRequest:\n\n\tGET /control/querylog_info\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\t\"enabled\": true | false\n\t\t\"interval\": 1 | 7 | 30 | 90\n\t\t\"anonymize_client_ip\": true | false\n\t}\n\n\n## Filtering\n\n![](doc/agh-filtering.png)\n\nThis is how DNS requests and responses are filtered by AGH:\n\n* 'dnsproxy' module receives DNS request from client and passes control to AGH\n* AGH applies filtering logic to the host name in DNS Question:\n\t* process Rewrite rules.\n\t\tCan set CNAME and a list of IP addresses.\n\t* process /etc/hosts entries.\n\t\tCan set a list of IP addresses or a hostname (for PTR requests).\n\t* match host name against filtering lists\n\t* match host name against blocked services rules\n\t* process SafeSearch rules\n\t* request SafeBrowsing & ParentalControl services and process their response\n* If the handlers above create a successful result that can be immediately sent to a client, it's passed back to 'dnsproxy' module\n* Otherwise, AGH passes the DNS request to an upstream server via 'dnsproxy' module\n* After 'dnsproxy' module has received a response from an upstream server, it passes control back to AGH\n* If the filtering logic for DNS request returned a 'whitelist' flag, AGH passes the response to a client\n* Otherwise, AGH applies filtering logic to each DNS record in response:\n\t* For CNAME records, the target name is matched against filtering lists (ignoring 'whitelist' rules)\n\t* For A and AAAA records, the IP address is matched against filtering lists (ignoring 'whitelist' rules)\n\n\n### Filters update mechanism\n\nFilters can be updated either manually by request from UI or automatically.\nAuto-update interval can be configured in UI.  If it is 0, auto-update is disabled.\nWhen the last modification date of filter files is older than auto-update interval, auto-update procedure is started.\nIf an enabled filter file doesn't exist, it's downloaded on application startup.  This includes the case when installation wizard is completed and there are no filter files yet.\nWhen auto-update time comes, server starts the update procedure by downloading filter files.  After new filter files are in place, it restarts DNS filtering module with new rules.\nOnly filters that are enabled by configuration can be updated.\nAs a result of the update procedure, all enabled filter files are written to disk, refreshed (their last modification date is equal to the current time) and loaded.\n\n\n### API: Get filtering parameters\n\nRequest:\n\n\tGET /control/filtering/status\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\t\"enabled\": true | false\n\t\t\"interval\": 0 | 1 | 12 | 1*24 || 3*24 || 7*24\n\t\t\"filters\":[\n\t\t\t{\n\t\t\t\"id\":1\n\t\t\t\"enabled\":true,\n\t\t\t\"url\":\"https://...\",\n\t\t\t\"name\":\"...\",\n\t\t\t\"rules_count\":1234,\n\t\t\t\"last_updated\":\"2019-09-04T18:29:30+00:00\",\n\t\t\t}\n\t\t\t...\n\t\t],\n\t\t\"whitelist_filters\":[\n\t\t\t{\n\t\t\t\"id\":1\n\t\t\t\"enabled\":true,\n\t\t\t\"url\":\"https://...\",\n\t\t\t\"name\":\"...\",\n\t\t\t\"rules_count\":1234,\n\t\t\t\"last_updated\":\"2019-09-04T18:29:30+00:00\",\n\t\t\t}\n\t\t\t...\n\t\t],\n\t\t\"user_rules\":[\"...\", ...]\n\t}\n\nFor both arrays `filters` and `whitelist_filters` there are unique values: id, url.\nID for each filter is assigned by Server - it's used for file names.\n\n\n### API: Set filtering parameters\n\nRequest:\n\n\tPOST /control/filtering/config\n\n\t{\n\t\t\"enabled\": true | false\n\t\t\"interval\": 0 | 1 | 12 | 1*24 || 3*24 || 7*24\n\t}\n\nResponse:\n\n\t200 OK\n\n\n### API: Refresh filters\n\nRequest:\n\n\tPOST /control/filtering/refresh\n\n\t{\n\t\t\"whitelist\": true\n\t}\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\t\"updated\": 123 // number of filters updated\n\t}\n\n\n### API: Add Filter\n\nRequest:\n\n\tPOST /control/filtering/add_url\n\n\t{\n\t\t\"name\": \"...\"\n\t\t\"url\": \"...\" // URL or an absolute file path\n\t\t\"whitelist\": true\n\t}\n\nResponse:\n\n\t200 OK\n\n\n### API: Set URL parameters\n\nRequest:\n\n\tPOST /control/filtering/set_url\n\n\t{\n\t\"url\": \"...\"\n\t\"whitelist\": true\n\t\"data\": {\n\t\t\"name\": \"...\"\n\t\t\"url\": \"...\"\n\t\t\"enabled\": true | false\n\t}\n\t}\n\nResponse:\n\n\t200 OK\n\n\n### API: Delete URL\n\nRequest:\n\n\tPOST /control/filtering/remove_url\n\n\t{\n\t\"url\": \"...\"\n\t\"whitelist\": true\n\t}\n\nResponse:\n\n\t200 OK\n\n\n### API: Domain Check\n\nCheck if host name is filtered.\n\nRequest:\n\n\tGET /control/filtering/check_host?name=hostname\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\t\"reason\":\"FilteredBlackList\",\n\t\t\"rules\":{\n\t\t\t\"filter_list_id\":42,\n\t\t\t\"text\":\"||doubleclick.net^\",\n\t\t},\n\t\t// If we have \"reason\":\"FilteredBlockedService\".\n\t\t\"service_name\": \"...\",\n\t\t// If we have \"reason\":\"Rewrite\".\n\t\t\"cname\": \"...\",\n\t\t\"ip_addrs\": [\"1.2.3.4\", ...]\n\t}\n\nThere are also deprecated properties `filter_id` and `rule` on the top level of\nthe response object.  Their usage should be replaced with\n`rules[*].filter_list_id` and `rules[*].text` correspondingly.  See the\n_OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file.\n\n## Log-in page\n\nAfter user completes the steps of installation wizard, he must log in into dashboard using his name and password.  After user successfully logs in, he gets the Cookie which allows the server to authenticate him next time without password.  After the Cookie is expired, user needs to perform log-in operation again.\n\nRequests to / or /index.html without a proper Cookie get redirected to Log-In page with prompt for name and password.  The server responds with 403 to all other requests (including all API methods) without a proper Cookie.\n\nYAML configuration:\n\n\tusers:\n\t- name: \"...\"\n\t  password: \"...\" // bcrypt hash\n\t...\n\n\nSession DB file:\n\n\tsession=\"...\" user=name expire=123456\n\t...\n\nSession data is SHA(random()+name+password).\nExpiration time is UNIX time when cookie gets expired.\n\nAny request to server must come with Cookie header:\n\n\tGET /...\n\tCookie: session=...\n\nIf not authenticated, server sends a redirect response:\n\n\t302 Found\n\tLocation: /login.html\n\n\n### Reset password\n\nThere is no mechanism to reset the password.  Instead, the administrator must use `htpasswd` utility to generate a new hash:\n\n\thtpasswd -B -n -b username password\n\nIt will print `username:<HASH>` to the terminal.  `<HASH>` value may be used in AGH YAML configuration file as a value to `password` setting:\n\n\tusers:\n\t- name: \"...\"\n\t  password: <HASH>\n\n\n\n### API: Log in\n\nPerform a log-in operation for administrator.  Server generates a session for this name+password pair, stores it in file.  UI needs to perform all requests with this value inside Cookie HTTP header.\n\nRequest:\n\n\tPOST /control/login\n\n\t{\n\t\tname: \"...\"\n\t\tpassword: \"...\"\n\t}\n\nResponse:\n\n\t200 OK\n\tSet-Cookie: session=...; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Path=/; HttpOnly\n\n\n### API: Log out\n\nPerform a log-out operation for administrator.  Server removes the session from its DB and sets an expired cookie value.\n\nRequest:\n\n\tGET /control/logout\n\nResponse:\n\n\t302 Found\n\tLocation: /login.html\n\tSet-Cookie: session=...; Expires=Thu, 01 Jan 1970 00:00:00 GMT\n\n\n### API: Get current user info\n\nRequest:\n\n\tGET /control/profile\n\nResponse:\n\n\t200 OK\n\n\t{\n\t\"name\":\"...\"\n\t}\n\nIf no client is configured then authentication is disabled and server sends an empty response.\n\n\n### Safe services\n\nCheck if host name is blocked by SB/PC service:\n\n* For each host name component, search for the result in cache by the first 2 bytes of SHA-256 hashes of host name components (max. is 4, i.e. sub2.sub1.host.com), excluding TLD:\n\n\t\thashes[] = cache_search(sha256(host.com)[0..1])\n\t\t...\n\n\tIf hash prefix is found, search for a full hash sum in the cached data.\n\tIf found, the host is blocked.\n\tIf not found, the host is not blocked - don't request data for this prefix from the Family server again.\n\tIf hash prefix is not found, request data for this prefix from the Family server.\n\n* Prepare query string which is generated from the first 2 bytes (converted to a 4-character string) of SHA-256 hashes of host name components (max. is 4, i.e. sub2.sub1.host.com), excluding TLD:\n\n\t\tqs = ... + string(sha256(sub.host.com)[0..1]) + \".\" + string(sha256(host.com)[0..1]) + \".sb.dns.adguard.com.\"\n\n\tFor PC `.pc.dns.adguard.com` suffix is used.\n\n* Send TXT query to Family server, receive response which contains the array of complete hash sums of the blocked hosts\n\n* Check if one of received hash sums (`hashes[]`) matches hash sums for our host name\n\n\t\thashes[0] <> sha256(host.com)\n\t\thashes[0] <> sha256(sub.host.com)\n\t\thashes[1] <> sha256(host.com)\n\t\thashes[1] <> sha256(sub.host.com)\n\t\t...\n\n* Store all received hash sums in cache:\n\n\t\tsha256(host.com)[0..1] -> hashes[0],hashes[1],...\n\t\tsha256(sub.host.com)[0..1] -> hashes[2],...\n\t\t...\n\n## API: Get DNS over HTTPS .mobileconfig\n\nRequest:\n\n\tGET /apple/doh.mobileconfig\n\nResponse:\n\n\t200 OK\n\n    DOH plist file\n\n## API: Get DNS over TLS .mobileconfig\n\nRequest:\n\n\tGET /apple/dot.mobileconfig\n\nResponse:\n\n\t200 OK\n\n    DOT plist file\n\n## ipset\n\nAGH can add IP addresses of the specified in configuration domain names to an ipset list.\n\nPrepare: user creates an ipset list and configures AGH for using it.\n\n\t1. User --( ipset create my_ipset hash:ip ) -> OS\n\t2. User --( ipset: host.com,host2.com/my_ipset )-> AGH\n\n\t\tSyntax:\n\n\t\t\tipset: \"DOMAIN[,DOMAIN].../IPSET1_NAME[,IPSET2_NAME]...\"\n\n\t\tIPv4 addresses are added to an ipset list with `ipv4` family, IPv6 addresses - to `ipv6` ipset list.\n\nRun-time: AGH adds IP addresses of a domain name to a corresponding ipset list.\n\n\t1. AGH --( resolve host.com )-> upstream\n\t2. AGH <-( host.com:[1.1.1.1,2.2.2.2] )-- upstream\n\t3. AGH --( ipset.add(my_ipset, [1.1.1.1,2.2.2.2] ))-> OS\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# AdGuard Home Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n<!--\n## [v0.108.0] – TBA\n\n## [v0.107.74] - 2026-03-24 (APPROX.)\n\nSee also the [v0.107.74 GitHub milestone][ms-v0.107.74].\n\n[ms-v0.107.74]: https://github.com/AdguardTeam/AdGuardHome/milestone/109?closed=1\n\nNOTE: Add new changes BELOW THIS COMMENT.\n-->\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.26.1][go-1.26.1].\n\n[go-1.26.1]: https://groups.google.com/g/golang-announce/c/EdhZqrQ98hkq\n\n### Fixed\n\n- Fixed clients block/unblock when moving clients between allowed and disallowed lists.\n\n<!--\nNOTE: Add new changes ABOVE THIS COMMENT.\n-->\n\n## [v0.107.73] - 2026-03-10\n\nSee also the [v0.107.73 GitHub milestone][ms-v0.107.73].\n\n### Security\n\n- Authentication is now applied to requests that have been upgraded from HTTP/2 Cleartext (H2C) requests to public resources.\n\n    **NOTE:** We thank @mandreko for reporting this security issue.\n\n[ms-v0.107.73]: https://github.com/AdguardTeam/AdGuardHome/milestone/108?closed=1\n\n## [v0.107.72] - 2026-02-19\n\nSee also the [v0.107.72 GitHub milestone][ms-v0.107.72].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.25.7][go-1.25.7].\n\n### Added\n\n- AdGuard Home now tracks the TLS certificate and key files for updates and reloads them after any updates are detected ([#3962]).\n\n- New query parameter `recent` in `GET /control/stats/` defines statistics lookback period in millieseconds.  See `openapi/openapi.yaml` for details.\n\n- New field `\"ignored_enabled\"` in `GetStatsConfigResponse` or `GetQueryLogConfigResponse`.  See `openapi/openapi.yaml` for details.\n\n### Changed\n\n- In addition to modifying the contents of a hosts file, deleting or renaming the file now also updates runtime clients and DNS filtering results.\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 32 to 33.\n\n- Added a new boolean field `ignored_enabled` in querylog and statistics config.\n\n    ```yaml\n    # BEFORE:\n    'querylog':\n      # …\n      'ignored':\n      - '|.^'\n    'statistics':\n      # …\n      'ignored':\n      - '|.^'\n\n    # AFTER:\n    'querylog':\n      # …\n      'ignored':\n      - '|.^'\n      'ignored_enabled': true\n    'statistics':\n      # …\n      'ignored':\n      - '|.^'\n      'ignored_enabled': true\n      ```\n\n    To roll back this change, set the `schema_version` back to `32`.\n\n### Fixed\n\n- Executable permissions in some Docker installations ([#8237]).\n\n- Unknown blocked services from both global and client configuration now logged instead of causing server crash.\n\n[#3962]: https://github.com/AdguardTeam/AdGuardHome/issues/3962\n[#8237]: https://github.com/AdguardTeam/AdGuardHome/issues/8237\n\n[go-1.25.7]: https://groups.google.com/g/golang-announce/c/K09ubi9FQFk\n[ms-v0.107.72]: https://github.com/AdguardTeam/AdGuardHome/milestone/107?closed=1\n\n## [v0.107.71] - 2025-12-08\n\nSee also the [v0.107.71 GitHub milestone][ms-v0.107.71].\n\n### Changed\n\n- Stale records in optimistic DNS cache now have an upper age limit controlled by `dns.cache_optimistic_max_age`.  The default value is 12 hours.\n\n- TTL for stale answers from optimistic DNS cache is now controlled by `dns.cache_optimistic_answer_ttl`.  The default value is 30 seconds.\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 31 to 32.\n\n- Added a new string fields `dns.cache_optimistic_answer_ttl` and `dns.cache_optimistic_max_age`.\n\n    ```yaml\n    # BEFORE:\n    'dns':\n      'cache_enabled': true\n      'cache_optimistic': true\n      # …\n\n    # AFTER:\n    'dns':\n      'cache_enabled': true\n      'cache_optimistic': true\n      'cache_optimistic_answer_ttl': '30s'\n      'cache_optimistic_max_age': '12h'\n      # …\n      ```\n\n    To roll back this change, set the `schema_version` back to `31`.\n\n### Fixed\n\n- Optimistic DNS cache not working ([#8148]).\n\n[#8148]: https://github.com/AdguardTeam/AdGuardHome/issues/8148\n\n[ms-v0.107.71]: https://github.com/AdguardTeam/AdGuardHome/milestone/106?closed=1\n\n## [v0.107.70] - 2025-12-03\n\nSee also the [v0.107.70 GitHub milestone][ms-v0.107.70].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.25.5][go-1.25.5].\n\n### Added\n\n- New field `\"start_time\"` in the `GET /control/status` response.\n\n### Changed\n\n- Stale records in optimistic DNS cache now have an upper age limit of 12 hours.\n\n- New blocked services UI.\n\n### Fixed\n\n- Generated mobileconfig could not be installed on macOS 26.1.\n\n[go-1.25.5]: https://groups.google.com/g/golang-announce/c/8FJoBkPddm4\n[ms-v0.107.70]: https://github.com/AdguardTeam/AdGuardHome/milestone/105?closed=1\n\n## [v0.107.69] - 2025-10-30\n\nSee also the [v0.107.69 GitHub milestone][ms-v0.107.69].\n\n### Changed\n\n- Node.js 24 is now used to build the frontend.\n\n### Deprecated\n\n- Node.js 20 and 22 support.\n\n### Fixed\n\n- DHCP settings could not be saved ([#8075]).\n\n- DNS Rewrite edit modal did not populate with the correct values ([#8072]).\n\n### Removed\n\n- The outdated querylog anonymization script.\n\n[#8075]: https://github.com/AdguardTeam/AdGuardHome/issues/8075\n[#8072]: https://github.com/AdguardTeam/AdGuardHome/issues/8072\n\n[ms-v0.107.69]: https://github.com/AdguardTeam/AdGuardHome/milestone/104?closed=1\n\n## [v0.107.68] - 2025-10-23\n\nSee also the [v0.107.68 GitHub milestone][ms-v0.107.68].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.25.3][go-1.25.3].\n\n### Added\n\n- New DNS rewrite settings endpoints `GET /control/rewrite/settings` and `PUT /control/rewrite/settings/update` ([#1765]).  See `openapi/openapi.yaml` for details.\n\n- New fields `\"groups\"` and `\"group_id\"` added to the HTTP API (`GET /control/blocked_services/all`).  See `openapi/openapi.yaml` for the full description.\n\n### Changed\n\n- `POST /control/rewrite/add` and `PUT /control/rewrite/update` now accept the optional field \"enabled\" ([#1765]).  See `openapi/openapi.yaml` for details.\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 30 to 31.\n\n- Added a new boolean field `filtering.rewrites_enabled` to globally enable/disable DNS rewrites.\n\n- Added a new boolean field `enabled` for each entry in `filtering.rewrites` to toggle individual rewrites.\n\n    ```yaml\n    # BEFORE:\n    'filtering':\n      'rewrites':\n        - 'domain': test.example\n          'answer': 192.0.2.0\n      # …\n\n    # AFTER:\n    'filtering':\n      'rewrites_enabled': true\n      'rewrites':\n        - 'domain': test.example\n          'answer': 192.0.2.0\n          'enabled': true\n      # …\n    ```\n\n    To roll back this change, set `schema_version` back to `30`.\n\n[#1765]: https://github.com/AdguardTeam/AdGuardHome/issues/1765\n\n[go-1.25.3]: https://groups.google.com/g/golang-announce/c/YEyj6FUNbik\n[ms-v0.107.68]: https://github.com/AdguardTeam/AdGuardHome/milestone/103?closed=1\n\n## [v0.107.67] - 2025-09-29\n\nSee also the [v0.107.67 GitHub milestone][ms-v0.107.67].\n\n### Added\n\n- The *HaGeZi's DNS Rebind Protection* filter for protecting against DNS rebinding attacks ([#102]).\n\n- Support for configuring the suggested default HTTP port for the installation wizard via the `ADGUARD_HOME_DEFAULT_WEB_PORT` environment variable (useful for vendors).\n\n### Changed\n\n- Optimized matching of filtering rules.\n\n### Fixed\n\n- Excessive configuration file overwrites when visiting the Web UI and a non-empty `language` is set.\n\n- Lowered the severity of log messages for failed deletion of old filter files ([#7964]).\n\n[#102]:  https://github.com/AdguardTeam/AdGuardHome/issues/102\n[#7964]: https://github.com/AdguardTeam/AdGuardHome/issues/7964\n\n[ms-v0.107.67]: https://github.com/AdguardTeam/AdGuardHome/milestone/102?closed=1\n\n## [v0.107.66] - 2025-09-15\n\nSee also the [v0.107.66 GitHub milestone][ms-v0.107.66].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.25.1][go-1.25.1].\n\n### Changed\n\n- Our snap package now uses the `core24` image as its base.\n\n- Outgoing HTTP requests now use the `User-Agent` header `AdGuardHome/v0.107.66` (where `v0.107.66` is the current version) instead of `Go-http-client/1.1` ([#7979]).\n\n### Fixed\n\n- Authentication errors in the Web UI when AdGuard Home is behind a proxy that sets Basic Auth headers ([#7987]).\n\n- The HTTP API `GET /control/profile` endpoint failing when no users were configured ([#7985]).\n\n- Missing warning on the *Encryption Settings* page when using a certificate without an IP address.\n\n[#7979]: https://github.com/AdguardTeam/AdGuardHome/issues/7979\n[#7985]: https://github.com/AdguardTeam/AdGuardHome/issues/7985\n[#7987]: https://github.com/AdguardTeam/AdGuardHome/issues/7987\n\n[go-1.25.1]:    https://groups.google.com/g/golang-announce/c/PtW9VW21NPs\n[ms-v0.107.66]: https://github.com/AdguardTeam/AdGuardHome/milestone/101?closed=1\n\n## [v0.107.65] - 2025-08-20\n\nSee also the [v0.107.65 GitHub milestone][ms-v0.107.65].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.6][go-1.24.6].\n\n### Added\n\n- A separate checkbox in the Web UI to enable or disable the global DNS response cache without losing the configured cache size.\n\n- A new `\"cache_enabled\"` field to the HTTP API (`GET /control/dns_info` and `POST /control/dns_config`).  See `openapi/openapi.yaml` for the full description.\n\n### Changed\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 29 to 30.\n\n- Added a new boolean field `dns.cache_enabled` to the configuration.  This field explicitly controls whether DNS caching is enabled, replacing the previous implicit logic based on `dns.cache_size`.\n\n    ```yaml\n    # BEFORE:\n    'dns':\n        # …\n        'cache_size': 123456\n\n    # AFTER:\n    'dns':\n        # …\n        'cache_enabled': true\n        'cache_size': 123456\n    ```\n\n    To roll back this change, set the schema_version back to `29`.\n\n### Fixed\n\n- Disabled state of *Top clients* action button in web UI ([#7923]).\n\n[#7923]: https://github.com/AdguardTeam/AdGuardHome/issues/7923\n\n[go-1.24.6]:    https://groups.google.com/g/golang-announce/c/x5MKroML2yM\n[ms-v0.107.65]: https://github.com/AdguardTeam/AdGuardHome/milestone/100?closed=1\n\n## [v0.107.64] - 2025-07-28\n\nSee also the [v0.107.64 GitHub milestone][ms-v0.107.64].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.5][go-1.24.5].\n\n### Fixed\n\n- TTL override calculation ([#7903]).\n- Validation process for DNSCrypt settings ([#7856]).\n\n[#7856]: https://github.com/AdguardTeam/AdGuardHome/issues/7856\n[#7903]: https://github.com/AdguardTeam/AdGuardHome/issues/7903\n\n[go-1.24.5]:    https://groups.google.com/g/golang-announce/c/gTNJnDXmn34\n[ms-v0.107.64]: https://github.com/AdguardTeam/AdGuardHome/milestone/99?closed=1\n\n## [v0.107.63] - 2025-06-26\n\nSee also the [v0.107.63 GitHub milestone][ms-v0.107.63].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.4][go-1.24.4].\n\n### Fixed\n\n- The hostnames of DHCP clients with multiple labels not being recognized.\n\n- Status reported by the systemd service implementation in cases of auto-restart after a failed start.\n\n[go-1.24.4]:    https://groups.google.com/g/golang-announce/c/ufZ8WpEsA3A\n[ms-v0.107.63]: https://github.com/AdguardTeam/AdGuardHome/milestone/98?closed=1\n\n## [v0.107.62] - 2025-05-27\n\nSee also the [v0.107.62 GitHub milestone][ms-v0.107.62].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.3][go-1.24.3].\n\n### Fixed\n\n- Clients with CIDR identifiers showing zero requests on the *Settings → Client settings* page ([#2945]).\n\n- Command line option `--update` when the `dns.serve_plain_dns` configuration property was disabled ([#7801]).\n\n- DNS cache not working for custom upstream configurations.\n\n- Validation process for the DNS-over-TLS, DNS-over-QUIC, and HTTPS ports on the *Encryption Settings* page.\n\n- Searching for persistent clients using an exact match for CIDR in the `POST /clients/search` HTTP API.\n\n[#2945]: https://github.com/AdguardTeam/AdGuardHome/issues/2945\n[#7801]: https://github.com/AdguardTeam/AdGuardHome/issues/7801\n\n[go-1.24.3]:    https://groups.google.com/g/golang-announce/c/UZoIkUT367A\n[ms-v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/milestone/97?closed=1\n\n## [v0.107.61] - 2025-04-22\n\nSee also the [v0.107.61 GitHub milestone][ms-v0.107.61].\n\n### Security\n\n- Any simultaneous requests that are considered duplicates will now only result in a single request to upstreams, reducing the chance of a cache poisoning attack succeeding.  This is controlled by the new configuration object `pending_requests`, which has a single `enabled` property, set to `true` by default.\n\n    **NOTE:** We thank [Xiang Li][mr-xiang-li] for reporting this security issue.  It's strongly recommended to leave it enabled, otherwise AdGuard Home will be vulnerable to untrusted clients.\n\n[mr-xiang-li]:  https://lixiang521.com/\n[ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1\n\n## [v0.107.60] - 2025-04-14\n\nSee also the [v0.107.60 GitHub milestone][ms-v0.107.60].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.2][go-1.24.2].\n\n### Changed\n\n- Alpine Linux version in `Dockerfile` has been updated to 3.21 ([#7588]).\n\n### Deprecated\n\n- Node 20 support, Node 22 will be required in future releases.\n\n    **NOTE:** `npm` may be replaced with a different tool, such as `pnpm` or `yarn`, in a future release.\n\n### Fixed\n\n- Filtering for DHCP clients ([#7734]).\n\n- Incorrect label on login page ([#7729]).\n\n- Validation process for the HTTPS port on the *Encryption Settings* page.\n\n### Removed\n\n- Node 18 support.\n\n[#7588]: https://github.com/AdguardTeam/AdGuardHome/issues/7588\n[#7729]: https://github.com/AdguardTeam/AdGuardHome/issues/7729\n[#7734]: https://github.com/AdguardTeam/AdGuardHome/issues/7734\n\n[go-1.24.2]:    https://groups.google.com/g/golang-announce/c/Y2uBTVKjBQk\n[ms-v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/milestone/95?closed=1\n\n## [v0.107.59] - 2025-03-21\n\nSee also the [v0.107.59 GitHub milestone][ms-v0.107.59].\n\n- Rules with the `client` modifier not working ([#7708]).\n\n- The search form not working in the query log ([#7704]).\n\n[#7704]: https://github.com/AdguardTeam/AdGuardHome/issues/7704\n[#7708]: https://github.com/AdguardTeam/AdGuardHome/issues/7708\n\n[ms-v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/milestone/94?closed=1\n\n## [v0.107.58] - 2025-03-19\n\nSee also the [v0.107.58 GitHub milestone][ms-v0.107.58].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.1][go-1.24.1].\n\n### Added\n\n- The ability to check filtering rules for host names using an optional query type and optional ClientID or client IP address ([#4036]).\n\n- Optional `client` and `qtype` URL query parameters to the `GET /control/check_host` HTTP API.\n\n### Fixed\n\n- Clearing the DNS cache on the *DNS settings* page now includes both global cache and custom client cache.\n\n- Invalid ICMPv6 Router Advertisement messages ([#7547]).\n\n- Disabled button for autofilled login form.\n\n- Formatting of elapsed times less than one millisecond.\n\n- Changes to global upstream DNS settings not applying to custom client upstream configurations.\n\n- The formatting of large numbers in the clients tables on the *Client settings* page ([#7583]).\n\n[#4036]: https://github.com/AdguardTeam/AdGuardHome/issues/4036\n[#7547]: https://github.com/AdguardTeam/AdGuardHome/issues/7547\n[#7583]: https://github.com/AdguardTeam/AdGuardHome/issues/7583\n\n[go-1.24.1]: https://groups.google.com/g/golang-announce/c/4t3lzH3I0eI\n[ms-v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/milestone/93?closed=1\n\n## [v0.107.57] - 2025-02-20\n\nSee also the [v0.107.57 GitHub milestone][ms-v0.107.57].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.23.6][go-1.23.6].\n\n### Added\n\n- The ability to specify the upstream timeout in the Web UI.\n\n### Changed\n\n- The *Fastest IP address* upstream mode now correctly collects statistics for all upstream DNS servers.\n\n### Fixed\n\n- The hostnames of DHCP clients not being shown in the *Top clients* table on the dashboard ([#7627]).\n\n- The formatting of large numbers in the upstream table and query log ([#7590]).\n\n[#7590]: https://github.com/AdguardTeam/AdGuardHome/issues/7590\n[#7627]: https://github.com/AdguardTeam/AdGuardHome/issues/7627\n\n[go-1.23.6]:    https://groups.google.com/g/golang-announce/c/xU1ZCHUZw3k\n[ms-v0.107.57]: https://github.com/AdguardTeam/AdGuardHome/milestone/92?closed=1\n\n## [v0.107.56] - 2025-01-23\n\nSee also the [v0.107.56 GitHub milestone][ms-v0.107.56].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.23.5][go-1.23.5].\n\n### Added\n\n- The new HTTP API `POST /clients/search` that finds clients by their IP addresses, CIDRs, MAC addresses, or ClientIDs.  See `openapi/openapi.yaml` for the full description.\n\n### Deprecated\n\n- The `GET /clients/find` HTTP API is deprecated.  Use the new `POST /clients/search` API.\n\n### Fixed\n\n- Request count link in the clients table ([#7513]).\n\n- The formatting of large numbers on the dashboard ([#7329]).\n\n[#7329]: https://github.com/AdguardTeam/AdGuardHome/issues/7329\n[#7513]: https://github.com/AdguardTeam/AdGuardHome/issues/7513\n\n[go-1.23.5]: https://groups.google.com/g/golang-announce/c/sSaUhLA-2SI\n[ms-v0.107.56]: https://github.com/AdguardTeam/AdGuardHome/milestone/91?closed=1\n\n## [v0.107.55] - 2024-12-11\n\nSee also the [v0.107.55 GitHub milestone][ms-v0.107.55].\n\n### Security\n\n- The permission check and migration on Windows has been fixed to use the Windows security model more accurately ([#7400]).\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.23.4][go-1.23.4].\n\n- The Windows executables are now signed.\n\n### Added\n\n- The `--no-permcheck` command-line option to disable checking and migration of permissions for the security-sensitive files and directories, which caused issues on Windows ([#7400]).\n\n### Fixed\n\n- Setup guide styles in Firefox.\n\n- Goroutine leak during the upstream DNS server test ([#7357]).\n\n- Goroutine leak during configuration update resulting in increased response time ([#6818]).\n\n[#7357]: https://github.com/AdguardTeam/AdGuardHome/issues/7357\n[#7400]: https://github.com/AdguardTeam/AdGuardHome/issues/7400\n\n[go-1.23.4]: https://groups.google.com/g/golang-announce/c/3DyiMkYx4Fo\n[ms-v0.107.55]: https://github.com/AdguardTeam/AdGuardHome/milestone/90?closed=1\n\n## [v0.107.54] - 2024-11-06\n\nSee also the [v0.107.54 GitHub milestone][ms-v0.107.54].\n\n### Security\n\n- Incorrect handling of sensitive files permissions on Windows ([#7314]).\n\n### Changed\n\n- Improved filtering performance ([#6818]).\n\n### Fixed\n\n- Repetitive statistics log messages ([#7338]).\n\n- Custom client cache ([#7250]).\n\n- Missing runtime clients with information from the system hosts file on first AdGuard Home start ([#7315]).\n\n[#6818]: https://github.com/AdguardTeam/AdGuardHome/issues/6818\n[#7250]: https://github.com/AdguardTeam/AdGuardHome/issues/7250\n[#7314]: https://github.com/AdguardTeam/AdGuardHome/issues/7314\n[#7315]: https://github.com/AdguardTeam/AdGuardHome/issues/7315\n[#7338]: https://github.com/AdguardTeam/AdGuardHome/issues/7338\n\n[ms-v0.107.54]: https://github.com/AdguardTeam/AdGuardHome/milestone/89?closed=1\n\n## [v0.107.53] - 2024-10-03\n\nSee also the [v0.107.53 GitHub milestone][ms-v0.107.53].\n\n### Security\n\n- Previous versions of AdGuard Home allowed users to add any system file it had access to as filters, exposing them to be world-readable.  To prevent this, AdGuard Home now allows adding filtering-rule list files only from files matching the patterns enumerated in the `filtering.safe_fs_patterns` property in the configuration file.\n\n    We thank @itz-d0dgy for reporting this vulnerability, designated CVE-2024-36814, to us.\n\n- Additionally, AdGuard Home will now try to change the permissions of its files and directories to more restrictive ones to prevent similar vulnerabilities as well as limit the access to the configuration.\n\n    We thank @go-compile for reporting this vulnerability, designated CVE-2024-36586, to us.\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.23.2][go-1.23.2].\n\n### Added\n\n- Support for 64-bit RISC-V architecture ([#5704]).\n\n- Ecosia search engine is now supported in safe search ([#5009]).\n\n### Changed\n\n- Upstream server URL domain names requirements has been relaxed and now follow the same rules as their domain specifications.\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 28 to 29.\n\n- The new array `filtering.safe_fs_patterns` contains glob patterns for paths of files that can be added as local filtering-rule lists.  The migration should add list files that have already been added, as well as the default value, `$DATA_DIR/userfilters/*`.\n\n### Fixed\n\n- Property `clients.runtime_sources.dhcp` in the configuration file not taking effect.\n\n- Stale Google safe search domains list ([#7155]).\n\n- Bing safe search from Edge sidebar ([#7154]).\n\n- Text overflow on the query log page ([#7119]).\n\n### Known issues\n\n- Due to the complexity of the Windows permissions architecture and poor support from the standard Go library, we have to postpone the proper automated Windows fix until the next release.\n\n    **Temporary workaround:**  Set the permissions of the `AdGuardHome` directory to more restrictive ones manually.  To do that:\n\n    1. Locate the `AdGuardHome` directory.\n\n    2. Right-click on it and navigate to *Properties → Security → Advanced.*\n\n    3. (You might need to disable permission inheritance to make them more restricted.)\n\n    4. Adjust to give the `Full control` access to only the user which runs AdGuard Home.  Typically, `Administrator`.\n\n[#5009]: https://github.com/AdguardTeam/AdGuardHome/issues/5009\n[#5704]: https://github.com/AdguardTeam/AdGuardHome/issues/5704\n[#7119]: https://github.com/AdguardTeam/AdGuardHome/issues/7119\n[#7154]: https://github.com/AdguardTeam/AdGuardHome/pull/7154\n[#7155]: https://github.com/AdguardTeam/AdGuardHome/pull/7155\n\n[go-1.23.2]:    https://groups.google.com/g/golang-announce/c/NKEc8VT7Fz0\n[ms-v0.107.53]: https://github.com/AdguardTeam/AdGuardHome/milestone/88?closed=1\n\n## [v0.107.52] - 2024-07-04\n\nSee also the [v0.107.52 GitHub milestone][ms-v0.107.52].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [Go 1.22.5][go-1.22.5].\n\n### Added\n\n- The ability to disable logging using the new `log.enabled` configuration property ([#7079]).\n\n### Changed\n\n- Frontend rewritten in TypeScript.\n\n- The `systemd`-based service now uses `journal` for logging by default.  It also doesn’t create the `/var/log/` directory anymore ([#7053]).\n\n    **NOTE:** With an installed service for changes to take effect, you need to reinstall the service using `-r` flag of the [install script][install-script] or via the CLI (with root privileges):\n\n    ```sh\n    ./AdGuardHome -s uninstall\n    ./AdGuardHome -s install\n    ```\n\n    Don’t forget to backup your configuration file and other important data before reinstalling the service.\n\n### Deprecated\n\n- Node 18 support, Node 20 will be required in future releases.\n\n### Fixed\n\n- Panic caused by missing user-specific blocked services object in configuration file ([#7069]).\n\n- Tracking `/etc/hosts` file changes causing panics within particular filesystems on start ([#7076]).\n\n[#7053]: https://github.com/AdguardTeam/AdGuardHome/issues/7053\n[#7069]: https://github.com/AdguardTeam/AdGuardHome/issues/7069\n[#7076]: https://github.com/AdguardTeam/AdGuardHome/issues/7076\n[#7079]: https://github.com/AdguardTeam/AdGuardHome/issues/7079\n\n[go-1.22.5]:      https://groups.google.com/g/golang-announce/c/gyb7aM1C9H4\n[install-script]: https://github.com/AdguardTeam/AdGuardHome/?tab=readme-ov-file#automated-install-linux-and-mac\n\n[ms-v0.107.52]: https://github.com/AdguardTeam/AdGuardHome/milestone/87?closed=1\n\n## [v0.107.51] - 2024-06-06\n\nSee also the [v0.107.51 GitHub milestone][ms-v0.107.51].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [Go 1.22.4][go-1.22.4].\n\n### Changed\n\n- The HTTP server’s write timeout has been increased from 1 minute to 5 minutes to match the one used by AdGuard Home’s HTTP client to fetch filtering-list data ([#7041]).\n\n[#7041]: https://github.com/AdguardTeam/AdGuardHome/issues/7041\n\n[go-1.22.4]:    https://groups.google.com/g/golang-announce/c/XbxouI9gY7k/\n[ms-v0.107.51]: https://github.com/AdguardTeam/AdGuardHome/milestone/86?closed=1\n\n## [v0.107.50] - 2024-05-23\n\nSee also the [v0.107.50 GitHub milestone][ms-v0.107.50].\n\n### Fixed\n\n- Broken private reverse DNS upstream servers validation causing update failures ([#7013]).\n\n[#7013]: https://github.com/AdguardTeam/AdGuardHome/issues/7013\n\n[ms-v0.107.50]: https://github.com/AdguardTeam/AdGuardHome/milestone/85?closed=1\n\n## [v0.107.49] - 2024-05-21\n\nSee also the [v0.107.49 GitHub milestone][ms-v0.107.49].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [Go 1.22.3][go-1.22.3].\n\n### Added\n\n- Support for comments in the ipset file ([#5345]).\n\n### Changed\n\n- Private rDNS resolution now also affects `SOA` and `NS` requests ([#6882]).\n\n- Rewrite rules mechanics were changed due to improved resolving in safe search.\n\n### Deprecated\n\n- Currently, AdGuard Home skips persistent clients that have duplicate fields when reading them from the configuration file.  This behaviour is deprecated and will cause errors on startup in a future release.\n\n### Fixed\n\n- Acceptance of duplicate UIDs for persistent clients at startup.  See also the section on client settings on the [Wiki page][wiki-config].\n\n- Domain specifications for top-level domains not considered for requests to unqualified domains ([#6744]).\n\n- Support for link-local subnets, i.e. `fe80::/16`, as client identifiers ([#6312]).\n\n- Issues with QUIC and HTTP/3 upstreams on older Linux kernel versions ([#6422]).\n\n- YouTube restricted mode is not enforced by HTTPS queries on Firefox.\n\n- Support for link-local subnets, i.e. `fe80::/16`, in the access settings ([#6192]).\n\n- The ability to apply an invalid configuration for private rDNS, which led to server not starting.\n\n- Ignoring query log for clients with ClientID set ([#5812]).\n\n- Subdomains of `in-addr.arpa` and `ip6.arpa` containing zero-length prefix incorrectly considered invalid when specified for private rDNS upstream servers ([#6854]).\n\n- Unspecified IP addresses aren’t checked when using \"Fastest IP address\" mode ([#6875]).\n\n[#5345]: https://github.com/AdguardTeam/AdGuardHome/issues/5345\n[#5812]: https://github.com/AdguardTeam/AdGuardHome/issues/5812\n[#6192]: https://github.com/AdguardTeam/AdGuardHome/issues/6192\n[#6312]: https://github.com/AdguardTeam/AdGuardHome/issues/6312\n[#6422]: https://github.com/AdguardTeam/AdGuardHome/issues/6422\n[#6744]: https://github.com/AdguardTeam/AdGuardHome/issues/6744\n[#6854]: https://github.com/AdguardTeam/AdGuardHome/issues/6854\n[#6875]: https://github.com/AdguardTeam/AdGuardHome/issues/6875\n[#6882]: https://github.com/AdguardTeam/AdGuardHome/issues/6882\n\n[go-1.22.3]:    https://groups.google.com/g/golang-announce/c/wkkO4P9stm0\n[ms-v0.107.49]: https://github.com/AdguardTeam/AdGuardHome/milestone/84?closed=1\n\n## [v0.107.48] - 2024-04-05\n\nSee also the [v0.107.48 GitHub milestone][ms-v0.107.48].\n\n### Fixed\n\n- Access settings not being applied to encrypted protocols ([#6890]).\n\n[#6890]: https://github.com/AdguardTeam/AdGuardHome/issues/6890\n\n[ms-v0.107.48]: https://github.com/AdguardTeam/AdGuardHome/milestone/83?closed=1\n\n## [v0.107.47] - 2024-04-04\n\nSee also the [v0.107.47 GitHub milestone][ms-v0.107.47].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [Go 1.22.2][go-1.22.2].\n\n### Changed\n\n- Time Zone Database is now embedded in the binary ([#6758]).\n\n- Failed authentication attempts show the originating IP address in the logs, if the request came from a trusted proxy ([#5829]).\n\n### Deprecated\n\n- Go 1.22 support.  Future versions will require at least Go 1.23 to build.\n\n- Currently, AdGuard Home uses a best-effort algorithm to fix invalid IDs of filtering-rule lists on startup.  This feature is deprecated, and invalid IDs will cause errors on startup in a future version.\n\n- Node.JS 16.  Future versions will require at least Node.JS 18 to build.\n\n### Fixed\n\n- Resetting DNS upstream mode when applying unrelated settings ([#6851]).\n\n- Symbolic links to the configuration file begin replaced by a copy of the real file upon startup on FreeBSD ([#6717]).\n\n### Removed\n\n- Go 1.21 support.\n\n[#5829]: https://github.com/AdguardTeam/AdGuardHome/issues/5829\n[#6717]: https://github.com/AdguardTeam/AdGuardHome/issues/6717\n[#6758]: https://github.com/AdguardTeam/AdGuardHome/issues/6758\n[#6851]: https://github.com/AdguardTeam/AdGuardHome/issues/6851\n\n[go-1.22.2]:    https://groups.google.com/g/golang-announce/c/YgW0sx8mN3M/\n[ms-v0.107.47]: https://github.com/AdguardTeam/AdGuardHome/milestone/82?closed=1\n\n## [v0.107.46] - 2024-03-20\n\nSee also the [v0.107.46 GitHub milestone][ms-v0.107.46].\n\n### Added\n\n- Ability to disable the use of system hosts file information for query resolution ([#6610]).\n\n- Ability to define custom directories for storage of query log files and statistics ([#5992]).\n\n### Changed\n\n- Private rDNS resolution (`dns.use_private_ptr_resolvers` in YAML configuration) now requires a valid \"Private reverse DNS servers\", when enabled ([#6820]).\n\n    **NOTE:** Disabling private rDNS resolution behaves effectively the same as if no private reverse DNS servers provided by user and by the OS.\n\n### Fixed\n\n- Statistics for 7 days displayed by day on the dashboard graph ([#6712]).\n\n- Missing \"served from cache\" label on long DNS server strings ([#6740]).\n\n- Incorrect tracking of the system hosts file’s changes ([#6711]).\n\n[#5992]: https://github.com/AdguardTeam/AdGuardHome/issues/5992\n[#6610]: https://github.com/AdguardTeam/AdGuardHome/issues/6610\n[#6711]: https://github.com/AdguardTeam/AdGuardHome/issues/6711\n[#6712]: https://github.com/AdguardTeam/AdGuardHome/issues/6712\n[#6740]: https://github.com/AdguardTeam/AdGuardHome/issues/6740\n[#6820]: https://github.com/AdguardTeam/AdGuardHome/issues/6820\n\n[ms-v0.107.46]: https://github.com/AdguardTeam/AdGuardHome/milestone/81?closed=1\n\n## [v0.107.45] - 2024-03-06\n\nSee also the [v0.107.45 GitHub milestone][ms-v0.107.45].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [Go 1.21.8][go-1.21.8].\n\n### Added\n\n- Context menu item in the Query Log to add a Client to the Persistent client list ([#6679]).\n\n### Changed\n\n- Starting with this release our scripts are using Go’s [forward compatibility mechanism][go-toolchain] for updating the Go version.\n\n    **Important note for porters:**  This change means that if your `go` version is 1.21+ but is different from the one required by AdGuard Home, the `go` tool will automatically download the required version.\n\n    If you want to use the version installed on your builder, run:\n\n    ```sh\n    go get go@$YOUR_VERSION\n    go mod tidy\n    ```\n\n    and call `make` with `GOTOOLCHAIN=local`.\n\n### Deprecated\n\n- Go 1.21 support.  Future versions will require at least Go 1.22 to build.\n\n### Fixed\n\n- Missing IP addresses in logs when querying for domain names from the ignore lists.\n\n- Blank page after resetting access clients ([#6634]).\n\n- Wrong algorithm for caching bootstrapped upstream addresses ([#6723]).\n\n### Removed\n\n- Go 1.20 support, as it has reached end of life.\n\n[#6634]: https://github.com/AdguardTeam/AdGuardHome/issues/6634\n[#6679]: https://github.com/AdguardTeam/AdGuardHome/issues/6679\n[#6723]: https://github.com/AdguardTeam/AdGuardHome/issues/6723\n\n[go-1.21.8]:    https://groups.google.com/g/golang-announce/c/5pwGVUPoMbg\n[go-toolchain]: https://go.dev/blog/toolchain\n[ms-v0.107.45]: https://github.com/AdguardTeam/AdGuardHome/milestone/80?closed=1\n\n## [v0.107.44] - 2024-02-06\n\nSee also the [v0.107.44 GitHub milestone][ms-v0.107.44].\n\n### Added\n\n- Timezones in the Etc/ area to the timezone list ([#6568]).\n\n- The schema version of the configuration file to the output of running `AdGuardHome` (or `AdGuardHome.exe`) with `-v --version` command-line options ([#6545]).\n\n- Ability to disable plain-DNS serving via UI if an encrypted protocol is already used ([#1660]).\n\n### Changed\n\n- The bootstrapped upstream addresses are now updated according to the TTL of the bootstrap DNS response ([#6321]).\n\n- Logging level of timeout errors is now `error` instead of `debug` ([#6574]).\n\n- The field `\"upstream_mode\"` in `POST /control/dns_config` and `GET /control/dns_info` HTTP APIs now accepts `load_balance` value.  Check `openapi/CHANGELOG.md` for more details.\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 27 to 28.\n\n- The new property `clients.persistent.*.uid`, which is a unique identifier of the persistent client.\n\n- The properties `dns.all_servers` and `dns.fastest_addr` were removed, their values migrated to newly added field `dns.upstream_mode` that describes the logic through which upstreams will be used.  See also a [Wiki page][wiki-config].\n\n    ```yaml\n    # BEFORE:\n    'dns':\n        # …\n        'all_servers': true\n        'fastest_addr': true\n\n    # AFTER:\n    'dns':\n        # …\n        'upstream_mode': 'parallel'\n    ```\n\n    To rollback this change, remove the new field `upstream_mode`, set back `dns.all_servers` and `dns.fastest_addr` properties in `dns` section, and change the `schema_version` back to `27`.\n\n### Fixed\n\n- “Invalid AddrPort” in the *Private reverse DNS servers* section on the *Settings → DNS settings* page.\n\n- Panic on using `--no-etc-hosts` flag ([#6644]).\n\n- Schedule display in the client settings after creating or updating.\n\n- Zero value in `querylog.size_memory` disables logging ([#6570]).\n\n- Non-anonymized IP addresses on the dashboard ([#6584]).\n\n- Maximum cache TTL requirement when editing minimum cache TTL in the Web UI ([#6409]).\n\n- Load balancing algorithm stuck on a single server ([#6480]).\n\n- Statistics for 7 days displayed as 168 hours on the dashboard.\n\n- Pre-filling the Edit static lease window with data ([#6534]).\n\n- Names defined in the `/etc/hosts` for a single address family wrongly considered undefined for another family ([#6541]).\n\n- Omitted CNAME records in safe search results, which can cause YouTube to not work on iOS ([#6352]).\n\n[#6321]: https://github.com/AdguardTeam/AdGuardHome/issues/6321\n[#6352]: https://github.com/AdguardTeam/AdGuardHome/issues/6352\n[#6409]: https://github.com/AdguardTeam/AdGuardHome/issues/6409\n[#6480]: https://github.com/AdguardTeam/AdGuardHome/issues/6480\n[#6534]: https://github.com/AdguardTeam/AdGuardHome/issues/6534\n[#6541]: https://github.com/AdguardTeam/AdGuardHome/issues/6541\n[#6545]: https://github.com/AdguardTeam/AdGuardHome/issues/6545\n[#6568]: https://github.com/AdguardTeam/AdGuardHome/issues/6568\n[#6570]: https://github.com/AdguardTeam/AdGuardHome/issues/6570\n[#6574]: https://github.com/AdguardTeam/AdGuardHome/issues/6574\n[#6584]: https://github.com/AdguardTeam/AdGuardHome/issues/6584\n[#6644]: https://github.com/AdguardTeam/AdGuardHome/issues/6644\n\n[ms-v0.107.44]: https://github.com/AdguardTeam/AdGuardHome/milestone/79?closed=1\n[wiki-config]:  https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration\n\n## [v0.107.43] - 2023-12-11\n\nSee also the [v0.107.43 GitHub milestone][ms-v0.107.43].\n\n### Fixed\n\n- Incorrect handling of IPv4-in-IPv6 addresses when binding to an unspecified address on some machines ([#6510]).\n\n[#6510]: https://github.com/AdguardTeam/AdGuardHome/issues/6510\n\n[ms-v0.107.43]: https://github.com/AdguardTeam/AdGuardHome/milestone/78?closed=1\n\n## [v0.107.42] - 2023-12-07\n\nSee also the [v0.107.42 GitHub milestone][ms-v0.107.42].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-39326, CVE-2023-45283, and CVE-2023-45285 Go vulnerabilities fixed in [Go 1.20.12][go-1.20.12].\n\n### Added\n\n- Ability to set client’s custom DNS cache ([#6263]).\n\n- Ability to disable plain-DNS serving through configuration file if an encrypted protocol is already enabled ([#1660]).\n\n- Ability to specify rate limiting settings in the Web UI ([#6369]).\n\n### Changed\n\n#### Configuration changes\n\n- The new property `dns.serve_plain_dns` has been added to the configuration file ([#1660]).\n\n- The property `dns.bogus_nxdomain` is now validated more strictly.\n\n- Added new properties `clients.persistent.*.upstreams_cache_enabled` and `clients.persistent.*.upstreams_cache_size` that describe cache configuration for each client’s custom upstream configuration.\n\n### Fixed\n\n- `ipset` entries family validation ([#6420]).\n\n- Pre-filling the *New static lease* window with data ([#6402]).\n\n- Protection pause timer synchronization ([#5759]).\n\n[#1660]: https://github.com/AdguardTeam/AdGuardHome/issues/1660\n[#5759]: https://github.com/AdguardTeam/AdGuardHome/issues/5759\n[#6263]: https://github.com/AdguardTeam/AdGuardHome/issues/6263\n[#6369]: https://github.com/AdguardTeam/AdGuardHome/issues/6369\n[#6402]: https://github.com/AdguardTeam/AdGuardHome/issues/6402\n[#6420]: https://github.com/AdguardTeam/AdGuardHome/issues/6420\n\n[go-1.20.12]:   https://groups.google.com/g/golang-announce/c/iLGK3x6yuNo/m/z6MJ-eB0AQAJ\n[ms-v0.107.42]: https://github.com/AdguardTeam/AdGuardHome/milestone/77?closed=1\n\n## [v0.107.41] - 2023-11-13\n\nSee also the [v0.107.41 GitHub milestone][ms-v0.107.41].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-45283 and CVE-2023-45284 Go vulnerabilities fixed in [Go 1.20.11][go-1.20.11].\n\n### Added\n\n- Ability to specify subnet lengths for IPv4 and IPv6 addresses, used for rate limiting requests, in the configuration file ([#6368]).\n\n- Ability to specify multiple domain specific upstreams per line, e.g.  `[/domain1/../domain2/]upstream1 upstream2 .. upstreamN` ([#4977]).\n\n### Changed\n\n- Increased the height of the ready-to-use filter lists dialog ([#6358]).\n\n- Improved logging of authentication failures ([#6357]).\n\n#### Configuration changes\n\n- New properties `dns.ratelimit_subnet_len_ipv4` and `dns.ratelimit_subnet_len_ipv6` have been added to the configuration file ([#6368]).\n\n### Fixed\n\n- Schedule timezone not being sent ([#6401]).\n\n- Average request processing time calculation ([#6220]).\n\n- Redundant truncation of long client names in the Top Clients table ([#6338]).\n\n- Scrolling column headers in the tables ([#6337]).\n\n- `$important,dnsrewrite` rules not overriding allowlist rules ([#6204]).\n\n- Dark mode DNS rewrite background ([#6329]).\n\n- Issues with QUIC and HTTP/3 upstreams on Linux ([#6335]).\n\n[#4977]: https://github.com/AdguardTeam/AdGuardHome/issues/4977\n[#6204]: https://github.com/AdguardTeam/AdGuardHome/issues/6204\n[#6220]: https://github.com/AdguardTeam/AdGuardHome/issues/6220\n[#6329]: https://github.com/AdguardTeam/AdGuardHome/issues/6329\n[#6335]: https://github.com/AdguardTeam/AdGuardHome/issues/6335\n[#6337]: https://github.com/AdguardTeam/AdGuardHome/issues/6337\n[#6338]: https://github.com/AdguardTeam/AdGuardHome/issues/6338\n[#6357]: https://github.com/AdguardTeam/AdGuardHome/issues/6357\n[#6358]: https://github.com/AdguardTeam/AdGuardHome/issues/6358\n[#6368]: https://github.com/AdguardTeam/AdGuardHome/issues/6368\n[#6401]: https://github.com/AdguardTeam/AdGuardHome/issues/6401\n\n[go-1.20.11]:   https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY/m/d-jSKR_jBwAJ\n[ms-v0.107.41]: https://github.com/AdguardTeam/AdGuardHome/milestone/76?closed=1\n\n## [v0.107.40] - 2023-10-18\n\nSee also the [v0.107.40 GitHub milestone][ms-v0.107.40].\n\n### Changed\n\n- *Block* and *Unblock* buttons of the query log moved to the tooltip menu ([#684]).\n\n### Fixed\n\n- Dashboard tables scroll issue ([#6180]).\n\n- The time shown in the statistics is one hour less than the current time ([#6296]).\n\n- Issues with QUIC and HTTP/3 upstreams on FreeBSD ([#6301]).\n\n- Panic on clearing the query log ([#6304]).\n\n[#684]:  https://github.com/AdguardTeam/AdGuardHome/issues/684\n[#6180]: https://github.com/AdguardTeam/AdGuardHome/issues/6180\n[#6296]: https://github.com/AdguardTeam/AdGuardHome/issues/6296\n[#6301]: https://github.com/AdguardTeam/AdGuardHome/issues/6301\n[#6304]: https://github.com/AdguardTeam/AdGuardHome/issues/6304\n\n[ms-v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/milestone/75?closed=1\n\n## [v0.107.39] - 2023-10-11\n\nSee also the [v0.107.39 GitHub milestone][ms-v0.107.39].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-39323 and CVE-2023-39325 Go vulnerabilities fixed in [Go 1.20.9][go-1.20.9] and [Go 1.20.10][go-1.20.10].\n\n### Added\n\n- Ability to edit static leases on *DHCP settings* page ([#1700]).\n\n- Ability to specify for how long clients should cache a filtered response, using the *Blocked response TTL* field on the *DNS settings* page ([#4569]).\n\n### Changed\n\n- `ipset` entries are updated more frequently ([#6233]).\n\n- Node.JS 16 is now required to build the frontend.\n\n### Fixed\n\n- Incorrect domain-specific upstream matching for `DS` queries ([#6156]).\n\n- Improper validation of password length ([#6280]).\n\n- Wrong algorithm for filtering self addresses from the list of private upstream DNS servers ([#6231]).\n\n- An accidental change in DNS rewrite priority ([#6226]).\n\n[#1700]: https://github.com/AdguardTeam/AdGuardHome/issues/1700\n[#4569]: https://github.com/AdguardTeam/AdGuardHome/issues/4569\n[#6156]: https://github.com/AdguardTeam/AdGuardHome/issues/6156\n[#6226]: https://github.com/AdguardTeam/AdGuardHome/issues/6226\n[#6231]: https://github.com/AdguardTeam/AdGuardHome/issues/6231\n[#6233]: https://github.com/AdguardTeam/AdGuardHome/issues/6233\n[#6280]: https://github.com/AdguardTeam/AdGuardHome/issues/6280\n\n[go-1.20.10]:   https://groups.google.com/g/golang-announce/c/iNNxDTCjZvo/m/UDd7VKQuAAAJ\n[go-1.20.9]:    https://groups.google.com/g/golang-announce/c/XBa1oHDevAo/m/desYyx3qAgAJ\n[ms-v0.107.39]: https://github.com/AdguardTeam/AdGuardHome/milestone/74?closed=1\n\n## [v0.107.38] - 2023-09-11\n\nSee also the [v0.107.38 GitHub milestone][ms-v0.107.38].\n\n### Fixed\n\n- Incorrect original answer when a response is filtered ([#6183]).\n\n- Comments in the *Fallback DNS servers* field in the UI ([#6182]).\n\n- Empty or default Safe Browsing and Parental Control settings ([#6181]).\n\n- Various UI issues.\n\n[#6181]: https://github.com/AdguardTeam/AdGuardHome/issues/6181\n[#6182]: https://github.com/AdguardTeam/AdGuardHome/issues/6182\n[#6183]: https://github.com/AdguardTeam/AdGuardHome/issues/6183\n\n[ms-v0.107.38]: https://github.com/AdguardTeam/AdGuardHome/milestone/73?closed=1\n\n## [v0.107.37] - 2023-09-07\n\nSee also the [v0.107.37 GitHub milestone][ms-v0.107.37].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-39318, CVE-2023-39319, and CVE-2023-39320 Go vulnerabilities fixed in [Go 1.20.8][go-1.20.8].\n\n### Added\n\n- AdBlock-style syntax support for ignored domains in logs and statistics ([#5720]).\n\n- [`Strict-Transport-Security`][hsts] header in the HTTP API and DNS-over-HTTPS responses when HTTPS is forced ([#2998]).  See [RFC 6797][rfc6797].\n\n- UI for the schedule of the service-blocking pause ([#951]).\n\n- IPv6 hints are now filtered in case IPv6 addresses resolving is disabled ([#6122]).\n\n- The ability to set fallback DNS servers in the configuration file and the UI ([#3701]).\n\n- While adding or updating blocklists, the title can now be parsed from `! Title:` definition of the blocklist’s source ([#6020]).\n\n- The ability to filter DNS HTTPS records including IPv4 and IPv6 hints ([#6053]).\n\n- Two new metrics showing total number of responses from each upstream DNS server and their average processing time in the Web UI ([#1453]).\n\n- The ability to set the port for the `pprof` debug API, see configuration changes below.\n\n### Changed\n\n- `$dnsrewrite` rules containing IPv4-mapped IPv6 addresses are now working consistently with legacy DNS rewrites and match the `AAAA` requests.\n\n- For non-A and non-AAAA requests, which has been filtered, the NODATA response is returned if the blocking mode isn’t set to `Null IP`.  In previous versions it returned NXDOMAIN response in such cases.\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 24 to 27.\n\n- Ignore rules blocking `.` in `querylog.ignored` and `statistics.ignored` have been migrated to AdBlock syntax (`|.^`).  To rollback this change, restore the rules and change the `schema_version` back to `26`.\n\n- Filtering-related settings have been moved from `dns` section of the YAML configuration file to the new section `filtering`:\n\n    ```yaml\n    # BEFORE:\n    'dns':\n      'filtering_enabled': true\n      'filters_update_interval': 24\n      'parental_enabled': false\n      'safebrowsing_enabled': false\n      'safebrowsing_cache_size': 1048576\n      'safesearch_cache_size': 1048576\n      'parental_cache_size': 1048576\n      'safe_search':\n        'enabled': false\n        'bing': true\n        'duckduckgo': true\n        'google': true\n        'pixabay': true\n        'yandex': true\n        'youtube': true\n      'rewrites': []\n      'blocked_services':\n        'schedule':\n          'time_zone': 'Local'\n        'ids': []\n      'protection_enabled':        true,\n      'blocking_mode':             'custom_ip',\n      'blocking_ipv4':             '1.2.3.4',\n      'blocking_ipv6':             '1:2:3::4',\n      'blocked_response_ttl':      10,\n      'protection_disabled_until': 'null',\n      'parental_block_host':       'p.dns.adguard.com',\n      'safebrowsing_block_host':   's.dns.adguard.com'\n\n    # AFTER:\n    'filtering':\n      'filtering_enabled': true\n      'filters_update_interval': 24\n      'parental_enabled': false\n      'safebrowsing_enabled': false\n      'safebrowsing_cache_size': 1048576\n      'safesearch_cache_size': 1048576\n      'parental_cache_size': 1048576\n      'safe_search':\n        'enabled': false\n        'bing': true\n        'duckduckgo': true\n        'google': true\n        'pixabay': true\n        'yandex': true\n        'youtube': true\n      'rewrites': []\n      'blocked_services':\n        'schedule':\n          'time_zone': 'Local'\n        'ids': []\n      'protection_enabled':        true,\n      'blocking_mode':             'custom_ip',\n      'blocking_ipv4':             '1.2.3.4',\n      'blocking_ipv6':             '1:2:3::4',\n      'blocked_response_ttl':      10,\n      'protection_disabled_until': 'null',\n      'parental_block_host':       'p.dns.adguard.com',\n      'safebrowsing_block_host':   's.dns.adguard.com',\n    ```\n\n    To rollback this change, remove the new object `filtering`, set back filtering properties in `dns` section, and change the `schema_version` back to `25`.\n\n- Property `debug_pprof` which used to setup profiling HTTP handler, is now moved to the new `pprof` object under `http` section.  The new object contains properties `enabled` and `port`:\n\n    ```yaml\n    # BEFORE:\n    'debug_pprof': true\n\n    # AFTER:\n    'http':\n      'pprof':\n        'enabled': true\n        'port': 6060\n    ```\n\n    Note that the new default `6060` is used as default.  To rollback this change, remove the new object `pprof`, set back `debug_pprof`, and change the `schema_version` back to `24`.\n\n### Fixed\n\n- Incorrect display date on statistics graph ([#5793]).\n\n- Missing query log entries and statistics on service restart ([#6100]).\n\n- Occasional DNS-over-QUIC and DNS-over-HTTP/3 errors ([#6133]).\n\n- Legacy DNS rewrites containing IPv4-mapped IPv6 addresses are now matching the `AAAA` requests, not `A` ([#6050]).\n\n- File log configuration, such as `max_size`, being ignored ([#6093]).\n\n- Panic on using a single-slash filtering rule.\n\n- Panic on shutting down while DNS requests are in process of filtering ([#5948]).\n\n[#1453]: https://github.com/AdguardTeam/AdGuardHome/issues/1453\n[#2998]: https://github.com/AdguardTeam/AdGuardHome/issues/2998\n[#3701]: https://github.com/AdguardTeam/AdGuardHome/issues/3701\n[#5720]: https://github.com/AdguardTeam/AdGuardHome/issues/5720\n[#5793]: https://github.com/AdguardTeam/AdGuardHome/issues/5793\n[#5948]: https://github.com/AdguardTeam/AdGuardHome/issues/5948\n[#6020]: https://github.com/AdguardTeam/AdGuardHome/issues/6020\n[#6050]: https://github.com/AdguardTeam/AdGuardHome/issues/6050\n[#6053]: https://github.com/AdguardTeam/AdGuardHome/issues/6053\n[#6093]: https://github.com/AdguardTeam/AdGuardHome/issues/6093\n[#6100]: https://github.com/AdguardTeam/AdGuardHome/issues/6100\n[#6122]: https://github.com/AdguardTeam/AdGuardHome/issues/6122\n[#6133]: https://github.com/AdguardTeam/AdGuardHome/issues/6133\n\n[go-1.20.8]:    https://groups.google.com/g/golang-announce/c/Fm51GRLNRvM/m/F5bwBlXMAQAJ\n[hsts]:         https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security\n[ms-v0.107.37]: https://github.com/AdguardTeam/AdGuardHome/milestone/72?closed=1\n[rfc6797]:      https://datatracker.ietf.org/doc/html/rfc6797\n\n## [v0.107.36] - 2023-08-02\n\nSee also the [v0.107.36 GitHub milestone][ms-v0.107.36].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-29409 Go vulnerability fixed in [Go 1.20.7][go-1.20.7].\n\n### Deprecated\n\n- Go 1.20 support.  Future versions will require at least Go 1.21 to build.\n\n### Fixed\n\n- Inability to block queries for the root domain, such as `NS .` queries, using the *Disallowed domains* feature on the *DNS settings* page ([#6049]).  Users who want to block `.` queries should use the `|.^` AdBlock rule or a similar regular expression.\n\n- Client hostnames not resolving when upstream server responds with zero-TTL records ([#6046]).\n\n### Removed\n\n- Go 1.19 support, as it has reached end of life.\n\n[#6046]: https://github.com/AdguardTeam/AdGuardHome/issues/6046\n[#6049]: https://github.com/AdguardTeam/AdGuardHome/issues/6049\n\n[go-1.20.7]:    https://groups.google.com/g/golang-announce/c/X0b6CsSAaYI/m/Efv5DbZ9AwAJ\n[ms-v0.107.36]: https://github.com/AdguardTeam/AdGuardHome/milestone/71?closed=1\n\n## [v0.107.35] - 2023-07-26\n\nSee also the [v0.107.35 GitHub milestone][ms-v0.107.35].\n\n### Changed\n\n- Improved reliability filtering-rule list updates on Unix systems.\n\n### Fixed\n\n- Occasional client information lookup failures that could lead to the DNS server getting stuck ([#6006]).\n\n- `bufio.Scanner: token too long` and other errors when trying to add filtering-rule lists with lines over 1024 bytes long or containing cosmetic rules ([#6003]).\n\n### Removed\n\n- Default exposure of the non-standard ports 784 and 8853 for DNS-over-QUIC in the `Dockerfile`.\n\n[#6003]: https://github.com/AdguardTeam/AdGuardHome/issues/6003\n[#6006]: https://github.com/AdguardTeam/AdGuardHome/issues/6006\n\n[ms-v0.107.35]: https://github.com/AdguardTeam/AdGuardHome/milestone/70?closed=1\n\n## [v0.107.34] - 2023-07-12\n\nSee also the [v0.107.34 GitHub milestone][ms-v0.107.34].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-29406 Go vulnerability fixed in [Go 1.19.11][go-1.19.11].\n\n### Added\n\n- Ability to ignore queries for the root domain, such as `NS .` queries ([#5990]).\n\n### Changed\n\n- Improved CPU and RAM consumption during updates of filtering-rule lists.\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 23 to 24.\n\n- Properties starting with `log_`, and `verbose` property, which used to set up logging are now moved to the new object `log` containing new properties `file`, `max_backups`, `max_size`, `max_age`, `compress`, `local_time`, and `verbose`:\n\n    ```yaml\n    # BEFORE:\n    'log_file': \"\"\n    'log_max_backups': 0\n    'log_max_size': 100\n    'log_max_age': 3\n    'log_compress': false\n    'log_localtime': false\n    'verbose': false\n\n    # AFTER:\n    'log':\n        'file': \"\"\n        'max_backups': 0\n        'max_size': 100\n        'max_age': 3\n        'compress': false\n        'local_time': false\n        'verbose': false\n    ```\n\n    To rollback this change, remove the new object `log`, set back `log_` and `verbose` properties and change the `schema_version` back to `23`.\n\n### Deprecated\n\n- Default exposure of the non-standard ports 784 and 8853 for DNS-over-QUIC in the `Dockerfile`.\n\n### Fixed\n\n- Two unspecified IPs when a host is blocked in two filter lists ([#5972]).\n\n- Incorrect setting of Parental Control cache size.\n\n- Excessive RAM and CPU consumption by Safe Browsing and Parental Control filters ([#5896]).\n\n### Removed\n\n- The `HEALTHCHECK` section and the use of `tini` in the `ENTRYPOINT` section in `Dockerfile` ([#5939]).  They caused a lot of issues, especially with tools like `docker-compose` and `podman`.\n\n    **NOTE:** Some Docker tools may cache `ENTRYPOINT` sections, so some users may be required to backup their configuration, stop the container, purge the old image, and reload it from scratch.\n\n[#5896]: https://github.com/AdguardTeam/AdGuardHome/issues/5896\n[#5972]: https://github.com/AdguardTeam/AdGuardHome/issues/5972\n[#5990]: https://github.com/AdguardTeam/AdGuardHome/issues/5990\n\n[go-1.19.11]:   https://groups.google.com/g/golang-announce/c/2q13H6LEEx0/m/sduSepLLBwAJ\n[ms-v0.107.34]: https://github.com/AdguardTeam/AdGuardHome/milestone/69?closed=1\n\n## [v0.107.33] - 2023-07-03\n\nSee also the [v0.107.33 GitHub milestone][ms-v0.107.33].\n\n### Added\n\n- The new command-line flag `--web-addr` is the address to serve the web UI on, in the host:port format.\n\n- The ability to set inactivity periods for filtering blocked services, both globally and per client, in the configuration file ([#951]).  The UI changes are coming in the upcoming releases.\n\n- The ability to edit rewrite rules via `PUT /control/rewrite/update` HTTP API and the Web UI ([#1577]).\n\n### Changed\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 20 to 23.\n\n- Properties `bind_host`, `bind_port`, and `web_session_ttl` which used to setup web UI binding configuration, are now moved to a new object `http` containing new properties `address` and `session_ttl`:\n\n    ```yaml\n    # BEFORE:\n    'bind_host': '1.2.3.4'\n    'bind_port': 8080\n    'web_session_ttl': 720\n\n    # AFTER:\n    'http':\n      'address': '1.2.3.4:8080'\n      'session_ttl': '720h'\n    ```\n\n    Note that the new `http.session_ttl` property is now a duration string.  To rollback this change, remove the new object `http`, set back `bind_host`, `bind_port`, `web_session_ttl`,  and change the `schema_version` back to `22`.\n\n- Property `clients.persistent.blocked_services`, which in schema versions 21 and earlier used to be a list containing ids of blocked services, is now an object containing ids and schedule for blocked services:\n\n    ```yaml\n    # BEFORE:\n    'clients':\n      'persistent':\n        - 'name': 'client-name'\n          'blocked_services':\n          - id_1\n          - id_2\n\n    # AFTER:\n    'clients':\n      'persistent':\n      - 'name': client-name\n        'blocked_services':\n          'ids':\n          - id_1\n          - id_2\n        'schedule':\n          'time_zone': 'Local'\n          'sun':\n            'start': '0s'\n            'end': '24h'\n          'mon':\n            'start': '1h'\n            'end': '23h'\n    ```\n\n    To rollback this change, replace `clients.persistent.blocked_services` object with the list of ids of blocked services and change the `schema_version` back to `21`.\n\n- Property `dns.blocked_services`, which in schema versions 20 and earlier used to be a list containing ids of blocked services, is now an object containing ids and schedule for blocked services:\n\n    ```yaml\n    # BEFORE:\n    'blocked_services':\n    - id_1\n    - id_2\n\n    # AFTER:\n    'blocked_services':\n      'ids':\n      - id_1\n      - id_2\n      'schedule':\n        'time_zone': 'Local'\n        'sun':\n          'start': '0s'\n          'end': '24h'\n        'mon':\n          'start': '10m'\n          'end': '23h30m'\n        'tue':\n          'start': '20m'\n          'end': '23h'\n        'wed':\n          'start': '30m'\n          'end': '22h30m'\n        'thu':\n          'start': '40m'\n          'end': '22h'\n        'fri':\n          'start': '50m'\n          'end': '21h30m'\n        'sat':\n          'start': '1h'\n          'end': '21h'\n    ```\n\n    To rollback this change, replace `dns.blocked_services` object with the list of ids of blocked services and change the `schema_version` back to `20`.\n\n### Deprecated\n\n- The `HEALTHCHECK` section and the use of `tini` in the `ENTRYPOINT` section in `Dockerfile` ([#5939]).  They cause a lot of issues, especially with tools like `docker-compose` and `podman`, and will be removed in a future release.\n\n- Flags `-h`, `--host`, `-p`, `--port` have been deprecated.  The `-h` flag will work as an alias for `--help`, instead of the deprecated `--host` in the future releases.\n\n### Fixed\n\n- Ignoring of `/etc/hosts` file when resolving the hostnames of upstream DNS servers ([#5902]).\n\n- Excessive error logging when using DNS-over-QUIC ([#5285]).\n\n- Inability to set `bind_host` in `AdGuardHome.yaml` in Docker ([#4231], [#4235]).\n\n- The blocklists can now be deleted properly ([#5700]).\n\n- Queries with the question-section target `.`, for example `NS .`, are now counted in the statistics and correctly shown in the query log ([#5910]).\n\n- Safe Search not working with `AAAA` queries for domains that don’t have `AAAA` records ([#5913]).\n\n[#951]:  https://github.com/AdguardTeam/AdGuardHome/issues/951\n[#1577]: https://github.com/AdguardTeam/AdGuardHome/issues/1577\n[#4231]: https://github.com/AdguardTeam/AdGuardHome/issues/4231\n[#4235]: https://github.com/AdguardTeam/AdGuardHome/pull/4235\n[#5285]: https://github.com/AdguardTeam/AdGuardHome/issues/5285\n[#5700]: https://github.com/AdguardTeam/AdGuardHome/issues/5700\n[#5902]: https://github.com/AdguardTeam/AdGuardHome/issues/5902\n[#5910]: https://github.com/AdguardTeam/AdGuardHome/issues/5910\n[#5913]: https://github.com/AdguardTeam/AdGuardHome/issues/5913\n[#5939]: https://github.com/AdguardTeam/AdGuardHome/discussions/5939\n\n[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1\n\n## [v0.107.32] - 2023-06-13\n\n### Fixed\n\n- DNSCrypt upstream not resetting the client and resolver information on dialing errors ([#5872]).\n\n## [v0.107.31] - 2023-06-08\n\nSee also the [v0.107.31 GitHub milestone][ms-v0.107.31].\n\n### Fixed\n\n- Startup errors on OpenWrt ([#5872]).\n\n- Plain-UDP upstreams always falling back to TCP, causing outages and slowdowns ([#5873], [#5874]).\n\n[#5872]: https://github.com/AdguardTeam/AdGuardHome/issues/5872\n[#5873]: https://github.com/AdguardTeam/AdGuardHome/issues/5873\n[#5874]: https://github.com/AdguardTeam/AdGuardHome/issues/5874\n\n[ms-v0.107.31]: https://github.com/AdguardTeam/AdGuardHome/milestone/67?closed=1\n\n## [v0.107.30] - 2023-06-07\n\nSee also the [v0.107.30 GitHub milestone][ms-v0.107.30].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-29402, CVE-2023-29403, and CVE-2023-29404 Go vulnerabilities fixed in [Go 1.19.10][go-1.19.10].\n\n### Fixed\n\n- Unquoted IPv6 bind hosts with trailing colons erroneously considered unspecified addresses are now properly validated ([#5752]).\n\n    **NOTE:** the Docker healthcheck script now also doesn’t interpret the `\"\"` value as unspecified address.\n\n- Incorrect `Content-Type` header value in `POST /control/version.json` and `GET /control/dhcp/interfaces` HTTP APIs ([#5716]).\n\n- Provided bootstrap servers are now used to resolve the hostnames of plain UDP/TCP upstream servers.\n\n[#5716]: https://github.com/AdguardTeam/AdGuardHome/issues/5716\n\n[go-1.19.10]:   https://groups.google.com/g/golang-announce/c/q5135a9d924/m/j0ZoAJOHAwAJ\n[ms-v0.107.30]: https://github.com/AdguardTeam/AdGuardHome/milestone/66?closed=1\n\n## [v0.107.29] - 2023-04-18\n\nSee also the [v0.107.29 GitHub milestone][ms-v0.107.29].\n\n### Added\n\n- The ability to exclude client activity from the query log or statistics by editing client’s settings on the respective page in the UI ([#1717], [#4299]).\n\n### Changed\n\n- Stored DHCP leases moved from `leases.db` to `data/leases.json`.  The file format has also been optimized.\n\n### Fixed\n\n- The `github.com/mdlayher/raw` dependency has been temporarily returned to support raw connections on Darwin ([#5712]).\n\n- Incorrect recording of blocked results as “Blocked by CNAME or IP” in the query log ([#5725]).\n\n- All Safe Search services being unchecked by default.\n\n- Panic when a DNSCrypt stamp is invalid ([#5721]).\n\n[#5712]: https://github.com/AdguardTeam/AdGuardHome/issues/5712\n[#5721]: https://github.com/AdguardTeam/AdGuardHome/issues/5721\n[#5725]: https://github.com/AdguardTeam/AdGuardHome/issues/5725\n[#5752]: https://github.com/AdguardTeam/AdGuardHome/issues/5752\n\n[ms-v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/milestone/65?closed=1\n\n## [v0.107.28] - 2023-04-12\n\nSee also the [v0.107.28 GitHub milestone][ms-v0.107.28].\n\n### Added\n\n- The ability to exclude client activity from the query log or statistics by using the new properties `ignore_querylog` and `ignore_statistics` of the items of the `clients.persistent` array ([#1717], [#4299]).  The UI changes are coming in the upcoming releases.\n\n- Better profiling information when `debug_pprof` is set to `true`.\n\n- IPv6 support in Safe Search for some services.\n\n- The ability to make bootstrap DNS lookups prefer IPv6 addresses to IPv4 ones using the new `dns.bootstrap_prefer_ipv6` configuration file property ([#4262]).\n\n- Docker container’s healthcheck ([#3290]).\n\n- The new HTTP API `POST /control/protection`, that updates protection state and adds an optional pause duration ([#1333]).  The format of request body is described in `openapi/openapi.yaml`.  The duration of this pause could also be set with the property `protection_disabled_until` in the `dns` object of the YAML configuration file.\n\n- The ability to create a static DHCP lease from a dynamic one more easily ([#3459]).\n\n- Two new HTTP APIs, `PUT /control/stats/config/update` and `GET control/stats/config`, which can be used to set and receive the query log configuration.  See `openapi/openapi.yaml` for the full description.\n\n- Two new HTTP APIs, `PUT /control/querylog/config/update` and `GET control/querylog/config`, which can be used to set and receive the statistics configuration.  See `openapi/openapi.yaml` for the full description.\n\n- The ability to set custom IP for EDNS Client Subnet by using the DNS-server configuration section on the DNS settings page in the UI ([#1472]).\n\n- The ability to manage Safe Search for each service by using the new `safe_search` property ([#1163]).\n\n### Changed\n\n- ARPA domain names containing a subnet within private networks now also considered private, behaving closer to [RFC 6761][rfc6761] ([#5567]).\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 17 to 20.\n\n- Property `statistics.interval`, which in schema versions 19 and earlier used to be an integer number of days, is now a string with a human-readable duration:\n\n    ```yaml\n    # BEFORE:\n    'statistics':\n      # …\n      'interval': 1\n\n    # AFTER:\n    'statistics':\n      # …\n      'interval': '24h'\n    ```\n\n  To rollback this change, convert the property back into days and change the `schema_version` back to `19`.\n\n- The `dns.safesearch_enabled` property has been replaced with `safe_search` object containing per-service settings.\n\n- The `clients.persistent.safesearch_enabled` property has been replaced with `safe_search` object containing per-service settings.\n\n    ```yaml\n      # BEFORE:\n      'safesearch_enabled': true\n\n      # AFTER:\n      'safe_search':\n        'enabled': true\n        'bing': true\n        'duckduckgo': true\n        'google': true\n        'pixabay': true\n        'yandex': true\n        'youtube': true\n    ```\n\n    To rollback this change, move the value of `dns.safe_search.enabled` into the `dns.safesearch_enabled`, then remove `dns.safe_search` property.  Do the same client’s specific `clients.persistent.safesearch` and then change the `schema_version` back to `17`.\n\n### Deprecated\n\n- The `POST /control/safesearch/enable` HTTP API is deprecated.  Use the new `PUT /control/safesearch/settings` API.\n\n- The `POST /control/safesearch/disable` HTTP API is deprecated.  Use the new `PUT /control/safesearch/settings` API\n\n- The `safesearch_enabled` property is deprecated in the following HTTP APIs:\n    - `GET /control/clients`;\n    - `POST /control/clients/add`;\n    - `POST /control/clients/update`;\n    - `GET /control/clients/find?ip0=...&ip1=...&ip2=...`.\n\n    Check `openapi/openapi.yaml` for more details.\n\n- The `GET /control/stats_info` HTTP API; use the new `GET /control/stats/config` API instead.\n\n    **NOTE:** If interval is custom then it will be equal to `90` days for compatibility reasons.  See `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.\n\n- The `POST /control/stats_config` HTTP API; use the new `PUT /control/stats/config/update` API instead.\n\n- The `GET /control/querylog_info` HTTP API; use the new `GET /control/querylog/config` API instead.\n\n    **NOTE:** If interval is custom then it will be equal to `90` days for compatibility reasons.  See `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.\n\n- The `POST /control/querylog_config` HTTP API; use the new `PUT /control/querylog/config/update` API instead.\n\n### Fixed\n\n- Logging of the client’s IP address after failed login attempts ([#5701]).\n\n[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163\n[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333\n[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472\n[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290\n[#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459\n[#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262\n[#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567\n[#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701\n\n[ms-v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/milestone/64?closed=1\n[rfc6761]:      https://datatracker.ietf.org/doc/html/rfc6761\n\n## [v0.107.27] - 2023-04-05\n\nSee also the [v0.107.27 GitHub milestone][ms-v0.107.27].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-24534, CVE-2023-24536, CVE-2023-24537, and CVE-2023-24538 Go vulnerabilities fixed in [Go 1.19.8][go-1.19.8].\n\n### Fixed\n\n- Query log not showing all filtered queries when the “Filtered” log filter is selected ([#5639]).\n\n- Panic in empty hostname in the filter’s URL ([#5631]).\n\n- Panic caused by empty top-level domain name label in `/etc/hosts` files ([#5584]).\n\n[#5584]: https://github.com/AdguardTeam/AdGuardHome/issues/5584\n[#5631]: https://github.com/AdguardTeam/AdGuardHome/issues/5631\n[#5639]: https://github.com/AdguardTeam/AdGuardHome/issues/5639\n\n[go-1.19.8]:    https://groups.google.com/g/golang-announce/c/Xdv6JL9ENs8/m/OV40vnafAwAJ\n[ms-v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/milestone/63?closed=1\n\n## [v0.107.26] - 2023-03-09\n\nSee also the [v0.107.26 GitHub milestone][ms-v0.107.26].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2023-24532 Go vulnerability fixed in [Go 1.19.7][go-1.19.7].\n\n### Added\n\n- The ability to set custom IP for EDNS Client Subnet by using the new `dns.edns_client_subnet.use_custom` and `dns.edns_client_subnet.custom_ip` properties ([#1472]).  The UI changes are coming in the upcoming releases.\n\n- The ability to use `dnstype` rules in the disallowed domains list ([#5468]).  This allows dropping requests based on their question types.\n\n### Changed\n\n#### Configuration changes\n\n- Property `edns_client_subnet`, which in schema versions 16 and earlier used to be a part of the `dns` object, is now part of the `dns.edns_client_subnet` object:\n\n    ```yaml\n    # BEFORE:\n    'dns':\n      # …\n      'edns_client_subnet': false\n\n    # AFTER:\n    'dns':\n      # …\n      'edns_client_subnet':\n        'enabled': false\n        'use_custom': false\n        'custom_ip': ''\n    ```\n\n    To rollback this change, move the value of `dns.edns_client_subnet.enabled` into the `dns.edns_client_subnet`, remove the properties `dns.edns_client_subnet.enabled`, `dns.edns_client_subnet.use_custom`, `dns.edns_client_subnet.custom_ip`, and change the `schema_version` back to `16`.\n\n### Fixed\n\n- Obsolete value of the Interface MTU DHCP option is now omitted ([#5281]).\n\n- Various dark theme bugs ([#5439], [#5441], [#5442], [#5515]).\n\n- Automatic update on MIPS64 and little-endian 32-bit MIPS architectures ([#5270], [#5373]).\n\n- Requirements to domain names in domain-specific upstream configurations have been relaxed to meet those from [RFC 3696][rfc3696] ([#4884]).\n\n- Failing service installation via script on FreeBSD ([#5431]).\n\n[#4884]: https://github.com/AdguardTeam/AdGuardHome/issues/4884\n[#5270]: https://github.com/AdguardTeam/AdGuardHome/issues/5270\n[#5281]: https://github.com/AdguardTeam/AdGuardHome/issues/5281\n[#5373]: https://github.com/AdguardTeam/AdGuardHome/issues/5373\n[#5431]: https://github.com/AdguardTeam/AdGuardHome/issues/5431\n[#5439]: https://github.com/AdguardTeam/AdGuardHome/issues/5439\n[#5441]: https://github.com/AdguardTeam/AdGuardHome/issues/5441\n[#5442]: https://github.com/AdguardTeam/AdGuardHome/issues/5442\n[#5468]: https://github.com/AdguardTeam/AdGuardHome/issues/5468\n[#5515]: https://github.com/AdguardTeam/AdGuardHome/issues/5515\n\n[go-1.19.7]:    https://groups.google.com/g/golang-announce/c/3-TpUx48iQY\n[ms-v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/milestone/62?closed=1\n[rfc3696]:      https://datatracker.ietf.org/doc/html/rfc3696\n\n## [v0.107.25] - 2023-02-21\n\nSee also the [v0.107.25 GitHub milestone][ms-v0.107.25].\n\n### Fixed\n\n- Panic when using unencrypted DNS-over-HTTPS ([#5518]).\n\n[#5518]: https://github.com/AdguardTeam/AdGuardHome/issues/5518\n\n[ms-v0.107.25]: https://github.com/AdguardTeam/AdGuardHome/milestone/61?closed=1\n\n## [v0.107.24] - 2023-02-15\n\nSee also the [v0.107.24 GitHub milestone][ms-v0.107.24].\n\n### Security\n\n- Go version has been updated, both because Go 1.18 has reached end of life an to prevent the possibility of exploiting the Go vulnerabilities fixed in [Go 1.19.6][go-1.19.6].\n\n### Added\n\n- The ability to disable statistics by using the new `statistics.enabled` property.  Previously it was necessary to set the `statistics_interval` to 0, losing the previous value ([#1717], [#4299]).\n\n- The ability to exclude domain names from the query log or statistics by using the new `querylog.ignored` or `statistics.ignored` properties ([#1717], [#4299]).  The UI changes are coming in the upcoming releases.\n\n### Changed\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 14 to 16.\n\n- Property `statistics_interval`, which in schema versions 15 and earlier used to be a part of the `dns` object, is now a part of the `statistics` object:\n\n    ```yaml\n    # BEFORE:\n    'dns':\n      # …\n      'statistics_interval': 1\n\n    # AFTER:\n    'statistics':\n      # …\n      'interval': 1\n    ```\n\n    To rollback this change, move the property back into the `dns` object and change the `schema_version` back to `15`.\n\n- The properties `dns.querylog_enabled`, `dns.querylog_file_enabled`, `dns.querylog_interval`, and `dns.querylog_size_memory` have been moved to the new `querylog` object.\n\n    ```yaml\n    # BEFORE:\n    'dns':\n      'querylog_enabled': true\n      'querylog_file_enabled': true\n      'querylog_interval': '2160h'\n      'querylog_size_memory': 1000\n\n    # AFTER:\n    'querylog':\n      'enabled': true\n      'file_enabled': true\n      'interval': '2160h'\n      'size_memory': 1000\n      'ignored': []\n    ```\n\n    To rollback this change, rename and move properties back into the `dns` object, remove `querylog` object and `querylog.ignored` property, and change the `schema_version` back to `14`.\n\n### Deprecated\n\n- Go 1.19 support.  Future versions will require at least Go 1.20 to build.\n\n### Fixed\n\n- Setting the AD (Authenticated Data) flag on responses that have the DO (DNSSEC OK) flag set but not the AD flag ([#5479]).\n\n- Client names resolved via reverse DNS not being updated ([#4939]).\n\n- The icon for League Of Legends on the Blocked services page ([#5433]).\n\n### Removed\n\n- Go 1.18 support, as it has reached end of life.\n\n[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717\n[#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299\n[#4939]: https://github.com/AdguardTeam/AdGuardHome/issues/4939\n[#5433]: https://github.com/AdguardTeam/AdGuardHome/issues/5433\n[#5479]: https://github.com/AdguardTeam/AdGuardHome/issues/5479\n\n[go-1.19.6]:    https://groups.google.com/g/golang-announce/c/V0aBFqaFs_E\n[ms-v0.107.24]: https://github.com/AdguardTeam/AdGuardHome/milestone/60?closed=1\n\n## [v0.107.23] - 2023-02-01\n\nSee also the [v0.107.23 GitHub milestone][ms-v0.107.23].\n\n### Added\n\n- DNS64 support ([#5117]).  The function may be enabled with new `use_dns64` property under `dns` object in the configuration along with `dns64_prefixes`, the set of exclusion prefixes to filter AAAA responses.  The Well-Known Prefix (`64:ff9b::/96`) is used if no custom prefixes are specified.\n\n### Fixed\n\n- Filtering rules with `*` as the hostname not working properly ([#5245]).\n\n- Various dark theme bugs ([#5375]).\n\n### Removed\n\n- The “beta frontend” and the corresponding APIs.  They never quite worked properly, and the future new version of AdGuard Home API will probably be different.\n\n    Correspondingly, the configuration parameter `beta_bind_port` has been removed as well.\n\n[#5117]: https://github.com/AdguardTeam/AdGuardHome/issues/5117\n[#5245]: https://github.com/AdguardTeam/AdGuardHome/issues/5245\n[#5375]: https://github.com/AdguardTeam/AdGuardHome/issues/5375\n\n[ms-v0.107.23]: https://github.com/AdguardTeam/AdGuardHome/milestone/59?closed=1\n\n## [v0.107.22] - 2023-01-19\n\nSee also the [v0.107.22 GitHub milestone][ms-v0.107.22].\n\n### Added\n\n- Experimental Dark UI theme ([#613]).\n\n- The new HTTP API `PUT /control/profile/update`, that updates current user language and UI theme.  The format of request body is described in `openapi/openapi.yaml`.\n\n### Changed\n\n- The HTTP API `GET /control/profile` now returns enhanced object with current user’s name, language, and UI theme.  The format of response body is described in `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.\n\n### Fixed\n\n- `AdGuardHome --update` freezing when another instance of AdGuard Home is running ([#4223], [#5191]).\n\n- The `--update` flag performing an update even when there is no version change.\n\n- Failing HTTPS redirection on saving the encryption settings ([#4898]).\n\n- Zeroing rules counter of erroneously edited filtering rule lists ([#5290]).\n\n- Filters updating strategy, which could sometimes lead to use of broken or incompletely downloaded lists ([#5258]).\n\n[#613]:  https://github.com/AdguardTeam/AdGuardHome/issues/613\n[#5191]: https://github.com/AdguardTeam/AdGuardHome/issues/5191\n[#5290]: https://github.com/AdguardTeam/AdGuardHome/issues/5290\n[#5258]: https://github.com/AdguardTeam/AdGuardHome/issues/5258\n\n[ms-v0.107.22]: https://github.com/AdguardTeam/AdGuardHome/milestone/58?closed=1\n\n## [v0.107.21] - 2022-12-15\n\nSee also the [v0.107.21 GitHub milestone][ms-v0.107.21].\n\n### Changed\n\n- The URLs of the default filters for new installations are synchronized to those introduced in v0.107.20 ([#5238]).\n\n    **NOTE:** Some users may need to re-add the lists from the vetted filter lists to update the URLs to the new ones.  Custom filters added by users themselves do not require re-adding.\n\n### Fixed\n\n- Errors popping up during updates of settings, which could sometimes cause the server to stop responding ([#5251]).\n\n[#5238]: https://github.com/AdguardTeam/AdGuardHome/issues/5238\n[#5251]: https://github.com/AdguardTeam/AdGuardHome/issues/5251\n\n[ms-v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/milestone/57?closed=1\n\n## [v0.107.20] - 2022-12-07\n\nSee also the [v0.107.20 GitHub milestone][ms-v0.107.20].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2022-41717 and CVE-2022-41720 Go vulnerabilities fixed in [Go 1.18.9][go-1.18.9].\n\n### Added\n\n- The ability to clear the DNS cache ([#5190]).\n\n### Changed\n\n- DHCP server initialization errors are now logged at debug level if the server itself disabled ([#4944]).\n\n### Fixed\n\n- Wrong validation error messages on the DHCP configuration page ([#5208]).\n\n- Slow upstream checks making the API unresponsive ([#5193]).\n\n- The TLS initialization errors preventing AdGuard Home from starting ([#5189]).  Instead, AdGuard Home disables encryption and shows an error message on the encryption settings page in the UI, which was the intended previous behavior.\n\n- URLs of some vetted blocklists.\n\n[#4944]: https://github.com/AdguardTeam/AdGuardHome/issues/4944\n[#5189]: https://github.com/AdguardTeam/AdGuardHome/issues/5189\n[#5190]: https://github.com/AdguardTeam/AdGuardHome/issues/5190\n[#5193]: https://github.com/AdguardTeam/AdGuardHome/issues/5193\n[#5208]: https://github.com/AdguardTeam/AdGuardHome/issues/5208\n\n[go-1.18.9]:    https://groups.google.com/g/golang-announce/c/L_3rmdT0BMU\n[ms-v0.107.20]: https://github.com/AdguardTeam/AdGuardHome/milestone/56?closed=1\n\n## [v0.107.19] - 2022-11-23\n\nSee also the [v0.107.19 GitHub milestone][ms-v0.107.19].\n\n### Added\n\n- The ability to block popular Mastodon instances ([AdguardTeam/HostlistsRegistry#100]).\n\n- The new `--update` command-line option, which allows updating AdGuard Home silently ([#4223]).\n\n### Changed\n\n- Minor UI changes.\n\n[#4223]: https://github.com/AdguardTeam/AdGuardHome/issues/4223\n\n[ms-v0.107.19]: https://github.com/AdguardTeam/AdGuardHome/milestone/55?closed=1\n\n[AdguardTeam/HostlistsRegistry#100]: https://github.com/AdguardTeam/HostlistsRegistry/pull/100\n\n## [v0.107.18] - 2022-11-08\n\nSee also the [v0.107.18 GitHub milestone][ms-v0.107.18].\n\n### Fixed\n\n- Crash on some systems when domains from system hosts files are processed ([#5089]).\n\n[#5089]: https://github.com/AdguardTeam/AdGuardHome/issues/5089\n\n[ms-v0.107.18]: https://github.com/AdguardTeam/AdGuardHome/milestone/54?closed=1\n\n## [v0.107.17] - 2022-11-02\n\nSee also the [v0.107.17 GitHub milestone][ms-v0.107.17].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2022-41716 Go vulnerability fixed in [Go 1.18.8][go-1.18.8].\n\n### Added\n\n- The warning message when adding a certificate having no IP addresses ([#4898]).\n\n- Several new blockable services ([#3972]).  Those will now be more in sync with the services that are already blockable in AdGuard DNS.\n\n- A new HTTP API, `GET /control/blocked_services/all`, that lists all available blocked services and their data, such as SVG icons ([#3972]).\n\n- The new optional `tls.override_tls_ciphers` property, which allows overriding TLS ciphers used by AdGuard Home ([#4925], [#4990]).\n\n- The ability to serve DNS on link-local IPv6 addresses ([#2926]).\n\n- The ability to put [ClientIDs][clientid] into DNS-over-HTTPS hostnames as opposed to URL paths ([#3418]).  Note that AdGuard Home checks the server name only if the URL does not contain a ClientID.\n\n### Changed\n\n- DNS-over-TLS resolvers aren’t returned anymore when the configured TLS certificate contains no IP addresses ([#4927]).\n\n- Responses with `SERVFAIL` code are now cached for at least 30 seconds.\n\n### Deprecated\n\n- The `GET /control/blocked_services/services` HTTP API; use the new `GET /control/blocked_services/all` API instead ([#3972]).\n\n### Fixed\n\n- ClientIDs not working when using DNS-over-HTTPS with HTTP/3.\n\n- Editing the URL of an enabled rule list also includes validation of the filter contents preventing from saving a bad one ([#4916]).\n\n- The default value of `dns.cache_size` accidentally set to 0 has now been reverted to 4 MiB ([#5010]).\n\n- Responses for which the DNSSEC validation had explicitly been omitted aren’t cached now ([#4942]).\n\n- Web UI not switching to HTTP/3 ([#4986], [#4993]).\n\n[#2926]: https://github.com/AdguardTeam/AdGuardHome/issues/2926\n[#3418]: https://github.com/AdguardTeam/AdGuardHome/issues/3418\n[#3972]: https://github.com/AdguardTeam/AdGuardHome/issues/3972\n[#4898]: https://github.com/AdguardTeam/AdGuardHome/issues/4898\n[#4916]: https://github.com/AdguardTeam/AdGuardHome/issues/4916\n[#4925]: https://github.com/AdguardTeam/AdGuardHome/issues/4925\n[#4942]: https://github.com/AdguardTeam/AdGuardHome/issues/4942\n[#4986]: https://github.com/AdguardTeam/AdGuardHome/issues/4986\n[#4990]: https://github.com/AdguardTeam/AdGuardHome/issues/4990\n[#4993]: https://github.com/AdguardTeam/AdGuardHome/issues/4993\n[#5010]: https://github.com/AdguardTeam/AdGuardHome/issues/5010\n\n[clientid]:     https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#clientid\n[go-1.18.8]:    https://groups.google.com/g/golang-announce/c/mbHY1UY3BaM\n[ms-v0.107.17]: https://github.com/AdguardTeam/AdGuardHome/milestone/53?closed=1\n\n## [v0.107.16] - 2022-10-07\n\nThis is a security update.  There is no GitHub milestone, since no GitHub issues were resolved.\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2022-2879, CVE-2022-2880, and CVE-2022-41715 Go vulnerabilities fixed in [Go 1.18.7][go-1.18.7].\n\n[go-1.18.7]: https://groups.google.com/g/golang-announce/c/xtuG5faxtaU\n\n## [v0.107.15] - 2022-10-03\n\nSee also the [v0.107.15 GitHub milestone][ms-v0.107.15].\n\n### Security\n\n- As an additional CSRF protection measure, AdGuard Home now ensures that requests that change its state but have no body (such as `POST /control/stats_reset` requests) do not have a `Content-Type` header set on them ([#4970]).\n\n### Added\n\n#### Experimental HTTP/3 Support\n\nSee [#3955] and the related issues for more details.  These features are still experimental and may break or change in the future.\n\n- DNS-over-HTTP/3 DNS and web UI client request support.  This feature must be explicitly enabled by setting the new property `dns.serve_http3` in the configuration file to `true`.\n\n- DNS-over-HTTP upstreams can now upgrade to HTTP/3 if the new configuration file property `dns.use_http3_upstreams` is set to `true`.\n\n- Upstreams with forced DNS-over-HTTP/3 and no fallback to prior HTTP versions using the `h3://` scheme.\n\n### Fixed\n\n- User-specific blocked services not applying correctly ([#4945], [#4982], [#4983]).\n- `only application/json is allowed` errors in various APIs ([#4970]).\n\n[#3955]: https://github.com/AdguardTeam/AdGuardHome/issues/3955\n[#4945]: https://github.com/AdguardTeam/AdGuardHome/issues/4945\n[#4970]: https://github.com/AdguardTeam/AdGuardHome/issues/4970\n[#4982]: https://github.com/AdguardTeam/AdGuardHome/issues/4982\n[#4983]: https://github.com/AdguardTeam/AdGuardHome/issues/4983\n\n[ms-v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/milestone/51?closed=1\n\n## [v0.107.14] - 2022-09-29\n\nSee also the [v0.107.14 GitHub milestone][ms-v0.107.14].\n\n### Security\n\nA Cross-Site Request Forgery (CSRF) vulnerability has been discovered.  We thank Daniel Elkabes from Mend.io for reporting this vulnerability to us.  This is [CVE-2022-32175].\n\n#### `SameSite` Policy\n\nThe `SameSite` policy on the AdGuard Home session cookies is now set to `Lax`.  Which means that the only cross-site HTTP request for which the browser is allowed to send the session cookie is navigating to the AdGuard Home domain.\n\n**Users are strongly advised to log out, clear browser cache, and log in again after updating.**\n\n#### Removal Of Plain-Text APIs (BREAKING API CHANGE)\n\nWe have implemented several measures to prevent such vulnerabilities in the future, but some of these measures break backwards compatibility for the sake of better protection.\n\nThe following APIs, which previously accepted or returned `text/plain` data, now accept or return data as JSON.  All new formats for the request and response bodies are documented in `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.\n\n- `GET  /control/i18n/current_language`;\n- `POST /control/dhcp/find_active_dhcp`;\n- `POST /control/filtering/set_rules`;\n- `POST /control/i18n/change_language`.\n\n#### Stricter Content-Type Checks (BREAKING API CHANGE)\n\nAll JSON APIs that expect a body now check if the request actually has `Content-Type` set to `application/json`.\n\n#### Other Security Changes\n\n- Weaker cipher suites that use the CBC (cipher block chaining) mode of operation have been disabled ([#2993]).\n\n### Added\n\n- Support for plain (unencrypted) HTTP/2 ([#4930]).  This is useful for AdGuard Home installations behind a reverse proxy.\n\n### Fixed\n\n- Incorrect path template in DDR responses ([#4927]).\n\n[#2993]: https://github.com/AdguardTeam/AdGuardHome/issues/2993\n[#4927]: https://github.com/AdguardTeam/AdGuardHome/issues/4927\n[#4930]: https://github.com/AdguardTeam/AdGuardHome/issues/4930\n\n[CVE-2022-32175]: https://www.cvedetails.com/cve/CVE-2022-32175\n[ms-v0.107.14]:   https://github.com/AdguardTeam/AdGuardHome/milestone/50?closed=1\n\n## [v0.107.13] - 2022-09-14\n\nSee also the [v0.107.13 GitHub milestone][ms-v0.107.13].\n\n### Added\n\n- The new optional `dns.ipset_file` property, which can be set in the configuration file.  It allows loading the `ipset` list from a file, just like `dns.upstream_dns_file` does for upstream servers ([#4686]).\n\n### Changed\n\n- The minimum DHCP message size is reassigned back to BOOTP’s constraint of 300 bytes ([#4904]).\n\n### Fixed\n\n- Panic when adding a static lease within the disabled DHCP server ([#4722]).\n\n[#4686]: https://github.com/AdguardTeam/AdGuardHome/issues/4686\n[#4722]: https://github.com/AdguardTeam/AdGuardHome/issues/4722\n[#4904]: https://github.com/AdguardTeam/AdGuardHome/issues/4904\n\n[ms-v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/milestone/49?closed=1\n\n## [v0.107.12] - 2022-09-07\n\nSee also the [v0.107.12 GitHub milestone][ms-v0.107.12].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2022-27664 and CVE-2022-32190 Go vulnerabilities fixed in [Go 1.18.6][go-1.18.6].\n\n### Added\n\n- New `bool`, `dur`, `u8`, and `u16` DHCP options to provide more convenience on options control by setting values in a human-readable format ([#4705]).  See also a [Wiki page][wiki-dhcp-opts].\n\n- New `del` DHCP option which removes the corresponding option from server’s response ([#4337]).  See also a [Wiki page][wiki-dhcp-opts].\n\n    **NOTE:** This modifier affects all the parameters in the response and not only the requested ones.\n\n- A new HTTP API, `GET /control/blocked_services/services`, that lists all available blocked services ([#4535]).\n\n### Changed\n\n- The DHCP options handling is now closer to the [RFC 2131][rfc-2131] ([#4705]).\n\n- When the DHCP server is enabled, queries for domain names under `dhcp.local_domain_name` not pointing to real DHCP client hostnames are now processed by filters ([#4865]).\n\n- The `DHCPREQUEST` handling is now closer to the [RFC 2131][rfc-2131] ([#4863]).\n\n- The internal DNS client, used to resolve hostnames of external clients and also during automatic updates, now respects the upstream mode settings for the main DNS client ([#4403]).\n\n### Deprecated\n\n- Ports 784 and 8853 for DNS-over-QUIC in Docker images.  Users who still serve DoQ on these ports are encouraged to move to the standard port 853.  These ports will be removed from the `EXPOSE` section of our `Dockerfile` in a future release.\n\n- Go 1.18 support.  Future versions will require at least Go 1.19 to build.\n\n### Fixed\n\n- The length of the DHCP server’s response is now at least 576 bytes as per [RFC 2131][rfc-2131] recommendation ([#4337]).\n\n- Dynamic leases created with empty hostnames ([#4745]).\n\n- Unnecessary logging of non-critical statistics errors ([#4850]).\n\n[#4337]: https://github.com/AdguardTeam/AdGuardHome/issues/4337\n[#4403]: https://github.com/AdguardTeam/AdGuardHome/issues/4403\n[#4535]: https://github.com/AdguardTeam/AdGuardHome/issues/4535\n[#4705]: https://github.com/AdguardTeam/AdGuardHome/issues/4705\n[#4745]: https://github.com/AdguardTeam/AdGuardHome/issues/4745\n[#4850]: https://github.com/AdguardTeam/AdGuardHome/issues/4850\n[#4863]: https://github.com/AdguardTeam/AdGuardHome/issues/4863\n[#4865]: https://github.com/AdguardTeam/AdGuardHome/issues/4865\n\n[go-1.18.6]:      https://groups.google.com/g/golang-announce/c/x49AQzIVX-s\n[ms-v0.107.12]:   https://github.com/AdguardTeam/AdGuardHome/milestone/48?closed=1\n[rfc-2131]:       https://datatracker.ietf.org/doc/html/rfc2131\n[wiki-dhcp-opts]: https://github.com/adguardTeam/adGuardHome/wiki/DHCP#config-4\n\n## [v0.107.11] - 2022-08-19\n\nSee also the [v0.107.11 GitHub milestone][ms-v0.107.11].\n\n### Added\n\n- Bilibili service blocking ([#4795]).\n\n### Changed\n\n- DNS-over-QUIC connections now use keepalive.\n\n### Fixed\n\n- Migrations from releases older than v0.107.7 failing ([#4846]).\n\n[#4795]: https://github.com/AdguardTeam/AdGuardHome/issues/4795\n[#4846]: https://github.com/AdguardTeam/AdGuardHome/issues/4846\n\n[ms-v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/milestone/47?closed=1\n\n## [v0.107.10] - 2022-08-17\n\nSee also the [v0.107.10 GitHub milestone][ms-v0.107.10].\n\n### Added\n\n- Arabic localization.\n\n- Support for Discovery of Designated Resolvers (DDR) according to the [RFC draft][ddr-draft] ([#4463]).\n\n### Changed\n\n- Our snap package now uses the `core22` image as its base ([#4843]).\n\n### Fixed\n\n- DHCP not working on most OSes ([#4836]).\n\n- `invalid argument` errors during update checks on older Linux kernels ([#4670]).\n\n- Data races and concurrent map access in statistics module ([#4358], [#4342]).\n\n[#4342]: https://github.com/AdguardTeam/AdGuardHome/issues/4342\n[#4358]: https://github.com/AdguardTeam/AdGuardHome/issues/4358\n[#4670]: https://github.com/AdguardTeam/AdGuardHome/issues/4670\n[#4843]: https://github.com/AdguardTeam/AdGuardHome/issues/4843\n\n[ddr-draft]:    https://datatracker.ietf.org/doc/html/draft-ietf-add-ddr-08\n[ms-v0.107.10]: https://github.com/AdguardTeam/AdGuardHome/milestone/46?closed=1\n\n## [v0.107.9] - 2022-08-03\n\nSee also the [v0.107.9 GitHub milestone][ms-v0.107.9].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2022-32189 Go vulnerability fixed in [Go 1.18.5][go-1.18.5].  Go 1.17 support has also been removed, as it has reached end of life and will not receive security updates.\n\n### Added\n\n- Domain-specific upstream servers test.  If such test fails, a warning message is shown ([#4517]).\n\n- `windows/arm64` support ([#3057]).\n\n### Changed\n\n- UI and update links have been changed to make them more resistant to DNS blocking.\n\n### Fixed\n\n- DHCP not working on most OSes ([#4836]).\n\n- Several UI issues ([#4775], [#4776], [#4782]).\n\n### Removed\n\n- Go 1.17 support, as it has reached end of life.\n\n[#3057]: https://github.com/AdguardTeam/AdGuardHome/issues/3057\n[#4517]: https://github.com/AdguardTeam/AdGuardHome/issues/4517\n[#4775]: https://github.com/AdguardTeam/AdGuardHome/issues/4775\n[#4776]: https://github.com/AdguardTeam/AdGuardHome/issues/4776\n[#4782]: https://github.com/AdguardTeam/AdGuardHome/issues/4782\n[#4836]: https://github.com/AdguardTeam/AdGuardHome/issues/4836\n\n[go-1.18.5]:   https://groups.google.com/g/golang-announce/c/YqYYG87xB10\n[ms-v0.107.9]: https://github.com/AdguardTeam/AdGuardHome/milestone/45?closed=1\n\n## [v0.107.8] - 2022-07-13\n\nSee also the [v0.107.8 GitHub milestone][ms-v0.107.8].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the CVE-2022-1705, CVE-2022-32148, CVE-2022-30631, and other Go vulnerabilities fixed in [Go 1.17.12][go-1.17.12].\n\n### Fixed\n\n- DHCP lease validation incorrectly letting users assign the IP address of the gateway as the address of the lease ([#4698]).\n\n- Updater no longer expects a hardcoded name for  `AdGuardHome` executable ([#4219]).\n\n- Inconsistent names of runtime clients from hosts files ([#4683]).\n\n- PTR requests for addresses leased by DHCP will now be resolved into hostnames under `dhcp.local_domain_name` ([#4699]).\n\n- Broken service installation on OpenWrt ([#4677]).\n\n[#4219]: https://github.com/AdguardTeam/AdGuardHome/issues/4219\n[#4677]: https://github.com/AdguardTeam/AdGuardHome/issues/4677\n[#4683]: https://github.com/AdguardTeam/AdGuardHome/issues/4683\n[#4698]: https://github.com/AdguardTeam/AdGuardHome/issues/4698\n[#4699]: https://github.com/AdguardTeam/AdGuardHome/issues/4699\n\n[go-1.17.12]:  https://groups.google.com/g/golang-announce/c/nqrv9fbR0zE\n[ms-v0.107.8]: https://github.com/AdguardTeam/AdGuardHome/milestone/44?closed=1\n\n## [v0.107.7] - 2022-06-06\n\nSee also the [v0.107.7 GitHub milestone][ms-v0.107.7].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the [CVE-2022-29526], [CVE-2022-30634], [CVE-2022-30629], [CVE-2022-30580], and [CVE-2022-29804] Go vulnerabilities.\n\n- Enforced password strength policy ([#3503]).\n\n### Added\n\n- Support for the final DNS-over-QUIC standard, [RFC 9250][rfc-9250] ([#4592]).\n\n- Support upstreams for subdomains of a domain only ([#4503]).\n\n- The ability to control each source of runtime clients separately via `clients.runtime_sources` configuration object ([#3020]).\n\n- The ability to customize the set of networks that are considered private through the new `dns.private_networks` property in the configuration file ([#3142]).\n\n- EDNS Client-Subnet information in the request details section of a query log record ([#3978]).\n\n- Support for hostnames for plain UDP upstream servers using the `udp://` scheme ([#4166]).\n\n- Logs are now collected by default on FreeBSD and OpenBSD when AdGuard Home is installed as a service ([#4213]).\n\n### Changed\n\n- On OpenBSD, the daemon script now uses the recommended `/bin/ksh` shell instead of the `/bin/sh` one ([#4533]).  To apply this change, backup your data and run `AdGuardHome -s uninstall && AdGuardHome -s install`.\n\n- The default DNS-over-QUIC port number is now `853` instead of `754` in accordance with [RFC 9250][rfc-9250] ([#4276]).\n\n- Reverse DNS now has a greater priority as the source of runtime clients’ information than ARP neighborhood.\n\n- Improved detection of runtime clients through more resilient ARP processing ([#3597]).\n\n- The TTL of responses served from the optimistic cache is now lowered to 10 seconds.\n\n- Domain-specific private reverse DNS upstream servers are now validated to allow only `*.in-addr.arpa` and `*.ip6.arpa` domains pointing to locally-served networks ([#3381]).\n\n    **NOTE:**  If you already have invalid entries in your configuration, consider removing them manually, since they essentially had no effect.\n\n- Response filtering is now performed using the record types of the answer section of messages as opposed to the type of the question ([#4238]).\n\n- Instead of adding the build time information, the build scripts now use the standardized environment variable [`SOURCE_DATE_EPOCH`][repr] to add the date of the commit from which the binary was built ([#4221]).  This should simplify reproducible builds for package maintainers and those who compile their own AdGuard Home.\n\n- The property `local_domain_name` is now in the `dhcp` object in the configuration file to avoid confusion ([#3367]).\n\n- The `dns.bogus_nxdomain` property in the configuration file now supports CIDR notation alongside IP addresses ([#1730]).\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 12 to 14.\n\n- Object `clients`, which in schema versions 13 and earlier was an array of actual persistent clients, is now consist of `persistent` and `runtime_sources` properties:\n\n    ```yaml\n    # BEFORE:\n    'clients':\n    - name: client-name\n      # …\n\n    # AFTER:\n    'clients':\n      'persistent':\n        - name: client-name\n          # …\n      'runtime_sources':\n        whois: true\n        arp: true\n        rdns: true\n        dhcp: true\n        hosts: true\n    ```\n\n    The value for `clients.runtime_sources.rdns` property is taken from `dns.resolve_clients` property.  To rollback this change, remove the `runtime_sources` property, move the contents of `persistent` into the `clients` itself, the value of `clients.runtime_sources.rdns` into the `dns.resolve_clients`, and change the `schema_version` back to `13`.\n\n- Property `local_domain_name`, which in schema versions 12 and earlier used to be a part of the `dns` object, is now a part of the `dhcp` object:\n\n    ```yaml\n    # BEFORE:\n    'dns':\n      # …\n      'local_domain_name': 'lan'\n\n    # AFTER:\n    'dhcp':\n      # …\n      'local_domain_name': 'lan'\n    ```\n\n    To rollback this change, move the property back into the `dns` object and change the `schema_version` back to `12`.\n\n### Deprecated\n\n- The `--no-etc-hosts` option.  Its functionality is now controlled by `clients.runtime_sources.hosts` configuration property.  v0.109.0 will remove the flag completely.\n\n### Fixed\n\n- Query log occasionally going into an infinite loop ([#4591]).\n\n- Service startup on boot on systems using SysV-init ([#4480]).\n\n- Detection of the stopped service status on macOS and Linux ([#4273]).\n\n- Case-sensitive ClientID ([#4542]).\n\n- Slow version update queries making other HTTP APIs unresponsive ([#4499]).\n\n- ARP tables refreshing process causing excessive PTR requests ([#3157]).\n\n[#1730]: https://github.com/AdguardTeam/AdGuardHome/issues/1730\n[#3020]: https://github.com/AdguardTeam/AdGuardHome/issues/3020\n[#3142]: https://github.com/AdguardTeam/AdGuardHome/issues/3142\n[#3157]: https://github.com/AdguardTeam/AdGuardHome/issues/3157\n[#3367]: https://github.com/AdguardTeam/AdGuardHome/issues/3367\n[#3381]: https://github.com/AdguardTeam/AdGuardHome/issues/3381\n[#3503]: https://github.com/AdguardTeam/AdGuardHome/issues/3503\n[#3597]: https://github.com/AdguardTeam/AdGuardHome/issues/3597\n[#3978]: https://github.com/AdguardTeam/AdGuardHome/issues/3978\n[#4166]: https://github.com/AdguardTeam/AdGuardHome/issues/4166\n[#4213]: https://github.com/AdguardTeam/AdGuardHome/issues/4213\n[#4221]: https://github.com/AdguardTeam/AdGuardHome/issues/4221\n[#4238]: https://github.com/AdguardTeam/AdGuardHome/issues/4238\n[#4273]: https://github.com/AdguardTeam/AdGuardHome/issues/4273\n[#4276]: https://github.com/AdguardTeam/AdGuardHome/issues/4276\n[#4480]: https://github.com/AdguardTeam/AdGuardHome/issues/4480\n[#4499]: https://github.com/AdguardTeam/AdGuardHome/issues/4499\n[#4503]: https://github.com/AdguardTeam/AdGuardHome/issues/4503\n[#4533]: https://github.com/AdguardTeam/AdGuardHome/issues/4533\n[#4542]: https://github.com/AdguardTeam/AdGuardHome/issues/4542\n[#4591]: https://github.com/AdguardTeam/AdGuardHome/issues/4591\n[#4592]: https://github.com/AdguardTeam/AdGuardHome/issues/4592\n\n[CVE-2022-29526]: https://www.cvedetails.com/cve/CVE-2022-29526\n[CVE-2022-29804]: https://www.cvedetails.com/cve/CVE-2022-29804\n[CVE-2022-30580]: https://www.cvedetails.com/cve/CVE-2022-30580\n[CVE-2022-30629]: https://www.cvedetails.com/cve/CVE-2022-30629\n[CVE-2022-30634]: https://www.cvedetails.com/cve/CVE-2022-30634\n[ms-v0.107.7]:    https://github.com/AdguardTeam/AdGuardHome/milestone/43?closed=1\n[rfc-9250]:       https://datatracker.ietf.org/doc/html/rfc9250\n\n## [v0.107.6] - 2022-04-13\n\nSee also the [v0.107.6 GitHub milestone][ms-v0.107.6].\n\n### Security\n\n- `User-Agent` HTTP header removed from outgoing DNS-over-HTTPS requests.\n\n- Go version has been updated to prevent the possibility of exploiting the [CVE-2022-24675], [CVE-2022-27536], and [CVE-2022-28327] Go vulnerabilities.\n\n### Added\n\n- Support for SVCB/HTTPS parameter `dohpath` in filtering rules with the `dnsrewrite` modifier according to the [RFC draft][dns-draft-02] ([#4463]).\n\n### Changed\n\n- Filtering rules with the `dnsrewrite` modifier that create SVCB or HTTPS responses should use `ech` instead of `echconfig` to conform with the [latest drafts][svcb-draft-08].\n\n### Deprecated\n\n- SVCB/HTTPS parameter name `echconfig` in filtering rules with the `dnsrewrite` modifier.  Use `ech` instead.  v0.109.0 will remove support for the outdated name `echconfig`.\n\n- Obsolete `--no-mem-optimization` option ([#4437]).  v0.109.0 will remove the flag completely.\n\n### Fixed\n\n- I/O timeout errors when checking for the presence of another DHCP server.\n\n- Network interfaces being incorrectly labeled as down during installation.\n\n- Rules for blocking the QQ service ([#3717]).\n\n### Removed\n\n- Go 1.16 support, since that branch of the Go compiler has reached end of life and doesn’t receive security updates anymore.\n\n[#3717]: https://github.com/AdguardTeam/AdGuardHome/issues/3717\n[#4437]: https://github.com/AdguardTeam/AdGuardHome/issues/4437\n[#4463]: https://github.com/AdguardTeam/AdGuardHome/issues/4463\n\n[CVE-2022-24675]: https://www.cvedetails.com/cve/CVE-2022-24675\n[CVE-2022-27536]: https://www.cvedetails.com/cve/CVE-2022-27536\n[CVE-2022-28327]: https://www.cvedetails.com/cve/CVE-2022-28327\n[dns-draft-02]:   https://datatracker.ietf.org/doc/html/draft-ietf-add-svcb-dns-02#section-5.1\n[ms-v0.107.6]:    https://github.com/AdguardTeam/AdGuardHome/milestone/42?closed=1\n[repr]:           https://reproducible-builds.org/docs/source-date-epoch/\n[svcb-draft-08]:  https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-08.html\n\n## [v0.107.5] - 2022-03-04\n\nThis is a security update.  There is no GitHub milestone, since no GitHub issues were resolved.\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the [CVE-2022-24921] Go vulnerability.\n\n[CVE-2022-24921]: https://www.cvedetails.com/cve/CVE-2022-24921\n\n## [v0.107.4] - 2022-03-01\n\nSee also the [v0.107.4 GitHub milestone][ms-v0.107.4].\n\n### Security\n\n- Go version has been updated to prevent the possibility of exploiting the [CVE-2022-23806], [CVE-2022-23772], and [CVE-2022-23773] Go vulnerabilities.\n\n### Fixed\n\n- Optimistic cache now responds with expired items even if those can’t be resolved again ([#4254]).\n\n- Unnecessarily complex hosts-related logic leading to infinite recursion in some cases ([#4216]).\n\n[#4216]: https://github.com/AdguardTeam/AdGuardHome/issues/4216\n[#4254]: https://github.com/AdguardTeam/AdGuardHome/issues/4254\n\n[CVE-2022-23772]: https://www.cvedetails.com/cve/CVE-2022-23772\n[CVE-2022-23773]: https://www.cvedetails.com/cve/CVE-2022-23773\n[CVE-2022-23806]: https://www.cvedetails.com/cve/CVE-2022-23806\n[ms-v0.107.4]:    https://github.com/AdguardTeam/AdGuardHome/milestone/41?closed=1\n\n## [v0.107.3] - 2022-01-25\n\nSee also the [v0.107.3 GitHub milestone][ms-v0.107.3].\n\n### Added\n\n- Support for a `dnsrewrite` modifier with an empty `NOERROR` response ([#4133]).\n\n### Fixed\n\n- Wrong set of ports checked for duplicates during the initial setup ([#4095]).\n\n- Incorrectly invalidated service domains ([#4120]).\n\n- Poor testing of domain-specific upstream servers ([#4074]).\n\n- Omitted aliases of hosts specified by another line within the OS’s hosts file ([#4079]).\n\n[#4074]: https://github.com/AdguardTeam/AdGuardHome/issues/4074\n[#4079]: https://github.com/AdguardTeam/AdGuardHome/issues/4079\n[#4095]: https://github.com/AdguardTeam/AdGuardHome/issues/4095\n[#4120]: https://github.com/AdguardTeam/AdGuardHome/issues/4120\n[#4133]: https://github.com/AdguardTeam/AdGuardHome/issues/4133\n\n[ms-v0.107.3]: https://github.com/AdguardTeam/AdGuardHome/milestone/40?closed=1\n\n## [v0.107.2] - 2021-12-29\n\nSee also the [v0.107.2 GitHub milestone][ms-v0.107.2].\n\n### Fixed\n\n- Infinite loops when TCP connections time out ([#4042]).\n\n[#4042]: https://github.com/AdguardTeam/AdGuardHome/issues/4042\n\n[ms-v0.107.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/38?closed=1\n\n## [v0.107.1] - 2021-12-29\n\nSee also the [v0.107.1 GitHub milestone][ms-v0.107.1].\n\n### Changed\n\n- The validation error message for duplicated allow- and blocklists in DNS settings now shows the duplicated elements ([#3975]).\n\n### Fixed\n\n- `ipset` initialization bugs ([#4027]).\n\n- Legacy DNS rewrites from a wildcard pattern to a subdomain ([#4016]).\n\n- Service not being stopped before running the `uninstall` service action ([#3868]).\n\n- Broken `reload` service action on FreeBSD.\n\n- Legacy DNS rewrites responding from upstream when a request other than `A` or `AAAA` is received ([#4008]).\n\n- Panic on port availability check during installation ([#3987]).\n\n- Incorrect application of rules from the OS’s hosts files ([#3998]).\n\n[#3868]: https://github.com/AdguardTeam/AdGuardHome/issues/3868\n[#3975]: https://github.com/AdguardTeam/AdGuardHome/issues/3975\n[#3987]: https://github.com/AdguardTeam/AdGuardHome/issues/3987\n[#3998]: https://github.com/AdguardTeam/AdGuardHome/issues/3998\n[#4008]: https://github.com/AdguardTeam/AdGuardHome/issues/4008\n[#4016]: https://github.com/AdguardTeam/AdGuardHome/issues/4016\n[#4027]: https://github.com/AdguardTeam/AdGuardHome/issues/4027\n\n[ms-v0.107.1]: https://github.com/AdguardTeam/AdGuardHome/milestone/37?closed=1\n\n## [v0.107.0] - 2021-12-21\n\nSee also the [v0.107.0 GitHub milestone][ms-v0.107.0].\n\n### Added\n\n- Upstream server information for responses from cache ([#3772]).  Note that old log entries concerning cached responses won’t include that information.\n\n- Finnish and Ukrainian localizations.\n\n- Setting the timeout for IP address pinging in the \"Fastest IP address\" mode through the new `fastest_timeout` property in the configuration file ([#1992]).\n\n- Static IP address detection on FreeBSD ([#3289]).\n\n- Optimistic cache ([#2145]).\n\n- New possible value of `6h` for `querylog_interval` property ([#2504]).\n\n- Blocking access using ClientIDs ([#2624], [#3162]).\n\n- `source` directives support in `/etc/network/interfaces` on Linux ([#3257]).\n\n- [RFC 9000][rfc-9000] support in QUIC.\n\n- Completely disabling statistics by setting the statistics interval to zero ([#2141]).\n\n- The ability to completely purge DHCP leases ([#1691]).\n\n- Settable timeouts for querying the upstream servers ([#2280]).\n\n- Configuration file properties to change group and user ID on startup on Unix ([#2763]).\n\n- Experimental OpenBSD support for AMD64 and 64-bit ARM CPUs ([#2439], [#3225], [#3226]).\n\n- Support for custom port in DNS-over-HTTPS profiles for Apple’s devices ([#3172]).\n\n- `darwin/arm64` support ([#2443]).\n\n- `freebsd/arm64` support ([#2441]).\n\n- Output of the default addresses of the upstreams used for resolving PTRs for private addresses ([#3136]).\n\n- Detection and handling of recurrent PTR requests for locally-served addresses ([#3185]).\n\n- The ability to completely disable reverse DNS resolving of IPs from locally-served networks ([#3184]).\n\n- New flag `--local-frontend` to serve dynamically changeable frontend files from disk as opposed to the ones that were compiled into the binary.\n\n### Changed\n\n- Port bindings are now checked for uniqueness ([#3835]).\n\n- The DNSSEC check now simply checks against the AD flag in the response ([#3904]).\n\n- Client objects in the configuration file are now sorted ([#3933]).\n\n- Responses from cache are now labeled ([#3772]).\n\n- Better error message for ED25519 private keys, which are not widely supported ([#3737]).\n\n- Cache now follows RFC more closely for negative answers ([#3707]).\n\n- `dnsrewrite` rules and other DNS rewrites will now be applied even when the protection is disabled ([#1558]).\n\n- DHCP gateway address, subnet mask, IP address range, and leases validations ([#3529]).\n\n- The `systemd` service script will now create the `/var/log` directory when it doesn’t exist ([#3579]).\n\n- Items in allowed clients, disallowed clients, and blocked hosts lists are now required to be unique ([#3419]).\n\n- The TLS private key previously saved as a string isn’t shown in API responses anymore ([#1898]).\n\n- Better OpenWrt detection ([#3435]).\n\n- DNS-over-HTTPS queries that come from HTTP proxies in the `trusted_proxies` list now use the real IP address of the client instead of the address of the proxy ([#2799]).\n\n- Clients who are blocked by access settings now receive a `REFUSED` response when a protocol other than DNS-over-UDP and DNSCrypt is used.\n\n- `dns.querylog_interval` property is now formatted in hours.\n\n- Query log search now supports internationalized domains ([#3012]).\n\n- Internationalized domains are now shown decoded in the query log with the original encoded version shown in request details ([#3013]).\n\n- When `/etc/hosts`-type rules have several IPs for one host, all IPs are now returned instead of only the first one ([#1381]).\n\n- Property `rlimit_nofile` is now in the `os` object of the configuration file, together with the new `group` and `user` properties ([#2763]).\n\n- Permissions on filter files are now `0o644` instead of `0o600` ([#3198]).\n\n#### Configuration changes\n\nIn this release, the schema version has changed from 10 to 12.\n\n- Property `dns.querylog_interval`, which in schema versions 11 and earlier used to be an integer number of days, is now a string with a human-readable duration:\n\n    ```yaml\n    # BEFORE:\n    'dns':\n      # …\n      'querylog_interval': 90\n\n    # AFTER:\n    'dns':\n      # …\n      'querylog_interval': '2160h'\n    ```\n\n    To rollback this change, convert the property back into days and change the `schema_version` back to `11`.\n\n- Property `rlimit_nofile`, which in schema versions 10 and earlier used to be on the top level, is now moved to the new `os` object:\n\n    ```yaml\n    # BEFORE:\n    'rlimit_nofile': 42\n\n    # AFTER:\n    'os':\n      'group': ''\n      'rlimit_nofile': 42\n      'user': ''\n    ```\n\n    To rollback this change, move the property on the top level and change the `schema_version` back to `10`.\n\n### Deprecated\n\n- Go 1.16 support.  v0.108.0 will require at least Go 1.17 to build.\n\n### Fixed\n\n- EDNS0 TCP keepalive option handling ([#3778]).\n\n- Rules with the `denyallow` modifier applying to IP addresses when they shouldn’t ([#3175]).\n\n- The length of the EDNS0 client subnet option appearing too long for some upstream servers ([#3887]).\n\n- Invalid redirection to the HTTPS web interface after saving enabled encryption settings ([#3558]).\n\n- Incomplete propagation of the client’s IP anonymization setting to the statistics ([#3890]).\n\n- Incorrect results with the `dnsrewrite` modifier for entries from the operating system’s hosts file ([#3815]).\n\n- Matching against rules with `|` at the end of the domain name ([#3371]).\n\n- Incorrect assignment of explicitly configured DHCP options ([#3744]).\n\n- Occasional panic during shutdown ([#3655]).\n\n- Addition of IPs into only one as opposed to all matching ipsets on Linux ([#3638]).\n\n- Removal of temporary filter files ([#3567]).\n\n- Panic when an upstream server responds with an empty question section ([#3551]).\n\n- 9GAG blocking ([#3564]).\n\n- DHCP now follows RFCs more closely when it comes to response sending and option selection ([#3443], [#3538]).\n\n- Occasional panics when reading old statistics databases ([#3506]).\n\n- `reload` service action on macOS and FreeBSD ([#3457]).\n\n- Inaccurate using of service actions in the installation script ([#3450]).\n\n- ClientID checking ([#3437]).\n\n- Discovering other DHCP servers on `darwin` and `freebsd` ([#3417]).\n\n- Switching listening address to unspecified one when bound to a single specified IPv4 address on Darwin (macOS) ([#2807]).\n\n- Incomplete HTTP response for static IP address.\n\n- DNSCrypt queries weren’t appearing in query log ([#3372]).\n\n- Wrong IP address for proxied DNS-over-HTTPS queries ([#2799]).\n\n- Domain name letter case mismatches in DNS rewrites ([#3351]).\n\n- Conflicts between IPv4 and IPv6 DNS rewrites ([#3343]).\n\n- Letter case mismatches in `CNAME` filtering ([#3335]).\n\n- Occasional breakages on network errors with DNS-over-HTTP upstreams ([#3217]).\n\n- Errors when setting static IP on Linux ([#3257]).\n\n- Treatment of domain names and FQDNs in custom rules with the `dnsrewrite` modifier that use the `PTR` type ([#3256]).\n\n- Redundant hostname generating while loading static leases with empty hostname ([#3166]).\n\n- Domain name case in responses ([#3194]).\n\n- Custom upstreams selection for clients with ClientIDs in DNS-over-TLS and DNS-over-HTTP ([#3186]).\n\n- Incorrect client-based filtering applying logic ([#2875]).\n\n### Removed\n\n- Go 1.15 support.\n\n[#1381]: https://github.com/AdguardTeam/AdGuardHome/issues/1381\n[#1558]: https://github.com/AdguardTeam/AdGuardHome/issues/1558\n[#1691]: https://github.com/AdguardTeam/AdGuardHome/issues/1691\n[#1898]: https://github.com/AdguardTeam/AdGuardHome/issues/1898\n[#1992]: https://github.com/AdguardTeam/AdGuardHome/issues/1992\n[#2141]: https://github.com/AdguardTeam/AdGuardHome/issues/2141\n[#2145]: https://github.com/AdguardTeam/AdGuardHome/issues/2145\n[#2280]: https://github.com/AdguardTeam/AdGuardHome/issues/2280\n[#2439]: https://github.com/AdguardTeam/AdGuardHome/issues/2439\n[#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441\n[#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443\n[#2504]: https://github.com/AdguardTeam/AdGuardHome/issues/2504\n[#2624]: https://github.com/AdguardTeam/AdGuardHome/issues/2624\n[#2763]: https://github.com/AdguardTeam/AdGuardHome/issues/2763\n[#2799]: https://github.com/AdguardTeam/AdGuardHome/issues/2799\n[#2807]: https://github.com/AdguardTeam/AdGuardHome/issues/2807\n[#3012]: https://github.com/AdguardTeam/AdGuardHome/issues/3012\n[#3013]: https://github.com/AdguardTeam/AdGuardHome/issues/3013\n[#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136\n[#3162]: https://github.com/AdguardTeam/AdGuardHome/issues/3162\n[#3166]: https://github.com/AdguardTeam/AdGuardHome/issues/3166\n[#3172]: https://github.com/AdguardTeam/AdGuardHome/issues/3172\n[#3175]: https://github.com/AdguardTeam/AdGuardHome/issues/3175\n[#3184]: https://github.com/AdguardTeam/AdGuardHome/issues/3184\n[#3185]: https://github.com/AdguardTeam/AdGuardHome/issues/3185\n[#3186]: https://github.com/AdguardTeam/AdGuardHome/issues/3186\n[#3194]: https://github.com/AdguardTeam/AdGuardHome/issues/3194\n[#3198]: https://github.com/AdguardTeam/AdGuardHome/issues/3198\n[#3217]: https://github.com/AdguardTeam/AdGuardHome/issues/3217\n[#3225]: https://github.com/AdguardTeam/AdGuardHome/issues/3225\n[#3226]: https://github.com/AdguardTeam/AdGuardHome/issues/3226\n[#3256]: https://github.com/AdguardTeam/AdGuardHome/issues/3256\n[#3257]: https://github.com/AdguardTeam/AdGuardHome/issues/3257\n[#3289]: https://github.com/AdguardTeam/AdGuardHome/issues/3289\n[#3335]: https://github.com/AdguardTeam/AdGuardHome/issues/3335\n[#3343]: https://github.com/AdguardTeam/AdGuardHome/issues/3343\n[#3351]: https://github.com/AdguardTeam/AdGuardHome/issues/3351\n[#3371]: https://github.com/AdguardTeam/AdGuardHome/issues/3371\n[#3372]: https://github.com/AdguardTeam/AdGuardHome/issues/3372\n[#3417]: https://github.com/AdguardTeam/AdGuardHome/issues/3417\n[#3419]: https://github.com/AdguardTeam/AdGuardHome/issues/3419\n[#3435]: https://github.com/AdguardTeam/AdGuardHome/issues/3435\n[#3437]: https://github.com/AdguardTeam/AdGuardHome/issues/3437\n[#3443]: https://github.com/AdguardTeam/AdGuardHome/issues/3443\n[#3450]: https://github.com/AdguardTeam/AdGuardHome/issues/3450\n[#3457]: https://github.com/AdguardTeam/AdGuardHome/issues/3457\n[#3506]: https://github.com/AdguardTeam/AdGuardHome/issues/3506\n[#3529]: https://github.com/AdguardTeam/AdGuardHome/issues/3529\n[#3538]: https://github.com/AdguardTeam/AdGuardHome/issues/3538\n[#3551]: https://github.com/AdguardTeam/AdGuardHome/issues/3551\n[#3558]: https://github.com/AdguardTeam/AdGuardHome/issues/3558\n[#3564]: https://github.com/AdguardTeam/AdGuardHome/issues/3564\n[#3567]: https://github.com/AdguardTeam/AdGuardHome/issues/3567\n[#3579]: https://github.com/AdguardTeam/AdGuardHome/issues/3579\n[#3638]: https://github.com/AdguardTeam/AdGuardHome/issues/3638\n[#3655]: https://github.com/AdguardTeam/AdGuardHome/issues/3655\n[#3707]: https://github.com/AdguardTeam/AdGuardHome/issues/3707\n[#3737]: https://github.com/AdguardTeam/AdGuardHome/issues/3737\n[#3744]: https://github.com/AdguardTeam/AdGuardHome/issues/3744\n[#3772]: https://github.com/AdguardTeam/AdGuardHome/issues/3772\n[#3778]: https://github.com/AdguardTeam/AdGuardHome/issues/3778\n[#3815]: https://github.com/AdguardTeam/AdGuardHome/issues/3815\n[#3835]: https://github.com/AdguardTeam/AdGuardHome/issues/3835\n[#3887]: https://github.com/AdguardTeam/AdGuardHome/issues/3887\n[#3890]: https://github.com/AdguardTeam/AdGuardHome/issues/3890\n[#3904]: https://github.com/AdguardTeam/AdGuardHome/issues/3904\n[#3933]: https://github.com/AdguardTeam/AdGuardHome/pull/3933\n\n[ms-v0.107.0]: https://github.com/AdguardTeam/AdGuardHome/milestone/23?closed=1\n[rfc-9000]:    https://datatracker.ietf.org/doc/html/rfc9000\n\n## [v0.106.3] - 2021-05-19\n\nSee also the [v0.106.3 GitHub milestone][ms-v0.106.3].\n\n### Added\n\n- Support for reinstall (`-r`) and uninstall (`-u`) flags in the installation script ([#2462]).\n\n- Support for DHCP `DECLINE` and `RELEASE` message types ([#3053]).\n\n### Changed\n\n- Add microseconds to log output.\n\n### Fixed\n\n- Intermittent \"Warning: ID mismatch\" errors ([#3087]).\n\n- Error when using installation script on some ARMv7 devices ([#2542]).\n\n- DHCP leases validation ([#3107], [#3127]).\n\n- Local PTR request recursion in Docker containers ([#3064]).\n\n- Ignoring client-specific filtering settings when filtering is disabled in general settings ([#2875]).\n\n- Disallowed domains are now case-insensitive ([#3115]).\n\n[#2462]: https://github.com/AdguardTeam/AdGuardHome/issues/2462\n[#2542]: https://github.com/AdguardTeam/AdGuardHome/issues/2542\n[#2875]: https://github.com/AdguardTeam/AdGuardHome/issues/2875\n[#3053]: https://github.com/AdguardTeam/AdGuardHome/issues/3053\n[#3064]: https://github.com/AdguardTeam/AdGuardHome/issues/3064\n[#3107]: https://github.com/AdguardTeam/AdGuardHome/issues/3107\n[#3115]: https://github.com/AdguardTeam/AdGuardHome/issues/3115\n[#3127]: https://github.com/AdguardTeam/AdGuardHome/issues/3127\n\n[ms-v0.106.3]: https://github.com/AdguardTeam/AdGuardHome/milestone/35?closed=1\n\n## [v0.106.2] - 2021-05-06\n\nSee also the [v0.106.2 GitHub milestone][ms-v0.106.2].\n\n### Fixed\n\n- Uniqueness validation for dynamic DHCP leases ([#3056]).\n\n[#3056]: https://github.com/AdguardTeam/AdGuardHome/issues/3056\n\n[ms-v0.106.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/34?closed=1\n\n## [v0.106.1] - 2021-04-30\n\nSee also the [v0.106.1 GitHub milestone][ms-v0.106.1].\n\n### Fixed\n\n- Local domain name handling when the DHCP server is disabled ([#3028]).\n\n- Normalization of previously-saved invalid static DHCP leases ([#3027]).\n\n- Validation of IPv6 addresses with zones in system resolvers ([#3022]).\n\n[#3022]: https://github.com/AdguardTeam/AdGuardHome/issues/3022\n[#3027]: https://github.com/AdguardTeam/AdGuardHome/issues/3027\n[#3028]: https://github.com/AdguardTeam/AdGuardHome/issues/3028\n\n[ms-v0.106.1]: https://github.com/AdguardTeam/AdGuardHome/milestone/33?closed=1\n\n## [v0.106.0] - 2021-04-28\n\nSee also the [v0.106.0 GitHub milestone][ms-v0.106.0].\n\n### Added\n\n- The ability to block user for login after configurable number of unsuccessful attempts for configurable time ([#2826]).\n\n- `denyallow` modifier for filters ([#2923]).\n\n- Hostname uniqueness validation in the DHCP server ([#2952]).\n\n- Hostname generating for DHCP clients which don’t provide their own ([#2723]).\n\n- New flag `--no-etc-hosts` to disable client domain name lookups in the operating system’s `/etc/hosts` files ([#1947]).\n\n- The ability to set up custom upstreams to resolve PTR queries for local addresses and to disable the automatic resolving of clients’ addresses ([#2704]).\n\n- Logging of the client’s IP address after failed login attempts ([#2824]).\n\n- Search by clients’ names in the query log ([#1273]).\n\n- Verbose version output with `-v --version` ([#2416]).\n\n- The ability to set a custom TLD or domain name for known hosts in the local network ([#2393], [#2961]).\n\n- The ability to serve DNS queries on multiple hosts and interfaces ([#1401]).\n\n- `ips` and `text` DHCP server options ([#2385]).\n\n- `SRV` records support in filtering rules with the `dnsrewrite` modifier ([#2533]).\n\n### Changed\n\n- Our DoQ implementation is now updated to conform to the latest standard [draft][doq-draft-02] ([#2843]).\n\n- Quality of logging ([#2954]).\n\n- Normalization of hostnames sent by DHCP clients ([#2946], [#2952]).\n\n- The access to the private hosts is now forbidden for users from external networks ([#2889]).\n\n- The reverse lookup for local addresses is now performed via local resolvers ([#2704]).\n\n- Stricter validation of the IP addresses of static leases in the DHCP server with regards to the netmask ([#2838]).\n\n- Stricter validation of `dnsrewrite` filtering rule modifier parameters ([#2498]).\n\n- New, more correct versioning scheme ([#2412]).\n\n### Deprecated\n\n- Go 1.15 support.  v0.107.0 will require at least Go 1.16 to build.\n\n### Fixed\n\n- Multiple answers for a `dnsrewrite` rule matching requests with repeating patterns in it ([#2981]).\n\n- Root server resolving when custom upstreams for hosts are specified ([#2994]).\n\n- Inconsistent resolving of DHCP clients when the DHCP server is disabled ([#2934]).\n\n- Comment handling in clients’ custom upstreams ([#2947]).\n\n- Overwriting of DHCPv4 options when using the HTTP API ([#2927]).\n\n- Assumption that MAC addresses always have the length of 6 octets ([#2828]).\n\n- Support for more than one `/24` subnet in DHCP ([#2541]).\n\n- Invalid filenames in the `mobileconfig` API responses ([#2835]).\n\n### Removed\n\n- Go 1.14 support.\n\n[#1273]: https://github.com/AdguardTeam/AdGuardHome/issues/1273\n[#1401]: https://github.com/AdguardTeam/AdGuardHome/issues/1401\n[#1947]: https://github.com/AdguardTeam/AdGuardHome/issues/1947\n[#2385]: https://github.com/AdguardTeam/AdGuardHome/issues/2385\n[#2393]: https://github.com/AdguardTeam/AdGuardHome/issues/2393\n[#2412]: https://github.com/AdguardTeam/AdGuardHome/issues/2412\n[#2416]: https://github.com/AdguardTeam/AdGuardHome/issues/2416\n[#2498]: https://github.com/AdguardTeam/AdGuardHome/issues/2498\n[#2533]: https://github.com/AdguardTeam/AdGuardHome/issues/2533\n[#2541]: https://github.com/AdguardTeam/AdGuardHome/issues/2541\n[#2704]: https://github.com/AdguardTeam/AdGuardHome/issues/2704\n[#2723]: https://github.com/AdguardTeam/AdGuardHome/issues/2723\n[#2824]: https://github.com/AdguardTeam/AdGuardHome/issues/2824\n[#2826]: https://github.com/AdguardTeam/AdGuardHome/issues/2826\n[#2828]: https://github.com/AdguardTeam/AdGuardHome/issues/2828\n[#2835]: https://github.com/AdguardTeam/AdGuardHome/issues/2835\n[#2838]: https://github.com/AdguardTeam/AdGuardHome/issues/2838\n[#2843]: https://github.com/AdguardTeam/AdGuardHome/issues/2843\n[#2889]: https://github.com/AdguardTeam/AdGuardHome/issues/2889\n[#2923]: https://github.com/AdguardTeam/AdGuardHome/issues/2923\n[#2927]: https://github.com/AdguardTeam/AdGuardHome/issues/2927\n[#2934]: https://github.com/AdguardTeam/AdGuardHome/issues/2934\n[#2946]: https://github.com/AdguardTeam/AdGuardHome/issues/2946\n[#2947]: https://github.com/AdguardTeam/AdGuardHome/issues/2947\n[#2952]: https://github.com/AdguardTeam/AdGuardHome/issues/2952\n[#2954]: https://github.com/AdguardTeam/AdGuardHome/issues/2954\n[#2961]: https://github.com/AdguardTeam/AdGuardHome/issues/2961\n[#2981]: https://github.com/AdguardTeam/AdGuardHome/issues/2981\n[#2994]: https://github.com/AdguardTeam/AdGuardHome/issues/2994\n\n[doq-draft-02]: https://tools.ietf.org/html/draft-ietf-dprive-dnsoquic-02\n[ms-v0.106.0]:  https://github.com/AdguardTeam/AdGuardHome/milestone/26?closed=1\n\n## [v0.105.2] - 2021-03-10\n\n### Security\n\n- Session token doesn’t contain user’s information anymore ([#2470]).\n\nSee also the [v0.105.2 GitHub milestone][ms-v0.105.2].\n\n### Fixed\n\n- Incomplete hostnames with trailing zero-bytes handling ([#2582]).\n\n- Wrong DNS-over-TLS ALPN configuration ([#2681]).\n\n- Inconsistent responses for messages with EDNS0 and AD when DNS caching is enabled ([#2600]).\n\n- Incomplete OpenWrt detection ([#2757]).\n\n- DHCP lease’s `expired` property incorrect time format ([#2692]).\n\n- Incomplete DNS upstreams validation ([#2674]).\n\n- Wrong parsing of DHCP options of the `ip` type ([#2688]).\n\n[#2470]: https://github.com/AdguardTeam/AdGuardHome/issues/2470\n[#2582]: https://github.com/AdguardTeam/AdGuardHome/issues/2582\n[#2600]: https://github.com/AdguardTeam/AdGuardHome/issues/2600\n[#2674]: https://github.com/AdguardTeam/AdGuardHome/issues/2674\n[#2681]: https://github.com/AdguardTeam/AdGuardHome/issues/2681\n[#2688]: https://github.com/AdguardTeam/AdGuardHome/issues/2688\n[#2692]: https://github.com/AdguardTeam/AdGuardHome/issues/2692\n[#2757]: https://github.com/AdguardTeam/AdGuardHome/issues/2757\n\n[ms-v0.105.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/32?closed=1\n\n## [v0.105.1] - 2021-02-15\n\nSee also the [v0.105.1 GitHub milestone][ms-v0.105.1].\n\n### Changed\n\n- Increased HTTP API timeouts ([#2671], [#2682]).\n\n- \"Permission denied\" errors when checking if the machine has a static IP no longer prevent the DHCP server from starting ([#2667]).\n\n- The server name sent by clients of TLS APIs is not only checked when `strict_sni_check` is enabled ([#2664]).\n\n- HTTP API request body size limit for the `POST /control/access/set` and `POST /control/filtering/set_rules` HTTP APIs is increased ([#2666], [#2675]).\n\n### Fixed\n\n- Error when enabling the DHCP server when AdGuard Home couldn’t determine if the machine has a static IP.\n\n- Optical issue on custom rules ([#2641]).\n\n- Occasional crashes during startup.\n\n- The property `\"range_start\"` in the `GET /control/dhcp/status` HTTP API response is now correctly named again ([#2678]).\n\n- DHCPv6 server’s `ra_slaac_only` and `ra_allow_slaac` properties aren’t reset to `false` on update anymore ([#2653]).\n\n- The `Vary` header is now added along with `Access-Control-Allow-Origin` to prevent cache-related and other issues in browsers ([#2658]).\n\n- The request body size limit is now set for HTTPS requests as well.\n\n- Incorrect version tag in the Docker release ([#2663]).\n\n- DNSCrypt queries weren’t marked as such in logs ([#2662]).\n\n[#2641]: https://github.com/AdguardTeam/AdGuardHome/issues/2641\n[#2653]: https://github.com/AdguardTeam/AdGuardHome/issues/2653\n[#2658]: https://github.com/AdguardTeam/AdGuardHome/issues/2658\n[#2662]: https://github.com/AdguardTeam/AdGuardHome/issues/2662\n[#2663]: https://github.com/AdguardTeam/AdGuardHome/issues/2663\n[#2664]: https://github.com/AdguardTeam/AdGuardHome/issues/2664\n[#2666]: https://github.com/AdguardTeam/AdGuardHome/issues/2666\n[#2667]: https://github.com/AdguardTeam/AdGuardHome/issues/2667\n[#2671]: https://github.com/AdguardTeam/AdGuardHome/issues/2671\n[#2675]: https://github.com/AdguardTeam/AdGuardHome/issues/2675\n[#2678]: https://github.com/AdguardTeam/AdGuardHome/issues/2678\n[#2682]: https://github.com/AdguardTeam/AdGuardHome/issues/2682\n\n[ms-v0.105.1]: https://github.com/AdguardTeam/AdGuardHome/milestone/31?closed=1\n\n## [v0.105.0] - 2021-02-10\n\nSee also the [v0.105.0 GitHub milestone][ms-v0.105.0].\n\n### Added\n\n- Added more services to the \"Blocked services\" list ([#2224], [#2401]).\n\n- `ipset` subdomain matching, just like `dnsmasq` does ([#2179]).\n\n- ClientID support for DNS-over-HTTPS, DNS-over-QUIC, and DNS-over-TLS ([#1383]).\n\n- The new `dnsrewrite` modifier for filters ([#2102]).\n\n- The host checking API and the query logs API can now return multiple matched rules ([#2102]).\n\n- Detecting of network interface configured to have static IP address via `/etc/network/interfaces` ([#2302]).\n\n- DNSCrypt protocol support ([#1361]).\n\n- A 5 second wait period until a DHCP server’s network interface gets an IP address ([#2304]).\n\n- `dnstype` modifier for filters ([#2337]).\n\n- HTTP API request body size limit ([#2305]).\n\n### Changed\n\n- `Access-Control-Allow-Origin` is now only set to the same origin as the domain, but with an HTTP scheme as opposed to `*` ([#2484]).\n\n- `workDir` now supports symlinks.\n\n- Stopped mounting together the directories `/opt/adguardhome/conf` and `/opt/adguardhome/work` in our Docker images ([#2589]).\n\n- When `dns.bogus_nxdomain` option is used, the server will now transform responses if there is at least one bogus address instead of all of them ([#2394]).  The new behavior is the same as in `dnsmasq`.\n\n- Post-updating relaunch possibility is now determined OS-dependently ([#2231], [#2391]).\n\n- Made the mobileconfig HTTP API more robust and predictable, add parameters and improve error response ([#2358]).\n\n- Improved HTTP requests handling and timeouts ([#2343]).\n\n- Our snap package now uses the `core20` image as its base ([#2306]).\n\n- New build system and various internal improvements ([#2271], [#2276], [#2297], [#2509], [#2552], [#2639], [#2646]).\n\n### Deprecated\n\n- Go 1.14 support.  v0.106.0 will require at least Go 1.15 to build.\n\n- The `darwin/386` port.  It will be removed in v0.106.0.\n\n- The `\"rule\"` and `\"filter_id\"` property in `GET /filtering/check_host` and `GET /querylog` responses.  They will be removed in v0.106.0 ([#2102]).\n\n### Fixed\n\n- Autoupdate bug in the Darwin (macOS) version ([#2630]).\n\n- Unnecessary conversions from `string` to `net.IP`, and vice versa ([#2508]).\n\n- Inability to set DNS cache TTL limits ([#2459]).\n\n- Possible freezes on slower machines ([#2225]).\n\n- A mitigation against records being shown in the wrong order on the query log page ([#2293]).\n\n- A JSON parsing error in query log ([#2345]).\n\n- Incorrect detection of the IPv6 address of an interface as well as another infinite loop in the `/dhcp/find_active_dhcp` HTTP API ([#2355]).\n\n### Removed\n\n- The undocumented ability to use hostnames as any of `bind_host` values in configuration.  Documentation requires them to be valid IP addresses, and now the implementation makes sure that that is the case ([#2508]).\n\n- `Dockerfile` ([#2276]).  Replaced with the script `scripts/make/build-docker.sh` which uses `scripts/make/Dockerfile`.\n\n- Support for pre-v0.99.3 format of query logs ([#2102]).\n\n[#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361\n[#1383]: https://github.com/AdguardTeam/AdGuardHome/issues/1383\n[#2102]: https://github.com/AdguardTeam/AdGuardHome/issues/2102\n[#2179]: https://github.com/AdguardTeam/AdGuardHome/issues/2179\n[#2224]: https://github.com/AdguardTeam/AdGuardHome/issues/2224\n[#2225]: https://github.com/AdguardTeam/AdGuardHome/issues/2225\n[#2231]: https://github.com/AdguardTeam/AdGuardHome/issues/2231\n[#2271]: https://github.com/AdguardTeam/AdGuardHome/issues/2271\n[#2276]: https://github.com/AdguardTeam/AdGuardHome/issues/2276\n[#2293]: https://github.com/AdguardTeam/AdGuardHome/issues/2293\n[#2297]: https://github.com/AdguardTeam/AdGuardHome/issues/2297\n[#2302]: https://github.com/AdguardTeam/AdGuardHome/issues/2302\n[#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304\n[#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305\n[#2306]: https://github.com/AdguardTeam/AdGuardHome/issues/2306\n[#2337]: https://github.com/AdguardTeam/AdGuardHome/issues/2337\n[#2343]: https://github.com/AdguardTeam/AdGuardHome/issues/2343\n[#2345]: https://github.com/AdguardTeam/AdGuardHome/issues/2345\n[#2355]: https://github.com/AdguardTeam/AdGuardHome/issues/2355\n[#2358]: https://github.com/AdguardTeam/AdGuardHome/issues/2358\n[#2391]: https://github.com/AdguardTeam/AdGuardHome/issues/2391\n[#2394]: https://github.com/AdguardTeam/AdGuardHome/issues/2394\n[#2401]: https://github.com/AdguardTeam/AdGuardHome/issues/2401\n[#2459]: https://github.com/AdguardTeam/AdGuardHome/issues/2459\n[#2484]: https://github.com/AdguardTeam/AdGuardHome/issues/2484\n[#2508]: https://github.com/AdguardTeam/AdGuardHome/issues/2508\n[#2509]: https://github.com/AdguardTeam/AdGuardHome/issues/2509\n[#2552]: https://github.com/AdguardTeam/AdGuardHome/issues/2552\n[#2589]: https://github.com/AdguardTeam/AdGuardHome/issues/2589\n[#2630]: https://github.com/AdguardTeam/AdGuardHome/issues/2630\n[#2639]: https://github.com/AdguardTeam/AdGuardHome/issues/2639\n[#2646]: https://github.com/AdguardTeam/AdGuardHome/issues/2646\n\n[ms-v0.105.0]: https://github.com/AdguardTeam/AdGuardHome/milestone/27?closed=1\n\n## [v0.104.3] - 2020-11-19\n\nSee also the [v0.104.3 GitHub milestone][ms-v0.104.3].\n\n### Fixed\n\n- The accidentally exposed profiler HTTP API ([#2336]).\n\n[#2336]: https://github.com/AdguardTeam/AdGuardHome/issues/2336\n\n[ms-v0.104.3]: https://github.com/AdguardTeam/AdGuardHome/milestone/30?closed=1\n\n## [v0.104.2] - 2020-11-19\n\nSee also the [v0.104.2 GitHub milestone][ms-v0.104.2].\n\n### Added\n\n- This changelog :-) ([#2294]).\n\n- `HACKING.md`, a guide for developers.\n\n### Changed\n\n- Improved tests output ([#2273]).\n\n### Fixed\n\n- Query logs from file not loading after the ones buffered in memory ([#2325]).\n\n- Unnecessary errors in query logs when switching between log files ([#2324]).\n\n- `404 Not Found` errors on the DHCP settings page on Windows.  The page now correctly shows that DHCP is not currently available on that OS ([#2295]).\n\n- Infinite loop in `/dhcp/find_active_dhcp` ([#2301]).\n\n[#2273]: https://github.com/AdguardTeam/AdGuardHome/issues/2273\n[#2294]: https://github.com/AdguardTeam/AdGuardHome/issues/2294\n[#2295]: https://github.com/AdguardTeam/AdGuardHome/issues/2295\n[#2301]: https://github.com/AdguardTeam/AdGuardHome/issues/2301\n[#2324]: https://github.com/AdguardTeam/AdGuardHome/issues/2324\n[#2325]: https://github.com/AdguardTeam/AdGuardHome/issues/2325\n\n[ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1\n\n<!--\n[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.74...HEAD\n[v0.107.74]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.73...v0.107.74\n-->\n\n[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.73...HEAD\n[v0.107.73]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.72...v0.107.73\n[v0.107.72]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.71...v0.107.72\n[v0.107.71]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.70...v0.107.71\n[v0.107.70]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.69...v0.107.70\n[v0.107.69]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.68...v0.107.69\n[v0.107.68]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.67...v0.107.68\n[v0.107.67]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.66...v0.107.67\n[v0.107.66]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.65...v0.107.66\n[v0.107.65]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.64...v0.107.65\n[v0.107.64]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.63...v0.107.64\n[v0.107.63]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.62...v0.107.63\n[v0.107.62]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...v0.107.62\n[v0.107.61]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61\n[v0.107.60]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60\n[v0.107.59]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...v0.107.59\n[v0.107.58]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58\n[v0.107.57]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.56...v0.107.57\n[v0.107.56]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.55...v0.107.56\n[v0.107.55]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.54...v0.107.55\n[v0.107.54]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.53...v0.107.54\n[v0.107.53]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.52...v0.107.53\n[v0.107.52]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.51...v0.107.52\n[v0.107.51]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.50...v0.107.51\n[v0.107.50]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.49...v0.107.50\n[v0.107.49]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.48...v0.107.49\n[v0.107.48]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.47...v0.107.48\n[v0.107.47]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.46...v0.107.47\n[v0.107.46]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.45...v0.107.46\n[v0.107.45]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.44...v0.107.45\n[v0.107.44]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.43...v0.107.44\n[v0.107.43]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.42...v0.107.43\n[v0.107.42]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.41...v0.107.42\n[v0.107.41]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.40...v0.107.41\n[v0.107.40]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.39...v0.107.40\n[v0.107.39]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.38...v0.107.39\n[v0.107.38]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.37...v0.107.38\n[v0.107.37]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.36...v0.107.37\n[v0.107.36]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.35...v0.107.36\n[v0.107.35]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.34...v0.107.35\n[v0.107.34]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...v0.107.34\n[v0.107.33]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...v0.107.33\n[v0.107.32]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.31...v0.107.32\n[v0.107.31]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.30...v0.107.31\n[v0.107.30]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...v0.107.30\n[v0.107.29]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...v0.107.29\n[v0.107.28]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...v0.107.28\n[v0.107.27]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27\n[v0.107.26]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26\n[v0.107.25]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.24...v0.107.25\n[v0.107.24]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.23...v0.107.24\n[v0.107.23]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.22...v0.107.23\n[v0.107.22]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...v0.107.22\n[v0.107.21]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...v0.107.21\n[v0.107.20]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.19...v0.107.20\n[v0.107.19]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.18...v0.107.19\n[v0.107.18]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...v0.107.18\n[v0.107.17]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...v0.107.17\n[v0.107.16]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...v0.107.16\n[v0.107.15]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15\n[v0.107.14]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14\n[v0.107.13]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13\n[v0.107.12]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12\n[v0.107.11]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...v0.107.11\n[v0.107.10]:  https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.9...v0.107.10\n[v0.107.9]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.8...v0.107.9\n[v0.107.8]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.7...v0.107.8\n[v0.107.7]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.6...v0.107.7\n[v0.107.6]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.5...v0.107.6\n[v0.107.5]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.4...v0.107.5\n[v0.107.4]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.3...v0.107.4\n[v0.107.3]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.2...v0.107.3\n[v0.107.2]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.1...v0.107.2\n[v0.107.1]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.0...v0.107.1\n[v0.107.0]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.106.3...v0.107.0\n[v0.106.3]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.106.2...v0.106.3\n[v0.106.2]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.106.1...v0.106.2\n[v0.106.1]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.106.0...v0.106.1\n[v0.106.0]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.2...v0.106.0\n[v0.105.2]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.1...v0.105.2\n[v0.105.1]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.0...v0.105.1\n[v0.105.0]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...v0.105.0\n[v0.104.3]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.2...v0.104.3\n[v0.104.2]:   https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.1...v0.104.2\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to AdGuard Home\n\nIf you want to contribute to AdGuard Home by filing or commenting on an issue or opening a pull request, please follow the instructions below.\n\n## General recommendations\n\nPlease don’t:\n\n- post comments like “+1” or “this”.  Use the :+1: reaction on the issue instead, as this allows us to actually see the level of support for issues.\n\n- file issues about localization errors or send localization updates as PRs.  We’re using [CrowdIn] to manage our translations and we generally update them before each Beta and Release build.  You can learn more about translating AdGuard products [in our Knowledge Base][kb-trans].\n\n- file issues about a particular filtering-rule list misbehaving.  These are tracked through the [separate form for filtering issues][form].\n\n- send or request updates to filtering-rule lists, such as the ones for the Blocked Services feature or the list of approved filtering-rule lists.  We update them from the [separate repository][hostlist] once before each Beta and Release build.\n\nPlease do:\n\n- follow the template instructions and provide data for reproducing issues.\n\n- write the title of your issue or pull request in English.  Any language is fine in the body, but it is important to keep the title in English to make it easier for people and bots to look up duplicated issues.\n\n[CrowdIn]:  https://crowdin.com/project/adguard-applications/en#/adguard-home\n[form]:     https://link.adtidy.org/forward.html?action=report&app=home&from=github\n[hostlist]: https://github.com/AdguardTeam/HostlistsRegistry\n[kb-trans]: https://kb.adguard.com/en/general/adguard-translations\n\n## Issues\n\n### Search first\n\nPlease make sure that the issue is not a duplicate or a question.  If it’s a duplicate, please react to the original issue with a thumbs up.  If it’s a question, please look through our [Wiki] and, if you haven’t found the answer, post it to the GitHub [Discussions] page.\n\n[Discussions]: https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a\n[Wiki]:        https://github.com/AdguardTeam/AdGuardHome/wiki\n\n### Follow the issue template\n\nDevelopers need to be able to reproduce the faulty behavior in order to fix an issue, so please make sure that you follow the instructions in the issue template carefully.\n\n## Pull requests\n\n### Discuss your changes first\n\nPlease discuss your changes by opening an issue.  The maintainers should evaluate your proposal, and it’s generally better if that’s done before any code is written.\n\n### Review your changes for style\n\nWe have a set of [code guidelines][hacking] that we expect the code to follow.  Please make sure you follow it.\n\n[hacking]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md\n\n### Test your changes\n\nMake sure that it passes linters and tests by running the corresponding Make targets.  For backend changes, it’s `make go-check`.  For frontend, run `make js-lint`.\n\nAdditionally, a manual test is often required.  While we’re constantly working on improving our test suites, they’re still not as good as we’d like them to be.\n"
  },
  {
    "path": "HACKING.md",
    "content": "# AdGuard Home developer guidelines\n\nThis document was moved to the [AdGuard Code Guidelines repository][repo].  All sections with IDs now only have links to the corresponding files and sections in that repository.\n\n## <a href=\"#git\" id=\"git\" name=\"git\">Git</a>\n\nThis section was moved to [its own document][git].\n\n## <a href=\"#go\" id=\"go\" name=\"go\">Go</a>\n\nThis section was moved to [its own document][go].\n\n### <a href=\"#code\" id=\"code\" name=\"code\">Code</a>\n\nThis subsection was moved to the [corresponding section][code] of the Go guidelines document.\n\n### <a href=\"#commenting\" id=\"commenting\" name=\"commenting\">Commenting</a>\n\nThis subsection was moved to the [corresponding section][cmnt] of the Go guidelines document.\n\n### <a href=\"#formatting\" id=\"formatting\" name=\"formatting\">Formatting</a>\n\nThis subsection was moved to the [corresponding section][fmt] of the Go guidelines document.\n\n### <a href=\"#naming\" id=\"naming\" name=\"naming\">Naming</a>\n\nThis subsection was moved to the [corresponding section][name] of the Go guidelines document.\n\n### <a href=\"#testing\" id=\"testing\" name=\"testing\">Testing</a>\n\nThis subsection was moved to the [corresponding section][test] of the Go guidelines document.\n\n### <a href=\"#recommended-reading\" id=\"recommended-reading\" name=\"recommended-reading\">Recommended Reading</a>\n\nThis subsection was moved to the [corresponding section][read] of the Go guidelines document.\n\n## <a href=\"#markdown\" id=\"markdown\" name=\"markdown\">Markdown</a>\n\nThis section was moved to [its own document][md].\n\n## <a href=\"#shell-scripting\" id=\"shell-scripting\" name=\"shell-scripting\">Shell Scripting</a>\n\nThis section was moved to [its own document][sh].\n\n### <a href=\"#shell-conditionals\" id=\"shell-conditionals\" name=\"shell-conditionals\">Shell Conditionals</a>\n\nThis subsection was moved to the [corresponding section][cond] of the Shell guidelines document.\n\n## <a href=\"#text-including-comments\" id=\"text-including-comments\" name=\"text-including-comments\">Text, Including Comments</a>\n\nThis section was moved to [its own document][txt].\n\n## <a href=\"#yaml\" id=\"yaml\" name=\"yaml\">YAML</a>\n\nThis section was moved to [its own document][yaml].\n\n[cmnt]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md#commenting\n[code]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md#code\n[cond]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Shell.md#shell-conditionals\n[fmt]:  https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md#formatting\n[git]:  https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Git.md\n[go]:   https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md\n[md]:   https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Markdown.md\n[name]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md#naming\n[read]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md#recommended-reading\n[repo]: https://github.com/AdguardTeam/CodeGuidelines\n[sh]:   https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Shell.md\n[test]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md#testing\n[txt]:  https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Text.md\n[yaml]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/YAML.md\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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    <program>  Copyright (C) <year>  <name of author>\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<https://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<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "Makefile",
    "content": "# Keep the Makefile POSIX-compliant.  We currently allow hyphens in target\n# names, but that may change in the future.\n#\n# See https://pubs.opengroup.org/onlinepubs/9799919799/utilities/make.html.\n.POSIX:\n\n# This comment is used to simplify checking local copies of the Makefile.  Bump\n# this number every time a significant change is made to this Makefile.\n#\n# AdGuard-Project-Version: 12\n\n# Don't name these macros \"GO\" etc., because GNU Make apparently makes them\n# exported environment variables with the literal value of \"${GO:-go}\" and so\n# on, which is not what we need.  Use a dot in the name to make sure that users\n# don't have an environment variable with the same name.\n#\n# See https://unix.stackexchange.com/q/646255/105635.\nGO.MACRO = $${GO:-go}\nVERBOSE.MACRO = $${VERBOSE:-0}\n\nCHANNEL = development\nCLIENT_DIR = client\nDEPLOY_SCRIPT_PATH = not/a/real/path\nDIST_DIR = dist\nGOAMD64 = v1\nGOPROXY = https://proxy.golang.org|direct\nGOTELEMETRY = off\nGOTOOLCHAIN = go1.26.1\nGPG_KEY = devteam@adguard.com\nGPG_KEY_PASSPHRASE = not-a-real-password\nNPM = npm\nNPM_FLAGS = --prefix $(CLIENT_DIR)\nNPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress\nRACE = 0\nREVISION = $${REVISION:-$$(git rev-parse --short HEAD)}\nSIGN = 1\nSIGNER_API_KEY = not-a-real-key\nVERSION = v0.0.0\n\nNEXTAPI = 0\n\n# Macros for the build-release target.  If FRONTEND_PREBUILT is 0, the default,\n# the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands into\n# BUILD_RELEASE_DEPS_0, and so both frontend and backend dependencies are\n# fetched and the frontend is built.  Otherwise, if FRONTEND_PREBUILT is 1, only\n# backend dependencies are fetched and the frontend isn't rebuilt.\n#\n# TODO(a.garipov): We could probably do that from .../build-release.sh, but that\n# would mean either calling make from inside make or duplicating commands in two\n# places, both of which don't seem to me like nice solutions.\nFRONTEND_PREBUILT = 0\nBUILD_RELEASE_DEPS_0 = deps js-build\nBUILD_RELEASE_DEPS_1 = go-deps\n\n# TODO(f.setrakov): Remove the bin directory from the paths, as it is no longer\n# needed.\nENV = env \\\n\tCHANNEL='$(CHANNEL)' \\\n\tDEPLOY_SCRIPT_PATH='$(DEPLOY_SCRIPT_PATH)' \\\n\tDIST_DIR='$(DIST_DIR)' \\\n\tGO=\"$(GO.MACRO)\" \\\n\tGOAMD64='$(GOAMD64)' \\\n\tGOPROXY='$(GOPROXY)' \\\n\tGOTELEMETRY='$(GOTELEMETRY)' \\\n\tGOTOOLCHAIN='$(GOTOOLCHAIN)' \\\n\tGPG_KEY='$(GPG_KEY)' \\\n\tGPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)' \\\n\tNEXTAPI='$(NEXTAPI)' \\\n\tPATH=\"$${PWD}/bin:$$(\"$(GO.MACRO)\" env GOPATH)/bin:$${PATH}\" \\\n\tRACE='$(RACE)' \\\n\tREVISION=\"$(REVISION)\" \\\n\tSIGN='$(SIGN)' \\\n\tSIGNER_API_KEY='$(SIGNER_API_KEY)' \\\n\tVERBOSE=\"$(VERBOSE.MACRO)\" \\\n\tVERSION=\"$(VERSION)\" \\\n\n# Keep the line above blank.\n\nENV_MISC = env \\\n\tPATH=\"$${PWD}/bin:$$(\"$(GO.MACRO)\" env GOPATH)/bin:$${PATH}\" \\\n\tVERBOSE=\"$(VERBOSE.MACRO)\" \\\n\n# Keep the line above blank.\n\n# Keep this target first, so that a naked make invocation triggers a full build.\n.PHONY: build\nbuild: deps quick-build\n\n.PHONY: init\ninit: ; git config core.hooksPath ./scripts/hooks\n\n.PHONY: quick-build\nquick-build: js-build go-build\n\n.PHONY: deps lint test\ndeps: js-deps go-deps\nlint: js-lint go-lint\ntest: js-test go-test\n\n# Here and below, keep $(SHELL) in quotes, because on Windows this will expand\n# to something like \"C:/Program Files/Git/usr/bin/sh.exe\".\n.PHONY: build-docker\nbuild-docker: ; $(ENV) \"$(SHELL)\" ./scripts/make/build-docker.sh\n\n.PHONY: build-release\nbuild-release: $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT))\n\t$(ENV) \"$(SHELL)\" ./scripts/make/build-release.sh\n\n.PHONY: js-build js-deps js-typecheck js-lint js-test js-test-e2e\njs-build:     ; $(NPM) $(NPM_FLAGS) run build-prod\njs-deps:      ; $(NPM) $(NPM_INSTALL_FLAGS) ci\njs-typecheck: ; $(NPM) $(NPM_FLAGS) run typecheck\njs-lint:      ; $(NPM) $(NPM_FLAGS) run lint\njs-test:      ; $(NPM) $(NPM_FLAGS) run test\njs-test-e2e:  ; $(NPM) $(NPM_FLAGS) run test:e2e\n\n# TODO(a.garipov): Think about making RACE='1' the default for all targets.\n.PHONY: go-bench go-build go-deps go-env go-fuzz go-lint go-test go-upd-tools\ngo-bench:     ; $(ENV)          \"$(SHELL)\"    ./scripts/make/go-bench.sh\ngo-build:     ; $(ENV)          \"$(SHELL)\"    ./scripts/make/go-build.sh\ngo-deps:      ; $(ENV)          \"$(SHELL)\"    ./scripts/make/go-deps.sh\ngo-env:       ; $(ENV)          \"$(GO.MACRO)\" env\ngo-fuzz:      ; $(ENV)          \"$(SHELL)\"    ./scripts/make/go-fuzz.sh\ngo-lint:      ; $(ENV)          \"$(SHELL)\"    ./scripts/make/go-lint.sh\ngo-test:      ; $(ENV) RACE='1' \"$(SHELL)\"    ./scripts/make/go-test.sh\ngo-upd-tools: ; $(ENV)          \"$(SHELL)\"    ./scripts/make/go-upd-tools.sh\n\n.PHONY: go-check\ngo-check: go-lint go-test\n\n# A quick check to make sure that all operating systems relevant to the\n# development of the project can be typechecked and built successfully.\n#\n# NOTE: It is also important to check on both 32- and 64-bit systems.\n.PHONY: go-os-check\ngo-os-check:\n\t$(ENV) GOOS='darwin'  \"$(GO.MACRO)\" vet ./...\n\t$(ENV) GOOS='freebsd' \"$(GO.MACRO)\" vet ./...\n\t$(ENV) GOOS='openbsd' \"$(GO.MACRO)\" vet ./...\n\t$(ENV) GOOS='windows' \"$(GO.MACRO)\" vet ./...\n\n\t$(ENV) GOARCH='amd64' GOOS='linux' \"$(GO.MACRO)\" vet ./...\n\t$(ENV) GOARCH='386'   GOOS='linux' \"$(GO.MACRO)\" vet ./...\n\n.PHONY: txt-lint\ntxt-lint: ; $(ENV) \"$(SHELL)\" ./scripts/make/txt-lint.sh\n\n.PHONY: md-lint sh-lint\nmd-lint: ; $(ENV_MISC) \"$(SHELL)\" ./scripts/make/md-lint.sh\nsh-lint: ; $(ENV_MISC) \"$(SHELL)\" ./scripts/make/sh-lint.sh\n\n# TODO(a.garipov):  Re-add openapi-lint.\n"
  },
  {
    "path": "README.md",
    "content": "&nbsp;\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"doc/adguard_home_darkmode.svg\">\n    <img alt=\"AdGuard Home\" src=\"doc/adguard_home_lightmode.svg\" width=\"300px\">\n  </picture>\n</p>\n<h3 align=\"center\">Privacy protection center for you and your devices</h3>\n<p align=\"center\">\n  Free and open source, powerful network-wide ads & trackers blocking DNS server.\n</p>\n<p align=\"center\">\n  <a href=\"https://adguard.com/\">AdGuard.com</a> |\n  <a href=\"https://github.com/AdguardTeam/AdGuardHome/wiki\">Wiki</a> |\n  <a href=\"https://reddit.com/r/Adguard\">Reddit</a> |\n  <a href=\"https://twitter.com/AdGuard\">Twitter</a> |\n  <a href=\"https://t.me/adguard_en\">Telegram</a>\n  <br/><br/>\n  <a href=\"https://codecov.io/github/AdguardTeam/AdGuardHome?branch=master\">\n    <img src=\"https://img.shields.io/codecov/c/github/AdguardTeam/AdGuardHome/master.svg\" alt=\"Code Coverage\"/>\n  </a>\n  <a href=\"https://goreportcard.com/report/AdguardTeam/AdGuardHome\">\n    <img src=\"https://goreportcard.com/badge/github.com/AdguardTeam/AdGuardHome\" alt=\"Go Report Card\"/>\n  </a>\n  <a href=\"https://hub.docker.com/r/adguard/adguardhome\">\n    <img alt=\"Docker Pulls\" src=\"https://img.shields.io/docker/pulls/adguard/adguardhome.svg?maxAge=604800\"/>\n  </a>\n  <br/>\n  <a href=\"https://github.com/AdguardTeam/AdGuardHome/releases\">\n    <img src=\"https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg\" alt=\"Latest release\"/>\n  </a>\n  <a href=\"https://snapcraft.io/adguard-home\">\n    <img alt=\"adguard-home\" src=\"https://snapcraft.io/adguard-home/badge.svg\"/>\n  </a>\n</p>\n<br/>\n<p align=\"center\">\n  <img src=\"https://cdn.adtidy.org/public/Adguard/Common/adguard_home.gif\" width=\"800\"/>\n</p>\n<hr/>\n\nAdGuard Home is a network-wide software for blocking ads and tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that.\n\nIt operates as a DNS server that re-routes tracking domains to a “black hole”, thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS] servers, and both share a lot of code.\n\n[AdGuard DNS]: https://adguard-dns.io/\n\n- [Getting Started](#getting-started)\n    - [Automated install (Linux/Unix/MacOS/FreeBSD/OpenBSD)](#automated-install-linux-and-mac)\n    - [Alternative methods](#alternative-methods)\n    - [Guides](#guides)\n    - [API](#api)\n- [Comparing AdGuard Home to other solutions](#comparison)\n    - [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns)\n    - [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)\n    - [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)\n    - [Known limitations](#comparison-limitations)\n- [How to build from source](#how-to-build)\n    - [Prerequisites](#prerequisites)\n    - [Building](#building)\n- [Contributing](#contributing)\n    - [Test unstable versions](#test-unstable-versions)\n    - [Reporting issues](#reporting-issues)\n    - [Help with translations](#translate)\n    - [Other](#help-other)\n- [Projects that use AdGuard Home](#uses)\n- [Acknowledgments](#acknowledgments)\n- [Privacy](#privacy)\n\n## <a href=\"#getting-started\" id=\"getting-started\" name=\"getting-started\">Getting Started</a>\n\n### <a href=\"#automated-install-linux-and-mac\" id=\"automated-install-linux-and-mac\" name=\"automated-install-linux-and-mac\">Automated install (Linux/Unix/MacOS/FreeBSD/OpenBSD)</a>\n\nTo install with `curl` run the following command:\n\n```sh\ncurl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v\n```\n\nTo install with `wget` run the following command:\n\n```sh\nwget --no-verbose -O - https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v\n```\n\nTo install with `fetch` run the following command:\n\n```sh\nfetch -o - https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v\n```\n\nThe script also accepts some options:\n\n- `-c <channel>` to use specified channel;\n- `-r` to reinstall AdGuard Home;\n- `-u` to uninstall AdGuard Home;\n- `-v` for verbose output.\n\nNote that options `-r` and `-u` are mutually exclusive.\n\n### <a href=\"#alternative-methods\" id=\"alternative-methods\" name=\"alternative-methods\">Alternative methods</a>\n\n#### <a href=\"#manual-installation\" id=\"manual-installation\" name=\"manual-installation\">Manual installation</a>\n\nPlease read the **[Getting Started][wiki-start]** article on our Wiki to learn how to install AdGuard Home manually, and how to configure your devices to use it.\n\n#### <a href=\"#docker\" id=\"docker\" name=\"docker\">Docker</a>\n\nYou can use our official Docker image on [Docker Hub].\n\n#### <a href=\"#snap-store\" id=\"snap-store\" name=\"snap-store\">Snap Store</a>\n\nIf you're running **Linux,** there's a secure and easy way to install AdGuard Home: get it from the [Snap Store].\n\n[Docker Hub]: https://hub.docker.com/r/adguard/adguardhome\n[Snap Store]: https://snapcraft.io/adguard-home\n[wiki-start]: https://adguard-dns.io/kb/adguard-home/getting-started/\n\n### <a href=\"#guides\" id=\"guides\" name=\"guides\">Guides</a>\n\nSee our [Wiki][wiki].\n\n[wiki]: https://github.com/AdguardTeam/AdGuardHome/wiki\n\n### <a href=\"#api\" id=\"api\" name=\"api\">API</a>\n\nIf you want to integrate with AdGuard Home, you can use our [REST API][openapi]. Alternatively, you can use this [python client][pyclient], which is used to build the [AdGuard Home Hass.io Add-on][hassio].\n\n[hassio]:   https://www.home-assistant.io/integrations/adguard/\n[openapi]:  https://github.com/AdguardTeam/AdGuardHome/tree/master/openapi\n[pyclient]: https://pypi.org/project/adguardhome/\n\n## <a href=\"#comparison\" id=\"comparison\" name=\"comparison\">Comparing AdGuard Home to other solutions</a>\n\n### <a href=\"#comparison-adguard-dns\" id=\"comparison-adguard-dns\" name=\"comparison-adguard-dns\">How is this different from public AdGuard DNS servers?</a>\n\nRunning your own AdGuard Home server allows you to do much more than using a public DNS server. It's a completely different level. See for yourself:\n\n- Choose what exactly the server blocks and permits.\n\n- Monitor your network activity.\n\n- Add your own custom filtering rules.\n\n- **Most importantly, it's your own server, and you are the only one who's in control.**\n\n### <a href=\"#comparison-pi-hole\" id=\"comparison-pi-hole\" name=\"comparison-pi-hole\">How does AdGuard Home compare to Pi-Hole</a>\n\nAt this point, AdGuard Home has a lot in common with Pi-Hole. Both block ads and trackers using the so-called “DNS sinkholing” method and both allow customizing what's blocked.\n\n> [!NOTE]\n> We're not going to stop here. DNS sinkholing is not a bad starting point, but this is just the beginning.\n\nAdGuard Home provides a lot of features out-of-the-box with no need to install and configure additional software. We want it to be simple to the point when even casual users can set it up with minimal effort.\n\n> [!NOTE]\n> Some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature.\n\n| Feature                                                                 | AdGuard&nbsp;Home | Pi-Hole                                                   |\n|-------------------------------------------------------------------------|-------------------|-----------------------------------------------------------|\n| Blocking ads and trackers                                               | ✅                | ✅                                                        |\n| Customizing blocklists                                                  | ✅                | ✅                                                        |\n| Built-in DHCP server                                                    | ✅                | ✅                                                        |\n| HTTPS for the Admin interface                                           | ✅                | Kind of, but you'll need to manually configure lighttpd   |\n| Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅                | ❌ (requires additional software)                         |\n| Cross-platform                                                          | ✅                | ❌ (not natively, only via Docker)                        |\n| Running as a DNS-over-HTTPS or DNS-over-TLS server                      | ✅                | ❌ (requires additional software)                         |\n| Blocking phishing and malware domains                                   | ✅                | ❌ (requires non-default blocklists)                      |\n| Parental control (blocking adult domains)                               | ✅                | ❌ (requires non-default blocklists)                      |\n| Force Safe search on search engines                                     | ✅                | ❌                                                        |\n| Per-client (device) configuration                                       | ✅                | ✅                                                        |\n| Access settings (choose who can use AGH DNS)                            | ✅                | ❌                                                        |\n| Running [without root privileges][wiki-noroot]                          | ✅                | ❌                                                        |\n\n[wiki-noroot]: https://adguard-dns.io/kb/adguard-home/getting-started/#running-without-superuser\n\n### <a href=\"#comparison-adblock\" id=\"comparison-adblock\" name=\"comparison-adblock\">How does AdGuard Home compare to traditional ad blockers</a>\n\nIt depends.\n\nDNS sinkholing is capable of blocking a big percentage of ads, but it lacks the flexibility and the power of traditional ad blockers. You can get a good impression about the difference between these methods by reading [this article][blog-adaway], which compares AdGuard for Android (a traditional ad blocker) to hosts-level ad blockers (which are almost identical to DNS-based blockers in their capabilities). This level of protection is enough for some users.\n\nAdditionally, using a DNS-based blocker can help to block ads, tracking and analytics requests on other types of devices, such as SmartTVs, smart speakers or other kinds of IoT devices (on which you can't install traditional ad blockers).\n\n### <a href=\"#comparison-limitations\" id=\"comparison-limitations\" name=\"comparison-limitations\">Known limitations</a>\n\nHere are some examples of what cannot be blocked by a DNS-level blocker:\n\n- YouTube, Twitch ads;\n\n- Facebook, Twitter, Instagram sponsored posts.\n\nEssentially, any advertising that shares a domain with content cannot be blocked by a DNS-level blocker.\n\nIs there a chance to handle this in the future?  DNS will never be enough to do this. Our only option is to use a content blocking proxy like what we do in the standalone AdGuard applications. We're [going to bring][issue-1228] this feature support to AdGuard Home in the future. Unfortunately, even in this case, there still will be cases when this won't be enough or would require quite a complicated configuration.\n\n[blog-adaway]: https://adguard.com/blog/adguard-vs-adaway-dns66.html\n[issue-1228]:  https://github.com/AdguardTeam/AdGuardHome/issues/1228\n\n## <a href=\"#how-to-build\" id=\"how-to-build\" name=\"how-to-build\">How to build from source</a>\n\n### <a href=\"#prerequisites\" id=\"prerequisites\" name=\"prerequisites\">Prerequisites</a>\n\nRun `make init` to prepare the development environment.\n\nYou will need this to build AdGuard Home:\n\n- [Go](https://golang.org/dl/) v1.25 or later;\n- [Node.js](https://nodejs.org/en/download/) v24.10.0 or later;\n- [npm](https://www.npmjs.com/) v10.8 or later;\n\n### <a href=\"#building\" id=\"building\" name=\"building\">Building</a>\n\nOpen your terminal and execute these commands:\n\n```sh\ngit clone https://github.com/AdguardTeam/AdGuardHome\ncd AdGuardHome\nmake\n```\n\n> [!WARNING]\n> The non-standard `-j` flag is currently not supported, so building with `make -j 4` or setting your `MAKEFLAGS` to include, for example, `-j 4` is likely to break the build. If you do have your `MAKEFLAGS` set to that, and you don't want to change it, you can override it by running `make -j 1`.\n\nCheck the [`Makefile`][src-makefile] to learn about other commands.\n\n#### <a href=\"#building-cross\" id=\"building-cross\" name=\"building-cross\">Building for a different platform</a>\n\nYou can build AdGuard Home for any OS/ARCH that Go supports. In order to do this, specify `GOOS` and `GOARCH` environment variables as macros when running `make`.\n\nFor example:\n\n```sh\nenv GOOS='linux' GOARCH='arm64' make\n```\n\nor:\n\n```sh\nmake GOOS='linux' GOARCH='arm64'\n```\n\n#### <a href=\"#preparing-releases\" id=\"preparing-releases\" name=\"preparing-releases\">Preparing releases</a>\n\nYou'll need [`snapcraft`] to prepare a release build. Once installed, run the following command:\n\n```sh\nmake build-release CHANNEL='...' VERSION='...'\n```\n\nSee the [`build-release` target documentation][targ-release].\n\n#### <a href=\"#docker-image\" id=\"docker-image\" name=\"docker-image\">Docker image</a>\n\nRun `make build-docker` to build the Docker image locally (the one that we publish to DockerHub). Please note, that we're using [Docker Buildx][buildx] to build our official image.\n\nYou may need to prepare before using these builds:\n\n- (Linux-only) Install Qemu:\n\n  ```sh\n  docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes\n  ```\n\n- Prepare the builder:\n\n  ```sh\n  docker buildx create --name buildx-builder --driver docker-container --use\n  ```\n\nSee the [`build-docker` target documentation][targ-docker].\n\n#### <a href=\"#debugging-the-frontend\" id=\"debugging-the-frontend\" name=\"debugging-the-frontend\">Debugging the frontend</a>\n\nWhen you need to debug the frontend without recompiling the production version every time, for example to check how your labels would look on a form, you can run the frontend build a development environment.\n\n1. In a separate terminal, run:\n\n   ```sh\n   ( cd ./client/ && env NODE_ENV='development' npm run watch )\n   ```\n\n2. Run your `AdGuardHome` binary with the `--local-frontend` flag, which instructs AdGuard Home to ignore the built-in frontend files and use those from the `./build/` directory.\n\n3. Now any changes you make in the `./client/` directory should be recompiled and become available on the web UI. Make sure that you disable the browser cache to make sure that you actually get the recompiled version.\n\n[`snapcraft`]:  https://snapcraft.io/\n[buildx]:       https://docs.docker.com/buildx/working-with-buildx/\n[src-makefile]: https://github.com/AdguardTeam/AdGuardHome/blob/master/Makefile\n[targ-docker]:  https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-dockersh-build-a-multi-architecture-docker-image\n[targ-release]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-releasesh-build-a-release-for-all-platforms\n\n#### <a href=\"#e2e-frontend-tests\" id=\"e2e-frontend-tests\" name=\"e2e-frontend-tests\">End-to-End (E2E) Frontend Tests</a>\n\nAdGuard Home uses [Playwright](https://playwright.dev) for E2E testing. Tests are located in `tests/e2e`.\n\n**Running Tests:**\n- `npm run test:e2e` – run all tests (headless).\n- `npm run test:e2e:interactive` – run tests interactively.\n- `npm run test:e2e:debug` – run tests in debug mode.\n- `npm run test:e2e:codegen` – generate new test code.\n\n**Setup:**\n1. Run `npm install` to install dependencies.\n2. Run `npx playwright install` to set up required browsers.\n\n> **Warning:** Playwright will download and install its own browser binaries for testing, which may differ from the browsers installed on your system.\n\n## <a href=\"#contributing\" id=\"contributing\" name=\"contributing\">Contributing</a>\n\nYou are welcome to fork this repository, make your changes and [submit a pull request][pr]. Please make sure you follow our [code guidelines][guide] though.\n\nPlease note that we don't expect people to contribute to both UI and backend parts of the program simultaneously. Ideally, the backend part is implemented first, i.e. configuration, API, and the functionality itself. The UI part can be implemented later in a different pull request by a different person.\n\n[guide]: https://github.com/AdguardTeam/CodeGuidelines/\n[pr]:    https://github.com/AdguardTeam/AdGuardHome/pulls\n\n### <a href=\"#test-unstable-versions\" id=\"test-unstable-versions\" name=\"test-unstable-versions\">Test unstable versions</a>\n\nThere are two update channels that you can use:\n\n- `beta`: beta versions of AdGuard Home. More or less stable versions, usually released every two weeks or more often.\n\n- `edge`: the newest version of AdGuard Home from the development branch. New updates are pushed to this channel daily.\n\nThere are three options how you can install an unstable version:\n\n1. [Snap Store]: look for the `beta` and `edge` channels.\n\n2. [Docker Hub]: look for the `beta` and `edge` tags.\n\n3. Standalone builds. Use the automated installation script or look for the available builds [on the Wiki][wiki-platf].\n\n   Script to install a beta version:\n\n   ```sh\n   curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c beta\n   ```\n\n   Script to install an edge version:\n\n   ```sh\n   curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge\n   ```\n\n[wiki-platf]: https://github.com/AdguardTeam/AdGuardHome/wiki/Platforms\n\n### <a href=\"#reporting-issues\" id=\"reporting-issues\" name=\"reporting-issues\">Report issues</a>\n\nIf you run into any problem or have a suggestion, head to [this page][iss] and click on the “New issue” button. Please follow the instructions in the issue form carefully and don't forget to start by searching for duplicates.\n\n[iss]: https://github.com/AdguardTeam/AdGuardHome/issues\n\n### <a href=\"#translate\" id=\"translate\" name=\"translate\">Help with translations</a>\n\nIf you want to help with AdGuard Home translations, please learn more about translating AdGuard products [in our Knowledge Base][kb-trans]. You can contribute to the [AdGuardHome project on CrowdIn][crowdin].\n\n[crowdin]:  https://crowdin.com/project/adguard-applications/en#/adguard-home\n[kb-trans]: https://kb.adguard.com/en/general/adguard-translations\n\n### <a href=\"#help-other\" id=\"help-other\" name=\"help-other\">Other</a>\n\nAnother way you can contribute is by [looking for issues][iss-help] marked as `help wanted`, asking if the issue is up for grabs, and sending a PR fixing the bug or implementing the feature.\n\n[iss-help]: https://github.com/AdguardTeam/AdGuardHome/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22\n\n## <a href=\"#uses\" id=\"uses\" name=\"uses\">Projects that use AdGuard Home</a>\n\nPlease note that these projects are not affiliated with AdGuard, but are made by third-party developers and fans.\n\n- [AdGuard Home Remote](https://apps.apple.com/app/apple-store/id1543143740): iOS app by [Joost](https://rocketscience-it.nl/).\n\n- [Python library](https://github.com/frenck/python-adguardhome) by [@frenck](https://github.com/frenck).\n\n- [Home Assistant add-on](https://github.com/hassio-addons/addon-adguard-home) by [@frenck](https://github.com/frenck).\n\n- [OpenWrt LUCI app](https://github.com/kongfl888/luci-app-adguardhome) by [@kongfl888](https://github.com/kongfl888) (originally by [@rufengsuixing](https://github.com/rufengsuixing)).\n\n- [AdGuardHome sync](https://github.com/bakito/adguardhome-sync) by [@bakito](https://github.com/bakito).\n\n- [Terminal-based, real-time traffic monitoring and statistics for your AdGuard Home instance](https://github.com/Lissy93/AdGuardian-Term) by [@Lissy93](https://github.com/Lissy93)\n\n- [AdGuard Home on GLInet routers](https://forum.gl-inet.com/t/adguardhome-on-gl-routers/10664) by [Gl-Inet](https://gl-inet.com/).\n\n- [Cloudron app](https://git.cloudron.io/cloudron/adguard-home-app) by [@gramakri](https://github.com/gramakri).\n\n- [Asuswrt-Merlin-AdGuardHome-Installer](https://github.com/jumpsmm7/Asuswrt-Merlin-AdGuardHome-Installer) by [@jumpsmm7](https://github.com/jumpsmm7) aka [@SomeWhereOverTheRainBow](https://www.snbforums.com/members/somewhereovertherainbow.64179/).\n\n- [Node.js library](https://github.com/Andrea055/AdguardHomeAPI) by [@Andrea055](https://github.com/Andrea055/).\n\n- [Browser Extension](https://github.com/satheshshiva/Adguard-Home-Browser-Ext) by [@satheshshiva](https://github.com/satheshshiva/).\n\n- [Zabbix Template for AdGuard Home](https://github.com/diasdmhub/AdGuard_Home_Zabbix_Template) by [@diasdmhub](https://github.com/diasdmhub).\n\n- [Chocolatey package](https://community.chocolatey.org/packages/adguardhome/) by [niks255](https://community.chocolatey.org/profiles/niks255).\n\n## <a href=\"#acknowledgments\" id=\"acknowledgments\" name=\"acknowledgments\">Acknowledgments</a>\n\nThis software wouldn't have been possible without:\n\n- [Go](https://golang.org/dl/) and its libraries:\n    - [gcache](https://github.com/bluele/gcache)\n    - [miekg's dns](https://github.com/miekg/dns)\n    - [go-yaml](https://github.com/go-yaml/yaml)\n    - [service](https://godoc.org/github.com/kardianos/service)\n    - [dnsproxy](https://github.com/AdguardTeam/dnsproxy)\n    - [urlfilter](https://github.com/AdguardTeam/urlfilter)\n- [Node.js](https://nodejs.org/) and its libraries:\n    - [React.js](https://reactjs.org)\n    - [Tabler](https://github.com/tabler/tabler)\n    - And many more Node.js packages.\n- [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)\n\nYou might have seen that [CoreDNS] was mentioned here before, but we've stopped using it in AdGuard Home.\n\nFor the full list of all Node.js packages in use, please take a look at [`client/package.json`][src-packagejson] file.\n\n[CoreDNS]:         https://coredns.io\n[src-packagejson]: https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json\n\n## <a href=\"#privacy\" id=\"privacy\" name=\"privacy\">Privacy</a>\n\nOur main idea is that you are the one, who should be in control of your data. So it is only natural, that AdGuard Home does not collect any usage statistics, and does not use any web services unless you configure it to do so. See also the [full privacy policy][privacy] with every bit that *could in theory be sent* by AdGuard Home is available.\n\n[privacy]: https://adguard.com/en/privacy/home.html\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting vulnerabilities\n\nPlease send your vulnerability reports to <security@adguard.com>.  To make sure that your report reaches us, please:\n\n1. Include the words “AdGuard Home” and “vulnerability” to the subject line as well as a short description of the vulnerability.  For example:\n\n   > AdGuard Home API vulnerability: possible XSS attack\n\n1. Make sure that the message body contains a clear description of the vulnerability.\n\nIf you have not received a reply to your email within 7 days, please make sure to follow up with us again at <security@adguard.com>.  Once again, make sure that the word “vulnerability” is in the subject line.\n"
  },
  {
    "path": "bamboo-specs/bamboo.yaml",
    "content": "---\n!include release.yaml\n\n---\n!include snapcraft.yaml\n\n---\n!include test.yaml\n"
  },
  {
    "path": "bamboo-specs/release.yaml",
    "content": "---\n'version': 2\n'plan':\n    'project-key': 'AGH'\n    'key': 'AGHBSNAPSPECS'\n    'name': 'AdGuard Home - Build and publish release'\n# Make sure to sync any changes with the branch overrides below.\n'variables':\n    # This variable is used to override Docker caching, for example to rerun a\n    # flaky test suite.\n    'cacheBuster': '0'\n    'channel': 'edge'\n    'dockerFrontend': 'adguard/home-js-builder:4.0'\n    'dockerGo': 'adguard/go-builder:1.26.1--1'\n\n'stages':\n  - 'Build frontend':\n        'manual': false\n        'final': false\n        'jobs':\n          - 'Build frontend'\n\n  - 'Make release':\n        'manual': false\n        'final': false\n        'jobs':\n          - 'Make release'\n\n  - 'Make and publish docker':\n        'manual': false\n        'final': false\n        'jobs':\n          - 'Make and publish docker'\n\n  - 'Publish to static storage':\n        'manual': false\n        'final': false\n        'jobs':\n          - 'Publish to static storage'\n\n  - 'Publish to GitHub Releases':\n        'manual': false\n        'final': false\n        'jobs':\n          - 'Publish to GitHub Releases'\n\n'Build frontend':\n    'artifacts':\n      - 'name': 'AdGuardHome frontend'\n        'pattern': 'build/**'\n        'shared': true\n        'required': true\n    'key': 'BF'\n    'other':\n        'clean-working-dir': true\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n                - |-\n                  #!/bin/sh\n\n                  set -e -f -u -x\n\n                  docker info\n\n                  docker build \\\n                      --build-arg \"BASE_IMAGE=${bamboo.dockerFrontend}\" \\\n                      --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                      --output '.' \\\n                      --progress 'plain' \\\n                      --target 'builder-exporter' \\\n                      -f ./docker/frontend.Dockerfile \\\n                      .\n\n'Make release':\n    'artifact-subscriptions':\n      - 'artifact': 'AdGuardHome frontend'\n    # TODO(a.garipov): Use more fine-grained artifact rules.\n    'artifacts':\n      - 'name': 'AdGuardHome dists'\n        'pattern': 'dist/**'\n        'shared': true\n        'required': true\n    'key': 'MR'\n    'other':\n        'clean-working-dir': true\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'checkout':\n            'repository': 'bamboo-deploy-publisher'\n            # The paths are always relative to the working directory.\n            'path': 'bamboo-deploy-publisher'\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                # Explicitly checkout the revision that we need.\n                git checkout \"${bamboo.repository.revision.number}\"\n\n                version=\"$(env CHANNEL=${bamboo_channel} sh ./scripts/make/version.sh)\"\n                readonly version\n\n                docker info\n\n                docker build \\\n                    --build-arg \"BASE_IMAGE=${bamboo_dockerGo}\" \\\n                    --build-arg \"BRANCH=${bamboo_planRepository_branchName}\" \\\n                    --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                    --build-arg \"CHANNEL=${bamboo_channel}\" \\\n                    --build-arg \"DEPLOY_SCRIPT_PATH=./bamboo-deploy-publisher/deploy.sh\" \\\n                    --build-arg \"GPG_SECRET_KEY=${bamboo_gpgSecretKeyPart1}${bamboo_gpgSecretKeyPart2}\" \\\n                    --build-arg \"GPG_KEY_PASSPHRASE=${bamboo_gpgPassword}\" \\\n                    --build-arg \"REVISION=${bamboo_repository_revision_number}\" \\\n                    --build-arg \"SIGN=1\" \\\n                    --build-arg \"SIGNER_API_KEY=${bamboo_adguardHomeWinSignerSecretApiKey}\" \\\n                    --build-arg \"SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)\" \\\n                    --build-arg \"VERSION=$version\" \\\n                    --output '.' \\\n                    --progress 'plain' \\\n                    --target 'builder-exporter' \\\n                    -f ./docker/ci.Dockerfile \\\n                    .\n\n'Make and publish docker':\n    'key': 'MPD'\n    'other':\n        'clean-working-dir': true\n    'final-tasks':\n      - 'clean'\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'environment':\n                DOCKER_CLI_EXPERIMENTAL=enabled\n            'scripts':\n              - |\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                # Install Qemu, create builder.\n                docker version -f '{{ .Server.Experimental }}'\n                docker buildx rm buildx-builder || :\n                docker buildx create \\\n                    --name buildx-builder \\\n                    --driver docker-container \\\n                    --use\n                docker buildx inspect --bootstrap\n\n                # Login to DockerHub.\n                docker login \\\n                    -u=\"${bamboo.dockerHubUsername}\" \\\n                    -p=\"${bamboo.dockerHubPassword}\"\n\n                # Boot the builder.\n                docker buildx inspect --bootstrap\n\n                # Print Docker info.\n                docker info\n                docker buildx version\n\n                # Prepare and push the build.\n                env \\\n                    CHANNEL=\"${bamboo.channel}\" \\\n                    REVISION=\"${bamboo.repository.revision.number}\" \\\n                    DIST_DIR='dist' \\\n                    DOCKER_IMAGE_NAME='adguard/adguardhome' \\\n                    DOCKER_PUSH='1' \\\n                    VERBOSE='1' \\\n                    sh ./scripts/make/build-docker.sh\n\n'Publish to static storage':\n    'key': 'PUB'\n    'other':\n        'clean-working-dir': true\n    'final-tasks':\n      - 'clean'\n    'tasks':\n      - 'clean'\n      - 'checkout':\n            'repository': 'bamboo-deploy-publisher'\n            'path': 'bamboo-deploy-publisher'\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                cd ./dist/\n\n                CHANNEL=\"${bamboo.channel}\"\n                export CHANNEL\n\n                ../bamboo-deploy-publisher/deploy.sh adguard-home-\"$CHANNEL\"\n\n'Publish to GitHub Releases':\n    'key': 'PTGR'\n    'other':\n        'clean-working-dir': true\n    'final-tasks':\n      - 'clean'\n    'tasks':\n      - 'clean'\n      - 'checkout':\n            'repository': 'bamboo-deploy-publisher'\n            'path': 'bamboo-deploy-publisher'\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                channel=\"${bamboo.channel}\"\n                readonly channel\n\n                if [ \"$channel\" != 'release' ] && [ \"${channel}\" != 'beta' ]\n                then\n                        echo \"don't publish to GitHub Releases for this channel\"\n\n                        exit 0\n                fi\n\n                cd ./dist/\n\n                env\\\n                        GITHUB_TOKEN=\"${bamboo.githubPublicRepoPassword}\"\\\n                        ../bamboo-deploy-publisher/deploy.sh adguard-home-github\n\n'triggers':\n    # Don't use minute values that end with a zero or a five as these are often\n    # used in CI and so resources during these minutes can be quite busy.\n  - 'cron': '0 42 13 ? * MON-FRI *'\n'branches':\n    'create': 'manually'\n    'delete':\n        'after-deleted-days': 1\n        'after-inactive-days': 30\n    'integration':\n        'push-on-success': false\n        'merge-from': 'AdGuard Home   - Build and publish release'\n    'link-to-jira': true\n\n'notifications':\n  - 'events':\n      - 'plan-completed'\n    'recipients':\n      - 'webhook':\n            'name': 'Build webhook'\n            'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo?channel=adguard-qa-dns-builds'\n\n'labels': []\n'other':\n    'concurrent-build-plugin': 'system-default'\n\n'branch-overrides':\n    # beta-vX.Y branches are the branches into which the commits that are needed\n    # to release a new patch version are initially cherry-picked.\n  - '^beta-v[0-9]+\\.[0-9]+':\n        # Build betas on release branches manually.\n        'triggers': []\n        # Set the default release channel on the release branch to beta, as we may\n        # need to build a few of these.\n        'variables':\n            'channel': 'beta'\n            'dockerFrontend': 'adguard/home-js-builder:4.0'\n            'dockerGo': 'adguard/go-builder:1.26.1--1'\n    # release-vX.Y.Z branches are the branches from which the actual final\n    # release is built.\n  - '^release-v[0-9]+\\.[0-9]+\\.[0-9]+':\n        # Disable integration branches for release branches.\n        'branch-config':\n            'integration':\n                'push-on-success': false\n                'merge-from': 'beta-v0.107'\n        # Build final releases on release branches manually.\n        'triggers': []\n        # Set the default release channel on the final branch to release, as these\n        # are the ones that actually get released.\n        'variables':\n            'channel': 'release'\n            'dockerFrontend': 'adguard/home-js-builder:4.0'\n            'dockerGo': 'adguard/go-builder:1.26.1--1'\n"
  },
  {
    "path": "bamboo-specs/snapcraft.yaml",
    "content": "---\n# This part of the release build is separate from the one described in\n# release.yaml, because the Snapcraft infrastructure is brittle, and timeouts\n# during logins and uploads often lead to release blocking.\n'version': 2\n'plan':\n    'project-key': 'AGH'\n    'key': 'AGHSNAP'\n    'name': 'AdGuard Home - Build and publish Snapcraft release'\n# Make sure to sync any changes with the branch overrides below.\n'variables':\n    # This variable is used to override Docker caching, for example to rerun a\n    # flaky test suite.\n    'cacheBuster': '0'\n    'channel': 'edge'\n    'dockerSnap': 'adguard/snap-builder:2.1'\n    'snapcraftChannel': 'edge'\n\n'stages':\n  - 'Build packages':\n        'manual': false\n        'final': false\n        'jobs':\n          - 'Build packages'\n\n  - 'Publish to Snapstore':\n        'manual': false\n        'final': false\n        'jobs':\n          - 'Publish to Snapstore'\n\n'Build packages':\n    'artifacts':\n      - 'name': 'i386_snap'\n        'pattern': 'AdGuardHome_i386.snap'\n        'shared': true\n        'required': true\n      - 'name': 'amd64_snap'\n        'pattern': 'AdGuardHome_amd64.snap'\n        'shared': true\n        'required': true\n      - 'name': 'armhf_snap'\n        'pattern': 'AdGuardHome_armhf.snap'\n        'shared': true\n        'required': true\n      - 'name': 'arm64_snap'\n        'pattern': 'AdGuardHome_arm64.snap'\n        'shared': true\n        'required': true\n    'key': 'BP'\n    'other':\n        'clean-working-dir': true\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n            - |\n              #!/bin/sh\n\n              set -e -f -u -x\n\n              docker info\n\n              docker build \\\n                  --build-arg \"BASE_IMAGE=${bamboo_dockerSnap}\" \\\n                  --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                  --build-arg \"CHANNEL=${bamboo_channel}\" \\\n                  --build-arg \"VERSION=${bamboo_buildNumber}\" \\\n                  --output '.' \\\n                  --progress 'plain' \\\n                  --target 'builder-exporter' \\\n                  -f ./docker/snapcraft.Dockerfile \\\n                  .\n\n'Publish to Snapstore':\n    'artifact-subscriptions':\n      - 'artifact': 'i386_snap'\n      - 'artifact': 'amd64_snap'\n      - 'artifact': 'armhf_snap'\n      - 'artifact': 'arm64_snap'\n    'key': 'PTS'\n    'other':\n        'clean-working-dir': true\n    'final-tasks':\n      - 'clean'\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                docker info\n\n                docker build \\\n                    --build-arg \"BASE_IMAGE=${bamboo_dockerSnap}\" \\\n                    --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                    --build-arg \"SNAPCRAFT_CHANNEL=${bamboo_snapcraftChannel}\" \\\n                    --build-arg \"SNAPCRAFT_STORE_CREDENTIALS=${bamboo_snapcraftMacaroonPassword}\" \\\n                    --build-arg \"VERSION=${bamboo_buildNumber}\" \\\n                    --output '.' \\\n                    --progress 'plain' \\\n                    --target 'publisher' \\\n                    -f ./docker/snapcraft.Dockerfile \\\n                    .\n\n'triggers':\n    # Don't use minute values that end with a zero or a five as these are often\n    # used in CI and so resources during these minutes can be quite busy.\n    #\n    # NOTE: The time is chosen to be exactly one hour after the main release\n    # build as defined as in release.yaml.\n  - 'cron': '0 42 14 ? * MON-FRI *'\n'branches':\n    'create': 'manually'\n    'delete':\n        'after-deleted-days': 1\n        'after-inactive-days': 30\n    'integration':\n        'push-on-success': false\n        'merge-from': 'AdGuard Home - Build and publish Snapcraft release'\n    'link-to-jira': true\n\n'notifications':\n  - 'events':\n      - 'plan-completed'\n    'recipients':\n      - 'webhook':\n            'name': 'Build webhook'\n            'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo?channel=adguard-qa-dns-builds'\n\n'labels': []\n'other':\n    'concurrent-build-plugin': 'system-default'\n\n'branch-overrides':\n    # beta-vX.Y branches are the branches into which the commits that are needed\n    # to release a new patch version are initially cherry-picked.\n  - '^beta-v[0-9]+\\.[0-9]+':\n        # Build betas on release branches manually.\n        'triggers': []\n        # Set the default release channel on the release branch to beta, as we may\n        # need to build a few of these.\n        'variables':\n            'channel': 'beta'\n            'dockerSnap': 'adguard/snap-builder:2.1'\n            'snapcraftChannel': 'beta'\n    # release-vX.Y.Z branches are the branches from which the actual final\n    # release is built.\n  - '^release-v[0-9]+\\.[0-9]+\\.[0-9]+':\n        # Disable integration branches for release branches.\n        'branch-config':\n            'integration':\n                'push-on-success': false\n                'merge-from': 'beta-v0.107'\n        # Build final releases on release branches manually.\n        'triggers': []\n        # Set the default release channel on the final branch to release, as these\n        # are the ones that actually get released.\n        'variables':\n            'channel': 'release'\n            'dockerSnap': 'adguard/snap-builder:2.1'\n            'snapcraftChannel': 'candidate'\n"
  },
  {
    "path": "bamboo-specs/test.yaml",
    "content": "---\n'version': 2\n'plan':\n    'project-key': 'AGH'\n    'key': 'AHBRTSPECS'\n    'name': 'AdGuard Home - Build and run tests'\n'variables':\n    # This variable is used to override Docker caching, for example to rerun a\n    # flaky test suite.\n    'cacheBuster': '0'\n    'channel': 'development'\n    'dockerFrontend': 'adguard/home-js-builder:4.0'\n    'dockerGo': 'adguard/go-builder:1.26.1--1'\n\n'stages':\n  - 'Tests':\n        'manual': false\n        'final': false\n        'jobs':\n          - 'Test frontend'\n          - 'Test backend'\n\n  - 'Frontend':\n        manual: false\n        final: false\n        jobs:\n          - 'Build frontend'\n\n  - 'Artifact':\n        manual: false\n        final: false\n        jobs:\n          - 'Artifact'\n\n  - 'E2E':\n      manual: false\n      final: false\n      jobs:\n        - 'Test e2e'\n\n'Test frontend':\n    'final-tasks':\n      - 'clean'\n    'key': 'JSTEST'\n    'other':\n        'clean-working-dir': true\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                docker info\n\n                docker build \\\n                    --build-arg \"BASE_IMAGE=${bamboo.dockerFrontend}\" \\\n                    --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                    --output '.' \\\n                    --progress 'plain' \\\n                    --target 'tester' \\\n                    -f ./docker/frontend.Dockerfile \\\n                    .\n\n'Test backend':\n    'final-tasks':\n      - 'test-parser':\n          # The default pattern, '**/test-reports/*.xml', works, so don't set\n          # the test-results property.\n          'type': 'junit'\n          'ignore-time': true\n      - 'clean'\n    'key': 'GOTEST'\n    'other':\n        'clean-working-dir': true\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                docker info\n\n                docker build \\\n                    --build-arg \"BASE_IMAGE=${bamboo_dockerGo}\" \\\n                    --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                    --output '.' \\\n                    --progress 'plain' \\\n                    --target 'tester-exporter' \\\n                    -f ./docker/ci.Dockerfile \\\n                    .\n\n                exit_code=\"$(cat ./test-reports/test-exit-code.txt)\"\n                readonly exit_code\n\n                exit \"$exit_code\"\n\n'Build frontend':\n    'artifacts':\n      - 'name': 'AdGuardHome frontend'\n        'pattern': 'build/**'\n        'shared': true\n        'required': true\n    'key': 'BF'\n    'other':\n         'clean-working-dir': true\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |-\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                docker info\n\n                docker build \\\n                    --build-arg \"BASE_IMAGE=${bamboo.dockerFrontend}\" \\\n                    --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                    --output '.' \\\n                    --progress 'plain' \\\n                    --target 'builder-exporter' \\\n                    -f ./docker/frontend.Dockerfile \\\n                    .\n\n'Artifact':\n    'artifact-subscriptions':\n      - 'artifact': 'AdGuardHome frontend'\n    'artifacts':\n      - 'name': 'AdGuardHome_windows_amd64'\n        'pattern': 'dist/AdGuardHome_windows_amd64.zip'\n        'shared': true\n        'required': true\n      - 'name': 'AdGuardHome_darwin_amd64'\n        'pattern': 'dist/AdGuardHome_darwin_amd64.zip'\n        'shared': true\n        'required': true\n      - 'name': 'AdGuardHome_linux_amd64'\n        'pattern': 'dist/AdGuardHome_linux_amd64.tar.gz'\n        'shared': true\n        'required': true\n    'key': 'ART'\n    'other':\n         'clean-working-dir': true\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |-\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                version=\"$(env CHANNEL=${bamboo_channel} sh ./scripts/make/version.sh)\"\n                readonly version\n\n                docker info\n\n                docker build \\\n                    --build-arg \"ARCH=amd64\" \\\n                    --build-arg \"BASE_IMAGE=${bamboo_dockerGo}\" \\\n                    --build-arg \"BRANCH=${bamboo_planRepository_branchName}\" \\\n                    --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                    --build-arg \"CHANNEL=${bamboo_channel}\" \\\n                    --build-arg \"OS=windows darwin linux\" \\\n                    --build-arg \"REVISION=${bamboo_repository_revision_number}\" \\\n                    --build-arg \"SIGN=0\" \\\n                    --build-arg \"SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)\" \\\n                    --build-arg \"VERSION=$version\" \\\n                    --output '.' \\\n                    --progress 'plain' \\\n                    --target 'builder-exporter' \\\n                    -f ./docker/ci.Dockerfile \\\n                    .\n\n'Test e2e':\n    'artifact-subscriptions':\n      - 'artifact': 'AdGuardHome_linux_amd64'\n      - 'artifact': 'AdGuardHome frontend'\n    'key': 'E2ETEST'\n    'other':\n        'clean-working-dir': true\n    'tasks':\n      - 'checkout':\n            'force-clean-build': true\n      - 'script':\n            'interpreter': 'SHELL'\n            'scripts':\n              - |\n                #!/bin/sh\n\n                set -e -f -u -x\n\n                tar -xzf dist/AdGuardHome_linux_amd64.tar.gz -C /tmp\n\n                mv /tmp/AdGuardHome/AdGuardHome ./AdGuardHome\n\n                docker info\n\n                docker build \\\n                    --build-arg \"BASE_IMAGE=${bamboo.dockerFrontend}\" \\\n                    --build-arg \"CACHE_BUSTER=${bamboo_cacheBuster}\" \\\n                    --output '.' \\\n                    --progress 'plain' \\\n                    --target 'e2etester' \\\n                    -f ./docker/frontend.Dockerfile \\\n                    .\n\n'branches':\n    'create': 'for-pull-request'\n    'delete':\n        'after-deleted-days': 1\n        'after-inactive-days': 5\n    'integration':\n        'push-on-success': false\n        'merge-from': 'AdGuard Home - Build and run tests'\n    'link-to-jira': true\n\n'notifications':\n  - 'events':\n      - 'plan-status-changed'\n    'recipients':\n      - 'webhook':\n            'name': 'Build webhook'\n            'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo'\n\n'labels': []\n'other':\n    'concurrent-build-plugin': 'system-default'\n\n'branch-overrides':\n    # rc-vX.Y.Z branches are the release candidate branches. They are created\n    # from the release branch and are used to build the release candidate\n    # images.\n  - '^rc-v[0-9]+\\.[0-9]+\\.[0-9]+':\n        # Set the default release channel on the release branch to beta, as we\n        # may need to build a few of these.\n        'variables':\n            'dockerFrontend': 'adguard/home-js-builder:4.0'\n            'dockerGo': 'adguard/go-builder:1.26.1--1'\n            'channel': 'candidate'\n"
  },
  {
    "path": "build/gitkeep",
    "content": "Keep this file non-hidden for Go's embedding to work.\n"
  },
  {
    "path": "changelog.config.js",
    "content": "module.exports = {\n    \"disableEmoji\": true,\n    \"list\": [\n        \"+ \",\n        \"* \",\n        \"- \",\n    ],\n    \"maxMessageLength\": 64,\n    \"minMessageLength\": 3,\n    \"questions\": [\n        \"type\",\n        \"scope\",\n        \"subject\",\n        \"body\",\n        \"issues\",\n    ],\n    \"scopes\": [\n        \"\",\n        \"ui\",\n        \"global\",\n        \"filtering\",\n        \"home\",\n        \"dnsforward\",\n        \"dhcpd\",\n        \"querylog\",\n        \"documentation\",\n    ],\n    \"types\": {\n        \"+ \": {\n            \"description\": \"A new feature\",\n            \"emoji\": \"\",\n            \"value\": \"+ \"\n        },\n        \"* \": {\n            \"description\": \"A code change that neither fixes a bug or adds a feature\",\n            \"emoji\": \"\",\n            \"value\": \"* \"\n        },\n        \"- \": {\n            \"description\": \"A bug fix\",\n            \"emoji\": \"\",\n            \"value\": \"- \"\n        }\n    }\n};\n"
  },
  {
    "path": "client/.eslintrc.json",
    "content": "{\n  \"plugins\": [\n    \"prettier\"\n  ],\n  \"extends\": [\n    \"airbnb-base\",\n    \"prettier\",\n    \"eslint:recommended\",\n    \"plugin:react/recommended\",\n    \"plugin:@typescript-eslint/recommended\"\n  ],\n  \"parser\": \"@typescript-eslint/parser\",\n  \"env\": {\n    \"jest\": true,\n    \"node\": true,\n    \"browser\": true,\n    \"commonjs\": true\n  },\n  \"settings\": {\n    \"react\": {\n      \"pragma\": \"React\",\n      \"version\": \"16.4\"\n    },\n    \"import/resolver\": {\n      \"node\": {\n        \"extensions\": [\n          \".js\",\n          \".jsx\",\n          \".ts\",\n          \".tsx\"\n        ]\n      }\n    }\n  },\n  \"rules\": {\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n    \"@typescript-eslint/no-unused-vars\": [\n      \"error\",\n      {\n        \"argsIgnorePattern\": \"^_\"\n      }\n    ],\n    \"import/extensions\": [\n      \"error\",\n      \"ignorePackages\",\n      {\n        \"js\": \"never\",\n        \"jsx\": \"never\",\n        \"ts\": \"never\",\n        \"tsx\": \"never\"\n      }\n    ],\n    \"class-methods-use-this\": \"off\",\n    \"no-shadow\": \"off\",\n    \"camelcase\": \"off\",\n    \"no-console\": [\n      \"warn\",\n      {\n        \"allow\": [\n          \"warn\",\n          \"error\"\n        ]\n      }\n    ],\n    \"import/no-extraneous-dependencies\": [\n      \"error\",\n      {\n        \"devDependencies\": true\n      }\n    ],\n    \"import/prefer-default-export\": \"off\",\n    \"no-alert\": \"off\",\n    \"arrow-body-style\": \"off\",\n    \"max-len\": [\n      \"error\",\n      120,\n      2,\n      {\n        \"ignoreUrls\": true,\n        \"ignoreComments\": false,\n        \"ignoreRegExpLiterals\": true,\n        \"ignoreStrings\": true,\n        \"ignoreTemplateLiterals\": true\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "client/.gitattributes",
    "content": "*.ts text eol=lf\n"
  },
  {
    "path": "client/.prettierrc",
    "content": "{\n  \"printWidth\": 120,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"bracketSpacing\": true,\n  \"bracketSameLine\": true,\n  \"tabWidth\": 4,\n  \"semi\": true,\n  \"arrowParens\": \"always\",\n}\n"
  },
  {
    "path": "client/.stylelintrc.js",
    "content": "module.exports = {\n  rules: {\n      \"selector-type-no-unknown\": true,\n      \"block-closing-brace-empty-line-before\": \"never\",\n      \"block-no-empty\": true,\n      \"block-opening-brace-newline-after\": \"always\",\n      \"block-opening-brace-space-before\": \"always\",\n      \"color-hex-case\": \"lower\",\n      \"color-named\": \"never\",\n      \"color-no-invalid-hex\": true,\n      \"length-zero-no-unit\": true,\n      \"declaration-block-trailing-semicolon\": \"always\",\n      \"custom-property-empty-line-before\": [\"always\", {\n          \"except\": [\n              \"after-custom-property\",\n              \"first-nested\"\n          ]\n      }],\n      \"declaration-block-no-duplicate-properties\": true,\n      \"declaration-colon-space-after\": \"always\",\n      \"declaration-empty-line-before\": [\"always\", {\n          \"except\": [\n              \"after-declaration\",\n              \"first-nested\",\n              \"after-comment\"\n          ]\n      }],\n      \"font-weight-notation\": \"numeric\",\n      \"indentation\": [4, {\n          \"except\": [\"value\"]\n      }],\n      \"max-empty-lines\": 2,\n      \"no-missing-end-of-source-newline\": true,\n      \"number-leading-zero\": \"always\",\n      \"property-no-unknown\": true,\n      \"rule-empty-line-before\": [\"always-multi-line\", {\n          \"except\": [\"first-nested\"],\n          \"ignore\": [\"after-comment\"]\n      }],\n      \"string-quotes\": \"double\",\n      \"value-list-comma-space-after\": \"always\",\n      \"unit-case\": \"lower\"\n  }\n}\n"
  },
  {
    "path": "client/babel.config.cjs",
    "content": "module.exports = (api) => {\n    api.cache(false);\n    return {\n        presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'],\n        plugins: [\n            '@babel/plugin-transform-runtime',\n            '@babel/plugin-transform-class-properties',\n            '@babel/plugin-transform-object-rest-spread',\n            '@babel/plugin-transform-nullish-coalescing-operator',\n            '@babel/plugin-transform-optional-chaining',\n            'react-hot-loader/babel',\n        ],\n    };\n};\n"
  },
  {
    "path": "client/constants.js",
    "content": "export const BUILD_ENVS = {\n    dev: 'development',\n    prod: 'production',\n};\n\nexport const BASE_URL = 'control';\n"
  },
  {
    "path": "client/dev.eslintrc",
    "content": "{\n    \"extends\": \".eslintrc\",\n    \"rules\": {\n        \"no-debugger\":\"warn\"\n    }\n}\n"
  },
  {
    "path": "client/global.d.ts",
    "content": "import React from 'react';\n\ndeclare module '*.svg' {\n    const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;\n    export default content;\n}\n"
  },
  {
    "path": "client/package.json",
    "content": "{\n  \"name\": \"dashboard\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build-dev\": \"cross-env NODE_ENV=development BUILD_ENV=dev webpack --config webpack.dev.js\",\n    \"build-prod\": \"cross-env BUILD_ENV=prod webpack --config webpack.prod.js\",\n    \"watch\": \"cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch\",\n    \"watch:hot\": \"cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js\",\n    \"lint\": \"eslint --ext .ts,.tsx src\",\n    \"lint:fix\": \"eslint --ext .ts,.tsx src --fix\",\n    \"test\": \"vitest --run\",\n    \"test:watch\": \"vitest --watch\",\n    \"test:e2e\": \"npx playwright test tests/e2e\",\n    \"test:e2e:interactive\": \"npx playwright test --headed\",\n    \"test:e2e:debug\": \"npx playwright test --debug\",\n    \"test:e2e:codegen\": \"npx playwright codegen\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"typecheck:watch\": \"tsc --noEmit --watch\"\n  },\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@nivo/line\": \"^0.64.0\",\n    \"axios\": \"^0.21.1\",\n    \"classnames\": \"^2.5.1\",\n    \"clsx\": \"^2.1.1\",\n    \"countries-and-timezones\": \"^3.6.0\",\n    \"date-fns\": \"^1.29.0\",\n    \"i18next\": \"^19.6.2\",\n    \"i18next-browser-languagedetector\": \"^4.2.0\",\n    \"ipaddr.js\": \"^1.9.1\",\n    \"js-yaml\": \"^3.14.0\",\n    \"lodash\": \"^4.17.19\",\n    \"nanoid\": \"^3.1.9\",\n    \"popper.js\": \"^1.16.1\",\n    \"prop-types\": \"^15.8.1\",\n    \"query-string\": \"^6.13.1\",\n    \"react\": \"^16.13.1\",\n    \"react-click-outside\": \"^3.0.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-hook-form\": \"^7.54.0\",\n    \"react-i18next\": \"^11.7.2\",\n    \"react-modal\": \"^3.11.2\",\n    \"react-popper-tooltip\": \"^2.11.1\",\n    \"react-redux\": \"^7.2.0\",\n    \"react-redux-loading-bar\": \"^4.6.0\",\n    \"react-router-dom\": \"^5.2.0\",\n    \"react-router-hash-link\": \"^1.2.2\",\n    \"react-select\": \"^3.1.0\",\n    \"react-table\": \"^6.11.4\",\n    \"react-transition-group\": \"^4.4.5\",\n    \"redux\": \"^4.0.5\",\n    \"redux-actions\": \"^2.6.5\",\n    \"redux-thunk\": \"^2.3.0\",\n    \"ts-migrate\": \"^0.1.35\",\n    \"url-polyfill\": \"^1.1.12\",\n    \"yaml\": \"2.8.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.24.5\",\n    \"@babel/plugin-transform-class-properties\": \"^7.24.1\",\n    \"@babel/plugin-transform-nullish-coalescing-operator\": \"^7.24.1\",\n    \"@babel/plugin-transform-object-rest-spread\": \"^7.24.5\",\n    \"@babel/plugin-transform-optional-chaining\": \"^7.24.5\",\n    \"@babel/plugin-transform-runtime\": \"^7.24.3\",\n    \"@babel/preset-env\": \"^7.24.5\",\n    \"@babel/preset-react\": \"^7.24.1\",\n    \"@playwright/test\": \"1.56.0\",\n    \"@types/lodash\": \"^4.17.4\",\n    \"@types/node\": \"^22.13.10\",\n    \"@types/react\": \"^17.0.80\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@types/react-redux\": \"^7.1.33\",\n    \"@types/react-router-dom\": \"^5.3.3\",\n    \"@types/react-table\": \"^7.7.20\",\n    \"@types/redux-actions\": \"^2.6.5\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.11.0\",\n    \"@typescript-eslint/parser\": \"^7.10.0\",\n    \"babel-loader\": \"^9.1.3\",\n    \"clean-webpack-plugin\": \"^4.0.0\",\n    \"copy-webpack-plugin\": \"^12.0.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"css-loader\": \"^7.1.2\",\n    \"eslint\": \"^8.57.0\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-prettier\": \"^5.1.3\",\n    \"eslint-plugin-react\": \"^7.34.1\",\n    \"eslint-plugin-react-hooks\": \"^4.6.2\",\n    \"file-loader\": \"^6.2.0\",\n    \"html-webpack-plugin\": \"^5.6.0\",\n    \"jscodeshift\": \"^0.15.2\",\n    \"jsdom\": \"^27.0.0\",\n    \"mini-css-extract-plugin\": \"^2.9.0\",\n    \"path\": \"^0.12.7\",\n    \"postcss-loader\": \"^8.1.1\",\n    \"prettier\": \"^3.2.5\",\n    \"react-hot-loader\": \"^4.13.1\",\n    \"style-loader\": \"^4.0.0\",\n    \"stylelint\": \"^16.5.0\",\n    \"ts-loader\": \"^9.5.1\",\n    \"url-loader\": \"^4.1.1\",\n    \"vitest\": \"^3.1.1\",\n    \"webpack\": \"^5.91.0\",\n    \"webpack-cli\": \"^5.1.4\",\n    \"webpack-dev-server\": \"^5.0.4\",\n    \"webpack-merge\": \"^5.10.0\"\n  },\n  \"browserslist\": {\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ],\n    \"production\": [\n      \">1%\",\n      \"last 4 chrome version\",\n      \"last 4 firefox version\",\n      \"last 4 safari version\",\n      \"firefox esr\",\n      \"not ie < 9\"\n    ]\n  }\n}\n"
  },
  {
    "path": "client/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\n\nimport path from 'path';\nimport { CONFIG_FILE_PATH } from './tests/constants';\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n    testDir: './tests/e2e',\n    globalSetup: path.resolve('./tests/e2e/globalSetup.ts'),\n    globalTeardown: path.resolve('./tests/e2e/globalTeardown.ts'),\n    timeout: 5000,\n    /* Run tests in files in parallel */\n    fullyParallel: true,\n    /* Fail the build on CI if you accidentally left test.only in the source code. */\n    forbidOnly: !!process.env.CI,\n    /* Retry on CI only */\n    retries: process.env.CI ? 2 : 0,\n    /* Opt out of parallel tests on CI. */\n    workers: process.env.CI ? 1 : undefined,\n    /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n    reporter: [['html', { open: 'never' }]],\n    /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n    use: {\n        /* Base URL to use in actions like `await page.goto('/')`. */\n        baseURL: 'http://127.0.0.1:3000',\n\n        /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n        trace: 'on-first-retry',\n        launchOptions: {\n            headless: true,\n        },\n    },\n\n    /* Configure projects for major browsers */\n    projects: [\n        {\n            name: 'chromium',\n            use: { ...devices['Desktop Chrome'] },\n        },\n    ],\n    webServer: process.env.CI\n        ? {\n              stdout: 'pipe',\n              command: `./AdGuardHome --local-frontend -v -c ${CONFIG_FILE_PATH}`,\n              url: 'http://127.0.0.1:3000',\n              cwd: '..',\n              timeout: 10000,\n          }\n        : undefined,\n});\n"
  },
  {
    "path": "client/prod.eslintrc",
    "content": "{\n    \"rules\": {\n        // disallow the use of debugger\n        \"no-debugger\": \"error\",\n    }\n}\n"
  },
  {
    "path": "client/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\">\n        <meta name=\"google\" content=\"notranslate\">\n        <meta http-equiv=\"x-dns-prefetch-control\" content=\"off\">\n        <meta name=\"mobile-web-app-capable\" content=\"yes\" />\n        <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n        <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\">\n        <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"assets/apple-touch-icon-180x180.png\" />\n        <link rel=\"mask-icon\" href=\"assets/safari-pinned-tab.svg\" color=\"#67B279\">\n        <link rel=\"icon\" type=\"image/png\" href=\"assets/favicon.png\" sizes=\"48x48\">\n        <title>AdGuard Home</title>\n        <style>\n            .wrapper {\n                display: flex;\n                flex-direction: column;\n                align-items: center;\n                justify-content: center;\n                min-height: 100vh;\n            }\n\n            [data-theme=\"DARK\"] .wrapper {\n                background-color: #f5f7fb;\n            }\n        </style>\n    </head>\n    <body>\n        <noscript>\n            You need to enable JavaScript to run this app.\n        </noscript>\n        <div id=\"root\">\n            <div class=\"wrapper\"></div>\n        </div>\n        <script>\n            (function() {\n                var LOCAL_STORAGE_THEME_KEY = 'account_theme';\n                var theme = 'light';\n\n                try {\n                    theme = window.localStorage.getItem(LOCAL_STORAGE_THEME_KEY);\n                } catch(e) {\n                    console.error(e);\n                }\n\n                document.body.dataset.theme = theme;\n            })();\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "client/public/install.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\">\n        <meta name=\"google\" content=\"notranslate\">\n        <meta name=\"mobile-web-app-capable\" content=\"yes\" />\n        <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n        <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\">\n        <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"assets/apple-touch-icon-180x180.png\" />\n        <link rel=\"mask-icon\" href=\"assets/safari-pinned-tab.svg\" color=\"#67B279\">\n        <link rel=\"icon\" type=\"image/png\" href=\"assets/favicon.png\" sizes=\"48x48\">\n        <title>Setup AdGuard Home</title>\n    </head>\n    <body>\n        <noscript>\n            You need to enable JavaScript to run this app.\n        </noscript>\n        <div id=\"root\"></div>\n    </body>\n</html>\n"
  },
  {
    "path": "client/public/login.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\">\n        <meta name=\"google\" content=\"notranslate\">\n        <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"assets/apple-touch-icon-180x180.png\" />\n        <meta name=\"mobile-web-app-capable\" content=\"yes\" />\n        <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n        <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\">\n        <link rel=\"mask-icon\" href=\"assets/safari-pinned-tab.svg\" color=\"#67B279\">\n        <link rel=\"icon\" type=\"image/png\" href=\"assets/favicon.png\" sizes=\"48x48\">\n        <title>Login</title>\n    </head>\n    <body>\n        <noscript>\n            You need to enable JavaScript to run this app.\n        </noscript>\n        <div id=\"root\"></div>\n        <script>\n            (function() {\n                var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\n                var currentTheme = prefersDark ? 'dark' : 'light';\n                document.body.dataset.theme = currentTheme;\n            })();\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "client/src/__locales/ar.json",
    "content": "{\n  \"access_allowed_desc\": \"قائمة CIDRs أو عناوين IP أو <a> ClientIDs </a>. إذا كانت هذه القائمة تحتوي على إدخالات ، فسيقبل AdGuard Home الطلبات من هؤلاء العملاء فقط.\",\n  \"access_allowed_title\": \"العملاء المسموحين\",\n  \"access_blocked_desc\": \"لا ينبغي الخلط بينه وبين المرشحات. يسقط AdGuard Home استعلامات DNS المطابقة لهذه المجالات ، ولا تظهر هذه الاستعلامات حتى في سجل الاستعلام. يمكنك تحديد أسماء النطاقات الدقيقة أو أحرف البدل أو قواعد تصفية عناوين URL ، على سبيل المثال \\\"example.org\\\" أو \\\"*.example.org\\\" أو \\\"|| example.org ^\\\" في المقابل.\",\n  \"access_blocked_title\": \"النطاقات غير المسموح بها\",\n  \"access_desc\": \"هنا يمكنك ضبط قواعد الوصول لخادم AdGuard Home DNS\",\n  \"access_disallowed_desc\": \"قائمة CIDRs أو عناوين IP أو <a> ClientIDs </a>. إذا كانت هذه القائمة تحتوي على إدخالات ، فسيقوم AdGuard Home بإسقاط الطلبات من هؤلاء العملاء. يتم تجاهل هذا الحقل إذا كانت هناك إدخالات في العملاء المسموح لهم.\",\n  \"access_disallowed_title\": \"العملاء غير المسموحين\",\n  \"access_settings_saved\": \"تم حفظ إعدادات الوصول بنجاح\",\n  \"access_title\": \"إعدادات الوصول\",\n  \"actions_table_header\": \"الإجراءات\",\n  \"add_allowlist\": \"إضافة قائمة السماح\",\n  \"add_blocklist\": \"إضافة قائمة الحظر\",\n  \"add_custom_list\": \"أضف قائمة مخصصة\",\n  \"add_persistent_client\": \"إضافة كعميل دائم\",\n  \"address\": \"العنوان\",\n  \"adg_will_drop_dns_queries\": \"سيقوم AdGuard Home بإسقاط جميع استعلامات DNS من هذا العميل.\",\n  \"all_lists_up_to_date_toast\": \"جميع القوائم محدثة بالفعل\",\n  \"all_queries\": \"كافة الاستفسارات\",\n  \"allow_this_client\": \"السماح لهذا العميل\",\n  \"allowed\": \"المسموح به\",\n  \"anonymize_client_ip\": \"إخفاء عنوان IP العميل\",\n  \"anonymize_client_ip_desc\": \"لا تقم بحفظ كامل عنوان IP العميل في السجلات والإحصائيات\",\n  \"anonymizer_notification\": \"<0>ملاحظة:</0> تم تمكين إخفاء هُوِيَّة IP. يمكنك تعطيله في <1>الإعدادات العامة</1>.\",\n  \"answer\": \"الإجابة\",\n  \"apply_btn\": \"تطبيق\",\n  \"auto_clients_desc\": \"معلومات حول عناوين IP للأجهزة التي تستخدم أو قد تستخدم AdGuard Home. يتم جمع هذه المعلومات من عدة مصادر، بما في ذلك ملفات المضيفين، و DNS العكسي، إلخ.\",\n  \"auto_clients_title\": \"Runtime clients\",\n  \"autofix_warning_list\": \"سيقوم بتنفيذ هذه المهام: <0> إلغاء تنشيط نظام DNSStubListener </0> <0> تعيين عنوان خادم DNS إلى 127.0.0.1 </0> <0> استبدال هدف الارتباط الرمزي لـ /etc/resolv.conf بـ / run / systemd /resolve/resolv.conf </0> <0> إيقاف DNSStubListener (إعادة تحميل خدمة حل نظام d) </0>\",\n  \"autofix_warning_result\": \"نتيجة لذلك ، ستتم معالجة جميع طلبات DNS من نظامك بواسطة AdGuard Home افتراضيًا.\",\n  \"autofix_warning_text\": \"إذا قمت بالنقر فوق \\\"إصلاح\\\" ، فسيقوم AdGuard Home بتهيئة نظامك لاستخدام خادم AdGuard Home DNS.\",\n  \"average_processing_time\": \"متوسط وقت المعالجة\",\n  \"average_processing_time_hint\": \"متوسط الوقت بالمللي ثانية عند معالجة طلب DNS\",\n  \"average_upstream_response_time\": \"متوسط وقت استجابة المنبع\",\n  \"back\": \"رجوع\",\n  \"block\": \"حظر\",\n  \"block_all\": \"حجب الكل\",\n  \"block_domain_use_filters_and_hosts\": \"حظر النطاقات باستخدام عوامل التصفية وملفات المضيفين\",\n  \"block_for_this_client_only\": \"احجب هذا العميل فقط\",\n  \"block_services\": \"حظر خدمات معينة\",\n  \"blocked_adult_websites\": \"محظور بواسطة الرِّقابة الأبوية\",\n  \"blocked_by\": \"<0>تم حظره بواسطة الفلاتر</0>\",\n  \"blocked_by_cname_or_ip\": \"حظر بواسطة CNAME or IP\",\n  \"blocked_by_response\": \"حظر بواسطة CNAME or IP in response\",\n  \"blocked_response_ttl\": \"حظر استجابة TTL\",\n  \"blocked_response_ttl_desc\": \"تحديد عدد الثواني التي يجب على العملاء تخزين الاستجابة التي تمت تصفيتها مؤقتًا\",\n  \"blocked_safebrowsing\": \"محظور بواسطة التصفح الآمن\",\n  \"blocked_service\": \"الخدمات المحجوبة\",\n  \"blocked_services\": \"الخوادم المحجوبة\",\n  \"blocked_services_desc\": \"يسمح بحجب المواقع والخدمات الشعبية بسرعة.\",\n  \"blocked_services_global\": \"استخدام خدمات الحظر العالمية\",\n  \"blocked_services_saved\": \"تم حفظ الخوادم المحجوبة بنجاح\",\n  \"blocked_threats\": \"التهديدات المحظورة\",\n  \"blocking_ipv4\": \"حجب عنوان IPv4\",\n  \"blocking_ipv4_desc\": \"سيتم إرجاع عنوان IP لطلب محظور\",\n  \"blocking_ipv6\": \"حجب عنوان IPv6\",\n  \"blocking_ipv6_desc\": \"سيتم إرجاع عنوان IP لطلب AAAA محظور\",\n  \"blocking_mode\": \"وضع الحجب\",\n  \"blocking_mode_custom_ip\": \"استجابة IP مخصصة بعنوان IP تم تعيينه يدويًا\",\n  \"blocking_mode_default\": \"الافتراضي: الرد بعنوان IP صفري (0.0.0.0 لـ A ؛ :: لـ AAAA) عند حظره بواسطة قاعدة نمط Adblock ؛ الرد بعنوان IP المحدد في القاعدة عند حظره بواسطة / etc / hosts-style rule\",\n  \"blocking_mode_null_ip\": \"IP Null: الاستجابة بعنوان IP صفري (0.0.0.0 لـ A ؛ :: لـ AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: الرد باستخدام رمز NXDOMAIN\",\n  \"blocking_mode_refused\": \"مرفوض: رد برمز مرفوض\",\n  \"blocklist\": \"قائمة الحظر\",\n  \"bootstrap_dns\": \"خوادم Bootstrap DNS\",\n  \"bootstrap_dns_desc\": \"عناوين IP لخوادم DNS المستخدمة لحل عناوين IP الخاصة بمحللات DoH/DoT التي تحددها كمصدرين رئيسيين. التعليقات غير مسموح بها.\",\n  \"cache_cleared\": \"تم مسح ذاكرة التخزين المؤقت لنظام أسماء النطاق بنجاح\",\n  \"cache_enabled\": \"تفعيل ذاكرة التخزين المؤقت\",\n  \"cache_enabled_desc\": \"تخزين استجابات DNS محليًا.\",\n  \"cache_optimistic\": \"متفائل التخزين المؤقت\",\n  \"cache_optimistic_desc\": \"اجعل AdGuard Home يستجيب من ذاكرة التخزين المؤقت حتى عندما تنتهي صلاحية الإدخالات وحاول أيضًا تحديثها.\",\n  \"cache_size\": \"حجم ذاكرة التخزين المؤقت\",\n  \"cache_size_desc\": \"حجم ذاكرة التخزين المؤقت DNS (بالبايتات).\",\n  \"cache_size_validation\": \"يجب أن يكون حجم الذاكرة المؤقتة أكبر من الصفر عند تفعيلها.\",\n  \"cache_ttl_max_override\": \"تجاوز الحد الاقصى من مدة البقاء TTL\",\n  \"cache_ttl_max_override_desc\": \"قم بتعيين الحد الأقصى لقيمة الوقت للعيش (بالثواني) للإدخالات في ذاكرة التخزين المؤقت لنظام أسماء النطاقات.\",\n  \"cache_ttl_min_override\": \"تجاوز الحد الأدنى من مدة البقاء TTL\",\n  \"cache_ttl_min_override_desc\": \"قم بتمديد قيم فترة البقاء القصيرة (بالثواني) المستلمة من الخادم الرئيسي عند تخزين استجابات DNS مؤقتًا.\",\n  \"cancel_btn\": \"إلغاء\",\n  \"category_label\": \"الفئة\",\n  \"check\": \"تحقق\",\n  \"check_client_id\": \"معرّف العميل (ClientID أو عنوان IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"تحقق مما إذا تم فلترة اسم المضيف.\",\n  \"check_dhcp_servers\": \"تحقق من خوادم DHCP\",\n  \"check_dns_record\": \"حدد نوع سجل DNS\",\n  \"check_enter_client_id\": \"أدخل معرّف العميل\",\n  \"check_hostname\": \"اسم المضيف أو اسم النطاق\",\n  \"check_ip\": \"عناوين الـ IP: {{ip}}\",\n  \"check_not_found\": \"غير موجود في قوائم التصفية الخاصة بك\",\n  \"check_reason\": \"سبب: {{reason}}\",\n  \"check_service\": \"أسم الخدمة: {{service}}\",\n  \"check_title\": \"تحقق من الفلترة\",\n  \"check_updates_btn\": \"التحقق من وجود تحديثات\",\n  \"check_updates_now\": \"تحقق من وجود تحديثات الآن\",\n  \"choose_allowlist\": \"اختر قوائم السماح\",\n  \"choose_blocklist\": \"اختر قوائم الحظر\",\n  \"choose_from_list\": \"اختر من القائمة\",\n  \"city\": \"المدينة\",\n  \"clear_cache\": \"مسح ذاكرة التخزين المؤقت\",\n  \"click_to_view_queries\": \"انقر لعرض الـ queries\",\n  \"client_add\": \"إضافة عميل\",\n  \"client_added\": \"تم اضافة العميل \\\"{{key}}\\\" بنجاح\",\n  \"client_blocked\": \"تم حظر العميل \\\"{{ip}}\\\" بنجاح\",\n  \"client_confirm_block\": \"هل أنت متأكد من أنك تريد منع العميل \\\"{{ip}}\\\"؟\",\n  \"client_confirm_delete\": \"هل أنت متأكد من أنك تريد حذف العميل \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"هل تريد بالتأكيد إلغاء حظر العميل \\\"{{ip}}\\\"؟\",\n  \"client_deleted\": \"تم حذف العميل \\\"{{key}}\\\" بنجاح\",\n  \"client_details\": \"تفاصيل العميل\",\n  \"client_edit\": \"تعديل العميل\",\n  \"client_global_settings\": \"استخدم إعدادات عالمية\",\n  \"client_id\": \"عنوان العميل الشخصي\",\n  \"client_id_desc\": \"يمكن تحديد هوية العميل. اعرف المزيد عن كيفية تحديد هوية العملاء <a> هنا</a>.\",\n  \"client_id_placeholder\": \"ادخل عنوان العميل الشخصي\",\n  \"client_identifier\": \"المعّرف\",\n  \"client_identifier_desc\": \"يمكن التعرف على العملاء من خلال عنوان IP أو CIDR أو عنوان MAC أو ClientID (يمكن استخدامه في DoT / DoH / DoQ). تعرف على المزيد حول كيفية تحديد العملاء <0> هنا </0>.\",\n  \"client_name\": \"العميل {{id}}\",\n  \"client_new\": \"عميل جديد\",\n  \"client_settings\": \"إعدادات العميل\",\n  \"client_table_header\": \"عميل\",\n  \"client_unblocked\": \"تم إلغاء حظر العميل \\\"{{ip}}\\\" بنجاح\",\n  \"client_updated\": \"تم تحديث العميل \\\"{{key}}\\\" بنجاح\",\n  \"clients_desc\": \"قم بضبط سجلات العميل الدائمة للأجهزة المتصلة بـ AdGuard Home\",\n  \"clients_not_found\": \"لم يتم العثور على عملاء\",\n  \"clients_title\": \"العملاء الدائمين\",\n  \"compact\": \"المدمج\",\n  \"config_successfully_saved\": \"تم حفظ الاعدادات بنجاح\",\n  \"configure\": \"ضبط\",\n  \"confirm_dns_cache_clear\": \"هل تريد بالتأكيد مسح ذاكرة التخزين المؤقت لنظام أسماء النطاقات DNS؟\",\n  \"confirm_static_ip\": \"سيقوم AdGuard Home بتهيئة {{ip}} ليكون عنوان IP الثابت الخاص بك. هل تريد المتابعة؟\",\n  \"copyright\": \"حقوق النشر\",\n  \"country\": \"الدولة\",\n  \"custom_filter_rules\": \"قواعد التصفية المخصصة\",\n  \"custom_filter_rules_hint\": \"أدخل قاعدة واحدة على السطر يمكنك استخدام قواعد adblock أو بناء جملة ملفات المضيفين\",\n  \"custom_filtering_rules\": \"قواعد التصفية المخصصة\",\n  \"custom_ip\": \"عنوان IP مخصص\",\n  \"custom_retention_input\": \"أدخل الاحتفاظ بالساعات\",\n  \"custom_rotation_input\": \"أدخل التناوب بالساعات\",\n  \"dashboard\": \"لوحة القيادة\",\n  \"date\": \"التاريخ\",\n  \"default\": \"إفتراضي\",\n  \"delete_confirm\": \"هل أنت متأكد من أنك تريد حذف \\\"{{key}}\\\"؟\",\n  \"delete_table_action\": \"حذف\",\n  \"descr\": \"الوصف\",\n  \"details\": \"التفاصيل\",\n  \"dhcp_add_static_lease\": \"إضافة عقد إيجار ثابت\",\n  \"dhcp_config_saved\": \"الإعدادات محفوظة لخادم DHCP\",\n  \"dhcp_description\": \"إذا كان جهاز الراوتر الخاص بك لا يوفر إعدادات DHCP ، يمكنك استخدام خادم DHCP المدمج في AdGuard.\",\n  \"dhcp_disable\": \"عطل خادم DHCP\",\n  \"dhcp_dynamic_ip_found\": \"يستخدم نظامك عنوان IP الديناميكي للواجهة <0>{{interfaceName}}</0>. من أجل استعمال خادم DHCP ، يجب تعيين عنوان IP ثابت. عنوان IP الحالي الخاص بك هو <0>{{ipAddress}}</0>. إذا ضغطت على زر تفعيل DHCP سنقوم تلقائيًا بتعيين عنوان الIP هذا على أنه ثابت.\",\n  \"dhcp_edit_static_lease\": \"تحرير عقد الإيجار الثابت\",\n  \"dhcp_enable\": \"فعل خادم DHCP\",\n  \"dhcp_error\": \"لم نتمكن من تحديد ما إذا كان هناك خادم DHCP آخر في الشبكة.\",\n  \"dhcp_form_gateway_input\": \"IP البوابة\",\n  \"dhcp_form_lease_input\": \"مدة الإيجار\",\n  \"dhcp_form_lease_title\": \"مدة تأجير DHCP (بالثواني)\",\n  \"dhcp_form_range_end\": \"نطاق النهاية\",\n  \"dhcp_form_range_start\": \"نطاق البداية\",\n  \"dhcp_form_range_title\": \"مجموعة عناوين IP\",\n  \"dhcp_form_subnet_input\": \"قناع الشبكة الفرعية\",\n  \"dhcp_found\": \"تم العثور على خادم DHCP نشط على الشبكة. وبالتالي لا ينصح بتفعيل خادم DHCP المدمج.\",\n  \"dhcp_hardware_address\": \"عناوين الأجهزة\",\n  \"dhcp_interface_select\": \"حدد واجهة DHCP\",\n  \"dhcp_ip_addresses\": \"عناوين الـIP\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 إعدادات\",\n  \"dhcp_ipv6_settings\": \"إعدادات DHCP IPv6\",\n  \"dhcp_lease_added\": \"تمت أضافة مدة الايجار \\\"{{key}}\\\" بنجاح\",\n  \"dhcp_lease_deleted\": \"تمت ازالة مدة الايجار \\\"{{key}}\\\" بنجاح\",\n  \"dhcp_lease_updated\": \"Static lease \\\"{{key}}\\\" تمّ التحديث بنجاح\",\n  \"dhcp_leases\": \"عقود إيجار DHCP\",\n  \"dhcp_leases_not_found\": \"لم يتم العثور على عقود إيجار DHCP\",\n  \"dhcp_new_static_lease\": \"عقد إيجار ثابت جديد\",\n  \"dhcp_not_found\": \"من الآمن تمكين خادم DHCP المدمج - لم نعثر على أي خوادم DHCP نشطة على الشبكة. ومع ذلك ، نشجعك على إعادة التحقق يدويًا لأن اختبارنا التلقائي في الوقت الحالي لا يوفر ضمانًا بنسبة 100٪.\",\n  \"dhcp_reset\": \"هل أنت متأكد من أنك تريد إعادة تعيين تكوين DHCP؟\",\n  \"dhcp_reset_leases\": \"إعادة تعيين كافة عقود الإيجار\",\n  \"dhcp_reset_leases_confirm\": \"هل أنت متأكد أنك تريد إعادة تعيين كافة عقود الإيجار؟\",\n  \"dhcp_reset_leases_success\": \"إعادة تعيين تأجير DHCP بنجاح\",\n  \"dhcp_settings\": \"إعدادات DHCP\",\n  \"dhcp_static_ip_error\": \"من أجل استخدام خادم DHCP ، يجب تعيين عنوان IP ثابت. فشلنا في تحديد ما إذا تم تكوين واجهة الشبكة هذه باستخدام عنوان IP ثابت. يرجى تعيين عنوان IP ثابت يدويًا.\",\n  \"dhcp_static_leases\": \"إيجارات DHCP الثابتة\",\n  \"dhcp_static_leases_not_found\": \"لم يتم العثور على عقود إيجار ثابتة DHCP\",\n  \"dhcp_table_expires\": \"تنتهي\",\n  \"dhcp_table_hostname\": \"اسم المضيف\",\n  \"dhcp_title\": \"خادم DHCP (تجريبي!)\",\n  \"dhcp_warning\": \"إذا كنت تريد تمكين خادم DHCP على أي حال ، فتأكد من عدم وجود خادم DHCP نشط آخر في شبكتك. خلاف ذلك ، يمكن أن يعطل خدمة الإنترنت للأجهزة المتصلة!\",\n  \"disable_for_hours\": \"لمدة {{count}} ساعة\",\n  \"disable_for_hours_plural\": \"لمدة {{count}} ساعات\",\n  \"disable_for_minutes\": \"لمدة {{count}} دقيقة\",\n  \"disable_for_minutes_plural\": \"لمدة {{count}} دقيقة\",\n  \"disable_for_seconds\": \"لـ {{count}} ثانية\",\n  \"disable_for_seconds_plural\": \"لمدة {{count}} ثانية\",\n  \"disable_ipv6\": \"قم بتعطيل تحليل عناوين IPv6\",\n  \"disable_ipv6_desc\": \"قم بإسقاط كافة استعلامات DNS لعناوين IPv6 (اكتب AAAA) وقم بإزالة تلميحات IPv6 من استجابات HTTPS.\",\n  \"disable_notify_for_hours\": \"تعطيل الحماية لـ {{count}} ساعة\",\n  \"disable_notify_for_hours_plural\": \"تعطيل الحماية لـ {{count}} ساعات\",\n  \"disable_notify_for_minutes\": \"تعطيل الحماية لـ {{count}} دقيقة\",\n  \"disable_notify_for_minutes_plural\": \"تعطيل الحماية لـ {{count}} دقائق\",\n  \"disable_notify_for_seconds\": \"تعطيل الحماية لـ {{count}} ثانية\",\n  \"disable_notify_for_seconds_plural\": \"تعطيل الحماية ل {{count}} ثواني\",\n  \"disable_notify_until_tomorrow\": \"تعطيل الحماية حتى الغد\",\n  \"disable_protection\": \"تعطيل الحماية\",\n  \"disable_rewrites\": \"تعطيل قواعد إعادة الكتابة\",\n  \"disable_until_tomorrow\": \"حتى الغد\",\n  \"disabled\": \"معطلة\",\n  \"disabled_dhcp\": \"خادم DHCP غير مفعل\",\n  \"disabled_filtering_toast\": \"تم تعطيل الفلترة\",\n  \"disabled_parental_toast\": \"تعطيل الرقابة الأبوية\",\n  \"disabled_protection\": \"الحماية غير مفعلة\",\n  \"disabled_safe_browsing_toast\": \"تم تعطيل التصفح الآمن\",\n  \"disabled_safe_search_toast\": \"تعطيل البحث الآمن\",\n  \"disallow_this_client\": \"منع هذا العميل\",\n  \"dns_addresses\": \"عناوين DNS\",\n  \"dns_allowlists\": \"قوائم السماح لـ DNS\",\n  \"dns_allowlists_desc\": \"سيتم السماح بالنطاقات من قوائم DNS المسموحة حتى لو كانت في أي من قوائم الحظر\",\n  \"dns_blocklists\": \"قوائم حظر DNS\",\n  \"dns_blocklists_desc\": \"سيقوم AdGuard Home بحظر النطاقات المطابقة لقوائم الحظر\",\n  \"dns_cache_config\": \"ضبط الملفات المؤقتة لـ DNS\",\n  \"dns_cache_config_desc\": \"هنا تستطيع ضبط اعدادات الـ DNS وملفاته\",\n  \"dns_cache_size\": \"حجم ذاكرة التخزين المؤقت لنظام أسماء النطاقات، بالبايت\",\n  \"dns_config\": \"إعداد خادم DNS\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"خصوصية DNS\",\n  \"dns_providers\": \"فيما يلي قائمة <0> بموفري DNS المعروفين </0> للاختيار من بينها.\",\n  \"dns_query\": \"DNS Queries\",\n  \"dns_rewrites\": \"إعادة كتابة DNS\",\n  \"dns_settings\": \"إعدادات الـ DNS\",\n  \"dns_start\": \"خادم DNS قيد التشغيل\",\n  \"dns_status_error\": \"خطأ في التحقق من حالة خادم الـ DNS\",\n  \"dns_test_not_ok_toast\": \"خادم \\\"{{key}}\\\": لا يمكن استخدامه يرجى التحقق من كتابته بشكل صحيح\",\n  \"dns_test_ok_toast\": \"تعمل خوادم DNS المحددة بشكل صحيح\",\n  \"dns_test_parsing_error_toast\": \"القسم {{section}}: السطر {{line}}: لا يمكن استخدامه، يرجى التحقق من أنك قد كتبته بشكل صحيح\",\n  \"dns_test_warning_toast\": \"المنبع \\\"{{key}}\\\" لا يستجيب لطلبات الاختبار وقد لا يعمل بشكل صحيح\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"تفعيل DNSSEC\",\n  \"dnssec_enable_desc\": \"قم بتعيين علامة DNSSEC في استعلامات DNS الواردة وتحقق من النتيجة (مطلوب محلل يدعم DNSSEC).\",\n  \"domain\": \"النطاق\",\n  \"domain_desc\": \"أدخل اسم النطاق أو حرف البدل الذي تريد إعادة كتابته.\",\n  \"domain_name_table_header\": \"اسم النطاق\",\n  \"domain_or_client\": \"الدومين أو العميل\",\n  \"down\": \"تحت\",\n  \"download_mobileconfig\": \"حمّل ملف الإعدادات\",\n  \"download_mobileconfig_doh\": \"حمّل .mobileconfig for DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"حمل .mobileconfig for DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"تحرير القائمة البيضاء\",\n  \"edit_blocklist\": \"تحرير قائمة الحظر\",\n  \"edit_table_action\": \"تحرير\",\n  \"edns_cs_desc\": \"أضف EDNS الشبكة الفرعية للعميل (ECS) إلى الطلبات الأولية وقم بتسجيل القيم المرسلة من قبل العملاء في سجل الاستعلام.\",\n  \"edns_enable\": \"فعل EDNS client subnet\",\n  \"edns_use_custom_ip\": \"استخدام IP مخصص لـ EDNS\",\n  \"edns_use_custom_ip_desc\": \"السماح باستخدام IP مخصص لـ EDNS\",\n  \"elapsed\": \"المنقضي\",\n  \"empty_response_status\": \"فارغ\",\n  \"enable_protection\": \"تفعيل الحماية\",\n  \"enable_protection_timer\": \"سيتم تمكين الحماية في {{time}}\",\n  \"enable_rewrites\": \"تفعيل قواعد إعادة الكتابة\",\n  \"enable_upstream_dns_cache\": \"تمكين التخزين المؤقت لنظام أسماء النطاقات DNS لتكوين المنبع المخصص لهذا العميل\",\n  \"enabled_dhcp\": \"خادم DHCP مفعل\",\n  \"enabled_filtering_toast\": \"تم تمكين الفلترة\",\n  \"enabled_parental_toast\": \"تفعيل الرقابة الأبوية\",\n  \"enabled_protection\": \"الحماية مفعلة\",\n  \"enabled_safe_browsing_toast\": \"تم تمكين التصفح الآمن\",\n  \"enabled_save_search_toast\": \"تفعيل البحث الآمن\",\n  \"enabled_table_header\": \"قيد التشغيل\",\n  \"encryption_certificate_path\": \"مسار الشهادة\",\n  \"encryption_certificates\": \"الشهادات\",\n  \"encryption_certificates_desc\": \"من أجل استخدام التشفير ، تحتاج إلى تقديم سلسلة شهادات SSL صالحة لنطاقك. يمكنك الحصول على شهادة مجانية على <0>{{link}}</0> أو يمكنك شرائها من أحد المراجع المصدقة الموثوقة.\",\n  \"encryption_certificates_input\": \"انسخ / الصق الشهادات المشفرة PEM هنا.\",\n  \"encryption_certificates_source_content\": \"الصق محتويات الشهادات\",\n  \"encryption_certificates_source_path\": \"قم بتعيين مسار ملف الشهادات\",\n  \"encryption_chain_invalid\": \"سلسلة الشهادات غير صالحة\",\n  \"encryption_chain_valid\": \"سلسلة الشهادات صالحة\",\n  \"encryption_config_saved\": \"تم حفظ اعدادات التشفير\",\n  \"encryption_desc\": \"دعم التشفير (HTTPS / TLS) لكل من DNS وواجهة ويب المسؤول\",\n  \"encryption_doq\": \"DNS-over-QUIC port\",\n  \"encryption_doq_desc\": \"إذا تم ضبط هذا المنفذ، فسيقوم AdGuard Home بتشغيل خادم DNS-over-QUIC على هذا المنفذ.\",\n  \"encryption_dot\": \"منفذ DNS-over-TLS\",\n  \"encryption_dot_desc\": \"إذا تم ضبط هذا المنفذ ، فسيقوم AdGuard Home بتشغيل خادم DNS-over-TLS على هذا المنفذ.\",\n  \"encryption_enable\": \"تمكين التشفير (HTTPS و DNS-over-HTTPS و DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"إذا تم تمكين التشفير فستعمل واجهة مسؤول AdGuard Home عبر HTTPS وسيستمع خادم DNS للطلبات عبر DNS-over-HTTPS و DNS-over-TLS.\",\n  \"encryption_expire\": \"يتنهي في\",\n  \"encryption_hostnames\": \"اسم المستضيف\",\n  \"encryption_https\": \"منفذ HTTPS\",\n  \"encryption_https_desc\": \"إذا تم تكوين منفذ HTTPS ، فسيتم الوصول إلى واجهة مشرف AdGuard Home عبر HTTPS ، وستوفر أيضًا DNS-over-HTTPS على موقع '/dns-query'.\",\n  \"encryption_issuer\": \"المصدر\",\n  \"encryption_key\": \"مفتاح خاص\",\n  \"encryption_key_input\": \"انسخ / الصق مفتاحك الخاص المشفر بـ PEM لشهادتك هنا\",\n  \"encryption_key_invalid\": \"هذا مفتاح خاص {{type}} غير صالح\",\n  \"encryption_key_source_content\": \"الصق محتويات المفتاح الخاص\",\n  \"encryption_key_source_path\": \"قم بتعيين طريق ملف مفتاح خاص\",\n  \"encryption_key_valid\": \"هذا مفتاح خاص {{type}} صالح\",\n  \"encryption_plain_dns_desc\": \"الـDNS العادي مفعل افتراضيًا. يمكنك تعطيله لإجبار جميع الأجهزة على استخدام DNS المشفر. للقيام بذلك، يجب عليك تفعيل بروتوكول DNS المشفر على الأقل\",\n  \"encryption_plain_dns_enable\": \"تمكين DNS العادي\",\n  \"encryption_plain_dns_error\": \"لتعطيل DNS العادي، قم بتمكين بروتوكول DNS المشفر على الأقل\",\n  \"encryption_private_key_path\": \"مسار المفتاح الخاص\",\n  \"encryption_redirect\": \"إعادة التوجيه إلى HTTPS تلقائيًا\",\n  \"encryption_redirect_desc\": \"إذا تم تحديده ، فسيقوم AdGuard Home بإعادة توجيهك تلقائيًا من عناوين HTTP إلى عناوين HTTPS.\",\n  \"encryption_reset\": \"هل أنت متأكد أنك تريد إعادة تعيين إعدادات التشفير؟\",\n  \"encryption_server\": \"اسم الخادم\",\n  \"encryption_server_desc\": \"من أجل استخدام HTTPS ، تحتاج إلى إدخال اسم الخادم الذي يتطابق مع شهادة SSL أو شهادة البدل. إذا لم يتم تعيين الحقل ، فسيقبل اتصالات TLS لأي مجال.\",\n  \"encryption_server_enter\": \"ادخل عنوان النطاق الخاص بك\",\n  \"encryption_settings\": \"إعدادات التعمية\",\n  \"encryption_status\": \"الحالة\",\n  \"encryption_subject\": \"الموضوع\",\n  \"encryption_title\": \"التعمية\",\n  \"encryption_warning\": \"تحذير\",\n  \"enforce_safe_search\": \"استخدم البحث الآمن\",\n  \"enforce_save_search_hint\": \"سيفرض AdGuard Home البحث الآمن في محركات البحث التالية: Google وYouTube وBing وDuckDuckGo وEcosia وYandex وPixabay.\",\n  \"enforced_save_search\": \"فرض البحث الآمن\",\n  \"enter_cache_size\": \"أدخل حجم ذاكرة التخزين المؤقت (بايت)\",\n  \"enter_cache_ttl_max_override\": \"أدخل الحد الاقصى من مدة البقاء (بالثواني)\",\n  \"enter_cache_ttl_min_override\": \"أدخل الحد الأدنى من مدة البقاء (بالثواني)\",\n  \"enter_name_hint\": \"أدخل الاسم\",\n  \"enter_url_or_path_hint\": \"إدخال عنوان URL أو مسار مطلق للقائمة\",\n  \"enter_valid_allowlist\": \"أدخل عنوان URL صالحًا لقائمة السماح\",\n  \"enter_valid_blocklist\": \"إدخال عنوان URL صالح إلى قائمة الحظر\",\n  \"error_details\": \"مزيد من التفاصيل حول الخطأ\",\n  \"example_comment\": \"! ها هو التعليق.\",\n  \"example_comment_hash\": \"# تعليق أيضًا\",\n  \"example_comment_meaning\": \"فقط تعليق;\",\n  \"example_meaning_filter_block\": \"منع الوصول إلى نطاق example.org وجميع نطاقاته الفرعية\",\n  \"example_meaning_filter_whitelist\": \"إلغاء حظر الوصول إلى نطاق example.org وجميع نطاقاته الفرعية\",\n  \"example_meaning_host_block\": \"الرد ب 127.0.0.1 على example.org (ولكن ليس لنطاقاته الفرعية);\",\n  \"example_multiple_upstreams_reserved\": \"منابع متعددة <0>لمجالات محددة</0>;\",\n  \"example_regex_meaning\": \"منع الوصول إلى النطاقات المطابقة للتعبير العادي المحدد.\",\n  \"example_rewrite_domain\": \"أعد كتابة الردود لاسم النطاق هذا فقط.\",\n  \"example_rewrite_wildcard\": \"أعد كتابة الردود لجميع النطاقات الفرعية <0> example.org </0>.\",\n  \"example_upstream_comment\": \"تعليق.\",\n  \"example_upstream_doh\": \"مشفر <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS المشفر مع فرض <0> HTTP / 3</0> ولا يوجد رجوع إلى HTTP / 2 أو أقل ؛\",\n  \"example_upstream_doq\": \"encrypted <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"مشفر<0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"regular DNS (over UDP);\",\n  \"example_upstream_regular_port\": \"DNS عادي (عبر UDP ، مع المنفذ) ؛\",\n  \"example_upstream_reserved\": \"من المنبع <0>لمجالات محددة</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;\",\n  \"example_upstream_tcp\": \"regular DNS (over TCP);\",\n  \"example_upstream_tcp_hostname\": \"regular DNS (over TCP, hostname);\",\n  \"example_upstream_tcp_port\": \"DNS عادي (عبر TCP ، مع منفذ) ؛\",\n  \"example_upstream_udp\": \"regular DNS (over UDP, hostname);\",\n  \"examples_title\": \"أمثلة\",\n  \"fallback_dns_desc\": \"قائمة الخوادم الاحتياطية المستخدمة في حالة عدم الاستجابة من خوادم DNS الرئيسية. تمتلك تلك الخوادم والخوادم الرئيسية نفس الأوامر.\",\n  \"fallback_dns_placeholder\": \"أدخل خادم DNS احتياطي واحد لكل سطر\",\n  \"fallback_dns_title\": \"خوادم DNS الاحتياطية\",\n  \"faq\": \"الأسئلة المتداولة\",\n  \"fastest_addr\": \"أسرع عنوان IP\",\n  \"fastest_addr_desc\": \"انتظر الردود من <b>جميع</b> خوادم DNS، وقم بقياس سرعة اتصال TCP لكل خادم، ثم أعد عنوان IP الخاص بالخادم الأسرع اتصالًا. <br/>قد يؤدي هذا الوضع إلى إبطاء استعلامات DNS بشكل كبير إذا لم يستجب أحد أو أكثر من الخوادم العلوية. تأكد من أن خوادمك العلوية مستقرة وأن مهلة الاستجابة لديك منخفضة.\",\n  \"filter\": \"فلتر\",\n  \"filter_added_successfully\": \"تم إضافة القائمة بنجاح\",\n  \"filter_allowlist\": \"تحذير: سيؤدي هذا الإجراء أيضًا إلى استبعاد القاعدة \\\"{{disallowed_rule}}\\\" من قائمة العملاء المسموح لهم.\",\n  \"filter_category_general\": \"عام\",\n  \"filter_category_general_desc\": \"القوائم التي تحظر التتبع والإعلانات على معظم الأجهزة\",\n  \"filter_category_other\": \"أخرى\",\n  \"filter_category_other_desc\": \"قوائم الحظر الأخرى\",\n  \"filter_category_regional\": \"إقليمي\",\n  \"filter_category_regional_desc\": \"القوائم التي تركز على الإعلانات الإقليمية وخوادم التتبع\",\n  \"filter_category_security\": \"الأمان\",\n  \"filter_category_security_desc\": \"القوائم المصممة خصيصًا لحظر النطاقات الخبيثة والتصيد الاحتيالي والخداع\",\n  \"filter_removed_successfully\": \"تم ازالته من القائمة بنجاح\",\n  \"filter_updated\": \"تم تحديث القائمة بنجاح\",\n  \"filtered\": \"تمت الفلترة\",\n  \"filtered_custom_rules\": \"تمت تصفيتها حسب قواعد التصفية المخصصة\",\n  \"filtering_rules_learn_more\": \"<0> اعرف المزيد </0> حول إنشاء قوائم المضيفين الخاصة بك.\",\n  \"filters\": \"الفلاتر\",\n  \"filters_and_hosts_hint\": \"يفهم AdGuard Home قواعد حظر الإعلانات الاساسية وملفات الهوست.\",\n  \"filters_block_toggle_hint\": \"يمكنك إعداد قواعد حظر في <a>المرشحات</a> اعدادات.\",\n  \"filters_configuration\": \"اضبط الفلاتر\",\n  \"filters_enable\": \"تفعيل الفلاتر\",\n  \"filters_interval\": \"الفاصل الزمني لتحديث الفلاتر\",\n  \"fix\": \"يصلح\",\n  \"for_last_days\": \"لآخر {{value}} يوم\",\n  \"for_last_days_plural\": \"لآخر {{count}} ايام\",\n  \"for_last_hours\": \"لآخر {{count}} ساعة\",\n  \"for_last_hours_plural\": \"لآخر {{count}} ساعة\",\n  \"forgot_password\": \"نسيت كلمة المرور؟\",\n  \"forgot_password_desc\": \"يرجى اتباع <0> هذه الخطوات </0> لإنشاء كلمة مرور جديدة لحساب المستخدم الخاص بك.\",\n  \"form_add_id\": \"أضافة معّرف\",\n  \"form_answer\": \"أدخل عنوان IP أو اسم النطاق\",\n  \"form_client_name\": \"ادخل اسم العميل\",\n  \"form_domain\": \"أدخل اسم النطاق أو حرف البدل\",\n  \"form_enter_blocked_response_ttl\": \"أدخل وقت الاستجابة المحظورة TTL (بالثواني)\",\n  \"form_enter_host\": \"ادخل اسم المضيف\",\n  \"form_enter_hostname\": \"أدخل اسم الhostname\",\n  \"form_enter_id\": \"ادخل المعّرف\",\n  \"form_enter_ip\": \"ادخل عنوان IP\",\n  \"form_enter_mac\": \"ادخل MAC\",\n  \"form_enter_rate_limit\": \"ادخل حد التقييم\",\n  \"form_enter_rate_limit_subnet_len\": \"أدخل طول بادئة الشبكة الفرعية لتحديد معدل الحد الأقصى\",\n  \"form_enter_subnet_ip\": \"أدخل عنوان IP في الشبكة الفرعية \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"أدخل مدة مهلة الخادم العلوي بالثواني\",\n  \"form_error_answer_format\": \"تنسيق إجابة غير صالح\",\n  \"form_error_client_id_format\": \"يجب أن يحتوي معرف العميل على الأرقام والأحرف الصغيرة والواصلات فقط\",\n  \"form_error_domain_format\": \"تنسيق النطاق غير صالح\",\n  \"form_error_equal\": \"يجب ألا تكون متساوية\",\n  \"form_error_gateway_ip\": \"لا يمكن الحصول على عنوان IP الخاص بالبوابة\",\n  \"form_error_ip4_format\": \"عنوان IPv4 غير صالح\",\n  \"form_error_ip4_gateway_format\": \"عنوان IPv4 غير صالح للبوابة\",\n  \"form_error_ip6_format\": \"عنوان IPv6 غير صالح\",\n  \"form_error_ip_format\": \"عنوان IP غير صحيح\",\n  \"form_error_mac_format\": \"عنوان MAC غير صالح\",\n  \"form_error_password\": \"كلمة السر غير مطابقة\",\n  \"form_error_password_length\": \"يجب أن تتكون كلمة المرور من {{min}} إلى {{max}} من الأحرف في الأقل\",\n  \"form_error_port\": \"أدخل رقم منفذ صالح\",\n  \"form_error_port_range\": \"أدخل رقم المنفذ في النطاق 80-65535\",\n  \"form_error_port_unsafe\": \"منفذ غير آمن\",\n  \"form_error_positive\": \"يجب أن يكون أكبر من 0\",\n  \"form_error_required\": \"الحقل مطلوب\",\n  \"form_error_server_name\": \"اسم الخادم غير صالح\",\n  \"form_error_subnet\": \"لا تحتوي الشبكة الفرعية \\\"{{cidr}}\\\" على عنوان IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"تنسيق رابط غير صالح\",\n  \"form_error_url_or_path_format\": \"عنوان URL أو المسار المطلق للقائمة غير صالح\",\n  \"form_select_tags\": \"حدد علامات العميل\",\n  \"found_in_known_domain_db\": \"تم العثور عليه في قاعدة بيانات دومينات معروفة.\",\n  \"friday\": \"الجمعة\",\n  \"friday_short\": \"الجمعة\",\n  \"gateway_or_subnet_invalid\": \"قناع الشبكة الفرعية غير صالح\",\n  \"general_settings\": \"الإعدادات العامة\",\n  \"general_statistics\": \"الإحصاءات العامة\",\n  \"get_started\": \"أبدأ\",\n  \"greater_range_start_error\": \"يجب أن يكون أكبر من نطاق البداية\",\n  \"homepage\": \"الصفحة الرئيسية\",\n  \"host_whitelisted\": \"المضيف مسموح به\",\n  \"ignore_domains\": \"المجالات التي تم تجاهلها (مفصولة بسطر جديد)\",\n  \"ignore_domains_desc_query\": \"لا تتم كتابة الاستعلامات المطابقة لهذه القواعد في سجل الاستعلامات\",\n  \"ignore_domains_desc_stats\": \"لا تتم كتابة الاستعلامات المطابقة لهذه القواعد في الإحصائيات\",\n  \"ignore_domains_title\": \"نطاقات تم تجاهلها\",\n  \"ignore_query_log\": \"تجاهل هذا العميل في سجل الاستعلام\",\n  \"ignore_statistics\": \"تجاهل هذا العميل في الإحصائيات\",\n  \"install_auth_confirm\": \"تأكيد كلمة المرور\",\n  \"install_auth_desc\": \"يجب إعداد مصادقة كلمة المرور لواجهة ويب مسؤول AdGuard Home. في حال كان AdGuard Home لا يمكن الوصول إليه إلا في شبكتك المحلية ، فلا يزال من المهم حمايته من الوصول غير المقيد.\",\n  \"install_auth_password\": \"الكلمة السرية\",\n  \"install_auth_password_enter\": \"أدخل كلمة المرور\",\n  \"install_auth_title\": \"المصادقة\",\n  \"install_auth_username\": \"اسم المستخدم\",\n  \"install_auth_username_enter\": \"أدخل اسم المستخدم\",\n  \"install_devices_address\": \"يستمع خادم AdGuard Home DNS إلى العناوين التالية\",\n  \"install_devices_android_list_1\": \"من الشاشة الرئيسية لقائمة Android ، انقر فوق الإعدادات.\",\n  \"install_devices_android_list_2\": \"اضغط على Wi-Fi في القائمة. ستظهر الشاشة التي تسرد جميع الشبكات المتاحة (من المستحيل تعيين DNS مخصص لاتصال المحمول).\",\n  \"install_devices_android_list_3\": \"اضغط لفترة طويلة على الشبكة التي تتصل بها ثم اضغط على تعديل الشبكة\",\n  \"install_devices_android_list_4\": \"في بعض الأجهزة قد تحتاج إلى تحديد المربع المتقدم لرؤية المزيد من الإعدادات لضبط إعدادات DNS لنظام اندرويد ستحتاج إلى تبديل إعدادات IP من DHCP إلى ثابت.\",\n  \"install_devices_android_list_5\": \"قم بتغيير قيم DNS 1 و DNS 2 المعينة لعناوين خادم AdGuard Home\",\n  \"install_devices_desc\": \"لبدء استخدام AdGuard Home، تحتاج إلى إعداد أجهزتك لاستخدامها.\",\n  \"install_devices_ios_list_1\": \"من الشاشة الرئيسية انقر فوق الإعدادات\",\n  \"install_devices_ios_list_2\": \"اختر Wi-Fi في القائمة اليسرى (من المستحيل ضبط الـ DNS لشبكات الجوال).\",\n  \"install_devices_ios_list_3\": \"اضغط على اسم الشبكة النشطة حاليًا.\",\n  \"install_devices_ios_list_4\": \"في حقل DNS ، أدخل عناوين خادم AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"انقر فوق أيقونة Apple وانتقل إلى تفضيلات النظام.\",\n  \"install_devices_macos_list_2\": \"اضغط على الشبكة.\",\n  \"install_devices_macos_list_3\": \"حدد الاتصال الأول في قائمتك وانقر فوق خيارات متقدمة.\",\n  \"install_devices_macos_list_4\": \"حدد علامة التبويب DNS وأدخل عناوين خادم AdGuard Home.\",\n  \"install_devices_router\": \"راوتر\",\n  \"install_devices_router_desc\": \"يغطي هذا الإعداد تلقائيا جميع الأجهزة المتصلة بجهاز التوجيه المنزلي، دون الحاجة إلى تكوين كل منها يدويا.\",\n  \"install_devices_router_list_1\": \"افتح تفضيلات جهاز التوجيه الخاص بك. عادة، يمكنك الوصول إليه من متصفحك عبر عنوان URL، مثل http://192.168.0.1/ أو http://192.168.1.1/. قد يطلب منك إدخال كلمة مرور. إذا كنت لا تتذكر ذلك، يمكنك في كثير من الأحيان إعادة تعيين كلمة المرور عن طريق الضغط على زر في جهاز التوجيه نفسه، ولكن كن على علم بأنه إذا تم اختيار هذا الإجراء، فمن المحتمل أن تفقد إعدادات جهاز التوجيه بأكمله. إذا كان جهاز التوجيه الخاص بك يتطلب تطبيقا لإعداده، فيرجى تثبيت التطبيق على هاتفك أو الكمبيوتر الشخصي واستخدامه للوصول إلى إعدادات جهاز التوجيه.\",\n  \"install_devices_router_list_2\": \"ابحث عن إعدادات DHCP / DNS. ابحث عن أحرف DNS بجوار الحقل الذي يسمح بمجموعتين أو ثلاث مجموعات من الأرقام ، كل واحدة مقسمة إلى أربع مجموعات من واحد إلى ثلاثة أرقام.\",\n  \"install_devices_router_list_3\": \"أدخل عناوين خادم AdGuard Home هناك.\",\n  \"install_devices_router_list_4\": \"في بعض أنواع أجهزة التوجيه ، لا يمكن إعداد خادم DNS مخصص. في هذه الحالة ، قد يساعد إعداد AdGuard Home باعتباره <0>خادم DHCP</0>. بخلاف ذلك ، يجب عليك التحقق من دليل جهاز التوجيه حول كيفية تخصيص خوادم DNS على طراز جهاز التوجيه المحدد الخاص بك.\",\n  \"install_devices_title\": \"قم بإعداد أجهزتك\",\n  \"install_devices_windows_list_1\": \"افتح لوحة التحكم من خلال قائمة ابدأ أو بحث Windows.\",\n  \"install_devices_windows_list_2\": \"انتقل إلى فئة الشبكة والإنترنت ثم إلى مركز الشبكة والمشاركة.\",\n  \"install_devices_windows_list_3\": \"على الجانب الأيسر من الشاشة ، ابحث عن \\\"تغيير إعدادات المحول\\\" وانقر عليها.\",\n  \"install_devices_windows_list_4\": \"حدد اتصالك النشط ، وانقر فوقه بزر الماوس الأيمن واختر خصائص.\",\n  \"install_devices_windows_list_5\": \"ابحث عن \\\"Internet Protocol Version 4 (TCP / IPv4)\\\" (أو ، لـ IPv6 ، \\\"Internet Protocol Version 6 (TCP / IPv6)\\\") في القائمة ، حدده ثم انقر فوق خصائص مرة أخرى.\",\n  \"install_devices_windows_list_6\": \"اختر \\\"استخدام عناوين خادم DNS التالية\\\" وأدخل عناوين خادم AdGuard Home.\",\n  \"install_saved\": \"تم الحفظ بنجاح\",\n  \"install_settings_all_interfaces\": \"جميع الواجهات\",\n  \"install_settings_dns\": \"خَادِم DNS\",\n  \"install_settings_dns_desc\": \"ستحتاج إلى ضبط أجهزتك أو جهاز التوجيه الخاص بك لاستخدام خادم DNS على العناوين التالية:\",\n  \"install_settings_interface_link\": \"ستكون واجهة الويب الخاصة بمسؤول AdGuard Home متاحة على العناوين التالية:\",\n  \"install_settings_listen\": \"واجهة الاستماع\",\n  \"install_settings_port\": \"المنفذ\",\n  \"install_settings_title\": \"واجهة ويب المسؤول\",\n  \"install_static_configure\": \"اكتشف AdGuard Home استخدام عنوان IP الديناميكي <0> {{ip}} </0>. هل تريد تعيينه كعنوان ثابت؟\",\n  \"install_static_error\": \"لا يمكن لـ AdGuard Home تكوينه تلقائيًا لواجهة الشبكة هذه. الرجاء البحث عن تعليمات حول كيفية القيام بذلك يدويًا.\",\n  \"install_static_ok\": \"أخبار جيدة! تم ضبط عنوان IP الثابت بالفعل\",\n  \"install_step\": \"خطوة\",\n  \"install_submit_desc\": \"انتهى إجراء الإعداد وأنت على استعداد لبدء استخدام AdGuard Home\",\n  \"install_submit_title\": \"تهانينا!\",\n  \"install_welcome_desc\": \"AdGuard Home هو إعلان ومتتبع على مستوى الشبكة يمنع خادم DNS. الغرض منه هو السماح لك بالتحكم في شبكتك بأكملها وجميع أجهزتك، ولا يتطلب استخدام برنامج من جانب العميل.\",\n  \"install_welcome_title\": \"مرحبًا بك في AdGuard Home!\",\n  \"interval_24_hour\": \"24 ساعة\",\n  \"interval_6_hour\": \"6 ساعات\",\n  \"interval_days\": \"{{count}} يوم\",\n  \"interval_days_plural\": \"{{count}} الأيام\",\n  \"interval_hours\": \"{{count}} ساعة\",\n  \"interval_hours_plural\": \"{{count}} ساعات\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"عنوان IP\",\n  \"known_tracker\": \"متعقب معروف\",\n  \"last_rule_in_allowlist\": \"لا يمكن منع هذا العميل لأن استبعاد القاعدة \\\"{{disallowed_rule}}\\\" سيؤدي إلى تعطيل قائمة \\\"العملاء المسموح لهم\\\".\",\n  \"last_time_updated_table_header\": \"آخر تحديث\",\n  \"list_confirm_delete\": \"هل أنت متأكد أنك تريد حذف هذه القائمة؟\",\n  \"list_label\": \"قائمه\",\n  \"list_updated\": \"قائمة {{count}} محدثة\",\n  \"list_updated_plural\": \"قوائم {{count}} محدثة\",\n  \"list_url_table_header\": \"قائمة الروابط\",\n  \"load_balancing\": \"موازنة الأحمال\",\n  \"load_balancing_desc\": \"استعلام عن خادم واحد في كل مرة.<br/>يستخدم AdGuard Home خوارزمية عشوائية مرجحة لاختيار الخوادم ذات أقل عدد من عمليات البحث الفاشلة وأقل متوسط زمن للاستعلام.\",\n  \"loading_table_status\": \"التحميل جارٍ...\",\n  \"local_ptr_default_resolver\": \"بشكل افتراضي ، يستخدم AdGuard Home محللات DNS العكسية التالية: {{ip}}.\",\n  \"local_ptr_desc\": \"يتم استخدام خوادم DNS من قبل AdGuard Home لطلبات PTR و SOA و NS الخاصة. يعتبر الطلب خاصًا إذا طلب مجال APRA يحتوي على نِقَاط فرعية ضمن نطاقات IP خاصة (مثل \\\"192.168.12.34\\\") ويأتي من عميل له عنوان IP خاص. إذا لم يتم تعيينها، فسيتم استخدام محللات DNS الافتراضية الخاصة بنظام تشغيلك، باستثناء عناوين IP الخاصة بـ AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"لم يتمكن AdGuard Home من تحديد محللات DNS العكسية المناسبة لهذا النظام.\",\n  \"local_ptr_placeholder\": \"أدخل عنوان IP واحد لكل سطر\",\n  \"local_ptr_title\": \"خوادم DNS العكسية الخاصة\",\n  \"location\": \"الموقع\",\n  \"log_and_stats_section_label\": \"سجل الاستعلام والإحصائيات\",\n  \"lower_range_start_error\": \"يجب أن يكون أقل من نطاق البداية\",\n  \"main_settings\": \"الاعدادات الرئيسية\",\n  \"make_static\": \"اجعلها ثابتة\",\n  \"manual_update\": \"الرجاء <a> اتباع هذه الخطوات </a> للتحديث يدويًا.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"الإثنين\",\n  \"monday_short\": \"الإثنين\",\n  \"name\": \"اسم\",\n  \"name_table_header\": \"الاِسْم\",\n  \"netname\": \"اسم الشبكة\",\n  \"network\": \"الشبكة\",\n  \"new_allowlist\": \"قائمة السماح الجديدة\",\n  \"new_blocklist\": \"قائمة حظر جديدة\",\n  \"next\": \"التالي\",\n  \"next_btn\": \"التالي\",\n  \"no_blocklist_added\": \"لم يتم إضافة قوائم الحظر\",\n  \"no_clients_found\": \"لم يتم العثور على عملاء\",\n  \"no_domains_found\": \"لم يتم العثور على النطاق\",\n  \"no_logs_found\": \"لم يتم العثور على سجلات\",\n  \"no_servers_specified\": \"لم يتم تحديد خوادم\",\n  \"no_upstreams_data_found\": \"لم يتم العثور على بيانات خوادم upstream\",\n  \"no_whitelist_added\": \"لم تتم إضافة قوائم السماح\",\n  \"nothing_found\": \"لم يتم العثور على شيء\",\n  \"null_ip\": \"عنوان IP فارغ\",\n  \"number_of_dns_query_blocked_24_hours\": \"عدد طلبات DNS المحظورة بواسطة فلاتر adblock وقوائم حظر المضيفين\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"عدد من المواقع (الإباحية) للبالغين تم حجبها\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"عدد طلبات DNS التي تم حظرها من قبل وحدة أمان التصفح AdGuard\",\n  \"number_of_dns_query_days\": \"عدد استعلامات DNS التي تمت معالجتها لآخر {{count}} يوم\",\n  \"number_of_dns_query_days_plural\": \"عدد استعلامات DNS التي تمت معالجتها لآخر {{count}} أيام\",\n  \"number_of_dns_query_hours\": \"عدد استفسارات DNS التي تمت معالجتها لآخر {{count}} ساعة\",\n  \"number_of_dns_query_hours_plural\": \"عدد استعلامات DNS التي تمت معالجتها خلال آخر {{count}} ساعة\",\n  \"number_of_dns_query_to_safe_search\": \"عدد طلبات DNS لمحركات البحث التي تم فرض البحث الآمن عنها\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"قيد الإيقاف\",\n  \"on\": \"قيد التشغيل\",\n  \"open_dashboard\": \"افتح لوحة التحكم\",\n  \"orgname\": \"اسم المنظمة\",\n  \"original_response\": \"الرد الأصلي\",\n  \"out_of_range_error\": \"يجب أن يكون خارج النطاق \\\"{{start}}\\\" - \\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"الصفحة\",\n  \"parallel_requests\": \"الطلبات الموازية\",\n  \"parental_control\": \"الرِّقابة الأبوية\",\n  \"password_label\": \"كلمة المرور\",\n  \"password_placeholder\": \"ادخل كلمة المرور\",\n  \"plain_dns\": \"عنوان DNS العادي\",\n  \"port_53_faq_link\": \"غالبًا ما يتم احتلال المنفذ 53 بواسطة خدمات \\\"DNSStubListener\\\" أو \\\"حل النظام\\\". يرجى قراءة <0> هذه التعليمات </0> حول كيفية حل هذه المشكلة.\",\n  \"previous_btn\": \"السابق\",\n  \"privacy_policy\": \"سياسة الخصوصية\",\n  \"processing_update\": \"يُرجى الانتظار ، يتم تحديث صفحة AdGuard الرئيسية\",\n  \"protection_section_label\": \"الحماية\",\n  \"protocol\": \"البروتوكول\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"سجل الQuery\",\n  \"query_log_clear\": \"مسح سجلات الاستعلام\",\n  \"query_log_cleared\": \"تم مسح سجل الاستعلام بنجاح\",\n  \"query_log_configuration\": \"تكوين السجلات\",\n  \"query_log_confirm_clear\": \"هل أنت متأكد من أنك تريد محو كامل سجل التصفية؟\",\n  \"query_log_disabled\": \"سجل الاستعلام معطل ويمكن تهيئته من<0>الاعدادات</0>\",\n  \"query_log_enable\": \"تمكين السجل\",\n  \"query_log_filtered\": \"تم الفلترة بواسطة {{filter}}\",\n  \"query_log_response_status\": \"الحالات: {{value}}\",\n  \"query_log_retention\": \"تناوب سجلات الاستعلام\",\n  \"query_log_retention_confirm\": \"هل أنت متيقِّن من أنك تريد تغيير دوران سجل الاستعلام؟ إذا قمت بتقليل قيمة الفاصل الزمني، ستفقد بعض البيانات\",\n  \"query_log_strict_search\": \"استخدم علامات الاقتباس المزدوجة للبحث الدقيق\",\n  \"query_log_updated\": \"تم تحديث سجل الاستعلام بنجاح\",\n  \"rate_limit\": \"حدود التقييم\",\n  \"rate_limit_desc\": \"عدد الطلبات في الثانية المسموح بها لكل عميل. جعله على 0 يعني عدم وجود حد.\",\n  \"rate_limit_subnet_len_ipv4\": \"طول بادئة الشبكة لعناوين IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"طول بادئة الشبكة لعناوين IPv4 المستخدمة لتحديد معدل الحد الأقصى. الافتراضي هو 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"يجب أن يكون طول بادئة الشبكة IPv4 بين 0 و 32\",\n  \"rate_limit_subnet_len_ipv6\": \"طول بادئة الشبكة لعناوين IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"طول بادئة الشبكة لعناوين IPv6 المستخدمة لتحديد معدل الحد الأقصى. الافتراضي هو 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"يجب أن يكون طول بادئة الشبكة IPv6 بين 0 و 128\",\n  \"rate_limit_whitelist\": \"قائمة السماح بتحديد معدل الحد الأقصى\",\n  \"rate_limit_whitelist_desc\": \"عناوين IP المستبعدة من تحديد معدل الحد الأقصى\",\n  \"rate_limit_whitelist_placeholder\": \"أدخل عنوان IP واحد لكل سطر\",\n  \"refresh_btn\": \"تحديث\",\n  \"refresh_statics\": \"تحيين الإحصائيات\",\n  \"refused\": \"مرفوض\",\n  \"report_an_issue\": \"الإبلاغ عن مشكلة\",\n  \"request_details\": \"طلب التفاصيل\",\n  \"request_table_header\": \"طلب\",\n  \"requests_count\": \"عدد الطلبات\",\n  \"reset_settings\": \"إعادة ضبط الإعدادات\",\n  \"resolve_clients_desc\": \"حل عكسيًا لعناوين IP للعملاء في أسماء مضيفيهم عن طريق إرسال استعلامات PTR إلى أدوات الحل المقابلة (خوادم DNS الخاصة للعملاء المحليين ، والخوادم الأولية للعملاء الذين لديهم عناوين IP عامة).\",\n  \"resolve_clients_title\": \"تفعيل التحليل العكسي لعناوين IP للعملاء\",\n  \"response_code\": \"كود الاستجابة\",\n  \"response_details\": \"تفاصيل الاستجابة\",\n  \"response_table_header\": \"استجابة\",\n  \"response_time\": \"وقت الاستجابة\",\n  \"rewrite_A\": \"<0> A </0>: قيمة خاصة ، احتفظ بسجلات <0> A </0> من upstream\",\n  \"rewrite_AAAA\": \"<0> AAAA </0>: قيمة خاصة ، احتفظ بسجلات <0> AAAA </0> من upstream\",\n  \"rewrite_add\": \"إضافة إعادة كتابة DNS\",\n  \"rewrite_added\": \"تمت إضافة إعادة كتابة DNS لـ \\\"{{key}}\\\" بنجاح\",\n  \"rewrite_applied\": \"يتم تطبيق قاعدة إعادة الكتابة\",\n  \"rewrite_confirm_delete\": \"هل أنت متأكد من أنك تريد حذف إعادة كتابة DNS لـ \\\"{{key}}\\\"؟\",\n  \"rewrite_deleted\": \"تم حذف إعادة كتابة DNS لـ \\\"{{key}}\\\" بنجاح\",\n  \"rewrite_desc\": \"يسمح بتكوين استجابة DNS المخصصة بسهولة لاسم نطاق معين.\",\n  \"rewrite_domain_name\": \"اسم النطاق: أضف سجل CNAME\",\n  \"rewrite_edit\": \"تحرير إعادة كتابة DNS\",\n  \"rewrite_hosts_applied\": \"أعيد كتابتها بواسطة قاعدة ملف المضيفين\",\n  \"rewrite_ip_address\": \"عنوان IP: استخدم عنوان IP هذا في استجابة A أو AAAA\",\n  \"rewrite_not_found\": \"لم يتم العثور على إعادة كتابة DNS\",\n  \"rewrite_settings_updated\": \"تم تحديث إعدادات إعادة كتابة DNS بنجاح\",\n  \"rewrite_updated\": \"تم تحديث إعادة كتابة DNS بنجاح\",\n  \"rewrites_disabled_table_header\": \"تم تعطيل عمليات إعادة الكتابة\",\n  \"rewrites_enabled_table_header\": \"إعادة الكتابة مفعلة\",\n  \"rewritten\": \"أعيدت كتابته\",\n  \"rows_table_footer_text\": \"صفوف\",\n  \"rule_added_to_custom_filtering_toast\": \"تم إضافة إلى قواعد الفلترة المخصصة: {{rule}}\",\n  \"rule_label\": \"قاعدة (قواعد)\",\n  \"rule_removed_from_custom_filtering_toast\": \"تم إزالة قاعدة من قواعد الفلترة المخصصة: {{rule}}\",\n  \"rules_count_table_header\": \"عدد القواعد\",\n  \"safe_browsing\": \"تصفح آمن\",\n  \"safe_search\": \"البحث الآمن\",\n  \"saturday\": \"السبت\",\n  \"saturday_short\": \"السبت\",\n  \"save_btn\": \"حفظ\",\n  \"save_config\": \"حفظ الإعدادات\",\n  \"schedule_add\": \"إضافة جدول زمني\",\n  \"schedule_current_timezone\": \"المنطقة الزمنية الحالية: {{value}}\",\n  \"schedule_desc\": \"تعيين فترات عدم النشاط للخدمات المحظورة\",\n  \"schedule_edit\": \"تحرير الجدول الزمني\",\n  \"schedule_from\": \"من\",\n  \"schedule_invalid_select\": \"يجب أن يكون وقت البَدْء قبل وقت الانتهاء\",\n  \"schedule_modal_description\": \"سيحل هذا الجدول الزمني محل أي جداول موجودة لنفس اليوم من الأسبوع. يمكن أن يكون لكل يوم من أيام الأسبوع مدّة خمول واحدة فقط.\",\n  \"schedule_modal_time_off\": \"لا يوجد حظر للخدمة:\",\n  \"schedule_new\": \"جدول زمني جديد\",\n  \"schedule_remove\": \"إزالة الجدول الزمني\",\n  \"schedule_save\": \"حفظ الجدول الزمني\",\n  \"schedule_select_days\": \"اختر الأيام\",\n  \"schedule_services\": \"إيقاف حظر الخدمة مؤقتًا\",\n  \"schedule_services_desc\": \"تهيئة جدول إيقاف فلتر خدمة الحظر\",\n  \"schedule_services_desc_client\": \"تهيئة جدول إيقاف فلتر خدمة الحظر لهذا العميل\",\n  \"schedule_time_all_day\": \"طوال اليوم\",\n  \"schedule_timezone\": \"قم باختيار منطقة زمنية\",\n  \"schedule_to\": \"إلى\",\n  \"served_from_cache_label\": \"يتم تقديمه من ذاكرة التخزين المؤقت\",\n  \"service_name\": \"أسم الخدمة\",\n  \"set_static_ip\": \"قم بتعيين عنوان IP ثابت\",\n  \"settings\": \"الإعدادات\",\n  \"settings_custom\": \"مخصص\",\n  \"settings_global\": \"عالمي\",\n  \"setup_config_to_enable_dhcp_server\": \"أضبط الاعدادات لتمكين خادم DHCP\",\n  \"setup_dns_notice\": \"من أجل استخدام <0> DNS-over-HTTPS </0> أو <1> DNS-over-TLS </1> ، تحتاج إلى <1> تكوين التشفير </1> في إعدادات AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0> DNS-over-TLS: </0> استخدم سلسلة <1> {{address}} </1>.\",\n  \"setup_dns_privacy_2\": \"<0> DNS-over-HTTPS: </0> استخدم سلسلة <1> {{address}} </1>.\",\n  \"setup_dns_privacy_3\": \"<0> فيما يلي قائمة بالبرامج التي يمكنك استخدامها. </0>\",\n  \"setup_dns_privacy_4\": \"على جهاز iOS 14 أو macOS Big Sur ، يمكنك تنزيل ملف \\\".mobileconfig\\\" خاص يضيف خوادم <highlight> DNS-over-HTTPS </highlight> أو <highlight> DNS-over-TLS </highlight> إلى إعدادات DNS.\",\n  \"setup_dns_privacy_android_1\": \"يدعم Android 9 DNS-over-TLS أصلاً. لتكوينه ، انتقل إلى الإعدادات → الشبكة والإنترنت → متقدم → DNS الخاص وأدخل اسم المجال الخاص بك هناك.\",\n  \"setup_dns_privacy_android_2\": \"<0> AdGuard لنظام Android </0> يدعم <1> DNS-over-HTTPS </1> و <1> DNS-over-TLS </1>.\",\n  \"setup_dns_privacy_android_3\": \"<0> Intra </0> يضيف دعم <1> DNS-over-HTTPS </1> إلى Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"اعدادات iOS و macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0> DNSCloak </0> يدعم <1> DNS-over-HTTPS </1> ، ولكن من أجل تكوينه لاستخدام الخادم الخاص بك ، ستحتاج إلى إنشاء <2> DNS Stamp </2> لذلك.\",\n  \"setup_dns_privacy_ios_2\": \"<0> AdGuard لنظام iOS </0> يدعم إعداد <1> DNS-over-HTTPS </1> و <1> DNS-over-TLS </1> الإعداد.\",\n  \"setup_dns_privacy_other_1\": \"يمكن أن يكون AdGuard Home نفسه عميل DNS آمنًا على أي نظام أساسي.\",\n  \"setup_dns_privacy_other_2\": \"يدعم <0> dnsproxy </0> جميع بروتوكولات DNS الآمنة المعروفة.\",\n  \"setup_dns_privacy_other_3\": \"<0> dnscrypt-proxy </0> يدعم <1> DNS-over-HTTPS </1>.\",\n  \"setup_dns_privacy_other_4\": \"يدعم <0> Mozilla Firefox </0> <1> DNS-over-HTTPS </1>.\",\n  \"setup_dns_privacy_other_5\": \"ستجد المزيد من التطبيقات <0> هنا </0> و <1> هنا </1>.\",\n  \"setup_dns_privacy_other_title\": \"تطبيقات أخرى\",\n  \"setup_guide\": \"دليل الإعداد\",\n  \"show_all_filter_type\": \"إظهار الكل\",\n  \"show_blocked_responses\": \"ما تمّ حظره\",\n  \"show_filtered_type\": \"إظهار ماتمت تصفيته\",\n  \"show_processed_responses\": \"تمت معالجتها\",\n  \"show_whitelisted_responses\": \"المسموح به\",\n  \"sign_in\": \"تسجيل الدخول\",\n  \"sign_out\": \"تسجيل الخروج\",\n  \"source_label\": \"المصدر\",\n  \"static_ip\": \"عنوان IP ثابت\",\n  \"static_ip_desc\": \"AdGuard Home هو خادم لذلك يحتاج إلى عنوان IP ثابت ليعمل بشكل صحيح. خلاف ذلك ، في مرحلة ما ، قد يقوم جهاز التوجيه الخاص بك بتعيين عنوان IP مختلف لهذا الجهاز.\",\n  \"statistics_clear\": \"إعادة تعيين الإحصائيات\",\n  \"statistics_clear_confirm\": \"هل أنت متيقِّن من أنك تريد مسح الإحصائيات؟\",\n  \"statistics_cleared\": \"تم مسح الإحصائيات بنجاح\",\n  \"statistics_configuration\": \"تكوين الإحصائيات\",\n  \"statistics_enable\": \"تفعيل الاحصائيات\",\n  \"statistics_retention\": \"الاحتفاظ بالإحصاءات\",\n  \"statistics_retention_confirm\": \"هل أنت متأكد أنك تريد تغيير الاحتفاظ بالإحصاءات؟ إذا قمت بتقليل قيمة الفاصل الزمني ، فستفقد بعض البيانات\",\n  \"statistics_retention_desc\": \"إذا قمت بتقليل قيمة الفاصل الزمني ، فستفقد بعض البيانات\",\n  \"stats_adult\": \"حظر مواقع الويب الخاصة بالبالغين\",\n  \"stats_disabled\": \"تم تعطيل الإحصائيات. يمكنك تشغيله من <0> صفحة الإعدادات </0>.\",\n  \"stats_disabled_short\": \"تم تعطيل الإحصائيات\",\n  \"stats_malware_phishing\": \"حسر البرامج الضارة / والتصيّد\",\n  \"stats_params\": \"ضبط الاحصائيات\",\n  \"stats_query_domain\": \"اعلى النطاقات التي تم الاستعلام عنها\",\n  \"subnet_error\": \"يجب أن تكون العناوين في شبكة فرعية واحدة\",\n  \"sunday\": \"الأحد\",\n  \"sunday_short\": \"الأحد\",\n  \"system_host_files\": \"ملفات الهوست للنظام\",\n  \"table_client\": \"العميل\",\n  \"table_name\": \"الاسم\",\n  \"tags_desc\": \"يمكنك تحديد العلامات التي تتوافق مع العميل. قم بتضمين العلامات في قواعد التصفية لتطبيقها بدقة أكبر. <0> معرفة المزيد </0>.\",\n  \"tags_title\": \"وسوم\",\n  \"test_upstream_btn\": \"اختبار upstream\",\n  \"theme_auto\": \"تلقائي\",\n  \"theme_auto_desc\": \"تلقائي (بناءً على نظام ألوان جهازك)\",\n  \"theme_dark\": \"داكن\",\n  \"theme_dark_desc\": \"المظهر الداكن\",\n  \"theme_light\": \"فاتح\",\n  \"theme_light_desc\": \"المظهر فاتح\",\n  \"thursday\": \"الخميس\",\n  \"thursday_short\": \"الخميس\",\n  \"time_table_header\": \"الوقت\",\n  \"top_blocked_domains\": \"اعلى الدومينات المحظورة\",\n  \"top_clients\": \"كبار العملاء\",\n  \"top_upstreams\": \"أعلى الخوادم upstream\",\n  \"topline_expired_certificate\": \"انتهت صلاحية شهادة SSL الخاصة بك. قم بتحديث <0>إعدادات التشفير</0>.\",\n  \"topline_expiring_certificate\": \"شهادة SSL الخاصة بك على وشك الانتهاء. قم بتحديث <0>إعدادات التشفير</0>.\",\n  \"tracker_source\": \"مصدر المتعقب\",\n  \"try_again\": \"حاول مرة أخرى\",\n  \"ttl_cache_validation\": \"يجب أن يكون الحد الأدنى لتجاوز TTL لذاكرة التخزين المؤقت أقل من أو يساوي الحد الأقصى\",\n  \"tuesday\": \"الثلاثاء\",\n  \"tuesday_short\": \"الثلاثاء\",\n  \"type_table_header\": \"النوع\",\n  \"unavailable_dhcp\": \"DHCP غير متوفر\",\n  \"unavailable_dhcp_desc\": \"لا يمكن لـ AdGuard Home تشغيل خادم DHCP على نظام التشغيل الخاص بك\",\n  \"unblock\": \"إلغاء الحظر\",\n  \"unblock_all\": \"إلغاء حجب الكل\",\n  \"unblock_for_this_client_only\": \"إلغاء حجب هذا العميل فقط\",\n  \"unknown_filter\": \"فلتر غير معروف {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} متوفر الآن! <0>انقر هنا</0> لمزيد من المعلومات.\",\n  \"update_failed\": \"فشل التحديث التلقائي. الرجاء <a> اتباع هذه الخطوات </a> للتحديث يدويًا.\",\n  \"update_now\": \"تحديث الآن\",\n  \"updated_custom_filtering_toast\": \"تحديث قواعد الفلترة المخصصة\",\n  \"updated_save_search_toast\": \"تم تحديث إعدادات البحث الآمن\",\n  \"updated_upstream_dns_toast\": \"تم حفظ خوادم Upstream بنجاح\",\n  \"updates_checked\": \"يتوفر إصدار جديد من AdGuard Home\",\n  \"updates_version_equal\": \"AdGuard Home محدث\",\n  \"upstream\": \"Upstream\",\n  \"upstream_dns\": \"خادم DNS لـ Upstream\",\n  \"upstream_dns_cache_configuration\": \"تهيئة ذاكرة التخزين المؤقت لنظام أسماء النطاقات المستقبلي\",\n  \"upstream_dns_client_desc\": \"إذا احتفظت بهذا الحقل فارغًا ، فسيستخدم AdGuard Home الخوادم التي تم تكوينها في<0>DNS إعدادات</0>.\",\n  \"upstream_dns_configured_in_file\": \"تم اعداده في {{path}}\",\n  \"upstream_dns_help\": \"أدخل عنوان خادم واحد في كل سطر. <a>تعرف على المزيد</a> حول تكوين خوادم DNS الأولية.\",\n  \"upstream_parallel\": \"استخدم الاستعلامات المتوازية لتسريع عملية الحل عن طريق الاستعلام عن جميع الخوادم المنبع في وقت واحد.\",\n  \"upstream_timeout\": \"مهلة الاتصال بالخادم العلوي\",\n  \"upstream_timeout_desc\": \"يحدد عدد الثواني المنتظرة لاستجابة الخادم العلوي\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"استخدم خدمة الويب الأمنية لتصفح AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"سيتحقق AdGuard Home مما إذا كان النطاق محظورًا بواسطة خدمة الويب الخاصة بأمان التصفح. سيستخدم واجهة برمجة تطبيقات بحث صديقة للخصوصية لإجراء الفحص: يتم إرسال بادئة قصيرة فقط من تجزئة اسم المجال SHA256 إلى الخادم.\",\n  \"use_adguard_parental\": \"استخدام خدمة AdGuard للرقابة الأبوية على الويب\",\n  \"use_adguard_parental_hint\": \"سيتحقق AdGuard Home مما إذا كان النطاق يحتوي على محتوى للبالغين. إنه يستخدم نفس واجهة برمجة التطبيقات الصديقة للخصوصية مثل خدمة الويب الأمنية للتصفح.\",\n  \"use_private_ptr_resolvers_desc\": \"حل طلبات PTR و SOA و NS لنطاقات ARPA التي تحتوي على عناوين IP خاصة من خلال خوادم المنبع الخاصة و DHCP و /etc/المضيفات، إلخ. إذا تم تعطيلها، سيستجيب AdGuard Home لجميع هذه الطلبات باستخدام NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"استخدم محللات DNS العكسية الخاصة\",\n  \"use_saved_key\": \"استخدم المفتاح المحفوظ مسبقًا\",\n  \"username_label\": \"اسم المستخدم\",\n  \"username_placeholder\": \"ادخل اسم المستخدم\",\n  \"validated_with_dnssec\": \"تم التحقق من صحتها باستخدام DNSSEC\",\n  \"version\": \"الإصدار\",\n  \"version_request_error\": \"فشل التحقق من التحديث. يرجى التحقق من اتصالك بالإنترنت.\",\n  \"wednesday\": \"الأربعاء\",\n  \"wednesday_short\": \"الأربعاء\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/be.json",
    "content": "{\n  \"access_allowed_desc\": \"Спіс CIDR, IP-адрасоў або <a>ClientID</a>. Калі ў гэтым спісе ёсць запісы, AdGuard Home будзе прымаць запыты толькі ад гэтых кліентаў.\",\n  \"access_allowed_title\": \"Дазволеныя кліенты\",\n  \"access_blocked_desc\": \"Не блытаць з фільтрамі. AdGuard Home будзе ігнараваць запыты DNS, якія адпавядаюць гэтым даменам. Яны нават не з'явяцца ў журнале запытаў. Вы можаце ўказаць дакладныя даменныя імёны, падстаноўчыя знакі або правілы фільтрацыі URL-адрасоў, напрыклад, «example.org», «*.example.org» або «||example.org^».\",\n  \"access_blocked_title\": \"Забароненыя дамены\",\n  \"access_desc\": \"Тут вы можаце наладзіць правілы доступу да сервер DNSу AdGuard Home\",\n  \"access_disallowed_desc\": \"Спіс CIDR, IP-адрасоў або <a>ClientID</a>. Калі ў гэтым спісе ёсць запісы, AdGuard Home будзе скасоўваць запыты ад гэтых кліентаў. Гэта поле ігнаруецца, калі спіс дазволеных кліентаў змяшчае запісы.\",\n  \"access_disallowed_title\": \"Забароненыя кліенты\",\n  \"access_settings_saved\": \"Налады доступу паспяхова захаваны\",\n  \"access_title\": \"Налады доступу\",\n  \"actions_table_header\": \"Дзеянні\",\n  \"add_allowlist\": \"Дадаць у спіс дазволеных\",\n  \"add_blocklist\": \"Дадаць у спіс заблакіраваных\",\n  \"add_custom_list\": \"Дадаць карыстальніцкі спіс\",\n  \"add_persistent_client\": \"Дадаць як пастаяннага кліента\",\n  \"address\": \"Адрас\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home скіне ўсе DNS-запыты ад гэтага кліента.\",\n  \"all_lists_up_to_date_toast\": \"Усе спісы ўжо абноўлены\",\n  \"all_queries\": \"Усе запыты\",\n  \"allow_this_client\": \"Дазволіць гэтага кліента\",\n  \"allowed\": \"Дазволеныя\",\n  \"anonymize_client_ip\": \"Ананімізацыя IP-адрасы кліента\",\n  \"anonymize_client_ip_desc\": \"Не захоўвайце поўны IP-адрас кліента ў журналах або статыстыцы\",\n  \"anonymizer_notification\": \"<0>Заўвага:</0> Ананімізацыя IP уключана. Вы можаце адключыць яе ў <1>Агульных наладах</1>.\",\n  \"answer\": \"Адказ\",\n  \"apply_btn\": \"Ужыць\",\n  \"auto_clients_desc\": \"Інфармацыя аб IP-адрасах прылад, якія выкарыстоўваюць або могуць выкарыстоўваць AdGuard Home. Гэта інфармацыя збіраецца з некалькіх крыніц, уключаючы файлы вузлоў, зваротны DNS і г.д.\",\n  \"auto_clients_title\": \"Кліенты Runtime\",\n  \"autofix_warning_list\": \"Будуць выконвацца наступныя заданні: <0>Дэактываваць сістэмны DNSStubListener</0> <0>Усталяваць адрас сервера DNS на 127.0.0.1</0> <0>Стварыць сімвалічную спасылку /etc/resolv.conf на /run/systemd/resolve/resolv.conf</0> <0>Спыніць DNSStubListener (перазагрузіць сістэмную службу)</0>.\",\n  \"autofix_warning_result\": \"У выніку ўсе запыты DNS ад вашай сістэмы будуць прадвызначана апрацоўвацца AdGuard Home.\",\n  \"autofix_warning_text\": \"Пры націску «Выправіць» AdGuard Home сканфігурыруе вашу сістэму для выкарыстання сервера DNS ад AdGuard Home.\",\n  \"average_processing_time\": \"Сярэдні час апрацоўкі запыту\",\n  \"average_processing_time_hint\": \"Сярэдні час для апрацоўкі запыту DNS  у мілісекундах\",\n  \"average_upstream_response_time\": \"Сярэдні час водгуку upstream-сервера\",\n  \"back\": \"Назад\",\n  \"block\": \"Заблакіраваць\",\n  \"block_all\": \"Заблакіраваць усё\",\n  \"block_domain_use_filters_and_hosts\": \"Блакаваць дамены з выкарыстаннем фільтраў і файлаў хастоў\",\n  \"block_for_this_client_only\": \"Заблакаваць толькі для гэтага кліента\",\n  \"block_services\": \"Выбраць заблакаваныя сэрвісы\",\n  \"blocked_adult_websites\": \"Заблакавана Бацькоўскім кантролем\",\n  \"blocked_by\": \"<0>Заблакіравана фільтрамі</0>\",\n  \"blocked_by_cname_or_ip\": \"Заблакавана з дапамогай CNAME ці IP\",\n  \"blocked_by_response\": \"Заблакавана па CNAME ці IP у адказе\",\n  \"blocked_response_ttl\": \"Заблакіраваны адказ TTL\",\n  \"blocked_response_ttl_desc\": \"Паказвае, на працягу колькіх секунд кліенты павінны кэшаваць адфільтраваць адказ\",\n  \"blocked_safebrowsing\": \"Заблакіравана модулем «Бяспека прагляду»\",\n  \"blocked_service\": \"Заблакіраваны сэрвіс\",\n  \"blocked_services\": \"Заблакіраваныя сэрвісы\",\n  \"blocked_services_desc\": \"Дазваляе хутка заблакаваць папулярныя сайты і сэрвісы.\",\n  \"blocked_services_global\": \"Выкарыстаць глабальныя заблакаваныя сэрвісы\",\n  \"blocked_services_saved\": \"Заблакаваныя сэрвісы паспяхова захаваны\",\n  \"blocked_threats\": \"Заблакіравана пагроз\",\n  \"blocking_ipv4\": \"Блакіроўка IPv4\",\n  \"blocking_ipv4_desc\": \"IP-адрас, што вяртаецца пры блакаванню A-запыту\",\n  \"blocking_ipv6\": \"Блакіроўка IPv6\",\n  \"blocking_ipv6_desc\": \"IP-адрас, што вяртаецца пры блакаванню AAAA-запыту\",\n  \"blocking_mode\": \"Рэжым блакіроўкі\",\n  \"blocking_mode_custom_ip\": \"Карыстацкі IP: Адказвае з ручна наладжаным IP-адрасам\",\n  \"blocking_mode_default\": \"Стандартны: Адказвае з нулёвым IP-адрасам (0.0.0.0 для A; :: для AAAA), калі заблакавана правілам у стылі Adblock; адказвае з IP-адрасам, паказаным у правіле, калі заблакавана правілам у стылі /etc/hosts-style\",\n  \"blocking_mode_null_ip\": \"Нулёвы IP: Адказвае з нулёвым IP-адрасам (0.0.0.0 для A; :: для AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Адказвае з кодам NXDOMAIN\\n\",\n  \"blocking_mode_refused\": \"REFUSED: Адказвае з кодам REFUSED\",\n  \"blocklist\": \"Спіс заблакіраваных\",\n  \"bootstrap_dns\": \"Bootstrap сервер DNSы\",\n  \"bootstrap_dns_desc\": \"IP-адрасы сервераў DNS, якія выкарыстоўваюцца для вырашэння IP-адрасоў рэзолвераў DoH/DoT. Яны пазначаюцца ў якасці сервераў upstream. Каментарыі забаронены.\",\n  \"cache_cleared\": \"Кэш DNS паспяхова ачышчаны\",\n  \"cache_enabled\": \"Уключыць кэш\",\n  \"cache_enabled_desc\": \"Захоўвайце адказы DNS лакальна.\",\n  \"cache_optimistic\": \"Аптымістычнае кэшаванне\",\n  \"cache_optimistic_desc\": \"Прымусьце AdGuard Home адказваць з кэша, нават калі тэрмін дзеяння запісаў скончыўся, а таксама паспрабуйце абнавіць іх.\",\n  \"cache_size\": \"Памер кэшу\",\n  \"cache_size_desc\": \"Памер кэша DNS (у байтах).\",\n  \"cache_ttl_max_override\": \"Перавызначыць максімальны TTL\",\n  \"cache_ttl_max_override_desc\": \"Усталюйце максімальнае TTL-значэнне (секунды) для запісаў у кэшы DNS.\",\n  \"cache_ttl_min_override\": \"Перавызначыць мінімальны TTL\",\n  \"cache_ttl_min_override_desc\": \"Пашырыць кароткія значэнні часу жыцця (секунды), атрыманыя ад сервера вышэй па плыні пры кэшаванні адказаў DNS.\",\n  \"cancel_btn\": \"Скасаваць\",\n  \"category_label\": \"Катэгорыя\",\n  \"check\": \"Праверыць\",\n  \"check_client_id\": \"Ідэнтыфікатар кліента (ClientID або IP-адрас)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Праверыць фільтрацыю імя хаста\",\n  \"check_dhcp_servers\": \"Праверыць DHCP-серверы\",\n  \"check_dns_record\": \"Выберыце тып запісу DNS\",\n  \"check_enter_client_id\": \"Увядзіце ідэнтыфікатар кліента\",\n  \"check_hostname\": \"Назва вузла або даменнае імя\",\n  \"check_ip\": \"IP-адрасы: {{ip}}\",\n  \"check_not_found\": \"Не знойдзена ў вашым спісе фільтраў\",\n  \"check_reason\": \"Прычына: {{reason}}\",\n  \"check_service\": \"Назва сэрвісу: {{service}}\",\n  \"check_title\": \"Праверыць фільтрацыю\",\n  \"check_updates_btn\": \"Праверыць абнаўленні\",\n  \"check_updates_now\": \"Праверыць абнаўленні\",\n  \"choose_allowlist\": \"Выберыце спіс дазволеных\",\n  \"choose_blocklist\": \"Выберыце спіс заблакіраваных\",\n  \"choose_from_list\": \"Абраць са спіса\",\n  \"city\": \"Горад\",\n  \"clear_cache\": \"Ачысціць кэш\",\n  \"click_to_view_queries\": \"Націсніце, каб прагледзець запыты\",\n  \"client_add\": \"Дадаць кліента\",\n  \"client_added\": \"Кліент «{{key}}» паспяхова дададзены\",\n  \"client_blocked\": \"Кліент «{{ip}}» паспяхова заблакаваны\",\n  \"client_confirm_block\": \"Вы ўпэўнены, што хочаце заблакаваць кліента «{{ip}}»?\",\n  \"client_confirm_delete\": \"Вы ўпэўнены, што хочаце выдаліць кліента «{{key}}»?\",\n  \"client_confirm_unblock\": \"Вы ўпэўнены, што хочаце адблакаваць кліента «{{ip}}»?\",\n  \"client_deleted\": \"Кліент «{{key}}» паспяхова выдалены\",\n  \"client_details\": \"Дэталі кліента\",\n  \"client_edit\": \"Рэдагаванне кліента\",\n  \"client_global_settings\": \"Выкарыстаць глабальныя налады\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Кліенты могуць ідэнтыфікавацца па ClientID. <a>Тут</a> вы можаце даведацца больш пра ідэнтыфікацыю кліентаў.\",\n  \"client_id_placeholder\": \"Увядзіце ClientID\",\n  \"client_identifier\": \"Ідэнтыфікатар\",\n  \"client_identifier_desc\": \"Кліентаў можна ідэнтыфікаваць па іх IP-адрасе, CIDR, MAC-адрасе або ClientID (можна выкарыстоўваць для DoT/DoH/DoQ). Даведайцеся больш пра тое, як ідэнтыфікаваць кліентаў <0>тут</0>.\",\n  \"client_name\": \"Кліент {{id}}\",\n  \"client_new\": \"Новы кліент\",\n  \"client_settings\": \"Налады кліентаў\",\n  \"client_table_header\": \"Кліент\",\n  \"client_unblocked\": \"Кліент «{{ip}}» паспяхова адблакаваны\",\n  \"client_updated\": \"Кліент «{{key}}» паспяхова абноўлены\",\n  \"clients_desc\": \"Наладзьце пастаянныя запісы кліентаў для прылад, падлучаных да AdGuard Home\",\n  \"clients_not_found\": \"Кліенты не знойдзены\",\n  \"clients_title\": \"Пастаянныя кліенты\",\n  \"compact\": \"Кампактна\",\n  \"config_successfully_saved\": \"Канфігурацыя паспяхова захавана\",\n  \"configure\": \"Сканфігурыраваць\",\n  \"confirm_dns_cache_clear\": \"Вы ўпэўнены, што хочаце ачысціць кэш DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home наладзіць {{ip}} у якасці вашага статычнага IP-адраса. Хочаце працягнуць?\",\n  \"copyright\": \"Аўтарскае права\",\n  \"country\": \"Краіна\",\n  \"custom_filter_rules\": \"Карыстальніцкія правілы фільтрацыі\",\n  \"custom_filter_rules_hint\": \"Уводзьце па адным правіле на радок. Вы можаце выкарыстоўваць правілы блакавання ці сінтаксіс файлаў hosts.\",\n  \"custom_filtering_rules\": \"Карыстальніцкія правілы фільтрацыі\",\n  \"custom_ip\": \"Карыстальніцкі IP\",\n  \"custom_retention_input\": \"Увядзіце тэрмін захоўвання ў гадзінах\",\n  \"custom_rotation_input\": \"Увядзіце частату ратацыі ў гадзінах\",\n  \"dashboard\": \"Панэль кіравання\",\n  \"date\": \"Дата\",\n  \"default\": \"Прадвызначаны\",\n  \"delete_confirm\": \"Вы ўпэўнены, што хочаце выдаліць «{{key}}»?\",\n  \"delete_table_action\": \"Выдаліць\",\n  \"descr\": \"Апісанне\",\n  \"details\": \"Падрабязнасці\",\n  \"dhcp_add_static_lease\": \"Дадаць статычную арэнду\",\n  \"dhcp_config_saved\": \"Канфігурацыя DHCP-сервера паспяхова захавана\",\n  \"dhcp_description\": \"Калі ваш роўтар не падае налады DHCP, вы можаце выкарыстоўваць уласны ўбудаваны DHCP-сервер AdGuard.\",\n  \"dhcp_disable\": \"Адключыць сервер DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Ваша сістэма выкарыстоўвае дынамічную канфігурацыю IP-адраса для інтэрфейсу <0>{{interfaceName}}</0>. Неабходна задаць статычны IP-адрас для выкарыстання сервера DHCP. Ваш бягучы IP-адрас: <0>{{ipAddress}}</0>. AdGuard Home аўтаматычна прызначыць яго ў якасці статычнага, калі вы націснеце кнопку «Уключыць DHCP».\",\n  \"dhcp_edit_static_lease\": \"Рэдагаваць статычную арэнду\",\n  \"dhcp_enable\": \"Уключыць сервер DHCP\",\n  \"dhcp_error\": \"AdGuard Home не можа вызначыць, ці ёсць у сетцы іншы актыўны DHCP-сервер\",\n  \"dhcp_form_gateway_input\": \"IP-адрас шлюза\",\n  \"dhcp_form_lease_input\": \"Працягласць арэнды\",\n  \"dhcp_form_lease_title\": \"Час арэнды DHCP (у секундах)\",\n  \"dhcp_form_range_end\": \"Канец дыяпазону\",\n  \"dhcp_form_range_start\": \"Пачатак дыяпазону\",\n  \"dhcp_form_range_title\": \"Дыяпазон IP-адрасоў\",\n  \"dhcp_form_subnet_input\": \"Маска падсеткі\",\n  \"dhcp_found\": \"Некаторыя актыўныя DHCP-серверы знойдзены ў сеціве. Улучэнне ўбудаванага DHCP-сервера небяспечнае.\",\n  \"dhcp_hardware_address\": \"Апаратны адрас\",\n  \"dhcp_interface_select\": \"Выбраць інтэрфейс DHCP\",\n  \"dhcp_ip_addresses\": \"IP-адрасы\",\n  \"dhcp_ipv4_settings\": \"Налады DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Налады DHCP IPv6\",\n  \"dhcp_lease_added\": \"Статычная арэнда «{{key}}» паспяхова дададзена\",\n  \"dhcp_lease_deleted\": \"Статычная арэнда «{{key}}» паспяхова выдалена\",\n  \"dhcp_lease_updated\": \"Статычная арэнда «{{key}}» паспяхова абноўлена\",\n  \"dhcp_leases\": \"Арэндныя DHCP\",\n  \"dhcp_leases_not_found\": \"Арэнда DHCP не выяўлена\",\n  \"dhcp_new_static_lease\": \"Новая статычная арэнда\",\n  \"dhcp_not_found\": \"Актыўныя DHCP-серверы ў сеціве не знойдзены. Вы можаце бяспечна ўключыць убудаваны сервер DHCP.\",\n  \"dhcp_reset\": \"Вы ўпэўнены, што хочаце скінуць налады DHCP?\",\n  \"dhcp_reset_leases\": \"Скінуць усё арэнды\",\n  \"dhcp_reset_leases_confirm\": \"Вы ўпэўнены, што хочаце выдаліць усё арэнды?\",\n  \"dhcp_reset_leases_success\": \"Арэнды DHCP паспяхова выдалены\",\n  \"dhcp_settings\": \"Налады DHCP\",\n  \"dhcp_static_ip_error\": \"Для таго, каб выкарыстоўваць DHCP-сервер, павінен быць усталяваны статычны IP-адрас. Мы не змаглі вызначыць, ці выкарыстоўвае гэты інтэрфейс сеціва статычны IP-адрас. Калі ласка, усталюйце яго ручна.\",\n  \"dhcp_static_leases\": \"Статычныя арэнды DHCP\",\n  \"dhcp_static_leases_not_found\": \"Не знойдзена статычных арэнд DHCP\",\n  \"dhcp_table_expires\": \"Міне\",\n  \"dhcp_table_hostname\": \"Назва вузла\",\n  \"dhcp_title\": \"DHCP-сервер (эксперыментальны!)\",\n  \"dhcp_warning\": \"Калі вы ўсё адно хочаце ўключыць DHCP-сервер, пераканайцеся, што ў сеціве больш няма актыўных DHCP-сервераў. Інакш гэта можа зламаць доступ у сеціва для падлучаных прылад!\",\n  \"disable_for_hours\": \"На {{count}} гадзін\",\n  \"disable_for_hours_plural\": \"На {{count}} гадзін\",\n  \"disable_for_minutes\": \"На {{count}} хвіліну\",\n  \"disable_for_minutes_plural\": \"На {{count}} хвілін\",\n  \"disable_for_seconds\": \"На {{count}} секунд\",\n  \"disable_for_seconds_plural\": \"На {{count}} секунд\",\n  \"disable_ipv6\": \"Адключыць IPv6\",\n  \"disable_ipv6_desc\": \"Ігнараваць усе запыты DNS для адрасоў IPv6 (тып AAAA) і выдаленне дадзеных IPv6 з адказаў тыпу HTTPS.\",\n  \"disable_notify_for_hours\": \"Адключыць абарону на {{count}} гадзін\",\n  \"disable_notify_for_hours_plural\": \"Адключыць абарону на {{count}} гадзін\",\n  \"disable_notify_for_minutes\": \"Адключыць абарону на {{count}} хвіліну\",\n  \"disable_notify_for_minutes_plural\": \"Адключыць абарону на {{count}} хвілін\",\n  \"disable_notify_for_seconds\": \"Адключыць абарону на {{count}} секунд\",\n  \"disable_notify_for_seconds_plural\": \"Адключыць абарону на {{count}} секунд\",\n  \"disable_notify_until_tomorrow\": \"Адключыць абарону да заўтра\",\n  \"disable_protection\": \"Адключыць абарону\",\n  \"disable_until_tomorrow\": \"Да заўтра\",\n  \"disabled\": \"Адключана\",\n  \"disabled_dhcp\": \"DHCP-сервер адключаны\",\n  \"disabled_filtering_toast\": \"Фільтрацыя адключана\",\n  \"disabled_parental_toast\": \"Адключаны бацькоўскі кантроль\",\n  \"disabled_protection\": \"Абарона адключана\",\n  \"disabled_safe_browsing_toast\": \"Модуль «Бяспека прагляду» адключаны\",\n  \"disabled_safe_search_toast\": \"Адключаны бяспечны пошук\",\n  \"disallow_this_client\": \"Забараніць доступ гэтаму кліенту\",\n  \"dns_addresses\": \"Адрасы DNS\",\n  \"dns_allowlists\": \"Спісы дазволеных DNS\",\n  \"dns_allowlists_desc\": \"Дамены з белых спісаў DNS будуць дазволены, нават калі яны знаходзяцца ў любым з чорных спісаў.\",\n  \"dns_blocklists\": \"Спіс заблакіраваных DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home будзе блакаваць дамены з чорных спісаў.\",\n  \"dns_cache_config\": \"Налада кэша DNS\",\n  \"dns_cache_config_desc\": \"Тут можна наладзіць кэш DNS\",\n  \"dns_cache_size\": \"Памер кэша DNS, у байтах\",\n  \"dns_config\": \"Канфігурацыя сервера DNS\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"Прыватнасць DNS\",\n  \"dns_providers\": \"<0>Спіс вядомых DNS-правайдараў</0> на выбар.\",\n  \"dns_query\": \"Запыты DNS\",\n  \"dns_rewrites\": \"Перазапісы DNS\",\n  \"dns_settings\": \"Налады DNS\",\n  \"dns_start\": \"сервер DNS запускаецца\",\n  \"dns_status_error\": \"Памылка праверкі стану сервера DNS\",\n  \"dns_test_not_ok_toast\": \"Сервер «{{key}}»: немагчыма выкарыстоўваць, праверце слушнасць напісання\",\n  \"dns_test_ok_toast\": \"Паказаныя серверы DNS працуюць карэктна\",\n  \"dns_test_parsing_error_toast\": \"Раздзел {{section}}: радок {{line}}: немагчыма выкарыстоўваць, праверце слушнасць напісання\",\n  \"dns_test_warning_toast\": \"Upstream «{{key}}» не адказвае на тэставыя запыты і можа не працаваць належным чынам\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Уключыць DNSSEC\",\n  \"dnssec_enable_desc\": \"Усталюйце сцяг DNSSEC у выходных DNS-запытах і праверце вынік (патрабуецца распазнальнік з падтрымкай DNSSEC)\",\n  \"domain\": \"Дамен\",\n  \"domain_desc\": \"Увядзіце імя ці маску дамена, які вы хочаце перанакіраваць.\",\n  \"domain_name_table_header\": \"Даменнае імя\",\n  \"domain_or_client\": \"Дамен ці кліент\",\n  \"down\": \"Уніз\",\n  \"download_mobileconfig\": \"Загрузіць файл канфігурацыі\",\n  \"download_mobileconfig_doh\": \"Спампаваць .mobileconfig для DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Спампаваць .mobileconfig для DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Рэдагаваць спіс дазволеных\",\n  \"edit_blocklist\": \"Рэдагаваць спіс заблакіраваных\",\n  \"edit_table_action\": \"Рэдагаваць\",\n  \"edns_cs_desc\": \"Дадайце параметр EDNS Client Subnet (ECS) да запытаў upstream і запісвайце значэнні ў журнал запытаў, якія адпраўляюцца кліентамі.\",\n  \"edns_enable\": \"Уключыць адпраўленне EDNS Client Subnet\",\n  \"edns_use_custom_ip\": \"Выкарыстоўваць указаны IP для DNS\",\n  \"edns_use_custom_ip_desc\": \"Дазволіць выкарыстоўваць уласны IP для DNS\",\n  \"elapsed\": \"Выдаткавана\",\n  \"empty_response_status\": \"Пуста\",\n  \"enable_protection\": \"Уключыць абарону\",\n  \"enable_protection_timer\": \"Абарона будзе ўключана ў {{time}}\",\n  \"enable_upstream_dns_cache\": \"Уключыць кэшаванне DNS для карыстальніцкай канфігурацыі сервера upstream гэтага кліента\",\n  \"enabled_dhcp\": \"Сервер DHCP уключаны\",\n  \"enabled_filtering_toast\": \"Фільтрацыя ўключана\",\n  \"enabled_parental_toast\": \"Уключаны бацькоўскі кантроль\",\n  \"enabled_protection\": \"Абарона ўкл.\",\n  \"enabled_safe_browsing_toast\": \"Модуль «Бяспека прагляду» ўключаны\",\n  \"enabled_save_search_toast\": \"Уключаны бяспечны пошук\",\n  \"enabled_table_header\": \"Уключаныя\",\n  \"encryption_certificate_path\": \"Шлях да сертыфіката\",\n  \"encryption_certificates\": \"Сертыфікаты\",\n  \"encryption_certificates_desc\": \"Для выкарыстання шыфравання вам трэба падаць валідны ланцужок SSL-сертыфікатаў для вашага дамена. Вы можаце атрымаць дармовы сертыфікат на <0>{{link}}</0> ці вы можаце купіць яго ў аднаго з давераных Цэнтраў Сертыфікацыі.\",\n  \"encryption_certificates_input\": \"Скапіюйце сюды сертыфікаты ў PEM-кадоўцы.\",\n  \"encryption_certificates_source_content\": \"Уставіць змесціва сертыфікатаў\",\n  \"encryption_certificates_source_path\": \"Паказаць шлях да файла сертыфікатаў\",\n  \"encryption_chain_invalid\": \"Ланцужок сертыфікатаў не валідны\",\n  \"encryption_chain_valid\": \"Ланцужок сертыфікатаў валідны\",\n  \"encryption_config_saved\": \"Налады шыфравання захаваны\",\n  \"encryption_desc\": \"Падтрымка шыфравання (HTTPS/QUIC/TLS) для DNS і ўэб-інтэрфейсу адміністравання\",\n  \"encryption_doq\": \"Порт DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"Калі гэты порт наладжаны, AdGuard Home запусціць сервер DNS-over-QUIC на гэтым порце.\",\n  \"encryption_dot\": \"Порт DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Калі гэты порт наладжаны, AdGuard Home запусціць DNS-over-TLS-сервер на гэтаму порту.\",\n  \"encryption_enable\": \"Уключыць шыфраванне (HTTPS, DNS-over-HTTPS і DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Калі шыфраванне ўлучана, ўэб-інтэрфейс AdGuard Home будзе працаваць па HTTPS, а сервер DNS будзе таксама працаваць па DNS-over-HTTPS і DNS-over-TLS.\",\n  \"encryption_expire\": \"Міне\",\n  \"encryption_hostnames\": \"Назвы вузлоў\",\n  \"encryption_https\": \"Порт HTTPS\",\n  \"encryption_https_desc\": \"Калі порт HTTPS наладжаны, ўэб-інтэрфейс адміністравання AdGuard Home будзе даступны праз HTTPS, а таксама DNS-over-HTTPS сервер будзе даступны па шляху '/dns-query'.\",\n  \"encryption_issuer\": \"Выдавец\",\n  \"encryption_key\": \"Прыватны ключ\",\n  \"encryption_key_input\": \"Скапіюйце сюды прыватны ключ у PEM-кадоўцы.\",\n  \"encryption_key_invalid\": \"Нявалідны {{type}} прыватны ключ\",\n  \"encryption_key_source_content\": \"Уставіць змесціва прыватнага ключа\",\n  \"encryption_key_source_path\": \"Задаць шлях да прыватнага файла ключа\",\n  \"encryption_key_valid\": \"Валідны {{type}} прыватны ключ\",\n  \"encryption_plain_dns_desc\": \"Звычайны DNS уключаны прадвызначана. Вы можаце адключыць яго, каб прымусіць усе прылады выкарыстоўваць зашыфраваны DNS. Для гэтага неабходна ўключыць як мінімум адзін зашыфраваны пратакол DNS\",\n  \"encryption_plain_dns_enable\": \"Уключыць звычайны DNS\",\n  \"encryption_plain_dns_error\": \"Уключыце прынамсі адзін зашыфраваны пратакол DNS для адключэння звычайнага DNS\",\n  \"encryption_private_key_path\": \"Шлях да прыватнага ключа\",\n  \"encryption_redirect\": \"Аўтаматычна перанакіроўваць на HTTPS\",\n  \"encryption_redirect_desc\": \"Калі ўлучана, AdGuard Home будзе аўтаматычна перанакіроўваць вас з HTTP на HTTPS адрас.\",\n  \"encryption_reset\": \"Вы ўпэўнены, што хочаце скінуць налады шыфравання?\",\n  \"encryption_server\": \"Назва сервера\",\n  \"encryption_server_desc\": \"Калі ўстаноўлена, AdGuard Home вызначае ClientID, адказвае на запыты DDR і выконвае дадатковыя праверкі злучэння. Калі не ўстаноўлена, гэтыя функцыі адключаны. Павінна адпавядаць аднаму з імёнаў DNS у сертыфікаце.\",\n  \"encryption_server_enter\": \"Увядзіце сваё даменнае імя\",\n  \"encryption_settings\": \"Налады шыфравання\",\n  \"encryption_status\": \"Статус\",\n  \"encryption_subject\": \"Аб'ект\",\n  \"encryption_title\": \"Шыфраванне\",\n  \"encryption_warning\": \"Папярэджанне\",\n  \"enforce_safe_search\": \"Выкарыстоўваць бяспечны пошук\",\n  \"enforce_save_search_hint\": \"AdGuard Home будзе ажыццяўляць бяспечны пошук у наступных пошукавых сістэмах: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Прымусовы бяспечны пошук\",\n  \"enter_cache_size\": \"Увядзіце памер кэша (байты)\",\n  \"enter_cache_ttl_max_override\": \"Увядзіце максімальны TTL (секунды)\",\n  \"enter_cache_ttl_min_override\": \"Увядзіце мінімальны TTL (секунды)\",\n  \"enter_name_hint\": \"Увядзіце імя\",\n  \"enter_url_or_path_hint\": \"Увядзіце URL-адрас ці абсалютны шлях да спіса\",\n  \"enter_valid_allowlist\": \"Дадайце дзейны URL-адрас у белы спіс.\",\n  \"enter_valid_blocklist\": \"Дадайце дзейны URL-адрас у чорны спіс.\",\n  \"error_details\": \"Падрабязнасці памылкі\",\n  \"example_comment\": \"! Так можна дадаваць каментарый.\",\n  \"example_comment_hash\": \"# Таксама каментарый.\",\n  \"example_comment_meaning\": \"проста каментарый;\",\n  \"example_meaning_filter_block\": \"заблакаваць доступ да example.org і ўсім яго паддаменам;\",\n  \"example_meaning_filter_whitelist\": \"адблакаваць доступ да example.org і ўсім яго паддаменам;\",\n  \"example_meaning_host_block\": \"адказаць 127.0.0.1 для example.org (але не для яго паддаменаў);\",\n  \"example_multiple_upstreams_reserved\": \"некалькі сервераў upstream <0>для пэўных даменаў</0>;\",\n  \"example_regex_meaning\": \"блакаваць доступ да даменаў, якія адпавядаюць зададзенаму рэгулярнаму выразу.\",\n  \"example_rewrite_domain\": \"перапісаць адказы толькі для гэтага даменнага імі.\",\n  \"example_rewrite_wildcard\": \"перанакіроўвае адказы для ўсіх паддаменаў <0>example.org</0>.\",\n  \"example_upstream_comment\": \"каментарый.\",\n  \"example_upstream_doh\": \"зашыфраваны <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"зашыфраваны DNS-над-HTTPS з прымусовым <0>HTTP/3</0> і без вяртання да HTTP/2 або ніжэй;\",\n  \"example_upstream_doq\": \"зашыфраваны <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"зашыфраваны <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"звычайны DNS (наўзверх UDP);\",\n  \"example_upstream_regular_port\": \"звычайны DNS (праз UDP, імя хаста);\",\n  \"example_upstream_reserved\": \"upstream <0>для канкрэтных даменаў</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> для <1>DNSCrypt</1> або <2>DNS-over-HTTPS</2> рэзолвераў;\",\n  \"example_upstream_tcp\": \"звычайны DNS (наўзверх TCP);\",\n  \"example_upstream_tcp_hostname\": \"звычайны DNS (праз TCP, імя хаста);\",\n  \"example_upstream_tcp_port\": \"звычайны DNS (праз TCP, імя хаста);\",\n  \"example_upstream_udp\": \"звычайны DNS (праз UDP, імя хаста);\",\n  \"examples_title\": \"Прыклады\",\n  \"fallback_dns_desc\": \"Спіс запасных сервераў DNS, якія выкарыстоўваюцца пры немагчымасці атрымаць адказ ад сервераў upstream DNS. Сінтаксіс супадае з полем сервера upstream вышэй.\",\n  \"fallback_dns_placeholder\": \"Увядзіце па адным рэзервовым серверы DNS у радку\",\n  \"fallback_dns_title\": \"Рэзервовыя сервер DNSы\",\n  \"faq\": \"Частыя пытанні\",\n  \"fastest_addr\": \"Найхутчэйшы IP-адрас\",\n  \"fastest_addr_desc\": \"Чакайце адказаў ад <b>усіх</b> сервераў DNS, вымярайце хуткасць злучэння TCP для кожнага сервера і вяртайце IP-адрас сервера з найвышэйшай хуткасцю злучэння.<br/>Гэты рэжым можа значна запаволіць запыты DNS, калі адзін або некалькі сервераў upstream не адказваюць. Пераканайцеся, што вашыя серверы upstream стабільныя, і ваш upstream timeout нізкі.\",\n  \"filter\": \"Фільтр\",\n  \"filter_added_successfully\": \"Спіс паспяхова дададзены\",\n  \"filter_allowlist\": \"УВАГА: Гэта дзеянне таксама выключыць правіла «{{disallowed_rule}}» са спіса дазволеных кліентаў.\",\n  \"filter_category_general\": \"Агульныя\",\n  \"filter_category_general_desc\": \"Спісы, якія блакуюць асочванне і рэкламу на большасці прылад\",\n  \"filter_category_other\": \"Іншае\",\n  \"filter_category_other_desc\": \"Іншыя спісы заблакіраваных\",\n  \"filter_category_regional\": \"Рэгіянальныя\",\n  \"filter_category_regional_desc\": \"Спісы, якія факусуюцца на рэгіянальнай рэкламе і серверах асочвання\",\n  \"filter_category_security\": \"Бяспека\",\n  \"filter_category_security_desc\": \"Спісы, якія спецыялізуюцца на блакаванні шкодных праграм, фішынгавых ці махлярскіх даменаў\",\n  \"filter_removed_successfully\": \"Спіс паспяхова выдалены\",\n  \"filter_updated\": \"Спіс паспяхова абноўлены\",\n  \"filtered\": \"Адфільтраваныя\",\n  \"filtered_custom_rules\": \"Адфільтраваны з дапамогай карыстальніцкіх правіл фільтрацыі\",\n  \"filtering_rules_learn_more\": \"<0>Даведайцеся больш</0> пра стварэнне ўласных спісаў блакавання хастоў.\",\n  \"filters\": \"Фільтры\",\n  \"filters_and_hosts_hint\": \"AdGuard Home распазнае базавыя правілы блакавання і сінтаксіс файлаў hosts.\",\n  \"filters_block_toggle_hint\": \"Вы можаце наладзіць правілы блакавання ў «<a>Фільтрах</a>».\",\n  \"filters_configuration\": \"Налада фільтраў\",\n  \"filters_enable\": \"Уключыць фільтры\",\n  \"filters_interval\": \"Інтэрвал абнаўлення фільтраў\",\n  \"fix\": \"Выправіць\",\n  \"for_last_days\": \"за апошні {{count}} дзень\",\n  \"for_last_days_plural\": \"за апошнія {{count}} дзён\",\n  \"for_last_hours\": \"за апошнюю {{count}} гадзіну\",\n  \"for_last_hours_plural\": \"за апошнія {{count}} гадзін\",\n  \"forgot_password\": \"Забылі пароль?\",\n  \"forgot_password_desc\": \"Выканайце <0>гэтыя дзеянні</0>, каб стварыць новы пароль для вашага ўліковага запісу.\",\n  \"form_add_id\": \"Дадаць ідэнтыфікатар\",\n  \"form_answer\": \"Увядзіце IP-адрас або даменнае імя\",\n  \"form_client_name\": \"Увядзіце назву кліента\",\n  \"form_domain\": \"Увядзіце даменнае імя або падстаноўчыя знакі\",\n  \"form_enter_blocked_response_ttl\": \"Увядзіце TTL заблакіраванага адказу (у секундах)\",\n  \"form_enter_host\": \"Увядзіце назву вузла\",\n  \"form_enter_hostname\": \"Увядзіце назву вузла\",\n  \"form_enter_id\": \"Увядзіце ідэнтыфікатар\",\n  \"form_enter_ip\": \"Увядзіце IP\",\n  \"form_enter_mac\": \"Увядзіце MAC\",\n  \"form_enter_rate_limit\": \"Увядзіце абмежаванне хуткасці\",\n  \"form_enter_rate_limit_subnet_len\": \"Увядзіце даўжыню прэфікса падсеткі для абмежавання хуткасці\",\n  \"form_enter_subnet_ip\": \"Увядзіце IP-адрас у падсеткі «{{cidr}}»\",\n  \"form_enter_upstream_timeout\": \"Увядзіце працягласць часу чакання сервера upstream у секундах.\",\n  \"form_error_answer_format\": \"Няслушны фармат адказу\",\n  \"form_error_client_id_format\": \"ClientID павінен утрымліваць толькі лічбы, малыя літары і злучкі\",\n  \"form_error_domain_format\": \"Няслушны фармат дамена\",\n  \"form_error_equal\": \"Нельга, каб былі роўнымі\",\n  \"form_error_gateway_ip\": \"Арэнда не можа мець IP-адрас шлюза\",\n  \"form_error_ip4_format\": \"Памылковы IPv4-адрас\",\n  \"form_error_ip4_gateway_format\": \"Няслушны IPv4-адрас шлюза\",\n  \"form_error_ip6_format\": \"Памылковы IPv6-адрас\",\n  \"form_error_ip_format\": \"Памылковы IP-адрас\",\n  \"form_error_mac_format\": \"Памылковы MAC-адрас\",\n  \"form_error_password\": \"Паролі не супадаюць\",\n  \"form_error_password_length\": \"Пароль павінен утрымліваць ад {{min}} да {{max}} сімвалаў\",\n  \"form_error_port\": \"Увядзіце карэктны нумар порта\",\n  \"form_error_port_range\": \"Увядзіце нумар порта з інтэрвалу 80-65535\",\n  \"form_error_port_unsafe\": \"Небяспечны порт\",\n  \"form_error_positive\": \"Павінна быць больш 0\",\n  \"form_error_required\": \"Абавязковае поле\",\n  \"form_error_server_name\": \"Памылковая назва сервера\",\n  \"form_error_subnet\": \"Падсетка «{{cidr}}» не ўтрымвае IP-адраса «{{ip}}»\",\n  \"form_error_url_format\": \"Памылковы фармат URL-адраса\",\n  \"form_error_url_or_path_format\": \"Няслушны URL ці абсалютны шлях да спіса\",\n  \"form_select_tags\": \"Выбраць тэгі кліента\",\n  \"found_in_known_domain_db\": \"Знойдзены ў базе вядомых даменаў.\",\n  \"friday\": \"Пятніца\",\n  \"friday_short\": \"Птн\",\n  \"gateway_or_subnet_invalid\": \"Памылковая маска падсеткі\",\n  \"general_settings\": \"Агульныя налады\",\n  \"general_statistics\": \"Агульная статыстыка\",\n  \"get_started\": \"Пачаць\",\n  \"greater_range_start_error\": \"Павінна быць больш за пачатак дыяпазону\",\n  \"homepage\": \"Хатняя старонка\",\n  \"host_whitelisted\": \"Вузел знаходзіцца ў спісе дазволеных\",\n  \"ignore_domains\": \"Ігнаруемыя дамены (парадкова)\",\n  \"ignore_domains_desc_query\": \"Запыты, якія адпавядаюць гэтым правілам, не запісваюцца ў журнал запытаў\",\n  \"ignore_domains_desc_stats\": \"Запыты, якія адпавядаюць гэтым правілам, не запісваюцца ў статыстыку\",\n  \"ignore_domains_title\": \"Дамены, якія ігнаруюцца\",\n  \"ignore_query_log\": \"Ігнараваць гэтага кліента ў журнале запытаў\",\n  \"ignore_statistics\": \"Ігнараваць гэтага кліента ў статыстыцы\",\n  \"install_auth_confirm\": \"Пацвердзіць пароль\",\n  \"install_auth_desc\": \"Настойліва рэкамендуецца наладзіць аўтэнтыфікацыю паролем для ўэб-інтэрфейсу AdGuard Home. Нават калі ён даступны толькі ў вашай лакальнай сетцы, важна абараніць яго ад неабмежаванага доступу.\",\n  \"install_auth_password\": \"Пароль\",\n  \"install_auth_password_enter\": \"Увядзіце пароль\",\n  \"install_auth_title\": \"Аўтэнтыфікацыі\",\n  \"install_auth_username\": \"Імя карыстальніка\",\n  \"install_auth_username_enter\": \"Увядзіце імя карыстальніка\",\n  \"install_devices_address\": \"сервер DNS AdGuard Home даступны па наступных адрасах\",\n  \"install_devices_android_list_1\": \"У меню кіравання націсніце абразок «Налады».\",\n  \"install_devices_android_list_2\": \"Абярыце пункт «Wi-Fi». З'явіцца экран са спісам даступных сетак (наладка DNS недаступная для мабільных сетак).\",\n  \"install_devices_android_list_3\": \"Доўгім націскам па сетцы выклікайце меню, а потым націсніце «Змяніць сетку».\",\n  \"install_devices_android_list_4\": \"На некаторых прыладах можа запатрабавацца націснуць «Пашыраныя налады». Каб атрымаць магчымасць змяняць налады DNS, вам запатрабуецца перамкнуць «Налады IP» на «Карыстацкія».\",\n  \"install_devices_android_list_5\": \"Зараз можна змяніць палі «DNS 1» і «DNS 2». Увядзіце ў іх адрасы AdGuard Home.\",\n  \"install_devices_desc\": \"Для таго, каб выкарыстоўваць AdGuard Home, вам трэба наладзіць вашы прылады на яго выкарыстанне.\",\n  \"install_devices_ios_list_1\": \"Увайдзіце ў меню налад прылады.\",\n  \"install_devices_ios_list_2\": \"Абярыце пункт «Wi-Fi» (для мабільных сетак ручная наладка DNS немагчыма).\",\n  \"install_devices_ios_list_3\": \"Націсніце на назву актыўнай у дадзены момант сеткі.\",\n  \"install_devices_ios_list_4\": \"У поле «DNS» увядзіце ўвядзіце адрасы AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Клікніце па абразку Apple і перайдзіце ў Сістэмныя налады.\",\n  \"install_devices_macos_list_2\": \"Націсніце на значок «Сетка».\",\n  \"install_devices_macos_list_3\": \"Абярыце першае падлучэнне ў спісе і націсніце кнопку «Дадаткова».\",\n  \"install_devices_macos_list_4\": \"Абярыце ўкладку «DNS» і дадайце адрасы AdGuard Home.\",\n  \"install_devices_router\": \"Маршрутызатар\",\n  \"install_devices_router_desc\": \"Такая наладка аўтаматычна пакрые ўсе прылады, што выкарыстоўваюць ваш хатні роўтар, і вам не трэба будзе наладжваць кожнае з іх у асобнасці.\",\n  \"install_devices_router_list_1\": \"Адкрыйце налады вашага роўтара. Звычайна вы можаце адкрыць іх у вашым браўзары, напрыклад, http://192.168.0.1/ ці http://192.168.1.1/. Вас могуць папрасіць увесці пароль. Калі вы не помніце яго, пароль часта можна скінуць, націснуўшы на кнопку на самым роўтары. Некаторыя роўтары патрабуюць адмысловага дадатку, які ў гэтым выпадку павінен быць ужо ўсталявана на ваш кампутар ці тэлефон.\",\n  \"install_devices_router_list_2\": \"Знайдзіце налады DHCP ці DNS. Знайдзіце літары «DNS» поруч з тэкставым полем, у якое можна ўвесці два ці тры шэрагі лічбаў, падзеленых на 4 групы ад адной до трох лічбаў.\",\n  \"install_devices_router_list_3\": \"Увядзіце туды адрас вашага AdGuard Home.\",\n  \"install_devices_router_list_4\": \"Не некаторых тыпах маршрутызатараў наладзіць карыстальніцкі DNS немагчыма. У гэтым выпадку можа дапамагчы налада AdGuard Home у якасці <0'>сервера DHCP</0>. У адваротным выпадку вам неабходна будзе звярнуцца да дапаможніка па наладзе сервераў DNS для вашай мадэлі маршрутызатара.\",\n  \"install_devices_title\": \"Наладзьце вашы прылады\",\n  \"install_devices_windows_list_1\": \"Адкрыйце Панэль кіравання праз меню «Пуск» ці праз пошук Windows.\",\n  \"install_devices_windows_list_2\": \"Перайдзіце ў «Сеціва і інтэрнэт», а потым у «Цэнтр кіравання сеціва і агульным доступам».\",\n  \"install_devices_windows_list_3\": \"У левым боку экрана клікніце «Змена параметраў адаптара».\",\n  \"install_devices_windows_list_4\": \"Пстрыкніце правай кнопкай мышы ваша актыўнае злучэнне і абярыце Уласцівасці.\",\n  \"install_devices_windows_list_5\": \"Знайдзіце ў спісе пункт «IP версіі 4 (TCP/IPv4)», вылучыце яго і потым ізноў націсніце «Уласцівасці».\",\n  \"install_devices_windows_list_6\": \"Выберыце «Выкарыстоўваць наступныя адрасы сервераў DNS» і ўвядзіце адрас сервера AdGuard Home.\",\n  \"install_saved\": \"Паспяхова захавана\",\n  \"install_settings_all_interfaces\": \"Усе інтэрфейсы\",\n  \"install_settings_dns\": \"Сервер DNS\",\n  \"install_settings_dns_desc\": \"Вам неабходна сканфігурыраваць свае прылады або маршрутызатар, каб выкарыстоўваць сервер DNS на наступных адрасах:\",\n  \"install_settings_interface_link\": \"Ваш ўэб-інтэрфейс адміністравання AdGuard Home будзе даступны па наступных адрасах:\",\n  \"install_settings_listen\": \"Сеткавы інтэрфейс\",\n  \"install_settings_port\": \"Порт\",\n  \"install_settings_title\": \"Вэб-інтэрфейс адміністратара\",\n  \"install_static_configure\": \"Мы выявілі выкарыстанне дынамічнага IP-адраса — <0>{{ip}}</0>. Хочаце выкарыстоўваць яго ў якасці статычнага адраса?\",\n  \"install_static_error\": \"AdGuard Home не можа аўтаматычна наладзіць яго для гэтага інтэрфейса сеціва. Калі ласка, паглядзіце інструкцыю пра тое, як гэта зрабіць ручна.\",\n  \"install_static_ok\": \"Добрыя навіны! Ваш статычны IP-адрас ужо наладжаны\",\n  \"install_step\": \"Крок\",\n  \"install_submit_desc\": \"Працэдура налады завершана і вы гатовы пачаць выкарыстанне AdGuard Home.\",\n  \"install_submit_title\": \"Віншуем!\",\n  \"install_welcome_desc\": \"AdGuard Home – гэта сервер DNS, што блакуе рэкламу і трэкінг. Яго мэта – даць вам магчымасць кантраляваць усю ваша сеціва і ўсе падлучаныя прылады. Ён не патрабуе ўсталёўкі кліенцкіх праграм.\",\n  \"install_welcome_title\": \"Сардэчна запрашаем у AdGuard Home!\",\n  \"interval_24_hour\": \"24 гадзіны\",\n  \"interval_6_hour\": \"6 гадзін\",\n  \"interval_days\": \"{{count}} дзень\",\n  \"interval_days_plural\": \"{{count}} дні\",\n  \"interval_hours\": \"{{count}} гадзіна\",\n  \"interval_hours_plural\": \"{{count}} гадзіны\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP-адрас\",\n  \"known_tracker\": \"Вядомыя праграмы сачэння\",\n  \"last_rule_in_allowlist\": \"Няможна заблакаваць гэтага кліента, бо вынятак правіла «{{disallowed_rule}}» АДКЛЮЧЫЦЬ рэжым белага спіса.\",\n  \"last_time_updated_table_header\": \"Апошняе абнаўленне\",\n  \"list_confirm_delete\": \"Вы ўпэўнены, што хочаце выдаліць гэты спіс?\",\n  \"list_label\": \"Спіс\",\n  \"list_updated\": \"{{count}} спіс абноўлены\",\n  \"list_updated_plural\": \"{{count}} спісы абноўлены\",\n  \"list_url_table_header\": \"URL-адрас спіса\",\n  \"load_balancing\": \"Балансіроўка нагрузкі\",\n  \"load_balancing_desc\": \"Запытвайце па адным серверы upstream за раз.<br/>AdGuard Home выкарыстоўвае выпадковы алгарытм з вагой для выбару сервераў з найменшай колькасцю памылак і найменшым сярэднім часам запыту.\",\n  \"loading_table_status\": \"Загрузка...\",\n  \"local_ptr_default_resolver\": \"Прадвызначана AdGuard Home выкарыстоўвае наступныя зваротныя рэзолверы DNS: {{ip}}.\",\n  \"local_ptr_desc\": \"Серверы DNS, якія выкарыстоўваюць AdGuard Home для privaten PTR, SOA і NS запытаў. Запыт лічыцца privaten, калі ён запытвае ARPA дамен, які ўключае падсетку ў рамках прыватных IP дыяпазонаў (напрыклад, \\\"192.168.12.34\\\") і паступае ад кліента з прыватным IP-адрасам. Калі не ўстаноўлена, будуць выкарыстоўвацца стандартныя DNS рэзолверы вашай АС, за выключэннем IP-адрасоў AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home не змог вызначыць прыдатныя прыватныя адваротныя DNS-рэзолверы для гэтай сістэмы.\",\n  \"local_ptr_placeholder\": \"Увядзіце па адным адрасе на радок\",\n  \"local_ptr_title\": \"Прыватныя сервер DNSы\",\n  \"location\": \"Месцазнаходжанне\",\n  \"log_and_stats_section_label\": \"Журнал запытаў і статыстыка\",\n  \"lower_range_start_error\": \"Павінна быць менш за пачатак дыяпазону\",\n  \"main_settings\": \"Асноўныя налады\",\n  \"make_static\": \"Зрабіць статычным\",\n  \"manual_update\": \"Калі ласка, <a>кіруйцеся інструкцыі</a> для абнаўлення ручна.\",\n  \"milliseconds_abbreviation\": \"мс\",\n  \"monday\": \"Панядзелак\",\n  \"monday_short\": \"Пан\",\n  \"name\": \"Назва\",\n  \"name_table_header\": \"Назва\",\n  \"netname\": \"Назва сеткі\",\n  \"network\": \"Сетка\",\n  \"new_allowlist\": \"Новы спіс дазволеных\",\n  \"new_blocklist\": \"Новы спіс заблакіраваных\",\n  \"next\": \"Далей\",\n  \"next_btn\": \"Далей\",\n  \"no_blocklist_added\": \"Спісы заблакіраваных не дададзены\",\n  \"no_clients_found\": \"Кліенты не знойдзены\",\n  \"no_domains_found\": \"Дамены не знойдзены\",\n  \"no_logs_found\": \"Журналы не знойдзены\",\n  \"no_servers_specified\": \"Не пазначаны серверы\",\n  \"no_upstreams_data_found\": \"Даныя аб серверах upstream не знойдзены\",\n  \"no_whitelist_added\": \"Спісы дазволеных не дададзены\",\n  \"nothing_found\": \"Нічога не знойдзена\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Колькасць DNS-запытаў, заблакаваных фільтрамі і блок-спісамі\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Колькасць заблакаваных «сайтаў для дарослых»\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Колькасць DNS-запытаў, заблакаваных модулем Антыфішынгу AdGuard\",\n  \"number_of_dns_query_days\": \"Колькасць DNS-запытаў за апошні {{count}} дзень\",\n  \"number_of_dns_query_days_plural\": \"Колькасць DNS запытаў, апрацаваных за апошнія {{count}} дзён\",\n  \"number_of_dns_query_hours\": \"Колькасць DNS-запытаў, апрацаваных за апошнюю {{count}} гадзіну\",\n  \"number_of_dns_query_hours_plural\": \"Колькасць DNS-запытаў, апрацаваных за апошнія {{count}} гадзін\",\n  \"number_of_dns_query_to_safe_search\": \"Колькасць запытаў DNS для пошукавых сістэм, для якіх быў ужыты Бяспечны пошук\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"ВЫКЛЮЧАНА\",\n  \"on\": \"УКЛЮЧАНА\",\n  \"open_dashboard\": \"Адкрыць Панэль кіравання\",\n  \"orgname\": \"Назва арганізацыі\",\n  \"original_response\": \"Арыгінальны адказ\",\n  \"out_of_range_error\": \"Павінна быць па-за дыяпазонам «{{start}}»-«{{end}}»\",\n  \"page_table_footer_text\": \"Старонка\",\n  \"parallel_requests\": \"Паралельныя запыты\",\n  \"parental_control\": \"Бацькоўскі кантроль\",\n  \"password_label\": \"Пароль\",\n  \"password_placeholder\": \"Увядзіце пароль\",\n  \"plain_dns\": \"Звычайны DNS\",\n  \"port_53_faq_link\": \"Порт 53 часта заняты службамі «DNSStubListener» ці «systemd-resolved». Азнаёмцеся з <0>інструкцыяй</0> пра тое, як гэта дазволіць.\",\n  \"previous_btn\": \"Папярэдні\",\n  \"privacy_policy\": \"Палітыка прыватнасці\",\n  \"processing_update\": \"Калі ласка, пачакайце, AdGuard Home абнаўляецца\",\n  \"protection_section_label\": \"Абарона\",\n  \"protocol\": \"Пратакол\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Журнал запытаў\",\n  \"query_log_clear\": \"Ачысціць журналы запытаў\",\n  \"query_log_cleared\": \"Журнал запытаў паспяхова ачышчаны\",\n  \"query_log_configuration\": \"Канфігурацыя журналаў\",\n  \"query_log_confirm_clear\": \"Вы сапраўды хочаце ачысціць увесь журнал запытаў?\",\n  \"query_log_disabled\": \"Журнал запытаў адключаны і яго можна сканфігурыраваць у <0>наладах</0>\",\n  \"query_log_enable\": \"Уключыць журнал\",\n  \"query_log_filtered\": \"Адфільтравана з дапамогай {{filter}}\",\n  \"query_log_response_status\": \"Статус: {{value}}\",\n  \"query_log_retention\": \"Ратацыя журналаў запыту\",\n  \"query_log_retention_confirm\": \"Вы сапраўды хочаце змяніць чаргаванне журнала запытаў? Некаторыя даныя могуць быць страчаны, калі паменшыць інтэрвал\",\n  \"query_log_strict_search\": \"Ужывайце падвойныя двукоссі для строгага пошуку\",\n  \"query_log_updated\": \"Журнал запытаў паспяхова абноўлены\",\n  \"rate_limit\": \"Абмежаванні хуткасці\",\n  \"rate_limit_desc\": \"Абмежаванне на колькасць запытаў у секунду для кожнага кліента (0 — неабмежавана)\",\n  \"rate_limit_subnet_len_ipv4\": \"Даўжыня прэфікса падсеткі для адрасоў IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Даўжыня прэфікса падсеткі для адрасоў IPv4, якія выкарыстоўваецца для абмежавання хуткасці. Прадвызначанае значэнне - 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Даўжыня прэфікса падсеткі IPv4 павінна быць ад 0 да 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Даўжыня прэфікса падсеткі для адрасоў IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Даўжыня прэфікса падсеткі для адрасоў IPv6, якія выкарыстоўваецца для абмежавання хуткасці. Прадвызначанае значэнне - 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Даўжыня прэфікса падсеткі IPv6 павінна быць ад 0 да 128\",\n  \"rate_limit_whitelist\": \"Белы спіс з абмежаваннем хуткасці\",\n  \"rate_limit_whitelist_desc\": \"IP-адрасы выключаны з абмежавання хуткасці\",\n  \"rate_limit_whitelist_placeholder\": \"Увядзіце па адным адрасе на радок\",\n  \"refresh_btn\": \"Абнавіць\",\n  \"refresh_statics\": \"Абнавіць статыстыку\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Паведаміць аб праблеме\",\n  \"request_details\": \"Запытаць падрабязнасці\",\n  \"request_table_header\": \"Запыт\",\n  \"requests_count\": \"Колькасць запытаў\",\n  \"reset_settings\": \"Скінуць налады\",\n  \"resolve_clients_desc\": \"AdGuard Home будзе спрабаваць аўтаматычна вызначыць даменавыя імёны кліентаў праз PTR-запыты да адпаведных сервераў (прыватны сервер DNS для лакальных кліентаў, upstream-серверы для кліентаў з публічным IP-адрасам).\",\n  \"resolve_clients_title\": \"Уключыць запытванне даменавых імёнаў для кліентаў\",\n  \"response_code\": \"Код адказу\",\n  \"response_details\": \"Падрабязнасці адказу\",\n  \"response_table_header\": \"Адказ\",\n  \"response_time\": \"Час адказу\",\n  \"rewrite_A\": \"<0>A</0>: спецыяльнае значэнне, захоўваць запісы <0>A</0> з upstream\",\n  \"rewrite_AAAA\": \"<0>AAAAA</0>: спецыяльнае значэнне, захоўваць запісы <0>AAAA</0> з upstream\",\n  \"rewrite_add\": \"Дадаць перазапіс DNS\",\n  \"rewrite_added\": \"Правіла перанакіравання DNS для «{{key}}» паспяхова дададзена\",\n  \"rewrite_applied\": \"Ужыта правіла перанакіравання\",\n  \"rewrite_confirm_delete\": \"Вы ўпэўнены, што хочаце выдаліць правіла перанакіравання DNS для «{{key}}»?\",\n  \"rewrite_deleted\": \"Правіла перанакіравання DNS для «{{key}}» паспяхова выдалена\",\n  \"rewrite_desc\": \"Дазваляе лёгка сканфігурыраваць карыстальніцкі адказ DNS для пэўнага даменнага імя.\",\n  \"rewrite_domain_name\": \"Даменнае імя: дадаць запіс CNAME\",\n  \"rewrite_edit\": \"Рэдагаваць перазапіс DNS\",\n  \"rewrite_hosts_applied\": \"Перапісана па правіле файла hosts\",\n  \"rewrite_ip_address\": \"IP адрас: скарыстайце гэты IP у выглядзе А ці АААА адказу\",\n  \"rewrite_not_found\": \"Не знойдзена правілаў перанакіравання DNS\",\n  \"rewrite_updated\": \"Перазапіс DNS паспяхова абноўлены\",\n  \"rewritten\": \"Перапісаныя\",\n  \"rows_table_footer_text\": \"радкоў\",\n  \"rule_added_to_custom_filtering_toast\": \"Правіла дадзена ў карыстальніцкія правілы фільтрацыі: {{rule}}\",\n  \"rule_label\": \"Правіла(ы)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Правіла выдалена з карыстальніцкіх правіл фільтрацыі: {{rule}}\",\n  \"rules_count_table_header\": \"Колькасць правіл\",\n  \"safe_browsing\": \"Бяспечны прагляд\",\n  \"safe_search\": \"Бяспечны пошук\",\n  \"saturday\": \"Субота\",\n  \"saturday_short\": \"Суб\",\n  \"save_btn\": \"Захаваць\",\n  \"save_config\": \"Захаваць канфігурацыю\",\n  \"schedule_add\": \"Дадаць расклад\",\n  \"schedule_current_timezone\": \"Бягучы гадзінны пояс: {{value}}\",\n  \"schedule_desc\": \"Усталюйце перыяды бяздзейнасці для заблакаваных сэрвісаў\",\n  \"schedule_edit\": \"Рэдагаваць расклад\",\n  \"schedule_from\": \"З\",\n  \"schedule_invalid_select\": \"Час пачатку павінен быць перад часам заканчэння\",\n  \"schedule_modal_description\": \"Гэты расклад заменіць усе існуючыя расклады на той жа дзень тыдня. Кожны дзень тыдня можа мець толькі адзін перыяд бяздзейнасці.\",\n  \"schedule_modal_time_off\": \"Блакіроўка сэрвісаў адключаная:\",\n  \"schedule_new\": \"Новы расклад\",\n  \"schedule_remove\": \"Выдаліць расклад\",\n  \"schedule_save\": \"Захаваць расклад\",\n  \"schedule_select_days\": \"Выбраць дні\",\n  \"schedule_services\": \"Паўза блакавання сэрвісаў\",\n  \"schedule_services_desc\": \"Настройка раскладу паўзы фільтра блакавання сэрвісаў\",\n  \"schedule_services_desc_client\": \"Настройка раскладу паўзы фільтра блакавання сэрвісаў для дадзенага кліента\",\n  \"schedule_time_all_day\": \"Увесь дзень\",\n  \"schedule_timezone\": \"Выберыце гадзінны пояс\",\n  \"schedule_to\": \"Да\",\n  \"served_from_cache_label\": \"Атрымана з кэшу\",\n  \"service_name\": \"Назва сэрвісу\",\n  \"set_static_ip\": \"Усталяваць статычны IP-адрас\",\n  \"settings\": \"Налады\",\n  \"settings_custom\": \"Карыстальніцкія\",\n  \"settings_global\": \"Глабальныя\",\n  \"setup_config_to_enable_dhcp_server\": \"Наладзіць канфігурацыю для ўключэння DHCP-сервера\",\n  \"setup_dns_notice\": \"Каб выкарыстоўваць <1>DNS-over-HTTPS</1> ці <1>DNS-over-TLS</1>, вам патрэбна <0>наладзіць шыфраванне</0> у наладах AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Выкарыстоўвайце радок <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Выкарыстоўвайце радок <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Ніжэй прыведзены спіс праграмнага забеспячэння, які можна выкарыстоўваць.</0>\",\n  \"setup_dns_privacy_4\": \"На прыладах з iOS 14 і macOS Big Sur вы можаце спампаваць адмысловы файл '.mobileconfig', які дадае <highlight>DNS-over-HTTPS</highlight> ці <highlight>DNS-over-TLS</highlight> серверы ў налады DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 без дадатковых праграм падтрымлівае DNS-over-TLS. Перайдзіце ў Налады → Сетка і інтэрнэт → Дадаткова → Прыватны DNS, а потым увядзіце туды сваё даменнае імя, каб наладзіць гэтую функцыю.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard для Android</0> падтрымлівае <1>DNS-over-HTTPS</1> і <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> дадае падтрымку <1>DNS-over-HTTPS</1> на Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Канфігурацыя для iOS і macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> падтрымлівае <1>DNS-over-HTTPS</1>, але для таго, каб вам сканфігурыраваць яго на выкарыстанне свайго ўласнага сервера, вам спатрэбіцца <2>DNS Stamp</2> для яго.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard для iOS</0> падтрымлівае <1>DNS-over-HTTPS</1> і <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home сам можа быць кліентам зашыфраванага DNS на любай платформе.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> падтрымлівае ўсе вядомыя абароненыя пратаколы DNS.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> падтрымлівае <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> падтрымлівае <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Вы можаце знайсці яшчэ варыянты <0>тут</0> і <1>тут</1>.\",\n  \"setup_dns_privacy_other_title\": \"Іншыя развязкі\",\n  \"setup_guide\": \"Дапаможнік па наладцы\",\n  \"show_all_filter_type\": \"Паказаць усё\",\n  \"show_blocked_responses\": \"Заблакіравана\",\n  \"show_filtered_type\": \"Паказаць адфільтраваныя\",\n  \"show_processed_responses\": \"Апрацавана\",\n  \"show_whitelisted_responses\": \"Дазволеныя\",\n  \"sign_in\": \"Увайсці\",\n  \"sign_out\": \"Выйсці\",\n  \"source_label\": \"Крыніца\",\n  \"static_ip\": \"Статычны IP-адрас\",\n  \"static_ip_desc\": \"AdGuard Home з'яўляецца серверам, таму для карэктнай працы яму патрэбен статычны IP-адрас. У адваротным выпадку, у нейкі момант ваш роўтар можа прысвоіць гэтай прыладзе іншы IP-адрас.\",\n  \"statistics_clear\": \"Ачысціць статыстыку\",\n  \"statistics_clear_confirm\": \"Вы ўпэўнены, што хочаце ачысціць статыстыку?\",\n  \"statistics_cleared\": \"Статыстыка паспяхова вычышчана\",\n  \"statistics_configuration\": \"Канфігурацыя статыстыкі\",\n  \"statistics_enable\": \"Уключыць статыстыку\",\n  \"statistics_retention\": \"Захаванне статыстыкі\",\n  \"statistics_retention_confirm\": \"Вы сапраўды хочаце змяніць статыстыку ўтрымання? Некаторыя даныя могуць быць страчаны, калі паменшыць інтэрвал\",\n  \"statistics_retention_desc\": \"Калі вы паменшыце значэнне інтэрвалу, некаторыя даныя могуць быць страчаны\",\n  \"stats_adult\": \"Заблакаваныя «дарослыя» сайты\",\n  \"stats_disabled\": \"Статыстыка была адключаная. Вы можаце ўключыць яго <0>на старонцы налад </0>.\",\n  \"stats_disabled_short\": \"Статыстыка была адключаная\",\n  \"stats_malware_phishing\": \"Заблакаваныя шкодныя і фішынгавыя сайты\",\n  \"stats_params\": \"Канфігурацыя статыстыкі\",\n  \"stats_query_domain\": \"Дамены, якія часта запытваюцца\",\n  \"subnet_error\": \"Адрасы павінны быць у адной падсетцы\",\n  \"sunday\": \"Нядзеля\",\n  \"sunday_short\": \"Ндз\",\n  \"system_host_files\": \"Сістэмныя файлы hosts\",\n  \"table_client\": \"Кліент\",\n  \"table_name\": \"Назва\",\n  \"tags_desc\": \"Вы можаце выбраць тэгі, якія адпавядаюць кліенту. Уключыце тэгі ў правілы фільтрацыі, каб прымяняць іх больш дакладна. <0>Даведацца больш</0>.\",\n  \"tags_title\": \"Тэгі\",\n  \"test_upstream_btn\": \"Тэст сервераў upstream\",\n  \"theme_auto\": \"Аўта\",\n  \"theme_auto_desc\": \"Аўто (на аснове каляровай схемы вашага прылады)\",\n  \"theme_dark\": \"Цёмная\",\n  \"theme_dark_desc\": \"Цёмная тэма\",\n  \"theme_light\": \"Светлая\",\n  \"theme_light_desc\": \"Светлая тэма\",\n  \"thursday\": \"Чацвер\",\n  \"thursday_short\": \"Чцв\",\n  \"time_table_header\": \"Час\",\n  \"top_blocked_domains\": \"Дамены, якія часта блакіруюцца\",\n  \"top_clients\": \"Самыя актыўныя кліенты\",\n  \"top_upstreams\": \"Самыя частыя серверы upstream\",\n  \"topline_expired_certificate\": \"Ваш SSL-сертыфікат мінуў. Абновіце <0>Налады шыфравання</0>.\",\n  \"topline_expiring_certificate\": \"Ваш SSL-сертыфікат хутка мінае. Абновіце <0>Налады шыфравання</0>.\",\n  \"tracker_source\": \"Крыніца праграмы сачэння\",\n  \"try_again\": \"Паспрабаваць яшчэ раз\",\n  \"ttl_cache_validation\": \"Мінімальнае значэнне TTL кэша павінна быць менш ці роўна максімальнаму значэнню\",\n  \"tuesday\": \"Аўторак\",\n  \"tuesday_short\": \"Аўт\",\n  \"type_table_header\": \"Тып\",\n  \"unavailable_dhcp\": \"DHCP недаступны\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home не можа запусціць DHCP-сервер на вашай АС\",\n  \"unblock\": \"Разблакіраваць\",\n  \"unblock_all\": \"Разблакіраваць усе\",\n  \"unblock_for_this_client_only\": \"Адблакаваць толькі для гэтага кліента\",\n  \"unknown_filter\": \"Невядомы фільтр {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} ужо даступная! <0>Націсніце сюды</0>, каб даведацца больш.\",\n  \"update_failed\": \"Памылка аўто-абнаўлення. Калі ласка, <a>кіруйцеся інструкцыі</a> для абнаўлення ручна.\",\n  \"update_now\": \"Абнавіць\",\n  \"updated_custom_filtering_toast\": \"Карыстальніцкія правілы паспяхова захаваны\",\n  \"updated_save_search_toast\": \"Налады бяспечнага пошуку абноўлены\",\n  \"updated_upstream_dns_toast\": \"Upstream сервер DNSы абноўлены\",\n  \"updates_checked\": \"Даступна новая версія AdGuard Home\",\n  \"updates_version_equal\": \"Версія AdGuard Home актуальная\",\n  \"upstream\": \"Сервер Upstream\",\n  \"upstream_dns\": \"Серверы upstream DNS\",\n  \"upstream_dns_cache_configuration\": \"Канфігурацыя кэшу сервера upstream DNS\",\n  \"upstream_dns_client_desc\": \"Калі пакінуць поле пустым, AdGuard Home будзе звяртацца да сервераў, паказаных у <0>наладах DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Наладжаны ў {{path}}\",\n  \"upstream_dns_help\": \"Увядзіце адрасы сервераў па адным у радку. <a>Даведацца больш </a> аб канфігурацыі сервераў upstream DNS.\",\n  \"upstream_parallel\": \"Ужыць адначасныя запыты да ўсіх сервераў для паскарэння апрацоўкі запыту\",\n  \"upstream_timeout\": \"Час чакання для upstream.\",\n  \"upstream_timeout_desc\": \"Указвае колькасць секунд, якія трэба пачакаць для адказу ад сервера upstream\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Выкарыстаць Бяспечную навігацыю AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home праверыць, ці блакіруецца дамен вэб-сэрвісам «Бяспека прагляду». Праграма скарыстаецца бяспечнымі API, каб выканаць праверку: на сервер адпраўляецца толькі кароткі прэфікс хэшу SHA256 даменнага імя.\",\n  \"use_adguard_parental\": \"Ужывайце модуль Бацькоўскага кантролю AdGuard \",\n  \"use_adguard_parental_hint\": \"AdGuard Home праверыць, ці ўтрымвае дамен матэрыялы 18+. Ён выкарыстоўвае той жа API для забеспячэння прыватнасці, што і ўэб-служба бяспекі браўзара.\",\n  \"use_private_ptr_resolvers_desc\": \"Вырашаць запытамі PTR, SOA і NS для даменаў ARPA, што ўтрымліваюць прыватныя IP-адрасы, праз прыватныя серверы upstream, DHCP, /etc/hosts і гэтак далей. Калі адключана, AdGuard Home будзе адказваць на ўсе такія запыты з NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Ужываць прыватныя адваротныя DNS-рэзолверы\",\n  \"use_saved_key\": \"Скарыстаць захаваны раней ключ\",\n  \"username_label\": \"Імя карыстальніка\",\n  \"username_placeholder\": \"Увядзіце імя карыстальніка\",\n  \"validated_with_dnssec\": \"Пацверджана з дапамогай DNSSEC\",\n  \"version\": \"Версія\",\n  \"version_request_error\": \"Памылка пры праверцы наяўнасці абнаўленняў. Праверце ваша інтэрнэт-злучэнне.\",\n  \"wednesday\": \"Серада\",\n  \"wednesday_short\": \"Срд\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/bg.json",
    "content": "{\n  \"access_allowed_desc\": \"Списък от CIDR, IP адреси или <a>ClientIDs</a>. Ако този списък съдържа записи, AdGuard Home ще приема заявки само от тези клиенти.\",\n  \"access_allowed_title\": \"Разрешени клиенти\",\n  \"access_blocked_desc\": \"Не бъркайте с филтри. AdGuard Home отхвърля DNS запитванията, съвпадащи с тези домейни, и тези запитвания дори не се появяват в дневника на запитванията. Можете да зададете точни имена на домейни, символи за подмяна или правила за филтриране на URL адреси, например \\\"example.org\\\", \\\"*.example.org\\\" или \\\"||example.org^\\\".\",\n  \"access_blocked_title\": \"Недопустими домейни\",\n  \"access_desc\": \"Тук можете да конфигурирате правилата за достъп до DNS сървъра на AdGuard Home\",\n  \"access_disallowed_desc\": \"Списък с CIDR, IP адреси или <a>ClientIDs</a>. Ако този списък съдържа записи, AdGuard Home ще отхвърли заявките от тези клиенти. Това поле се игнорира, ако има записи в разрешените клиенти.\",\n  \"access_disallowed_title\": \"Забранени клиенти\",\n  \"access_settings_saved\": \"Настройките за достъп бяха успешно запазени\",\n  \"access_title\": \"Настройки за достъп\",\n  \"actions_table_header\": \"Действия\",\n  \"add_allowlist\": \"Добавяне на списък с разрешения\",\n  \"add_blocklist\": \"Добавяне на списък за блокиране\",\n  \"add_custom_list\": \"Добавете персонализиран списък\",\n  \"add_persistent_client\": \"Добавяне като постоянен клиент\",\n  \"address\": \"Адрес\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home ще отхвърли всички DNS запитвания от този клиент.\",\n  \"all_lists_up_to_date_toast\": \"Всички списъци вече са актуални\",\n  \"all_queries\": \"Всички запитвания\",\n  \"allow_this_client\": \"Позволете на този клиент\",\n  \"allowed\": \"В белия списък\",\n  \"anonymize_client_ip\": \"Анонимен IP адрес на клиента\",\n  \"anonymize_client_ip_desc\": \"Не запазвайте пълния IP адрес на клиента в регистрите или статистиката\",\n  \"anonymizer_notification\": \"<0>Забележка:</0> Анонимизация на IP е включена. Можете да я деактивирате в <1>Общите настройки</1>.\",\n  \"answer\": \"Отговор\",\n  \"apply_btn\": \"Приложи\",\n  \"auto_clients_desc\": \"Информация за IP адреси на устройства, които използват или могат да използват AdGuard Home. Тази информация се събира от няколко източника, включително файлове с хост, обратен DNS и др.\",\n  \"auto_clients_title\": \"Клиенти по време на работа\",\n  \"autofix_warning_list\": \"Той ще изпълни следните задачи: <0>Деактивиране на системния DNSStubListener</0> <0>Задайте адреса на DNS сървъра на 127.0.0.1</0> <0>Заменете целта на символичната връзка от /etc/resolv.conf с /run/systemd/resolve/resolv.conf</0> <0>Спиране на DNSStubListener (презареждане на услугата systemd-resolved)</0>\",\n  \"autofix_warning_result\": \"В резултат на това всички DNS заявки от вашата система ще бъдат обработвани от AdGuard Home по подразбиране.\",\n  \"autofix_warning_text\": \"Ако щракнете на \\\"Поправи\\\", AdGuard Home ще конфигурира вашата система да използва DNS сървър на AdGuard Home.\",\n  \"average_processing_time\": \"Средно време за обработка\",\n  \"average_processing_time_hint\": \"Средно време за обработка на DNS заявки в милисекунди\",\n  \"average_upstream_response_time\": \"Средно време за отговор на upstream\",\n  \"back\": \"Назад\",\n  \"block\": \"Блокирай\",\n  \"block_all\": \"Блокирайте всичко\",\n  \"block_domain_use_filters_and_hosts\": \"Блокирани домейни - общи и местни филтри\",\n  \"block_for_this_client_only\": \"Блокирайте само за този клиент\",\n  \"block_services\": \"Блокиране на специфични услуги\",\n  \"blocked_adult_websites\": \"Блокирано от Родителски Надзор\",\n  \"blocked_by\": \"<0>Блокирани от</0>\",\n  \"blocked_by_cname_or_ip\": \"Блокирано от CNAME или IP\",\n  \"blocked_by_response\": \"Блокирано от CNAME или IP в отговор\",\n  \"blocked_response_ttl\": \"TTL на блокиран отговор\",\n  \"blocked_response_ttl_desc\": \"Указва за колко секунди клиентите трябва да кешират филтриран отговор\",\n  \"blocked_safebrowsing\": \"Блокирано от безопасно сърфиране\",\n  \"blocked_service\": \"Блокирана услуга\",\n  \"blocked_services\": \"Блокирани услуги\",\n  \"blocked_services_desc\": \"Позволява бързо блокиране на популярни сайтове и услуги.\",\n  \"blocked_services_global\": \"Използвайте глобални блокирани услуги\",\n  \"blocked_services_saved\": \"Блокираните услуги успешно запазени\",\n  \"blocked_threats\": \"Блокирани заплахи\",\n  \"blocking_ipv4\": \"Блокиране на IPv4\",\n  \"blocking_ipv4_desc\": \"IP адрес, който ще се върне за блокирано A запитване\",\n  \"blocking_ipv6\": \"Блокиране на IPv6\",\n  \"blocking_ipv6_desc\": \"IP адрес, който трябва да се върне за блокирано AAAA запитване\",\n  \"blocking_mode\": \"Режим на блокиране\",\n  \"blocking_mode_custom_ip\": \"Персонализиран IP: Отговорете с ръчно зададен IP адрес\",\n  \"blocking_mode_default\": \"По подразбиране: Отговаряйте с код REFUSED, когато е блокиран от правило в стил Adblock; отговаряйте с IP адреса, посочен в правилото, когато е блокиран от правило в стил /etc/hosts.\",\n  \"blocking_mode_null_ip\": \"Нулев IP: Отговорете с нулев IP адрес (0.0.0.0 за A; :: за AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Отговаряйте с код NXDOMAIN\",\n  \"blocking_mode_refused\": \"ОТКАЗАНО: Отговорете с код ОТКАЗАН\",\n  \"blocklist\": \"Черен списък\",\n  \"bootstrap_dns\": \"Bootstrap DNS сървъри\",\n  \"bootstrap_dns_desc\": \"IP адреси на DNS сървърите, използвани за разрешаване на IP адресите на DoH/DoT разпознавателите, които определяте като нагоре по веригата. Коментарите не са разрешени.\",\n  \"cache_cleared\": \"Кешът на DNS успешно изчистен\",\n  \"cache_enabled\": \"Активиране на кеша\",\n  \"cache_enabled_desc\": \"Съхранявайте DNS отговорите локално\",\n  \"cache_optimistic\": \"Оптимистично кеширане\",\n  \"cache_optimistic_desc\": \"Нека AdGuard Home да отговори от кеша дори когато записите са изтекли и също така опитайте да ги обновите.\",\n  \"cache_size\": \"Размер на кеша\",\n  \"cache_size_desc\": \"Размер на кеша на DNS (в байтове).\",\n  \"cache_size_validation\": \"Размерът на кеша трябва да е по-голям от нула, когато е активиран.\",\n  \"cache_ttl_max_override\": \"Презаписване на максимален TTL\",\n  \"cache_ttl_max_override_desc\": \"Задайте максимална стойност на времето за живот (секунди) за записи в кеша на DNS.\",\n  \"cache_ttl_min_override\": \"Преобръщане на минималния TTL\",\n  \"cache_ttl_min_override_desc\": \"Разширете кратките стойности на времето на живот (секунди), получени от upstream сървъра при кеширане на DNS отговори.\",\n  \"cancel_btn\": \"Откажи\",\n  \"category_label\": \"Категория\",\n  \"check\": \"Провери\",\n  \"check_client_id\": \"Идентификатор на клиента (ClientID или IP адрес)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Проверете дали името на хоста е филтрирано.\",\n  \"check_dhcp_servers\": \"Проверка за активен DHCP сървър\",\n  \"check_dns_record\": \"Изберете тип DNS запис\",\n  \"check_enter_client_id\": \"Въведете идентификатор на клиента\",\n  \"check_hostname\": \"Име на хост или домейн\",\n  \"check_ip\": \"IP адреси: {{ip}}\",\n  \"check_not_found\": \"Не е намерено в списъците ви с филтри\",\n  \"check_reason\": \"Причина: {{reason}}\",\n  \"check_service\": \"Име на услугата: {{service}}\",\n  \"check_title\": \"Проверете филтрирането\",\n  \"check_updates_btn\": \"Провери за актуализация\",\n  \"check_updates_now\": \"Провери за актуализации\",\n  \"choose_allowlist\": \"Изберете списъци с разрешения\",\n  \"choose_blocklist\": \"Изберете списъци за блокиране\",\n  \"choose_from_list\": \"Изберете от списъка\",\n  \"city\": \"Град\",\n  \"clear_cache\": \"Изчисти кеша\",\n  \"click_to_view_queries\": \"Натиснете, за да видите запитвания\",\n  \"client_add\": \"Добавяне на клиент\",\n  \"client_added\": \"Клиент \\\"{{key}}\\\" успешно добавен\",\n  \"client_blocked\": \"Клиентът \\\"{{ip}}\\\" е успешно блокиран\",\n  \"client_confirm_block\": \"Наистина ли искате да блокирате клиента \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Наистина ли искате да изтриете клиента \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Наистина ли искате да деблокирате клиента \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Клиент \\\"{{key}}\\\" успешно изтрит\",\n  \"client_details\": \"Детайли на клиента\",\n  \"client_edit\": \"Редактиране на клиент\",\n  \"client_global_settings\": \"Използвайте глобалните настройки\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Клиентите могат да бъдат идентифицирани чрез ClientID. Научете повече за начина на идентифициране на клиенти <a>тук</a>.\",\n  \"client_id_placeholder\": \"Въведете ClientID\",\n  \"client_identifier\": \"Идентификатор\",\n  \"client_identifier_desc\": \"Клиентите могат да бъдат идентифицирани по техния IP адрес, CIDR, MAC адрес или ClientID (може да се използва за DoT/DoH/DoQ). Научете повече как да идентифицирате клиенти <0>тук</0>.\",\n  \"client_name\": \"Клиент {{id}}\",\n  \"client_new\": \"Нов клиент\",\n  \"client_settings\": \"Kлиентски настройки\",\n  \"client_table_header\": \"Клиент\",\n  \"client_unblocked\": \"Клиентът \\\"{{ip}}\\\" е успешно деблокиран\",\n  \"client_updated\": \"Клиент \\\"{{key}}\\\" успешно обновен\",\n  \"clients_desc\": \"Конфигурирайте клиентски записи за устройства, свързани с AdGuard Home\",\n  \"clients_not_found\": \"Нямa намерени адреси\",\n  \"clients_title\": \"Постоянни клиенти\",\n  \"compact\": \"Compact\",\n  \"config_successfully_saved\": \"Конфигурацията беше успешно запазена\",\n  \"configure\": \"Конфигурация\",\n  \"confirm_dns_cache_clear\": \"Сигурни ли сте, че искате да изчистите кеша на DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home ще конфигурира {{ip}} да бъде вашият статичен IP адрес. Искате ли да продължите?\",\n  \"copyright\": \"Авторско право\",\n  \"country\": \"Държава\",\n  \"custom_filter_rules\": \"Местни правила за филтриране\",\n  \"custom_filter_rules_hint\": \"Въвеждайте всяко правило на нов ред. Може да използвате adblock или hosts файлов синтаксис.\",\n  \"custom_filtering_rules\": \"Местни правила за филтриране\",\n  \"custom_ip\": \"Персонализиран IP\",\n  \"custom_retention_input\": \"Въведете срок на задържане в часове\",\n  \"custom_rotation_input\": \"Въведете ротация в часове\",\n  \"dashboard\": \"Табло\",\n  \"date\": \"Дата\",\n  \"default\": \"По подразбиране\",\n  \"delete_confirm\": \"Наистина ли искате да изтриете \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Изтрий\",\n  \"descr\": \"Описание\",\n  \"details\": \"Детайли\",\n  \"dhcp_add_static_lease\": \"Добавяне на статична наемна\",\n  \"dhcp_config_saved\": \"Запиши конфигурацията на DHCP сървъра\",\n  \"dhcp_description\": \"Ако рутера ви не раздава DHCP адреси, може да използвате вградения в AdGuard DHCP сървър.\",\n  \"dhcp_disable\": \"Забрани DHCP сървъра\",\n  \"dhcp_dynamic_ip_found\": \"Вашата система използва динамична конфигурация на IP адреса за интерфейс <0>{{interfaceName}}</0>. За да използвате DHCP сървър, трябва да бъде зададен статичен IP адрес. Вашият текущ IP адрес е <0>{{ipAddress}}</0>. AdGuard Home автоматично ще зададе този IP адрес като статичен, ако натиснете бутона \\\"Активиране на DHCP сървър\\\".\",\n  \"dhcp_edit_static_lease\": \"Редактиране на статичен наем\",\n  \"dhcp_enable\": \"Рзреши DHCP сървъра\",\n  \"dhcp_error\": \"AdGuard Home не можа да определи дали има друг активен DHCP сървър в мрежата\",\n  \"dhcp_form_gateway_input\": \"IP шлюз\",\n  \"dhcp_form_lease_input\": \"Отчет за раздадени адреси\",\n  \"dhcp_form_lease_title\": \"Отдадени адреси (секунди)\",\n  \"dhcp_form_range_end\": \"Последен адрес\",\n  \"dhcp_form_range_start\": \"Първи адрес\",\n  \"dhcp_form_range_title\": \"Група от IP адреси\",\n  \"dhcp_form_subnet_input\": \"Мрежова маска\",\n  \"dhcp_found\": \"Вашата мрежа вече има активен DHCP сървър. Не е безопасно ползването на втори!\",\n  \"dhcp_hardware_address\": \"Хардуерни адреси (MAC)\",\n  \"dhcp_interface_select\": \"Изберете мрежов адаптер за DHCP\",\n  \"dhcp_ip_addresses\": \"IP адреси\",\n  \"dhcp_ipv4_settings\": \"Change to 'DHCP IPv4 настройки.'\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 настройки\",\n  \"dhcp_lease_added\": \"Статичният наем \\\"{{key}}\\\" е успешно добавен\",\n  \"dhcp_lease_deleted\": \"Статичният лизинг \\\"{{key}}\\\" беше успешно изтрит\",\n  \"dhcp_lease_updated\": \"Статичният наем \\\"{{key}}\\\" е успешно обновен\",\n  \"dhcp_leases\": \"DHCP раздадени адреси\",\n  \"dhcp_leases_not_found\": \"Няма намерени активни DHCP адреси\",\n  \"dhcp_new_static_lease\": \"Нова статична наемна\",\n  \"dhcp_not_found\": \"Вашата мрежа няма активен DHCP сървър. Безопасно е ползването на вградения DHCP сървър.\",\n  \"dhcp_reset\": \"Сигурни ли сте, че искате да нулирате конфигурацията на DHCP?\",\n  \"dhcp_reset_leases\": \"Нулирайте всички наеми\",\n  \"dhcp_reset_leases_confirm\": \"Наистина ли искате да нулирате всички наеми?\",\n  \"dhcp_reset_leases_success\": \"Наемите на DHCP бяха успешно нулирани\",\n  \"dhcp_settings\": \"Настройки на DHCP\",\n  \"dhcp_static_ip_error\": \"За да използвате DHCP сървър, трябва да бъде зададен статичен IP адрес. AdGuard Home не успя да определи дали този мрежов интерфейс е конфигуриран с помощта на статичен IP адрес. Моля, задайте статичен IP адрес ръчно.\",\n  \"dhcp_static_leases\": \"DHCP статични наеми\",\n  \"dhcp_static_leases_not_found\": \"Няма намерени статични наеми DHCP\",\n  \"dhcp_table_expires\": \"История\",\n  \"dhcp_table_hostname\": \"Име на устройство\",\n  \"dhcp_title\": \"DHCP сървър (тестови!)\",\n  \"dhcp_warning\": \"Ако искате да използвате вградения DHCP сървър, трябва да няма друг активен DHCP в мрежата Ви!\",\n  \"disable_for_hours\": \"За {{count}} час\",\n  \"disable_for_hours_plural\": \"За {{count}} часа\",\n  \"disable_for_minutes\": \"За {{count}} минута\",\n  \"disable_for_minutes_plural\": \"За {{count}} минути\",\n  \"disable_for_seconds\": \"За {{count}} секунди\",\n  \"disable_for_seconds_plural\": \"За {{count}} секунди\",\n  \"disable_ipv6\": \"Изключете IPv6 протокола\",\n  \"disable_ipv6_desc\": \"Отхвърли всички DNS запитвания за IPv6 адреси (тип AAAA) и премахни IPv6 подсказките от HTTPS отговорите.\",\n  \"disable_notify_for_hours\": \"Деактивиране на защитата за {{count}} час\",\n  \"disable_notify_for_hours_plural\": \"Деактивиране на защитата за {{count}} часа\",\n  \"disable_notify_for_minutes\": \"Деактивиране на защитата за {{count}} минута\",\n  \"disable_notify_for_minutes_plural\": \"Деактивиране на защитата за {{count}} минути\",\n  \"disable_notify_for_seconds\": \"Отключи защитата за {{count}} секунда\",\n  \"disable_notify_for_seconds_plural\": \"Деактивиране на защитата за {{count}} секунди\",\n  \"disable_notify_until_tomorrow\": \"Деактивиране на защитата до утре\",\n  \"disable_protection\": \"Забрани защита\",\n  \"disable_rewrites\": \"Деактивиране на правилата за преписване\",\n  \"disable_until_tomorrow\": \"До утре\",\n  \"disabled\": \"Изключен\",\n  \"disabled_dhcp\": \"DHCP е забранен\",\n  \"disabled_filtering_toast\": \"Забрани филтрирането\",\n  \"disabled_parental_toast\": \"Забрани Родителски Надзор\",\n  \"disabled_protection\": \"Защитата е забранена\",\n  \"disabled_safe_browsing_toast\": \"Забрани безопасно-сърфиране\",\n  \"disabled_safe_search_toast\": \"Забрани Безопасно Търсене\",\n  \"disallow_this_client\": \"Забранете на този клиент\",\n  \"dns_addresses\": \"DNS адреси\",\n  \"dns_allowlists\": \"DNS разрешени списъци\",\n  \"dns_allowlists_desc\": \"Домейните от списъците с разрешения на DNS ще бъдат разрешени, дори ако са в някой от списъците с блокирани.\",\n  \"dns_blocklists\": \"DNS блоксписъци\",\n  \"dns_blocklists_desc\": \"AdGuard Home ще блокира домейни, които съвпадат с блоксписъците.\",\n  \"dns_cache_config\": \"Конфигурация на DNS кеш\",\n  \"dns_cache_config_desc\": \"Тук можете да конфигурирате кеша на DNS\",\n  \"dns_cache_size\": \"Размер на кеша на DNS, в байтове\",\n  \"dns_config\": \"Конфигурация на DNS сървъра\",\n  \"dns_over_https\": \"DNS-пред-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-над-TLS\",\n  \"dns_privacy\": \"Доверителност на DNS\",\n  \"dns_providers\": \"Ето един <0>списък с известни DNS доставчици</0>, от който да избирате.\",\n  \"dns_query\": \"DNS запитвания\",\n  \"dns_rewrites\": \"DNS презаписи\",\n  \"dns_settings\": \"DNS настройки\",\n  \"dns_start\": \"DNS сървърът стартира\",\n  \"dns_status_error\": \"Грешка при проверка на статуса на DNS сървъра\",\n  \"dns_test_not_ok_toast\": \"Сървър \\\"{{key}}\\\": не работи, моля проверете дали е въведен коректно\",\n  \"dns_test_ok_toast\": \"Въведените DNS сървъри работят коректно\",\n  \"dns_test_parsing_error_toast\": \"Раздел {{section}}: ред {{line}}: не може да се използва, моля, проверете дали сте го написали правилно\",\n  \"dns_test_warning_toast\": \"Ъпстриймът \\\"{{key}}\\\" не отговаря на тестовите заявки и може да не работи правилно\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Включете DNSSEC\",\n  \"dnssec_enable_desc\": \"Задайте флага DNSSEC в изходящите DNS запитвания и проверете резултата (нужен е резолвер с активиран DNSSEC).\",\n  \"domain\": \"Домейн\",\n  \"domain_desc\": \"Въведете име на домейн или wildcard, който искате да бъде пренаписан.\",\n  \"domain_name_table_header\": \"Име на домейн\",\n  \"domain_or_client\": \"Домейн или клиент\",\n  \"down\": \"Не работи\",\n  \"download_mobileconfig\": \"Изтеглете конфигурационния файл\",\n  \"download_mobileconfig_doh\": \"Изтеглете .mobileconfig за DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Изтеглете .mobileconfig за DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Редактиране на списък с разрешения\",\n  \"edit_blocklist\": \"Редактиране на списък за блокиране\",\n  \"edit_table_action\": \"Редактирай\",\n  \"edns_cs_desc\": \"Добавете опцията за подмрежа на клиента EDNS (ECS) към запитванията нагоре и записвайте стойностите, изпратени от клиентите в журнала на запитванията.\",\n  \"edns_enable\": \"Активиране на подмрежата на клиенти EDNS\",\n  \"edns_use_custom_ip\": \"Използване на персонализиран IP за EDNS\",\n  \"edns_use_custom_ip_desc\": \"Разрешаване на използването на персонализиран IP за EDNS\",\n  \"elapsed\": \"Изтекло\",\n  \"empty_response_status\": \"Празен\",\n  \"enable_protection\": \"Разреши защита\",\n  \"enable_protection_timer\": \"Защитата ще бъде активирана след {{time}}\",\n  \"enable_rewrites\": \"Активиране на правила за пренаписване\",\n  \"enable_upstream_dns_cache\": \"Активирайте кеширането на DNS за тази персонализирана конфигурация на upstream на клиента\",\n  \"enabled_dhcp\": \"DHCP е разрешен\",\n  \"enabled_filtering_toast\": \"Разреши фитрирането\",\n  \"enabled_parental_toast\": \"Разреши Родителски Надзор\",\n  \"enabled_protection\": \"Защитата е разрешена\",\n  \"enabled_safe_browsing_toast\": \"Рзреши безопасно-сърфиране\",\n  \"enabled_save_search_toast\": \"Разреши Безопасно Търсене\",\n  \"enabled_table_header\": \"Разреши\",\n  \"encryption_certificate_path\": \"Път на удостоверението\",\n  \"encryption_certificates\": \"Сертификати\",\n  \"encryption_certificates_desc\": \"За да използвате сигурна връзка, ще трябва да осигурите SSL сертификати за вашия домейн. Може да заявите безплатен от <0>{{link}}</0> или да закупите от Certificate Authorities.\",\n  \"encryption_certificates_input\": \"Копирай/постави вашия PEM-кодиран сертификат тук.\",\n  \"encryption_certificates_source_content\": \"Поставете съдържанието на сертификатите\",\n  \"encryption_certificates_source_path\": \"Задайте път за файл на сертификатите\",\n  \"encryption_chain_invalid\": \"Йерархията от сертификати е невалидна\",\n  \"encryption_chain_valid\": \"Йерархията от сертификати е валидна\",\n  \"encryption_config_saved\": \"Конфигурацията е успешно записана\",\n  \"encryption_desc\": \"Подържа се сигурна връзка (HTTPS/TLS) включително за DNS и страницата за администрация\",\n  \"encryption_doq\": \"Порт на DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"Ако този порт е конфигуриран, AdGuard Home ще стартира DNS-over-QUIC сървър на този порт. Той е експериментален и може да не е надежден. Освен това в момента не съществуват много клиенти, които да го подкрепят.\",\n  \"encryption_dot\": \"DNS-върху-TLS порт\",\n  \"encryption_dot_desc\": \"Ако порта е конфигуриран, AdGuard Home ще стартира и сървър за DNS-върху-TLS.\",\n  \"encryption_enable\": \"Разpеши криптиране (HTTPS, DNS-върху-HTTPS, и DNS-върху-TLS)\",\n  \"encryption_enable_desc\": \"Ако сте разрешили криптиране, страницата за Администрация на AdGuard Home ще бъде достъпна през HTTPS, и DNS сървъра ще отговаря също на запитвания DNS-върху-HTTPS и DNS-върху-TLS.\",\n  \"encryption_expire\": \"Годен до\",\n  \"encryption_hostnames\": \"Имена на хоста\",\n  \"encryption_https\": \"HTTPS порт\",\n  \"encryption_https_desc\": \"Ако зададете HTTPS порт, страницата за Администрация на AdGuard Home ще бъде достъпна на HTTPS, и също ще отговаря на DNS-върху-HTTPS '/dns-запитвания'.\",\n  \"encryption_issuer\": \"Изпълнител\",\n  \"encryption_key\": \"Частен ключ\",\n  \"encryption_key_input\": \"Копирай/постави вашия PEM-кодиран чpастен ключ за вашия сертификат тук.\",\n  \"encryption_key_invalid\": \"Това е невалиден {{type}} частен ключ\",\n  \"encryption_key_source_content\": \"Поставете съдържанието на ключа\",\n  \"encryption_key_source_path\": \"Задайте пътя до файла на частния ключ\",\n  \"encryption_key_valid\": \"Това е валиден {{type}} частен ключ\",\n  \"encryption_plain_dns_desc\": \"Обикновеният DNS е активиран по подразбиране. Можете да го деактивирате, за да принудите всички устройства да използват криптиран DNS. За да направите това, трябва да активирате поне един криптиран DNS протокол\",\n  \"encryption_plain_dns_enable\": \"Активиране на обикновен DNS\",\n  \"encryption_plain_dns_error\": \"За да деактивирате обикновения DNS, активирайте поне един криптиран DNS протокол\",\n  \"encryption_private_key_path\": \"Път на частния ключ\",\n  \"encryption_redirect\": \"Автоматично пренасочване към HTTPS\",\n  \"encryption_redirect_desc\": \"Служи за автоматично пренасочване от HTTP към HTTPS на страницата за Администрация в AdGuard Home.\",\n  \"encryption_reset\": \"Сигурни ли сте че искате да изтриете настройките за криптиране?\",\n  \"encryption_server\": \"Име на сървъра\",\n  \"encryption_server_desc\": \"Ако е зададено, AdGuard Home открива ClientIDs, отговаря на DDR запитвания и извършва допълнителни проверки на връзките. Ако не е зададено, тези функции са деактивирани. Трябва да съвпада с едно от имената на DNS в сертификата.\",\n  \"encryption_server_enter\": \"Въведете име на домейна\",\n  \"encryption_settings\": \"Настройки на криптиране\",\n  \"encryption_status\": \"Състояние\",\n  \"encryption_subject\": \"Тема\",\n  \"encryption_title\": \"Криптиране\",\n  \"encryption_warning\": \"Внимание\",\n  \"enforce_safe_search\": \"Включи Безопасно Търсене\",\n  \"enforce_save_search_hint\": \"AdGuard Home ще прилага безопасно търсене в следните търсачки: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Активирано Безопасно Търсене\",\n  \"enter_cache_size\": \"Въведете размер на кеша (байтове)\",\n  \"enter_cache_ttl_max_override\": \"Въведете максимален TTL (секунди)\",\n  \"enter_cache_ttl_min_override\": \"Въведете минимален TTL (секунди)\",\n  \"enter_name_hint\": \"Въведи име\",\n  \"enter_url_or_path_hint\": \"Въведете URL адрес или абсолютен път на списъка\",\n  \"enter_valid_allowlist\": \"Въведете валиден URL за списъка с разрешения.\",\n  \"enter_valid_blocklist\": \"Въведете валиден URL за списъка за блокиране.\",\n  \"error_details\": \"Подробности за грешка\",\n  \"example_comment\": \"! Това е коментар\",\n  \"example_comment_hash\": \"# Това е също коментар\",\n  \"example_comment_meaning\": \"пример за коментар\",\n  \"example_meaning_filter_block\": \"Блокирай достъп до домейн example.org и всички под домейни.\",\n  \"example_meaning_filter_whitelist\": \"Разреши достъп до домейн example.org и всичките му под домейни.\",\n  \"example_meaning_host_block\": \"AdGuard Home ще отговори с 127.0.0.1 = празен адрес за домейн example.org (но не и за под домейни).\",\n  \"example_multiple_upstreams_reserved\": \"множество upstreams <0>за специфични домейни</0>;\",\n  \"example_regex_meaning\": \"блокирай достъп до домейни който съвпадат със следното\",\n  \"example_rewrite_domain\": \"пренапиши отговорите само за това име на домейн.\",\n  \"example_rewrite_wildcard\": \"пренапиши отговорите за всички <0>example.org</0> поддомейни.\",\n  \"example_upstream_comment\": \"Можете да поставите коментар\",\n  \"example_upstream_doh\": \"криптиран <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-върху-HTTPS</a>\",\n  \"example_upstream_doh3\": \"криптиран DNS-върху-HTTPS с наложен <0>HTTP/3</0> и без резервиране на HTTP/2 или по-ниско;\",\n  \"example_upstream_doq\": \"криптиран <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"криптиран <a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-върху-TLS</a>\",\n  \"example_upstream_regular\": \"класически DNS (UDP протокол)\",\n  \"example_upstream_regular_port\": \"обикновен DNS (върху UDP, с порт);\",\n  \"example_upstream_reserved\": \"upstream <0>за специфични домейни</0>;\",\n  \"example_upstream_sdns\": \"може да ползвате <a href='https://dnscrypt.info/stamps/' target='_blank'>DNS Подписване</a> за <a href='https://dnscrypt.info/' target='_blank'>DNSCrypt</a> или <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-върху-HTTPS</a> сървъри\",\n  \"example_upstream_tcp\": \"класически DNS (TCP протокол)\",\n  \"example_upstream_tcp_hostname\": \"обикновен DNS (върху TCP, име на хост);\",\n  \"example_upstream_tcp_port\": \"обикновен DNS (върху TCP, с порт);\",\n  \"example_upstream_udp\": \"обикновен DNS (върху UDP, име на хост);\",\n  \"examples_title\": \"Примери\",\n  \"fallback_dns_desc\": \"Списък на резервни DNS сървъри, използвани, когато основните DNS сървъри не отговарят. Синтаксисът е същият, както в основното поле за upstream.\",\n  \"fallback_dns_placeholder\": \"Въведете по един резервен DNS сервер на ред\",\n  \"fallback_dns_title\": \"Резервни DNS сървъри\",\n  \"faq\": \"ЧЗВ\",\n  \"fastest_addr\": \"Най-бърз IP адрес\",\n  \"fastest_addr_desc\": \"Изчакайте отговори от <b>всички</b> DNS сървъри, измерете TCP скоростта на свързване за всеки сървър и върнете IP адреса на сървъра с най-бързата скорост на свързване.<br/>Този режим може значително да забави DNS запитвания, ако един или повече сървъри нагоре по веригата не отговарят. Уверете се, че вашите сървъри нагоре по веригата са стабилни и времето за изчакване нагоре по веригата е ниско.\",\n  \"filter\": \"Филтър\",\n  \"filter_added_successfully\": \"Списъкът беше успешно добавен\",\n  \"filter_allowlist\": \"ВНИМАНИЕ: Тази операция ще изключи правилото \\\"{{disallowed_rule}}\\\" от списъка на разрешените клиенти.\",\n  \"filter_category_general\": \"General\",\n  \"filter_category_general_desc\": \"Списъци, които блокират проследяването и рекламата на повечето устройства\",\n  \"filter_category_other\": \"Друго\",\n  \"filter_category_other_desc\": \"Други списъци с блокиране\",\n  \"filter_category_regional\": \"Регионални\",\n  \"filter_category_regional_desc\": \"Списъци, които се фокусират върху регионалните реклами и сървърите за проследяване\",\n  \"filter_category_security\": \"Сигурност\",\n  \"filter_category_security_desc\": \"Списъци, предназначени специално за блокиране на злонамерени, фишинг и измамни домейни\",\n  \"filter_removed_successfully\": \"Списъкът е успешно премахнат\",\n  \"filter_updated\": \"Списъкът е актуализиран успешно\",\n  \"filtered\": \"Филтрирано\",\n  \"filtered_custom_rules\": \"Филтрирано по персонализирани правила за филтриране\",\n  \"filtering_rules_learn_more\": \"<0>Научете повече</0> за създаването на собствени списъци с хостове.\",\n  \"filters\": \"Филтри\",\n  \"filters_and_hosts_hint\": \"AdGuard Home разбира adblock и host синтаксис.\",\n  \"filters_block_toggle_hint\": \"Може да зададете собствени настройки в <a>Филтри</a>.\",\n  \"filters_configuration\": \"Конфигурация на филтри\",\n  \"filters_enable\": \"Активиране на филтри\",\n  \"filters_interval\": \"Интервал за актуализация на филтрите\",\n  \"fix\": \"Поправи\",\n  \"for_last_days\": \"за последния {{count}} ден\",\n  \"for_last_days_plural\": \"за последните {{count}} дни\",\n  \"for_last_hours\": \"за последния {{count}} час\",\n  \"for_last_hours_plural\": \"за последните {{count}} часа\",\n  \"forgot_password\": \"Забравили сте паролата?\",\n  \"forgot_password_desc\": \"Моля, следвайте <0>тези стъпки</0>, за да създадете нова парола за вашия акаунт.\",\n  \"form_add_id\": \"Добавете идентификатор\",\n  \"form_answer\": \"Въведете IP адрес или име на домейн\",\n  \"form_client_name\": \"Въведете името на клиента\",\n  \"form_domain\": \"Въведете име на домейн или wildcard\",\n  \"form_enter_blocked_response_ttl\": \"Въведете TTL за блокиран отговор (секунди)\",\n  \"form_enter_host\": \"Въведете име на хост.\",\n  \"form_enter_hostname\": \"Въведете име на хост\",\n  \"form_enter_id\": \"Въведете идентификатор\",\n  \"form_enter_ip\": \"Въведете IP\",\n  \"form_enter_mac\": \"Въведете MAC\",\n  \"form_enter_rate_limit\": \"Въведете ограничение на скоростта\",\n  \"form_enter_rate_limit_subnet_len\": \"Въведете дължина на префикса на подсетевите адреси за ограничаване на скоростта\",\n  \"form_enter_subnet_ip\": \"Въведете IP адрес в подмрежата \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Въведете времевия интервал за изчакване на сървър нагоре по веригата в секунди.\",\n  \"form_error_answer_format\": \"Невалиден формат на отговор\",\n  \"form_error_client_id_format\": \"ClientID трябва да съдържа само цифри, малки букви и тирета\",\n  \"form_error_domain_format\": \"Невалиден формат на домейн\",\n  \"form_error_equal\": \"Не трябва да съвпада\",\n  \"form_error_gateway_ip\": \"Наемът не може да има IP адреса на шлюза\",\n  \"form_error_ip4_format\": \"Невалиден IPv4 адрес\",\n  \"form_error_ip4_gateway_format\": \"Невалиден IPv4 адрес на шлюза\",\n  \"form_error_ip6_format\": \"Невалиден IPv6 адрес\",\n  \"form_error_ip_format\": \"Невалиден IPv4 адрес\",\n  \"form_error_mac_format\": \"Невалиден MAC адрес\",\n  \"form_error_password\": \"Паролата не съвпада\",\n  \"form_error_password_length\": \"Паролата трябва да бъде дълга от {{min}} до {{max}} символа\",\n  \"form_error_port\": \"Моля въведете валиден порт\",\n  \"form_error_port_range\": \"Въведете порт в диапазона 80-65535\",\n  \"form_error_port_unsafe\": \"Не е безопасно да използвате този порт\",\n  \"form_error_positive\": \"Проверете дали е положително число\",\n  \"form_error_required\": \"Задължително поле\",\n  \"form_error_server_name\": \"Невалидно име на сървъра\",\n  \"form_error_subnet\": \"Подмрежата \\\"{{cidr}}\\\" не съдържа IP адреса \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Невалиден формат на URL\",\n  \"form_error_url_or_path_format\": \"Невалиден URL или абсолютен път на списъка\",\n  \"form_select_tags\": \"Изберете тагове на клиента\",\n  \"found_in_known_domain_db\": \"Намерен в списъците с домейни.\",\n  \"friday\": \"Петък\",\n  \"friday_short\": \"Пт\",\n  \"gateway_or_subnet_invalid\": \"Невалидна маска на подмрежа\",\n  \"general_settings\": \"Общи настройки\",\n  \"general_statistics\": \"Обща статисика\",\n  \"get_started\": \"Да започваме\",\n  \"greater_range_start_error\": \"Трябва да бъде по-голямо от началото на диапазона\",\n  \"homepage\": \"Домашна страница\",\n  \"host_whitelisted\": \"Хостът е разрешен\",\n  \"ignore_domains\": \"Игнорирани домейни (разделени с нов ред)\",\n  \"ignore_domains_desc_query\": \"Запитванията за тези домейни не се записват в дневника на запитванията\",\n  \"ignore_domains_desc_stats\": \"Запитванията за тези домейни не се записват в статистиката\",\n  \"ignore_domains_title\": \"Игнорирани домейни\",\n  \"ignore_query_log\": \"Игнорирайте този клиент в логовете на заявките\",\n  \"ignore_statistics\": \"Игнорирайте този клиент в статистиките\",\n  \"install_auth_confirm\": \"Потвърдете паролата\",\n  \"install_auth_desc\": \"Много е важно да зададете име и парола за достъп до вашия панел за администрация на AdGuard Home. Препоръчваме ви да зададете име и парола независимо че го ползвате само в къщи.\",\n  \"install_auth_password\": \"Парола\",\n  \"install_auth_password_enter\": \"Въведете парола\",\n  \"install_auth_title\": \"Удостоверяване\",\n  \"install_auth_username\": \"Потребител\",\n  \"install_auth_username_enter\": \"Въведете потребител\",\n  \"install_devices_address\": \"AdGuard Home DNS сървърът е на следния адрес\",\n  \"install_devices_android_list_1\": \"Изберете Android Меню от домашния екран, и цъкнете на Настройки.\",\n  \"install_devices_android_list_2\": \"Цъкнете на Wi-Fi меню. На екрана ще се появат всички безжични прежи (там няма възможност за въвеждане на DNS настройки).\",\n  \"install_devices_android_list_3\": \"Цъкнете и задръжде върху Вие сте свързани с.., и кликнете на Модифицирай мрежа.\",\n  \"install_devices_android_list_4\": \"На някой устройства може да е неоходимо да маркирате покажи Разширени, за да видите всички настройки. За да промените Android DNS настройките, може да се наложи да промените IP настройките от DHCP на Статични.\",\n  \"install_devices_android_list_5\": \"Променете стойностите на DNS 1 и DNS 2 да използват AdGuard Home сървъра.\",\n  \"install_devices_desc\": \"Да започнете да използвате AdGuard Home, е необходимо да настроите вашите устройства.\",\n  \"install_devices_ios_list_1\": \"От начален екран, цъкнете на Settings.\",\n  \"install_devices_ios_list_2\": \"Изберете Wi-Fi от лявото меню (там няма възможност за въвеждане на DNS настройки).\",\n  \"install_devices_ios_list_3\": \"Натиснете на името на активната мрежа.\",\n  \"install_devices_ios_list_4\": \"В полето за DNS изберете ръчно и въведете адреса на AdGuard Home сървъра.\",\n  \"install_devices_macos_list_1\": \"Цъкнете на Apple иконката и изберете System Preferences...\",\n  \"install_devices_macos_list_2\": \"Цъкнете на Network.\",\n  \"install_devices_macos_list_3\": \"Изберете зелената-активна връзка в списъка и кликнете на Advanced.\",\n  \"install_devices_macos_list_4\": \"Изберете DNS таб и кликнете на + за да въведете адреса на AdGuard Home сървъра.\",\n  \"install_devices_router\": \"Рутер\",\n  \"install_devices_router_desc\": \"Ако настроите вашият рутер няма нужда ръчно да настройвате всяко едно от устрйствата в мрежата.\",\n  \"install_devices_router_list_1\": \"Отворете страницата за настройки на вашия рутер. Обикновено тя се намира на URL (тук http://192.168.0.1/ или тук http://192.168.1.1/). За достъп може да ви трябва парола. Ако сте забравили паролата може да я ресетнете като натиснета скрития ресет бутон - внимание това ще ресетне всички настройки на рутера до фабрични! Някой рутери могат да бъдате администрирани от софтуер или приложение, който би трябвало да е вече инсталиран на компютъра/телефона ви.\",\n  \"install_devices_router_list_2\": \"Намерета DHCP/DNS настройки. В под раздел DHCP рзгледайте и намерете къде е полето за DNS настройка в което може да въведете персонализирани настройки за DNS сървъри.\",\n  \"install_devices_router_list_3\": \"Въведете адресът на AdGuard Home сървъра.\",\n  \"install_devices_router_list_4\": \"На някои типове маршрутизатори не може да се зададе персонализиран DNS сървър. В този случай може да помогне настройването на AdGuard Home като <0>DHCP сървър</0>. В противен случай трябва да проверите ръководството на маршрутизатора за информация как да персонализирате DNS сървърите на конкретния модел на вашия маршрутизатор.\",\n  \"install_devices_title\": \"Настройте вашето устройство\",\n  \"install_devices_windows_list_1\": \"Отворете Контролния Панел през Старт меню или чрез функция търсене на Windows.\",\n  \"install_devices_windows_list_2\": \"Вървете до Настрйки на Мрежи и Интернет и от там изберете Мрежи и Център за Споделяне.\",\n  \"install_devices_windows_list_3\": \"От ляво на екрана намерете Смени настроки на мрежовия адаптер и кликнете на него.\",\n  \"install_devices_windows_list_4\": \"Изберете този който е активен, дясно-кликване и изберета Свойства.\",\n  \"install_devices_windows_list_5\": \"Намерете Интернет Протокол Версия 4 (TCP/IP) в списъка, изберете и кликнете отново на Свойства.\",\n  \"install_devices_windows_list_6\": \"Изберете Използвай следните адреси за DNS сърсъри и въведете адреса на AdGuard Home сървъра ви.\",\n  \"install_saved\": \"Успешно записано\",\n  \"install_settings_all_interfaces\": \"Всички интерфейси\",\n  \"install_settings_dns\": \"DNS сървър\",\n  \"install_settings_dns_desc\": \"За да работи, ще трябва да настроите вашият рутер или устройства да ползват DNS сървър с адрес:\",\n  \"install_settings_interface_link\": \"Вашата AdGuard Home страница за администрация ще е достъпна на този адрес:\",\n  \"install_settings_listen\": \"Активни интерфейси\",\n  \"install_settings_port\": \"Порт\",\n  \"install_settings_title\": \"Администрация\",\n  \"install_static_configure\": \"AdGuard Home е открил, че динамичният IP адрес <0>{{ip}}</0> се използва. Искате ли да бъде зададен като ваш статичен адрес?\",\n  \"install_static_error\": \"AdGuard Home не може автоматично да го конфигурира за този мрежов интерфейс. Моля, потърсете инструкция как да го направите ръчно.\",\n  \"install_static_ok\": \"Добри новини! Статичният IP адрес вече е конфигуриран\",\n  \"install_step\": \"Стъпка\",\n  \"install_submit_desc\": \"Настройката е завършена, може да започнете да ползвате AdGuard Home.\",\n  \"install_submit_title\": \"Поздравления!\",\n  \"install_welcome_desc\": \"AdGuard Home e мрежово решение за блокиране на реклами и тракери на DNS ниво. Създадено е за да ви даде пълен контрол над мрежата и всичките ви устройства, без да е необходимо допълнително инсталиране на друг софтуер.\",\n  \"install_welcome_title\": \"Добре дошли в AdGuard Home!\",\n  \"interval_24_hour\": \"24 часа\",\n  \"interval_6_hour\": \"6 часа\",\n  \"interval_days\": \"{{count}} ден\",\n  \"interval_days_plural\": \"{{count}} дни\",\n  \"interval_hours\": \"{{count}} час\",\n  \"interval_hours_plural\": \"{{count}} часа\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Потребителски IP адрес\",\n  \"known_tracker\": \"Известен проследяващ\",\n  \"last_rule_in_allowlist\": \"Не можете да забраните този клиент, тъй като изключването на правилото \\\"{{disallowed_rule}}\\\" ще ДЕАКТИВИРА списъка с \\\"Разрешени клиенти\\\".\",\n  \"last_time_updated_table_header\": \"Последно обновен\",\n  \"list_confirm_delete\": \"Сигурни ли сте, че искате да изтриете този списък?\",\n  \"list_label\": \"Списък\",\n  \"list_updated\": \"{{count}} списък актуализиран\",\n  \"list_updated_plural\": \"{{count}} списъка актуализирани\",\n  \"list_url_table_header\": \"URL на списъка\",\n  \"load_balancing\": \"Балансиране на натоварването\",\n  \"load_balancing_desc\": \"Запитвайте по един сървър нагоре по веригата наведнъж.<br/>AdGuard Home използва тежестен произволен алгоритъм за избор на сървъри с най-малък брой неуспешни заявки и най-ниско средно време за запитване.\",\n  \"loading_table_status\": \"Зареждане...\",\n  \"local_ptr_default_resolver\": \"По подразбиране AdGuard Home използва следните обратни DNS резолвери: {{ip}}.\",\n  \"local_ptr_desc\": \"Сървърите на DNS, използвани от AdGuard Home за частни запроси PTR, SOA и NS. Запитване се счита за частно, ако иска домейн ARPA, съдържащ подмрежа в рамките на частни IP диапазони (като \\\"192.168.12.34\\\") и идва от клиент с частен IP адрес. Ако не е зададено, ще се използват стандартните DNS резолвари на вашата ОС, с изключение на IP адресите на AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home не можа да определи подходящи частни обратни DNS резолвери за тази система.\",\n  \"local_ptr_placeholder\": \"Въведете по един IP адрес на ред\",\n  \"local_ptr_title\": \"Частни обратни DNS сървъри\",\n  \"location\": \"Местоположение\",\n  \"log_and_stats_section_label\": \"Лог на заявките и статистика\",\n  \"lower_range_start_error\": \"Трябва да е по-малко от началото на диапазона\",\n  \"main_settings\": \"Основни настройки\",\n  \"make_static\": \"Направи статичен\",\n  \"manual_update\": \"Моля, <a>следвайте тези стъпки</a>, за да актуализирате ръчно.\",\n  \"milliseconds_abbreviation\": \"мс\",\n  \"monday\": \"Понеделник\",\n  \"monday_short\": \"Пон\",\n  \"name\": \"Име\",\n  \"name_table_header\": \"Име\",\n  \"netname\": \"Име на мрежата\",\n  \"network\": \"Мрежа\",\n  \"new_allowlist\": \"Нов списък с разрешения\",\n  \"new_blocklist\": \"Нов списък за блокиране\",\n  \"next\": \"Следващ\",\n  \"next_btn\": \"Следващ\",\n  \"no_blocklist_added\": \"Няма добавени списъци с блокирани\",\n  \"no_clients_found\": \"Нямa намерени адреси\",\n  \"no_domains_found\": \"Няма намерени резултати\",\n  \"no_logs_found\": \"Няма история\",\n  \"no_servers_specified\": \"Няма избрани услуги\",\n  \"no_upstreams_data_found\": \"Не са намерени данни за upstream-ове\",\n  \"no_whitelist_added\": \"Няма добавени списъци с разрешени\",\n  \"nothing_found\": \"Нищо не е намерено\",\n  \"null_ip\": \"Нулев IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Сума на блокирани DNS заявки от филтрите за реклама и местни\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Сума на блокирани сайтове за възрастни\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Сума на блокирани DNS заявки от AdGuard свързани със сигурността\",\n  \"number_of_dns_query_days\": \"Броят на обработените DNS запитвания за последния {{count}} ден\",\n  \"number_of_dns_query_days_plural\": \"Броят на обработените DNS запитвания за последните {{count}} дни\",\n  \"number_of_dns_query_hours\": \"Броят на DNS запитвания, обработени за последния {{count}} час\",\n  \"number_of_dns_query_hours_plural\": \"Броят на DNS запитвания, обработени за последните {{count}} часа\",\n  \"number_of_dns_query_to_safe_search\": \"Сума на DNS заявки при който е приложено Безопасно Търсене\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"ИЗКЛЮЧЕНО\",\n  \"on\": \"ВКЛЮЧЕНО\",\n  \"open_dashboard\": \"Отвори табло\",\n  \"orgname\": \"Име на организацията\",\n  \"original_response\": \"Оригинален отговор\",\n  \"out_of_range_error\": \"Трябва да е извън обхвата \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Страница\",\n  \"parallel_requests\": \"Паралелни заявки\",\n  \"parental_control\": \"Родителски контрол\",\n  \"password_label\": \"Парола\",\n  \"password_placeholder\": \"Въведете парола\",\n  \"plain_dns\": \"Обикновен DNS\",\n  \"port_53_faq_link\": \"Порт 53 често е зает от \\\"DNSStubListener\\\" или \\\"systemd-resolved\\\" услуги. Моля, прочетете <0>тази инструкция</0> как да решите това.\",\n  \"previous_btn\": \"Предходен\",\n  \"privacy_policy\": \"Правила за поверителност\",\n  \"processing_update\": \"Моля, изчакайте, AdGuard Home се актуализира\",\n  \"protection_section_label\": \"Защита\",\n  \"protocol\": \"Протокол\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"История на заявките\",\n  \"query_log_clear\": \"Изчисти дневниците на запитванията\",\n  \"query_log_cleared\": \"Дневникът на запитванията е изчистен успешно\",\n  \"query_log_configuration\": \"Конфигурация на дневниците\",\n  \"query_log_confirm_clear\": \"Сигурни ли сте, че искате да изчистите целия дневник на запитванията?\",\n  \"query_log_disabled\": \"Дневникът на запитванията е деактивиран и може да бъде конфигуриран в <0>настройките</0>\",\n  \"query_log_enable\": \"Активиране на историята\",\n  \"query_log_filtered\": \"Филтрирано от {{filter}}\",\n  \"query_log_response_status\": \"Статус: {{value}}\",\n  \"query_log_retention\": \"Ротация на дневниците на запитвания\",\n  \"query_log_retention_confirm\": \"Наистина ли искате да промените ротацията на дневника със запитвания? Ако намалите стойността на интервала, някои данни ще бъдат изгубени\",\n  \"query_log_strict_search\": \"Използвайте двойни кавички за стриктно търсене\",\n  \"query_log_updated\": \"Журналът на запитванията беше успешно актуализиран\",\n  \"rate_limit\": \"Ограничение на скоростта\",\n  \"rate_limit_desc\": \"Броят на заявките на секунда, разрешени за всеки клиент. Задаването на 0 означава, че няма ограничение.\",\n  \"rate_limit_subnet_len_ipv4\": \"Дължина на префикса на подмрежата за IPv4 адреси\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Дължина на префикса на подмрежата за IPv4 адреси, използвани за ограничаване на скоростта. Стандартно е 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Дължината на префикса на подмрежата IPv4 трябва да бъде между 0 и 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Дължина на префикса на подмрежата за IPv6 адреси\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Дължина на префикса на подсетеви адреси за IPv6, използвани за ограничаване на скоростта. Стандартната стойност е 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Дължината на префикса на подсетевите адреси за IPv6 трябва да бъде между 0 и 128\",\n  \"rate_limit_whitelist\": \"Разрешен списък за ограничаване на скоростта\",\n  \"rate_limit_whitelist_desc\": \"IP адреси, изключени от ограничаването на скоростта\",\n  \"rate_limit_whitelist_placeholder\": \"Въведете по един IP адрес на ред\",\n  \"refresh_btn\": \"Обнови\",\n  \"refresh_statics\": \"Обнови статистиката\",\n  \"refused\": \"ОТКАЗАНО\",\n  \"report_an_issue\": \"Съобщи за проблем\",\n  \"request_details\": \"Поискайте подробности\",\n  \"request_table_header\": \"Заявка\",\n  \"requests_count\": \"Сума на заявките\",\n  \"reset_settings\": \"Изтрий всички настройки\",\n  \"resolve_clients_desc\": \"Обратно разрешаване на IP адресите на клиентите в техните имена на хостове, като се изпращат PTR заявки до съответните резолвъри (частни DNS сървъри за локални клиенти, upstream сървъри за клиенти с публични IP адреси).\",\n  \"resolve_clients_title\": \"Активиране на обратното разрешаване на IP адресите на клиентите\",\n  \"response_code\": \"Код на отговор\",\n  \"response_details\": \"Подробности за отговора\",\n  \"response_table_header\": \"Отговор\",\n  \"response_time\": \"Време за отговор\",\n  \"rewrite_A\": \"<0>A</0>: специална стойност, запазете <0>A</0> записи от upstream\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: специална стойност, запазете <0>AAAA</0> записи от upstream\",\n  \"rewrite_add\": \"Добави DNS презапис\",\n  \"rewrite_added\": \"DNS презапис за \\\"{{key}}\\\" успешно добавен\",\n  \"rewrite_applied\": \"Правилото за презапис е приложено\",\n  \"rewrite_confirm_delete\": \"Сигурни ли сте, че искате да изтриете DNS презапис за \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"DNS презапис за \\\"{{key}}\\\" успешно изтрит\",\n  \"rewrite_desc\": \"Позволява лесно да конфигурирате персонализиран DNS отговор за конкретно име на домейн.\",\n  \"rewrite_domain_name\": \"Име на домейн: добавете запис CNAME\",\n  \"rewrite_edit\": \"Редактиране на ДНС перезаписване\",\n  \"rewrite_hosts_applied\": \"Преписано по правило на файла hosts\",\n  \"rewrite_ip_address\": \"IP адрес: използвайте този IP в отговор A или AAAA\",\n  \"rewrite_not_found\": \"Не са намерени DNS презаписи\",\n  \"rewrite_settings_updated\": \"Успешно актуализиране на настройките за презапис на DNS\",\n  \"rewrite_updated\": \"ДНС перезаписването беше успешно актуализирано\",\n  \"rewrites_disabled_table_header\": \"Презаписванията са деактивирани\",\n  \"rewrites_enabled_table_header\": \"Пренаписванията са активирани\",\n  \"rewritten\": \"Преработено\",\n  \"rows_table_footer_text\": \"редове\",\n  \"rule_added_to_custom_filtering_toast\": \"Добавено до местни правила за филтриране: {{rule}}\",\n  \"rule_label\": \"Правило\",\n  \"rule_removed_from_custom_filtering_toast\": \"Премахнато от местни правила за филтриране: {{rule}}\",\n  \"rules_count_table_header\": \"Правила общо\",\n  \"safe_browsing\": \"Безопасно сърфиране\",\n  \"safe_search\": \"Безопасно търсене\",\n  \"saturday\": \"Събота\",\n  \"saturday_short\": \"Съб\",\n  \"save_btn\": \"Запази\",\n  \"save_config\": \"Запиши настройките\",\n  \"schedule_add\": \"Добавяне на график\",\n  \"schedule_current_timezone\": \"Текуща часова зона: {{value}}\",\n  \"schedule_desc\": \"Настройте периодите на неактивност за блокираните услуги\",\n  \"schedule_edit\": \"Редактиране на график\",\n  \"schedule_from\": \"От\",\n  \"schedule_invalid_select\": \"Времето за начало трябва да е преди времето за край\",\n  \"schedule_modal_description\": \"Този график ще замени всички съществуващи графици за същия ден от седмицата. Всеки ден от седмицата може да има само един период на неактивност.\",\n  \"schedule_modal_time_off\": \"Няма блокиране на услуги:\",\n  \"schedule_new\": \"Нов график\",\n  \"schedule_remove\": \"Премахване на график\",\n  \"schedule_save\": \"Запиши графика\",\n  \"schedule_select_days\": \"Изберете дни\",\n  \"schedule_services\": \"Пауза на блокирането на услугата\",\n  \"schedule_services_desc\": \"Конфигурирайте графика за паузи на филтъра за блокиране на услуги\",\n  \"schedule_services_desc_client\": \"Конфигурирайте графика за паузи на филтъра за блокиране на услуги за този клиент\",\n  \"schedule_time_all_day\": \"Цял ден\",\n  \"schedule_timezone\": \"Изберете часова зона\",\n  \"schedule_to\": \"До\",\n  \"served_from_cache_label\": \"Предоставено от кеша\",\n  \"service_name\": \"Име на услугата\",\n  \"set_static_ip\": \"Задайте статичен IP адрес\",\n  \"settings\": \"Настройки\",\n  \"settings_custom\": \"Персонализиране\",\n  \"settings_global\": \"Глобални\",\n  \"setup_config_to_enable_dhcp_server\": \"Настройка на конфигурация за активиране на DHCP сървър\",\n  \"setup_dns_notice\": \"За да използвате <1>DNS-over-HTTPS</1> или <1>DNS-over-TLS</1>, трябва да <0>конфигурирате Шифроване</0> в настройките на AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-върху-TLS:</0> Използвайте <1>{{address}}</1> низ.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-върху-HTTPS:</0> Използвайте <1>{{address}}</1> низ.\",\n  \"setup_dns_privacy_3\": \"<0>Ето списък с приложения, които можете да използвате.</0>\",\n  \"setup_dns_privacy_4\": \"На устройство с iOS 14 или macOS Big Sur можете да изтеглите специален файл '.mobileconfig', който добавя <highlight>DNS-over-HTTPS</highlight> или <highlight>DNS-over-TLS</highlight> сървъри към настройките на DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 поддържа DNS-върху-TLS нативно. За да го конфигурирате, отидете на Настройки → Мрежа и интернет → Разширени настройки → Частен DNS и въведете името на домейна си там.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard за Android</0> поддържа <1>DNS-върху-HTTPS</1> и <1>DNS-върху-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> добавя поддръжка за <1>DNS-върху-HTTPS</1> към Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Конфигурация за iOS и macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> поддържа <1>DNS-over-HTTPS</1>, но за да го конфигурирате да използва вашия собствен сървър, ще трябва да генерирате <2>DNS Stamp</2> за него.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard за iOS</0> поддържа <1>DNS-over-HTTPS</1> и <1>DNS-over-TLS</1> конфигурация.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home сам може да бъде сигурен клиент на DNS на всяка платформа.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> поддържа всички известни сигурни DNS протоколи.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> поддържа <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> поддържа <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Ще намерите повече реализации <0>тук</0> и <1>тук</1>.\",\n  \"setup_dns_privacy_other_title\": \"Други реализации\",\n  \"setup_guide\": \"Наръчник за настройка\",\n  \"show_all_filter_type\": \"Покажи всички\",\n  \"show_blocked_responses\": \"Блокирано\",\n  \"show_filtered_type\": \"Покажи филтрирани\",\n  \"show_processed_responses\": \"Обработен\",\n  \"show_whitelisted_responses\": \"В белия списък\",\n  \"sign_in\": \"Вход\",\n  \"sign_out\": \"Изход\",\n  \"source_label\": \"Източник\",\n  \"static_ip\": \"Статичен IPv4 адрес\",\n  \"static_ip_desc\": \"AdGuard Home е сървър, затова му е нужна статична IP адреса, за да функционира правилно. В противен случай, в определен момент, вашия рутер може да присвои различен IP адрес на това устройство.\",\n  \"statistics_clear\": \"Нулирай статистиката\",\n  \"statistics_clear_confirm\": \"Наистина ли искате да изчистите статистиката?\",\n  \"statistics_cleared\": \"Статистиките бяха успешно изчистени\",\n  \"statistics_configuration\": \"Конфигурация на статистиката\",\n  \"statistics_enable\": \"Активиране на статистиките\",\n  \"statistics_retention\": \"Запазване на статистиката\",\n  \"statistics_retention_confirm\": \"Наистина ли искате да промените запазването на статистиките? Ако намалите стойността на интервала, някои данни ще бъдат изгубени\",\n  \"statistics_retention_desc\": \"Ако намалите стойността на интервала, някои данни ще бъдат загубени\",\n  \"stats_adult\": \"сайтове за възрастни\",\n  \"stats_disabled\": \"Статистиката е деактивирана. Можете да я активирате от <0>страницата за настройки</0>.\",\n  \"stats_disabled_short\": \"Статистиката е деактивирана.\",\n  \"stats_malware_phishing\": \"вируси/атаки\",\n  \"stats_params\": \"Конфигурация на статистиката\",\n  \"stats_query_domain\": \"Най-отваряни страници\",\n  \"subnet_error\": \"Адресите трябва да бъдат в една подмрежа\",\n  \"sunday\": \"Неделя\",\n  \"sunday_short\": \"Нд\",\n  \"system_host_files\": \"Файлове на системен хост\",\n  \"table_client\": \"Клиент\",\n  \"table_name\": \"Име\",\n  \"tags_desc\": \"Можете да изберете таговете, които съответстват на клиента. Включете таговете в правилата за филтриране, за да ги приложите по-точно. <0>Научете повече</0>.\",\n  \"tags_title\": \"Тагове\",\n  \"test_upstream_btn\": \"Тествай главния DNS\",\n  \"theme_auto\": \"Автоматично\",\n  \"theme_auto_desc\": \"Авто (въз основа на цветната схема на вашето устройство)\",\n  \"theme_dark\": \"Тъмна тема\",\n  \"theme_dark_desc\": \"Тъмна тема\",\n  \"theme_light\": \"Светла тема\",\n  \"theme_light_desc\": \"Светла тема\",\n  \"thursday\": \"Четвъртък\",\n  \"thursday_short\": \"Чт\",\n  \"time_table_header\": \"Време\",\n  \"top_blocked_domains\": \"Най-блокирани страници\",\n  \"top_clients\": \"Най-активни IP адреси\",\n  \"top_upstreams\": \"Най-добри upstream сървъри\",\n  \"topline_expired_certificate\": \"Вашият SSL сертификат е изтекъл. Обнови <0>Настройки за криптиране</0>.\",\n  \"topline_expiring_certificate\": \"Вашият SSL сертификат изтича. Обнови <0>Настройки за криптиране</0>.\",\n  \"tracker_source\": \"Източник на проследяването\",\n  \"try_again\": \"Опитай пак\",\n  \"ttl_cache_validation\": \"Минималната стойност на TTL за кеша трябва да бъде по-малка или равна на максималната\",\n  \"tuesday\": \"Вторник\",\n  \"tuesday_short\": \"Вт\",\n  \"type_table_header\": \"Тип\",\n  \"unavailable_dhcp\": \"DHCP не е наличен\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home не може да стартира DHCP сървър във вашата операционна система\",\n  \"unblock\": \"Отблокирай\",\n  \"unblock_all\": \"Отблокирай всичко\",\n  \"unblock_for_this_client_only\": \"Деблокирайте само за този клиент\",\n  \"unknown_filter\": \"Непознат филтър {{filterId}}\",\n  \"update_announcement\": \"Има нова AdGuard Home {{version}}! <0>Цъкни тук</0> за повече информация.\",\n  \"update_failed\": \"Авто-актуализацията не успя. Моля, <a>следвайте тези стъпки</a>, за да актуализирате ръчно.\",\n  \"update_now\": \"Актуализирай сега\",\n  \"updated_custom_filtering_toast\": \"Обновени местни правила за филтриране\",\n  \"updated_save_search_toast\": \"Настройките за безопасно търсене са обновени\",\n  \"updated_upstream_dns_toast\": \"Глобалните DNS сървъри са обновени\",\n  \"updates_checked\": \"Достъпна е нова версия на AdGuard Home\",\n  \"updates_version_equal\": \"AdGuard Home е актуален.\",\n  \"upstream\": \"upstream\",\n  \"upstream_dns\": \"Главен DNS сървър\",\n  \"upstream_dns_cache_configuration\": \"Конфигурация на кеша на upstream DNS\",\n  \"upstream_dns_client_desc\": \"Ако оставите това поле празно, AdGuard Home ще използва сървърите, конфигурирани в <0>настройките на DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Конфигуриран в {{path}}\",\n  \"upstream_dns_help\": \"Въведете един адрес на сървър на ред. <a>Научете повече</a> за конфигуриране на upstream DNS сървъри.\",\n  \"upstream_parallel\": \"Използване на паралелни заявки за ускоряване на разрешаването на домейни чрез едновременно запитване към всички сървъри нагоре по веригата.\",\n  \"upstream_timeout\": \"Време за изчакване на сървър нагоре по веригата\",\n  \"upstream_timeout_desc\": \"Определя броя секунди, които да изчакате за отговор от сървъра нагоре по веригата.\",\n  \"upstreams\": \"Нагоре по веригата\",\n  \"use_adguard_browsing_sec\": \"Използвайте AdGuard модул за сигурността\",\n  \"use_adguard_browsing_sec_hint\": \"Модул Сигурност в AdGuard Home проверява всяка страница която отваряте дали е в черните списъци застрашаващи вашата сигурност. Използва се програмен интерфейс който защитава вашата анонимност и изпраща само SHA256 сума базирана на част от домейна който посещавате.\",\n  \"use_adguard_parental\": \"Включи AdGuard Родителски Надзор\",\n  \"use_adguard_parental_hint\": \"Модул XXX в AdGuard Home ще провери дали страницата има материали за възвъстни. Използва се същия API за анонимност като при модула за Сигурност.\",\n  \"use_private_ptr_resolvers_desc\": \"Разрешаване на заявки PTR, SOA и NS за домейни ARPA, съдържащи частни IP адреси чрез частни сървъри, DHCP, /etc/hosts и др. Ако е деактивирано, AdGuard Home ще отговори на всички такива заявки с NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Използвайте частни обратни DNS резолвери\",\n  \"use_saved_key\": \"Използвайте предварително запазения ключ\",\n  \"username_label\": \"Потребител\",\n  \"username_placeholder\": \"Въведете потребител\",\n  \"validated_with_dnssec\": \"Проверено с DNSSEC\",\n  \"version\": \"версия\",\n  \"version_request_error\": \"Неуспешна проверка за актуализации. Моля, проверете връзката си с Интернет.\",\n  \"wednesday\": \"Сряда\",\n  \"wednesday_short\": \"Ср\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/cs.json",
    "content": "{\n  \"access_allowed_desc\": \"Seznam CIDR, IP adres nebo <a>ID klientů</a>. Pokud tento seznam obsahuje položky, AdGuard Home bude přijímat požadavky pouze od těchto klientů.\",\n  \"access_allowed_title\": \"Povolení klienti\",\n  \"access_blocked_desc\": \"Nezaměňujte to s filtry. AdGuard Home zruší dotazy DNS odpovídající těmto doménám a tyto dotazy se neobjeví ani v protokolu dotazů. Zde můžete určit přesné názvy domén, zástupné znaky a pravidla filtrování URL adres, např. \\\"example.org\\\", \\\"*.example.org\\\" nebo \\\"||example.org^\\\".\",\n  \"access_blocked_title\": \"Blokované domény\",\n  \"access_desc\": \"Zde můžete konfigurovat pravidla přístupu pro server DNS AdGuard Home\",\n  \"access_disallowed_desc\": \"Seznam CIDR, IP adres nebo <a>ID klientů</a>. Pokud tento seznam obsahuje položky, AdGuard Home bude odmítat požadavky od těchto klientů. Pokud jsou povolení klienti nakonfigurováni, je toto pole ignorováno.\",\n  \"access_disallowed_title\": \"Blokovaní klienti\",\n  \"access_settings_saved\": \"Nastavení přístupu bylo úspěšně uloženo\",\n  \"access_title\": \"Nastavení přístupu\",\n  \"actions_table_header\": \"Akce\",\n  \"add_allowlist\": \"Přidat seznam povolených\",\n  \"add_blocklist\": \"Přidat seznam blokovaných\",\n  \"add_custom_list\": \"Přidat vlastní seznam\",\n  \"add_persistent_client\": \"Přidat jako trvalého klienta\",\n  \"address\": \"Adresa\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home zruší všechny DNS dotazy tohoto klienta.\",\n  \"all_lists_up_to_date_toast\": \"Všechny seznamy jsou již aktuální\",\n  \"all_queries\": \"Všechny dotazy\",\n  \"allow_this_client\": \"Povolit tohoto klienta\",\n  \"allowed\": \"Povoleno\",\n  \"anonymize_client_ip\": \"Anonymizovat IP klienta\",\n  \"anonymize_client_ip_desc\": \"Neukládat úplnou IP adresu klienta do protokolů a statistik\",\n  \"anonymizer_notification\": \"<0>Poznámka:</0> Anonymizace IP je zapnuta. Můžete ji vypnout v <1>Obecných nastaveních</1>.\",\n  \"answer\": \"Odpověď\",\n  \"apply_btn\": \"Použít\",\n  \"auto_clients_desc\": \"Informace o IP adresách zařízení, která používají nebo mohou používat AdGuard Home. Tyto informace se získávají z několika zdrojů, včetně souborů hosts, reverzního DNS atd.\",\n  \"auto_clients_title\": \"Spuštění klienti\",\n  \"autofix_warning_list\": \"Jsou prováděny následující úlohy: <0>Deaktivace systému DNSStubListener</0> <0>Nastavení adresy serveru DNS na 127.0.0.1</0> <0>Nahrazení cíle symbolického odkazu z /etc/resolv.conf do /run/systemd/resolve/resolv.conf</0> <0>Zastavení služby DNSStubListener (znovu načtení služby systemd-resolved)</0>\",\n  \"autofix_warning_result\": \"Výsledkem je, že všechny požadavky DNS z vašeho systému jsou ve výchozím nastavení zpracovány službou AdGuard Home.\",\n  \"autofix_warning_text\": \"Pokud kliknete na „Opravit“, AdGuard Home nakonfiguruje váš systém tak, aby používal DNS server AdGuard Home.\",\n  \"average_processing_time\": \"Průměrný čas zpracování\",\n  \"average_processing_time_hint\": \"Průměrný čas zpracování požadavků DNS v milisekundách\",\n  \"average_upstream_response_time\": \"Průměrná doba odezvy odchozích připojení\",\n  \"back\": \"Zpět\",\n  \"block\": \"Blokovat\",\n  \"block_all\": \"Blokovat vše\",\n  \"block_domain_use_filters_and_hosts\": \"Blokovat domény pomocí filtrů a seznamů adres\",\n  \"block_for_this_client_only\": \"Blokovat pouze pro tohoto klienta\",\n  \"block_services\": \"Blokovat specifické služby\",\n  \"blocked_adult_websites\": \"Blokováno modulem Rodičovská kontrola\",\n  \"blocked_by\": \"<0>Blokováno filtry</0>\",\n  \"blocked_by_cname_or_ip\": \"Zakázáno dle CNAME nebo IP\",\n  \"blocked_by_response\": \"Zakázáno dle CNAME nebo IP v odpovědi\",\n  \"blocked_response_ttl\": \"TTL blokované odezvy\",\n  \"blocked_response_ttl_desc\": \"Určuje, na kolik sekund by měli klienti ukládat filtrovanou odezvu do mezipaměti\",\n  \"blocked_safebrowsing\": \"Blokováno modulem Bezpečné prohlížení\",\n  \"blocked_service\": \"Blokovaná služba\",\n  \"blocked_services\": \"Blokované služby\",\n  \"blocked_services_desc\": \"Umožňuje rychle blokovat oblíbené weby a služby.\",\n  \"blocked_services_global\": \"Použít globální blokované služby\",\n  \"blocked_services_saved\": \"Blokované služby byly úspěšně uloženy\",\n  \"blocked_threats\": \"Blokované hrozby\",\n  \"blocking_ipv4\": \"Blokování IPv4\",\n  \"blocking_ipv4_desc\": \"IP adresa, která se má vrátit v případě blokovaného požadavku typu A\",\n  \"blocking_ipv6\": \"Blokování IPv6\",\n  \"blocking_ipv6_desc\": \"IP adresa, která se má vrátit v případě blokovaného požadavku typu AAAA\",\n  \"blocking_mode\": \"Režim blokování\",\n  \"blocking_mode_custom_ip\": \"Vlastní IP. odezva s ručně nastavenou IP adresou\",\n  \"blocking_mode_default\": \"Výchozí: Odezva s nulovou IP adresou (0.0.0.0 pro A; :: pro AAAA), pokud je blokováno pravidlem ve stylu Adblock; odezva pomocí IP adresy uvedené v pravidle, pokud je blokováno pravidlem /etc/hosts-style\",\n  \"blocking_mode_null_ip\": \"Nulová IP: Odezva s nulovou IP adresou (0.0.0.0 pro A; :: pro AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Odezva s kódem NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: Odezva pomocí kódu REFUSED\",\n  \"blocklist\": \"Zakázaný\",\n  \"bootstrap_dns\": \"Bootstrap DNS servery\",\n  \"bootstrap_dns_desc\": \"IP adresy DNS serverů používaných k překladu IP adres řešitelů DoH/DoT, které zadáte jako odchozí servery. Komentáře nejsou povoleny.\",\n  \"cache_cleared\": \"Mezipaměť DNS úspěšně vymazána\",\n  \"cache_enabled\": \"Povolit mezipaměť\",\n  \"cache_enabled_desc\": \"Ukládejte odezvy DNS lokálně.\",\n  \"cache_optimistic\": \"Optimistické ukládání do mezipaměti\",\n  \"cache_optimistic_desc\": \"Nechte AdGuard Home odpovědět z mezipaměti, i když už platnost položek skončila. Také se je pokuste obnovit.\",\n  \"cache_size\": \"Velikost mezipaměti\",\n  \"cache_size_desc\": \"Velikost mezipaměti DNS (v bajtech).\",\n  \"cache_size_validation\": \"Velikost mezipaměti musí být větší než nula, pokud je tato funkce povolena.\",\n  \"cache_ttl_max_override\": \"Přepsat maximální hodnotu TTL\",\n  \"cache_ttl_max_override_desc\": \"Nastavte maximální hodnotu TTL (v sekundách) pro položky v mezipaměti DNS.\",\n  \"cache_ttl_min_override\": \"Přepsat minimální hodnotu TTL\",\n  \"cache_ttl_min_override_desc\": \"Prodlužte nejkratší hodnotu TTL (v sekundách) obdrženou z odchozího serveru při ukládání DNS odpovědí do mezipaměti.\",\n  \"cancel_btn\": \"Zrušit\",\n  \"category_label\": \"Kategorie\",\n  \"check\": \"Zkontrolovat\",\n  \"check_client_id\": \"Identifikátor klienta (ClientID nebo IP adresa)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Zkontrolujte, zda je název hostitele filtrován.\",\n  \"check_dhcp_servers\": \"Zkontrolovat DHCP servery\",\n  \"check_dns_record\": \"Vyberte typ DNS záznamu\",\n  \"check_enter_client_id\": \"Zadejte identifikátor klienta\",\n  \"check_hostname\": \"Název hostitele nebo domény\",\n  \"check_ip\": \"IP adresy: {{ip}}\",\n  \"check_not_found\": \"Nenalezeno ve Vašich seznamech filtrů\",\n  \"check_reason\": \"Důvod: {{reason}}\",\n  \"check_service\": \"Název služby: {{service}}\",\n  \"check_title\": \"Zkontrolovat filtrování\",\n  \"check_updates_btn\": \"Zkontrolovat aktualizace\",\n  \"check_updates_now\": \"Zkontrolovat aktualizace nyní\",\n  \"choose_allowlist\": \"Vyberte seznamy povolených\",\n  \"choose_blocklist\": \"Vyberte seznamy zakázaných\",\n  \"choose_from_list\": \"Vybrat ze seznamu\",\n  \"city\": \"Město\",\n  \"clear_cache\": \"Vymazat mezipaměť\",\n  \"click_to_view_queries\": \"Klikněte pro zobrazení dotazů\",\n  \"client_add\": \"Přidat klienta\",\n  \"client_added\": \"Klient \\\"{{key}}\\\" byl úspěšně přidán\",\n  \"client_blocked\": \"Klient „{{ip}}“ byl úspěšně zablokován\",\n  \"client_confirm_block\": \"Opravdu chcete zablokovat klienta „{{ip}}“?\",\n  \"client_confirm_delete\": \"Opravdu chcete odstranit klienta \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Opravdu chcete odblokovat klienta „{{ip}}“?\",\n  \"client_deleted\": \"Klient \\\"{{key}}\\\" byl úspěšně odstraněn\",\n  \"client_details\": \"Detaily klienta\",\n  \"client_edit\": \"Upravit klienta\",\n  \"client_global_settings\": \"Použít globální nastavení\",\n  \"client_id\": \"ID klienta\",\n  \"client_id_desc\": \"Klienty lze identifikovat pomocí ID klienta. <a>Zde</a> se můžete dozvědět více o tom, jak klienty identifikovat.\",\n  \"client_id_placeholder\": \"Zadejte ID klienta\",\n  \"client_identifier\": \"Identifikátor\",\n  \"client_identifier_desc\": \"Klienti můžou být identifikováni podle jejich IP adresy, CIDR, MAC adresy nebo ID klienta (může být použito pro DoT/DoH/DoQ). <0>Zde</0> se můžete dozvědět více o tom, jak klienty identifikovat.\",\n  \"client_name\": \"Klient {{id}}\",\n  \"client_new\": \"Nový klient\",\n  \"client_settings\": \"Nastavení klienta\",\n  \"client_table_header\": \"Klient\",\n  \"client_unblocked\": \"Klient „{{ip}}“ byl úspěšně odblokován\",\n  \"client_updated\": \"Klient \\\"{{key}}\\\" byl úspěšně aktualizován\",\n  \"clients_desc\": \"Konfigurace stálých klientských záznamů pro zařízení připojená k AdGuard Home\",\n  \"clients_not_found\": \"Nenalezeni žádní klienti\",\n  \"clients_title\": \"Stálí klienti\",\n  \"compact\": \"Kompaktní\",\n  \"config_successfully_saved\": \"Konfigurace byla úspěšně uložena\",\n  \"configure\": \"Konfigurovat\",\n  \"confirm_dns_cache_clear\": \"Opravdu chcete vymazat mezipaměť DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home nakonfiguruje {{ip}} jako statickou IP adresu. Chcete pokračovat?\",\n  \"copyright\": \"Autorská práva\",\n  \"country\": \"Země\",\n  \"custom_filter_rules\": \"Vlastní pravidla filtrování\",\n  \"custom_filter_rules_hint\": \"Na každý řádek vložte jedno pravidlo. Můžete použít buď pravidla blokování reklam nebo syntaxe hostitelských souborů.\",\n  \"custom_filtering_rules\": \"Vlastní pravidla filtrování\",\n  \"custom_ip\": \"Vlastní IP\",\n  \"custom_retention_input\": \"Zadejte retenci v hodinách\",\n  \"custom_rotation_input\": \"Zadejte rotaci v hodinách\",\n  \"dashboard\": \"Hlavní panel\",\n  \"date\": \"Datum\",\n  \"default\": \"Výchozí\",\n  \"delete_confirm\": \"Opravdu chcete odstranit \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Vymazat\",\n  \"descr\": \"Popis\",\n  \"details\": \"Detaily\",\n  \"dhcp_add_static_lease\": \"Přidat statický pronájem\",\n  \"dhcp_config_saved\": \"Konfigurace DHCP byla úspěšně uložena\",\n  \"dhcp_description\": \"Pokud váš router neposkytuje možnost nastavit DHCP, můžete použít vlastní vestavěný DHCP server AdGuardu.\",\n  \"dhcp_disable\": \"Vypnout DHCP server\",\n  \"dhcp_dynamic_ip_found\": \"Váš systém používá konfiguraci dynamické IP adresy pro rozhraní <0>{{interfaceName}}</0>. Pro použití serveru DHCP musí být nastavena statická IP adresa. Vaše aktuální IP adresa je <0>{{ipAddress}}</0>. AdGuard Home automaticky nastaví tuto IP adresu jako statickou, pokud stisknete tlačítko \\\"Zapnout DHCP server\\\".\",\n  \"dhcp_edit_static_lease\": \"Upravit statický pronájem\",\n  \"dhcp_enable\": \"Zapnout DHCP server\",\n  \"dhcp_error\": \"AdGuard Home nemohl určit, zda je v síti jiný aktivní server DHCP\",\n  \"dhcp_form_gateway_input\": \"IP brána\",\n  \"dhcp_form_lease_input\": \"Doba trvání pronájmu\",\n  \"dhcp_form_lease_title\": \"Doba pronájmu DHCP (v sekundách)\",\n  \"dhcp_form_range_end\": \"Konec rozsahu\",\n  \"dhcp_form_range_start\": \"Začátek rozsahu\",\n  \"dhcp_form_range_title\": \"Rozsah IP adres\",\n  \"dhcp_form_subnet_input\": \"Maska podsítě\",\n  \"dhcp_found\": \"V síti byly nalezeny aktivní DHCP servery. Není bezpečné zapnout vestavěný DHCP server.\",\n  \"dhcp_hardware_address\": \"Hardwarová adresa\",\n  \"dhcp_interface_select\": \"Vybrat rozhraní DHCP\",\n  \"dhcp_ip_addresses\": \"IP adresa\",\n  \"dhcp_ipv4_settings\": \"Nastavení DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Nastavení DHCP IPv6\",\n  \"dhcp_lease_added\": \"Statický pronájem \\\"{{key}}\\\" byl úspěšně přidán\",\n  \"dhcp_lease_deleted\": \"Statický pronájem \\\"{{key}}\\\" byl úspěšně odstraněn\",\n  \"dhcp_lease_updated\": \"Statický pronájem \\\"{{key}}\\\" byl úspěšně aktualizován\",\n  \"dhcp_leases\": \"Pronájem DHCP\",\n  \"dhcp_leases_not_found\": \"Nebyly nalezeny žádné pronájmy DHCP\",\n  \"dhcp_new_static_lease\": \"Nový statický pronájem\",\n  \"dhcp_not_found\": \"Je bezpečné zapnout vestavěný server DHCP - v síti jsme nenalezli žádné aktivní servery DHCP. Doporučujeme však, abyste to znovu zkontrolovali ručně, protože v současné době náš automatický test neposkytuje 100% záruku.\",\n  \"dhcp_reset\": \"Opravdu chcete resetovat konfiguraci DHCP?\",\n  \"dhcp_reset_leases\": \"Resetovat všechny pronájmy\",\n  \"dhcp_reset_leases_confirm\": \"Opravdu chcete resetovat všechny pronájmy?\",\n  \"dhcp_reset_leases_success\": \"Pronájmy DHCP byli úspěšně resetovány\",\n  \"dhcp_settings\": \"Nastavení DHCP\",\n  \"dhcp_static_ip_error\": \"Pro použití serveru DHCP musí být nastavena statická IP adresa. AdGuard Home nemohl zjistit, zda je toto síťové rozhraní nakonfigurováno pomocí statické adresy IP. Nastavte prosím statickou IP adresu ručně.\",\n  \"dhcp_static_leases\": \"Statické pronájmy DHCP\",\n  \"dhcp_static_leases_not_found\": \"Nebyly nalezeny žádné statické pronájmy DHCP\",\n  \"dhcp_table_expires\": \"Vyprší\",\n  \"dhcp_table_hostname\": \"Název hostitele\",\n  \"dhcp_title\": \"DHCP server (experimentální!)\",\n  \"dhcp_warning\": \"Pokud přesto chcete server DHCP povolit, ujistěte se, že ve Vaší síti není žádný jiný aktivní server DHCP, protože by to mohlo narušit připojení k Internetu pro zařízení v síti!\",\n  \"disable_for_hours\": \"Na {{count}} hod.\",\n  \"disable_for_hours_plural\": \"Na {{count}} hod.\",\n  \"disable_for_minutes\": \"Na {{count}} min.\",\n  \"disable_for_minutes_plural\": \"Na {{count}} min.\",\n  \"disable_for_seconds\": \"Na {{count}} sek.\",\n  \"disable_for_seconds_plural\": \"Na {{count}} sek.\",\n  \"disable_ipv6\": \"Zakázat řešení IPv6 adres\",\n  \"disable_ipv6_desc\": \"Odstranění všech dotazů DNS na adresy IPv6 (typ AAAA) a odstranění náznaků IPv6 z odpovědí HTTPS.\",\n  \"disable_notify_for_hours\": \"Vypnout ochranu na {{count}} hod.\",\n  \"disable_notify_for_hours_plural\": \"Vypnout ochranu na {{count}} hod.\",\n  \"disable_notify_for_minutes\": \"Vypnout ochranu na {{count}} min.\",\n  \"disable_notify_for_minutes_plural\": \"Vypnout ochranu na {{count}} min.\",\n  \"disable_notify_for_seconds\": \"Vypnout ochranu na {{count}} sek.\",\n  \"disable_notify_for_seconds_plural\": \"Vypnout ochranu na {{count}} sek.\",\n  \"disable_notify_until_tomorrow\": \"Vypnout ochranu do zítřka\",\n  \"disable_protection\": \"Vypnout ochranu\",\n  \"disable_rewrites\": \"Deaktivovat pravidla přepisů\",\n  \"disable_until_tomorrow\": \"Do zítřka\",\n  \"disabled\": \"Vypnuto\",\n  \"disabled_dhcp\": \"DHCP server vypnutý\",\n  \"disabled_filtering_toast\": \"Vypnuté filtrování\",\n  \"disabled_parental_toast\": \"Vypnutá Rodičovská kontrola\",\n  \"disabled_protection\": \"Ochrana vypnuta\",\n  \"disabled_safe_browsing_toast\": \"Vypnuté bezpečné prohlížení\",\n  \"disabled_safe_search_toast\": \"Vypnuté bezpečné vyhledávání\",\n  \"disallow_this_client\": \"Blokovat tohoto klienta\",\n  \"dns_addresses\": \"Adresy DNS\",\n  \"dns_allowlists\": \"DNS seznam povolených\",\n  \"dns_allowlists_desc\": \"Domény z DNS seznamu povolených budou povoleny, i když se nacházejí v některém ze seznamů blokovaných.\",\n  \"dns_blocklists\": \"DNS seznam blokovaných\",\n  \"dns_blocklists_desc\": \"AdGuard Home bude blokovat domény na seznamu blokovaných.\",\n  \"dns_cache_config\": \"Konfigurace mezipaměti DNS\",\n  \"dns_cache_config_desc\": \"Zde můžete konfigurovat mezipaměť DNS\",\n  \"dns_cache_size\": \"Velikost mezipaměti DNS v bajtech\",\n  \"dns_config\": \"Konfigurace DNS serveru\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"Soukromí DNS\",\n  \"dns_providers\": \"Zde je <0>seznam známých poskytovatelů DNS</0>, z nichž si můžete vybrat.\",\n  \"dns_query\": \"Dotazy DNS\",\n  \"dns_rewrites\": \"Přesměrování DNS\",\n  \"dns_settings\": \"Nastavení DNS\",\n  \"dns_start\": \"Spouští se DNS server\",\n  \"dns_status_error\": \"Chyba při kontrole stavu DNS serveru\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": nemohl být použit, zkontrolujte, zda jste ho správně napsali\",\n  \"dns_test_ok_toast\": \"Specifikované DNS servery pracují správně\",\n  \"dns_test_parsing_error_toast\": \"Sekce {{section}}: řádek {{line}}: nelze použít, zkontrolujte prosím, zda jste ho správně napsali\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" neodpovídá na testovací požadavky a nemusí fungovat správně\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Zapnout DNSSEC\",\n  \"dnssec_enable_desc\": \"Nastavte příznak DNSSEC v následujících DNS dotazech a zkontrolujte výsledek (je potřebný překladač se zapnutým DNSSEC).\",\n  \"domain\": \"Doména\",\n  \"domain_desc\": \"Zadejte zástupný znak nebo název domény, kterou chcete přepsat.\",\n  \"domain_name_table_header\": \"Název domény\",\n  \"domain_or_client\": \"Doména nebo klient\",\n  \"down\": \"Dolů\",\n  \"download_mobileconfig\": \"Stáhnout konfigurační soubor\",\n  \"download_mobileconfig_doh\": \"Stáhnout .mobileconfig pro DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Stáhnout .mobileconfig pro DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Upravit seznam povolených\",\n  \"edit_blocklist\": \"Upravit seznam blokovaných\",\n  \"edit_table_action\": \"Upravit\",\n  \"edns_cs_desc\": \"Přidá možnost podsítě klienta EDNS (ECS) do odchozích požadavků a zaznamá hodnoty odeslané klienty do protokolu dotazů.\",\n  \"edns_enable\": \"Povolit klientskou podsíť EDNS\",\n  \"edns_use_custom_ip\": \"Použít vlastní IP pro EDNS\",\n  \"edns_use_custom_ip_desc\": \"Povolit použití vlastní IP pro EDNS\",\n  \"elapsed\": \"Uplynulý čas\",\n  \"empty_response_status\": \"Prázdná\",\n  \"enable_protection\": \"Zapnout ochranu\",\n  \"enable_protection_timer\": \"Ochrana bude zapnuta za {{time}}\",\n  \"enable_rewrites\": \"Povolit pravidla přepisů\",\n  \"enable_upstream_dns_cache\": \"Povolit ukládání do mezipaměti DNS pro vlastní konfiguraci odchozího připojení tohoto klienta\",\n  \"enabled_dhcp\": \"DHCP server zapnutý\",\n  \"enabled_filtering_toast\": \"Zapnuté filtrování\",\n  \"enabled_parental_toast\": \"Zapnutá Rodičovská kontrola\",\n  \"enabled_protection\": \"Ochrana zapnuta\",\n  \"enabled_safe_browsing_toast\": \"Zapnuté bezpečné prohlížení\",\n  \"enabled_save_search_toast\": \"Zapnuté bezpečné vyhledávání\",\n  \"enabled_table_header\": \"Zapnuto\",\n  \"encryption_certificate_path\": \"Cesta k certifikátu\",\n  \"encryption_certificates\": \"Certifikáty\",\n  \"encryption_certificates_desc\": \"Chcete-li používat šifrování, musíte pro svou doménu poskytnout platný řetězec certifikátů SSL. Certifikát můžete získat bezplatně na adrese <0>{{link}}</ 0>, nebo jej můžete zakoupit od jednoho z důvěryhodných certifikačních úřadů.\",\n  \"encryption_certificates_input\": \"Zde můžete nakopírovat/vložit certifikáty PEM.\",\n  \"encryption_certificates_source_content\": \"Vložte obsahy certifikátů\",\n  \"encryption_certificates_source_path\": \"Nastavte cestu k souboru certifikátů\",\n  \"encryption_chain_invalid\": \"Certifikační řetězec je neplatný\",\n  \"encryption_chain_valid\": \"Certifikační řetězec je platný\",\n  \"encryption_config_saved\": \"Konfigurace šifrování byla uložena\",\n  \"encryption_desc\": \"Podpora šifrování (HTTPS/TLS) pro webové rozhraní DNS i administrátora\",\n  \"encryption_doq\": \"DNS-over-QUIC port\",\n  \"encryption_doq_desc\": \"Pokud je tento port nakonfigurován, AdGuard Home bude na tomto portu spouštět DNS-over-QUIC server.\",\n  \"encryption_dot\": \"DNS-over-TLS port\",\n  \"encryption_dot_desc\": \"Pokud je tento port nakonfigurován, AdGuard Home bude na tomto portu spouštět DNS-over-TLS server.\",\n  \"encryption_enable\": \"Povolit šifrování (HTTPS, DNS-over-HTTPS a DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Pokud je šifrování zapnuto, administrátorské rozhraní AdGuard Home bude pracovat skrze HTTPS a DNS server bude naslouchat požadavky přes DNS-over-HTTPS a DNS-over-TLS.\",\n  \"encryption_expire\": \"Vyprší\",\n  \"encryption_hostnames\": \"Názvy hostitelů\",\n  \"encryption_https\": \"HTTPS port\",\n  \"encryption_https_desc\": \"Pokud je nakonfigurován port HTTPS, AdGuard Home administrátorské rozhraní bude přístupné přes HTTPS a bude také poskytovat DNS-over-HTTPS na '/dns-query'.\",\n  \"encryption_issuer\": \"Vydavatel\",\n  \"encryption_key\": \"Osobní kód\",\n  \"encryption_key_input\": \"Zde můžete nakopírovat/vložit soukromý klíč k certifikátu PEM.\",\n  \"encryption_key_invalid\": \"Toto je neplatný {{type}} osobní klíč\",\n  \"encryption_key_source_content\": \"Vložte obsahy soukromého klíče\",\n  \"encryption_key_source_path\": \"Nastavte cestu k souboru soukromého klíče\",\n  \"encryption_key_valid\": \"Toto je platný {{type}} osobní klíč\",\n  \"encryption_plain_dns_desc\": \"Ve výchozím nastavení je povolen běžný DNS. Můžete ho zakázat, aby všechna zařízení používala šifrovaný DNS. Chcete-li to provést, musíte povolit alespoň jeden šifrovaný protokol DNS\",\n  \"encryption_plain_dns_enable\": \"Povolit běžný DNS\",\n  \"encryption_plain_dns_error\": \"Chcete-li zakázat běžný DNS, povolte alespoň jeden šifrovaný protokol DNS\",\n  \"encryption_private_key_path\": \"Cesta k soukromému klíčí\",\n  \"encryption_redirect\": \"Automaticky přesměrovat na HTTPS\",\n  \"encryption_redirect_desc\": \"Pokud je zaškrtnuto, AdGuard Home vás automaticky přesměruje z adres HTTP na HTTPS.\",\n  \"encryption_reset\": \"Opravdu chcete obnovit nastavení šifrování?\",\n  \"encryption_server\": \"Název serveru\",\n  \"encryption_server_desc\": \"Pokud je nastaveno, AdGuard Home detekuje ClientID, odpovídá na dotazy DDR a provádí další ověření připojení. Pokud není nastaveno, jsou tyto funkce vypnuty. Musí odpovídat jednomu z názvů DNS v certifikátu.\",\n  \"encryption_server_enter\": \"Zadejte název domény\",\n  \"encryption_settings\": \"Nastavení šifrování\",\n  \"encryption_status\": \"Stav\",\n  \"encryption_subject\": \"Subjekt\",\n  \"encryption_title\": \"Šifrování\",\n  \"encryption_warning\": \"Varování\",\n  \"enforce_safe_search\": \"Použít bezpečné vyhledávání\",\n  \"enforce_save_search_hint\": \"AdGuard Home vynutí bezpečné vyhledávání v následujících vyhledávačích: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Vynucené bezpečné vyhledávání\",\n  \"enter_cache_size\": \"Zadejte velikost mezipaměti (v bajtech)\",\n  \"enter_cache_ttl_max_override\": \"Zadejte maximální hodnotu TTL (v sekundách)\",\n  \"enter_cache_ttl_min_override\": \"Zadejte minimální hodnotu TTL (v sekundách)\",\n  \"enter_name_hint\": \"Zadejte název\",\n  \"enter_url_or_path_hint\": \"Zadejte URL nebo úplnou cestu k seznamu\",\n  \"enter_valid_allowlist\": \"Zadejte platnou adresu URL na seznam povolených.\",\n  \"enter_valid_blocklist\": \"Zadejte platnou adresu URL na seznam blokovaných.\",\n  \"error_details\": \"Podrobnosti chyby\",\n  \"example_comment\": \"! Sem se přidává komentář.\",\n  \"example_comment_hash\": \"# Také komentář.\",\n  \"example_comment_meaning\": \"jen komentář;\",\n  \"example_meaning_filter_block\": \"zablokovat přístup k doméně example.org a všem jejím subdoménám\",\n  \"example_meaning_filter_whitelist\": \"odblokovat přístup k doméně example.org a všem jejím subdoménám\",\n  \"example_meaning_host_block\": \"odezva s adresou 127.0.0.1 pro doménu example.org (ale ne pro její subdomény);\",\n  \"example_multiple_upstreams_reserved\": \"více odchozích připojení <0>pro konkrétní domény</0>;\",\n  \"example_regex_meaning\": \"blokuje přístup doménám, které vyhovují regulárnímu výrazu.\",\n  \"example_rewrite_domain\": \"přepsat odpovědi pouze pro tento název domény.\",\n  \"example_rewrite_wildcard\": \"přepsat odpovědi pro všechny subdomény <0>example.org</0>.\",\n  \"example_upstream_comment\": \"komentář.\",\n  \"example_upstream_doh\": \"šifrovaný <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"šifrovaný DNS-over-HTTPS s vynuceným <0>HTTP/3</0> a bez možnosti zpětného přechodu na HTTP/2 nebo nižší;\",\n  \"example_upstream_doq\": \"šifrovaný <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"šifrovaný <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"obvyklý DNS (přes UDP);\",\n  \"example_upstream_regular_port\": \"obvyklý DNS (skrze UDP, s portem);\",\n  \"example_upstream_reserved\": \"odchozí DNS připojení <0>pro konkrétní doménu(y)</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS razítka</0> pro <1>DNSCrypt</1> nebo <2>DNS-over-HTTPS</2> řešitele;\",\n  \"example_upstream_tcp\": \"obvyklý DNS (přes TCP);\",\n  \"example_upstream_tcp_hostname\": \"obvyklý DNS (skrze TCP, název hostitele);\",\n  \"example_upstream_tcp_port\": \"obvyklý DNS (skrze TCP, s portem);\",\n  \"example_upstream_udp\": \"obvyklý DNS (skrze UDP, název hostitele);\",\n  \"examples_title\": \"Příklady\",\n  \"fallback_dns_desc\": \"Seznam záložních DNS serverů používaných v případě, že odchozí DNS servery neodpovídají. Syntaxe je stejná jako v hlavním poli pro odchozí servery výše.\",\n  \"fallback_dns_placeholder\": \"Zadejte jeden záložní DNS server na řádek\",\n  \"fallback_dns_title\": \"Záložní DNS servery\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Nejrychlejší IP adresa\",\n  \"fastest_addr_desc\": \"Počká na odpovědi <b>všech</b> serverů DNS, změří rychlost připojení TCP pro každý server a vrátí IP adresu serveru s nejvyšší rychlostí připojení.<br/>Tento režim může výrazně zpomalit dotazy DNS, pokud jeden nebo více odchozích serverů neodpovídá. Ujistěte se, že vaše odchozí servery jsou stabilní a že časový limit odchozích serverů je nízký.\",\n  \"filter\": \"Filtr\",\n  \"filter_added_successfully\": \"Seznam byl úspěšně přidán\",\n  \"filter_allowlist\": \"VAROVÁNÍ: Tato akce také vyloučí pravidlo \\\"{{disallowed_rule}}\\\" ze seznamu povolených klientů.\",\n  \"filter_category_general\": \"Obecné\",\n  \"filter_category_general_desc\": \"Seznamy, které blokují slídiče a reklamu na většině zařízení\",\n  \"filter_category_other\": \"Ostatní\",\n  \"filter_category_other_desc\": \"Další seznamy zakázaných\",\n  \"filter_category_regional\": \"Regionální\",\n  \"filter_category_regional_desc\": \"Seznamy, které jsou zaměřené na regionální reklamy a sledovací servery\",\n  \"filter_category_security\": \"Bezpečnost\",\n  \"filter_category_security_desc\": \"Seznamy určené na blokování nebezpečných, zákeřných nebo podvodných domén\",\n  \"filter_removed_successfully\": \"Seznam byl úspěšně odstraněn\",\n  \"filter_updated\": \"Seznam byl úspěšně aktualizován\",\n  \"filtered\": \"Filtrováno\",\n  \"filtered_custom_rules\": \"Filtrováno pomocí vlastních pravidel filtrování\",\n  \"filtering_rules_learn_more\": \"<0>Další informace</0> o vytváření vlastních seznamů hostitelů.\",\n  \"filters\": \"Filtry\",\n  \"filters_and_hosts_hint\": \"AdGuard Home zná základní pravidla blokování reklam a syntaxe hostsitelských souborů.\",\n  \"filters_block_toggle_hint\": \"Pravidla blokování můžete nastavit v nastavení <a>Filtry</a>.\",\n  \"filters_configuration\": \"Konfigurace filtrů\",\n  \"filters_enable\": \"Povolit filtry\",\n  \"filters_interval\": \"Interval aktualizace filtrů\",\n  \"fix\": \"Opravit\",\n  \"for_last_days\": \"za posledních {{count}} dní\",\n  \"for_last_days_plural\": \"za posledních {{count}} dní\",\n  \"for_last_hours\": \"za poslední {{count}} hodinu\",\n  \"for_last_hours_plural\": \"za posledních {{count}} hodin\",\n  \"forgot_password\": \"Zapomněli jste heslo?\",\n  \"forgot_password_desc\": \"Prosím, následujte <0>tyto kroky</0> k vytvoření nového hesla pro váš uživatelský účet.\",\n  \"form_add_id\": \"Přidat identifikátor\",\n  \"form_answer\": \"Zadejte IP adresu nebo název domény\",\n  \"form_client_name\": \"Zadejte název klienta\",\n  \"form_domain\": \"Zadejte doménu\",\n  \"form_enter_blocked_response_ttl\": \"Zadejte TTL blokované odezvy (v sekundách)\",\n  \"form_enter_host\": \"Zadejte název hostitele\",\n  \"form_enter_hostname\": \"Zadejte název hostitele\",\n  \"form_enter_id\": \"Zadejte identifikátor\",\n  \"form_enter_ip\": \"Zadejte IP\",\n  \"form_enter_mac\": \"Zadejte MAC\",\n  \"form_enter_rate_limit\": \"Zadejte rychlostní limit\",\n  \"form_enter_rate_limit_subnet_len\": \"Zadejte délku předpony podsítě pro omezení rychlosti\",\n  \"form_enter_subnet_ip\": \"Zadejte adresu IP adresu do podsítě \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Zadejte dobu časového limitu odchozího serveru v sekundách\",\n  \"form_error_answer_format\": \"Neplatný formát odpovědi\",\n  \"form_error_client_id_format\": \"ID klienta musí obsahovat pouze čísla, malá písmena a spojovníky\",\n  \"form_error_domain_format\": \"Neplatný formát domény\",\n  \"form_error_equal\": \"Nesmí se shodovat\",\n  \"form_error_gateway_ip\": \"Pronájem nemůže mít IP adresu brány\",\n  \"form_error_ip4_format\": \"Neplatná adresa IPv4\",\n  \"form_error_ip4_gateway_format\": \"Neplatná adresa IPv4 brány\",\n  \"form_error_ip6_format\": \"Neplatná adresa IPv6\",\n  \"form_error_ip_format\": \"Neplatná IP adresa\",\n  \"form_error_mac_format\": \"Neplatná adresa MAC\",\n  \"form_error_password\": \"Heslo se neshoduje\",\n  \"form_error_password_length\": \"Heslo musí obsahovat od {{min}} do {{max}} znaků\",\n  \"form_error_port\": \"Zadejte platné číslo portu\",\n  \"form_error_port_range\": \"Zadejte číslo portu v rozmezí 80-65535\",\n  \"form_error_port_unsafe\": \"Nezabezpečený port\",\n  \"form_error_positive\": \"Musí být větší než 0\",\n  \"form_error_required\": \"Povinné pole\",\n  \"form_error_server_name\": \"Neplatný název serveru\",\n  \"form_error_subnet\": \"Podsíť \\\"{{cidr}}\\\" neobsahuje IP adresu \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Neplatný formát URL\",\n  \"form_error_url_or_path_format\": \"Neplatná URL nebo úplná cesta k seznamu\",\n  \"form_select_tags\": \"Vyberte značky klienta\",\n  \"found_in_known_domain_db\": \"Nalezeno v databázi známých domén\",\n  \"friday\": \"Pátek\",\n  \"friday_short\": \"Pátek\",\n  \"gateway_or_subnet_invalid\": \"Neplatná maska podsítě\",\n  \"general_settings\": \"Obecná nastavení\",\n  \"general_statistics\": \"Obecné statistiky\",\n  \"get_started\": \"Začínáme\",\n  \"greater_range_start_error\": \"Musí být větší než začátek rozsahu\",\n  \"homepage\": \"Domovská stránka\",\n  \"host_whitelisted\": \"Hostitel je na seznamu povolených\",\n  \"ignore_domains\": \"Ignorované domény (oddělené novým řádkem)\",\n  \"ignore_domains_desc_query\": \"Dotazy odpovídající těmto pravidlům se do záznamu dotazů nezapisují\",\n  \"ignore_domains_desc_stats\": \"Dotazy odpovídající těmto pravidlům se do statistik nezapisují\",\n  \"ignore_domains_title\": \"Ignorované domény\",\n  \"ignore_query_log\": \"Ignorovat tohoto klienta v protokolu dotazů\",\n  \"ignore_statistics\": \"Ignorovat tohoto klienta ve statistikách\",\n  \"install_auth_confirm\": \"Potvrďte heslo\",\n  \"install_auth_desc\": \"V administrátorském webovém rozhraní AdGuard Home musí být nastaveno ověřovací heslo. I když je AdGuard Home přístupný pouze v místní síti, je důležité jej chránit před neomezeným přístupem.\",\n  \"install_auth_password\": \"Heslo\",\n  \"install_auth_password_enter\": \"Zadejte heslo\",\n  \"install_auth_title\": \"Ověřování\",\n  \"install_auth_username\": \"Uživatelské jméno\",\n  \"install_auth_username_enter\": \"Zadejte uživatelské jméno\",\n  \"install_devices_address\": \"DNS server AdGuard Home používá následujíce adresy\",\n  \"install_devices_android_list_1\": \"Na domovské obrazovce nabídky Android klepněte na Nastavení.\",\n  \"install_devices_android_list_2\": \"V nabídce klepněte na Wi-Fi. Zobrazí se obrazovka se seznamem všech dostupných sítí (není možné nastavit vlastní DNS pro mobilní připojení).\",\n  \"install_devices_android_list_3\": \"Dlouze stiskněte síť, ke které jste připojeni, a klepněte na položku Změnit síť.\",\n  \"install_devices_android_list_4\": \"V některých zařízeních bude pravděpodobně nutné zaškrtnout políčko Rozšířené a zobrazit další nastavení. Chcete-li upravit nastavení DNS systému Android, budete muset přepnout nastavení IP adresy z DHCP na Statickou.\",\n  \"install_devices_android_list_5\": \"Změňte hodnoty DNS 1 a DNS 2 na adresy serveru AdGuard Home.\",\n  \"install_devices_desc\": \"Chcete-li začít používat aplikaci AdGuard Home, musíte nakonfigurovat zařízení tak, aby ji mohla používat.\",\n  \"install_devices_ios_list_1\": \"Na domovské obrazovce klepněte na Nastavení.\",\n  \"install_devices_ios_list_2\": \"V levé nabídce vyberte Wi-Fi (není možné nastavit vlastní DNS pro mobilní připojení).\",\n  \"install_devices_ios_list_3\": \"Klepněte na název aktuální aktivní sítě.\",\n  \"install_devices_ios_list_4\": \"Do políčka DNS zadejte adresy serveru AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Klikněte na ikonu Apple a přejděte na položku Systémové předvolby.\",\n  \"install_devices_macos_list_2\": \"Klikněte na Síť.\",\n  \"install_devices_macos_list_3\": \"Vyberte první připojení v seznamu a klepněte na tlačítko Pokročilé.\",\n  \"install_devices_macos_list_4\": \"Vyberte kartu DNS a zadejte adresy serveru AdGuard Home.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Toto nastavení automaticky pokryje všechna zařízení připojená k Vašemu domácímu routeru a nebudete je muset konfigurovat ručně.\",\n  \"install_devices_router_list_1\": \"Otevřete předvolby pro router. Obvykle k němu můžete přistupovat z prohlížeče prostřednictvím adresy URL, např. http://192.168.0.1/ nebo http://192.168.1.1/. Můžete být vyzváni k zadání hesla. Pokud si ho nepamatujete, můžete heslo resetovat stisknutím tlačítka na samotném routeru, ale mějte na paměti, že pokud zvolíte tento postup, pravděpodobně ztratíte celou konfiguraci routeru. Pokud váš router vyžaduje k nastavení aplikaci, nainstalujte si ji do telefonu nebo počítače a použijte ji pro přístup k nastavení routeru.\",\n  \"install_devices_router_list_2\": \"Vyhledejte nastavení DHCP/DNS. Hledejte zkratku DNS vedle pole, které umožňuje vložit dvě nebo tři sady čísel, každé rozděleno do čtyř skupin s jedním až třemi číslicemi.\",\n  \"install_devices_router_list_3\": \"Zadejte adresy Vašeho serveru AdGuard Home.\",\n  \"install_devices_router_list_4\": \"Na některých typech routerů nemůžete nastavit vlastní DNS server. V tomto případě může AdGuard Home pomoci, pokud jej nastavíte jako <0>DHCP server</0>. V ostatních případech byste si v manuálu k Vašemu routeru měli zjistit, jak přizpůsobit vlastní DNS servery.\",\n  \"install_devices_title\": \"Nakonfigurujte vaše zařízení\",\n  \"install_devices_windows_list_1\": \"Otevřete ovládací panel prostřednictvím nabídky Start nebo vyhledání v systému Windows.\",\n  \"install_devices_windows_list_2\": \"Přejděte na kategorii Síť a Internet a poté na Centrum sítí a sdílení.\",\n  \"install_devices_windows_list_3\": \"Na levé straně panelu klikněte na \\\"Změnit nastavení adaptéru\\\".\",\n  \"install_devices_windows_list_4\": \"Vyberte své aktivní spojení, klikněte na něj pravým tlačítkem myši a zvolte Vlastnosti.\",\n  \"install_devices_windows_list_5\": \"V seznamu najděte \\\"Internet Protocol Version 4 (TCP/IP)\\\", (nebo IPv6, \\\"Internet Protocol Version 6 (TCP/IPv6)\\\"), vyberte jej a znovu klikněte na Vlastnosti.\",\n  \"install_devices_windows_list_6\": \"Zvolte \\\"Použít následující adresy serveru DNS\\\" a zadejte adresy serveru AdGuard Home.\",\n  \"install_saved\": \"Úspěšně uloženo\",\n  \"install_settings_all_interfaces\": \"Všechna rozhraní\",\n  \"install_settings_dns\": \"DNS server\",\n  \"install_settings_dns_desc\": \"Budete muset nakonfigurovat Vaše zařízení nebo router, aby používali DNS server na následujících adresách:\",\n  \"install_settings_interface_link\": \"Vaše administrátorské webové rozhraní AdGuard Home bude k dispozici na těchto adresách:\",\n  \"install_settings_listen\": \"Síťové rozhraní\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Administrátorské webové rozhraní\",\n  \"install_static_configure\": \"AdGuard Home detekoval, že se používá dynamická IP adresa <0>{{ip}}</0>. Chcete ji použít jako statickou adresu?\",\n  \"install_static_error\": \"AdGuard Home nemůže automaticky nakonfigurovat toto síťové rozhraní. Prosím vyhledejte návod, jak to provést ručně.\",\n  \"install_static_ok\": \"Skvělá zpráva! Statická IP adresa je již nakonfigurována\",\n  \"install_step\": \"Krok\",\n  \"install_submit_desc\": \"Nastavení je dokončeno a nyní jste připraveni začít používat AdGuard Home.\",\n  \"install_submit_title\": \"Gratulujeme!\",\n  \"install_welcome_desc\": \"AdGuard Home je síťový DNS server pro blokování reklam a slídičů. Jeho cílem je, abyste ovládali celou Vaši síť a všechny Vaše zařízení, přičemž se nevyžaduje použití jakéhokoliv programu na straně klienta.\",\n  \"install_welcome_title\": \"Vítejte v AdGuard Home!\",\n  \"interval_24_hour\": \"24 hodin\",\n  \"interval_6_hour\": \"6 hodin\",\n  \"interval_days\": \"Dny: {{count}}\",\n  \"interval_days_plural\": \"Dny: {{count}}\",\n  \"interval_hours\": \"Hodiny: {{count}}\",\n  \"interval_hours_plural\": \"Hodiny: {{count}}\",\n  \"ip\": \"IP adresa\",\n  \"ip_address\": \"IP adresa\",\n  \"known_tracker\": \"Známý slídič\",\n  \"last_rule_in_allowlist\": \"Nelze zakázat tohoto klienta, protože vyloučení pravidla \\\"{{disallowed_rule}}\\\" ZRUŠÍ seznam \\\"Povolených klientů\\\".\",\n  \"last_time_updated_table_header\": \"Čas poslední aktualizace\",\n  \"list_confirm_delete\": \"Opravdu chcete smazat tento seznam?\",\n  \"list_label\": \"Seznam\",\n  \"list_updated\": \"Byl aktualizován {{count}} seznam\",\n  \"list_updated_plural\": \"Aktualizované seznamy: {{count}}\",\n  \"list_url_table_header\": \"Seznam URL\",\n  \"load_balancing\": \"Optimalizace vytížení\",\n  \"load_balancing_desc\": \"Dotazy jednoho odchozího serveru ve stejný čas.<br/>AdGuard Home používá náhodný algoritmus pro výběr serverů s nejnižším počtem neúspěšných vyhledávání a nejnižší průměrnou dobou vyhledávání.\",\n  \"loading_table_status\": \"Načítání...\",\n  \"local_ptr_default_resolver\": \"Ve výchozím nastavení používá AdGuard Home následující reverzní DNS řešitele: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS servery používané AdGuard Home pro soukromé požadavky PTR, SOA a NS. Požadavek je považován za soukromý, pokud požaduje doménu ARPA obsahující podsíť v rámci soukromých IP rozsahů (například \\\"192.168.12.34\\\") a pochází od klienta se soukromou IP adresou. Pokud není nastaveno, budou použity výchozí DNS řešitele vašeho operačního systému, s výjimkou IP adres AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home nemohl určit vhodné soukromé reverzní DNS řešitele pro tento systém.\",\n  \"local_ptr_placeholder\": \"Zadejte jednu IP adresu na řádek\",\n  \"local_ptr_title\": \"Soukromé reverzní DNS servery\",\n  \"location\": \"Umístění\",\n  \"log_and_stats_section_label\": \"Protokol dotazů a statistiky\",\n  \"lower_range_start_error\": \"Musí být menší než začátek rozsahu\",\n  \"main_settings\": \"Hlavní nastavení\",\n  \"make_static\": \"Nastavit jako statickou\",\n  \"manual_update\": \"Prosím <a>následujte tyto kroky</a> a aktualizujte ručně.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Pondělí\",\n  \"monday_short\": \"Pondělí\",\n  \"name\": \"Název\",\n  \"name_table_header\": \"Název\",\n  \"netname\": \"Název sítě\",\n  \"network\": \"Síť\",\n  \"new_allowlist\": \"Nový seznam povolených\",\n  \"new_blocklist\": \"Nový seznam blokovaných\",\n  \"next\": \"Další\",\n  \"next_btn\": \"Další\",\n  \"no_blocklist_added\": \"Nebyl přidán žádný seznam blokovaných\",\n  \"no_clients_found\": \"Nenalezeny žádní klienti\",\n  \"no_domains_found\": \"Nenalezeny žádné domény\",\n  \"no_logs_found\": \"Nenalezeny žádné protokoly\",\n  \"no_servers_specified\": \"Nebyly specifikovány žádné servery\",\n  \"no_upstreams_data_found\": \"Nebyla nalezena žádná data odchozích připojení\",\n  \"no_whitelist_added\": \"Nebyl přidán žádný seznam povolených\",\n  \"nothing_found\": \"Nic nenalezeno\",\n  \"null_ip\": \"Nulová IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Počet požadavků DNS zablokovaných filtrem reklam a seznamy blokování hostitelů\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Počet zablokovaných stránek pro dospělé\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Počet požadavků DNS zablokovaných AdGuard modulem Bezpečné prohlížení\",\n  \"number_of_dns_query_days\": \"Počet DNS dotazů zpracovaných za posledních {{count}} den\",\n  \"number_of_dns_query_days_plural\": \"Počet DNS dotazů zpracovaných za posledních {{count}} dní\",\n  \"number_of_dns_query_hours\": \"Počet DNS dotazů zpracovaných za poslední {{count}} hodinu\",\n  \"number_of_dns_query_hours_plural\": \"Počet DNS dotazů zpracovaných za posledních {{count}} hodin\",\n  \"number_of_dns_query_to_safe_search\": \"Počet požadavků DNS na vyhledávače, při kterých bylo vynucené bezpečné vyhledávání\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"VYPNUTO\",\n  \"on\": \"ZAPNUTO\",\n  \"open_dashboard\": \"Otevřít hlavní panel\",\n  \"orgname\": \"Název organizace\",\n  \"original_response\": \"Původní odezva\",\n  \"out_of_range_error\": \"Musí být mimo rozsah \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Stránka\",\n  \"parallel_requests\": \"Paralelní požadavky\",\n  \"parental_control\": \"Rodičovská ochrana\",\n  \"password_label\": \"Heslo\",\n  \"password_placeholder\": \"Zadejte heslo\",\n  \"plain_dns\": \"Běžný DNS\",\n  \"port_53_faq_link\": \"Port 53 je často obsazen službami \\\"DNSStubListener\\\" nebo \\\"systemd-resolved\\\". Přečtěte si <0>tento návod</0> o tom, jak to vyřešit.\",\n  \"previous_btn\": \"Předchozí\",\n  \"privacy_policy\": \"Zásady ochrany osobních údajů\",\n  \"processing_update\": \"Čekejte prosím, AdGuard Home se aktualizuje\",\n  \"protection_section_label\": \"Ochrana\",\n  \"protocol\": \"Protokol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Protokol dotazů\",\n  \"query_log_clear\": \"Vymazat protokoly dotazů\",\n  \"query_log_cleared\": \"Protokol dotazů byl úspěšně vymazán\",\n  \"query_log_configuration\": \"Konfigurace protokolů\",\n  \"query_log_confirm_clear\": \"Opravdu chcete vymazat celý protokol dotazů?\",\n  \"query_log_disabled\": \"Protokol dotazu je zakázán a lze jej nakonfigurovat v <0>nastavení</0>\",\n  \"query_log_enable\": \"Povolit protokol\",\n  \"query_log_filtered\": \"Filtrováno pomocí {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotace protokolů dotazů\",\n  \"query_log_retention_confirm\": \"Opravdu chcete změnit rotaci protokolu dotazů? Pokud snížíte hodnotu intervalu, některá data budou ztracena\",\n  \"query_log_strict_search\": \"Pro striktní vyhledávání použijte dvojité uvozovky\",\n  \"query_log_updated\": \"Protokol dotazů byl úspěšně aktualizován\",\n  \"rate_limit\": \"Rychlostní limit\",\n  \"rate_limit_desc\": \"Počet požadavků za sekundu, které smí jeden klient provádět (0: neomezeno)\",\n  \"rate_limit_subnet_len_ipv4\": \"Délka předpony podsítě pro adresy IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Délka předpony podsítě pro adresy IPv4 používané pro omezení rychlosti. Výchozí hodnota je 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Délka předpony podsítě IPv4 by měla být mezi 0 a 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Délka předpony podsítě pro adresy IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Délka předpony podsítě pro adresy IPv6 používané pro omezení rychlosti. Výchozí hodnota je 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Délka předpony podsítě IPv6 by měla být mezi 0 a 128\",\n  \"rate_limit_whitelist\": \"Seznam výjimek pro omezení rychlosti\",\n  \"rate_limit_whitelist_desc\": \"IP adresy vyloučené z omezení rychlosti\",\n  \"rate_limit_whitelist_placeholder\": \"Zadejte jednu IP adresu na řádek\",\n  \"refresh_btn\": \"Obnovit\",\n  \"refresh_statics\": \"Obnovit statistiky\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Nahlásit problém\",\n  \"request_details\": \"Detaily požadavku\",\n  \"request_table_header\": \"Požadavek\",\n  \"requests_count\": \"Počet požadavků\",\n  \"reset_settings\": \"Resetovat nastavení\",\n  \"resolve_clients_desc\": \"Obráceně vyřešit IP adresy klientů na jejich názvy hostitelů zasláním dotazů PTR příslušným řešitelům (soukromé DNS servery pro místní klienty, odchozí servery pro klienty s veřejnou IP adresou).\",\n  \"resolve_clients_title\": \"Povolit zpětné řešení IP adres klientů\",\n  \"response_code\": \"Kód odezvy\",\n  \"response_details\": \"Detail odpovědi\",\n  \"response_table_header\": \"Odezva\",\n  \"response_time\": \"Čas odezvy\",\n  \"rewrite_A\": \"<0>A</0>: speciální hodnota, udržet záznamy typu <0>A</0> z odchozího serveru\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: speciální hodnota, udržet záznamy typu <0>AAAA</0> z odchozího serveru\",\n  \"rewrite_add\": \"Přidat přesměrování DNS\",\n  \"rewrite_added\": \"Přesměrování DNS pro „{{key}}“ úspěšně přidáno\",\n  \"rewrite_applied\": \"Aplikované pravidlo přesměrování\",\n  \"rewrite_confirm_delete\": \"Jste si jisti, že chcete smazat přesměrování DNS pro „{{key}}“?\",\n  \"rewrite_deleted\": \"Přesměrování DNS pro „{{key}}“ úspěšně smazáno\",\n  \"rewrite_desc\": \"Umožňuje snadno nakonfigurovat vlastní DNS odezvy pro konkrétní název domény.\",\n  \"rewrite_domain_name\": \"Název domény: Přidat záznam CNAME\",\n  \"rewrite_edit\": \"Upravit přesměrování DNS\",\n  \"rewrite_hosts_applied\": \"Přepsáno pravidlem souboru hosts\",\n  \"rewrite_ip_address\": \"IP address: použít tuto IP adresu v odpovědi typu A nebo AAAA\",\n  \"rewrite_not_found\": \"Přesměrování DNS nenalezeny\",\n  \"rewrite_settings_updated\": \"Nastavení přepisování DNS bylo úspěšně aktualizováno\",\n  \"rewrite_updated\": \"Přesměrování DNS bylo úspěšně aktualizováno\",\n  \"rewrites_disabled_table_header\": \"Přepisy jsou deaktivovány\",\n  \"rewrites_enabled_table_header\": \"Přepisy jsou povoleny\",\n  \"rewritten\": \"Přepsáno\",\n  \"rows_table_footer_text\": \"řádky\",\n  \"rule_added_to_custom_filtering_toast\": \"Pravidlo přidáno do vlastních pravidel filtrování: {{rule}}\",\n  \"rule_label\": \"Pravidla\",\n  \"rule_removed_from_custom_filtering_toast\": \"Pravidlo odstraněno z vlastních pravidel filtrování: {{rule}}\",\n  \"rules_count_table_header\": \"Počet pravidel\",\n  \"safe_browsing\": \"Bezpečné prohlížení\",\n  \"safe_search\": \"Bezpečné vyhledávání\",\n  \"saturday\": \"Sobota\",\n  \"saturday_short\": \"Sobota\",\n  \"save_btn\": \"Uložit\",\n  \"save_config\": \"Uložit konfiguraci\",\n  \"schedule_add\": \"Přidat plán\",\n  \"schedule_current_timezone\": \"Aktuální časové pásmo: {{value}}\",\n  \"schedule_desc\": \"Nastavení doby nečinnosti pro blokované služby\",\n  \"schedule_edit\": \"Upravit plán\",\n  \"schedule_from\": \"Od\",\n  \"schedule_invalid_select\": \"Čas zahájení musí být před časem ukončení\",\n  \"schedule_modal_description\": \"Tento plán nahradí všechny stávající plány pro stejný den v týdnu. Každý den v týdnu může mít pouze jedno období nečinnosti.\",\n  \"schedule_modal_time_off\": \"Žádné blokování služeb:\",\n  \"schedule_new\": \"Nový plán\",\n  \"schedule_remove\": \"Odstranit plán\",\n  \"schedule_save\": \"Uložit plán\",\n  \"schedule_select_days\": \"Vyberte dny\",\n  \"schedule_services\": \"Pozastavit blokování služeb\",\n  \"schedule_services_desc\": \"Konfigurace plánu pozastavení filtru blokování služeb\",\n  \"schedule_services_desc_client\": \"Konfigurace plánu pozastavení filtru blokování služeb pro tohoto klienta\",\n  \"schedule_time_all_day\": \"Všechny dny\",\n  \"schedule_timezone\": \"Vyberte časové pásmo\",\n  \"schedule_to\": \"Do\",\n  \"served_from_cache_label\": \"Převzato z mezipaměti\",\n  \"service_name\": \"Název služby\",\n  \"set_static_ip\": \"Nastavit statickou IP adresu\",\n  \"settings\": \"Nastavení\",\n  \"settings_custom\": \"Vlastní\",\n  \"settings_global\": \"Globální\",\n  \"setup_config_to_enable_dhcp_server\": \"Nastavte konfiguraci pro aktivaci DHCP serveru\",\n  \"setup_dns_notice\": \"Pro použití <1>DNS-over-HTTPS</1> nebo <1>DNS-over-TLS</1> potřebujete v nastaveních AdGuard Home <0>nakonfigurovat šifrování</0>.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Použít <1>{{address}}</1> řetězec.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Použít <1>{{address}}</1> řetězec.\",\n  \"setup_dns_privacy_3\": \"<0>Zde je seznam softwaru, který můžete použít.</0>\",\n  \"setup_dns_privacy_4\": \"Na zařízení se systémem iOS 14 nebo macOS Big Sur si můžete stáhnout speciální soubor '.mobileconfig', který do nastavení DNS přidává servery <highlight>DNS-over-HTTPS</highlight> nebo <highlight> DNS-over-TLS</highlight>.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 podporuje DNS-over-TLS nativně. Pokud ho chcete konfigurovat, přejděte na Nastavení → Síť a internet → Pokročilé → Soukromé DNS a tam zadejte název vaší domény.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard pro Android</0> podporuje <1>DNS-over-HTTPS</1> a <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> přidává podporu <1>DNS-over-HTTPS</1> pro Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Konfigurace pro iOS a macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> podporuje funkci <1>DNS-over-HTTPS</1>, ale abyste ji mohli nakonfigurovat pro používání vlastního serveru, musíte vygenerovat značku <2>DNS Stamp</2>.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard pro iOS</0> podporuje nastavení <1>DNS-over-HTTPS</1> a <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"Samotný AdGuard Home může být bezpečným klientem DNS na jakékoli platformě.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> podporuje všechny známé bezpečné DNS protokoly.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> podporuje <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> podporuje <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Další implementace naleznete <0>zde</0> a <1>zde</1>.\",\n  \"setup_dns_privacy_other_title\": \"Další implementace\",\n  \"setup_guide\": \"Průvodce nastavením\",\n  \"show_all_filter_type\": \"Zobrazit vše\",\n  \"show_blocked_responses\": \"Zablokované\",\n  \"show_filtered_type\": \"Zobrazit filtrované\",\n  \"show_processed_responses\": \"Zpracovaný\",\n  \"show_whitelisted_responses\": \"Povolené\",\n  \"sign_in\": \"Přihlásit se\",\n  \"sign_out\": \"Odhlásit se\",\n  \"source_label\": \"Zdroj\",\n  \"static_ip\": \"Statická IP adresa\",\n  \"static_ip_desc\": \"AdGuard Home je server, takže pro správné fungování potřebuje statickou IP adresu. V opačném případě může váš router tomuto zařízení přiřadit jinou IP adresu.\",\n  \"statistics_clear\": \" Vyčistit statistiky\",\n  \"statistics_clear_confirm\": \"Opravdu chcete vyčistit statistiky?\",\n  \"statistics_cleared\": \"Statistiky úspěšně vyčištěny\",\n  \"statistics_configuration\": \"Konfigurace statistik\",\n  \"statistics_enable\": \"Povolit statistiky\",\n  \"statistics_retention\": \"Uchovávání statistik\",\n  \"statistics_retention_confirm\": \"Opravdu chcete změnit uchovávání statistik? Pokud snížíte hodnotu intervalu, některá data budou ztracena\",\n  \"statistics_retention_desc\": \"Pokud hodnotu intervalu snížíte, některá data budou ztracena\",\n  \"stats_adult\": \"Blokované stránky pro dospělé\",\n  \"stats_disabled\": \"Statistiky byly vypnuty. Můžete je zapnout ze <0>stránky nastavení</0>.\",\n  \"stats_disabled_short\": \"Statistiky byly vypnuty\",\n  \"stats_malware_phishing\": \"Blokovaný malware/podvody\",\n  \"stats_params\": \"Konfigurace statistik\",\n  \"stats_query_domain\": \"Nejčastěji dotazované domény\",\n  \"subnet_error\": \"Adresy musí být v jedné podsíti\",\n  \"sunday\": \"Neděle\",\n  \"sunday_short\": \"Neděle\",\n  \"system_host_files\": \"Systémové soubory hostitelů\",\n  \"table_client\": \"Klient\",\n  \"table_name\": \"Název\",\n  \"tags_desc\": \"Můžete vybrat značky, které jsou přiřazeny klientovi. Značky mohou být zahrnuty do pravidel filtrování a umožňují Vám je přesněji použít. <0>Dozvědět se více</0>.\",\n  \"tags_title\": \"Značky\",\n  \"test_upstream_btn\": \"Test upstreamů\",\n  \"theme_auto\": \"Autom.\",\n  \"theme_auto_desc\": \"Automatický (podle barevného motivu vašeho zařízení)\",\n  \"theme_dark\": \"Tmavý\",\n  \"theme_dark_desc\": \"Tmavý motiv\",\n  \"theme_light\": \"Světlý\",\n  \"theme_light_desc\": \"Světlý motiv\",\n  \"thursday\": \"Čtvrtek\",\n  \"thursday_short\": \"Čtvrtek\",\n  \"time_table_header\": \"Čas\",\n  \"top_blocked_domains\": \"Nejčastěji blokované domény\",\n  \"top_clients\": \"Nejčastější klienti\",\n  \"top_upstreams\": \"Top odchozí připojení\",\n  \"topline_expired_certificate\": \"Váš SSL certifikát vypršel. Aktualizujte <0>Nastavení šifrování</0>.\",\n  \"topline_expiring_certificate\": \"Váš SSL certifikát brzy vyprší. Aktualizujte <0>Nastavení šifrování</0>.\",\n  \"tracker_source\": \"Zdroj slídiče\",\n  \"try_again\": \"Zkusit znovu\",\n  \"ttl_cache_validation\": \"Minimální přepis TTL mezipaměti musí být menší nebo roven maximální hodnotě\",\n  \"tuesday\": \"Úterý\",\n  \"tuesday_short\": \"Úterý\",\n  \"type_table_header\": \"Typ\",\n  \"unavailable_dhcp\": \"DHCP není k dispozici\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home nemůže na vašem operačním systému provozovat DHCP server\",\n  \"unblock\": \"Odblokovat\",\n  \"unblock_all\": \"Odblokovat vše\",\n  \"unblock_for_this_client_only\": \"Odblokovat pouze pro tohoto klienta\",\n  \"unknown_filter\": \"Neznámý filtr {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} je nyní k dispozici! <0>Klikněte zde<0> pro více informací.\",\n  \"update_failed\": \"Automatická aktualizace selhala. Prosím <a>následujte tyto kroky</a> a aktualizujte ručně.\",\n  \"update_now\": \"Aktualizovat nyní\",\n  \"updated_custom_filtering_toast\": \"Vlastní pravidla byla úspěšně uložena\",\n  \"updated_save_search_toast\": \"Nastavení Bezpečného vyhledávání aktualizováno\",\n  \"updated_upstream_dns_toast\": \"Odchozí servery byly úspěšně uloženy\",\n  \"updates_checked\": \"Nová verze AdGuard Home je k dispozici\\n\",\n  \"updates_version_equal\": \"AdGuard Home je aktuální\",\n  \"upstream\": \"Odchozí připojení\",\n  \"upstream_dns\": \"Odchozí DNS servery\",\n  \"upstream_dns_cache_configuration\": \"Konfigurace mezipaměti odchozího DNS\",\n  \"upstream_dns_client_desc\": \"Pokud toto pole ponecháte prázdné, AdGuard Home použije servery nakonfigurované v<0>DNS nastavení</0>.\",\n  \"upstream_dns_configured_in_file\": \"Konfigurováno v {{path}}\",\n  \"upstream_dns_help\": \"Zadejte adresu serveru, jedno připojení na řádek. <a>Zjistěte více</a> o konfiguraci odchozích DNS serverů.\",\n  \"upstream_parallel\": \"Použijte paralelní požadavky na urychlení řešení simultánním dotazováním na všechny navazující servery.\",\n  \"upstream_timeout\": \"Časový limit odchozího serveru\",\n  \"upstream_timeout_desc\": \"Určuje počet sekund čekání na odpověď od odchozího serveru\",\n  \"upstreams\": \"Odesláno\",\n  \"use_adguard_browsing_sec\": \"Použít službu AdGuard Bezpečné prohlížení\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home zkontroluje, zda je doména blokována ve službě Bezpečné prohlížení. Použije vyhledávací API přátelské k ochraně soukromí na provedení kontroly: na server je odeslána pouze krátká předpona SHA256 otisku názvu domény.\",\n  \"use_adguard_parental\": \"Použít službu AdGuard Rodičovská kontrola\",\n  \"use_adguard_parental_hint\": \"AdGuard Home zkontroluje, zda doména obsahuje materiály pro dospělé. Používá stejné API přátelské k ochraně osobních údajů jako služba Bezpečnost prohlížení.\",\n  \"use_private_ptr_resolvers_desc\": \"Řešení požadavků PTR, SOA a NS pro domény ARPA obsahující soukromé IP adresy prostřednictvím soukromých odchozích serverů, DHCP, /etc/hosts atd. Pokud je zakázáno, AdGuard Home bude na všechny takové požadavky odpovídat pomocí NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Použít soukromé reverzní rDNS řešitele\",\n  \"use_saved_key\": \"Použít dříve uložený klíče\",\n  \"username_label\": \"Uživatelské jméno\",\n  \"username_placeholder\": \"Zadejte uživatelské jméno\",\n  \"validated_with_dnssec\": \"Ověřeno pomocí DNSSEC\",\n  \"version\": \"Verze\",\n  \"version_request_error\": \"Kontrola aktualizace se nezdařila. Zkontrolujte prosím připojení k Internetu.\",\n  \"wednesday\": \"Středa\",\n  \"wednesday_short\": \"Středa\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/da.json",
    "content": "{\n  \"access_allowed_desc\": \"En liste over CIDR'er, IP-adresser eller <a>KlientID'er</a>. Har listen poster, accepterer AdGuard Home kun forespørgsler fra disse klienter.\",\n  \"access_allowed_title\": \"Tilladte klienter\",\n  \"access_blocked_desc\": \"Ikke at forveksle med filtre. AdGuard Home dropper DNS-forespørgsler matchende disse domæner, ej heller vil forespørgslerne optræde i forespørgselsloggen. Der kan angives præcise domænenavne, jokertegn eller URL-filterregler, f.eks. \\\"eksempel.org\\\", \\\"*.eksempel.org\\\", \\\"||eksempel.org^\\\" eller tilsvarende.\",\n  \"access_blocked_title\": \"Ikke tilladte domæner\",\n  \"access_desc\": \"Her kan adgangsregler for AdGuard Home DNS-serveren opsættes\",\n  \"access_disallowed_desc\": \"En liste over CIDR'er, IP-adresser eller <a>KlientID'er</a>. Har listen poster, dropper AdGuard Home forespørgsler fra disse klienter. Har Tilladte klienter poster, ignoreres dette felt.\",\n  \"access_disallowed_title\": \"Ikke tilladte klienter\",\n  \"access_settings_saved\": \"Adgangsindstillinger gemt\",\n  \"access_title\": \"Adgangsindstillinger\",\n  \"actions_table_header\": \"Handlinger\",\n  \"add_allowlist\": \"Tilføj hvidliste\",\n  \"add_blocklist\": \"Tilføj sortliste\",\n  \"add_custom_list\": \"Tilføj en tilpasset liste\",\n  \"add_persistent_client\": \"Tilføj som vedvarende klient\",\n  \"address\": \"Adresse\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home vil afbryde alle DNS-forespørgsler fra denne klient.\",\n  \"all_lists_up_to_date_toast\": \"Alle lister er allerede opdaterede\",\n  \"all_queries\": \"Alle forespørgsler\",\n  \"allow_this_client\": \"Tillad denne klient\",\n  \"allowed\": \"Tilladt\",\n  \"anonymize_client_ip\": \"Anonymisér klient-IP\",\n  \"anonymize_client_ip_desc\": \"Gem ikke fuld klient IP-adresse i logfiler eller statistikker\",\n  \"anonymizer_notification\": \"<0>Bemærk:</0> IP-anonymisering er aktiveret. Det kan deaktiveres via <1>Generelle indstillinger</1>.\",\n  \"answer\": \"Svar\",\n  \"apply_btn\": \"Anvend\",\n  \"auto_clients_desc\": \"Oplysninger om IP-adresser på enheder, som (måske) bruger AdGuard Home. Disse oplysninger indsamles fra flere kilder, herunder hosts-filer, reverse DNS mv.\",\n  \"auto_clients_title\": \"Klienter (runtime)\",\n  \"autofix_warning_list\": \"Den vil udføre disse opgaver: <0>Deaktivere system DNSStubListener</0> <0>Opsætte DNS-serveradressen til 127.0.0.1</0> <0>Erstatte symbolsk linkmål for /etc/resolv.conf med /run/systemd/resolve/resolv.conf</0> <0>Stoppe DNSStubListener (genindlæs systemd-opløst tjeneste)</0>\",\n  \"autofix_warning_result\": \"Det betyder, at alle DNS-forespørgsler fra dit system som standard behandles af AdGuard Home.\",\n  \"autofix_warning_text\": \"Klikker du på \\\"Reparér\\\", opsætter AdGuard Home dit system til brug med AdGuard Home DNS-server.\",\n  \"average_processing_time\": \"Gennemsnitlig behandlingstid\",\n  \"average_processing_time_hint\": \"Gennemsnitlig behandlingstid i millisekunder af DNS-forespørgsel\",\n  \"average_upstream_response_time\": \"Gennemsnitlig upstream-responstid\",\n  \"back\": \"Tilbage\",\n  \"block\": \"Blokering\",\n  \"block_all\": \"Blokér alle\",\n  \"block_domain_use_filters_and_hosts\": \"Blokér domæner vha. filtre og værtsfiler\",\n  \"block_for_this_client_only\": \"Blokér kun for denne klient\",\n  \"block_services\": \"Blokere specifikke tjenester\",\n  \"blocked_adult_websites\": \"Blokeret af Forælderkontrol\",\n  \"blocked_by\": \"<0>Blokeret af Filtre</0>\",\n  \"blocked_by_cname_or_ip\": \"Blokeret af CNAME eller IP\",\n  \"blocked_by_response\": \"Blokeret af CNAME eller IP i svar\",\n  \"blocked_response_ttl\": \"Blokeret svar TTL\",\n  \"blocked_response_ttl_desc\": \"Angiver, i hvor mange sekunder klienterne skal cache-lagre et filtreret svar\",\n  \"blocked_safebrowsing\": \"Blokeret af Safe Browsing\",\n  \"blocked_service\": \"Blokeret tjeneste\",\n  \"blocked_services\": \"Blokerede tjenester\",\n  \"blocked_services_desc\": \"Gør det muligt hurtigt at blokere populære websteder og tjenester.\",\n  \"blocked_services_global\": \"Brug globale blokerede tjenester\",\n  \"blocked_services_saved\": \"Blokerede tjenester er gemt\",\n  \"blocked_threats\": \"Blokerede Trusler\",\n  \"blocking_ipv4\": \"IPv4-blokering\",\n  \"blocking_ipv4_desc\": \"Returneret IP-adresse for en blokeret A-forespørgsel\",\n  \"blocking_ipv6\": \"IPv6-blokering\",\n  \"blocking_ipv6_desc\": \"Returneret IP-adresse for en blokeret AAAA-forespørgsel\",\n  \"blocking_mode\": \"Blokeringstilstand\",\n  \"blocking_mode_custom_ip\": \"Tilpasset IP: Svar med en manuelt indstillet IP-adresse\",\n  \"blocking_mode_default\": \"Standard: Svar med nul IP-adresse (0.0.0.0 for A; :: for AAAA), når blokeret af Adblock-lignende regel. Svar med IP-adressen angivet i reglen, når blokeret af /etc/hosts-lignende regel\",\n  \"blocking_mode_null_ip\": \"Null IP: Svar med nul IP-adresse (0.0.0.0 for A; :: for AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Svar med NXDOMAIN-kode\",\n  \"blocking_mode_refused\": \"NÆGTET: Svar med en NÆGTET-kode\",\n  \"blocklist\": \"Sortliste\",\n  \"bootstrap_dns\": \"Bootstrap DNS-servere\",\n  \"bootstrap_dns_desc\": \"IP-adresser på DNS-servere, som bruges til at opløse IP-adresser på de DoH/DoT-opløsere, som angives som upstreams. Kommentarer er ikke tilladt.\",\n  \"cache_cleared\": \"DNS-cache hermed ryddet\",\n  \"cache_enabled\": \"Aktivér cache\",\n  \"cache_enabled_desc\": \"Opbevar DNS-svar lokalt.\",\n  \"cache_optimistic\": \"Optimistisk caching\",\n  \"cache_optimistic_desc\": \"Får AdGuard Home til at svare fra cachen, selv når posterne er udløbet, og prøver også at opdatere dem.\",\n  \"cache_size\": \"Cache-størrelse\",\n  \"cache_size_desc\": \"DNS-cachestørrelse (i bytes).\",\n  \"cache_size_validation\": \"Cache-størrelsen skal være større end nul, når den er aktiveret.\",\n  \"cache_ttl_max_override\": \"Tilsidesæt maksimal TTL\",\n  \"cache_ttl_max_override_desc\": \"Angiv en maksimal time-to-live (sekunder) for poster i DNS-cachen.\",\n  \"cache_ttl_min_override\": \"Tilsidesæt minimum TTL\",\n  \"cache_ttl_min_override_desc\": \"Forlæng korte time-to-live værdier (sekunder) modtaget fra upstream-serveren, når DNS-svar cachelagres\",\n  \"cancel_btn\": \"Afbryd\",\n  \"category_label\": \"Kategori\",\n  \"check\": \"Tjek\",\n  \"check_client_id\": \"Klientidentifikator (ClientID eller IP-adresse)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Tjek, om værtsnavnet filtreres.\",\n  \"check_dhcp_servers\": \"Søg efter DHCP-servere\",\n  \"check_dns_record\": \"Vælg DNS-posttype\",\n  \"check_enter_client_id\": \"Angiv klientidentifikator\",\n  \"check_hostname\": \"Værts- eller domænenavn\",\n  \"check_ip\": \"IP-adresser: {{ip}}\",\n  \"check_not_found\": \"Ikke fundet i dine filterlister\",\n  \"check_reason\": \"Årsag: {{reason}}\",\n  \"check_service\": \"Tjenestenavn: {{service}}\",\n  \"check_title\": \"Tjek filtreringen\",\n  \"check_updates_btn\": \"Søg efter opdateringer\",\n  \"check_updates_now\": \"Søg efter opdateringer nu\",\n  \"choose_allowlist\": \"Vælg hvidlister\",\n  \"choose_blocklist\": \"Vælg sortlister\",\n  \"choose_from_list\": \"Vælg fra listen\",\n  \"city\": \"By\",\n  \"clear_cache\": \"Ryd cache\",\n  \"click_to_view_queries\": \"Klik for at se forespørgsler\",\n  \"client_add\": \"Tilføj Klient\",\n  \"client_added\": \"Klient \\\"{{key}}\\\" tilføjet\",\n  \"client_blocked\": \"Klient \\\"{{ip}}\\\" blev blokeret\",\n  \"client_confirm_block\": \"Sikker på, at du vil blokere klienten \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Sikker på, at du vil slette klient \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Sikker på, at du vil afblokere klienten \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Klient \\\"{{key}}\\\" slettet\",\n  \"client_details\": \"Klientoplysninger\",\n  \"client_edit\": \"Redigér Klient\",\n  \"client_global_settings\": \"Brug globale indstillinger\",\n  \"client_id\": \"KlientID\",\n  \"client_id_desc\": \"Klienter kan identificeres via KlientID. Læs mere om, hvordan klienter identificeres <a>hér</a>.\",\n  \"client_id_placeholder\": \"Angiv en KlientID\",\n  \"client_identifier\": \"Identifikator\",\n  \"client_identifier_desc\": \"Klienter kan identificeres ud fra IP-/MAC-adresser, CIDR eller et særligt KlientID (kan bruges til DoT/DoH/DoQ). Læs mere om, hvordan klienter identificeres <0>hér</0>.\",\n  \"client_name\": \"Klient {{id}}\",\n  \"client_new\": \"Ny Klient\",\n  \"client_settings\": \"Klientindstillinger\",\n  \"client_table_header\": \"Klient\",\n  \"client_unblocked\": \"Klient \\\"{{ip}}\\\" blev afblokeret\",\n  \"client_updated\": \"Klient \\\"{{key}}\\\" opdateret\",\n  \"clients_desc\": \"Opsæt permanente klientposter for enheder tilsluttet AdGuard Home\",\n  \"clients_not_found\": \"Ingen klienter fundet\",\n  \"clients_title\": \"Blivende klienter\",\n  \"compact\": \"Kompakt\",\n  \"config_successfully_saved\": \"Opsætning er gemt\",\n  \"configure\": \"Opsæt\",\n  \"confirm_dns_cache_clear\": \"Sikker på, at DNS-cache skal ryddes?\",\n  \"confirm_static_ip\": \"AdGuard Home vil opsætte {{ip}} som din statiske IP-adresse. Fortsæt?\",\n  \"copyright\": \"Ophavsrettighed\",\n  \"country\": \"Land\",\n  \"custom_filter_rules\": \"Tilpassede filtreringsregler\",\n  \"custom_filter_rules_hint\": \"Angiv én regel pr. linje. Du kan bruge enten adblockingregler eller værtsfilsyntaks.\",\n  \"custom_filtering_rules\": \"Tilpassede filtreringsregler\",\n  \"custom_ip\": \"Tilpasset IP\",\n  \"custom_retention_input\": \"Angiv opbevaringstid i timer\",\n  \"custom_rotation_input\": \"Angiv rotationstid i timer\",\n  \"dashboard\": \"Kontrolpanel\",\n  \"date\": \"Dato\",\n  \"default\": \"Standard\",\n  \"delete_confirm\": \"Sikker på, at du vil slette \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Slet\",\n  \"descr\": \"Beskrivelse\",\n  \"details\": \"Detaljer\",\n  \"dhcp_add_static_lease\": \"Tilføj statisk lease\",\n  \"dhcp_config_saved\": \"DHCP-opsætning gemt\",\n  \"dhcp_description\": \"Har din router ingen DHCP-indstillinger, kan du bruge AdGuards egen, indbyggede DHCP-server.\",\n  \"dhcp_disable\": \"Deaktivere DHCP-server\",\n  \"dhcp_dynamic_ip_found\": \"Dit system bruger en dynamisk IP-adresseopsætning til interface <0>{{interfaceName}}</0>. For at kunne bruge DHCP-serveren skal en statisk IP-adresse indstilles. Din aktuelle IP-adresse er <0>{{ipAddress}}</0>. AdGuard Home vil automatisk indstille denne IP-adresse som din statiske hvis du trykker på knappen \\\"Aktivér DHCP-server\\\".\",\n  \"dhcp_edit_static_lease\": \"Redigér statisk tildeling\",\n  \"dhcp_enable\": \"Aktivere DHCP-server\",\n  \"dhcp_error\": \"AdGuard Home kunne ikke afgøres, om der findes en anden DHCP-server på netværket\",\n  \"dhcp_form_gateway_input\": \"Gateway IP\",\n  \"dhcp_form_lease_input\": \"Lease-varighed\",\n  \"dhcp_form_lease_title\": \"DHCP-lease tid (i sekunder)\",\n  \"dhcp_form_range_end\": \"Intervalslut\",\n  \"dhcp_form_range_start\": \"Intervalstart\",\n  \"dhcp_form_range_title\": \"Interval af IP-adresser\",\n  \"dhcp_form_subnet_input\": \"Undernetmaske\",\n  \"dhcp_found\": \"En aktiv DHCP-server er fundet på netværket. Det er ikke sikkert at aktivere den indbyggede DHCP-server.\",\n  \"dhcp_hardware_address\": \"Hardware-adresse\",\n  \"dhcp_interface_select\": \"Vælg DHCP-interface\",\n  \"dhcp_ip_addresses\": \"IP-adresser\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4-indstillinger\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6-indstillinger\",\n  \"dhcp_lease_added\": \"Statisk lease \\\"{{key}}\\\" tilføjet\",\n  \"dhcp_lease_deleted\": \"Statisk lease \\\"{{key}}\\\" slettet\",\n  \"dhcp_lease_updated\": \"Statisk tildeling \\\"{{key}}\\\" hermed opdateret\",\n  \"dhcp_leases\": \"DHCP-leases\",\n  \"dhcp_leases_not_found\": \"Ingen DHCP-leases fundet\",\n  \"dhcp_new_static_lease\": \"Ny statisk lease\",\n  \"dhcp_not_found\": \"Det er sikkert at aktivere den indbyggede DHCP-server, da AdGuard Home ingen aktive DHCP-servere fandt på netværket. Du bør dog stadig tjekke dette manuelt, da den automatiske skanning pt. ikke giver en 100% garanti.\",\n  \"dhcp_reset\": \"Sikker på, at du vil nulstille DHCP-opsætningen?\",\n  \"dhcp_reset_leases\": \"Nulstil alle gyldighedsperioder\",\n  \"dhcp_reset_leases_confirm\": \"Sikker på, at du vil nulstille alle gyldighedsperioder?\",\n  \"dhcp_reset_leases_success\": \"DHCP- gyldighedsperioder nulstillet\",\n  \"dhcp_settings\": \"DHCP-indstillinger\",\n  \"dhcp_static_ip_error\": \"For at kunne bruge DHCP-serveren skal der opsættes en statisk IP-adresse. Da det ikke kunne afgøres, om denne netværksinterface er opsat vha. en statisk IP-adresse, bedes du opsætte en manuelt.\",\n  \"dhcp_static_leases\": \"DHCP statiske leases\",\n  \"dhcp_static_leases_not_found\": \"Intet DHCP statisk leases fundet\",\n  \"dhcp_table_expires\": \"Udløber\",\n  \"dhcp_table_hostname\": \"Værtsnavn\",\n  \"dhcp_title\": \"DHCP-server (eksperimentel!)\",\n  \"dhcp_warning\": \"Vil du alligevel aktivere DHCP-serveren, så sørg for at der ikke er nogen anden aktiv DHCP-server på dit netværk, da dette kan ødelægge Internetkonnektiviteten for netværksenhederne!\",\n  \"disable_for_hours\": \"I {{count}} time\",\n  \"disable_for_hours_plural\": \"I {{count}} timer\",\n  \"disable_for_minutes\": \"I {{count}} minut\",\n  \"disable_for_minutes_plural\": \"I {{count}} minutter\",\n  \"disable_for_seconds\": \"I {{count}} sekund\",\n  \"disable_for_seconds_plural\": \"I {{count}} sekunder\",\n  \"disable_ipv6\": \"Deaktivér IPv6-adresseopløsning\",\n  \"disable_ipv6_desc\": \"Drop alle DNS-forespørgsler for IPv6-adresser (type AAAA), og fjern IPv6-tips fra HTTPS-svar.\",\n  \"disable_notify_for_hours\": \"Deaktivere beskyttelse i {{count}} time\",\n  \"disable_notify_for_hours_plural\": \"Deaktivere beskyttelse i {{count}} timer\",\n  \"disable_notify_for_minutes\": \"Deaktivere beskyttelse i {{count}} minut\",\n  \"disable_notify_for_minutes_plural\": \"Deaktivere beskyttelse i {{count}} minutter\",\n  \"disable_notify_for_seconds\": \"Deaktivere beskyttelse i {{count}} sekund\",\n  \"disable_notify_for_seconds_plural\": \"Deaktivere beskyttelse i {{count}} sekunder\",\n  \"disable_notify_until_tomorrow\": \"Deaktiver beskyttelse indtil i morgen\",\n  \"disable_protection\": \"Deaktivér beskyttelse\",\n  \"disable_rewrites\": \"Slå omskrivningsregler fra\",\n  \"disable_until_tomorrow\": \"Indtil i morgen\",\n  \"disabled\": \"Deaktiveret\",\n  \"disabled_dhcp\": \"DHCP-server deaktiveret\",\n  \"disabled_filtering_toast\": \"Filtrering deaktiveret\",\n  \"disabled_parental_toast\": \"Forældrekontrol deaktiveret\",\n  \"disabled_protection\": \"Beskyttelse deaktiveret\",\n  \"disabled_safe_browsing_toast\": \"Sikker browsing deaktiveret\",\n  \"disabled_safe_search_toast\": \"Sikker søgning deaktiveret\",\n  \"disallow_this_client\": \"Afvis denne klient\",\n  \"dns_addresses\": \"DNS-adresser\",\n  \"dns_allowlists\": \"DNS-hvidlister\",\n  \"dns_allowlists_desc\": \"Domæner fra DNS-hvidlisterne tillades, selv hvis de er på nogle af sortlisterne.\",\n  \"dns_blocklists\": \"DNS-sortlister\",\n  \"dns_blocklists_desc\": \"AdGuard Home blokerer domæner matchende sortlisterne.\",\n  \"dns_cache_config\": \"DNS-cacheopsætning\",\n  \"dns_cache_config_desc\": \"Hér kan DNS-cache opsættes.\",\n  \"dns_cache_size\": \"DNS-cachestørrelse i bytes\",\n  \"dns_config\": \"DNS-serveropsætning\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-Quic\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS-fortrolighed\",\n  \"dns_providers\": \"Her er en <0>liste over kendte DNS-udbydere</ 0> at vælge imellem.\",\n  \"dns_query\": \"DNS-forespørgsler\",\n  \"dns_rewrites\": \"DNS-omskrivninger\",\n  \"dns_settings\": \"DNS-indstillinger\",\n  \"dns_start\": \"DNS-server starter\",\n  \"dns_status_error\": \"Fejl under tjek af DNS-serverstatus.\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": Kunne ikke bruges. Tjek, at du har angivet den korrekt\",\n  \"dns_test_ok_toast\": \"Angivne DNS-servere fungerer korrekt\",\n  \"dns_test_parsing_error_toast\": \"Sektion {{section}}: linje {{line}}: kunne ikke anvendes. Tjek at den er angivet korrekt\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" svarer ikke på testforespørgsler og fungerer muligvis ikke korrekt\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Aktivér DNSSEC\",\n  \"dnssec_enable_desc\": \"Sæt DNSSEC-flag i de udgående DNS-forespørgsler, og tjek resultatet (DNSSEC-understøttet opløser er krævet).\",\n  \"domain\": \"Domæne\",\n  \"domain_desc\": \"Angiv domænenavnet eller jokertegnene, du ønsker omskrevet.\",\n  \"domain_name_table_header\": \"Domænenavn\",\n  \"domain_or_client\": \"Domæne eller klient\",\n  \"down\": \"Ned\",\n  \"download_mobileconfig\": \"Download opsætningsfil\",\n  \"download_mobileconfig_doh\": \"Download .mobileconfig til DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Download .mobileconfig til DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Hvidlisteredigering\",\n  \"edit_blocklist\": \"Sortlisteredigering\",\n  \"edit_table_action\": \"Redigér\",\n  \"edns_cs_desc\": \"Tilføj indstillingen EDNS Client Subnet (ECS) til upstream-forespørgsler og log de af klienterne sendte værdier i forespørgselsloggen.\",\n  \"edns_enable\": \"Aktivér EDNS-klientundernet\",\n  \"edns_use_custom_ip\": \"Brug tilpasset IP til EDNS\",\n  \"edns_use_custom_ip_desc\": \"Tillad brug af tilpasset IP til EDNS\",\n  \"elapsed\": \"Varighed\",\n  \"empty_response_status\": \"Tomt\",\n  \"enable_protection\": \"Aktivér beskyttelse\",\n  \"enable_protection_timer\": \"Beskyttelse deaktiveres om {{time}}\",\n  \"enable_rewrites\": \"Slå omskrivningsregler til\",\n  \"enable_upstream_dns_cache\": \"Aktivér DNS-cachelagring for denne klients tilpassede upstream-opsætning\",\n  \"enabled_dhcp\": \"DHCP-server aktiveret\",\n  \"enabled_filtering_toast\": \"Filtrering aktiveret\",\n  \"enabled_parental_toast\": \"Forældrekontrol aktiveret\",\n  \"enabled_protection\": \"Beskyttelse aktiveret\",\n  \"enabled_safe_browsing_toast\": \"Sikker browsing aktiveret\",\n  \"enabled_save_search_toast\": \"Sikker søgning aktiveret\",\n  \"enabled_table_header\": \"Aktiveret\",\n  \"encryption_certificate_path\": \"Certifikatsti\",\n  \"encryption_certificates\": \"Certifikater\",\n  \"encryption_certificates_desc\": \"For at kunne bruge kryptering skal du angive en gyldig SSL-certifikatkæde til dit domæne. Du kan få et gratis certifikat via <0>{{link}}</ 0>, eller du kan købe det via en af de betroede Certifikatmyndigheder.\",\n  \"encryption_certificates_input\": \"Kopiér/indsæt dine PEM-kodede certifikater hér.\",\n  \"encryption_certificates_source_content\": \"Indsæt certifikatets indhold\",\n  \"encryption_certificates_source_path\": \"Opsæt en certifikatfilsti\",\n  \"encryption_chain_invalid\": \"Certifikatkæden er ugyldig\",\n  \"encryption_chain_valid\": \"Certifikatkæden er gyldig\",\n  \"encryption_config_saved\": \"Krypteringsopsætning gemt\",\n  \"encryption_desc\": \"Krypteringsunderstøttelse (HTTPS/TLS) til både DNS og admin-webgrænseflade\",\n  \"encryption_doq\": \"DNS-over-QUIC port\",\n  \"encryption_doq_desc\": \"Er denne port opsat, vil AdGuard Home køre en DNS-over-QUIC server på denne port. \",\n  \"encryption_dot\": \"DNS-over-TLS port\",\n  \"encryption_dot_desc\": \"Er denne port opsat, vil AdGuard Home køre en DNS-over-TLS server på denne port.\",\n  \"encryption_enable\": \"Aktivér Kryptering (HTTPS, DNS-over-HTTPS og DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Er kryptering aktiveret, fungerer AdGuard Home admin grænsefladen over HTTPS, og DNS-serveren lytter efter forespørgsler via DNS-over-HTTPS og DNS-over-TLS.\",\n  \"encryption_expire\": \"Udløber\",\n  \"encryption_hostnames\": \"Værtsnavne\",\n  \"encryption_https\": \"HTTPS-port\",\n  \"encryption_https_desc\": \"Er HTTPS-porten opsat, vil AdGuard Home admin grænsefladen være tilgængelig via HTTPS, og den vil muliggøre DNS-over-HTTPS på '/dns-query' placeringen.\",\n  \"encryption_issuer\": \"Udsteder\",\n  \"encryption_key\": \"Privat nøgle\",\n  \"encryption_key_input\": \"Kopiér/indsæt dine PEM-kodede private nøgle til dit certifikat hér.\",\n  \"encryption_key_invalid\": \"Dette er en ugyldig {{type}} privat nøgle\",\n  \"encryption_key_source_content\": \"Indsæt indholdet af den private nøgle\",\n  \"encryption_key_source_path\": \"Indstil en privat nøglefilsti\",\n  \"encryption_key_valid\": \"Dette er en gyldig {{type}} privat nøgle\",\n  \"encryption_plain_dns_desc\": \"Almindelig DNS er aktiveret som standard. Den kan deaktiveres for at tvinge alle enheder til at bruge krypteret DNS. For at gøre dette, aktivér mindst én krypteret DNS-protokol\",\n  \"encryption_plain_dns_enable\": \"Aktivér almindelig DNS\",\n  \"encryption_plain_dns_error\": \"Aktivér mindst én krypteret DNS-protokol for at deaktivere almindelig DNS\",\n  \"encryption_private_key_path\": \"Private nøgle-sti\",\n  \"encryption_redirect\": \"Omdirigér automatisk til HTTPS\",\n  \"encryption_redirect_desc\": \"Hvis afkrydset, omdirigerer AdGuard Home dig automatisk fra HTTP- til HTTPS-adresser.\",\n  \"encryption_reset\": \"Sikker på, at du vil nulstille krypteringsindstillingerne?\",\n  \"encryption_server\": \"Servernavn\",\n  \"encryption_server_desc\": \"Hvis indstillet, registrerer AdGuard Home ClientID'er, svarer på DDR-forespørgsler og udfører yderligere forbindelsesvalideringer. Hvis ikke er indstillet, er disse funktioner deaktiveret. Skal matche et af DNS-navnene i certifikatet.\",\n  \"encryption_server_enter\": \"Angiv dit domænenavn\",\n  \"encryption_settings\": \"Krypteringsindstillinger\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Emne\",\n  \"encryption_title\": \"Kryptering\",\n  \"encryption_warning\": \"Advarsel\",\n  \"enforce_safe_search\": \"Brug sikker søgning\",\n  \"enforce_save_search_hint\": \"AdGuard Home vil håndhæve sikker søgning i flg. søgemaskiner: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Håndhævet sikker søgning\",\n  \"enter_cache_size\": \"Angiv cache-størrelse (bytes)\",\n  \"enter_cache_ttl_max_override\": \"Angiv maksimum TTL (sekunder)\",\n  \"enter_cache_ttl_min_override\": \"Angiv minimum TTL (sekunder)\",\n  \"enter_name_hint\": \"Angiv navn\",\n  \"enter_url_or_path_hint\": \"Angiv en URL eller en absolut listesti\",\n  \"enter_valid_allowlist\": \"Angiv en gyldig URL til hvidlisten.\",\n  \"enter_valid_blocklist\": \"Angiv en gyldig URL til sortlisten.\",\n  \"error_details\": \"Fejloplysninger\",\n  \"example_comment\": \"! Hér angives en kommentar.\",\n  \"example_comment_hash\": \"# Også en kommentar.\",\n  \"example_comment_meaning\": \"kun en kommentar;\",\n  \"example_meaning_filter_block\": \"blokér adgang til eksmpel.dk-domænet og alle underdomæner;\",\n  \"example_meaning_filter_whitelist\": \"afblokér adgang til eksempel.dk-domænet og alle underdomæner;\",\n  \"example_meaning_host_block\": \"besvar med 127.0.0.1 for eksempel.dk-domænet (men ikke underdomænerne);\",\n  \"example_multiple_upstreams_reserved\": \"flere upstreams <0>til bestemte domæner</0>;\",\n  \"example_regex_meaning\": \"blokér adgang til domæner matchernde det angivne regulære udtryk\",\n  \"example_rewrite_domain\": \"omskriv kun svar for dette domænenavn.\",\n  \"example_rewrite_wildcard\": \"omskriv svar for alle <0>example.org</0> underdomæner.\",\n  \"example_upstream_comment\": \"en kommentaren.\",\n  \"example_upstream_doh\": \"krypteret <0>DNS-over-HTTPS</0>\",\n  \"example_upstream_doh3\": \"krypteret DNS-over-HTTPS med tvungen <0>HTTP/3</0> uden fallback til HTTP/2 eller lavere;\",\n  \"example_upstream_doq\": \"krypteret <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"krypteret <0>DNS-over-TLS</0>\",\n  \"example_upstream_regular\": \"almindelig DNS (over UDP)\",\n  \"example_upstream_regular_port\": \"almindelig DNS (over UDP, med port);\",\n  \"example_upstream_reserved\": \"en upstream <0>for bestemte domæner</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> til <1>DNSCrypt</1> eller <2>DNS-over-HTTPS</2>-opløsere;\",\n  \"example_upstream_tcp\": \"almindelig DNS (over TCP)\",\n  \"example_upstream_tcp_hostname\": \"almindelig DNS (over TCP, værtsnavn);\",\n  \"example_upstream_tcp_port\": \"almindelig DNS (over TCP, med port);\",\n  \"example_upstream_udp\": \"almindelig DNS (over UDP, værtsnavn);\",\n  \"examples_title\": \"Eksempler\",\n  \"fallback_dns_desc\": \"Liste over reserve (fallback) DNS-servere, som bruges, når upstream DNS-servere ikke reagerer. Samme syntaks som i upstream-hovedfeltet ovenfor.\",\n  \"fallback_dns_placeholder\": \"Angiv én reserve DNS-server pr. linje\",\n  \"fallback_dns_title\": \"Reserve DNS-servere\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Hurtigste IP-adresse\",\n  \"fastest_addr_desc\": \"Vent på svar fra <b>alle</b> DNS-servere, mål TCP-forbindelseshastigheden for hver server, og returner IP-adressen på serveren med den hurtigste forbindelseshastighed.<br/>Denne tilstand kan sinke DNS-forespørgsler, betydeligt hvis en eller flere upstream-servere ikke svarer. Sørg for, at upstream-serverene er stabile, og at upstream-timeouten er lav.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"Listen er tilføjet\",\n  \"filter_allowlist\": \"ADVARSEL: Denne handling udelukker også reglen \\\"{{disallowed_rule}}\\\" fra listen over tilladte klienter.\",\n  \"filter_category_general\": \"Generelt\",\n  \"filter_category_general_desc\": \"Lister, som blokerer sporing og reklamer på de fleste enheder\",\n  \"filter_category_other\": \"Andre\",\n  \"filter_category_other_desc\": \"Andre blokeringslister\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Lister målrettet regionale annoncer og sporingsservere\",\n  \"filter_category_security\": \"Sikkerhed\",\n  \"filter_category_security_desc\": \"Lister designet specifikt til at blokere malware-, phishing- eller svindel-domæner\",\n  \"filter_removed_successfully\": \"Listen er blevet fjernet\",\n  \"filter_updated\": \"Listen er blevet opdateret\",\n  \"filtered\": \"Filtreret\",\n  \"filtered_custom_rules\": \"Filtreret af tilpassede filtreringsregler\",\n  \"filtering_rules_learn_more\": \"<0>Læs mere</0> om at oprette dine egne værtslister.\",\n  \"filters\": \"Filtre\",\n  \"filters_and_hosts_hint\": \"AdGuard Home forstår basis adblockingregler og værtsfilsyntaks.\",\n  \"filters_block_toggle_hint\": \"Du kan opsætte blokeringsregler i <a>Filterindstillingerne</a>.\",\n  \"filters_configuration\": \"Filteropsætninger\",\n  \"filters_enable\": \"Aktivér filtre\",\n  \"filters_interval\": \"Filteropdateringsinterval\",\n  \"fix\": \"Korrigér\",\n  \"for_last_days\": \"den seneste {{count}} dag\",\n  \"for_last_days_plural\": \"de seneste {{count}} dage\",\n  \"for_last_hours\": \"den seneste {{count}} time\",\n  \"for_last_hours_plural\": \"de seneste {{count}} timer\",\n  \"forgot_password\": \"Glemt adgangskode?\",\n  \"forgot_password_desc\": \"Følg <0>disse trin</0> for at oprette en ny adgangskode til din brugerkonto.\",\n  \"form_add_id\": \"Tilføj identifikator\",\n  \"form_answer\": \"Angiv IP-adresse eller domænenavn\",\n  \"form_client_name\": \"Angiv klientnavn\",\n  \"form_domain\": \"Angiv domænenavn eller jokertegn\",\n  \"form_enter_blocked_response_ttl\": \"Angiv blokeringssvar TTL (sekunder)\",\n  \"form_enter_host\": \"Angiv et værtsnavn\",\n  \"form_enter_hostname\": \"Angiv værtsnavn\",\n  \"form_enter_id\": \"Angiv identifikator\",\n  \"form_enter_ip\": \"Angiv IP\",\n  \"form_enter_mac\": \"Angiv MAC\",\n  \"form_enter_rate_limit\": \"Angiv hyppighedsgrænse\",\n  \"form_enter_rate_limit_subnet_len\": \"Angiv længden på undernetpræfiks til hastighedsbegrænsning\",\n  \"form_enter_subnet_ip\": \"Indtast en IP-adresse i subnettet \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Angiv varigheden af upstream-server timeout i sekunder\",\n  \"form_error_answer_format\": \"Ugyldigt svarformat\",\n  \"form_error_client_id_format\": \"KlientID må kun indeholde cifre, minuskler og bindestreger\",\n  \"form_error_domain_format\": \"Ugyldigt domæneformat\",\n  \"form_error_equal\": \"Må ikke svare til.\",\n  \"form_error_gateway_ip\": \"Lease kan ikke have gatewayens IP-adresse\",\n  \"form_error_ip4_format\": \"Ugyldig IPv4-adresse\",\n  \"form_error_ip4_gateway_format\": \"Ugyldig IPv4 gateway-adresse\",\n  \"form_error_ip6_format\": \"Ugyldig IPv6-adresse\",\n  \"form_error_ip_format\": \"Ugyldig IP-adresse\",\n  \"form_error_mac_format\": \"Ugyldig MAC-adresse\",\n  \"form_error_password\": \"Adgangskoder matcher ikke.\",\n  \"form_error_password_length\": \"Adgangskode skal udgøre fra {{min}} til {{max}} tegn\",\n  \"form_error_port\": \"Angiv gyldigt portnummer\",\n  \"form_error_port_range\": \"Angiv portnummer i intervallet 80-65535\",\n  \"form_error_port_unsafe\": \"Ikke-sikker port\",\n  \"form_error_positive\": \"Skal være større end 0\",\n  \"form_error_required\": \"Obligatorisk felt\",\n  \"form_error_server_name\": \"Ugyldigt servernavn\",\n  \"form_error_subnet\": \"Undernet \\\"{{cidr}}\\\" indeholder ikke IP-adressen \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Ugyldigt URL-format\",\n  \"form_error_url_or_path_format\": \"Ugyldig URL eller absolut sti til liste.\",\n  \"form_select_tags\": \"Vælg klient tags\",\n  \"found_in_known_domain_db\": \"Fundet i databasen med kendte domæner.\",\n  \"friday\": \"Fredag\",\n  \"friday_short\": \"Fre\",\n  \"gateway_or_subnet_invalid\": \"Ugyldig undernetmaske\",\n  \"general_settings\": \"Generelle indstillinger\",\n  \"general_statistics\": \"Generelle statistikker\",\n  \"get_started\": \"Komme I Gang\",\n  \"greater_range_start_error\": \"Skal være større end starten på ​​området\",\n  \"homepage\": \"Hjemmeside\",\n  \"host_whitelisted\": \"Værten er hvidlistet\",\n  \"ignore_domains\": \"Ignorerede domæner (adskilt af ny linje)\",\n  \"ignore_domains_desc_query\": \"Forespørgsler, der matcher disse regler, skrives ikke til forespørgselsloggen\",\n  \"ignore_domains_desc_stats\": \"Forespørgsler, der matcher disse regler, skrives ikke til statistikken\",\n  \"ignore_domains_title\": \"Ignorerede domæner\",\n  \"ignore_query_log\": \"Ignorér denne klient i forespørgselslog\",\n  \"ignore_statistics\": \"Ignorér denne klient i statistik\",\n  \"install_auth_confirm\": \"Bekræft adgangskode\",\n  \"install_auth_desc\": \"Adgangskodegodkendelse på din AdGuard Home admin-webflade skal opsættes. Selv hvis AdGuard Home kun er tilgængelig på lokalnetværket, er beskyttelse mod uautoriseret og ubegrænset adgang stadig vigtig.\",\n  \"install_auth_password\": \"Adgangskode\",\n  \"install_auth_password_enter\": \"Angiv adgangskode\",\n  \"install_auth_title\": \"Godkendelse\",\n  \"install_auth_username\": \"Brugernavn\",\n  \"install_auth_username_enter\": \"Angiv brugernavn\",\n  \"install_devices_address\": \"AdGuard Home DNS-server lytter på flg. adresser\",\n  \"install_devices_android_list_1\": \"Tryk på Indstillinger på Android-startskærmen.\",\n  \"install_devices_android_list_2\": \"Tryk på Wi-Fi i menuen. Alle tilgængelige netværk vises på skærmen (det er umuligt at angive tilpasset DNS til mobilforbindelse).\",\n  \"install_devices_android_list_3\": \"Langt tryk på det netværk, du er forbundet til, og tryk på Redigér Netværk.\",\n  \"install_devices_android_list_4\": \"På visse enheder skal du muligvis afkrydse feltet Avanceret for at se yderligere indstillinger. For at ændre dine Android DNS-indstillinger skal du skifte IP-indstillingerne fra DHCP til Statisk.\",\n  \"install_devices_android_list_5\": \"Skift de aktuelle DNS 1- og DNS 2-værdier til dine AdGuard Home-serveradresser.\",\n  \"install_devices_desc\": \"For brug af AdGuard Home, skal dine enheder opsættes til at bruge den.\",\n  \"install_devices_ios_list_1\": \"Tryk på Indstillinger på Hjem-skærmen.\",\n  \"install_devices_ios_list_2\": \"Vælg Wi-Fi i menuen til venstre (det er umuligt at opsætte DNS for mobilnetværker).\",\n  \"install_devices_ios_list_3\": \"Tryk på navnet for det aktuelt aktive netværk.\",\n  \"install_devices_ios_list_4\": \"Angiv dine AdGuard Home-serveradresser i DNS-feltet.\",\n  \"install_devices_macos_list_1\": \"Klik på Apple-ikonet og gå til Systempræferencer.\",\n  \"install_devices_macos_list_2\": \"Klik på Netværk.\",\n  \"install_devices_macos_list_3\": \"Vælg den første forbindelse på din liste, og klik på Avanceret.\",\n  \"install_devices_macos_list_4\": \"Vælg fanen DNS og angiv dine AdGuard Home-serveradresser.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Denne opsætning dækker automatisk alle enheder tilsluttet din hjemmerouter, ingen manuel opsætning af nogen enhed nødvendig.\",\n  \"install_devices_router_list_1\": \"Åbn præferencerne for din router. Normalt kan du tilgå disse fra din browser via en URL såsom http://192.168.0.1/ eller http://192.168.1.1/. Du anmodes muligvis om at angive en adgangskode. Kan du ikke huske den, kan du ofte nulstille adgangskoden ved hjælp af en knap på selve routeren, men vær opmærksom på, at vælges denne procedure, mister du sandsynligvis hele routerkonfigureringen. Hvis din router kræver en app for at konfigurere den, skal du installere appen på din telefon eller pc og bruge den til at få adgang til routerens indstillinger.\",\n  \"install_devices_router_list_2\": \"Find DHCP-/DNS-indstillingerne. Kig efter DNS-bogstaverne ved siden af et felt, der tillader input af to eller tre sæt tal, hver opdelt i fire grupper med et til tre cifre.\",\n  \"install_devices_router_list_3\": \"Angiv dine AdGuard Home-serveradresser dér.\",\n  \"install_devices_router_list_4\": \"På visse routertyper kan en tilpasset DNS-server ikke opsættes. I så tilfælde kan det hjælpe, hvis du opsætter AdGuard Home som en <0>DHCP-server</0>. Ellers bør du tjekke i routermanualen, hvordan du tilpasser DNS-servere i din givne routermodel.\",\n  \"install_devices_title\": \"Opsæt dine enheder\",\n  \"install_devices_windows_list_1\": \"Åbn Kontrolpanel via menuen Start eller Windows-søgning.\",\n  \"install_devices_windows_list_2\": \"Gå til kategorien Netværk og Internet og derefter til Netværks- og delingscenter.\",\n  \"install_devices_windows_list_3\": \"Find og klik på \\\"Skift adapterindstillinger\\\" i venstre panel.\",\n  \"install_devices_windows_list_4\": \"Højreklik på den aktive forbindelse, og vælg Egenskaber.\",\n  \"install_devices_windows_list_5\": \"Find \\\"Internet Protocol Version 4 (TCP/IPv4)\\\" (eller for IPv6, \\\"Internet Protocol Version 6 (TCP/IPv6)\\\") på listen, vælg den og klik derefter på Egenskaber igen.\",\n  \"install_devices_windows_list_6\": \"Vælg \\\"Brug følgende DNS-serveradresser og angiv dine AdGuard Home-serveradresser.\",\n  \"install_saved\": \"Gemt\",\n  \"install_settings_all_interfaces\": \"Alle grænseflader\",\n  \"install_settings_dns\": \"DNS-server\",\n  \"install_settings_dns_desc\": \"Du skal opsætte dine enheder eller router til at bruge DNS-serveren på flg. adresser:\",\n  \"install_settings_interface_link\": \"Din AdGuard Home admin webgrænseflade vil være tilgængelig på flg. adresser:\",\n  \"install_settings_listen\": \"Overvågningsgrænseflade\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Admin Webgrænseflade\",\n  \"install_static_configure\": \"AsGuard Home har registreret, at den dynamisk IP-adresse <0>{{ip}}</0> bruges. Opsæt denne som din statiske adresse?\",\n  \"install_static_error\": \"AdGuard Home kan ikke opsætte den automatisk for denne netværksgrænseflade. Søg information om, hvordan dette gøres manuelt.\",\n  \"install_static_ok\": \"Gode nyheder! Den statiske IP-adresse er allerede opsat\",\n  \"install_step\": \"Trin\",\n  \"install_submit_desc\": \"Opsætningsproceduren er fuldført, og du nu er klar til at begynde at bruge AdGuard Home.\",\n  \"install_submit_title\": \"Tillykke!\",\n  \"install_welcome_desc\": \"AdGuard Home er en netværksbaseret tracker- og adblocking DNS-server, hvis formål er at lade dig kontrollere hele dit netværk og alle dine enheder, og det kræver ikke brug af klientsoftware.\",\n  \"install_welcome_title\": \"Velkommen til AdGuard Home!\",\n  \"interval_24_hour\": \"24 timer\",\n  \"interval_6_hour\": \"6 timer\",\n  \"interval_days\": \"{{count}} dag\",\n  \"interval_days_plural\": \"{{count}} dage\",\n  \"interval_hours\": \"{{count}} time\",\n  \"interval_hours_plural\": \"{{count}} timer\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP-adresse\",\n  \"known_tracker\": \"Kendt tracker\",\n  \"last_rule_in_allowlist\": \"Kan ikke afvise denne klient, da udelukkelse af reglen \\\"{{disallowed_rule}}\\\" DEAKTIVERER listen \\\"Tilladte klienter\\\".\",\n  \"last_time_updated_table_header\": \"Senest opdateret\",\n  \"list_confirm_delete\": \"Sikker på, at du vil slette denne liste?\",\n  \"list_label\": \"Liste\",\n  \"list_updated\": \"{{count}} liste opdateret\",\n  \"list_updated_plural\": \"{{count}} lister opdateret\",\n  \"list_url_table_header\": \"Liste-URL\",\n  \"load_balancing\": \"Belastningsfordeling\",\n  \"load_balancing_desc\": \"Forespørg én upstream-server ad gangen.<br/>AdGuard Home bruger en vægtet tilfældighedsalgoritme til vælg af servere med det laveste antal fejlslagne opslag og den laveste gennemsnitlige opslagstid.\",\n  \"loading_table_status\": \"Indlæser...\",\n  \"local_ptr_default_resolver\": \"AdGuard Home bruger som standard flg. reverse DNS-opløsere: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS-serverne brugt af AdGuard Home til private PTR-, SOA- og NS-forespørgsler. En forespørgsel anses som privat, hvis den omhandler et ARPA-domæne indeholdende et undernet i et privat IP-områder, (såsom \\\"192.168.12.34\\\") og kommer fra en klient med en privat adresse. Hvis ikke opsat, bruger AdGuard Home OS'ets adresser på standard DNS-opløserne, bortset fra AdGuard Home-adresserne.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home kunne ikke fastslå egnede private reverse DNS-opløsere for dette system.\",\n  \"local_ptr_placeholder\": \"Angiv én IP-adresse pr. linje\",\n  \"local_ptr_title\": \"Private reverse DNS-servere\",\n  \"location\": \"Placering\",\n  \"log_and_stats_section_label\": \"Forespørgselslog og statistik\",\n  \"lower_range_start_error\": \"Skal være mindre end starten på området\",\n  \"main_settings\": \"Hovedindstillinger\",\n  \"make_static\": \"Gør statisk\",\n  \"manual_update\": \"<a>Følg disse trin</a> for at opdatere manuelt.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Mandag\",\n  \"monday_short\": \"Man\",\n  \"name\": \"Navn\",\n  \"name_table_header\": \"Navn\",\n  \"netname\": \"Netværksnavn\",\n  \"network\": \"Netværk\",\n  \"new_allowlist\": \"Ny hvidliste\",\n  \"new_blocklist\": \"Ny sortliste\",\n  \"next\": \"Næste\",\n  \"next_btn\": \"Næste\",\n  \"no_blocklist_added\": \"Ingen sortlister tilføjet\",\n  \"no_clients_found\": \"Ingen klienter fundet\",\n  \"no_domains_found\": \"Ingen domæner fundet\",\n  \"no_logs_found\": \"Ingen logger fundet\",\n  \"no_servers_specified\": \"Ingen servere angivet\",\n  \"no_upstreams_data_found\": \"Ingen upstreams-data fundet\",\n  \"no_whitelist_added\": \"Ingen hvidlister tilføjet\",\n  \"nothing_found\": \"Intet blev fundet\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Antallet af DNS-forespørgsler blokeret af adblockfiltre og værtssortlister\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Antallet af blokerede voksenwebsteder\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Antallet af DNS-forespørgsler blokeret af AdGuards browsingsikkerhedsmodul\",\n  \"number_of_dns_query_days\": \"Antallet af DNS-forespørgsler behandlet den seneste {{count}} dag\",\n  \"number_of_dns_query_days_plural\": \"Antallet af DNS-forespørgsler behandlet de seneste {{count}} dage\",\n  \"number_of_dns_query_hours\": \"Antallet af DNS-forespørgsler behandlet den seneste {{count}} time\",\n  \"number_of_dns_query_hours_plural\": \"Antallet af DNS-forespørgsler behandlet de seneste {{count}} timer\",\n  \"number_of_dns_query_to_safe_search\": \"Antallet af DNS-forespørgsler til søgemaskiner, hvor Sikker Søgning blev håndhævet\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"FRA\",\n  \"on\": \"TIL\",\n  \"open_dashboard\": \"Åbn Dashboard\",\n  \"orgname\": \"Organisationsnavn\",\n  \"original_response\": \"Oprindeligt svar\",\n  \"out_of_range_error\": \"Skal være uden for området \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Side\",\n  \"parallel_requests\": \"Parallelle forespørgsler\",\n  \"parental_control\": \"Forældrekontrol\",\n  \"password_label\": \"Adgangskode\",\n  \"password_placeholder\": \"Angiv adgangskode\",\n  \"plain_dns\": \"Almindelig DNS\",\n  \"port_53_faq_link\": \"Port 53 optages ofte af \\\"DNSStubListener\\\" eller \\\"systemd-resolved\\\" tjenester. Læs <0>denne instruktion</0> om, hvordan du løser dette.\",\n  \"previous_btn\": \"Foregående\",\n  \"privacy_policy\": \"Fortrolighedspolitik\",\n  \"processing_update\": \"Vent venligst, AdGuard Home bliver opdateret\",\n  \"protection_section_label\": \"Beskyttelse\",\n  \"protocol\": \"Protokol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Forespørgselslog\",\n  \"query_log_clear\": \"Ryd forespørgselslogfiler\",\n  \"query_log_cleared\": \"Forespørgselsloggen er blevet ryddet\",\n  \"query_log_configuration\": \"Opsætning af logger\",\n  \"query_log_confirm_clear\": \"Sikker på, at du vil rydde hele forespørgselsloggen?\",\n  \"query_log_disabled\": \"Forespørgselsloggen er deaktiveret og kan opsættes i <0>indstillingerne</0>\",\n  \"query_log_enable\": \"Aktivér log\",\n  \"query_log_filtered\": \"Filtreret af {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotation af forespørgselslog\",\n  \"query_log_retention_confirm\": \"Sikker på, at forespørgselsloggens rotationstid skal ændres? Mindskes intervalværdien, mistes nogle data\",\n  \"query_log_strict_search\": \"Brug dobbelt anførselstegn til stringent søgning\",\n  \"query_log_updated\": \"Forespørgselsloggen er blevet opdateret\",\n  \"rate_limit\": \"Hyppighedsgrænse\",\n  \"rate_limit_desc\": \"Antallet af forespørgsler pr. sekund tilladt pr. klient (værdien 0 = ubegrænset)\",\n  \"rate_limit_subnet_len_ipv4\": \"Længde på undernetpræfiks for IPv4-adresser\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Længde på undernetpræfiks for IPv4-adresser til hastighedsbegrænsning. Standard er 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Længden på IPv4-undernetpræfiks skal være mellem 0 og 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Længde på undernetpræfiks for IPv6-adresser\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Længde på undernetpræfiks for IPv6-adresser til hastighedsbegrænsning. Standard er 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Længden på IPv6-undernetpræfiks skal være mellem 0 og 128\",\n  \"rate_limit_whitelist\": \"Hvidliste til hastighedsbegrænsning\",\n  \"rate_limit_whitelist_desc\": \"IP-adresser undtaget fra hastighedsbegrænsning\",\n  \"rate_limit_whitelist_placeholder\": \"Angiv én IP-adresse pr. linje\",\n  \"refresh_btn\": \"Opdatér\",\n  \"refresh_statics\": \"Opdatér statistikerne\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Anmeld et problem\",\n  \"request_details\": \"Anmod om detaljer\",\n  \"request_table_header\": \"Forespørgsel\",\n  \"requests_count\": \"Antal forespørgsler\",\n  \"reset_settings\": \"Nulstil indstillinger\",\n  \"resolve_clients_desc\": \"Opløs klienters IP-adresser reverseret til deres værtsnavne ved at sende PTR-forespørgsler til korresponderende opløsere (private DNS-servere til lokale klienter, upstream-servere til klienter med offentlige IP-adresser).\",\n  \"resolve_clients_title\": \"Aktivér omvendt løsning af klienters IP-adresser\",\n  \"response_code\": \"Responskode\",\n  \"response_details\": \"Svardetaljer\",\n  \"response_table_header\": \"Svar\",\n  \"response_time\": \"Responstid\",\n  \"rewrite_A\": \"<0>A</0>: Særlig værdi, hold <0>A</0> poster fra upstream\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: Særlig værdi, hold <0>AAAA</0> poster fra upstream\",\n  \"rewrite_add\": \"Tilføj DNS-omskrivning\",\n  \"rewrite_added\": \"DNS-omskrivning for \\\"{{key}}\\\" blev tilføjet\",\n  \"rewrite_applied\": \"Omskrivningsregel effektueret\",\n  \"rewrite_confirm_delete\": \"Sikker på, at du vil slette DNS-omskrivning for \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"DNS-omskrivning for \\\"{{key}}\\\" blev slettet\",\n  \"rewrite_desc\": \"Gør det nemt at opsætte det tilpassede DNS-svar for et specifikt domænenavn.\",\n  \"rewrite_domain_name\": \"Domænenavn: Tilføj en CNAME-post\",\n  \"rewrite_edit\": \"Redigér DNS-omskrivning\",\n  \"rewrite_hosts_applied\": \"Omskrevet af værtsfilreglen\",\n  \"rewrite_ip_address\": \"IP-adresse: Brug denne IP i et A- eller AAAA-svar\",\n  \"rewrite_not_found\": \"Ingen DNS-omskrivninger fundet\",\n  \"rewrite_settings_updated\": \"DNS-omskrivningsindstilinger opdateret\",\n  \"rewrite_updated\": \"DNS-omskrivning hermed opdateret\",\n  \"rewrites_disabled_table_header\": \"Omskrivninger er slået fra\",\n  \"rewrites_enabled_table_header\": \"Omskrivninger er slået til\",\n  \"rewritten\": \"Omskrevet\",\n  \"rows_table_footer_text\": \"rækker\",\n  \"rule_added_to_custom_filtering_toast\": \"Regel føjet til de tilpassede filtreringsregler: {{rule}}\",\n  \"rule_label\": \"Regel/Regler\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regel fjernet fra de tilpassede filtreringsregler: {{rule}}\",\n  \"rules_count_table_header\": \"Antal regler\",\n  \"safe_browsing\": \"Sikker Browsing\",\n  \"safe_search\": \"Sikker søgning\",\n  \"saturday\": \"Lørdag\",\n  \"saturday_short\": \"Lør\",\n  \"save_btn\": \"Gem\",\n  \"save_config\": \"Gem opsætning\",\n  \"schedule_add\": \"Tilføj tidsplan\",\n  \"schedule_current_timezone\": \"Aktuel tidszone: {{value}}\",\n  \"schedule_desc\": \"Sæt inaktivitetsperioder for blokerede tjenester\",\n  \"schedule_edit\": \"Redigér tidsplan\",\n  \"schedule_from\": \"Fra\",\n  \"schedule_invalid_select\": \"Starttidspunkt skal være før sluttidspunkt\",\n  \"schedule_modal_description\": \"Denne tidsplan vil erstatte alle eksisterende tidsplaner for den samme ugedag. Hver ugedag kan kun have én inaktivitetsperiode.\",\n  \"schedule_modal_time_off\": \"Ingen tjenesteblokering:\",\n  \"schedule_new\": \"Ny tidsplan\",\n  \"schedule_remove\": \"Fjern tidsplan\",\n  \"schedule_save\": \"Gem tidsplan\",\n  \"schedule_select_days\": \"Vælg dage\",\n  \"schedule_services\": \"Pausering af tjenesteblokering\",\n  \"schedule_services_desc\": \"Opsæt pauseringstidsplan for det tjenesteblokerende filter\",\n  \"schedule_services_desc_client\": \"Opsæt pauseringstidsplan for det tjenesteblokerende filter for denne klient\",\n  \"schedule_time_all_day\": \"Hele dagen\",\n  \"schedule_timezone\": \"Vælg tidszone\",\n  \"schedule_to\": \"Til\",\n  \"served_from_cache_label\": \"Leveret fra cache\",\n  \"service_name\": \"Tjenestenavn\",\n  \"set_static_ip\": \"Opsæt en statisk IP-adresse\",\n  \"settings\": \"Indstillinger\",\n  \"settings_custom\": \"Tilpasset\",\n  \"settings_global\": \"Global\",\n  \"setup_config_to_enable_dhcp_server\": \"Opsæt indstillinger for at aktivere DHCP-server\",\n  \"setup_dns_notice\": \"For at kunne bruge <1>DNS-over-HTTPS</1> eller <1>DNS-over-TLS</1>, skal du <0>opsætte Krypteringen</0> i AdGuard Homes indstillinger.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Brug <1>{{address}}</1> streng.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Brug <1>{{address}}</1> streng.\",\n  \"setup_dns_privacy_3\": \"<0>Her er en liste over software, du kan bruge.</0>\",\n  \"setup_dns_privacy_4\": \"På en iOS 14- eller macOS Big Sur-enhed kan du downloade en særlig '.mobileconfig' -fil, der føjer <highlight>DNS-over-HTTPS</highlight> eller <highlight>DNS-over-TLS</highlight> servere til DNS-indstillingerne.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 har indbygget understøttelse af DNS-over-TLS. For at opsætte den, gå til Indstillinger → Netværk og Internet → Avanceret → Privat DNS og angiv dit domænenavn.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard til Android</0> understøtter <1>DNS-over-HTTPS</1> og <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> føjer <1>DNS-over-HTTPS</1> understøttelse til Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS- og macOS-opsætning\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> understøtter <1>DNS-over-HTTPS</1>, men for at opsætte den til brug af din egen server, skal du generere et <2>DNS Stamp</2> til den.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard til iOS</0> understøtter <1>DNS-over-HTTPS</1> og <1>DNS-over-TLS</1> opsætning.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home kan være en sikker DNS-klient på enhver platform.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> understøtter alle kendte sikre DNS-protokoller.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> understøtter <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> understøtter <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Du kan finde flere implementeringer <0>hér</0> og <1>hér</1>.\",\n  \"setup_dns_privacy_other_title\": \"Andre implementeringer\",\n  \"setup_guide\": \"Installationsvejledning\",\n  \"show_all_filter_type\": \"Vis alle\",\n  \"show_blocked_responses\": \"Blokeret\",\n  \"show_filtered_type\": \"Vis filtrerede\",\n  \"show_processed_responses\": \"Behandlet\",\n  \"show_whitelisted_responses\": \"Hvidlistet\",\n  \"sign_in\": \"Log ind\",\n  \"sign_out\": \"Log ud\",\n  \"source_label\": \"Kilde\",\n  \"static_ip\": \"Statisk IP-adresse\",\n  \"static_ip_desc\": \"AdGuard Home er en server, så den behøver en statisk IP-adresse for at fungere korrekt, da din router ellers på et tidspunkt vil kunne tildele en anden IP-adresse til denne enhed.\",\n  \"statistics_clear\": \"Ryd statistikker\",\n  \"statistics_clear_confirm\": \"Sikker på, at du vil slette statistikkerne?\",\n  \"statistics_cleared\": \"Statistikkerne er ryddet\",\n  \"statistics_configuration\": \"Statistikopsætning\",\n  \"statistics_enable\": \"Aktivér statistikker\",\n  \"statistics_retention\": \"Statistikbevarelse\",\n  \"statistics_retention_confirm\": \"Sikker på, at du vil ændre på statistikbevaring? Mindskes intervalværdien, vil nogle data gå tabt\",\n  \"statistics_retention_desc\": \"Mindskes intervalværdien, vil nogle data gå tabt\",\n  \"stats_adult\": \"Blokerede voksne websteder\",\n  \"stats_disabled\": \"Statistikker er deaktiveret. De kan aktiveres via <0>indstillingssiden</0>.\",\n  \"stats_disabled_short\": \"Statistikker er deaktiveret\",\n  \"stats_malware_phishing\": \"Blokeret malware/phishing\",\n  \"stats_params\": \"Statistikopsætning\",\n  \"stats_query_domain\": \"Mest forespurgte domæner\",\n  \"subnet_error\": \"Adresser ska være i ét undernet\",\n  \"sunday\": \"Søndag\",\n  \"sunday_short\": \"Søn\",\n  \"system_host_files\": \"System hosts-filer\",\n  \"table_client\": \"Klient\",\n  \"table_name\": \"Navn\",\n  \"tags_desc\": \"Der kan vælges tags, som svarer til klienten. Medtag tags i filtreringsregler for at anvende dem mere præcist. <0>Læs mere</0>.\",\n  \"tags_title\": \"Tags\",\n  \"test_upstream_btn\": \"Test upstreams\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Auto (baseret på enhedens farveskema)\",\n  \"theme_dark\": \"Mørkt\",\n  \"theme_dark_desc\": \"Mørkt tema\",\n  \"theme_light\": \"Lyst\",\n  \"theme_light_desc\": \"Lyst tema\",\n  \"thursday\": \"Torsdag\",\n  \"thursday_short\": \"Tors\",\n  \"time_table_header\": \"Tid\",\n  \"top_blocked_domains\": \"Hyppigst blokerede domæner\",\n  \"top_clients\": \"Hyppigste klienter\",\n  \"top_upstreams\": \"Top-upstreams\",\n  \"topline_expired_certificate\": \"Dit SSL-certifikat er udløbet. Opdatér <0>Krypteringsindstillinger</0>.\",\n  \"topline_expiring_certificate\": \"Dit SSL-certifikat er ved at udløbe. Opdatér <0>Krypteringsindstillinger</0>.\",\n  \"tracker_source\": \"Tracker-kilde\",\n  \"try_again\": \"Prøv igen\",\n  \"ttl_cache_validation\": \"Minimum cache TTL-værdi skal være mindre end eller lig med den maksimale værdi\",\n  \"tuesday\": \"Tirsdag\",\n  \"tuesday_short\": \"Tirs\",\n  \"type_table_header\": \"Type\",\n  \"unavailable_dhcp\": \"DHCP utilgængelig\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home kan ikke køre en DHCP-server i dit OS\",\n  \"unblock\": \"Afblokering\",\n  \"unblock_all\": \"Afblokér alle\",\n  \"unblock_for_this_client_only\": \"Afblokér kun for denne klient\",\n  \"unknown_filter\": \"Ukendt filter {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} er nu tilgængelig! <0>Kik hér</0> for mere info.\",\n  \"update_failed\": \"Autoopdatering mislykkedes. Følg <a>disse trin</a> for at opdatere manuelt.\",\n  \"update_now\": \"Opdatér nu\",\n  \"updated_custom_filtering_toast\": \"Tilpassede regler er gemt\",\n  \"updated_save_search_toast\": \"Sikker søgning opdateret\",\n  \"updated_upstream_dns_toast\": \"Upstream-servere er gemt\",\n  \"updates_checked\": \"En ny version af AdGuard Home er tilgængelig\\n\",\n  \"updates_version_equal\": \"AdGuard Home er opdateret\",\n  \"upstream\": \"Upstream\",\n  \"upstream_dns\": \"Upstream DNS-servere\",\n  \"upstream_dns_cache_configuration\": \"Upstream DNS-cacheopsætning\",\n  \"upstream_dns_client_desc\": \"Holdes dette felt tomt, bruger AdGuard Home de i <0>DNS-indstillingerne</0> opsatte servere.\",\n  \"upstream_dns_configured_in_file\": \"Opsat i {{path}}\",\n  \"upstream_dns_help\": \"Angiv én serveradresse pr. linje. <a>Læs mere</a> om opsætning af upstream DNS-servere.\",\n  \"upstream_parallel\": \"Brug parallelforespørgsler til at accelerere fortolkningen ved at forespørge alle upstream-servere samtidigt.\",\n  \"upstream_timeout\": \"Upstream-timeout\",\n  \"upstream_timeout_desc\": \"Angiver antallet af sekunder, der skal ventes på et svar fra upstream-serveren\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Brug AdGuards webtjeneste til browsingsikkerhed\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home vil tjekke, om domænet er blokeret af browsingsikkerhedswebtjenesten. Den bruger en fortrolighedsvenlig opslags-API til at udføre tjekket: Kun et kort præfiks af domænenavns SHA256-hash'en sendes til serveren.\",\n  \"use_adguard_parental\": \"Brug AdGuards forældrekontrolwebtjeneste\",\n  \"use_adguard_parental_hint\": \"AdGuard Home vil tjekke, om domænet indeholder voksenindhold vha. den samme fortrolighedsvenlige API som browsingsikkerhedswebtjenesten.\",\n  \"use_private_ptr_resolvers_desc\": \"Opløs PTR-, SOA- og NS-forespørgsler til ARPA-domæner indeholdende private adresser vha. private upstream-servere, DHCP, /etc/hosts mv. Hvis deaktiveret, besvarer AdGuard Home sådanne forespørgsler med NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Brug private reverse DNS-opløsere\",\n  \"use_saved_key\": \"Brug den tidligere gemte nøgle\",\n  \"username_label\": \"Brugernavn\",\n  \"username_placeholder\": \"Angiv brugernavn\",\n  \"validated_with_dnssec\": \"Valideret med DNSSEC\",\n  \"version\": \"Version\",\n  \"version_request_error\": \"Opdateringstjek mislykkedes. Tjek internetforbindelsen.\",\n  \"wednesday\": \"Onsdag\",\n  \"wednesday_short\": \"Ons\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/de.json",
    "content": "{\n  \"access_allowed_desc\": \"Eine Liste von CIDRs, IP-Adressen oder <a>Client-IDs</a>. Wenn diese Liste gefüllt ist, akzeptiert AdGuard Home nur Anfragen von diesen Clients.\",\n  \"access_allowed_title\": \"Zugelassene Clients\",\n  \"access_blocked_desc\": \"Nicht zu verwechseln mit Filtern. AdGuard Home verwirft DNS-Anfragen, die mit diesen Domains übereinstimmen, und diese Abfragen werden nicht einmal im Abfrageprotokoll angezeigt. Sie können exakte Domainnamen, Wildcards oder URL-Filterregeln angeben, z. B. „example.org“, „*.example.org“ oder „||example.org^“.\",\n  \"access_blocked_title\": \"Nicht zugelassene Domains\",\n  \"access_desc\": \"Hier können Sie die Zugriffsregeln für den DNS-Server von AdGuard Home konfigurieren\",\n  \"access_disallowed_desc\": \"Eine Liste von CIDRs, IP-Adressen oder <a>ClientIDs</a>. Wenn diese Liste gefüllt ist, weist AdGuard Home Anfragen von diesen Clients zurück. Dieses Feld wird ignoriert, wenn es Einträge in der Liste „Zugelassene Clients“ gibt.\",\n  \"access_disallowed_title\": \"Nicht zugelassene Clients\",\n  \"access_settings_saved\": \"Zugriffseinstellungen erfolgreich gespeichert\",\n  \"access_title\": \"Zugriffsrechte\",\n  \"actions_table_header\": \"Aktionen\",\n  \"add_allowlist\": \"Zulassungsliste hinzufügen\",\n  \"add_blocklist\": \"Blockliste hinzufügen\",\n  \"add_custom_list\": \"Eigene Liste hinzufügen\",\n  \"add_persistent_client\": \"Als dauerhaften Client hinzufügen\",\n  \"address\": \"Adresse\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home wird alle DNS-Abfragen von diesem Client verwerfen.\",\n  \"all_lists_up_to_date_toast\": \"Alle Listen sind bereits auf dem neuesten Stand\",\n  \"all_queries\": \"Alle Anfragen\",\n  \"allow_this_client\": \"Diesen Client zulassen\",\n  \"allowed\": \"Zugelassen\",\n  \"anonymize_client_ip\": \"Client-IP anonymisieren\",\n  \"anonymize_client_ip_desc\": \"Vollständige IP-Adresse des Clients nicht in Protokollen und Statistiken speichern\",\n  \"anonymizer_notification\": \"<0>Hinweis:</0> Die IP-Anonymisierung ist aktiviert. Sie können sie in den <1>Allgemeinen Einstellungen</1> deaktivieren.\",\n  \"answer\": \"Antwort\",\n  \"apply_btn\": \"Anwenden\",\n  \"auto_clients_desc\": \"Informationen über IP-Adressen der Geräten, die AdGuard Home nutzen oder nutzen könnten. Diese Informationen werden aus verschiedenen Quellen gesammelt, darunter Hosts-Dateien, Reverse-DNS usw.\",\n  \"auto_clients_title\": \"Laufzeit-Clients\",\n  \"autofix_warning_list\": \"Es werden folgende Aufgaben ausgeführt: <0>Deaktivieren des DNSStubListener-Systems</0> <0>Festlegen der DNS-Server-Adresse auf 127.0.0.1</0> <0>Ersetzen des symbolischen Linkziels von /etc/resolv.conf auf /run/systemd/resolve/resolv.conf</0> <0>Anhalten des DNSStubListener (systemseitig aufgelöster Dienst wird nachladen)</0>\",\n  \"autofix_warning_result\": \"Als Folge daraus werden alle DNS-Anforderungen von Ihrem System standardmäßig von AdGuardHome verarbeitet.\",\n  \"autofix_warning_text\": \"Wenn Sie auf „Beheben“ klicken, konfiguriert AdGuardHome Ihr System für die Verwendung des AdGuardHome-DNS-Servers.\",\n  \"average_processing_time\": \"Durchschnittliche Bearbeitungsdauer\",\n  \"average_processing_time_hint\": \"Durchschnittliche Zeit in Millisekunden zur Bearbeitung von DNS-Anfragen\",\n  \"average_upstream_response_time\": \"Durchschnittliche Upstream-Antwortzeit\",\n  \"back\": \"Zurück\",\n  \"block\": \"Sperren\",\n  \"block_all\": \"Alle sperren\",\n  \"block_domain_use_filters_and_hosts\": \"Domains durch Filter und Host-Dateien sperren\",\n  \"block_for_this_client_only\": \"Nur für diesen Client sperren\",\n  \"block_services\": \"Bestimmte Dienste sperren\",\n  \"blocked_adult_websites\": \"Gesperrt durch Kindersicherung\",\n  \"blocked_by\": \"<0>Durch Filter gesperrt</0>\",\n  \"blocked_by_cname_or_ip\": \"Gesperrt durch CNAME oder IP\",\n  \"blocked_by_response\": \"Gesperrt nach Antwort von CNAME oder IP\",\n  \"blocked_response_ttl\": \"Gültigkeitsdauer der blockierten Antwort\",\n  \"blocked_response_ttl_desc\": \"Gibt an, wie viele Sekunden lang die Clients eine gefilterte Antwort zwischenspeichern sollen\",\n  \"blocked_safebrowsing\": \"Gesperrt durch Internetsicherheit\",\n  \"blocked_service\": \"Gesperrte Dienste\",\n  \"blocked_services\": \"Gesperrte Dienste\",\n  \"blocked_services_desc\": \"Ermöglicht das schnelle Sperren beliebter Websites und Dienste.\",\n  \"blocked_services_global\": \"Global gesperrte Dienste verwenden\",\n  \"blocked_services_saved\": \"Gesperrte Dienste erfolgreich gespeichert\",\n  \"blocked_threats\": \"Gesperrte Bedrohungen\",\n  \"blocking_ipv4\": \"IPv4-Sperren\",\n  \"blocking_ipv4_desc\": \"IP-Adresse, die für eine gesperrte A-Anfrage zurückgegeben werden soll\",\n  \"blocking_ipv6\": \"IPv6-Sperren\",\n  \"blocking_ipv6_desc\": \"IP-Adresse, die für eine gesperrte AAAA-Anfrage zurückgegeben werden soll\",\n  \"blocking_mode\": \"Sperrmodus\",\n  \"blocking_mode_custom_ip\": \"Benutzerdefinierte IP: Mit einer manuell eingestellten IP-Adresse antworten\",\n  \"blocking_mode_default\": \"Standard: Mit Null IP Adress (0.0.0.0 for A; :: for AAAA) antworten, wenn sie durch eine Regel im Adblock-Stil gesperrt sind; mit der in der Regel angegebenen IP-Adresse antworten, wenn sie durch eine Regel im /etc/hosts-Stil gesperrt wurde\",\n  \"blocking_mode_null_ip\": \"Null-IP: Antworten mit Null-IP-Adresse (0.0.0.0.0 für A; :: für AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Mit NXDOMAIN-Code antworten\",\n  \"blocking_mode_refused\": \"REFUSED: mit abgelehntem Code REFUSED\",\n  \"blocklist\": \"Sperrliste\",\n  \"bootstrap_dns\": \"Bootstrap-DNS-Server\",\n  \"bootstrap_dns_desc\": \"IP-Adressen der DNS-Server, die zum Auflösen der IP-Adressen von DoH/DoT Upstream-Servern verwendet werden, die Sie angegeben haben. Kommentare sind nicht erlaubt.\",\n  \"cache_cleared\": \"DNS-Cache erfolgreich geleert\",\n  \"cache_enabled\": \"Cache aktivieren\",\n  \"cache_enabled_desc\": \"DNS-Antworten lokal speichern.\",\n  \"cache_optimistic\": \"Optimistisches Caching\",\n  \"cache_optimistic_desc\": \"Sorgt dafür, dass AdGuard Home auch dann aus dem Cache antwortet, wenn die Einträge abgelaufen sind, und versucht zudem, diese zu aktualisieren.\",\n  \"cache_size\": \"Größe des Cache\",\n  \"cache_size_desc\": \"Größe des DNS-Caches (in Bytes).\",\n  \"cache_size_validation\": \"Die Cachegröße muss größer als Null sein, wenn diese Option aktiviert ist.\",\n  \"cache_ttl_max_override\": \"TTL-Höchstwert überschreiben\",\n  \"cache_ttl_max_override_desc\": \"Maximalen Time-to-Live-Wert (Sekunden) für Einträge im DNS-Cache festlegen.\",\n  \"cache_ttl_min_override\": \"TTL-Minimalwert überschreiben\",\n  \"cache_ttl_min_override_desc\": \"Kurze Time-to-Live-Werte (Sekunden) verlängern, die vom Upstream-Server beim Caching von DNS-Antworten empfangen werden.\",\n  \"cancel_btn\": \"Abbrechen\",\n  \"category_label\": \"Kategorie\",\n  \"check\": \"Prüfen\",\n  \"check_client_id\": \"Client-Kennung (ClientID oder IP-Adresse)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Prüfen, ob der Hostname gefiltert wird.\",\n  \"check_dhcp_servers\": \"Auf DHCP-Server prüfen\",\n  \"check_dns_record\": \"DNS-Datensatztyp auswählen\",\n  \"check_enter_client_id\": \"Client-Kennung eingeben\",\n  \"check_hostname\": \"Hostname oder Domainname\",\n  \"check_ip\": \"IP-Adressen: {{ip}}\",\n  \"check_not_found\": \"Nicht in Ihren Filterlisten enthalten\",\n  \"check_reason\": \"Grund: {{reason}}\",\n  \"check_service\": \"Dienstname: {{service}}\",\n  \"check_title\": \"Filterung überprüfen\",\n  \"check_updates_btn\": \"Nach Aktualisierungen suchen\",\n  \"check_updates_now\": \"Jetzt nach Aktualisierungen suchen\",\n  \"choose_allowlist\": \"Zulassungslisten auswählen\",\n  \"choose_blocklist\": \"Blocklisten auswählen\",\n  \"choose_from_list\": \"Aus Liste auswählen\",\n  \"city\": \"Stadt\",\n  \"clear_cache\": \"Cache leeren\",\n  \"click_to_view_queries\": \"Anklicken, um Abfragen anzuzeigen\",\n  \"client_add\": \"Client hinzufügen\",\n  \"client_added\": \"Client „{{key}}“ erfolgreich hinzugefügt\",\n  \"client_blocked\": \"Client „{{ip}}“ erfolgreich gesperrt\",\n  \"client_confirm_block\": \"Möchten Sie den Client „{{ip}}“ wirklich sperren?\",\n  \"client_confirm_delete\": \"Möchten Sie den Client „{{key}}“ wirklich löschen?\",\n  \"client_confirm_unblock\": \"Möchten Sie den Client „{{ip}}“ wirklich entsperren?\",\n  \"client_deleted\": \"Client „{{key}}“ erfolgreich entfernt\",\n  \"client_details\": \"Einzelheiten des Clients\",\n  \"client_edit\": \"Client bearbeiten\",\n  \"client_global_settings\": \"Allgemeine Einstellungen nutzen\",\n  \"client_id\": \"Client-ID\",\n  \"client_id_desc\": \"Verschiedene Clients können durch eine spezielle Client-ID identifiziert werden. <a>Hier</a> können Sie mehr darüber erfahren, wie Sie Clients identifizieren können.\",\n  \"client_id_placeholder\": \"Client-ID eingeben\",\n  \"client_identifier\": \"Bezeichner\",\n  \"client_identifier_desc\": \"Clients können durch die IP-Adresse, CIDR, MAC-Adresse oder eine spezielle Client-ID (können für DoT/DoH/DoQ verwendet werden) identifiziert werden. <0>Hier</0> erfahren Sie mehr darüber, wie Sie Kunden identifizieren.\",\n  \"client_name\": \"Client {{id}}\",\n  \"client_new\": \"Neuer Client\",\n  \"client_settings\": \"Client-Einstellungen\",\n  \"client_table_header\": \"Client\",\n  \"client_unblocked\": \"Client „{{ip}}“ erfolgreich entsperrt\",\n  \"client_updated\": \"Client „{{key}}“ erfolgreich aktualisiert\",\n  \"clients_desc\": \"Datensätze persistenter Clients für Geräte konfigurieren, die mit AdGuard Home verbunden sind\",\n  \"clients_not_found\": \"Keine Clients gefunden\",\n  \"clients_title\": \"Persistente Clients\",\n  \"compact\": \"Kompakt\",\n  \"config_successfully_saved\": \"Konfiguration erfolgreich gespeichert\",\n  \"configure\": \"Konfigurieren\",\n  \"confirm_dns_cache_clear\": \"Möchten Sie den DNS-Cache wirklich leeren?\",\n  \"confirm_static_ip\": \"AdGuard Home konfiguriert {{ip}} als Ihre feste IP-Adresse. Möchten Sie fortfahren?\",\n  \"copyright\": \"Urheberrecht\",\n  \"country\": \"Land\",\n  \"custom_filter_rules\": \"Benutzerdefinierte Filterregeln\",\n  \"custom_filter_rules_hint\": \"Geben Sie pro Zeile eine Regel ein. Sie können entweder Werbefilterregeln oder Host-Datei-Syntax verwenden.\",\n  \"custom_filtering_rules\": \"Benutzerdefinierte Filterregeln\",\n  \"custom_ip\": \"Benutzerdefinierte IP\",\n  \"custom_retention_input\": \"Rückhaltezeit in Stunden eingeben\",\n  \"custom_rotation_input\": \"Rotation in Stunden eingeben\",\n  \"dashboard\": \"Übersicht\",\n  \"date\": \"Datum\",\n  \"default\": \"Standard\",\n  \"delete_confirm\": \"Möchten Sie „{{key}}“ wirklich löschen?\",\n  \"delete_table_action\": \"Löschen\",\n  \"descr\": \"Beschreibung\",\n  \"details\": \"Details\",\n  \"dhcp_add_static_lease\": \"Statische Zuweisung hinzufügen\",\n  \"dhcp_config_saved\": \"DHCP-Konfiguration erfolgreich gespeichert\",\n  \"dhcp_description\": \"Wenn Ihr Router keine DHCP-Einstellungen bietet, können Sie den integrierten DHCP-Server von AdGuard verwenden.\",\n  \"dhcp_disable\": \"DHCP-Server deaktivieren\",\n  \"dhcp_dynamic_ip_found\": \"Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}</0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}</0>. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP-Server aktivieren“ klicken.\",\n  \"dhcp_edit_static_lease\": \"Statische Zuweisung bearbeiten\",\n  \"dhcp_enable\": \"DHCP-Server aktivieren\",\n  \"dhcp_error\": \"AdGuard Home konnte keinen anderen aktiven DHCP-Server im Netzwerk feststellen\",\n  \"dhcp_form_gateway_input\": \"Gateway-IP\",\n  \"dhcp_form_lease_input\": \"Dauer der Zuweisung\",\n  \"dhcp_form_lease_title\": \"DHCP-Zuweisungs-Dauer (in Sekunden)\",\n  \"dhcp_form_range_end\": \"Bereichsende\",\n  \"dhcp_form_range_start\": \"Bereichsbeginn\",\n  \"dhcp_form_range_title\": \"Bereich von IP-Adressen\",\n  \"dhcp_form_subnet_input\": \"Subnetz-Maske\",\n  \"dhcp_found\": \"Es wurde ein aktiver DHCP-Server im Netzwerk gefunden. Es wird nicht empfohlen, den integrierten DHCP-Server zu aktivieren.\",\n  \"dhcp_hardware_address\": \"Hardware-Adresse\",\n  \"dhcp_interface_select\": \"DHCP-Benutzeroberfläche auswählen\",\n  \"dhcp_ip_addresses\": \"IP-Adressen\",\n  \"dhcp_ipv4_settings\": \"DHCP-IPv4-Einstellungen\",\n  \"dhcp_ipv6_settings\": \"DHCP-IPv6-Einstellungen\",\n  \"dhcp_lease_added\": \"Statische Zuweisung „{{key}}“ erfolgreich hinzugefügt\",\n  \"dhcp_lease_deleted\": \"Statische Zuweisung „{{key}}“ erfolgreich entfernt\",\n  \"dhcp_lease_updated\": \"Statische Zuweisung „{{key}}“ erfolgreich aktualisiert\",\n  \"dhcp_leases\": \"DHCP-Zuweisungen\",\n  \"dhcp_leases_not_found\": \"Keine DHCP-Zuweisungen gefunden\",\n  \"dhcp_new_static_lease\": \"Neue statische Zuweisung\",\n  \"dhcp_not_found\": \"Es ist sicherer, den integrierten DHCP-Server zu aktivieren, da AdGuard Home keine aktiven DHCP-Server im Netzwerk vorgefunden hat. Sie sollten dies jedoch noch einmal manuell überprüfen, da die automatische Überprüfung derzeit keine 100%ige Garantie bietet.\",\n  \"dhcp_reset\": \"Möchten Sie die DHCP-Konfiguration wirklich zurücksetzen?\",\n  \"dhcp_reset_leases\": \"Alle Zuweisungen zurücksetzen\",\n  \"dhcp_reset_leases_confirm\": \"Möchten Sie wirklich alle Zuweisungen zurücksetzen?\",\n  \"dhcp_reset_leases_success\": \"DHCP-Zuweisungen erfolgreich zurückgesetzt\",\n  \"dhcp_settings\": \"DHCP-Einstellungen\",\n  \"dhcp_static_ip_error\": \"Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Es konnte nicht ermittelt werden, ob diese Netzwerkschnittstelle mit statischer IP-Adresse konfiguriert ist. Bitte legen Sie eine statische IP-Adresse manuell fest.\",\n  \"dhcp_static_leases\": \"DHCP statische Zuweisungen\",\n  \"dhcp_static_leases_not_found\": \"Keine statischen DHCP-Zuweisungen gefunden\",\n  \"dhcp_table_expires\": \"Gültig bis\",\n  \"dhcp_table_hostname\": \"Hostname\",\n  \"dhcp_title\": \"DHCP-Server (experimental!)\",\n  \"dhcp_warning\": \"Wenn Sie den DHCP-Server trotzdem aktivieren möchten, stellen Sie sicher, dass sich in Ihrem Netzwerk kein anderer aktiver DHCP-Server befindet. Andernfalls kann es bei angeschlossenen Geräten zu einem Ausfall des Internets kommen!\",\n  \"disable_for_hours\": \"Für {{count}} Stunde\",\n  \"disable_for_hours_plural\": \"Für {{count}} Stunden\",\n  \"disable_for_minutes\": \"Für {{count}} Minute\",\n  \"disable_for_minutes_plural\": \"Für {{count}} Minuten\",\n  \"disable_for_seconds\": \"Für {{count}} Sekunde\",\n  \"disable_for_seconds_plural\": \"Für {{count}} Sekunden\",\n  \"disable_ipv6\": \"IPv6 deaktivieren\",\n  \"disable_ipv6_desc\": \"Alle DNS-Anfragen für IPv6-Adressen (Typ AAAA) verwerfen und IPv6-Hinweise aus HTTPS-Antworten entfernen.\",\n  \"disable_notify_for_hours\": \"Schutz für {{count}} Stunde deaktivieren\",\n  \"disable_notify_for_hours_plural\": \"Schutz für {{count}} Stunden deaktivieren\",\n  \"disable_notify_for_minutes\": \"Schutz für {{count}} Minute deaktivieren\",\n  \"disable_notify_for_minutes_plural\": \"Schutz für {{count}} Minuten deaktivieren\",\n  \"disable_notify_for_seconds\": \"Schutz für {{count}} Sekunde deaktivieren\",\n  \"disable_notify_for_seconds_plural\": \"Schutz für {{count}} Sekunden deaktivieren\",\n  \"disable_notify_until_tomorrow\": \"Schutz bis morgen deaktivieren\",\n  \"disable_protection\": \"Schutz deaktivieren\",\n  \"disable_rewrites\": \"Umschreibregeln deaktivieren\",\n  \"disable_until_tomorrow\": \"Bis morgen\",\n  \"disabled\": \"Deaktiviert\",\n  \"disabled_dhcp\": \"DHCP-Server deaktiviert\",\n  \"disabled_filtering_toast\": \"Filtern deaktiviert\",\n  \"disabled_parental_toast\": \"Kindersicherung deaktiviert\",\n  \"disabled_protection\": \"Schutz deaktiviert\",\n  \"disabled_safe_browsing_toast\": \"Internetsicherheit deaktiviert\",\n  \"disabled_safe_search_toast\": \"Sichere Suche deaktiviert\",\n  \"disallow_this_client\": \"Diesen Client sperren\",\n  \"dns_addresses\": \"DNS-Adressen\",\n  \"dns_allowlists\": \"DNS-Zulassungslisten\",\n  \"dns_allowlists_desc\": \"Domains aus DNS-Positivlisten werden auch dann zugelassen, wenn sie in einer der Sperrlisten enthalten sind.\",\n  \"dns_blocklists\": \"DNS-Blocklisten\",\n  \"dns_blocklists_desc\": \"AdGuard Home sperrt Domains, die in den Sperrlisten enthalten sind.\",\n  \"dns_cache_config\": \"Konfiguration des DNS-Cache\",\n  \"dns_cache_config_desc\": \"Hier können Sie den DNS-Cache konfigurieren\",\n  \"dns_cache_size\": \"Größe des DNS-Cache, in Bytes\",\n  \"dns_config\": \"DNS-Serverkonfiguration\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS-Datenschutz\",\n  \"dns_providers\": \"Hier finden Sie eine <0>Liste der bekannten DNS-Anbieter</0> zur Auswahl.\",\n  \"dns_query\": \"DNS-Anfragen\",\n  \"dns_rewrites\": \"DNS-Umschreibungen\",\n  \"dns_settings\": \"DNS-Einstellungen\",\n  \"dns_start\": \"DNS-Server wird gestartet\",\n  \"dns_status_error\": \"Fehler bei Statusabfrage des DNS-Server\",\n  \"dns_test_not_ok_toast\": \"Server „{{key}}“: konnte nicht verwendet werden, bitte überprüfen Sie die korrekte Schreibweise\",\n  \"dns_test_ok_toast\": \"Angegebene DNS-Server arbeiten ordnungsgemäß\",\n  \"dns_test_parsing_error_toast\": \"Abschnitt {{section}}: Zeile {{line}}: konnte nicht verwendet werden, bitte überprüfen Sie, ob alles richtig geschrieben ist\",\n  \"dns_test_warning_toast\": \"Upstream „{{key}}“ reagiert nicht auf Testanfragen und funktioniert möglicherweise nicht fehlerfrei\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"DNSSEC aktivieren\",\n  \"dnssec_enable_desc\": \"DNSSEC-Flag in den ausgehenden DNS-Abfragen mitsenden und das Ergebnis überprüfen (DNSSEC-fähiger Resolver erforderlich)\",\n  \"domain\": \"Domain\",\n  \"domain_desc\": \"Geben Sie den Domain-Namen oder den Platzhalter ein, der umgeschrieben werden soll.\",\n  \"domain_name_table_header\": \"Domainname\",\n  \"domain_or_client\": \"Domain oder Client\",\n  \"down\": \"Nicht erreichbar\",\n  \"download_mobileconfig\": \"Konfigurationsdatei herunterladen\",\n  \"download_mobileconfig_doh\": \".mobileconfig für DNS-über-HTTPS herunterladen\",\n  \"download_mobileconfig_dot\": \".mobileconfig für DNS-über-TLS herunterladen\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Zulassungsliste bearbeiten\",\n  \"edit_blocklist\": \"Blockliste bearbeiten\",\n  \"edit_table_action\": \"Bearbeiten\",\n  \"edns_cs_desc\": \"Die Option EDNS Client Subnetz (ECS) zu Upstream-Anfragen hinzufügen und die von Clients gesendeten Werte protokollieren.\",\n  \"edns_enable\": \"EDNS-Client-Subnetz aktivieren\",\n  \"edns_use_custom_ip\": \"Benutzerdefinierte IP für EDNS verwenden\",\n  \"edns_use_custom_ip_desc\": \"Benutzerdefinierte IP für EDNS zulassen\",\n  \"elapsed\": \"Verstrichen\",\n  \"empty_response_status\": \"Leer\",\n  \"enable_protection\": \"Schutz aktivieren\",\n  \"enable_protection_timer\": \"Der Schutz wird in {{time}} wieder aktiviert\",\n  \"enable_rewrites\": \"Umschreibregeln aktivieren\",\n  \"enable_upstream_dns_cache\": \"Caching für die benutzerdefinierte Upstream-Server-Konfiguration dieses Clients aktivieren\",\n  \"enabled_dhcp\": \"DHCP-Server aktiviert\",\n  \"enabled_filtering_toast\": \"Filtern aktiviert\",\n  \"enabled_parental_toast\": \"Kindersicherung aktiviert\",\n  \"enabled_protection\": \"Schutz aktiviert\",\n  \"enabled_safe_browsing_toast\": \"Internetsicherheit aktiviert\",\n  \"enabled_save_search_toast\": \"Sichere Suche aktiviert\",\n  \"enabled_table_header\": \"Aktiviert\",\n  \"encryption_certificate_path\": \"Zertifikatspfad\",\n  \"encryption_certificates\": \"Zertifikate\",\n  \"encryption_certificates_desc\": \"Um die Verschlüsselung verwenden zu können, müssen Sie eine gültige SSL-Zertifikatskette für Ihre Domain angeben. Sie können ein kostenloses Zertifikat für <0>{{link}}</0> erhalten oder es bei einer der vertrauenswürdigen Zertifizierungsstellen kaufen.\",\n  \"encryption_certificates_input\": \"Kopieren Sie Ihre PEM-codierten Zertifikate und fügen Sie sie hier ein.\",\n  \"encryption_certificates_source_content\": \"Inhalt des Zertifikats einfügen\",\n  \"encryption_certificates_source_path\": \"Pfad für die Zertifikatsdatei festlegen\",\n  \"encryption_chain_invalid\": \"Zertifikatskette ist ungültig\",\n  \"encryption_chain_valid\": \"Zertifikatskette ist gültig\",\n  \"encryption_config_saved\": \"Verschlüsselungskonfiguration gespeichert\",\n  \"encryption_desc\": \"Unterstützung von Verschlüsselung (HTTPS/QUIC/TLS) für DNS- und Admin-Weboberfläche\",\n  \"encryption_doq\": \"Port für DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"Wenn dieser Port eingerichtet ist, wird AdGuard Home einen DNS-over-QUIC-Server auf diesem Port ausführen. \",\n  \"encryption_dot\": \"DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Wenn dieser Port konfiguriert ist, führt AdGuard Home auf diesem Port einen DNS-over-TLS-Server aus.\",\n  \"encryption_enable\": \"Verschlüsselung aktivieren (HTTPS, DNS-over-HTTPS und DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Wenn die Verschlüsselung aktiviert ist, funktioniert die AdGuard Home Admin-Oberfläche über HTTPS, und der DNS-Server wartet auf Anfragen über DNS-over-HTTPS und DNS-over-TLS.\",\n  \"encryption_expire\": \"Gültig bis\",\n  \"encryption_hostnames\": \"Hostnamen\",\n  \"encryption_https\": \"HTTPS-Port\",\n  \"encryption_https_desc\": \"Wenn der HTTPS-Port konfiguriert ist, ist die AdGuard Home-Administrationsschnittstelle über HTTPS zugänglich und bietet auch DNS-over-HTTPS am Server „/dns-query“.\",\n  \"encryption_issuer\": \"Ausgestellt von\",\n  \"encryption_key\": \"Privater Schlüssel\",\n  \"encryption_key_input\": \"Kopieren Sie Ihren PEM-codierten privaten Schlüssel für Ihr Zertifikat und fügen Sie ihn hier ein.\",\n  \"encryption_key_invalid\": \"Das ist ein ungültiger {{type}} privater Schlüssel\",\n  \"encryption_key_source_content\": \"Inhalt des privaten Schlüssels einfügen\",\n  \"encryption_key_source_path\": \"Pfad für private Schlüsseldatei festlegen\",\n  \"encryption_key_valid\": \"Das ist ein gültiger {{type}} privater Schlüssel\",\n  \"encryption_plain_dns_desc\": \"Einfaches DNS ist standardmäßig aktiviert. Sie können es deaktivieren, um alle Geräte zu zwingen, verschlüsseltes DNS zu verwenden. Dazu müssen Sie mindestens ein verschlüsseltes DNS-Protokoll aktivieren\",\n  \"encryption_plain_dns_enable\": \"Einfaches DNS aktivieren\",\n  \"encryption_plain_dns_error\": \"Um einfaches DNS zu deaktivieren, aktivieren Sie mindestens ein verschlüsseltes DNS-Protokoll\",\n  \"encryption_private_key_path\": \"Pfad des privaten Schlüssels\",\n  \"encryption_redirect\": \"Automatisch auf HTTPS umleiten\",\n  \"encryption_redirect_desc\": \"Wenn aktiviert, leitet AdGuard Home Sie automatisch von HTTP- auf HTTPS-Adressen um.\",\n  \"encryption_reset\": \"Möchten Sie die Verschlüsselungseinstellungen wirklich zurücksetzen?\",\n  \"encryption_server\": \"Servername\",\n  \"encryption_server_desc\": \"Wenn diese Option aktiviert ist, erkennt AdGuard Home ClientIDs, antwortet auf DDR-Anfragen und führt zusätzliche Verbindungsüberprüfungen durch. Wenn sie nicht gesetzt ist, sind diese Funktionen deaktiviert. Muss mit einem der DNS-Namen im Zertifikat übereinstimmen.\",\n  \"encryption_server_enter\": \"Domain-Namen eingeben\",\n  \"encryption_settings\": \"Verschlüsselungseinstellungen\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Ausgestellt für\",\n  \"encryption_title\": \"Verschlüsselung\",\n  \"encryption_warning\": \"Warnhinweis\",\n  \"enforce_safe_search\": \"Sichere Suche verwenden\",\n  \"enforce_save_search_hint\": \"AdGuard kann Sichere Suche für folgende Suchmaschinen erzwingen: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex und Pixabay.\",\n  \"enforced_save_search\": \"Sichere Suche erzwungen\",\n  \"enter_cache_size\": \"Größe des Cache (Bytes) eingeben\",\n  \"enter_cache_ttl_max_override\": \"TTL-Höchstwert eingeben (in Sekunden)\",\n  \"enter_cache_ttl_min_override\": \"TTL-Minimalwert eingeben (in Sekunden)\",\n  \"enter_name_hint\": \"Name eingeben\",\n  \"enter_url_or_path_hint\": \"URL oder absoluten Pfad der Liste eingeben\",\n  \"enter_valid_allowlist\": \"Gültige Webadresse zur Positivliste eingeben.\",\n  \"enter_valid_blocklist\": \"Gültige Webadresse zur Sperrliste eingeben.\",\n  \"error_details\": \"Fehlerdetails\",\n  \"example_comment\": \"! Hier steht ein Kommentar.\",\n  \"example_comment_hash\": \"# Auch ein Kommentar.\",\n  \"example_comment_meaning\": \"nur ein Kommentar;\",\n  \"example_meaning_filter_block\": \"Zugriff auf die Domain example.org und alle ihre Subdomains sperren;\",\n  \"example_meaning_filter_whitelist\": \"Zugriff auf die Domain example.org und alle ihre Subdomains entsperren;\",\n  \"example_meaning_host_block\": \"Adresse 127.0.0.1 für die Domain example.org zurückgeben (aber nicht für ihre Subdomains);\",\n  \"example_multiple_upstreams_reserved\": \"mehrere Upstreams <0>für bestimmte Domains</0>;\",\n  \"example_regex_meaning\": \"Zugriff auf die Domains sperren, die dem angegebenen regulären Ausdruck entsprechen.\",\n  \"example_rewrite_domain\": \"Antworten nur für diesen Domain-Namen umschreiben.\",\n  \"example_rewrite_wildcard\": \"Antworten nur für alle <0>example.org</0> Subdomains umschreiben.\",\n  \"example_upstream_comment\": \"ein Kommentar.\",\n  \"example_upstream_doh\": \"verschlüsseltes <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"verschlüsseltes DNS-over-HTTPS mit erzwungenem <0>HTTP/3</0> und keinem Fallback zu HTTP/2 oder darunter;\",\n  \"example_upstream_doq\": \"verschlüsseltes <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"verschlüsseltes <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"reguläres DNS (over UDP);\",\n  \"example_upstream_regular_port\": \"normales DNS (über UDP, mit Port);\",\n  \"example_upstream_reserved\": \"ein Upstream <0>für bestimmte Domains</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS-Stempel</0> für <1>DNSCrypt</1> oder <2>DNS-over-HTTPS</2> Resolver;\",\n  \"example_upstream_tcp\": \"reguläres DNS (over TCP);\",\n  \"example_upstream_tcp_hostname\": \"normales DNS (über TCP, Hostname);\",\n  \"example_upstream_tcp_port\": \"normales DNS (über TCP, mit Port);\",\n  \"example_upstream_udp\": \"normales DNS (über UDP, Hostname);\",\n  \"examples_title\": \"Beispiele\",\n  \"fallback_dns_desc\": \"Liste der Fallback-DNS-Server, die verwendet werden, wenn die Upstream-DNS-Server nicht antworten. Die Syntax ist die gleiche wie im Hauptfeld für Upstream-Server oben.\",\n  \"fallback_dns_placeholder\": \"Geben Sie einen Fallback-DNS-Server pro Zeile ein\",\n  \"fallback_dns_title\": \"Fallback-DNS-Server\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Schnellste IP-Adresse\",\n  \"fastest_addr_desc\": \"Auf Antworten von <b>allen</b> DNS-Servern warten, die TCP-Verbindungsgeschwindigkeit für jeden Server messen und die IP-Adresse des Servers mit der schnellsten Verbindungsgeschwindigkeit zurückgeben.<br/>Dieser Modus kann DNS-Anfragen erheblich verlangsamen, wenn ein oder mehrere Server nicht antworten. Stellen Sie sicher, dass Ihre Server stabil laufen und das Upstream-Timeout niedrig ist.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"Der Filter wurde erfolgreich hinzugefügt\",\n  \"filter_allowlist\": \"Warnhinweis: Durch diese Aktion wird außerdem die Regel „{{disallowed_rule}}“ aus der Liste der zugelassenen Clients ausgeschlossen.\",\n  \"filter_category_general\": \"Allgemein\",\n  \"filter_category_general_desc\": \"Listen, die Tracking und Werbung auf den meisten Geräten sperren\",\n  \"filter_category_other\": \"Weitere\",\n  \"filter_category_other_desc\": \"Weitere Sperrlisten\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Listen, die sich auf regionale Werbeanzeigen und Tracking-Server konzentrieren\",\n  \"filter_category_security\": \"Sicherheit\",\n  \"filter_category_security_desc\": \"Listen, die auf das Sperren von Malware-, Phishing- oder Scam-Domains spezialisiert sind\",\n  \"filter_removed_successfully\": \"Der Filter wurde erfolgreich entfernt\",\n  \"filter_updated\": \"Der Filter wurde erfolgreich aktualisiert\",\n  \"filtered\": \"Gefiltert\",\n  \"filtered_custom_rules\": \"Nach benutzerdefinierten Filterregeln gefiltert\",\n  \"filtering_rules_learn_more\": \"<0>Erfahren Sie mehr</0> über die Erstellung eigener Hosts-Listen.\",\n  \"filters\": \"Filter\",\n  \"filters_and_hosts_hint\": \"AdGuard Home versteht grundlegende Werbefilterregeln und Host-Datei-Syntax.\",\n  \"filters_block_toggle_hint\": \"Sie können Blockierregeln in den <a>Filter</a>einstellungen erstellen.\",\n  \"filters_configuration\": \"Filterkonfiguration\",\n  \"filters_enable\": \"Filter aktivieren\",\n  \"filters_interval\": \"Aktualisierungsintervall der Filter\",\n  \"fix\": \"Beheben\",\n  \"for_last_days\": \"am letzten {{count}} Tag\",\n  \"for_last_days_plural\": \"in den letzten {{count}} Tage\",\n  \"for_last_hours\": \"in die letzte {{count}} Stunde\",\n  \"for_last_hours_plural\": \"in den letzten {{count}} Stunden\",\n  \"forgot_password\": \"Passwort vergessen?\",\n  \"forgot_password_desc\": \"Bitte folgen Sie <0>dieser Anleitung</0>, um ein neues Passwort für Ihr Benutzerkonto zu erstellen.\",\n  \"form_add_id\": \"Kennung hinzufügen\",\n  \"form_answer\": \"IP-Adresse oder Domainname eingeben\",\n  \"form_client_name\": \"Clientnamen eingeben\",\n  \"form_domain\": \"Domain eingeben\",\n  \"form_enter_blocked_response_ttl\": \"Geben Sie die Gültigkeitsdauer für blockierte Antworten ein (in Sekunden)\",\n  \"form_enter_host\": \"Gerätenamen eingeben\",\n  \"form_enter_hostname\": \"Gerätenamen eingeben\",\n  \"form_enter_id\": \"Kennung eingeben\",\n  \"form_enter_ip\": \"IP-Adresse eingeben\",\n  \"form_enter_mac\": \"MAC-Adresse eingeben\",\n  \"form_enter_rate_limit\": \"Begrenzungswert eingeben\",\n  \"form_enter_rate_limit_subnet_len\": \"Geben Sie die Subnetzpräfixlänge für die Ratebegrenzung ein\",\n  \"form_enter_subnet_ip\": \"IP-Adresse zum Subnetz „{{cidr}}“ hinzufügen\",\n  \"form_enter_upstream_timeout\": \"Geben Sie die Timeout-Dauer des Upstream-Servers in Sekunden ein\",\n  \"form_error_answer_format\": \"Ungültiges Antwortformat\",\n  \"form_error_client_id_format\": \"Client-ID muss nur Zahlen, Kleinbuchstaben und Bindestriche enthalten\",\n  \"form_error_domain_format\": \"Ungültiges Domainformat\",\n  \"form_error_equal\": \"Sollten nicht übereinstimmen\",\n  \"form_error_gateway_ip\": \"Lease kann nicht die IP-Adresse des Gateways haben\",\n  \"form_error_ip4_format\": \"Ungültige IPv4-Adresse\",\n  \"form_error_ip4_gateway_format\": \"Ungültige IPv4-Adresse des Gateways\",\n  \"form_error_ip6_format\": \"Ungültige IPv6-Adresse\",\n  \"form_error_ip_format\": \"Ungültige IP-Adresse\",\n  \"form_error_mac_format\": \"Ungültige MAC-Adresse\",\n  \"form_error_password\": \"Passwörter stimmen nicht überein\",\n  \"form_error_password_length\": \"Das Passwort muss zwischen {{min}} und {{max}} Zeichen enthalten\",\n  \"form_error_port\": \"Geben Sie eine gültige Portnummer ein\",\n  \"form_error_port_range\": \"Geben Sie die Portnummer zwischen 80 und 65535 ein\",\n  \"form_error_port_unsafe\": \"Unsicherer Port\",\n  \"form_error_positive\": \"Muss größer als 0 sein\",\n  \"form_error_required\": \"Pflichtfeld\",\n  \"form_error_server_name\": \"Ungültiger Servername\",\n  \"form_error_subnet\": \"Subnetz „{{cidr}}“ enthält nicht die IP-Adresse „{{ip}}“\",\n  \"form_error_url_format\": \"Ungültiges URL-Format\",\n  \"form_error_url_or_path_format\": \"Ungültige URL oder absoluter Pfad der Liste\",\n  \"form_select_tags\": \"Schlagwörter des Clients auswählen\",\n  \"found_in_known_domain_db\": \"In der Datenbank der bekannten Domains gefunden.\",\n  \"friday\": \"Freitag\",\n  \"friday_short\": \"Fr\",\n  \"gateway_or_subnet_invalid\": \"Ungültige Subnetzmaske\",\n  \"general_settings\": \"Allgemeine Einstellungen\",\n  \"general_statistics\": \"Allgemeine Statistiken\",\n  \"get_started\": \"Anfangen\",\n  \"greater_range_start_error\": \"Muss größer als der Bereichsbeginn sein\",\n  \"homepage\": \"Startseite\",\n  \"host_whitelisted\": \"Der Host ist in der Positivliste enthalten\",\n  \"ignore_domains\": \"Ignorierte Domains (durch Zeilenumbruch getrennt)\",\n  \"ignore_domains_desc_query\": \"Abfragen, die diesen Regeln entsprechen, werden nicht in das Abfrageprotokoll aufgenommen\",\n  \"ignore_domains_desc_stats\": \"Abfragen, die diesen Regeln entsprechen, werden nicht in die Statistik aufgenommen\",\n  \"ignore_domains_title\": \"Ignorierte Domains\",\n  \"ignore_query_log\": \"Diesen Client im Abfrageprotokoll ignorieren\",\n  \"ignore_statistics\": \"Diesen Client in der Statistik ignorieren\",\n  \"install_auth_confirm\": \"Passwort bestätigen\",\n  \"install_auth_desc\": \"Die Passwort-Authentifizierung für Ihre AdGuard-Home-Admin-Web-Oberfläche muss konfiguriert werden. Auch wenn AdGuard Home nur in Ihrem lokalen Netzwerk zugänglich ist, so ist es dennoch wichtig, es vor unberechtigtem Zugriff zu schützen.\",\n  \"install_auth_password\": \"Passwort\",\n  \"install_auth_password_enter\": \"Passwort eingeben\",\n  \"install_auth_title\": \"Authentifizierung\",\n  \"install_auth_username\": \"Benutzername\",\n  \"install_auth_username_enter\": \"Benutzernamen eingeben\",\n  \"install_devices_address\": \"Der AdGuard-Home-DNS-Server lauscht unter folgenden Adressen\",\n  \"install_devices_android_list_1\": \"Tippen Sie auf dem Startbildschirm des Android-Menüs auf „Einstellungen“.\",\n  \"install_devices_android_list_2\": \"Tippen Sie im Menü auf „WLAN“. Der Bildschirm mit allen verfügbaren Netzwerken wird angezeigt (es ist nicht möglich, einen benutzerdefinierten DNS für die mobile Verbindung einzustellen).\",\n  \"install_devices_android_list_3\": \"Drücken Sie lange auf das Netzwerk, mit dem Sie verbunden sind, und tippen Sie auf „Netzwerk ändern“.\",\n  \"install_devices_android_list_4\": \"Bei einigen Geräten müssen Sie möglicherweise das Kontrollkästchen für „Erweitert“ aktivieren, um weitere Einstellungen anzuzeigen. Um Ihre Android-DNS-Einstellungen anzupassen, müssen Sie die IP-Einstellungen von „DHCP“ auf „Statisch“ umstellen.\",\n  \"install_devices_android_list_5\": \"Ändern Sie die Werte für „DNS 1“ und „DNS 2“ auf Ihre AdGuard Home-Serveradressen.\",\n  \"install_devices_desc\": \"Um AdGuard Home nutzen zu können, müssen Sie Ihre Geräte so konfigurieren, dass sie es auch wirklich nutzen.\",\n  \"install_devices_ios_list_1\": \"Tippen Sie auf dem Startbildschirm auf „Einstellungen“.\",\n  \"install_devices_ios_list_2\": \"Wählen Sie „WLAN“ im linken Menü (es ist nicht möglich, DNS für mobile Netzwerke zu konfigurieren).\",\n  \"install_devices_ios_list_3\": \"Tippen Sie auf den Namen des aktuell aktiven Netzwerks.\",\n  \"install_devices_ios_list_4\": \"Geben Sie im DNS-Feld Ihre AdGuard Home-Serveradressen ein.\",\n  \"install_devices_macos_list_1\": \"Klicken Sie auf das Apple-Symbol und gehen Sie zu Systemeinstellungen.\",\n  \"install_devices_macos_list_2\": \"Klicken Sie auf „Netzwerk“.\",\n  \"install_devices_macos_list_3\": \"Wählen Sie die erste Verbindung in Ihrer Liste aus und klicken Sie auf „Weitere Optionen“.\",\n  \"install_devices_macos_list_4\": \"Wählen Sie den Tab „DNS“ und geben Sie dort Ihre AdGuard Home-Serveradressen ein.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Diese Einrichtung deckt automatisch alle an Ihren Heimrouter angeschlossenen Geräte ab, und Sie müssen nicht jedes einzelne davon manuell konfigurieren.\",\n  \"install_devices_router_list_1\": \"Öffnen Sie die Einstellungen für Ihren Router. In der Regel können Sie über eine URL (z. B. http://192.168.0.1/ oder http://192.168.1.1) von Ihrem Browser aus darauf zugreifen. Möglicherweise werden Sie aufgefordert, ein Passwort einzugeben. Wenn Sie sich nicht mehr daran erinnern, können Sie das Passwort oft durch Drücken einer Taste am Router selbst zurücksetzen, aber seien Sie sich bewusst, dass Sie bei dieser Vorgehensweise wahrscheinlich die gesamte Routerkonfiguration verlieren. Wenn für die Einrichtung Ihres Routers eine App erforderlich ist, installieren Sie bitte die App auf Ihrem mobilen Endgerät oder PC und verwenden Sie sie für den Zugriff auf die Einstellungen des Routers.\",\n  \"install_devices_router_list_2\": \"Wechseln Sie zu den DHCP/DNS-Einstellungen. Suchen sie dort nach einem Eintrag „DNS“ und einem Bereich, welches zwei oder drei Zahlengruppen zulässt, die jeweils in vier Blöcke von ein bis drei Ziffern unterteilt sind.\",\n  \"install_devices_router_list_3\": \"Geben Sie dort Ihre AdGuard Home Server-Adressen ein.\",\n  \"install_devices_router_list_4\": \"Bei einigen Routertypen kann kein eigener DNS-Server eingerichtet werden. In diesem Fall kann es helfen, AdGuard Home als <0>DHCP-Server</0> einzurichten. Andernfalls sollten Sie im Handbuch des Routers nachsehen, wie Sie DNS-Server auf Ihrem konkreten Router-Modell anpassen können.\",\n  \"install_devices_title\": \"Konfigurieren Sie Ihre Geräte\",\n  \"install_devices_windows_list_1\": \"Öffnen Sie die Systemsteuerung über das Startmenü oder die Windows-Suche.\",\n  \"install_devices_windows_list_2\": \"Öffnen Sie die Kategorie „Netzwerk und Internet“ und dann „Netzwerk- und Freigabecenter“.\",\n  \"install_devices_windows_list_3\": \"Klicken Sie in der linken Leiste auf „Adaptereinstellungen ändern“.\",\n  \"install_devices_windows_list_4\": \"Klicken Sie mit der rechten Maustaste auf Ihre aktive Verbindung und wählen Sie „Eigenschaften“.\",\n  \"install_devices_windows_list_5\": \"Suchen Sie in der Liste nach „Internet Protokoll Version 4 (TCP/IP)“ (oder, für IPv6, „Internet Protocol Version 6 (TCP/IPv6)“), markieren Sie diese und klicken Sie dann erneut auf „Eigenschaften“.\",\n  \"install_devices_windows_list_6\": \"Wählen Sie „Folgende DNS-Serveradressen verwenden“ und geben Sie Ihre AdGuard Home-Serveradressen ein.\",\n  \"install_saved\": \"Erfolgreich gespeichert\",\n  \"install_settings_all_interfaces\": \"Alle Schnittstellen\",\n  \"install_settings_dns\": \"DNS-Server\",\n  \"install_settings_dns_desc\": \"Sie müssen Ihre Geräte oder Ihren Router so konfigurieren, dass er den DNS-Server unter den folgenden Adressen verwendet:\",\n  \"install_settings_interface_link\": \"Ihre AdGuard-Home-Admin-Weboberfläche ist unter den folgenden Adressen verfügbar:\",\n  \"install_settings_listen\": \"Netzwerk-Schnittstelle\\n\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Admin-Weboberfläche\",\n  \"install_static_configure\": \"AdGuard Home hat festgestellt, dass die dynamische IP-Adresse <0>{{ip}}</0> verwendet wird. Möchten Sie, dass es als statische Adresse festgelegt wird?\",\n  \"install_static_error\": \"AdGuard Home kann nicht automatisch für diese Netzwerkschnittstelle konfiguriert werden. Bitte suchen Sie nach einer Anleitung, wie Sie dies manuell durchführen können.\",\n  \"install_static_ok\": \"Gute Nachrichten! Die feste IP-Adresse ist bereits konfiguriert\",\n  \"install_step\": \"Schritt\",\n  \"install_submit_desc\": \"Die Einrichtung ist abgeschlossen und Sie können mit der Verwendung von AdGuard Home beginnen.\",\n  \"install_submit_title\": \"Gratulation!\",\n  \"install_welcome_desc\": \"AdGuard Home ist ein netzwerkweiter Werbung- und Tracking sperrender DNS-Server. Sein Zweck ist es, Ihnen die Kontrolle über Ihr gesamtes Netzwerk und alle Ihre Geräte zu ermöglichen, und es ist nicht erforderlich, eine clientseitige Anwendung zu verwenden.\",\n  \"install_welcome_title\": \"Willkommen bei AdGuard Home!\",\n  \"interval_24_hour\": \"24 Stunden\",\n  \"interval_6_hour\": \"6 Stunden\",\n  \"interval_days\": \"{{count}} Tag\",\n  \"interval_days_plural\": \"{{count}} Tage\",\n  \"interval_hours\": \"{{count}} Stunde\",\n  \"interval_hours_plural\": \"{{count}} Stunden\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP-Adresse\",\n  \"known_tracker\": \"Bekannte Tracker\",\n  \"last_rule_in_allowlist\": \"Dieser Client kann nicht gesperrt werden, da das Ausschließen der Regel „{{disallowed_rule}}“ die Liste „Zugelassene Clients“ deaktivieren würde.\",\n  \"last_time_updated_table_header\": \"Letztes Update\",\n  \"list_confirm_delete\": \"Möchten Sie diese Liste wirklich löschen?\",\n  \"list_label\": \"Liste\",\n  \"list_updated\": \"{{count}} Liste aktualisiert\",\n  \"list_updated_plural\": \"{{count}} Listen aktualisiert\",\n  \"list_url_table_header\": \"Adressliste\",\n  \"load_balancing\": \"Lastverteilung\",\n  \"load_balancing_desc\": \"Es wird jeweils ein Upstream-Server abgefragt.<br/> AdGuard Home verwendet einen gewichteten Zufallsalgorithmus, um die Server mit der geringsten Anzahl an fehlgeschlagenen Suchvorgängen und der niedrigsten durchschnittlichen Suchzeit auszuwählen.\",\n  \"loading_table_status\": \"Wird geladen …\",\n  \"local_ptr_default_resolver\": \"Standardmäßig verwendet AdGuard Home die folgenden inversen DNS-Resolver: {{ip}}.\",\n  \"local_ptr_desc\": \"Von AdGuard Home verwendete DNS-Server für private PTR-, SOA- und NS-Anfragen. Eine Anfrage gilt als privat, wenn sie nach einer ARPA-Domain fragt, die ein Subnetz innerhalb privater IP-Bereiche enthält (z. B. „192.168.12.34“) und von einem Client mit privater IP-Adresse stammt. Wenn nicht eingestellt, werden die Standard-DNS-Auflöser Ihres Betriebssystems verwendet, außer für die IP-Adressen von AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home konnte keine geeigneten privaten Invers-DNS-Resolver für dieses System ermitteln.\",\n  \"local_ptr_placeholder\": \"Geben Sie eine IP-Adresse pro Zeile ein\",\n  \"local_ptr_title\": \"Private inverse DNS-Server\",\n  \"location\": \"Ort\",\n  \"log_and_stats_section_label\": \"Abfrageprotokoll und Statistik\",\n  \"lower_range_start_error\": \"Muss niedriger als der Bereichsbeginn sein\",\n  \"main_settings\": \"Grundeinstellungen\",\n  \"make_static\": \"Statisch machen\",\n  \"manual_update\": \"Bitte <a>befolgen Sie diese Schritte</a>, um manuell zu aktualisieren.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Montag\",\n  \"monday_short\": \"Mo\",\n  \"name\": \"Name\",\n  \"name_table_header\": \"Name\",\n  \"netname\": \"Netzwerkname\",\n  \"network\": \"Netzwerk\",\n  \"new_allowlist\": \"Neue Zulassungsliste\",\n  \"new_blocklist\": \"Neue Blockliste\",\n  \"next\": \"Weiter\",\n  \"next_btn\": \"Nächste\",\n  \"no_blocklist_added\": \"Keine Blocklisten hinzugefügt\",\n  \"no_clients_found\": \"Keine Clients gefunden\",\n  \"no_domains_found\": \"Keine Domains gefunden\",\n  \"no_logs_found\": \"Keine Protokolle gefunden\",\n  \"no_servers_specified\": \"Keine Server festgelegt\",\n  \"no_upstreams_data_found\": \"Keine Upstream-Daten gefunden\",\n  \"no_whitelist_added\": \"Keine Zulassungslisten hinzugefügt\",\n  \"nothing_found\": \"Nichts gefunden\",\n  \"null_ip\": \"Null-IP-Adresse\",\n  \"number_of_dns_query_blocked_24_hours\": \"Anzahl der durch Werbefilter und Host-Sperrlisten abgelehnte DNS-Anfragen\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Anzahl der gesperrten Websites mit jugendgefährdenden Inhalten\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Anzahl der durch das AdGuard-Modul „Internetsicherheit“ gesperrten DNS-Anfragen\",\n  \"number_of_dns_query_days\": \"Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen\",\n  \"number_of_dns_query_days_plural\": \"Anzahl der DNS-Abfragen, die in den letzten {{count}} Tagen verarbeitet wurden\",\n  \"number_of_dns_query_hours\": \"Die Anzahl der DNS-Anfragen, die in der letzten {{count}} Stunde verarbeitet wurden\",\n  \"number_of_dns_query_hours_plural\": \"Die Anzahl der DNS-Anfragen, die in den letzten {{count}} Stunden verarbeitet wurden\",\n  \"number_of_dns_query_to_safe_search\": \"Anzahl der DNS-Anfragen bei denen Sichere Suche für Suchanfragen erzwungen wurde\",\n  \"nxdomain\": \"NXDomain\",\n  \"off\": \"AUS\",\n  \"on\": \"AN\",\n  \"open_dashboard\": \"Übersicht öffnen\",\n  \"orgname\": \"Name der Organisation\",\n  \"original_response\": \"Ursprüngliche Antwort\",\n  \"out_of_range_error\": \"Muss außerhalb des Bereichs „{{start}}“-„{{end}}“ liegen\",\n  \"page_table_footer_text\": \"Seite\",\n  \"parallel_requests\": \"Paralleles Abfragen\",\n  \"parental_control\": \"Kindersicherung\",\n  \"password_label\": \"Passwort\",\n  \"password_placeholder\": \"Passwort eingeben\",\n  \"plain_dns\": \"Einfaches DNS\",\n  \"port_53_faq_link\": \"Port 53 wird oft von Diensten wie „DNSStubListener“ oder „system-resolved“ belegt. Bitte lesen Sie <0>diese Anweisung</0>, wie dies behoben werden kann.\",\n  \"previous_btn\": \"Vorherige\",\n  \"privacy_policy\": \"Datenschutzerklärung\",\n  \"processing_update\": \"Bitte warten Sie, AdGuard Home wird aktualisiert …\",\n  \"protection_section_label\": \"Schutz\",\n  \"protocol\": \"Protokoll\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Abfrageprotokoll\",\n  \"query_log_clear\": \"Abfrageprotokolle leeren\",\n  \"query_log_cleared\": \"Das Abfrageprotokoll wurde erfolgreich gelöscht\",\n  \"query_log_configuration\": \"Konfiguration der Protokolle\",\n  \"query_log_confirm_clear\": \"Möchten Sie wirklich das Abfrageprotokoll vollständig löschen?\",\n  \"query_log_disabled\": \"Das Abfrageprotokoll ist deaktiviert und kann in den <0>Einstellungen</0> konfiguriert werden.\",\n  \"query_log_enable\": \"Protokoll aktivieren\",\n  \"query_log_filtered\": \"Gefiltert nach {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotation der Abfrageprotokolle\",\n  \"query_log_retention_confirm\": \"Möchten Sie die Abfrageprotokollrotation wirklich ändern? Wenn Sie den Intervallwert verringern, gehen einige Daten verloren\",\n  \"query_log_strict_search\": \"Doppelte Anführungszeichen für die strikte Suche verwenden\",\n  \"query_log_updated\": \"Das Abfrageprotokoll wurde erfolgreich aktualisiert\",\n  \"rate_limit\": \"Begrenzungswert\",\n  \"rate_limit_desc\": \"Die Anzahl der Anfragen pro Sekunde, die ein einzelner Client stellen darf. Das Setzen auf 0 bedeutet keine Begrenzung.\",\n  \"rate_limit_subnet_len_ipv4\": \"Länge des Subnetzpräfixes für IPv4-Adressen\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Subnetpräfixlänge für IPv4-Adressen, die für die Ratebegrenzung verwendet werden. Der Standardwert ist 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Die Subnetzpräfixlänge für IPv4-Adressen sollte zwischen 0 und 32 liegen\",\n  \"rate_limit_subnet_len_ipv6\": \"Subnetzpräfixlänge für IPv6-Adressen\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Subnetpräfixlänge für IPv6-Adressen, die für die Ratebegrenzung verwendet werden. Der Standardwert ist 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Die Subnetzpräfixlänge für IPv6-Adressen sollte zwischen 0 und 128 liegen\",\n  \"rate_limit_whitelist\": \"Zulassungsliste für die Ratebegrenzung\",\n  \"rate_limit_whitelist_desc\": \"IP-Adressen, die von der Ratebegrenzung ausgeschlossen sind\",\n  \"rate_limit_whitelist_placeholder\": \"Geben Sie eine IP-Adresse - eine pro Zeile\",\n  \"refresh_btn\": \"Aktualisieren\",\n  \"refresh_statics\": \"Statistiken aktualisieren\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Diese Webseite melden\",\n  \"request_details\": \"Informationen zur Anfrage\",\n  \"request_table_header\": \"Anfrage\",\n  \"requests_count\": \"Anzahl der Anfragen\",\n  \"reset_settings\": \"Einstellungen zurücksetzen\",\n  \"resolve_clients_desc\": \"Inverses Auflösen der IP-Adressen der Clients in ihre Hostnamen durch Senden von PTR-Anfragen an die entsprechenden Resolver (private DNS-Server für lokale Kunden, Upstream-Server für Kunden mit öffentlichen IP-Adressen).\",\n  \"resolve_clients_title\": \"Hostnamenauflösung der Clients aktivieren\",\n  \"response_code\": \"Antwortcode\",\n  \"response_details\": \"Einzelheiten der Antwort\",\n  \"response_table_header\": \"Antwort\",\n  \"response_time\": \"Antwortzeit\",\n  \"rewrite_A\": \"<0>A</0>: spezieller Wert, <0>A</0>-Datensätze des \\n vorgeschalteten Servers beibehalten\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: spezieller Wert, <0>AAAA</0>-Datensätze des vorgeschalteten Servers beibehalten\",\n  \"rewrite_add\": \"DNS-Umschreibung hinzufügen\",\n  \"rewrite_added\": \"DNS-Umschreibung für „{{key}}“ erfolgreich hinzugefügt\",\n  \"rewrite_applied\": \"Umschreibungsregel ist angewendet\",\n  \"rewrite_confirm_delete\": \"Möchten Sie die DNS-Umschreibung für „{{key}}“ wirklich entfernen?\",\n  \"rewrite_deleted\": \"DNS-Umschreibung für „{{key}}“ erfolgreich entfernt\",\n  \"rewrite_desc\": \"Ermöglicht die einfache Konfiguration der benutzerdefinierten DNS-Antwort für einen bestimmten Domainnamen.\",\n  \"rewrite_domain_name\": \"Domänenname: einen CNAME-Eintrag hinzufügen\",\n  \"rewrite_edit\": \"DNS-Rewrite bearbeiten\",\n  \"rewrite_hosts_applied\": \"Von Hostdatei-Regel umgeschrieben\",\n  \"rewrite_ip_address\": \"IP-Adresse: Verwenden Sie diese IP in einer A- oder AAAA-Antwort\",\n  \"rewrite_not_found\": \"Keine DNS-Umschreibungen gefunden\",\n  \"rewrite_settings_updated\": \"DNS-Umschreibungs-Einstellungen aktualisiert\",\n  \"rewrite_updated\": \"DNS-Rewrite erfolgreich aktualisiert\",\n  \"rewrites_disabled_table_header\": \"Umschreibungen sind deaktiviert\",\n  \"rewrites_enabled_table_header\": \"Umschreibungen sind aktiviert\",\n  \"rewritten\": \"Umgeschrieben\",\n  \"rows_table_footer_text\": \"Zeilen\",\n  \"rule_added_to_custom_filtering_toast\": \"Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt: {{rule}}\",\n  \"rule_label\": \"Regel(n)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regel wurde aus den benutzerdefinierten Filterregeln entfernt: {{rule}}\",\n  \"rules_count_table_header\": \"Regeln total\",\n  \"safe_browsing\": \"Internetsicherheit\",\n  \"safe_search\": \"Sichere Suche\",\n  \"saturday\": \"Samstag\",\n  \"saturday_short\": \"Sa\",\n  \"save_btn\": \"Speichern\",\n  \"save_config\": \"Konfiguration speichern\",\n  \"schedule_add\": \"Zeitplan hinzufügen\",\n  \"schedule_current_timezone\": \"Aktuelle Zeitzone: {{value}}\",\n  \"schedule_desc\": \"Inaktivitätszeiträume für blockierte Dienste festlegen\",\n  \"schedule_edit\": \"Zeitplan bearbeiten\",\n  \"schedule_from\": \"Von\",\n  \"schedule_invalid_select\": \"Die Startzeit muss vor der Endzeit liegen\",\n  \"schedule_modal_description\": \"Dieser Zeitplan wird alle bestehenden Zeitpläne für denselben Wochentag ersetzen. Jeder Wochentag kann nur einen Inaktivitätszeitraum haben.\",\n  \"schedule_modal_time_off\": \"Keine Dienstblockierung:\",\n  \"schedule_new\": \"Neuer Zeitplan\",\n  \"schedule_remove\": \"Zeitplan entfernen\",\n  \"schedule_save\": \"Zeitplan speichern\",\n  \"schedule_select_days\": \"Tage auswählen\",\n  \"schedule_services\": \"Dienstblockierung anhalten\",\n  \"schedule_services_desc\": \"Konfigurieren Sie den Pausenplan des Dienstblockierungsfilters\",\n  \"schedule_services_desc_client\": \"Konfigurieren Sie den Pausenplan des Dienstblockierungsfilters für diesen Client\",\n  \"schedule_time_all_day\": \"Ganzen Tag\",\n  \"schedule_timezone\": \"Wählen Sie eine Zeitzone\",\n  \"schedule_to\": \"Bis\",\n  \"served_from_cache_label\": \"Aus dem Cache abgerufen\",\n  \"service_name\": \"Name des Dienstes\",\n  \"set_static_ip\": \"Feste IP-Adresse festlegen\",\n  \"settings\": \"Einstellungen\",\n  \"settings_custom\": \"Benutzerdefiniert\",\n  \"settings_global\": \"Allgemein\",\n  \"setup_config_to_enable_dhcp_server\": \"Einrichten der Konfiguration zur Aktivierung des DHCP-Servers\",\n  \"setup_dns_notice\": \"Um <1>DNS-over-HTTTPS</1> oder <1>DNS-over-TLS</1> verwenden zu können, müssen Sie in den AdGuard Home Einstellungen die <0>Verschlüsselung konfigurieren</0>.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Zeichenkette <1>{{address}}</1> verwenden.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Zeichenkette <1>{{address}}</1> verwenden.\",\n  \"setup_dns_privacy_3\": \"<0>Hier ist eine Liste von Software, die Sie verwenden können.</0>\",\n  \"setup_dns_privacy_4\": \"Auf einem iOS 14 oder macOS Big Sur-Gerät können Sie eine spezielle Datei „.mobileconfig“ herunterladen, die Server für <highlight>DNS-über-HTTPS</highlight> oder <highlight>DNS-über-TLS</highlight> zu den DNS-Einstellungen hinzufügt.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 unterstützt DNS-over-TLS nativ. Um es zu konfigurieren, gehen Sie zu „Einstellungen“ → „Netzwerk & Internet“ → „Erweitert“ → „Privater DNS“ und geben Sie dort Ihren Domainnamen ein.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard für Android</0> unterstützt <1>DNS-over-HTTTPS</1> und <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"„<0>Intra</0>“ fügt <1>DNS-over-HTTPS</1>-Unterstützung zu Android hinzu.\",\n  \"setup_dns_privacy_ioc_mac\": \"Konfiguration für iOS und macOS\",\n  \"setup_dns_privacy_ios_1\": \"„<0>DNSCloak</0>“ unterstützt <1>DNS-over-HTTPS</1>, aber um es so zu konfigurieren, dass es Ihren eigenen Server verwendet, müssen Sie einen <2>DNS-Stempel</2> dafür generieren.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard für iOS</0> unterstützt die Einrichtung von <1>DNS-over-HTTTPS</1> und <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home selbst kann ein sicherer DNS-Client auf jeder Plattform sein.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> unterstützt alle bekannten sicheren DNS-Protokolle.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> unterstützt <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> unterstützt <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Weitere Implementierungen finden Sie <0>hier</0> und <1>hier</1>.\",\n  \"setup_dns_privacy_other_title\": \"Weitere Implementierungen\",\n  \"setup_guide\": \"Einrichtungsassistent\",\n  \"show_all_filter_type\": \"Alle anzeigen\",\n  \"show_blocked_responses\": \"Gesperrt\",\n  \"show_filtered_type\": \"Gefilterte anzeigen\",\n  \"show_processed_responses\": \"Verarbeitet\",\n  \"show_whitelisted_responses\": \"Auf der Positivliste\",\n  \"sign_in\": \"Anmelden\",\n  \"sign_out\": \"Abmelden\",\n  \"source_label\": \"Quelle\",\n  \"static_ip\": \"Feste IP-Adresse\",\n  \"static_ip_desc\": \"AdGuard Home ist ein Server und benötigt daher eine feste IP-Adresse, um ordnungsgemäß zu funktionieren. Andernfalls weist Ihr Router diesem Gerät möglicherweise irgendwann eine andere IP-Adresse zu.\",\n  \"statistics_clear\": \"Statistiken leeren\",\n  \"statistics_clear_confirm\": \"Möchten Sie die Statistiken wirklich löschen?\",\n  \"statistics_cleared\": \"Statistiken wurden erfolgreich gelöscht\",\n  \"statistics_configuration\": \"Statistikkonfiguration\",\n  \"statistics_enable\": \"Statistiken aktivieren\",\n  \"statistics_retention\": \"Statistiken speichern\",\n  \"statistics_retention_confirm\": \"Möchten Sie wirklich die Aufbewahrung der Statistiken ändern? Wenn Sie den Zeitabstand verringern, gehen einige Daten verloren.\",\n  \"statistics_retention_desc\": \"Wenn Sie den Intervallwert verringern, gehen einige Daten verloren\",\n  \"stats_adult\": \"Gesperrte jugendgefährdende Websites\",\n  \"stats_disabled\": \"Die Statistiken wurden deaktiviert. Sie können diese in den <0>Einstellungen</0> erneut aktivieren.\",\n  \"stats_disabled_short\": \"Die Statistiken wurden deaktiviert\",\n  \"stats_malware_phishing\": \"Gesperrte Schädliche/Phishing-Websites\",\n  \"stats_params\": \"Statistikkonfiguration\",\n  \"stats_query_domain\": \"Am häufigsten angefragte Domains\",\n  \"subnet_error\": \"Die Adressen müssen innerhalb eines Subnetzes liegen\",\n  \"sunday\": \"Sonntag\",\n  \"sunday_short\": \"So\",\n  \"system_host_files\": \"Hosts-Datei des Systems\",\n  \"table_client\": \"Client\",\n  \"table_name\": \"Name\",\n  \"tags_desc\": \"Sie können die Schlagwörter auswählen, die dem Client entsprechen. Die Schlagwörter können in die Filterregeln aufgenommen werden und erlauben Ihnen, sie genauer anzuwenden. <0>Mehr erfahren</0>.\",\n  \"tags_title\": \"Schlagwörter\",\n  \"test_upstream_btn\": \"Upstreams testen\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Automatisch (basierend auf dem Farbschema Ihres Geräts)\",\n  \"theme_dark\": \"Dunkel\",\n  \"theme_dark_desc\": \"Dunkles Design\",\n  \"theme_light\": \"Hell\",\n  \"theme_light_desc\": \"Helles Design\",\n  \"thursday\": \"Donnerstag\",\n  \"thursday_short\": \"Do\",\n  \"time_table_header\": \"Zeit\",\n  \"top_blocked_domains\": \"Am häufigsten gesperrte Domains\",\n  \"top_clients\": \"Top Clients\",\n  \"top_upstreams\": \"Top Upstreams\",\n  \"topline_expired_certificate\": \"Ihr SSL-Zertifikat ist abgelaufen. Aktualisieren Sie Ihre <0>Verschlüsselungseinstellungen</0>.\",\n  \"topline_expiring_certificate\": \"Ihr SSL-Zertifikat läuft demnächst ab. Aktualisieren Sie Ihre <0>Verschlüsselungseinstellungen</0>.\",\n  \"tracker_source\": \"Tracker-Quelle\",\n  \"try_again\": \"Erneut versuchen\",\n  \"ttl_cache_validation\": \"Der Überschreibungswert für die minimale TTL muss kleiner oder gleich dem für die maximale TTL sein\",\n  \"tuesday\": \"Dienstag\",\n  \"tuesday_short\": \"Di\",\n  \"type_table_header\": \"Typ\",\n  \"unavailable_dhcp\": \"DHCP ist nicht verfügbar\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home konnte keinen DHCP-Server auf Ihrem Betriebssystem ausführen\",\n  \"unblock\": \"Entsperren\",\n  \"unblock_all\": \"Alle entsperren\",\n  \"unblock_for_this_client_only\": \"Nur für diesen Client freigeben\",\n  \"unknown_filter\": \"Unbekannter Filter {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} ist jetzt verfügbar! <0>Klicken Sie hier</0> für weitere Informationen.\",\n  \"update_failed\": \"Das automatische Aktualisieren ist fehlgeschlagen. Bitte <a>folgen Sie den Schritten</a>, um manuell zu aktualisieren.\",\n  \"update_now\": \"Jetzt aktualisieren\",\n  \"updated_custom_filtering_toast\": \"Benutzerdefinierten Filterregeln erfolgreich gespeichert\",\n  \"updated_save_search_toast\": \"Einstellungen für die sichere Suche aktualisiert\",\n  \"updated_upstream_dns_toast\": \"Upstream-Server erfolgreich gespeichert\",\n  \"updates_checked\": \"Neue Version von AdGuard Home ist jetzt verfügbar\",\n  \"updates_version_equal\": \"AdGuard Home ist aktuell\",\n  \"upstream\": \"Upstream\",\n  \"upstream_dns\": \"Upstream-DNS-Server\",\n  \"upstream_dns_cache_configuration\": \"Konfiguration des Upstream-DNS-Cache\",\n  \"upstream_dns_client_desc\": \"Wenn Sie dieses Feld leer lassen, verwendet AdGuard Home die Server, die in den <0>DNS-Einstellungen</0> konfiguriert sind.\",\n  \"upstream_dns_configured_in_file\": \"Konfiguriert in {{path}}\",\n  \"upstream_dns_help\": \"Geben Sie pro Zeile eine Serveradresse ein. <a>Weitere Informationen</a> zur Konfiguration von Upstream-DNS-Servern.\",\n  \"upstream_parallel\": \"Parallele Abfragen verwenden, um das Auflösen zu beschleunigen, indem alle Upstream-Server gleichzeitig abgefragt werden.\",\n  \"upstream_timeout\": \"Upstream-Timeout\",\n  \"upstream_timeout_desc\": \"Gibt die Anzahl der Sekunden an, die auf eine Antwort des Upstream-Servers gewartet werden soll\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"AdGuard Webservice für Internetsicherheit nutzen\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home prüft, ob die Domain durch den Webdienst für Internetsicherheit auf eine Sperrliste gesetzt wurde. Es verwendet eine datenschutzfreundliche Lookup-API, um die Prüfung durchzuführen: Nur ein kurzes Präfix des Domänennamens SHA256-Hash wird an den Server gesendet.\",\n  \"use_adguard_parental\": \"AdGuard Webservice für Kindersicherung verwenden\",\n  \"use_adguard_parental_hint\": \"AdGuard Home wird prüfen, ob die Domain jugendgefährdende Inhalte enthält. Zum Schutz Ihrer Privatsphäre wird die selbe API wie für den Webservice für Internetsicherheit verwendet.\",\n  \"use_private_ptr_resolvers_desc\": \"Löst PTR-, SOA- und NS-Anfragen für ARD-Domains mit privaten IP-Adressen über private Upstream-Server, DHCP, /etc/hosts usw. auf. Ist diese Option deaktiviert, antwortet AdGuard Home auf alle derartigen Anfragen mit NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Private Reverse-DNS-Resolver verwenden\",\n  \"use_saved_key\": \"Zuvor gespeicherten Schlüssel verwenden\",\n  \"username_label\": \"Benutzername\",\n  \"username_placeholder\": \"Benutzernamen eingeben\",\n  \"validated_with_dnssec\": \"Bestätigt mit DNSSEC\",\n  \"version\": \"Version\",\n  \"version_request_error\": \"Aktualisierungsprüfung fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.\",\n  \"wednesday\": \"Mittwoch\",\n  \"wednesday_short\": \"Mi\",\n  \"whois\": \"Whois\"\n}\n"
  },
  {
    "path": "client/src/__locales/en.json",
    "content": "{\n  \"access_allowed_desc\": \"A list of CIDRs, IP addresses, or <a>ClientIDs</a>. If this list has entries, AdGuard Home will accept requests only from these clients.\",\n  \"access_allowed_title\": \"Allowed clients\",\n  \"access_blocked_desc\": \"Not to be confused with filters. AdGuard Home drops DNS queries matching these domains, and these queries don't even appear in the query log. You can specify exact domain names, wildcards, or URL filter rules, e.g. \\\"example.org\\\", \\\"*.example.org\\\", or \\\"||example.org^\\\" correspondingly.\",\n  \"access_blocked_title\": \"Disallowed domains\",\n  \"access_desc\": \"Here you can configure access rules for the AdGuard Home DNS server\",\n  \"access_disallowed_desc\": \"A list of CIDRs, IP addresses, or <a>ClientIDs</a>. If this list has entries, AdGuard Home will drop requests from these clients. This field is ignored if there are entries in Allowed clients.\",\n  \"access_disallowed_title\": \"Disallowed clients\",\n  \"access_settings_saved\": \"Access settings successfully saved\",\n  \"access_title\": \"Access settings\",\n  \"actions_table_header\": \"Actions\",\n  \"add_allowlist\": \"Add allowlist\",\n  \"add_blocklist\": \"Add blocklist\",\n  \"add_custom_list\": \"Add a custom list\",\n  \"add_persistent_client\": \"Add as persistent client\",\n  \"address\": \"Address\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home will be dropping all DNS queries from this client.\",\n  \"all_lists_up_to_date_toast\": \"All lists are already up-to-date\",\n  \"all_queries\": \"All queries\",\n  \"allow_this_client\": \"Allow this client\",\n  \"allowed\": \"Allowed\",\n  \"anonymize_client_ip\": \"Anonymize client IP\",\n  \"anonymize_client_ip_desc\": \"Don't save the client's full IP address to logs or statistics\",\n  \"anonymizer_notification\": \"<0>Note:</0> IP anonymization is enabled. You can disable it in <1>General settings</1>.\",\n  \"answer\": \"Answer\",\n  \"apply_btn\": \"Apply\",\n  \"auto_clients_desc\": \"Information about IP addresses of devices that are using or may use AdGuard Home. This information is gathered from several sources, including hosts files, reverse DNS, etc.\",\n  \"auto_clients_title\": \"Runtime clients\",\n  \"autofix_warning_list\": \"It will perform these tasks: <0>Deactivate system DNSStubListener</0> <0>Set DNS server address to 127.0.0.1</0> <0>Replace symbolic link target of /etc/resolv.conf with /run/systemd/resolve/resolv.conf</0> <0>Stop DNSStubListener (reload systemd-resolved service)</0>\",\n  \"autofix_warning_result\": \"As a result all DNS requests from your system will be processed by AdGuard Home by default.\",\n  \"autofix_warning_text\": \"If you click \\\"Fix\\\", AdGuard Home will configure your system to use AdGuard Home DNS server.\",\n  \"average_processing_time\": \"Average processing time\",\n  \"average_processing_time_hint\": \"Average time in milliseconds on processing a DNS request\",\n  \"average_upstream_response_time\": \"Average upstream response time\",\n  \"back\": \"Back\",\n  \"block\": \"Block\",\n  \"block_all\": \"Block all\",\n  \"block_domain_use_filters_and_hosts\": \"Block domains using filters and hosts files\",\n  \"block_for_this_client_only\": \"Block for this client only\",\n  \"block_services\": \"Block specific services\",\n  \"blocked_adult_websites\": \"Blocked by Parental Control\",\n  \"blocked_by\": \"<0>Blocked by Filters</0>\",\n  \"blocked_by_cname_or_ip\": \"Blocked by CNAME or IP\",\n  \"blocked_by_response\": \"Blocked by CNAME or IP in response\",\n  \"blocked_response_ttl\": \"Blocked response TTL\",\n  \"blocked_response_ttl_desc\": \"Specifies for how many seconds the clients should cache a filtered response\",\n  \"blocked_safebrowsing\": \"Blocked by Safe Browsing\",\n  \"blocked_service\": \"Blocked service\",\n  \"blocked_services\": \"Blocked services\",\n  \"blocked_services_desc\": \"Allows to quickly block popular sites and services.\",\n  \"blocked_services_global\": \"Use global blocked services\",\n  \"blocked_services_saved\": \"Blocked services successfully saved\",\n  \"blocked_threats\": \"Blocked Threats\",\n  \"blocking_ipv4\": \"Blocking IPv4\",\n  \"blocking_ipv4_desc\": \"IP address to be returned for a blocked A request\",\n  \"blocking_ipv6\": \"Blocking IPv6\",\n  \"blocking_ipv6_desc\": \"IP address to be returned for a blocked AAAA request\",\n  \"blocking_mode\": \"Blocking mode\",\n  \"blocking_mode_custom_ip\": \"Custom IP: Respond with a manually set IP address\",\n  \"blocking_mode_default\": \"Default: Respond with zero IP address (0.0.0.0 for A; :: for AAAA) when blocked by Adblock-style rule; respond with the IP address specified in the rule when blocked by /etc/hosts-style rule\",\n  \"blocking_mode_null_ip\": \"Null IP: Respond with zero IP address (0.0.0.0 for A; :: for AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Respond with NXDOMAIN code\",\n  \"blocking_mode_refused\": \"REFUSED: Respond with REFUSED code\",\n  \"blocklist\": \"Blocklist\",\n  \"bootstrap_dns\": \"Bootstrap DNS servers\",\n  \"bootstrap_dns_desc\": \"IP addresses of DNS servers used to resolve IP addresses of the DoH/DoT resolvers you specify as upstreams. Comments are not permitted.\",\n  \"cache_cleared\": \"DNS cache successfully cleared\",\n  \"cache_enabled\": \"Enable cache\",\n  \"cache_enabled_desc\": \"Store DNS responses locally.\",\n  \"cache_optimistic\": \"Optimistic caching\",\n  \"cache_optimistic_desc\": \"Make AdGuard Home respond from the cache even when the entries are expired and also try to refresh them.\",\n  \"cache_size\": \"Cache size\",\n  \"cache_size_desc\": \"DNS cache size (in bytes).\",\n  \"cache_size_validation\": \"The cache size must be greater than zero when enabled.\",\n  \"cache_ttl_max_override\": \"Override maximum TTL\",\n  \"cache_ttl_max_override_desc\": \"Set a maximum time-to-live value (seconds) for entries in the DNS cache.\",\n  \"cache_ttl_min_override\": \"Override minimum TTL\",\n  \"cache_ttl_min_override_desc\": \"Extend short time-to-live values (seconds) received from the upstream server when caching DNS responses.\",\n  \"cancel_btn\": \"Cancel\",\n  \"category_label\": \"Category\",\n  \"check\": \"Check\",\n  \"check_client_id\": \"Client identifier (ClientID or IP address)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Check if a host name is filtered.\",\n  \"check_dhcp_servers\": \"Check for DHCP servers\",\n  \"check_dns_record\": \"Select DNS record type\",\n  \"check_enter_client_id\": \"Enter client identifier\",\n  \"check_hostname\": \"Hostname or domain name\",\n  \"check_ip\": \"IP addresses: {{ip}}\",\n  \"check_not_found\": \"Not found in your filter lists\",\n  \"check_reason\": \"Reason: {{reason}}\",\n  \"check_service\": \"Service name: {{service}}\",\n  \"check_title\": \"Check the filtering\",\n  \"check_updates_btn\": \"Check for updates\",\n  \"check_updates_now\": \"Check for updates now\",\n  \"choose_allowlist\": \"Choose allowlists\",\n  \"choose_blocklist\": \"Choose blocklists\",\n  \"choose_from_list\": \"Choose from the list\",\n  \"city\": \"City\",\n  \"clear_cache\": \"Clear cache\",\n  \"click_to_view_queries\": \"Click to view queries\",\n  \"client_add\": \"Add Client\",\n  \"client_added\": \"Client \\\"{{key}}\\\" successfully added\",\n  \"client_blocked\": \"Client \\\"{{ip}}\\\" successfully blocked\",\n  \"client_confirm_block\": \"Are you sure you want to block the client \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Are you sure you want to delete client \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Are you sure you want to unblock the client \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Client \\\"{{key}}\\\" successfully deleted\",\n  \"client_details\": \"Client details\",\n  \"client_edit\": \"Edit Client\",\n  \"client_global_settings\": \"Use global settings\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Clients can be identified by ClientID. Learn more about how to identify clients <a>here</a>.\",\n  \"client_id_placeholder\": \"Enter a ClientID\",\n  \"client_identifier\": \"Identifier\",\n  \"client_identifier_desc\": \"Clients can be identified by their IP address, CIDR, MAC address, or ClientID (can be used for DoT/DoH/DoQ). Learn more about how to identify clients <0>here</0>.\",\n  \"client_name\": \"Client {{id}}\",\n  \"client_new\": \"New Client\",\n  \"client_settings\": \"Client settings\",\n  \"client_table_header\": \"Client\",\n  \"client_unblocked\": \"Client \\\"{{ip}}\\\" successfully unblocked\",\n  \"client_updated\": \"Client \\\"{{key}}\\\" successfully updated\",\n  \"clients_desc\": \"Configure persistent client records for devices connected to AdGuard Home\",\n  \"clients_not_found\": \"No clients found\",\n  \"clients_title\": \"Persistent clients\",\n  \"compact\": \"Compact\",\n  \"config_successfully_saved\": \"Configuration successfully saved\",\n  \"configure\": \"Configure\",\n  \"confirm_dns_cache_clear\": \"Are you sure you want to clear DNS cache?\",\n  \"confirm_static_ip\": \"AdGuard Home will configure {{ip}} to be your static IP address. Do you want to proceed?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"Country\",\n  \"custom_filter_rules\": \"Custom filtering rules\",\n  \"custom_filter_rules_hint\": \"Enter one rule on a line. You can use either adblock rules or hosts files syntax.\",\n  \"custom_filtering_rules\": \"Custom filtering rules\",\n  \"custom_ip\": \"Custom IP\",\n  \"custom_retention_input\": \"Enter retention in hours\",\n  \"custom_rotation_input\": \"Enter rotation in hours\",\n  \"dashboard\": \"Dashboard\",\n  \"date\": \"Date\",\n  \"default\": \"Default\",\n  \"delete_confirm\": \"Are you sure you want to delete \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Delete\",\n  \"descr\": \"Description\",\n  \"details\": \"Details\",\n  \"dhcp_add_static_lease\": \"Add static lease\",\n  \"dhcp_config_saved\": \"DHCP configuration successfully saved\",\n  \"dhcp_description\": \"If your router does not provide DHCP settings, you can use AdGuard's own built-in DHCP server.\",\n  \"dhcp_disable\": \"Disable DHCP server\",\n  \"dhcp_dynamic_ip_found\": \"Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}</0>. In order to use DHCP server, a static IP address must be set. Your current IP address is <0>{{ipAddress}}</0>. AdGuard Home will automatically set this IP address as static if you press the \\\"Enable DHCP server\\\" button.\",\n  \"dhcp_edit_static_lease\": \"Edit static lease\",\n  \"dhcp_enable\": \"Enable DHCP server\",\n  \"dhcp_error\": \"AdGuard Home could not determine if there is another active DHCP server on the network\",\n  \"dhcp_form_gateway_input\": \"Gateway IP\",\n  \"dhcp_form_lease_input\": \"Lease duration\",\n  \"dhcp_form_lease_title\": \"DHCP lease time (in seconds)\",\n  \"dhcp_form_range_end\": \"Range end\",\n  \"dhcp_form_range_start\": \"Range start\",\n  \"dhcp_form_range_title\": \"Range of IP addresses\",\n  \"dhcp_form_subnet_input\": \"Subnet mask\",\n  \"dhcp_found\": \"An active DHCP server is found on the network. It is not safe to enable the built-in DHCP server.\",\n  \"dhcp_hardware_address\": \"Hardware address\",\n  \"dhcp_interface_select\": \"Select DHCP interface\",\n  \"dhcp_ip_addresses\": \"IP addresses\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 Settings\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 Settings\",\n  \"dhcp_lease_added\": \"Static lease \\\"{{key}}\\\" successfully added\",\n  \"dhcp_lease_deleted\": \"Static lease \\\"{{key}}\\\" successfully deleted\",\n  \"dhcp_lease_updated\": \"Static lease \\\"{{key}}\\\" successfully updated\",\n  \"dhcp_leases\": \"DHCP leases\",\n  \"dhcp_leases_not_found\": \"No DHCP leases found\",\n  \"dhcp_new_static_lease\": \"New static lease\",\n  \"dhcp_not_found\": \"It is safe to enable the built-in DHCP server because AdGuard Home didn't find any active DHCP servers on the network. However, you should re-check that manually as the automatic probing doesn't currently provide a 100% guarantee.\",\n  \"dhcp_reset\": \"Are you sure you want to reset the DHCP configuration?\",\n  \"dhcp_reset_leases\": \"Reset all leases\",\n  \"dhcp_reset_leases_confirm\": \"Are you sure you want to reset all leases?\",\n  \"dhcp_reset_leases_success\": \"DHCP leases successfully reset\",\n  \"dhcp_settings\": \"DHCP settings\",\n  \"dhcp_static_ip_error\": \"In order to use DHCP server a static IP address must be set. AdGuard Home failed to determine if this network interface is configured using a static IP address. Please set a static IP address manually.\",\n  \"dhcp_static_leases\": \"DHCP static leases\",\n  \"dhcp_static_leases_not_found\": \"No DHCP static leases found\",\n  \"dhcp_table_expires\": \"Expires\",\n  \"dhcp_table_hostname\": \"Hostname\",\n  \"dhcp_title\": \"DHCP server (experimental!)\",\n  \"dhcp_warning\": \"If you want to enable DHCP server anyway, make sure that there is no other active DHCP server in your network, as this may break the Internet connectivity for devices on the network!\",\n  \"disable_for_hours\": \"For {{count}} hour\",\n  \"disable_for_hours_plural\": \"For {{count}} hours\",\n  \"disable_for_minutes\": \"For {{count}} minute\",\n  \"disable_for_minutes_plural\": \"For {{count}} minutes\",\n  \"disable_for_seconds\": \"For {{count}} second\",\n  \"disable_for_seconds_plural\": \"For {{count}} seconds\",\n  \"disable_ipv6\": \"Disable resolving of IPv6 addresses\",\n  \"disable_ipv6_desc\": \"Drop all DNS queries for IPv6 addresses (type AAAA) and remove IPv6 hints from HTTPS responses.\",\n  \"disable_notify_for_hours\": \"Disable protection for {{count}} hour\",\n  \"disable_notify_for_hours_plural\": \"Disable protection for {{count}} hours\",\n  \"disable_notify_for_minutes\": \"Disable protection for {{count}} minute\",\n  \"disable_notify_for_minutes_plural\": \"Disable protection for {{count}} minutes\",\n  \"disable_notify_for_seconds\": \"Disable protection for {{count}} second\",\n  \"disable_notify_for_seconds_plural\": \"Disable protection for {{count}} seconds\",\n  \"disable_notify_until_tomorrow\": \"Disable protection until tomorrow\",\n  \"disable_protection\": \"Disable protection\",\n  \"disable_rewrites\": \"Disable rewrite rules\",\n  \"disable_until_tomorrow\": \"Until tomorrow\",\n  \"disabled\": \"Disabled\",\n  \"disabled_dhcp\": \"DHCP server disabled\",\n  \"disabled_filtering_toast\": \"Disabled filtering\",\n  \"disabled_parental_toast\": \"Disabled Parental Control\",\n  \"disabled_protection\": \"Disabled protection\",\n  \"disabled_safe_browsing_toast\": \"Disabled Safe Browsing\",\n  \"disabled_safe_search_toast\": \"Disabled Safe Search\",\n  \"disallow_this_client\": \"Disallow this client\",\n  \"dns_addresses\": \"DNS addresses\",\n  \"dns_allowlists\": \"DNS allowlists\",\n  \"dns_allowlists_desc\": \"Domains from DNS allowlists will be allowed even if they are in any of the blocklists.\",\n  \"dns_blocklists\": \"DNS blocklists\",\n  \"dns_blocklists_desc\": \"AdGuard Home will block domains matching the blocklists.\",\n  \"dns_cache_config\": \"DNS cache configuration\",\n  \"dns_cache_config_desc\": \"Here you can configure DNS cache\",\n  \"dns_cache_size\": \"DNS cache size, in bytes\",\n  \"dns_config\": \"DNS server configuration\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS Privacy\",\n  \"dns_providers\": \"Here is a <0>list of known DNS providers</0> to choose from.\",\n  \"dns_query\": \"DNS Queries\",\n  \"dns_rewrites\": \"DNS rewrites\",\n  \"dns_settings\": \"DNS settings\",\n  \"dns_start\": \"DNS server is starting up\",\n  \"dns_status_error\": \"Error checking the DNS server status\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": could not be used, please check that you've written it correctly\",\n  \"dns_test_ok_toast\": \"Specified DNS servers are working correctly\",\n  \"dns_test_parsing_error_toast\": \"Section {{section}}: line {{line}}: could not be used, please check that you've written it correctly\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" does not respond to test requests and may not work properly\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Enable DNSSEC\",\n  \"dnssec_enable_desc\": \"Set DNSSEC flag in the outcoming DNS queries and check the result (DNSSEC-enabled resolver is required).\",\n  \"domain\": \"Domain\",\n  \"domain_desc\": \"Enter the domain name or wildcard you want to be rewritten.\",\n  \"domain_name_table_header\": \"Domain name\",\n  \"domain_or_client\": \"Domain or client\",\n  \"down\": \"Down\",\n  \"download_mobileconfig\": \"Download configuration file\",\n  \"download_mobileconfig_doh\": \"Download .mobileconfig for DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Download .mobileconfig for DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Edit allowlist\",\n  \"edit_blocklist\": \"Edit blocklist\",\n  \"edit_table_action\": \"Edit\",\n  \"edns_cs_desc\": \"Add the EDNS Client Subnet option (ECS) to upstream requests and log the values sent by the clients in the query log.\",\n  \"edns_enable\": \"Enable EDNS client subnet\",\n  \"edns_use_custom_ip\": \"Use custom IP for EDNS\",\n  \"edns_use_custom_ip_desc\": \"Allow to use custom IP for EDNS\",\n  \"elapsed\": \"Elapsed\",\n  \"empty_response_status\": \"Empty\",\n  \"enable_protection\": \"Enable protection\",\n  \"enable_protection_timer\": \"Protection will be enabled in {{time}}\",\n  \"enable_rewrites\": \"Enable rewrite rules\",\n  \"enable_upstream_dns_cache\": \"Enable DNS caching for this client's custom upstream configuration\",\n  \"enabled_dhcp\": \"DHCP server enabled\",\n  \"enabled_filtering_toast\": \"Enabled filtering\",\n  \"enabled_parental_toast\": \"Enabled Parental Control\",\n  \"enabled_protection\": \"Enabled protection\",\n  \"enabled_safe_browsing_toast\": \"Enabled Safe Browsing\",\n  \"enabled_save_search_toast\": \"Enabled Safe Search\",\n  \"enabled_table_header\": \"Enabled\",\n  \"encryption_certificate_path\": \"Certificate path\",\n  \"encryption_certificates\": \"Certificates\",\n  \"encryption_certificates_desc\": \"In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.\",\n  \"encryption_certificates_input\": \"Copy/paste your PEM-encoded certificates here.\",\n  \"encryption_certificates_source_content\": \"Paste the certificates contents\",\n  \"encryption_certificates_source_path\": \"Set a certificates file path\",\n  \"encryption_chain_invalid\": \"Certificate chain is invalid\",\n  \"encryption_chain_valid\": \"Certificate chain is valid\",\n  \"encryption_config_saved\": \"Encryption configuration saved\",\n  \"encryption_desc\": \"Encryption (HTTPS/QUIC/TLS) support for both DNS and admin web interface\",\n  \"encryption_doq\": \"DNS-over-QUIC port\",\n  \"encryption_doq_desc\": \"If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port.\",\n  \"encryption_dot\": \"DNS-over-TLS port\",\n  \"encryption_dot_desc\": \"If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.\",\n  \"encryption_enable\": \"Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.\",\n  \"encryption_expire\": \"Expires\",\n  \"encryption_hostnames\": \"Hostnames\",\n  \"encryption_https\": \"HTTPS port\",\n  \"encryption_https_desc\": \"If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '/dns-query' location.\",\n  \"encryption_issuer\": \"Issuer\",\n  \"encryption_key\": \"Private key\",\n  \"encryption_key_input\": \"Copy/paste your PEM-encoded private key for your certificate here.\",\n  \"encryption_key_invalid\": \"This is an invalid {{type}} private key\",\n  \"encryption_key_source_content\": \"Paste the private key contents\",\n  \"encryption_key_source_path\": \"Set a private key file path\",\n  \"encryption_key_valid\": \"This is a valid {{type}} private key\",\n  \"encryption_plain_dns_desc\": \"Plain DNS is enabled by default. You can disable it to force all devices to use encrypted DNS. To do this, you must enable at least one encrypted DNS protocol\",\n  \"encryption_plain_dns_enable\": \"Enable plain DNS\",\n  \"encryption_plain_dns_error\": \"To disable plain DNS, enable at least one encrypted DNS protocol\",\n  \"encryption_private_key_path\": \"Private key path\",\n  \"encryption_redirect\": \"Redirect to HTTPS automatically\",\n  \"encryption_redirect_desc\": \"If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.\",\n  \"encryption_reset\": \"Are you sure you want to reset encryption settings?\",\n  \"encryption_server\": \"Server name\",\n  \"encryption_server_desc\": \"If set, AdGuard Home detects ClientIDs, responds to DDR queries, and performs additional connection validations. If not set, these features are disabled. Must match one of the DNS Names in the certificate.\",\n  \"encryption_server_enter\": \"Enter your domain name\",\n  \"encryption_settings\": \"Encryption settings\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Subject\",\n  \"encryption_title\": \"Encryption\",\n  \"encryption_warning\": \"Warning\",\n  \"enforce_safe_search\": \"Use Safe Search\",\n  \"enforce_save_search_hint\": \"AdGuard Home will enforce safe search in the following search engines: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Enforced safe search\",\n  \"enter_cache_size\": \"Enter cache size (bytes)\",\n  \"enter_cache_ttl_max_override\": \"Enter maximum TTL (seconds)\",\n  \"enter_cache_ttl_min_override\": \"Enter minimum TTL (seconds)\",\n  \"enter_name_hint\": \"Enter name\",\n  \"enter_url_or_path_hint\": \"Enter a URL or an absolute path of the list\",\n  \"enter_valid_allowlist\": \"Enter a valid URL to the allowlist.\",\n  \"enter_valid_blocklist\": \"Enter a valid URL to the blocklist.\",\n  \"error_details\": \"Error details\",\n  \"example_comment\": \"! Here goes a comment.\",\n  \"example_comment_hash\": \"# Also a comment.\",\n  \"example_comment_meaning\": \"just a comment;\",\n  \"example_meaning_filter_block\": \"block access to example.org and all its subdomains;\",\n  \"example_meaning_filter_whitelist\": \"unblock access to example.org and all its subdomains;\",\n  \"example_meaning_host_block\": \"respond with 127.0.0.1 for example.org (but not for its subdomains);\",\n  \"example_multiple_upstreams_reserved\": \"multiple upstreams <0>for specific domains</0>;\",\n  \"example_regex_meaning\": \"block access to domains matching the specified regular expression.\",\n  \"example_rewrite_domain\": \"rewrite responses for this domain name only.\",\n  \"example_rewrite_wildcard\": \"rewrite responses for all <0>example.org</0> subdomains.\",\n  \"example_upstream_comment\": \"a comment.\",\n  \"example_upstream_doh\": \"encrypted <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"encrypted DNS-over-HTTPS with forced <0>HTTP/3</0> and no fallback to HTTP/2 or below;\",\n  \"example_upstream_doq\": \"encrypted <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"encrypted <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"regular DNS (over UDP);\",\n  \"example_upstream_regular_port\": \"regular DNS (over UDP, with port);\",\n  \"example_upstream_reserved\": \"an upstream <0>for specific domains</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;\",\n  \"example_upstream_tcp\": \"regular DNS (over TCP);\",\n  \"example_upstream_tcp_hostname\": \"regular DNS (over TCP, hostname);\",\n  \"example_upstream_tcp_port\": \"regular DNS (over TCP, with port);\",\n  \"example_upstream_udp\": \"regular DNS (over UDP, hostname);\",\n  \"examples_title\": \"Examples\",\n  \"fallback_dns_desc\": \"List of fallback DNS servers used when upstream DNS servers are not responding. The syntax is the same as in the main upstreams field above.\",\n  \"fallback_dns_placeholder\": \"Enter one fallback DNS server per line\",\n  \"fallback_dns_title\": \"Fallback DNS servers\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Fastest IP address\",\n  \"fastest_addr_desc\": \"Wait for responses from <b>all</b> DNS servers, measure the TCP connection speed for each server, and return the IP address of the server with the fastest connection speed.<br/>This mode can significantly slow down DNS queries, if one or more upstream servers are not responding. Make sure that your upstream servers are stable and your upstream timeout is low.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"The list has been successfully added\",\n  \"filter_allowlist\": \"WARNING: This action also will exclude the rule \\\"{{disallowed_rule}}\\\" from the list of allowed clients.\",\n  \"filter_category_general\": \"General\",\n  \"filter_category_general_desc\": \"Lists that block tracking and advertising on most of the devices\",\n  \"filter_category_other\": \"Other\",\n  \"filter_category_other_desc\": \"Other blocklists\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Lists that focus on regional ads and tracking servers\",\n  \"filter_category_security\": \"Security\",\n  \"filter_category_security_desc\": \"Lists designed specifically to block malicious, phishing, and scam domains\",\n  \"filter_removed_successfully\": \"The list has been successfully removed\",\n  \"filter_updated\": \"The list has been successfully updated\",\n  \"filtered\": \"Filtered\",\n  \"filtered_custom_rules\": \"Filtered by Custom filtering rules\",\n  \"filtering_rules_learn_more\": \"<0>Learn more</0> about creating your own hosts lists.\",\n  \"filters\": \"Filters\",\n  \"filters_and_hosts_hint\": \"AdGuard Home understands basic adblock rules and hosts files syntax.\",\n  \"filters_block_toggle_hint\": \"You can setup blocking rules in the <a>Filters</a> settings.\",\n  \"filters_configuration\": \"Filters configuration\",\n  \"filters_enable\": \"Enable filters\",\n  \"filters_interval\": \"Filter update interval\",\n  \"fix\": \"Fix\",\n  \"for_last_days\": \"for the last {{count}} day\",\n  \"for_last_days_plural\": \"for the last {{count}} days\",\n  \"for_last_hours\": \"for the last {{count}} hour\",\n  \"for_last_hours_plural\": \"for the last {{count}} hours\",\n  \"forgot_password\": \"Forgot password?\",\n  \"forgot_password_desc\": \"Please follow <0>these steps</0> to create a new password for your user account.\",\n  \"form_add_id\": \"Add identifier\",\n  \"form_answer\": \"Enter IP address or domain name\",\n  \"form_client_name\": \"Enter client name\",\n  \"form_domain\": \"Enter domain name or wildcard\",\n  \"form_enter_blocked_response_ttl\": \"Enter blocked response TTL (seconds)\",\n  \"form_enter_host\": \"Enter a host name\",\n  \"form_enter_hostname\": \"Enter hostname\",\n  \"form_enter_id\": \"Enter identifier\",\n  \"form_enter_ip\": \"Enter IP\",\n  \"form_enter_mac\": \"Enter MAC\",\n  \"form_enter_rate_limit\": \"Enter rate limit\",\n  \"form_enter_rate_limit_subnet_len\": \"Enter subnet prefix length for rate limiting\",\n  \"form_enter_subnet_ip\": \"Enter an IP address in the subnet \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Enter the upstream server timeout duration in seconds\",\n  \"form_error_answer_format\": \"Invalid answer format\",\n  \"form_error_client_id_format\": \"ClientID must contain only numbers, lowercase letters, and hyphens\",\n  \"form_error_domain_format\": \"Invalid domain format\",\n  \"form_error_equal\": \"Must not be equal\",\n  \"form_error_gateway_ip\": \"Lease can't have the IP address of the gateway\",\n  \"form_error_ip4_format\": \"Invalid IPv4 address\",\n  \"form_error_ip4_gateway_format\": \"Invalid IPv4 address of the gateway\",\n  \"form_error_ip6_format\": \"Invalid IPv6 address\",\n  \"form_error_ip_format\": \"Invalid IP address\",\n  \"form_error_mac_format\": \"Invalid MAC address\",\n  \"form_error_password\": \"Password mismatch\",\n  \"form_error_password_length\": \"Password must be {{min}} to {{max}} characters long\",\n  \"form_error_port\": \"Enter valid port number\",\n  \"form_error_port_range\": \"Enter port number in the range of 80-65535\",\n  \"form_error_port_unsafe\": \"Unsafe port\",\n  \"form_error_positive\": \"Must be greater than 0\",\n  \"form_error_required\": \"Required field\",\n  \"form_error_server_name\": \"Invalid server name\",\n  \"form_error_subnet\": \"Subnet \\\"{{cidr}}\\\" does not contain the IP address \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Invalid URL format\",\n  \"form_error_url_or_path_format\": \"Invalid URL or absolute path of the list\",\n  \"form_select_tags\": \"Select client tags\",\n  \"found_in_known_domain_db\": \"Found in the known domains database.\",\n  \"friday\": \"Friday\",\n  \"friday_short\": \"Fri\",\n  \"gateway_or_subnet_invalid\": \"Invalid subnet mask\",\n  \"general_settings\": \"General settings\",\n  \"general_statistics\": \"General statistics\",\n  \"get_started\": \"Get Started\",\n  \"greater_range_start_error\": \"Must be greater than range start\",\n  \"homepage\": \"Homepage\",\n  \"host_whitelisted\": \"The host is allowed\",\n  \"ignore_domains\": \"Ignored domains (separated by newline)\",\n  \"ignore_domains_desc_query\": \"Queries matching these rules are not written to the query log\",\n  \"ignore_domains_desc_stats\": \"Queries matching these rules are not written to the statistics\",\n  \"ignore_domains_title\": \"Ignored domains\",\n  \"ignore_query_log\": \"Ignore this client in query log\",\n  \"ignore_statistics\": \"Ignore this client in statistics\",\n  \"install_auth_confirm\": \"Confirm password\",\n  \"install_auth_desc\": \"Password authentication to your AdGuard Home admin web interface must be configured. Even if AdGuard Home is accessible only in your local network, it is still important to protect it from unrestricted access.\",\n  \"install_auth_password\": \"Password\",\n  \"install_auth_password_enter\": \"Enter password\",\n  \"install_auth_title\": \"Authentication\",\n  \"install_auth_username\": \"Username\",\n  \"install_auth_username_enter\": \"Enter username\",\n  \"install_devices_address\": \"AdGuard Home DNS server is listening on the following addresses\",\n  \"install_devices_android_list_1\": \"From the Android Menu home screen, tap Settings.\",\n  \"install_devices_android_list_2\": \"Tap Wi-Fi on the menu. The screen listing all of the available networks will be shown (it is impossible to set custom DNS for mobile connection).\",\n  \"install_devices_android_list_3\": \"Long press the network you're connected to, and tap Modify Network.\",\n  \"install_devices_android_list_4\": \"On some devices, you may need to check the box for Advanced to see further settings. To adjust your Android DNS settings, you will need to switch the IP settings from DHCP to Static.\",\n  \"install_devices_android_list_5\": \"Change DNS 1 and DNS 2 values to your AdGuard Home server addresses.\",\n  \"install_devices_desc\": \"To start using AdGuard Home, you need to configure your devices to use it.\",\n  \"install_devices_ios_list_1\": \"From the home screen, tap Settings.\",\n  \"install_devices_ios_list_2\": \"Choose Wi-Fi in the left menu (it is impossible to configure DNS for mobile networks).\",\n  \"install_devices_ios_list_3\": \"Tap on the name of the currently active network.\",\n  \"install_devices_ios_list_4\": \"In the DNS field enter your AdGuard Home server addresses.\",\n  \"install_devices_macos_list_1\": \"Click the Apple icon and go to System Preferences.\",\n  \"install_devices_macos_list_2\": \"Click Network.\",\n  \"install_devices_macos_list_3\": \"Select the first connection in your list and click Advanced.\",\n  \"install_devices_macos_list_4\": \"Select the DNS tab and enter your AdGuard Home server addresses.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"This setup automatically covers all devices connected to your home router, no need to configure each of them manually.\",\n  \"install_devices_router_list_1\": \"Open the preferences for your router. Usually, you can access it from your browser via a URL, such as http://192.168.0.1/ or http://192.168.1.1/. You may be prompted to enter a password. If you don't remember it, you can often reset the password by pressing a button on the router itself, but be aware that if this procedure is chosen, you will probably lose the entire router configuration. If your router requires an app to set it up, please install the app on your phone or PC and use it to access the router’s settings.\",\n  \"install_devices_router_list_2\": \"Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.\",\n  \"install_devices_router_list_3\": \"Enter your AdGuard Home server addresses there.\",\n  \"install_devices_router_list_4\": \"On some router types, a custom DNS server cannot be set up. In that case, setting up AdGuard Home as a <0>DHCP server</0> may help. Otherwise, you should check the router manual on how to customize DNS servers on your specific router model.\",\n  \"install_devices_title\": \"Configure your devices\",\n  \"install_devices_windows_list_1\": \"Open Control Panel through Start menu or Windows search.\",\n  \"install_devices_windows_list_2\": \"Go to Network and Internet category and then to Network and Sharing Center.\",\n  \"install_devices_windows_list_3\": \"In the left panel, click \\\"Change adapter settings\\\".\",\n  \"install_devices_windows_list_4\": \"Right-click your active connection and select Properties.\",\n  \"install_devices_windows_list_5\": \"Find \\\"Internet Protocol Version 4 (TCP/IPv4)\\\" (or, for IPv6, \\\"Internet Protocol Version 6 (TCP/IPv6)\\\") in the list, select it and then click on Properties again.\",\n  \"install_devices_windows_list_6\": \"Choose \\\"Use the following DNS server addresses\\\" and enter your AdGuard Home server addresses.\",\n  \"install_saved\": \"Saved successfully\",\n  \"install_settings_all_interfaces\": \"All interfaces\",\n  \"install_settings_dns\": \"DNS server\",\n  \"install_settings_dns_desc\": \"You will need to configure your devices or router to use the DNS server on the following addresses:\",\n  \"install_settings_interface_link\": \"Your AdGuard Home admin web interface will be available on the following addresses:\",\n  \"install_settings_listen\": \"Listen interface\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Admin Web Interface\",\n  \"install_static_configure\": \"AdGuard Home has detected that the dynamic IP address <0>{{ip}}</0> is used. Do you want it to be set as your static address?\",\n  \"install_static_error\": \"AdGuard Home cannot configure it automatically for this network interface. Please look for an instruction on how to do this manually.\",\n  \"install_static_ok\": \"Good news! The static IP address is already configured\",\n  \"install_step\": \"Step\",\n  \"install_submit_desc\": \"The setup procedure is complete and you're now ready to start using AdGuard Home.\",\n  \"install_submit_title\": \"Congratulations!\",\n  \"install_welcome_desc\": \"AdGuard Home is a network-wide ad-and-tracker blocking DNS server. Its purpose is to let you control your entire network and all your devices, and it does not require using a client-side program.\",\n  \"install_welcome_title\": \"Welcome to AdGuard Home!\",\n  \"interval_24_hour\": \"24 hours\",\n  \"interval_6_hour\": \"6 hours\",\n  \"interval_days\": \"{{count}} day\",\n  \"interval_days_plural\": \"{{count}} days\",\n  \"interval_hours\": \"{{count}} hour\",\n  \"interval_hours_plural\": \"{{count}} hours\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP address\",\n  \"known_tracker\": \"Known tracker\",\n  \"last_rule_in_allowlist\": \"Cannot disallow this client because excluding the rule \\\"{{disallowed_rule}}\\\" will DISABLE \\\"Allowed clients\\\" list.\",\n  \"last_time_updated_table_header\": \"Last time updated\",\n  \"list_confirm_delete\": \"Are you sure you want to delete this list?\",\n  \"list_label\": \"List\",\n  \"list_updated\": \"{{count}} list updated\",\n  \"list_updated_plural\": \"{{count}} lists updated\",\n  \"list_url_table_header\": \"List URL\",\n  \"load_balancing\": \"Load-balancing\",\n  \"load_balancing_desc\": \"Query one upstream server at a time.<br/>AdGuard Home uses a weighted random algorithm to select servers with the lowest number of failed lookups and the lowest average lookup time.\",\n  \"loading_table_status\": \"Loading...\",\n  \"local_ptr_default_resolver\": \"By default, AdGuard Home uses the following reverse DNS resolvers: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS servers used by AdGuard Home for private PTR, SOA, and NS requests. A request is considered private if it asks for an ARPA domain containing a subnet within private IP ranges (such as \\\"192.168.12.34\\\") and comes from a client with a private IP address. If not set, the default DNS resolvers of your OS will be used, except for the AdGuard Home IP addresses.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home could not determine suitable private reverse DNS resolvers for this system.\",\n  \"local_ptr_placeholder\": \"Enter one IP address per line\",\n  \"local_ptr_title\": \"Private reverse DNS servers\",\n  \"location\": \"Location\",\n  \"log_and_stats_section_label\": \"Query log and statistics\",\n  \"lower_range_start_error\": \"Must be lower than range start\",\n  \"main_settings\": \"Main settings\",\n  \"make_static\": \"Make static\",\n  \"manual_update\": \"Please <a>follow these steps</a> to update manually.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Monday\",\n  \"monday_short\": \"Mon\",\n  \"name\": \"Name\",\n  \"name_table_header\": \"Name\",\n  \"netname\": \"Network name\",\n  \"network\": \"Network\",\n  \"new_allowlist\": \"New allowlist\",\n  \"new_blocklist\": \"New blocklist\",\n  \"next\": \"Next\",\n  \"next_btn\": \"Next\",\n  \"no_blocklist_added\": \"No blocklists added\",\n  \"no_clients_found\": \"No clients found\",\n  \"no_domains_found\": \"No domains found\",\n  \"no_logs_found\": \"No logs found\",\n  \"no_servers_specified\": \"No servers specified\",\n  \"no_upstreams_data_found\": \"No upstreams data found\",\n  \"no_whitelist_added\": \"No allowlists added\",\n  \"nothing_found\": \"Nothing found\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"The number of DNS requests blocked by adblock filters and hosts blocklists\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"The number of adult websites blocked\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"The number of DNS requests blocked by the AdGuard browsing security module\",\n  \"number_of_dns_query_days\": \"The number of DNS queries processed for the last {{count}} day\",\n  \"number_of_dns_query_days_plural\": \"The number of DNS queries processed for the last {{count}} days\",\n  \"number_of_dns_query_hours\": \"The number of DNS queries processed for the last {{count}} hour\",\n  \"number_of_dns_query_hours_plural\": \"The number of DNS queries processed for the last {{count}} hours\",\n  \"number_of_dns_query_to_safe_search\": \"The number of DNS requests to search engines for which Safe Search was enforced\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"OFF\",\n  \"on\": \"ON\",\n  \"open_dashboard\": \"Open Dashboard\",\n  \"orgname\": \"Organization name\",\n  \"original_response\": \"Original response\",\n  \"out_of_range_error\": \"Must be out of range \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Page\",\n  \"parallel_requests\": \"Parallel requests\",\n  \"parental_control\": \"Parental Control\",\n  \"password_label\": \"Password\",\n  \"password_placeholder\": \"Enter password\",\n  \"plain_dns\": \"Plain DNS\",\n  \"port_53_faq_link\": \"Port 53 is often occupied by \\\"DNSStubListener\\\" or \\\"systemd-resolved\\\" services. Please read <0>this instruction</0> on how to resolve this.\",\n  \"previous_btn\": \"Previous\",\n  \"privacy_policy\": \"Privacy Policy\",\n  \"processing_update\": \"Please wait, AdGuard Home is being updated\",\n  \"protection_section_label\": \"Protection\",\n  \"protocol\": \"Protocol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Query Log\",\n  \"query_log_clear\": \"Clear query logs\",\n  \"query_log_cleared\": \"The query log has been successfully cleared\",\n  \"query_log_configuration\": \"Logs configuration\",\n  \"query_log_confirm_clear\": \"Are you sure you want to clear the entire query log?\",\n  \"query_log_disabled\": \"The query log is disabled and can be configured in the <0>settings</0>\",\n  \"query_log_enable\": \"Enable log\",\n  \"query_log_filtered\": \"Filtered by {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Query logs rotation\",\n  \"query_log_retention_confirm\": \"Are you sure you want to change query log rotation? If you decrease the interval value, some data will be lost\",\n  \"query_log_strict_search\": \"Use double quotes for strict search\",\n  \"query_log_updated\": \"The query log has been successfully updated\",\n  \"rate_limit\": \"Rate limit\",\n  \"rate_limit_desc\": \"The number of requests per second allowed per client. Setting it to 0 means no limit.\",\n  \"rate_limit_subnet_len_ipv4\": \"Subnet prefix length for IPv4 addresses\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Subnet prefix length for IPv4 addresses used for rate limiting. The default is 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"The IPv4 subnet prefix length should be between 0 and 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Subnet prefix length for IPv6 addresses\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Subnet prefix length for IPv6 addresses used for rate limiting. The default is 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"The IPv6 subnet prefix length should be between 0 and 128\",\n  \"rate_limit_whitelist\": \"Rate limiting allowlist\",\n  \"rate_limit_whitelist_desc\": \"IP addresses excluded from rate limiting\",\n  \"rate_limit_whitelist_placeholder\": \"Enter one IP address per line\",\n  \"refresh_btn\": \"Refresh\",\n  \"refresh_statics\": \"Refresh statistics\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Report an issue\",\n  \"request_details\": \"Request details\",\n  \"request_table_header\": \"Request\",\n  \"requests_count\": \"Requests count\",\n  \"reset_settings\": \"Reset settings\",\n  \"resolve_clients_desc\": \"Reversely resolve clients' IP addresses into their hostnames by sending PTR queries to corresponding resolvers (private DNS servers for local clients, upstream servers for clients with public IP addresses).\",\n  \"resolve_clients_title\": \"Enable reverse resolving of clients' IP addresses\",\n  \"response_code\": \"Response code\",\n  \"response_details\": \"Response details\",\n  \"response_table_header\": \"Response\",\n  \"response_time\": \"Response time\",\n  \"rewrite_A\": \"<0>A</0>: special value, keep <0>A</0> records from the upstream\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: special value, keep <0>AAAA</0> records from the upstream\",\n  \"rewrite_add\": \"Add DNS rewrite\",\n  \"rewrite_added\": \"DNS rewrite for \\\"{{key}}\\\" successfully added\",\n  \"rewrite_applied\": \"Rewrite rule is applied\",\n  \"rewrite_confirm_delete\": \"Are you sure you want to delete DNS rewrite for \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"DNS rewrite for \\\"{{key}}\\\" successfully deleted\",\n  \"rewrite_desc\": \"Allows to easily configure custom DNS response for a specific domain name.\",\n  \"rewrite_domain_name\": \"Domain name: add a CNAME record\",\n  \"rewrite_edit\": \"Edit DNS rewrite\",\n  \"rewrite_hosts_applied\": \"Rewritten by the hosts file rule\",\n  \"rewrite_ip_address\": \"IP address: use this IP in an A or AAAA response\",\n  \"rewrite_not_found\": \"No DNS rewrites found\",\n  \"rewrite_settings_updated\": \"DNS rewrite settings successfully updated\",\n  \"rewrite_updated\": \"DNS rewrite successfully updated\",\n  \"rewrites_disabled_table_header\": \"Rewrites are disabled\",\n  \"rewrites_enabled_table_header\": \"Rewrites are enabled\",\n  \"rewritten\": \"Rewritten\",\n  \"rows_table_footer_text\": \"rows\",\n  \"rule_added_to_custom_filtering_toast\": \"Rule added to the custom filtering rules: {{rule}}\",\n  \"rule_label\": \"Rule(s)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Rule removed from the custom filtering rules: {{rule}}\",\n  \"rules_count_table_header\": \"Rules count\",\n  \"safe_browsing\": \"Safe Browsing\",\n  \"safe_search\": \"Safe Search\",\n  \"saturday\": \"Saturday\",\n  \"saturday_short\": \"Sat\",\n  \"save_btn\": \"Save\",\n  \"save_config\": \"Save configuration\",\n  \"schedule_add\": \"Add schedule\",\n  \"schedule_current_timezone\": \"Current time zone: {{value}}\",\n  \"schedule_desc\": \"Set inactivity periods for blocked services\",\n  \"schedule_edit\": \"Edit schedule\",\n  \"schedule_from\": \"From\",\n  \"schedule_invalid_select\": \"Start time must be before end time\",\n  \"schedule_modal_description\": \"This schedule will replace any existing schedules for the same day of the week. Each day of the week can have only one inactivity period.\",\n  \"schedule_modal_time_off\": \"No service blocking:\",\n  \"schedule_new\": \"New schedule\",\n  \"schedule_remove\": \"Remove schedule\",\n  \"schedule_save\": \"Save schedule\",\n  \"schedule_select_days\": \"Select days\",\n  \"schedule_services\": \"Pause service blocking\",\n  \"schedule_services_desc\": \"Configure the pause schedule of the service-blocking filter\",\n  \"schedule_services_desc_client\": \"Configure the pause schedule of the service-blocking filter for this client\",\n  \"schedule_time_all_day\": \"All day\",\n  \"schedule_timezone\": \"Select a time zone\",\n  \"schedule_to\": \"To\",\n  \"served_from_cache_label\": \"Served from cache\",\n  \"service_name\": \"Service name\",\n  \"set_static_ip\": \"Set a static IP address\",\n  \"settings\": \"Settings\",\n  \"settings_custom\": \"Custom\",\n  \"settings_global\": \"Global\",\n  \"setup_config_to_enable_dhcp_server\": \"Setup configuration to enable DHCP server\",\n  \"setup_dns_notice\": \"In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Use <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Use <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_3\": \"<0>Here's a list of software you can use.</0>\",\n  \"setup_dns_privacy_4\": \"On an iOS 14 or macOS Big Sur device you can download special '.mobileconfig' file that adds <highlight>DNS-over-HTTPS</highlight> or <highlight>DNS-over-TLS</highlight> servers to the DNS settings.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 supports DNS-over-TLS natively. To configure it, go to Settings → Network & internet → Advanced → Private DNS and enter your domain name there.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0> supports <1>DNS-over-HTTPS</1> and <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> adds <1>DNS-over-HTTPS</1> support to Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS and macOS configuration\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> supports <1>DNS-over-HTTPS</1>, but in order to configure it to use your own server, you'll need to generate a <2>DNS Stamp</2> for it.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard for iOS</0> supports <1>DNS-over-HTTPS</1> and <1>DNS-over-TLS</1> setup.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home itself can be a secure DNS client on any platform.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> supports all known secure DNS protocols.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"You will find more implementations <0>here</0> and <1>here</1>.\",\n  \"setup_dns_privacy_other_title\": \"Other implementations\",\n  \"setup_guide\": \"Setup Guide\",\n  \"show_all_filter_type\": \"Show all\",\n  \"show_blocked_responses\": \"Blocked\",\n  \"show_filtered_type\": \"Show filtered\",\n  \"show_processed_responses\": \"Processed\",\n  \"show_whitelisted_responses\": \"Allowed\",\n  \"sign_in\": \"Sign in\",\n  \"sign_out\": \"Sign out\",\n  \"source_label\": \"Source\",\n  \"static_ip\": \"Static IP Address\",\n  \"static_ip_desc\": \"AdGuard Home is a server so it needs a static IP address to function properly. Otherwise, at some point, your router may assign a different IP address to this device.\",\n  \"statistics_clear\": \"Clear statistics\",\n  \"statistics_clear_confirm\": \"Are you sure you want to clear statistics?\",\n  \"statistics_cleared\": \"Statistics successfully cleared\",\n  \"statistics_configuration\": \"Statistics configuration\",\n  \"statistics_enable\": \"Enable statistics\",\n  \"statistics_retention\": \"Statistics retention\",\n  \"statistics_retention_confirm\": \"Are you sure you want to change statistics retention? If you decrease the interval value, some data will be lost\",\n  \"statistics_retention_desc\": \"If you decrease the interval value, some data will be lost\",\n  \"stats_adult\": \"Blocked adult websites\",\n  \"stats_disabled\": \"The statistics have been disabled. You can turn it on from the <0>settings page</0>.\",\n  \"stats_disabled_short\": \"The statistics have been disabled\",\n  \"stats_malware_phishing\": \"Blocked malware/phishing\",\n  \"stats_params\": \"Statistics configuration\",\n  \"stats_query_domain\": \"Top queried domains\",\n  \"subnet_error\": \"Addresses must be in one subnet\",\n  \"sunday\": \"Sunday\",\n  \"sunday_short\": \"Sun\",\n  \"system_host_files\": \"System hosts files\",\n  \"table_client\": \"Client\",\n  \"table_name\": \"Name\",\n  \"tags_desc\": \"You can select tags that correspond to the client. Include tags in filtering rules to apply them more precisely. <0>Learn more</0>.\",\n  \"tags_title\": \"Tags\",\n  \"test_upstream_btn\": \"Test upstreams\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Auto (based on the color scheme of your device)\",\n  \"theme_dark\": \"Dark\",\n  \"theme_dark_desc\": \"Dark theme\",\n  \"theme_light\": \"Light\",\n  \"theme_light_desc\": \"Light theme\",\n  \"thursday\": \"Thursday\",\n  \"thursday_short\": \"Thu\",\n  \"time_table_header\": \"Time\",\n  \"top_blocked_domains\": \"Top blocked domains\",\n  \"top_clients\": \"Top clients\",\n  \"top_upstreams\": \"Top upstreams\",\n  \"topline_expired_certificate\": \"Your SSL certificate is expired. Update <0>Encryption settings</0>.\",\n  \"topline_expiring_certificate\": \"Your SSL certificate is about to expire. Update <0>Encryption settings</0>.\",\n  \"tracker_source\": \"Tracker source\",\n  \"try_again\": \"Try again\",\n  \"ttl_cache_validation\": \"Minimum cache TTL override must be less than or equal to the maximum\",\n  \"tuesday\": \"Tuesday\",\n  \"tuesday_short\": \"Tue\",\n  \"type_table_header\": \"Type\",\n  \"unavailable_dhcp\": \"DHCP is unavailable\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home cannot run a DHCP server on your OS\",\n  \"unblock\": \"Unblock\",\n  \"unblock_all\": \"Unblock all\",\n  \"unblock_for_this_client_only\": \"Unblock for this client only\",\n  \"unknown_filter\": \"Unknown filter {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} is now available! <0>Click here</0> for more info.\",\n  \"update_failed\": \"Auto-update failed. Please <a>follow these steps</a> to update manually.\",\n  \"update_now\": \"Update now\",\n  \"updated_custom_filtering_toast\": \"Custom rules successfully saved\",\n  \"updated_save_search_toast\": \"Safe Search settings updated\",\n  \"updated_upstream_dns_toast\": \"Upstream servers successfully saved\",\n  \"updates_checked\": \"A new version of AdGuard Home is available\",\n  \"updates_version_equal\": \"AdGuard Home is up-to-date\",\n  \"upstream\": \"Upstream\",\n  \"upstream_dns\": \"Upstream DNS servers\",\n  \"upstream_dns_cache_configuration\": \"Upstream DNS cache configuration\",\n  \"upstream_dns_client_desc\": \"If you keep this field empty, AdGuard Home will use the servers configured in the <0>DNS settings</0>.\",\n  \"upstream_dns_configured_in_file\": \"Configured in {{path}}\",\n  \"upstream_dns_help\": \"Enter one server address per line. <a>Learn more</a> about configuring upstream DNS servers.\",\n  \"upstream_parallel\": \"Use parallel queries to speed up resolving by querying all upstream servers simultaneously.\",\n  \"upstream_timeout\": \"Upstream timeout\",\n  \"upstream_timeout_desc\": \"Specifies the number of seconds to wait for a response from the upstream server\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Use AdGuard browsing security web service\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home will check if the domain is blocked by the browsing security web service. It will use privacy-friendly lookup API to perform the check: only a short prefix of the domain name SHA256 hash is sent to the server.\",\n  \"use_adguard_parental\": \"Use AdGuard parental control web service\",\n  \"use_adguard_parental_hint\": \"AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.\",\n  \"use_private_ptr_resolvers_desc\": \"Resolve PTR, SOA, and NS requests for ARPA domains containing private IP addresses through private upstream servers, DHCP, /etc/hosts, etc. If disabled, AdGuard Home will respond to all such requests with NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Use private reverse DNS resolvers\",\n  \"use_saved_key\": \"Use the previously saved key\",\n  \"username_label\": \"Username\",\n  \"username_placeholder\": \"Enter username\",\n  \"validated_with_dnssec\": \"Validated with DNSSEC\",\n  \"version\": \"Version\",\n  \"version_request_error\": \"Update check failed. Please check your Internet connection.\",\n  \"wednesday\": \"Wednesday\",\n  \"wednesday_short\": \"Wed\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/es.json",
    "content": "{\n  \"access_allowed_desc\": \"Lista de CIDR, direcciones IP o <a>ID de clientes</a>. Si esta lista tiene entradas, AdGuard Home aceptará peticiones solo de estos clientes.\",\n  \"access_allowed_title\": \"Clientes permitidos\",\n  \"access_blocked_desc\": \"No debe confundirse con filtros. AdGuard Home descartará las consultas DNS que coincidan con estos dominios, y estas consultas ni siquiera aparecerán en el registro de consultas. Puedes especificar nombres de dominio exactos, comodines o reglas de filtrado de URL, por ejemplo: \\\"ejemplo.org\\\", \\\"*.ejemplo.org\\\" o \\\"||ejemplo.org^\\\" correspondientemente.\",\n  \"access_blocked_title\": \"Dominios no permitidos\",\n  \"access_desc\": \"Aquí puedes configurar las reglas de acceso para el servidor DNS de AdGuard Home\",\n  \"access_disallowed_desc\": \"Lista de CIDR, direcciones IP o <a>ID de clientes</a>. Si esta lista tiene entradas, AdGuard Home descartará las peticiones de estos clientes. Este campo será ignorado si hay entradas en clientes permitidos.\",\n  \"access_disallowed_title\": \"Clientes no permitidos\",\n  \"access_settings_saved\": \"Configuración de acceso guardado correctamente\",\n  \"access_title\": \"Configuración de acceso\",\n  \"actions_table_header\": \"Acciones\",\n  \"add_allowlist\": \"Añadir lista de permitido\",\n  \"add_blocklist\": \"Añadir lista de bloqueo\",\n  \"add_custom_list\": \"Añadir lista personalizada\",\n  \"add_persistent_client\": \"Añadir como cliente persistente\",\n  \"address\": \"Dirección\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home descartará todas las consultas DNS de este cliente.\",\n  \"all_lists_up_to_date_toast\": \"Todas las listas ya están actualizadas\",\n  \"all_queries\": \"Todas las consultas\",\n  \"allow_this_client\": \"Permitir a este cliente\",\n  \"allowed\": \"Permitido\",\n  \"anonymize_client_ip\": \"Anonimizar IP del cliente\",\n  \"anonymize_client_ip_desc\": \"No guarda la dirección IP completa del cliente en registros o estadísticas\",\n  \"anonymizer_notification\": \"<0>Nota:</0> La anonimización de IP está habilitada. Puedes deshabilitarla en <1>Configuración general</1>.\",\n  \"answer\": \"Respuesta\",\n  \"apply_btn\": \"Aplicar\",\n  \"auto_clients_desc\": \"Información sobre las direcciones IP de los dispositivos que utilizan o pueden utilizar AdGuard Home. Esta información se recopila de varias fuentes, incluidos archivos hosts, DNS inverso, etc.\",\n  \"auto_clients_title\": \"Clientes activos\",\n  \"autofix_warning_list\": \"Realizará estas tareas: <0>Deshabilitar el sistema DNSStubListener</0> <0>Establecer la dirección del servidor DNS en 127.0.0.1</0> <0>Reemplazar el destino del enlace simbólico de /etc/resolv.conf por /run/systemd/resolve/resolv.conf</0> <0>Detener DNSStubListener (recargar el servicio systemd-resolved)</0>\",\n  \"autofix_warning_result\": \"Como resultado, todas las peticiones DNS de tu sistema serán procesadas por AdGuard Home de manera predeterminada.\",\n  \"autofix_warning_text\": \"Si haces clic en \\\"Corregir\\\", AdGuard Home configurará tu sistema para utilizar el servidor DNS de AdGuard Home.\",\n  \"average_processing_time\": \"Tiempo promedio de procesamiento\",\n  \"average_processing_time_hint\": \"Tiempo promedio en milisegundos al procesar una petición DNS\",\n  \"average_upstream_response_time\": \"Tiempo promedio de respuesta del proveedor DNS\",\n  \"back\": \"Atrás\",\n  \"block\": \"Bloquear\",\n  \"block_all\": \"Bloquear todo\",\n  \"block_domain_use_filters_and_hosts\": \"Bloquear dominios usando filtros y archivos hosts\",\n  \"block_for_this_client_only\": \"Bloquear solo para este cliente\",\n  \"block_services\": \"Bloquear servicios específicos\",\n  \"blocked_adult_websites\": \"Bloqueado por control parental\",\n  \"blocked_by\": \"<0>Bloqueado por filtros</0>\",\n  \"blocked_by_cname_or_ip\": \"Bloqueado por CNAME o IP\",\n  \"blocked_by_response\": \"Bloqueado por CNAME o IP en respuesta\",\n  \"blocked_response_ttl\": \"Respuesta TTL bloqueada\",\n  \"blocked_response_ttl_desc\": \"Especifica durante cuántos segundos los clientes deben almacenar en caché una respuesta filtrada\",\n  \"blocked_safebrowsing\": \"Bloqueado por navegación segura\",\n  \"blocked_service\": \"Servicio bloqueado\",\n  \"blocked_services\": \"Servicios bloqueados\",\n  \"blocked_services_desc\": \"Permite bloquear rápidamente sitios y servicios populares.\",\n  \"blocked_services_global\": \"Usar servicios bloqueados globalmente\",\n  \"blocked_services_saved\": \"Servicios bloqueados guardado correctamente\",\n  \"blocked_threats\": \"Amenazas bloqueadas\",\n  \"blocking_ipv4\": \"Bloqueo de IPv4\",\n  \"blocking_ipv4_desc\": \"Dirección IP devolverá una petición A bloqueada\",\n  \"blocking_ipv6\": \"Bloqueo de IPv6\",\n  \"blocking_ipv6_desc\": \"Dirección IP devolverá una petición AAAA bloqueada\",\n  \"blocking_mode\": \"Modo de bloqueo\",\n  \"blocking_mode_custom_ip\": \"IP personalizada: Responde con una dirección IP establecida manualmente\",\n  \"blocking_mode_default\": \"Predeterminado: Responde con dirección IP cero (0.0.0.0 para A; :: para AAAA) cuando está bloqueado por la regla de estilo Adblock; responde con la dirección IP especificada en la regla cuando está bloqueado por una regla de estilo /etc/hosts\",\n  \"blocking_mode_null_ip\": \"IP nulo: Responde con dirección IP cero (0.0.0.0 para A; :: para AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Responde con el código NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: Responde con el código REFUSED\",\n  \"blocklist\": \"Lista de bloqueo\",\n  \"bootstrap_dns\": \"Servidores DNS de arranque\",\n  \"bootstrap_dns_desc\": \"Direcciones IP de los servidores DNS utilizados para resolver las direcciones IP de los resolutores DoH/DoT que especifiques como proveedores DNS. No se permiten comentarios.\",\n  \"cache_cleared\": \"Caché DNS borrado correctamente\",\n  \"cache_enabled\": \"Habilitar caché\",\n  \"cache_enabled_desc\": \"Almacena las respuestas DNS localmente.\",\n  \"cache_optimistic\": \"Caché optimista\",\n  \"cache_optimistic_desc\": \"Haz que AdGuard Home responda desde la caché incluso cuando las entradas estén expiradas y también intente actualizarlas.\",\n  \"cache_size\": \"Tamaño de la caché\",\n  \"cache_size_desc\": \"Tamaño de la caché DNS (en bytes).\",\n  \"cache_size_validation\": \"El tamaño de la cache debe ser mayor que cero cuando está habilitado.\",\n  \"cache_ttl_max_override\": \"Anular TTL máximo\",\n  \"cache_ttl_max_override_desc\": \"Establece un valor de tiempo de vida (en segundos) máximo para las entradas en la caché DNS.\",\n  \"cache_ttl_min_override\": \"Anular TTL mínimo\",\n  \"cache_ttl_min_override_desc\": \"Amplía el corto tiempo de vida (en segundos) de los valores recibidos del proveedor DNS al almacenar en caché las respuestas DNS.\",\n  \"cancel_btn\": \"Cancelar\",\n  \"category_label\": \"Categoría\",\n  \"check\": \"Comprobar\",\n  \"check_client_id\": \"Identificador del cliente (ID de cliente o dirección IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Comprueba si un nombre del host está siendo filtrado.\",\n  \"check_dhcp_servers\": \"Comprobar si hay servidores DHCP\",\n  \"check_dns_record\": \"Selecciona el tipo de registro DNS\",\n  \"check_enter_client_id\": \"Ingresa el identificador del cliente\",\n  \"check_hostname\": \"Nombre de host o nombre de dominio\",\n  \"check_ip\": \"Direcciones IP: {{ip}}\",\n  \"check_not_found\": \"No se ha encontrado en tus listas de filtros\",\n  \"check_reason\": \"Razón: {{reason}}\",\n  \"check_service\": \"Nombre del servicio: {{service}}\",\n  \"check_title\": \"Comprobar filtrado\",\n  \"check_updates_btn\": \"Buscar actualizaciones\",\n  \"check_updates_now\": \"Buscar actualizaciones ahora\",\n  \"choose_allowlist\": \"Elegir listas de permitido\",\n  \"choose_blocklist\": \"Elegir listas de bloqueo\",\n  \"choose_from_list\": \"Elegir de la lista\",\n  \"city\": \"Ciudad\",\n  \"clear_cache\": \"Borrar caché\",\n  \"click_to_view_queries\": \"Clic para ver las consultas\",\n  \"client_add\": \"Añadir cliente\",\n  \"client_added\": \"Cliente \\\"{{key}}\\\" añadido correctamente\",\n  \"client_blocked\": \"Cliente \\\"{{ip}}\\\" bloqueado correctamente\",\n  \"client_confirm_block\": \"¿Estás seguro de que deseas bloquear al cliente \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"¿Estás seguro de que deseas eliminar el cliente \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"¿Estás seguro de que deseas desbloquear al cliente \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Cliente \\\"{{key}}\\\" eliminado correctamente\",\n  \"client_details\": \"Detalles del cliente\",\n  \"client_edit\": \"Editar cliente\",\n  \"client_global_settings\": \"Usar configuración global\",\n  \"client_id\": \"ID de cliente\",\n  \"client_id_desc\": \"Los clientes pueden ser identificados por un ID de cliente. Obtén más información sobre cómo identificar clientes <a>aquí</a>.\",\n  \"client_id_placeholder\": \"Ingresa el ID del cliente\",\n  \"client_identifier\": \"Identificador\",\n  \"client_identifier_desc\": \"Los clientes pueden ser identificados por su dirección IP, MAC, CIDR o un ID de cliente (puede ser utilizado para DoT/DoH/DoQ). Obtén más información sobre cómo identificar clientes <0>aquí</0>.\",\n  \"client_name\": \"Cliente {{id}}\",\n  \"client_new\": \"Cliente nuevo\",\n  \"client_settings\": \"Configuración de clientes\",\n  \"client_table_header\": \"Cliente\",\n  \"client_unblocked\": \"Cliente \\\"{{ip}}\\\" desbloqueado correctamente\",\n  \"client_updated\": \"Cliente \\\"{{key}}\\\" actualizado correctamente\",\n  \"clients_desc\": \"Configurar registros de clientes persistentes para dispositivos conectados a AdGuard Home\",\n  \"clients_not_found\": \"No se han encontrado clientes\",\n  \"clients_title\": \"Clientes persistentes\",\n  \"compact\": \"Compacto\",\n  \"config_successfully_saved\": \"Configuración guardada correctamente\",\n  \"configure\": \"Configurar\",\n  \"confirm_dns_cache_clear\": \"¿Estás seguro de que deseas borrar la caché DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home configurará {{ip}} para ser tu dirección IP estática. ¿Deseas continuar?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"País\",\n  \"custom_filter_rules\": \"Reglas de filtrado personalizado\",\n  \"custom_filter_rules_hint\": \"Ingresa una regla por línea. Puedes utilizar reglas de bloqueo o la sintaxis de los archivos hosts.\",\n  \"custom_filtering_rules\": \"Reglas de filtrado personalizado\",\n  \"custom_ip\": \"IP personalizada\",\n  \"custom_retention_input\": \"Ingresa la retención en horas\",\n  \"custom_rotation_input\": \"Ingresa la rotación en horas\",\n  \"dashboard\": \"Panel de control\",\n  \"date\": \"Fecha\",\n  \"default\": \"Predeterminado\",\n  \"delete_confirm\": \"¿Estás seguro de que deseas eliminar \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Eliminar\",\n  \"descr\": \"Descripción\",\n  \"details\": \"Detalles\",\n  \"dhcp_add_static_lease\": \"Añadir asignación estática\",\n  \"dhcp_config_saved\": \"Configuración DHCP guardado correctamente\",\n  \"dhcp_description\": \"Si tu router no proporciona la configuración DHCP, puedes utilizar el propio servidor DHCP incorporado de AdGuard.\",\n  \"dhcp_disable\": \"Deshabilitar servidor DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Tu sistema utiliza la configuración de dirección IP dinámica para la interfaz <0>{{interfaceName}}</0>. Para poder utilizar el servidor DHCP se debe establecer una dirección IP estática. Tu dirección IP actual es <0>{{ipAddress}}</0>. AdGuard Home establecerá automáticamente esta dirección IP como estática si presionas el botón \\\"Habilitar servidor DHCP\\\".\",\n  \"dhcp_edit_static_lease\": \"Editar asignación estática\",\n  \"dhcp_enable\": \"Habilitar servidor DHCP\",\n  \"dhcp_error\": \"AdGuard Home no pudo determinar si hay otro servidor DHCP activo en la red\",\n  \"dhcp_form_gateway_input\": \"IP de puerta de enlace\",\n  \"dhcp_form_lease_input\": \"Duración de asignación\",\n  \"dhcp_form_lease_title\": \"Tiempo de asignación DHCP (en segundos)\",\n  \"dhcp_form_range_end\": \"Final de rango\",\n  \"dhcp_form_range_start\": \"Inicio de rango\",\n  \"dhcp_form_range_title\": \"Rango de direcciones IP\",\n  \"dhcp_form_subnet_input\": \"Máscara de subred\",\n  \"dhcp_found\": \"Un servidor DHCP activo se encuentra en la red. No es seguro habilitar el servidor DHCP incorporado.\",\n  \"dhcp_hardware_address\": \"Dirección MAC\",\n  \"dhcp_interface_select\": \"Seleccionar interfaz DHCP\",\n  \"dhcp_ip_addresses\": \"Direcciones IP\",\n  \"dhcp_ipv4_settings\": \"Configuración DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Configuración DHCP IPv6\",\n  \"dhcp_lease_added\": \"Asignación estática \\\"{{key}}\\\" añadido correctamente\",\n  \"dhcp_lease_deleted\": \"Asignación estática \\\"{{key}}\\\" eliminado correctamente\",\n  \"dhcp_lease_updated\": \"Asignación estática \\\"{{key}}\\\" actualizado correctamente\",\n  \"dhcp_leases\": \"Asignaciones DHCP\",\n  \"dhcp_leases_not_found\": \"No se han encontrado asignaciones DHCP\",\n  \"dhcp_new_static_lease\": \"Nueva asignación estática\",\n  \"dhcp_not_found\": \"Es seguro habilitar el servidor DHCP incorporado porque AdGuard Home no encontró ningún servidor DHCP activo en la red. Sin embargo, deberías volver a comprobarlo manualmente, ya que nuestra prueba automática no ofrece actualmente una garantía del 100 %.\",\n  \"dhcp_reset\": \"¿Estás seguro de que deseas restablecer la configuración DHCP?\",\n  \"dhcp_reset_leases\": \"Restablecer todas las asignaciones\",\n  \"dhcp_reset_leases_confirm\": \"¿Estás seguro de que deseas restablecer todas las asignaciones?\",\n  \"dhcp_reset_leases_success\": \"Asignaciones DHCP restablecidas correctamente\",\n  \"dhcp_settings\": \"Configuración DHCP\",\n  \"dhcp_static_ip_error\": \"Para poder utilizar el servidor DHCP se debe establecer una dirección IP estática. AdGuard Home no pudo determinar si esta interfaz de red está configurada utilizando una dirección IP estática. Por favor establece una dirección IP estática manualmente.\",\n  \"dhcp_static_leases\": \"Asignaciones DHCP estáticas\",\n  \"dhcp_static_leases_not_found\": \"No se han encontrado asignaciones DHCP estáticas\",\n  \"dhcp_table_expires\": \"Expira\",\n  \"dhcp_table_hostname\": \"Nombre del host\",\n  \"dhcp_title\": \"Servidor DHCP (experimental)\",\n  \"dhcp_warning\": \"Si de todos modos deseas habilitar el servidor DHCP, asegúrate de que no hay otro servidor DHCP activo en tu red. ¡De lo contrario, puedes dejar sin conexión a Internet a los dispositivos conectados!\",\n  \"disable_for_hours\": \"Por {{count}} hora\",\n  \"disable_for_hours_plural\": \"Por {{count}} horas\",\n  \"disable_for_minutes\": \"Por {{count}} minuto\",\n  \"disable_for_minutes_plural\": \"Por {{count}} minutos\",\n  \"disable_for_seconds\": \"Por {{count}} segundo\",\n  \"disable_for_seconds_plural\": \"Por {{count}} segundos\",\n  \"disable_ipv6\": \"Deshabilitar resolución de direcciones IPv6\",\n  \"disable_ipv6_desc\": \"Descarta todas las consultas DNS para direcciones IPv6 (tipo AAAA) y elimina las sugerencias IPv6 de las respuestas HTTPS.\",\n  \"disable_notify_for_hours\": \"Deshabilitar protección por {{count}} hora\",\n  \"disable_notify_for_hours_plural\": \"Deshabilitar protección por {{count}} horas\",\n  \"disable_notify_for_minutes\": \"Deshabilitar protección por {{count}} minuto\",\n  \"disable_notify_for_minutes_plural\": \"Deshabilitar protección por {{count}} minutos\",\n  \"disable_notify_for_seconds\": \"Deshabilitar protección por {{count}} segundo\",\n  \"disable_notify_for_seconds_plural\": \"Deshabilitar protección por {{count}} segundos\",\n  \"disable_notify_until_tomorrow\": \"Deshabilitar protección hasta mañana\",\n  \"disable_protection\": \"Deshabilitar protección\",\n  \"disable_rewrites\": \"Deshabilitar reglas de reescritura\",\n  \"disable_until_tomorrow\": \"Hasta mañana\",\n  \"disabled\": \"Deshabilitado\",\n  \"disabled_dhcp\": \"Servidor DHCP deshabilitado\",\n  \"disabled_filtering_toast\": \"Filtrado deshabilitado\",\n  \"disabled_parental_toast\": \"Control parental deshabilitado\",\n  \"disabled_protection\": \"Protección deshabilitada\",\n  \"disabled_safe_browsing_toast\": \"Navegación segura deshabilitada\",\n  \"disabled_safe_search_toast\": \"Búsqueda segura deshabilitada\",\n  \"disallow_this_client\": \"No permitir a este cliente\",\n  \"dns_addresses\": \"Direcciones DNS\",\n  \"dns_allowlists\": \"Listas de permitido DNS\",\n  \"dns_allowlists_desc\": \"Los dominios de las listas de permitido DNS serán permitidos incluso si están en cualquiera de las listas de bloqueo.\",\n  \"dns_blocklists\": \"Listas de bloqueo DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home bloqueará los dominios que coincidan con las listas de bloqueo.\",\n  \"dns_cache_config\": \"Configuración de la caché DNS\",\n  \"dns_cache_config_desc\": \"Aquí puedes configurar la caché DNS\",\n  \"dns_cache_size\": \"Tamaño de la caché DNS, en bytes\",\n  \"dns_config\": \"Configuración del servidor DNS\",\n  \"dns_over_https\": \"DNS mediante HTTPS\",\n  \"dns_over_quic\": \"DNS mediante QUIC\",\n  \"dns_over_tls\": \"DNS mediante TLS\",\n  \"dns_privacy\": \"DNS cifrado\",\n  \"dns_providers\": \"Aquí hay una <0>lista de proveedores DNS</0> conocidos para elegir.\",\n  \"dns_query\": \"Consultas DNS\",\n  \"dns_rewrites\": \"Reescrituras DNS\",\n  \"dns_settings\": \"Configuración DNS\",\n  \"dns_start\": \"El servidor DNS está iniciando\",\n  \"dns_status_error\": \"Error al obtener el estado del servidor DNS\",\n  \"dns_test_not_ok_toast\": \"Servidor \\\"{{key}}\\\": no se puede utilizar, por favor revisa si lo has escrito correctamente\",\n  \"dns_test_ok_toast\": \"Los servidores DNS especificados funcionan correctamente\",\n  \"dns_test_parsing_error_toast\": \"No se pudo utilizar la sección {{section}}: línea {{line}}:, verifica si la escribiste correctamente\",\n  \"dns_test_warning_toast\": \"Proveedor DNS \\\"{{key}}\\\" no responde a las peticiones de prueba y es posible que no funcione correctamente\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Habilitar DNSSEC\",\n  \"dnssec_enable_desc\": \"Establece el indicador DNSSEC en las consultas DNS salientes y comprueba el resultado (se requiere un resolutor habilitado para DNSSEC).\",\n  \"domain\": \"Dominio\",\n  \"domain_desc\": \"Ingresa el nombre del dominio o comodín que deseas reescribir.\",\n  \"domain_name_table_header\": \"Nombre del dominio\",\n  \"domain_or_client\": \"Dominio o cliente\",\n  \"down\": \"Abajo\",\n  \"download_mobileconfig\": \"Descargar archivo de configuración\",\n  \"download_mobileconfig_doh\": \"Descargar .mobileconfig para DNS mediante HTTPS\",\n  \"download_mobileconfig_dot\": \"Descargar .mobileconfig para DNS mediante TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Editar lista de permitido\",\n  \"edit_blocklist\": \"Editar lista de bloqueo\",\n  \"edit_table_action\": \"Editar\",\n  \"edns_cs_desc\": \"Añade la opción subred de cliente EDNS (ECS) a las peticiones del proveedor DNS y registra los valores enviados por los clientes en el registro de consultas.\",\n  \"edns_enable\": \"Habilitar subred de cliente EDNS\",\n  \"edns_use_custom_ip\": \"Usar IP personalizada para EDNS\",\n  \"edns_use_custom_ip_desc\": \"Permitir el uso de IP personalizada para EDNS\",\n  \"elapsed\": \"Transcurrido\",\n  \"empty_response_status\": \"Vacío\",\n  \"enable_protection\": \"Habilitar protección\",\n  \"enable_protection_timer\": \"La protección se habilitará a las {{time}}\",\n  \"enable_rewrites\": \"Habilitar reglas de reescritura\",\n  \"enable_upstream_dns_cache\": \"Habilitar el almacenamiento en caché del DNS para la configuración personalizada de este cliente\",\n  \"enabled_dhcp\": \"Servidor DHCP habilitado\",\n  \"enabled_filtering_toast\": \"Filtrado habilitado\",\n  \"enabled_parental_toast\": \"Control parental habilitado\",\n  \"enabled_protection\": \"Protección habilitada\",\n  \"enabled_safe_browsing_toast\": \"Navegación segura habilitada\",\n  \"enabled_save_search_toast\": \"Búsqueda segura habilitada\",\n  \"enabled_table_header\": \"Habilitado\",\n  \"encryption_certificate_path\": \"Ruta de acceso al certificado\",\n  \"encryption_certificates\": \"Certificados\",\n  \"encryption_certificates_desc\": \"Para utilizar el cifrado, debes proporcionar una cadena de certificado SSL válida para tu dominio. Puedes obtener un certificado gratuito en <0>{{link}}</0> o puedes comprarlo en una de las autoridades de certificación de confianza.\",\n  \"encryption_certificates_input\": \"Copia/pega aquí tu certificado codificado PEM.\",\n  \"encryption_certificates_source_content\": \"Pegar el contenido del certificado\",\n  \"encryption_certificates_source_path\": \"Establecer una ruta para el archivo de certificado\",\n  \"encryption_chain_invalid\": \"La cadena de certificado no es válida\",\n  \"encryption_chain_valid\": \"La cadena de certificado es válida\",\n  \"encryption_config_saved\": \"Configuración de cifrado guardado\",\n  \"encryption_desc\": \"Soporte de cifrado (HTTPS/QUIC/TLS) tanto para DNS como para la interfaz web de administración\",\n  \"encryption_doq\": \"Puerto DNS mediante QUIC\",\n  \"encryption_doq_desc\": \"Si este puerto está configurado, AdGuard Home ejecutará un servidor DNS mediante QUIC en este puerto.\",\n  \"encryption_dot\": \"Puerto DNS mediante TLS\",\n  \"encryption_dot_desc\": \"Si este puerto está configurado, AdGuard Home ejecutará un servidor DNS mediante TLS en este puerto.\",\n  \"encryption_enable\": \"Habilitar cifrado (HTTPS, DNS mediante HTTPS y DNS mediante TLS)\",\n  \"encryption_enable_desc\": \"Si el cifrado está habilitado, la interfaz de administración de AdGuard Home funcionará a través de HTTPS, y el servidor DNS escuchará las peticiones DNS mediante HTTPS y DNS mediante TLS.\",\n  \"encryption_expire\": \"Expira\",\n  \"encryption_hostnames\": \"Nombres de hosts\",\n  \"encryption_https\": \"Puerto HTTPS\",\n  \"encryption_https_desc\": \"Si el puerto HTTPS está configurado, la interfaz de administración de AdGuard Home será accesible a través de HTTPS, y también proporcionará DNS mediante HTTPS en la ubicación '/dns-query'.\",\n  \"encryption_issuer\": \"Emisor\",\n  \"encryption_key\": \"Clave privada\",\n  \"encryption_key_input\": \"Copia/pega aquí tu clave privada codificada PEM para tu certificado.\",\n  \"encryption_key_invalid\": \"Esta es una clave privada {{type}} no válida\",\n  \"encryption_key_source_content\": \"Pegar el contenido de la clave privada\",\n  \"encryption_key_source_path\": \"Establecer una ruta de archivo de clave privada\",\n  \"encryption_key_valid\": \"Esta es una clave privada {{type}} válida\",\n  \"encryption_plain_dns_desc\": \"El DNS simple está habilitado de manera predeterminada. Puedes deshabilitarlo para obligar a todos los dispositivos a utilizar DNS cifrado. Para ello, debe habilitar al menos un protocolo DNS cifrado\",\n  \"encryption_plain_dns_enable\": \"Habilitar DNS simple\",\n  \"encryption_plain_dns_error\": \"Para deshabilitar el DNS simple, habilita al menos un protocolo DNS cifrado\",\n  \"encryption_private_key_path\": \"Ruta de acceso a la clave privada\",\n  \"encryption_redirect\": \"Redireccionar a HTTPS automáticamente\",\n  \"encryption_redirect_desc\": \"Si está marcada, AdGuard Home lo redireccionará automáticamente de direcciones HTTP a HTTPS.\",\n  \"encryption_reset\": \"¿Estás seguro de que deseas restablecer la configuración de cifrado?\",\n  \"encryption_server\": \"Nombre del servidor\",\n  \"encryption_server_desc\": \"Si se configura, AdGuard Home detecta los ID de clientes, responde a las consultas DDR y realiza validaciones de conexión adicionales. Si no se configura, estas funciones se deshabilitarán. Debe coincidir con uno de los nombres DNS del certificado.\",\n  \"encryption_server_enter\": \"Ingresa el nombre del dominio\",\n  \"encryption_settings\": \"Configuración de cifrado\",\n  \"encryption_status\": \"Estado\",\n  \"encryption_subject\": \"Asunto\",\n  \"encryption_title\": \"Cifrado\",\n  \"encryption_warning\": \"Advertencia\",\n  \"enforce_safe_search\": \"Usar búsqueda segura\",\n  \"enforce_save_search_hint\": \"AdGuard Home reforzará la búsqueda segura en los siguientes motores de búsqueda: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex y Pixabay.\",\n  \"enforced_save_search\": \"Búsquedas seguras forzadas\",\n  \"enter_cache_size\": \"Ingresa el tamaño de la caché (bytes)\",\n  \"enter_cache_ttl_max_override\": \"Ingresa el TTL máximo (en segundos)\",\n  \"enter_cache_ttl_min_override\": \"Ingresa el TTL mínimo (en segundos)\",\n  \"enter_name_hint\": \"Ingresa el nombre\",\n  \"enter_url_or_path_hint\": \"Ingresa una URL o ruta absoluta para la lista\",\n  \"enter_valid_allowlist\": \"Ingresa una URL válida para la lista de permitido.\",\n  \"enter_valid_blocklist\": \"Ingresa una URL válida para la lista de bloqueo.\",\n  \"error_details\": \"Detalles del error\",\n  \"example_comment\": \"! Aquí va un comentario.\",\n  \"example_comment_hash\": \"# También un comentario.\",\n  \"example_comment_meaning\": \"solo un comentario.\",\n  \"example_meaning_filter_block\": \"bloquea el acceso al dominio ejemplo.org y a todos sus subdominios.\",\n  \"example_meaning_filter_whitelist\": \"desbloquea el acceso al dominio ejemplo.org y a todos sus subdominios.\",\n  \"example_meaning_host_block\": \"responde con 127.0.0.1 para ejemplo.org (pero no para sus subdominios).\",\n  \"example_multiple_upstreams_reserved\": \"múltiples proveedores DNS <0>para dominios específicos</0>.\",\n  \"example_regex_meaning\": \"bloquea el acceso a los dominios que coincidan con la expresión regular especificada.\",\n  \"example_rewrite_domain\": \"reescribe las respuestas solo para este nombre de dominio.\",\n  \"example_rewrite_wildcard\": \"reescribe las respuestas para todos los subdominios de <0>ejemplo.org</0>.\",\n  \"example_upstream_comment\": \"un comentario.\",\n  \"example_upstream_doh\": \"cifrado <0>DNS mediante HTTPS</0>.\",\n  \"example_upstream_doh3\": \"cifrado DNS mediante HTTPS con <0>HTTP/3</0> forzado y sin alternativa a HTTP/2 o inferior.\",\n  \"example_upstream_doq\": \"cifrado <0>DNS mediante QUIC</0>.\",\n  \"example_upstream_dot\": \"cifrado <0>DNS mediante TLS</0>.\",\n  \"example_upstream_regular\": \"DNS regular (mediante UDP).\",\n  \"example_upstream_regular_port\": \"DNS regular (mediante UDP, con puerto).\",\n  \"example_upstream_reserved\": \"un proveedor DNS <0>para un dominio específico</0>.\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> para <1>DNSCrypt</1> o resolutores <2>DNS mediante HTTPS</2>.\",\n  \"example_upstream_tcp\": \"DNS regular (mediante TCP).\",\n  \"example_upstream_tcp_hostname\": \"DNS regular (mediante TCP, nombre del host).\",\n  \"example_upstream_tcp_port\": \"DNS regular (mediante TCP, con puerto).\",\n  \"example_upstream_udp\": \"DNS regular (mediante UDP, nombre del host).\",\n  \"examples_title\": \"Ejemplos\",\n  \"fallback_dns_desc\": \"Lista de servidores DNS alternativos utilizados cuando los proveedores DNS no responden. La sintaxis es la misma que en el campo de los principales proveedores DNS anterior.\",\n  \"fallback_dns_placeholder\": \"Ingresa un servidor DNS alternativo por línea\",\n  \"fallback_dns_title\": \"Servidores DNS alternativos\",\n  \"faq\": \"Preguntas frecuentes\",\n  \"fastest_addr\": \"Dirección IP más rápida\",\n  \"fastest_addr_desc\": \"Espera respuestas de <b>todos</b> los servidores DNS, mide la velocidad de conexión TCP de cada servidor y devuelve la dirección IP del servidor con la velocidad de conexión más rápida.<br/>Este modo puede ralentizar significativamente las consultas DNS, si uno o más proveedores DNS no responden. Asegúrate de que tus proveedores DNS sean estables y de que el tiempo de espera tu proveedor DNS sea bajo.\",\n  \"filter\": \"Filtro\",\n  \"filter_added_successfully\": \"La lista ha sido añadida correctamente\",\n  \"filter_allowlist\": \"ADVERTENCIA: Esta acción también excluirá la regla \\\"{{disallowed_rule}}\\\" de la lista de clientes permitidos.\",\n  \"filter_category_general\": \"General\",\n  \"filter_category_general_desc\": \"Listas que bloquean rastreadores y anuncios en la mayoría de los dispositivos\",\n  \"filter_category_other\": \"Otro\",\n  \"filter_category_other_desc\": \"Otras listas de bloqueo\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Listas que se centran en anuncios regionales y servidores de rastreo\",\n  \"filter_category_security\": \"Seguridad\",\n  \"filter_category_security_desc\": \"Listas diseñadas específicamente para bloquear dominios de malware, phishing y estafa\",\n  \"filter_removed_successfully\": \"La lista ha sido eliminada correctamente\",\n  \"filter_updated\": \"La lista ha sido actualizada correctamente\",\n  \"filtered\": \"Filtrado\",\n  \"filtered_custom_rules\": \"Filtrado por reglas de filtrado personalizado\",\n  \"filtering_rules_learn_more\": \"<0>Más información</0> sobre cómo crear tus propias listas de hosts.\",\n  \"filters\": \"Filtros\",\n  \"filters_and_hosts_hint\": \"AdGuard Home entiende las reglas básicas de bloqueo y la sintaxis de los archivos hosts.\",\n  \"filters_block_toggle_hint\": \"Puedes configurar las reglas de bloqueo en la configuración de <a>filtros</a>.\",\n  \"filters_configuration\": \"Configuración de filtros\",\n  \"filters_enable\": \"Habilitar filtros\",\n  \"filters_interval\": \"Intervalo de actualización\",\n  \"fix\": \"Corregir\",\n  \"for_last_days\": \"durante el último {{count}} día\",\n  \"for_last_days_plural\": \"durante los últimos {{count}} días\",\n  \"for_last_hours\": \"de la última {{count}} hora\",\n  \"for_last_hours_plural\": \"de las últimas {{count}} horas\",\n  \"forgot_password\": \"¿Olvidaste tu contraseña?\",\n  \"forgot_password_desc\": \"Por favor sigue <0>estos pasos</0> para crear una nueva contraseña para tu cuenta de usuario.\",\n  \"form_add_id\": \"Añadir identificador\",\n  \"form_answer\": \"Ingresa la dirección IP o el nombre del dominio\",\n  \"form_client_name\": \"Ingresa el nombre del cliente\",\n  \"form_domain\": \"Ingresa el nombre del dominio o comodín\",\n  \"form_enter_blocked_response_ttl\": \"Ingresa el TTL de respuesta bloqueada (en segundos)\",\n  \"form_enter_host\": \"Ingresa un nombre de host\",\n  \"form_enter_hostname\": \"Ingresa el nombre del host\",\n  \"form_enter_id\": \"Ingresa el identificador\",\n  \"form_enter_ip\": \"Ingresa la IP\",\n  \"form_enter_mac\": \"Ingresa la MAC\",\n  \"form_enter_rate_limit\": \"Ingresa el límite de cantidad\",\n  \"form_enter_rate_limit_subnet_len\": \"Ingresa la longitud del prefijo de subred para limitar la cantidad\",\n  \"form_enter_subnet_ip\": \"Ingresa una dirección IP en la subred \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Ingresa la duración de tiempo de espera del proveedor DNS en segundos\",\n  \"form_error_answer_format\": \"Formato de respuesta no válido\",\n  \"form_error_client_id_format\": \"El ID de cliente debe contener solo números, letras minúsculas y guiones\",\n  \"form_error_domain_format\": \"Formato de dominio no válido\",\n  \"form_error_equal\": \"No debe ser igual\",\n  \"form_error_gateway_ip\": \"La asignación no puede tener la dirección IP de la puerta de enlace\",\n  \"form_error_ip4_format\": \"Dirección IPv4 no válida\",\n  \"form_error_ip4_gateway_format\": \"Dirección IPv4 no válida de la puerta de enlace\",\n  \"form_error_ip6_format\": \"Dirección IPv6 no válida\",\n  \"form_error_ip_format\": \"Dirección IP no válida\",\n  \"form_error_mac_format\": \"Dirección MAC no válida\",\n  \"form_error_password\": \"La contraseña no coincide\",\n  \"form_error_password_length\": \"La contraseña debe tener entre {{min}} y {{max}} caracteres\",\n  \"form_error_port\": \"Ingresa un número de puerto válido\",\n  \"form_error_port_range\": \"Ingresa el número del puerto en el rango de 80 a 65535\",\n  \"form_error_port_unsafe\": \"Puerto inseguro\",\n  \"form_error_positive\": \"Debe ser mayor que 0\",\n  \"form_error_required\": \"Campo obligatorio\",\n  \"form_error_server_name\": \"Nombre de servidor no válido\",\n  \"form_error_subnet\": \"La subred \\\"{{cidr}}\\\" no contiene la dirección IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Formato de URL no válido\",\n  \"form_error_url_or_path_format\": \"URL o ruta absoluta no válida para la lista\",\n  \"form_select_tags\": \"Selecciona las etiquetas del cliente\",\n  \"found_in_known_domain_db\": \"Encontrado en la base de datos de dominios conocidos.\",\n  \"friday\": \"Viernes\",\n  \"friday_short\": \"Vie.\",\n  \"gateway_or_subnet_invalid\": \"Máscara de subred no válida\",\n  \"general_settings\": \"Configuración general\",\n  \"general_statistics\": \"Estadísticas generales\",\n  \"get_started\": \"Comenzar\",\n  \"greater_range_start_error\": \"Debe ser mayor que el inicio de rango\",\n  \"homepage\": \"Página de inicio\",\n  \"host_whitelisted\": \"El host está permitido\",\n  \"ignore_domains\": \"Dominios ignorados (separados por una nueva línea)\",\n  \"ignore_domains_desc_query\": \"Las consultas que coinciden con estas reglas no aparecen en el registro de consultas\",\n  \"ignore_domains_desc_stats\": \"Las consultas que coinciden con estas reglas no aparecen en las estadísticas\",\n  \"ignore_domains_title\": \"Dominios ignorados\",\n  \"ignore_query_log\": \"Ignorar este cliente en el registro de consultas\",\n  \"ignore_statistics\": \"Ignorar este cliente en las estadísticas\",\n  \"install_auth_confirm\": \"Confirmar contraseña\",\n  \"install_auth_desc\": \"Debe configurarse la autenticación por contraseña para la interfaz web de administración de AdGuard Home. Incluso si AdGuard Home es accesible solo en tu red local, es importante protegerlo del acceso no autorizado.\",\n  \"install_auth_password\": \"Contraseña\",\n  \"install_auth_password_enter\": \"Ingresa tu contraseña\",\n  \"install_auth_title\": \"Autenticación\",\n  \"install_auth_username\": \"Usuario\",\n  \"install_auth_username_enter\": \"Ingresa tu nombre de usuario\",\n  \"install_devices_address\": \"El servidor DNS de AdGuard Home está escuchando en las siguientes direcciones\",\n  \"install_devices_android_list_1\": \"En la pantalla de inicio del menú Android, pulsa en Configuración.\",\n  \"install_devices_android_list_2\": \"Pulsa Wi-Fi en el menú. Aparecerá la pantalla que lista todas las redes disponibles (es imposible configurar un DNS personalizado para la conexión móvil).\",\n  \"install_devices_android_list_3\": \"Mantén presionado la red a la que estás conectado y pulsa Modificar red.\",\n  \"install_devices_android_list_4\": \"En algunos dispositivos, es posible que debas marcar la casilla Avanzado para ver más configuraciones. Para ajustar la configuración DNS de Android, deberás cambiar la configuración de IP de DHCP a Estática.\",\n  \"install_devices_android_list_5\": \"Cambia los valores de DNS 1 y DNS 2 a las direcciones de tu servidor AdGuard Home.\",\n  \"install_devices_desc\": \"Para comenzar a utilizar AdGuard Home, debes configurar tus dispositivos para usarlo.\",\n  \"install_devices_ios_list_1\": \"En la pantalla de inicio, pulsa en Configuración.\",\n  \"install_devices_ios_list_2\": \"Elige Wi-Fi en el menú de la izquierda (es imposible configurar DNS para redes móviles).\",\n  \"install_devices_ios_list_3\": \"Pulsa sobre el nombre de la red actualmente activa.\",\n  \"install_devices_ios_list_4\": \"En el campo DNS ingresa las direcciones de tu servidor AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Haz clic en el icono de Apple y ve a Preferencias del sistema.\",\n  \"install_devices_macos_list_2\": \"Haz clic en Red.\",\n  \"install_devices_macos_list_3\": \"Selecciona la primera conexión de la lista y haz clic en Avanzado.\",\n  \"install_devices_macos_list_4\": \"Selecciona la pestaña DNS e ingresa las direcciones de tu servidor AdGuard Home.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Esta configuración cubre automáticamente todos los dispositivos conectados a tu router doméstico y no necesitarás configurar cada uno manualmente.\",\n  \"install_devices_router_list_1\": \"Abre las preferencias de tu router. Por lo general, puedes acceder a él desde tu navegador a través de una URL como http://192.168.0.1/ o http://192.168.1.1/. Es posible que se te pida que ingreses la contraseña. Si no lo recuerdas, a menudo puedes restablecer la contraseña presionando un botón en el router, pero ten en cuenta que si eliges este procedimiento, probablemente se perderá toda la configuración del router. Si tu router requiere una aplicación para configurarlo, instala la aplicación en tu teléfono o PC y utilízala para acceder a la configuración del router.\",\n  \"install_devices_router_list_2\": \"Busca la configuración de DHCP/DNS. Busca las letras DNS junto a un campo que permita ingresar dos o tres grupos de números, cada uno dividido en cuatro grupos de uno a tres dígitos.\",\n  \"install_devices_router_list_3\": \"Ingresa las direcciones de tu servidor AdGuard Home allí.\",\n  \"install_devices_router_list_4\": \"En algunos tipos de router, no se puede configurar un servidor DNS personalizado. En ese caso, configurar AdGuard Home como <0>servidor DHCP</0> puede ayudar. De lo contrario, debes consultar el manual del router para saber cómo personalizar los servidores DNS en tu modelo de router específico.\",\n  \"install_devices_title\": \"Configura tus dispositivos\",\n  \"install_devices_windows_list_1\": \"Abre el Panel de control a través del menú Inicio o en el buscador de Windows.\",\n  \"install_devices_windows_list_2\": \"Ve a la categoría Redes e Internet, luego a Centro de redes y recursos compartidos.\",\n  \"install_devices_windows_list_3\": \"En el panel izquierdo, haz clic en \\\"Cambiar configuración del adaptador\\\".\",\n  \"install_devices_windows_list_4\": \"Haz clic derecho en tu conexión activa y selecciona Propiedades.\",\n  \"install_devices_windows_list_5\": \"Busca en la lista el \\\"Protocolo de Internet versión 4 (TCP/IPv4)\\\" (o \\\"Protocolo de Internet versión 6 (TCP/IPv6)\\\"), selecciónalo y vuelve a hacer clic en Propiedades.\",\n  \"install_devices_windows_list_6\": \"Elige \\\"Usar las siguientes direcciones de servidor DNS\\\" e ingresa las direcciones de tu servidor AdGuard Home.\",\n  \"install_saved\": \"Guardado correctamente\",\n  \"install_settings_all_interfaces\": \"Todas las interfaces\",\n  \"install_settings_dns\": \"Servidor DNS\",\n  \"install_settings_dns_desc\": \"Deberás configurar tus dispositivos o router para usar el servidor DNS en las siguientes direcciones:\",\n  \"install_settings_interface_link\": \"La interfaz web de administración de AdGuard Home estará disponible en las siguientes direcciones:\",\n  \"install_settings_listen\": \"Interfaz de escucha\",\n  \"install_settings_port\": \"Puerto\",\n  \"install_settings_title\": \"Interfaz web de administración\",\n  \"install_static_configure\": \"AdGuard Home ha detectado que se utiliza la dirección IP dinámica <0>{{ip}}</0>. ¿Deseas que se establezca como tu dirección estática?\",\n  \"install_static_error\": \"AdGuard Home no puede configurarlo automáticamente para esta interfaz de red. Busca instrucciones sobre cómo hacer esto manualmente.\",\n  \"install_static_ok\": \"¡Buenas noticias! La dirección IP estática ya está configurada\",\n  \"install_step\": \"Paso\",\n  \"install_submit_desc\": \"El proceso de configuración está completo y ahora estás listo para comenzar a usar AdGuard Home.\",\n  \"install_submit_title\": \"¡Felicitaciones!\",\n  \"install_welcome_desc\": \"AdGuard Home es un servidor DNS para bloqueo de anuncios y rastreadores a nivel de red. Su propósito es permitirte controlar toda tu red y todos tus dispositivos, y no requiere el uso de un programa del lado del cliente.\",\n  \"install_welcome_title\": \"¡Bienvenido a AdGuard Home!\",\n  \"interval_24_hour\": \"24 horas\",\n  \"interval_6_hour\": \"6 horas\",\n  \"interval_days\": \"{{count}} día\",\n  \"interval_days_plural\": \"{{count}} días\",\n  \"interval_hours\": \"{{count}} hora\",\n  \"interval_hours_plural\": \"{{count}} horas\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Dirección IP\",\n  \"known_tracker\": \"Rastreador conocido\",\n  \"last_rule_in_allowlist\": \"No se puede desautorizar a este cliente porque al excluir la regla \\\"{{disallowed_rule}}\\\" DESHABILITARÁ la lista de \\\"Clientes permitidos\\\".\",\n  \"last_time_updated_table_header\": \"Última actualización\",\n  \"list_confirm_delete\": \"¿Estás seguro de que deseas eliminar esta lista?\",\n  \"list_label\": \"Lista\",\n  \"list_updated\": \"{{count}} lista actualizada\",\n  \"list_updated_plural\": \"{{count}} listas actualizadas\",\n  \"list_url_table_header\": \"URL de la lista\",\n  \"load_balancing\": \"Balanceo de carga\",\n  \"load_balancing_desc\": \"Consulta un proveedor DNS a la vez.<br/>AdGuard Home utiliza un algoritmo aleatorio ponderado para seleccionar los servidores con el menor número de fallos y el menor tiempo promedio de búsqueda.\",\n  \"loading_table_status\": \"Cargando...\",\n  \"local_ptr_default_resolver\": \"Por defecto, AdGuard Home utiliza los siguientes resolutores DNS inversos: {{ip}}.\",\n  \"local_ptr_desc\": \"Servidores DNS que AdGuard Home utiliza para peticiones privadas PTR, SOA y NS. Una petición se considera privada si solicita un dominio ARPA que contiene una subred dentro de rangos de IP privados (como \\\"192.168.12.34\\\") y proviene de un cliente con una dirección IP privada. Si no se configura, se utilizarán los resolutores DNS predeterminados de tu sistema operativo, excepto para las direcciones IP de AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home no pudo determinar los resolutores DNS inversos y privados adecuados para este sistema.\",\n  \"local_ptr_placeholder\": \"Ingresa una dirección IP por línea\",\n  \"local_ptr_title\": \"Servidores DNS inversos y privados\",\n  \"location\": \"Ubicación\",\n  \"log_and_stats_section_label\": \"Registro de consultas y estadísticas\",\n  \"lower_range_start_error\": \"Debe ser inferior que el inicio de rango\",\n  \"main_settings\": \"Configuración principal\",\n  \"make_static\": \"Hacer estático\",\n  \"manual_update\": \"Por favor <a>sigue estos pasos</a> para actualizar manualmente.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Lunes\",\n  \"monday_short\": \"Lun.\",\n  \"name\": \"Nombre\",\n  \"name_table_header\": \"Nombre\",\n  \"netname\": \"Nombre de la red\",\n  \"network\": \"Red\",\n  \"new_allowlist\": \"Nueva lista de permitido\",\n  \"new_blocklist\": \"Nueva lista de bloqueo\",\n  \"next\": \"Siguiente\",\n  \"next_btn\": \"Siguiente\",\n  \"no_blocklist_added\": \"No se han añadido listas de bloqueo\",\n  \"no_clients_found\": \"No se han encontrado clientes\",\n  \"no_domains_found\": \"No se han encontrado dominios\",\n  \"no_logs_found\": \"No se han encontrado registros\",\n  \"no_servers_specified\": \"No hay servidores especificados\",\n  \"no_upstreams_data_found\": \"No se han encontrado datos de proveedores DNS\",\n  \"no_whitelist_added\": \"No se han añadido listas de permitido\",\n  \"nothing_found\": \"No se ha encontrado nada\",\n  \"null_ip\": \"IP nulo\",\n  \"number_of_dns_query_blocked_24_hours\": \"Número de peticiones DNS bloqueadas por los filtros y listas de bloqueo de hosts\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Número de sitios web para adultos bloqueado\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Número de peticiones DNS bloqueadas por el módulo de seguridad de navegación de AdGuard\",\n  \"number_of_dns_query_days\": \"Número de consultas DNS procesadas durante el último {{count}} día\",\n  \"number_of_dns_query_days_plural\": \"Número de consultas DNS procesadas durante los últimos {{count}} días\",\n  \"number_of_dns_query_hours\": \"Número de consultas DNS procesadas durante la última {{count}} hora\",\n  \"number_of_dns_query_hours_plural\": \"Número de consultas DNS procesadas durante las últimas {{count}} horas\",\n  \"number_of_dns_query_to_safe_search\": \"Número de peticiones DNS a los motores de búsqueda para los que se aplicó la búsqueda segura forzada\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"Inactivo\",\n  \"on\": \"Activo\",\n  \"open_dashboard\": \"Abrir panel de control\",\n  \"orgname\": \"Nombre de la organización\",\n  \"original_response\": \"Respuesta original\",\n  \"out_of_range_error\": \"Debe estar fuera del rango \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Página\",\n  \"parallel_requests\": \"Consultas paralelas\",\n  \"parental_control\": \"Control parental\",\n  \"password_label\": \"Contraseña\",\n  \"password_placeholder\": \"Ingresa tu contraseña\",\n  \"plain_dns\": \"DNS simple\",\n  \"port_53_faq_link\": \"El puerto 53 suele estar ocupado por los servicios \\\"DNSStubListener\\\" o \\\"systemd-resolved\\\". Por favor lee <0>esta instrucción</0> sobre cómo resolver esto.\",\n  \"previous_btn\": \"Atrás\",\n  \"privacy_policy\": \"Política de privacidad\",\n  \"processing_update\": \"Por favor espera, AdGuard Home se está actualizando\",\n  \"protection_section_label\": \"Protección\",\n  \"protocol\": \"Protocolo\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Registro de consultas\",\n  \"query_log_clear\": \"Borrar registros de consultas\",\n  \"query_log_cleared\": \"El registro de consultas se ha borrado correctamente\",\n  \"query_log_configuration\": \"Configuración de registros\",\n  \"query_log_confirm_clear\": \"¿Estás seguro de que deseas borrar todo el registro de consultas?\",\n  \"query_log_disabled\": \"El registro de consultas está deshabilitado y se puede configurar en la <0>configuración</0>\",\n  \"query_log_enable\": \"Habilitar registro\",\n  \"query_log_filtered\": \"Filtrado por {{filter}}\",\n  \"query_log_response_status\": \"Estado: {{value}}\",\n  \"query_log_retention\": \"Rotanción de registros de consultas\",\n  \"query_log_retention_confirm\": \"¿Estás seguro de que deseas cambiar la rotación del registro de consultas? Si disminuyes el valor del intervalo, se perderán algunos datos\",\n  \"query_log_strict_search\": \"Usar comillas dobles para una búsqueda estricta\",\n  \"query_log_updated\": \"El registro de consultas se ha actualizado correctamente\",\n  \"rate_limit\": \"Límite de cantidad\",\n  \"rate_limit_desc\": \"Número de peticiones por segundo permitidas por cliente. Establecerlo en 0 significa que no hay límite.\",\n  \"rate_limit_subnet_len_ipv4\": \"Longitud del prefijo de subred para direcciones IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Longitud del prefijo de subred para direcciones IPv4 utilizadas para limitar la cantidad. El valor predeterminado es 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"La longitud del prefijo de subred IPv4 debe estar entre 0 y 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Longitud del prefijo de subred para direcciones IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Longitud del prefijo de subred para direcciones IPv6 utilizadas para limitar la cantidad. El valor predeterminado es 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"La longitud del prefijo de subred IPv6 debe estar entre 0 y 128\",\n  \"rate_limit_whitelist\": \"Lista de permitido de límite de cantidad\",\n  \"rate_limit_whitelist_desc\": \"Direcciones IP excluidas del límite de cantidad\",\n  \"rate_limit_whitelist_placeholder\": \"Ingresa una dirección IP por línea\",\n  \"refresh_btn\": \"Actualizar\",\n  \"refresh_statics\": \"Actualizar estadísticas\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Reportar un error\",\n  \"request_details\": \"Detalles de la petición\",\n  \"request_table_header\": \"Petición\",\n  \"requests_count\": \"Número de peticiones\",\n  \"reset_settings\": \"Restablecer configuración\",\n  \"resolve_clients_desc\": \"Resuelve de manera inversa las direcciones IP de los clientes a sus nombres de hosts enviando consultas PTR a los resolutores correspondientes (servidores DNS privados para clientes locales, proveedores DNS para clientes con direcciones IP públicas).\",\n  \"resolve_clients_title\": \"Habilitar la resolución inversa de las direcciones IP de clientes\",\n  \"response_code\": \"Código de respuesta\",\n  \"response_details\": \"Detalles de la respuesta\",\n  \"response_table_header\": \"Respuesta\",\n  \"response_time\": \"Tiempo de respuesta\",\n  \"rewrite_A\": \"<0>A</0>: valor especial, mantiene registros <0>A</0> del proveedor DNS\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: valor especial, mantiene registros <0>AAAA</0> del proveedor DNS\",\n  \"rewrite_add\": \"Añadir reescritura DNS\",\n  \"rewrite_added\": \"Reescritura DNS para \\\"{{key}}\\\" añadido correctamente\",\n  \"rewrite_applied\": \"Regla de reescritura aplicada\",\n  \"rewrite_confirm_delete\": \"¿Estás seguro de que deseas eliminar la reescritura DNS para \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"Reescritura DNS para \\\"{{key}}\\\" eliminado correctamente\",\n  \"rewrite_desc\": \"Permite configurar fácilmente la respuesta DNS personalizada para un nombre de dominio específico.\",\n  \"rewrite_domain_name\": \"Nombre de dominio: añade un registro CNAME\",\n  \"rewrite_edit\": \"Editar reescritura DNS\",\n  \"rewrite_hosts_applied\": \"Reescrito por la regla del archivo hosts\",\n  \"rewrite_ip_address\": \"Dirección IP: utiliza esta IP en una respuesta A o AAAA\",\n  \"rewrite_not_found\": \"No se han encontrado reescrituras DNS\",\n  \"rewrite_settings_updated\": \"La configuración de reescritura de DNS se actualizó correctamente\",\n  \"rewrite_updated\": \"Reescritura DNS actualizada correctamente\",\n  \"rewrites_disabled_table_header\": \"Las reescrituras están deshabilitadas\",\n  \"rewrites_enabled_table_header\": \"Las reescrituras están habilitadas\",\n  \"rewritten\": \"Reescrito\",\n  \"rows_table_footer_text\": \"filas\",\n  \"rule_added_to_custom_filtering_toast\": \"Regla añadida a las reglas de filtrado personalizado: {{rule}}\",\n  \"rule_label\": \"Regla\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regla eliminada de las reglas de filtrado personalizado: {{rule}}\",\n  \"rules_count_table_header\": \"Número de reglas\",\n  \"safe_browsing\": \"Navegación segura\",\n  \"safe_search\": \"Búsqueda segura\",\n  \"saturday\": \"Sábado\",\n  \"saturday_short\": \"Sáb.\",\n  \"save_btn\": \"Guardar\",\n  \"save_config\": \"Guardar configuración\",\n  \"schedule_add\": \"Añadir horario\",\n  \"schedule_current_timezone\": \"Zona horaria actual: {{value}}\",\n  \"schedule_desc\": \"Establecer periodos de inactividad para servicios bloqueados\",\n  \"schedule_edit\": \"Editar horario\",\n  \"schedule_from\": \"De\",\n  \"schedule_invalid_select\": \"El tiempo de inicio debe de ir antes del tiempo de finalización\",\n  \"schedule_modal_description\": \"Este horario sustituirá cualquier horario existente para el mismo día de la semana. Cada día de la semana solo puede tener un periodo de inactividad.\",\n  \"schedule_modal_time_off\": \"Detener servicio de bloqueo:\",\n  \"schedule_new\": \"Nuevo horario\",\n  \"schedule_remove\": \"Eliminar horario\",\n  \"schedule_save\": \"Guardar horario\",\n  \"schedule_select_days\": \"Selecciona los dias\",\n  \"schedule_services\": \"Pausar servicio de bloqueo\",\n  \"schedule_services_desc\": \"Configura el horario programado de pausa del servicio de bloqueo\",\n  \"schedule_services_desc_client\": \"Configurar el horario programado de pausa del bloqueo de servicio filtrado para este cliente\",\n  \"schedule_time_all_day\": \"Todo el dia\",\n  \"schedule_timezone\": \"Selecciona una zona horaria\",\n  \"schedule_to\": \"A\",\n  \"served_from_cache_label\": \"Servido desde la caché\",\n  \"service_name\": \"Nombre del servicio\",\n  \"set_static_ip\": \"Establecer una dirección IP estática\",\n  \"settings\": \"Configuración\",\n  \"settings_custom\": \"Personalizado\",\n  \"settings_global\": \"Global\",\n  \"setup_config_to_enable_dhcp_server\": \"Configuración para habilitar el servidor DHCP\",\n  \"setup_dns_notice\": \"Para utilizar <1>DNS mediante HTTPS</1> o <1>DNS mediante TLS</1>, debes <0>configurar el cifrado</0> en la configuración de AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS mediante TLS:</0> Utiliza la cadena <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS mediante HTTPS:</0> Utiliza la cadena <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Aquí hay una lista de software que puedes usar.</0>\",\n  \"setup_dns_privacy_4\": \"En un dispositivo iOS 14 o macOS Big Sur puedes descargar el archivo especial '.mobileconfig' que añade servidores <highlight>DNS mediante HTTPS</highlight> o <highlight>DNS mediante TLS</highlight> a la configuración del DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 soporta DNS mediante TLS de forma nativa. Para configurarlo, ve a Configuración → Red e Internet → Avanzado → DNS privado e ingresa el nombre del dominio allí.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard para Android</0> soporta <1>DNS mediante HTTPS</1> y <1>DNS mediante TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> añade soporte a Android para <1>DNS mediante HTTPS</1>.\",\n  \"setup_dns_privacy_ioc_mac\": \"Configuración de iOS y macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> soporta <1>DNS mediante HTTPS</1>, pero para configurarlo y que uses tu propio servidor, necesitarás generar un <2>DNS Stamp</2> para ello.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard para iOS</0> soporta la configuración <1>DNS mediante HTTPS</1> y <1>DNS mediante TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home en sí mismo puede ser un cliente DNS seguro en cualquier plataforma.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> soporta todos los protocolos DNS seguros conocidos.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> soporta <1>DNS mediante HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> soporta <1>DNS mediante HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Encontrarás más implementaciones <0>aquí</0> y <1>aquí</1>.\",\n  \"setup_dns_privacy_other_title\": \"Otras implementaciones\",\n  \"setup_guide\": \"Guía de configuración\",\n  \"show_all_filter_type\": \"Mostrar todo\",\n  \"show_blocked_responses\": \"Bloqueado\",\n  \"show_filtered_type\": \"Mostrar filtrados\",\n  \"show_processed_responses\": \"Procesado\",\n  \"show_whitelisted_responses\": \"Permitido\",\n  \"sign_in\": \"Iniciar sesión\",\n  \"sign_out\": \"Cerrar sesión\",\n  \"source_label\": \"Fuente\",\n  \"static_ip\": \"Dirección IP estática\",\n  \"static_ip_desc\": \"AdGuard Home es un servidor, por lo que necesita una dirección IP estática para funcionar correctamente. De lo contrario, en algún momento tu router puede asignar una dirección IP diferente a este dispositivo.\",\n  \"statistics_clear\": \"Borrar estadísticas\",\n  \"statistics_clear_confirm\": \"¿Estás seguro de que deseas borrar las estadísticas?\",\n  \"statistics_cleared\": \"Estadísticas borradas correctamente\",\n  \"statistics_configuration\": \"Configuración de estadísticas\",\n  \"statistics_enable\": \"Habilitar estadísticas\",\n  \"statistics_retention\": \"Retención de estadísticas\",\n  \"statistics_retention_confirm\": \"¿Estás seguro de que deseas cambiar la retención de estadísticas? Si disminuyes el valor del intervalo, se perderán algunos datos\",\n  \"statistics_retention_desc\": \"Si disminuyes el valor del intervalo, se perderán algunos datos\",\n  \"stats_adult\": \"Sitios web para adultos bloqueado\",\n  \"stats_disabled\": \"Las estadísticas se han deshabilitado. Puedes habilitarlas desde la <0>página de configuración</0>.\",\n  \"stats_disabled_short\": \"Las estadísticas se han deshabilitado\",\n  \"stats_malware_phishing\": \"Malware/phishing bloqueado\",\n  \"stats_params\": \"Configuración de estadísticas\",\n  \"stats_query_domain\": \"Dominios más consultados\",\n  \"subnet_error\": \"Las direcciones deben estar en una subred\",\n  \"sunday\": \"Domingo\",\n  \"sunday_short\": \"Dom.\",\n  \"system_host_files\": \"Archivos hosts del sistema\",\n  \"table_client\": \"Cliente\",\n  \"table_name\": \"Nombre\",\n  \"tags_desc\": \"Puedes seleccionar las etiquetas que correspondan al cliente. Incluye etiquetas en las reglas de filtrado para aplicarlas con mayor precisión. <0>Más información</0>.\",\n  \"tags_title\": \"Etiquetas\",\n  \"test_upstream_btn\": \"Probar proveedores DNS\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Automático (basado en el esquema de colores de tu dispositivo)\",\n  \"theme_dark\": \"Oscuro\",\n  \"theme_dark_desc\": \"Tema oscuro\",\n  \"theme_light\": \"Claro\",\n  \"theme_light_desc\": \"Tema claro\",\n  \"thursday\": \"Jueves\",\n  \"thursday_short\": \"Jue.\",\n  \"time_table_header\": \"Hora\",\n  \"top_blocked_domains\": \"Dominios más bloqueados\",\n  \"top_clients\": \"Clientes más frecuentes\",\n  \"top_upstreams\": \"Proveedores DNS más frecuentes\",\n  \"topline_expired_certificate\": \"Tu certificado SSL ha expirado. Actualiza la <0>configuración de cifrado</0>.\",\n  \"topline_expiring_certificate\": \"Tu certificado SSL está a punto de expirar. Actualiza la <0>configuración de cifrado</0>.\",\n  \"tracker_source\": \"Fuente del rastreador\",\n  \"try_again\": \"Volver a intentar\",\n  \"ttl_cache_validation\": \"La anulación TTL mínimo de la caché debe ser menor o igual al máximo\",\n  \"tuesday\": \"Martes\",\n  \"tuesday_short\": \"Mar.\",\n  \"type_table_header\": \"Tipo\",\n  \"unavailable_dhcp\": \"DHCP no disponible\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home no puede ejecutar un servidor DHCP en tu SO\",\n  \"unblock\": \"Desbloquear\",\n  \"unblock_all\": \"Desbloquear todo\",\n  \"unblock_for_this_client_only\": \"Desbloquear solo para este cliente\",\n  \"unknown_filter\": \"Filtro desconocido {{filterId}}\",\n  \"update_announcement\": \"¡AdGuard Home {{version}} ya está disponible! <0>Haz clic aquí</0> para más información.\",\n  \"update_failed\": \"Error en la actualización automática. Por favor <a>sigue estos pasos</a> para actualizar manualmente.\",\n  \"update_now\": \"Actualizar ahora\",\n  \"updated_custom_filtering_toast\": \"Reglas personalizadas guardadas correctamente\",\n  \"updated_save_search_toast\": \"Configuración de búsqueda segura actualizada\",\n  \"updated_upstream_dns_toast\": \"Proveedores DNS guardados correctamente\",\n  \"updates_checked\": \"La nueva versión de AdGuard Home está disponible\",\n  \"updates_version_equal\": \"AdGuard Home está actualizado\",\n  \"upstream\": \"Proveedor DNS\",\n  \"upstream_dns\": \"Proveedores DNS\",\n  \"upstream_dns_cache_configuration\": \"Configuración de la caché del proveedor DNS\",\n  \"upstream_dns_client_desc\": \"Si se mantiene este campo vacío, AdGuard Home utilizará los servidores configurados en la <0>configuración DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Configurado en {{path}}\",\n  \"upstream_dns_help\": \"Ingresa una dirección de servidor por línea. <a>Más información</a> sobre la configuración de los proveedores DNS.\",\n  \"upstream_parallel\": \"Usar consultas paralelas para acelerar la resolución al consultar simultáneamente a todos los proveedores DNS.\",\n  \"upstream_timeout\": \"Tiempo de espera del proveedor DNS\",\n  \"upstream_timeout_desc\": \"Especifica el número de segundos que se debe esperar para recibir una respuesta del proveedor DNS\",\n  \"upstreams\": \"Proveedores DNS\",\n  \"use_adguard_browsing_sec\": \"Usar el servicio web de seguridad de navegación de AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home comprobará si el dominio está bloqueado por el servicio web de seguridad de navegación. Utilizará la API de búsqueda amigable con la privacidad para realizar la comprobación: solo se envía al servidor un prefijo corto del nombre de dominio con hash SHA256.\",\n  \"use_adguard_parental\": \"Usar el control parental de AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home comprobará si el dominio contiene materiales para adultos. Utiliza la misma API amigable con la privacidad del servicio web de seguridad de navegación.\",\n  \"use_private_ptr_resolvers_desc\": \"Resuelve peticiones PTR, SOA y NS para dominios ARPA que contienen direcciones IP privadas a través de proveedores DNS privados, DHCP, /etc/hosts, etc. Si se deshabilita, AdGuard Home responderá a todas estas peticiones con NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Usar resolutores DNS inversos y privados\",\n  \"use_saved_key\": \"Usar la clave guardada previamente\",\n  \"username_label\": \"Usuario\",\n  \"username_placeholder\": \"Ingresa tu nombre de usuario\",\n  \"validated_with_dnssec\": \"Validado con DNSSEC\",\n  \"version\": \"Versión\",\n  \"version_request_error\": \"Error buscar la actualización. Comprueba tu conexión a Internet.\",\n  \"wednesday\": \"Miércoles\",\n  \"wednesday_short\": \"Mié.\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/fa.json",
    "content": "{\n  \"access_allowed_desc\": \"یک لیست از CIDR یا آدرس های IP.اگر پیکربندی شود،AdGuard Home درخواست ها را فقط از این آدرس ها می پذیرد.\",\n  \"access_allowed_title\": \"کلاینت های مجاز\",\n  \"access_blocked_desc\": \"این را با فیلتر ها به اشتباه نگیرید.AdGuard Home  جستار DNS را با این دامنه ها در جستار سوال ها نمی پذیرد.\",\n  \"access_blocked_title\": \"دامنه های مسدود شده\",\n  \"access_desc\": \"در اینجا میتوانید دستورات دسترسی را برای DNS سرور AdGuard Home وارد کنید.\",\n  \"access_disallowed_desc\": \"یک لیست از CIDR یا آدرس های IP.اگر پیکربندی شود،AdGuard Home درخواست ها را از این آدرس های IP نمی پذیرد.\",\n  \"access_disallowed_title\": \"کلاینت های غیرمجاز\",\n  \"access_settings_saved\": \"تنظیمات دسترسی با موفقیت ذخیره شد\",\n  \"access_title\": \"تنظیمات دسترسی\",\n  \"actions_table_header\": \"اقدامات\",\n  \"add_allowlist\": \"افزودن لیست مجاز\",\n  \"add_blocklist\": \"افزودن لیست سیاه\",\n  \"add_custom_list\": \"یک لیست سفارشی اضافه کنید\",\n  \"add_persistent_client\": \"به عنوان مشتری دائمی اضافه کنید\",\n  \"address\": \"آدرس\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home تمام جستارهای DNS از این سرویس‌گیرنده را حذف خواهد کرد.\",\n  \"all_lists_up_to_date_toast\": \"همه لیست ها از قبل بروز اند\",\n  \"all_queries\": \"تمام جستارها\",\n  \"allow_this_client\": \"به این مشتری اجازه دهید\",\n  \"allowed\": \"اجازه داده شده\",\n  \"anonymize_client_ip\": \"گمنام کردن IP کلاینت\",\n  \"anonymize_client_ip_desc\": \"آدرس IP کلاینت در وقایع و آمارها را ذخیره نکن\",\n  \"anonymizer_notification\": \"<0>توجه:</0> ناشناس سازی IP فعال است. می توانید آن را در<1>تنظیمات عمومی غیرفعال کنید</1>.\",\n  \"answer\": \"پاسخ\",\n  \"apply_btn\": \"اِعمال\",\n  \"auto_clients_desc\": \"داده‌ها در کلاینت‌ها که از AdGuard Home استفاده می‌کنند، اما در پیکربندی ذخیره نمی‌شوند.\",\n  \"auto_clients_title\": \"کلاینت ها (زمان اِجرا)\",\n  \"autofix_warning_list\": \"این وظایف را اجرا میکند: <0>غیرفعالسازی DNSStubListener سیستم</0> <0>تنظیم آدرس DNS 127.0.0.1</0> سرور به <0>جایگزینی لینک نمادی هدف /etc/resolv.conf به/run/systemd/resolve/resolv.conf</0> <0>توقف DNSStubListener (بارگیری مجدد سرویس systemd-resolved)</0>\",\n  \"autofix_warning_result\": \"در نتیجه همه درخواست های DNS از سیستم شما بطور پیش فرض با AdGuardHome پردازش خواهد شد.\",\n  \"autofix_warning_text\": \"اگر روی \\\"تعمیر\\\" کلیک کنید، AdGuardHome سیستم شما را برای استفاده از DNS سرور AdGuardHome پیکربندی می کند.\",\n  \"average_processing_time\": \"میانگین زمان پردازش\",\n  \"average_processing_time_hint\": \"زمان میانگین بر هزارم ثانیه در پردازش درخواست DNS\",\n  \"average_upstream_response_time\": \"میانگین زمان پاسخ دهی بالادست\",\n  \"back\": \"قبلی\",\n  \"block\": \"مسدود کردن\",\n  \"block_all\": \"مسدودسازی همه\",\n  \"block_domain_use_filters_and_hosts\": \"مسدودسازی دامنه ها توسط فیلترها و فایل های میزبان\",\n  \"block_for_this_client_only\": \"مسدود کردن فقط برای این مشتری\",\n  \"block_services\": \"مسدودسازی سرویس های خاص\",\n  \"blocked_adult_websites\": \"وبسایت غیراخلاقی مسدود شده\",\n  \"blocked_by\": \"<0/>مسدود شده با<0>\",\n  \"blocked_by_cname_or_ip\": \"توسط CNAME یا IP مسدود شده است\",\n  \"blocked_by_response\": \"مسدودسازی با CNAME  یا IP در پاسخ\",\n  \"blocked_response_ttl\": \"TTL پاسخ مسدود شده\",\n  \"blocked_response_ttl_desc\": \"مشخص می کند که کلاینت ها برای چند ثانیه یک پاسخ فیلتر شده را در حافظه پنهان نگه دارند\",\n  \"blocked_safebrowsing\": \"بستن وب گردی اَمن\",\n  \"blocked_service\": \"سرویس مسدود شده\",\n  \"blocked_services\": \"سرویس های مسدود شده\",\n  \"blocked_services_desc\": \"مسدودسازی سریع سایت های عمومی و سرویس ها را اجازه می دهد.\",\n  \"blocked_services_global\": \"از سرویس های مسدود شده سراسری استفاده کن\",\n  \"blocked_services_saved\": \"سرویس های مسدود شده با موفقیت ذخیره شد\",\n  \"blocked_threats\": \"تهدیدات مسدود شده\",\n  \"blocking_ipv4\": \"مسدودسازی IPv4\",\n  \"blocking_ipv4_desc\": \"آدرس آی پی برگشت داده شده برای درخواست مسدود شده A\",\n  \"blocking_ipv6\": \"مسدودسازی IPv6\",\n  \"blocking_ipv6_desc\": \"آدرس آی پی برگشت داده شده برای درخواست مسدود شده AAAA\",\n  \"blocking_mode\": \"حالت مسدودسازی\",\n  \"blocking_mode_custom_ip\": \"آی پی دستی: پاسخ با آدرس آی پی دستی تنظیم شده\",\n  \"blocking_mode_default\": \"پیش فرض: وقتی مسدود شود با دستور سبک-مسدودساز تبلیغ REFUSED پاسخ میدهد،پاسخ با آدرس آی پی تعیین شده در دستور وقتی با دستور /etc/hosts-style rule مسدود شود\",\n  \"blocking_mode_null_ip\": \"Null IP: پاسخ با آدرس آی پی صفر(0.0.0.0 برای A; :: برای AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: پاسخ با کُد NXDOMAIN\",\n  \"blocking_mode_refused\": \"رد: پاسخ با کد رد شده\",\n  \"blocklist\": \"لیست سیاه\",\n  \"bootstrap_dns\": \"خودراه انداز سرورهای DNS\",\n  \"bootstrap_dns_desc\": \"آدرس‌های IP سرورهای DNS که برای حل کردن آدرس‌های IP حل‌کننده‌های DoH/DoT که به‌عنوان upstream مشخص می‌کنید، استفاده می‌شوند. اظهار نظر مجاز نیست.\",\n  \"cache_cleared\": \"حافظه پنهان DNS با موفقیت حذف شد\",\n  \"cache_enabled\": \"فعال کردن حافظه پنهان\",\n  \"cache_enabled_desc\": \"پاسخ‌های DNS را به صورت محلی ذخیره کنید.\",\n  \"cache_optimistic\": \"حالت ویژه پردازش\",\n  \"cache_optimistic_desc\": \"AdGuard Home را وادار می کند که از سمت حافظه پنهان پاسخ دهد حتی وقتی که موارد وارد شده منقضی شده باشد و همچنین سعی بر تازه کردن آنها می کند.\",\n  \"cache_size\": \"اندازه کش\",\n  \"cache_size_desc\": \"اندازه کش DNS (بر حسب بایت).\",\n  \"cache_size_validation\": \"اندازه حافظه پنهان هنگام فعال بودن باید بزرگتر از صفر باشد.\",\n  \"cache_ttl_max_override\": \"نادیده گرفتن حداکثر TTL\",\n  \"cache_ttl_max_override_desc\": \"حداکثر مقدار زمان تا زنده (ثانیه) را برای ورودی‌های کش DNS تنظیم کنید.\",\n  \"cache_ttl_min_override\": \"نادیده گرفتن حداقل TTL\",\n  \"cache_ttl_min_override_desc\": \"در هنگام ذخیره پاسخ های DNS، مقادیر کوتاه مدت زمان (ثانیه) دریافت شده از سرور بالادست را افزایش دهید.\",\n  \"cancel_btn\": \"لغو\",\n  \"category_label\": \"دسته بندی\",\n  \"check\": \"بررسی\",\n  \"check_client_id\": \"شناسه مشتری (ClientID یا نشانی IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"برسی اینکه نام میزبان فیلتر شده است یا نه\",\n  \"check_dhcp_servers\": \"بررسی برای سرورهای DHCP\",\n  \"check_dns_record\": \"نوع رکورد DNS را انتخاب کنید\",\n  \"check_enter_client_id\": \"شناسه مشتری را وارد کنید\",\n  \"check_hostname\": \"نام میزبان یا نام دامنه\",\n  \"check_ip\": \"آدرس آی پی: {{ip}}\",\n  \"check_not_found\": \"در لیست فیلترهای شما یافت نشد\",\n  \"check_reason\": \"علت: {{reason}}\",\n  \"check_service\": \"نام سرویس: {{service}}\",\n  \"check_title\": \"بررسی فیلترینگ\",\n  \"check_updates_btn\": \"بررسی بروز رسانی\",\n  \"check_updates_now\": \"حالا بررسی برای بروز رسانی\",\n  \"choose_allowlist\": \"لیست های مسدود را انتخاب کنید\",\n  \"choose_blocklist\": \"لیست های مسدود را انتخاب کنید\",\n  \"choose_from_list\": \"از لیست انتخاب کنید\",\n  \"city\": \"شهر\",\n  \"clear_cache\": \"پاک کردن کش\",\n  \"click_to_view_queries\": \"برای مشاهده جستارها کلیک کنید\",\n  \"client_add\": \"افزودن کلاینت\",\n  \"client_added\": \"کلاینت \\\"{{key}}\\\" را با موفقیت اضافه کرد\",\n  \"client_blocked\": \"کلاینت \\\"{{ip}}\\\" با موفقیت مسدود شد\",\n  \"client_confirm_block\": \"آیا واقعا میخواهید کلاینت \\\"{{ip}}\\\" را مسدود کنید؟\",\n  \"client_confirm_delete\": \"آیا واقعا میخواهید \\\"{{key}}\\\" کلاینت را حذف کنید؟\",\n  \"client_confirm_unblock\": \"آیا واقعا میخواهید کلاینت \\\"{{ip}}\\\" را باز کنید؟\",\n  \"client_deleted\": \"کلاینت \\\"{{key}}\\\" را با موفقیت حذف کرد\",\n  \"client_details\": \"جزئیات کلاینت\",\n  \"client_edit\": \"ویرایش کلاینت\",\n  \"client_global_settings\": \"استفاده از تنظیمات سراسری\",\n  \"client_id\": \"آیدی کاربر\",\n  \"client_id_desc\": \"کاربران را می توانید با آیدی کاربر شناسایی کنید. برای اطلاعات بیشتر درباره نحوه شناسایی کاربران <a>اینجا</a> کلیک کنید.\",\n  \"client_id_placeholder\": \"آیدی کاربر را وارد کنید\",\n  \"client_identifier\": \"احراز با\",\n  \"client_identifier_desc\": \"کلاینت میتواند با آدرس آی پی یا آدرس مَک احراز شود. لطفا توجه کنید،که استفاده از مَک بعنوان عامل احراز زمانی امکان دارد که  AdGuard Home نیز <0>سرور DHCP </0> باشد\",\n  \"client_name\": \"مشتری {{id}}\",\n  \"client_new\": \"کلاینت جدید\",\n  \"client_settings\": \"تنظیمات کلاینت\",\n  \"client_table_header\": \"کلاینت\",\n  \"client_unblocked\": \"کلاینت \\\"{{ip}}\\\" با موفقیت باز شد\",\n  \"client_updated\": \"کلاینت \\\"{{key}}\\\" با موفقیت بروز رسانی شد\",\n  \"clients_desc\": \"پیکربندی دستگاه های متصل شده به AdGuard Home\",\n  \"clients_not_found\": \"کلاینتی یافت نشد\",\n  \"clients_title\": \"کلاینت ها\",\n  \"compact\": \"فشرده\",\n  \"config_successfully_saved\": \"پیکربندی با موفقیت ذخیره شد\",\n  \"configure\": \"پیکربندی\",\n  \"confirm_dns_cache_clear\": \"آیا واقعا می‌خواهید حافظه پنهان DNS را پاک کنید؟\",\n  \"confirm_static_ip\": \"AdGuard Home {{ip}} بعنوان آدرس آی پی ثابت شما پیکربندی می کند. ادامه میدهید؟\",\n  \"copyright\": \"حق مالکیت\",\n  \"country\": \"کشور\",\n  \"custom_filter_rules\": \"دستورات فیلترینگ دستی\",\n  \"custom_filter_rules_hint\": \"یک دستور در خط وارد کنید.میتوانید از دستورات مسدودساز تبلیغ یا نحو فایل های میزبان استفاده کنید.\",\n  \"custom_filtering_rules\": \"دستورات فیلترینگ دستی\",\n  \"custom_ip\": \"آی پی دستی\",\n  \"custom_retention_input\": \"زمان ذخیره را برحسب ساعت وارد کنید\",\n  \"custom_rotation_input\": \"زمان چرخش را بر حسب ساعت وارد کنید\",\n  \"dashboard\": \"داشبورد\",\n  \"date\": \"تاریخ\",\n  \"default\": \"پيش فرض\",\n  \"delete_confirm\": \"آیا میخواهید \\\"{{key}}\\\" را حذف کنید؟\",\n  \"delete_table_action\": \"حذف\",\n  \"descr\": \"توضيحات\",\n  \"details\": \"جزئیات\",\n  \"dhcp_add_static_lease\": \"افزودن اجاره ایستا\",\n  \"dhcp_config_saved\": \"پیکربندی سرور DHCP ذخیره شده است\",\n  \"dhcp_description\": \"اگر روتر شما تنظیمات DHCP ارائه نمی کند،میتوانید از سرور DHCP تو-کار خود AdGuard استفاده کنید.\",\n  \"dhcp_disable\": \"غیرفعالسازی سرور DHCP\",\n  \"dhcp_dynamic_ip_found\": \"سیستم شما آدرس آی پی متغییر برای این رابط استفاده می کند <0>{{interfaceName}}</0>. به منظور استفاده از سرور DHCP آدرس ثابت باید تعیین شود. آدرس آی پی فعلی شما هست <0>{{ipAddress}}</0>. اگر شما دکمه DHCP را فشار دهید ما این آدرس آی پی را بعنوان ثابت تنظیم می کنیم.\",\n  \"dhcp_edit_static_lease\": \"ویرایش اجاره ایستا\",\n  \"dhcp_enable\": \"فعالسازی سرور DHCP\",\n  \"dhcp_error\": \"ما نمیتوانیم تشخیص دهیم آیا یک سرور DHCP دیگر در شبکه موجود است یا نه.\",\n  \"dhcp_form_gateway_input\": \"آی پی دروازه\",\n  \"dhcp_form_lease_input\": \"مدت اجاره\",\n  \"dhcp_form_lease_title\": \"زمان اجاره DHCP (در ثانیه)\",\n  \"dhcp_form_range_end\": \"انتهای دامنه\",\n  \"dhcp_form_range_start\": \"آغاز دامنه\",\n  \"dhcp_form_range_title\": \"دامنه آدرس های آی پی\",\n  \"dhcp_form_subnet_input\": \"ماسک زیر شبکه\",\n  \"dhcp_found\": \"تعدادی سرور DHCP فعال در شبکه یافت شد.فعالسازی سرور DHCP توکار اَمن نیست.\",\n  \"dhcp_hardware_address\": \"آدرس سخت افزار\",\n  \"dhcp_interface_select\": \"رابط DHCP را انتخاب کنید\",\n  \"dhcp_ip_addresses\": \"آدرس آی پی\",\n  \"dhcp_ipv4_settings\": \"تنظیمات DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"تنظیمات DHCP IPv4\",\n  \"dhcp_lease_added\": \"اجاره ایستا \\\"{{key}}\\\" با موفقیت اضافه شد\",\n  \"dhcp_lease_deleted\": \"اجاره ایستا \\\"{{key}}\\\" با موفقیت حذف شد\",\n  \"dhcp_lease_updated\": \"اجاره ایستا \\\"{{key}}\\\" با موفقیت به روزرسانی شد\",\n  \"dhcp_leases\": \"اجاره DHCP\",\n  \"dhcp_leases_not_found\": \"اجاره DHCP یافت نشد\",\n  \"dhcp_new_static_lease\": \"اجاره ایستا جدید\",\n  \"dhcp_not_found\": \"سرورهای فعال DHCP در شبکه یافت نشد.فعالسازی سرور DHCP تو-کار اَمن است.\",\n  \"dhcp_reset\": \"آیا میخواهید پیکربندی DHCP را ریست کنید؟\",\n  \"dhcp_reset_leases\": \"بازگردانی همه مجوزهای منابع\",\n  \"dhcp_reset_leases_confirm\": \"آیا می خواهید تمام مجوزهای منابع را بازگردانی کنید؟\",\n  \"dhcp_reset_leases_success\": \"مجوزهای منابع DHCP با موفقیت بازگردانی شد\",\n  \"dhcp_settings\": \"تنظیمات DHCP\",\n  \"dhcp_static_ip_error\": \"به منظور استفاده از سرور DHCP یک آدرس آی پی ثابت باید تنظیم شود.ما موفق به تشخیص اینکه  آیا رابط این شبکه برای استفاده از آی پی ثابت تنظیم شده است یا نه موفق نشدیم.لطفا آدرس آی پی ثابت را دستی تنظیم کنید.\",\n  \"dhcp_static_leases\": \"اجاره DHCP ایستا\",\n  \"dhcp_static_leases_not_found\": \"هیچ اجاره DHCP ایستا یافت نشد\",\n  \"dhcp_table_expires\": \"انقضاء\",\n  \"dhcp_table_hostname\": \"نام میزبان\",\n  \"dhcp_title\": \"سرور DHCP\",\n  \"dhcp_warning\": \"اگر میخواهید DHCP سرور توکار را فعال کنید،مطمئن شوید DHCP سرور دیگری فعال نباشد.در غیر اینصورت،آن دسترسی به اینترنت را برای دستگاه های وصل شده قطع می کند!\",\n  \"disable_for_hours\": \"برای {{count}} ساعت\",\n  \"disable_for_hours_plural\": \"برای {{count}} ساعت\",\n  \"disable_for_minutes\": \"برای {{count}} دقیقه\",\n  \"disable_for_minutes_plural\": \"برای {{count}} دقیقه\",\n  \"disable_for_seconds\": \"برای {{count}} ثانیه\",\n  \"disable_for_seconds_plural\": \"برای {{count}} ثانیه\",\n  \"disable_ipv6\": \"غیرفعالسازی IPv6\",\n  \"disable_ipv6_desc\": \"تمام درخواست‌های DNS برای آدرس‌های IPv6 را رها کنید (تایپ AAAA) و نکات IPv6 را از پاسخ‌های HTTPS حذف کنید.\",\n  \"disable_notify_for_hours\": \"محافظت را برای {{count}} ساعت غیرفعال کنید\",\n  \"disable_notify_for_hours_plural\": \"محافظت را برای {{count}} ساعت غیرفعال کنید\",\n  \"disable_notify_for_minutes\": \"محافظت را برای {{count}} دقیقه غیرفعال کنید\",\n  \"disable_notify_for_minutes_plural\": \"محافظت را برای {{count}} دقیقه غیرفعال کنید\",\n  \"disable_notify_for_seconds\": \"محافظت را برای {{count}} ثانیه غیرفعال کنید\",\n  \"disable_notify_for_seconds_plural\": \"محافظت را برای {{count}} ثانیه غیرفعال کنید\",\n  \"disable_notify_until_tomorrow\": \"محافظت را تا فردا غیرفعال کنید\",\n  \"disable_protection\": \"غيرفعالسازي حفاظت\",\n  \"disable_rewrites\": \"غیرفعال کردن قوانین بازنویسی\",\n  \"disable_until_tomorrow\": \"تا فردا\",\n  \"disabled\": \"غير فعال شده\",\n  \"disabled_dhcp\": \"سرور DHCP  غیرفعال شده است\",\n  \"disabled_filtering_toast\": \"فیلترینگ غیرفعال شده است\",\n  \"disabled_parental_toast\": \"نظارت والدین غیرفعال شده است\",\n  \"disabled_protection\": \"حفاظت غير فعال شده\",\n  \"disabled_safe_browsing_toast\": \"وب گردی اَمن غیر فعال شده است\",\n  \"disabled_safe_search_toast\": \"جستجوی اَمن غیرفعال شده\",\n  \"disallow_this_client\": \"این مشتری را رد کنید\",\n  \"dns_addresses\": \"آدرس های DNS\",\n  \"dns_allowlists\": \"لیست مجاز DNS\",\n  \"dns_allowlists_desc\": \"دامنه ها از لیست مجاز DNS اجازه داده میشوند حتی اگر آنها در هر لیست سیاهی باشند.\",\n  \"dns_blocklists\": \"لیست سیاه DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home دامنه های مطابق با لیست سیاه را مسدود می کند.\",\n  \"dns_cache_config\": \"تنظیمات کش DNS\",\n  \"dns_cache_config_desc\": \"در اینجا می توانید کش DNS را تنظیم کنید\",\n  \"dns_cache_size\": \"اندازه کش DNS، بر حسب بایت\",\n  \"dns_config\": \"پیکربندی DNS سرور\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"حریم خصوصی DNS\",\n  \"dns_providers\": \"در اینجا یک <0>لیست از سرویس های ارائه دهنده DNS</0> برای انتخاب هست.\",\n  \"dns_query\": \"جستار DNS\",\n  \"dns_rewrites\": \"بازنویسی های DNS\",\n  \"dns_settings\": \"تنظیمات DNS\",\n  \"dns_start\": \"سرور DNS در حال شروع است\",\n  \"dns_status_error\": \"خطا در دریافت وضعیت DNS\",\n  \"dns_test_not_ok_toast\": \"سرور \\\"{{key}}\\\": نمیتواند مورد استفاده قرار گیرد،لطفا بررسی کنید آن را بدرستی نوشته اید\",\n  \"dns_test_ok_toast\": \"سرورهای DNS تعیین شده بدرستی کار می کنند\",\n  \"dns_test_parsing_error_toast\": \"بخش {{section}}: خط {{line}}: نمی‌تواند مورد استفاده قرار گیرد،لطفا بررسی کنید آن را به‌درستی نوشته‌اید\",\n  \"dns_test_warning_toast\": \"بالادست \\\"{{key}}\\\" به درخواست های آزمایشی پاسخ نمی‌دهد و ممکن است به درستی کار نکند\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"فعالسازی DNSSEC\",\n  \"dnssec_enable_desc\": \"تنظیم نشان DNSSEC در جستارهای حاصل DNS و بررسی نتیجه (تفکیک کننده DNSSEC-فعال شده نیاز است)\",\n  \"domain\": \"دامنه\",\n  \"domain_desc\": \"نامه دامنه یا علامت تطبیقی را برای بازنویسی وارد کنید.\",\n  \"domain_name_table_header\": \"نام دامنه\",\n  \"domain_or_client\": \"دامنه یا کلاینت\",\n  \"down\": \"کار نمی کند\",\n  \"download_mobileconfig\": \"دانلود فایل تنظیمات\",\n  \"download_mobileconfig_doh\": \"Mobileconfig. را برای DNS-over-HTTPS دانلود کنید\",\n  \"download_mobileconfig_dot\": \"Mobileconfig. را برای DNS-over-TLS دانلود کنید\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"ویرایش لیست مجاز\",\n  \"edit_blocklist\": \"ویرایش لیست سیاه\",\n  \"edit_table_action\": \"ويرايش\",\n  \"edns_cs_desc\": \"اگر فعال باشد،AdGuard Home زیرشبکه های کلاینت ها را به سرورهای DNS می فرستد.\",\n  \"edns_enable\": \"فعالسازی زیرشبکه کلاینت EDNS\",\n  \"edns_use_custom_ip\": \"از IP سفارشی برای EDNS استفاده کنید\",\n  \"edns_use_custom_ip_desc\": \"اجازه استفاده از IP سفارشی برای EDNS\",\n  \"elapsed\": \"سپری شده\",\n  \"empty_response_status\": \"خالی\",\n  \"enable_protection\": \"فعالسازي حفاظت\",\n  \"enable_protection_timer\": \"حفاظت در {{time}} فعال خواهد شد\",\n  \"enable_rewrites\": \"فعال کردن قوانین بازنویسی\",\n  \"enable_upstream_dns_cache\": \"ذخیره DNS را برای پیکربندی بالادستی سفارشی این مشتری فعال کنید\",\n  \"enabled_dhcp\": \"سرور DHCP  فعال شده است\",\n  \"enabled_filtering_toast\": \"فیلترینگ فعال شده است\",\n  \"enabled_parental_toast\": \"نظارت والدین فعال شده است\",\n  \"enabled_protection\": \"حفاظت فعال شده\",\n  \"enabled_safe_browsing_toast\": \"وب گردی اَمن فعال شده است\",\n  \"enabled_save_search_toast\": \"جستجوی اَمن فعال شده\",\n  \"enabled_table_header\": \"فعال شده\",\n  \"encryption_certificate_path\": \"مسیر گواهینامه\",\n  \"encryption_certificates\": \"گواهینامه ها\",\n  \"encryption_certificates_desc\": \"به منظور استفاده از رمزگُذاری، شما باید زنجیره گواهینامه اِس اِس اِل معتبر برای دامنه خود ارائه دهید. میتوانید گواهینامه رایگان از <0>{{link}}</0> بگیرید یا میتوانید آن را از مراجع گواهینامه معتبر بخرید.\",\n  \"encryption_certificates_input\": \"کپی/چسباندن گواهینامه پی ای اِم کد گذاری شده در اینجا.\",\n  \"encryption_certificates_source_content\": \"چسباندن محتوای گواهینامه\",\n  \"encryption_certificates_source_path\": \"تنظیم مسیر فایل گواهینامه\",\n  \"encryption_chain_invalid\": \"زنجیره گواهینامه نامعتبر است\",\n  \"encryption_chain_valid\": \"زنجیره گواهینامه معتبر است\",\n  \"encryption_config_saved\": \"پیکربندی رمزگذاری ذخیره شد\",\n  \"encryption_desc\": \"پشتیبانی رمزگُذاری (HTTPS/TLS) برای DNS و رابط آدمین وب\",\n  \"encryption_doq\": \"درگاه DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"اگر این درگاه پیکربندی شده باشد، AdGuard Home یک سرور DNS-over-QUIC را بر روی این پورت اجرا خواهد کرد.\",\n  \"encryption_dot\": \"پورت DNS-over-TLS\",\n  \"encryption_dot_desc\": \"اگر این پورت پیکربندی شده باشد،AdGuard Home یک DNS-over-TLS سرور روی این پورت اجرا می کند\",\n  \"encryption_enable\": \"فعالسازی رمزگُذاری (HTTPS, DNS-over-HTTPS, و DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"اگر رمزگُذاري فعال شده باشد،رابط آدمین AdGuard Home روی HTTPS کار خواهد کرد،و سرور DNS به درخواست های روی DNS-over-HTTPS و DNS-over-TLS گوش خواهد داد.\",\n  \"encryption_expire\": \"تاریخ انقضاء\",\n  \"encryption_hostnames\": \"نام میزبان\",\n  \"encryption_https\": \"پورت HTTPS\",\n  \"encryption_https_desc\": \"اگر پورت HTTPS پیکربندی شده باشد،رابط آدمین AdGuard Home از طریق HTTPS قابل دسترسی خواهد بود و آن همچنین DNS-over-HTTPS را در مکان '/dns-query' ارائه می دهد.\",\n  \"encryption_issuer\": \"صادر کننده\",\n  \"encryption_key\": \"کلید خصوصی\",\n  \"encryption_key_input\": \"کپی/چسباندن کلید گواهینامه پی ای اِم کد گذاری شده در اینجا.\",\n  \"encryption_key_invalid\": \"این یک کلید خصوصی {{type}} نامعتبر است\",\n  \"encryption_key_source_content\": \"چسباندن محتوای کلید خصوصی\",\n  \"encryption_key_source_path\": \"تنظیم مسیر فایل کلید خصوصی\",\n  \"encryption_key_valid\": \"این یک کلید خصوصی {{type}} معتبر است\",\n  \"encryption_plain_dns_desc\": \"DNS ساده به طور پیش فرض فعال است. می توانید آن را غیرفعال کنید تا همه دستگاه ها مجبور شوند از DNS رمزگذاری شده استفاده کنند. برای انجام این کار، باید حداقل یک پروتکل DNS رمزگذاری شده را فعال کنید\",\n  \"encryption_plain_dns_enable\": \"فعالسازی DNS ساده\",\n  \"encryption_plain_dns_error\": \"برای غیرفعال کردن DNS ساده، حداقل یک پروتکل DNS رمزگذاری شده را فعال کنید\",\n  \"encryption_private_key_path\": \"مسیر کلید خصوصی\",\n  \"encryption_redirect\": \"تغییر مسیر خودکار به HTTPS\",\n  \"encryption_redirect_desc\": \"اگر انتخاب شده باشد،AdGuard Home  خودکار شما را از آدرس HTTP به HTTPS منتقل می کند\",\n  \"encryption_reset\": \"آیا میخواهید تنظیمات رمزگُذاری به پیش فرض بازگردد؟\",\n  \"encryption_server\": \"نام سرور\",\n  \"encryption_server_desc\": \"به منظور استفاده از HTTPS،شما باید نام سرور مطابق با گواهینامه اِس اِس اِل را وارد کنید.\",\n  \"encryption_server_enter\": \"نام دامنه خود را وارد کنید\",\n  \"encryption_settings\": \"تنظیمات رمزگُذاری\",\n  \"encryption_status\": \"وضعیت\",\n  \"encryption_subject\": \"موضوع\",\n  \"encryption_title\": \"رمزگُذاری\",\n  \"encryption_warning\": \"هشدار\",\n  \"enforce_safe_search\": \"اجبار جستجوی اَمن\",\n  \"enforce_save_search_hint\": \"AdGuard Home جستجوی ایمن را در موتورهای جستجوی زیر اعمال می کند: Google، YouTube، Bing، DuckDuckGo، Ecosia، Yandex، Pixabay.\",\n  \"enforced_save_search\": \"جستجوی اَمن اجبار شده\",\n  \"enter_cache_size\": \"اندازه حافظه پنهان را وارد کنید (بایت)\",\n  \"enter_cache_ttl_max_override\": \"حداکثر TTL (ثانیه) را وارد کنید\",\n  \"enter_cache_ttl_min_override\": \"حداقل TTL را وارد کنید (ثانیه)\",\n  \"enter_name_hint\": \"نام را وارد کنید\",\n  \"enter_url_or_path_hint\": \"یک آدرس یا یک مسیر کامل لیست وارد کنید\",\n  \"enter_valid_allowlist\": \"آدرس معتبر برای لیست مجاز وارد کنید.\",\n  \"enter_valid_blocklist\": \"آدرس معتبر برای لیست سیاه وارد کنید.\",\n  \"error_details\": \"جزئیات خطا\",\n  \"example_comment\": \"! در اینجا نظر قرار می گیرد\",\n  \"example_comment_hash\": \"# همچنین یک نظر\",\n  \"example_comment_meaning\": \"فقط یک توضیح\",\n  \"example_meaning_filter_block\": \"مسدودسازی دسترسی به دامنه example.org و همه زیر دامنه ها آن\",\n  \"example_meaning_filter_whitelist\": \"بازکردن دسترسی به دامنه example.org و همه زیر دامنه ها آن\",\n  \"example_meaning_host_block\": \"AdGuard Home بر می گردد به آدرس 127.0.0.1 برای دامنه example.org (اما نه زیر دامنه های آن)\",\n  \"example_multiple_upstreams_reserved\": \"می‌توانید چندین سرور بالادست را برای دامنه‌های خاص تعیین کنید؛\",\n  \"example_regex_meaning\": \"مسدودسازی دسترسی به دامنه مطابق <0>با عبارت منظم خاص</0>\",\n  \"example_rewrite_domain\": \"فقط بازنویسی پاسخ برای این دامنه.\",\n  \"example_rewrite_wildcard\": \"بازنویسی پاسخ ها برای همه زیردامنه های <0>example.org</0>.\",\n  \"example_upstream_comment\": \"یک نظر.\",\n  \"example_upstream_doh\": \"کُدگذاری شده <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a>\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS رمزگذاری شده با HTTP/3 اجباری و بدون بازگشت به HTTP/2 یا پایین‌تر؛\",\n  \"example_upstream_doq\": \"رمزگذاری شده <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"کُدگذاری شده <a href='https://en.wikipedia.org/wiki/DNS_over_TLS' target='_blank'>DNS-over-TLS</a>\",\n  \"example_upstream_regular\": \"DNS عادی (بر UDP)\",\n  \"example_upstream_regular_port\": \"dNS معمولی (بیش از UDP، با پورت)؛;\",\n  \"example_upstream_reserved\": \"میتوانید جریان ارسالی DNS <0> را برای یک دامنه مشخص تعیین کنید </0>\",\n  \"example_upstream_sdns\": \"شما میتوانید از <a href='https://dnscrypt.info/stamps/' target='_blank'>DNS Stamps</a> برای <a href='https://dnscrypt.info/' target='_blank'>DNSCrypt</a> یا <a href='https://en.wikipedia.org/wiki/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a> resolvers استفاده کنید\",\n  \"example_upstream_tcp\": \"DNS عادی (بر TCP)\",\n  \"example_upstream_tcp_hostname\": \"DNS معمولی (از طریق TCP، نام میزبان)؛;\",\n  \"example_upstream_tcp_port\": \"DNS معمولی (از طریق TCP، با پورت)؛;\",\n  \"example_upstream_udp\": \"DNS معمولی (از طریق UDP، نام میزبان)؛;\",\n  \"examples_title\": \"مثال ها\",\n  \"fallback_dns_desc\": \"لیست سرورهای DNS بازگشتی که در زمانی که سرورهای DNS بالادستی پاسخ نمی‌دهند استفاده می‌شوند. نحو مانند فیلد بالادستی اصلی بالا است.\",\n  \"fallback_dns_placeholder\": \"یک سرور DNS بازگشتی در هر خط وارد کنید\",\n  \"fallback_dns_title\": \"سرورهای DNS بازگشتی\",\n  \"faq\": \"پرسش و پاسخ\",\n  \"fastest_addr\": \"سریعترین آدرس آی پی\",\n  \"fastest_addr_desc\": \"منتظر پاسخ‌های <b>همه</b> سرورهای DNS باشید، سرعت اتصال TCP را برای هر سرور اندازه‌گیری کنید و آدرس IP سرور را با سریع‌ترین سرعت اتصال برگردانید.<br/>اگر یک یا چند سرور بالادستی پاسخ نمی‌دهند، این حالت می‌تواند به‌طور قابل‌توجهی درخواست‌های DNS را کاهش دهد. اطمینان حاصل کنید که سرورهای بالادست شما پایدار هستند و تایم اوت بالادست شما کم است.\",\n  \"filter\": \"فیلتر\",\n  \"filter_added_successfully\": \"فیلتر با موفقیت اضافه شد\",\n  \"filter_allowlist\": \"هشدار: این اقدام همچنین باعث استثناء رویه \\\"{{disallowed_rule}}\\\" از فهرست سرویس‌گیرندگان مجاز می‌شود.\",\n  \"filter_category_general\": \"General\",\n  \"filter_category_general_desc\": \"فهرست‌هایی که ردیابی و تبلیغات را بر روی اکثر دستگاه‌ها مسدود می‌کنند\",\n  \"filter_category_other\": \"ساير\",\n  \"filter_category_other_desc\": \"سایر لیست های بلاک شده\",\n  \"filter_category_regional\": \"منطقه‌ای\",\n  \"filter_category_regional_desc\": \"فهرست‌هایی که بر روی تبلیغات منطقه‌ای و سرورهای ردیابی تمرکز دارند\",\n  \"filter_category_security\": \"مسدودسازی بدافزار و فیشینگ\",\n  \"filter_category_security_desc\": \"فهرست‌هایی طراحی شده است که به طور خاص برای مسدودسازی دامنه‌های مخرب، طعمه‌گذاری و کلاهبرداری هستند\",\n  \"filter_removed_successfully\": \"لیست با موفقیت حذف شد\",\n  \"filter_updated\": \"فیلتر با موفقیت بروز رسانی شد\",\n  \"filtered\": \"فیلتر شده\",\n  \"filtered_custom_rules\": \"با دستورات فیلترینگ دستی فیلتر شده است\",\n  \"filtering_rules_learn_more\": \"درباره ایجاد لیست سیاه میزبان برای خود <0>بیشتر بدانید</0>.\",\n  \"filters\": \"فيلترها\",\n  \"filters_and_hosts_hint\": \"AdGuard Home دستورات پایه مسدودساز تبلیغ و نحو فایل های میزبان را درک می کند.\",\n  \"filters_block_toggle_hint\": \"میتوانید دستورات مسدودسازی را در تنظیمات <a>فیلترها</a> راه اندازی کنید.\",\n  \"filters_configuration\": \"پیکربندی فیلترها\",\n  \"filters_enable\": \"فعالسازی فیلترها\",\n  \"filters_interval\": \"فاصله بروز رسانی فیلترها\",\n  \"fix\": \"تعمیر\",\n  \"for_last_days\": \"برای {{count}} روز آخر\",\n  \"for_last_days_plural\": \"برای {{count}} روز گذشته\",\n  \"for_last_hours\": \"برای {{count}} ساعت گذشته\",\n  \"for_last_hours_plural\": \"برای {{count}} ساعت گذشته\",\n  \"forgot_password\": \"رمزعبور را فراموش کرده اید؟\",\n  \"forgot_password_desc\": \"لطفا <0>این مراحل</0> را برای ایجاد رمزعبور جدید برای حساب کاربری خود دنبال کنید.\",\n  \"form_add_id\": \"افزودن احرازکننده\",\n  \"form_answer\": \"نام دامنه یا آدرس آی پی را وارد کنید\",\n  \"form_client_name\": \"نام کلاینت را وارد کنید\",\n  \"form_domain\": \"نام دامنه را وارد کنید\",\n  \"form_enter_blocked_response_ttl\": \"پاسخ مسدود شده TTL (ثانیه) را وارد کنید\",\n  \"form_enter_host\": \"نام میزبان را وارد کنید\",\n  \"form_enter_hostname\": \"نام میزبان را وارد کنید\",\n  \"form_enter_id\": \"خطای احرازکننده\",\n  \"form_enter_ip\": \"آی پی را وارد کنید\",\n  \"form_enter_mac\": \"مَک را وارد کنید\",\n  \"form_enter_rate_limit\": \"میزان محدودیت را وارد کنید\",\n  \"form_enter_rate_limit_subnet_len\": \"طول پیشوند زیر شبکه را برای محدود کردن نرخ وارد کنید\",\n  \"form_enter_subnet_ip\": \"یک آدرس آی پی در زیر شبکه \\\"{{cidr}}\\\" وارد کنید\",\n  \"form_enter_upstream_timeout\": \"مدت زمان سرورهای بالادستی را بر حسب ثانیه وارد کنید\",\n  \"form_error_answer_format\": \"فرمت پاسخ اشتباه است\",\n  \"form_error_client_id_format\": \"فرمت شناسه کلاینت نامعتبر است\",\n  \"form_error_domain_format\": \"فرمت دامنه اشتباه است\",\n  \"form_error_equal\": \"نباید برابر باشد\",\n  \"form_error_gateway_ip\": \"Lease نمی تواند آدرس IP دروازه را داشته باشد\",\n  \"form_error_ip4_format\": \"فرمت نامعتبر IPv4\",\n  \"form_error_ip4_gateway_format\": \"قالب IPv4 درگاه نامعتبر است\",\n  \"form_error_ip6_format\": \"فرمت نامعتبر IPv6\",\n  \"form_error_ip_format\": \"فرمت IPv4 نامعتبر است\",\n  \"form_error_mac_format\": \"فرمت مَک نامعتبر است\",\n  \"form_error_password\": \"رمزعبور تطبیق ندارد\",\n  \"form_error_password_length\": \"رمزعبور باید بین {{min}} تا {{max}} کاراکتر باشد.\",\n  \"form_error_port\": \"مقدار پورت معتبر وارد کنید\",\n  \"form_error_port_range\": \"مقدار پورت را در محدوده 80-65535 وارد کنید\",\n  \"form_error_port_unsafe\": \"این پورت غیر ایمن است\",\n  \"form_error_positive\": \"باید بزرگتر از 0 باشد\",\n  \"form_error_required\": \"فیلد مورد نیاز\",\n  \"form_error_server_name\": \"نام سرور نامعتبر است\",\n  \"form_error_subnet\": \"زیرشبکه\\\"{{cidr}}\\\"آدرس آی پی {{ip}} را در بر ندارد\",\n  \"form_error_url_format\": \"فرمت آدرس نامعتبر است\",\n  \"form_error_url_or_path_format\": \"آدرس نامعتبر یا یک مسیر کامل لیست\",\n  \"form_select_tags\": \"انتخاب برچسب کلاینت\",\n  \"found_in_known_domain_db\": \"در پایگاه داده دامنه های شناخته شده پیدا شد\",\n  \"friday\": \"جمعه\",\n  \"friday_short\": \"جمعه\",\n  \"gateway_or_subnet_invalid\": \"پوشش زیرشبکه نامعتبر است\",\n  \"general_settings\": \"تنظیمات عمومی\",\n  \"general_statistics\": \"آمار عمومی\",\n  \"get_started\": \"شروع به کار\",\n  \"greater_range_start_error\": \"باید بیشتر از شروع دامنه باشد\",\n  \"homepage\": \"صفحه خانگي\",\n  \"host_whitelisted\": \"سایت در لیست سفید است\",\n  \"ignore_domains\": \"دامنه های نادیده گرفته شده (با خط جدید جدا شده‌اند)\",\n  \"ignore_domains_desc_query\": \"پرس و جوهایی که با این قوانین مطابقت دارند در گزارش پرس و جو نوشته نمی‌شوند\",\n  \"ignore_domains_desc_stats\": \"جستارهای مطابق با این قوانین در آمار نوشته نمی‌شوند\",\n  \"ignore_domains_title\": \"دامنه‌های نادیده گرفته شده\",\n  \"ignore_query_log\": \"این مشتری را در گزارش پرس و جو نادیده بگیرید\",\n  \"ignore_statistics\": \"این مشتری را در آمار نادیده بگیرید\",\n  \"install_auth_confirm\": \"تأیید رمزعبور\",\n  \"install_auth_desc\": \"بسیار توصیه میشود که رمزعبور احراز هویت را برای رابط وب آدمین AdGuard Home پیکربندی کنید.حتی اگر فقط در شبکه محلی خود قابل دسترسی باشد،برای حفاظت و مسدود کردن دسترسی غیر مجاز و نامحدود این بسیار ضروری است.\",\n  \"install_auth_password\": \"رمزعبور\",\n  \"install_auth_password_enter\": \"رمزعبور را وارد کنید\",\n  \"install_auth_title\": \"احراز هویت\",\n  \"install_auth_username\": \"نام کاربر\",\n  \"install_auth_username_enter\": \"نام کاربر را وارد کنید\",\n  \"install_devices_address\": \"DNS سرور AdGuard Home به آدرس های زیر گوش میدهد\",\n  \"install_devices_android_list_1\": \"از منوی صفحه خانه آندروئید،تنظیمات را فشار دهید\",\n  \"install_devices_android_list_2\": \"وای فای را در منو فشار دهید،صفحه لیست کردن همه شبکه های موجود نشان داده میشود (تنظیم DNS دستی برای ارتباط موبایلی غیرممکن است)\",\n  \"install_devices_android_list_3\": \"به شبکه ای که متصل شده اید فشار طولانی دهید و ویرایش شبکه را انتخاب کنید.\",\n  \"install_devices_android_list_4\": \"در برخی دستگاه ها،شما ممکن است کادر پیشرفته را برای تنظیمات بعدی بررسی کنید.برای تنظیم DNS آندروئید خود،نیاز است شما از تنظیمات IP را از DHCP  به Staticتغییر دهید.\",\n  \"install_devices_android_list_5\": \"گروه مقادیر DNS 1 و DNS 2 را به آدرس سرور AdGuard Home خود تغییر دهید.\",\n  \"install_devices_desc\": \"به منظور اینکه AdGuard Home شروع به کار کند،باید دستگاه خود را برای استفاده از آن پیکربندی کنید.\",\n  \"install_devices_ios_list_1\": \"از صفحه خانه،تنظیمات را فشار دهید.\",\n  \"install_devices_ios_list_2\": \"وای فای را از منوی چپ انتخاب کنید (پیکربندی DNS دستی برای ارتباط موبایلی غیرممکن است).\",\n  \"install_devices_ios_list_3\": \"روی نام شبکه فعال فعلی کلیک کنید.\",\n  \"install_devices_ios_list_4\": \"در فیلد DNS آدرس سرور AdGuard Home را وارد کنید\",\n  \"install_devices_macos_list_1\": \"روی آیکون اَپل کلیک کرده و بروید به اولویت های سیستم\",\n  \"install_devices_macos_list_2\": \"روی شبکه کلیک کنید\",\n  \"install_devices_macos_list_3\": \"اولین ارتباط را از لیست خود انتخاب و روی پیشرفته کلیک کنید.\",\n  \"install_devices_macos_list_4\": \"تب DNS را انتخاب و آدرس های سرور AdGuard Home  خود را وارد کنید.\",\n  \"install_devices_router\": \"روتر\",\n  \"install_devices_router_desc\": \"این راه انداز خودکار همه دستگاه های متصل شده به روتر خانه را پوشش میدهد و نیازی نیست شما هر یک از آنها را دستی پیکربندی کنید.\",\n  \"install_devices_router_list_1\": \"اولویت ها را برای روتر خود باز کنید.معمولا میتوانید آن را ز طریق مرورگر از طریق آدرسی مانند ( http://192.168.0.1/ یا http://192.168.1.1/) دسترسی داشته باشید.ممکن است رمزعبور پرسیده شود،اگر آن را بخاطر ندارید،غالبا میتوان رمزعبور را با فشردن دکمه پشت روتر ریست کرد.برخی روترها برنامه خاصی نیاز دارد که باید در رایانه/گوشی نصب شده باشد.\",\n  \"install_devices_router_list_2\": \"تنظیمات DHCP/DNS را بیابید.دنبال حروف DNS بگردید در فیلدی که اجازه دو یا سه گروه عدد را میدهد و هر کدام در چهار گروه سه عددی شکسته شده است\",\n  \"install_devices_router_list_3\": \"آدرس سرور AdGuard Home  خود را آنجا وارد کنید\",\n  \"install_devices_router_list_4\": \"شما نمیتوانید DNS سرور سفارشی در برخی از روترها تنظیم کنید. در این مورد اگر شما AdGuard Home را بعنوان DHCP سرور راه اندازی کنید میتواند کمک کند. در غیر اینصورت باید راهنمای سفارشی سازی DNS سرورها برای مدل خاص روتر خود را انتخاب کنید.\",\n  \"install_devices_title\": \"پیکربندی دستگاه شما\",\n  \"install_devices_windows_list_1\": \"کنترل پنل را از طریق استارت منو یا جستجوی ویندوز باز کنید.\",\n  \"install_devices_windows_list_2\": \"بروید به شبکه و دسته اینترنت و سپس به شبکه و مرکز اشتراک گذاری\",\n  \"install_devices_windows_list_3\": \"در سمت چپ صفحه تنظیمات آداپتور را تغییر داده و روی آن کلیک کنید\",\n  \"install_devices_windows_list_4\": \"ارتباط فعال خود را انتخاب کرده،روی آن راست کلیک کرده و مشخصات را انتخاب کنید.\",\n  \"install_devices_windows_list_5\": \"پروتکل اینترنت نسخه 4 (TCP/IP) را در لیست بیابید،آن را انتخاب و سپس روی مشخصات دوباره کلیک کنید.\",\n  \"install_devices_windows_list_6\": \"گزینه استفاده از آدرس DNS سرور زیر را انتخاب کرده و آدرس سرور AdGuard Home  خود را وارد کنید.\",\n  \"install_saved\": \"با موفقیت ذخیره نشد\",\n  \"install_settings_all_interfaces\": \"همه رابط ها\",\n  \"install_settings_dns\": \"سرور DNS\",\n  \"install_settings_dns_desc\": \"نیاز است شما دستگاه یا روتر خود را برای استفاده از سرور DNS روی آدرس های زیر پیکربندی کنید:\",\n  \"install_settings_interface_link\": \"رابط صفحه وب آدمین AdGuard Home شما در این آدرس قابل دسترسی خواهد بود:\",\n  \"install_settings_listen\": \"رابط گوش دادن\",\n  \"install_settings_port\": \"پورت\",\n  \"install_settings_title\": \"رابط وب آدمین\",\n  \"install_static_configure\": \"ما تشخیص دادیم از آدرس آی پی پویا استفاده شده است — <0>{{ip}}</0>. آیا میخواهید از آن بعنوان آدرس ثابت استفاده کنید؟\",\n  \"install_static_error\": \"AdGuard Home نمیتواند رابط این شبکه را خودکار پیکربندی کند. لطفا دستورالعمل چگونگی انجام دستی آن را مطالعه کنید.\",\n  \"install_static_ok\": \"خبر خوب! آدرس آی پی ثابت از قبل پیکربندی شده است\",\n  \"install_step\": \"گام\",\n  \"install_submit_desc\": \"روش راه اندازی به پایان رسیده و شما آماده استفاده از AdGuard Home هستید.\",\n  \"install_submit_title\": \"تبریک می گوییم!\",\n  \"install_welcome_desc\": \"AdGuard Home یک شبکه گسترده و ردیاب و مسدوساز تبلیغ با سرور DNS است.هدف آن این است که به شما اجازه کنترل کل شبکه و همه دستگاه های شما را بدهد و آن نیازی به برنامه سمت-کاربر ندارد.\",\n  \"install_welcome_title\": \"به AdGuard Home خوش آمدید!\",\n  \"interval_24_hour\": \"24 ساعت\",\n  \"interval_6_hour\": \"6 ساعت\",\n  \"interval_days\": \"{{value}} روز\",\n  \"interval_days_plural\": \"{{count}} روز\",\n  \"interval_hours\": \"{{count}} ساعت\",\n  \"interval_hours_plural\": \"{{count}} ساعت\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"آدرس آی پی\",\n  \"known_tracker\": \"ردیاب های شناخته شده\",\n  \"last_rule_in_allowlist\": \"نمی توان این مشتری را غیر فعال کرد چرا که استثناء قاعده \\\"{{disallowed_rule}}\\\" باعث غیر فعال شدن فهرست \\\"مشتریان مجاز\\\" می شود.\",\n  \"last_time_updated_table_header\": \"زمان آخرین بروزرسانی\",\n  \"list_confirm_delete\": \"آیا واقعا میخواهید این لیست را حذف کنید؟\",\n  \"list_label\": \"لیست\",\n  \"list_updated\": \"{{count}} لیست بروز رسانی شد\",\n  \"list_updated_plural\": \"{{count}} لیست بروز رسانی شد\",\n  \"list_url_table_header\": \"لیست آدرس\",\n  \"load_balancing\": \"متعادل کننده بار\",\n  \"load_balancing_desc\": \"یک سرور بالادستی را در یک زمان پرس‌و‌جو کنید.<br/>AdGuard Home از یک الگوریتم تصادفی وزنی برای انتخاب سرورهایی با کمترین تعداد جستجوی ناموفق و کمترین میانگین زمان جستجو استفاده می‌کند.\",\n  \"loading_table_status\": \"بارگیری...\",\n  \"local_ptr_default_resolver\": \"به طور پیش فرض، AdGuard Home از تعیین کننده های DNS معکوس زیر استفاده می کند: {{ip}}.\",\n  \"local_ptr_desc\": \"سرورهای DNS که توسط AdGuard Home برای درخواست‌های خصوصی PTR، SOA و NS استفاده می‌شوند. اگر درخواست یک دامنه ARPA حاوی یک زیرشبکه در محدوده IP خصوصی (مانند \\\"192.168.12.34\\\") و از مشتری با آدرس IP خصوصی باشد، خصوصی در نظر گرفته می شود. اگر تنظیم نشود، به جز آدرس‌های IP Home AdGuard، از حل‌کننده‌های پیش‌فرض DNS سیستم عامل شما استفاده می‌شود.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home نتوانست برای این دستگاه تعیین کننده های DNS معکوس محرمانه مناسب را معین کند.\",\n  \"local_ptr_placeholder\": \"یک آدرس IP در هر خط وارد کنید\",\n  \"local_ptr_title\": \"سرورهای خصوصی DNS\",\n  \"location\": \"مکان\",\n  \"log_and_stats_section_label\": \"گزارش پرس و جو و آمار\",\n  \"lower_range_start_error\": \"باید کمتر از شروع دامنه باشد\",\n  \"main_settings\": \"تنظیمات اصلی\",\n  \"make_static\": \"ایستا کنید\",\n  \"manual_update\": \"لطفا<a>این مراحل را دنبال کنید</a>تا به طور دستی بروزرسانی نمایید.\",\n  \"milliseconds_abbreviation\": \"هـ ثـ\",\n  \"monday\": \"دوشنبه\",\n  \"monday_short\": \"دوشنبه\",\n  \"name\": \"نام\",\n  \"name_table_header\": \"نام\",\n  \"netname\": \"نام شبکه\",\n  \"network\": \"شبکه\",\n  \"new_allowlist\": \"لیست مجاز جدید\",\n  \"new_blocklist\": \"لیست سیاه جدید\",\n  \"next\": \"بعدی\",\n  \"next_btn\": \"بعدی\",\n  \"no_blocklist_added\": \"به لیست سیاه اضافه نشد\",\n  \"no_clients_found\": \"کلاینتی یافت نشد\",\n  \"no_domains_found\": \"دامنه یافت نشد\",\n  \"no_logs_found\": \"وقایع یافت نشد\",\n  \"no_servers_specified\": \"سروری تعیین نشده است\",\n  \"no_upstreams_data_found\": \"هیچ اطلاعاتی در مورد سرورهای بالادست یافت نشد\",\n  \"no_whitelist_added\": \"به لیست مجاز اضافه نشد\",\n  \"nothing_found\": \"هیچ چیز یافت نشد\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"تعداد درخواست DNS مسدود شده با فیلترهای مسدودساز تبلیغ و لیست سیاه میزبان\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"تعداد وبسایت های غیر اخلاقی مسدود شده\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"تعداد درخواست DNS مسدود شده با مدل امنیت وب گردی AdGuard\",\n  \"number_of_dns_query_days\": \"تعداد جستار DNS پردازش شده در {{count}} روز آخر\",\n  \"number_of_dns_query_days_plural\": \"تعداد جستار DNS پردازش شده در {{count}} روز گذشته\",\n  \"number_of_dns_query_hours\": \"تعداد جستارهای DNS پردازش شده برای {{count}} ساعت گذشته\",\n  \"number_of_dns_query_hours_plural\": \"تعداد جستارهای DNS پردازش شده برای {{count}} ساعت گذشته\",\n  \"number_of_dns_query_to_safe_search\": \"تعداد درخواست های DNS برای موتور جستجو که جستجوی اَمن اجبار شده\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"خاموش\",\n  \"on\": \"روشن\",\n  \"open_dashboard\": \"بازکردن داشبورد\",\n  \"orgname\": \"نام سازمان\",\n  \"original_response\": \"پاسخ اصلی\",\n  \"out_of_range_error\": \"باید خارج از دامنه باشد\\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"صفحه\",\n  \"parallel_requests\": \"درخواست های موازی\",\n  \"parental_control\": \"نظارت والدین\",\n  \"password_label\": \"رمزعبور\",\n  \"password_placeholder\": \"رمزعبور را وارد کنید\",\n  \"plain_dns\": \"DNS ساده\",\n  \"port_53_faq_link\": \"درگاه ۵۳ اغلب توسط خدمت‌های \\\"DNSStubListener\\\" یا \\\"systemd-resolved\\\" اشغال می‌شود. لطفاً <0>دستورالعمل‌ها</0> را برای حل این موضوع مطالعه کنید.\",\n  \"previous_btn\": \"قبلی\",\n  \"privacy_policy\": \"سیاست حریم خصوصی\",\n  \"processing_update\": \"منتظر بمانید،AdGuard Home در حال بروز رسانی است\",\n  \"protection_section_label\": \"حفاظت\",\n  \"protocol\": \"پروتکل\",\n  \"punycode\": \"پونیکد\",\n  \"query_log\": \"جستار وقایع\",\n  \"query_log_clear\": \"پاکسازی وقایع جستار\",\n  \"query_log_cleared\": \"وقایع جستار با موفقیت پاک شد\",\n  \"query_log_configuration\": \"پیکربندی وقایع\",\n  \"query_log_confirm_clear\": \"آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟\",\n  \"query_log_disabled\": \"وقایع جستار غیرفعال شده است و میتواند در <0>تنظیمات</0> پیکربندی شود\",\n  \"query_log_enable\": \"فعالسازی وقایع\",\n  \"query_log_filtered\": \"فیلتر شده با {{filter}}\",\n  \"query_log_response_status\": \"وضعیت: {{value}}\",\n  \"query_log_retention\": \"حفظ وقایع جستار برای\",\n  \"query_log_retention_confirm\": \"آیا واقعا میخواهید مدت حفظ وقایع جستار را تغییر دهید؟ اگر فاصله را کاهش دهید، برخی داده ها حذف میشود\",\n  \"query_log_strict_search\": \"برای جستجوی موکد از علامت نقل قول دوتایی استفاده کنید\",\n  \"query_log_updated\": \"فیلتر با موفقیت بروز رسانی شد\",\n  \"rate_limit\": \"میزان محدودیت\",\n  \"rate_limit_desc\": \"تعداد درخواست های بر ثانیه مجازی که یک کلاینت میتواند بسازد (0: نامحدود)\",\n  \"rate_limit_subnet_len_ipv4\": \"طول پیشوند زیرشبکه برای نشانی‌های IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"طول پیشوند زیرشبکه برای نشانی‌های IPv4 استفاده شده برای محدودیت نرخ. پیش‌فرض ۲۴ است\",\n  \"rate_limit_subnet_len_ipv4_error\": \"طول پیشوند زیرشبکه IPv4 باید بین ۰ و ۳۲ باشد\",\n  \"rate_limit_subnet_len_ipv6\": \"طول پیشوند زیرشبکه برای نشانی‌های IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"طول پیشوند زیرشبکه برای نشانی‌های IPv6 استفاده شده برای محدودیت نرخ. پیش‌فرض ۵۶ است\",\n  \"rate_limit_subnet_len_ipv6_error\": \"طول پیشوند زیرشبکه IPv6 باید بین ۰ تا ۱۲۸ باشد\",\n  \"rate_limit_whitelist\": \"لیست مجاز محدود کننده نرخ\",\n  \"rate_limit_whitelist_desc\": \"نشانی‌های آی پی استثناء شده از محدودیت نرخ\",\n  \"rate_limit_whitelist_placeholder\": \"یک آدرس IP در هر خط وارد کنید\",\n  \"refresh_btn\": \"تازه سازی\",\n  \"refresh_statics\": \"تازه سازی آمار\",\n  \"refused\": \"مرفوض\",\n  \"report_an_issue\": \"گزارش یک مشکل\",\n  \"request_details\": \"درخواست جزئیات\",\n  \"request_table_header\": \"درخواست\",\n  \"requests_count\": \"تعداد درخواست ها\",\n  \"reset_settings\": \"ریست تنظیمات\",\n  \"resolve_clients_desc\": \"در صورت فعال بودن،AdGuard Home به طور خودکار اقدام به تعیین نام های سرویس دهنده ی سرویس گیرندگان از آدرس های آی پی با ارسال یک درخواست PTR به یک تعیین کننده ی همتا خواهد کرد (سرور خصوصی DNS برای سرویس گیرندگان محلی،سرور مادر برای سرویس گیرندگان با آی پی عمومی).\",\n  \"resolve_clients_title\": \"فعال سازی تعیین نام های سرویس دهنده ی سرویس گیرندگان\",\n  \"response_code\": \"کد پاسخ\",\n  \"response_details\": \"جزئیات پاسخ\",\n  \"response_table_header\": \"پاسخ\",\n  \"response_time\": \"زمان پاسخ\",\n  \"rewrite_A\": \"<0>A</0>: مقدار ویژه، <0>A</0> سوابق از بالادست را نگه دارید\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: مقدار ویژه، <0>AAAA</0> سوابق از بالادست را نگه دارید\",\n  \"rewrite_add\": \"افزودن بازنویسی DNS\",\n  \"rewrite_added\": \"بازنویسی DNS برای \\\"{{key}}\\\" با موفقیت اضافه شد\",\n  \"rewrite_applied\": \"دستور بازنویسی اِعمال شد\",\n  \"rewrite_confirm_delete\": \"آیا واقعا میخواهید بازنویسی DNS برای \\\"{{key}}\\\" را حذف کنید؟\",\n  \"rewrite_deleted\": \"بازنویسی DNS برای \\\"{{key}}\\\" با موفقیت حذف شد\",\n  \"rewrite_desc\": \"به آسانی اجازه پیکربندی پاسخ DNS دستی برای یک نام دامنه خاص را می دهد.\",\n  \"rewrite_domain_name\": \"نام دامنه: یک رکورد CNAME اضافه کنید\",\n  \"rewrite_edit\": \"بازنویسی DNS را ویرایش کنید\",\n  \"rewrite_hosts_applied\": \"بازنویسی با دستور فایل میزبان\",\n  \"rewrite_ip_address\": \"آدرس IP: از این IP در پاسخ A یا AAAA استفاده کنید\",\n  \"rewrite_not_found\": \"بازنویسی DNS یافت نشد\",\n  \"rewrite_settings_updated\": \"تنظيمات بازنویسی DNS با موفقیت به‌روزرسانی شد\",\n  \"rewrite_updated\": \"بازنویسی DNS با موفقیت به روز شد\",\n  \"rewrites_disabled_table_header\": \"بازنویسی‌ها غیرفعال هستند\",\n  \"rewrites_enabled_table_header\": \"بازنویسی‌ها فعال هستند\",\n  \"rewritten\": \"بازنویسی شده\",\n  \"rows_table_footer_text\": \"سطر\",\n  \"rule_added_to_custom_filtering_toast\": \"دستور به دستورات فیلترینگ دستی اضافه شد {{rule}}\",\n  \"rule_label\": \"دستور\",\n  \"rule_removed_from_custom_filtering_toast\": \"دستور از دستورات فیلترینگ دستی حذف شد {{rule}}\",\n  \"rules_count_table_header\": \"تعداد دستور\",\n  \"safe_browsing\": \"وب گردی اَمن\",\n  \"safe_search\": \"جستجوی اَمن\",\n  \"saturday\": \"شنبه\",\n  \"saturday_short\": \"شنبه\",\n  \"save_btn\": \"ذخیره\",\n  \"save_config\": \"ذخیره پیکربندی\",\n  \"schedule_add\": \"برنامه اضافه کنید\",\n  \"schedule_current_timezone\": \"منطقه زمانی فعلی: {{value}}\",\n  \"schedule_desc\": \"دوره های عدم فعالیت را برای سرویس های مسدود شده تنظیم کنید\",\n  \"schedule_edit\": \"ویرایش برنامه\",\n  \"schedule_from\": \"از\",\n  \"schedule_invalid_select\": \"زمان شروع باید قبل از زمان پایان باشد\",\n  \"schedule_modal_description\": \"این برنامه جایگزین برنامه‌های موجود برای همان روز هفته خواهد شد. هر روز از هفته می تواند تنها یک دوره عدم فعالیت داشته باشد.\",\n  \"schedule_modal_time_off\": \"بدون مسدود کردن سرویس:\",\n  \"schedule_new\": \"برنامه جدید\",\n  \"schedule_remove\": \"حذف برنامه\",\n  \"schedule_save\": \"ذخیره برنامه\",\n  \"schedule_select_days\": \"روزها را انتخاب کنید\",\n  \"schedule_services\": \"توقف توقف سرویس\",\n  \"schedule_services_desc\": \"برنامه مکث فیلتر مسدودکننده سرویس را پیکربندی کنید\",\n  \"schedule_services_desc_client\": \"برنامه توقف موقت فیلتر مسدودکننده سرویس را برای این سرویس گیرنده پیکربندی کنید\",\n  \"schedule_time_all_day\": \"تمام روز\",\n  \"schedule_timezone\": \"یک منطقه زمانی را انتخاب کنید\",\n  \"schedule_to\": \"تا\",\n  \"served_from_cache_label\": \"از حافظه پنهان ارائه می‌شود\",\n  \"service_name\": \"نام خدمت\",\n  \"set_static_ip\": \"تنظیم یک آدرس آی پی ثابت\",\n  \"settings\": \"تنظيمات\",\n  \"settings_custom\": \"دستي\",\n  \"settings_global\": \"سراسری\",\n  \"setup_config_to_enable_dhcp_server\": \"پیکربندی را برای فعال کردن سرور DHCP تنظیم کنید\",\n  \"setup_dns_notice\": \"به منظور استفاده از <1>DNS-over-HTTPS</1> یا <1>DNS-over-TLS</1>، شما نیاز به <0>پیکربندی رمزگذاری</0> در تنظیمات AdGuard Home دارید.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> استفاده از<1>{{address}}</1> .\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> استفاده از <1>{{address}}</1> .\",\n  \"setup_dns_privacy_3\": \"<0>لطفا توجه کنید که پروتکل های رمزگذاری شده DNS فقط در آندروئید 9 پشتیبانی می شود. پس برای سیستم عامل های دیگر نیاز است که برنامه دیگری نصب کنید.</0><0>در اینجا میتوانید لیست نرم افزارهای قابل استفاده را ببینید.</0>\",\n  \"setup_dns_privacy_4\": \"در یک دستگاه iOS 14 یا macOS Big Sur می‌توانید فایل ویژه '.mobileconfig' را دانلود کنید که <highlight>DNS-over-HTTPS</highlight> یا <highlight>DNS-over-TLS</highlight> سرورها را به تنظیمات DNS اضافه می‌کند.\",\n  \"setup_dns_privacy_android_1\": \"آندروئید 9 بطور پیش فرض از  DNS-over-TLS پشتیبانی می کند. برای پیکربندی آن، بروید به تنظیمات → شبکه & اینترنت → پیشرفته → DNS خصوصی و نام دامنه را آنجا وارد کنید.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0> پشتیبانی از <1>DNS-over-HTTPS</1> و <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> قابلیت <1>DNS-over-HTTPS</1> را به آندروئید اضافه می کند.\",\n  \"setup_dns_privacy_ioc_mac\": \"پیکربندی iOS و macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> پشتیبانی از <1>DNS-over-HTTPS</1>, اما بمنظور پیکربندی آن برای استفاده بعنوان سرور خود،شما نیاز دارید که <2>DNS Stamp</2> برای آن تولید کنید.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard for iOS</0> پشتیبانی از <1>DNS-over-HTTPS</1> و راه اندازی <1>DNS-over-TLS</1> .\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home خودش میتواند کلاینت DNS اَمن را در هر سیستم عاملی پیاده کند.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> از همه پروتکل های DNS شناخته شده پشتیبانی می کند.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> supportsپشتیبانی از <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> پشتیبانی از <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"شما میتوانید راه کارهای بیشتر را در <0>اینجا</0> و <1>اینجا</1> پیدا کنید.\",\n  \"setup_dns_privacy_other_title\": \"سایر راه کارها\",\n  \"setup_guide\": \"راهنمای راه اندازی\",\n  \"show_all_filter_type\": \"نمایش همه\",\n  \"show_blocked_responses\": \"مسدود شده\",\n  \"show_filtered_type\": \"نمایش فیلتر شده\",\n  \"show_processed_responses\": \"پردازش شده\",\n  \"show_whitelisted_responses\": \"لیست سفید\",\n  \"sign_in\": \"ورود\",\n  \"sign_out\": \"خروج\",\n  \"source_label\": \"منبع\",\n  \"static_ip\": \"آدرس آی پی ثابت\",\n  \"static_ip_desc\": \"AdGuard Home یک سرور است بنابراین به یک آدرس آی پی ثابت برای کارکرد درست نیاز دارد. در غیراینصورت، در نهایت، روتر شما میتواند آدرس آی پی متفاوت به این دستگاه اختصاص دهد.\",\n  \"statistics_clear\": \" پاکسازی آمار\",\n  \"statistics_clear_confirm\": \"آیا واقعا میخواهید آمار را پاک کنید؟\",\n  \"statistics_cleared\": \"آمارها با موفقیت حذف شد\",\n  \"statistics_configuration\": \"پیکربندی آمارها\",\n  \"statistics_enable\": \"فعالسازی داده های آماری\",\n  \"statistics_retention\": \"مدت حفظ آمارها\",\n  \"statistics_retention_confirm\": \"آیا واقعا میخواهید مدت حفظ آمار را تغییر دهید؟ اگر فاصله را کاهش دهید، برخی داده ها حذف میشود\",\n  \"statistics_retention_desc\": \"اگر مقدار فاصله را کاهش دهید،برخی داده ها از بین خواهد رفت\",\n  \"stats_adult\": \"وبسایت غیراخلاقی مسدود شده است\",\n  \"stats_disabled\": \"آمار غیرفعال شده است. شما می توانید از قسمت <0>صفحه تنظیمات</0> آن را روشن نمایید.\",\n  \"stats_disabled_short\": \"آمار غیرفعال شده است\",\n  \"stats_malware_phishing\": \"بدافزار/فیشینگ مسدود شده است\",\n  \"stats_params\": \"پیکربندی آمار\",\n  \"stats_query_domain\": \"دامنه جستار بالا\",\n  \"subnet_error\": \"آدرس ها باید در یک زیرشبکه باشند\",\n  \"sunday\": \"یکشنبه\",\n  \"sunday_short\": \"یکشنبه\",\n  \"system_host_files\": \"فایل های هوست سیستم\",\n  \"table_client\": \"کلاینت\",\n  \"table_name\": \"نام\",\n  \"tags_desc\": \"میتوانید برچسب مطابق با کلاینت را انتخاب کنید. برچسب ها میتواند شامل دستورات فیلترینگ بوده و به شما اجازه اعمال دقیق تر را میدهد. <0>بیشتر بدانید</0>\",\n  \"tags_title\": \"برچسب ها\",\n  \"test_upstream_btn\": \"تست جریان ارسالی\",\n  \"theme_auto\": \"خودکار\",\n  \"theme_auto_desc\": \"اتوماتیک (بر اساس طرح رنگی دستگاه شما)\",\n  \"theme_dark\": \"پوسته تیره\",\n  \"theme_dark_desc\": \"پوسته تیره\",\n  \"theme_light\": \"پوسته روشن\",\n  \"theme_light_desc\": \"پوسته روشن\",\n  \"thursday\": \"پنج شنبه\",\n  \"thursday_short\": \"پنج شنبه\",\n  \"time_table_header\": \"زمان\",\n  \"top_blocked_domains\": \"دامنه های بیشتر مسدود شده\",\n  \"top_clients\": \"بالاترین کلاینت ها\",\n  \"top_upstreams\": \"سرورهای بالادست بالا\",\n  \"topline_expired_certificate\": \"گواهینامه اِس اِس اِل شما منقضی شده است. <0>تنظیمات رمزگُذاری</0> را بروز رسانی کنید.\",\n  \"topline_expiring_certificate\": \"گواهینامه اِس اِس اِل شما در صدد انقضاء است.  <0>تنظیمات رمزگُذاری</0> را بروز رسانی کنید.\",\n  \"tracker_source\": \"منبع ردیاب\",\n  \"try_again\": \"امتحان دوباره\",\n  \"ttl_cache_validation\": \"حداقل لغو TTL حافظه نهان باید کمتر یا مساوی حداکثر باشد\",\n  \"tuesday\": \"سهشنبه\",\n  \"tuesday_short\": \"سه شنبه\",\n  \"type_table_header\": \"نوع\",\n  \"unavailable_dhcp\": \"DHCP در دسترس نیست\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home نمی تواند سرور DHCP را روی سیستم عامل شما اجرا کند\",\n  \"unblock\": \"رفع انسداد\",\n  \"unblock_all\": \"گشودن همه\",\n  \"unblock_for_this_client_only\": \"رفع مسدودیت فقط برای این کاربر\",\n  \"unknown_filter\": \"فیلتر ناشناخته {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} در دسترس است! <0>اینجا کلیک</0> کنید برای اطلاعات بیشتر.\",\n  \"update_failed\": \"بروز رسانی خودکار موفق نشد. لطفا <a>مراحل را دنبال کرده</a> تا بطور دستی بروز رسانی کنید.\",\n  \"update_now\": \"حالا بروز رسانی\",\n  \"updated_custom_filtering_toast\": \"دستورات فیلترینگ دستی بروز رسانی شده است\",\n  \"updated_save_search_toast\": \"تنظیمات جستجوی ایمن به روز شد\",\n  \"updated_upstream_dns_toast\": \"سرورهای DNS جریان ارسالی بروز رسانی شده است\",\n  \"updates_checked\": \"نسخه جدیدی از AdGuard Home در دسترس است\",\n  \"updates_version_equal\": \"AdGuard Home بروز است\",\n  \"upstream\": \"سرور مادر\",\n  \"upstream_dns\": \"سرورهای DNS جریان ارسالی\",\n  \"upstream_dns_cache_configuration\": \"پیکربندی نهانگاه DNS بالادست\",\n  \"upstream_dns_client_desc\": \"اگر این فیلد را خالی نگه دارید، AdGuard Home از سرور پیکربندی شده در <0> تنظیماتDNS </0> استفاده می کند.\",\n  \"upstream_dns_configured_in_file\": \"در {{path}} پیکربندی شده است\",\n  \"upstream_dns_help\": \"یک آدرس سرور در هر خط وارد کنید. <a>درباره پیکربندی سرورهای DNS بالادستی</a> بیشتر بیاموزید.\",\n  \"upstream_parallel\": \"استفاده از جستار موازی برای سرعت دادن به تفکیک با جستار همزمان همه جریان های ارسالی\",\n  \"upstream_timeout\": \"مهلت زمانی بالادست\",\n  \"upstream_timeout_desc\": \"تعداد ثانیه‌هایی را که باید منتظر پاسخ سرور بالادستی باشید مشخص می‌کند\",\n  \"upstreams\": \"جریان ارسالی\",\n  \"use_adguard_browsing_sec\": \"استفاده از سرویس وب امنیت وب گردی AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home بررسی می کند اگر دامنه در سرویس وب امنیت وب گردی در لیست سیاه است.آن از اِی پی آی دارای حریم خصوصی برای بررسی استفاده می کند:فقط پیشوند کوتاه نام دامنه هش SHA256 به سرور ارسال خواهد شد.\",\n  \"use_adguard_parental\": \"از سرویس وب نظارت والدین AdGuard استفاده کن\",\n  \"use_adguard_parental_hint\": \"AdGuard Home بررسی می کند اگر دامنه حاوی موارد غیر اخلاقی است.آن از همان اِی پی آی دارای حریم خصوصی سرویس وب امنیت وب گردی استفاده می کند.\",\n  \"use_private_ptr_resolvers_desc\": \"درخواست‌های PTR، SOA، و NS را برای دامنه‌های ARPA حاوی آدرس‌های IP خصوصی از طریق سرورهای بالادستی خصوصی، DHCP، /etc/host‌ها و غیره حل کنید. اگر غیرفعال باشد، AdGuard Home به همه این درخواست‌ها با NXDOMAIN پاسخ می‌دهد.\",\n  \"use_private_ptr_resolvers_title\": \"از تعیین کننده های rDNS محرمانه استفاده کنید\",\n  \"use_saved_key\": \"از کلید ذخیره شده قبلی استفاده کنید\",\n  \"username_label\": \"نام کاربر\",\n  \"username_placeholder\": \"نام کاربر را وارد کنید\",\n  \"validated_with_dnssec\": \"معتبر سازی با DNSSEC\",\n  \"version\": \"نسخه\",\n  \"version_request_error\": \"بررسی بروزرسانی موفق نشد.لطفا ارتباط اینترنتی خود را بررسی کنید\",\n  \"wednesday\": \"چهار شنبه\",\n  \"wednesday_short\": \"چهارشنبه\",\n  \"whois\": \"هوئیز\"\n}\n"
  },
  {
    "path": "client/src/__locales/fi.json",
    "content": "{\n  \"access_allowed_desc\": \"Lista CIDR-merkinnöistä, IP-osoitteista tai <a>ClientID</a>-tunnisteista. Jos listalla on kohteita, hyväksyy AdGuard Home pyyntöjä vain näiltä päätelaitteilta.\",\n  \"access_allowed_title\": \"Sallitut päätelaitteet\",\n  \"access_blocked_desc\": \"Ei pidä sekoittaa suodattimiin. AdGuard Home hylkää näiden verkkotunnusten DNS-pyynnöt, eivätkä nämä pyynnöt myöskään näy pyyntöhistoriassa. Tähän voidaan syöttää tarkkoja verkkotunnuksia, jokerimerkkejä tai URL-suodatussääntöjä, kuten \\\"example.org\\\", \\\"*.example.org\\\" tai \\\"||example.org^\\\".\",\n  \"access_blocked_title\": \"Estetyt verkkotunnukset\",\n  \"access_desc\": \"Tässä voidaan määrittää AdGuard Homen DNS-palvelimen käyttöoikeussääntöjä.\",\n  \"access_disallowed_desc\": \"Lista CIDR-merkinnöistä, IP-osoitteista tai <a>ClientID</a>-tunnisteista. Jos listalla on kohteita, hylkää AdGuard Home näiden päätelaitteiden pyynnöt. Tätä kenttää ei huomioida, jos sallittuja päätelaitteita on määritetty.\",\n  \"access_disallowed_title\": \"Estetyt päätelaitteet\",\n  \"access_settings_saved\": \"Käytön asetukset tallennettiin\",\n  \"access_title\": \"Käytön asetukset\",\n  \"actions_table_header\": \"Toiminnot\",\n  \"add_allowlist\": \"Lisää sallittujen lista\",\n  \"add_blocklist\": \"Lisää estolista\",\n  \"add_custom_list\": \"Lisää oma lista\",\n  \"add_persistent_client\": \"Lisää pysyvänä päätelaitteena\",\n  \"address\": \"Osoite\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home hylkää tämän päätelaitteen DNS-pyynnöt.\",\n  \"all_lists_up_to_date_toast\": \"Kaikki listat ovat ajan tasalla\",\n  \"all_queries\": \"Kaikki pyynnöt\",\n  \"allow_this_client\": \"Salli tämä päätelaite\",\n  \"allowed\": \"Sallitut\",\n  \"anonymize_client_ip\": \"Piilota päätelaitteen IP-osoite\",\n  \"anonymize_client_ip_desc\": \"Älä tallenna päätelaitteen täydellistä IP-osoitetta historiaan ja tilastoihin.\",\n  \"anonymizer_notification\": \"<0>Huomioi:</0> IP-osoitteen anonymisointi on käytössä. Voit poistaa sen käytöstä <1>Yleisistä asetuksista</1>.\",\n  \"answer\": \"Vastaus\",\n  \"apply_btn\": \"Käytä\",\n  \"auto_clients_desc\": \"Päätelaitteet, joita ei ole määritetty pysyviksi ja jotka voivat silti käyttää AdGuard Homea. Näitä tietoja kertään useista lähteistä, mm. hosts-tiedostoista ja kääteis-DNS:llä.\",\n  \"auto_clients_title\": \"Määrittämättömät päätelaitteet\",\n  \"autofix_warning_list\": \"Suorittaa toiminnot: <0>Poistaa käytöstä järjestelmän DNSStubListener-palvelun</0> <0>Määrittää DNS-palvelimen osoitteeksi 127.0.0.1</0> <0>Muuttaa sijainnnin /etc/resolv.conf symbolisen linkin kohteeksi /run/systemd/resolve/resolv.conf</0> <0>Pysäyttää DNSStubListener-palvelun (uudelleenlataa systemd-resolved -palvelu)</0>\",\n  \"autofix_warning_result\": \"Tämän jälkeen järjestelmäsi kaikki DNS-pyynnöt käsittelee oletusarvoisesti AdGuard Home.\",\n  \"autofix_warning_text\": \"Jos painat \\\"Korjaa\\\", AdGuard Home määrittää järjestelmäsi käyttämään AdGuard Homen DNS-palvelinta.\",\n  \"average_processing_time\": \"Keskimääräinen käsittelyaika\",\n  \"average_processing_time_hint\": \"Keskimääräinen DNS-pyynnön käsittelyyn kulutettu aika millisekunteina\",\n  \"average_upstream_response_time\": \"Ylävirran keskimääräinen vasteaika\",\n  \"back\": \"Palaa takaisin\",\n  \"block\": \"Estä\",\n  \"block_all\": \"Estä kaikki\",\n  \"block_domain_use_filters_and_hosts\": \"Estä verkkotunnuksia suodattimilla ja hosts-tiedostoilla\",\n  \"block_for_this_client_only\": \"Estä vain tältä päätelaitteelta\",\n  \"block_services\": \"Estä tietyt palvelut\",\n  \"blocked_adult_websites\": \"Estetty lapsilukolla\",\n  \"blocked_by\": \"<0>Suodatinten estämää</0>\",\n  \"blocked_by_cname_or_ip\": \"Estetty CNAME:n tai IP:n perusteella\",\n  \"blocked_by_response\": \"Estetty vastauksen CNAME:n tai IP:n perusteella\",\n  \"blocked_response_ttl\": \"Estetyn vastauksen elinaika\",\n  \"blocked_response_ttl_desc\": \"Määrittää montako sekuntia päätteiden tulee puskuroida suodatettuja vastauksia.\",\n  \"blocked_safebrowsing\": \"Turvallisen selauksen estämät\",\n  \"blocked_service\": \"Estetty palvelu\",\n  \"blocked_services\": \"Estetyt palvelut\",\n  \"blocked_services_desc\": \"Mahdollistaa suosittujen sivustojen ja palveluiden nopean eston.\",\n  \"blocked_services_global\": \"Käytä yleisiä estettyjä palveluita\",\n  \"blocked_services_saved\": \"Estetyt palvelut tallennettiin\",\n  \"blocked_threats\": \"Estetyt uhat\",\n  \"blocking_ipv4\": \"IPv4-esto\",\n  \"blocking_ipv4_desc\": \"Estettyyn A-pyyntöön palautettava IP-osoite\",\n  \"blocking_ipv6\": \"IPv6-esto\",\n  \"blocking_ipv6_desc\": \"Estettyyn AAAA-pyyntöön palautettava IP-osoite\",\n  \"blocking_mode\": \"Estotila\",\n  \"blocking_mode_custom_ip\": \"Mukautettu IP: Vastaa manuaalisesti määritetyllä IP-osoitteella\",\n  \"blocking_mode_default\": \"Oletus: Vastaa IP-nollaosoitteella (0.0.0.0 korvaa A; :: korvaa AAAA) kun estetään mainoseston säännöllä; vastaa säännön määrittämällä IP-osoitteella kun estetään /etc/hosts-tyyppisellä säännöllä\",\n  \"blocking_mode_null_ip\": \"Tyhjä IP: Vastaa IP-nollaosoitteella (0.0.0.0 korvaa A; :: korvaa AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Vastaa NXDOMAIN-tiedolla\",\n  \"blocking_mode_refused\": \"REFUSED: Vastaa REFUSED-koodilla\",\n  \"blocklist\": \"Estolista\",\n  \"bootstrap_dns\": \"Bootstrap DNS-palvelimet\",\n  \"bootstrap_dns_desc\": \"Ylävirroiksi määrittämiesi DoH/DoT-resolverien IP-osoitteiden selvitykseen käytettävien DNS-palvelimien IP-osoitteet. Kommentteja ei sallita.\",\n  \"cache_cleared\": \"DNS-välimuistin tyhjennys onnistui\",\n  \"cache_enabled\": \"Ota välimuisti käyttöön\",\n  \"cache_enabled_desc\": \"Tallenna DNS-vastaukset paikallisesti.\",\n  \"cache_optimistic\": \"Optimistinen välimuisti\",\n  \"cache_optimistic_desc\": \"Pakota AdGuard Home vastaamaan välimuistista vaikka tiedot olisivat vanhentuneet. Pyri samalla myös päivittämään tiedot.\",\n  \"cache_size\": \"Välimuistin koko\",\n  \"cache_size_desc\": \"DNS-välimuistin koko (tavuina).\",\n  \"cache_size_validation\": \"Välimuistin koon on oltava suurempi kuin nolla, kun se on käytössä.\",\n  \"cache_ttl_max_override\": \"Korvaa enimmäis-TTL\",\n  \"cache_ttl_max_override_desc\": \"Määritä DNS-välimuistin kohteiden enimmäiselinaika (sekunteina).\",\n  \"cache_ttl_min_override\": \"Korvaa vähimmäis-TTL\",\n  \"cache_ttl_min_override_desc\": \"Pidennä ylävirtapalvelimelta vastaanotettuja, lyhyitä elinaika-arvoja (sekunteina) tallennettaessa DNS-vastauksia välimuistiin.\",\n  \"cancel_btn\": \"Peruuta\",\n  \"category_label\": \"Luokitus\",\n  \"check\": \"Tarkasta\",\n  \"check_client_id\": \"Asiakkaan tunniste (ClientID tai IP-osoite)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Tarkasta onko isäntänimi suodatettu.\",\n  \"check_dhcp_servers\": \"Etsi DHCP-palvelimia\",\n  \"check_dns_record\": \"Valitse DNS-tietueen tyyppi\",\n  \"check_enter_client_id\": \"Syötä asiakastunniste\",\n  \"check_hostname\": \"Isäntänimi tai verkkotunnus\",\n  \"check_ip\": \"IP-osoitteet: {{ip}}\",\n  \"check_not_found\": \"Ei löytynyt suodatinlistoilta\",\n  \"check_reason\": \"Syy: {{reason}}\",\n  \"check_service\": \"Palvelun nimi: {{service}}\",\n  \"check_title\": \"Tarkasta suodatus\",\n  \"check_updates_btn\": \"Tarkista päivitykset\",\n  \"check_updates_now\": \"Tarkista päivitykset nyt\",\n  \"choose_allowlist\": \"Valitse sallittujen listat\",\n  \"choose_blocklist\": \"Valitse estolistat\",\n  \"choose_from_list\": \"Valitse listalta\",\n  \"city\": \"Kaupunki\",\n  \"clear_cache\": \"Tyhjennä välimuisti\",\n  \"click_to_view_queries\": \"Paina näyttääksesi pyynnöt\",\n  \"client_add\": \"Lisää päätelaite\",\n  \"client_added\": \"Päätelaite \\\"{{key}}\\\" lisättiin\",\n  \"client_blocked\": \"Päätelaite \\\"{{ip}}\\\" estettiin\",\n  \"client_confirm_block\": \"Haluatko varmasti estää päätelaitteen \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Haluatko varmasti poistaa päätelaitteen \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Haluatko varmasti sallia päätelaitteen \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Päätelaite \\\"{{key}}\\\" poistettiin\",\n  \"client_details\": \"Päätelaitteen tiedot\",\n  \"client_edit\": \"Muokkaa päätelaitetta\",\n  \"client_global_settings\": \"Käytä yleisiä asetuksia\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Päätelaitteet voidaan tunnistaa erityisillä ClientID-tunnisteilla. Lue lisää päätelaitteiden tunnistuksesta <a>täältä</a>.\",\n  \"client_id_placeholder\": \"Syötä ClientID\",\n  \"client_identifier\": \"Tunniste\",\n  \"client_identifier_desc\": \"Päätelaitteet voidaan tunnistaa IP- tai MAC-osoitteista, CIDR-merkinnöistä tai erityisistä ClientID-tunnisteista (voidaan käyttää DoT/DoH/DoQ yhteydessä). Lue lisää päätelaitteiden tunnistuksesta <0>täältä</0>.\",\n  \"client_name\": \"Päätelaite {{id}}\",\n  \"client_new\": \"Uusi päätelaite\",\n  \"client_settings\": \"Päätelaiteasetukset\",\n  \"client_table_header\": \"Asiakas\",\n  \"client_unblocked\": \"Päätelaite \\\"{{ip}}\\\" sallittiin\",\n  \"client_updated\": \"Päätelaite \\\"{{key}}\\\" päivitettiin\",\n  \"clients_desc\": \"Määritä AdGuard Homeen pysyvästi yhdistettyjen päätelaitteiden tiedot.\",\n  \"clients_not_found\": \"Päätelaitteita ei löytynyt\",\n  \"clients_title\": \"Pysyvät päätelaitteet\",\n  \"compact\": \"Tiivis\",\n  \"config_successfully_saved\": \"Asetukset tallennettiin\",\n  \"configure\": \"Määritä\",\n  \"confirm_dns_cache_clear\": \"Haluatko varmasti tyhjentää DNS-välimuistin?\",\n  \"confirm_static_ip\": \"AdGuard Home määrittää IP-osoitteen {{ip}} kiinteäksi. Haluatko jatkaa?\",\n  \"copyright\": \"Tekijänoikeus\",\n  \"country\": \"Maa\",\n  \"custom_filter_rules\": \"Omat suodatussäännöt\",\n  \"custom_filter_rules_hint\": \"Syötä yksi sääntö per rivi. Voit käyttää mainoseston sääntöjen tai hosts-tiedostojen syntakseja.\",\n  \"custom_filtering_rules\": \"Omat suodatussäännöt\",\n  \"custom_ip\": \"Mukautettu IP-osoite\",\n  \"custom_retention_input\": \"Syötä säilytysaika tunteina\",\n  \"custom_rotation_input\": \"Syötä uudistusaika tunteina\",\n  \"dashboard\": \"Tila\",\n  \"date\": \"Päiväys\",\n  \"default\": \"Oletus\",\n  \"delete_confirm\": \"Haluatko varmasti poistaa kohteen \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Poista\",\n  \"descr\": \"Kuvaus\",\n  \"details\": \"Yksityiskohdat\",\n  \"dhcp_add_static_lease\": \"Lisää kiinteä laina\",\n  \"dhcp_config_saved\": \"DHCP-asetukset tallennettiin\",\n  \"dhcp_description\": \"Jollei reitittimesi tarjoa DHCP-asetuksia, voit käyttää AdGuard Homen omaa sisäänrakennettua DHCP-palvelinta.\",\n  \"dhcp_disable\": \"Poista DHCP-palvelin käytöstä\",\n  \"dhcp_dynamic_ip_found\": \"Järjestelmäsi käyttää verkkosovittimelle <0>{{interfaceName}}</0> dynaamista IP-osoitetta. Jotta voit käyttää DHCP-palvelinta, on sovittimelle määritettävä kiinteä IP-osoite. Nykyinen IP-osoitteesi on <0>{{ipAddress}}</0>. Tämä osoite määritetään automaattisesti kiinteäksi, jos painat \\\"Ota DHCP-palvelin käyttöön\\\" -painiketta.\",\n  \"dhcp_edit_static_lease\": \"Muokkaa kiinteää laina\",\n  \"dhcp_enable\": \"Ota DHCP-palvelin käyttöön\",\n  \"dhcp_error\": \"AdGuard Home ei voinut tunnistaa, onko verkossa toista aktiivista DHCP-palvelinta\",\n  \"dhcp_form_gateway_input\": \"Yhdyskäytävän IP-osoite\",\n  \"dhcp_form_lease_input\": \"Lainan kesto\",\n  \"dhcp_form_lease_title\": \"DHCP-lainan kesto (sekunteina)\",\n  \"dhcp_form_range_end\": \"Alueen päätös\",\n  \"dhcp_form_range_start\": \"Alueen aloitus\",\n  \"dhcp_form_range_title\": \"IP-osoitealue\",\n  \"dhcp_form_subnet_input\": \"Aliverkon peite\",\n  \"dhcp_found\": \"Verkossa havaittiin aktiivinen DHCP-palvelin. Sisäänrakennetun DHCP-palvelimen käyttöönotto ei ole turvallista.\",\n  \"dhcp_hardware_address\": \"Laiteosoite (MAC)\",\n  \"dhcp_interface_select\": \"Valitse DHCP:lle käytettävä verkkosovitin\",\n  \"dhcp_ip_addresses\": \"IP-osoitteet\",\n  \"dhcp_ipv4_settings\": \"DHCP:n IPv4-asetukset\",\n  \"dhcp_ipv6_settings\": \"DHCP:n IPv6-asetukset\",\n  \"dhcp_lease_added\": \"Kiinteä laina \\\"{{key}}\\\" lisättiin\",\n  \"dhcp_lease_deleted\": \"Kiinteä laina \\\"{{key}}\\\" poistettiin\",\n  \"dhcp_lease_updated\": \"Kiinteä laina \\\"{{key}}\\\" päivitettiin\",\n  \"dhcp_leases\": \"DHCP-lainat\",\n  \"dhcp_leases_not_found\": \"DHCP-lainoja ei löytynyt\",\n  \"dhcp_new_static_lease\": \"Uusi kiinteä laina\",\n  \"dhcp_not_found\": \"On turvallista ottaa sisäänrakennettu DHCP-palvelin käyttöön, koska AdGuard Home ei havainnut verkossa muita aktiivisia DHCP-palvelimia. Suosittelemme, että varmistat tämän vielä itse, koska automaattinen tunnistus ei ole 100% varma.\",\n  \"dhcp_reset\": \"Haluatko varmasti palauttaa DHCP-asetukset?\",\n  \"dhcp_reset_leases\": \"Tyhjennä kaikki lainat\",\n  \"dhcp_reset_leases_confirm\": \"Haluatko varmasti tyhjentää kaikki lainat?\",\n  \"dhcp_reset_leases_success\": \"DHCP-lainojen tyhjennys onnistui\",\n  \"dhcp_settings\": \"DHCP-asetukset\",\n  \"dhcp_static_ip_error\": \"Jotta DHCP-palvelinta voidaan käyttää, on määritettävä kiinteä IP-osoite. AdGuard Home ei voinut tunnistaa, onko tälle verkkosovittimelle määritetty IP-osoite kiinteä. Määritä kiinteä IP-osoite itse.\",\n  \"dhcp_static_leases\": \"Kiinteät DHCP-lainat\",\n  \"dhcp_static_leases_not_found\": \"Kiinteitä DHCP-lainoja ei löytynyt\",\n  \"dhcp_table_expires\": \"Erääntyy\",\n  \"dhcp_table_hostname\": \"Isäntänimi\",\n  \"dhcp_title\": \"DHCP-palvelin (kokeellinen!)\",\n  \"dhcp_warning\": \"Jos tahdot kuitenkin ottaa DHCP-palvelimen käyttöön, varmista, ettei verkossasi ole muita aktiivisia DHCP-palvelimia, koska tämä voi rikkoa Internet-yhteyden muilta verkon laitteilta!\",\n  \"disable_for_hours\": \"{{count}} tunniksi\",\n  \"disable_for_hours_plural\": \"{{count}} tunniksi\",\n  \"disable_for_minutes\": \"{{count}} minuutiksi\",\n  \"disable_for_minutes_plural\": \"{{count}} minuutiksi\",\n  \"disable_for_seconds\": \"{{count}} sekunniksi\",\n  \"disable_for_seconds_plural\": \"{{count}} sekunniksi\",\n  \"disable_ipv6\": \"Älä selvitä IPv6-osoitteita\",\n  \"disable_ipv6_desc\": \"Hylkää kaikki IPv6-osoitteiden DNS-pyynnöt (tyyppi AAAA) ja poista HTTPS-vastausten IPv6-tiedot.\",\n  \"disable_notify_for_hours\": \"Poista suojaus käytöstä {{count}} tunniksi\",\n  \"disable_notify_for_hours_plural\": \"Poista suojaus käytöstä {{count}} tunniksi\",\n  \"disable_notify_for_minutes\": \"Poista suojaus käytöstä {{count}} minuutiksi\",\n  \"disable_notify_for_minutes_plural\": \"Poista suojaus käytöstä {{count}} minuutiksi\",\n  \"disable_notify_for_seconds\": \"Poista suojaus käytöstä {{count}} sekunniksi\",\n  \"disable_notify_for_seconds_plural\": \"Poista suojaus käytöstä {{count}} sekunniksi\",\n  \"disable_notify_until_tomorrow\": \"Poista suojaus käytöstä huomiseen asti\",\n  \"disable_protection\": \"Poista suojaus käytöstä\",\n  \"disable_rewrites\": \"Poista uudelleenkirjoitussäännöt käytöstä\",\n  \"disable_until_tomorrow\": \"Huomiseen asti\",\n  \"disabled\": \"Ei käytössä\",\n  \"disabled_dhcp\": \"DHCP-palvelin poistettiin käytöstä\",\n  \"disabled_filtering_toast\": \"Suodatus poistettiin käytöstä\",\n  \"disabled_parental_toast\": \"Lapsilukko poistettiin käytöstä\",\n  \"disabled_protection\": \"Suojaus poistettiin käytöstä\",\n  \"disabled_safe_browsing_toast\": \"Turvallinen selaus poistettiin käytöstä\",\n  \"disabled_safe_search_toast\": \"Turvallinen haku poistettiin käytöstä\",\n  \"disallow_this_client\": \"Estä tämä päätelaite\",\n  \"dns_addresses\": \"DNS-osoitteet\",\n  \"dns_allowlists\": \"DNS-sallinnat\",\n  \"dns_allowlists_desc\": \"DNS-sallittujen listalla olevat verkkotunnukset sallitaan myös silloin, jos ne ovat jollain muulla estolistalla.\",\n  \"dns_blocklists\": \"DNS-estot\",\n  \"dns_blocklists_desc\": \"AdGuard Home estää estolistalla olevat verkkotunnukset.\",\n  \"dns_cache_config\": \"DNS-välimuistin määritys\",\n  \"dns_cache_config_desc\": \"Tässä voit määrittää DNS-välimuistin.\",\n  \"dns_cache_size\": \"DNS-välimuistin koko tavuina\",\n  \"dns_config\": \"DNS-palvelimen määritys\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS-tietosuoja\",\n  \"dns_providers\": \"Katso <0>luettelo tunnetuista DNS-palveluista</0>, joista valita.\",\n  \"dns_query\": \"DNS-pyyntöä\",\n  \"dns_rewrites\": \"DNS-uudelleenohjaukset\",\n  \"dns_settings\": \"DNS-asetukset\",\n  \"dns_start\": \"DNS-palvelin käynnistyy\",\n  \"dns_status_error\": \"Virhe tarkistettaessa DNS-palvelimen tilaa\",\n  \"dns_test_not_ok_toast\": \"Palvelin \\\"{{key}}\\\": Ei voitu käyttää, tarkista oikeinkirjoitus\",\n  \"dns_test_ok_toast\": \"Määritetyt DNS-palvelimet toimivat oikein\",\n  \"dns_test_parsing_error_toast\": \"Osio {{section}}: rivi {{line}}: Ei voitu käyttää, tarkista oikeinkirjoitus\",\n  \"dns_test_warning_toast\": \"Datavuon \\\"{{key}}\\\" ei vastaa testipyyntöihin eikä välttämättä toimi kunnolla\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Ota DNSSEC käyttöön\",\n  \"dnssec_enable_desc\": \"Määritä DNSSEC-lippu ulos lähteville DNS-pyynnöille ja tarkasta tulos (vaatii DNSSEC-yhteensopivan resolverin).\",\n  \"domain\": \"Verkkotunnus\",\n  \"domain_desc\": \"Syötä korvattava verkkotunnus tai jokerimerkki.\",\n  \"domain_name_table_header\": \"Verkkotunnus\",\n  \"domain_or_client\": \"Verkkotunnus tai päätelaite\",\n  \"down\": \"yhteydetön\",\n  \"download_mobileconfig\": \"Lataa asetustiedosto\",\n  \"download_mobileconfig_doh\": \"Lataa .mobileconfig-tiedosto DNS-over-HTTPS -käytölle\",\n  \"download_mobileconfig_dot\": \"Lataa .mobileconfig-tiedosto DNS-over-TLS -käytölle\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Muokkaa sallittujen listaa\",\n  \"edit_blocklist\": \"Muokkaa estolistaa\",\n  \"edit_table_action\": \"Muokkaa\",\n  \"edns_cs_desc\": \"Lisää EDNS Client Subnet (ECS) -valinta ylävirran pyyntöihin ja kirjaa päätelaitteiden lähettämät arvot pyyntöhistoriaan.\",\n  \"edns_enable\": \"Käytä EDNS-päätelaitealivekkoa\",\n  \"edns_use_custom_ip\": \"Käytä omaa IP-osoitetta EDNS:lle\",\n  \"edns_use_custom_ip_desc\": \"Salli oman IP-osoitteen käyttö EDNS-mekanismille.\",\n  \"elapsed\": \"Kesto\",\n  \"empty_response_status\": \"Tyhjä\",\n  \"enable_protection\": \"Ota suojaus käyttöön\",\n  \"enable_protection_timer\": \"Suojaus otetaan käyttöön {{time}} kuluttua\",\n  \"enable_rewrites\": \"Ota uudelleenkirjoitussäännöt käyttöön\",\n  \"enable_upstream_dns_cache\": \"Käytä DNS-välimuistia tämän päätelaitteen mukautetuissa ylävirtamäärityksissä\",\n  \"enabled_dhcp\": \"DHCP-palvelin otettiin käyttöön\",\n  \"enabled_filtering_toast\": \"Suodatus otettiin käyttöön\",\n  \"enabled_parental_toast\": \"Lapsilukko otettiin käyttöön\",\n  \"enabled_protection\": \"Suojaus otettiin käyttöön\",\n  \"enabled_safe_browsing_toast\": \"Turvallinen selaus otettiin käyttöön\",\n  \"enabled_save_search_toast\": \"Turvallinen haku otettiin käyttöön\",\n  \"enabled_table_header\": \"Käytössä\",\n  \"encryption_certificate_path\": \"Varmenteen sijainti\",\n  \"encryption_certificates\": \"Varmenteet\",\n  \"encryption_certificates_desc\": \"Salauksen käyttämiseksi, on syötettävä verkkotunnuksellesi myönnetty, aito SSL-varmenneketju. Voit hankkia ilmaisen varmenteen osoitteesta <0>{{link}}</0> tai ostaa sellaisen joltakin luotetulta varmentajalta.\",\n  \"encryption_certificates_input\": \"Kopioi/liitä PEM-koodatut varmenteesi tähän.\",\n  \"encryption_certificates_source_content\": \"Liitä varmenteen sisältö\",\n  \"encryption_certificates_source_path\": \"Määritä varmennetiedoston sijainti\",\n  \"encryption_chain_invalid\": \"Varmenneketju ei kelpaa\",\n  \"encryption_chain_valid\": \"Varmenneketju on kelvollinen\",\n  \"encryption_config_saved\": \"Salausasetukset tallennettiin\",\n  \"encryption_desc\": \"Salaustuki (HTTPS/TLS) DNS:lle ja verkkokäyttölliittymälle.\",\n  \"encryption_doq\": \"DNS-over-QUIC-portti\",\n  \"encryption_doq_desc\": \"Jos portti on määritetty, AdGuard Home suorittaa DNS-over-QUIC-palvelimen tässä portissa.\",\n  \"encryption_dot\": \"DNS-over-TLS -portti\",\n  \"encryption_dot_desc\": \"Jos portti on määritetty, AdGuard Home suorittaa DNS-over-TLS -palvelimen tässä portissa.\",\n  \"encryption_enable\": \"Käytä salausta (HTTPS, DNS-over-HTTPS ja DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Jos salaus on käytössä, AdGuard Homen hallinta on käytettävissä HTTPS-yhteydellä ja DNS-palvelin kuuntelee pyyntöjä DNS-over-HTTPS ja DNS-over-TLS -yhteyksillä.\",\n  \"encryption_expire\": \"Erääntyy\",\n  \"encryption_hostnames\": \"Isäntänimet\",\n  \"encryption_https\": \"HTTPS-portti\",\n  \"encryption_https_desc\": \"Jos HTTPS-portti on määritetty, on AdGuard Homen hallintapaneeli käytettävissä HTTPS-yhteydellä ja lisäksi tämä mahdollistaa myös DNS-over-HTTPS -yhteyden '/dns-query' -kohteessa.\",\n  \"encryption_issuer\": \"Toimittaja\",\n  \"encryption_key\": \"Yksityinen avain\",\n  \"encryption_key_input\": \"Kopioi/liitä tähän varmenteesi PEM-koodattu yksityinen avain.\",\n  \"encryption_key_invalid\": \"Tämä yksityinen {{type}}-avain ei kelpaa\",\n  \"encryption_key_source_content\": \"Liitä yksityisen avaimen sisältö\",\n  \"encryption_key_source_path\": \"Määritä yksityisen avaimen tiedostopolku\",\n  \"encryption_key_valid\": \"Tämä yksityinen {{type}}-avain on kelvollinen\",\n  \"encryption_plain_dns_desc\": \"Tavallinen DNS on oletusarvoisesti käytössä. Voit poistaa sen käytöstä pakottaaksesi kaikki laitteet käyttämään salattua DNS:ää. Tätä varten sinun on otettava käyttöön ainakin yksi salattu DNS-protokolla.\",\n  \"encryption_plain_dns_enable\": \"Käytä tavallista DNS:ää\",\n  \"encryption_plain_dns_error\": \"Voit poistaa tavallisen DNS:n käytöstä ottamalla käyttöön ainakin yhden salatun DNS-protokollan.\",\n  \"encryption_private_key_path\": \"Yksityisen avaimen sijainti\",\n  \"encryption_redirect\": \"Automaattinen HTTPS-ohjaus\",\n  \"encryption_redirect_desc\": \"Jos käytössä, AdGuard Home ohjaa HTTP-osoitteet automaattisesti HTTPS-osoitteisiin.\",\n  \"encryption_reset\": \"Haluatko varmasti palauttaa salausasetukset?\",\n  \"encryption_server\": \"Palvelimen nimi\",\n  \"encryption_server_desc\": \"Jos määritetty, AdGuard Home tunnistaa ClientID-tunnisteet, vastaa DDR-pyyntöihin ja suorittaa yhteyden lisätarkistuksia. Jos ei määritetty, nämä ominaisuudet eivät ole käytössä. On vastattava yhtä varmenteen DNS-nimistä.\",\n  \"encryption_server_enter\": \"Syötä verkkotunnuksesi\",\n  \"encryption_settings\": \"Salausasetukset\",\n  \"encryption_status\": \"Tila\",\n  \"encryption_subject\": \"Aihe\",\n  \"encryption_title\": \"Salaus\",\n  \"encryption_warning\": \"Varoitus\",\n  \"enforce_safe_search\": \"Käytä turvallista hakua\",\n  \"enforce_save_search_hint\": \"AdGuard Home pakottaa turvallisen haun käyttöön seuraavissa hakukoneissa: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex ja Pixabay.\",\n  \"enforced_save_search\": \"Turvallinen haku pakotettiin\",\n  \"enter_cache_size\": \"Syötä välimuistin koko (tavuina)\",\n  \"enter_cache_ttl_max_override\": \"Syötä enimmäis-TTL (sekunteina)\",\n  \"enter_cache_ttl_min_override\": \"Syötä vähimmäis-TTL (sekunteina)\",\n  \"enter_name_hint\": \"Syötä nimi\",\n  \"enter_url_or_path_hint\": \"Syötä listan URL-osoite tai tarkka tiedostosijainti\",\n  \"enter_valid_allowlist\": \"Syötä sallittujen listan URL-osoite.\",\n  \"enter_valid_blocklist\": \"Syötä estolistan URL-osoite.\",\n  \"error_details\": \"Virheen tiedot\",\n  \"example_comment\": \"! Tähän tulee kommentti.\",\n  \"example_comment_hash\": \"# Tämäkin on kommentti.\",\n  \"example_comment_meaning\": \"vain kommentti;\",\n  \"example_meaning_filter_block\": \"estä pääsy verkkotunnukseen example.org sekä kaikkiin sen aliverkkotunnuksiin;\",\n  \"example_meaning_filter_whitelist\": \"salli pääsy verkkotunnukseen example.org sekä kaikkiin sen aliverkkotunnuksiin;\",\n  \"example_meaning_host_block\": \"vastaa verkkotunnukselle example.org IP-osoitteella 127.0.0.1 (muttei sen aliverkkotunnuksille);\",\n  \"example_multiple_upstreams_reserved\": \"useita ylävirtoja <0>tietyille verkkotunnuksille</0>;\",\n  \"example_regex_meaning\": \"estä pääsy määritettyä säännöllistä lauseketta vastaaviin verkkotunnuksiin.\",\n  \"example_rewrite_domain\": \"korvaa vain tämän verkkotunnuksen vastaukset\",\n  \"example_rewrite_wildcard\": \"korvaa verkkotunnuksen <0>example.org</0> kaikkien aliverkkotunnusten vastaukset\",\n  \"example_upstream_comment\": \"kommentti.\",\n  \"example_upstream_doh\": \"salattu <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"salattu DNS-over-HTTPS <0>HTTP/3</0>-pakotuksella, ilman HTTP/2 (tai alempi) -varmistusta;\",\n  \"example_upstream_doq\": \"salattu <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"salattu <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"tavallinen DNS (UDP);\",\n  \"example_upstream_regular_port\": \"tavallinen DNS (UDP, portti);\",\n  \"example_upstream_reserved\": \"ylävirta <0>tietyille verkkotunnuksille</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamp</0> -merkinnät <1>DNSCrypt</1> tai <2>DNS-over-HTTPS</2> -resolvereille;\",\n  \"example_upstream_tcp\": \"tavallinen DNS (TCP);\",\n  \"example_upstream_tcp_hostname\": \"tavallinen DNS (TCP, isäntänimi);\",\n  \"example_upstream_tcp_port\": \"tavallinen DNS (TCP, portti);\",\n  \"example_upstream_udp\": \"tavallinen DNS (UDP, isäntänimi);\",\n  \"examples_title\": \"Esimerkkejä\",\n  \"fallback_dns_desc\": \"Listaus DNS-varapalvelimista, joita käytetään kun lähtevät DNS-palvelimet eivät vastaa. Syntaksi on sama kuin yllä olevassa pääylävirrat-kentässä.\",\n  \"fallback_dns_placeholder\": \"Syötä yksi DNS-varapalvelin per rivi\",\n  \"fallback_dns_title\": \"DNS-varapalvelimet\",\n  \"faq\": \"UKK\",\n  \"fastest_addr\": \"Nopein IP-osoite\",\n  \"fastest_addr_desc\": \"Odota <b>kaikkien</b> DNS-palvelimien vastauksia, mittaa TCP-yhteyden nopeus jokaiselle palvelimelle ja palauta palvelimen IP-osoite, jolla on nopein yhteyden nopeus.<br/>Tämä tila voi merkittävästi hidastaa DNS-pyyntöjä, jos yksi tai useampi ylävirtapalvelin ei vastaa. Varmista, että ylävirtapalvelimesi ovat vakaat ja aikakatkaisusi on alhainen.\",\n  \"filter\": \"Suodatin\",\n  \"filter_added_successfully\": \"Lista lisättiin\",\n  \"filter_allowlist\": \"VAROITUS: Toiminto ohittaa \\\"{{disallowed_rule}}\\\" -säännön sallittujen päätelaitteiden listalta.\",\n  \"filter_category_general\": \"Yleiset\",\n  \"filter_category_general_desc\": \"Listat, jotka estävät seurannan ja mainokset useimmilla laitteilla\",\n  \"filter_category_other\": \"Muut\",\n  \"filter_category_other_desc\": \"Muut estolistat\",\n  \"filter_category_regional\": \"Alueelliset\",\n  \"filter_category_regional_desc\": \"Listat, jotka painottavat alueellisia mainoksia ja seurantapalvelimia\",\n  \"filter_category_security\": \"Tietoturva\",\n  \"filter_category_security_desc\": \"Tietojenkalastelu-, huijaus- ja muiden haitallisten verkkotunnusten estoon erikoistuneet listat\",\n  \"filter_removed_successfully\": \"Lista poistettiin\",\n  \"filter_updated\": \"Listan päivitettiin\",\n  \"filtered\": \"Suodatetut\",\n  \"filtered_custom_rules\": \"Suodatettu omilla suodatussäännöillä\",\n  \"filtering_rules_learn_more\": \"<0>Lue lisää</0> omien hosts-listojesi luonnista.\",\n  \"filters\": \"Suodattimet\",\n  \"filters_and_hosts_hint\": \"AdGuard Home ymmärtää mainoseston perussääntöjen sekä hosts-tiedostojen syntakseja.\",\n  \"filters_block_toggle_hint\": \"Voit määrittää estosääntöjä <a>suodatinasetuksissa</a>.\",\n  \"filters_configuration\": \"Suodatinten määritys\",\n  \"filters_enable\": \"Ota suodattimet käyttöön\",\n  \"filters_interval\": \"Suodatinpäivitysten tiheys\",\n  \"fix\": \"Korjaa\",\n  \"for_last_days\": \"viimeisten {{count}} päivän ajalta\",\n  \"for_last_days_plural\": \"viimeisten {{count}} päivän ajalta\",\n  \"for_last_hours\": \"viimeisen {{count}} tunnin ajalta\",\n  \"for_last_hours_plural\": \"viimeisen {{count}} tunnin ajalta\",\n  \"forgot_password\": \"Salasana unohtunut?\",\n  \"forgot_password_desc\": \"Luo käyttäjätilillesi uusi salasana seuraamalla <0>näitä ohjeita</0>.\",\n  \"form_add_id\": \"Lisää tunniste\",\n  \"form_answer\": \"Syötä IP-osoite tai verkkotunnus\",\n  \"form_client_name\": \"Syötä päätelaitteen nimi\",\n  \"form_domain\": \"Syötä verkkotunnus tai jokerimerkki\",\n  \"form_enter_blocked_response_ttl\": \"Syötä estetyn vastauksen elinaika (sekuntia)\",\n  \"form_enter_host\": \"Syötä osoite\",\n  \"form_enter_hostname\": \"Syötä isäntänimi\",\n  \"form_enter_id\": \"Muokkaa tunnistetta\",\n  \"form_enter_ip\": \"Syötä IP-osoite\",\n  \"form_enter_mac\": \"Syötä MAC-osoite\",\n  \"form_enter_rate_limit\": \"Syötä pyyntörajoitus\",\n  \"form_enter_rate_limit_subnet_len\": \"Syötä pyyntörajoitukseen käytettävä aliverkon etuliitteen pituus\",\n  \"form_enter_subnet_ip\": \"Syötä aliverkossa \\\"{{cidr}}\\\" oleva IP-osoite\",\n  \"form_enter_upstream_timeout\": \"Syötä ylävirran palvelimen aikakatkaisu kesto sekunteina\",\n  \"form_error_answer_format\": \"Virheellinen vastauksen muoto\",\n  \"form_error_client_id_format\": \"ClientID-tunniste voi sisältää ainoastaan numeroita, pieniä kirjaimia sekä yhdysviivoja\",\n  \"form_error_domain_format\": \"Virheellinen verkkotunnuksen muoto\",\n  \"form_error_equal\": \"Ei voi olla sama\",\n  \"form_error_gateway_ip\": \"Lainalla ei voi olla yhdyskäytävän IP-osoitetta\",\n  \"form_error_ip4_format\": \"Virheellinen IPv4-osoite\",\n  \"form_error_ip4_gateway_format\": \"Virheellinen yhdyskäytävän IPv4-osoite\",\n  \"form_error_ip6_format\": \"Virheellinen IPv6-osoite\",\n  \"form_error_ip_format\": \"Virheellinen IP-osoite\",\n  \"form_error_mac_format\": \"Virheellinen MAC-osoite\",\n  \"form_error_password\": \"Salasanat eivät täsmää\",\n  \"form_error_password_length\": \"Salasanan on oltava {{min}} - {{max}} merkkiä pitkä\",\n  \"form_error_port\": \"Syötä kelvollinen portti\",\n  \"form_error_port_range\": \"Syötä portti väliltä 80-65535\",\n  \"form_error_port_unsafe\": \"Portti ei ole turvallinen\",\n  \"form_error_positive\": \"Oltava suurempi kuin 0\",\n  \"form_error_required\": \"Pakollinen kenttä\",\n  \"form_error_server_name\": \"Virheellinen palvelimen nimi\",\n  \"form_error_subnet\": \"Aliverkko \\\"{{cidr}}\\\" ei sisällä IP-osoitetta \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Virheellinen URL-osoitteen muoto\",\n  \"form_error_url_or_path_format\": \"Syötä listan URL-osoite tai tarkka tiedostosijainti\",\n  \"form_select_tags\": \"Valitse päätelaitteen tunnisteet\",\n  \"found_in_known_domain_db\": \"Löytyi tunnettujen verkkotunnusten tietokannasta.\",\n  \"friday\": \"Perjantai\",\n  \"friday_short\": \"Pe\",\n  \"gateway_or_subnet_invalid\": \"Virheellinen aliverkon peite\",\n  \"general_settings\": \"Yleiset asetukset\",\n  \"general_statistics\": \"Yleiset tilastot\",\n  \"get_started\": \"Aloita\",\n  \"greater_range_start_error\": \"Oltava alueen aloitusarvoa suurempi\",\n  \"homepage\": \"Verkkosivusto\",\n  \"host_whitelisted\": \"Isäntä on sallittu\",\n  \"ignore_domains\": \"Ohitettavat verkkotunnukset (erotettu rivinvaihdolla)\",\n  \"ignore_domains_desc_query\": \"Näitä sääntöjä vastaavia pyyntöjä ei tallenneta pyyntöhistoriaan.\",\n  \"ignore_domains_desc_stats\": \"Sääntöihin sopivat kyselyt eivät kirjoitu tilastoihin\",\n  \"ignore_domains_title\": \"Ohitettavat verkkotunnukset\",\n  \"ignore_query_log\": \"Älä huomioi tätä päätelaitetta pyyntöhistoriassa\",\n  \"ignore_statistics\": \"Älä huomioi tätä päätettä tilastoissa\",\n  \"install_auth_confirm\": \"Vahvista salasana\",\n  \"install_auth_desc\": \"AdGuard Homen hallinnalle on määritettävä salasanasuojaus. Vaikka se olisikin tavoitettavissa vain lähiverkon välityksellä, on silti tärkeää suojata se luvattomalta käytöltä.\",\n  \"install_auth_password\": \"Salasana\",\n  \"install_auth_password_enter\": \"Syötä salasana\",\n  \"install_auth_title\": \"Tunnistautuminen\",\n  \"install_auth_username\": \"Käyttäjätunnus\",\n  \"install_auth_username_enter\": \"Syötä käyttäjätunnus\",\n  \"install_devices_address\": \"AdGuard Homen DNS-palvelin kuuntelee seuraavissa osoitteissa\",\n  \"install_devices_android_list_1\": \"Paina Android-laitteesi aloitusnäytöstä tai sovellusvalikosta \\\"Asetukset\\\".\",\n  \"install_devices_android_list_2\": \"Paina \\\"Yhteydet\\\" ja sitten \\\"Wi-Fi\\\". Kaikki käytettävissä olevat langattomat verkot näytetään (mobiiliverkolle ei ole mahdollista määrittää omaa DNS-palvelinta).\",\n  \"install_devices_android_list_3\": \"Paina yhdistetyn verkon vieressä olevaa asetuskuvaketta tai paina verkkoa pitkään ja valitse \\\"Muokkaa verkkoa\\\".\",\n  \"install_devices_android_list_4\": \"Saatat joutua painamaan \\\"Lisäasetukset\\\" nähdäksesi enemmän valintoja. Muuttaaksesi DNS-asetuksia, on \\\"IP-asetukset\\\" -kohdan \\\"DHCP\\\" -valinta vaihdettava \\\"Staattinen\\\" -valintaan.\",\n  \"install_devices_android_list_5\": \"Syötä \\\"DNS 1\\\" ja \\\"DNS 2\\\" -kenttiin AdGuard Home -palvelimesi osoitteet.\",\n  \"install_devices_desc\": \"AdGuard Homen käytön aloittamiseksi, on laitteet määritettävä käyttämään sitä.\",\n  \"install_devices_ios_list_1\": \"Paina aloitusnäytöstä \\\"Asetukset\\\".\",\n  \"install_devices_ios_list_2\": \"Valitse vasemmalta \\\"Wi-Fi\\\" (mobiiliverkolle ei ole mahdollista määrittää omaa DNS-palvelinta).\",\n  \"install_devices_ios_list_3\": \"Valitse tällä hetkellä aktiivinen verkko.\",\n  \"install_devices_ios_list_4\": \"Syötä \\\"DNS\\\" -kenttään AdGuard Home -palvelimesi osoitteet.\",\n  \"install_devices_macos_list_1\": \"Paina Omena-kuvaketta ja valitse \\\"Järjestelmäasetukset\\\".\",\n  \"install_devices_macos_list_2\": \"Paina \\\"Verkko\\\".\",\n  \"install_devices_macos_list_3\": \"Valitse listan ensimmäinen yhteys ja paina \\\"Lisävalinnat\\\".\",\n  \"install_devices_macos_list_4\": \"Valitse DNS-välilehti ja syötä AdGuard Home -palvelimesi osoitteet.\",\n  \"install_devices_router\": \"Reititin\",\n  \"install_devices_router_desc\": \"Asennus kattaa kaikki reitittimeen liitetyt laitteet, eikä niitä tarvitse määrittää erikseen yksitellen.\",\n  \"install_devices_router_list_1\": \"Avaa reitittimesi hallinta. Yleensä se avautuu selaimen kautta, URL-osoitteella, kuten http://192.168.0.1 tai http://192.168.1.1. Saatat joutua syöttämään käyttäjätunnuksen ja salasanan. Jos et muista tai tiedä sitä, voit yleensä palauttaa salasanan (ja kaikki muut!) reitittimen asetukset oletusarvoihin painamalla laitteessa olevaa reset-painiketta muutaman sekunnin ajan. Jos reitittimen määritys vaatii erillisen sovelluksen käyttöä, asenna se mobiililaitteelle tai tietokoneelle ja käytä reitittimen hallintaa sen kautta. Tutustu reitittimen käyttöoppaaseen.\",\n  \"install_devices_router_list_2\": \"Etsi DHCP/DNS-asetukset. Etsi kirjainyhdistelmää DNS sellaisen kenttien vierestä, joihin voidaan syöttää kaksi tai kolme numerosarjaa, joista jokainen on eroteltu neljään ryhmään, joista jokainen sisältää yhdestä kolmeen numeroa.\",\n  \"install_devices_router_list_3\": \"Syötä sinne AdGuard Home -palvelimesi osoitteet.\",\n  \"install_devices_router_list_4\": \"Joissakin reitittimissä ei ole mahdollista määrittää omaa DNS-palvelinta. Tällöin AdGuard Homen määritys <0>DHCP-palvelimeksi</0> voi auttaa. Muutoin on selvitettävä reitittimen käyttöohjeesta, miten sen DNS-palvelinasetukset muutetaan.\",\n  \"install_devices_title\": \"Määritä laitteet\",\n  \"install_devices_windows_list_1\": \"Avaa \\\"Ohjauspaneeli\\\" Käynnistä-valikon tai Windowsin haun kautta.\",\n  \"install_devices_windows_list_2\": \"Avaa \\\"Verkko ja Internet\\\" -ryhmä ja sitten \\\"Verkko ja jakamiskeskus\\\".\",\n  \"install_devices_windows_list_3\": \"Paina ikkunan vasemmasta laidasta \\\"Muuta sovittimen asetuksia\\\".\",\n  \"install_devices_windows_list_4\": \"Paina aktiivista yhteyttäsi hiiren kakkospainikkeella ja valitse \\\"Ominaisuudet\\\".\",\n  \"install_devices_windows_list_5\": \"Etsi listasta \\\"Internet Protocol Version 4 (TCP/IPv4)\\\" (tai IPv6:lle \\\"Internet Protocol Version 6 (TCP/IPv6)\\\"), valitse se ja paina jälleen \\\"Ominaisuudet\\\".\",\n  \"install_devices_windows_list_6\": \"Valitse \\\"Käytä seuraavia DNS-palvelinten osoitteita\\\" ja syötä AdGuard Home -palvelimesi osoitteet.\",\n  \"install_saved\": \"Tallenus onnistui\",\n  \"install_settings_all_interfaces\": \"Kaikki verkkosovittimet\",\n  \"install_settings_dns\": \"DNS-palvelin\",\n  \"install_settings_dns_desc\": \"Sinun on määritettävä laitteesi tai reitittimesi käyttämään DNS-palvelinta seuraavissa osoitteissa:\",\n  \"install_settings_interface_link\": \"AdGuard Home -asennuksesi hallintapaneeli on käytettävissä seuraavilla osoitteilla:\",\n  \"install_settings_listen\": \"Käytettävä verkkosovitin\",\n  \"install_settings_port\": \"Portti\",\n  \"install_settings_title\": \"Hallintapaneeli\",\n  \"install_static_configure\": \"AdGuard Home havaitsi, että käytössä on dynaaminen IP-osoitteen <0>{{ip}}</0>. Haluatko määrittää sen kiinteäksi osoitteeksi?\",\n  \"install_static_error\": \"AdGuard Home ei voi määrittää sitä tälle verkkosovittimelle automaattisesti. Etsi ohjeita tämän suorittamiseksi itse.\",\n  \"install_static_ok\": \"Hyviä uutisia! Kiinteä IP-osoite on jo määritetty.\",\n  \"install_step\": \"Vaihe\",\n  \"install_submit_desc\": \"Asennus on valmis ja AdGuard Home on valmis käyttöön.\",\n  \"install_submit_title\": \"Onnittelut!\",\n  \"install_welcome_desc\": \"AdGuard Home on verkonlaajuinen mainoksia ja seurantoja estävä DNS-palvelin. Sen tarkoitus on mahdollistaa verkon sekä siihen liitettyjen laitteiden hallinta ja valvonta, eikä se vaadi asiakasohjelmistojen asennusta päätelaitteille.\",\n  \"install_welcome_title\": \"Tervetuloa AdGuard Homeen!\",\n  \"interval_24_hour\": \"24 tunnilta\",\n  \"interval_6_hour\": \"6 tuntia\",\n  \"interval_days\": \"{{count}} päivä\",\n  \"interval_days_plural\": \"{{count}} päivää\",\n  \"interval_hours\": \"{{count}} tunti\",\n  \"interval_hours_plural\": \"{{count}} tuntia\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP-osoite\",\n  \"known_tracker\": \"Tunnettu seuranta\",\n  \"last_rule_in_allowlist\": \"Et voi estää tätä päätelaitetta, koska säännön \\\"{{disallowed_rule}}\\\" ohitus POISTAA KÄYTÖSTÄ \\\"Sallitut päätelaitteet\\\" -listan.\",\n  \"last_time_updated_table_header\": \"Viimeisin päivitys\",\n  \"list_confirm_delete\": \"Haluatko varmasti poistaa tämän listan?\",\n  \"list_label\": \"Lista\",\n  \"list_updated\": \"{{count}} lista päivitettiin\",\n  \"list_updated_plural\": \"{{count}} listaa päivitettiin\",\n  \"list_url_table_header\": \"Listan URL\",\n  \"load_balancing\": \"Kuormantasaus\",\n  \"load_balancing_desc\": \"Lähetä kysely kerrallaan yhdelle ylävirtapalvelimelle.<br/>AdGuard Home käyttää painotettua satunnaisalgoritmia valitakseen palvelimia, joilla on vähiten epäonnistuneita hakuja ja keskimääräinen lyhin hakuaika.\",\n  \"loading_table_status\": \"Ladataan...\",\n  \"local_ptr_default_resolver\": \"Oletusarvoisesti AdGuard Home käyttää seuraavia käänteis-DNS-resolvereita: {{ip}}.\",\n  \"local_ptr_desc\": \"AdGuard Homen yksityisille PTR-, SOA- ja NS-pyynnöille käyttämät DNS-palvelimet. Pyyntöä luokitellaan yksityiseksi, jos se pyytää yksityistä IP-aluetta (kuten \\\"192.168.12.34\\\") käyttävän aliverkon sisältävää ARPA-verkkotunnusta ja on lähtöisin päätteeltä, jolla on yksityinen IP-osoite. Jos tätä ei ole määritetty, käytetään käyttöjärjestelmän oletusarvoisia DNS-resolvereita (AdGuard Homen IP-osoitteet pois lukien).\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home ei voinut määrittää tälle järjestelmälle sopivaa yksityistä käänteis-DNS-resolveria.\",\n  \"local_ptr_placeholder\": \"Syötä yksi IP-osoite per rivi\",\n  \"local_ptr_title\": \"Yksityiset käänteis-DNS-palvelimet\",\n  \"location\": \"Sijainti\",\n  \"log_and_stats_section_label\": \"Pyyntöhistoria ja tilastot\",\n  \"lower_range_start_error\": \"Oltava alueen aloitusarvoa pienempi\",\n  \"main_settings\": \"Pääasetukset\",\n  \"make_static\": \"Tallenna kiinteäksi\",\n  \"manual_update\": \"Seuraa <a>näitä ohjeita</a> päivittääksesi manuaalisesti.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Maanantai\",\n  \"monday_short\": \"Ma\",\n  \"name\": \"Nimi\",\n  \"name_table_header\": \"Nimi\",\n  \"netname\": \"Verkon nimi\",\n  \"network\": \"Verkko\",\n  \"new_allowlist\": \"Uusi sallittujen lista\",\n  \"new_blocklist\": \"Uusi estolista\",\n  \"next\": \"Seuraava\",\n  \"next_btn\": \"Seuraava\",\n  \"no_blocklist_added\": \"Estolistoja ei ole lisätty\",\n  \"no_clients_found\": \"Päätelaitteita ei löytynyt\",\n  \"no_domains_found\": \"Verkkotunnuksia ei löytynyt\",\n  \"no_logs_found\": \"Historiatietoja ei ole\",\n  \"no_servers_specified\": \"Palvelimia ei ole määritetty\",\n  \"no_upstreams_data_found\": \"Ylävirtatietoja ei löytynyt\",\n  \"no_whitelist_added\": \"Sallittujen listoja ei ole lisätty\",\n  \"nothing_found\": \"Ei tuloksia\",\n  \"null_ip\": \"Tyhjä IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Mainoseston suodattimien ja hosts-estolistojen estämien DNS-pyyntöjen määrä\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Estettyjen aikuisille tarkoitettujen sivustojen määrä\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"AdGuardin Turvallinen selaus -moduulin estämien DNS-pyyntöjen määrä\",\n  \"number_of_dns_query_days\": \"Käsiteltyjen DNS-pyyntöjen määrä viimeisten {{count}} päivän ajalta\",\n  \"number_of_dns_query_days_plural\": \"Käsiteltyjen DNS-pyyntöjen määrä viimeisten {{count}} päivän ajalta\",\n  \"number_of_dns_query_hours\": \"Viimeisen {{count}} tunnin aikana käsiteltyjen DNS-kyselyiden määrä\",\n  \"number_of_dns_query_hours_plural\": \"Viimeisen {{count}} tunnin aikana käsiteltyjen DNS-kyselyiden määrä\",\n  \"number_of_dns_query_to_safe_search\": \"DNS-pyyntöjen määrä, joille turvallinen haku pakotettiin käyttöön\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"Ei käytössä\",\n  \"on\": \"Käytössä\",\n  \"open_dashboard\": \"Avaa hallintapaneeli\",\n  \"orgname\": \"Organisaation nimi\",\n  \"original_response\": \"Alkuperäinen vastaus\",\n  \"out_of_range_error\": \"Oltava alueen \\\"{{start}}\\\" - \\\"{{end}}\\\" ulkopuolella\",\n  \"page_table_footer_text\": \"Sivu\",\n  \"parallel_requests\": \"Rinnakkaiset pyynnöt\",\n  \"parental_control\": \"Lapsilukko\",\n  \"password_label\": \"Salasana\",\n  \"password_placeholder\": \"Syötä salasana\",\n  \"plain_dns\": \"Tavallinen DNS\",\n  \"port_53_faq_link\": \"Portti 53 on usein \\\"DNSStubListener\\\" tai \\\"systemd-resolved\\\" -palveluiden varaama. Lue <0>nämä ohjeet</0> tämän ratkaisemiseksi.\",\n  \"previous_btn\": \"Edellinen\",\n  \"privacy_policy\": \"Tietosuojakäytäntö\",\n  \"processing_update\": \"Odota kun AdGuard Home päivittyy\",\n  \"protection_section_label\": \"Suojaus\",\n  \"protocol\": \"Protokolla\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Pyyntöhistoria\",\n  \"query_log_clear\": \"Tyhjennä pyyntöhistoria\",\n  \"query_log_cleared\": \"Pyyntöhistorian tyhjennys onnistui\",\n  \"query_log_configuration\": \"Historian määritys\",\n  \"query_log_confirm_clear\": \"Haluatko varmasti tyhjentää pyyntöhistorian?\",\n  \"query_log_disabled\": \"Pyyntöhistoria ei ole käytössä. Voit ottaa sen käyttöön <0>asetuksista</0>.\",\n  \"query_log_enable\": \"Käytä historiaa\",\n  \"query_log_filtered\": \"Suodattanut {{filter}}\",\n  \"query_log_response_status\": \"Tila: {{value}}\",\n  \"query_log_retention\": \"Pyyntöhistorian kierto\",\n  \"query_log_retention_confirm\": \"Haluatko varmasti muuttaa pyyntöhistorian kiertoa? Jos pienennät aikaväliä, osa tiedoista menetetään.\",\n  \"query_log_strict_search\": \"Käytä tarkalle haulle lainausmerkkejä\",\n  \"query_log_updated\": \"Pyyntöhistorian päivitys onnistui\",\n  \"rate_limit\": \"Pyyntöajoitus\",\n  \"rate_limit_desc\": \"Päätelaitteelle sallittu pyyntöjen enimmäismäärä sekunnissa. Arvo 0 tarkoittaa rajatonta.\",\n  \"rate_limit_subnet_len_ipv4\": \"IPv4-osoitteiden aliverkon etuliitteen pituus\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Pyyntörajoitukseen käytettävien IPv4-osoitteiden aliverkon etuliitteen pituus. Oletusarvo on 24.\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4-aliverkon etuliitteen pituuden tulee olla väliltä 0–32.\",\n  \"rate_limit_subnet_len_ipv6\": \"IPv6-osoitteiden aliverkon etuliitteen pituus\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Pyyntörajoitukseen käytettävien IPv6-osoitteiden aliverkon etuliitteen pituus. Oletusarvo on 56.\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6-aliverkon etuliitteen pituuden tulee olla väliltä 0–128.\",\n  \"rate_limit_whitelist\": \"Pyyntörajoituksen ohituslista\",\n  \"rate_limit_whitelist_desc\": \"IP-osoitteet, jotka eivät kuulu pyyntörajoituksen piiriin.\",\n  \"rate_limit_whitelist_placeholder\": \"Syötä yksi IP-osoite per rivi\",\n  \"refresh_btn\": \"Päivitä\",\n  \"refresh_statics\": \"Päivitä tilastot\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Ilmoita ongelmasta\",\n  \"request_details\": \"Pyynnön tiedot\",\n  \"request_table_header\": \"Pyyntö\",\n  \"requests_count\": \"Pyyntöjen määrä\",\n  \"reset_settings\": \"Tyhjennä asetukset\",\n  \"resolve_clients_desc\": \"Selvitä päätelaitteiden IP-osoitteiden isäntänimet käänteisesti lähettämällä PTR-pyynnöt sopiville resolvereille (yksityiset DNS-palvelimet paikallisille päätelaitteille, ylävirtapalvelimet päätelaitteille, joilla on julkiset IP-osoitteet).\",\n  \"resolve_clients_title\": \"Käytä päätelaitteiden IP-osoitteille käänteistä selvitystä\",\n  \"response_code\": \"Vastauksen koodi\",\n  \"response_details\": \"Vastauksen tiedot\",\n  \"response_table_header\": \"Vastaus\",\n  \"response_time\": \"Vasteaika\",\n  \"rewrite_A\": \"<0>A</0>: erityinen arvo, säilytä ylävirran <0>A</0>-tiedot\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: erityinen arvo, säilytä ylävirran <0>AAAA</0>-tiedot\",\n  \"rewrite_add\": \"Lisää DNS-uudelleenohjaus\",\n  \"rewrite_added\": \"Kohteen \\\"{{key}}\\\" DNS-uudelleenohjaus lisättiin\",\n  \"rewrite_applied\": \"Uudelleenohjattu säännöllä\",\n  \"rewrite_confirm_delete\": \"Haluatko varmasti poistaa DNS-uudelleenohjauksen kohteelle \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"Kohteen \\\"{{key}}\\\" DNS-uudelleenohjaus poistettiin\",\n  \"rewrite_desc\": \"Mahdollistaa oman DNS-vastauksen helpon määrityksen tietylle verkkotunnukselle.\",\n  \"rewrite_domain_name\": \"Verkkotunnus: lisää CNAME-tietue.\",\n  \"rewrite_edit\": \"Muokkaa DNS-uudelleenohjausta\",\n  \"rewrite_hosts_applied\": \"Hosts-tiedoston säännön korvaama\",\n  \"rewrite_ip_address\": \"IP-osoite: käytä tätä IP-osoitetta A tai AAAA -vastauksessa.\",\n  \"rewrite_not_found\": \"DNS-uudelleenohjauksia ei löytynyt\",\n  \"rewrite_settings_updated\": \"DNS-uudelleenkirjoitusasetukset päivitetty onnistuneesti\",\n  \"rewrite_updated\": \"DNS-uudelleenohjaukset päivitettiin\",\n  \"rewrites_disabled_table_header\": \"Uudelleenkirjoitukset ovat poissa käytöstä\",\n  \"rewrites_enabled_table_header\": \"Uudelleenkirjoitukset ovat käytössä\",\n  \"rewritten\": \"Uudelleenohjatut\",\n  \"rows_table_footer_text\": \"riviä\",\n  \"rule_added_to_custom_filtering_toast\": \"Sääntö lisättiin omiin suodatussääntöihin: {{rule}}\",\n  \"rule_label\": \"Säännöt\",\n  \"rule_removed_from_custom_filtering_toast\": \"Sääntö poistettiin omista suodatussäännöistä: {{rule}}\",\n  \"rules_count_table_header\": \"Sääntöjen määrä\",\n  \"safe_browsing\": \"Turvallinen selaus\",\n  \"safe_search\": \"Turvallinen haku\",\n  \"saturday\": \"Lauantai\",\n  \"saturday_short\": \"La\",\n  \"save_btn\": \"Tallenna\",\n  \"save_config\": \"Tallenna asetukset\",\n  \"schedule_add\": \"Lisää ajoitus\",\n  \"schedule_current_timezone\": \"Nykyinen aikavyöhyke: {{value}}\",\n  \"schedule_desc\": \"Aseta estettujen palveluiden käyttämättömyysjaksot\",\n  \"schedule_edit\": \"Muokkaa ajoitus\",\n  \"schedule_from\": \"Alkaen\",\n  \"schedule_invalid_select\": \"Aloitusaika on oltava ennen lopetusaikaa\",\n  \"schedule_modal_description\": \"Tämä ajoitus korvaa kaikki nykyiset kyseisen viikonpäivän ajoitukset. Jokaisella viikonpäivällä voi olla vain yksi toimettomuusjakso.\",\n  \"schedule_modal_time_off\": \"Ei palveluestoa:\",\n  \"schedule_new\": \"Uusi ajoitus\",\n  \"schedule_remove\": \"Poista ajoitus\",\n  \"schedule_save\": \"Tallenna ajoitus\",\n  \"schedule_select_days\": \"Valitse päivät\",\n  \"schedule_services\": \"Pysäytä palveluesto\",\n  \"schedule_services_desc\": \"Määritä palvelunestosuodattimen pysäytysajoitus.\",\n  \"schedule_services_desc_client\": \"Määritä palvelunestosuodattimen pysäytysajoitus tälle päätteelle.\",\n  \"schedule_time_all_day\": \"Koko päivän\",\n  \"schedule_timezone\": \"Valitse aikavyöhyke\",\n  \"schedule_to\": \"Päättyen\",\n  \"served_from_cache_label\": \"Toimitettu välimuistista\",\n  \"service_name\": \"Palvelun nimi\",\n  \"set_static_ip\": \"Määritä kiinteä IP-osoite\",\n  \"settings\": \"Asetukset\",\n  \"settings_custom\": \"Mukautettu\",\n  \"settings_global\": \"Yleinen\",\n  \"setup_config_to_enable_dhcp_server\": \"Määritä asetukset DHCP-palvelimen käyttöönottoa varten\",\n  \"setup_dns_notice\": \"<1>DNS-over-HTTPS</1> tai <1>DNS-over-TLS</1> -toteutuksia varten, on AdGuard Homen <0>Salausasetukset</0> määritettävä.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Käytä merkkijonoa <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Käytä merkkijonoa <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Tässä on lista ohjelmistoista, joita voit käyttää.</0>\",\n  \"setup_dns_privacy_4\": \"iOS 14 ja macOS Big Sur -laitteille voidaan ladata erityinen '.mobileconfig' -tiedosto, joka lisää DNS-asetuksiin <highlight>DNS-over-HTTPS</highlight> tai <highlight>DNS-over-TLS</highlight> -palvelimet.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 tukee DNS-over-TLS -toteutusta natiivisti. Se määritetään syöttämällä oma verkkotunnus kohtaan \\\"Asetukset\\\" → \\\"Yhteydet\\\" → \\\"Lisää yhteysasetuksia\\\" → \\\"Yksityinen DNS\\\".\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard Androidille</0> tukee <1>DNS-over-HTTPS</1> ja <1>DNS-over-TLS</1> -toteutuksia.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> lisää <1>DNS-over-HTTPS</1> tuen Androidiin.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS ja macOS -asetukset\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> tukee <1>DNS-over-HTTPS</1>, mutta oman palvelimen käyttö' varten sille on luotava <2>DNS Stamp</2> -merkintä.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard iOS:lle</0> tukee <1>DNS-over-HTTPS</1> ja <1>DNS-over-TLS</1> -toteutuksia.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home voi itse olla suojattu DNS -pääte millä tahansa alustalla.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> tukee kaikkia tunnettuja suojattuja DNS-protokollia.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> tukee <1>DNS-over-HTTPS</1> -protokollaa.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> tukee <1>DNS-over-HTTPS</1>-toteutusta.\",\n  \"setup_dns_privacy_other_5\": \"Löydät lisää toteutuksia <0>täältä</0> ja <1>täältä</1>.\",\n  \"setup_dns_privacy_other_title\": \"Muita toteutuksia\",\n  \"setup_guide\": \"Asennusopas\",\n  \"show_all_filter_type\": \"Näytä kaikki\",\n  \"show_blocked_responses\": \"Estetty\",\n  \"show_filtered_type\": \"Näytä suodatetut\",\n  \"show_processed_responses\": \"Käsitelty\",\n  \"show_whitelisted_responses\": \"Sallitut\",\n  \"sign_in\": \"Kirjaudu\",\n  \"sign_out\": \"Kirjaudu ulos\",\n  \"source_label\": \"Lähde\",\n  \"static_ip\": \"Kiiteä IP-osoite\",\n  \"static_ip_desc\": \"AdGuard Home on palvelin, joten se tarvitsee kiinteän IP-osoitteen toimiakseen oikein. Muutoin reitittimesi saattaa määrittää sille jossakin vaiheessa uuden, dynaamisen IP-osoitteen.\",\n  \"statistics_clear\": \"Tyhjennä tilastot\",\n  \"statistics_clear_confirm\": \"Haluatko varmasti tyhjentää tilastot?\",\n  \"statistics_cleared\": \"Tilastot tyhjennettiin\",\n  \"statistics_configuration\": \"Tilastoinnin määritys\",\n  \"statistics_enable\": \"Ota tilastointi käyttöön\",\n  \"statistics_retention\": \"Tilastojen säilytys\",\n  \"statistics_retention_confirm\": \"Haluatko varmasti muuttaa tilastojen säilytysaikaa? Jos aikaa lyhennetään, joitakin tietoja menetetään.\",\n  \"statistics_retention_desc\": \"Jos aikajaksoa lyhennetään, joitakin tietoja menetetään.\",\n  \"stats_adult\": \"Estetyt aikuisille tarkoitetut sivustot\",\n  \"stats_disabled\": \"Tilastointi ei ole käytössä. Voit ottaa sen käyttöön <0>asetuksista</0>.\",\n  \"stats_disabled_short\": \"Tilastointi ei ole käytössä\",\n  \"stats_malware_phishing\": \"Estetyt haittaohjelmat/tietojenkalastelut\",\n  \"stats_params\": \"Tilastoinnin määritys\",\n  \"stats_query_domain\": \"Kysytyimmät verkkotunnukset\",\n  \"subnet_error\": \"Osoitteiden tulee olla yhdessä aliverkossa\",\n  \"sunday\": \"Sunnuntai\",\n  \"sunday_short\": \"Su\",\n  \"system_host_files\": \"Järjestelmän hosts-tiedostot\",\n  \"table_client\": \"Asiakas\",\n  \"table_name\": \"Nimi\",\n  \"tags_desc\": \"Voit valita päätelaitetta vastaavia tunnisteita. Tunnisteet voidaan sisällyttää suodatussääntöihin ja näin voit kohdistaa niitä tarkemmin. <0>Lue lisää</0>.\",\n  \"tags_title\": \"Tunnisteet\",\n  \"test_upstream_btn\": \"Testaa ylävirtoja\",\n  \"theme_auto\": \"Automaattinen\",\n  \"theme_auto_desc\": \"Automaattinen (seuraa laitteen väriteemaa)\",\n  \"theme_dark\": \"Tumma\",\n  \"theme_dark_desc\": \"Tumma teema\",\n  \"theme_light\": \"Vaalea\",\n  \"theme_light_desc\": \"Vaalea teema\",\n  \"thursday\": \"Torstai\",\n  \"thursday_short\": \"To\",\n  \"time_table_header\": \"Aika\",\n  \"top_blocked_domains\": \"Estetyimmät verkkotunnukset\",\n  \"top_clients\": \"Käytetyimmät päätelaitteet\",\n  \"top_upstreams\": \"Käytetyimmät ylävirrat\",\n  \"topline_expired_certificate\": \"SSL-varmenteesi on erääntynyt. Päivitä <0>Salausasetukset</0>.\",\n  \"topline_expiring_certificate\": \"SSL-varmenteesi on erääntymässä. Päivitä <0>Salausasetukset</0>.\",\n  \"tracker_source\": \"Seurannan lähde\",\n  \"try_again\": \"Yritä uudelleen\",\n  \"ttl_cache_validation\": \"Välimuistin vähimmäiselinajan on oltava pienempi tai sama kuin enimmäiselinajan\",\n  \"tuesday\": \"Tiistai\",\n  \"tuesday_short\": \"Ti\",\n  \"type_table_header\": \"Tyyppi\",\n  \"unavailable_dhcp\": \"DHCP ei ole käytettävissä\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home ei voi suorittaa DHCP-palvelinta käyttöjärjestelmässäsi\",\n  \"unblock\": \"Salli\",\n  \"unblock_all\": \"Salli kaikki\",\n  \"unblock_for_this_client_only\": \"Salli vain tälle päätelaitteelle\",\n  \"unknown_filter\": \"Tuntematon suodatin {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} on nyt saatavilla! <0>Paina tästä</0> saadaksesi lisätietoja.\",\n  \"update_failed\": \"Automaattinen päivitys epäonnistui. Seuraa <a>näitä ohjeita</a> päivittääksesi manuaalisesti.\",\n  \"update_now\": \"Päivitä nyt\",\n  \"updated_custom_filtering_toast\": \"Omat suodatussäännöt päivitettiin\",\n  \"updated_save_search_toast\": \"Turvallisen haun asetukset päivitettiin\",\n  \"updated_upstream_dns_toast\": \"Ylävirtapalvelimet tallennettiin\",\n  \"updates_checked\": \"Uusi versio AdGuard Home -ohjelmasta on saatavana\\n\",\n  \"updates_version_equal\": \"AdGuard Home on ajan tasalla\",\n  \"upstream\": \"Ylävirta\",\n  \"upstream_dns\": \"Ylävirran DNS-palvelimet\",\n  \"upstream_dns_cache_configuration\": \"Ylävirran DNS-välimuistin määritykset\",\n  \"upstream_dns_client_desc\": \"Jos tämä on tyhjä, käyttää AdGuard Home <0>DNS-asetuksissa</0> määritettyjä palvelimia.\",\n  \"upstream_dns_configured_in_file\": \"Määritetty tiedostossa {{path}}\",\n  \"upstream_dns_help\": \"Syötä yksi palvelinosoite per rivi. <a>Lue lisää</a> ylävirran DNS-palvelinten määrityksestä.\",\n  \"upstream_parallel\": \"Käytä rinnakkaisia pyyntöjä ja nopeuta selvitystä käyttämällä kaikkia ylävirtapalvelimia samanaikaisesti.\",\n  \"upstream_timeout\": \"Ylöspäin suuntautuva aikakatkaisu\",\n  \"upstream_timeout_desc\": \"Määrittää odotettavien sekuntien määrä, ennen kuin saadaan vastaus ylävirtapalvelimelta\",\n  \"upstreams\": \"Ylävirrat\",\n  \"use_adguard_browsing_sec\": \"Käytä AdGuardin turvallisen selauksen palvelua\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home tarkistaa onko verkkotunnus turvallisen selauksen verkkopalvelun estämä. Se käyttää tarkastukseen tietosuojapainotteista rajapintaa: palvelimelle lähetetään vain pieni osa verkkotunnuksen SHA256-hajautusarvosta.\",\n  \"use_adguard_parental\": \"Käytä AdGuardin lapsilukko-palvelua\",\n  \"use_adguard_parental_hint\": \"AdGuard Home tarkistaa, sisältääkö verkkotunnus aikuisille tarkoitettua sisältöä. Se käyttää samaa tietosuojapainotteista rajapintaa, kuin turvallisen selauksen palvelu.\",\n  \"use_private_ptr_resolvers_desc\": \"Selvitä yksityisiä IP-osoitteita sisältävien ARPA-verkkotunnusten PTR-, SOA- ja NS-pyynnöt käyttäen yksityisiä ylävirtapalvelimia, DHCP:tä, /etc/hosts-määrityksiä, yms. Jos tämä ei ole käytössä, AdGuard Home vastaa tällaisiin pyyntöihin NXDOMAIN-tiedolla.\",\n  \"use_private_ptr_resolvers_title\": \"Käytä yksityisiä käänteis-DNS-resolvereita\",\n  \"use_saved_key\": \"Käytä aiemmin tallennettua avainta\",\n  \"username_label\": \"Käyttäjätunnus\",\n  \"username_placeholder\": \"Syötä käyttäjätunnus\",\n  \"validated_with_dnssec\": \"DNSSEC-vahvistettu\",\n  \"version\": \"Versio\",\n  \"version_request_error\": \"Päivitystarkistus epäonnistui. Tarkista Internet-yhteytesi.\",\n  \"wednesday\": \"Keskiviikko\",\n  \"wednesday_short\": \"Ke\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/fr.json",
    "content": "{\n  \"access_allowed_desc\": \"Une liste de CIDRs, d'adresses IP, ou de <a>ClientIDs</a>. Si cette liste comporte des entrées, AdGuard Home n'acceptera que les demandes provenant de ces clients.\",\n  \"access_allowed_title\": \"Clients autorisés\",\n  \"access_blocked_desc\": \"A ne pas confondre avec les filtres. AdGuard Home rejette les requêtes DNS correspondant à ces domaines, et ces requêtes n'apparaissent même pas dans le journal des requêtes. Vous pouvez spécifier des noms de domaine exacts, des caractères génériques ou des règles de filtrage d'URL, par exemple « exemple.org », « *.exemple.org » ou « ||example.org^ » de manière correspondante.\",\n  \"access_blocked_title\": \"Domaines interdits\",\n  \"access_desc\": \"Ici vous pouvez configurer les règles d'accès au serveur DNS AdGuard Home\",\n  \"access_disallowed_desc\": \"Une liste de CIDRs, d'adresses IP, ou de <a>ClientIDs</a>. Si cette liste comporte des entrées, AdGuard Home abandonnera les demandes provenant de ces clients. Ce champ est ignoré s'il y a des entrées dans Clients autorisés.\",\n  \"access_disallowed_title\": \"Clients non autorisés\",\n  \"access_settings_saved\": \"Paramètres d'accès enregistrés avec succès\",\n  \"access_title\": \"Paramètres d'accès\",\n  \"actions_table_header\": \"Actions\",\n  \"add_allowlist\": \"Ajouter liste d’autorisation\",\n  \"add_blocklist\": \"Ajouter liste de blocage\",\n  \"add_custom_list\": \"Ajouter une liste personnalisée\",\n  \"add_persistent_client\": \"Ajouter comme client persistant\",\n  \"address\": \"Addresse\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home ignorera toutes les requêtes DNS de ce client.\",\n  \"all_lists_up_to_date_toast\": \"Toutes les listes sont déjà à jour\",\n  \"all_queries\": \"Toutes les requêtes\",\n  \"allow_this_client\": \"Autoriser ce client\",\n  \"allowed\": \"Autorisées\",\n  \"anonymize_client_ip\": \"Anonymiser l’IP du client\",\n  \"anonymize_client_ip_desc\": \"Ne pas enregistrer l’adresse IP complète du client dans les journaux et statistiques\",\n  \"anonymizer_notification\": \"<0>Note :</0> L'anonymisation IP est activée. Vous pouvez la désactiver dans les <1>paramètres généraux</1>.\",\n  \"answer\": \"Réponse\",\n  \"apply_btn\": \"Appliquer\",\n  \"auto_clients_desc\": \"Informations sur les adresses IP des appareils qui utilisent ou pourraient utiliser AdGuard Home. Ces informations sont recueillies à partir de plusieurs sources, notamment les fichiers hosts, le DNS inverse, etc.\",\n  \"auto_clients_title\": \"Clients d'exécution\",\n  \"autofix_warning_list\": \"Ceci effectuera les tâches suivantes : <0>Désactiver le système DNSStubListener</0> <0>Définir l’adresse du serveur DNS à 127.0.0.1 </0> <0>Remplacer la cible du lien symbolique de /etc/resolv.conf par /run/systemd/resolve/resolv.conf</0> <0>Arrêter DNSStubListener (recharger le service résolu par systemd)</0>\",\n  \"autofix_warning_result\": \"Par conséquent, toutes les demandes DNS de votre système seront traitées par AdGuardHome par défaut.\",\n  \"autofix_warning_text\": \"Si vous cliquez sur « Réparer », AdGuard Home configurera votre système pour utiliser le serveur DNS AdGuard Home.\",\n  \"average_processing_time\": \"Temps moyen de traitement\",\n  \"average_processing_time_hint\": \"Temps moyen (en millisecondes) de traitement d'une requête DNS\",\n  \"average_upstream_response_time\": \"Temps de réponse moyen en amont\",\n  \"back\": \"Retour\",\n  \"block\": \"Bloquer\",\n  \"block_all\": \"Tout bloquer\",\n  \"block_domain_use_filters_and_hosts\": \"Bloquez les domaines à l'aide des filtres et fichiers hosts\",\n  \"block_for_this_client_only\": \"Bloquer uniquement pour ce client\",\n  \"block_services\": \"Bloquer des services spécifiques\",\n  \"blocked_adult_websites\": \"Bloqué par le Contrôle Parental\",\n  \"blocked_by\": \"<0>Bloqué par Filtres</0>\",\n  \"blocked_by_cname_or_ip\": \"Bloqué par CNAME ou adresse IP\",\n  \"blocked_by_response\": \"Bloqué par un CNAME ou une réponse IP\",\n  \"blocked_response_ttl\": \"Réponse bloquée TTL\",\n  \"blocked_response_ttl_desc\": \"Spécifie pendant combien de secondes les clients doivent mettre en cache une réponse filtrée\",\n  \"blocked_safebrowsing\": \"Bloqué par la Navigation sécurisée\",\n  \"blocked_service\": \"Service bloqué\",\n  \"blocked_services\": \"Services bloqués\",\n  \"blocked_services_desc\": \"Permet de bloquer les sites et services populaires rapidement.\",\n  \"blocked_services_global\": \"Utiliser les services bloqués globaux\",\n  \"blocked_services_saved\": \"Services bloqués enregistrés\",\n  \"blocked_threats\": \"Menaces bloquées\",\n  \"blocking_ipv4\": \"Blocage IPv4\",\n  \"blocking_ipv4_desc\": \"Adresse IP à renvoyer pour une demande A bloquée\",\n  \"blocking_ipv6\": \"Blocage IPv6\",\n  \"blocking_ipv6_desc\": \"Adresse IP à renvoyer pour une demande AAAA bloquée\",\n  \"blocking_mode\": \"Mode du blocage\",\n  \"blocking_mode_custom_ip\": \"IP personnalisée : Répondre avec une adresse IP définie manuellement\",\n  \"blocking_mode_default\": \"Par défaut : Répondre avec adresse IP zéro (0.0.0.0 pour A ; :: pour AAAA) lorsque bloqué par la règle de style Adblock ; répondre avec l’adresse IP spécifiée dans la règle lorsque bloquée par la règle du style /etc/hosts\",\n  \"blocking_mode_null_ip\": \"IP nulle : Répondre avec une adresse IP nulle (0.0.0.0 pour A ; :: pour AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN : Répondre avec le code NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED : Répondre avec le code REFUSED\",\n  \"blocklist\": \"Liste de blocage\",\n  \"bootstrap_dns\": \"Serveurs DNS d'amorçage\",\n  \"bootstrap_dns_desc\": \"Les adresses IP des serveurs DNS utilisées pour résoudre les adresses IP des résolveurs DoH/DoT que vous spécifiez comme en amont. Les commentaires ne sont pas autorisés.\",\n  \"cache_cleared\": \"Le cache DNS a été vidé\",\n  \"cache_enabled\": \"Activer le cache\",\n  \"cache_enabled_desc\": \"Stockez les réponses DNS localement.\",\n  \"cache_optimistic\": \"Caching optimiste\",\n  \"cache_optimistic_desc\": \"Faites en sorte qu'AdGuard Home réponde à partir du cache même lorsque les entrées ont expiré et essayez également de les actualiser.\",\n  \"cache_size\": \"Taille du cache\",\n  \"cache_size_desc\": \"Taille du cache DNS (en octets).\",\n  \"cache_size_validation\": \"La taille du cache doit être supérieure à zéro lorsqu'elle est activée.\",\n  \"cache_ttl_max_override\": \"Remplacer le TTL maximum\",\n  \"cache_ttl_max_override_desc\": \"Établir la valeur de durée de vie TTL maximale (en secondes) pour les saisies dans le cache du DNS .\",\n  \"cache_ttl_min_override\": \"Remplacer le TTL minimum\",\n  \"cache_ttl_min_override_desc\": \"Prolonger les valeurs courtes de durée de vie (en secondes) reçues du serveur en amont lors de la mise en cache des réponses DNS .\",\n  \"cancel_btn\": \"Annuler\",\n  \"category_label\": \"Catégorie\",\n  \"check\": \"Vérifier\",\n  \"check_client_id\": \"Identifiant du client (ClientID ou adresse IP)\",\n  \"check_cname\": \"CNAME : {{cname}}\",\n  \"check_desc\": \"Vérifier si le nom d’hôte est filtré .\",\n  \"check_dhcp_servers\": \"Rechercher les serveurs DHCP\",\n  \"check_dns_record\": \"Sélectionnez le type d'enregistrement DNS\",\n  \"check_enter_client_id\": \"Saisissez l'identifiant du client\",\n  \"check_hostname\": \"Nom d'hôte ou nom de domaine\",\n  \"check_ip\": \"Adresses IP : {{ip}}\",\n  \"check_not_found\": \"Introuvable dans vos listes de filtres\",\n  \"check_reason\": \"Raison : {{reason}}\",\n  \"check_service\": \"Nom du service : {{service}}\",\n  \"check_title\": \"Vérification du filtrage\",\n  \"check_updates_btn\": \"Vérifier les mises à jour\",\n  \"check_updates_now\": \"Vérifier les mises à jour\",\n  \"choose_allowlist\": \"Choisir les listes blanches\",\n  \"choose_blocklist\": \"Choisir les listes de blocage\",\n  \"choose_from_list\": \"Choisissez dans la liste\",\n  \"city\": \"Ville\",\n  \"clear_cache\": \"Vider le cache\",\n  \"click_to_view_queries\": \"Cliquer pour voir les requêtes\",\n  \"client_add\": \"Ajouter un client\",\n  \"client_added\": \"Le client « {{key}} » a été ajouté\",\n  \"client_blocked\": \"Client « {{ip}} » bloqué\",\n  \"client_confirm_block\": \"Voulez-vous vraiment bloquer le client « {{ip}} » ?\",\n  \"client_confirm_delete\": \"Voulez-vous vraiment supprimer le client « {{key}} » ?\",\n  \"client_confirm_unblock\": \"Voulez-vous vraiment débloquer le client « {{ip}} » ?\",\n  \"client_deleted\": \"Le client « {{key}} » a été supprimé\",\n  \"client_details\": \"Détails du client\",\n  \"client_edit\": \"Modifier le client\",\n  \"client_global_settings\": \"Utiliser les paramètres généraux\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Les clients différents peuvent être identifiés par aide d'un ClientID spécial. Vous trouverez plus d'information sur l'identification des clients <a>ici</a> .\",\n  \"client_id_placeholder\": \"Saisissez le ClientID\",\n  \"client_identifier\": \"Identifiant\",\n  \"client_identifier_desc\": \"Les clients peuvent être identifiés par leur adresse IP, CIDR, adresse MAC ou ClientID (peut être utilisé pour DoT/DoH/DoQ). En savoir plus sur la façon d'identifier les clients <0>ici</0>.\",\n  \"client_name\": \"Client {{id}}\",\n  \"client_new\": \"Nouveau client\",\n  \"client_settings\": \"Paramètres du client\",\n  \"client_table_header\": \"Client\",\n  \"client_unblocked\": \"Client « {{ip}} » débloqué\",\n  \"client_updated\": \"Le client « {{key}} » a été mis à jour\",\n  \"clients_desc\": \"Configurer des dossiers de clients persistants pour les appareils connectés à AdGuard Home\",\n  \"clients_not_found\": \"Aucun client trouvé\",\n  \"clients_title\": \"Clients persistants\",\n  \"compact\": \"Compact\",\n  \"config_successfully_saved\": \"Configuration sauvegardée\",\n  \"configure\": \"Configurer\",\n  \"confirm_dns_cache_clear\": \"Voulez-vous vraiment vider le cache DNS ?\",\n  \"confirm_static_ip\": \"AdGuard Home configurera {{ip}} pour être votre adresse IP statique. Voulez-vous poursuivre ?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"Pays\",\n  \"custom_filter_rules\": \"Règles de filtrage d'utilisateur\",\n  \"custom_filter_rules_hint\": \"Saisissez la règle en une ligne. C'est possible d'utiliser les règles de blocage ou la syntaxe des fichiers hosts.\",\n  \"custom_filtering_rules\": \"Règles de filtrage personnalisées\",\n  \"custom_ip\": \"IP personnalisée\",\n  \"custom_retention_input\": \"Saisir la rétention en heures\",\n  \"custom_rotation_input\": \"Saisir la rotation en heures\",\n  \"dashboard\": \"Tableau de bord\",\n  \"date\": \"Date\",\n  \"default\": \"Par défaut\",\n  \"delete_confirm\": \"Voulez-vous vraiment supprimer « {{key}} »?\",\n  \"delete_table_action\": \"Supprimer\",\n  \"descr\": \"Description\",\n  \"details\": \"Détails\",\n  \"dhcp_add_static_lease\": \"Ajoutez un bail statique\",\n  \"dhcp_config_saved\": \"Configuration du serveur DHCP sauvegardée\",\n  \"dhcp_description\": \"Si votre routeur ne fonctionne pas avec les réglages DHCP, vous pouvez utiliser le serveur DHCP par défaut d'AdGuard.\",\n  \"dhcp_disable\": \"Désactiver le serveur DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Votre système utilise une configuration d'adresses IP dynamiques pour l'interface <0>{{interfaceName}}</0>. Pour utiliser un serveur DHCP, une adresse IP statique est requise. Votre adresse IP actuelle est <0>{{ipAddress}}</0>. AdGuard Home va automatiquement définir cette adresse IP comme statique si vous appuyez sur le bouton « Activer le serveur DHCP ».\",\n  \"dhcp_edit_static_lease\": \"Modifier le bail statique\",\n  \"dhcp_enable\": \"Activer le serveur DHCP\",\n  \"dhcp_error\": \"AdGuard Home ne peut pas déterminer s'il y a un autre serveur DHCP actif sur le réseau.\",\n  \"dhcp_form_gateway_input\": \"IP de la passerelle\",\n  \"dhcp_form_lease_input\": \"Durée de la location\",\n  \"dhcp_form_lease_title\": \"Période de location du serveur DHCP (secondes)\",\n  \"dhcp_form_range_end\": \"Fin de la rangée\",\n  \"dhcp_form_range_start\": \"Début de la rangée\",\n  \"dhcp_form_range_title\": \"Rangée des adresses IP\",\n  \"dhcp_form_subnet_input\": \"Masque de sous-réseau\",\n  \"dhcp_found\": \"Il y a plusieurs serveurs DHCP actifs sur le réseau. Ce n'est pas prudent d'activer le serveur DHCP intégré en ce moment.\",\n  \"dhcp_hardware_address\": \"Adresse de la machine\",\n  \"dhcp_interface_select\": \"Sélectionner l'interface du serveur DHCP\",\n  \"dhcp_ip_addresses\": \"Adresses IP\",\n  \"dhcp_ipv4_settings\": \"Paramètres IPv4 du DHCP\",\n  \"dhcp_ipv6_settings\": \"Paramètres IPv6 du DHCP\",\n  \"dhcp_lease_added\": \"« {{key}} » de bail statique ajoutée\",\n  \"dhcp_lease_deleted\": \"« {{key}} » de bail statique supprimée\",\n  \"dhcp_lease_updated\": \"Bail statique « {{key}} » mis à jour correctement\",\n  \"dhcp_leases\": \"Locations des serveurs DHCP\",\n  \"dhcp_leases_not_found\": \"Aucun bail DHCP trouvé\",\n  \"dhcp_new_static_lease\": \"Nouveau bail statique\",\n  \"dhcp_not_found\": \"C'est sécuritaire d'activer le serveur DHCP intégré car AdGuard Home n'a pas trouvé de serveur DHCP actif sur le réseau. Toutefois, il vaut mieux revérifier ça manuellement, comme le test automatique ne donne pas 100% de garantie.\",\n  \"dhcp_reset\": \"Voulez-vous vraiment réinitialiser votre configuration DHCP ?\",\n  \"dhcp_reset_leases\": \"Réinitialiser tous les baux\",\n  \"dhcp_reset_leases_confirm\": \"Voulez-vous vraiment réinitialiser tous les baux DHCP?\",\n  \"dhcp_reset_leases_success\": \"Les baux DHCP ont été réinitialisés avec succès\",\n  \"dhcp_settings\": \"Paramètres DHCP\",\n  \"dhcp_static_ip_error\": \"Pour utiliser un serveur DHCP, une adresse IP statique est requise. AdGuard Home n'a pas réussi à déterminer si cette interface réseau est configurée via une adresse IP statique. Veuillez définir une adresse IP statique manuellement.\",\n  \"dhcp_static_leases\": \"Baux statiques DHCP\",\n  \"dhcp_static_leases_not_found\": \"Aucun bail statique DHCP trouvé\",\n  \"dhcp_table_expires\": \"Expire le\",\n  \"dhcp_table_hostname\": \"Nom d'hôte\",\n  \"dhcp_title\": \"Serveur DHCP (expérimental !)\",\n  \"dhcp_warning\": \"Si vous souhaitez tout de même activer le serveur DHCP, assurez-vous qu'il n'y a pas d'autre serveur DHCP actif sur votre réseau. Sinon, cela peut perturber la connexion Internet sur tous les appareils connectés au réseau !\",\n  \"disable_for_hours\": \"Pendant {{count}} heure\",\n  \"disable_for_hours_plural\": \"Pendant {{count}} heures\",\n  \"disable_for_minutes\": \"Pendant {{count}} minute\",\n  \"disable_for_minutes_plural\": \"Pendant {{count}} minutes\",\n  \"disable_for_seconds\": \"Pendant {{count}} seconde\",\n  \"disable_for_seconds_plural\": \"Pendant {{count}} secondes\",\n  \"disable_ipv6\": \"Désactiver la résolution des adresses IPv6\",\n  \"disable_ipv6_desc\": \"Supprimer toutes les requêtes DNS pour les adresses IPv6 (type AAAA) et supprimer les indices IPv6 des réponses HTTPS.\",\n  \"disable_notify_for_hours\": \"Désactiver la protection pendant {{count}} heure\",\n  \"disable_notify_for_hours_plural\": \"Désactiver la protection pendant {{count}} heures\",\n  \"disable_notify_for_minutes\": \"Désactiver la protection pendant {{count}} minute\",\n  \"disable_notify_for_minutes_plural\": \"Désactiver la protection pendant {{count}} minutes\",\n  \"disable_notify_for_seconds\": \"Désactiver la protection pendant {{count}} seconde\",\n  \"disable_notify_for_seconds_plural\": \"Désactiver la protection pendant {{count}} secondes\",\n  \"disable_notify_until_tomorrow\": \"Désactiver la protection jusqu'à demain\",\n  \"disable_protection\": \"Désactiver la protection\",\n  \"disable_rewrites\": \"Désactiver les règles de réécriture\",\n  \"disable_until_tomorrow\": \"Jusqu'à demain\",\n  \"disabled\": \"Désactivé\",\n  \"disabled_dhcp\": \"Serveur DHCP désactivé\",\n  \"disabled_filtering_toast\": \"Filtrage désactivé\",\n  \"disabled_parental_toast\": \"Contrôle Parental désactivé\",\n  \"disabled_protection\": \"Protection désactivée\",\n  \"disabled_safe_browsing_toast\": \"Navigation sécurisée désactivée\",\n  \"disabled_safe_search_toast\": \"Recherche Sécurisée désactivée\",\n  \"disallow_this_client\": \"Interdire ce client\",\n  \"dns_addresses\": \"Adresses DNS\",\n  \"dns_allowlists\": \"Listes d’autorisation DNS\",\n  \"dns_allowlists_desc\": \"Les domaines des listes blanches des DNS seront autorisés même s’ils figurent dans une des listes de blocage.\",\n  \"dns_blocklists\": \"Listes de blocage DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home bloquera les domaines correspondant aux listes de blocage.\",\n  \"dns_cache_config\": \"Configuration du cache DNS\",\n  \"dns_cache_config_desc\": \"Ici, vous pouvez configurer le cache DNS\",\n  \"dns_cache_size\": \"Taille du cache DNS, en bytes\",\n  \"dns_config\": \"Configuration du serveur DNS\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"Confidentialité DNS\",\n  \"dns_providers\": \"Voici une <0>liste de fournisseurs DNS connus</0>.\",\n  \"dns_query\": \"Requêtes DNS\",\n  \"dns_rewrites\": \"Réécritures DNS\",\n  \"dns_settings\": \"Paramètres DNS\",\n  \"dns_start\": \"Démarrage du serveur DNS\",\n  \"dns_status_error\": \"Erreur lors de la vérification du statut du serveur DNS\",\n  \"dns_test_not_ok_toast\": \"Impossible d'utiliser le serveur « {{key}} »: veuillez vérifier si le nom saisi est bien correct\",\n  \"dns_test_ok_toast\": \"Les serveurs DNS spécifiés fonctionnent correctement\",\n  \"dns_test_parsing_error_toast\": \"La section {{section}}: ligne {{line}}: n'a pas pu être utilisée, veuillez vérifier que vous l'avez écrite correctement\",\n  \"dns_test_warning_toast\": \"L'amont « {{key}} » ne répond pas aux demandes de test et peut ne pas fonctionner correctement\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Activer DNSSEC\",\n  \"dnssec_enable_desc\": \"Activez le signe DNSSEC dans les requêtes DNS sortantes et vérifiez le résultat (un résolveur compatible DNSSEC est nécessaire).\",\n  \"domain\": \"Domaine\",\n  \"domain_desc\": \"Saisissez le nom de domaine ou le caractère générique que vous souhaitez réécrire.\",\n  \"domain_name_table_header\": \"Nom de domaine\",\n  \"domain_or_client\": \"Domaine ou client\",\n  \"down\": \"Descendant\",\n  \"download_mobileconfig\": \"Télécharger le fichier de configuration\",\n  \"download_mobileconfig_doh\": \"Télécharger .mobileconfig pour DNS-sur-HTTPS\",\n  \"download_mobileconfig_dot\": \"Télécharger .mobileconfig pour DNS-sur-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Modifier la liste d’autorisation\",\n  \"edit_blocklist\": \"Modifier la liste de blocage\",\n  \"edit_table_action\": \"Modifier\",\n  \"edns_cs_desc\": \"Ajouter l'option du sous-réseau Client EDNS (ECS) au requêtes en amont et enregistrer les valeurs envoyées par les clients dans le journal des requêtes.\",\n  \"edns_enable\": \"Activer le sous-réseau du client EDNS\",\n  \"edns_use_custom_ip\": \"Utiliser une IP personnalisée pour EDNS\",\n  \"edns_use_custom_ip_desc\": \"Autoriser l'utilisation d'une adresse IP personnalisée pour EDNS\",\n  \"elapsed\": \"Écoulé\",\n  \"empty_response_status\": \"Vide\",\n  \"enable_protection\": \"Activer la protection\",\n  \"enable_protection_timer\": \"La protection sera activée dans {{time}}\",\n  \"enable_rewrites\": \"Activer les règles de réécriture\",\n  \"enable_upstream_dns_cache\": \"Activer la mise en cache pour la configuration personnalisée du serveur en amont de ce client\",\n  \"enabled_dhcp\": \"Serveur DHCP activé\",\n  \"enabled_filtering_toast\": \"Filtrage activé\",\n  \"enabled_parental_toast\": \"Contrôle Parental activé\",\n  \"enabled_protection\": \"Protection activée\",\n  \"enabled_safe_browsing_toast\": \"Navigation sécurisée activée\",\n  \"enabled_save_search_toast\": \"Recherche Sécurisée activée\",\n  \"enabled_table_header\": \"Activé\",\n  \"encryption_certificate_path\": \"Emplacement du certificat\",\n  \"encryption_certificates\": \"Certificats\",\n  \"encryption_certificates_desc\": \"Pour utiliser le chiffrement, vous devez fournir une chaîne de certificats SSL valide pour votre domaine. Vous pouvez en obtenir une gratuitement sur <0>{{link}}</0> ou vous pouvez en acheter une via les Autorités de Certification de confiance.\",\n  \"encryption_certificates_input\": \"Copiez/coller vos certificats encodés PEM ici.\",\n  \"encryption_certificates_source_content\": \"Coller les contenus des certificats\",\n  \"encryption_certificates_source_path\": \"Définir un emplacement du fichier des certificats\",\n  \"encryption_chain_invalid\": \"Chaîne de certificat invalide\",\n  \"encryption_chain_valid\": \"Chaîne de certificat valide.\",\n  \"encryption_config_saved\": \"Configuration de chiffrement enregistrée\",\n  \"encryption_desc\": \"Le support du chiffrement (HTTPS/QUIC/TLS) pour les DNS et l'interface web administrateur\",\n  \"encryption_doq\": \"Port DNS sur QUIC\",\n  \"encryption_doq_desc\": \"Si ce port est configuré, AdGuard Home exécutera un serveur DNS sur QUIC sur ce port. \",\n  \"encryption_dot\": \"Port DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Si ce port est configuré, AdGuard Home exécutera un serveur DNS-over-TLS sur ce port.\",\n  \"encryption_enable\": \"Activer le chiffrement (HTTPS, DNS-over-HTTPS et DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Si le chiffrement est activé, l'interface administrateur AdGuard Home fonctionnera via HTTPS et le serveur DNS écoutera les requêtes via DNS-over-HTTPS et DNS-over-TLS.\",\n  \"encryption_expire\": \"Expire le\",\n  \"encryption_hostnames\": \"Noms d'hôte\",\n  \"encryption_https\": \"Port HTTPS\",\n  \"encryption_https_desc\": \"Si le port HTTPS est configuré, l'interface administrateur de AdGuard Home sera accessible via HTTPS et fournira aussi un service DNS-over-HTTPS sur l'emplacement '/dns-query'.\",\n  \"encryption_issuer\": \"Émetteur\",\n  \"encryption_key\": \"Clé privée\",\n  \"encryption_key_input\": \"Copiez/coller votre clé privée PEM encodée pour votre certificat ici.\",\n  \"encryption_key_invalid\": \"Ceci est une clé privée {{type}} invalide\",\n  \"encryption_key_source_content\": \"Coller les contenus de la clef privée\",\n  \"encryption_key_source_path\": \"Définir le chemin d'accès au fichier de clé privée\",\n  \"encryption_key_valid\": \"Ceci est une clé privée {{type}} valide\",\n  \"encryption_plain_dns_desc\": \"Le DNS simple est activé par défaut. Vous pouvez le désactiver pour forcer tous les appareils à utiliser un DNS crypté. Pour faire ça, vous devez activer au moins un protocole DNS crypté\",\n  \"encryption_plain_dns_enable\": \"Activer le DNS simple\",\n  \"encryption_plain_dns_error\": \"Pour désactiver le DNS simple, activez au moins un protocole DNS crypté\",\n  \"encryption_private_key_path\": \"Emplacement de la clef privée\",\n  \"encryption_redirect\": \"Redirection automatiquement vers HTTPS\",\n  \"encryption_redirect_desc\": \"Si coché, AdGuard Home vous redirigera automatiquement d'adresses HTTP vers HTTPS.\",\n  \"encryption_reset\": \"Voulez-vous vraiment réinitialiser les paramètres de chiffrement ?\",\n  \"encryption_server\": \"Nom du serveur\",\n  \"encryption_server_desc\": \"Si cette option est définie, AdGuard Home détecte les ClientID, répond aux requêtes DDR et effectue des validations de connexion supplémentaires. Si elle n'est pas définie, ces fonctions sont désactivées. Doit correspondre à l'un des noms DNS du certificat.\",\n  \"encryption_server_enter\": \"Entrez votre nom de domaine\",\n  \"encryption_settings\": \"Paramètres de chiffrement\",\n  \"encryption_status\": \"État\",\n  \"encryption_subject\": \"Objet\",\n  \"encryption_title\": \"Chiffrement\",\n  \"encryption_warning\": \"Attention\",\n  \"enforce_safe_search\": \"Utiliser la Recherche Sécurisée\",\n  \"enforce_save_search_hint\": \"AdGuard Home appliquera la recherche sécurisée dans les moteurs de recherche suivants : Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Recherche sécurisée forcée\",\n  \"enter_cache_size\": \"Entrer la taille du cache (octets)\",\n  \"enter_cache_ttl_max_override\": \"Entrez le TTL maximum (secondes)\",\n  \"enter_cache_ttl_min_override\": \"Entrez le TTL minimum (secondes)\",\n  \"enter_name_hint\": \"Saisir nom\",\n  \"enter_url_or_path_hint\": \"Entrez une URL ou un chemin absolu de la liste\",\n  \"enter_valid_allowlist\": \"Saisissez une URL valide vers la liste d’autorisation.\",\n  \"enter_valid_blocklist\": \"Saisissez une URL valide vers la liste de blocage.\",\n  \"error_details\": \"Détails des erreurs\",\n  \"example_comment\": \"! Voici un commentaire.\",\n  \"example_comment_hash\": \"# Aussi un commentaire.\",\n  \"example_comment_meaning\": \"juste un commentaire ;\",\n  \"example_meaning_filter_block\": \"bloque l’accès au domaine example.org et à tous ses sous-domaines ;\",\n  \"example_meaning_filter_whitelist\": \"débloque l’accès au domaine example.org et à tous ses sous-domaines ;\",\n  \"example_meaning_host_block\": \"AdGuard Home va retourner l'adresse 127.0.0.1 au domaine example.org (mais pas aux sous-domaines) ;\",\n  \"example_multiple_upstreams_reserved\": \"plusieurs amonts <0>pour des domaines spécifiques</0> ;\",\n  \"example_regex_meaning\": \"bloque l’accès aux domaines correspondants à l'expression régulière spécifiée .\",\n  \"example_rewrite_domain\": \"réécrivez les réponses pour ce nom de domaine uniquement.\",\n  \"example_rewrite_wildcard\": \"réécrire les réponses pour tous les sous-domaines <0>exemple.org</0>.\",\n  \"example_upstream_comment\": \" un commentaire.\",\n  \"example_upstream_doh\": \"<0>DNS-over-HTTPS</0> chiffré ;\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS chiffré avec <0>HTTP/3</0> forcé sans repli sur HTTP/2 ou inférieur ;\",\n  \"example_upstream_doq\": \"<0>DNS-over-QUIC</0> chiffré;\",\n  \"example_upstream_dot\": \"<0>DNS-over-TLS</0> chiffré ;\",\n  \"example_upstream_regular\": \"DNS classique (au-dessus de UDP) ;\",\n  \"example_upstream_regular_port\": \"DNS normal (sur UDP, avec port) ;\",\n  \"example_upstream_reserved\": \"un amont <0>pour des domaines spécifiques</0> ;\",\n  \"example_upstream_sdns\": \"vous pouvez utiliser <0>DNS Stamps</0> pour <1>DNSCrypt</1> ou les résolveurs <2>DNS_over_HTTPS</2> ;\",\n  \"example_upstream_tcp\": \"DNS classique (au-dessus de TCP) ;\",\n  \"example_upstream_tcp_hostname\": \"DNS normal (sur TCP, nom d’hôte) ;\",\n  \"example_upstream_tcp_port\": \"DNS normal (sur TCP, avec port) ;\",\n  \"example_upstream_udp\": \"DNS normal (sur UDP, nom d’hôte) ;\",\n  \"examples_title\": \"Exemples\",\n  \"fallback_dns_desc\": \"Liste des serveurs DNS de repli utilisés lorsque les serveurs DNS en amont ne répondent pas. La syntaxe est la même que dans le champ principal en amont ci-dessus.\",\n  \"fallback_dns_placeholder\": \"Saisissez un serveur DNS de repli par ligne\",\n  \"fallback_dns_title\": \"Serveurs DNS de repli\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Adresse IP la plus rapide\",\n  \"fastest_addr_desc\": \"Attente les réponses de <b>tous</b> les serveurs DNS, mesure de la vitesse de connexion TCP pour chaque serveur et renvoi de l'adresse IP du serveur avec la vitesse de connexion la plus rapide.<br/>Ce mode peut considérablement ralentir les requêtes DNS, si un ou plusieurs serveurs en amont ne répondent pas. Assurez-vous que vos serveurs en amont sont stables et que votre délai dépassé en amont est faible.\",\n  \"filter\": \"Filtre\",\n  \"filter_added_successfully\": \"Le filtre a été ajouté avec succès\",\n  \"filter_allowlist\": \"ATTENTION : Cette action exclura également la règle « {{disallowed_rule}} » de la liste des clients autorisés.\",\n  \"filter_category_general\": \"Général\",\n  \"filter_category_general_desc\": \"Listes qui bloquent le suivi et la publicité sur la plupart des appareils\",\n  \"filter_category_other\": \"Autre\",\n  \"filter_category_other_desc\": \"Autres listes noires\",\n  \"filter_category_regional\": \"Régional\",\n  \"filter_category_regional_desc\": \"Listes axées sur les annonces régionales et les serveurs de suivi\",\n  \"filter_category_security\": \"Sécurité\",\n  \"filter_category_security_desc\": \"Listes créées exprès pour bloquer les logiciels malveillants, des domaines hameçonneurs ou frauduleux\",\n  \"filter_removed_successfully\": \"La liste a été supprimée avec succès\",\n  \"filter_updated\": \"Le filtre a été mis à jour avec succès\",\n  \"filtered\": \"Filtré\",\n  \"filtered_custom_rules\": \"Filtré par des règles de filtrage personnalisées\",\n  \"filtering_rules_learn_more\": \"<0>Apprenez plus</0> sur la création de vos propres listes hosts.\",\n  \"filters\": \"Filtres\",\n  \"filters_and_hosts_hint\": \"AdGuard Home comprend les règles basiques de blocage ainsi que la syntaxe des fichiers hosts.\",\n  \"filters_block_toggle_hint\": \"Vous pouvez configurer les règles de filtrage dans les paramètres des <a>Filtres</a>.\",\n  \"filters_configuration\": \"Configuration des filtres\",\n  \"filters_enable\": \"Activer les filtres\",\n  \"filters_interval\": \"Intervalle de mise à jour des filtres\",\n  \"fix\": \"Corriger\",\n  \"for_last_days\": \"pour les {{count}} derniers jours\",\n  \"for_last_days_plural\": \"pour les {{count}} derniers jours\",\n  \"for_last_hours\": \"pendant la dernière {{count}} heure\",\n  \"for_last_hours_plural\": \"pendant les dernières {{count}} heures\",\n  \"forgot_password\": \"Mot de passe oublié ?\",\n  \"forgot_password_desc\": \"Veuillez suivre <0>ces quelques étapes</0> pour créer un nouveau mot de passe pour votre compte.\",\n  \"form_add_id\": \"Ajouter identifiant\",\n  \"form_answer\": \"Saisissez une adresse IP ou un nom de domaine\",\n  \"form_client_name\": \"Saisissez le nom du client\",\n  \"form_domain\": \"Saisissez un domaine ou caracrtère générique\",\n  \"form_enter_blocked_response_ttl\": \"Saisir le TTL de la réponse bloquée (secondes)\",\n  \"form_enter_host\": \"Saisissez un nom d’hôte\",\n  \"form_enter_hostname\": \"Saisissez un nom d'hôte\",\n  \"form_enter_id\": \"Entrer identifiant\",\n  \"form_enter_ip\": \"Saisissez l'IP\",\n  \"form_enter_mac\": \"Saisissez MAC\",\n  \"form_enter_rate_limit\": \"Entrez la limite de taux\",\n  \"form_enter_rate_limit_subnet_len\": \"Saisissez la longueur du préfixe de sous-réseau pour la limitation de débit\",\n  \"form_enter_subnet_ip\": \"Saisissez une adresse IP dans le sous-réseau « {{cidr}} »\",\n  \"form_enter_upstream_timeout\": \"Saisir le délai d'attente du serveur en amont en secondes\",\n  \"form_error_answer_format\": \"Format de réponse invalide\",\n  \"form_error_client_id_format\": \"L'ID du client ne doit contenir que des chiffres, des lettres minuscules et des traits d'union\",\n  \"form_error_domain_format\": \"Format de domaine invalide\",\n  \"form_error_equal\": \"Ne doit pas être égal\",\n  \"form_error_gateway_ip\": \"Le bail ne peut pas avoir d'adresse IP de la passerelle\",\n  \"form_error_ip4_format\": \"Adresse IPv4 invalide\",\n  \"form_error_ip4_gateway_format\": \"Adresse de passerelle IPv4 invalide\",\n  \"form_error_ip6_format\": \"Adresse IPv6 invalide\",\n  \"form_error_ip_format\": \"Adresse IP invalide\",\n  \"form_error_mac_format\": \"Adresse MAC invalide\",\n  \"form_error_password\": \"Mots de passe différents\",\n  \"form_error_password_length\": \"Le mot de passe doit comporter entre {{min}} et {{max}}  caractères\",\n  \"form_error_port\": \"Saisissez un numéro de port valide\",\n  \"form_error_port_range\": \"Saisissez une valeur de port entre 80 et 65535\",\n  \"form_error_port_unsafe\": \"Port non fiable\",\n  \"form_error_positive\": \"Doit être supérieur à 0\",\n  \"form_error_required\": \"Champ requis\",\n  \"form_error_server_name\": \"Nom de serveur invalide\",\n  \"form_error_subnet\": \"Le sous-réseau « {{cidr}} » ne contient pas l'adresse IP « {{ip}} »\",\n  \"form_error_url_format\": \"Format d’URL incorrect\",\n  \"form_error_url_or_path_format\": \"Lien URL soit chemin absolu de la liste invalide\",\n  \"form_select_tags\": \"Sélectionner les mots clés du client\",\n  \"found_in_known_domain_db\": \"Trouvé dans la base de données des domaines connus\",\n  \"friday\": \"Vendredi\",\n  \"friday_short\": \"Ven.\",\n  \"gateway_or_subnet_invalid\": \"Masque de sous-réseau invalide.\",\n  \"general_settings\": \"Paramètres généraux\",\n  \"general_statistics\": \"Statistiques générales\",\n  \"get_started\": \"C'est parti\",\n  \"greater_range_start_error\": \"Doit être supérieur au début de plage\",\n  \"homepage\": \"Page d'accueil\",\n  \"host_whitelisted\": \"L’hôte est autorisé\",\n  \"ignore_domains\": \"Domaines ignorés (séparés par une nouvelle ligne)\",\n  \"ignore_domains_desc_query\": \"Les requêtes correspondantes à ces règles ne sont pas écrites dans le journal des requêtes\",\n  \"ignore_domains_desc_stats\": \"Les requêtes correspondantes à ces règles ne sont pas écrites dans les statistiques\",\n  \"ignore_domains_title\": \"Domaines ignorés\",\n  \"ignore_query_log\": \"Ignorer ce client dans le journal des requêtes\",\n  \"ignore_statistics\": \"Ignorer ce client dans les statistiques\",\n  \"install_auth_confirm\": \"Confirmer le mot de passe\",\n  \"install_auth_desc\": \"C'est nécessaire de configurer l'authentification par aide de mot de passe pour accéder à l'interface web administrateur de votre AdGuard Home. Même si AdGuard Home n'est accessible que dans votre réseau local, c'est important d'y restreindre accès aux tiers.\",\n  \"install_auth_password\": \"Mot de passe\",\n  \"install_auth_password_enter\": \"Saisir un mot de passe\",\n  \"install_auth_title\": \"Authentification\",\n  \"install_auth_username\": \"Nom d'utilisateur\",\n  \"install_auth_username_enter\": \"Saisir un nom d'utilisateur\",\n  \"install_devices_address\": \"Le serveur DNS AdGuard Home écoute sur les adresses suivantes\",\n  \"install_devices_android_list_1\": \"Depuis l'écran d'accueil Android, appuyez sur Paramètres.\",\n  \"install_devices_android_list_2\": \"Appuyez sur Wi-Fi dans le menu. Tous les réseaux disponibles s'afficheront (il est impossible de définir des DNS personnalisés pour les connexions mobiles).\",\n  \"install_devices_android_list_3\": \"Faites un appui long sur le réseau auquel vous êtes connecté(e) et appuyez sur Modifier le réseau.\",\n  \"install_devices_android_list_4\": \"Sur certains appareils, vous avez parfois besoin de cocher la case Avancés pour davantage de paramètres. Pour ajuster vos paramètres DNS Android, vous devrez basculer les paramètres IP de DHCP à Statique.\",\n  \"install_devices_android_list_5\": \"Modifiez les valeurs DNS 1 et DNS 2 pour vos adresses de serveur AdGuard Home.\",\n  \"install_devices_desc\": \"Pour commencer à utiliser AdGuard Home, vous devez configurer vos appareils.\",\n  \"install_devices_ios_list_1\": \"Depuis l'écran d'accueil, appuyez sur Paramètres.\",\n  \"install_devices_ios_list_2\": \"Choisissez Wi-Fi dans le menu de gauche (il est impossible de configurer les DNS pour les réseaux mobiles).\",\n  \"install_devices_ios_list_3\": \"Appuyez sur le nom de votre réseau actuellement utilisé.\",\n  \"install_devices_ios_list_4\": \"Dans le champ DNS, saisissez votre adresse de serveur AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Cliquez sur l'icône Apple et allez dans les Préférences Système.\",\n  \"install_devices_macos_list_2\": \"Cliquez sur Réseau.\",\n  \"install_devices_macos_list_3\": \"Sélectionnez la première connexion dans votre liste et cliquez sur Avancés.\",\n  \"install_devices_macos_list_4\": \"Sélectionnez l'onglet DNS et saisissez votre adresse de serveur AdGuard.\",\n  \"install_devices_router\": \"Routeur\",\n  \"install_devices_router_desc\": \"Cette installation impactera automatiquement tous les appareils connectés à votre routeur et vous n'aurez pas besoin de configurer manuellement chaque appareil.\",\n  \"install_devices_router_list_1\": \"Ouvrez les préférences de votre routeur. Normalement vous pouvez y accéder depuis votre navigateur web via une URL du type http://192.168.0.1/ ou http://192.168.1.1/. Vous devrez peut-être renseigner le mot de passe. Si vous ne vous en rappelez pas, vous pouvez normalement le réinitialiser en appuyant sur le bouton du routeur, mais attention : si vous choisissez cette procédure, toute la configuration de votre routeur sera probablement perdue. Si votre routeur requière une application spécifique, installez-la sur votre ordinateur/téléphone et utilisez-la pour accéder aux paramètres du routeur.\",\n  \"install_devices_router_list_2\": \"Trouvez les paramètres DHCP/DNS. Recherchez les lettres DNS près d'une zone qui permet la saisie de 2 ou 3 blocs de chiffres, chacun composé de 4 parties de 1 à 3 chiffres.\",\n  \"install_devices_router_list_3\": \"Saisissez vos adresses de serveur AdGuard Home ici.\",\n  \"install_devices_router_list_4\": \"Vous ne pouvez pas définir un serveur DNS personnalisé sur certains types de routeurs. Dans ce cas, la configuration de AdGuard Home en tant que <0>serveur DHCP</0> peut aider. Sinon, vous devez rechercher le manuel sur la façon de personnaliser les serveurs DNS pour votre modèle de routeur particulier.\",\n  \"install_devices_title\": \"Configurer vos appareils\",\n  \"install_devices_windows_list_1\": \"Ouvrez votre Panneau de configuration depuis le menu Démarrer ou la recherche Windows.\",\n  \"install_devices_windows_list_2\": \"Allez dans la catégorie Réseau et Internet et ensuite dans le Centre Réseau et Partage.\",\n  \"install_devices_windows_list_3\": \"Cliquez « Modifier les paramètres de l'adaptateur » sur le panneau à gauche.\",\n  \"install_devices_windows_list_4\": \"Cliquez avec le bouton droit de la souris sur votre connexion active et sélectionnez Propriétés.\",\n  \"install_devices_windows_list_5\": \"Recherchez « Protocole Internet Version 4 (TCP/IPv4) » (soit, pour IPv6, « Protocole Internet Version 6 (TCP/IPv6) ») dans la liste, sélectionnez-la puis cliquez à nouveau sur Propriétés.\",\n  \"install_devices_windows_list_6\": \"Sélectionnez « Utiliser l’adresse de serveur DNS suivante » et saisissez votre adresse de serveur AdGuard Home.\",\n  \"install_saved\": \"Enregistré avec succès\",\n  \"install_settings_all_interfaces\": \"Toutes les interfaces\",\n  \"install_settings_dns\": \"Serveur DNS\",\n  \"install_settings_dns_desc\": \"Vous devrez configurer vos appareils et votre routeur pour utiliser le serveur DNS sur les adresses suivantes :\",\n  \"install_settings_interface_link\": \"Votre interface web administrateur AdGuard Home sera disponible sur les adresses suivantes :\",\n  \"install_settings_listen\": \"Interface d'écoute\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Interface web administrateur\",\n  \"install_static_configure\": \"AdGuard Home a détecté qu’une adresse IP dynamique est utilisée — <0>{{ip}}</0>. Souhaitez-vous l’utiliser comme votre adresse statique ?\",\n  \"install_static_error\": \"AdGuard Home ne peut pas le configurer automatiquement pour cette interface réseau. Veuillez rechercher une instruction sur la façon de procéder manuellement.\",\n  \"install_static_ok\": \"Bonne nouvelle ! L’adresse IP statique est déjà configurée\",\n  \"install_step\": \"Étape\",\n  \"install_submit_desc\": \"La procédure d'installation est terminée et vous êtes prêt(e) à utiliser AdGuard Home.\",\n  \"install_submit_title\": \"Félicitations !\",\n  \"install_welcome_desc\": \"AdGuard Home est un seveur DNS pour bloquer les pubs et traceurs sur tout un réseau. Son but est de vous donner le contrôle sur l'ensemble de votre réseau et tous vos appareils sans programme côté client supplémentaire.\",\n  \"install_welcome_title\": \"Bienvenue sur AdGuard Home !\",\n  \"interval_24_hour\": \"24 heures\",\n  \"interval_6_hour\": \"6 heures\",\n  \"interval_days\": \"{{count}} jour\",\n  \"interval_days_plural\": \"{{count}} jours\",\n  \"interval_hours\": \"{{count}} heure\",\n  \"interval_hours_plural\": \"{{count}} heures\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Adresse IP\",\n  \"known_tracker\": \"Traqueur connu\",\n  \"last_rule_in_allowlist\": \"Impossible d’interdire ce client, car l’exclusion de la règle « {{disallowed_rule}} » DÉSACTIVERA la liste des « clients autorisés ».\",\n  \"last_time_updated_table_header\": \"Dernière mise à jour\",\n  \"list_confirm_delete\": \"Voulez-vous vraiment supprimer cette liste ?\",\n  \"list_label\": \"Liste\",\n  \"list_updated\": \"{{count}} liste mise à jour\",\n  \"list_updated_plural\": \"{{count}} listes mises à jour\",\n  \"list_url_table_header\": \"URL de la liste\",\n  \"load_balancing\": \"Équilibrage de charge\",\n  \"load_balancing_desc\": \"Une requête par serveur en amont à la fois.<br/>AdGuard Home utilise un algorithme aléatoire pondéré pour sélectionner les serveurs avec le plus petit nombre d'échecs de recherche et le temps de recherche moyen le plus bas.\",\n  \"loading_table_status\": \"Chargement en cours ...\",\n  \"local_ptr_default_resolver\": \"Par défaut, AdGuard Home utilise les résolveurs DNS inversés suivants : {{ip}}.\",\n  \"local_ptr_desc\": \"Les serveurs DNS utilisés par AdGuard Home pour les requêtes privées PTR, SOA et NS. Une requête est considérée privée si elle demande un domaine ARPA contenant un sous-réseau entre les plages IP privées (par exemple « 192.168.12.34 ») et provient d'un client avec une adresse IP privée. Sans réglages additionnels, les résolveurs DNS par défaut de votre système d'exploitation seront utilisés, sauf pour les adresses IP d'AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home n'a pas pu déterminer de résolveurs DNS inversés privés appropriés pour ce système.\",\n  \"local_ptr_placeholder\": \"Saisissez une adresse IP par ligne\",\n  \"local_ptr_title\": \"Serveurs DNS privés inverses\",\n  \"location\": \"Localisation\",\n  \"log_and_stats_section_label\": \"Journal des requêtes et statistiques\",\n  \"lower_range_start_error\": \"Doit être inférieur au début de plage\",\n  \"main_settings\": \"Paramètres principaux\",\n  \"make_static\": \"Rendre statique\",\n  \"manual_update\": \"Veuillez <a>suivre ces étapes</a> pour mettre à jour manuellement.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Lundi\",\n  \"monday_short\": \"Lun.\",\n  \"name\": \"Nom\",\n  \"name_table_header\": \"Nom\",\n  \"netname\": \"Nom du réseau\",\n  \"network\": \"Réseau\",\n  \"new_allowlist\": \"Nouvelle liste d’autorisation\",\n  \"new_blocklist\": \"Nouvelle liste de blocage\",\n  \"next\": \"Suivant\",\n  \"next_btn\": \"Suivant\",\n  \"no_blocklist_added\": \"Aucune liste de blocage ajoutée\",\n  \"no_clients_found\": \"Pas de clients trouvés\",\n  \"no_domains_found\": \"Pas de domaines trouvés\",\n  \"no_logs_found\": \"Aucun journal trouvé\",\n  \"no_servers_specified\": \"Pas de serveurs spécifiés\",\n  \"no_upstreams_data_found\": \"Aucune donnée en amont trouvée\",\n  \"no_whitelist_added\": \"Aucune liste d’autorisation ajoutée\",\n  \"nothing_found\": \"Rien n'a été trouvé\",\n  \"null_ip\": \"IP nulle\",\n  \"number_of_dns_query_blocked_24_hours\": \"Le nombre de requêtes DNS bloquées par les filtres adblock et les listes de blocage des hôtes\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Le nombre de sites à contenu adulte bloqués\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Le nombre de requêtes DNS bloquées par le module Sécurité de navigation d'AdGuard\",\n  \"number_of_dns_query_days\": \"Le nombre de requêtes DNS traitées pour les {{count}} derniers jours\",\n  \"number_of_dns_query_days_plural\": \"Le nombre de requêtes DNS traitées ces {{count}} derniers jours\",\n  \"number_of_dns_query_hours\": \"Le nombre de requêtes DNS traitées pendant la dernière {{count}} heure\",\n  \"number_of_dns_query_hours_plural\": \"Le nombre de requêtes DNS traitées pendant les dernières {{count}} heures\",\n  \"number_of_dns_query_to_safe_search\": \"Le nombre de requêtes DNS faites avec la Recherche securisée\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"Désactivé\",\n  \"on\": \"Activé\",\n  \"open_dashboard\": \"Ouvrir le Tableau de bord\",\n  \"orgname\": \"Nom de l'organisation\",\n  \"original_response\": \"Réponse originale\",\n  \"out_of_range_error\": \"Doit être hors plage « {{start}} » - « {{end}} »\",\n  \"page_table_footer_text\": \"Page\",\n  \"parallel_requests\": \"Requêtes en parallèle\",\n  \"parental_control\": \"Contrôle Parental\",\n  \"password_label\": \"Mot de passe\",\n  \"password_placeholder\": \"Saisir un mot de passe\",\n  \"plain_dns\": \"DNS brut\",\n  \"port_53_faq_link\": \"Le port 53 est souvent occupé par les services « DNSStubListener » ou « systemd-resolved ». Veuillez lire <0>cette instruction</0> pour savoir comment résoudre ce problème.\",\n  \"previous_btn\": \"Précédent\",\n  \"privacy_policy\": \"Politique de confidentialité\",\n  \"processing_update\": \"Veuillez patienter, AdGuard Home est en cours de mise à jour\",\n  \"protection_section_label\": \"Protection\",\n  \"protocol\": \"Protocole\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Journal des requêtes\",\n  \"query_log_clear\": \"Effacer journal des requêtes\",\n  \"query_log_cleared\": \"Le journal des requêtes a été effacé\",\n  \"query_log_configuration\": \"Configuration du journal\",\n  \"query_log_confirm_clear\": \"Êtes-vous sûr de vouloir effacer tout le journal des requêtes ?\",\n  \"query_log_disabled\": \"Le journal des requêtes est désactivé et peut être configuré dans les <0>paramètres</0>\",\n  \"query_log_enable\": \"Activer le journal\",\n  \"query_log_filtered\": \"Filtré par {{filter}}\",\n  \"query_log_response_status\": \"Statut : {{value}}\",\n  \"query_log_retention\": \"Rotation des journaux de requêtes\",\n  \"query_log_retention_confirm\": \"Êtes-vous sûr de souhaiter modifier la rotation des journaux de requêtes ? Si vous diminuez la valeur de l'intervalle, certaines données seront perdues\",\n  \"query_log_strict_search\": \"Utilisez les doubles guillemets pour une recherche stricte\",\n  \"query_log_updated\": \"Le journal des requêtes a été mis à jour\",\n  \"rate_limit\": \"Limite de taux\",\n  \"rate_limit_desc\": \"Le nombre de requêtes par seconde qu’un seul client est autorisé à faire. Le réglage 0 fait illimité.\",\n  \"rate_limit_subnet_len_ipv4\": \"Longueur du préfixe de sous-réseau pour les adresses IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Longueur du préfixe de sous-réseau pour les adresses IPv4 utilisé pour la limitation de vitesse. La valeur par défaut est 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"La longueur du préfixe du sous-réseau IPv4 doit être entre 0 et 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Longueur du préfixe de sous-réseau pour les adresses IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Longueur du préfixe de sous-réseau pour les adresses IPv6 utilisé pour la limitation de débit. La valeur par défaut est 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"La longueur du préfixe du sous-réseau IPv6 doit être entre 0 et 128\",\n  \"rate_limit_whitelist\": \"Liste d'autorisation de limitation de débit\",\n  \"rate_limit_whitelist_desc\": \"Adresses IP exclues de la limitation du débit\",\n  \"rate_limit_whitelist_placeholder\": \"Saisissez une adresse IP par ligne\",\n  \"refresh_btn\": \"Actualiser\",\n  \"refresh_statics\": \"Actualiser les statistiques\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Signaler un problème\",\n  \"request_details\": \"Demander des détails\",\n  \"request_table_header\": \"Requête\",\n  \"requests_count\": \"Nombre de requêtes\",\n  \"reset_settings\": \"Réinitialiser les paramètres\",\n  \"resolve_clients_desc\": \"Résoudre inversement les adresses IP des clients en leurs noms d'hôtes en envoyant des requêtes PTR aux résolveurs correspondants (serveurs DNS privés pour les clients locaux, serveurs en amont pour les clients ayant une adresse IP publique).\",\n  \"resolve_clients_title\": \"Activer la résolution inverse des adresses IP des clients\",\n  \"response_code\": \"Code de réponse\",\n  \"response_details\": \"Détails de la réponse\",\n  \"response_table_header\": \"Réponse\",\n  \"response_time\": \"Temps de réponse\",\n  \"rewrite_A\": \"<0>A</0> : valeur spéciale, conserver les enregistrements <0>A</0> de l’amont\",\n  \"rewrite_AAAA\": \"<0>AAAA</0> : valeur spéciale, conserver les enregistrements <0>AAAA</0> de l’amont\",\n  \"rewrite_add\": \"Ajouter une réécriture DNS\",\n  \"rewrite_added\": \"Réécriture DNS pour « {{key}} » ajoutée\",\n  \"rewrite_applied\": \"Règle de réécriture appliquée\",\n  \"rewrite_confirm_delete\": \"Voulez-vous vraiment supprimer la réécriture DNS pour « {{key}} » ?\",\n  \"rewrite_deleted\": \"Réécriture DNS pour « {{key}} » supprimée\",\n  \"rewrite_desc\": \"Permet de configurer facilement la réponse DNS personnalisée pour un nom de domaine spécifique.\",\n  \"rewrite_domain_name\": \"Nom de domaine : ajouter un enregistrement CNAME\",\n  \"rewrite_edit\": \"Modifier la réécriture DNS\",\n  \"rewrite_hosts_applied\": \"Réécrit par la règle du fichier hosts\",\n  \"rewrite_ip_address\": \"Adresse IP : utilisez cette IP dans une réponse A ou AAAA\",\n  \"rewrite_not_found\": \"Aucune réécriture DNS trouvée\",\n  \"rewrite_settings_updated\": \"Les paramètres de réécriture DNS ont été mis à jour avec succès\",\n  \"rewrite_updated\": \"Réécriture DNS mise à jour\",\n  \"rewrites_disabled_table_header\": \"Les réécritures sont désactivées\",\n  \"rewrites_enabled_table_header\": \"Les réécritures sont activées\",\n  \"rewritten\": \"Réécrit\",\n  \"rows_table_footer_text\": \"lignes\",\n  \"rule_added_to_custom_filtering_toast\": \"Règle ajoutée aux règles d'utilisateur : {{rule}}\",\n  \"rule_label\": \"Règle(s)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Règle retirée des règles d'utilisateur : {{rule}}\",\n  \"rules_count_table_header\": \"Nombre des règles\",\n  \"safe_browsing\": \"Navigation sécurisée\",\n  \"safe_search\": \"Recherche Sécurisée\",\n  \"saturday\": \"Samedi\",\n  \"saturday_short\": \"Sam.\",\n  \"save_btn\": \"Enregistrer\",\n  \"save_config\": \"Sauvegarder la configuration\",\n  \"schedule_add\": \"Ajouter un horaire\",\n  \"schedule_current_timezone\": \"Fuseau horaire actuel : {{value}}\",\n  \"schedule_desc\": \"Définissez les périodes d'inactivité pour les services bloqués\",\n  \"schedule_edit\": \"Modifier l'horaire\",\n  \"schedule_from\": \"Du\",\n  \"schedule_invalid_select\": \"Le temps du début doit précéder le temps de la fin\",\n  \"schedule_modal_description\": \"Cet horaire remplacera tout autre planning existant pour le même jour de la semaine. Chaque jour de la semaine ne peut avoir qu'une seule période d'inactivité.\",\n  \"schedule_modal_time_off\": \"Suspendre le blocage :\",\n  \"schedule_new\": \"Nouvel horaire\",\n  \"schedule_remove\": \"Supprimer l'horaire\",\n  \"schedule_save\": \"Enregistrer l'horaire\",\n  \"schedule_select_days\": \"Sélectionnez les jours\",\n  \"schedule_services\": \"Suspendre le blocage des services\",\n  \"schedule_services_desc\": \"Configurez l'horaire de pauses du filtre de blocage de services\",\n  \"schedule_services_desc_client\": \"Configurez l'horaire de pauses du filtre de blocage de services pour ce client\",\n  \"schedule_time_all_day\": \"Toute la journée\",\n  \"schedule_timezone\": \"Sélectionnez un fuseau horaire\",\n  \"schedule_to\": \"Au\",\n  \"served_from_cache_label\": \"Servi depuis le cache\",\n  \"service_name\": \"Nom du service\",\n  \"set_static_ip\": \"Définir une adresse IP statique\",\n  \"settings\": \"Paramètres\",\n  \"settings_custom\": \"Personnalisé\",\n  \"settings_global\": \"Général\",\n  \"setup_config_to_enable_dhcp_server\": \"Configurer les paramètres pour activer le serveur DHCP\",\n  \"setup_dns_notice\": \"Pour utiliser le <1>DNS-over-HTTPS</1> ou le <1>DNS-over-TLS</1>, vous devez <0>configurer le Chiffrement</0> dans les paramètres de AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS :</0> Utiliser le string <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS :</0> Utiliser le string <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Voici une liste de logiciels que vous pouvez utiliser.</0>\",\n  \"setup_dns_privacy_4\": \"Si vous utilisez un appareil sur iOS 14 ou macOS Big Sur, vous pouvez télécharger un fichier spécial '.mobileconfig' pour ajouter les serveurs <highlight>DNS-sur-HTTPS</highlight> ou <highlight>DNS-sur-TLS</highlight> aux configurations DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 supporte nativement le DNS-over-TLS. Pour le configurer, allez dans Paramètres → Réseau et Internet → Avancés → DNS privé et saisissez votre nom de domaine ici.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard pour Android</0> supporte le <1>DNS-over-HTTPS</1> et le <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> ajoute le support <1>DNS-over-HTTPS</1> sur Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Configuration sur iOS et macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> supporte le <1>DNS-over-HTTPS</1> mais pour le configurer pour utiliser votre propre serveur, vous devrez générer un <2>DNS Stamp</2> pour lui.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard pour iOS</0> supporte les configurations <1>DNS-over-HTTPS</1> et <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home lui-même peut être un client DNS sécurisé sur n'importe quelle plateforme.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> supporte tous les protocoles DNS sécurisés connus.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> supporte le <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> supporte le <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Vous trouverez plus d'implémentations <0>ici</0> et <1>ici</1>.\",\n  \"setup_dns_privacy_other_title\": \"Autres implémentations\",\n  \"setup_guide\": \"Guide d'installation\",\n  \"show_all_filter_type\": \"Montrer tout\",\n  \"show_blocked_responses\": \"Bloqué\",\n  \"show_filtered_type\": \"Montrer les sites filtrés\",\n  \"show_processed_responses\": \"Traité\",\n  \"show_whitelisted_responses\": \"Autorisée\",\n  \"sign_in\": \"Connexion\",\n  \"sign_out\": \"Déconnexion\",\n  \"source_label\": \"Source\",\n  \"static_ip\": \"Adresse IP statique\",\n  \"static_ip_desc\": \"AdGuard Home est un serveur, il a donc besoin d’une adresse IP statique pour fonctionner correctement. Autrement, à un moment donné, votre routeur pourrait attribuer une adresse IP différente à cet appareil.\",\n  \"statistics_clear\": \" Effacer les statistiques\",\n  \"statistics_clear_confirm\": \"Voulez-vous vraiment effacer les statistiques ?\",\n  \"statistics_cleared\": \"Statistiques effacées\",\n  \"statistics_configuration\": \"Configuration des statistiques\",\n  \"statistics_enable\": \"Activer les statistiques\",\n  \"statistics_retention\": \"Maintien des statistiques\",\n  \"statistics_retention_confirm\": \"Êtes-vous sûr de vouloir modifier le maintien des statistiques ? Si vous diminuez la valeur de l'intervalle, certaines données seront perdues\",\n  \"statistics_retention_desc\": \"Si vous baissez la valeur de l'intervalle, des données seront perdues\",\n  \"stats_adult\": \"Sites à contenu adulte bloqués\",\n  \"stats_disabled\": \"Les statistiques ont été désactivées. Vous pouvez l'activer à partir de la <0>page des paramètres</0>.\",\n  \"stats_disabled_short\": \"Les statistiques ont été désactivées\",\n  \"stats_malware_phishing\": \"Tentative de malware/hameçonnage bloquée\",\n  \"stats_params\": \"Configuration des statistiques\",\n  \"stats_query_domain\": \"Domaines les plus recherchés\",\n  \"subnet_error\": \"Les adresses doivent être dans le même sous-réseau\",\n  \"sunday\": \"Dimanche\",\n  \"sunday_short\": \"Dim.\",\n  \"system_host_files\": \"Fichier d'hôtes système\",\n  \"table_client\": \"Client\",\n  \"table_name\": \"Nom\",\n  \"tags_desc\": \"Vous pouvez sélectionner les mots clés qui correspondent au client. Les mots clés peuvent être inclus dans les règles de filtrage et vous permettent de les appliquer plus précisément. <0>En savoir plus</0>.\",\n  \"tags_title\": \"Mots clés\",\n  \"test_upstream_btn\": \"Tester les upstreams\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Auto (en fonction de la palette de couleurs de votre appareil)\",\n  \"theme_dark\": \"Thème sombre\",\n  \"theme_dark_desc\": \"Thème sombre\",\n  \"theme_light\": \"Thème clair\",\n  \"theme_light_desc\": \"Thème clair\",\n  \"thursday\": \"Jeudi\",\n  \"thursday_short\": \"Jeu.\",\n  \"time_table_header\": \"Temps\",\n  \"top_blocked_domains\": \"Les domaines les plus fréquemment bloqués\",\n  \"top_clients\": \"Meilleurs clients\",\n  \"top_upstreams\": \"Top amonts\",\n  \"topline_expired_certificate\": \"Votre certificat SSL a expiré. Mettez à jour vos <0>Paramètres de chiffrement</0>.\",\n  \"topline_expiring_certificate\": \"Votre certificat SSL est sur le point d'expirer. Mettez à jour vos <0>Paramètres de chiffrement</0>.\",\n  \"tracker_source\": \"Source du traceur\",\n  \"try_again\": \"Réessayer\",\n  \"ttl_cache_validation\": \"La valeur TTL minimale du cache doit être inférieure ou égale à la valeur maximale\",\n  \"tuesday\": \"Mardi\",\n  \"tuesday_short\": \"Mar.\",\n  \"type_table_header\": \"Type\",\n  \"unavailable_dhcp\": \"Le DHCP n’est pas disponible\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home ne peut pas exécuter un serveur DHCP sur votre système d’exploitation\",\n  \"unblock\": \"Débloquer\",\n  \"unblock_all\": \"Tout débloquer\",\n  \"unblock_for_this_client_only\": \"Débloquer uniquement pour ce client\",\n  \"unknown_filter\": \"Filtre inconnu {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} est disponible ! <0>Cliquez ici</0> pour plus d'informations.\",\n  \"update_failed\": \"Échec de la mise à jour automatique. Veuillez <a>suivre ces étapes</a> pour mettre à jour manuellement.\",\n  \"update_now\": \"Mettre à jour maintenant\",\n  \"updated_custom_filtering_toast\": \"Règles d'utilisateur enregistrées\",\n  \"updated_save_search_toast\": \"Les paramètres de Recherche sécurisée sont mis à jour\",\n  \"updated_upstream_dns_toast\": \"Serveurs en amont enregistrés\",\n  \"updates_checked\": \"Une nouvelle version de AdGuard Home est disponible\",\n  \"updates_version_equal\": \"AdGuard Home est à jour\",\n  \"upstream\": \"Amont\",\n  \"upstream_dns\": \"Serveurs DNS upstream\",\n  \"upstream_dns_cache_configuration\": \"Configuration du cache DNS en amont\",\n  \"upstream_dns_client_desc\": \"Si vous laissez ce champ vide, AdGuard Home utilisera les serveurs configurés dans les <0>Paramètres DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Configuré dans {{path}}\",\n  \"upstream_dns_help\": \"Saisissez les adresses des serveurs, une par ligne. <a>En savoir plus</a> sur la configuration des serveurs DNS en amont.\",\n  \"upstream_parallel\": \"Utilisez des requêtes parallèles pour accélérer la résolution en requêtant simultanément tous les serveurs en amont.\",\n  \"upstream_timeout\": \"Délai d'attente en amont\",\n  \"upstream_timeout_desc\": \"Spécifie le nombre de secondes à attendre pour une réponse du serveur en amont\",\n  \"upstreams\": \"En amont\",\n  \"use_adguard_browsing_sec\": \"Utilisez le service Sécurité de navigation d'AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home vérifiera si le domaine est bloqué par le service web de sécurité de la navigation. Il utilisera une API de recherche respectueuse de la vie privée pour effectuer la vérification : seul un préfixe court du hachage SHA256 du nom de domaine est envoyé au serveur.\",\n  \"use_adguard_parental\": \"Utiliser le contrôle parental d'AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home va vérifier s'il y a du contenu pour adultes sur le domaine. Ce sera fait par aide du même API discret que celui utilisé par le service de Sécurité de navigation.\",\n  \"use_private_ptr_resolvers_desc\": \"Résolvez les requêtes PTR, SOA et NS pour les domaines ARPA contenant des adresses IP privées par aide des serveurs privés en amont, DHCP, /etc/hosts, etc. S'il est désactivé, AdGuard Home répondra à toutes ces requêtes avec NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Utiliser des résolveurs DNS inversés privés\",\n  \"use_saved_key\": \"Utiliser la clef précédemment enregistrée\",\n  \"username_label\": \"Nom d'utilisateur\",\n  \"username_placeholder\": \"Saisir un nom d'utilisateur\",\n  \"validated_with_dnssec\": \"Validé avec DNSSEC\",\n  \"version\": \"version\",\n  \"version_request_error\": \"Impossible de vérifier les mises à jour. Veuillez vérifier votre connexion internet.\",\n  \"wednesday\": \"Mercredi\",\n  \"wednesday_short\": \"Mer.\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/hr.json",
    "content": "{\n  \"access_allowed_desc\": \"Popis CIDR-ova, IP adresa ili <a>ClientIDs</a>. Ako ovaj popis ima unose, AdGuard Home prihvatit će zahtjeve samo tih klijenata.\",\n  \"access_allowed_title\": \"Dopušteni klijenti\",\n  \"access_blocked_desc\": \"Ne smije se miješati s filterima. AdGuard Home ispušta DNS upite koji odgovaraju tim domenama, a ti se upiti čak i ne pojavljuju u zapisniku upita. Možete navesti točne nazive domena, zamjenske znakove ili pravila filtriranja URL-a, npr || example.org example.org. example.org^\\\" u skladu s tim.\",\n  \"access_blocked_title\": \"Nedopuštene domene\",\n  \"access_desc\": \"Ovdje možete konfigurirati pravila pristupa za AdGuard Home DNS poslužitelj\",\n  \"access_disallowed_desc\": \"Popis CIDR-ova, IP adresa ili <a>ClientIDs</a>. Ako ovaj popis ima unose, AdGuard Home će odbaciti zahtjeve tih klijenata. Ovo polje se zanemaruje ako postoje unosi u Dopušteni klijenti.\",\n  \"access_disallowed_title\": \"Nedopušteni klijenti\",\n  \"access_settings_saved\": \"Postavke pristupa su uspješno spremljene\",\n  \"access_title\": \"Postavke pristupa\",\n  \"actions_table_header\": \"Radnje\",\n  \"add_allowlist\": \"Dodaj popis dopuštenih\",\n  \"add_blocklist\": \"Dodaj popis nedopuštenih\",\n  \"add_custom_list\": \"Dodajte prilagođeni popis\",\n  \"add_persistent_client\": \"Dodaj u spremljene klijente\",\n  \"address\": \"Adresa\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home odbaciti će sve DNS upite od ovog klijenta.\",\n  \"all_lists_up_to_date_toast\": \"Svi popisi su ažurirani\",\n  \"all_queries\": \"Svi upiti\",\n  \"allow_this_client\": \"Omogući ovog klijenta\",\n  \"allowed\": \"Dopušteno\",\n  \"anonymize_client_ip\": \"Anonimiraj IP klijenta\",\n  \"anonymize_client_ip_desc\": \"Ne spremajte cijelu IP adresu klijenta u zapisnike i statistike\",\n  \"anonymizer_notification\": \"<0>Napomena:</0>IP anonimizacija je omogućena. Možete ju onemogućiti u <1>općim postavkama</1>.\",\n  \"answer\": \"Odgovor\",\n  \"apply_btn\": \"Primijeni\",\n  \"auto_clients_desc\": \"Informacije o IP adresama uređaja koji koriste ili bi mogli koristiti AdGuard Home. Ove informacije prikupljaju se iz nekoliko izvora, uključujući datoteke hostova, obrnuti DNS itd.\",\n  \"auto_clients_title\": \"Runtime klijenti\",\n  \"autofix_warning_list\": \"Izvodi sljedeće radnje: <0>Deaktiviraj DNSStubListener sustav</0> <0>Postavi adresu DNS poslužitelja na 127.0.0.1</0> <0>Zamijeni simbolički cilj veze iz /etc/resolv.conf u /run/systemd/resolve/resolv.conf</0> <0>Zaustavi DNSStubListener (ponovno pokreni systemd-resolved uslugu)</0>\",\n  \"autofix_warning_result\": \"Kao rezultat toga, sve DNS zahtjeve iz vašeg sustava će AdGuard Home obraditi prema zadanim postavkama.\",\n  \"autofix_warning_text\": \"Ako pritisnete \\\"Popravi\\\", AdGuard Home će postaviti vaš sustav da koristi AdGuardHome DNS poslužitelj.\",\n  \"average_processing_time\": \"Prosječno vrijeme obrade\",\n  \"average_processing_time_hint\": \"Prosječno vrijeme u milisekundama za obradu DNS zahtjeva\",\n  \"average_upstream_response_time\": \"Prosječno vrijeme odziva upstream poslužitelja\",\n  \"back\": \"Natrag\",\n  \"block\": \"Blokiraj\",\n  \"block_all\": \"Blokiraj sve\",\n  \"block_domain_use_filters_and_hosts\": \"Blokiraj domene koristeći filtre ili hosts datoteke\",\n  \"block_for_this_client_only\": \"Blokiraj samo za ovog klijenta\",\n  \"block_services\": \"Blokiraj specifične usluge\",\n  \"blocked_adult_websites\": \"Blokirano Roditeljskom kontrolom\",\n  \"blocked_by\": \"<0>Blokirano filtrima</0>\",\n  \"blocked_by_cname_or_ip\": \"Blokirao CNAME ili IP\",\n  \"blocked_by_response\": \"Blokirano od strane CNAME-a ili IP-a u odgovoru\",\n  \"blocked_response_ttl\": \"TTL blokiranog odgovora\",\n  \"blocked_response_ttl_desc\": \"Određuje koliko sekundi bi klijenti trebali keširati filtrirani odgovor\",\n  \"blocked_safebrowsing\": \"Blokirano s Sigurnom pretragom\",\n  \"blocked_service\": \"Blokirane usluge\",\n  \"blocked_services\": \"Blokirane usluge\",\n  \"blocked_services_desc\": \"Omogućuje brzo blokiranje popularnih stranica i usluga.\",\n  \"blocked_services_global\": \"Koristi globalno blokirane usluge\",\n  \"blocked_services_saved\": \"Blokirane usluge su uspješno spremljene\",\n  \"blocked_threats\": \"Blokirane prijetnje\",\n  \"blocking_ipv4\": \"Blokiranje IPv4\",\n  \"blocking_ipv4_desc\": \"Povratna IP adresa za blokirane A zahtjeve\",\n  \"blocking_ipv6\": \"Blokiranje IPv6\",\n  \"blocking_ipv6_desc\": \"Povratna IP adresa za blokirane AAAA zahtjeve\",\n  \"blocking_mode\": \"Način blokiranja\",\n  \"blocking_mode_custom_ip\": \"Prilagođeni IP: Odgovor s ručno postavljenom IP adresom\",\n  \"blocking_mode_default\": \"Zadano: Odgovori s nultom IP adresom (0.0.0.0 za A; :: za AAAA) kada ga blokira Adblock slično pravilo; odgovorite s IP adresom definiranom u pravilu kada je blokirano od /etc/hosts sličnog pravila\",\n  \"blocking_mode_null_ip\": \"Nuliran IP: Odgovor s nuliranom IP adresom (0.0.0.0 za A; :: za AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Odgovor s NXDOMAIN kôdom\",\n  \"blocking_mode_refused\": \"REFUSED: Odgovorite s REFUSED kôdom\",\n  \"blocklist\": \"Popis nedopuštenih\",\n  \"bootstrap_dns\": \"Bootstrap DNS poslužitelji\",\n  \"bootstrap_dns_desc\": \"IP adrese DNS poslužitelja koji se koriste za rješavanje IP adresa DoH/DoT razrjeđivača koje navedete kao uzvodne. Komentari nisu dopušteni.\",\n  \"cache_cleared\": \"DNS predmemorija je uspješno izbrisana\",\n  \"cache_enabled\": \"Omogući predmemoriju\",\n  \"cache_enabled_desc\": \"Pohrani DNS Odgovore lokalno.\",\n  \"cache_optimistic\": \"Optimistično predmemoriranje\",\n  \"cache_optimistic_desc\": \"Učinite da AdGuard Home reagira iz predmemorije čak i kada su unosi istekli i pokušajte ih osvježiti.\",\n  \"cache_size\": \"Veličina predmemorije\",\n  \"cache_size_desc\": \"Veličina DNS cachea (u bajtovima).\",\n  \"cache_size_validation\": \"Veličina predmemorije mora biti veća od nule kada je omogućena.\",\n  \"cache_ttl_max_override\": \"Nadjačaj maksimum TTL-a\",\n  \"cache_ttl_max_override_desc\": \"Postavite maksimalnu vrijednost TTL-a (u sekundama) za zapise u DNS predmemoriju.\",\n  \"cache_ttl_min_override\": \"Nadjačaj minimum TTL-a\",\n  \"cache_ttl_min_override_desc\": \"Povećajte kratke vrijednosti TTL-a (u sekundama) primljene od upstream poslužitelja prilikom predmemoriranja DNS odgovora.\",\n  \"cancel_btn\": \"Poništi\",\n  \"category_label\": \"Kategorija\",\n  \"check\": \"Provjeri\",\n  \"check_client_id\": \"Identifikator klijenta (ClientID ili IP adresa)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Provjerite je li naziv hosta filtriran.\",\n  \"check_dhcp_servers\": \"Provjera DHCP poslužitelja\",\n  \"check_dns_record\": \"Odaberite tip DNS zapisa\",\n  \"check_enter_client_id\": \"Unesite identifikator klijenta\",\n  \"check_hostname\": \"Naziv poslužitelja ili naziv domene\",\n  \"check_ip\": \"IP adrese: {{ip}}\",\n  \"check_not_found\": \"Nije pronađeno na vašoj listi filtara\",\n  \"check_reason\": \"Razlog: {{reason}}\",\n  \"check_service\": \"Naziv usluge: {{service}}\",\n  \"check_title\": \"Provjerite filtriranje\",\n  \"check_updates_btn\": \"Provjeri ažuriranja\",\n  \"check_updates_now\": \"Provjeri ažuriranja sada\",\n  \"choose_allowlist\": \"Odaberite popis dopuštenih\",\n  \"choose_blocklist\": \"Odaberite popis nedopuštenih\",\n  \"choose_from_list\": \"Odaberite s popisa\",\n  \"city\": \"Grad\",\n  \"clear_cache\": \"Očisti predmemoriju\",\n  \"click_to_view_queries\": \"Kliknite za pregled upita\",\n  \"client_add\": \"Dodaj klijenta\",\n  \"client_added\": \"Klijent \\\"{{key}}\\\" je uspješno dodan\",\n  \"client_blocked\": \"Klijent \\\"{{ip}}\\\" je uspješno blokiran\",\n  \"client_confirm_block\": \"Jeste li sigurni da želite blokirati \\\"{{ip}}\\\" klijenta?\",\n  \"client_confirm_delete\": \"Jeste li sigurni da želite ukloniti \\\"{{key}}\\\" klijenta?\",\n  \"client_confirm_unblock\": \"Jeste li sigurni da želite odblokirati \\\"{{ip}}\\\" klijenta?\",\n  \"client_deleted\": \"Klijent \\\"{{key}}\\\" je uspješno uklonjen\",\n  \"client_details\": \"Detalji o klijentu\",\n  \"client_edit\": \"Uredi klijenta\",\n  \"client_global_settings\": \"Koristi globalne postavke\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Različiti klijenti mogu se prepoznati pomoću posebnog ClientID. <a>Ovdje</a> možete saznati više o tome kako prepoznati klijente.\",\n  \"client_id_placeholder\": \"Unesite ClientID\",\n  \"client_identifier\": \"Identifikator\",\n  \"client_identifier_desc\": \"Klijenti se mogu identificirati putem IP adrese, CIDR-a, MAC adrese ili posebnog ClientID (može se koristiti za DoT/DoH/DoQ). <0>Ovdje</0> možete saznati više o tome kako prepoznati klijente.\",\n  \"client_name\": \"Klijent {{id}}\",\n  \"client_new\": \"Novi klijent\",\n  \"client_settings\": \"Postavke klijenta\",\n  \"client_table_header\": \"Klijent\",\n  \"client_unblocked\": \"Klijent \\\"{{ip}}\\\" je uspješno odblokiran\",\n  \"client_updated\": \"Klijent \\\"{{key}}\\\" je uspješno ažuriran\",\n  \"clients_desc\": \"Konfigurirajte trajne zapise klijenata za uređaje povezane s AdGuard Home\",\n  \"clients_not_found\": \"Nema pronađenih klijenata\",\n  \"clients_title\": \"Uporni klijenti\",\n  \"compact\": \"Kompaktno\",\n  \"config_successfully_saved\": \"Postavke su uspješno spremljene\",\n  \"configure\": \"Konfiguriraj\",\n  \"confirm_dns_cache_clear\": \"Jeste li sigurni da želite očistiti DNS predmemoriju?\",\n  \"confirm_static_ip\": \"AdGuard Home će postaviti {{ip}} kao vašu statičku IP adresu. Želiš li nastaviti?\",\n  \"copyright\": \"Autorsko pravo\",\n  \"country\": \"Država\",\n  \"custom_filter_rules\": \"Prilagođena pravila filtriranja\",\n  \"custom_filter_rules_hint\": \"Unesite jedno pravilo po liniji. Možete koristiti sintaksu za pravila blokiranja oglasa ili za hosts datoteke.\",\n  \"custom_filtering_rules\": \"Prilagođena pravila filtriranja\",\n  \"custom_ip\": \"Prilagođen IP\",\n  \"custom_retention_input\": \"Unesite zadržavanje u satima\",\n  \"custom_rotation_input\": \"Unesite rotaciju u satima\",\n  \"dashboard\": \"Upravljačka ploča\",\n  \"date\": \"Datum\",\n  \"default\": \"Zadano\",\n  \"delete_confirm\": \"Jeste li sigurni da želite ukloniti \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Ukloni\",\n  \"descr\": \"Opis\",\n  \"details\": \"Detalji\",\n  \"dhcp_add_static_lease\": \"Dodaj static lease\",\n  \"dhcp_config_saved\": \"Postavke DHCP poslužitelja su uspješno spremljene\",\n  \"dhcp_description\": \"Ukoliko vaš router ne pruža DHCP postavke, možete koristiti AdGuardov ugrađeni DHCP poslužitelj.\",\n  \"dhcp_disable\": \"Onemogući DHCP poslužitelj\",\n  \"dhcp_dynamic_ip_found\": \"Vaš sustav koristi postavke dinamičke IP adrese za sučelje <0>{{interfaceName}}</0>. Za korištenje DHCP poslužitelja mora se postaviti statička IP adresa. Vaša trenutna IP adresa je <0>{{ipAddress}}</0>. AdGuard Home automatski će postaviti ovu IP adresu kao statičnu ako pritisnete gumb \\\"Omogući DHCP\\\".\",\n  \"dhcp_edit_static_lease\": \"Uredi statični lease\",\n  \"dhcp_enable\": \"Omogući DHCP poslužitelj\",\n  \"dhcp_error\": \"AdGuard Home nije mogao utvrditi postoji li drugi aktivni DHCP poslužitelj na mreži\",\n  \"dhcp_form_gateway_input\": \"Gateway IP\",\n  \"dhcp_form_lease_input\": \"Lease trajanje\",\n  \"dhcp_form_lease_title\": \"DHCP lease vrijeme (u sekundama)\",\n  \"dhcp_form_range_end\": \"Kraj raspona\",\n  \"dhcp_form_range_start\": \"Početak raspona\",\n  \"dhcp_form_range_title\": \"Raspon IP adresa\",\n  \"dhcp_form_subnet_input\": \"Subnet maskiranje\",\n  \"dhcp_found\": \"Aktivni DHCP poslužitelj je pronađen na mreži. Nije sigurno omogućiti ugrađeni DHCP poslužitelj.\",\n  \"dhcp_hardware_address\": \"Adresa hardvera\",\n  \"dhcp_interface_select\": \"Odaberite DHCP sučelje\",\n  \"dhcp_ip_addresses\": \"IP adrese\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 postavke\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 postavke\",\n  \"dhcp_lease_added\": \"Statični lease \\\"{{key}}\\\" je uspješno dodan\",\n  \"dhcp_lease_deleted\": \"Statični lease \\\"{{key}}\\\" je uspješno uklonjen\",\n  \"dhcp_lease_updated\": \"Statični lease \\\"{{key}}\\\" uspješno ažuriran\",\n  \"dhcp_leases\": \"DHCP leases\",\n  \"dhcp_leases_not_found\": \"Nisu pronađeni DHCP leases\",\n  \"dhcp_new_static_lease\": \"Novi static lease\",\n  \"dhcp_not_found\": \"Sigurno je omogućiti ugrađeni DHCP poslužitelj jer AdGuard Home nije pronašao nijedan aktivni DHCP poslužitelj na mreži. Međutim, preporučujemo vam da ponovo provjerite ručno, jer naš automatski test trenutno ne daje 100% jamstvo.\",\n  \"dhcp_reset\": \"Jeste li sigurni da želite poništiti DHCP postavke?\",\n  \"dhcp_reset_leases\": \"Ponovno postavljanje svih najmova\",\n  \"dhcp_reset_leases_confirm\": \"Jeste li sigurni da želite resetirati sve najmove?\",\n  \"dhcp_reset_leases_success\": \"DHCP najmovi uspješno se resetiraju\",\n  \"dhcp_settings\": \"DHCP postavke\",\n  \"dhcp_static_ip_error\": \"Da bi se koristio DHCP poslužitelj mora se postaviti statička IP adresa. AdGuard Home nije uspio utvrditi je li ovo mrežno sučelje konfigurirano pomoću statičke IP adrese. Ručno postavite statičku IP adresu.\",\n  \"dhcp_static_leases\": \"DHCP static leases\",\n  \"dhcp_static_leases_not_found\": \"Nisu pronađeni statični DHCP leases\",\n  \"dhcp_table_expires\": \"Istječe\",\n  \"dhcp_table_hostname\": \"Naziv računala\",\n  \"dhcp_title\": \"DHCP poslužitelj (eksperimentalno!)\",\n  \"dhcp_warning\": \"Ako svejedno želite omogućiti DHCP poslužitelj, provjerite da nema drugog aktivnog DHCP poslužitelja na vašoj mreži. Inače može pokvariti Internet za ostale povezane uređaje!\",\n  \"disable_for_hours\": \"Za {{count}} sati\",\n  \"disable_for_hours_plural\": \"Za {{count}} sati\",\n  \"disable_for_minutes\": \"Za {{count}} minuta\",\n  \"disable_for_minutes_plural\": \"Za {{count}} minuta\",\n  \"disable_for_seconds\": \"Za {{count}} sekundi\",\n  \"disable_for_seconds_plural\": \"Za {{count}} sekundi\",\n  \"disable_ipv6\": \"Onemogući razrješavanje IPv6 adresa\",\n  \"disable_ipv6_desc\": \"Ignorirajte sve DNS zahtjeve adresa IPv6 (tipa AAAA) i uklonite IPv6 podatke iz HTTPS odgovora.\",\n  \"disable_notify_for_hours\": \"Isključi zaštitu na {{count}} sati\",\n  \"disable_notify_for_hours_plural\": \"Isključi zaštitu na {{count}} sati\",\n  \"disable_notify_for_minutes\": \"Isključi zaštitu na {{count}} minuta\",\n  \"disable_notify_for_minutes_plural\": \"Isključi zaštitu na {{count}} minuta\",\n  \"disable_notify_for_seconds\": \"Isključi zaštitu na {{count}} sekundi\",\n  \"disable_notify_for_seconds_plural\": \"Onemogući zaštitu na {{count}} sekundi\",\n  \"disable_notify_until_tomorrow\": \"Isključi zaštitu do sutra\",\n  \"disable_protection\": \"Onemogući zaštitu\",\n  \"disable_rewrites\": \"Onemogući pravila prepisivanja\",\n  \"disable_until_tomorrow\": \"Do sutra\",\n  \"disabled\": \"Onemogućeno\",\n  \"disabled_dhcp\": \"DHCP poslužitelj je onemogućen\",\n  \"disabled_filtering_toast\": \"Onemogućeno filtriranje\",\n  \"disabled_parental_toast\": \"Onemogućen roditeljski nadzor\",\n  \"disabled_protection\": \"Onemogućena zaštita\",\n  \"disabled_safe_browsing_toast\": \"Onemogućena Sigurna pretraga\",\n  \"disabled_safe_search_toast\": \"Onemogućeno sigurno pretraživanje\",\n  \"disallow_this_client\": \"Onemogući ovog klijenta\",\n  \"dns_addresses\": \"DNS adrese\",\n  \"dns_allowlists\": \"DNS popisi dopuštenih\",\n  \"dns_allowlists_desc\": \"Domene iz DNS popisa dopuštenih će biti omogućene čak i kada se nalaze na nekoj listi nedopuštenih.\",\n  \"dns_blocklists\": \"DNS popisi nedopuštenih\",\n  \"dns_blocklists_desc\": \"AdGuard Home će blokirati domene koje odgovaraju popisu nedopuštenih.\",\n  \"dns_cache_config\": \"DNS predmemorija poslužitelja\",\n  \"dns_cache_config_desc\": \"Ovdje možete postaviti DNS predmemoriju\",\n  \"dns_cache_size\": \"Veličina DNS predmemorije, u bajtovima\",\n  \"dns_config\": \"DNS postavke poslužitelja\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS privatnost\",\n  \"dns_providers\": \"Ovo je <0>popis poznatih DNS poslužitelja</0> za izbor.\",\n  \"dns_query\": \"DNS upiti\",\n  \"dns_rewrites\": \"DNS prijepisi\",\n  \"dns_settings\": \"DNS postavke\",\n  \"dns_start\": \"Pokreće se DNS poslužitelj\",\n  \"dns_status_error\": \"Pogreška pri provjeravanju statusa DNS poslužitelja\",\n  \"dns_test_not_ok_toast\": \"\\\"{{key}}\\\" poslužitelja: ne može se upotrijebiti, provjerite jeste li to ispravno napisali\",\n  \"dns_test_ok_toast\": \"Odabrani DNS poslužitelji su trenutno aktivni\",\n  \"dns_test_parsing_error_toast\": \"Odjeljak {{section}}: redak {{line}}: nije moguće koristiti, provjerite jeste li ispravno napisali\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" ne odgovara na zahtjeve za testiranje i možda neće raditi ispravno\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Omogući DNSSEC\",\n  \"dnssec_enable_desc\": \"Postavite ZASTAVICU DNSSEC-a u ulaznim DNS upitima i provjerite rezultat (potreban je DNSSEC-omogućen razrješivač).\",\n  \"domain\": \"Domena\",\n  \"domain_desc\": \"Unesite naziv domene ili zamjenski znak koji želite prepisati.\",\n  \"domain_name_table_header\": \"Naziv domene\",\n  \"domain_or_client\": \"Domena ili klijent\",\n  \"down\": \"Ne radi\",\n  \"download_mobileconfig\": \"Preuzmite konfiguracijsku datoteku\",\n  \"download_mobileconfig_doh\": \"Preuzmi .mobileconfig za DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Preuzmi .mobileconfig za DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Uredi popis dopuštenih\",\n  \"edit_blocklist\": \"Uredi popis nedopuštenih\",\n  \"edit_table_action\": \"Uredi\",\n  \"edns_cs_desc\": \"Dodajte opciju EDNS klijentske podmreže (ECS) uzvodnim zahtjevima i zabilježite vrijednosti koje su klijenti poslali u dnevnik upita.\",\n  \"edns_enable\": \"Omogući podmrežu klijenta EDNS-a\",\n  \"edns_use_custom_ip\": \"Koristi prilagođeni IP za EDNS\",\n  \"edns_use_custom_ip_desc\": \"Dopusti korištenje prilagođenog IP-a za EDNS\",\n  \"elapsed\": \"Proteklo\",\n  \"empty_response_status\": \"Prazno\",\n  \"enable_protection\": \"Omogući zaštitu\",\n  \"enable_protection_timer\": \"Zaštita će biti omogućena u {{time}}\",\n  \"enable_rewrites\": \"Omogući pravila prepisivanja\",\n  \"enable_upstream_dns_cache\": \"Uključite keširanje za korisničku konfiguraciju upstream servera ovog klijenta\",\n  \"enabled_dhcp\": \"DHCP poslužitelj je omogućen\",\n  \"enabled_filtering_toast\": \"Omogućeno filtriranje\",\n  \"enabled_parental_toast\": \"Omogućen roditeljski nadzor\",\n  \"enabled_protection\": \"Omogućena zaštita\",\n  \"enabled_safe_browsing_toast\": \"Omogućena Sigurna pretraga\",\n  \"enabled_save_search_toast\": \"Omogućeno sigurno pretraživanje\",\n  \"enabled_table_header\": \"Omogućeno\",\n  \"encryption_certificate_path\": \"Putanja certifikata\",\n  \"encryption_certificates\": \"Certifikati\",\n  \"encryption_certificates_desc\": \"Da biste koristili šifriranje, za svoju domenu morate osigurati važeći lanac SSL certifikata. Besplatan certifikat možete dobiti na <0>{{link}}</0> ili ga možete kupiti od jednog od pouzdanih izdavatelja certifikata.\",\n  \"encryption_certificates_input\": \"Zalijepite svoje PEM-kodirane certifikate ovdje.\",\n  \"encryption_certificates_source_content\": \"Zalijepi sadržaj certifikata\",\n  \"encryption_certificates_source_path\": \"Dodajte datoteku certifikata\",\n  \"encryption_chain_invalid\": \"Lanac certifikata nije valjan\",\n  \"encryption_chain_valid\": \"Lanac certifikata je valjan\",\n  \"encryption_config_saved\": \"Konfiguracija šifriranja spremljena\",\n  \"encryption_desc\": \"Podrška šifriranja (HTTPS/QUIC/TLS) za DNS i administratorsko web sučelje\",\n  \"encryption_doq\": \"DNS-over-QUIC port (eksperimentalno)\",\n  \"encryption_doq_desc\": \"Ako je ovaj priključak konfiguriran, AdGuard Home će na ovom priključku pokretati DNS-over-QUIC poslužitelj.\",\n  \"encryption_dot\": \"DNS-over-TLS port\",\n  \"encryption_dot_desc\": \"Ako je ovaj port postavljen, AdGuard Home će pokrenuti DNS-over-TLS poslužitelj na ovom portu.\",\n  \"encryption_enable\": \"Omogući šifriranje (HTTPS, DNS-over-HTTPS i DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Ako je šifriranje omogućeno, administratorsko sučelje AdGuard Home će raditi preko HTTPS-a, a DNS poslužitelj će osluškivati zahtjeve preko DNS-over-HTTPS i DNS-over-TLS.\",\n  \"encryption_expire\": \"Istječe\",\n  \"encryption_hostnames\": \"Nazivi računala\",\n  \"encryption_https\": \"HTTPS port\",\n  \"encryption_https_desc\": \"Ako je HTTPS port postavljen, AdGuard Home administracijsko sučelje biti će dostupno putem HTTPS-a, a također će pružiti DNS-over-HTTPS na '/dns-query' lokaciji.\",\n  \"encryption_issuer\": \"Izdavač\",\n  \"encryption_key\": \"Privatni ključ\",\n  \"encryption_key_input\": \"Zalijepite svoj PEM-kodiran privatni ključ certifikata ovdje.\",\n  \"encryption_key_invalid\": \"Ovo je nevažeći {{type}} privatni ključ\",\n  \"encryption_key_source_content\": \"Zalijepi sadržaj privatnog ključa\",\n  \"encryption_key_source_path\": \"Dodajte putanju datoteke privatnog ključa\",\n  \"encryption_key_valid\": \"Ovo je valjani {{type}} privatni ključ\",\n  \"encryption_plain_dns_desc\": \"Obični DNS je omogućen prema zadanim postavkama. Možete ga onemogućiti kako biste prisilili sve uređaje da koriste šifrirani DNS. Da biste to učinili, morate omogućiti barem jedan kriptirani DNS protokol\",\n  \"encryption_plain_dns_enable\": \"Omogući obični DNS\",\n  \"encryption_plain_dns_error\": \"Da biste onemogućili obični DNS, omogućite barem jedan kriptirani DNS protokol\",\n  \"encryption_private_key_path\": \"Putanja privatnog ključa\",\n  \"encryption_redirect\": \"Automatski preusmjeri na HTTPS\",\n  \"encryption_redirect_desc\": \"Ako je omogućeno, AdGuard Home će vas automatski preusmjeravati s HTTP na HTTPS adrese.\",\n  \"encryption_reset\": \"Jeste li sigurni da želite poništiti postavke šifriranja?\",\n  \"encryption_server\": \"Naziv poslužitelja\",\n  \"encryption_server_desc\": \"Ako je postavljeno, AdGuard Home otkriva ClientID-ove, odgovara na DDR upite i provodi dodatne provjere valjanosti veze. Ako nije postavljeno, ove značajke su onemogućene. Mora odgovarati jednom od DNS naziva u certifikatu.\",\n  \"encryption_server_enter\": \"Unesite naziv domene\",\n  \"encryption_settings\": \"Postavke šifriranja\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Predmet\",\n  \"encryption_title\": \"Šifriranje\",\n  \"encryption_warning\": \"Upozorenje\",\n  \"enforce_safe_search\": \"Koristi sigurno pretraživanje\",\n  \"enforce_save_search_hint\": \"AdGuard Home provodit će sigurno pretraživanje u sljedećim tražilicama: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Omogućeno sigurno pretraživanje\",\n  \"enter_cache_size\": \"Unesite veličinu predmemorije (u bajtovima)\",\n  \"enter_cache_ttl_max_override\": \"Unesite maksimalni TTL (u sekundama)\",\n  \"enter_cache_ttl_min_override\": \"Unesite minimalni TTL (u sekundama)\",\n  \"enter_name_hint\": \"Unesite naziv\",\n  \"enter_url_or_path_hint\": \"Unesite URL ili putanju liste\",\n  \"enter_valid_allowlist\": \"Unesite valjani URL za popis dopuštenih.\",\n  \"enter_valid_blocklist\": \"Unesite valjani URL za popis nedopuštenih.\",\n  \"error_details\": \"Detalji o pogrešci\",\n  \"example_comment\": \"! Ovdje ide komentar.\",\n  \"example_comment_hash\": \"# Također komentar.\",\n  \"example_comment_meaning\": \"samo komentar;\",\n  \"example_meaning_filter_block\": \"blokira pristup example.org i svim njenim poddomenama;\",\n  \"example_meaning_filter_whitelist\": \"odblokira pristup example.org i svim njenim poddomenama;\",\n  \"example_meaning_host_block\": \"vratiti 127.0.0.1 adresu na example.org domenu (ali ne i poddomene);\",\n  \"example_multiple_upstreams_reserved\": \"višestruke upstream poslužitelje <0>za određene domene</0>;\",\n  \"example_regex_meaning\": \"blokira pristup domenama koje se podudaraju s regularnim izrazom.\",\n  \"example_rewrite_domain\": \"prepiši odgovore samo za ovaj naziv domene.\",\n  \"example_rewrite_wildcard\": \"prepiši odgovore za sve <0>example.org</0> poddomene.\",\n  \"example_upstream_comment\": \"komentar.\",\n  \"example_upstream_doh\": \"šifrirano <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"šifrirani DNS-over-HTTPS s prisilnim <0>HTTP/3</0> i nema povratka na HTTP/2 ili niže;\",\n  \"example_upstream_doq\": \"šifrirano <0>DNS-over-QUIC</0> (eksperimentalno);\",\n  \"example_upstream_dot\": \"šifrirano <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"zadani DNS (putem UDP);\",\n  \"example_upstream_regular_port\": \"obični DNS (preko UDP-a, s portom);\",\n  \"example_upstream_reserved\": \"upstream <0>za određene domene</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> za <1>DNSCrypt</1> ili <2>DNS-over-HTTPS</2> rezolvere;\",\n  \"example_upstream_tcp\": \"zadani DNS (putem TCP);\",\n  \"example_upstream_tcp_hostname\": \"obični DNS (preko TCP-a, ime hosta);\",\n  \"example_upstream_tcp_port\": \"obični DNS (preko TCP-a, s portom);\",\n  \"example_upstream_udp\": \"obični DNS (preko UDP-a, ime hosta);\",\n  \"examples_title\": \"Primjeri\",\n  \"fallback_dns_desc\": \"Popis rezervnih DNS poslužitelja koji se koriste kada uzvodni DNS poslužitelji ne odgovaraju. Sintaksa je ista kao u gornjem polju glavnog uzvodnog toka.\",\n  \"fallback_dns_placeholder\": \"Unesite jedan rezervni DNS poslužitelj po retku\",\n  \"fallback_dns_title\": \"Rezervni DNS poslužitelji\",\n  \"faq\": \"ČPP\",\n  \"fastest_addr\": \"Najbrža IP adresa\",\n  \"fastest_addr_desc\": \"Čekajte odgovore od <b>svi</b> DNS poslužitelja, izmjerite brzinu TCP veze za svaki poslužitelj i vratite IP adresu poslužitelja s najbržom brzinom veze.<br/>Ovaj način može značajno usporiti DNS upite ako jedan ili više uzvodnih poslužitelja ne odgovara. Provjerite jesu li vaši uzvodni poslužitelji stabilni i da je vrijeme čekanja uzvodnog nisko.\",\n  \"filter\": \"Filtar\",\n  \"filter_added_successfully\": \"Popis je uspješno dodan\",\n  \"filter_allowlist\": \"UPOZORENJE: Ova akcija će također isključiti pravilo \\\"{{disallowed_rule}}\\\" s popisa dopuštenih klijenata.\",\n  \"filter_category_general\": \"Općenito\",\n  \"filter_category_general_desc\": \"Popisi koji blokiraju pratitelje i oglase na većini uređaja\",\n  \"filter_category_other\": \"Ostalo\",\n  \"filter_category_other_desc\": \"Ostali popisi nedopuštenih\",\n  \"filter_category_regional\": \"Regionalno\",\n  \"filter_category_regional_desc\": \"Popisi koji se fokusiraju na regionalne oglase i poslužitelje za praćenje\",\n  \"filter_category_security\": \"Sigurnost\",\n  \"filter_category_security_desc\": \"Popisi posebno dizajnirani za blokiranje zlonamjernih domena, domena za krađu identiteta i prijevare\",\n  \"filter_removed_successfully\": \"Ovaj popis je uspješno uklonjen\",\n  \"filter_updated\": \"Ovaj popis je uspješno ažuriran\",\n  \"filtered\": \"Filtrirano\",\n  \"filtered_custom_rules\": \"Filtrirano prilagođenim pravilima filtriranja\",\n  \"filtering_rules_learn_more\": \"<0>Saznajte više</0> o stvaranju vlastitog popisa poslužitelja.\",\n  \"filters\": \"Filtri\",\n  \"filters_and_hosts_hint\": \"AdGuard Home razumije osnovna pravila blokiranja oglasa i sintaksu hosts datoteka.\",\n  \"filters_block_toggle_hint\": \"Pravila blokiranja možete postaviti u postavkama <a>filtara</a>.\",\n  \"filters_configuration\": \"Postavke filtara\",\n  \"filters_enable\": \"Omogući filtre\",\n  \"filters_interval\": \"Interval ažuriranja filtara\",\n  \"fix\": \"Popravi\",\n  \"for_last_days\": \"zadnjih {{count}} dana\",\n  \"for_last_days_plural\": \"zadnjih {{count}} dana\",\n  \"for_last_hours\": \"za posljednji {{count}} sat\",\n  \"for_last_hours_plural\": \"za posljednjih {{count}} sati\",\n  \"forgot_password\": \"Zaboravljena lozinka?\",\n  \"forgot_password_desc\": \"Slijedite <0>ove korake</0> da biste stvorili novu lozinku za svoj korisnički račun.\",\n  \"form_add_id\": \"Dodaj identifikator\",\n  \"form_answer\": \"Unesite IP adresu ili naziv domene\",\n  \"form_client_name\": \"Unesite naziv klijenta\",\n  \"form_domain\": \"Unesite naziv domene ili zamjenski znak\",\n  \"form_enter_blocked_response_ttl\": \"Unesite TTL blokiranog odgovora (sekunde)\",\n  \"form_enter_host\": \"Unesite naziv računala\",\n  \"form_enter_hostname\": \"Unesite naziv računala\",\n  \"form_enter_id\": \"Unesi identifikator\",\n  \"form_enter_ip\": \"Unesite IP adresu\",\n  \"form_enter_mac\": \"Unesite MAC adresu\",\n  \"form_enter_rate_limit\": \"Unesite ograničenje\",\n  \"form_enter_rate_limit_subnet_len\": \"Unesite duljinu prefiksa podmreže za ograničenje brzine\",\n  \"form_enter_subnet_ip\": \"Unesite IP adresu u podmrežu \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Unesite trajanje vremena čekanja uzvodnog poslužitelja u sekundama\",\n  \"form_error_answer_format\": \"Nevažeći format odgovora\",\n  \"form_error_client_id_format\": \"ID klijenta može sadržavati samo brojeve, mala slova i crtice\",\n  \"form_error_domain_format\": \"Nevažeći format domene\",\n  \"form_error_equal\": \"Ne bi trebalo biti jednako\",\n  \"form_error_gateway_ip\": \"Najam ne može imati IP adresu pristupnika\",\n  \"form_error_ip4_format\": \"Nevažeća IPv4 adresa\",\n  \"form_error_ip4_gateway_format\": \"Nepravilna IPV4 adresa čvora\",\n  \"form_error_ip6_format\": \"Nevažeći IPv6 adresa\",\n  \"form_error_ip_format\": \"Nepravilna IP adresa\",\n  \"form_error_mac_format\": \"Nevažeći MAC adresa\",\n  \"form_error_password\": \"Lozinka se ne podudara\",\n  \"form_error_password_length\": \"Lozinka mora sadržavati od {{min}} do {{max}} znakova\",\n  \"form_error_port\": \"Unesite važeći broj porta\",\n  \"form_error_port_range\": \"Unesite broj porta od 80 do 65536\",\n  \"form_error_port_unsafe\": \"Nesigurna port\",\n  \"form_error_positive\": \"Mora biti veće od 0\",\n  \"form_error_required\": \"Obavezno polje\",\n  \"form_error_server_name\": \"Nevažeće ime poslužitelja\",\n  \"form_error_subnet\": \"Podmrežu \\\"{{cidr}}\\\" ne sadrži IP adresu \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Nevažeći URL format\",\n  \"form_error_url_or_path_format\": \"Nevažeći URL ili putanja od liste\",\n  \"form_select_tags\": \"Odaberite oznake klijenta\",\n  \"found_in_known_domain_db\": \"Pronađeno u bazi poznatih domena.\",\n  \"friday\": \"Petak\",\n  \"friday_short\": \"Pet\",\n  \"gateway_or_subnet_invalid\": \"Nevažeća podmrežna maska\",\n  \"general_settings\": \"Opće postavke\",\n  \"general_statistics\": \"Opća statistika\",\n  \"get_started\": \"Započni\",\n  \"greater_range_start_error\": \"Mora biti veće od krajnjeg ranga\",\n  \"homepage\": \"Početna stranica\",\n  \"host_whitelisted\": \"Računalo je na popisu dopuštenih\",\n  \"ignore_domains\": \"Zanemarene domene (odvojene novim retkom)\",\n  \"ignore_domains_desc_query\": \"Upiti koji odgovaraju ovim pravilima ne upisuju se u zapisnik upita\",\n  \"ignore_domains_desc_stats\": \"Upiti koji odgovaraju ovim pravilima ne upisuju se u statistiku\",\n  \"ignore_domains_title\": \"Zanemarene domene\",\n  \"ignore_query_log\": \"Zanemari ovog klijenta u zapisniku upita\",\n  \"ignore_statistics\": \"Ignorirajte ovog klijenta u statistici\",\n  \"install_auth_confirm\": \"Potvrdi lozinku\",\n  \"install_auth_desc\": \"Provjera autentičnosti lozinke na web-sučelje AdGuard Home admin mora biti konfigurirana. Čak i ako je AdGuard Home dostupan samo u vašoj lokalnoj mreži, i dalje je važno zaštititi ga od neograničenog pristupa.\",\n  \"install_auth_password\": \"Lozinka\",\n  \"install_auth_password_enter\": \"Unesite lozinku\",\n  \"install_auth_title\": \"Autentikacija\",\n  \"install_auth_username\": \"Korisničko ime\",\n  \"install_auth_username_enter\": \"Unesite korisničko ime\",\n  \"install_devices_address\": \"AdGuard Home DNS poslužitelj osluškuje sljedeće adrese\",\n  \"install_devices_android_list_1\": \"Na početnom zaslonu Androida, odaberite Postavke.\",\n  \"install_devices_android_list_2\": \"Pritisnite Wi-Fi u izborniku. Prikazat će se zaslon s popisom svih dostupnih mreža (nemoguće je postaviti prilagođeni DNS za mobilnu vezu).\",\n  \"install_devices_android_list_3\": \"Dugo pritisnite na mrežu na koju ste povezani i odaberite Uredi mrežu.\",\n  \"install_devices_android_list_4\": \"Na nekim će uređajima možda trebati označiti Napredno za prikaz dodatnih postavki. Da biste prilagodili postavke Android DNS-a, morati će te prebaciti IP postavke s DHCP-a na Statičke.\",\n  \"install_devices_android_list_5\": \"Promijenite vrijednosti DNS-a 1 i DNS-a 2 u adrese AdGuard Home poslužitelja.\",\n  \"install_devices_desc\": \"Da biste započeli koristiti AdGuard Home, morate postaviti uređaje da ga koriste.\",\n  \"install_devices_ios_list_1\": \"Na početnom zaslonu odaberite Postavke.\",\n  \"install_devices_ios_list_2\": \"Odaberite Wi-Fi u lijevom izborniku (ne moguće je postaviti DNS za mobilne mreže).\",\n  \"install_devices_ios_list_3\": \"Pritisnite na naziv vaše trenutne mreže.\",\n  \"install_devices_ios_list_4\": \"U DNS polje unesite adrese svog AdGuard Home poslužitelja.\",\n  \"install_devices_macos_list_1\": \"Pritisnite na Apple ikonu i idite u Postavke sustava.\",\n  \"install_devices_macos_list_2\": \"Pritisnite na Mreža.\",\n  \"install_devices_macos_list_3\": \"Odaberite prvu vezu s vašeg popisa i pritisnite Napredno.\",\n  \"install_devices_macos_list_4\": \"Odaberite DNS karticu i unesite adrese svog AdGuard Home poslužitelja.\",\n  \"install_devices_router\": \"Usmjerivač (Router)\",\n  \"install_devices_router_desc\": \"Ova postavka automatski pokriva sve uređaje povezane s kućnim usmjerivačem, nema potrebe za ručnom konfiguracijom svakog od njih.\",\n  \"install_devices_router_list_1\": \"Otvorite postavke za router. Obično mu možete pristupiti iz preglednika putem URL-a, kao što je http://192.168.0.1/ ili http://192.168.1.1/. Od vas će se možda tražiti da unesete lozinku. Ako je se ne sjećate, lozinku možete često poništiti pritiskom na dumge na samom routeru. Neki routeri trebaju određenu aplikaciju, koja bi u tom slučaju trebala biti već instalirana na vašem računalu/telefonu.\",\n  \"install_devices_router_list_2\": \"Pronađite DHCP/DNS postavke. Potražite DNS slova pored polja koje dopušta dva ili tri skupa brojeva, svaki razdvojen u četiri skupine od jedne do tri znamenke.\",\n  \"install_devices_router_list_3\": \"Unesite adresu AdGuard Home poslužitelja ovdje.\",\n  \"install_devices_router_list_4\": \"Na nekim se vrstama usmjerivača ne može postaviti prilagođeni DNS poslužitelj. U ovom slučaju, može vam pomoći ako postavite AdGuard Home kao <0>DHCP poslužitelj</0>. U suprotnom, trebali biste potražiti priručnik o tome kako prilagoditi DNS poslužitelje za vaš određeni model routera.\",\n  \"install_devices_title\": \"Postavite vaše uređaje\",\n  \"install_devices_windows_list_1\": \"Otvorite Upravljačku ploču putem Start izbornika ili Windows pretrage.\",\n  \"install_devices_windows_list_2\": \"Idite na kategoriju Mreža i Internet i odaberite Centar za mreže i zajedničko korištenje.\",\n  \"install_devices_windows_list_3\": \"Na lijevoj strani zaslona kliknite \\\"Promjena postavki prilagodnika\\\".\",\n  \"install_devices_windows_list_4\": \"Desnom tipkom miša kliknite svoju aktivnu vezu i odaberite Svojstva.\",\n  \"install_devices_windows_list_5\": \"Na popisu pronađite \\\"Internet Protocol Version 4 (TCP/IPv4)\\\" (ili, za IPv6, \\\"Internet Protocol Version 6 (TCP/IPv6)\\\"), odaberite ga i zatim ponovno kliknite svojstva.\",\n  \"install_devices_windows_list_6\": \"Odaberite \\\"Koristi sljedeće adrese DNS poslužitelja\\\" i unesite adrese AdGuard Home poslužitelja.\",\n  \"install_saved\": \"Uspješno spremljeno\",\n  \"install_settings_all_interfaces\": \"Sva sučelja\",\n  \"install_settings_dns\": \"DNS poslužitelj\",\n  \"install_settings_dns_desc\": \"Potrebno je postaviti uređaj ili router da koristi DNS poslužitelj na sljedećim adresama:\",\n  \"install_settings_interface_link\": \"Web administratorsko sučelje AdGuard Home-a će biti dostupno na sljedećim adresama:\",\n  \"install_settings_listen\": \"Osluškuj sučelje\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Administratorsko web sučelje\",\n  \"install_static_configure\": \"AdGuard Home otkrio je da se koristi dinamička IP adresa <0>{{ip}}</0>. Želite li da bude postavljena kao vaša statička adresa?\",\n  \"install_static_error\": \"AdGuard Home ne može je automatski postaviti za ovo mrežno sučelje. Molimo potražite upute kako to učiniti ručno.\",\n  \"install_static_ok\": \"Dobre vijesti! Statička IP adresa već je postavljena\",\n  \"install_step\": \"Korak\",\n  \"install_submit_desc\": \"Postupak postavljanja je dovršen i sada ste spremni početi koristiti AdGuard Home.\",\n  \"install_submit_title\": \"Čestitamo!\",\n  \"install_welcome_desc\": \"AdGuard Home je DNS poslužitelj za blokiranje oglasa i pratitelja na cijeloj mreži. Njegova je svrha omogućiti vam upravljanje cijelom mrežom i svim svojim uređajima, a da to ne zahtijeva korištenje programa na strani klijenta.\",\n  \"install_welcome_title\": \"Dobrodošli u AdGuard Home!\",\n  \"interval_24_hour\": \"24 sata\",\n  \"interval_6_hour\": \"6 sati\",\n  \"interval_days\": \"{{count}} dan\",\n  \"interval_days_plural\": \"{{count}} dana\",\n  \"interval_hours\": \"{{count}} sata/i\",\n  \"interval_hours_plural\": \"{{count}} sata/i\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP adresa\",\n  \"known_tracker\": \"Poznati pratitelj\",\n  \"last_rule_in_allowlist\": \"Ovaj klijent nije moguće onemogućiti jer će isključivanje pravila \\\"{{disallowed_rule}}\\\" ONEMOGUĆITI popis \\\"Dopušteni klijenti\\\".\",\n  \"last_time_updated_table_header\": \"Zadnje ažurirano\",\n  \"list_confirm_delete\": \"Jeste li sigurni da želite ukloniti ovaj popis?\",\n  \"list_label\": \"Popis\",\n  \"list_updated\": \"{{count}} popis ažuriran\",\n  \"list_updated_plural\": \"{{count}} popisa ažurirana\",\n  \"list_url_table_header\": \"URL popisa\",\n  \"load_balancing\": \"Load-balancing\",\n  \"load_balancing_desc\": \"Pitajte jedan po jedan uzvodni poslužitelj.<br/>AdGuard Home koristi svoj ponderirani slučajni algoritam za odabir poslužitelja s najmanjim brojem neuspješnih pretraživanja i najnižim prosječnim vremenom pretraživanja.\",\n  \"loading_table_status\": \"Učitavanje...\",\n  \"local_ptr_default_resolver\": \"Prema zadanim postavkama AdGuard Home koristi sljedeće obrnute DNS razrješivače: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS poslužitelji koje koristi AdGuard Home za privatne PTR, SOA i NS zahtjeve. Zahtjev se smatra privatnim ako traži ARPA domenu koja sadrži podmrežu unutar privatnih IP raspona (kao što je \\\"192.168.12.34\\\") i dolazi od klijenta s privatnom IP adresom. Ako nije postavljeno, koristit će se zadani DNS rezolveri vašeg OS-a, osim za AdGuard Home IP adrese.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home nije mogao odrediti prikladne privatne obrnute DNS razrješivače za ovaj sustav.\",\n  \"local_ptr_placeholder\": \"Unesite jednu adresu poslužitelja po retku\",\n  \"local_ptr_title\": \"Privatni obrnuti DNS poslužitelji\",\n  \"location\": \"Lokacija\",\n  \"log_and_stats_section_label\": \"Zapisnik upita i statistika\",\n  \"lower_range_start_error\": \"Mora biti niže od početnog ranga\",\n  \"main_settings\": \"Opće postavke\",\n  \"make_static\": \"Učini statičnim\",\n  \"manual_update\": \"Molimo <a>pratite ove korake</a> za ručno ažuriranje.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Ponedjeljak\",\n  \"monday_short\": \"Pon\",\n  \"name\": \"Naziv\",\n  \"name_table_header\": \"Naziv\",\n  \"netname\": \"Naziv mreže\",\n  \"network\": \"Mreža\",\n  \"new_allowlist\": \"Novi popis dopuštenih\",\n  \"new_blocklist\": \"Novi popis nedopuštenih\",\n  \"next\": \"Sljedeće\",\n  \"next_btn\": \"Sljedeće\",\n  \"no_blocklist_added\": \"Nema dodanih popisa nedopuštenih\",\n  \"no_clients_found\": \"Nema pronađenih klijenata\",\n  \"no_domains_found\": \"Nije pronađena domena\",\n  \"no_logs_found\": \"Nema zapisa\",\n  \"no_servers_specified\": \"Nije odabran nijedan poslužitelj\",\n  \"no_upstreams_data_found\": \"Nema podataka o upstream poslužiteljima\",\n  \"no_whitelist_added\": \"Nema dodanih popisa dopuštenih\",\n  \"nothing_found\": \"Nema rezultata\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Broj DNS zahtjeva koji blokiraju filtri za blokiranje oglasa i popisi blokova hostova\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Broj blokiranih stranica s sadržajem za odrasle\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Broj DNS zahtjeva koje je blokirao modul AdGuard zaštita pregledavanja\",\n  \"number_of_dns_query_days\": \"Broj DNS upita obrađenih u posljednja {{count}} dan\",\n  \"number_of_dns_query_days_plural\": \"Broj DNS upita obrađenih u posljednja {{count}} dana\",\n  \"number_of_dns_query_hours\": \"Broj DNS upita obrađenih za posljednji {{count}} sat\",\n  \"number_of_dns_query_hours_plural\": \"Broj DNS upita obrađenih za posljednjih {{count}} sati\",\n  \"number_of_dns_query_to_safe_search\": \"Broj DNS zahtjeva prema pretraživačima za koje je omogućeno Sigurno pretraživanje\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"ISKLJUČENO\",\n  \"on\": \"UKLJUČENO\",\n  \"open_dashboard\": \"Otvori upravljačku ploču\",\n  \"orgname\": \"Naziv organizacije\",\n  \"original_response\": \"Originalni odgovor\",\n  \"out_of_range_error\": \"Mora biti izvan ranga \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Stranica\",\n  \"parallel_requests\": \"Paralelni zahtjevi\",\n  \"parental_control\": \"Roditeljska zaštita\",\n  \"password_label\": \"Lozinka\",\n  \"password_placeholder\": \"Unesite lozinku\",\n  \"plain_dns\": \"Obični DNS\",\n  \"port_53_faq_link\": \"Port 53 često zauzimaju usluge \\\"DNSStubListener\\\" ili \\\"systemd-resolved\\\". Molimo pročitajte <0>ove upute</0> o tome kako to riješiti.\",\n  \"previous_btn\": \"Prethodno\",\n  \"privacy_policy\": \"Politika privatnosti\",\n  \"processing_update\": \"Molimo pričekajte, AdGuard Home se ažurira\",\n  \"protection_section_label\": \"Zaštita\",\n  \"protocol\": \"Protokol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Zapisnik upita\",\n  \"query_log_clear\": \"Očisti zapisnik upita\",\n  \"query_log_cleared\": \"Zapisnik upita je uspješno uklonjen\",\n  \"query_log_configuration\": \"Postavke zapisa\",\n  \"query_log_confirm_clear\": \"Jeste li sigurni da želite ukloniti zapise upita?\",\n  \"query_log_disabled\": \"Zapisnik upita je onemogućen i može se postaviti u <0>postavkama</0>\",\n  \"query_log_enable\": \"Omogući zapise\",\n  \"query_log_filtered\": \"Filtrirao {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotacija dnevnika upita\",\n  \"query_log_retention_confirm\": \"Jeste li sigurni da želite promijeniti rotaciju dnevnika upita? Ako smanjite vrijednost intervala, neki će se podaci izgubiti\",\n  \"query_log_strict_search\": \"Koristite dvostruke navodnike za strogo pretraživanje\",\n  \"query_log_updated\": \"Zapisnik upita je uspješno ažuriran\",\n  \"rate_limit\": \"Ograničenje\",\n  \"rate_limit_desc\": \"Broj zahtjeva u sekundi koji su dopušteni po jednom klijentu. Postavljanje na 0 znači neograničeno.\",\n  \"rate_limit_subnet_len_ipv4\": \"Duljina prefiksa podmreže za IPv4 adrese\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Duljina prefiksa podmreže za IPv4 adrese koje se koriste za ograničavanje brzine. Zadana vrijednost je 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Dužina IPv4 prefiksa podmreže trebala bi biti između 0 i 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Duljina prefiksa podmreže za IPv6 adrese\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Duljina prefiksa podmreže za IPv6 adrese koje se koriste za ograničavanje brzine. Zadana vrijednost je 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Dužina IPv6 prefiksa podmreže trebala bi biti između 0 i 128\",\n  \"rate_limit_whitelist\": \"Popis dopuštenih za ograničavanje brzine\",\n  \"rate_limit_whitelist_desc\": \"IP adrese isključene iz ograničenja brzine\",\n  \"rate_limit_whitelist_placeholder\": \"Unesite jednu adresu poslužitelja po retku\",\n  \"refresh_btn\": \"Osvježi\",\n  \"refresh_statics\": \"Osvježi statistiku\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Prijavite problem\",\n  \"request_details\": \"Detalji zahtjeva\",\n  \"request_table_header\": \"Zahtjev\",\n  \"requests_count\": \"Broj zahtjeva\",\n  \"reset_settings\": \"Poništi postavke\",\n  \"resolve_clients_desc\": \"Obrnuto razriješite IP adrese klijenata u nazive glavnih računala slanjem PTR upita odgovarajućim razrješivačima (privatni DNS poslužitelji za lokalne klijente, uzvodni poslužitelji za klijente s javnim IP adresama).\",\n  \"resolve_clients_title\": \"Omogući obrnuto rješavanje IP adresa klijenata\",\n  \"response_code\": \"Responzivni kod\",\n  \"response_details\": \"Detalji odgovora\",\n  \"response_table_header\": \"Odgovor\",\n  \"response_time\": \"Vrijeme odziva\",\n  \"rewrite_A\": \"<0>A</0>: posebna vrijednost, ukloni <0>A</0> zapis od upstreama\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: posebna vrijednost, ukloni <0>AAAA</0> zapis od upstreama\",\n  \"rewrite_add\": \"Dodaj DNS prijepis\",\n  \"rewrite_added\": \"DNS prijepis za \\\"{{key}}\\\" je uspješno dodan\",\n  \"rewrite_applied\": \"Pravilo prijepisa je primjenjeno\",\n  \"rewrite_confirm_delete\": \"Jeste li sigurni da želite ukloniti DNS prijepis za \\\"{{key}}\\\" klijenta?\",\n  \"rewrite_deleted\": \"DNS prijepis za \\\"{{key}}\\\" je uspješno uklonjen\",\n  \"rewrite_desc\": \"Omogućuje jednostavno postavljanje prilagođenog DNS odgovora za određenu domenu.\",\n  \"rewrite_domain_name\": \"Naziv domene: Dodajte CNAME zapis\",\n  \"rewrite_edit\": \"Uredite prepisivanje DNS-a\",\n  \"rewrite_hosts_applied\": \"Prepisano od strane pravila hosts datoteke\",\n  \"rewrite_ip_address\": \"IP adresa: koristite ovu IP adresu u A ili AAAA odgovoru\",\n  \"rewrite_not_found\": \"Nema DNS prijepisa\",\n  \"rewrite_settings_updated\": \"Postavke prepisivanja DNS-a uspješno su ažurirane\",\n  \"rewrite_updated\": \"Prepisivanje DNS-a uspješno ažurirano\",\n  \"rewrites_disabled_table_header\": \"Prepisivanje je onemogućeno\",\n  \"rewrites_enabled_table_header\": \"Prepisivanje je omogućeno\",\n  \"rewritten\": \"Prepisano\",\n  \"rows_table_footer_text\": \"redova\",\n  \"rule_added_to_custom_filtering_toast\": \"Pravilo je dodano u prilagođena pravila filtriranja: {{rule}}\",\n  \"rule_label\": \"Pravilo/a\",\n  \"rule_removed_from_custom_filtering_toast\": \"Pravilo je uklonjeno iz prilagođenih pravila filtriranja: {{rule}}\",\n  \"rules_count_table_header\": \"Broj pravila\",\n  \"safe_browsing\": \"Sigurno surfanje\",\n  \"safe_search\": \"Sigurno pretraživanje\",\n  \"saturday\": \"Subota\",\n  \"saturday_short\": \"Sub\",\n  \"save_btn\": \"Spremi\",\n  \"save_config\": \"Spremi konfiguraciju\",\n  \"schedule_add\": \"Dodaj raspored\",\n  \"schedule_current_timezone\": \"Trenutna vremenska zona: {{value}}\",\n  \"schedule_desc\": \"Postavljanje razdoblja neaktivnosti za blokirane servise\",\n  \"schedule_edit\": \"Uredi raspored\",\n  \"schedule_from\": \"Od\",\n  \"schedule_invalid_select\": \"Vrijeme početka mora biti prije vremena završetka\",\n  \"schedule_modal_description\": \"Ovaj raspored zamijenit će sve postojeće rasporede za isti dan u tjednu. Svaki dan u tjednu može imati samo jedno razdoblje neaktivnosti.\",\n  \"schedule_modal_time_off\": \"Blokiranje usluga je onemogućeno:\",\n  \"schedule_new\": \"Novi raspored\",\n  \"schedule_remove\": \"Ukloni raspored\",\n  \"schedule_save\": \"Spremi raspored\",\n  \"schedule_select_days\": \"Odabir dana\",\n  \"schedule_services\": \"Pauziraj blokiranje servisa\",\n  \"schedule_services_desc\": \"Konfiguriranje rasporeda pauziranja filtra za blokiranje servisa\",\n  \"schedule_services_desc_client\": \"Konfiguriranje rasporeda pauziranja filtra za blokiranje servisa za ovog klijenta\",\n  \"schedule_time_all_day\": \"Cijeli dan\",\n  \"schedule_timezone\": \"Odabir vremenske zone\",\n  \"schedule_to\": \"Do\",\n  \"served_from_cache_label\": \"Posluženo iz predmemorije\",\n  \"service_name\": \"Naziv usluge\",\n  \"set_static_ip\": \"Postavite statičku IP adresu\",\n  \"settings\": \"Postavke\",\n  \"settings_custom\": \"Prilagođeno\",\n  \"settings_global\": \"Globalno\",\n  \"setup_config_to_enable_dhcp_server\": \"Postavite konfiguraciju za omogućavanje DHCP poslužitelja\",\n  \"setup_dns_notice\": \"Da biste koristili <1>DNS-over-HTTPS</1> ili <1>DNS-over-TLS</1>, morate <0>postaviti šifriranje</0> u AdGuard Home postavkama.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Koristite <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Koristite <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Evo popisa programa koje možete koristiti.</0>\",\n  \"setup_dns_privacy_4\": \"Na iOS 14 ili macOS Big Sur uređaju možete preuzeti posebnu datoteku '.mobileconfig' koja dodaje <highlight>DNS-over-HTTPS</highlight> ili <highlight>DNS-over-TLS</highlight> poslužitelje u postavke DNS-a.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 nativno podržava DNS-over-TLS. Da biste ga postavili, idite na Postavke → Mreža i internet → Napredno → Privatni DNS i tamo unesite svoje naziv domene.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard za Android</0> podržava <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> dodaje <1>DNS-over-HTTPS</1> podršku za Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS i macOS konfiguracija\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> podržava <1>DNS-over-HTTPS</1>, ali da biste ga postavili za upotrebu vašeg vlastitog poslužitelja, trebati će te generirati <2>DNS Stamp</2> za njega.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard za iOS</0> podržava <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home može poslužiti kao sigurni DNS klijent na svim platformama.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> podržava sve poznate sigurne DNS protokole.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> podržava <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> podržava <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Možete pronaći više implementacija <0>ovdje</0> i <1>ovdje</1>.\",\n  \"setup_dns_privacy_other_title\": \"Ostale implementacije\",\n  \"setup_guide\": \"Vodič za postavljanje\",\n  \"show_all_filter_type\": \"Prikaži sve\",\n  \"show_blocked_responses\": \"Blokirano\",\n  \"show_filtered_type\": \"Prikaži filtrirano\",\n  \"show_processed_responses\": \"Obrađeno\",\n  \"show_whitelisted_responses\": \"Na popisu dopuštenih\",\n  \"sign_in\": \"Prijava\",\n  \"sign_out\": \"Odjava\",\n  \"source_label\": \"Izvor\",\n  \"static_ip\": \"Statička IP adresa\",\n  \"static_ip_desc\": \"AdGuard Home je poslužitelj pa mu za pravilno funkcioniranje treba statička IP adresa. Inače, u određenom trenutku vaš router može ovom uređaju dodijeliti drugu IP adresu.\",\n  \"statistics_clear\": \"Poništi statistiku\",\n  \"statistics_clear_confirm\": \"Jeste li sigurni da želite poništiti statistiku?\",\n  \"statistics_cleared\": \"Statistika je uspješno uklonjenja\",\n  \"statistics_configuration\": \"Postavke statistike\",\n  \"statistics_enable\": \"Omogući statistiku\",\n  \"statistics_retention\": \"Spremanje statistike\",\n  \"statistics_retention_confirm\": \"Jeste li sigurni da želite promijeniti zadržavanje statistike? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni\",\n  \"statistics_retention_desc\": \"Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni\",\n  \"stats_adult\": \"Blokirane web stranice za odrasle\",\n  \"stats_disabled\": \"Statistika je onemogućena. Možete ga uključiti sa <0>stranice s postavkama</0>.\",\n  \"stats_disabled_short\": \"Statistika je onemogućena\",\n  \"stats_malware_phishing\": \"Blokiran zločudni program/krađe identiteta\",\n  \"stats_params\": \"Postavke statistike\",\n  \"stats_query_domain\": \"Top tražene domene\",\n  \"subnet_error\": \"Adrese moraju biti iz iste podmreže\",\n  \"sunday\": \"Nedjelja\",\n  \"sunday_short\": \"Ned\",\n  \"system_host_files\": \"Datoteke host sustava\",\n  \"table_client\": \"Klijent\",\n  \"table_name\": \"Naziv\",\n  \"tags_desc\": \"Možete odabrati oznake koje odgovaraju klijentu. Uključite oznake u pravila filtriranja kako biste ih preciznije primijenili. <0>Saznajte više</0>.\",\n  \"tags_title\": \"Oznake\",\n  \"test_upstream_btn\": \"Testiraj upstream-ove\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Automatski (na temelju sheme boja vašeg uređaja)\",\n  \"theme_dark\": \"Tamna\",\n  \"theme_dark_desc\": \"Tamna tema\",\n  \"theme_light\": \"Svijetla\",\n  \"theme_light_desc\": \"Svijetla tema\",\n  \"thursday\": \"Četvrtak\",\n  \"thursday_short\": \"Čet\",\n  \"time_table_header\": \"Vrijeme\",\n  \"top_blocked_domains\": \"Top blokirane domene\",\n  \"top_clients\": \"Top klijenti\",\n  \"top_upstreams\": \"Top upstream poslužitelji\",\n  \"topline_expired_certificate\": \"Vaš SSL certifikat je istekao. Ažurirajte <0>Postavke šifriranja</0>.\",\n  \"topline_expiring_certificate\": \"Vaš SSL certifikat uskoro ističe. Ažurirajte <0>Postavke šifriranja</0>.\",\n  \"tracker_source\": \"Izvor pratitelja\",\n  \"try_again\": \"Pokušajte ponovno\",\n  \"ttl_cache_validation\": \"Minimalno nadjačavanje TTL-a predmemorije mora biti manje ili jednako maksimalnom\",\n  \"tuesday\": \"Utorak\",\n  \"tuesday_short\": \"Uto\",\n  \"type_table_header\": \"Vrsta\",\n  \"unavailable_dhcp\": \"DHCP je nedostupan\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home ne može pokrenuti DHCP poslužitelj na vašem OS-u\",\n  \"unblock\": \"Odblokiraj\",\n  \"unblock_all\": \"Odblokiraj sve\",\n  \"unblock_for_this_client_only\": \"Odblokiraj samo za ovog klijenta\",\n  \"unknown_filter\": \"Nepoznati filtar {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} je dostupan! <0>Pritisnite ovdje</0> za više informacija.\",\n  \"update_failed\": \"Neuspješno automatsko ažuriranje. Molimo <a>pratite ove korake</a> za ručno ažuriranje.\",\n  \"update_now\": \"Ažuriraj sada\",\n  \"updated_custom_filtering_toast\": \"Prilagođena pravila uspješno su spremljena\",\n  \"updated_save_search_toast\": \"Ažurirane postavke sigurnog pretraživanja\",\n  \"updated_upstream_dns_toast\": \"Uzvodni poslužitelji uspješno su spremljeni\",\n  \"updates_checked\": \"Dostupna je nova verzija AdGuard Home-a\",\n  \"updates_version_equal\": \"AdGuard Home je ažuriran\",\n  \"upstream\": \"Upstream poslužitelj\",\n  \"upstream_dns\": \"Upstream DNS poslužitelji\",\n  \"upstream_dns_cache_configuration\": \"Konfiguracija predmemoriranja upstream DNS poslužitelja\",\n  \"upstream_dns_client_desc\": \"Ako ovo polje ostane prazno, AdGuard Home će upotrijebiti poslužitelje postavljene u <0>DNS postavkama</0>.\",\n  \"upstream_dns_configured_in_file\": \"Postavljeno u {{path}}\",\n  \"upstream_dns_help\": \"Unesite adrese poslužitelja po jednu u retku. <a>Saznajte više</a> o postavljanju uzlaznih DNS poslužitelja.\",\n  \"upstream_parallel\": \"Koristi paralelne upite kako bi ubrzali rješavanje istovremenim ispitavanjem svih upstream poslužitelja.\",\n  \"upstream_timeout\": \"Vrijeme čekanja na odgovore od upstream poslužitelja\",\n  \"upstream_timeout_desc\": \"Određuje broj sekundi čekanja na odgovor od uzvodnog poslužitelja\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Koristi AdGuard uslugu zaštite pregledavanja\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home provjerit će je li domena blokirana sigurnosnim web servisom za pregledavanje. Za izvođenje provjere koristit će API za pretraživanje prilagođen privatnosti: poslužitelju se šalje samo kratki prefiks naziva domene SHA256 hash.\",\n  \"use_adguard_parental\": \"Koristi web uslugu AdGuard roditeljske zaštite\",\n  \"use_adguard_parental_hint\": \"AdGuard Home provjeriti će sadrži li domena sadržaj za odrasle. Koristi isti API za zaštitu privatnosti kao i naša usluga zaštite pregledavanja.\",\n  \"use_private_ptr_resolvers_desc\": \"Razriješi PTR, SOA i NS zahtjeve za ARPA domene koje sadrže privatne IP adrese putem privatnih uzvodnih poslužitelja, DHCP-a, /etc/hostova itd. Ako je onemogućeno, AdGuard Home će na sve takve zahtjeve odgovoriti s NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Koristi privatne reverzne DNS razrješivače\",\n  \"use_saved_key\": \"Korištenje prethodno spremljenog ključa\",\n  \"username_label\": \"Korisničko ime\",\n  \"username_placeholder\": \"Unesite korisničko ime\",\n  \"validated_with_dnssec\": \"Potvrđeno s DNSSEC-om\",\n  \"version\": \"Verzija\",\n  \"version_request_error\": \"Ne uspješna provjera ažuriranja. Provjerite vašu Internetsku vezu.\",\n  \"wednesday\": \"Srijeda\",\n  \"wednesday_short\": \"Sri\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/hu.json",
    "content": "{\n  \"access_allowed_desc\": \"CIDR-ek, IP-címek vagy <a>ClientID-k</a> listája. Ha be van állítva, akkor az AdGuard Home csak azokat a lekérdezéseket engedélyezi, amelyek ezektől a kliensektől érkeznek.\",\n  \"access_allowed_title\": \"Engedélyezett kliensek\",\n  \"access_blocked_desc\": \"Ne keverje össze ezt a szűrőkkel. Az AdGuard Home az összes DNS kérést el fogja dobni, ami ezekkel a domainekkel megegyezik, és ezek a lekérések nem is fognak megjelenni a lekérdezési naplóban sem. Megadhatja a pontos domain neveket, a helyettesítő karaktereket vagy az URL szűrési szabályokat, pl. ennek megfelelően \\\"example.org\\\", \\\"*.example.org\\\", vagy \\\"||example.org^\\\".\",\n  \"access_blocked_title\": \"Nem engedélyezett domainek\",\n  \"access_desc\": \"Itt konfigurálhatja az AdGuard Home DNS-kiszolgáló hozzáférési szabályait\",\n  \"access_disallowed_desc\": \"CIDR-ek, IP-címek vagy <a>ClientID-k</a> listája. Ha be van állítva, akkor az AdGuard Home eldobja azokat a lekérdezéseket, amelyek ezektől a kliensektől érkeznek. Ha engedélyezett kliensek vannak ide bekonfigurálva, akkor pedig az a mező ki lesz hagyva.\",\n  \"access_disallowed_title\": \"Nem engedélyezett kliensek\",\n  \"access_settings_saved\": \"A hozzáférési beállítások sikeresen mentésre kerültek\",\n  \"access_title\": \"Hozzáférési beállítások\",\n  \"actions_table_header\": \"Műveletek\",\n  \"add_allowlist\": \"Engedélyezési lista hozzáadása\",\n  \"add_blocklist\": \"Blokkolási lista hozzáadása\",\n  \"add_custom_list\": \"Egyedi lista hozzáadása\",\n  \"add_persistent_client\": \"Hozzáadás állandó ügyfélként\",\n  \"address\": \"Cím\",\n  \"adg_will_drop_dns_queries\": \"Az AdGuard Home eldobja az összes DNS kérést erről a kliensről.\",\n  \"all_lists_up_to_date_toast\": \"Már minden lista naprakész\",\n  \"all_queries\": \"Minden kérés\",\n  \"allow_this_client\": \"Engedélyezés ennek a kliensnek\",\n  \"allowed\": \"Engedve\",\n  \"anonymize_client_ip\": \"Kliens IP-címének anonimizálása\",\n  \"anonymize_client_ip_desc\": \"Ne mentse el a kliens teljes IP-címét a naplókban és a statisztikákban\",\n  \"anonymizer_notification\": \"<0>Megjegyzés:</0> Az IP anonimizálás engedélyezve van. Az <1>Általános beállításoknál letilthatja</1> .\",\n  \"answer\": \"Válasz\",\n  \"apply_btn\": \"Alkalmaz\",\n  \"auto_clients_desc\": \"Az AdGuard Home-ot használó vagy esetleg használó eszközök IP-címeire vonatkozó információk. Ezeket az információkat több forrásból gyűjtik, beleértve a hosts fájlokat, a fordított DNS-t stb.\",\n  \"auto_clients_title\": \"Futási idejű kliensek\",\n  \"autofix_warning_list\": \"A következő feladatokat hajtja végre: <0>A DNSStubListener rendszer kikapcsolása</0><0>Beállítja a DNS-kiszolgáló címét 127.0.0.1-re.</0><0>Lecseréli az /etc/resolv.conf szimbolikus útvonalat erre: /run/systemd/resolve/resolv.conf</0><0>A DNSStubListener leállítása (a rendszer által feloldott szolgáltatás újratöltése)</0>\",\n  \"autofix_warning_result\": \"Mindennek eredményeként az Ön rendszeréből származó összes DNS-kérést alapértelmezés szerint az AdGuard Home dolgozza fel.\",\n  \"autofix_warning_text\": \"Ha a \\\"Javítás\\\" lehetőségre kattint, az AdGuard Home megpróbálja beállítani a rendszerét, hogy használja az AdGuard Home DNS szervert.\",\n  \"average_processing_time\": \"Átlagos feldolgozási idő\",\n  \"average_processing_time_hint\": \"A DNS lekérdezések feldolgozásához szükséges átlagos idő milliszekundumban\",\n  \"average_upstream_response_time\": \"Átlagos upstream válaszidő\",\n  \"back\": \"Vissza\",\n  \"block\": \"Blokkolás\",\n  \"block_all\": \"Összes blokkolása\",\n  \"block_domain_use_filters_and_hosts\": \"Domainek blokkolása szűrők és hosztfájlok használatával\",\n  \"block_for_this_client_only\": \"Tiltás csak ennek a kliensnek\",\n  \"block_services\": \"Adott szolgáltatások blokkolása\",\n  \"blocked_adult_websites\": \"Szülői felügyelet által blokkolva\",\n  \"blocked_by\": \"<0>Szűrők által blokkolt</0>\",\n  \"blocked_by_cname_or_ip\": \"CNAME vagy IP által blokkolva\",\n  \"blocked_by_response\": \"Blokkolva a CNAME vagy a válasz IP-címe alapján\",\n  \"blocked_response_ttl\": \"Tiltott válasz TTL-je\",\n  \"blocked_response_ttl_desc\": \"Meghatározza, hogy a klienseknek hány másodpercig kell gyorsítótárazniuk a szűrt választ\",\n  \"blocked_safebrowsing\": \"Blokkolva a Biztonságos böngészés által\",\n  \"blocked_service\": \"Blokkolt szolgáltatás\",\n  \"blocked_services\": \"Blokkolt szolgáltatások\",\n  \"blocked_services_desc\": \"Lehetővé teszi a népszerű oldalak és szolgáltatások blokkolását.\",\n  \"blocked_services_global\": \"A globálisan tiltott szolgáltatások használata\",\n  \"blocked_services_saved\": \"Blokkolt szolgáltatások sikeresen mentve\",\n  \"blocked_threats\": \"Blokkolt fenyegetések\",\n  \"blocking_ipv4\": \"IPv4 blokkolása\",\n  \"blocking_ipv4_desc\": \"A blokkolt A kéréshez visszaadandó IP-cím\",\n  \"blocking_ipv6\": \"IPv6 blokkolása\",\n  \"blocking_ipv6_desc\": \"A blokkolt AAAA kéréshez visszaadandó IP-cím\",\n  \"blocking_mode\": \"Blokkolás módja\",\n  \"blocking_mode_custom_ip\": \"Egyedi IP: Válasz egy kézzel beállított IP címmel\",\n  \"blocking_mode_default\": \"Alapértelmezés: Válaszoljon nulla IP-címmel (vagyis 0.0.0.0 az A-hoz, :: pedig az AAAA-hoz), amikor a blokkolás egy adblock-stílusú szabállyal történik; illetve válaszoljon egy, a szabály által meghatározott IP címmel, amikor a blokkolás egy /etc/hosts stílusú szabállyal történik\",\n  \"blocking_mode_null_ip\": \"Null IP: Nullákból álló IP-címmel válaszol (0.0.0.0 for A; :: for AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Az NXDOMAIN kóddal fog válaszolni\",\n  \"blocking_mode_refused\": \"REFUSED: Válaszoljon REFUSED kóddal\",\n  \"blocklist\": \"Tiltólista\",\n  \"bootstrap_dns\": \"Bootstrap DNS kiszolgálók\",\n  \"bootstrap_dns_desc\": \"A DNS-kiszolgálók IP-címei, amelyek a DoH/DoT-feloldók IP-címeinek feloldására szolgálnak, amelyeket upstreamként megadott. Megjegyzések nem megengedettek.\",\n  \"cache_cleared\": \"A DNS gyorsítótár sikeresen törlődött\",\n  \"cache_enabled\": \"Gyorsítótár engedélyezése\",\n  \"cache_enabled_desc\": \"A DNS-válaszok helyben történő tárolása.\",\n  \"cache_optimistic\": \"Optimista gyorsítótár\",\n  \"cache_optimistic_desc\": \"Lehetővé teszi, hogy az AdGuard Home a gyorsítótárból válaszoljon, még abban az esetben is, ha az ott lévő bejegyzések lejértak, és próbálja meg frissíteni őket.\",\n  \"cache_size\": \"Gyorsítótár mérete\",\n  \"cache_size_desc\": \"DNS gyorsítótár mérete (bájtokban).\",\n  \"cache_size_validation\": \"A gyorsítótár méretének engedélyezve nullánál nagyobbnak kell lennie.\",\n  \"cache_ttl_max_override\": \"A maximális TTL felülírása\",\n  \"cache_ttl_max_override_desc\": \"Állítson be egy maximális TTL értéket (másodpercben) a DNS gyorsítótár bejegyzéseihez.\",\n  \"cache_ttl_min_override\": \"A minimális TTL felülírása\",\n  \"cache_ttl_min_override_desc\": \"Megnöveli a DNS kiszolgálótól kapott rövid TTL értékeket (másodpercben), ha gyorsítótárazza a DNS kéréseket.\",\n  \"cancel_btn\": \"Mégse\",\n  \"category_label\": \"Kategória\",\n  \"check\": \"Ellenőrzés\",\n  \"check_client_id\": \"Kliens azonosító (ClientID vagy IP-cím)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Ellenőrzi, hogy a hosztnév szűrve van-e.\",\n  \"check_dhcp_servers\": \"DHCP szerverek keresése\",\n  \"check_dns_record\": \"DNS rekord típusának kiválasztása\",\n  \"check_enter_client_id\": \"Adja meg a kliens azonosítót\",\n  \"check_hostname\": \"Gazda név vagy domain név\",\n  \"check_ip\": \"IP-címek: {{ip}}\",\n  \"check_not_found\": \"Nem található az Ön szűrőlistái között\",\n  \"check_reason\": \"Indok: {{reason}}\",\n  \"check_service\": \"Szolgáltatás neve: {{service}}\",\n  \"check_title\": \"Szűrés ellenőrzése\",\n  \"check_updates_btn\": \"Frissítések keresése\",\n  \"check_updates_now\": \"Frissítések ellenőrzése most\",\n  \"choose_allowlist\": \"Engedélyezési lista választás\",\n  \"choose_blocklist\": \"Blokkolási lista választás\",\n  \"choose_from_list\": \"Választás a listából\",\n  \"city\": \"Város\",\n  \"clear_cache\": \"Gyorsítótár törlése\",\n  \"click_to_view_queries\": \"Kattintson a lekérésekért\",\n  \"client_add\": \"Kliens hozzáadása\",\n  \"client_added\": \"A(z) \\\"{{key}}\\\" kliens sikeresen hozzá lett adva\",\n  \"client_blocked\": \"A(z) \\\"{{ip}}\\\" kliens sikeresen blokkolva\",\n  \"client_confirm_block\": \"Biztosan blokkolni szeretné a(z) \\\"{{ip}}\\\" klienst?\",\n  \"client_confirm_delete\": \"Biztosan törölni szeretné a(z) \\\"{{key}}\\\" klienst?\",\n  \"client_confirm_unblock\": \"Biztosan fel szeretné oldani a(z) \\\"{{ip}}\\\" kliens blokkolását?\",\n  \"client_deleted\": \"A(z) \\\"{{key}}\\\" kliens sikeresen el lett távolítva\",\n  \"client_details\": \"Kliens részletei\",\n  \"client_edit\": \"Kliens módosítása\",\n  \"client_global_settings\": \"Globális beállítások használata\",\n  \"client_id\": \"Kliens azonosító (ClientID)\",\n  \"client_id_desc\": \"A kliensek a ClientID által kerülnek azonosításra. Tudjon meg többet arról <a>ide kattintva</a>, hogy miként történik a kliensek azonosítása.\",\n  \"client_id_placeholder\": \"Kliens azonosító (ClientID) megadása\",\n  \"client_identifier\": \"Azonosító\",\n  \"client_identifier_desc\": \"A kliensek azonosíthatók az IP cím, CIDR, MAC cím, vagy a ClientID (ami használható DoT/DoH/DoQ esetén) alapján. Tudjon meg többet arról <0>ide kattintva</0>, hogy miként lehet azonosítani a klienseket.\",\n  \"client_name\": \"Ügyfél {{id}}\",\n  \"client_new\": \"Új kliens\",\n  \"client_settings\": \"Kliens beállítások\",\n  \"client_table_header\": \"Kliens\",\n  \"client_unblocked\": \"A(z) \\\"{{ip}}\\\" kliens blokkolása sikeresen feloldva\",\n  \"client_updated\": \"A(z) \\\"{{key}}\\\" kliens sikeresen frissítve lett\",\n  \"clients_desc\": \"Állítsa be az AdGuard Home-ban fenntartott kliens rekordokat az egyes eszközeihez\",\n  \"clients_not_found\": \"Nem található kliens\",\n  \"clients_title\": \"Fenntartott kliensek\",\n  \"compact\": \"Kompakt\",\n  \"config_successfully_saved\": \"A beállítások sikeresen el lettek mentve\",\n  \"configure\": \"Beállítás\",\n  \"confirm_dns_cache_clear\": \"Biztos benne, hogy törölni szeretné a DNS-gyorsítótárat?\",\n  \"confirm_static_ip\": \"Az AdGuard Home beállítja az {{ip}} IP-címet az Ön statikus IP-címének. Biztosan folytatni kívánja?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"Ország\",\n  \"custom_filter_rules\": \"Egyéni szűrési szabályok\",\n  \"custom_filter_rules_hint\": \"Adjon meg egy szabályt egy sorban. Használhat egyszerű hirdetésblokkolási szabályokat vagy hosztfájl szintaxist.\",\n  \"custom_filtering_rules\": \"Egyéni szűrési szabályok\",\n  \"custom_ip\": \"Egyedi IP\",\n  \"custom_retention_input\": \"Adja meg a megőrzést órákban\",\n  \"custom_rotation_input\": \"Írja be a forgatást órákban\",\n  \"dashboard\": \"Irányítópult\",\n  \"date\": \"Dátum\",\n  \"default\": \"Alapértelmezett\",\n  \"delete_confirm\": \"Biztosan törli a \\\"{{key}}\\\" -t?\",\n  \"delete_table_action\": \"Törlés\",\n  \"descr\": \"Leírás\",\n  \"details\": \"Részletek\",\n  \"dhcp_add_static_lease\": \"Statikus bérlet hozzáadása\",\n  \"dhcp_config_saved\": \"DHCP beállítások sikeresen el lettek mentve\",\n  \"dhcp_description\": \"Ha a router nem nyújt DHCP beállításokat, akkor használhatja helyette az AdGuard saját, beépített DHCP szerverét.\",\n  \"dhcp_disable\": \"DHCP szerver letiltása\",\n  \"dhcp_dynamic_ip_found\": \"A rendszer dinamikus IP-cím konfigurációt használ az <0>{{interfaceName}}</0> interfészhez. A DHCP szerver használatához statikus IP-címet kell beállítani. Jelenlegi IP-címe: <0>{{ipAddress}}</0>. Automatikusan beállítjuk ezt az IP címet statikusnak, ha rányom a DHCP engedélyezése gombra.\",\n  \"dhcp_edit_static_lease\": \"Statikus bérlet szerkesztése\",\n  \"dhcp_enable\": \"DHCP szerver engedélyezése\",\n  \"dhcp_error\": \"Az AdGuard Home nem tudta megállapítani, hogy van-e másik aktív DHCP-szerver a hálózaton\",\n  \"dhcp_form_gateway_input\": \"Átjáró IP\",\n  \"dhcp_form_lease_input\": \"Bérlési idő\",\n  \"dhcp_form_lease_title\": \"DHCP bérlési ideje (másodpercben)\",\n  \"dhcp_form_range_end\": \"Tartomány vége\",\n  \"dhcp_form_range_start\": \"Tartomány kezdete\",\n  \"dhcp_form_range_title\": \"IP-címek tartománya\",\n  \"dhcp_form_subnet_input\": \"Alhálózati maszk\",\n  \"dhcp_found\": \"Egy aktív DHCP szerver található a hálózaton. Nem biztonságos a beépített DHCP szerver engedélyezése.\",\n  \"dhcp_hardware_address\": \"Hardvercím\",\n  \"dhcp_interface_select\": \"DHCP interfész kiválasztása\",\n  \"dhcp_ip_addresses\": \"IP-címek\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 Beállítások\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 Beállítások\",\n  \"dhcp_lease_added\": \"Statikus bérlet \\\"{{key}}\\\" sikeresen hozzáadva\",\n  \"dhcp_lease_deleted\": \"Statikus bérlet \\\"{{key}}\\\" sikeresen törölve\",\n  \"dhcp_lease_updated\": \"Statikus bérlet \\\"{{key}}\\\" sikeresen frissítve\",\n  \"dhcp_leases\": \"DHCP bérletek\",\n  \"dhcp_leases_not_found\": \"Nem találhatóak DHCP bérletek\",\n  \"dhcp_new_static_lease\": \"Új statikus bérlet\",\n  \"dhcp_not_found\": \"Biztonságos a beépített DHCP-kiszolgáló engedélyezése, mert az AdGuard Home nem talált aktív DHCP-kiszolgálót a hálózaton. Javasoljuk azonban, hogy ellenőrizze kézileg is, mivel az automatikus tesztünk jelenleg nem ad 100%-os garanciát.\",\n  \"dhcp_reset\": \"Biztosan visszaállítja a DHCP beállításokat?\",\n  \"dhcp_reset_leases\": \"Bérletek alaphelyzetbe\",\n  \"dhcp_reset_leases_confirm\": \"Biztosan visszaállítja az összes bérletet?\",\n  \"dhcp_reset_leases_success\": \"DHCP bérletek alaphelyzetbe állítva\",\n  \"dhcp_settings\": \"DHCP beállítások\",\n  \"dhcp_static_ip_error\": \"A DHCP szerver használatához statikus IP-címet kell beállítani. Nem sikerült meghatározni, hogy ez a hálózati interfész statikus IP-cím használatával van-e beállítva. Állítson be kézzel egy statikus IP-címet.\",\n  \"dhcp_static_leases\": \"Statikus DHCP bérletek\",\n  \"dhcp_static_leases_not_found\": \"Nem találhatóak statikus DHCP bérletek\",\n  \"dhcp_table_expires\": \"Lejár\",\n  \"dhcp_table_hostname\": \"Hosztnév\",\n  \"dhcp_title\": \"DHCP szerver (kísérleti!)\",\n  \"dhcp_warning\": \"Ha engedélyezni szeretné a DHCP-kiszolgálót, ellenőrizze, hogy nincs-e más aktív DHCP-kiszolgáló a hálózaton, mert ez megszakíthatja a hálózati eszközök internetkapcsolatát.\",\n  \"disable_for_hours\": \"{{count}} óráig\",\n  \"disable_for_hours_plural\": \"{{count}} óráig\",\n  \"disable_for_minutes\": \"{{count}} percig\",\n  \"disable_for_minutes_plural\": \"{{count}} percig\",\n  \"disable_for_seconds\": \"{{count}} másodpercig\",\n  \"disable_for_seconds_plural\": \"{{count}} másodpercig\",\n  \"disable_ipv6\": \"IPv6 címek feloldásának tiltása\",\n  \"disable_ipv6_desc\": \"Dobja el az IPv6-címekre vonatkozó összes DNS-lekérdezést (AAAA típusú), és távolítsa el az IPv6-tippeket a HTTPS-válaszokból.\",\n  \"disable_notify_for_hours\": \"Kapcsolja ki a védelmet {{count}} órára\",\n  \"disable_notify_for_hours_plural\": \"Kapcsolja ki a védelmet {{count}} órára\",\n  \"disable_notify_for_minutes\": \"Kapcsolja ki a védelmet {{count}} percre\",\n  \"disable_notify_for_minutes_plural\": \"Kapcsolja ki a védelmet {{count}} percre\",\n  \"disable_notify_for_seconds\": \"Kapcsolja ki a védelmet {{count}} másodpercre\",\n  \"disable_notify_for_seconds_plural\": \"Kapcsolja ki a védelmet {{count}} másodpercre\",\n  \"disable_notify_until_tomorrow\": \"Holnapig kapcsolja ki a védelmet\",\n  \"disable_protection\": \"Védelem letiltása\",\n  \"disable_rewrites\": \"Átírási szabályok kikapcsolása\",\n  \"disable_until_tomorrow\": \"Holnapig\",\n  \"disabled\": \"Kikapcsolva\",\n  \"disabled_dhcp\": \"DHCP szerver letiltva\",\n  \"disabled_filtering_toast\": \"Szűrés letiltva\",\n  \"disabled_parental_toast\": \"Szülői felügyelet letiltva\",\n  \"disabled_protection\": \"Védelem letiltva\",\n  \"disabled_safe_browsing_toast\": \"Bbiztonságos böngészés letiltva\",\n  \"disabled_safe_search_toast\": \"Biztonságos keresés letiltva\",\n  \"disallow_this_client\": \"Tiltás ennek a kliensnek\",\n  \"dns_addresses\": \"DNS címek\",\n  \"dns_allowlists\": \"DNS engedélyezési listák\",\n  \"dns_allowlists_desc\": \"A DNS engedélyezési listán szereplő domainek engedélyezve lesznek, akkor is, ha szerepelnek bármelyik blokkolási listán.\",\n  \"dns_blocklists\": \"DNS blokkolási listák\",\n  \"dns_blocklists_desc\": \"Az AdGuard Home blokkolni fogja azokat a domaineket, amik szerepelnek a blokkolási listán.\",\n  \"dns_cache_config\": \"DNS gyorsítótár beállításai\",\n  \"dns_cache_config_desc\": \"Itt tudja konfigurálni a DNS gyorsítótárat\",\n  \"dns_cache_size\": \"DNS gyorsítótár mérete, bájtokban\",\n  \"dns_config\": \"DNS szerver beállításai\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS Adatvédelem\",\n  \"dns_providers\": \"Itt van az <0>ismert DNS szolgáltatók listája</0>, amelyekből választhat.\",\n  \"dns_query\": \"DNS lekérdezés\",\n  \"dns_rewrites\": \"DNS átírások\",\n  \"dns_settings\": \"DNS beállítások\",\n  \"dns_start\": \"A DNS szerver indul\",\n  \"dns_status_error\": \"Hiba történt a DNS szerver állapotának ellenőrzésekor\",\n  \"dns_test_not_ok_toast\": \"Szerver \\\"{{key}}\\\": nem használható, ellenőrizze, hogy helyesen írta-e be\",\n  \"dns_test_ok_toast\": \"A megadott DNS-kiszolgálók megfelelően működnek\",\n  \"dns_test_parsing_error_toast\": \"Szekció {{section}}: sor {{line}}: nem használható, ellenőrizze, hogy helyesen írta-e be\",\n  \"dns_test_warning_toast\": \"A \\\"{{key}}\\\" feltöltés nem válaszol a tesztkérelmekre, és lehet, hogy nem működik megfelelően\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"DNSSEC engedélyezése\",\n  \"dnssec_enable_desc\": \"DNSSEC flag beállítása a kimenő DNS kérésekhez, majd az eredmény ellenőrzése (DNSSEC-engedélyezett feloldó szükséges).\",\n  \"domain\": \"Domain\",\n  \"domain_desc\": \"Adja meg a domain nevet vagy a helyettesítő karaktert ahhoz a címhez, amit át kíván íratni.\",\n  \"domain_name_table_header\": \"Domain név\",\n  \"domain_or_client\": \"Domain vagy kliens\",\n  \"down\": \"Nem elérhető\",\n  \"download_mobileconfig\": \"Konfigurációs fájl letöltése\",\n  \"download_mobileconfig_doh\": \".mobileconfig letöltése DNS-over-HTTPS-hez\",\n  \"download_mobileconfig_dot\": \".mobileconfig letöltése DNS-over-TLS-hez\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Engedélyezési lista módosítása\",\n  \"edit_blocklist\": \"Blokkolási lista módosítása\",\n  \"edit_table_action\": \"Szerkesztés\",\n  \"edns_cs_desc\": \"Adja hozzá az EDNS Client Subnet beállítást (ECS) a felfelé irányuló kérésekhez, és naplózza a kliensek által küldött értékeket a lekérdezési naplóban.\",\n  \"edns_enable\": \"EDNS kliens alhálózat engedélyezése\",\n  \"edns_use_custom_ip\": \"Használjon egyéni IP-címet az EDNS-hez\",\n  \"edns_use_custom_ip_desc\": \"Engedélyezze az egyéni IP-cím használatát az EDNS-hez\",\n  \"elapsed\": \"Eltelt\",\n  \"empty_response_status\": \"Üres\",\n  \"enable_protection\": \"Védelem engedélyezése\",\n  \"enable_protection_timer\": \"A védelem {{time}}-kor aktiválódik\",\n  \"enable_rewrites\": \"Átírási szabályok engedélyezése\",\n  \"enable_upstream_dns_cache\": \"A DNS gyorsítótárazásának engedélyezése az ügyfél egyéni upstream konfigurációjához\",\n  \"enabled_dhcp\": \"DHCP szerver engedélyezve\",\n  \"enabled_filtering_toast\": \"Szűrés engedélyezve\",\n  \"enabled_parental_toast\": \"Szülői felügyelet engedélyezve\",\n  \"enabled_protection\": \"Védelem engedélyezve\",\n  \"enabled_safe_browsing_toast\": \"Biztonságos böngészés engedélyezve\",\n  \"enabled_save_search_toast\": \"Biztonságos keresés engedélyezve\",\n  \"enabled_table_header\": \"Engedélyezve\",\n  \"encryption_certificate_path\": \"Tanúsítvány útvonala\",\n  \"encryption_certificates\": \"Tanúsítványok\",\n  \"encryption_certificates_desc\": \"A titkosítás használatához érvényes SSL tanúsítványláncot kell megadnia a domainjéhez. Ingyenes tanúsítványt kaphat a <0>{{link}}</0> webhelyen, vagy megvásárolhatja az egyik megbízható tanúsítványkibocsátó hatóságtól.\",\n  \"encryption_certificates_input\": \"Másolja be ide a PEM-kódolt tanúsítványt.\",\n  \"encryption_certificates_source_content\": \"Tanúsítvány tartalmának megadása\",\n  \"encryption_certificates_source_path\": \"Tanúsítványfájl útvonalának megadása\",\n  \"encryption_chain_invalid\": \"A tanúsítványlánc érvénytelen\",\n  \"encryption_chain_valid\": \"A tanúsítványlánc érvényes\",\n  \"encryption_config_saved\": \"Titkosítási beállítások mentve\",\n  \"encryption_desc\": \"Titkosítás (HTTPS/QUIC/TLS) támogatása mind a DNS, mind pedig a webes admin felület számára\",\n  \"encryption_doq\": \"DNS-over-QUIC port\",\n  \"encryption_doq_desc\": \"Ha ez a port be van állítva, akkor az AdGuard Home egy DNS-over-QUIC szerverként fog futni ezen a porton. \",\n  \"encryption_dot\": \"DNS-over-TLS port\",\n  \"encryption_dot_desc\": \"Ha ez a port be van állítva, az AdGuard Home DNS-over-TLS szerverként tud futni ezen a porton.\",\n  \"encryption_enable\": \"Titkosítás engedélyezése (HTTPS, DNS-over-HTTPS, és DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Ha a titkosítás engedélyezve van, az AdGuard Home admin felülete működik HTTPS-en keresztül, és a DNS szerver is várja a kéréseket DNS-over-HTTPS-en, valamint DNS-over-TLS-en keresztül.\",\n  \"encryption_expire\": \"Lejár\",\n  \"encryption_hostnames\": \"Hosztnevek\",\n  \"encryption_https\": \"HTTPS port\",\n  \"encryption_https_desc\": \"Ha a HTTPS port konfigurálva van, akkor az AdGuard Home admin felülete elérhető lesz a HTTPS-en keresztül, és ezenkívül DNS-over-HTTPS-t is biztosít a '/dns-query' helyen.\",\n  \"encryption_issuer\": \"Kibocsátó\",\n  \"encryption_key\": \"Privát kulcs\",\n  \"encryption_key_input\": \"Másolja ki és illessze be ide a tanúsítványa PEM-kódolt privát kulcsát.\",\n  \"encryption_key_invalid\": \"Ez egy érvénytelen {{type}} privát kulcs\",\n  \"encryption_key_source_content\": \"Privát kulcs tartalmának megadása\",\n  \"encryption_key_source_path\": \"Privát kulcsfájl útvonalának beállítása\",\n  \"encryption_key_valid\": \"Ez egy érvényes {{type}} privát kulcs\",\n  \"encryption_plain_dns_desc\": \"Az egyszerű DNS alapértelmezés szerint be van kapcsolva. Kikapcsolhatja, hogy az összes eszközt kényszerítse a titkosított DNS használatára. Ehhez legalább egy titkosított DNS protokollt engedélyeznie kell\",\n  \"encryption_plain_dns_enable\": \"Egyszerű DNS engedélyezése\",\n  \"encryption_plain_dns_error\": \"Az egyszerű DNS letiltásához engedélyezzen legalább egy titkosított DNS protokollt\",\n  \"encryption_private_key_path\": \"Privát kulcs útvonala\",\n  \"encryption_redirect\": \"Automatikus átirányítás HTTPS kapcsolatra\",\n  \"encryption_redirect_desc\": \"Ha be van jelölve, az AdGuard Home automatikusan átirányítja a HTTP kapcsolatokat a biztonságos HTTPS protokollra.\",\n  \"encryption_reset\": \"Biztosan visszaállítja a titkosítási beállításokat?\",\n  \"encryption_server\": \"Szerver neve\",\n  \"encryption_server_desc\": \"Ha be van állítva, az AdGuard Home észleli az ClientID-ket, válaszol a DDR-lekérdezésekre, és további kapcsolatellenőrzéseket végez. Ha nincs beállítva, ezek a funkciók le vannak tiltva. Meg kell egyeznie a tanúsítványban szereplő DNS-nevek egyikével.\",\n  \"encryption_server_enter\": \"Adja meg az Ön domain címét\",\n  \"encryption_settings\": \"Titkosítási beállítások\",\n  \"encryption_status\": \"Állapot\",\n  \"encryption_subject\": \"Tárgy\",\n  \"encryption_title\": \"Titkosítás\",\n  \"encryption_warning\": \"Figyelmeztetés\",\n  \"enforce_safe_search\": \"Biztonságos keresés használata\",\n  \"enforce_save_search_hint\": \"Az AdGuard Home a következő keresőmotorokban biztosítja a biztonságos keresést: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex és Pixabay.\",\n  \"enforced_save_search\": \"Kényszerített biztonságos keresés\",\n  \"enter_cache_size\": \"Adja meg a gyorsítótár méretét\",\n  \"enter_cache_ttl_max_override\": \"Adja meg a maximális TTL-t (másodpercben)\",\n  \"enter_cache_ttl_min_override\": \"Adja meg a minimális TTL-t (másodpercben)\",\n  \"enter_name_hint\": \"Adja meg a nevet\",\n  \"enter_url_or_path_hint\": \"Írjon be egy URL-t vagy egy útvonalat a listához\",\n  \"enter_valid_allowlist\": \"Adjon meg egy érvényes URL-t az engedélyezési listához.\",\n  \"enter_valid_blocklist\": \"Adjon meg egy érvényes URL-t a blokkolási listához.\",\n  \"error_details\": \"Hiba részletei\",\n  \"example_comment\": \"! Ide írhat egy megjegyzést.\",\n  \"example_comment_hash\": \"# Ez is egy megjegyzés.\",\n  \"example_comment_meaning\": \"csak egy megjegyzés;\",\n  \"example_meaning_filter_block\": \"letiltja a hozzáférést az example.org domainhez, valamint annak az összes aldomainjéhez is;\",\n  \"example_meaning_filter_whitelist\": \"feloldja a hozzáférést az example.org domainhez, valamint annak az összes aldomainjéhez is;\",\n  \"example_meaning_host_block\": \"az example.org-ot a 127.0.0.1-es címre oldja fel (de az aldomainjeit nem);\",\n  \"example_multiple_upstreams_reserved\": \"több upstream szerver <0>adott domainekhez</0>;\",\n  \"example_regex_meaning\": \"blokkolja a hozzáférést azokhoz a domainekhez, amik illeszkednek a megadott reguláris kifejezésre.\",\n  \"example_rewrite_domain\": \"csak ehhez a domainhez írja át a válaszokat.\",\n  \"example_rewrite_wildcard\": \"az <0>example.org</0> összes aldomainjéhez átírja a válaszokat.\",\n  \"example_upstream_comment\": \"egy megjegyzés.\",\n  \"example_upstream_doh\": \"titkosított <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"titkosított DNS-over-HTTPS kényszerített <0>HTTP/3-mal</0> és nincs visszalépés a HTTP/2-re vagy az alább;\",\n  \"example_upstream_doq\": \"titkosított <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"titkosított <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"hagyományos DNS (UDP felett);\",\n  \"example_upstream_regular_port\": \"normál DNS (UDP-n keresztül, porttal);\",\n  \"example_upstream_reserved\": \"Megadhat egy DNS kiszolgálót <0>egy adott domainhez vagy domainekhez</0>\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> a <1>DNSCrypt</1> vagy <2>DNS-over-HTTPS</2> feloldókhoz;\",\n  \"example_upstream_tcp\": \"hagyományos DNS (TCP felett);\",\n  \"example_upstream_tcp_hostname\": \"normál DNS (TCP felett, hostnév);\",\n  \"example_upstream_tcp_port\": \"normál DNS (TCP-n keresztül, porttal);\",\n  \"example_upstream_udp\": \"normál DNS (UDP felett, hostnév);\",\n  \"examples_title\": \"Példák\",\n  \"fallback_dns_desc\": \"Azoknak a tartalék DNS-szervereknek a listája, amelyeket akkor használnak, ha a felsőbbrendű DNS-szerverek nem válaszolnak. A szintaxis ugyanaz, mint a fő felsőbbrendű mezőben.\",\n  \"fallback_dns_placeholder\": \"Adjon meg egy alternatív DNS szervert soronként\",\n  \"fallback_dns_title\": \"Tartalék DNS-szerverek\",\n  \"faq\": \"GYIK\",\n  \"fastest_addr\": \"Leggyorsabb IP-cím\",\n  \"fastest_addr_desc\": \"Várjon a <b>minden</b> DNS szerver válaszára, mérje meg a TCP kapcsolat sebességét minden szervernél, és adja vissza a leggyorsabb kapcsolatú szerver IP-címét.<br/>Ez a mód jelentősen lelassíthatja a DNS lekérdezéseket, ha egy vagy több upstream szerver nem válaszol. Győződjön meg róla, hogy upstream szerverei stabilak és az upstream időtúllépés alacsony.\",\n  \"filter\": \"Szűrő\",\n  \"filter_added_successfully\": \"A lista sikeresen hozzá lett adva\",\n  \"filter_allowlist\": \"FIGYELMEZTETÉS: Ez a művelet a \\\"{{disallowed_rule}}\\\" szabályt is kizárja az engedélyezett ügyfelek listájából.\",\n  \"filter_category_general\": \"Általános\",\n  \"filter_category_general_desc\": \"Olyan listák, amelyek blokkolják a nyomkövetést és a hirdetéseket a legtöbb eszközön\",\n  \"filter_category_other\": \"Egyéb\",\n  \"filter_category_other_desc\": \"További tiltólisták\",\n  \"filter_category_regional\": \"Regionális\",\n  \"filter_category_regional_desc\": \"Olyan listák, amelyek a regionális hirdetések, valamint a nyomkövető szerverek ellen vannak kifejlesztve\",\n  \"filter_category_security\": \"Biztonság\",\n  \"filter_category_security_desc\": \"Kifejezetten a rosszindulatú, adathalász és átverős domainek blokkolására tervezett listák\",\n  \"filter_removed_successfully\": \"A lista sikeresen el lett távolítva\",\n  \"filter_updated\": \"A lista sikeresen frissítve lett\",\n  \"filtered\": \"Megszűrt\",\n  \"filtered_custom_rules\": \"Szűrve van az egyéni szűrési szabályok alapján\",\n  \"filtering_rules_learn_more\": \"<0>Tudjon meg többet</0> a saját hosztlisták létrehozásáról.\",\n  \"filters\": \"Szűrők\",\n  \"filters_and_hosts_hint\": \"Az AdGuard Home tudja értelmezni az alapvető hirdetésblokkolási szabályok, valamint a hosztfájlok szintaxisát.\",\n  \"filters_block_toggle_hint\": \"A <a> szűrőbeállításoknál</a> megadhatja a blokkolási szabályokat.\",\n  \"filters_configuration\": \"Szűrők beállításai\",\n  \"filters_enable\": \"Szűrők engedélyezése\",\n  \"filters_interval\": \"Szűrők frissítési gyakorisága:\",\n  \"fix\": \"Állandó\",\n  \"for_last_days\": \"az utóbbi {{count}} napban\",\n  \"for_last_days_plural\": \"az utóbbi {{count}} napban\",\n  \"for_last_hours\": \"az utolsó {{count}} órában\",\n  \"for_last_hours_plural\": \"az utolsó {{count}} órában\",\n  \"forgot_password\": \"Elfelejtette a jelszót?\",\n  \"forgot_password_desc\": \"Kérjük, hogy kövesse <0>ezeket a lépéseket</0> a jelszó visszaállításához.\",\n  \"form_add_id\": \"Azonosító hozzáadása\",\n  \"form_answer\": \"Adjon meg egy IP-címet vagy egy domain nevet\",\n  \"form_client_name\": \"Adja meg a kliens nevét\",\n  \"form_domain\": \"Adja meg a domain nevet vagy a helyettesítő karaktert\",\n  \"form_enter_blocked_response_ttl\": \"Írja be a blokkolt válasz TTL-jét (másodpercben)\",\n  \"form_enter_host\": \"Adja meg a hosztnevet\",\n  \"form_enter_hostname\": \"Adja meg a hosztnevet\",\n  \"form_enter_id\": \"Azonosító megadása\",\n  \"form_enter_ip\": \"IP-cím megadása\",\n  \"form_enter_mac\": \"MAC-cím megadása\",\n  \"form_enter_rate_limit\": \"Adja meg a kérések maximális számát\",\n  \"form_enter_rate_limit_subnet_len\": \"Adja meg az alhálózati előtag hosszát a sebességkorlátozáshoz\",\n  \"form_enter_subnet_ip\": \"Adjon meg egy IP címet az alhálózatban \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Adja meg az upstream szerver időtúllépési időt másodpercekben\",\n  \"form_error_answer_format\": \"Érvénytelen válasz formátum\",\n  \"form_error_client_id_format\": \"A ClientID (kliens azonosító) csak számokat, kisbetűket és kötőjeleket tartalmazhat\",\n  \"form_error_domain_format\": \"Érvénytelen domain formátum\",\n  \"form_error_equal\": \"Nem egyezhetnek\",\n  \"form_error_gateway_ip\": \"A bérleti szerződés nem tartalmazhatja az átjáró IP-címét\",\n  \"form_error_ip4_format\": \"Érvénytelen IPv4 cím\",\n  \"form_error_ip4_gateway_format\": \"Az átjáróhoz (gateway) érvénytelen IPv4 cím lett megadva\",\n  \"form_error_ip6_format\": \"Érvénytelen IPv6 cím\",\n  \"form_error_ip_format\": \"Érvénytelen IP-cím\",\n  \"form_error_mac_format\": \"Érvénytelen MAC cím\",\n  \"form_error_password\": \"A jelszavak nem egyeznek\",\n  \"form_error_password_length\": \"A jelszó legyen {{min}} és {{max}} karakter között\",\n  \"form_error_port\": \"Adjon meg egy érvényes portot\",\n  \"form_error_port_range\": \"Adjon meg egy portszámot a 80-65535 tartományon belül\",\n  \"form_error_port_unsafe\": \"Ez a port nem biztonságos\",\n  \"form_error_positive\": \"0-nál nagyobbnak kell lennie\",\n  \"form_error_required\": \"Kötelező mező\",\n  \"form_error_server_name\": \"Érvénytelen szervernév\",\n  \"form_error_subnet\": \"A(z) \\\"{{cidr}}\\\" alhálózat nem tartalmazza a(z) \\\"{{ip}}\\\" IP címet\",\n  \"form_error_url_format\": \"Érvénytelen URL formátum\",\n  \"form_error_url_or_path_format\": \"Helytelen URL vagy abszolút elérési útvonal a listához\",\n  \"form_select_tags\": \"Válasszon kliens címkéket\",\n  \"found_in_known_domain_db\": \"Benne van az ismert domainek listájában.\",\n  \"friday\": \"Péntek\",\n  \"friday_short\": \"Pén\",\n  \"gateway_or_subnet_invalid\": \"Az alhálózati maszk érvénytelen\",\n  \"general_settings\": \"Általános beállítások\",\n  \"general_statistics\": \"Általános statisztikák\",\n  \"get_started\": \"Kezdés\",\n  \"greater_range_start_error\": \"Nagyobbnak kell lennie, mint a tartomány kezdete\",\n  \"homepage\": \"Honlap\",\n  \"host_whitelisted\": \"Ez a hoszt a kivételek között szerepel\",\n  \"ignore_domains\": \"Figyelmen kívül hagyott domainek (újsorral elválasztva)\",\n  \"ignore_domains_desc_query\": \"Az ezeknek a szabályoknak megfelelő lekérdezések nem kerülnek a lekérdezési naplóba\",\n  \"ignore_domains_desc_stats\": \"Az ezeknek a szabályoknak megfelelő lekérdezések nem kerülnek be a statisztikákba\",\n  \"ignore_domains_title\": \"Figyelmen kívül hagyott domainek\",\n  \"ignore_query_log\": \"Figyelmen kívül hagyja ezt az ügyfelet a lekérdezési naplóban\",\n  \"ignore_statistics\": \"Hagyja figyelmen kívül ezt az ügyfelet a statisztikákban\",\n  \"install_auth_confirm\": \"Jelszó megerősítése\",\n  \"install_auth_desc\": \"Erősen ajánlott a jelszavas hitelesítés beállítása az AdGuard Home webes admin felületéhez. Még akkor is, ha csak a helyi hálózaton érhető el, óvja meg az illetéktelen hozzáférésektől.\",\n  \"install_auth_password\": \"Jelszó\",\n  \"install_auth_password_enter\": \"Jelszó megadása\",\n  \"install_auth_title\": \"Hitelesítés\",\n  \"install_auth_username\": \"Felhasználónév\",\n  \"install_auth_username_enter\": \"Felhasználónév megadása\",\n  \"install_devices_address\": \"Az AdGuard DNS szerver a következő címeket figyeli\",\n  \"install_devices_android_list_1\": \"Az Android kezdőképernyőjén érintse meg a Beállítások gombot.\",\n  \"install_devices_android_list_2\": \"Érintse meg a Wi-Fi gombot a menüben. Ekkor a képernyőre kerül az összes elérhető hálózat (mobilinternethez nem lehet egyedi DNS-t megadni).\",\n  \"install_devices_android_list_3\": \"Nyomjon hosszan arra a hálózatra a listából, amelyikre éppen csatlakozva van, majd válassza a Hálózat módosítása lehetőséget.\",\n  \"install_devices_android_list_4\": \"Egyes eszközökön előfordulhat, hogy a további beállítások megtekintéséhez a Speciális/haladó beállítások részt kell megnyitni. Az Android DNS-beállításainak módosításához ekkor az IP-beállításokat DHCP-ről statikusra kell váltania.\",\n  \"install_devices_android_list_5\": \"Változtassa meg a DNS 1 és a DNS 2 értékét az AdGuard Home szerver címeire.\",\n  \"install_devices_desc\": \"Az AdGuard Home használatának megkezdéséhez be kell állítania az eszközeit, hogy azok használni tudják.\",\n  \"install_devices_ios_list_1\": \"A kezdőképernyőn érintse meg a Beállítások gombot.\",\n  \"install_devices_ios_list_2\": \"Válassza ki a Wi-Fi-t a bal oldali menüből (mobilinternetnél nem lehetséges a DNS beállítása).\",\n  \"install_devices_ios_list_3\": \"Érintse meg a jelenleg használt hálózat nevét.\",\n  \"install_devices_ios_list_4\": \"A DNS mezőbe adja meg az AdGuard Home szerver címeit.\",\n  \"install_devices_macos_list_1\": \"Kattintson az Apple ikonra és válassza a Rendszerbeállításokat.\",\n  \"install_devices_macos_list_2\": \"Kattintson a Hálózat lehetőségre.\",\n  \"install_devices_macos_list_3\": \"Válassza ki az első kapcsolatot a listából és kattintson a Haladó beállításokra.\",\n  \"install_devices_macos_list_4\": \"Válassza ki a DNS fület és adja meg az AdGuard Home szerver címeit.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Ez a beállítás automatikusan kihat az összes eszközre, amik az otthoni routeréhez kapcsolódnak, nem szükséges őket egyenként, kézileg beállítani.\",\n  \"install_devices_router_list_1\": \"Nyissa meg a router beállításait. Ez általában a böngészőn keresztül történik egy URL megadásával (pl. http://192.168.0.1/ vagy http://192.168.1.1/). Ez az oldal valószínűleg felhasználónevet és jelszót fog kérni. Ha nem tudja a belépési adatokat, ellenőrizze a router dobozát, a router alján levő fehér címkét vagy a technikai dokumentációt az interneten. Végső esetben visszaállíthatja a routert, azonban ne feledje, hogyha ezt az eljárást választja, akkor valószínűleg elveszíti annak összes beállítását. Ha a router beállításához alkalmazásra van szükség, telepítse az alkalmazást a telefonjára vagy a számítógépére, és használja azt az útválasztó beállításainak eléréséhez.\",\n  \"install_devices_router_list_2\": \"Keresse meg a DHCP/DNS beállításokat. Keresse a DNS szót egy olyan mező mellett, amely egy 4 csoportból álló, 1-3 számjegyű számsort vár.\",\n  \"install_devices_router_list_3\": \"Adja meg az AdGuard Home szerver címét itt.\",\n  \"install_devices_router_list_4\": \"Bizonyos típusú routereknél nem állíthat be egyéni DNS-kiszolgálót. Ebben az esetben segíthet, ha az AdGuard Home-t DHCP-szerverként állítja be. Ellenkező esetben keresse meg az adott router kézikönyvében a DNS-kiszolgálók testreszabását.\",\n  \"install_devices_title\": \"Állítsa be az eszközeit\",\n  \"install_devices_windows_list_1\": \"Nyissa meg a Vezérlőpultot a Start menün vagy a Windows keresőn keresztül.\",\n  \"install_devices_windows_list_2\": \"Válassza a Hálózat és internet kategóriát, majd pedig a Hálózati és megosztási központot.\",\n  \"install_devices_windows_list_3\": \"A bal oldali panelben kattintson az \\\"Adapterbeállítások módosítása\\\" lehetőségre.\",\n  \"install_devices_windows_list_4\": \"Kattintson jobb egérgombbal az aktív kapcsolatra és válassza ki a Tulajdonságokat.\",\n  \"install_devices_windows_list_5\": \"Keresse meg az Internet Protocol Version 4 (TCP/IPv4) elemet a listában, válassza ki, majd ismét kattintson a Tulajdonságokra.\",\n  \"install_devices_windows_list_6\": \"Válassza a \\\"Következő DNS címek használata\\\" lehetőséget és adja meg az AdGuard Home szerver címeit.\",\n  \"install_saved\": \"Sikeres mentés\",\n  \"install_settings_all_interfaces\": \"Minden felület\",\n  \"install_settings_dns\": \"DNS szerver\",\n  \"install_settings_dns_desc\": \"Be kell állítania az eszközeit vagy a routerét, hogy használni tudja a DNS szervert a következő címeken:\",\n  \"install_settings_interface_link\": \"Az AdGuard Home webes admin felülete elérhető a következő címe(ke)n:\",\n  \"install_settings_listen\": \"Figyelő felület\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Webes admin felület\",\n  \"install_static_configure\": \"Úgy észleltük, hogy dinamikus IP-cím van használatban — <0>{{ip}}</0>. Szeretné ezt statikus IP-címként használni?\",\n  \"install_static_error\": \"Az AdGuard Home nem tudja automatikusan konfigurálni ezt a hálózati felületet. Kérjük, nézzen utána, hogyan kell ezt manuálisan elvégezni.\",\n  \"install_static_ok\": \"Jó hír! A statikus IP-cím már be van állítva\",\n  \"install_step\": \"Lépés\",\n  \"install_submit_desc\": \"A telepítési folyamat befejeződött, használatba veheti az AdGuard Home-ot.\",\n  \"install_submit_title\": \"Gratulálunk!\",\n  \"install_welcome_desc\": \"Az AdGuard Home egy, a teljes hálózatot lefedő DNS szerver, amely blokkolja a hirdetéseket és a nyomkövető rendszereket. Az a célja, hogy lehetővé tegye a teljes hálózat és az összes eszköz felügyeletét, emellett pedig nem igényel kliensoldali programot.\",\n  \"install_welcome_title\": \"Üdvözli az AdGuard Home!\",\n  \"interval_24_hour\": \"24 óra\",\n  \"interval_6_hour\": \"6 óra\",\n  \"interval_days\": \"{{count}} nap\",\n  \"interval_days_plural\": \"{{count}} nap\",\n  \"interval_hours\": \"{{count}} óra\",\n  \"interval_hours_plural\": \"{{count}} óra\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP cím\",\n  \"known_tracker\": \"Ismert követő\",\n  \"last_rule_in_allowlist\": \"Nem lehet letiltani ezt az ügyfelet, mert a \\\"{{disallowed_rule}}\\\" szabály kizárása letiltja az \\\"Allowed clients\\\" listát.\",\n  \"last_time_updated_table_header\": \"Utoljára frissítve\",\n  \"list_confirm_delete\": \"Biztosan törölni kívánja ezt a listát?\",\n  \"list_label\": \"Lista\",\n  \"list_updated\": \"{{count}} lista frissítve lett\",\n  \"list_updated_plural\": \"{{count}} lista frissítve lett\",\n  \"list_url_table_header\": \"Lista URL-je\",\n  \"load_balancing\": \"Terheléselosztás\",\n  \"load_balancing_desc\": \"Egyszerre csak egy upstream szerverről történjen lekérdezés.<br/>Az AdGuard Home egy súlyozott, véletlenszerű algoritmust fog használni a legkevesebb sikertelen lekérdezéssel és a legalacsonyabb átlagos lekérdezési idővel rendelkező szerverek kiválasztására.\",\n  \"loading_table_status\": \"Betöltés...\",\n  \"local_ptr_default_resolver\": \"Alapesetben az AdGuard Home a következő reverse DNS feloldókat használja: {{ip}}.\",\n  \"local_ptr_desc\": \"Az AdGuard Home által használt DNS szerverek privát PTR, SOA és NS kérésekhez. Egy kérés privátnak számít, ha ARPA domain-t kér, amely tartalmaz egy alhálózatot a privát IP hatótávolságon belül (például \\\"192.168.12.34\\\"), és egy privát IP-címmel rendelkező ügyféltől érkezik. Ha nincs beállítva, az OS alapértelmezett DNS feloldóit fogja használni, kivéve az AdGuard Home IP-címeit.\",\n  \"local_ptr_no_default_resolver\": \"Az AdGuard Home nem tudta meghatározni a privát reverse DNS feloldókat ehhez a rendszerhez.\",\n  \"local_ptr_placeholder\": \"Adjon meg egy IP-címet soronként\",\n  \"local_ptr_title\": \"Privát DNS szerverek\",\n  \"location\": \"Helyzet\",\n  \"log_and_stats_section_label\": \"Lekérdezési napló és statisztikák\",\n  \"lower_range_start_error\": \"Kisebb legyen, mint a tartomány kezdete\",\n  \"main_settings\": \"Fő beállítások\",\n  \"make_static\": \"Statikussá tétel\",\n  \"manual_update\": \"Kérjük, hogy <a>kövesse ezeket a lépéseket</a> a manuális frissítéshez.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Hétfő\",\n  \"monday_short\": \"Hét\",\n  \"name\": \"Név\",\n  \"name_table_header\": \"Név\",\n  \"netname\": \"Hálózat neve\",\n  \"network\": \"Hálózat\",\n  \"new_allowlist\": \"Új engedélyezési lista\",\n  \"new_blocklist\": \"Új blokkolási lista\",\n  \"next\": \"Következő\",\n  \"next_btn\": \"Következő\",\n  \"no_blocklist_added\": \"Nincsnek blokkolási listák hozzáadva\",\n  \"no_clients_found\": \"Nem található kliens\",\n  \"no_domains_found\": \"Nem található domain\",\n  \"no_logs_found\": \"Nem található napló\",\n  \"no_servers_specified\": \"Nincsenek megadott kiszolgálók\",\n  \"no_upstreams_data_found\": \"Nem található upstream szerver adat\",\n  \"no_whitelist_added\": \"Nincsenek engedélyezési listák hozzáadva\",\n  \"nothing_found\": \"Nincs találat\",\n  \"null_ip\": \"Null IP-cím\",\n  \"number_of_dns_query_blocked_24_hours\": \"A hirdetésblokkoló szűrők és a hosztfájlok által letiltott DNS kérések száma\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Blokkolt felnőtt tartalmak száma\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Az AdGuard böngészési biztonság modulja által letiltott DNS kérések száma\",\n  \"number_of_dns_query_days\": \"Lekérdezések száma az utolsó {{count}} napban\",\n  \"number_of_dns_query_days_plural\": \"Feldolgozott DNS lekérdezések száma az utolsó {{count}} napban\",\n  \"number_of_dns_query_hours\": \"Feldolgozott DNS lekérdezések száma az utolsó {{count}} órában\",\n  \"number_of_dns_query_hours_plural\": \"Feldolgozott DNS lekérdezések száma az utolsó {{count}} órában\",\n  \"number_of_dns_query_to_safe_search\": \"A biztonságos keresésre kényszerített DNS lekérdezések száma\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"KI\",\n  \"on\": \"BE\",\n  \"open_dashboard\": \"Irányítópult megnyitása\",\n  \"orgname\": \"Szervezet neve\",\n  \"original_response\": \"Eredeti válasz\",\n  \"out_of_range_error\": \"A következő tartományon kívül legyen: \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Oldal\",\n  \"parallel_requests\": \"Párhuzamos lekérések\",\n  \"parental_control\": \"Szülői felügyelet\",\n  \"password_label\": \"Jelszó\",\n  \"password_placeholder\": \"Jelszó megadása\",\n  \"plain_dns\": \"Egyszerű DNS\",\n  \"port_53_faq_link\": \"Az 53-as portot gyakran a \\\"DNSStubListener\\\" vagy a \\\"systemd-resolved\\\" (rendszer által feloldott) szolgáltatások használják. Kérjük, olvassa el <0>ezt az útmutatót</0> a probléma megoldásához.\",\n  \"previous_btn\": \"Előző\",\n  \"privacy_policy\": \"Adatvédelmi irányelvek\",\n  \"processing_update\": \"Kérjük várjon, az AdGuard Home frissítése folyamatban van\",\n  \"protection_section_label\": \"Védelem\",\n  \"protocol\": \"Protokoll\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Lekérdezési napló\",\n  \"query_log_clear\": \"Lekérdezési napló törlése\",\n  \"query_log_cleared\": \"A lekérdezési napló sikeresen törölve\",\n  \"query_log_configuration\": \"Naplózás beállítása\",\n  \"query_log_confirm_clear\": \"Biztosan törölni szerené a lekérdezési naplót?\",\n  \"query_log_disabled\": \"Lekérdezési napló kikapcsolva. Bekapcsolható a <0>beállításokban</0>\",\n  \"query_log_enable\": \"Naplózás engedélyezése\",\n  \"query_log_filtered\": \"{{filter}} által szűrve\",\n  \"query_log_response_status\": \"Állapot: {{value}}\",\n  \"query_log_retention\": \"Lekérdezési naplók megtartása\",\n  \"query_log_retention_confirm\": \"Biztos benne, hogy megváltoztatja a kérések naplójának megőrzési idejét? Ha csökkentette az értéket, a megadottnál korábbi adatok elvesznek\",\n  \"query_log_strict_search\": \"Használjon \\\"dupla idézőjelet\\\" a pontos kereséshez\",\n  \"query_log_updated\": \"A lekérdezési napló sikeresen frissítve lett\",\n  \"rate_limit\": \"Kérések korlátozása\",\n  \"rate_limit_desc\": \"Maximálisan hány kérést küldhet egy kliens másodpercenkén. Ha 0-ra állítja, akkor nincs korlátozás.\",\n  \"rate_limit_subnet_len_ipv4\": \"Az IPv4-címek alhálózati előtagjának hossza\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"A sebességkorlátozáshoz használt IPv4-címek alhálózati előtagjának hossza. Az alapértelmezett érték 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Az IPv4 alhálózati előtag hosszának 0 és 32 között kell lennie\",\n  \"rate_limit_subnet_len_ipv6\": \"Az IPv6-címek alhálózati előtagjának hossza\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"A sebességkorlátozáshoz használt IPv6-címek alhálózati előtagjának hossza. Az alapértelmezett érték 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Az IPv6 alhálózati előtag hosszának 0 és 128 között kell lennie\",\n  \"rate_limit_whitelist\": \"Sebességkorlátozó engedélyezési lista\",\n  \"rate_limit_whitelist_desc\": \"A sebességkorlátozásból kizárt IP-címek\",\n  \"rate_limit_whitelist_placeholder\": \"Adjon meg egy IP-címet soronként\",\n  \"refresh_btn\": \"Frissítés\",\n  \"refresh_statics\": \"Statisztikák frissítése\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Probléma bejelentése\",\n  \"request_details\": \"Kérés részletei\",\n  \"request_table_header\": \"Kérés\",\n  \"requests_count\": \"Kérések száma\",\n  \"reset_settings\": \"Beállítások visszaállítása\",\n  \"resolve_clients_desc\": \"Fordítva oldja fel a kliensek IP címeit a hosztneveikre azáltal, hogy PTR lekérdezéseket küld a megfelelő feloldóknak (privát DNS szerverek a helyi kliensek számára, upstream szerverek a nyilvános IP címmel rendelkező kliensek számára).\",\n  \"resolve_clients_title\": \"Kliensek IP címeinek fordított feloldása\",\n  \"response_code\": \"Válaszkód\",\n  \"response_details\": \"Válasz részletei\",\n  \"response_table_header\": \"Válasz\",\n  \"response_time\": \"Válaszidő\",\n  \"rewrite_A\": \"<0>A</0>: speciális érték, megtartja az upstream felől érkező <0>A</0> rekordokat\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: speciális érték, megtartja az upstream felől érkező <0>AAAA</0> rekordokat\",\n  \"rewrite_add\": \"DNS átírás hozzáadása\",\n  \"rewrite_added\": \"DNS átírás a(z) \\\"{{key}}\\\" kulcshoz sikeresen hozzáadva\",\n  \"rewrite_applied\": \"Alkalmazott átírási szabály\",\n  \"rewrite_confirm_delete\": \"Biztosan törölni szeretné a DNS átírást ehhez: \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"DNS átírás a(z) \\\"{{key}}\\\" kulcshoz sikeresen törölve\",\n  \"rewrite_desc\": \"Lehetővé teszi, hogy egyszerűen beállítson egyéni DNS választ egy adott domain névhez.\",\n  \"rewrite_domain_name\": \"Domain név: CNAME rekord hozzáadása\",\n  \"rewrite_edit\": \"DNS újraírás szerkesztése\",\n  \"rewrite_hosts_applied\": \"Átírva egy hoszt szabály által\",\n  \"rewrite_ip_address\": \"IP-cím: használja ezt az IP-t A vagy AAAA válaszban\",\n  \"rewrite_not_found\": \"Nem találhatók DNS átírások\",\n  \"rewrite_settings_updated\": \"A DNS újraírási beállítások sikeresen frissítve lettek\",\n  \"rewrite_updated\": \"A DNS újraírása sikeresen frissítve\",\n  \"rewrites_disabled_table_header\": \"Az átírások le vannak tiltva\",\n  \"rewrites_enabled_table_header\": \"Az átírások be vannak kapcsolva\",\n  \"rewritten\": \"Átírt\",\n  \"rows_table_footer_text\": \"sor\",\n  \"rule_added_to_custom_filtering_toast\": \"Szabály hozzáadva az egyéni szűrőszabályokhoz: {{rule}}\",\n  \"rule_label\": \"Szabály(ok)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Szabály eltávolítva az egyéni szűrőszabályok közül: {{rule}}\",\n  \"rules_count_table_header\": \"Szabályok száma\",\n  \"safe_browsing\": \"Biztonságos böngészés\",\n  \"safe_search\": \"Biztonságos keresés\",\n  \"saturday\": \"Szombat\",\n  \"saturday_short\": \"Szom\",\n  \"save_btn\": \"Mentés\",\n  \"save_config\": \"Konfiguráció mentése\",\n  \"schedule_add\": \"Ütemezés hozzáadása\",\n  \"schedule_current_timezone\": \"Jelenlegi időzóna: {{value}}\",\n  \"schedule_desc\": \"Inaktivitási időszakok beállítása a blokkolt szolgáltatásokhoz\",\n  \"schedule_edit\": \"Ütemezés szerkesztése\",\n  \"schedule_from\": \"Ettől:\",\n  \"schedule_invalid_select\": \"A kezdési időpontnak a befejezési időpont előtt kell lennie\",\n  \"schedule_modal_description\": \"Ez az ütemezés felváltja a hét ugyanazon napjára vonatkozó meglévő ütemezéseket. A hét minden napján csak egy inaktivitási időszak lehet.\",\n  \"schedule_modal_time_off\": \"Nincs szolgáltatás blokkolás:\",\n  \"schedule_new\": \"Új ütemezés\",\n  \"schedule_remove\": \"Ütemezés eltávolítása\",\n  \"schedule_save\": \"Ütemezés mentése\",\n  \"schedule_select_days\": \"Napok kiválasztása\",\n  \"schedule_services\": \"A szolgáltatás blokkolásának szüneteltetése\",\n  \"schedule_services_desc\": \"Állítsa be a szolgáltatásblokkoló szűrő szüneteltetési ütemezését\",\n  \"schedule_services_desc_client\": \"Állítsa be a szolgáltatásblokkoló szűrő szüneteltetési ütemezését ehhez az ügyfélhez\",\n  \"schedule_time_all_day\": \"Egész nap\",\n  \"schedule_timezone\": \"Válasszon időzónát\",\n  \"schedule_to\": \"Eddig:\",\n  \"served_from_cache_label\": \"Gyorsítótárból kiszolgálva\",\n  \"service_name\": \"Szolgáltatás neve\",\n  \"set_static_ip\": \"Statikus IP-cím beállítása\",\n  \"settings\": \"Beállítások\",\n  \"settings_custom\": \"Egyéni\",\n  \"settings_global\": \"Globális\",\n  \"setup_config_to_enable_dhcp_server\": \"Konfiguráció beállítása a DHCP-kiszolgáló engedélyezéséhez\",\n  \"setup_dns_notice\": \"Ahhoz, hogy a <1>DNS-over-HTTPS</1> vagy a <1>DNS-over-TLS</1> valamelyikét használja, muszáj <0>beállítania a titkosítást</0> az AdGuard Home beállításaiban.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Használja a(z) <1>{{address}}</1> szöveget.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Használja a(z) <1>{{address}}</1> szöveget.\",\n  \"setup_dns_privacy_3\": \"<0>Azon szoftverek listája, amelyeket használhat.</0>\",\n  \"setup_dns_privacy_4\": \"Az iOS14-en vagy a macOS Big Sur eszközökön le tud tölteni egy speciális, '.mobileconfig' nevű fájlt, ami hozzáadja a <highlight>DNS-over-HTTPS</highlight> vagy a <highlight>DNS-over-TLS</highlight> szervereket a DNS beállításokhoz.\",\n  \"setup_dns_privacy_android_1\": \"Az Android 9 natív módon támogatja a DNS-over-TLS-t. A beállításához menjen a Beállítások → Hálózat & internet → Speciális → Privát DNS menübe, és adja meg itt a domaint.\",\n  \"setup_dns_privacy_android_2\": \"Az <0>AdGuard for Android</0> támogatja a <1>DNS-over-HTTPS</1>-t és a <1>DNS-over-TLS</1>-t.\",\n  \"setup_dns_privacy_android_3\": \"Az <0>Intra</0> hozzáadja a <1>DNS-over-HTTPS</1> támogatást az Androidhoz.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS és macOS konfiguráció\",\n  \"setup_dns_privacy_ios_1\": \"A <0>DNSCloak</0> támogatja a <1>DNS-over-HTTPS</1>-t, de ahhoz, hogy a saját szerverhez konfigurálhassa, létre kell hoznia egy <2>DNS bélyeget</2> hozzá.\",\n  \"setup_dns_privacy_ios_2\": \"Az <0>AdGuard for iOS</0> támogatja a <1>DNS-over-HTTPS</1> és a <1>DNS-over-TLS</1> beállításokat.\",\n  \"setup_dns_privacy_other_1\": \"Maga az AdGuard Home bármilyen platformon biztonságos DNS-kliens lehet.\",\n  \"setup_dns_privacy_other_2\": \"A <0>dnsproxy</0> támogatja az összes ismert biztonságos DNS protokollt.\",\n  \"setup_dns_privacy_other_3\": \"A <0>dnscrypt-proxy</0> támogatja a <1>DNS-over-HTTPS</1>-t.\",\n  \"setup_dns_privacy_other_4\": \"A <0>Mozilla Firefox</0> támogatja a <1>DNS-over-HTTPS</1>-t.\",\n  \"setup_dns_privacy_other_5\": \"További megvalósításokat találhat <0>ide</0> és <1>ide</1> kattintva.\",\n  \"setup_dns_privacy_other_title\": \"Egyéb megvalósítások\",\n  \"setup_guide\": \"Beállítási útmutató\",\n  \"show_all_filter_type\": \"Összes mutatása\",\n  \"show_blocked_responses\": \"Blokkolva\",\n  \"show_filtered_type\": \"Szűrtek megjelenítése\",\n  \"show_processed_responses\": \"Feldolgozva\",\n  \"show_whitelisted_responses\": \"Kivételezett\",\n  \"sign_in\": \"Bejelentkezés\",\n  \"sign_out\": \"Kijelentkezés\",\n  \"source_label\": \"Forrás\",\n  \"static_ip\": \"Statikus IP-cím\",\n  \"static_ip_desc\": \"Az AdGuard Home egy szerver, tehát statikus IP-címre van szüksége a megfelelő működéshez. Ellenkező esetben a router valamikor más IP-címet rendelhet ehhez az eszközhöz.\",\n  \"statistics_clear\": \"Statisztikák visszaállítása\",\n  \"statistics_clear_confirm\": \"Biztosan vissza akarja állítani a statisztikákat?\",\n  \"statistics_cleared\": \"A statisztikák sikeresen vissza lettek állítva\",\n  \"statistics_configuration\": \"Statisztikai beállítások\",\n  \"statistics_enable\": \"Statisztikák engedélyezése\",\n  \"statistics_retention\": \"Statisztika megőrzése\",\n  \"statistics_retention_confirm\": \"Biztos benne, hogy megváltoztatja a statisztika megőrzési idejét? Ha csökkentette az értéket, a megadottnál korábbi adatok elvesznek\",\n  \"statistics_retention_desc\": \"Ha csökkenti az intervallum értékét, az előtte levő adatok elvesznek\",\n  \"stats_adult\": \"Blokkolt felnőtt tartalom\",\n  \"stats_disabled\": \"Ezek a statisztikák ki lettek kapcsolva. Be tudja kapcsolni őket a <0>beállítások oldalon</0>.\",\n  \"stats_disabled_short\": \"A statisztikák ki lettek kapcsolva\",\n  \"stats_malware_phishing\": \"Blokkolt kártevő/adathalászat\",\n  \"stats_params\": \"Statisztikai beállítások\",\n  \"stats_query_domain\": \"Leglátogatottabb domainek\",\n  \"subnet_error\": \"A címeknek egy alhálózatban kell lenniük\",\n  \"sunday\": \"Vasárnap\",\n  \"sunday_short\": \"Vas\",\n  \"system_host_files\": \"Rendszer hosztfájlok\",\n  \"table_client\": \"Kliens\",\n  \"table_name\": \"Név\",\n  \"tags_desc\": \"Kiválaszthatja a klienseknek megfelelő címkéket. A címkék beilleszthetők a szűrési szabályokba, és lehetővé teszik azok pontosabb alkalmazását. <0>További információ</0>.\",\n  \"tags_title\": \"Címkék\",\n  \"test_upstream_btn\": \"Upstreamek tesztelése\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Automatikus (az eszköz színsémájától függően)\",\n  \"theme_dark\": \"Sötét\",\n  \"theme_dark_desc\": \"Sötét téma\",\n  \"theme_light\": \"Világos\",\n  \"theme_light_desc\": \"Világos téma\",\n  \"thursday\": \"Csütörtök\",\n  \"thursday_short\": \"Csüt\",\n  \"time_table_header\": \"Idő\",\n  \"top_blocked_domains\": \"Legtöbbet blokkolt domainek\",\n  \"top_clients\": \"Legaktívabb kliensek\",\n  \"top_upstreams\": \"Top upstream szerverek\",\n  \"topline_expired_certificate\": \"Az SSL-tanúsítványa lejárt. Frissítse a <0>Titkosítási beállításokat</0>.\",\n  \"topline_expiring_certificate\": \"Az SSL-tanúsítványa hamarosan lejár. Frissítse a <0>Titkosítási beállításokat</0>.\",\n  \"tracker_source\": \"Követő forrása\",\n  \"try_again\": \"Próbálja újra\",\n  \"ttl_cache_validation\": \"A minimális gyorsítótár TTL értéknek kisebbnek vagy egyenlőnek kell lennie a maximum értékkel\",\n  \"tuesday\": \"Kedd\",\n  \"tuesday_short\": \"Kedd\",\n  \"type_table_header\": \"Típus\",\n  \"unavailable_dhcp\": \"A DHCP nem elérhető\",\n  \"unavailable_dhcp_desc\": \"Az AdGuard Home nem tud DHCP szervert futtatni az operációs rendszerén\",\n  \"unblock\": \"Feloldás\",\n  \"unblock_all\": \"Összes feloldása\",\n  \"unblock_for_this_client_only\": \"Feloldás csak ennek a kliensnek\",\n  \"unknown_filter\": \"Ismeretlen szűrő: {{filterId}}\",\n  \"update_announcement\": \"Az AdGuard Home {{version}} verziója elérhető! <0>Kattintson ide</0> további információkért.\",\n  \"update_failed\": \"Az automatikus frissítés nem sikerült. Kérjük, hogy <a>kövesse ezeket a lépéseket</a> a manuális frissítéshez.\",\n  \"update_now\": \"Frissítés most\",\n  \"updated_custom_filtering_toast\": \"Egyéni szűrőszabályok sikeresen mentve\",\n  \"updated_save_search_toast\": \"A Biztonságos keresés beállításai frissítve\",\n  \"updated_upstream_dns_toast\": \"Upstream szerverek sikeresen mentve\",\n  \"updates_checked\": \"Elérhető az AdGuard Home új verziója\",\n  \"updates_version_equal\": \"Az AdGuard Home naprakész\",\n  \"upstream\": \"Upstream szerver\",\n  \"upstream_dns\": \"Upstream DNS-kiszolgálók\",\n  \"upstream_dns_cache_configuration\": \"Upstream DNS gyorsítótár konfigurációja\",\n  \"upstream_dns_client_desc\": \"Ha üresen hagyja ezt a mezőt, az AdGuard Home azokat a szervereket fogja használni, amik a <0>DNS beállításokban</0> vannak beállítva.\",\n  \"upstream_dns_configured_in_file\": \"Beállítva itt: {{path}}\",\n  \"upstream_dns_help\": \"Adja meg a szerverek címeit soronként. <a>Tudjon meg többet</a> a DNS szerverek bekonfigurálásáról.\",\n  \"upstream_parallel\": \"Használjon párhuzamos lekéréseket a domainek feloldásának felgyorsításához az összes upstream kiszolgálóra való egyidejű lekérdezéssel.\",\n  \"upstream_timeout\": \"Upstream időtúllépés\",\n  \"upstream_timeout_desc\": \"Megadja, hogy hány másodpercet kell várni az upstream szervertől érkező válaszra\",\n  \"upstreams\": \"Upstream-ek\",\n  \"use_adguard_browsing_sec\": \"Használja az AdGuard böngészési biztonság webszolgáltatását\",\n  \"use_adguard_browsing_sec_hint\": \"Az AdGuard Home ellenőrzi, hogy a böngészési biztonsági modul a domaint tiltólistára tette-e. Az ellenőrzés elvégzéséhez egy adatvédelmet tiszteletben tartó API-t fog használni: a domain név egy rövid előtagját elküldi SHA256 kódolással a szerver felé.\",\n  \"use_adguard_parental\": \"Használja az AdGuard szülői felügyelet webszolgáltatását\",\n  \"use_adguard_parental_hint\": \"Az AdGuard Home ellenőrzi, hogy a domain tartalmaz-e felnőtteknek szóló anyagokat. Ugyanazokat az adatvédelmi API-kat használja, mint a böngésző biztonsági webszolgáltatás.\",\n  \"use_private_ptr_resolvers_desc\": \"A privát IP-címeket tartalmazó ARPA domainek PTR, SOA és NS kéréseinek megoldása privát upstream szerverek, DHCP, /etc/hosts stb. keresztül. Ha kikapcsolásra kerül, az AdGuard Home minden ilyen kérésre NXDOMAIN-nel fog válaszolni.\",\n  \"use_private_ptr_resolvers_title\": \"Privát reverse DNS feloldók használata\",\n  \"use_saved_key\": \"Előzőleg mentett kulcs használata\",\n  \"username_label\": \"Felhasználónév\",\n  \"username_placeholder\": \"Felhasználónév megadása\",\n  \"validated_with_dnssec\": \"DNSSEC által ellenőrizve\",\n  \"version\": \"Verzió\",\n  \"version_request_error\": \"A frissítések ellenőrzése sikertelen. Ellenőrizze az internetkapcsolatot.\",\n  \"wednesday\": \"Szerda\",\n  \"wednesday_short\": \"Szer\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/id.json",
    "content": "{\n  \"access_allowed_desc\": \"Daftar CIDR, alamat IP, atau <a>ClientID</a>. Jika daftar ini memiliki entri, AdGuard Home hanya akan menerima permintaan dari klien ini.\",\n  \"access_allowed_title\": \"Klien yang diizinkan\",\n  \"access_blocked_desc\": \"Jangan dikelirukan dengan filter. AdGuard Home membuang kueri DNS yang cocok dengan domain ini, dan kueri ini bahkan tidak muncul di catatan kueri. Anda dapat menentukan nama domain, karakter pengganti, atau aturan filter URL yang tepat, misalnya \\\"example.org\\\", \\\"*.example.org\\\", atau \\\"||example.org^\\\" secara bersamaan.\",\n  \"access_blocked_title\": \"Domain yang diblokir\",\n  \"access_desc\": \"Disini anda dapat mengatur aturan akses untuk server AdGuard Home DNS\",\n  \"access_disallowed_desc\": \"Daftar CIDR, alamat IP, atau <a>ClientID</a>. Jika daftar ini memiliki entri, AdGuard Home akan membatalkan permintaan dari klien ini. Kolom ini diabaikan jika ada entri di daftar putih klien.\",\n  \"access_disallowed_title\": \"Klien yang tidak diizinkan\",\n  \"access_settings_saved\": \"Pengaturan akses berhasil disimpan\",\n  \"access_title\": \"Pengaturan akses\",\n  \"actions_table_header\": \"Aksi\",\n  \"add_allowlist\": \"Tambahkan daftar putih\",\n  \"add_blocklist\": \"Tambahkan daftar hitam\",\n  \"add_custom_list\": \"Tambahkan daftar kustom\",\n  \"add_persistent_client\": \"Tambahkan sebagai klien persisten\",\n  \"address\": \"Alamat\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home akan menghapus semua permintaan DNS dari klien ini.\",\n  \"all_lists_up_to_date_toast\": \"Semua daftar sudah diperbarui\",\n  \"all_queries\": \"Semua permintaan\",\n  \"allow_this_client\": \"Ijinkan klien ini\",\n  \"allowed\": \"Dibolehkan\",\n  \"anonymize_client_ip\": \"Anonim IP klien\",\n  \"anonymize_client_ip_desc\": \"Jangan simpan alamat lengkap IP klien dalam catatan atau statistik\",\n  \"anonymizer_notification\": \"<0>Catatan:</0> Anonimisasi IP diaktifkan. Anda dapat menonaktifkannya di <1>Pengaturan umum</1> .\",\n  \"answer\": \"Jawaban\",\n  \"apply_btn\": \"Terapkan\",\n  \"auto_clients_desc\": \"Informasi tentang alamat IP perangkat yang menggunakan atau mungkin menggunakan AdGuard Home. Informasi ini dikumpulkan dari beberapa sumber, termasuk berkas host, DNS terbalik, dll.\",\n  \"auto_clients_title\": \"Klien runtime\",\n  \"autofix_warning_list\": \"Ini akan melakukan tugas berikut: <0>Nonaktifkan sistem DNSStubListener</0> <0>Atur alamat server DNS ke 127.0.0.1</0> <0>Ganti target tautan simbolis /etc/resolv.conf dengan /run/systemd/resolve/resolv.conf</0> <0>Hentikan DNSStubListener (muat ulang layanan sistemd-resolved)</0>\",\n  \"autofix_warning_result\": \"Hasilnya, semua permintaan DNS dari sistem anda akan diproses oleh AdGuardHome secara standar.\",\n  \"autofix_warning_text\": \"Apabila anda menekan \\\"Perbaiki\\\", AdGuardHome akan mengatur sistem anda untuk menggunakan server DNS AdGuardHome.\",\n  \"average_processing_time\": \"Rata-rata waktu pemrosesan\",\n  \"average_processing_time_hint\": \"Rata-rata waktu dalam milidetik untuk pemrosesan sebuah permintaan DNS\",\n  \"average_upstream_response_time\": \"Rata-rata waktu respons hulu\",\n  \"back\": \"Kembali\",\n  \"block\": \"Blok\",\n  \"block_all\": \"Blokir semua\",\n  \"block_domain_use_filters_and_hosts\": \"Blokir domain menggunakan filter dan berkas host\",\n  \"block_for_this_client_only\": \"Blok hanya untuk klien ini\",\n  \"block_services\": \"Blokir layanan tertentu\",\n  \"blocked_adult_websites\": \"Diblokir oleh Kontrol Orang Tua\",\n  \"blocked_by\": \"<0>Diblokir oleh</0>\",\n  \"blocked_by_cname_or_ip\": \"Diblokir oleh CNAME atau IP\",\n  \"blocked_by_response\": \"Diblokir oleh CNAME atau IP sebagai respon\",\n  \"blocked_response_ttl\": \"Respons TTL terblokir\",\n  \"blocked_response_ttl_desc\": \"Menentukan berapa detik klien harus menyimpan respons yang difilter dalam cache\",\n  \"blocked_safebrowsing\": \"Diblokir oleh Penjelajahan Aman\",\n  \"blocked_service\": \"Layanan terblokir\",\n  \"blocked_services\": \"Layanan terblokir\",\n  \"blocked_services_desc\": \"Memungkinkan untuk dengan cepat memblokir situs dan layanan populer.\",\n  \"blocked_services_global\": \"Gunakan layanan global yang diblokir\",\n  \"blocked_services_saved\": \"Layanan terblokir berhasil disimpan\",\n  \"blocked_threats\": \"Ancaman terblokir\",\n  \"blocking_ipv4\": \"Blokiran IPv4\",\n  \"blocking_ipv4_desc\": \"Alamat IP yang akan dikembalikan untuk permintaan A yang diblokir\",\n  \"blocking_ipv6\": \"Blokiran IPv6\",\n  \"blocking_ipv6_desc\": \"Alamat IP yang akan dikembalikan untuk permintaan AAAA yang diblokir\",\n  \"blocking_mode\": \"Mode blokir\",\n  \"blocking_mode_custom_ip\": \"IP kustom: respon dengan alamat IP yang diset secara manual\",\n  \"blocking_mode_default\": \"Standar: Tanggapi dengan alamat IP nol (0.0.0.0 untuk A; :: untuk AAAA) saat diblokir oleh aturan gaya Adblock; tanggapi dengan alamat IP yang ditentukan dalam aturan ketika diblokir oleh aturan /etc/hosts-style\",\n  \"blocking_mode_null_ip\": \"Null IP: Respon pakai alamat IP kosong (0.0.0.0 untuk A; :: untuk AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Respon pakai kode NXDOMAIN\",\n  \"blocking_mode_refused\": \"DITOLAK: Respon dengan kode DITOLAK\",\n  \"blocklist\": \"Daftar blokir\",\n  \"bootstrap_dns\": \"Server DNS bootstrap\",\n  \"bootstrap_dns_desc\": \"Alamat IP server DNS yang digunakan untuk menyelesaikan alamat IP penyelesai DoH/DoT yang Anda tentukan sebagai hulu. Tidak diizinkan untuk berkomentar.\",\n  \"cache_cleared\": \"Cache DNS berhasil dihapus\",\n  \"cache_enabled\": \"Aktifkan cache\",\n  \"cache_enabled_desc\": \"Menyimpan respons DNS secara lokal.\",\n  \"cache_optimistic\": \"Caching yang optimis\",\n  \"cache_optimistic_desc\": \"Buat AdGuard Home merespons dari cache bahkan ketika entri telah kedaluwarsa dan juga mencoba untuk menyegarkannya.\",\n  \"cache_size\": \"Ukuran cache\",\n  \"cache_size_desc\": \"Ukuran cache DNS (dalam byte).\",\n  \"cache_size_validation\": \"Ukuran cache harus lebih besar dari nol saat diaktifkan.\",\n  \"cache_ttl_max_override\": \"Tumpuk TTL maksimum\",\n  \"cache_ttl_max_override_desc\": \"Tetapkan nilai maksimum time-to-live (detik) untuk entri dalam cache DNS.\",\n  \"cache_ttl_min_override\": \"Tumpuk TTL minimum\",\n  \"cache_ttl_min_override_desc\": \"Perpanjang nilai time-to-live (detik) yang diterima dari server hulu saat menyimpan respons DNS.\",\n  \"cancel_btn\": \"Batal\",\n  \"category_label\": \"Kategori\",\n  \"check\": \"Periksa\",\n  \"check_client_id\": \"Pengidentifikasi klien (ClientID atau alamat IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Periksa apakah nama host telah tersaring.\",\n  \"check_dhcp_servers\": \"Cek untuk server DHCP\",\n  \"check_dns_record\": \"Pilih tipe catatan DNS\",\n  \"check_enter_client_id\": \"Masukkan pengenal klien\",\n  \"check_hostname\": \"Nama host atau nama domain\",\n  \"check_ip\": \"Alamat IP: {{ip}}\",\n  \"check_not_found\": \"Tidak di temukan di daftar penyaringan anda\",\n  \"check_reason\": \"Alasan: {{reason}}\",\n  \"check_service\": \"Nama layanan: {{service}}\",\n  \"check_title\": \"Periksa penyaringan\",\n  \"check_updates_btn\": \"Cek pembaruan\",\n  \"check_updates_now\": \"Periksa pembaruan sekarang\",\n  \"choose_allowlist\": \"Pilih daftar putih\",\n  \"choose_blocklist\": \"Pilih daftar hitam\",\n  \"choose_from_list\": \"Pilih dari daftar\",\n  \"city\": \"Kota\",\n  \"clear_cache\": \"Hapus cache\",\n  \"click_to_view_queries\": \"Klik untuk lihat permintaan\",\n  \"client_add\": \"Tambahkan Klien\",\n  \"client_added\": \"Klien \\\"{{key}}\\\" berhasil ditambahkan\",\n  \"client_blocked\": \"Klien \\\"{{ip}}\\\" berhasil diblokir\",\n  \"client_confirm_block\": \"Apa anda yakin ingin mem-blokir klien ini \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Apakah anda yakin ingin menghapus klien \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Apa anda yakin ingin meng-unblock klien ini \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Klien \\\"{{key}}\\\" berhasil dihapus\",\n  \"client_details\": \"Detail klien\",\n  \"client_edit\": \"Ubah Klien\",\n  \"client_global_settings\": \"Gunakan pengaturan global\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Klien dapat diidentifikasi oleh ClientID. Pelajari lebih lanjut tentang cara mengidentifikasi klien <a>di sini</a>.\",\n  \"client_id_placeholder\": \"Masukkan ClientID\",\n  \"client_identifier\": \"Identifikasi\",\n  \"client_identifier_desc\": \"Klien dapat diidentifikasi berdasarkan alamat IP, CIDR, alamat MAC, atau ClientID (dapat digunakan untuk DoT/DoH/DoQ). Pelajari lebih lanjut tentang cara mengidentifikasi klien <0>di sini</0>.\",\n  \"client_name\": \"Klien {{id}}\",\n  \"client_new\": \"Klien Baru\",\n  \"client_settings\": \"Pengaturan klien\",\n  \"client_table_header\": \"Klien\",\n  \"client_unblocked\": \"Klien \\\"{{ip}}\\\" berhasil membuka blokir\",\n  \"client_updated\": \"Klien \\\"{{key}}\\\" berhasil diperbarui\",\n  \"clients_desc\": \"Konfigurasikan catatan klien persisten untuk perangkat yang terhubung ke AdGuard Home\",\n  \"clients_not_found\": \"Tidak ada klien ditemukan\",\n  \"clients_title\": \"Klien persisten\",\n  \"compact\": \"Rapat\",\n  \"config_successfully_saved\": \"Konfigurasi berhasil disimpan\",\n  \"configure\": \"Konfigurasi\",\n  \"confirm_dns_cache_clear\": \"Apakah Anda yakin ingin menghapus cache DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home akan mengonfigurasi {{ip}} menjadi alamat IP statis Anda. Anda ingin melanjutkan?\",\n  \"copyright\": \"Hak cipta\",\n  \"country\": \"Negara\",\n  \"custom_filter_rules\": \"Aturan penyaringan khusus\",\n  \"custom_filter_rules_hint\": \"Masukkan satu aturan pada satu baris. Anda dapat menggunakan aturan adblock atau sintaks berkas host.\",\n  \"custom_filtering_rules\": \"Aturan penyaringan khusus\",\n  \"custom_ip\": \"Custom IP\",\n  \"custom_retention_input\": \"Masukkan retensi dalam hitungan jam\",\n  \"custom_rotation_input\": \"Masukkan rotasi dalam hitungan jam\",\n  \"dashboard\": \"Beranda\",\n  \"date\": \"Tanggal\",\n  \"default\": \"Standar\",\n  \"delete_confirm\": \"Apakah anda yakin ingin menghapus \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Hapus\",\n  \"descr\": \"Deskripsi\",\n  \"details\": \"Detail\",\n  \"dhcp_add_static_lease\": \"Tambah static lease\",\n  \"dhcp_config_saved\": \"Pengaturan server DHCP tersimpan\",\n  \"dhcp_description\": \"Jika router Anda tidak mendukung pengaturan DHCP, Anda dapat menggunakan server DHCP bawaan AdGuard.\",\n  \"dhcp_disable\": \"Nonaktifkan server DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Sistem Anda menggunakan konfigurasi alamat IP dinamis untuk antarmuka <0>{{interfaceName}}</0>. Untuk menggunakan server DHCP, alamat IP statis harus ditetapkan. Alamat IP Anda saat ini adalah <0>{{ipAddress}}</0>. AdGuard Home akan secara otomatis menetapkan alamat IP ini sebagai statis jika Anda menekan tombol Aktifkan DHCP.\",\n  \"dhcp_edit_static_lease\": \"Mengedit static lease\",\n  \"dhcp_enable\": \"Aktifkan server DHCP\",\n  \"dhcp_error\": \"AdGuard Home tidak dapat menentukan apakah ada server DHCP aktif lain pada jaringan\",\n  \"dhcp_form_gateway_input\": \"IP gateway\",\n  \"dhcp_form_lease_input\": \"Durasi lease\",\n  \"dhcp_form_lease_title\": \"Waktu DHCP lease (dalam detik)\",\n  \"dhcp_form_range_end\": \"Rentang akhir\",\n  \"dhcp_form_range_start\": \"Rentang awal\",\n  \"dhcp_form_range_title\": \"Rentang alamat IP\",\n  \"dhcp_form_subnet_input\": \"Subnet mask\",\n  \"dhcp_found\": \"Ditemukan beberapa server DHCP aktif di dalam jaringan. Tidak aman untuk menyalakan server DHCP bawaan.\",\n  \"dhcp_hardware_address\": \"Alamat perangkat keras\",\n  \"dhcp_interface_select\": \"Pilih antarmuka DHCP\",\n  \"dhcp_ip_addresses\": \"Alamat IP\",\n  \"dhcp_ipv4_settings\": \"Pengaturan DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Pengaturan DHCP IPv6\",\n  \"dhcp_lease_added\": \"Static lease \\\"{{key}}\\\" berhasil ditambahkan\",\n  \"dhcp_lease_deleted\": \"Static lease \\\"{{key}}\\\" berhasil dihapus\",\n  \"dhcp_lease_updated\": \"Static lease \\\"{{key}}\\\" berhasil diperbarui\",\n  \"dhcp_leases\": \"DHCP leases\",\n  \"dhcp_leases_not_found\": \"DHCP lease tidak ditemukan\",\n  \"dhcp_new_static_lease\": \"Static lease baru\",\n  \"dhcp_not_found\": \"Aman untuk mengaktifkan server DHCP yang dibangun karena rumah AdGuard tidak menemukan server DHCP yang aktif pada jaringan. Namun, Anda harus memeriksa ulang secara manual sebagai penyelidikan otomatis tidak memberikan jaminan 100%.\",\n  \"dhcp_reset\": \"Apakah anda yakin ingin mengatur ulang konfigurasi DHCP anda?\",\n  \"dhcp_reset_leases\": \"Atur ulang semua kontrak\",\n  \"dhcp_reset_leases_confirm\": \"Apakah Anda yakin ingin mengatur ulang kontrak Anda?\",\n  \"dhcp_reset_leases_success\": \"Kontrak DHCP berhasil diatur ulang\",\n  \"dhcp_settings\": \"Pengaturan DHCP\",\n  \"dhcp_static_ip_error\": \"Jika ingin menggunakan server DHCP, alamat IP statis harus diatur. AdGuard Home gagal menentukan jika antarmuka jaringan ini dikonfigurasi menggunakan alamat IP statis. Silakan atur alamat IP statis secara manual.\",\n  \"dhcp_static_leases\": \"DHCP static leases\",\n  \"dhcp_static_leases_not_found\": \"DHCP static lease tidak ditemukan\",\n  \"dhcp_table_expires\": \"Kadaluwarsa\",\n  \"dhcp_table_hostname\": \"Nama host\",\n  \"dhcp_title\": \"Server DHCP\",\n  \"dhcp_warning\": \"Jika Anda tetap ingin mengaktifkan server DHCP, pastikan tidak ada server DHCP lain yang aktif di jaringan Anda, karena hal ini dapat memutus konektivitas Internet untuk perangkat di jaringan!\",\n  \"disable_for_hours\": \"Selama {{count}} jam\",\n  \"disable_for_hours_plural\": \"Untuk {{count}} jam\",\n  \"disable_for_minutes\": \"Selama {{count}} menit\",\n  \"disable_for_minutes_plural\": \"Selama {{count}} menit\",\n  \"disable_for_seconds\": \"Selama {{count}} detik\",\n  \"disable_for_seconds_plural\": \"Selama {{count}} detik\",\n  \"disable_ipv6\": \"Nonaktifkan penyelesaian alamat IPv6\",\n  \"disable_ipv6_desc\": \"Hapus semua kueri DNS untuk alamat IPv6 (ketik AAAA) dan hapus petunjuk IPv6 dari respons HTTPS.\",\n  \"disable_notify_for_hours\": \"Hentikan perlindungan selama {{count}} jam\",\n  \"disable_notify_for_hours_plural\": \"Hentikan perlindungan selama {{count}} jam\",\n  \"disable_notify_for_minutes\": \"Hentikan perlindungan selama {{count}} menit\",\n  \"disable_notify_for_minutes_plural\": \"Hentikan perlindungan selama {{count}} menit\",\n  \"disable_notify_for_seconds\": \"Hentikan perlindungan selama {{count}} detik\",\n  \"disable_notify_for_seconds_plural\": \"Hentikan perlindungan selama {{count}} detik\",\n  \"disable_notify_until_tomorrow\": \"Hentikan perlindungan sampai besok\",\n  \"disable_protection\": \"Matikan perlindungan\",\n  \"disable_rewrites\": \"Nonaktifkan aturan penulisan ulang\",\n  \"disable_until_tomorrow\": \"Sampai besok\",\n  \"disabled\": \"Tidak aktif\",\n  \"disabled_dhcp\": \"Server DHCP dinonaktifkan\",\n  \"disabled_filtering_toast\": \"Penyaringan nonaktif\",\n  \"disabled_parental_toast\": \"Kontrol orang tua dinonaktifkan\",\n  \"disabled_protection\": \"Perlindungan dimatikan\",\n  \"disabled_safe_browsing_toast\": \"Penjelajahan Aman dinonaktifkan\",\n  \"disabled_safe_search_toast\": \"Pencarian aman dinonaktifkan\",\n  \"disallow_this_client\": \"Cabut ijin untuk klien ini\",\n  \"dns_addresses\": \"Alamat DNS\",\n  \"dns_allowlists\": \"Daftar putih DNS\",\n  \"dns_allowlists_desc\": \"Domain dari daftar putih DNS akan diizinkan bahkan jika mereka ada juga di daftar hitam.\",\n  \"dns_blocklists\": \"Daftar blokir DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home akan memblokir domain yang cocok dengan daftar hitam.\",\n  \"dns_cache_config\": \"Konfigurasi cache DNS\",\n  \"dns_cache_config_desc\": \"Disini Anda bisa mengonfigurasi cache DNS\",\n  \"dns_cache_size\": \"Ukuran cache DNS, dalam byte\",\n  \"dns_config\": \"Konfigurasi server DNS\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS Privasi\",\n  \"dns_providers\": \"Berikut adalah <0>daftar penyedia DNS yang dikenal</0> untuk dipilih.\",\n  \"dns_query\": \"Kueri DNS\",\n  \"dns_rewrites\": \"DNS rewrite\",\n  \"dns_settings\": \"Pengaturan DNS\",\n  \"dns_start\": \"Server DNS sedang dinyalakan\",\n  \"dns_status_error\": \"Kesalahan dalam mendapatkan status server DNS\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": tidak dapat digunakan, mohon cek bahwa Anda telah menulisnya dengan benar\",\n  \"dns_test_ok_toast\": \"Server DNS yang ditentukan bekerja dengan benar\",\n  \"dns_test_parsing_error_toast\": \"Bagian {{section}}: baris {{line}}: tidak dapat digunakan, mohon cek bahwa Anda telah menulisnya dengan benar\",\n  \"dns_test_warning_toast\": \"Hulu \\\"{{key}}\\\" tidak menanggapi permintaan pengujian dan mungkin tidak berfungsi dengan benar\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Aktifkan DNSSEC\",\n  \"dnssec_enable_desc\": \"Atur bendera DNSSEC di permintaan keluar DNS dan periksa hasilnya (resolver berkemampuan DNSSEC diperlukan)\",\n  \"domain\": \"Domain\",\n  \"domain_desc\": \"Masukkan nama domain atau wildcard yang ingin Anda tulis ulang.\",\n  \"domain_name_table_header\": \"Nama domain\",\n  \"domain_or_client\": \"Domain atau klien\",\n  \"down\": \"Padam\",\n  \"download_mobileconfig\": \"Unduh berkas konfigurasi\",\n  \"download_mobileconfig_doh\": \"Unduh .mobileconfig untuk DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Unduh .mobileconfig untuk DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Edit daftar putih\",\n  \"edit_blocklist\": \"Edit daftar hitam\",\n  \"edit_table_action\": \"Ubah\",\n  \"edns_cs_desc\": \"Tambahkan opsi EDNS Client Subnet (ECS) ke permintaan hulu dan catat nilai yang dikirim oleh klien dalam catatan kueri.\",\n  \"edns_enable\": \"Aktifkan EDNS Klien Subnet\",\n  \"edns_use_custom_ip\": \"Gunakan IP kustom untuk EDNS\",\n  \"edns_use_custom_ip_desc\": \"Izinkan untuk menggunakan IP kustom untuk EDNS\",\n  \"elapsed\": \"Berlalu\",\n  \"empty_response_status\": \"Kosong\",\n  \"enable_protection\": \"Aktifkan perlindungan\",\n  \"enable_protection_timer\": \"Perlindungan akan diaktifkan dalam {{time}}\",\n  \"enable_rewrites\": \"Aktifkan aturan penulisan ulang\",\n  \"enable_upstream_dns_cache\": \"Aktifkan cache DNS untuk konfigurasi hulu kustom pada klien ini\",\n  \"enabled_dhcp\": \"Server DHCP diaktifkan\",\n  \"enabled_filtering_toast\": \"Penyaringan aktif\",\n  \"enabled_parental_toast\": \"Kontrol orang tua diaktifkan\",\n  \"enabled_protection\": \"Perlidungan aktif\",\n  \"enabled_safe_browsing_toast\": \"Penjelajahan Aman Diaktifkan\",\n  \"enabled_save_search_toast\": \"Pencarian aman diaktifkan\",\n  \"enabled_table_header\": \"Diaktifkan\",\n  \"encryption_certificate_path\": \"Path sertifikat\",\n  \"encryption_certificates\": \"Sertifikat\",\n  \"encryption_certificates_desc\": \"Untuk menggunakan enkripsi, Anda perlu memberikan rantai sertifikat SSL yang valid untuk domain Anda. Anda bisa mendapatkan sertifikat gratis di <0>{{link}}</0> atau Anda dapat membelinya dari salah satu Otoritas Sertifikat tepercaya.\",\n  \"encryption_certificates_input\": \"Salin / rekatkan sertifikat PEM yang disandikan di sini.\",\n  \"encryption_certificates_source_content\": \"Tempel konten sertifikat\",\n  \"encryption_certificates_source_path\": \"Tetapkan path berkas sertifikat\",\n  \"encryption_chain_invalid\": \"Rantai sertifikat tidak valid\",\n  \"encryption_chain_valid\": \"Rantai sertifikat valid\",\n  \"encryption_config_saved\": \"Pengaturan enkripsi telah tersimpan\",\n  \"encryption_desc\": \"Dukungan enkripsi (HTTPS/QUIC/TLS) untuk DNS dan antarmuka web admin\",\n  \"encryption_doq\": \"Port DNS-over-QUIC \",\n  \"encryption_doq_desc\": \"Jika port ini dikonfigurasi, AdGuard Home akan menjalankan server DNS melalui QUIC pada port ini.\",\n  \"encryption_dot\": \"Port DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Jika port ini terkonfigurasi, AdGuard Home akan menjalankan server DNS-over-TLS dalam port ini\",\n  \"encryption_enable\": \"Nyalakan Enkripsi (HTTPS, DNS-over-HTTPS, dan DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Jika enkripsi diaktifkan, antarmuka admin AdGuard Home akan berfungsi dengan HTTPS, dan server DNS akan mendengarkan permintaan melalui DNS-over-HTTPS dan DNS-over-TLS.\",\n  \"encryption_expire\": \"Kedaluwarsa\",\n  \"encryption_hostnames\": \"Nama host\",\n  \"encryption_https\": \"Port HTTPS\",\n  \"encryption_https_desc\": \"Jika port HTTPS dikonfigurasi, antarmuka admin Home AdGuard akan dapat diakses melalui HTTPS, dan itu juga akan memberikan DNS-over-HTTPS di lokasi '/ dns-query'.\",\n  \"encryption_issuer\": \"Penerbit\",\n  \"encryption_key\": \"Kunci privat\",\n  \"encryption_key_input\": \"Salin / rekatkan kunci pribadi PEM berkode untuk sertifikat Anda di sini.\",\n  \"encryption_key_invalid\": \"Ini adalah kunci pribadi {{type}} yang tidak valid\",\n  \"encryption_key_source_content\": \"Tempel konten kunci pribadi\",\n  \"encryption_key_source_path\": \"Tetapkan lokasi berkas kunci pribadi\",\n  \"encryption_key_valid\": \"Ini adalah kunci pribadi {{type}} yang valid\",\n  \"encryption_plain_dns_desc\": \"DNS biasa diaktifkan secara standar. Anda dapat menonaktifkannya untuk memaksa semua perangkat menggunakan DNS terenkripsi. Untuk melakukan ini, Anda harus mengaktifkan setidaknya satu protokol DNS terenkripsi\",\n  \"encryption_plain_dns_enable\": \"Aktifkan DNS biasa\",\n  \"encryption_plain_dns_error\": \"Untuk menonaktifkan DNS biasa, aktifkan setidaknya satu protokol DNS terenkripsi\",\n  \"encryption_private_key_path\": \"Path kunci pribadi\",\n  \"encryption_redirect\": \"Alihkan ke HTTPS secara otomatis\",\n  \"encryption_redirect_desc\": \"Jika dicentang, AdGuard Home akan secara otomatis mengarahkan anda dari HTTP ke alamat HTTPS.\",\n  \"encryption_reset\": \"Anda yakin ingin mengatur ulang pengaturan enkripsi?\",\n  \"encryption_server\": \"Nama server\",\n  \"encryption_server_desc\": \"Jika disetel, AdGuard Home mendeteksi ClientID, merespons kueri DDR, dan melakukan validasi koneksi tambahan. Jika tidak disetel, fitur-fitur ini dinonaktifkan. Harus cocok dengan salah satu Nama DNS dalam sertifikat.\",\n  \"encryption_server_enter\": \"Masukkan nama domain anda\",\n  \"encryption_settings\": \"Pengaturan enkripsi\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Subjek\",\n  \"encryption_title\": \"Enkripsi\",\n  \"encryption_warning\": \"Peringatan\",\n  \"enforce_safe_search\": \"Pakai pencarian aman\",\n  \"enforce_save_search_hint\": \"AdGuard Home akan memberlakukan pencarian yang aman di mesin pencari berikut ini: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Paksa pencarian aman\",\n  \"enter_cache_size\": \"Masukkan ukuran cache (bytes)\",\n  \"enter_cache_ttl_max_override\": \"Masukkan TTL maksimum (detik)\",\n  \"enter_cache_ttl_min_override\": \"Masukkan TTL minimum (detik)\",\n  \"enter_name_hint\": \"Masukkan nama\",\n  \"enter_url_or_path_hint\": \"Masukan sebuah URL atau jalur absolut dari daftar\",\n  \"enter_valid_allowlist\": \"Masukkan valid URL ke daftar putih.\",\n  \"enter_valid_blocklist\": \"Masukkan valid URL ke daftar hitam.\",\n  \"error_details\": \"Detail kesalahan\",\n  \"example_comment\": \"! Komentar di sini.\",\n  \"example_comment_hash\": \"# Juga sebuah komentar.\",\n  \"example_comment_meaning\": \"hanya sebuah komentar;\",\n  \"example_meaning_filter_block\": \"blokir akses ke example.org dan seluruh subdomainnya;\",\n  \"example_meaning_filter_whitelist\": \"buka blokir akses ke domain example.org dan seluruh subdomainnya;\",\n  \"example_meaning_host_block\": \"merespons dengan 127.0.0.1 untuk example.org (tetapi tidak untuk subdomainnya);\",\n  \"example_multiple_upstreams_reserved\": \"beberapa hulu <0>untuk domain tertentu</0>;\",\n  \"example_regex_meaning\": \"blokir akses ke domain yang cocok dengan ekspresi reguler yang ditentukan.\",\n  \"example_rewrite_domain\": \"tulis ulang respon hanya untuk domain ini saja.\",\n  \"example_rewrite_wildcard\": \"tulis ulang respon untuk semua subdomain <0>contoh.org</0>.\",\n  \"example_upstream_comment\": \"komentar.\",\n  \"example_upstream_doh\": \"<0>DNS melalui HTTPS</0> terenkripsi;\",\n  \"example_upstream_doh3\": \"DNS melalui HTTPS terenkripsi dengan <0>HTTP/3</0> secara paksa dan tidak ada cadangan ke HTTP/2 atau lebih rendah;\",\n  \"example_upstream_doq\": \"<0>DNS melalui QUIC</0> terenkripsi;\",\n  \"example_upstream_dot\": \"<0>DNS melalui TLS</0> terenkripsi;\",\n  \"example_upstream_regular\": \"DNS biasa (melalui UDP);\",\n  \"example_upstream_regular_port\": \"DNS biasa (melalui UDP, dengan port);\",\n  \"example_upstream_reserved\": \"hulu <0>untuk domain tertentu</0>;\",\n  \"example_upstream_sdns\": \"<0>Stempel DNS</0> untuk <1>DNSCrypt</1> atau pengarah <2>DNS melalui HTTPS</2>;\",\n  \"example_upstream_tcp\": \"DNS biasa (melalui TCP);\",\n  \"example_upstream_tcp_hostname\": \"DNS biasa (melalui TCP, nama host);\",\n  \"example_upstream_tcp_port\": \"DNS biasa (melalui TCP, dengan port);\",\n  \"example_upstream_udp\": \"DNS biasa (melalui UDP, nama host);\",\n  \"examples_title\": \"Contoh\",\n  \"fallback_dns_desc\": \"Daftar server DNS cadangan yang digunakan ketika server hulu DNS tidak merespons. Sintaksnya sama dengan kolom hulu utama di atas.\",\n  \"fallback_dns_placeholder\": \"Masukkan satu server DNS cadangan per baris\",\n  \"fallback_dns_title\": \"Server DNS cadangan\",\n  \"faq\": \"Tanya Jawab\",\n  \"fastest_addr\": \"Alamat IP tercepat\",\n  \"fastest_addr_desc\": \"Tunggu respons dari <b>semua</b> server DNS, ukur kecepatan koneksi TCP untuk setiap server, dan kembalikan alamat IP server dengan kecepatan koneksi tercepat.<br/>Mode ini dapat memperlambat permintaan DNS secara signifikan, jika satu atau beberapa server hulu tidak merespons. Pastikan server hulu Anda stabil dan batas waktu hulu Anda rendah.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"Filter telah berhasil ditambahkan\",\n  \"filter_allowlist\": \"PERINGATAN: Tindakan ini juga akan mengecualikan aturan \\\"{{disallowed_rule}}\\\" dari daftar klien yang diizinkan.\",\n  \"filter_category_general\": \"Umum\",\n  \"filter_category_general_desc\": \"Daftar yang memblokir pelacakan dan iklan di sebagian besar perangkat\",\n  \"filter_category_other\": \"Lainnya\",\n  \"filter_category_other_desc\": \"Daftar hitam lain\",\n  \"filter_category_regional\": \"Wilayah\",\n  \"filter_category_regional_desc\": \"Daftar yang berfokus pada iklan regional dan server pelacakan\",\n  \"filter_category_security\": \"Keamanan\",\n  \"filter_category_security_desc\": \"Daftar yang khusus pada pemblokiran malware, phishing, atau domain penipuan\",\n  \"filter_removed_successfully\": \"Daftar ini telah sukses dihapus\",\n  \"filter_updated\": \"Daftar telah sukses diperbarui\",\n  \"filtered\": \"Tersaring\",\n  \"filtered_custom_rules\": \"Tersaring oleh aturan penyaring Buatan\",\n  \"filtering_rules_learn_more\": \"<0>Pelajari lebih lanjut</0> tentang membuat daftar hitam host Anda sendiri.\",\n  \"filters\": \"Penyaring\",\n  \"filters_and_hosts_hint\": \"AdGuard Home memahami aturan dasar adblock dan sintak berkas host.\",\n  \"filters_block_toggle_hint\": \"Anda dapat menyiapkan aturan pemblokiran dalam pengaturan <a>Filter</a>.\",\n  \"filters_configuration\": \"Konfigurasi filter\",\n  \"filters_enable\": \"Aktifkan filter\",\n  \"filters_interval\": \"Interval pembaruan filter\",\n  \"fix\": \"Perbaiki\",\n  \"for_last_days\": \"untuk {{count}} hari terakhir\",\n  \"for_last_days_plural\": \"selama {{count}} hari terakhir\",\n  \"for_last_hours\": \"selama {{count}} jam terakhir\",\n  \"for_last_hours_plural\": \"selama {{count}} jam terakhir\",\n  \"forgot_password\": \"Lupa kata sandi?\",\n  \"forgot_password_desc\": \"Ikuti <0>langkah-langkah ini</0> untuk membuat kata sandi baru untuk akun pengguna Anda.\",\n  \"form_add_id\": \"Tambahkan pengenal\",\n  \"form_answer\": \"Masaukan alamat IP atau nama domain\",\n  \"form_client_name\": \"Masukkan nama klien\",\n  \"form_domain\": \"Masukkan nama domain\",\n  \"form_enter_blocked_response_ttl\": \"Masukkan TTL respons yang diblokir (detik)\",\n  \"form_enter_host\": \"Masukkan nama host\",\n  \"form_enter_hostname\": \"Masukkan hostname\",\n  \"form_enter_id\": \"Masukkan pengenal\",\n  \"form_enter_ip\": \"Masukkan IP\",\n  \"form_enter_mac\": \"Masukkan MAC\",\n  \"form_enter_rate_limit\": \"Masukkan batas nilai\",\n  \"form_enter_rate_limit_subnet_len\": \"Masukkan panjang awalan subnet untuk pembatasan kecepatan\",\n  \"form_enter_subnet_ip\": \"Masukkan alamat IP di subnet \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Masukkan durasi batas waktu server hulu dalam hitungan detik\",\n  \"form_error_answer_format\": \"Format jawaban tidak valid\",\n  \"form_error_client_id_format\": \"ClientID hanya boleh berisi angka, huruf kecil, dan tanda hubung\",\n  \"form_error_domain_format\": \"Format domain tidak valid\",\n  \"form_error_equal\": \"Tidak boleh sama\",\n  \"form_error_gateway_ip\": \"Lease tidak dapat memiliki gerbang alamat IP\",\n  \"form_error_ip4_format\": \"Alamat IPv4 tidak valid\",\n  \"form_error_ip4_gateway_format\": \"Alamat IPv4 gateway tidak valid\",\n  \"form_error_ip6_format\": \"Alamat IPv6 tidak valid\",\n  \"form_error_ip_format\": \"Alamat IP tidak valid\",\n  \"form_error_mac_format\": \"Alamat MAC tidak valid\",\n  \"form_error_password\": \"Kata sandi tidak cocok\",\n  \"form_error_password_length\": \"Kata sandi harus terdiri dari {{min}} hingga {{max}}\",\n  \"form_error_port\": \"Masukkan nomor port yang valid\",\n  \"form_error_port_range\": \"Masukkan nomor port di kisaran 80-65535\",\n  \"form_error_port_unsafe\": \"Port tidak aman\",\n  \"form_error_positive\": \"Harus lebih dari 0\",\n  \"form_error_required\": \"Kolom yang harus diisi\",\n  \"form_error_server_name\": \"Nama server tidak valid\",\n  \"form_error_subnet\": \"Subnet \\\"{{cidr}}\\\" tidak berisi alamat IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Format URL tidak valid\",\n  \"form_error_url_or_path_format\": \"URL atau jalur absolut dari daftar tidak valid\",\n  \"form_select_tags\": \"Pilih tag klien\",\n  \"found_in_known_domain_db\": \"Ditemukan di basis data domain yang dikenal.\",\n  \"friday\": \"Jumat\",\n  \"friday_short\": \"Jum\",\n  \"gateway_or_subnet_invalid\": \"Subnet samaran tidak valid\",\n  \"general_settings\": \"Pengaturan umum\",\n  \"general_statistics\": \"Statistik umum\",\n  \"get_started\": \"Mari mulai\",\n  \"greater_range_start_error\": \"Harus lebih besar dari rentang awal\",\n  \"homepage\": \"Beranda\",\n  \"host_whitelisted\": \"Host didaftar putihkan\",\n  \"ignore_domains\": \"Domain yang diabaikan (dipisahkan oleh baris baru)\",\n  \"ignore_domains_desc_query\": \"Kueri yang cocok dengan aturan ini tidak ditulis ke catatan kueri\",\n  \"ignore_domains_desc_stats\": \"Kueri yang cocok dengan aturan ini tidak ditulis ke statistik\",\n  \"ignore_domains_title\": \"Domain yang diabaikan\",\n  \"ignore_query_log\": \"Abaikan klien ini di catatan kueri\",\n  \"ignore_statistics\": \"Abaikan klien ini di statistik\",\n  \"install_auth_confirm\": \"Konfirmasi kata sandi\",\n  \"install_auth_desc\": \"Otentikasi kata sandi ke antarmuka web admin AdGuard Home Anda harus dikonfigurasi. Meskipun AdGuard Home hanya dapat diakses di jaringan lokal Anda, tetap penting untuk melindunginya dari akses tak terbatas.\",\n  \"install_auth_password\": \"Kata Sandi\",\n  \"install_auth_password_enter\": \"Masukkan kata sandi\",\n  \"install_auth_title\": \"Otentikasi\",\n  \"install_auth_username\": \"Nama Pengguna\",\n  \"install_auth_username_enter\": \"Masukkan nama pengguna\",\n  \"install_devices_address\": \"Server DNS AdGuard Home akan menggunakan alamat berikut\",\n  \"install_devices_android_list_1\": \"Dari layar beranda Menu Android, ketuk Pengaturan.\",\n  \"install_devices_android_list_2\": \"Ketuk Wi-Fi pada menu. Layar akan mencantumkan semua jaringan yang tersedia dan akan ditampilkan (tidak mungkin untuk mengatur DNS khusus untuk koneksi seluler).\",\n  \"install_devices_android_list_3\": \"Tekan lama jaringan yang terhubung, dan ketuk Ubah Jaringan.\",\n  \"install_devices_android_list_4\": \"Pada beberapa perangkat, Anda mungkin perlu mencentang kotak Advanced untuk melihat pengaturan lebih lanjut. Untuk menyesuaikan pengaturan DNS Android Anda, Anda perlu mengalihkan pengaturan IP dari DHCP ke Statis.\",\n  \"install_devices_android_list_5\": \"Ubah nilai DNS 1 dan DNS 2 ke alamat server AdGuard Home Anda.\",\n  \"install_devices_desc\": \"Agar AdGuard Home dapat berfungsi dengan baik, anda perlu mengkonfigurasi perangkat ada untuk menggunakannya\",\n  \"install_devices_ios_list_1\": \"Dari layar beranda, ketuk Pengaturan.\",\n  \"install_devices_ios_list_2\": \"Pilih Wi-Fi di menu sebelah kiri (tidak mungkin untuk mengkonfigurasi DNS untuk jaringan seluler).\",\n  \"install_devices_ios_list_3\": \"Ketuk nama jaringan yang saat ini aktif.\",\n  \"install_devices_ios_list_4\": \"Di kolom DNS, masukkan alamat server AdGuard Home Anda.\",\n  \"install_devices_macos_list_1\": \"Klik ikon Apple dan buka Preferensi Sistem.\",\n  \"install_devices_macos_list_2\": \"Klik Jaringan.\",\n  \"install_devices_macos_list_3\": \"Pilih koneksi pertama dalam daftar dan klik Advanced.\",\n  \"install_devices_macos_list_4\": \"Pilih tab DNS dan masukkan alamat server AdGuard Anda.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Penyiapan ini secara otomatis mencakup semua perangkat yang terhubung ke router rumah Anda, tidak perlu mengkonfigurasi masing-masing perangkat secara manual.\",\n  \"install_devices_router_list_1\": \"Buka preferensi untuk router Anda. Biasanya, Anda dapat mengaksesnya dari peramban Anda melalui URL, seperti http://192.168.0.1/ atau http://192.168.1.1/. Anda mungkin diminta untuk memasukkan kata sandi. Jika Anda tidak mengingatnya, Anda sering kali dapat mengatur ulang kata sandi dengan menekan tombol pada router itu sendiri, tetapi perlu diketahui bahwa jika prosedur ini dipilih, Anda mungkin akan kehilangan seluruh konfigurasi router. Jika router Anda memerlukan aplikasi untuk menyiapkannya, pasang aplikasi tersebut di ponsel atau PC Anda dan gunakan untuk mengakses pengaturan router.\",\n  \"install_devices_router_list_2\": \"Temukan pengaturan DHCP / DNS. Cari huruf DNS di sebelah kolom yang memungkinkan dua atau tiga set angka, masing-masing dipecah menjadi empat kelompok dengan satu hingga tiga digit.\",\n  \"install_devices_router_list_3\": \"Masukkan alamat server AdGuard Home disana\",\n  \"install_devices_router_list_4\": \"Anda tidak dapat menyetel server DNS kustom pada beberapa tipe router. Dalam hal ini mungkin membantu jika Anda mengatur AdGuard Home sebagai <0>server DHCP</0>. Jika tidak, Anda harus mencari petunjuk tentang cara mengkustomisasi server DNS untuk model router khusus Anda.\",\n  \"install_devices_title\": \"Konfigurasikan perangkat anda\",\n  \"install_devices_windows_list_1\": \"Buka Panel Kontrol melalui menu Start atau pencarian Windows.\",\n  \"install_devices_windows_list_2\": \"Masuk ke kategori Jaringan dan Internet (Network and Internet) dan kemudian ke Pusat Jaringan dan Berbagi (Network and Sharing Center).\",\n  \"install_devices_windows_list_3\": \"Di panel kiri, klik \\\"Ubah pengaturan adaptor\\\".\",\n  \"install_devices_windows_list_4\": \"Klik kanan koneksi aktif Anda dan pilih Properti.\",\n  \"install_devices_windows_list_5\": \"Temukan \\\"Protokol Internet Versi 4 (TCP/IPv4)\\\" (atau, untuk IPv6, \\\"Protokol Internet Versi 6 (TCP/IPv6)\\\") dalam daftar, pilih dan kemudian klik Properti lagi.\",\n  \"install_devices_windows_list_6\": \"Pilih \\\"Gunakan alamat server DNS berikut\\\" dan masukkan alamat server Beranda AdGuard Anda.\",\n  \"install_saved\": \"Berhasil disimpan\",\n  \"install_settings_all_interfaces\": \"Semua antarmuka\",\n  \"install_settings_dns\": \"Server DNS\",\n  \"install_settings_dns_desc\": \"Anda perlu mengkonfigurasi perangkat atau router anda untuk menggunakan server DNS berikut ini\",\n  \"install_settings_interface_link\": \"Laman administrasi AdGuard Home akan tersedia di alamat berikut ini\",\n  \"install_settings_listen\": \"Antarmuka pengoperasian\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Antarmuka Halaman Admin\",\n  \"install_static_configure\": \"AdGuard Home mendeteksi alamat IP dinamis  <0>{{ip}}</0> digunakan. Anda ingin menggunakannya sebagai alamat statis Anda?\",\n  \"install_static_error\": \"AdGuard Home tidak dapat mengonfigurasinya secara otomatis untuk antarmuka jaringan ini. Silakan cari instruksi tentang cara melakukan ini secara manual.\",\n  \"install_static_ok\": \"Kabar baik! Alamat IP statis sudah dikonfigurasi\",\n  \"install_step\": \"langkah\",\n  \"install_submit_desc\": \"Prosedur pengaturan telah selesai, dan anda siap untuk mulai menggunakan AdGuard Home.\",\n  \"install_submit_title\": \"Selamat!\",\n  \"install_welcome_desc\": \"AdGuard Home adalah server DNS pemblokir iklan dan pelacak di seluruh jaringan. Tujuannya untuk memungkinkan Anda mengendalikan seluruh jaringan dan semua perangkat Anda, dan tidak perlu menggunakan program sisi klien.\",\n  \"install_welcome_title\": \"Selamat datang di AdGuard Home!\",\n  \"interval_24_hour\": \"24 jam\",\n  \"interval_6_hour\": \"6 jam\",\n  \"interval_days\": \"{{count}} hari\",\n  \"interval_days_plural\": \"{{count}} hari\",\n  \"interval_hours\": \"{{count}} jam\",\n  \"interval_hours_plural\": \"{{count}} jam\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Alamat IP\",\n  \"known_tracker\": \"Pelacak yang dikenal\",\n  \"last_rule_in_allowlist\": \"Tidak dapat melarang klien ini karena mengecualikan aturan \\\"{{disallowed_rule}}\\\" akan MENONAKTIFKAN daftar \\\"Klien yang diizinkan\\\".\",\n  \"last_time_updated_table_header\": \"Terakhir diperbaharui\",\n  \"list_confirm_delete\": \"Anda yakin ingin menghapus daftar ini?\",\n  \"list_label\": \"Daftar\",\n  \"list_updated\": \"{{count}} daftar terbarui\",\n  \"list_updated_plural\": \"{{count}} daftar terbarui\",\n  \"list_url_table_header\": \"Daftar URL\",\n  \"load_balancing\": \"Penyeimbang beban\",\n  \"load_balancing_desc\": \"Kueri satu server hulu dalam satu waktu.<br/>AdGuard Home menggunakan algoritma acak tertimbang untuk memilih server dengan jumlah pencarian gagal terendah dan waktu pencarian rata-rata terendah.\",\n  \"loading_table_status\": \"Memuat...\",\n  \"local_ptr_default_resolver\": \"Secara bawaan, AdGuard Home menggunakan pemecah DNS terbalik: {{ip}}.\",\n  \"local_ptr_desc\": \"Server DNS yang digunakan oleh AdGuard Home untuk permintaan PTR, SOA, dan NS pribadi. Permintaan dianggap pribadi jika meminta domain ARPA yang berisi subnet dalam rentang IP pribadi (seperti \\\"192.168.12.34\\\") dan berasal dari klien dengan alamat IP pribadi. Jika tidak ditetapkan, standar pemecah DNS milik OS Anda akan digunakan, kecuali untuk alamat IP AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home tidak dapat menentukan pemecah DNS terbalik yang sesuai untuk sistem ini.\",\n  \"local_ptr_placeholder\": \"Masukkan satu alamat IP per baris\",\n  \"local_ptr_title\": \"Server pembalik DNS pribadi\",\n  \"location\": \"Lokasi\",\n  \"log_and_stats_section_label\": \"Catatan kueri dan statistik\",\n  \"lower_range_start_error\": \"Harus lebih rendah dari rentang awal\",\n  \"main_settings\": \"Pengaturan utama\",\n  \"make_static\": \"Jadikan statis\",\n  \"manual_update\": \"Silakan <a>mengikuti langkah berikut</a> untuk memperbarui secara manual.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Senin\",\n  \"monday_short\": \"Sen\",\n  \"name\": \"Nama\",\n  \"name_table_header\": \"Nama\",\n  \"netname\": \"Nama jaringan\",\n  \"network\": \"Jaringan\",\n  \"new_allowlist\": \"Daftar putih baru\",\n  \"new_blocklist\": \"Daftar hitam baru\",\n  \"next\": \"Selanjutnya\",\n  \"next_btn\": \"Selanjutnya\",\n  \"no_blocklist_added\": \"Tidak ada daftar hitam yang ditambahkan\",\n  \"no_clients_found\": \"Tidak ditemukan klien\",\n  \"no_domains_found\": \"Domain tidak ditemukan\",\n  \"no_logs_found\": \"Tidak ditemukan catatan\",\n  \"no_servers_specified\": \"Sever tidak disebutkan\",\n  \"no_upstreams_data_found\": \"Tidak ada data hulu yang ditemukan\",\n  \"no_whitelist_added\": \"Tidak ada daftar putih yang ditambahkan\",\n  \"nothing_found\": \"Tidak ditemukan\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Jumlah permintaan DNS yang diblokir oleh filter adblock dan daftar hitam host\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Jumlah situs web dewasa yang diblokir\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Jumlah permintaan DNS yang diblokir oleh modul keamanan penjelajahan AdGuard\",\n  \"number_of_dns_query_days\": \"Jumlah kueri DNS diproses selama {{value}} hari terakhir\",\n  \"number_of_dns_query_days_plural\": \"Jumlah kueri DNS yang diproses selama {{count}} hari terakhir\",\n  \"number_of_dns_query_hours\": \"Jumlah kueri DNS diproses selama {{{count}} jam terakhir\",\n  \"number_of_dns_query_hours_plural\": \"Jumlah kueri DNS diproses selama {{count}} jam terakhir\",\n  \"number_of_dns_query_to_safe_search\": \"Jumlah perminataan DNS ke mesin pencari yang dipaksa Pencarian Aman\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"MATI\",\n  \"on\": \"HIDUP\",\n  \"open_dashboard\": \"Buka Beranda\",\n  \"orgname\": \"Nama organisasi\",\n  \"original_response\": \"Respon asli\",\n  \"out_of_range_error\": \"Harus di luar rentang \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Halaman\",\n  \"parallel_requests\": \"Permintaan paralel\",\n  \"parental_control\": \"Pengawasan Orang Tua\",\n  \"password_label\": \"Kata sandi\",\n  \"password_placeholder\": \"Masukkan kata sandi\",\n  \"plain_dns\": \"Plain DNS\",\n  \"port_53_faq_link\": \"Port 53 sering ditempati oleh layanan \\\"DNSStubListener\\\" atau \\\"systemd-resolved\\\". Silakan baca <0>instruksi ini</0> tentang cara menyelesaikan ini.\",\n  \"previous_btn\": \"Sebelumnya\",\n  \"privacy_policy\": \"Kebijakan Privasi\",\n  \"processing_update\": \"Silahkan tunggu, AdGuard Home sedang diperbarui\",\n  \"protection_section_label\": \"Perlindungan\",\n  \"protocol\": \"Protokol\",\n  \"punycode\": \"Kode kecil\",\n  \"query_log\": \"Catatan Kueri\",\n  \"query_log_clear\": \"Hapus catatan kueri\",\n  \"query_log_cleared\": \"Catatan kueri berhasil dihapus\",\n  \"query_log_configuration\": \"Konfigurasi catatan\",\n  \"query_log_confirm_clear\": \"Apakah Anda yakin ingin menghapus seluruh catatan kueri?\",\n  \"query_log_disabled\": \"Catatan kueri dinonaktifkan dan dapat dikonfigurasi di <0>pengaturan</0>\",\n  \"query_log_enable\": \"Aktifkan catatan\",\n  \"query_log_filtered\": \"Difilter oleh {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotasi kueri log\",\n  \"query_log_retention_confirm\": \"Apakah Anda yakin ingin mengubah rotasi kueri log? Jika Anda menurunkan nilai interval, beberapa data akan hilang\",\n  \"query_log_strict_search\": \"Gunakan tanda kutip ganda untuk pencarian ketat\",\n  \"query_log_updated\": \"Catatan kueri berhasil diperbarui\",\n  \"rate_limit\": \"Batas nilai\",\n  \"rate_limit_desc\": \"Jumlah permintaan per detik yang diperbolehkan untuk satu klien. Atur ke 0 untuk tidak terbatas.\",\n  \"rate_limit_subnet_len_ipv4\": \"Panjang awalan subnet untuk alamat IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Panjang awalan subnet untuk alamat IPv4 yang digunakan untuk pembatasan kecepatan. Standarnya adalah 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Panjang awalan subnet IPv4 harus antara 0 dan 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Panjang awalan subnet untuk alamat IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Panjang awalan subnet untuk alamat IPv6 yang digunakan untuk pembatasan kecepatan. Standarnya adalah 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Panjang awalan subnet IPv6 harus antara 0 dan 128\",\n  \"rate_limit_whitelist\": \"Daftar pembatasan tarif yang diizinkan\",\n  \"rate_limit_whitelist_desc\": \"Alamat IP dikecualikan dari pembatasan tarif\",\n  \"rate_limit_whitelist_placeholder\": \"Masukkan satu alamat IP per baris\",\n  \"refresh_btn\": \"Segarkan\",\n  \"refresh_statics\": \"Segarkan statistik\",\n  \"refused\": \"DITOLAK\",\n  \"report_an_issue\": \"Lapor masalah\",\n  \"request_details\": \"Detai permintaan\",\n  \"request_table_header\": \"Permintaan\",\n  \"requests_count\": \"Jumlah permintaan\",\n  \"reset_settings\": \"Setel ulang pengaturan\",\n  \"resolve_clients_desc\": \"Selesaikan alamat IP klien secara terbalik ke dalam nama host mereka dengan mengirimkan kueri PTR ke penyelesai yang sesuai (server DNS pribadi untuk klien lokal, server hulu untuk klien dengan alamat IP publik).\",\n  \"resolve_clients_title\": \"Aktifkan resolusi hostname klien\",\n  \"response_code\": \"Kode respon\",\n  \"response_details\": \"Detail respon\",\n  \"response_table_header\": \"Respon\",\n  \"response_time\": \"Waktu respons\",\n  \"rewrite_A\": \"<0>A</0>: nilai khusus, biarkan <0>A</0> merekam dari hulu\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: nilai khusus, biarkan <0>AAAA</0> merekam dari hulu\",\n  \"rewrite_add\": \"Tambahkan penulisan ulang DNS\",\n  \"rewrite_added\": \"DNS rewrite untuk \\\"{{key}}\\\" berhasil ditambahkan\",\n  \"rewrite_applied\": \"Aturan Rewrite yang diterapkan\",\n  \"rewrite_confirm_delete\": \"Apakah anda yakin ingin menghapus DNS rewrite untuk \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"DNS rewrite untuk \\\"{{key}}\\\" berhasil dihapus\",\n  \"rewrite_desc\": \"Memungkinkan untuk dengan mudah mengkonfigurasi respons DNS kustom untuk nama domain tertentu.\",\n  \"rewrite_domain_name\": \"Nama domain: tambah ke rekaman CNAME\",\n  \"rewrite_edit\": \"Edit penulisan ulang DNS\",\n  \"rewrite_hosts_applied\": \"Ditulis ulang oleh aturan berkas host\",\n  \"rewrite_ip_address\": \"Alamat IP: pakai IP ini dalam respons A atau AAAA\",\n  \"rewrite_not_found\": \"Tidak ada DNS rewrite ditemukan\",\n  \"rewrite_settings_updated\": \"Pengaturan penulisan ulang DNS berhasil diperbarui\",\n  \"rewrite_updated\": \"Penulisan ulang DNS berhasil diperbarui\",\n  \"rewrites_disabled_table_header\": \"Penulisan ulang dinonaktifkan\",\n  \"rewrites_enabled_table_header\": \"Penulisan ulang diaktifkan\",\n  \"rewritten\": \"Tulis ulang\",\n  \"rows_table_footer_text\": \"baris\",\n  \"rule_added_to_custom_filtering_toast\": \"Aturan ditambah ke aturan penyaringan khusus: {{rule}}\",\n  \"rule_label\": \"Atura(n)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Aturan dihapus dari aturan penyaringan khusus: {{rule}}\",\n  \"rules_count_table_header\": \"Jumlah Aturan\",\n  \"safe_browsing\": \"Penjelajahan Aman\",\n  \"safe_search\": \"Pencarian aman\",\n  \"saturday\": \"Sabtu\",\n  \"saturday_short\": \"Sab\",\n  \"save_btn\": \"Simpan\",\n  \"save_config\": \"Simpan pengaturan\",\n  \"schedule_add\": \"Tambahkan jadwal\",\n  \"schedule_current_timezone\": \"Zona waktu saat ini: {{value}}\",\n  \"schedule_desc\": \"Tetapkan periode tidak aktif untuk layanan yang diblokir\",\n  \"schedule_edit\": \"Edit jadwal\",\n  \"schedule_from\": \"Dari\",\n  \"schedule_invalid_select\": \"Waktu mulai harus sebelum waktu akhir\",\n  \"schedule_modal_description\": \"Jadwal ini akan menggantikan jadwal sekarang untuk hari yang sama. Setiap hari di setiap minggu hanya boleh ada satu periode tidak aktif.\",\n  \"schedule_modal_time_off\": \"Tidak ada pemblokiran layanan:\",\n  \"schedule_new\": \"Jadwal baru\",\n  \"schedule_remove\": \"Hapus jadwal\",\n  \"schedule_save\": \"Simpan jadwal\",\n  \"schedule_select_days\": \"Pilih hari\",\n  \"schedule_services\": \"Jeda pemblokiran layanan\",\n  \"schedule_services_desc\": \"Mengonfigurasi jadwal jeda filter pemblokiran layanan\",\n  \"schedule_services_desc_client\": \"Mengonfigurasi jadwal jeda filter pemblokiran layanan untuk klien ini\",\n  \"schedule_time_all_day\": \"Sepanjang hari\",\n  \"schedule_timezone\": \"Pilih zona waktu\",\n  \"schedule_to\": \"Hingga\",\n  \"served_from_cache_label\": \"Disajikan dari cache\",\n  \"service_name\": \"Nama layanan\",\n  \"set_static_ip\": \"Atur alamat IP statik\",\n  \"settings\": \"Pengaturan\",\n  \"settings_custom\": \"Kustom\",\n  \"settings_global\": \"Global\",\n  \"setup_config_to_enable_dhcp_server\": \"Setel konfigurasi untuk aktifkan server DHCP\",\n  \"setup_dns_notice\": \"Jikalau ingin menggunakan <1>DNS-over-HTTPS</1> atau <1>DNS-over-TLS</1>, Anda perlu <0>mengatur Enkripsi</0> pada pengaturan AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS melalui TLS:</0> Gunakan <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-TLS:</0> Memakai <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_3\": \"<0>Berikut daftar perangkat lunak yang dapat Anda gunakan.</0>\",\n  \"setup_dns_privacy_4\": \"Pada perangkat iOS 14 atau macOS Big Sur, Anda dapat mengunduh berkas khusus '.mobileconfig' yang menambahkan server <highlight>DNS melalui HTTPS</highlight> atau <highlight>DNS melalui TLS</highlight> ke pengaturan DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 mendukung DNS-over-TLS secara asli. Untuk mengkonfigurasinya, buka Pengaturan → Jaringan & internet → Tingkat Lanjut → DNS Pribadi dan masukkan nama domain Anda di sana.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard untuk Android</0> mendukung <1>DNS-over-HTTPS</1> dan <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> menambahkan dukungan <1>DNS-over-HTTPS</1> untuk Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Konfigurasi iOS dan macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> mendukung <1>DNS-over-HTTPS</1>, tetapi untuk mengkonfigurasinya untuk menggunakan server Anda sendiri, Anda harus membuat <2>DNS Stamp</2> untuk itu.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard untuk iOS</0> mendukung <1>DNS-over-HTTPS</1> dan pengaturan <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home sendiri dapat menjadi klien DNS aman di platform apa pun.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> mendukung semua protokol DNS aman yang diketahui.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> mendukung <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> mendukung <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Anda akan menemukan lebih banyak implementasi <0>di sini</0> dan <1>di sini</1>.\",\n  \"setup_dns_privacy_other_title\": \"Implementasi lain\",\n  \"setup_guide\": \"Panduan Penyiapan\",\n  \"show_all_filter_type\": \"Tampilkan semua\",\n  \"show_blocked_responses\": \"Diblokir\",\n  \"show_filtered_type\": \"Tampilkan disaring\",\n  \"show_processed_responses\": \"Terproses\",\n  \"show_whitelisted_responses\": \"Dalam Daftar Putih\",\n  \"sign_in\": \"Masuk\",\n  \"sign_out\": \"Keluar\",\n  \"source_label\": \"Sumber\",\n  \"static_ip\": \"Alamat IP statis\",\n  \"static_ip_desc\": \"AdGuard Home adalah server jadi perlu alamat IP statis agar berfungsi dengan benar. Jika tidak, pada titik tertentu, router Anda dapat menetapkan alamat IP yang berbeda untuk perangkat ini.\",\n  \"statistics_clear\": \"Hapus statistik\",\n  \"statistics_clear_confirm\": \"Apakah Anda yakin ingin menghapus statistik?\",\n  \"statistics_cleared\": \"Statistik berhasil dihapus\",\n  \"statistics_configuration\": \"Konfigurasi statistik\",\n  \"statistics_enable\": \"Aktifkan statistik\",\n  \"statistics_retention\": \"Statistik disimpan\",\n  \"statistics_retention_confirm\": \"Apakah Anda yakin ingin mengubah retensi statistik? Jika Anda menurunkan nilai interval, beberapa data akan hilang\",\n  \"statistics_retention_desc\": \"Jika Anda menurunkan nilai interval, beberapa data akan hilang\",\n  \"stats_adult\": \"Situs dewasa terblokir\",\n  \"stats_disabled\": \"Statistik telah dinonaktifkan. Anda dapat mengaktifkannya dari <0>halaman setelan</0>.\",\n  \"stats_disabled_short\": \"Statistik telah dinonaktifkan\",\n  \"stats_malware_phishing\": \"Malware/phishing terblokir\",\n  \"stats_params\": \"Konfigurasi statistik\",\n  \"stats_query_domain\": \"Kueri domain teratas\",\n  \"subnet_error\": \"Alamat harus dalam satu subnet\",\n  \"sunday\": \"Minggu\",\n  \"sunday_short\": \"Ming\",\n  \"system_host_files\": \"Berkas host sistem\",\n  \"table_client\": \"Klien\",\n  \"table_name\": \"Nama\",\n  \"tags_desc\": \"Anda dapat memilih tag yang sesuai dengan klien. Sertakan tag dalam aturan pemfilteran untuk menerapkannya dengan lebih akurat. <0>Pelajari lebih lanjut</0>.\",\n  \"tags_title\": \"Tag\",\n  \"test_upstream_btn\": \"Uji hulu\",\n  \"theme_auto\": \"Otomatis\",\n  \"theme_auto_desc\": \"Otomatis (berdasarkan skema warna perangkat anda)\",\n  \"theme_dark\": \"Gelap\",\n  \"theme_dark_desc\": \"Tema gelap\",\n  \"theme_light\": \"Terang\",\n  \"theme_light_desc\": \"Tema terang\",\n  \"thursday\": \"Kamis\",\n  \"thursday_short\": \"Kam\",\n  \"time_table_header\": \"Waktu\",\n  \"top_blocked_domains\": \"Domain diblokir teratas\",\n  \"top_clients\": \"Klien teratas\",\n  \"top_upstreams\": \"Hulu teratas\",\n  \"topline_expired_certificate\": \"Sertifikat SSL Anda kedaluwarsa. Perbarui <0>Pengaturan enkripsi</0>.\",\n  \"topline_expiring_certificate\": \"Sertifikat SSL Anda hampir kedaluwarsa. Perbarui <0>Pengaturan enkripsi</0>.\",\n  \"tracker_source\": \"Sumber pelacak\",\n  \"try_again\": \"Coba lagi\",\n  \"ttl_cache_validation\": \"Nilai TTL cache minimum harus kurang dari atau sama dengan nilai maksimum\",\n  \"tuesday\": \"Selasa\",\n  \"tuesday_short\": \"Sel\",\n  \"type_table_header\": \"Tipe\",\n  \"unavailable_dhcp\": \"DHCP tidak tersedia\",\n  \"unavailable_dhcp_desc\": \"Adguard Home tidak dapat menjalankan server DHCP pada OS Anda\",\n  \"unblock\": \"Buka Blokir\",\n  \"unblock_all\": \"Buka semua blokir\",\n  \"unblock_for_this_client_only\": \"Jangan diblok hanya untuk klien ini\",\n  \"unknown_filter\": \"Penyaringan {{filterId}} tidak dikenal\",\n  \"update_announcement\": \"AdGuard Home {{version}} sekarang tersedia! <0>Klik di sini</0> untuk info lebih lanjut.\",\n  \"update_failed\": \"Pembaruan otomatis gagal. Silakan <a>ikuti langkah-langkah berikut</a> untuk memperbarui secara manual.\",\n  \"update_now\": \"Perbarui sekarang\",\n  \"updated_custom_filtering_toast\": \"Aturan kustom berhasil disimpan\",\n  \"updated_save_search_toast\": \"Pengaturan Pencarian Aman telah diperbarui\",\n  \"updated_upstream_dns_toast\": \"Server hulu berhasil disimpan\",\n  \"updates_checked\": \"Versi baru AdGuard Home tersedia\",\n  \"updates_version_equal\": \"AdGuard Home sudah tebaru\",\n  \"upstream\": \"Hulu\",\n  \"upstream_dns\": \"Server DNS hulu\",\n  \"upstream_dns_cache_configuration\": \"Konfigurasi cache DNS hulu\",\n  \"upstream_dns_client_desc\": \"Jika Anda biarkan kolom ini kosong, AdGuard Home akan menggunakan server yang dikonfigurasi di <0>pengaturan DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Diatur dalam {{path}}\",\n  \"upstream_dns_help\": \"Masukkan satu alamat server per baris. <a>Pelajari lebih lanjut</a> mengenai cara mengonfigurasi server DNS hulu.\",\n  \"upstream_parallel\": \"Gunakan kueri paralel untuk mempercepat penyelesaian dengan mengkueri seluruh server hulu secara bersamaan.\",\n  \"upstream_timeout\": \"Batas waktu hulu\",\n  \"upstream_timeout_desc\": \"Menentukan jumlah detik untuk menunggu respons dari server hulu\",\n  \"upstreams\": \"Hulu\",\n  \"use_adguard_browsing_sec\": \"Gunakan layanan web Keamanan Penjelajahan AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home akan memeriksa apakah domain diblokir oleh layanan web keamanan penjelajahan. Ini akan menggunakan API pencarian yang ramah privasi untuk melakukan pemeriksaan: hanya awalan singkat dari hash nama domain SHA256 yang dikirim ke server.\",\n  \"use_adguard_parental\": \"Gunakan layanan web kontrol orang tua AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home akan mengecek jika domain mengandung materi dewasa. Akan menggunakan API yang ramah privasi yang sama sebagai layanan web keamanan penjelajahan.\",\n  \"use_private_ptr_resolvers_desc\": \"Menyelesaikan permintaan PTR, SOA, dan NS untuk domain ARPA yang berisi alamat IP pribadi melalui server hulu pribadi, DHCP, /etc/hosts, dll. Jika dinonaktifkan, AdGuard Home akan merespons semua permintaan tersebut dengan NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Gunakan server pembalik DNS pribadi\",\n  \"use_saved_key\": \"Gunakan kunci yang disimpan sebelumnya\",\n  \"username_label\": \"Nama pengguna\",\n  \"username_placeholder\": \"Masukkan nama pengguna\",\n  \"validated_with_dnssec\": \"Tervalidasi dengan DNSSEC\",\n  \"version\": \"versi\",\n  \"version_request_error\": \"Pemeriksaan pembaruan gagal. Harap periksa koneksi internet anda.\",\n  \"wednesday\": \"Rabu\",\n  \"wednesday_short\": \"Rab\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/it.json",
    "content": "{\n  \"access_allowed_desc\": \"Un elenco di CIDR, indirizzi IP, o <a>ClientID</a>. Se l'elenco conterrà elementi, AdGuard Home accetterà richieste solo da questi client.\",\n  \"access_allowed_title\": \"Client permessi\",\n  \"access_blocked_desc\": \"Da non confondere con i filtri. AdGuard Home eliminerà le richieste DNS corrispondenti a questi domini e queste richieste non verranno visualizzate nel relativo registro. Puoi specificare nomi di dominio esatti, caratteri jolly o regole di filtraggio URL, ad esempio \\\"esempio.org\\\", \\\"*.esempio.org\\\" o \\\"||esempio.org^\\\".\",\n  \"access_blocked_title\": \"Domini bloccati\",\n  \"access_desc\": \"Qui puoi configurare le regole d'accesso per il server DNS di AdGuard Home\",\n  \"access_disallowed_desc\": \"Un elenco di CIDR, indirizzi IP o <a>ClientID</a>. Se l'elenco conterrà degli elementi, AdGuard Home rifiuterà richieste da questi client. Questo campo verrà ignorato se ci saranno elementi nei client Consentiti.\",\n  \"access_disallowed_title\": \"Client non permessi\",\n  \"access_settings_saved\": \"Impostazioni di accesso salvate correttamente\",\n  \"access_title\": \"Impostazioni di accesso\",\n  \"actions_table_header\": \"Azioni\",\n  \"add_allowlist\": \"Aggiungi lista bianca\",\n  \"add_blocklist\": \"Aggiungi lista nera\",\n  \"add_custom_list\": \"Aggiungi elenco personalizzato\",\n  \"add_persistent_client\": \"Aggiungi come client persistente\",\n  \"address\": \"Indirizzo\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home eliminerà tutte le richieste DNS da questo client.\",\n  \"all_lists_up_to_date_toast\": \"Tutti gli elenchi sono aggiornati\",\n  \"all_queries\": \"Tutte le richieste\",\n  \"allow_this_client\": \"Consenti questo client\",\n  \"allowed\": \"Consentito\",\n  \"anonymize_client_ip\": \"Anonimizza client IP\",\n  \"anonymize_client_ip_desc\": \"Non salvare l'indirizzo IP completo del client nel registro o nelle statistiche\",\n  \"anonymizer_notification\": \"<0>Attenzione:</0> L'anonimizzazione dell'IP è abilitata. Puoi disabilitarla in <1>Impostazioni generali</1>.\",\n  \"answer\": \"Risposta\",\n  \"apply_btn\": \"Applica\",\n  \"auto_clients_desc\": \"Informazioni sugli indirizzi IP dei dispositivi che utilizzano o potrebbero utilizzare AdGuard Home. Queste informazioni vengono raccolte da diverse fonti, inclusi file host, DNS inverso, ecc.\",\n  \"auto_clients_title\": \"Client in tempo reale\",\n  \"autofix_warning_list\": \"Eseguirà queste attività: <0> Disattiva DNSStubListener di sistema </0> <0> Imposta l'indirizzo del server DNS su 127.0.0.1 </0> <0> Sostituisci la destinazione del collegamento simbolico di /etc/resolv.conf su / run / systemd /resolve/resolv.conf </0> <0> Arresta DNSStubListener (ricarica il servizio systemd-resolved) </0>\",\n  \"autofix_warning_result\": \"Di conseguenza, tutte le richieste DNS dal sistema verranno elaborate da AdGuardHome per impostazione predefinita.\",\n  \"autofix_warning_text\": \"Se fai clic su \\\"Correggi\\\", AdGuardHome configurerà il tuo sistema per utilizzare il server DNS AdGuardHome.\",\n  \"average_processing_time\": \"Tempo di elaborazione medio\",\n  \"average_processing_time_hint\": \"Tempo medio in millisecondi per elaborare una richiesta DNS\",\n  \"average_upstream_response_time\": \"Tempo medio di risposta upstream\",\n  \"back\": \"Indietro\",\n  \"block\": \"Blocca\",\n  \"block_all\": \"Blocca tutto\",\n  \"block_domain_use_filters_and_hosts\": \"Blocca domini utilizzando filtri e file hosts\",\n  \"block_for_this_client_only\": \"Blocca solo per questo client\",\n  \"block_services\": \"Blocca servizi specifici\",\n  \"blocked_adult_websites\": \"Bloccato da Controllo Parentale\",\n  \"blocked_by\": \"<0>Bloccato dai Filtri</0>\",\n  \"blocked_by_cname_or_ip\": \"Bloccato da CNAME o IP\",\n  \"blocked_by_response\": \"Bloccato per CNAME o IP in risposta\",\n  \"blocked_response_ttl\": \"Risposta TTL bloccata\",\n  \"blocked_response_ttl_desc\": \"Specifica per quanti secondi i client devono tenere nella cache una risposta filtrata\",\n  \"blocked_safebrowsing\": \"Bloccato da Navigazione Sicura\",\n  \"blocked_service\": \"Servizio bloccato\",\n  \"blocked_services\": \"Servizi bloccati\",\n  \"blocked_services_desc\": \"Consente di bloccare rapidamente siti e servizi popolari.\",\n  \"blocked_services_global\": \"Utilizza le servizi globali bloccati\",\n  \"blocked_services_saved\": \"Servizi bloccati salvati correttamente\",\n  \"blocked_threats\": \"Minacce bloccate\",\n  \"blocking_ipv4\": \"Blocca IPv4\",\n  \"blocking_ipv4_desc\": \"Indirizzo IP per una richiesta DNS IPv4 bloccata\",\n  \"blocking_ipv6\": \"Blocca IPv6\",\n  \"blocking_ipv6_desc\": \"Indirizzo IP restituito per una richiesta DNS IPv6 bloccata\",\n  \"blocking_mode\": \"Modalità di blocco\",\n  \"blocking_mode_custom_ip\": \"IP personalizzato: Rispondi con un indirizzo IP impostato manualmente\",\n  \"blocking_mode_default\": \"Risponde con un indirizzo IP pari a zero (0.0.0.0 per A; :: per AAAA) quando bloccato da una regola in stile Blocca-annunci; risponde con l'indirizzo IP specificato nella regola quando bloccato da una regola in stile /etc/hosts\",\n  \"blocking_mode_null_ip\": \"IP nullo: Rispondi con indirizzo IP zero (0.0.0.0 per A; :: per AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Rispondi con il codice NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: Risposta con codice di REFUSED\",\n  \"blocklist\": \"Lista nera\",\n  \"bootstrap_dns\": \"Server DNS bootstrap\",\n  \"bootstrap_dns_desc\": \"Indirizzi IP dei server DNS utilizzati per risolvere gli indirizzi IP dei resolver DoH/DoT specificati come upstream. I commenti non sono ammessi.\",\n  \"cache_cleared\": \"Cache DNS è stata cancellata correttamente\",\n  \"cache_enabled\": \"Abilita la cache\",\n  \"cache_enabled_desc\": \"Memorizza localmente le risposte DNS.\",\n  \"cache_optimistic\": \"Optimistic caching\",\n  \"cache_optimistic_desc\": \"Fai in modo che AdGuard Home risponda dalla cache anche quando le voci risultano scadute e prova anche ad aggiornarle.\",\n  \"cache_size\": \"Dimensioni cache\",\n  \"cache_size_desc\": \"Dimensioni memoria temporanea DNS (in byte).\",\n  \"cache_size_validation\": \"La dimensione della cache deve essere maggiore di zero quando abilitata.\",\n  \"cache_ttl_max_override\": \"Sovrascrivi TTL massimo\",\n  \"cache_ttl_max_override_desc\": \"Imposta un valore di durata massima (secondi) per le voci nella cache DNS.\",\n  \"cache_ttl_min_override\": \"Sovrascrivi TTL minimo\",\n  \"cache_ttl_min_override_desc\": \"Estende i valori di breve durata (in secondi) ricevuti dal server upstream durante la memorizzazione nella cache delle risposte DNS.\",\n  \"cancel_btn\": \"Annulla\",\n  \"category_label\": \"Categoria\",\n  \"check\": \"Controlla\",\n  \"check_client_id\": \"Identificatore client (ClientID o indirizzo IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Verifica che il nome host sia filtrato.\",\n  \"check_dhcp_servers\": \"Controlla la presenza di server DHCP\",\n  \"check_dns_record\": \"Seleziona il tipo di registrazione DNS\",\n  \"check_enter_client_id\": \"Inserisci identificatore client\",\n  \"check_hostname\": \"Nome host o nome di dominio\",\n  \"check_ip\": \"Indirizzi IP: {{ip}}\",\n  \"check_not_found\": \"Non trovato negli elenchi dei filtri\",\n  \"check_reason\": \"Motivo: {{reason}}\",\n  \"check_service\": \"Nome servizio: {{service}}\",\n  \"check_title\": \"Controlla il filtro\",\n  \"check_updates_btn\": \"Ricerca aggiornamenti\",\n  \"check_updates_now\": \"Ricerca aggiornamenti ora\",\n  \"choose_allowlist\": \"Scegli liste bianche\",\n  \"choose_blocklist\": \"Scegli liste nere\",\n  \"choose_from_list\": \"Scegli dall'elenco\",\n  \"city\": \"Città\",\n  \"clear_cache\": \"Cancella cache\",\n  \"click_to_view_queries\": \"Clicca per visualizzare le richieste\",\n  \"client_add\": \"Aggiungi Client\",\n  \"client_added\": \"Client \\\"{{key}}\\\" aggiunto correttamente\",\n  \"client_blocked\": \"Client \\\"{{ip}}\\\" bloccato correttamente\",\n  \"client_confirm_block\": \"Sei sicuro di voler bloccare il client \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Sei sicuro di voler eliminare il client \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Sei sicuro di voler sbloccare il client \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Client \\\"{{key}}\\\" eliminato correttamente\",\n  \"client_details\": \"Dettagli client\",\n  \"client_edit\": \"Modifica Client\",\n  \"client_global_settings\": \"Utilizza le impostazioni globali\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"I client possono essere identificati attraverso un ClientID. <a>Qui</a> potrai saperne di più sui metodi per identificarli.\",\n  \"client_id_placeholder\": \"Inserisci un ClientID\",\n  \"client_identifier\": \"Identificatore\",\n  \"client_identifier_desc\": \"I client possono essere identificati attraverso il loro indirizzo IP, CIDR, indirizzo MAC o ClientID (che può essere utilizzato per DoT/DoH/DoQ). <0>Qui</0> potrai saperne di più sui metodi per identificarli.\",\n  \"client_name\": \"Client {{id}}\",\n  \"client_new\": \"Nuovo Client\",\n  \"client_settings\": \"Impostazioni client\",\n  \"client_table_header\": \"Client\",\n  \"client_unblocked\": \"Client \\\"{{ip}}\\\" sbloccato correttamente\",\n  \"client_updated\": \"Client \\\"{{key}}\\\" aggiornato correttamente\",\n  \"clients_desc\": \"Configura le registrazioni dei client persistenti per i dispositivi connessi ad AdGuard Home\",\n  \"clients_not_found\": \"Nessun client trovato\",\n  \"clients_title\": \"Client persistenti\",\n  \"compact\": \"Compatto\",\n  \"config_successfully_saved\": \"Configurazione salvata correttamente\",\n  \"configure\": \"Configura\",\n  \"confirm_dns_cache_clear\": \"Sei sicuro di voler cancellare la cache DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home configurerà {{ip}} come indirizzo IP statico. Desideri procedere?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"Regione\",\n  \"custom_filter_rules\": \"Regole filtri personalizzate\",\n  \"custom_filter_rules_hint\": \"Inserisci una regola per riga. Puoi utilizzare la sintassi delle regole blocca-annunci o quelle dei file hosts.\",\n  \"custom_filtering_rules\": \"Regole filtri personalizzati\",\n  \"custom_ip\": \"IP personalizzato\",\n  \"custom_retention_input\": \"Inserisci la conservazione in ore\",\n  \"custom_rotation_input\": \"Inserisci la rotazione in ore\",\n  \"dashboard\": \"Cruscotto\",\n  \"date\": \"Data\",\n  \"default\": \"Predefinito\",\n  \"delete_confirm\": \"Sei sicuro di voler cancellare \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Elimina\",\n  \"descr\": \"Descrizione\",\n  \"details\": \"Dettagli\",\n  \"dhcp_add_static_lease\": \"Aggiungi lease statico\",\n  \"dhcp_config_saved\": \"Salvataggio configurazione server DHCP riuscito\",\n  \"dhcp_description\": \"Se il tuo router non supporta la configurazione delle impostazioni del DHCP puoi utilizzare il server DHCP incluso in AdGuard.\",\n  \"dhcp_disable\": \"Disattiva server DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Il tuo sistema utilizza una configurazione di indirizzi IP dinamici per l'interfaccia <0>{{interfaceName}}</0>. Per poter utilizzare un server DHCP, è necessario impostare un indirizzo IP statico. Il tuo indirizzo IP attuale è <0>{{ipAddress}}</0>. AdGuard Home imposterà automaticamente questo indirizzo come statico quando cliccherai il pulsante \\\"Attiva server DHCP\\\".\",\n  \"dhcp_edit_static_lease\": \"Modifica locazione statica\",\n  \"dhcp_enable\": \"Attiva server DHCP\",\n  \"dhcp_error\": \"AdGuard Home non può determinare se è presente un altro server DHCP attivo nella rete\",\n  \"dhcp_form_gateway_input\": \"IP Gateway\",\n  \"dhcp_form_lease_input\": \"Durata lease\",\n  \"dhcp_form_lease_title\": \"Tempo di lease DHCP (in secondi)\",\n  \"dhcp_form_range_end\": \"Intervallo finale\",\n  \"dhcp_form_range_start\": \"Intervallo iniziale\",\n  \"dhcp_form_range_title\": \"Intervallo di indirizzi IP\",\n  \"dhcp_form_subnet_input\": \"Maschera di sottorete\",\n  \"dhcp_found\": \"Trovati server DHCP attivi nella rete. Non è consigliato attivare il server DHCP built-in\",\n  \"dhcp_hardware_address\": \"Indirizzo hardware\",\n  \"dhcp_interface_select\": \"Seleziona l'interfaccia DHCP\",\n  \"dhcp_ip_addresses\": \"Indirizzi IP\",\n  \"dhcp_ipv4_settings\": \"Impostazioni DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Impostazioni DHCP IPv6\",\n  \"dhcp_lease_added\": \"Lease statici \\\"{{key}}\\\" aggiunti correttamente\",\n  \"dhcp_lease_deleted\": \"Lease statico \\\"{{key}}\\\" eliminato correttamente\",\n  \"dhcp_lease_updated\": \"Locazione statica \\\"{{key}}\\\" aggiornata con successo\",\n  \"dhcp_leases\": \"Leases DHCP\",\n  \"dhcp_leases_not_found\": \"Nessun lease DHCP trovato\",\n  \"dhcp_new_static_lease\": \"Nuovo lease statico\",\n  \"dhcp_not_found\": \"È sicuro attivare il server DHCP integrato poiché AdGuard Home non ha rilevato alcun server DHCP attivo sulla rete. Tuttavia, dovresti effettuare un ricontrollo manuale poiché la ricerca automatica attualmente non garantisce un'affidabilità del 100%.\",\n  \"dhcp_reset\": \"Sei sicuro di voler ripristinare la configurazione DHCP?\",\n  \"dhcp_reset_leases\": \"Reimposta tutti i temporanei\",\n  \"dhcp_reset_leases_confirm\": \"Sei sicuro di voler ripristinare tutti i temporanei?\",\n  \"dhcp_reset_leases_success\": \"DHCP temporanei reimpostati correttamente\",\n  \"dhcp_settings\": \"Impostazioni DHCP\",\n  \"dhcp_static_ip_error\": \"Per utilizzare il server DHCP è necessario impostare un indirizzo IP statico. AdGuard Home non è riuscito a determinare se questa interfaccia di rete è configurata utilizzando un indirizzo IP statico. Ti preghiamo di impostare manualmente un indirizzo IP statico.\",\n  \"dhcp_static_leases\": \"Leases DHCP statici\",\n  \"dhcp_static_leases_not_found\": \"Non è stato trovato nessun leases statico DHCP\",\n  \"dhcp_table_expires\": \"Scaduto\",\n  \"dhcp_table_hostname\": \"Nome host\",\n  \"dhcp_title\": \"Server DHCP (sperimentale!)\",\n  \"dhcp_warning\": \"Se desideri attivare il server DHCP integrato, assicurati che non vi siano altri server DHCP attivi, ciò potrebbe causare problemi di connessione alla rete per i dispositivi collegati!\",\n  \"disable_for_hours\": \"Per {{count}} ora\",\n  \"disable_for_hours_plural\": \"Per {{count}} ore\",\n  \"disable_for_minutes\": \"Per {{count}} minuto\",\n  \"disable_for_minutes_plural\": \"Per {{count}} minuti\",\n  \"disable_for_seconds\": \"Per {{count}} secondo\",\n  \"disable_for_seconds_plural\": \"Per {{count}} secondi\",\n  \"disable_ipv6\": \"Disattiva risoluzione indirizzi IPv6\",\n  \"disable_ipv6_desc\": \"Eliminare tutte le query DNS per gli indirizzi IPv6 (tipo AAAA) e rimuovere i suggerimenti IPv6 dalle risposte HTTPS.\",\n  \"disable_notify_for_hours\": \"Disattiva la protezione per {{count}} ora\",\n  \"disable_notify_for_hours_plural\": \"Disattiva la protezione per {{count}} ore\",\n  \"disable_notify_for_minutes\": \"Disattiva protezione per {{count}} minuto\",\n  \"disable_notify_for_minutes_plural\": \"Disattiva la protezione per {{count}} minuti\",\n  \"disable_notify_for_seconds\": \"Disattiva la protezione per {{count}} secondo\",\n  \"disable_notify_for_seconds_plural\": \"Disattiva la protezione per {{count}} secondi\",\n  \"disable_notify_until_tomorrow\": \"Disattiva la protezione fino a domani\",\n  \"disable_protection\": \"Disattiva protezione\",\n  \"disable_rewrites\": \"Disabilita le regole di riscrittura\",\n  \"disable_until_tomorrow\": \"Fino a domani\",\n  \"disabled\": \"Disattivato\",\n  \"disabled_dhcp\": \"Server DHCP disattivato\",\n  \"disabled_filtering_toast\": \"Disattiva filtri\",\n  \"disabled_parental_toast\": \"Il Controllo Parentale è disattivato\",\n  \"disabled_protection\": \"Protezione disattivata\",\n  \"disabled_safe_browsing_toast\": \"Disattiva Navigazione Sicura\",\n  \"disabled_safe_search_toast\": \"La Ricerca Sicura è disattivata\",\n  \"disallow_this_client\": \"Blocca questo client\",\n  \"dns_addresses\": \"Indirizzo DNS\",\n  \"dns_allowlists\": \"Liste bianche DNS\",\n  \"dns_allowlists_desc\": \"I domini DNS nelle liste bianche saranno consentiti anche fossero presenti in una delle liste nere.\",\n  \"dns_blocklists\": \"Liste nere DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home bloccherà i domini che corrispondenti alla lista nera.\",\n  \"dns_cache_config\": \"Configurazione cache DNS\",\n  \"dns_cache_config_desc\": \"Qui puoi configurare la cache DNS\",\n  \"dns_cache_size\": \"Dimensioni cache DNS (in byte)\",\n  \"dns_config\": \"Configurazione server DNS\",\n  \"dns_over_https\": \"DNS su HTTPS\",\n  \"dns_over_quic\": \"DNS su QUIC\",\n  \"dns_over_tls\": \"DNS su TLS\",\n  \"dns_privacy\": \"Privacy DNS\",\n  \"dns_providers\": \"Qui c'è un <0>elenco di fornitori DNS noti</0> da cui scegliere.\",\n  \"dns_query\": \"Richieste DNS\",\n  \"dns_rewrites\": \"Riscrittura DNS\",\n  \"dns_settings\": \"Impostazioni DNS\",\n  \"dns_start\": \"Il server DNS si sta avviando\",\n  \"dns_status_error\": \"Errore nel recupero dello stato del server DNS\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": non può essere utilizzato, assicurati di averlo digitato correttamente\",\n  \"dns_test_ok_toast\": \"I server DNS specificati funzionano correttamente\",\n  \"dns_test_parsing_error_toast\": \"Sezione {{section}}: riga {{line}}: non può essere usata, controlla se l'hai scritta correttamente\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" non risponde alle richieste di test e potrebbe non funzionare correttamente\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Attiva DNSSEC\",\n  \"dnssec_enable_desc\": \"Imposta il flag DNSSEC sulle richieste DNS in uscita e ne verifica il risultato (è richiesto un risolutore attivo per DNSSEC).\",\n  \"domain\": \"Dominio\",\n  \"domain_desc\": \"Inserire il nome di dominio o carattere jolly che si vuole riscrivere.\",\n  \"domain_name_table_header\": \"Nome dominio\",\n  \"domain_or_client\": \"Dominio o client\",\n  \"down\": \"Spenta\",\n  \"download_mobileconfig\": \"Scarica file di configurazione\",\n  \"download_mobileconfig_doh\": \"Scarica .mobileconfig per DNS su HTTPS\",\n  \"download_mobileconfig_dot\": \"Scarica .mobileconfig per DNS su TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Modifica lista bianca\",\n  \"edit_blocklist\": \"Modifica lista nera\",\n  \"edit_table_action\": \"Modifica\",\n  \"edns_cs_desc\": \"Aggiunge l'opzione EDNS Client Subnet (ECS) alle richieste upstream e registra i valori inviati dai client nel registro delle richieste.\",\n  \"edns_enable\": \"Attiva client di sottorete EDNS\",\n  \"edns_use_custom_ip\": \"Usa IP personalizzato per EDNS\",\n  \"edns_use_custom_ip_desc\": \"Consentire l'uso di un IP personalizzato per EDNS\",\n  \"elapsed\": \"Trascorso\",\n  \"empty_response_status\": \"Vuoto\",\n  \"enable_protection\": \"Attiva protezione\",\n  \"enable_protection_timer\": \"La protezione verrà attivata in {{time}}\",\n  \"enable_rewrites\": \"Abilita le regole di riscrittura\",\n  \"enable_upstream_dns_cache\": \"Abilita cache DNS per la configurazione upstream personalizzata del client\",\n  \"enabled_dhcp\": \"Server DHCP attivo\",\n  \"enabled_filtering_toast\": \"Attiva filtri\",\n  \"enabled_parental_toast\": \"Il Controllo Parentale è attivo\",\n  \"enabled_protection\": \"Protezione attiva\",\n  \"enabled_safe_browsing_toast\": \"Attiva Navigazione Sicura\",\n  \"enabled_save_search_toast\": \"La Ricerca Sicura è attiva\",\n  \"enabled_table_header\": \"Attivo\",\n  \"encryption_certificate_path\": \"Percorso di certificato\",\n  \"encryption_certificates\": \"Certificati\",\n  \"encryption_certificates_desc\": \"Per utilizzare la crittografia, è necessario fornire una catena di certificati SSL valida per il proprio dominio. Puoi ottenere un certificato gratuito su <0> {{link}} </ 0> o puoi acquistarlo da una delle Autorità di certificazione attendibili.\",\n  \"encryption_certificates_input\": \"Copia / incolla qui i certificati codificati PEM.\",\n  \"encryption_certificates_source_content\": \"Incolla i contenuti di certificato\",\n  \"encryption_certificates_source_path\": \"Definisci un percorso alle file dei certificati\",\n  \"encryption_chain_invalid\": \"La catena di certificati non è valida\",\n  \"encryption_chain_valid\": \"La catena di certificati è valida\",\n  \"encryption_config_saved\": \"Configurazione crittografia salvata\",\n  \"encryption_desc\": \"Supporto alla crittografia (HTTPS/QUIC/TLS) per DNS ed interfaccia web di amministrazione\",\n  \"encryption_doq\": \"Porta DNS su QUIC\",\n  \"encryption_doq_desc\": \"Se questa porta è configurata, AdGuard Home eseguirà un server DNS su porta QUIC. \",\n  \"encryption_dot\": \"DNS su porta TLS\",\n  \"encryption_dot_desc\": \"Se questa porta è configurata, AdGuard Home eseguirà un server DNS su TLS su questa porta.\",\n  \"encryption_enable\": \"Attiva crittografia (HTTPS, DNS su HTTPS e DNS su TLS)\",\n  \"encryption_enable_desc\": \"Se la crittografia è attiva, l'interfaccia di amministrazione di AdGuard Home funzionerà su HTTPS e il server DNS ascolterà le richieste su DNS su HTTPS e DNS su TLS.\",\n  \"encryption_expire\": \"Scaduto\",\n  \"encryption_hostnames\": \"Nomi host\",\n  \"encryption_https\": \"Porta HTTPS\",\n  \"encryption_https_desc\": \"Se la porta HTTPS è configurata, l'interfaccia di amministrazione di AdGuard Home sarà accessibile tramite HTTPS e fornirà anche DNS su HTTPS nella posizione \\\"/ dns-query\\\".\",\n  \"encryption_issuer\": \"Emittente\",\n  \"encryption_key\": \"Chiave privata\",\n  \"encryption_key_input\": \"Copia/Incolla qui la tua chiave privata codificata PEM per il tuo certificato.\",\n  \"encryption_key_invalid\": \"Questa è una chiave privata {{type}} non valida\",\n  \"encryption_key_source_content\": \"Incolla i contenuti della chiave privata\",\n  \"encryption_key_source_path\": \"Imposta un percorso per il file della chiave privata\",\n  \"encryption_key_valid\": \"Questa è una chiave privata {{type}} valida\",\n  \"encryption_plain_dns_desc\": \"Il DNS semplice è abilitato per impostazione predefinita. Puoi disabilitarlo per forzare tutti i dispositivi a usare DNS crittografati. Per fare ciò è necessario abilitare almeno un protocollo DNS crittografato\",\n  \"encryption_plain_dns_enable\": \"Abilita DNS semplice\",\n  \"encryption_plain_dns_error\": \"Per disabilitare il DNS semplice, abilitare almeno un protocollo DNS crittografato\",\n  \"encryption_private_key_path\": \"Percorso della chiave privata\",\n  \"encryption_redirect\": \"Reindirizza automaticamente a HTTPS\",\n  \"encryption_redirect_desc\": \"Se selezionato, AdGuard Home ti reindirizzerà automaticamente da indirizzi HTTP a HTTPS.\",\n  \"encryption_reset\": \"Sei sicuro di voler ripristinare le impostazioni di crittografia?\",\n  \"encryption_server\": \"Nome server\",\n  \"encryption_server_desc\": \"Se impostato, AdGuard Home rileva i ClientID, risponde alle query DDR ed esegue ulteriori convalide della connessione. Se non sono impostate, queste funzioni sono disabilitate. Deve corrispondere a uno dei nomi DNS nel certificato.\",\n  \"encryption_server_enter\": \"Inserisci il tuo nome di dominio\",\n  \"encryption_settings\": \"Impostazioni di crittografia\",\n  \"encryption_status\": \"Stato\",\n  \"encryption_subject\": \"Soggetto\",\n  \"encryption_title\": \"crittografia\",\n  \"encryption_warning\": \"Attenzione\",\n  \"enforce_safe_search\": \"Utilizza Ricerca Sicura\",\n  \"enforce_save_search_hint\": \"AdGuard Home applicherà la ricerca sicura nei seguenti motori di ricerca: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Ricerca sicura forzata\",\n  \"enter_cache_size\": \"Immetti dimensioni cache (in byte)\",\n  \"enter_cache_ttl_max_override\": \"Immetti TTL massimo (in secondi)\",\n  \"enter_cache_ttl_min_override\": \"Immetti TTL minimo (in secondi)\",\n  \"enter_name_hint\": \"Inserisci nome\",\n  \"enter_url_or_path_hint\": \"Inmetti un URL o il percorso assoluto dell'elenco\",\n  \"enter_valid_allowlist\": \"Inserisci un URL valido alla lista bianca.\",\n  \"enter_valid_blocklist\": \"Inserisci un URL valido alla lista nera.\",\n  \"error_details\": \"Dettagli errore\",\n  \"example_comment\": \"! Qui va un commento.\",\n  \"example_comment_hash\": \"# Anche un commento.\",\n  \"example_comment_meaning\": \"solo un commento;\",\n  \"example_meaning_filter_block\": \"blocca accesso al dominio example.org e a tutti i suoi sottodomini;\",\n  \"example_meaning_filter_whitelist\": \"consente l'accesso al dominio esempio.org e a tutti i relativi sottodomini;\",\n  \"example_meaning_host_block\": \"restituisce 127.0.0.1 per example.org (ma non per i suoi sottodomini);\",\n  \"example_multiple_upstreams_reserved\": \"upstream multipli <0>per domini specifici</0>;\",\n  \"example_regex_meaning\": \"blocca l'accesso ai domini corrispondenti alla specifica espressione regolare.\",\n  \"example_rewrite_domain\": \"riscrivi risposte per questo dominio soltanto.\",\n  \"example_rewrite_wildcard\": \"riscrivi risposte per tutti i sottodomini di <0>esempio.org</0>.\",\n  \"example_upstream_comment\": \"un commento.\",\n  \"example_upstream_doh\": \"<0>DNS su HTTPS</0> crittografato;\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS crittografato con <0>HTTP/3 forzato</0> e nessun fallback su HTTP/2 o inferiore;\",\n  \"example_upstream_doq\": \"<0>DNS su QUIC</0> crittografato;\",\n  \"example_upstream_dot\": \"<0>DNS su TLS</0> crittografato;\",\n  \"example_upstream_regular\": \"DNS regolare (over UDP);\",\n  \"example_upstream_regular_port\": \"DNS regolare (su UDP, con porta);\",\n  \"example_upstream_reserved\": \"un upstream <0>per specifici domini</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> per <1>DNSCrypt</1> oppure i risolutori <2>DNS su HTTPS</2>;\",\n  \"example_upstream_tcp\": \"DNS regolare (over TCP);\",\n  \"example_upstream_tcp_hostname\": \"DNS regolare (over TCP, nome host);\",\n  \"example_upstream_tcp_port\": \"DNS regolare (su TCP, con porta);\",\n  \"example_upstream_udp\": \"DNS regolare (over UDP, nome host);\",\n  \"examples_title\": \"Esempi\",\n  \"fallback_dns_desc\": \"Elenco dei server DNS fallback utilizzati quando i server DNS upstream non rispondono. La sintassi è la stessa del campo principale upstream sopra.\",\n  \"fallback_dns_placeholder\": \"Inserisci un server DNS fallback per riga\",\n  \"fallback_dns_title\": \"Server DNS di fallback\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Indirizzo IP più veloce\",\n  \"fastest_addr_desc\": \"Attendi le risposte da <b>tutti i</b> server DNS, misura la velocità di connessione TCP per ogni server e restituisci l'indirizzo IP del server con la velocità di connessione più elevata.<br/>Questa modalità può rallentare notevolmente le query DNS, se uno o più server upstream non rispondono. Assicurati che i tuoi server upstream siano stabili e che il timeout upstream sia basso.\",\n  \"filter\": \"Filtro\",\n  \"filter_added_successfully\": \"L'elenco è stato aggiunto correttamente\",\n  \"filter_allowlist\": \"ATTENZIONE: Quest'azione escluderà anche la regola \\\"{{disallowed_rule}}\\\" dall'elenco di clienti consentiti.\",\n  \"filter_category_general\": \"Generali\",\n  \"filter_category_general_desc\": \"Liste che bloccano tracciatori e inserzioni nella maggioranza dei dispositivi\",\n  \"filter_category_other\": \"Altro\",\n  \"filter_category_other_desc\": \"Altre liste nere\",\n  \"filter_category_regional\": \"Regionale\",\n  \"filter_category_regional_desc\": \"Elenchi focalizzati su annunci regionali e server tracciatori\",\n  \"filter_category_security\": \"Sicurezza\",\n  \"filter_category_security_desc\": \"Elenchi progettati specificamente per bloccare domini malevoli, di phishing o truffa\",\n  \"filter_removed_successfully\": \"L'elenco è stata correttamente rimosso\",\n  \"filter_updated\": \"L'elenco è stato aggiornato correttamente\",\n  \"filtered\": \"Filtrato\",\n  \"filtered_custom_rules\": \"Filtrato dalle regole filtro personalizzate\",\n  \"filtering_rules_learn_more\": \"<0>Leggi altro</0> su come creare i tuoi elenchi host.\",\n  \"filters\": \"Filtri\",\n  \"filters_and_hosts_hint\": \"AdGuard Home è in grado di comprendere la sintassi delle regole blocca-annunci o quelle dei file hosts.\",\n  \"filters_block_toggle_hint\": \"Puoi impostare le regole di blocco nelle impostazioni dei <a>Filtri</a>.\",\n  \"filters_configuration\": \"Configurazione filtri\",\n  \"filters_enable\": \"Attiva i filtri\",\n  \"filters_interval\": \"Intervallo aggiornamento filtro\",\n  \"fix\": \"Risolvi\",\n  \"for_last_days\": \"per gli ultimi {{count}} giorni\",\n  \"for_last_days_plural\": \"per gli ultimi {{count}} giorni\",\n  \"for_last_hours\": \"per l'ultima {{count}} ora\",\n  \"for_last_hours_plural\": \"per le ultime {{count}} ore\",\n  \"forgot_password\": \"Hai perso la password?\",\n  \"forgot_password_desc\": \"Per favore segui <0>questi passaggi</0> per creare una nuova password per il tuo profilo.\",\n  \"form_add_id\": \"Aggiungi identificatore\",\n  \"form_answer\": \"Inserisci l'indirizzo IP o il nome del dominio\",\n  \"form_client_name\": \"Inserisci nome client\",\n  \"form_domain\": \"Inserisci il dominio\",\n  \"form_enter_blocked_response_ttl\": \"Inserisci tempo di vita (TTL) della risposta bloccata (secondi)\",\n  \"form_enter_host\": \"Inserisci un nome per l'host\",\n  \"form_enter_hostname\": \"Inserisci hostname\",\n  \"form_enter_id\": \"Inserisci identificatore\",\n  \"form_enter_ip\": \"Inserisci IP\",\n  \"form_enter_mac\": \"Inserisci MAC\",\n  \"form_enter_rate_limit\": \"Imposta limite delle richieste\",\n  \"form_enter_rate_limit_subnet_len\": \"Inserisci lunghezza prefisso di sottorete per limitazione velocità\",\n  \"form_enter_subnet_ip\": \"Inserisci un indirizzo IP nella subnet \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Inserisci la durata del timeout del server upstream in secondi\",\n  \"form_error_answer_format\": \"Formato di risposta non valido\",\n  \"form_error_client_id_format\": \"Il ClientID deve contenere solo numeri, lettere minuscole, e trattini\",\n  \"form_error_domain_format\": \"Formato del dominio non valido\",\n  \"form_error_equal\": \"Non deve essere uguale\",\n  \"form_error_gateway_ip\": \"Il leasing non può avere l'indirizzo IP del gateway\",\n  \"form_error_ip4_format\": \"Indirizzo IPv4 non valido\",\n  \"form_error_ip4_gateway_format\": \"Indirizzo gateway IPv4 non valido\",\n  \"form_error_ip6_format\": \"Indirizzo IPv6 non valido\",\n  \"form_error_ip_format\": \"Indirizzo IP non valido\",\n  \"form_error_mac_format\": \"Indirizzo MAC non valido\",\n  \"form_error_password\": \"Password non corrispondente\",\n  \"form_error_password_length\": \"La password deve contenere da {{min}} a {{max}} caratteri\",\n  \"form_error_port\": \"Immettere un valore di porta valido\",\n  \"form_error_port_range\": \"Immettere il valore della porta nell'intervallo 80-65535\",\n  \"form_error_port_unsafe\": \"Questa porta non è sicura\",\n  \"form_error_positive\": \"Deve essere maggiore di 0\",\n  \"form_error_required\": \"Campo richiesto\",\n  \"form_error_server_name\": \"Nome server non valido\",\n  \"form_error_subnet\": \"Il subnet \\\"{{cidr}}\\\" non contiene l'indirizzo IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Formato URL non valido\",\n  \"form_error_url_or_path_format\": \"URL o percorso assoluto dell'elenco non validi\",\n  \"form_select_tags\": \"Seleziona i tag client\",\n  \"found_in_known_domain_db\": \"Trovato nel database dei domini noti.\",\n  \"friday\": \"Venerdì\",\n  \"friday_short\": \"Ven\",\n  \"gateway_or_subnet_invalid\": \"Maschera di sottorete non valida\",\n  \"general_settings\": \"Impostazioni generali\",\n  \"general_statistics\": \"Statistiche generali\",\n  \"get_started\": \"Inizia\",\n  \"greater_range_start_error\": \"Deve essere maggiore dell'intervallo di inizio\",\n  \"homepage\": \"Pagina principale\",\n  \"host_whitelisted\": \"L'host è stato aggiunto alla lista bianca\",\n  \"ignore_domains\": \"Domini ignorati (separati da nuova riga)\",\n  \"ignore_domains_desc_query\": \"Le richieste che corrispondono a queste regole non vengono scritte nel registro delle richieste\",\n  \"ignore_domains_desc_stats\": \"Le richieste che corrispondono a queste regole non vengono scritte nelle statistiche\",\n  \"ignore_domains_title\": \"Domini ignorati\",\n  \"ignore_query_log\": \"Ignora questo client nel registro delle richieste\",\n  \"ignore_statistics\": \"Ignora questo cliente nelle statistiche\",\n  \"install_auth_confirm\": \"Conferma password\",\n  \"install_auth_desc\": \"L'autenticazione con password sulla tua interfaccia web da amministratore di AdGuard Home dev'esser configurata. Anche se AdGuard Home è accessibile solo dalla tua rete locale, è comunque importante proteggerlo da accessi non limitati.\",\n  \"install_auth_password\": \"Password\",\n  \"install_auth_password_enter\": \"Inserisci password\",\n  \"install_auth_title\": \"Autenticazione\",\n  \"install_auth_username\": \"Nome utente\",\n  \"install_auth_username_enter\": \"Inserisci nome utente\",\n  \"install_devices_address\": \"Il server DNS di AdGuard Home sta ascoltando sui seguenti indirizzi\",\n  \"install_devices_android_list_1\": \"Dalla schermata Home Menu di Android, clicca Impostazioni.\",\n  \"install_devices_android_list_2\": \"Clicca sulla voce Wi-Fi nel menu. Verrà visualizzata una schermata che elencherà tutte le reti disponibili (sarà impossibile impostare il DNS personalizzato per la connessione mobile).\",\n  \"install_devices_android_list_3\": \"Premi a lungo la rete a cui sei connesso e clicca Modifica rete.\",\n  \"install_devices_android_list_4\": \"Su alcuni dispositivi, potrebbe essere necessario selezionare la casella Avanzate per visualizzare ulteriori impostazioni. Per regolare le impostazioni del tuo DNS Android, dovrai cambiare le impostazioni IP da DHCP a Statico.\",\n  \"install_devices_android_list_5\": \"Cambia i valori DNS 1 e DNS 2 negli indirizzi del tuo server AdGuard Home.\",\n  \"install_devices_desc\": \"Affinché AdGuard Home inizi a funzionare, è necessario configurare i dispositivi per utilizzarlo.\",\n  \"install_devices_ios_list_1\": \"Dalla schermata principale, clicca Impostazioni.\",\n  \"install_devices_ios_list_2\": \"Scegli Wi-Fi nel menu a sinistra (impossibile configurare DNS per reti mobile).\",\n  \"install_devices_ios_list_3\": \"Clicca sul nome della rete attualmente attiva.\",\n  \"install_devices_ios_list_4\": \"Nel campo DNS inserisci gli indirizzi del tuo server AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Fai clic sull'icona Apple e dirigiti sulle Preferenze di Sistema.\",\n  \"install_devices_macos_list_2\": \"Clicca su Rete.\",\n  \"install_devices_macos_list_3\": \"Seleziona la prima connessione nel tuo elenco e clicca su Avanzate.\",\n  \"install_devices_macos_list_4\": \"Seleziona la scheda DNS e inserisci gli indirizzi del tuo server AdGuard Home\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Questa configurazione copre automaticamente tutti i dispositivi collegati al router di casa, non è necessario configurarli manualmente.\",\n  \"install_devices_router_list_1\": \"Accedi alle preferenze del tuo router. Di solito, puoi farlo dal tuo browser tramite un URL, come http://192.168.0.1/ o http://192.168.1.1/. Potrebbe esserti chiesto di inserire una password. Se non dovessi ricordarla, puoi reimpostare la password premendo un pulsante presente sullo stesso router, ma tieni presente che scegliendo questa procedura, probabilmente perderai l'intera configurazione del router. Se il tuo router necessitasse di un'app per configurarlo, installala sul tuo telefono o PC e utilizzala per accedere alle impostazioni del router.\",\n  \"install_devices_router_list_2\": \"Trova le impostazioni DHCP / DNS. Cerca le lettere DNS accanto a un campo che consente due o tre serie di numeri, ciascuno suddiviso in quattro gruppi di 1-3 cifre.\",\n  \"install_devices_router_list_3\": \"Inserisci qui gli indirizzi del tuo server AdGuard Home.\",\n  \"install_devices_router_list_4\": \"Su alcuni tipi di router, non è possibile configurare un server DNS personalizzato. In tal caso, configurare AdGuard Home come un <0>server DHCP</0> potrebbe aiutare. In alternativa, dovresti leggere il manuale di istruzioni per capire come personalizzare i server DNS sul tuo specifico modello di router.\",\n  \"install_devices_title\": \"Configura i tuoi dispositivi\",\n  \"install_devices_windows_list_1\": \"Apri il Pannello di controllo tramite il menu Start o la ricerca di Windows.\",\n  \"install_devices_windows_list_2\": \"Vai a Rete e categoria Internet e poi a Centro connessioni di rete e condivisione.\",\n  \"install_devices_windows_list_3\": \"Sul lato sinistro dello schermo, clicca su \\\"Cambia impostazioni adattatore\\\".\",\n  \"install_devices_windows_list_4\": \"Fai clic destro sulla tua connessione attiva e seleziona Proprietà.\",\n  \"install_devices_windows_list_5\": \"Trova \\\"Protocollo Internet versione 4 (TCP/IPv4)\\\" (o, per IPv6, \\\"Protocollo Internet versione 6 (TCP/IPv6)\\\" nell'elenco, selezionalo e quindi clicca nuovamente su Proprietà.\",\n  \"install_devices_windows_list_6\": \"Scegli \\\"Utilizza i seguenti indirizzi server DNS\\\" e inserisci i tuoi indirizzi server AdGuard Home.\",\n  \"install_saved\": \"Salvataggio riuscito\",\n  \"install_settings_all_interfaces\": \"Tutte le interfacce\",\n  \"install_settings_dns\": \"Server DNS\",\n  \"install_settings_dns_desc\": \"Sarà necessario configurare i dispositivi o il router per utilizzare il server DNS nei seguenti indirizzi:\",\n  \"install_settings_interface_link\": \"La tua interfaccia web di amministrazione di AdGuard Home sarà disponibile ai seguenti indirizzi:\",\n  \"install_settings_listen\": \"Interfaccia d'ascolto\",\n  \"install_settings_port\": \"Porta\",\n  \"install_settings_title\": \"Interfaccia Web dell'Admin\",\n  \"install_static_configure\": \"AdGuard Home ha rilevato l'utilizzo dell'indirizzo IP dinamico <0> {{ip}} </0>. Desideri impostarlo come indirizzo statico?\",\n  \"install_static_error\": \"AdGuard Home non può configurarlo automaticamente per questa interfaccia di rete. Ti suggeriamo di cercare un metodo alternativo per effettuare tale operazione manualmente.\",\n  \"install_static_ok\": \"Buone notizie! L'indirizzo IP statico è già configurato\",\n  \"install_step\": \"Passo\",\n  \"install_submit_desc\": \"La procedura di configurazione è completa e ora sei pronto per iniziare ad utilizzare AdGuard Home.\",\n  \"install_submit_title\": \"Felicitazioni!\",\n  \"install_welcome_desc\": \"AdGuard Home è un server DNS che blocca annunci e tracciatori a livello di rete. Il suo scopo è quello di permetterti il controllo dell'intera rete e di tutti i dispositivi, e non richiede l'utilizzo di un programma lato client.\",\n  \"install_welcome_title\": \"Benvenuto in AdGuard Home!\",\n  \"interval_24_hour\": \"24 ore\",\n  \"interval_6_hour\": \"6 ore\",\n  \"interval_days\": \"{{count}} giorni\",\n  \"interval_days_plural\": \"{{count}} giorni\",\n  \"interval_hours\": \"{{count}} ora\",\n  \"interval_hours_plural\": \"{{count}} ore\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Indirizzo IP\",\n  \"known_tracker\": \"Tracciatore noto\",\n  \"last_rule_in_allowlist\": \"Impossibile bloccare questo client perché escludere la regola \\\"{{disallowed_rule}}\\\" DISATIVERÁ l'elenco \\\"Clienti consentiti\\\".\",\n  \"last_time_updated_table_header\": \"Ultimo aggiornamento\",\n  \"list_confirm_delete\": \"Sei sicuro di voler eliminare questo elenco?\",\n  \"list_label\": \"Elenco\",\n  \"list_updated\": \"{{count}} elenco aggiornato\",\n  \"list_updated_plural\": \"{{count}} elenchi aggiornati\",\n  \"list_url_table_header\": \"Elenco URL\",\n  \"load_balancing\": \"Bilanciamento del carico\",\n  \"load_balancing_desc\": \"Esegui una query su un server upstream alla volta.<br/>AdGuard Home utilizza un algoritmo casuale ponderato per selezionare i server con il minor numero di ricerche fallite e il tempo medio di ricerca più basso.\",\n  \"loading_table_status\": \"Caricamento...\",\n  \"local_ptr_default_resolver\": \"Per impostazione predefinita, AdGuard Home utilizzerà i seguenti risolutori DNS inversi: {{ip}}.\",\n  \"local_ptr_desc\": \"I server DNS usati da AdGuard Home per richieste private PTR, SOA e NS. Una richiesta è considerata privata se richiede un dominio ARPA contenente una sottorete all'interno di intervalli IP privati (come \\\"192.168.12.34\\\") e proviene da un client con un indirizzo IP privato. Se non impostato, saranno usati i risolutori DNS predefiniti del tuo sistema operativo, ad eccezione degli indirizzi IP di AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home non è stato in grado di determinare i risolutori DNS inversi privati adatti per questo sistema.\",\n  \"local_ptr_placeholder\": \"Inserisci un indirizzo IP per riga\",\n  \"local_ptr_title\": \"Server DNS privati inversi\",\n  \"location\": \"Locazione\",\n  \"log_and_stats_section_label\": \"Registro richieste e statistiche\",\n  \"lower_range_start_error\": \"Deve essere inferiore dell'intervallo di inizio\",\n  \"main_settings\": \"Impostazioni principali\",\n  \"make_static\": \"Rendere statico\",\n  \"manual_update\": \"Ti invitiamo a <a>seguire questi passaggi</a> per aggiornare manualmente.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Lunedi\",\n  \"monday_short\": \"Lun\",\n  \"name\": \"Nome\",\n  \"name_table_header\": \"Nome\",\n  \"netname\": \"Nome Network\",\n  \"network\": \"Rete\",\n  \"new_allowlist\": \"Nuova lista bianca\",\n  \"new_blocklist\": \"Nuova lista nera\",\n  \"next\": \"Prossimo\",\n  \"next_btn\": \"Successivo\",\n  \"no_blocklist_added\": \"Non è stata aggiunta alcuna lista nera\",\n  \"no_clients_found\": \"Nessun client trovato\",\n  \"no_domains_found\": \"Nessun dominio trovato\",\n  \"no_logs_found\": \"Nessun registro trovato\",\n  \"no_servers_specified\": \"Nessun server specificato\",\n  \"no_upstreams_data_found\": \"Nessun dato upstream trovato\",\n  \"no_whitelist_added\": \"Non è stata aggiunta alcuna lista bianca\",\n  \"nothing_found\": \"Non è stato trovato nulla\",\n  \"null_ip\": \"Nessun IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Numero di richieste DNS bloccate dai filtri per annunci e dagli elenchi di blocco host\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Numero di siti web per adulti bloccati\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Numero di richieste DNS bloccate dal modulo sicurezza di navigazione di AdGuard\",\n  \"number_of_dns_query_days\": \"Numero di richieste DNS elaborate negli ultimi {{count}} giorni\",\n  \"number_of_dns_query_days_plural\": \"Numero di richieste DNS elaborate negli ultimi {{count}} giorni\",\n  \"number_of_dns_query_hours\": \"Numero di richieste DNS processate nell'ultima {{count}} ora\",\n  \"number_of_dns_query_hours_plural\": \"Numero di richieste DNS processate nelle ultime {{count}} ore\",\n  \"number_of_dns_query_to_safe_search\": \"Numero di richieste DNS dai motori di ricerca per i quali la Ricerca Sicura è stata forzata\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"DISATTIVATO\",\n  \"on\": \"ATTIVO\",\n  \"open_dashboard\": \"Apri pannello di controllo\",\n  \"orgname\": \"Nome dell'organizzazione\",\n  \"original_response\": \"Responso originale\",\n  \"out_of_range_error\": \"Deve essere fuori intervallo \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Pagina\",\n  \"parallel_requests\": \"Richieste parallele\",\n  \"parental_control\": \"Controllo Parentale\",\n  \"password_label\": \"Password\",\n  \"password_placeholder\": \"Inserisci password\",\n  \"plain_dns\": \"DNS semplice\",\n  \"port_53_faq_link\": \"La Porta 53 è spesso occupata dai servizi \\\"DNSStubListener\\\" o \\\"systemd-resolved\\\". Ti suggeriamo di leggere <0>queste istruzioni</0> per risolvere il problema.\",\n  \"previous_btn\": \"Precedente\",\n  \"privacy_policy\": \"Politica sulla Riservatezza\",\n  \"processing_update\": \"Attendi per favore, AdGuard Home si sta aggiornando\",\n  \"protection_section_label\": \"Protezione\",\n  \"protocol\": \"Protocollo\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Registro Richieste\",\n  \"query_log_clear\": \"Cancella registri richieste\",\n  \"query_log_cleared\": \"Il registro richieste è stato correttamente cancellato\",\n  \"query_log_configuration\": \"Configurazione registri\",\n  \"query_log_confirm_clear\": \"Sei sicuro di voler eliminare il registro richieste?\",\n  \"query_log_disabled\": \"Il registro richieste è stato disattivato e può essere configurata dalle <0>impostazioni</0>\",\n  \"query_log_enable\": \"Attiva registro\",\n  \"query_log_filtered\": \"Filtrato da {{filter}}\",\n  \"query_log_response_status\": \"Stato: {{value}}\",\n  \"query_log_retention\": \"Rotazione dei registri richieste\",\n  \"query_log_retention_confirm\": \"Sei sicuro di voler modificare il registro delle richieste? Se si riduce il valore dell'intervallo, alcuni dati andranno persi\",\n  \"query_log_strict_search\": \"Utilizzare le doppie virgolette per una ricerca precisa\",\n  \"query_log_updated\": \"Il registro richieste è stato correttamente aggiornato\",\n  \"rate_limit\": \"Limite delle richieste\",\n  \"rate_limit_desc\": \"Il numero di richieste al secondo consentite da un singolo client. Impostare questo valore a 0 rimuove le limitazioni.\",\n  \"rate_limit_subnet_len_ipv4\": \"Lunghezza prefisso di sottorete per indirizzi IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Lunghezza prefisso sottorete per indirizzi IPv4 usati per la limitazione della velocità. Valore predefinito 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"La lunghezza del prefisso di sottorete IPv4 deve essere compresa tra 0 e 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Lunghezza prefisso di sottorete per indirizzi IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Lunghezza prefisso di sottorete per indirizzi IPv6 usati per la limitazione della velocità. Valore predefinito 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"La lunghezza del prefisso di sottorete IPv6 deve essere compresa tra 0 e 128\",\n  \"rate_limit_whitelist\": \"Lista consentita per limitazione velocità\",\n  \"rate_limit_whitelist_desc\": \"Indirizzi IP esclusi dalla limitazione della velocità\",\n  \"rate_limit_whitelist_placeholder\": \"Inserisci un indirizzo IP per riga\",\n  \"refresh_btn\": \"Aggiorna\",\n  \"refresh_statics\": \"Aggiorna statistiche\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Segnala un problema\",\n  \"request_details\": \"Dettagli della richiesta\",\n  \"request_table_header\": \"Richiesta\",\n  \"requests_count\": \"Numero richieste\",\n  \"reset_settings\": \"Reimposta impostazioni\",\n  \"resolve_clients_desc\": \"Risolve inversamente gli indirizzi IP dei client nei loro nomi host inviando richieste PTR ai risolutori corrispondenti (server DNS privati per client locali, server upstream per client con indirizzi IP pubblici).\",\n  \"resolve_clients_title\": \"Attiva la risoluzione inversa degli indirizzi IP dei client\",\n  \"response_code\": \"Codice di risposta\",\n  \"response_details\": \"Dettagli di Risposta\",\n  \"response_table_header\": \"Risposta\",\n  \"response_time\": \"Tempo di risposta\",\n  \"rewrite_A\": \"<0>A</0>: valore speciale, mantieni registrazioni <0>A</0> dall'upstream\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: valore speciale, mantieni registrazioni <0>AAAA</0> dall'upstream\",\n  \"rewrite_add\": \"Aggiungi la riscrittura DNS\",\n  \"rewrite_added\": \"Riscrittura DNS per \\\"{{key}}\\\" aggiunta correttamente\",\n  \"rewrite_applied\": \"Regola di riscrittura applicata\",\n  \"rewrite_confirm_delete\": \"Sei sicuro di voler cancellare la riscrittura DNS per \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"La riscrittura DNS per \\\"{{key}}\\\" è stata eliminata correttamente\",\n  \"rewrite_desc\": \"Consente di configurare facilmente la risposta DNS personalizzata per un nome di dominio specifico.\",\n  \"rewrite_domain_name\": \"Nome dominio: aggiungi una registrazione CNAME\",\n  \"rewrite_edit\": \"Modifica della riscrittura DNS\",\n  \"rewrite_hosts_applied\": \"Riscritto dal file delle regole host\",\n  \"rewrite_ip_address\": \"Indirizzo IP: utilizza questo IP in una risposta A o AAAA\",\n  \"rewrite_not_found\": \"Nessuna riscrittura DNS trovata\",\n  \"rewrite_settings_updated\": \"Impostazioni di riscrittura DNS aggiornate correttamente\",\n  \"rewrite_updated\": \"Riscrittura DNS aggiornata correttamente\",\n  \"rewrites_disabled_table_header\": \"Le riscritture sono disabilitate\",\n  \"rewrites_enabled_table_header\": \"Le riscritture sono abilitate\",\n  \"rewritten\": \"Riscritto\",\n  \"rows_table_footer_text\": \"righe\",\n  \"rule_added_to_custom_filtering_toast\": \"Regola aggiunta alle regole dei filtri personalizzate: {{rule}}\",\n  \"rule_label\": \"Regola(e)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regola rimossa dalle regole dei filtri personalizzate: {{rule}}\",\n  \"rules_count_table_header\": \"Numero regole\",\n  \"safe_browsing\": \"Navigazione Sicura\",\n  \"safe_search\": \"Ricerca Sicura\",\n  \"saturday\": \"Sabato\",\n  \"saturday_short\": \"Sab\",\n  \"save_btn\": \"Salva\",\n  \"save_config\": \"Salva configurazione\",\n  \"schedule_add\": \"Aggiungere programma\",\n  \"schedule_current_timezone\": \"Fuso orario attuale: {{value}}\",\n  \"schedule_desc\": \"Impostare periodi di inattività per i servizi bloccati\",\n  \"schedule_edit\": \"Modificare programma\",\n  \"schedule_from\": \"Dal\",\n  \"schedule_invalid_select\": \"L'ora di inizio deve essere precedente all'ora di fine\",\n  \"schedule_modal_description\": \"Questo pianificazione sostituirà tutti gli orari esistenti per lo stesso giorno della settimana. Ogni giorno della settimana può avere un solo periodo d'inattività.\",\n  \"schedule_modal_time_off\": \"Sospendere il blocco del servizio:\",\n  \"schedule_new\": \"Nuovo programma\",\n  \"schedule_remove\": \"Rimuovere programma\",\n  \"schedule_save\": \"Salvare programma\",\n  \"schedule_select_days\": \"Selezionare i giorni\",\n  \"schedule_services\": \"Sospendere il blocco del servizio\",\n  \"schedule_services_desc\": \"Configura la pianificazione della pausa del filtro di blocco dei servizi\",\n  \"schedule_services_desc_client\": \"Configura la pianificazione della pausa del filtro di blocco dei servizi per questo client\",\n  \"schedule_time_all_day\": \"Tutto il giorno\",\n  \"schedule_timezone\": \"Selezionare un fuso orario\",\n  \"schedule_to\": \"Al\",\n  \"served_from_cache_label\": \"Servito dalla cache\",\n  \"service_name\": \"Nome servizio\",\n  \"set_static_ip\": \"Imposta un indirizzo IP statico\",\n  \"settings\": \"Impostazioni\",\n  \"settings_custom\": \"Personalizzato\",\n  \"settings_global\": \"Globale\",\n  \"setup_config_to_enable_dhcp_server\": \"Configurazione dell'installazione per l'attivazione del server DHCP\",\n  \"setup_dns_notice\": \"Per utilizzare <1>DNS su HTTPS</1> o <1>DNS su TLS</1>, è necessario <0>configurare la crittografia</0> nelle impostazioni di AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS su TLS:</0> Utilizza la stringa <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS su HTTPS:</0> Utilizza la stringa <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Ecco un elenco di software che è possibile utilizzare.</0>\",\n  \"setup_dns_privacy_4\": \"Su un dispositivo iOS 14 o macOS Big Sur puoi scaricare uno speciale file '.mobileconfig' che aggiunge server <highlight>DNS su HTTPS</highlight> o <highlight>DNS su TLS</highlight> alle configurazioni DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 supporta DNS su TLS in modo nativo. Per configurarlo, vai su Impostazioni → Rete e Internet → Avanzate → DNS privato e inserisci qui il tuo nome di dominio.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard per Android</0> supporta <1>DNS su HTTPS</1> e <1>DNS su TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> aggiunge <1>DNS su HTTPS</1> il supporto ad Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"configurazione iOS e macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> supporta <1>DNS su HTTPS</1>, ma per configurarlo per l'utilizzo del proprio server, è necessario generare un <2> DNS Stamp</2> apposito.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard per iOS</0>supporta l'impostazione <1>DNS su HTTPS</1> e <1>DNS su TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home può essere un client DNS sicuro su qualsiasi piattaforma.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> supporta tutti i protocolli DNS sicuri noti.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> supporta <1>DNS su HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> supporta <1>DNS su HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Troverai più implementazioni <0>qui</0> e <1>qui</1>.\",\n  \"setup_dns_privacy_other_title\": \"Altre implementazion\",\n  \"setup_guide\": \"Configurazione guidata\",\n  \"show_all_filter_type\": \"Mostra tutti\",\n  \"show_blocked_responses\": \"Bloccato\",\n  \"show_filtered_type\": \"Mostra filtrati\",\n  \"show_processed_responses\": \"Processato\",\n  \"show_whitelisted_responses\": \"Consentito\",\n  \"sign_in\": \"Accedi\",\n  \"sign_out\": \"Esci\",\n  \"source_label\": \"Fonte\",\n  \"static_ip\": \"Indirizzo IP statico\",\n  \"static_ip_desc\": \"AdGuard Home è un server quindi ha bisogno di un indirizzo IP statico per funzionare correttamente. In caso contrario, ad un certo punto, il router potrebbe assegnare un indirizzo IP differente a questo dispositivo.\",\n  \"statistics_clear\": \"Azzera statistiche\",\n  \"statistics_clear_confirm\": \"Sei sicuro di voler azzerare le statistiche?\",\n  \"statistics_cleared\": \"Statistiche azzerate correttamente\",\n  \"statistics_configuration\": \"Configurazione delle statistiche\",\n  \"statistics_enable\": \"Attiva statistiche\",\n  \"statistics_retention\": \"Conservazione delle statistiche\",\n  \"statistics_retention_confirm\": \"Sei sicuro di voler modificare la conservazione delle statistiche? Se il valore di intervallo dovesse diminuire, alcuni dati andranno persi\",\n  \"statistics_retention_desc\": \"Se dovessi diminuire il valore di intervallo, alcuni dati andranno persi\",\n  \"stats_adult\": \"Siti per adulti bloccati\",\n  \"stats_disabled\": \"Le statistiche sono state disattivate. Puoi attivarle dalla <0>pagina delle impostazioni</0>.\",\n  \"stats_disabled_short\": \"Le statistiche sono state disattivate\",\n  \"stats_malware_phishing\": \"Malware/phishing bloccati\",\n  \"stats_params\": \"Configurazione delle statistiche\",\n  \"stats_query_domain\": \"Domini maggiormente richiesti\",\n  \"subnet_error\": \"Gli indirizzi devono trovarsi in una sottorete\",\n  \"sunday\": \"Domenica\",\n  \"sunday_short\": \"Dom\",\n  \"system_host_files\": \"File host di sistema\",\n  \"table_client\": \"Client\",\n  \"table_name\": \"Nome\",\n  \"tags_desc\": \"Puoi selezionare i tag che corrispondono al client. È possibile includere tag nelle regole di filtraggio per applicarli in modo più accurato. <0>Per saperne di più</0>.\",\n  \"tags_title\": \"Tag\",\n  \"test_upstream_btn\": \"Testa gli upstream\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Auto (in base alla combinazione di colori del tuo dispositivo)\",\n  \"theme_dark\": \"Scuro\",\n  \"theme_dark_desc\": \"Tema scuro\",\n  \"theme_light\": \"Chiaro\",\n  \"theme_light_desc\": \"Tema chiaro\",\n  \"thursday\": \"Giovedì\",\n  \"thursday_short\": \"Gio\",\n  \"time_table_header\": \"Ora\",\n  \"top_blocked_domains\": \"Domini maggiormente bloccati\",\n  \"top_clients\": \"Client più utilizzati\",\n  \"top_upstreams\": \"Top upstream\",\n  \"topline_expired_certificate\": \"Il tuo certificato SSL è scaduto. Aggiorna le <0> Impostazioni di crittografia </ 0>.\",\n  \"topline_expiring_certificate\": \"Il tuo certificato SSL sta per scadere. Aggiorna le<0> Impostazioni di crittografia </ 0>.\",\n  \"tracker_source\": \"Origine del tracciatore\",\n  \"try_again\": \"Riprova\",\n  \"ttl_cache_validation\": \"La sovrascrittura del valore TTL minimo della cache deve essere inferiore o uguale a quello massimo\",\n  \"tuesday\": \"Martedì\",\n  \"tuesday_short\": \"Mar\",\n  \"type_table_header\": \"Tipo\",\n  \"unavailable_dhcp\": \"DHCP non disponibile\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home non può eseguire un server DHCP sul tuo sistema operativo\",\n  \"unblock\": \"Sblocca\",\n  \"unblock_all\": \"Sblocca tutto\",\n  \"unblock_for_this_client_only\": \"Sblocca solo per questo client\",\n  \"unknown_filter\": \"Filtro sconosciuto {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} è ora disponibile! <0>Clicca qui</0> per più informazioni.\",\n  \"update_failed\": \"Aggiornamento automatico non riuscito. Ti suggeriamo di <a>seguire questi passaggi</a> per aggiornare manualmente.\",\n  \"update_now\": \"Aggiorna ora\",\n  \"updated_custom_filtering_toast\": \"Le regole personalizzate sono state correttamente salvate\",\n  \"updated_save_search_toast\": \"Impostazioni di Safe Search aggiornate\",\n  \"updated_upstream_dns_toast\": \"I server upstream sono stati salvati correttamente\",\n  \"updates_checked\": \"Nuova versione di AdGuard Home è disponibile\",\n  \"updates_version_equal\": \"AdGuard Home è aggiornato\",\n  \"upstream\": \"Upstream\",\n  \"upstream_dns\": \"Server DNS upstream\",\n  \"upstream_dns_cache_configuration\": \"Configurazione cache DNS upstream\",\n  \"upstream_dns_client_desc\": \"Se lasci questo spazio vuoto, AdGuard Home utilizzerà i server configurati nelle <0>impostazioni DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Configurato su {{path}}\",\n  \"upstream_dns_help\": \"Immetti un indirizzo server per linea. <a>Per saperne di più</a> sulla configurazione dei server DNS upstream.\",\n  \"upstream_parallel\": \"Utilizza richieste parallele per accelerare la risoluzione interrogando simultaneamente tutti i server upstream.\",\n  \"upstream_timeout\": \"Timeout upstream\",\n  \"upstream_timeout_desc\": \"Specifica il numero di secondi da attendere per una risposta dal server upstream\",\n  \"upstreams\": \"Upstream\",\n  \"use_adguard_browsing_sec\": \"Utilizza il servizio web AdGuard 'sicurezza di navigazione'\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home verificherà se il dominio è bloccato dal servizio web di sicurezza della navigazione. Utilizzerà l'API di ricerca rispettosa della privacy per eseguire il controllo: solo un breve prefisso hash SHA256 del nome di dominio viene inviato al server.\",\n  \"use_adguard_parental\": \"Utilizza il Controllo Parentale di AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home verificherà se il dominio contiene materiale per adulti. Utilizza le stesse API privacy-friendly del servizio web 'sicurezza di navigazione'.\",\n  \"use_private_ptr_resolvers_desc\": \"Risolvi le richieste PTR, SOA e NS per domini ARPA contenenti indirizzi IP privati tramite server upstream privati, DHCP, /etc/hosts, ecc. Se disabilitato, AdGuard Home risponderà a tutte queste richieste con NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Utilizza dei resolver rDNS privati\",\n  \"use_saved_key\": \"Utilizza la chiave salvata in precedenza\",\n  \"username_label\": \"Nome utente\",\n  \"username_placeholder\": \"Inserisci nome utente\",\n  \"validated_with_dnssec\": \"Verificato con DNSSEC\",\n  \"version\": \"Versione\",\n  \"version_request_error\": \"Ricerca aggiornamenti non riuscita. Per favore controlla la tua connessione internet.\",\n  \"wednesday\": \"Mercoledì\",\n  \"wednesday_short\": \"Mer\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/ja.json",
    "content": "{\n  \"access_allowed_desc\": \"CIDR、IPアドレス、または<a>ClientID</a>のリスト。このリストに入力がある場合、AdGuard Homeはリストに入っているクライアントからのみリクエストを受け入れます。\",\n  \"access_allowed_title\": \"許可されたクライアント\",\n  \"access_blocked_desc\": \"こちらをフィルタと混同しないでください。AdGuard Homeは、ここで入力されたドメインに一致するDNSクエリをドロップし、そういったクエリはクエリログにも表示されません。ここでは、「example.org」、「*.example.org」、「 ||example.org^ 」など、特定のドメイン名、ワイルドカード、URLフィルタルールを入力できます。\",\n  \"access_blocked_title\": \"拒否するドメイン\",\n  \"access_desc\": \"こちらでは、AdGuard Home DNSサーバーのアクセスルールを設定できます。\",\n  \"access_disallowed_desc\": \"CIDR、IPアドレス、または<a>ClientID</a>のリスト。リストに入力がある場合、AdGuard Homeはリストに入力されているクライアントからのリクエストを破棄します。※「許可されたクライアント」リストに入力項目がある場合、この「拒否するクライアント」設定は無視されます。\",\n  \"access_disallowed_title\": \"拒否するクライアント\",\n  \"access_settings_saved\": \"アクセス設定の保存に成功しました\",\n  \"access_title\": \"アクセス設定\",\n  \"actions_table_header\": \"操作\",\n  \"add_allowlist\": \"許可リストに追加する\",\n  \"add_blocklist\": \"ブロックリストに追加する\",\n  \"add_custom_list\": \"カスタムリストを追加する\",\n  \"add_persistent_client\": \"永続クライアントとして追加する\",\n  \"address\": \"アドレス\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Homeは、このクライアントからすべてのDNSクエリを落とします。\",\n  \"all_lists_up_to_date_toast\": \"すべてのリストは既に最新です\",\n  \"all_queries\": \"すべてのクエリ\",\n  \"allow_this_client\": \"このクライアントを許可する\",\n  \"allowed\": \"許可\",\n  \"anonymize_client_ip\": \"クライアントIPを匿名化する\",\n  \"anonymize_client_ip_desc\": \"ログと統計にクライアントのフルIPアドレスを保存しないようにします。\",\n  \"anonymizer_notification\": \"【<0>注意</0>】IPの匿名化が有効になっています。 <1>一般設定</1>で無効にできます。\",\n  \"answer\": \"応答\",\n  \"apply_btn\": \"適用する\",\n  \"auto_clients_desc\": \"AdGuard Home を使用している、または使用する可能性のあるデバイスの IP アドレスに関する情報です。この情報は、hosts ファイル、リバース DNS など、複数の情報源から収集されます。\",\n  \"auto_clients_title\": \"ランタイムクライアント\",\n  \"autofix_warning_list\": \"次のタスクを実行します：<0>システムDNSStubListenerを非アクティブ化します</0> <0>DNSサーバのアドレスを127.0.0.1に設定します</0> <0>/etc/resolv.confのシンボリックリンクの対象を/run/systemd/resolve/resolv.confに置換します</0> <0>DNSStubListenerを停止します（systemd-resolvedサービスをリロードします）</0>\",\n  \"autofix_warning_result\": \"その結果、システムからのすべてのDNSリクエストは、デフォルトでAdGuard Homeによって処理されます。\",\n  \"autofix_warning_text\": \"「修正」をクリックすると、AdGuardHomeはAdGuardHome DNSサーバを使用するようにシステムを構成します。\",\n  \"average_processing_time\": \"平均処理時間\",\n  \"average_processing_time_hint\": \"DNSリクエストの処理にかかる平均時間（ミリ秒単位）\",\n  \"average_upstream_response_time\": \"アップストリームの平均応答時間\",\n  \"back\": \"戻る\",\n  \"block\": \"ブロック\",\n  \"block_all\": \"すべてブロック\",\n  \"block_domain_use_filters_and_hosts\": \"フィルタとhostsファイルを使用してドメインをブロックする\",\n  \"block_for_this_client_only\": \"このクライアントに対してのみブロックする\",\n  \"block_services\": \"特定のサービスをブロックする\",\n  \"blocked_adult_websites\": \"ペアレンタルコントロールによってブロック済み\",\n  \"blocked_by\": \"<0>フィルタにブロックされたDNSクエリ</0>\",\n  \"blocked_by_cname_or_ip\": \"CNAMEもしくはIPアドレスによってブロック済み\",\n  \"blocked_by_response\": \"応答されたCNAMEかIPアドレスによるブロック\",\n  \"blocked_response_ttl\": \"Blocked Response TTL（ブロック済み応答のTTL）\",\n  \"blocked_response_ttl_desc\": \"フィルタリングされた応答をクライアントがキャッシュしておく時間（秒）を指定します。\",\n  \"blocked_safebrowsing\": \"セーフブラウジングによってブロック済み\",\n  \"blocked_service\": \"ブロックするサービス\",\n  \"blocked_services\": \"ブロックするサービス\",\n  \"blocked_services_desc\": \"人気のあるサイトやサービスを一気にブロック。\",\n  \"blocked_services_global\": \"ブロックするサービスに対しグローバル設定を使用する\",\n  \"blocked_services_saved\": \"ブロックするサービスを保存完了しました。\",\n  \"blocked_threats\": \"ブロックされた脅威\",\n  \"blocking_ipv4\": \"ブロック中のIPv4\",\n  \"blocking_ipv4_desc\": \"ブロックされたAリクエストに対して応答されるIPアドレス\",\n  \"blocking_ipv6\": \"ブロック中のIPv6\",\n  \"blocking_ipv6_desc\": \"ブロックされたAAAAリクエストに対して応答されるIPアドレス\",\n  \"blocking_mode\": \"ブロックモード\",\n  \"blocking_mode_custom_ip\": \"カスタムIP：手動で設定されたIPアドレスで応答します\",\n  \"blocking_mode_default\": \"デフォルト：Adblock系ルールによってブロックされると、ゼロIPアドレス（Aに対しては「0.0.0.0」、AAAAに対しては「::」）で応答します。/etc/hosts系ルールによってブロックされると、ルールにて指定されているIPアドレスで応答します。\",\n  \"blocking_mode_null_ip\": \"Null IP：ゼロのIPアドレスで応答します（Aの場合は0.0.0.0; AAAAの場合は::）\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN：NXDOMAINコードで応答します\",\n  \"blocking_mode_refused\": \"REFUSED: 「REFUSED」コードで応答します\",\n  \"blocklist\": \"ブロックリスト\",\n  \"bootstrap_dns\": \"ブートストラップDNSサーバ\",\n  \"bootstrap_dns_desc\": \"アップストリームとして指定したDoH/DoTリゾルバのIPアドレスを解決するために使用されるDNSサーバーのIPアドレスです。（コメントは許可されていません）\",\n  \"cache_cleared\": \"DNSキャッシュのクリア完了です。\",\n  \"cache_enabled\": \"キャッシュを有効にする\",\n  \"cache_enabled_desc\": \"DNSレスポンスをローカルに保存します。\",\n  \"cache_optimistic\": \"Optimistic cashing (オプティミスティック・キャッシュ)\",\n  \"cache_optimistic_desc\": \"エントリの有効期限が切れた場合でも、AdGuard Homeがキャッシュから応答するようにし、エントリの更新も試みます。\",\n  \"cache_size\": \"キャッシュサイズ\",\n  \"cache_size_desc\": \"DNSキャッシュサイズ（バイト単位）\",\n  \"cache_size_validation\": \"キャッシュが有効の場合、キャッシュサイズはゼロより大きい値でなければなりません\",\n  \"cache_ttl_max_override\": \"最大TTLの上書き（秒単位）\",\n  \"cache_ttl_max_override_desc\": \"DNSキャッシュ内のエントリの最大TTL（秒単位）を設定します。\",\n  \"cache_ttl_min_override\": \"最小TTLの上書き（秒単位）\",\n  \"cache_ttl_min_override_desc\": \"DNS応答をキャッシュするとき、アップストリームサーバーから受信した短いTTL（秒単位）を延長します。\",\n  \"cancel_btn\": \"キャンセル\",\n  \"category_label\": \"カテゴリ\",\n  \"check\": \"チェックする\",\n  \"check_client_id\": \"クライアント識別子 (ClientID または IP アドレス)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"ホスト名がフィルタリングされているかを確認できます。\",\n  \"check_dhcp_servers\": \"DHCPサーバをチェックする\",\n  \"check_dns_record\": \"DNSレコードタイプ（DNS record type）を選択\",\n  \"check_enter_client_id\": \"クライアント識別子を入力してください\",\n  \"check_hostname\": \"ホスト名またはドメイン名\",\n  \"check_ip\": \"IPアドレス: {{ip}}\",\n  \"check_not_found\": \"フィルタ一覧には見つかりません\",\n  \"check_reason\": \"理由: {{reason}}\",\n  \"check_service\": \"サービス名: {{service}}\",\n  \"check_title\": \"フィルタのチェック\",\n  \"check_updates_btn\": \"アップデートを確認する\",\n  \"check_updates_now\": \"今すぐアップデートを確認する\",\n  \"choose_allowlist\": \"許可リストの選択\",\n  \"choose_blocklist\": \"ブロックリストの選択\",\n  \"choose_from_list\": \"リストから選択する\",\n  \"city\": \"街\",\n  \"clear_cache\": \"キャッシュをクリアする\",\n  \"click_to_view_queries\": \"クエリを表示するにはクリックしてください\",\n  \"client_add\": \"クライアントを追加する\",\n  \"client_added\": \"クライアント \\\"{{key}}\\\" の追加に成功しました\",\n  \"client_blocked\": \"クライアント\\\"{{ip}}\\\"のブロックに成功しました\",\n  \"client_confirm_block\": \"クライアント\\\"{{ip}}\\\"をブロックしてもよろしいですか？\",\n  \"client_confirm_delete\": \"クライアント \\\"{{key}}\\\" を削除してもよろしいですか？\",\n  \"client_confirm_unblock\": \"クライアント\\\"{{ip}}\\\"のブロックを解除してもよろしいですか？\",\n  \"client_deleted\": \"クライアント \\\"{{key}}\\\" の削除に成功しました\",\n  \"client_details\": \"クライアントの詳細\",\n  \"client_edit\": \"クライアントの編集\",\n  \"client_global_settings\": \"グローバル設定を使用する\",\n  \"client_id\": \"ClientID（クライアントID）\",\n  \"client_id_desc\": \"それぞれのクライアントは、ClinetIDで識別できます。 <a>こちら</a>では、クライアントを識別する方法について詳しく知ることができます。\",\n  \"client_id_placeholder\": \"ClientIDを入力してください\",\n  \"client_identifier\": \"識別子\",\n  \"client_identifier_desc\": \"クライアントは、IPアドレス、CIDR、MACアドレス、またはClientID（DoT/DoH/DoQに使用可能）によって識別することができます。<0>こちら</0>にて、クライアントの識別方法についてより詳しくご確認いただけます。\",\n  \"client_name\": \"クライアント {{id}}\",\n  \"client_new\": \"新規クライアント\",\n  \"client_settings\": \"クライアント設定\",\n  \"client_table_header\": \"クライアント\",\n  \"client_unblocked\": \"クライアント\\\"{{ip}}\\\"のブロック解除に成功しました\",\n  \"client_updated\": \"クライアント \\\"{{key}}\\\" の更新に成功しました\",\n  \"clients_desc\": \"AdGuard Homeに接続されているデバイスの永続的クライアント記録を設定できます。\",\n  \"clients_not_found\": \"クライアント情報はありません\",\n  \"clients_title\": \"永速的クライアント\",\n  \"compact\": \"コンパクト\",\n  \"config_successfully_saved\": \"設定の保存に成功しました\",\n  \"configure\": \"保存\",\n  \"confirm_dns_cache_clear\": \"DNS キャッシュをクリアしてもよろしいですか？\",\n  \"confirm_static_ip\": \"AdGuard Homeは、{{ip}}を静的IPアドレスとして設定します。よろしいですか？\",\n  \"copyright\": \"著作権\",\n  \"country\": \"国\",\n  \"custom_filter_rules\": \"カスタム・フィルタリングルール\",\n  \"custom_filter_rules_hint\": \"1つの行に1つのルールを入力してください。 広告ブロックルールやhostsファイル構文を使用できます。\",\n  \"custom_filtering_rules\": \"カスタム・フィルタリングルール\",\n  \"custom_ip\": \"カスタムIP\",\n  \"custom_retention_input\": \"保持期間を入力してください（時間単位）\",\n  \"custom_rotation_input\": \"ローテーションを入力してください（時間単位）\",\n  \"dashboard\": \"ダッシュボード\",\n  \"date\": \"購入日時\",\n  \"default\": \"デフォルト\",\n  \"delete_confirm\": \"\\\"{{key}}\\\" を削除してもよろしいですか？\",\n  \"delete_table_action\": \"削除する\",\n  \"descr\": \"説明\",\n  \"details\": \"詳細\",\n  \"dhcp_add_static_lease\": \"静的割り当てを追加する\",\n  \"dhcp_config_saved\": \"DHCP構成が無事に保存されました。\",\n  \"dhcp_description\": \"あなたのルータがDHCPの設定を提供していないのなら、AdGuardに内蔵されているDHCPサーバを利用できます。\",\n  \"dhcp_disable\": \"DHCPサーバを無効にする\",\n  \"dhcp_dynamic_ip_found\": \"お使いのシステムは、インターフェース<0>{{interfaceName}}</0>用に動的IPアドレス構成を使用しています。DHCPサーバを使用するには、静的IPアドレスで設定する必要があります。あなたの現在のIPアドレスは<0>{{ipAddress}}</0>です。「DHCPサーバを有効にする」ボタンを押すと、AdGuard Homeは自動的にこのIPアドレスを静的IPアドレスとして設定します。\",\n  \"dhcp_edit_static_lease\": \"静的リースを編集\",\n  \"dhcp_enable\": \"DHCPサーバを有効にする\",\n  \"dhcp_error\": \"ネットワーク上に別の稼働中DHCPサーバがあるかどうか、AdGuard Homeは判断できませんでした\",\n  \"dhcp_form_gateway_input\": \"ゲートウェイIP\",\n  \"dhcp_form_lease_input\": \"割当期間\",\n  \"dhcp_form_lease_title\": \"DHCP割当時間（秒単位）\",\n  \"dhcp_form_range_end\": \"範囲の終了\",\n  \"dhcp_form_range_start\": \"範囲の開始\",\n  \"dhcp_form_range_title\": \"IPアドレスの範囲\",\n  \"dhcp_form_subnet_input\": \"サブネットマスク\",\n  \"dhcp_found\": \"ネットワーク内に動作しているDHCPサーバが見つかりました。内臓されているDHCPサーバを有効にするのは安全ではありません。\",\n  \"dhcp_hardware_address\": \"MACアドレス\",\n  \"dhcp_interface_select\": \"DHCPインタフェースの選択\",\n  \"dhcp_ip_addresses\": \"IPアドレス\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 設定\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 設定\",\n  \"dhcp_lease_added\": \"静的リース \\\"{{key}}\\\" の追加が完了しました。\",\n  \"dhcp_lease_deleted\": \"静的リース \\\"{{key}}\\\" の削除が完了しました。\",\n  \"dhcp_lease_updated\": \"静的リース \\\"{{key}}\\\" の更新に成功しました。\",\n  \"dhcp_leases\": \"DHCP割り当て\",\n  \"dhcp_leases_not_found\": \"DHCP割当はありません\",\n  \"dhcp_new_static_lease\": \"新規静的割り当て\",\n  \"dhcp_not_found\": \"AdGuard Homeはネットワーク上でアクティブなDHCPサーバーがないことを確認したため、内蔵DHCPサーバーを有効にしても安全です。ただし、現在の自動プロービング結果は100％保証ではないため、手動で再確認しておくことをお勧めします。\",\n  \"dhcp_reset\": \"DHCP構成をリセットしてよろしいですか？\",\n  \"dhcp_reset_leases\": \"すべてのリースをリセットする\",\n  \"dhcp_reset_leases_confirm\": \"すべてのリース（割り当て）をリセットしてもよろしいですか？\",\n  \"dhcp_reset_leases_success\": \"すべてのDHCPリース（割り当て）がリセット完了しました。\",\n  \"dhcp_settings\": \"DHCP設定\",\n  \"dhcp_static_ip_error\": \"DHCPサーバーを使用するには、静的IPアドレスを設定する必要があります。このネットワークインターフェースが静的IPアドレスを使用するように設定されているかどうかを、AdGuard Homeは判断できませんでした。手動で静的IPアドレスを設定してください。\",\n  \"dhcp_static_leases\": \"DHCP静的割り当て\",\n  \"dhcp_static_leases_not_found\": \"DHCP静的割り当てはありません\",\n  \"dhcp_table_expires\": \"有効期限\",\n  \"dhcp_table_hostname\": \"ホスト名\",\n  \"dhcp_title\": \"DHCPサーバ（※実験的）\",\n  \"dhcp_warning\": \"ともかくDHCPサーバを有効にしたい場合は、ネットワーク内で他に稼働中のDHCPサーバがないことを確認してください。そうでなければ、ネットワーク上デバイスでインターネット接続を壊してしまう可能性があります！\",\n  \"disable_for_hours\": \"{{count}}時間\",\n  \"disable_for_hours_plural\": \"{{count}}時間\",\n  \"disable_for_minutes\": \"{{count}}分間\",\n  \"disable_for_minutes_plural\": \"{{count}}分間\",\n  \"disable_for_seconds\": \"{{count}}秒間\",\n  \"disable_for_seconds_plural\": \"{{count}}秒間\",\n  \"disable_ipv6\": \"IPv6アドレスの解決を無効にする\",\n  \"disable_ipv6_desc\": \"IPv6アドレス（タイプAAAA）に対するDNSクエリをすべて破棄し、HTTPS応答から IPv6 hint を削除します。\",\n  \"disable_notify_for_hours\": \"保護を {{count}} 時間無効にする\",\n  \"disable_notify_for_hours_plural\": \"保護を {{count}} 時間無効にする\",\n  \"disable_notify_for_minutes\": \"保護を {{count}} 分間無効にする\",\n  \"disable_notify_for_minutes_plural\": \"保護を {{count}} 分間無効にする\",\n  \"disable_notify_for_seconds\": \"保護を {{count}} 秒間無効にする\",\n  \"disable_notify_for_seconds_plural\": \"保護を {{count}} 秒間無効にする\",\n  \"disable_notify_until_tomorrow\": \"明日まで保護を無効にする\",\n  \"disable_protection\": \"保護を無効にする\",\n  \"disable_rewrites\": \"Rewrite（書き換え）ルールを無効にする\",\n  \"disable_until_tomorrow\": \"明日まで\",\n  \"disabled\": \"無効\",\n  \"disabled_dhcp\": \"DHCPサーバを無効にしました\",\n  \"disabled_filtering_toast\": \"フィルタリングを無効にしました\",\n  \"disabled_parental_toast\": \"ペアレンタルコントロールが無効になりました\",\n  \"disabled_protection\": \"保護を無効にしました\",\n  \"disabled_safe_browsing_toast\": \"セーフブラウジングを無効にしました\",\n  \"disabled_safe_search_toast\": \"セーフサーチが無効になりました\",\n  \"disallow_this_client\": \"このクライアントを拒否する\",\n  \"dns_addresses\": \"DNSアドレス\",\n  \"dns_allowlists\": \"DNS許可リスト\",\n  \"dns_allowlists_desc\": \"DNS許可リストにあるドメインは、ブロックリストに含まれていても許可されます。\",\n  \"dns_blocklists\": \"DNSブロックリスト\",\n  \"dns_blocklists_desc\": \"AdGuard Homeは、ブロックリストに一致するドメインをブロックします。\",\n  \"dns_cache_config\": \"DNSキャッシュ設定\",\n  \"dns_cache_config_desc\": \"こちらではDNSキャッシュを設定できます。\",\n  \"dns_cache_size\": \"DNSキャッシュサイズ（バイト単位）\",\n  \"dns_config\": \"DNSサーバ設定\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNSプライバシー\",\n  \"dns_providers\": \"こちらは、選択可能な<0>既知のDNSプロバイダの一覧</0>です。\",\n  \"dns_query\": \"DNSクエリ\",\n  \"dns_rewrites\": \"DNS書き換え\",\n  \"dns_settings\": \"DNS設定\",\n  \"dns_start\": \"DNSサーバが起動処理中です\",\n  \"dns_status_error\": \"DNSサーバーステータスの確認エラー\",\n  \"dns_test_not_ok_toast\": \"サーバ \\\"{{key}}\\\": 使用できませんでした。正しく入力されているかどうかを確認してください\",\n  \"dns_test_ok_toast\": \"指定されたDNSサーバは正しく動作しています\",\n  \"dns_test_parsing_error_toast\": \"セクション {{section}}: 行 {{line}}: を使用できませんでした。正しく記述されているか確認してください\",\n  \"dns_test_warning_toast\": \"アップストリーム\\\"{{key}}\\\"はテストリクエストに応答せず、正しく動作しない可能性があります。\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"DNSSECを有効にする\",\n  \"dnssec_enable_desc\": \"送信するDNSクエリにDNSSECフラグを設定し、結果を確認します（DNSSEC対応リゾルバが必要です）。\",\n  \"domain\": \"ドメイン\",\n  \"domain_desc\": \"DNSリライトしたいドメイン名やワイルドカードを入力してください。\",\n  \"domain_name_table_header\": \"ドメイン名\",\n  \"domain_or_client\": \"ドメインまたはクライアント\",\n  \"down\": \"ダウン\",\n  \"download_mobileconfig\": \"設定ファイルをダウンロードする\",\n  \"download_mobileconfig_doh\": \"DNS-over-HTTPS用の .mobileconfig をダウンロード\",\n  \"download_mobileconfig_dot\": \"DNS-over-TLS用の .mobileconfig をダウンロード\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"許可リストの編集\",\n  \"edit_blocklist\": \"ブロックリストの編集\",\n  \"edit_table_action\": \"編集する\",\n  \"edns_cs_desc\": \"アップストリームリクエストにEDNSクライアントサブネットオプション（ECS）を追加し、クライアントから送信された値をクエリログに記録します。\",\n  \"edns_enable\": \"EDNSクライアントサブネットを有効にする\",\n  \"edns_use_custom_ip\": \"EDNSにカスタムIPを使用する\",\n  \"edns_use_custom_ip_desc\": \"EDNS に対してカスタム IP の使用を許可します。\",\n  \"elapsed\": \"経過時間\",\n  \"empty_response_status\": \"未定義\",\n  \"enable_protection\": \"保護を有効にする\",\n  \"enable_protection_timer\": \"保護は後 {{time}} で有効になります\",\n  \"enable_rewrites\": \"Rewrite（書き換え）ルールを有効にする\",\n  \"enable_upstream_dns_cache\": \"このクライアントのカスタムアップストリーム構成に対してDNSキャッシュを有効にする\",\n  \"enabled_dhcp\": \"DHCPサーバを有効にしました\",\n  \"enabled_filtering_toast\": \"フィルタリングを有効にしました\",\n  \"enabled_parental_toast\": \"ペアレンタルコントロールが有効になりました\",\n  \"enabled_protection\": \"保護を有効にしました\",\n  \"enabled_safe_browsing_toast\": \"セーフブラウジングを有効にしました\",\n  \"enabled_save_search_toast\": \"セーフサーチが有効になりました\",\n  \"enabled_table_header\": \"有効\",\n  \"encryption_certificate_path\": \"証明書のパスを入力してください\",\n  \"encryption_certificates\": \"証明書\",\n  \"encryption_certificates_desc\": \"暗号化を使用するには、ドメインに有効なSSL証明書チェーンを提供する必要があります。無料の証明書は<0> {{link}} </0>で入手できます。または、信頼できる認証局のいずれかから購入することもできます。\",\n  \"encryption_certificates_input\": \"ここにPEM形式の証明書をコピー／ペーストしてください。\",\n  \"encryption_certificates_source_content\": \"証明書の内容をペーストする\",\n  \"encryption_certificates_source_path\": \"証明書のパスを設定する\",\n  \"encryption_chain_invalid\": \"証明書チェーンが無効です\",\n  \"encryption_chain_valid\": \"証明書チェーンは有効です。\",\n  \"encryption_config_saved\": \"暗号化構成が保存されました。\",\n  \"encryption_desc\": \"DNSと管理者ウェブインターフェースの両方に対する暗号化（HTTPS/QUIC/TLS）サポート。\",\n  \"encryption_doq\": \"DNS-over-QUIC ポート\",\n  \"encryption_doq_desc\": \"このポートが設定されていると、AdGuard HomeはこのポートにてDNS-over-QUICサーバーを実行します。\",\n  \"encryption_dot\": \"DNS-over-TLS ポート\",\n  \"encryption_dot_desc\": \"このポートが設定されていると、AdGuard HomeはこのポートでDNS-over-TLSサーバを実行します。\",\n  \"encryption_enable\": \"暗号化を有効にする（HTTPS、DNS-over-HTTPS、DNS-over-TLS）\",\n  \"encryption_enable_desc\": \"暗号化が有効になっていると、AdGuard Home 管理インターフェースはHTTPS経由で動作し、DNSサーバはDNS-over-HTTPSおよびDNS-over-TLS経由で要求を待ち受けます。\",\n  \"encryption_expire\": \"有効期限\",\n  \"encryption_hostnames\": \"ホスト名\",\n  \"encryption_https\": \"HTTPS ポート\",\n  \"encryption_https_desc\": \"HTTPSポートが設定されていると、AdGuard Home 管理インターフェースはHTTPS経由でアクセス可能になり、そして「/dns-query」の場所にDNS-over-HTTPSも提供されます。\",\n  \"encryption_issuer\": \"発行者\",\n  \"encryption_key\": \"秘密鍵\",\n  \"encryption_key_input\": \"ここに証明書のためのPEM形式の秘密鍵をコピー／ペーストしてください。\",\n  \"encryption_key_invalid\": \"これは無効な{{type}}プライベートキーです\",\n  \"encryption_key_source_content\": \"秘密鍵の内容をペーストする\",\n  \"encryption_key_source_path\": \"秘密鍵ファイルのパスを設定\",\n  \"encryption_key_valid\": \"これは有効な{{type}}プライベートキーです。\",\n  \"encryption_plain_dns_desc\": \"プレーンDNSはデフォルトで有効になっています。無効にして、すべてのデバイスに暗号化された DNS の使用を強制適用できます。これを行うには、少なくとも 1 つの暗号化されたDNSプロトコルを有効にする必要があります。\",\n  \"encryption_plain_dns_enable\": \"プレーンDNSを有効にする\",\n  \"encryption_plain_dns_error\": \"プレーンDNSを無効にするには、暗号化DNSプロトコルを少なくとも 1 つ有効にしてください\",\n  \"encryption_private_key_path\": \"秘密鍵のパスを入力してください\",\n  \"encryption_redirect\": \"HTTPSに自動的にリダイレクト\",\n  \"encryption_redirect_desc\": \"チェックすると、AdGuard Homeは自動的にHTTPからHTTPSアドレスへリダイレクトします。\",\n  \"encryption_reset\": \"暗号化設定をリセットして良いですか？\",\n  \"encryption_server\": \"サーバー名\",\n  \"encryption_server_desc\": \"こちらでサーバー名を設定すると、AdGuard HomeはClientIDを検出し、DDRクエリに応答し、追加の接続検証を実行します。設定されていない場合、これらの機能は無効になります。※証明書のDNS名のいずれかに一致する必要があります。\",\n  \"encryption_server_enter\": \"ドメイン名を入力してください\",\n  \"encryption_settings\": \"暗号化設定\",\n  \"encryption_status\": \"ステータス\",\n  \"encryption_subject\": \"件名\",\n  \"encryption_title\": \"暗号化\",\n  \"encryption_warning\": \"警告\",\n  \"enforce_safe_search\": \"セーフサーチを使用する\",\n  \"enforce_save_search_hint\": \"AdGuard Homeは、次の検索エンジンでセーフサーチを強制適用します: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay\",\n  \"enforced_save_search\": \"強制されたセーフサーチ\",\n  \"enter_cache_size\": \"キャッシュサイズ（バイト単位）を入力してください\",\n  \"enter_cache_ttl_max_override\": \"最大TTL（秒単位）を入力してください\",\n  \"enter_cache_ttl_min_override\": \"最小TTL（秒単位）を入力してください\",\n  \"enter_name_hint\": \"名称を入力\",\n  \"enter_url_or_path_hint\": \"リストのURLまたは絶対パスを入力してください\",\n  \"enter_valid_allowlist\": \"許可リストへ有効なURLを入力してください。\",\n  \"enter_valid_blocklist\": \"ブロックリストへ有効なURLを入力してください。\",\n  \"error_details\": \"エラー詳細\",\n  \"example_comment\": \"! コメント本文\",\n  \"example_comment_hash\": \"# これもコメントです\",\n  \"example_comment_meaning\": \"コメントが入ります。\",\n  \"example_meaning_filter_block\": \"example.orgドメインとそのすべてのサブドメインへのアクセスをブロックします。\",\n  \"example_meaning_filter_whitelist\": \"example.orgドメインとそのすべてのサブドメインへのアクセスのブロックを解除します。\",\n  \"example_meaning_host_block\": \"AdGuard Homeは、example.orgドメイン（サブドメインを除く）に対して127.0.0.1のアドレスを返すようになります。\",\n  \"example_multiple_upstreams_reserved\": \"<0>特定ドメイン</0>のための複数のアップストリームサーバー;\",\n  \"example_regex_meaning\": \"指定の正規表現に一致するドメインへのアクセスをブロックします。\",\n  \"example_rewrite_domain\": \"このドメイン名のみへのレスポンスをリライトする\",\n  \"example_rewrite_wildcard\": \"<0>example.org</0>のすべてのサブドメインへのレスポンスをリライトする\",\n  \"example_upstream_comment\": \"コメントを追加できます。\",\n  \"example_upstream_doh\": \"暗号化されている <0>DNS-over-HTTPS</0>。\",\n  \"example_upstream_doh3\": \"暗号化されたDNS-over-HTTPS（<0>HTTP/3</0>の強制、HTTP/2 以下へのフォールバックなし）\",\n  \"example_upstream_doq\": \"暗号化 <0>DNS-over-QUIC</0>。\",\n  \"example_upstream_dot\": \"暗号化されている <0>DNS-over-TLS</0>。\",\n  \"example_upstream_regular\": \"通常のDNS（over UDP）。\",\n  \"example_upstream_regular_port\": \"レギュラーDNS（over UDP、ポート付き)\",\n  \"example_upstream_reserved\": \"<0>特定のドメイン</0>に対してDNSアップストリームを指定できます。\",\n  \"example_upstream_sdns\": \"<1>DNSCrypt</1> または <2>DNS-over-HTTPS</2> リゾルバのための <0>DNS Stamps</0>。\",\n  \"example_upstream_tcp\": \"通常のDNS（over TCP）。\",\n  \"example_upstream_tcp_hostname\": \"通常のDNS（over TCP, ホスト名）。\",\n  \"example_upstream_tcp_port\": \"レギュラーDNS（over TCP、ポート付き);\",\n  \"example_upstream_udp\": \"通常のDNS（over UDP, ホスト名）。\",\n  \"examples_title\": \"例\",\n  \"fallback_dns_desc\": \"アップストリームDNSサーバーが応答しない場合に使用されるフォールバックDNSサーバーのリストです。構文は上記のmain upstreamsフィールドと同じです。\",\n  \"fallback_dns_placeholder\": \"フォールバックDNSサーバーを1行に1つずつ入力してください。\",\n  \"fallback_dns_title\": \"フォールバックDNSサーバー\",\n  \"faq\": \"よくある質問\",\n  \"fastest_addr\": \"最速のIPアドレス\",\n  \"fastest_addr_desc\": \"<b>すべての</b>DNSサーバーからの応答を待ち、各サーバーのTCP接続速度を測定し、最も接続速度の速いサーバーのIPアドレスを返します。<br/>※このモードでは、1つまたは複数のアップストリームサーバーが応答しない場合、DNSクエリが大幅に遅くなることがあります。アップストリームサーバーが安定していることを確認し、アップストリームタイムアウトは小さくしておいてください。\",\n  \"filter\": \"フィルタ\",\n  \"filter_added_successfully\": \"フィルタの追加に成功しました\",\n  \"filter_allowlist\": \"【注意】このアクションは、許可されたクライアントのリストから「{{disallowed_rule}}」というルールも除外します。\",\n  \"filter_category_general\": \"一般\",\n  \"filter_category_general_desc\": \"ほとんどのデバイスにて追跡と広告をブロックするリストです。\",\n  \"filter_category_other\": \"その他\",\n  \"filter_category_other_desc\": \"その他のブロックリストです。\",\n  \"filter_category_regional\": \"地域別\",\n  \"filter_category_regional_desc\": \"それぞれの地域の広告と追跡サーバをターゲットするリストです。\",\n  \"filter_category_security\": \"セキュリティ\",\n  \"filter_category_security_desc\": \"マルウェア、フィッシング、詐欺ドメインをブロックするために設計された専用リストです。\",\n  \"filter_removed_successfully\": \"リストの削除に成功しました。\",\n  \"filter_updated\": \"フィルタの更新に成功しました\",\n  \"filtered\": \"フィルタで処理\",\n  \"filtered_custom_rules\": \"カスタム・フィルタリングルールによる処理されました\",\n  \"filtering_rules_learn_more\": \"独自ホストリストの作成についての<0>詳細はこちら</0>。\",\n  \"filters\": \"フィルタ\",\n  \"filters_and_hosts_hint\": \"AdGuard Homeは、基本的な広告ブロックルールとhostsファイルの構文を理解します。\",\n  \"filters_block_toggle_hint\": \"<a>フィルタ</a>の設定でブロックするルールを設定することができます。\",\n  \"filters_configuration\": \"フィルタ設定\",\n  \"filters_enable\": \"フィルタを有効にする\",\n  \"filters_interval\": \"フィルタの更新頻度\",\n  \"fix\": \"改善\",\n  \"for_last_days\": \"過去{{count}}日間以内\",\n  \"for_last_days_plural\": \"過去{{count}}日間以内\",\n  \"for_last_hours\": \"過去{{count}}時間\",\n  \"for_last_hours_plural\": \"過去{{count}}時間\",\n  \"forgot_password\": \"パスワードをお忘れですか？\",\n  \"forgot_password_desc\": \"<0>こちらの手順</0>に従って、新しいパスワードを作成してください。\",\n  \"form_add_id\": \"識別子を追加する\",\n  \"form_answer\": \"IPアドレスかドメイン名を入力\",\n  \"form_client_name\": \"クライアント名を入力してください\",\n  \"form_domain\": \"ドメイン名を入力してください\",\n  \"form_enter_blocked_response_ttl\": \"ブロック済み応答のTTL（秒単位）を入力してください\",\n  \"form_enter_host\": \"ホスト名を入力してください\",\n  \"form_enter_hostname\": \"ホスト名を入力してください\",\n  \"form_enter_id\": \"識別子を入力してください\",\n  \"form_enter_ip\": \"IPアドレスを入力してください\",\n  \"form_enter_mac\": \"MACアドレスを入力してください\",\n  \"form_enter_rate_limit\": \"頻度制限を入力してください\",\n  \"form_enter_rate_limit_subnet_len\": \"rate limiting（レート制限）のためのサブネットプレフィックス長を入力してください\",\n  \"form_enter_subnet_ip\": \"サブネット「{{cidr}}」内のIPアドレスを入力してください\",\n  \"form_enter_upstream_timeout\": \"アップストリームサーバーのタイムアウト時間を秒単位で入力します。\",\n  \"form_error_answer_format\": \"応答のフォーマットが無効です\",\n  \"form_error_client_id_format\": \"ClientIDには、数字、小文字、ハイフン以外は使用できません\",\n  \"form_error_domain_format\": \"ドメイン名のフォーマットが無効です\",\n  \"form_error_equal\": \"同じ値であってはなりません\",\n  \"form_error_gateway_ip\": \"リースはゲートウェイのIPアドレスになっていることができません\",\n  \"form_error_ip4_format\": \"IPv4アドレスが無効です\",\n  \"form_error_ip4_gateway_format\": \"ゲートウェイのIPv4アドレスが無効です\",\n  \"form_error_ip6_format\": \"IPv6アドレスが無効です\",\n  \"form_error_ip_format\": \"IPアドレスが無効です\",\n  \"form_error_mac_format\": \"MACアドレスが無効です\",\n  \"form_error_password\": \"パスワードが一致しません\",\n  \"form_error_password_length\": \"パスワードの長さは{{min}}〜{{max}}文字にしてください。\",\n  \"form_error_port\": \"有効なポート番号を入力してください\",\n  \"form_error_port_range\": \"80〜65535 の範囲内でポート番号を入力してください\",\n  \"form_error_port_unsafe\": \"これは不安全なポートです\",\n  \"form_error_positive\": \"0より大きい値でなければなりません\",\n  \"form_error_required\": \"必須項目です\",\n  \"form_error_server_name\": \"サーバー名が無効です\",\n  \"form_error_subnet\": \"IPアドレス「{{ip}}」がサブネット「{{cidr}}」に含まれていません\",\n  \"form_error_url_format\": \"URLフォーマットが無効です\",\n  \"form_error_url_or_path_format\": \"リストのURLまたは絶対パスが無効です\",\n  \"form_select_tags\": \"クライアントのタグを選択する\",\n  \"found_in_known_domain_db\": \"既知のドメインデータベースに見つかりました。\",\n  \"friday\": \"金曜日\",\n  \"friday_short\": \"金\",\n  \"gateway_or_subnet_invalid\": \"サブネットマスクが無効です\",\n  \"general_settings\": \"一般設定\",\n  \"general_statistics\": \"全般的な統計\",\n  \"get_started\": \"始めましょう\",\n  \"greater_range_start_error\": \"範囲開始値より大きい値でなければなりません\",\n  \"homepage\": \"ホームページ\",\n  \"host_whitelisted\": \"ホストはホワイトリストに登録されています\",\n  \"ignore_domains\": \"無視するドメイン（それぞれ改行で区切ってください)\",\n  \"ignore_domains_desc_query\": \"これらのルールに一致するクエリはクエリログに書き込まれません。\",\n  \"ignore_domains_desc_stats\": \"これらのルールに一致するクエリは統計に書き込まれません。\",\n  \"ignore_domains_title\": \"無視するドメイン\",\n  \"ignore_query_log\": \"クエリ・ログでこのクライアントを無視する\",\n  \"ignore_statistics\": \"統計でこのクライアントを無視する\",\n  \"install_auth_confirm\": \"パスワード（確認用）\",\n  \"install_auth_desc\": \"AdGuard Homeの管理ウェブインターフェースにパスワード認証を設定する必要があります。AdGuard Homeがローカルネットワークでのみアクセス可能であっても、制限のないアクセスから保護することは重要です。\",\n  \"install_auth_password\": \"パスワード\",\n  \"install_auth_password_enter\": \"パスワードを入力してください\",\n  \"install_auth_title\": \"認証\",\n  \"install_auth_username\": \"ユーザ名\",\n  \"install_auth_username_enter\": \"ユーザ名を入力してください\",\n  \"install_devices_address\": \"AdGuard HomeのDNSサーバは次のアドレスで待ち受けています\",\n  \"install_devices_android_list_1\": \"Androidメニューのホーム画面から、「設定」をタップします。\",\n  \"install_devices_android_list_2\": \"メニューの「Wi-Fi」をタップします。利用可能なすべてのネットワークの一覧が表示されます（モバイル接続用にカスタムDNSを設定することは不可能です）。\",\n  \"install_devices_android_list_3\": \"接続しているネットワークを長押しして、「ネットワークの変更」をタップします。\",\n  \"install_devices_android_list_4\": \"一部のデバイスでは、詳細設定のボックスをチェックして詳細設定を確認する必要があります。AndroidのDNS設定を調整するには、IP設定を「DHCP」から「静的IP」へ切り替える必要があります。\",\n  \"install_devices_android_list_5\": \"DNS 1とDNS 2の値をお使いのAdGuard Homeサーバーのアドレスに変更してください。\",\n  \"install_devices_desc\": \"AdGuard Homeの利用を開始するには、あなたのデバイスがAdGuard Homeを利用するように設定する必要があります。\",\n  \"install_devices_ios_list_1\": \"ホーム画面から「設定」をタップします。\",\n  \"install_devices_ios_list_2\": \"左側のメニューで「Wi-Fi」を選択します（モバイルネットワーク用にDNSを設定することは不可能です）。\",\n  \"install_devices_ios_list_3\": \"現在使用中のネットワークの名前をタップします。\",\n  \"install_devices_ios_list_4\": \"「DNS」欄に、AdGuard Homeサーバのアドレスを入力します。\",\n  \"install_devices_macos_list_1\": \"Apple アイコンをクリックして「システム環境設定」へ移動します。\",\n  \"install_devices_macos_list_2\": \"「ネットワーク」をクリックします。\",\n  \"install_devices_macos_list_3\": \"一覧の最初の接続を選択して「詳細...」をクリックします。\",\n  \"install_devices_macos_list_4\": \"「DNS」タブを選択して、AdGuard Homeサーバのアドレスを入力します。\",\n  \"install_devices_router\": \"ルータ\",\n  \"install_devices_router_desc\": \"このセットアップは、ルータに接続されているすべてのデバイスを自動的にカバーしますので、各デバイスを手動で設定する必要はありません。\",\n  \"install_devices_router_list_1\": \"ルーターの設定を開きます（通常、URLの http://192.168.0.1/ または http://192.168.1.1/ などを入力してブラウザからアクセスできます）。\\nパスワードの入力を求められることがあります。パスワードを覚えていない場合は、ルータにあるボタンを押してパスワードをリセットできます（※この場合、ルーターで設定されている構成が初期化される可能性が高いのでご注意ください）。\\n一部のルーターは設定用アプリを必要とします。その場合、設定用アプリをお使いのコンピュータ/スマホにインストールして、そのアプリからルーターの設定にアクセスしてください。\",\n  \"install_devices_router_list_2\": \"DHCP／DNSの設定を見つけます。DNSの文字のある入力欄を探します。それは、1〜3桁の数字で4つのグループに分けられた入力欄で、２〜３セットを許可されている欄です。\",\n  \"install_devices_router_list_3\": \"そこにAdGuard Homeサーバのアドレスを入力します。\",\n  \"install_devices_router_list_4\": \"一部のルーターでは、カスタムDNSサーバーを設定できません。この場合、AdGuard Homeを<0>DHCPサーバ</0>として設定してみることがおすすめです。それ以外の場合は、特定のルータモデルにおいて、DNSサーバーをカスタマイズする方法に関するマニュアル等をご確認ください。\",\n  \"install_devices_title\": \"あなたのデバイスの設定\",\n  \"install_devices_windows_list_1\": \"「スタート」メニューまたはWindowsの検索から「設定」を開きます。\",\n  \"install_devices_windows_list_2\": \"「ネットワークとインターネット」カテゴリに移動し、さらに「ネットワークと共有センター」へ移動します。\",\n  \"install_devices_windows_list_3\": \"左パネルにある「アダプターの設定を変更」をクリックします。\",\n  \"install_devices_windows_list_4\": \"動作中の接続を右クリックし、「プロパティ」を選択します。\",\n  \"install_devices_windows_list_5\": \"一覧から「インターネット プロトコル バージョン4（TCP/IPv4）」（もしくはIPv6の場合「インターネット プロトコル バージョン6（TCP/IPv6）」）を見つけ、それを選択してから、もう一度「プロパティ」をクリックします。\",\n  \"install_devices_windows_list_6\": \"「次のDNSサーバーアドレスを使う」を選択して、お使いのAdGuard Homeサーバーアドレスを入力します。\",\n  \"install_saved\": \"保存に成功しました\",\n  \"install_settings_all_interfaces\": \"すべてのインターフェイス\",\n  \"install_settings_dns\": \"DNSサーバ\",\n  \"install_settings_dns_desc\": \"次のアドレスでDNSサーバを使用するようにあなたのデバイスまたはルータを設定する必要があります:\",\n  \"install_settings_interface_link\": \"AdGuard Homeの管理ウェブインターフェイスは、次のアドレスで利用可能になります:\",\n  \"install_settings_listen\": \"待ち受けインターフェイス\",\n  \"install_settings_port\": \"ポート\",\n  \"install_settings_title\": \"管理用ウェブインターフェイス\",\n  \"install_static_configure\": \"動的IPアドレス <0>{{ip}}</0> が使用されていることをAdGuard Homeが検出しました。このアドレスをあなたの静的アドレスとして設定しますか？\",\n  \"install_static_error\": \"AdGuard Homeは、このネットワークインターフェースを自動構成することはできません。手動で行う方法に関する取扱説明書を探してください。\",\n  \"install_static_ok\": \"良いニュースです！ 既に静的IPアドレスで構成されています\",\n  \"install_step\": \"手順\",\n  \"install_submit_desc\": \"セットアップが完了し、AdGuard Homeのご利用を開始する準備が整いました。\",\n  \"install_submit_title\": \"おめでとうございます！\",\n  \"install_welcome_desc\": \"AdGuard Homeは、ネットワーク全体で広告と追跡をブロックするDNSサーバです。その目的は、ネットワークとデバイスのすべてをあなたが制御できるようにすることであり、クライアント側のプログラムを使用する必要はありません。\",\n  \"install_welcome_title\": \"ようこそ、AdGuard Home へ！\",\n  \"interval_24_hour\": \"24時間\",\n  \"interval_6_hour\": \"6時間\",\n  \"interval_days\": \"{{count}}日\",\n  \"interval_days_plural\": \"{{count}}日\",\n  \"interval_hours\": \"{{count}}時間\",\n  \"interval_hours_plural\": \"{{count}}時間\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IPアドレス\",\n  \"known_tracker\": \"既知のトラッカー\",\n  \"last_rule_in_allowlist\": \"ルール「{{disallowed_rule}}」を除外すると「許可されたクライアント」リストが無効になるため、このクライアントを拒否することはできません。\",\n  \"last_time_updated_table_header\": \"最終更新時刻\",\n  \"list_confirm_delete\": \"このリストを削除してもよろしいですか？\",\n  \"list_label\": \"リスト\",\n  \"list_updated\": \"{{count}}個のリストが更新されました\",\n  \"list_updated_plural\": \"{{count}}個のリストが更新されました\",\n  \"list_url_table_header\": \"URLリスト\",\n  \"load_balancing\": \"ロードバランシング\",\n  \"load_balancing_desc\": \"一度に1つのアップストリームサーバーをクエリします。<br/>AdGuard Home は、重み付き乱択アルゴリズムを使用して、ルックアップに失敗した回数が最も少なく、平均ルックアップ時間が最も短いサーバーを選択します。\",\n  \"loading_table_status\": \"読み込み中…\",\n  \"local_ptr_default_resolver\": \"デフォルトでは、AdGuard Homeは次のリバースDNSリゾルバを使用します: {{ip}}\",\n  \"local_ptr_desc\": \"AdGuard Home がプライベート PTR、SOA、および NS リクエストに使用する DNS サーバー。プライベート IP 範囲内のサブネット (「192.168.12.34」など) を含む ARPA ドメインを要求し、プライベート IP アドレスを持つクライアントから来たリクエストが、プライベートリクエストとみなされます。本設定が特に指定されていない場合、OS のデフォルト DNS リゾルバ（AdGuard Home の IP アドレスを除く）が使用されます。\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Homeは、このシステムに適したプライベートリバースDNSリゾルバを特定できませんでした。\",\n  \"local_ptr_placeholder\": \"IPアドレスを1行に1つずづ入力してください。\",\n  \"local_ptr_title\": \"プライベートリバースDNSサーバー\",\n  \"location\": \"ロケーション\",\n  \"log_and_stats_section_label\": \"クエリ・ログと統計情報\",\n  \"lower_range_start_error\": \"範囲開始よりも低い値である必要があります\",\n  \"main_settings\": \"メイン設定\",\n  \"make_static\": \"静的（static）にする\",\n  \"manual_update\": \"手動でアップデートするには、<a>こちらの手順</a>を使ってください。\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"月曜日\",\n  \"monday_short\": \"月\",\n  \"name\": \"名前\",\n  \"name_table_header\": \"名称\",\n  \"netname\": \"ネットワーク名\",\n  \"network\": \"ネットワーク\",\n  \"new_allowlist\": \"新しい許可リスト\",\n  \"new_blocklist\": \"新しいブロックリスト\",\n  \"next\": \"次へ\",\n  \"next_btn\": \"次へ\",\n  \"no_blocklist_added\": \"ブロックリストには何も追加されていません\",\n  \"no_clients_found\": \"クライアント情報はありません\",\n  \"no_domains_found\": \"ドメイン情報はありません\",\n  \"no_logs_found\": \"ログはありません\",\n  \"no_servers_specified\": \"サーバが指定されていません\",\n  \"no_upstreams_data_found\": \"アップストリームのデータが見つかりません\",\n  \"no_whitelist_added\": \"許可リストには何も追加されていません\",\n  \"nothing_found\": \"何も見つかりません\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"広告ブロックフィルタとhostsブロックリストによってブロックされたDNSリクエストの数\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"ブロックされたアダルトウェブサイトの数\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"AdGuardブラウジングセキュリティモジュールによってブロックされたDNSリクエストの数\",\n  \"number_of_dns_query_days\": \"過去{{count}}日間に処理されたDNSクエリの数\",\n  \"number_of_dns_query_days_plural\": \"過去{{count}}日間に処理されたDNSクエリの数\",\n  \"number_of_dns_query_hours\": \"過去{{count}}時間に処理されたDNSクエリの数\",\n  \"number_of_dns_query_hours_plural\": \"過去{{count}}時間に処理されたDNSクエリの数\",\n  \"number_of_dns_query_to_safe_search\": \"セーフサーチが強制適用された検索エンジンへのDNSリクエストの数\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"オフ\",\n  \"on\": \"オン\",\n  \"open_dashboard\": \"ダッシュボードを開きます\",\n  \"orgname\": \"組織名\",\n  \"original_response\": \"当初の応答\",\n  \"out_of_range_error\": \"\\\"{{start}}\\\"〜\\\"{{end}}\\\" の範囲外である必要があります\",\n  \"page_table_footer_text\": \"ページ\",\n  \"parallel_requests\": \"並列リクエスト\",\n  \"parental_control\": \"ペアレンタルコントロール\",\n  \"password_label\": \"パスワード\",\n  \"password_placeholder\": \"パスワードを入力して下さい\",\n  \"plain_dns\": \"通常のDNS\",\n  \"port_53_faq_link\": \"多くの場合、ポート53は \\\"DNSStubListener\\\" または \\\"systemd-resolved\\\" サービスによって利用されています。これを解決する方法については、<0>この手順</0>をお読みください。\",\n  \"previous_btn\": \"前へ\",\n  \"privacy_policy\": \"プライバシーポリシー\",\n  \"processing_update\": \"AdGuard Homeを更新しています。しばらくお待ちください\",\n  \"protection_section_label\": \"AdGuardによる保護\",\n  \"protocol\": \"プロトコル\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"クエリ・ログ\",\n  \"query_log_clear\": \"クエリ・ログを消去する\",\n  \"query_log_cleared\": \"クエリ・ログの消去に成功しました\",\n  \"query_log_configuration\": \"ログ設定\",\n  \"query_log_confirm_clear\": \"クエリ・ログ全体を消去してもよろしいですか？\",\n  \"query_log_disabled\": \"クエリ・ログは無効になっており、<0>設定</0>で構成できます\",\n  \"query_log_enable\": \"ログを有効にする\",\n  \"query_log_filtered\": \"{{filter}}によるフィルタ\",\n  \"query_log_response_status\": \"ステータス: {{value}}\",\n  \"query_log_retention\": \"クエリ・ログのローテーション\",\n  \"query_log_retention_confirm\": \"クエリ・ログのローテーションを変更してもよろしいですか？ 間隔の値を減らすと、一部のデータが失われます\",\n  \"query_log_strict_search\": \"完全一致検索には二重引用符を使用します\",\n  \"query_log_updated\": \"クエリ・ログの更新が成功しました\",\n  \"rate_limit\": \"頻度制限\",\n  \"rate_limit_desc\": \"一つのクライアントに対して許可される1秒あたりのリクエスト数（「0」に設定すると、制限なしになります）\",\n  \"rate_limit_subnet_len_ipv4\": \"IPv4 アドレスのサブネットプレフィックス長\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"rate limiting（レート制限）に使用される IPv4 アドレスのサブネットプレフィックス長です。デフォルト値は 24 です。\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4 サブネットプレフィックス長は0〜32の範囲内である必要があります。\",\n  \"rate_limit_subnet_len_ipv6\": \"IPv6 アドレスのサブネットプレフィックス長\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"rate limiting（レート制限）に使用される IPv6 アドレスのサブネットプレフィックス長です。デフォルト値は 56 です。\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6 サブネットのプレフィックス長は0〜128の範囲内である必要があります。\",\n  \"rate_limit_whitelist\": \"rate limiting（レート制限）の許可リスト\",\n  \"rate_limit_whitelist_desc\": \"rate limiting（レート制限）の対象から外すIPアドレスを指定できます。\",\n  \"rate_limit_whitelist_placeholder\": \"IPアドレスを1行に1つずづ入力してください。\",\n  \"refresh_btn\": \"最新にする\",\n  \"refresh_statics\": \"統計データを最新にする\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"問題を報告する\",\n  \"request_details\": \"要求の詳細\",\n  \"request_table_header\": \"リクエスト\",\n  \"requests_count\": \"リクエスト数\",\n  \"reset_settings\": \"設定をリセットする\",\n  \"resolve_clients_desc\": \"対応するリゾルバー（ローカルクライアントの場合はプライベートDNSサーバ、パブリックIPを持つクライアントの場合はアップストリームサーバー）にPTRクエリを送信することにより、クライアントのIPアドレスをホストネームに逆解決します。\",\n  \"resolve_clients_title\": \"クライアントのIPアドレスの逆解決を有効にする\",\n  \"response_code\": \"応答コード\",\n  \"response_details\": \"応答の詳細\",\n  \"response_table_header\": \"応答\",\n  \"response_time\": \"応答時間\",\n  \"rewrite_A\": \"<0>A</0>：特別な値、アップストリームからの<0>A</0>記録を保持します。\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>：特別な値、アップストリームからの<0>AAAA</0>記録を保持します。\",\n  \"rewrite_add\": \"DNS書き換え情報を追加する\",\n  \"rewrite_added\": \"\\\"{{key}}\\\" のDNS書き換え情報を追加完了しました\",\n  \"rewrite_applied\": \"書き換えルールを適用済み\",\n  \"rewrite_confirm_delete\": \"\\\"{{key}}\\\" のDNS書き換え情報を削除してもよろしいですか？\",\n  \"rewrite_deleted\": \"\\\"{{key}}\\\" のDNS書き換え情報を削除完了しました\",\n  \"rewrite_desc\": \"特定のドメイン名に対するDNS応答を簡単にカスタマイズすることを可能にします。\",\n  \"rewrite_domain_name\": \"ドメイン名入力した場合：CNAME記録が追加されます。\",\n  \"rewrite_edit\": \"DNS rewrite を編集する\",\n  \"rewrite_hosts_applied\": \"hostsファイルのルールによって書き換え済み\",\n  \"rewrite_ip_address\": \"IPアドレス入力した場合：AまたはAAAA応答でこのIPが使用されます。\",\n  \"rewrite_not_found\": \"DNS書き換え情報はありません\",\n  \"rewrite_settings_updated\": \"DNS rewrite設定の更新が正常に完了しました。\",\n  \"rewrite_updated\": \"DNS rewrite を更新完了しました。\",\n  \"rewrites_disabled_table_header\": \"Rewrites（書き換え）は無効になっています\",\n  \"rewrites_enabled_table_header\": \"Rewrites（書き換え）は有効になっています\",\n  \"rewritten\": \"書換\",\n  \"rows_table_footer_text\": \"行\",\n  \"rule_added_to_custom_filtering_toast\": \"ルールをカスタム・フィルタリングルールに追加しました {{rule}}\",\n  \"rule_label\": \"ルール\",\n  \"rule_removed_from_custom_filtering_toast\": \"ルールをカスタム・フィルタリングルールから除去しました {{rule}}\",\n  \"rules_count_table_header\": \"ルール数\",\n  \"safe_browsing\": \"セーフブラウジング\",\n  \"safe_search\": \"セーフサーチ\",\n  \"saturday\": \"土曜日\",\n  \"saturday_short\": \"土\",\n  \"save_btn\": \"保存する\",\n  \"save_config\": \"構成を保存する\",\n  \"schedule_add\": \"スケジュールを追加する\",\n  \"schedule_current_timezone\": \"現在のタイムゾーン: {{value}}\",\n  \"schedule_desc\": \"ブロックされたサービスの非アクティブ期間を設定できます。\",\n  \"schedule_edit\": \"スケジュールを編集する\",\n  \"schedule_from\": \"開始時間\",\n  \"schedule_invalid_select\": \"開始時間は終了時間より前である必要があります\",\n  \"schedule_modal_description\": \"※このスケジュールは、同じ曜日に対する既存スケジュールがある場合、すべて置き換えます。\\n各曜日ごとに設定できる非アクティブ期間は一つに限ります。\",\n  \"schedule_modal_time_off\": \"サービスブロックなし期間:\",\n  \"schedule_new\": \"新スケジュールの追加\",\n  \"schedule_remove\": \"スケジュールを削除する\",\n  \"schedule_save\": \"スケジュールを保存する\",\n  \"schedule_select_days\": \"曜日を選択\",\n  \"schedule_services\": \"サービスブロックの一時停止\",\n  \"schedule_services_desc\": \"サービスブロックフィルタの一時停止スケジュールを設定できます。\",\n  \"schedule_services_desc_client\": \"このクライアントに対するサービスブロックフィルタの一時停止スケジュールを設定できます。\",\n  \"schedule_time_all_day\": \"まる一日\",\n  \"schedule_timezone\": \"タイムゾーンを選択\",\n  \"schedule_to\": \"終了時間\",\n  \"served_from_cache_label\": \"キャッシュからの配信:\",\n  \"service_name\": \"サービス名\",\n  \"set_static_ip\": \"静的IPアドレスを設定する\",\n  \"settings\": \"設定\",\n  \"settings_custom\": \"カスタム\",\n  \"settings_global\": \"グローバル\",\n  \"setup_config_to_enable_dhcp_server\": \"DHCPサーバーを有効にするには構成を設定してください\",\n  \"setup_dns_notice\": \"<1>DNS-over-HTTPS</1>または<1>DNS-over-TLS</1>を使用するには、AdGuard Home 設定の<0>暗号化設定</0>が必要です。\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> <1>{{address}}</1>という文字列を使用してください。\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> <1>{{address}}</1>という文字列を使用してください。\",\n  \"setup_dns_privacy_3\": \"<0>使用できるソフトウェアのリストは次の通りです。</0>\",\n  \"setup_dns_privacy_4\": \"iOS 14 または macOS Big Sur デバイスにて、<highlight>DNS-over-HTTPS</highlight>または<highlight>DNS-over-TLS</highlight>サーバをDNS設定へ追加する特別な「.mobileconfig」ファイルをダウンロードできます。\",\n  \"setup_dns_privacy_android_1\": \"Android 9はDNS-over-TLSをネイティブにサポートします。設定するには、設定 → ネットワークとインターネット → 詳細設定 → プライベートDNS へ遷移し、そこにドメイン名を入力してください。\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0>は、<1>DNS-over-HTTPS</1>と<1>DNS-over-TLS</1>をサポートしています。\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0>は、Androidに<1>DNS-over-HTTPS</1>サポートを追加します。\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS と macOS での設定\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0>は<1>DNS-over-HTTPS</1>をサポートしますが、自身のサーバで使用するように設定するには、<2>DNS Stamp</2>を生成する必要があります。\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard for iOS</0>は、<1>DNS-over-HTTPS</1>と<1>DNS-over-TLS</1>の設定をサポートしています。\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home 自身は、どのプラットフォームでも安全なDNSクライアントになることができます。\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0>は、既知のすべてのセキュアDNSプロトコルをサポートしています。\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0>は<1>DNS-over-HTTPS</1>をサポートします。\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0>は<1>DNS-over-HTTPS</1>をサポートしています。\",\n  \"setup_dns_privacy_other_5\": \"もっと多くの実装を<0>ここ</0>や<1>ここ</1>で見つけられます。\",\n  \"setup_dns_privacy_other_title\": \"その他の機能\",\n  \"setup_guide\": \"セットアップガイド\",\n  \"show_all_filter_type\": \"すべて表示\",\n  \"show_blocked_responses\": \"ブロック済\",\n  \"show_filtered_type\": \"フィルタされたログを表示\",\n  \"show_processed_responses\": \"処理済\",\n  \"show_whitelisted_responses\": \"ホワイトリストにあり\",\n  \"sign_in\": \"サインイン\",\n  \"sign_out\": \"サインアウト\",\n  \"source_label\": \"ソース\",\n  \"static_ip\": \"静的IPアドレス\",\n  \"static_ip_desc\": \"AdGuard Homeはサーバであり、正しく機能させるには静的IPアドレスが必要です。そうしないと、ある時点で、ルータがこのデバイスに異なるIPアドレスを割り当てるかもしれません。\",\n  \"statistics_clear\": \"統計を消去する\",\n  \"statistics_clear_confirm\": \"統計を消去してもよろしいですか？\",\n  \"statistics_cleared\": \"統計の消去に成功しました\",\n  \"statistics_configuration\": \"統計設定\",\n  \"statistics_enable\": \"統計を有効にする\",\n  \"statistics_retention\": \"統計保持\",\n  \"statistics_retention_confirm\": \"統計の保持を変更してもよろしいですか？ 期間を短くすると、一部のデータが失われます\",\n  \"statistics_retention_desc\": \"※保持期間を短くすると、一部のデータは失われます。\",\n  \"stats_adult\": \"ブロックされたアダルトウェブサイト\",\n  \"stats_disabled\": \"統計は無効化されています。<0>設定ページ</0>でオンにすることができます。\",\n  \"stats_disabled_short\": \"統計は無効化されています\",\n  \"stats_malware_phishing\": \"ブロックされたマルウェア／フィッシング\",\n  \"stats_params\": \"統計設定\",\n  \"stats_query_domain\": \"最も問合せされたドメイン\",\n  \"subnet_error\": \"両アドレスが同じサブネット内にある必要があります\",\n  \"sunday\": \"日曜日\",\n  \"sunday_short\": \"日\",\n  \"system_host_files\": \"システムのhostsファイル\",\n  \"table_client\": \"クライアント\",\n  \"table_name\": \"名前\",\n  \"tags_desc\": \"クライアントに対応するタグを選択できます。フィルタリングルールにタグを含めることで、ルールをより正確に適用できます。 <0>詳細はこちら</0>\",\n  \"tags_title\": \"タグ\",\n  \"test_upstream_btn\": \"アップストリームをテストする\",\n  \"theme_auto\": \"自動\",\n  \"theme_auto_desc\": \"自動（デバイスの配色に合わせる）\",\n  \"theme_dark\": \"ダーク\",\n  \"theme_dark_desc\": \"ダークテーマ\",\n  \"theme_light\": \"ライト\",\n  \"theme_light_desc\": \"ライトテーマ\",\n  \"thursday\": \"木曜日\",\n  \"thursday_short\": \"木\",\n  \"time_table_header\": \"時刻\",\n  \"top_blocked_domains\": \"最もブロックされたドメイン\",\n  \"top_clients\": \"トップクライアント\",\n  \"top_upstreams\": \"上位のアップストリーム\",\n  \"topline_expired_certificate\": \"SSL証明書は期限切れです。<0>暗号化設定</0>を更新します。\",\n  \"topline_expiring_certificate\": \"SSL証明書は期限切れになります。<0>暗号化設定</0>を更新します。\",\n  \"tracker_source\": \"追跡元\",\n  \"try_again\": \"再試行する\",\n  \"ttl_cache_validation\": \"最小キャッシュTTL上書き値は最大値以下にする必要があります\",\n  \"tuesday\": \"火曜日\",\n  \"tuesday_short\": \"火\",\n  \"type_table_header\": \"種類\",\n  \"unavailable_dhcp\": \"DHCPは利用できません\",\n  \"unavailable_dhcp_desc\": \"AdGuard Homeはお使いのOS上でDHCPサーバを実行できません。\",\n  \"unblock\": \"ブロック解除\",\n  \"unblock_all\": \"すべてのブロックを解除\",\n  \"unblock_for_this_client_only\": \"このクライアントに対してのみブロックを解除する\",\n  \"unknown_filter\": \"不明なフィルタ {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}}がリリースされました。詳しくは<0>こちらをクリック</0>してください。\",\n  \"update_failed\": \"自動更新に失敗しました。手動で更新するには、<a>手順に従って</a>ください。\",\n  \"update_now\": \"今すぐ更新する\",\n  \"updated_custom_filtering_toast\": \"カスタムルールを保存しました。\",\n  \"updated_save_search_toast\": \"セーフ サーチの設定が更新されました。\",\n  \"updated_upstream_dns_toast\": \"アップストリームサーバーを保存しました。\",\n  \"updates_checked\": \"AdGuard Homeの新バージョンが利用可能です。\",\n  \"updates_version_equal\": \"AdGuard Homeは既に最新です\",\n  \"upstream\": \"アップストリーム\",\n  \"upstream_dns\": \"アップストリームDNSサーバー\",\n  \"upstream_dns_cache_configuration\": \"Upstream DNS cache configuration（アップストリームDNSキャッシュの構成）\",\n  \"upstream_dns_client_desc\": \"このフィールドを未入力のままにすると、AdGuard Homeは<0>DNS設定</0>で構成されたサーバを使用します。\",\n  \"upstream_dns_configured_in_file\": \"{{path}} にて設定されています\",\n  \"upstream_dns_help\": \"サーバのアドレスは1行に1つずつ入力してください。アップストリームDNSサーバーの構成設定について詳しくは<a>こちら</a>でご確認いただけます。\",\n  \"upstream_parallel\": \"並列リクエストを使用する（同時にすべてのアップストリームサーバーに処理要求することで解決スピードが向上）\",\n  \"upstream_timeout\": \"Upstream timeout（アップストリームタイムアウト）\",\n  \"upstream_timeout_desc\": \"アップストリームサーバーからの応答を待つ秒数を指定します。\",\n  \"upstreams\": \"アップストリーム\",\n  \"use_adguard_browsing_sec\": \"AdGuardブラウジングセキュリティ・ウェブサービスを使用する\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Homeは、ブラウジング・セキュリティ・ウェブサービスによってドメインがブロックされているかを確認します。 確認は、プライバシーに配慮したルックアップAPIを使用して行います（ドメイン名のSHA256ハッシュの短いプレフィックスのみがサーバーに送信されます）。\",\n  \"use_adguard_parental\": \"AdGuardペアレンタルコントロール・ウェブサービスを使用する\",\n  \"use_adguard_parental_hint\": \"AdGuard Homeは、ドメインにアダルトコンテンツが含まれているかどうかを確認します。 ブラウジングセキュリティ・ウェブサービスと同じプライバシーに優しいAPIを使用します。\",\n  \"use_private_ptr_resolvers_desc\": \"プライベートアップストリームサーバー、DHCP、/etc/hosts などを通じて、プライベート IP アドレスを含む ARPA ドメインの PTR、SOA、および NS リクエストを解決します。無効にした場合、AdGuard Home はこのようなリクエストのすべてに NXDOMAIN で応答します。\",\n  \"use_private_ptr_resolvers_title\": \"プライベートリバースDNSリゾルバを使用\",\n  \"use_saved_key\": \"以前に保存したキーを使用する\",\n  \"username_label\": \"ユーザ名\",\n  \"username_placeholder\": \"ユーザ名を入力してください\",\n  \"validated_with_dnssec\": \"DNSSECにて検証済\",\n  \"version\": \"バージョン\",\n  \"version_request_error\": \"アップデート確認に失敗しました。インターネット接続を確認してください。\",\n  \"wednesday\": \"水曜日\",\n  \"wednesday_short\": \"水\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/ko.json",
    "content": "{\n  \"access_allowed_desc\": \"CIDR, IP 주소 또는 <a>ClientID</a> 목록입니다. 이 목록에 항목이 있는 경우, AdGuard Home은 이러한 클라이언트의 요청만 수락합니다.\",\n  \"access_allowed_title\": \"허용된 클라이언트\",\n  \"access_blocked_desc\": \"이 기능을 필터와 혼동하지 마세요. AdGuard Home은 이 도메인에 대한 DNS 요청을 무시합니다. 여기에서는 'example.org' '*. example.org', '|| example.org ^'와 같은 특정 도메인 이름, 와일드 카드, URL 필터 규칙을 지정할 수 있습니다.\",\n  \"access_blocked_title\": \"차단된 도메인\",\n  \"access_desc\": \"여기에서 AdGuard Home DNS 서버에 대한 액세스 규칙을 설정할 수 있습니다\",\n  \"access_disallowed_desc\": \"CIDR, IP 주소 또는 <a>ClientID</a> 목록입니다. 이 목록에 항목이 있는 경우, AdGuard Home은 이러한 클라이언트의 요청을 무시합니다. 허용된 클라이언트에 항목이 있는 경우, 이 필드는 무시됩니다.\",\n  \"access_disallowed_title\": \"차단된 클라이언트\",\n  \"access_settings_saved\": \"액세스 설정이 성공적으로 저장되었습니다.\",\n  \"access_title\": \"접근 설정\",\n  \"actions_table_header\": \"가능한 동작\",\n  \"add_allowlist\": \"허용 목록 추가\",\n  \"add_blocklist\": \"차단 목록 추가\",\n  \"add_custom_list\": \"사용자 정의 목록 추가\",\n  \"add_persistent_client\": \"저장된 클라이언트에 추가\",\n  \"address\": \"주소\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home은 이 클라이언트에서 모든 DNS 쿼리를 삭제합니다.\",\n  \"all_lists_up_to_date_toast\": \"모든 리스트가 이미 최신입니다\",\n  \"all_queries\": \"모든 쿼리\",\n  \"allow_this_client\": \"클라이언트 허용\",\n  \"allowed\": \"허용됨\",\n  \"anonymize_client_ip\": \"클라이언트 IP 익명화\",\n  \"anonymize_client_ip_desc\": \"클라이언트의 전체 IP 주소를 로그와 통계에 저장하저장하지 마세요\",\n  \"anonymizer_notification\": \"<0>참고:</0> IP 익명화가 활성화되었습니다. <1>일반 설정</1>에서 비활성화할 수 있습니다.\",\n  \"answer\": \"응답\",\n  \"apply_btn\": \"적용\",\n  \"auto_clients_desc\": \"AdGuard Home을 사용 중이거나 사용할 수 있는 기기의 IP 주소에 대한 정보가 표시됩니다. 이 정보는 호스트 파일, 역방향 DNS 등 여러 소스에서 수집됩니다.\",\n  \"auto_clients_title\": \"런타임 클라이언트\",\n  \"autofix_warning_list\": \"다음 작업을 진행합니다: <0>DNSStubListener 시스템 비활성화</0> <0>DNS 서버 주소를 127.0.0.1로 설정</0> <0>/etc/resolv.conf의 심볼릭 링크 타겟을 /run/systemd/resolve/resolv.conf로 변경</0> <0>DNSStubListener 중지 (systemd-resolved 서비스 새로고침)</0>\",\n  \"autofix_warning_result\": \"결과적으로 시스템의 모든 DNS 요청은 기본적으로 AdGuard Home에 의해 처리됩니다.\",\n  \"autofix_warning_text\": \"'수정'을 클릭하면 AdGuard Home이 AdGuard Home DNS 서버를 사용하도록 시스템을 설정합니다.\",\n  \"average_processing_time\": \"평균처리 시간\",\n  \"average_processing_time_hint\": \"DNS 요청 처리시 평균 시간(밀리초)\",\n  \"average_upstream_response_time\": \"평균 업스트림 응답 시간\",\n  \"back\": \"뒤로\",\n  \"block\": \"차단\",\n  \"block_all\": \"차단\",\n  \"block_domain_use_filters_and_hosts\": \"필터 및 호스트 파일을 사용하여 도메인 차단\",\n  \"block_for_this_client_only\": \"이 클라이언트에 대해서만 차단\",\n  \"block_services\": \"특정 서비스 차단\",\n  \"blocked_adult_websites\": \"자녀 보호에 의해 차단됨\",\n  \"blocked_by\": \"<0>필터에 의해 차단됨</0>\",\n  \"blocked_by_cname_or_ip\": \"CNAME 또는 IP에 의해 차단됨\",\n  \"blocked_by_response\": \"응답 중 차단된 CNAME 또는 IP\",\n  \"blocked_response_ttl\": \"차단된 TTL 응답\",\n  \"blocked_response_ttl_desc\": \"클라이언트가 필터링된 응답을 캐시해야 하는 시간(초)을 지정합니다.\",\n  \"blocked_safebrowsing\": \"세이프 브라우징에 의해 차단됨\",\n  \"blocked_service\": \"차단된 서비스\",\n  \"blocked_services\": \"차단된 서비스\",\n  \"blocked_services_desc\": \"인기 있는 사이트와 서비스를 빠르게 차단할 수 있습니다.\",\n  \"blocked_services_global\": \"글로벌 차단 서비스 사용\",\n  \"blocked_services_saved\": \"차단된 서비스가 성공적으로 저장되었습니다.\",\n  \"blocked_threats\": \"차단된 위협\",\n  \"blocking_ipv4\": \"IPv4 차단\",\n  \"blocking_ipv4_desc\": \"차단된 A 요청에 대해서 반환할 IP 주소\",\n  \"blocking_ipv6\": \"IPv6 차단\",\n  \"blocking_ipv6_desc\": \"차단된 AAAA 요청에 대해서 반환할 IP 주소\",\n  \"blocking_mode\": \"차단 모드\",\n  \"blocking_mode_custom_ip\": \"커스텀 IP: 직접 설정한 IP 주소로 응답합니다\",\n  \"blocking_mode_default\": \"기본: Adblock 스타일 규칙에 의해 차단되면 제로 IP 주소(A는 0.0.0.0; AAAA는 ::)로 응답합니다; /etc/hosts 스타일 규칙에 의해 차단되면 규칙에 정의된 IP 주소로 응답합니다\",\n  \"blocking_mode_null_ip\": \"Null IP: 제로 IP 주소 (A는 0.0.0.0; AAAA는 ::) 로 응답합니다\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: NXDOMAIN 코드로 응답\",\n  \"blocking_mode_refused\": \"REFUSED: REFUSED 코드로 응답\",\n  \"blocklist\": \"차단 목록\",\n  \"bootstrap_dns\": \"부트스트랩 DNS 서버\",\n  \"bootstrap_dns_desc\": \"업스트림으로 지정한 DoH/DoT 리졸버의 IP 주소를 확인하는 데 사용되는 DNS 서버의 IP 주소입니다. 주석은 허용되지 않습니다.\",\n  \"cache_cleared\": \"DNS 캐시를 성공적으로 지웠습니다\",\n  \"cache_enabled\": \"캐시 활성화\",\n  \"cache_enabled_desc\": \"DNS 응답을 로컬에 저장합니다.\",\n  \"cache_optimistic\": \"옵티미스틱 캐시\",\n  \"cache_optimistic_desc\": \"세션이 만료되었거나 새로고침을 시도하는 경우에도 AdGuard Home이 캐시를 기반으로 응답하도록 합니다.\",\n  \"cache_size\": \"캐시 크기\",\n  \"cache_size_desc\": \"DNS 캐시 크기 (바이트)\",\n  \"cache_size_validation\": \"활성화된 경우 캐시 크기는 0보다 커야 합니다.\",\n  \"cache_ttl_max_override\": \"최대 TTL (초) 무시\",\n  \"cache_ttl_max_override_desc\": \"DNS 캐시의 항목에 대한 최대 TTL 값(초)을 설정합니다.\",\n  \"cache_ttl_min_override\": \"최소 TTL (초) 무시\",\n  \"cache_ttl_min_override_desc\": \"DNS 응답을 캐싱할 때 업스트림 서버에서 수신한 짧은 TTL 값(초)을 확장합니다.\",\n  \"cancel_btn\": \"취소\",\n  \"category_label\": \"카테고리\",\n  \"check\": \"확인\",\n  \"check_client_id\": \"클라이언트 식별자(클라이언트 ID 또는 IP 주소)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"호스트 이름이 필터링되는지 확인합니다.\",\n  \"check_dhcp_servers\": \"DHCP 서버 체크\",\n  \"check_dns_record\": \"DNS 레코드 유형 선택\",\n  \"check_enter_client_id\": \"클라이언트 식별자 입력\",\n  \"check_hostname\": \"호스트 이름 또는 도메인 이름\",\n  \"check_ip\": \"IP 주소: {{ip}}\",\n  \"check_not_found\": \"필터 목록에서 찾을 수 없음\",\n  \"check_reason\": \"이유: {{reason}}\",\n  \"check_service\": \"서비스 이름: {{service}}\",\n  \"check_title\": \"필터링 확인\",\n  \"check_updates_btn\": \"업데이트 확인\",\n  \"check_updates_now\": \"지금 업데이트 확인\",\n  \"choose_allowlist\": \"허용 목록 선택\",\n  \"choose_blocklist\": \"차단 목록 선택\",\n  \"choose_from_list\": \"목록에서 선택\",\n  \"city\": \"도시\",\n  \"clear_cache\": \"캐시 지우기\",\n  \"click_to_view_queries\": \"쿼리를 보려면 클릭합니다\",\n  \"client_add\": \"클라이언트 추가\",\n  \"client_added\": \"클라이언트 '{{key}}'이(가) 정상적으로 추가되었습니다\",\n  \"client_blocked\": \"클라이언트 '{{ip}}'(이)가 성공적으로 차단되었습니다\",\n  \"client_confirm_block\": \"정말로 클라이언트 '{{ip}}'을(를) 차단하시겠습니까?\",\n  \"client_confirm_delete\": \"정말 클라이언트 '{{key}}'을(를) 삭제하시겠습니까?\",\n  \"client_confirm_unblock\": \"정말로 클라이언트 '{{ip}}'의 차단을 해제하시겠습니까?\",\n  \"client_deleted\": \"클라이언트 '{{key}}'이(가) 정상적으로 삭제되었습니다\",\n  \"client_details\": \"클라이언트 정보\",\n  \"client_edit\": \"클라이언트 수정\",\n  \"client_global_settings\": \"글로벌 설정 사용\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"클라이언트는 ClientID로 식별할 수 있습니다. <a>여기</a>에서 클라이언트를 식별하는 방법을 자세히 알아보세요.\",\n  \"client_id_placeholder\": \"ClientID 입력\",\n  \"client_identifier\": \"식별자\",\n  \"client_identifier_desc\": \"클라이언트는 IP 주소, CIDR, MAC 주소 또는 ClientID(DoT/DoH/DoQ에 사용 가능)로 식별할 수 있습니다. <0>여기에서</0> 클라이언트를 식별하는 방법에 대한 자세한 내용은 확인하실 수 있습니다.\",\n  \"client_name\": \"클라이언트 {{id}}\",\n  \"client_new\": \"새 클라이언트\",\n  \"client_settings\": \"클라이언트 설정\",\n  \"client_table_header\": \"클라이언트\",\n  \"client_unblocked\": \"클라이언트 '{{ip}}'의 차단을 성공적으로 해제했습니다\",\n  \"client_updated\": \"클라이언트 '{{key}}'이(가) 정상적으로 업데이트되었습니다\",\n  \"clients_desc\": \"AdGuard Home에 연결된 기기에 대한 영구 클라이언트 레코드를 설정합니다\",\n  \"clients_not_found\": \"클라이언트 없음\",\n  \"clients_title\": \"영구 클라이언트\",\n  \"compact\": \"콤팩트\",\n  \"config_successfully_saved\": \"설정이 성공적으로 저장되었습니다.\",\n  \"configure\": \"설정하기\",\n  \"confirm_dns_cache_clear\": \"정말로 DNS 캐시를 지우시겠습니까?\",\n  \"confirm_static_ip\": \"AdGuard Home이 {{ip}}를 고정 IP 주소로 설정하려고 합니다. 계속하시겠습니까?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"지역\",\n  \"custom_filter_rules\": \"커스텀 필터링 규칙\",\n  \"custom_filter_rules_hint\": \"한 라인에 한 규칙만 입력하세요. 광고 차단 규칙과 호스트 파일 문법 중 하나를 사용할 수 있습니다\",\n  \"custom_filtering_rules\": \"커스텀 필터링 규칙\",\n  \"custom_ip\": \"사용자 지정 IP\",\n  \"custom_retention_input\": \"시간 단위로 보존 기간 입력\",\n  \"custom_rotation_input\": \"시간 단위로 로테이션 입력\",\n  \"dashboard\": \"대시보드\",\n  \"date\": \"날짜\",\n  \"default\": \"기본\",\n  \"delete_confirm\": \"'{{key}}'을(를) 삭제하시겠습니까?\",\n  \"delete_table_action\": \"삭제\",\n  \"descr\": \"설명\",\n  \"details\": \"정보\",\n  \"dhcp_add_static_lease\": \"고정 임대 추가\",\n  \"dhcp_config_saved\": \"DHCP 구성이 성공적으로 저장되었습니다\",\n  \"dhcp_description\": \"라우터가 DHCP 설정을 제공하지 않으면 AdGuard의 자체 기본 제공 DHCP 서버를 사용할 수 있습니다.\",\n  \"dhcp_disable\": \"DHCP 서버 비활성화\",\n  \"dhcp_dynamic_ip_found\": \"시스템은 <0>{{interfaceName}}</0> 인터페이스에 동적 IP 주소를 사용합니다. DHCP 서버를 사용하려면 고정 IP 주소를 설정해야 합니다. 현재 IP 주소는 <0>{{ipAddress}}</0>입니다. 'DHCP 서버 활성화' 버튼을 누르면 AdGuard Home이 이 IP 주소를 고정 IP 주소로 자동 설정합니다.\",\n  \"dhcp_edit_static_lease\": \"고정 임대 수정\",\n  \"dhcp_enable\": \"DHCP 서버 활성화\",\n  \"dhcp_error\": \"AdGuard Home이 네트워크에 다른 활성 DHCP 서버가 있는지 확인할 수 없습니다\",\n  \"dhcp_form_gateway_input\": \"게이트웨이 IP\",\n  \"dhcp_form_lease_input\": \"임대 기간\",\n  \"dhcp_form_lease_title\": \"DHCP 임대 시간 (초 단위로 표시됩니다)\",\n  \"dhcp_form_range_end\": \"범위 끝\",\n  \"dhcp_form_range_start\": \"범위 시작\",\n  \"dhcp_form_range_title\": \"IP 주소 범위\",\n  \"dhcp_form_subnet_input\": \"서브넷 마스크\",\n  \"dhcp_found\": \"네트워크에 활성 DHCP 서버가 있습니다. 기본 제공 DHCP 서버를 활성화하는 것은 안전하지 않습니다.\",\n  \"dhcp_hardware_address\": \"하드웨어 주소\",\n  \"dhcp_interface_select\": \"DHCP 인터페이스 선택\",\n  \"dhcp_ip_addresses\": \"IP 주소\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 설정\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 설정\",\n  \"dhcp_lease_added\": \"'{{key}}' 고정 임대 정상적으로 추가되었습니다\",\n  \"dhcp_lease_deleted\": \"'{{key}}' 고정 임대 정상적으로 삭제되었습니다\",\n  \"dhcp_lease_updated\": \"'{{key}}' 고정 임대 정상적으로 업데이트되었습니다.\",\n  \"dhcp_leases\": \"DHCP 임대\",\n  \"dhcp_leases_not_found\": \"DHCP 임대를 찾을 수 없음\",\n  \"dhcp_new_static_lease\": \"새 고정 임대\",\n  \"dhcp_not_found\": \"AdGuard Home이 네트워크에서 활성화된 DHCP 서버를 찾지 못했기 때문에 DHCP 서버를 활성화하는 것이 안전합니다. 하지만 자동 검색이 완전히 안전하지 않기 때문에 수동으로 다시 확인하는 걸 권장합니다.\",\n  \"dhcp_reset\": \"정말로 DHCP 설정을 초기화할까요?\",\n  \"dhcp_reset_leases\": \"모든 임대 초기화\",\n  \"dhcp_reset_leases_confirm\": \"정말로 모든 임대를 초기화할까요?\",\n  \"dhcp_reset_leases_success\": \"DHCP 임대 성공적으로 초기화됨\",\n  \"dhcp_settings\": \"DHCP 설정\",\n  \"dhcp_static_ip_error\": \"DHCP 서버를 사용하려면 고정 IP 주소를 설정해야 합니다. AdGuard Home이 이 네트워크 인터페이스가 고정 IP 주소를 사용하는지 확인할 수 없습니다. 고정 IP 주소를 수동으로 설정하십시오.\",\n  \"dhcp_static_leases\": \"DHCP 고정 임대\",\n  \"dhcp_static_leases_not_found\": \"DHCP 고정 임대를 찾을 수 없음\",\n  \"dhcp_table_expires\": \"만료\",\n  \"dhcp_table_hostname\": \"호스트 이름\",\n  \"dhcp_title\": \"DHCP 서버 (시험!)\",\n  \"dhcp_warning\": \"DHCP 서버를 사용하려면 네트워크에 다른 활성화된 DHCP 서버가 없는지 확인해 주세요. 다른 활성 DHCP 서버가 있다면, 연결된 장치의 인터넷이 끊길 수 있습니다.\",\n  \"disable_for_hours\": \"{{count}}시간\",\n  \"disable_for_hours_plural\": \"{{count}}시간\",\n  \"disable_for_minutes\": \"{{count}}분\",\n  \"disable_for_minutes_plural\": \"{{count}}분간\",\n  \"disable_for_seconds\": \"{{count}}초\",\n  \"disable_for_seconds_plural\": \"{{count}}초\",\n  \"disable_ipv6\": \"IPv6 주소 확인 비활성화\",\n  \"disable_ipv6_desc\": \"IPv6 주소(AAAA 유형)에 대한 모든 DNS 쿼리를 무시하고 HTTPS 유형 응답에서 IPv6 데이터를 제거합니다.\",\n  \"disable_notify_for_hours\": \"{{count}}시간 동안 보호 기능 비활성화\",\n  \"disable_notify_for_hours_plural\": \"{{count}}시간 동안 보호 기능 비활성화\",\n  \"disable_notify_for_minutes\": \"{{count}}분 동안 보호 기능 비활성화\",\n  \"disable_notify_for_minutes_plural\": \"{{count}}분 동안 보호 기능 비활성화\",\n  \"disable_notify_for_seconds\": \"{{count}}초 동안 보호 기능 비활성화\",\n  \"disable_notify_for_seconds_plural\": \"{{count}}초 동안 보호 기능 비활성화\",\n  \"disable_notify_until_tomorrow\": \"내일까지 보호 기능 비활성화\",\n  \"disable_protection\": \"보호 비활성화\",\n  \"disable_rewrites\": \"Rewrite(재작성) 규칙 비활성화\",\n  \"disable_until_tomorrow\": \"내일까지\",\n  \"disabled\": \"비활성화 됨\",\n  \"disabled_dhcp\": \"DHCP 서버 비활성화됨\",\n  \"disabled_filtering_toast\": \"필터링 비활성화됨\",\n  \"disabled_parental_toast\": \"자녀 보호 비활성화됨\",\n  \"disabled_protection\": \"보호 비활성화됨\",\n  \"disabled_safe_browsing_toast\": \"세이프 브라우징 비활성화됨\",\n  \"disabled_safe_search_toast\": \"세이프서치 비활성화됨\",\n  \"disallow_this_client\": \"클라이언트 거부\",\n  \"dns_addresses\": \"DNS 주소\",\n  \"dns_allowlists\": \"DNS 허용 목록\",\n  \"dns_allowlists_desc\": \"DNS 허용 목록에 있는 도메인은 아무 차단 목록에 있어도 허용됩니다.\",\n  \"dns_blocklists\": \"DNS 차단 목록\",\n  \"dns_blocklists_desc\": \"AdGuard Home은 차단 목록과 일치하는 도메인을 차단합니다.\",\n  \"dns_cache_config\": \"DNS 캐시 구성\",\n  \"dns_cache_config_desc\": \"여기에서 DNS 캐시를 구성 할 수 있습니다\",\n  \"dns_cache_size\": \"DNS 캐시 크기(바이트)\",\n  \"dns_config\": \"DNS 서버 설정\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS 프라이버시\",\n  \"dns_providers\": \"다음은 선택할 수 있는 <0>알려진 DNS 공급자 목록</0>입니다.\",\n  \"dns_query\": \"DNS 쿼리\",\n  \"dns_rewrites\": \"DNS 변경\",\n  \"dns_settings\": \"DNS 설정\",\n  \"dns_start\": \"DNS 서버를 시작하고 있습니다\",\n  \"dns_status_error\": \"DNS 서버 상태를 확인하는 동안 오류가 발생했습니다\",\n  \"dns_test_not_ok_toast\": \"서버 '{{key}}': 사용할 수 없습니다, 제대로 작성했는지 확인하세요\",\n  \"dns_test_ok_toast\": \"지정된 DNS 서버가 올바르게 작동하고 있습니다.\",\n  \"dns_test_parsing_error_toast\": \"섹션 {{section}}: 줄 {{line}}: 사용할 수 없으며, 올바르게 작성했는지 확인하세요.\",\n  \"dns_test_warning_toast\": \"업스트림 '{{key}}'이(가) 테스트 요청에 응답하지 않으며 제대로 작동하지 않을 수 있습니다\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"DNSSEC 활성화\",\n  \"dnssec_enable_desc\": \"발신 DNS 쿼리에서 DNSSEC 플래그를 설정하고 결과를 확인합니다 (DNSSEC-enabled resolver 필수).\",\n  \"domain\": \"도메인\",\n  \"domain_desc\": \"다시 작성할 도메인 이름 또는 와일드카드를 입력합니다.\",\n  \"domain_name_table_header\": \"도메인명\",\n  \"domain_or_client\": \"도메인 또는 클라이언트\",\n  \"down\": \"다운로드\",\n  \"download_mobileconfig\": \"설정 파일 내려받기\",\n  \"download_mobileconfig_doh\": \"DNS-over-HTTPS용 .mobileconfig 다운로드\",\n  \"download_mobileconfig_dot\": \"DNS-over-TLS용 .mobileconfig 다운로드\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"허용 목록 수정\",\n  \"edit_blocklist\": \"차단 목록 수정\",\n  \"edit_table_action\": \"편집\",\n  \"edns_cs_desc\": \"업스트림 요청에 EDNS 클라이언트 서브넷 옵션(ECS)을 추가하고 쿼리 로그에 클라이언트가 보낸 값을 기록합니다.\",\n  \"edns_enable\": \"EDNS 클라이언트 서브넷 활성화\",\n  \"edns_use_custom_ip\": \"EDNS에 사용자 지정 IP 사용\",\n  \"edns_use_custom_ip_desc\": \"EDNS에 사용자 지정 IP 사용하도록 허용합니다.\",\n  \"elapsed\": \"소요\",\n  \"empty_response_status\": \"비어있음\",\n  \"enable_protection\": \"보호 활성화\",\n  \"enable_protection_timer\": \"{{time}}에 보호 기능이 활성화됩니다.\",\n  \"enable_rewrites\": \"Rewrite(재작성) 규칙 활성화\",\n  \"enable_upstream_dns_cache\": \"이 클라이언트의 사용자 지정 업스트림 설정에서 DNS 캐싱 사용\",\n  \"enabled_dhcp\": \"DHCP 서버 활성화됨\",\n  \"enabled_filtering_toast\": \"필터링 활성화됨\",\n  \"enabled_parental_toast\": \"자녀 보호 활성화됨\",\n  \"enabled_protection\": \"보호 활성화됨\",\n  \"enabled_safe_browsing_toast\": \"세이프 브라우징 활성화됨\",\n  \"enabled_save_search_toast\": \"세이프서치 활성화됨\",\n  \"enabled_table_header\": \"활성화됨\",\n  \"encryption_certificate_path\": \"인증서 경로\",\n  \"encryption_certificates\": \"인증서\",\n  \"encryption_certificates_desc\": \"암호화를 사용하려면 도메인에 대해 올바른 SSL 인증서 체인을 제공해야 합니다. <0>{{link}}</0>에서 무료 증명서를 받을 수도 있고, 신뢰할 수있는 인증 기관에서 구입할 수 있습니다.\",\n  \"encryption_certificates_input\": \"PEM으로 인코딩된 인증서 여기에 복사/붙여넣기하세요.\",\n  \"encryption_certificates_source_content\": \"인증서 내용 붙여넣기\",\n  \"encryption_certificates_source_path\": \"인증서 파일 경로 설정\",\n  \"encryption_chain_invalid\": \"인증서 체인이 유효하지 않습니다\",\n  \"encryption_chain_valid\": \"인증서 체인이 유효합니다\",\n  \"encryption_config_saved\": \"암호화 구성이 저장되었습니다\",\n  \"encryption_desc\": \"DNS 및 관리 웹 인터페이스에 대한 암호화(HTTPS/TLS)를 지원합니다\",\n  \"encryption_doq\": \"DNS-over-QUIC 포트\",\n  \"encryption_doq_desc\": \"이 포트가 설정된 경우 AdGuard Home은 해당 포트에서 DNS-over-QUIC 서버를 실행합니다. \",\n  \"encryption_dot\": \"DNS-over-TLS 포트\",\n  \"encryption_dot_desc\": \"이 포트가 구성된 경우 AdGuard Home 이 포트에서 DNS-over-TLS 서버를 실행합니다.\",\n  \"encryption_enable\": \"암호화 활성화 (HTTPS, DNS-over-HTTPS 및 DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"암호화가 활성화 된 경우 AdGuard Home 관리자 인터페이스는 HTTPS를 통해 작동하고 DNS 서버는 DNS-over-HTTPS 및 DNS-over-TLS를 통해 요청을 수신합니다.\",\n  \"encryption_expire\": \"만료\",\n  \"encryption_hostnames\": \"호스트 이름\",\n  \"encryption_https\": \"HTTP 포트\",\n  \"encryption_https_desc\": \"HTTPS 포트가 구성되면 HTTPS를 통해 AdGuard Home 관리자 인터페이스에 액세스할 수 있으며, '/dns-query' 위치에 DNS-over-HTTPS도 제공합니다.\",\n  \"encryption_issuer\": \"발행자\",\n  \"encryption_key\": \"개인 키\",\n  \"encryption_key_input\": \"PEM으로 인코딩된 개인 키를 여기에 복사/붙여넣기하세요.\",\n  \"encryption_key_invalid\": \"유효하지 않는 {{type}} 개인 키입니다\",\n  \"encryption_key_source_content\": \"비밀키 내용 붙여넣기\",\n  \"encryption_key_source_path\": \"비밀키 파일 경로 설정\",\n  \"encryption_key_valid\": \"유효한 {{type}} 개인 키입니다\",\n  \"encryption_plain_dns_desc\": \"평문 DNS가 기본으로 설정되어 있습니다. 비활성화해서 모든 기기가 암호화된 DNS를 사용하도록 할 수 있습니다. 그러려면 암호화된 DNS 프로토콜을 하나 이상 활성화해야 합니다.\",\n  \"encryption_plain_dns_enable\": \"평문 DNS 활성화\",\n  \"encryption_plain_dns_error\": \"평문 DNS를 비활성화하려면, 암호화된 DNS 프로토콜을 하나 이상 활성화하세요\",\n  \"encryption_private_key_path\": \"비밀키 경로\",\n  \"encryption_redirect\": \"HTTPS로 자동 리디렉션\",\n  \"encryption_redirect_desc\": \"상자를 체크하면 AdGuard Home 자동으로 사용자를 HTTP에서 HTTPS 주소로 리디렉션합니다.\",\n  \"encryption_reset\": \"암호화 설정을 재설정하시겠습니까?\",\n  \"encryption_server\": \"서버 이름\",\n  \"encryption_server_desc\": \"설정된 경우 AdGuard Home은 ClientID를 감지하고 DDR 쿼리에 응답하고 추가 연결 유효성 검사를 수행합니다. 설정하지 않으면 이러한 기능이 비활성화됩니다. 인증서의 DNS 이름 중 하나와 일치해야 합니다.\",\n  \"encryption_server_enter\": \"도메인 이름을 입력하세요.\",\n  \"encryption_settings\": \"암호화 설정\",\n  \"encryption_status\": \"상태\",\n  \"encryption_subject\": \"대상\",\n  \"encryption_title\": \"암호화\",\n  \"encryption_warning\": \"주의\",\n  \"enforce_safe_search\": \"세이프서치 사용\",\n  \"enforce_save_search_hint\": \"AdGuard Home은 Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay와 같은 검색 엔진에서 세이프서치를 시행합니다.\",\n  \"enforced_save_search\": \"세이프서치 강제\",\n  \"enter_cache_size\": \"캐시 크기를 입력하세요\",\n  \"enter_cache_ttl_max_override\": \"최대 TTL을 입력하세요\",\n  \"enter_cache_ttl_min_override\": \"최소 TTL을 입력하세요\",\n  \"enter_name_hint\": \"이름을 입력하세요\",\n  \"enter_url_or_path_hint\": \"URL 또는 목록의 절대 경로를 입력하세요\",\n  \"enter_valid_allowlist\": \"허용 목록에 유효한 URL을 입력해주세요.\",\n  \"enter_valid_blocklist\": \"차단 목록에 유효한 URL을 입력해주세요.\",\n  \"error_details\": \"오류 상세 정보\",\n  \"example_comment\": \"! 댓글을 추가하는 방법\",\n  \"example_comment_hash\": \"# 이것 또한 댓글입니다.\",\n  \"example_comment_meaning\": \"이것은 단지 댓글입니다;\",\n  \"example_meaning_filter_block\": \"example.org 및 모든 하위 도메인에 대한 접근 차단;\",\n  \"example_meaning_filter_whitelist\": \"example.org 을 포함한 모든 서브 도메인 접근을 차단 해제합니다.\",\n  \"example_meaning_host_block\": \"example.org에 대해 127.0.0.1로 응답합니다 (하위 도메인은 아님);\",\n  \"example_multiple_upstreams_reserved\": \"<0>특정 도메인</0>에 대한 여러 업스트림\",\n  \"example_regex_meaning\": \"특정 정규 표현식에 맞는 도메인 접근을 차단합니다.\",\n  \"example_rewrite_domain\": \"이 도메인 이름에 대한 응답을 변경합니다.\",\n  \"example_rewrite_wildcard\": \"모든 서브 도메인에 대한 <0>example.org</0> 응답을 변경합니다\",\n  \"example_upstream_comment\": \"댓글.\",\n  \"example_upstream_doh\": \"암호화된 <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"암호화된 DNS-over-HTTPS가 강제로 <0>HTTP/3</0>를 사용하며 HTTP/2 이하로 폴백하지 않습니다.\",\n  \"example_upstream_doq\": \"암호화된 <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"암호화된 <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"일반 DNS (UDP을 통한 접속);\",\n  \"example_upstream_regular_port\": \"일반 DNS (UDP 이용, 포트 포함);\",\n  \"example_upstream_reserved\": \"<0>특정 도메인에 대한</0> 업스트림;\",\n  \"example_upstream_sdns\": \"<1>DNSCrypt</1> 또는 <2>DNS-over-HTTPS</2> 리졸버를 위한 <0>DNS 스탬프</0>;\",\n  \"example_upstream_tcp\": \"일반 DNS (TCP를 통한 접속);\",\n  \"example_upstream_tcp_hostname\": \"일반 DNS (TCP를 통한, 호스트명);\",\n  \"example_upstream_tcp_port\": \"일반 DNS (TCP 이용, 포트 포함);\",\n  \"example_upstream_udp\": \"일반 DNS (UDP를 통한, 호스트명);\",\n  \"examples_title\": \"예시\",\n  \"fallback_dns_desc\": \"업스트림 DNS 서버가 응답하지 않을 때 사용되는 폴백 DNS 서버 목록입니다. 구문은 위의 기본 업스트림 필드와 동일합니다.\",\n  \"fallback_dns_placeholder\": \"한 줄에 하나의 폴백 DNS 서버를 입력하세요.\",\n  \"fallback_dns_title\": \"폴백 DNS 서버\",\n  \"faq\": \"자주 묻는 질문\",\n  \"fastest_addr\": \"가장 빠른 IP 주소\",\n  \"fastest_addr_desc\": \"<b>모든</b> DNS 서버의 응답을 기다렸다가 각 서버의 TCP 연결 속도를 측정하여 연결 속도가 가장 빠른 서버의 IP 주소를 반환합니다.<br/>이 모드는 하나 이상의 업스트림 서버가 응답하지 않는 경우, DNS 쿼리 속도가 상당히 느려질 수 있습니다. 업스트림 서버가 안정적이고 업스트림 타임아웃이 짧은지 확인하세요.\",\n  \"filter\": \"필터\",\n  \"filter_added_successfully\": \"목록이 성공적으로 추가됨\",\n  \"filter_allowlist\": \"경고: 이 경우 허용된 클라이언트 목록에서 '{{disallowed_rule}}' 규칙 또한 제외됩니다.\",\n  \"filter_category_general\": \"일반 목록\",\n  \"filter_category_general_desc\": \"대부분의 기기에서 추적 및 광고를 차단하는 목록\",\n  \"filter_category_other\": \"기타\",\n  \"filter_category_other_desc\": \"기타 차단 목록\",\n  \"filter_category_regional\": \"지역 목록\",\n  \"filter_category_regional_desc\": \"지역 광고 및 추적 서버에 중점을 둔 목록\",\n  \"filter_category_security\": \"보안 목록\",\n  \"filter_category_security_desc\": \"악성 및 피싱 도메인을 차단하는 목록\",\n  \"filter_removed_successfully\": \"목록이 성공적으로 제거되었습니다\",\n  \"filter_updated\": \"필터가 성공적으로 업데이트됨\",\n  \"filtered\": \"필터링됨\",\n  \"filtered_custom_rules\": \"사용자 정의 필터링 규칙으로 필터링됨\",\n  \"filtering_rules_learn_more\": \"차단 리스트를 직접 호스트하는 법을 <0>알아보세요</0>.\",\n  \"filters\": \"필터\",\n  \"filters_and_hosts_hint\": \"AdGuard Home은 기본적인 광고 차단 규칙과 호스트 파일 문법을 읽을 수 있습니다\",\n  \"filters_block_toggle_hint\": \"차단규칙<a>필터</a>을 설정할 수 있습니다.\",\n  \"filters_configuration\": \"필터 구성\",\n  \"filters_enable\": \"필터 활성화\",\n  \"filters_interval\": \"필터 업데이트 주기\",\n  \"fix\": \"수정\",\n  \"for_last_days\": \"마지막 {{count}} 일\",\n  \"for_last_days_plural\": \"마지막 {{count}} 일의 기록\",\n  \"for_last_hours\": \"마지막 {{count}} 시간\",\n  \"for_last_hours_plural\": \"마지막 {{count}} 시간의 기록\",\n  \"forgot_password\": \"비밀번호를 잊어버렸나요?\",\n  \"forgot_password_desc\": \"다음과 같은 <0>단계</0>를 따라 귀하의 사용자 계정을 위한 새로운 비밀번호를 생성하세요.\",\n  \"form_add_id\": \"식별자 추가\",\n  \"form_answer\": \"IP 주소 또는 도메인 이름을 입력하세요\",\n  \"form_client_name\": \"클라이언트 이름 입력\",\n  \"form_domain\": \"도메인 이름 또는 와일드카드를 입력합니다\",\n  \"form_enter_blocked_response_ttl\": \"차단된 응답 TTL(초)을 입력하세요.\",\n  \"form_enter_host\": \"호스트 이름을 입력해주세요\",\n  \"form_enter_hostname\": \"호스트 이름을 입력해주세요\",\n  \"form_enter_id\": \"식별자 입력\",\n  \"form_enter_ip\": \"IP 입력\",\n  \"form_enter_mac\": \"MAC 입력\",\n  \"form_enter_rate_limit\": \"한도 제한 입력하기\",\n  \"form_enter_rate_limit_subnet_len\": \"속도 제한을 위한 서브넷 접두사 길이를 입력하세요\",\n  \"form_enter_subnet_ip\": \"서브넷 '{{cidr}}' 내의 IP 주소 입력\",\n  \"form_enter_upstream_timeout\": \"업스트림 서버 응답 제한 시간을 초 단위로 입력하세요.\",\n  \"form_error_answer_format\": \"답변 형식이 잘못되었습니다\",\n  \"form_error_client_id_format\": \"ClientID는 숫자, 소문자 및 붙임표(-)만 포함해야 합니다\",\n  \"form_error_domain_format\": \"도메인 형식이 잘못되었습니다\",\n  \"form_error_equal\": \"동일하지 않아야 합니다\",\n  \"form_error_gateway_ip\": \"임대는 게이트웨이의 IP 주소를 가질 수 없습니다\",\n  \"form_error_ip4_format\": \"잘못된 IPv4 형식\",\n  \"form_error_ip4_gateway_format\": \"잘못된 게이트웨이 IPv4 형식\",\n  \"form_error_ip6_format\": \"잘못된 IPv6 주소\",\n  \"form_error_ip_format\": \"잘못된 IP 주소\",\n  \"form_error_mac_format\": \"잘못된 MAC 주소\",\n  \"form_error_password\": \"비밀번호 불일치\",\n  \"form_error_password_length\": \"비밀번호는 {{min}}~{{max}}자 길이여야 합니다.\",\n  \"form_error_port\": \"유효한 포트 번호를 입력하세요\",\n  \"form_error_port_range\": \"80-65535 범위의 포트 번호를 입력하세요\",\n  \"form_error_port_unsafe\": \"안전하지 않은 포트입니다\",\n  \"form_error_positive\": \"0보다 커야 합니다\",\n  \"form_error_required\": \"필수 영역\",\n  \"form_error_server_name\": \"유효하지 않은 서버 이름\",\n  \"form_error_subnet\": \"서브넷 '{{cidr}}'에 '{{ip}}' IP 주소가 없습니다\",\n  \"form_error_url_format\": \"잘못된 URL 형식\",\n  \"form_error_url_or_path_format\": \"목록의 URL 또는 절대 경로가 잘못되었습니다\",\n  \"form_select_tags\": \"클라이언트 태그 선택\",\n  \"found_in_known_domain_db\": \"알려진 도메인 데이터베이스에서 발견됨.\",\n  \"friday\": \"금요일\",\n  \"friday_short\": \"금\",\n  \"gateway_or_subnet_invalid\": \"잘못된 서브넷 마스크\",\n  \"general_settings\": \"일반 설정\",\n  \"general_statistics\": \"일반 통계\",\n  \"get_started\": \"시작하기\",\n  \"greater_range_start_error\": \"범위 시작보다 큰 값이어야 합니다\",\n  \"homepage\": \"홈페이지\",\n  \"host_whitelisted\": \"예외 목록에 있는 호스트\",\n  \"ignore_domains\": \"무시된 도메인(줄 바꿈으로 구분)\",\n  \"ignore_domains_desc_query\": \"이러한 규칙과 일치하는 쿼리는 쿼리 로그에 기록되지 않습니다.\",\n  \"ignore_domains_desc_stats\": \"이러한 규칙과 일치하는 쿼리는 통계에 기록되지 않습니다.\",\n  \"ignore_domains_title\": \"무시된 도메인\",\n  \"ignore_query_log\": \"쿼리 로그에서 이 클라이언트 무시\",\n  \"ignore_statistics\": \"통계에서 이 클라이언트 무시\",\n  \"install_auth_confirm\": \"비밀번호 확인\",\n  \"install_auth_desc\": \"AdGuard Home 관리자 웹 인터페이스에 암호를 사용하는것이 권장됩니다. 로컬 네트워크에서만 액세스할 수 있더라도 혹시 모를 외부 액세스로부터 보호하는 것도 중요합니다.\",\n  \"install_auth_password\": \"비밀번호\",\n  \"install_auth_password_enter\": \"비밀번호 입력\",\n  \"install_auth_title\": \"인증\",\n  \"install_auth_username\": \"사용자 이름\",\n  \"install_auth_username_enter\": \"사용자 이름 입력\",\n  \"install_devices_address\": \"AdGuard Home DNS 서버는 다음의 주소를 받고 있습니다.\",\n  \"install_devices_android_list_1\": \"안드로이드 메뉴 홈 화면에서 설정을 누르세요.\",\n  \"install_devices_android_list_2\": \"메뉴에서 Wi-Fi를 클릭하세요. 사용 가능한 모든 네트워크가 나열된 화면이 표시됩니다 (모바일 연결을 위해 사용자 지정 DNS를 설정할 수 없습니다).\",\n  \"install_devices_android_list_3\": \"연결된 네트워크를 길게 누르고 네트워크 수정을 누르세요.\",\n  \"install_devices_android_list_4\": \"일부 장치에서는 추가설정을 하려면 고급란을 설정해야합니다. 안드로이드 DNS 설정을 조절하려면 IP설정을 DHCP에서 고정(Static) 으로 전환하세요.\",\n  \"install_devices_android_list_5\": \"DNS 1 및 DNS 2 값을 AdGuard Home 서버 주소로 변경하세요.\",\n  \"install_devices_desc\": \"AdGuard Home을 사용하려면, 당신의 기기를 설정해야합니다.\",\n  \"install_devices_ios_list_1\": \"홈 화면에서 설정을 누르세요.\\n\",\n  \"install_devices_ios_list_2\": \"왼쪽 메뉴에서 Wi-Fi 선택하세요 ( 모바일 네트워크에 대한 DNS를 구성할 수 없습니다).\\n\",\n  \"install_devices_ios_list_3\": \"현재 활성 네트워크의 이름을 누르세요.\",\n  \"install_devices_ios_list_4\": \"DNS 필드에 AdGuard Home 서버 주소를 입력하세요.\",\n  \"install_devices_macos_list_1\": \"Apple 아이콘을 클릭하고 시스템 기본 설정으로 이동하세요.\",\n  \"install_devices_macos_list_2\": \"네트워크를 클릭하세요.\",\n  \"install_devices_macos_list_3\": \"목록에서 첫 번째 연결을 선택하고 고급을 클릭해주세요.\",\n  \"install_devices_macos_list_4\": \"DNS 탭을 선택하고 AdGuard Home 서버 주소를 입력하세요.\",\n  \"install_devices_router\": \"라우터\",\n  \"install_devices_router_desc\": \"이 설정은 이제 홈 라우터에 연결된 모든 기기에 자동으로 적용되므로 각 기기를 수동으로 구성할 필요가 없습니다.\",\n  \"install_devices_router_list_1\": \"라우터의 환경 설정을 여세요. 환경 설정은 다음의 주소(http://192.168.0.1/ 혹은 http://192.168.1.1/)를 통해 브라우저로 접근 가능합니다. 비밀번호를 입력해야 할 수 있습니다. 비밀번호를 잊었다면 라우터 기기에 있는 버튼을 눌러 비밀번호를 초기화할 수 있지만 라우터 설정이 손실될 수 있습니다. 라우터 설정에 앱이 필요한 경우, 휴대폰이나 컴퓨터에 앱을 설치하고 이를 사용하여 라우터 설정에 액세스하세요.\",\n  \"install_devices_router_list_2\": \"각각 1~3자리 숫자의 네 그룹으로 분할된 두 세트의 숫자를 허용하는 필드 옆에 있는 DNS 문자를 찾으세요.\",\n  \"install_devices_router_list_3\": \"AdGuard Home 서버 주소를 입력하세요\",\n  \"install_devices_router_list_4\": \"일부 라우터 유형에서는 사용자 정의 DNS 서버를 설정할 수 없습니다. 이 경우에는 AdGuard Home을 <0>DHCP 서버</0>로 설정할 수 있습니다. 그렇지 않으면 특정 라우터 모델에 맞게 DNS 서버를 설정하는 방법을 찾아야 합니다.\",\n  \"install_devices_title\": \"디바이스를 설정하기\",\n  \"install_devices_windows_list_1\": \"시작 메뉴 또는 윈도우 검색을 통해 제어판을 엽니다.\",\n  \"install_devices_windows_list_2\": \"네트워크 및 인터넷 카테고리로 이동한 다음 네트워크 및 공유 센터로 이동합니다.\",\n  \"install_devices_windows_list_3\": \"화면 왼쪽에서 '어댑터 설정 변경'을 클릭합니다.\",\n  \"install_devices_windows_list_4\": \"활성 연결을 선택한 후 우클릭으로 속성을 선택합니다.\",\n  \"install_devices_windows_list_5\": \"목록에서 '인터넷 프로토콜 버전 4(TCP/IP)' (또는 IPv6의 경우 '인터넷 프로토콜 버전 6(TCP/IPv6)')를 찾아 선택하고 속성을 클릭합니다.\",\n  \"install_devices_windows_list_6\": \"'DNS 서버 주소 사용'을 선택하고 AdGuard Home 서버 주소 입력합니다.\",\n  \"install_saved\": \"성공적으로 저장되었습니다\",\n  \"install_settings_all_interfaces\": \"모든 인터페이스\",\n  \"install_settings_dns\": \"DNS 서버\",\n  \"install_settings_dns_desc\": \"다음 주소의 DNS 서버를 사용하도록 장치 또는 라우터를 구성해야 합니다.\",\n  \"install_settings_interface_link\": \"AdGuard Home 관리자 웹 인터페이스는 다음 주소로 제공됨:\",\n  \"install_settings_listen\": \"네트워크 인터페이스\",\n  \"install_settings_port\": \"포트\",\n  \"install_settings_title\": \"관리자 웹 인터페이스\",\n  \"install_static_configure\": \"AdGuard Home이 동적 IP 주소를 사용하는 것을 감지했습니다 - <0>{{ip}}</0>. 정말로 이걸 고정 IP로 사용하시겠습니까?\",\n  \"install_static_error\": \"AdGuard Home는 이 네트워크 인터페이스에서 자동 설정할 수 없습니다. 여기에서 어떻게 이걸 수동으로 할 수 있는지 확인해주세요.\",\n  \"install_static_ok\": \"좋은 소식입니다! 고정 IP 주소가 이미 설정되어있네요\",\n  \"install_step\": \"단계\",\n  \"install_submit_desc\": \"설치 절차가 완료되었으며 이제 AdGuard Home을 사용할 준비가 되었습니다.\",\n  \"install_submit_title\": \"축하합니다!\",\n  \"install_welcome_desc\": \"AdGuard Home은 광범위한 네트워크 광고와 추적 DNS 서버를 차단 합니다. 그것의 목적은 당신이 당신의 전체 네트워크와 당신의 모든 기기를 제어하는 것이며, 그것은 클라이언트의 프로그램을 사용할 필요가 없습니다.\",\n  \"install_welcome_title\": \"AdGuard Home에 오신 것을 환영합니다!\",\n  \"interval_24_hour\": \"24시간\",\n  \"interval_6_hour\": \"6시간\",\n  \"interval_days\": \"{{count}} 일\",\n  \"interval_days_plural\": \"{{count}} 일\",\n  \"interval_hours\": \"{{count}} 시간\",\n  \"interval_hours_plural\": \"{{count}} 시간\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP 주소\",\n  \"known_tracker\": \"알려진 추적기\",\n  \"last_rule_in_allowlist\": \"'{{disallowed_rule}}' 규칙을 제외하면 '허용된 클라이언트' 목록이 꺼지므로 해당 클라이언트를 제외할 수 없습니다.\",\n  \"last_time_updated_table_header\": \"마지막 업데이트\",\n  \"list_confirm_delete\": \"정말로 이 목록을 제거하시겠습니까?\",\n  \"list_label\": \"목록\",\n  \"list_updated\": \"{{count}} 리스트 업데이트됨\",\n  \"list_updated_plural\": \"{{count}} 리스트 업데이트됨\",\n  \"list_url_table_header\": \"리스트 URL\",\n  \"load_balancing\": \"로드 밸런싱\",\n  \"load_balancing_desc\": \"한 번에 하나의 업스트림 서버를 쿼리합니다.<br/>AdGuard Home은 가중 무작위 알고리즘을 사용하여 조회 실패 횟수가 가장 적고 평균 조회 시간이 가장 짧은 서버를 선택합니다.\",\n  \"loading_table_status\": \"로딩중...\",\n  \"local_ptr_default_resolver\": \"기본적으로 AdGuard Home에서는 {{ip}} 역방향 DNS 서버를 이용합니다.\",\n  \"local_ptr_desc\": \"AdGuard Home에서 비공개 PTR, SOA 및 NS 요청에 사용하는 DNS 서버입니다. 요청이 비공개 IP 범위 내의 서브넷(예: \\\"192.168.12.34\\\")을 포함하는 ARPA 도메인을 요청하고 비공개 IP 주소를 가진 클라이언트로부터 오는 경우 비공개로 간주됩니다. 설정하지 않으면 AdGuard Home IP 주소를 제외한 OS의 기본 DNS 리졸버가 사용됩니다.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home에서 이 시스템에 적합한 사설 역방향 프라이빗 DNS 서버를 결정할 수 없습니다.\",\n  \"local_ptr_placeholder\": \"한 줄에 하나씩 IP 주소를 입력하세요.\",\n  \"local_ptr_title\": \"프라이빗 역방향 DNS 서버\",\n  \"location\": \"위치\",\n  \"log_and_stats_section_label\": \"쿼리 로그 및 통계\",\n  \"lower_range_start_error\": \"범위 시작보다 작은 값이어야 합니다\",\n  \"main_settings\": \"기본 설정\",\n  \"make_static\": \"정적으로 만들기\",\n  \"manual_update\": \"<a>절차를 따라</a> 수동으로 업데이트하십시오.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"월요일\",\n  \"monday_short\": \"월\",\n  \"name\": \"이름\",\n  \"name_table_header\": \"이름\",\n  \"netname\": \"네트워크 이름\",\n  \"network\": \"네트워크\",\n  \"new_allowlist\": \"새 허용 목록\",\n  \"new_blocklist\": \"새 차단 목록\",\n  \"next\": \"다음\",\n  \"next_btn\": \"다음\",\n  \"no_blocklist_added\": \"차단 목록이 추가되지 않음\",\n  \"no_clients_found\": \"클라이언트가 없습니다\",\n  \"no_domains_found\": \"도메인이 없습니다\",\n  \"no_logs_found\": \"로그 기록 찾을 수 없음\",\n  \"no_servers_specified\": \"지정된 서버 없음\",\n  \"no_upstreams_data_found\": \"업스트림 데이터 없음\",\n  \"no_whitelist_added\": \"허용 목록이 추가되지 않음\",\n  \"nothing_found\": \"아무것도 찾을 수 없습니다\",\n  \"null_ip\": \"빈 IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"광고 차단 필터 및 호스트 차단 목록에 의해 차단된 DNS 요청 수\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"차단된 성인 웹 사이트의 수\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"AdGuard 브라우징 보안 모듈에 의해 차단된 DNS 요청 수\",\n  \"number_of_dns_query_days\": \"최근 {{count}}일 동안 처리된 DNS 쿼리의 수\",\n  \"number_of_dns_query_days_plural\": \"최근 {{count}}일 동안 처리된 DNS 쿼리의 수\",\n  \"number_of_dns_query_hours\": \"최근 {{count}}시간 동안 처리된 DNS 쿼리의 수\",\n  \"number_of_dns_query_hours_plural\": \"최근 {{count}}시간 동안 처리된 DNS 쿼리의 수\",\n  \"number_of_dns_query_to_safe_search\": \"세이프서치가 적용된 검색 엔진에 대해 DNS 요청 수\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"OFF\",\n  \"on\": \"ON\",\n  \"open_dashboard\": \"대시보드 열기\",\n  \"orgname\": \"단체 이름\",\n  \"original_response\": \"원래 응답\",\n  \"out_of_range_error\": \"'{{start}}'-'{{end}}' 범위 밖이어야 합니다\",\n  \"page_table_footer_text\": \"페이지\",\n  \"parallel_requests\": \"병렬 처리 요청\",\n  \"parental_control\": \"자녀 보호\",\n  \"password_label\": \"비밀번호\",\n  \"password_placeholder\": \"비밀번호 입력\",\n  \"plain_dns\": \"평문 DNS\",\n  \"port_53_faq_link\": \"53번 포트는 보통 'DNSStubListener나 'systemd-resolved' 서비스가 이미 사용하고 있습니다. 이 문제에 대한 해결 방법을 <0>설명</0>에서 찾아보세요.\",\n  \"previous_btn\": \"이전\",\n  \"privacy_policy\": \"개인정보취급방침\",\n  \"processing_update\": \"잠시만 기다려주세요, AdGuard Home가 업데이트 중입니다.\",\n  \"protection_section_label\": \"보호\",\n  \"protocol\": \"프로토콜\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"쿼리 로그\",\n  \"query_log_clear\": \"쿼리 로그 비우기\",\n  \"query_log_cleared\": \"쿼리 로그를 성공적으로 초기화했습니다\",\n  \"query_log_configuration\": \"로그 구성\",\n  \"query_log_confirm_clear\": \"정말로 모든 쿼리 로그를 비우시겠습니까?\",\n  \"query_log_disabled\": \"쿼리 로그가 비활성화되어 있으며 <0>설정</0>에서 설정할 수 있습니다\",\n  \"query_log_enable\": \"로그 활성화\",\n  \"query_log_filtered\": \"필터: {{filter}}\",\n  \"query_log_response_status\": \"상태: {{value}}\",\n  \"query_log_retention\": \"쿼리 로그 로테이션\",\n  \"query_log_retention_confirm\": \"쿼리 로그 로테이션을 변경하시겠습니까? 간격 값을 줄이면 일부 데이터가 손실됩니다.\",\n  \"query_log_strict_search\": \"검색을 제한하려면 쌍따옴표를 사용해주세요\",\n  \"query_log_updated\": \"질의 로그가 성공적으로 업데이트되었습니다\",\n  \"rate_limit\": \"한도 제한\",\n  \"rate_limit_desc\": \"단일 클라이언트에서 허용 가능한 초 당 요청 생성 숫자 (0: 무제한)\",\n  \"rate_limit_subnet_len_ipv4\": \"IPv4 주소의 서브넷 접두사 길이\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"속도 제한에 사용되는 IPv4 주소의 서브넷 접두사 길이입니다. 기본값은 24입니다.\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4 서브넷 접두사 길이는 0에서 32 사이여야 합니다.\",\n  \"rate_limit_subnet_len_ipv6\": \"IPv6 주소의 서브넷 접두사 길이\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"속도 제한에 사용되는 IPv6 주소의 서브넷 접두사 길이입니다. 기본값은 56입니다.\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6 서브넷 접두사 길이는 0에서 128 사이여야 합니다.\",\n  \"rate_limit_whitelist\": \"속도 제한 허용 목록\",\n  \"rate_limit_whitelist_desc\": \"속도 제한에서 제외되는 IP 주소\",\n  \"rate_limit_whitelist_placeholder\": \"한 줄에 하나씩 IP 주소를 입력하세요.\",\n  \"refresh_btn\": \"새로고침\",\n  \"refresh_statics\": \"통계 새로 고침\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"문제 신고\",\n  \"request_details\": \"요청 세부 사항\",\n  \"request_table_header\": \"요청\",\n  \"requests_count\": \"요청 수\",\n  \"reset_settings\": \"설정 초기화\",\n  \"resolve_clients_desc\": \"해당 서버에 대한 PTR 쿼리를 통해 클라이언트의 도메인 이름을 정의합니다. (로컬 클라이언트의 경우 프라이빗 DNS 서버, 공용 IP 주소가 있는 클라이언트의 경우 업스트림 서버).\",\n  \"resolve_clients_title\": \"클라이언트 IP 주소에 대한 호스트명 확인 활성화\",\n  \"response_code\": \"응답 코드\",\n  \"response_details\": \"응답 정보\",\n  \"response_table_header\": \"응답\",\n  \"response_time\": \"응답 시간\",\n  \"rewrite_A\": \"<0> A</0>: 특수 값, 업스트림에서 <0> A</0> 기록 유지\",\n  \"rewrite_AAAA\": \"<0> AAAA</0>: 특수 값, 업스트림에서 <0> AAAA</0> 기록 유지\",\n  \"rewrite_add\": \"DNS 변환 정보를 추가합니다\",\n  \"rewrite_added\": \"'{{key}}'에 대한 DNS 수정 정보를 성공적으로 추가 됩니다\",\n  \"rewrite_applied\": \"리디렉션 규칙이 적용됩니다\",\n  \"rewrite_confirm_delete\": \"'{{key}}'에 대한 DNS 변경 정보를 삭제하시겠습니까?\",\n  \"rewrite_deleted\": \"'{{key}}'에 대한 DNS 수정 정보를 성공적으로 삭제 됩니다\",\n  \"rewrite_desc\": \"특정 도메인 이름에 대한 사용자 지정 DNS 응답을 쉽게 구성할 수 있습니다.\",\n  \"rewrite_domain_name\": \"도메인 이름: CNAME 레코드 추가\",\n  \"rewrite_edit\": \"DNS 다시 쓰기 편집\",\n  \"rewrite_hosts_applied\": \"호스트 파일 규칙에 따라 재작성\",\n  \"rewrite_ip_address\": \"IP 주소: 이 IP를 A 또는 AAAA 응답에 사용합니다\",\n  \"rewrite_not_found\": \"DNS 변경 정보를 찾을 수 없습니다\",\n  \"rewrite_settings_updated\": \"DNS 재작성 설정이 성공적으로 업데이트되었습니다.\",\n  \"rewrite_updated\": \"DNS 다시 쓰기 업데이트 완료\",\n  \"rewrites_disabled_table_header\": \"Rewrites(재작성)이 비활성화됩니다.\",\n  \"rewrites_enabled_table_header\": \"Rewrites(재작성)이 활성화됩니다.\",\n  \"rewritten\": \"재작성됨\",\n  \"rows_table_footer_text\": \"행\",\n  \"rule_added_to_custom_filtering_toast\": \"사용자 정의 필터링 규칙에 추가된 규칙 {{rule}}\",\n  \"rule_label\": \"규칙\",\n  \"rule_removed_from_custom_filtering_toast\": \"사용자 정의 필터링 규칙에서 규칙 제거 {{rule}}\",\n  \"rules_count_table_header\": \"규칙 개수\",\n  \"safe_browsing\": \"세이프 브라우징\",\n  \"safe_search\": \"세이프서치\",\n  \"saturday\": \"토요일\",\n  \"saturday_short\": \"토\",\n  \"save_btn\": \"저장\",\n  \"save_config\": \"구성 저장\",\n  \"schedule_add\": \"일정 추가\",\n  \"schedule_current_timezone\": \"현재 시간대: {{value}}\",\n  \"schedule_desc\": \"차단된 서비스에 대한 비활성 기간을 설정하세요.\",\n  \"schedule_edit\": \"일정 수정\",\n  \"schedule_from\": \"시작 시간\",\n  \"schedule_invalid_select\": \"시작 시간은 종료 시간 이전이어야 합니다.\",\n  \"schedule_modal_description\": \"이 일정은 같은 요일의 기존 일정을 대체합니다. 각 요일은 단 한 번의 비활성 기간만 가질 수 있습니다.\",\n  \"schedule_modal_time_off\": \"서비스 차단이 비활성화된 요일 및 시간\",\n  \"schedule_new\": \"새로운 일정\",\n  \"schedule_remove\": \"일정 제거\",\n  \"schedule_save\": \"일정 저장\",\n  \"schedule_select_days\": \"요일 선택\",\n  \"schedule_services\": \"서비스 차단 일시 중지\",\n  \"schedule_services_desc\": \"서비스 차단 필터의 일시 중지 일정을 구성하세요.\",\n  \"schedule_services_desc_client\": \"이 클라이언트에 대한 서비스 차단 필터의 일시 중지 일정을 구성하세요.\",\n  \"schedule_time_all_day\": \"하루 종일\",\n  \"schedule_timezone\": \"표준 시간대 선택\",\n  \"schedule_to\": \"종료 시간\",\n  \"served_from_cache_label\": \"캐시에서 가져옴\",\n  \"service_name\": \"서비스 이름\",\n  \"set_static_ip\": \"고정 IP 주소 설정\",\n  \"settings\": \"설정\",\n  \"settings_custom\": \"사용자\",\n  \"settings_global\": \"글로벌\",\n  \"setup_config_to_enable_dhcp_server\": \"DHCP 서버를 활성화하기 위한 설정 구성\",\n  \"setup_dns_notice\": \"<1>DNS-over-HTTPS</1> 또는 <1>DNS-over-TLS를</1> 사용하려면 AdGuard Home 설정에서 <0>암호화를 구성해야 합니다.</0>\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> <1>{{address}}</1> 사용하세요.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> <1>{{address}}</1> 사용하세요.\",\n  \"setup_dns_privacy_3\": \"<0>사용할 수 있는 소프트웨어는 다음과 같습니다.</0>\",\n  \"setup_dns_privacy_4\": \"iOS 14 또는 macOS Big Sur 기기에서 DNS 설정에 <highlight>DNS-over-HTTPS</highlight> 또는 <highlight>DNS-over-TLS</highlight> 서버를 추가하는 특수 '.mobileconfig' 파일을 다운로드할 수 있습니다.\",\n  \"setup_dns_privacy_android_1\": \"Android 9는 기본적으로 DNS-over-TLS를 지원합니다. 구성하려면 설정 → 네트워크 및 인터넷 → 고급 → 개인 DNS로 이동하여 도메인 이름을 입력하세요.\",\n  \"setup_dns_privacy_android_2\": \"<0>Android용 AdGuard</0>DNS-over-HTTPS <1>및</1> DNS-over-TLS <1>지원합니다</1>\",\n  \"setup_dns_privacy_android_3\": \"<0>인트라</0> 안드로이드에 <1>DNS-over-HTTPS </1>지원 추가합니다.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS 및 macOS 설정\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak은</0> <1>DNS-over-HTTPS를</1> 지원하지만, 자신의 서버를 사용하도록 구성하려면 <2>DNS 스탬프를</2> 생성해야 합니다.\",\n  \"setup_dns_privacy_ios_2\": \"<0>iOS용 AdGuard는</0> <1>DNS-over-HTTPS </1>및 <1>DNS-over-TLS</1> 설정을 지원합니다.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home 모든 플랫폼에서 안전한 DNS 클라이언트가 될 수 있습니다.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy는</0> 알려진 모든 안전한 DNS 프로토콜을 지원합니다.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> <1>DNS-over-HTTPS</1> 지원합니다.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0><1>DNS-over-HTTPS</1>지원합니다.\",\n  \"setup_dns_privacy_other_5\": \"<0>이곳이나</0> <1>이곳을</1> 클릭하여 더 많은 구현에 대한 정보를 확인하세요.\",\n  \"setup_dns_privacy_other_title\": \"기타 구현\",\n  \"setup_guide\": \"설치 안내\",\n  \"show_all_filter_type\": \"모두 표시\",\n  \"show_blocked_responses\": \"차단됨\",\n  \"show_filtered_type\": \"필터된 것 표시\",\n  \"show_processed_responses\": \"처리됨\",\n  \"show_whitelisted_responses\": \"예외 적용됨\",\n  \"sign_in\": \"로그인\",\n  \"sign_out\": \"로그아웃\",\n  \"source_label\": \"소스\",\n  \"static_ip\": \"고정 IP 주소\",\n  \"static_ip_desc\": \"AdGuard Home는 서버라서 정상적으로 작동하려면 고정 IP 주소가 필요합니다. 그렇지 않다면 라우터가 언젠가 이 기기에 다른 IP 주소를 할당할 수도 있습니다.\",\n  \"statistics_clear\": \"통계 초기화\",\n  \"statistics_clear_confirm\": \"통계를 정말로 초기화하시겠습니까?\",\n  \"statistics_cleared\": \"통계를 성공적으로 초기화했습니다.\",\n  \"statistics_configuration\": \"통계 구성\",\n  \"statistics_enable\": \"통계 활성화\",\n  \"statistics_retention\": \"통계 저장 기간\",\n  \"statistics_retention_confirm\": \"정말로 통계 저장 기간을 변경하시겠습니까? 저장 주기를 낮출 경우, 일부 데이터가 손실됩니다\",\n  \"statistics_retention_desc\": \"주기를 줄이면, 일부 데이터가 손실됩니다\",\n  \"stats_adult\": \"차단된 성인 웹사이트\",\n  \"stats_disabled\": \"통계 기능이 꺼졌습니다. <0>설정 페이지</0>에서 켤 수 있습니다.\",\n  \"stats_disabled_short\": \"통계 꺼짐\",\n  \"stats_malware_phishing\": \"차단된 멀웨어/피싱\",\n  \"stats_params\": \"통계 구성\",\n  \"stats_query_domain\": \"쿼리 도메인\",\n  \"subnet_error\": \"주소는 하나의 서브넷에 있어야 합니다\",\n  \"sunday\": \"일요일\",\n  \"sunday_short\": \"일\",\n  \"system_host_files\": \"시스템 호스트 파일\",\n  \"table_client\": \"클라이언트\",\n  \"table_name\": \"이름\",\n  \"tags_desc\": \"클라이언트에 해당하는 태그를 선택할 수 있습니다. 필터링 규칙에 태그를 포함시키면 더 정확하게 적용시킬 수 있습니다. <0>자세히 알아보기</0>.\",\n  \"tags_title\": \"태그\",\n  \"test_upstream_btn\": \"업스트림 테스트\",\n  \"theme_auto\": \"자동\",\n  \"theme_auto_desc\": \"자동(기기의 색 구성표에 따라 설정)\",\n  \"theme_dark\": \"다크 테마\",\n  \"theme_dark_desc\": \"다크 테마\",\n  \"theme_light\": \"라이트 테마\",\n  \"theme_light_desc\": \"라이트 테마\",\n  \"thursday\": \"목요일\",\n  \"thursday_short\": \"목\",\n  \"time_table_header\": \"시간\",\n  \"top_blocked_domains\": \"차단된 도메인\",\n  \"top_clients\": \"클라이언트\",\n  \"top_upstreams\": \"상위 업스트림\",\n  \"topline_expired_certificate\": \"SSL 인증서가 만료되었습니다. 업데이트<0> 암호화 설정</0>.\",\n  \"topline_expiring_certificate\": \"SSL 인증서가 곧 만료됩니다. 업데이트<0> 암호화 설정</0>.\",\n  \"tracker_source\": \"추적기 소스\",\n  \"try_again\": \"다시 시도해주세요\",\n  \"ttl_cache_validation\": \"최소 캐시 TTL 값은 최대 값보다 이하여야 합니다\",\n  \"tuesday\": \"화요일\",\n  \"tuesday_short\": \"화\",\n  \"type_table_header\": \"유형\",\n  \"unavailable_dhcp\": \"DHCP가 사용 불가능합니다.\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home은 이 OS에서 DHCP 서버를 구동할 수 없습니다.\",\n  \"unblock\": \"차단 해제\",\n  \"unblock_all\": \"차단 해제\",\n  \"unblock_for_this_client_only\": \"이 클라이언트에 대해서만 차단 해제\",\n  \"unknown_filter\": \"알려지지 않은 필터 {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} 사용 가능합니다! <0>이곳</0>을 클릭하여 더 많은 정보를 확인하세요.\",\n  \"update_failed\": \"자동 업데이트 실패 되었습니다. <a> 단계를 따라 수동으로 업데이트하세요</a>\",\n  \"update_now\": \"지금 업데이트\",\n  \"updated_custom_filtering_toast\": \"사용자 정의 규칙이 성공적으로 저장되었습니다\",\n  \"updated_save_search_toast\": \"세이프서치 설정 업데이트됨\",\n  \"updated_upstream_dns_toast\": \"업스트림 서버가 성공적으로 저장되었습니다\",\n  \"updates_checked\": \"AdGuard Home의 새 버전을 사용할 수 있습니다\",\n  \"updates_version_equal\": \"AdGuard Home 최신 상태입니다.\",\n  \"upstream\": \"업스트림\",\n  \"upstream_dns\": \"업스트림 DNS 서버\",\n  \"upstream_dns_cache_configuration\": \"업스트림 DNS 캐시 설정\",\n  \"upstream_dns_client_desc\": \"이 값을 비워둔다면 AdGuard Home은 <0>DNS 설정</0>에 설정되어 있는 값을 사용합니다.\",\n  \"upstream_dns_configured_in_file\": \"{{path}}에서 구성됨\",\n  \"upstream_dns_help\": \"서버 주소를 한 줄에 하나씩 입력해주십시오. 업스트림 DNS 서버 구성에 대해 <a>자세히 알아보십시오</a>.\",\n  \"upstream_parallel\": \"쿼리 처리 속도를 높이려면 모든 업스트림 서버에서 동시에 병렬 쿼리를 사용해주세요.\",\n  \"upstream_timeout\": \"업스트림 제한 시간\",\n  \"upstream_timeout_desc\": \"업스트림 서버의 응답을 기다리는 시간(초)을 지정합니다.\",\n  \"upstreams\": \"업스트림\",\n  \"use_adguard_browsing_sec\": \"AdGuard 브라우징 보안 웹 서비스 사용\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home은 개인 정보를 보호하는 API를 사용하여 검색 보안 웹 서비스에 의해 도메인이 차단되었는지 확인합니다. 도메인 이름 SHA256 해시의 짧은 접두사만 서버로 전송됩니다.\",\n  \"use_adguard_parental\": \"AdGuard 자녀 보호 웹 서비스 사용\",\n  \"use_adguard_parental_hint\": \"AdGuard Home은 도메인에 성인 자료가 포함되어 있는지 확인합니다. 브라우징 보안 웹 서비스와 동일한 개인정보 보호 API를 사용함.\",\n  \"use_private_ptr_resolvers_desc\": \"사설 업스트림 서버, DHCP, / etc/hosts 등을 통해 사설 IP 주소가 포함된 ARPA 도메인에 대한 PTR, SOA 및 NS 요청을 처리합니다. 비활성화하면 AdGuard Home은 NXDOMAIN을 사용하여 이러한 모든 요청에 응답합니다.\",\n  \"use_private_ptr_resolvers_title\": \"프라이빗 역방향 DNS 리졸버 사용\",\n  \"use_saved_key\": \"이전에 저장했던 키 사용하기\",\n  \"username_label\": \"사용자 이름\",\n  \"username_placeholder\": \"사용자 이름 입력\",\n  \"validated_with_dnssec\": \"DNSSEC로 검증됨\",\n  \"version\": \"버전\",\n  \"version_request_error\": \"업데이트 체크에 실패했습니다. 인터넷 연결 상태를 확인해주세요.\",\n  \"wednesday\": \"수요일\",\n  \"wednesday_short\": \"수\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/nl.json",
    "content": "{\n  \"access_allowed_desc\": \"Een lijst met CIDR's, IP-adressen of <a>Client-ID's</a>. Indien geconfigureerd, accepteert AdGuard Home alleen verzoeken van deze cliënts.\",\n  \"access_allowed_title\": \"Toegestane gebruikers\",\n  \"access_blocked_desc\": \"Verwar dit niet met filters. AdGuard Home zal deze DNS-zoekopdrachten niet uitvoeren die deze domeinen in de zoekopdracht bevatten. Hier kan je de exacte domeinnamen, wildcards en URL-filter-regels specifiëren, bijv. \\\"example.org\\\", \\\"*.example.org\\\" of \\\"||example.org^\\\".\",\n  \"access_blocked_title\": \"Niet toegelaten domeinen\",\n  \"access_desc\": \"Hier kan je toegangsregels voor de AdGuard Home DNS-server instellen\",\n  \"access_disallowed_desc\": \"Een lijst met CIDR's, IP-adressen of <a>Client-ID's</a>. Indien geconfigureerd, zal AdGuard Home verzoeken van deze klanten verwerpen. Als toegestane cliënts zijn geconfigureerd, wordt dit veld genegeerd.\",\n  \"access_disallowed_title\": \"Verworpen gebruikers\",\n  \"access_settings_saved\": \"Toegangsinstellingen succesvol opgeslagen\",\n  \"access_title\": \"Toegangs instellingen\",\n  \"actions_table_header\": \"Actie\",\n  \"add_allowlist\": \"Toelatingslijst toevoegen\",\n  \"add_blocklist\": \"Blokkeerlijst toevoegen\",\n  \"add_custom_list\": \"Aangepaste lijst toevoegen\",\n  \"add_persistent_client\": \"Toevoegen als permanente client\",\n  \"address\": \"Adres\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home zal alle DNS-aanvragen van deze cliënt laten vervallen.\",\n  \"all_lists_up_to_date_toast\": \"Alle lijsten zijn reeds actueel\",\n  \"all_queries\": \"Alle vragen\",\n  \"allow_this_client\": \"Toepassing/systeem toelaten\",\n  \"allowed\": \"Toegestaan\",\n  \"anonymize_client_ip\": \"Cliënt IP anonimiseren\",\n  \"anonymize_client_ip_desc\": \"Het volledige IP-adres van de cliënt niet opnemen in logboeken en statistiekbestanden\",\n  \"anonymizer_notification\": \"<0>Opmerking:</0> IP-anonimisering is ingeschakeld. Je kunt het uitschakelen in <1>Algemene instellingen</1>.\",\n  \"answer\": \"Antwoord\",\n  \"apply_btn\": \"Toepassen\",\n  \"auto_clients_desc\": \"Informatie over IP-adressen van apparaten die AdGuard Home gebruiken of kunnen gebruiken. Deze informatie wordt verzameld uit verschillende bronnen, waaronder hosts-bestanden, reverse DNS, enz.\",\n  \"auto_clients_title\": \"Runtime-clients\",\n  \"autofix_warning_list\": \"De volgende taken worden uitgevoerd: <0> Deactiveren van Systeem DNSStubListener</0> <0> DNS-serveradres instellen op 127.0.0.1 </0> <0> Symbolisch koppelingsdoel van /etc/resolv.conf vervangen door /run/systemd/resolve/resolv.conf </0> <0> Stop DNSStubListener (herlaad systemd-resolved service) </0>\",\n  \"autofix_warning_result\": \"Als gevolg hiervan worden alle DNS-aanvragen van je systeem standaard door AdGuard Home verwerkt.\",\n  \"autofix_warning_text\": \"Als je op \\\"Repareren\\\" klikt, configureert AdGuard Home jouw systeem om de AdGuard Home DNS-server te gebruiken.\",\n  \"average_processing_time\": \"Gemiddelde procestijd\",\n  \"average_processing_time_hint\": \"Gemiddelde verwerkingstijd in milliseconden van een DNS aanvraag\",\n  \"average_upstream_response_time\": \"Gemiddelde upstream responstijd\",\n  \"back\": \"Terug\",\n  \"block\": \"Blokkeren\",\n  \"block_all\": \"Blokkeer alles\",\n  \"block_domain_use_filters_and_hosts\": \"Domeinen blokkeren d.m.v. filters en host-bestanden\",\n  \"block_for_this_client_only\": \"Alleen voor deze cliënt blokkeren\",\n  \"block_services\": \"Specifieke services blokkeren\",\n  \"blocked_adult_websites\": \"Geblokkeerd door ouderlijk toezicht\",\n  \"blocked_by\": \"<0>Geblokkeerd door Filters</0>\",\n  \"blocked_by_cname_or_ip\": \"Geblokkeerd via CNAME of IP\",\n  \"blocked_by_response\": \"Geblokkeerd door CNAME of IP als antwoord\",\n  \"blocked_response_ttl\": \"Geblokkeerde reactie TTL\",\n  \"blocked_response_ttl_desc\": \"Hiermee geef je op hoeveel seconden de clients een gefilterd antwoord in de cache moeten opslaan\",\n  \"blocked_safebrowsing\": \"Geblokkeerd door Veilig browsen\",\n  \"blocked_service\": \"Geblokkeerde service\",\n  \"blocked_services\": \"Geblokkeerde services\",\n  \"blocked_services_desc\": \"Hiermee kunt u populaire sites en services snel blokkeren.\",\n  \"blocked_services_global\": \"Gebruik algemeen geblokkeerde services\",\n  \"blocked_services_saved\": \"Geblokkeerde services succesvol opgeslagen\",\n  \"blocked_threats\": \"Geblokkeerde bedreigingen\",\n  \"blocking_ipv4\": \"Blokkeren IP4\",\n  \"blocking_ipv4_desc\": \"IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek\",\n  \"blocking_ipv6\": \"Blokkeren IP6\",\n  \"blocking_ipv6_desc\": \"IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek\",\n  \"blocking_mode\": \"Blocking modus\",\n  \"blocking_mode_custom_ip\": \"Aangepast IP: Reageer met een handmatige ingesteld IP adres\",\n  \"blocking_mode_default\": \"Standaard: Reageer met een nul IP-adres (0.0.0.0 for A; :: voor AAAA) wanneer geblokkeerd door een Adblock-type regel; reageer met het IP-adres dat is opgegeven in de regel wanneer geblokkeerd door een /etc/hosts type regel\",\n  \"blocking_mode_null_ip\": \"Nul IP: Reageer met een nul IP-adres (0.0.0.0 voor A; :: voor AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Reageer met NXDOMAIN code\",\n  \"blocking_mode_refused\": \"REFUSED: Antwoorden met REFUSED code\",\n  \"blocklist\": \"Blokkeerlijst\",\n  \"bootstrap_dns\": \"Bootstrap DNS-servers\",\n  \"bootstrap_dns_desc\": \"IP-adressen van DNS-servers die worden gebruikt om IP-adressen om te zetten van de DoH/DoT-resolvers die je opgeeft als upstreams. Opmerkingen zijn niet toegestaan.\",\n  \"cache_cleared\": \"DNS-cache succesvol gewist\",\n  \"cache_enabled\": \"Cache inschakelen\",\n  \"cache_enabled_desc\": \"DNS-antwoorden lokaal opslaan.\",\n  \"cache_optimistic\": \"Optimistisch cachen\",\n  \"cache_optimistic_desc\": \"Laat AdGuard Home reageren vanuit de cache, zelfs als de vermeldingen zijn verlopen en probeer deze ook te vernieuwen.\",\n  \"cache_size\": \"Cache grootte\",\n  \"cache_size_desc\": \"DNS-cache grootte (in bytes).\",\n  \"cache_size_validation\": \"De cachegrootte moet groter zijn dan nul wanneer deze is ingeschakeld.\",\n  \"cache_ttl_max_override\": \"Maximale TTL overschrijven\",\n  \"cache_ttl_max_override_desc\": \"Instellen van maximum time-to-live waarde (seconden) voor opslag in de DNS cache.\",\n  \"cache_ttl_min_override\": \"Minimale TTL overschrijven\",\n  \"cache_ttl_min_override_desc\": \"Uitbreiden van korte Time-To-Live waardes (seconden) ontvangen van de upstream server bij het cachen van DNS antwoorden.\",\n  \"cancel_btn\": \"Annuleren\",\n  \"category_label\": \"Categorie\",\n  \"check\": \"Controleren\",\n  \"check_client_id\": \"Client identificator (ClientID of IP-adres)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Controleren of een hostnaam wordt gefilterd.\",\n  \"check_dhcp_servers\": \"Zoek achter DHCP servers\",\n  \"check_dns_record\": \"Selecteer type DNS-record\",\n  \"check_enter_client_id\": \"Voer Client identificator in\",\n  \"check_hostname\": \"Hostnaam of domeinnaam\",\n  \"check_ip\": \"IP-adressen: {{ip}}\",\n  \"check_not_found\": \"Niet in je lijst met filters gevonden\",\n  \"check_reason\": \"Reden: {{reason}}\",\n  \"check_service\": \"Servicenaam: {{service}}\",\n  \"check_title\": \"De filtering controleren\",\n  \"check_updates_btn\": \"Controleren op updates\",\n  \"check_updates_now\": \"Nu controleren op updates\",\n  \"choose_allowlist\": \"Toelatingslijsten selecteren\",\n  \"choose_blocklist\": \"Blokkeringslijsten selecteren\",\n  \"choose_from_list\": \"Uit de lijst selecteren\",\n  \"city\": \"Stad\",\n  \"clear_cache\": \"Cache wissen\",\n  \"click_to_view_queries\": \"Klik om queries te bekijken\",\n  \"client_add\": \"Voeg gebruiker toe\",\n  \"client_added\": \"Gebruiker \\\"{{key}}\\\" met succes toegevoegd\",\n  \"client_blocked\": \"Client \\\"{{ip}}\\\" wordt nu geblokkeerd\",\n  \"client_confirm_block\": \"Weet je zeker dat je client \\\"{{ip}}\\\" wil blokkeren?\",\n  \"client_confirm_delete\": \"Weet je zeker dat je deze gebruiker \\\"{{key}}\\\" wilt verwijderen?\",\n  \"client_confirm_unblock\": \"Weet je zeker dat je client \\\"{{ip}}\\\" niet meer wil blokkeren?\",\n  \"client_deleted\": \"Gebruiker \\\"{{key}}\\\" met succes verwijderd\",\n  \"client_details\": \"Client details\",\n  \"client_edit\": \"Wijzig gebruiker\",\n  \"client_global_settings\": \"Gebruik globale instelling\",\n  \"client_id\": \"Client-ID\",\n  \"client_id_desc\": \"Clients kunnen worden geïdentificeerd door hun Client-ID. <a>Hier</a> vind je meer informatie over het identificeren van clienten.\",\n  \"client_id_placeholder\": \"Client-ID invoeren\",\n  \"client_identifier\": \"Identificeer via\",\n  \"client_identifier_desc\": \"Cliënten kunnen worden geïdentificeerd door hun IP-adres, CIDR, MAC-adres of Client-ID (kan gebruikt worden voor DoT/DoH/DoQ). <0>Hier</0> kan je meer lezen over het identificeren van cliënten.\",\n  \"client_name\": \"Client {{id}}\",\n  \"client_new\": \"Nieuwe gebruiker\",\n  \"client_settings\": \"Cliëntinstellingen\",\n  \"client_table_header\": \"Gebruiker\",\n  \"client_unblocked\": \"Client \\\"{{ip}}\\\" wordt niet meer geblokkeerd\",\n  \"client_updated\": \"Gebruiker \\\"{{key}}\\\" met succes ge-updated\",\n  \"clients_desc\": \"Permanente client-records configureren voor apparaten verbonden met AdGuard Home\",\n  \"clients_not_found\": \"Geen gebruikers gevonden\",\n  \"clients_title\": \"Permanente clients\",\n  \"compact\": \"Compact\",\n  \"config_successfully_saved\": \"Configuratie succesvol opgeslagen\",\n  \"configure\": \"Bewerk\",\n  \"confirm_dns_cache_clear\": \"Weet je zeker dat je de DNS-cache wilt wissen?\",\n  \"confirm_static_ip\": \"AdGuard Home configureert {{ip}} als jouw statische IP-adres. Wil je doorgaan?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"Land\",\n  \"custom_filter_rules\": \"Aangepaste filterregels\",\n  \"custom_filter_rules_hint\": \"Voer één regel op een regel in. U kunt adblock-regels gebruiken of de syntaxis van hosts-bestanden gebruiken.\",\n  \"custom_filtering_rules\": \"Aangepaste filter regels\",\n  \"custom_ip\": \"Aangepast IP\",\n  \"custom_retention_input\": \"Voer retentie in uren in\",\n  \"custom_rotation_input\": \"Voer rotatie in uren in\",\n  \"dashboard\": \"Dashboard\",\n  \"date\": \"Datum\",\n  \"default\": \"Standaard\",\n  \"delete_confirm\": \"Weet je zeker dat je \\\"{{key}}\\\" wilt verwijderen?\",\n  \"delete_table_action\": \"Verwijderen\",\n  \"descr\": \"Beschrijving\",\n  \"details\": \"Details\",\n  \"dhcp_add_static_lease\": \"Voeg statische lease toe\",\n  \"dhcp_config_saved\": \"DHCP configuratie succesvol opgeslagen\",\n  \"dhcp_description\": \"Indien je router geen DHCP instellingen heeft, kan je AdGuard's eigen ingebouwde DHCP server gebruiken.\",\n  \"dhcp_disable\": \"DHCP server uitschakelen\",\n  \"dhcp_dynamic_ip_found\": \"Je systeem gebruikt dynamische IP-adres configuratie voor interface <0>{{interfaceName}}</0>. Om de DHCP server te gebruiken moet er een statisch IP-adres worden ingesteld. Je huidige IP-adres is <0>{{ipAddress}}</0>. AdGuard Home zal automatisch dit IP-adres als statisch IP-adres instellen wanneer je op de knop \\\"DHCP inschakelen\\\" drukt.\",\n  \"dhcp_edit_static_lease\": \"Statische lease bewerken\",\n  \"dhcp_enable\": \"DHCP server inschakelen\",\n  \"dhcp_error\": \"AdGuard Home kon niet bepalen of er een andere actieve DHCP server op het netwerk aanwezig is\",\n  \"dhcp_form_gateway_input\": \"Gateway IP\",\n  \"dhcp_form_lease_input\": \"Lease tijd totaal\",\n  \"dhcp_form_lease_title\": \"DHCP lease tijd (in seconden)\",\n  \"dhcp_form_range_end\": \"Laatste adres\",\n  \"dhcp_form_range_start\": \"Eerste adres\",\n  \"dhcp_form_range_title\": \"Bereik van IP adressen\",\n  \"dhcp_form_subnet_input\": \"Subnet mask\",\n  \"dhcp_found\": \"Actieve DHCP server(s) gevonden op het netwerk. het is NIET veilig om de ingebouwde DHCP server in te schakelen.\",\n  \"dhcp_hardware_address\": \"Hardware adres\",\n  \"dhcp_interface_select\": \"DHCP interface selecteren\",\n  \"dhcp_ip_addresses\": \"IP adressen\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 instellingen\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 instellingen\",\n  \"dhcp_lease_added\": \"Statische uitgifte \\\"{{key}}\\\" met succes toegevoegd\",\n  \"dhcp_lease_deleted\": \"Statische uitgifte \\\"{{key}}\\\" met succes verwijderd\",\n  \"dhcp_lease_updated\": \"Statische lease \\\"{{key}}\\\" succesvol bijgewerkt\",\n  \"dhcp_leases\": \"DHCP lease overzicht\",\n  \"dhcp_leases_not_found\": \"Geen DHCP lease gevonden\",\n  \"dhcp_new_static_lease\": \"Voeg static lease toe\",\n  \"dhcp_not_found\": \"Het is veilig om de ingebouwde DHCP server in te schakelen omdat AdGuard Home geen actieve DHCP servers vond op het netwerk. We raden je echter aan om het handmatig opnieuw te controleren, omdat onze automatische test momenteel geen 100% garantie geeft.\",\n  \"dhcp_reset\": \"Weet je zeker dat je de DHCP configuratie wil resetten?\",\n  \"dhcp_reset_leases\": \"Alle leases resetten\",\n  \"dhcp_reset_leases_confirm\": \"Weet je zeker dat je alle leases wilt resetten?\",\n  \"dhcp_reset_leases_success\": \"DHCP-leases succesvol gereset\",\n  \"dhcp_settings\": \"DHCP instellingen\",\n  \"dhcp_static_ip_error\": \"Om de DHCP server te gebruiken, moet een statisch IP-adres worden ingesteld. AdGuard Home heeft niet kunnen vaststellen of de netwerkinterface is geconfigureerd met een statisch IP-adres. Stel handmatig een statisch IP-adres in.\",\n  \"dhcp_static_leases\": \"DHCP statische lease\",\n  \"dhcp_static_leases_not_found\": \"Geen DHCP static lease gevonden\",\n  \"dhcp_table_expires\": \"Verloopt op\",\n  \"dhcp_table_hostname\": \"Host naam\",\n  \"dhcp_title\": \"DHCP server (experimenteel!)\",\n  \"dhcp_warning\": \"Indien je de ingebouwde DHCP server wilt inschakelen, let dan op dat er geen andere actieve DHCP server aanwezig is in je netwerk. Dit kan de internetverbinding instabiel maken voor sommige apparaten in je netwerk!\",\n  \"disable_for_hours\": \"Voor {{count}} uur\",\n  \"disable_for_hours_plural\": \"Voor {{count}} uren\",\n  \"disable_for_minutes\": \"Voor {{count}} minuut\",\n  \"disable_for_minutes_plural\": \"Voor {{count}} minuten\",\n  \"disable_for_seconds\": \"Voor {{count}} seconde\",\n  \"disable_for_seconds_plural\": \"Voor {{count}} seconden\",\n  \"disable_ipv6\": \"Oplossen IPv6-adressen uitschakelen\",\n  \"disable_ipv6_desc\": \"Alle DNS-query's voor IPv6-adressen (type AAAA) verwijderen en IPv6-hints uit HTTPS-antwoorden verwijderen.\",\n  \"disable_notify_for_hours\": \"Bescherming uitschakelen voor {{count}} uur\",\n  \"disable_notify_for_hours_plural\": \"Bescherming uitschakelen voor {{count}} uren\",\n  \"disable_notify_for_minutes\": \"Bescherming uitschakelen voor {{count}} minuut\",\n  \"disable_notify_for_minutes_plural\": \"Bescherming uitschakelen voor {{count}} minuten\",\n  \"disable_notify_for_seconds\": \"Bescherming uitschakelen voor {{count}} seconde\",\n  \"disable_notify_for_seconds_plural\": \"Bescherming uitschakelen voor {{count}} seconden\",\n  \"disable_notify_until_tomorrow\": \"Bescherming uitschakelen tot morgen\",\n  \"disable_protection\": \"Bescherming uitschakelen\",\n  \"disable_rewrites\": \"Herschrijfregels uitschakelen\",\n  \"disable_until_tomorrow\": \"Tot morgen\",\n  \"disabled\": \"Uitgeschakeld\",\n  \"disabled_dhcp\": \"DHCP server uitschakelen\",\n  \"disabled_filtering_toast\": \"Filters uitgeschakeld\",\n  \"disabled_parental_toast\": \"Uitgeschakeld ouderlijk toezicht\",\n  \"disabled_protection\": \"Bescherming uitgeschakeld\",\n  \"disabled_safe_browsing_toast\": \"Veilig browsen uitgeschakeld\",\n  \"disabled_safe_search_toast\": \"Uitgeschakeld Veilig zoeken\",\n  \"disallow_this_client\": \"Toepassing/systeem niet toelaten\",\n  \"dns_addresses\": \"DNS adressen\",\n  \"dns_allowlists\": \"DNS-toelatingslijsten\",\n  \"dns_allowlists_desc\": \"Domeinen van DNS-toelatingslijsten zijn toegestaan, zelfs als ze op een van de blokkeerlijsten staan.\",\n  \"dns_blocklists\": \"DNS blokkeerlijsten\",\n  \"dns_blocklists_desc\": \"AdGuard Home zal domeinen blokkeren die voorkomen in de blokkeerlijsten.\",\n  \"dns_cache_config\": \"DNS cache configuratie\",\n  \"dns_cache_config_desc\": \"Hier kan de DNS cache geconfigureerd worden\",\n  \"dns_cache_size\": \"DNS-cachegrootte, in bytes\",\n  \"dns_config\": \"DNS-server configuratie\",\n  \"dns_over_https\": \"DNS-via-HTTPS\",\n  \"dns_over_quic\": \"DNS-via-QUIC\",\n  \"dns_over_tls\": \"DNS-via-TLS\",\n  \"dns_privacy\": \"DNS Privacy\",\n  \"dns_providers\": \"hier is een <0>lijst of gekende DNS providers</0> waarvan je kan kiezen.\",\n  \"dns_query\": \"DNS-queries\",\n  \"dns_rewrites\": \"DNS herschrijvingen\",\n  \"dns_settings\": \"DNS instellingen\",\n  \"dns_start\": \"DNS-server aan het opstarten\",\n  \"dns_status_error\": \"Fout bij het controleren van de DNS-server status\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": kon niet worden gebruikt, controleer of je het correct hebt geschreven\",\n  \"dns_test_ok_toast\": \"Opgegeven DNS-servers werken correct\",\n  \"dns_test_parsing_error_toast\": \"Sectie {{section}}: regel {{line}}: kan niet worden gebruikt. Controleer of je het correct hebt geschreven\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" reageert niet op testverzoeken en werkt mogelijk niet goed\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"DNSSEC inschakelen\",\n  \"dnssec_enable_desc\": \"Zet de DNSSEC-vlag aan bij uitgaande DNS-query's en controleer het resultaat (DNSSEC-compatibele resolver is vereist)\",\n  \"domain\": \"Domein\",\n  \"domain_desc\": \"Voer de domeinnaam of wildcard in die herschreven moet worden.\",\n  \"domain_name_table_header\": \"Domein naam\",\n  \"domain_or_client\": \"Domein of cliënt\",\n  \"down\": \"Uitgeschakeld\",\n  \"download_mobileconfig\": \"Configuratiebestand downloaden\",\n  \"download_mobileconfig_doh\": \".mobileconfig voor DNS-via-HTTPS downloaden\",\n  \"download_mobileconfig_dot\": \".mobileconfig voor DNS-via-TLS downloaden\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Toelatingslijst bewerken\",\n  \"edit_blocklist\": \"Blokkeerlijst beheren\",\n  \"edit_table_action\": \"Bewerk\",\n  \"edns_cs_desc\": \"De EDNS Client Subnet-optie (ECS) toevoegen aan upstream-verzoeken en de waarden die door de clients zijn verzonden registreren in het querylogboek.\",\n  \"edns_enable\": \"EDNS client subnet inschakelen\",\n  \"edns_use_custom_ip\": \"Aangepast IP-adres gebruiken voor EDNS\",\n  \"edns_use_custom_ip_desc\": \"Toestaan om aangepast IP-adres voor EDNS te gebruiken\",\n  \"elapsed\": \"Verstreken\",\n  \"empty_response_status\": \"Leeg\",\n  \"enable_protection\": \"Bescherming inschakelen\",\n  \"enable_protection_timer\": \"Bescherming wordt ingeschakeld over {{time}}\",\n  \"enable_rewrites\": \"Herschrijfregels inschakelen\",\n  \"enable_upstream_dns_cache\": \"DNS-caching inschakelen voor de aangepaste upstream-configuratie van deze client\",\n  \"enabled_dhcp\": \"DHCP server inschakelen\",\n  \"enabled_filtering_toast\": \"Filters ingeschakeld\",\n  \"enabled_parental_toast\": \"Ingeschakeld Ouderlijk toezicht\",\n  \"enabled_protection\": \"Bescherming ingeschakeld\",\n  \"enabled_safe_browsing_toast\": \"Veilig browsen ingeschakeld\",\n  \"enabled_save_search_toast\": \"Ingeschakeld Veilig zoeken\",\n  \"enabled_table_header\": \"Ingeschakeld\",\n  \"encryption_certificate_path\": \"Certificaat pad\",\n  \"encryption_certificates\": \"Certificaten\",\n  \"encryption_certificates_desc\": \"Om encryptie te gebruiken, moet u een geldige SSL certificaat voor uw domein opgeven. U kunt een gratis certificaat krijgen op <0> {{link}} </0> of u kunt het kopen bij een van de vertrouwde certificaatautoriteiten.\",\n  \"encryption_certificates_input\": \"Kopieër en plak je PEM-gecodeerde certificaten hier.\",\n  \"encryption_certificates_source_content\": \"Inhoud certificaten plakken\",\n  \"encryption_certificates_source_path\": \"Certificaten bestandspad instellen\",\n  \"encryption_chain_invalid\": \"Certificaatketen is ongeldig\",\n  \"encryption_chain_valid\": \"Certificaatketen is geldig\",\n  \"encryption_config_saved\": \"Versleuteling configuratie opgeslagen\",\n  \"encryption_desc\": \"Encryptie (HTTPS/TLS) ondersteuning voor DNS en admin web interface\",\n  \"encryption_doq\": \"DNS-over-QUIC poort\",\n  \"encryption_doq_desc\": \"Als deze poort is geconfigureerd, zal AdGuard Home een DNS-via-QUIC server gebruiken via deze poort.\",\n  \"encryption_dot\": \"DNS-via-TLS poort\",\n  \"encryption_dot_desc\": \"Indien deze poort is geconfigureerd, zal AdGuard Home gebruik maken van een DNS-via-TLS server via deze poort.\",\n  \"encryption_enable\": \"Activeer encryptie (HTTPS, DNS-via-HTTPS, en DNS-via-TLS)\",\n  \"encryption_enable_desc\": \"Als encryptie is geactiveerd, is de AdGuard Home beheerders interface toegankelijk via HTTPS en de DNS-server zal luisteren naar aanvragen via DNS-via-HTTPS en DNS-via-TLS.\",\n  \"encryption_expire\": \"Verloopt\",\n  \"encryption_hostnames\": \"Hostnamen\",\n  \"encryption_https\": \"HTTPS poort\",\n  \"encryption_https_desc\": \"Als de HTTPS-poort is geconfigureerd, is de AdGuard Home beheerders interface toegankelijk via HTTPS en biedt deze ook DNS-via-HTTPS op de locatie '/ dns-query'.\",\n  \"encryption_issuer\": \"Uitgever\",\n  \"encryption_key\": \"Prive sleutel\",\n  \"encryption_key_input\": \"Kopieër en plak je PEM-gecodeerde prive sleutel voor je certificaat hier.\",\n  \"encryption_key_invalid\": \"Dit is een ongeldige {{type}} privésleutel\",\n  \"encryption_key_source_content\": \"Inhoud privé sleutel plakken\",\n  \"encryption_key_source_path\": \"Bestandspad voor privésleutel instellen\",\n  \"encryption_key_valid\": \"Dit is een geldige {{type}} privésleutel\",\n  \"encryption_plain_dns_desc\": \"Gewone DNS is standaard ingeschakeld. Je kunt het uitschakelen om alle apparaten te dwingen versleutelde DNS te gebruiken. Om dit te doen, moet je ten minste één versleuteld DNS-protocol inschakelen\",\n  \"encryption_plain_dns_enable\": \"Gewone DNS inschakelen\",\n  \"encryption_plain_dns_error\": \"Als je gewone DNS wilt uitschakelen, schakel je ten minste één versleuteld DNS-protocol in\",\n  \"encryption_private_key_path\": \"Privé sleutel pad\",\n  \"encryption_redirect\": \"Herleid automatisch naar HTTPS\",\n  \"encryption_redirect_desc\": \"Indien ingeschakeld, zal AdGuard Home je automatisch herleiden van HTTP naar HTTPS.\",\n  \"encryption_reset\": \"Ben je zeker dat je de encryptie instellingen wil resetten?\",\n  \"encryption_server\": \"Server naam\",\n  \"encryption_server_desc\": \"Indien ingesteld, detecteert AdGuard Home Client-ID's, reageert op DDR-zoekopdrachten en voert aanvullende verbindingsvalidaties uit. Indien niet ingesteld, zijn deze functies uitgeschakeld. Moet overeenkomen met een van de DNS-namen in het certificaat.\",\n  \"encryption_server_enter\": \"Voer domein naam in\",\n  \"encryption_settings\": \"Encryptie instellingen\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Onderwerp\",\n  \"encryption_title\": \"Encryptie\",\n  \"encryption_warning\": \"Waarschuwing\",\n  \"enforce_safe_search\": \"Veilig zoeken gebruiken\",\n  \"enforce_save_search_hint\": \"AdGuard Home dwingt veilig zoeken af in de volgende zoekmachines: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Geforceerd veilig zoeken\",\n  \"enter_cache_size\": \"Cache grootte invoeren (bytes)\",\n  \"enter_cache_ttl_max_override\": \"Maximum TTL invoeren (seconden)\",\n  \"enter_cache_ttl_min_override\": \"Minimum TTL invoeren (seconden)\",\n  \"enter_name_hint\": \"Voeg naam toe\",\n  \"enter_url_or_path_hint\": \"Voer een URL in of het pad van de lijst\",\n  \"enter_valid_allowlist\": \"Voer een geldige URL naar de toelatingslijst in.\",\n  \"enter_valid_blocklist\": \"Voer een geldige URL in voor de blokkeerlijst.\",\n  \"error_details\": \"Fout details\",\n  \"example_comment\": \"! Hier komt een opmerking.\",\n  \"example_comment_hash\": \"# Ook een opmerking.\",\n  \"example_comment_meaning\": \"zomaar een opmerking;\",\n  \"example_meaning_filter_block\": \"blokkeer toegang tot example.org en alle subdomeinen ervan;\",\n  \"example_meaning_filter_whitelist\": \"deblokkeer toegang tot example.org en alle subdomeinen ervan;\",\n  \"example_meaning_host_block\": \"127.0.0.1 voor het domein example.org retourneren (maar niet diens subdomeinen);\",\n  \"example_multiple_upstreams_reserved\": \"meerdere upstreams <0>voor specifieke domeinen</0>;\",\n  \"example_regex_meaning\": \"toegang blokkeren tot de domeinen die overeenkomen met de opgegeven reguliere expressie.\",\n  \"example_rewrite_domain\": \"herschrijf reacties uitsluitend voor deze domeinnaam.\",\n  \"example_rewrite_wildcard\": \"herschrijf reacties voor alle subdomeinen van <0>example.org</0>.\",\n  \"example_upstream_comment\": \"een commentaar.\",\n  \"example_upstream_doh\": \"versleutelde <0>DNS-via-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"versleutelde DNS-over-HTTPS met geforceerde <0>HTTP/3</0> en geen terugval naar HTTP/2 of lager;\",\n  \"example_upstream_doq\": \"versleutelde <0>DNS-via-QUIC</0>;\",\n  \"example_upstream_dot\": \"versleutelde <0>DNS-via-TLS</0>;\",\n  \"example_upstream_regular\": \"standaard DNS (over UDP);\",\n  \"example_upstream_regular_port\": \"standaard DNS (via UDP, met poort);\",\n  \"example_upstream_reserved\": \"een upstream <0>voor specifieke domeinen</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> voor <1>DNSCrypt</1> of <2>DNS-via-HTTPS</2> oplossingen;\",\n  \"example_upstream_tcp\": \"standaard DNS (over TCP);\",\n  \"example_upstream_tcp_hostname\": \"standaard DNS (via TCP, hostnaam);\",\n  \"example_upstream_tcp_port\": \"standaard DNS (via TCP, met poort);\",\n  \"example_upstream_udp\": \"standaard DNS (via UDP, hostnaam);\",\n  \"examples_title\": \"Voorbeelden\",\n  \"fallback_dns_desc\": \"Lijst met DNS-back-up-noodservers die worden gebruikt wanneer upstream DNS-servers niet reageren. De syntaxis is hetzelfde als in het veld hoofdstroomopwaarts hierboven.\",\n  \"fallback_dns_placeholder\": \"Voer één DNS-back-upserver per regel in\",\n  \"fallback_dns_title\": \"Back-up DNS-servers\",\n  \"faq\": \"Veel gestelde vragen\",\n  \"fastest_addr\": \"Snelste IP adres\",\n  \"fastest_addr_desc\": \"Wacht op reacties van <b>alle</b> DNS-servers, meet de TCP-verbindingssnelheid voor elke server en retourneer het IP-adres van de server met de hoogste verbindingssnelheid.<br/>Deze modus kan DNS-query's aanzienlijk vertragen als een of meer upstream-servers niet reageren. Zorg ervoor dat je upstream-servers stabiel zijn en dat je upstream-time-out laag is.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"De lijst is succesvol toegevoegd\",\n  \"filter_allowlist\": \"WAARSCHUWING: Deze actie zal ook de regel \\\"{{disallowed_rule}}\\\" uitsluiten van de lijst met toegestane clients.\",\n  \"filter_category_general\": \"Algemeen\",\n  \"filter_category_general_desc\": \"Lijsten die volgers en advertenties op de meeste apparaten blokkeert\",\n  \"filter_category_other\": \"Overig\",\n  \"filter_category_other_desc\": \"Overige blokkeerlijsten\",\n  \"filter_category_regional\": \"Regionaal\",\n  \"filter_category_regional_desc\": \"Lijsten die focussen op regionale ads en tracking servers\",\n  \"filter_category_security\": \"Beveiliging\",\n  \"filter_category_security_desc\": \"Lijsten gespecialiseerd in het blokkeren van malware, phising of scamdomeinen\",\n  \"filter_removed_successfully\": \"De lijst is succesvol verwijderd\",\n  \"filter_updated\": \"De lijst is succesvol geüpdatet\",\n  \"filtered\": \"Gefilterd\",\n  \"filtered_custom_rules\": \"Gefilterd door aangepaste filterregels\",\n  \"filtering_rules_learn_more\": \"<0>Meer informatie</0> over het maken van je eigen host lijsten.\",\n  \"filters\": \"Filters\",\n  \"filters_and_hosts_hint\": \"AdGuard Home kan overweg met basic adblock regels en hosts bestanden syntaxis.\",\n  \"filters_block_toggle_hint\": \"Je kan blokkeringsregels toevoegen in de <a>Filters</a> instellingen.\",\n  \"filters_configuration\": \"Filters instellingen\",\n  \"filters_enable\": \"Filters inschakelen\",\n  \"filters_interval\": \"Filter update frequentie\",\n  \"fix\": \"Los op\",\n  \"for_last_days\": \"sinds de laatste {{count}} dagen\",\n  \"for_last_days_plural\": \"sinds de laatste {{count}} dagen\",\n  \"for_last_hours\": \"voor het afgelopen {{count}} uur\",\n  \"for_last_hours_plural\": \"voor de afgelopen {{count}} uren\",\n  \"forgot_password\": \"Wachtwoord vergeten?\",\n  \"forgot_password_desc\": \"Volg <0>deze stappen</0> om een nieuw wachtwoord voor uw gebruikersaccount te maken.\",\n  \"form_add_id\": \"ID toevoegen\",\n  \"form_answer\": \"Vul IP adres of domeinnaam in\",\n  \"form_client_name\": \"Vul gebruikersnaam in\",\n  \"form_domain\": \"Vul domein of wildcard in\",\n  \"form_enter_blocked_response_ttl\": \"Voer geblokkeerd antwoord TTL in (seconden)\",\n  \"form_enter_host\": \"Voer een hostnaam in\",\n  \"form_enter_hostname\": \"Vul hostnaam in\",\n  \"form_enter_id\": \"ID invoeren\",\n  \"form_enter_ip\": \"Vul IP in\",\n  \"form_enter_mac\": \"Vul MAC in\",\n  \"form_enter_rate_limit\": \"Voer ratio limiet in\",\n  \"form_enter_rate_limit_subnet_len\": \"Voer de lengte van het subnetvoorvoegsel in voor snelheidsbeperking\",\n  \"form_enter_subnet_ip\": \"Voer een IP-adres in voor het subnet “{{cidr}}”\",\n  \"form_enter_upstream_timeout\": \"Voer de time-outduur van de upstream-server in seconden in\",\n  \"form_error_answer_format\": \"Ongeldige opmaak antwoord\",\n  \"form_error_client_id_format\": \"Client-ID mag alleen cijfers, kleine letters en koppeltekens bevatten\",\n  \"form_error_domain_format\": \"Ongeldige opmaak domein\",\n  \"form_error_equal\": \"Mag niet gelijk zijn\",\n  \"form_error_gateway_ip\": \"Lease kan niet het IP-adres van de gateway hebben\",\n  \"form_error_ip4_format\": \"Ongeldig IPv4-adres\",\n  \"form_error_ip4_gateway_format\": \"Ongeldig IPv4-adres van de gateway\",\n  \"form_error_ip6_format\": \"Ongeldig IPv6-adres\",\n  \"form_error_ip_format\": \"Ongeldig IP-adres\",\n  \"form_error_mac_format\": \"Ongeldig MAC-adres\",\n  \"form_error_password\": \"Wachtwoord komt niet overeen\",\n  \"form_error_password_length\": \"Wachtwoord moet {{min}} tot {{max}} tekens lang zijn\",\n  \"form_error_port\": \"Geldig poortnummer invoeren\",\n  \"form_error_port_range\": \"Poortnummer invoeren tussen 80 en 65535\",\n  \"form_error_port_unsafe\": \"Onveilige poort\",\n  \"form_error_positive\": \"Moet groter zijn dan 0\",\n  \"form_error_required\": \"Vereist veld\",\n  \"form_error_server_name\": \"Ongeldige servernaam\",\n  \"form_error_subnet\": \"Subnet “{{cidr}}” bevat niet het IP-adres “{{ip}}”\",\n  \"form_error_url_format\": \"Ongeldig URL-opmaak\",\n  \"form_error_url_or_path_format\": \"Ongeldig URL of pad van de lijst\",\n  \"form_select_tags\": \"Client tags selecteren\",\n  \"found_in_known_domain_db\": \"Gevonden in de bekende domeingegevensbank.\",\n  \"friday\": \"vrijdag\",\n  \"friday_short\": \"vr\",\n  \"gateway_or_subnet_invalid\": \"Ongeldig subnetmasker\",\n  \"general_settings\": \"Algemene instellingen\",\n  \"general_statistics\": \"Algemene statistieken\",\n  \"get_started\": \"Beginnen\",\n  \"greater_range_start_error\": \"Moet groter zijn dan begin reeks\",\n  \"homepage\": \"Startpagina\",\n  \"host_whitelisted\": \"De host staat op de toelatingslijst\",\n  \"ignore_domains\": \"Genegeerde domeinen (gescheiden door nieuwe regel)\",\n  \"ignore_domains_desc_query\": \"Zoekopdrachten die aan deze regels voldoen, worden niet naar het zoeklogboek geschreven\",\n  \"ignore_domains_desc_stats\": \"Zoekopdrachten die aan deze regels voldoen, worden niet naar de statistieken geschreven\",\n  \"ignore_domains_title\": \"Genegeerde domeinen\",\n  \"ignore_query_log\": \"Deze client negeren in het aanvragenlogboek\",\n  \"ignore_statistics\": \"Deze client negeren in de statistieken\",\n  \"install_auth_confirm\": \"Bevestig wachtwoord\",\n  \"install_auth_desc\": \"Wachtwoordverificatie voor je AdGuard Home-beheerderswebinterface moet worden geconfigureerd. Zelfs als AdGuard Home alleen toegankelijk is in je lokale netwerk, is het nog steeds belangrijk om het te beschermen tegen onbeperkte toegang.\",\n  \"install_auth_password\": \"Wachtwoord\",\n  \"install_auth_password_enter\": \"Voer wachtwoord in\",\n  \"install_auth_title\": \"Authenticatie\",\n  \"install_auth_username\": \"Gebruikersnaam\",\n  \"install_auth_username_enter\": \"Voer gebruikersnaam in\",\n  \"install_devices_address\": \"AdGuard Home DNS-server luistert naar de volgende adressen\",\n  \"install_devices_android_list_1\": \"Tik op het startscherm van het Android-menu op Instellingen.\",\n  \"install_devices_android_list_2\": \"Tik op wifi in het menu. Het scherm met alle beschikbare netwerken wordt getoond (het is niet mogelijk om een aangepaste DNS in te stellen voor een mobiele verbinding).\",\n  \"install_devices_android_list_3\": \"Druk lang op het netwerk waarmee je bent verbonden en tik op Netwerk instellingen aanpassen.\",\n  \"install_devices_android_list_4\": \"Op sommige apparaten moet u het vakje aanvinken voor Geavanceerd om verdere instellingen te bekijken. Om uw Android DNS-instellingen aan te passen, moet u de IP-instellingen wijzigen van DHCP in Statisch.\",\n  \"install_devices_android_list_5\": \"Wijzig de DNS 1-waarden en DNS 2-waarden in jouw AdGuard Home server adressen.\",\n  \"install_devices_desc\": \"Om AdGuard Home te laten werken, moet u uw apparaten configureren om deze te gebruiken.\",\n  \"install_devices_ios_list_1\": \"Tik op het startscherm op Instellingen.\",\n  \"install_devices_ios_list_2\": \"Kies Wi-Fi in het linkermenu (DNS kan niet worden geconfigureerd voor mobiele netwerken).\",\n  \"install_devices_ios_list_3\": \"Tik op de naam van het momenteel actieve netwerk.\",\n  \"install_devices_ios_list_4\": \"Voer in het DNS veld jouw AdGuard Home server adressen in.\",\n  \"install_devices_macos_list_1\": \"Klik op het Apple-pictogram en ga naar Systeemvoorkeuren.\",\n  \"install_devices_macos_list_2\": \"Klik op Netwerk.\",\n  \"install_devices_macos_list_3\": \"Selecteer de eerste verbinding in jouw lijst en klik op Geavanceerd.\",\n  \"install_devices_macos_list_4\": \"Selecteer het tabblad DNS en voer jouw AdGuard Home server adressen in.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Deze installatie zal automatisch alle apparaten die op je thuisrouter zijn aangesloten beschermen en je hoeft ze niet allemaal handmatig te configureren.\",\n  \"install_devices_router_list_1\": \"Open de instellingen van jouw router. Meestal kan je deze vanuit jouw browser openen via een URL, zoals http://192.168.0.1/ of http://192.168.1.1/. Mogelijk wordt er gevraagd om een wachtwoord in te voeren. Als je het niet meer weet, kan je het wachtwoord vaak opnieuw instellen door op een knop op de router zelf te drukken, maar weet wel dat je dan de volledige routerconfiguratie kwijt bent (terug naar fabrieksinstellingen). Voor sommige routers is een specifieke toepassing/app vereist, die in dat geval op jouw computer/smartphone/tablet moet geïnstalleerd zijn.\",\n  \"install_devices_router_list_2\": \"Zoek de DHCP/DNS-instellingen. Zoek naar de DNS-letters naast een veld dat twee of drie reeksen nummers toestaat, elk verdeeld in vier groepen van één tot drie cijfers.\",\n  \"install_devices_router_list_3\": \"Voer je AdGuard Home server adressen daar in.\",\n  \"install_devices_router_list_4\": \"Je kan een DNS-server niet instellen op sommige routers. In dat geval kan het een oplossing zijn om AdGuard Home te definiëren als een <0>DHCP-server</0>. Je kan ook in de handleiding van je router kijken hoe je een DNS-server aanpast.\",\n  \"install_devices_title\": \"Configureer uw apparaten\",\n  \"install_devices_windows_list_1\": \"Open het Configuratiescherm via het menu Start of Windows zoeken.\",\n  \"install_devices_windows_list_2\": \"Ga naar de categorie Netwerk en Internet en vervolgens naar Netwerkcentrum.\",\n  \"install_devices_windows_list_3\": \"Aan de linkerkant van het scherm, klik op \\\"Adapter-instellingen wijzigen\\\".\",\n  \"install_devices_windows_list_4\": \"Klik met de rechtermuisknop op jouw actieve verbinding en kies Eigenschappen.\",\n  \"install_devices_windows_list_5\": \"Zoek \\\"Internet Protocol versie 4 (TCP/IPv4)\\\" (of, voor IPv6, \\\"Internet Protocol versie 6 (TCP/IPv6)\\\") in de lijst, selecteer het en klik vervolgens opnieuw op Eigenschappen.\",\n  \"install_devices_windows_list_6\": \"Kies \\\"Gebruik de volgende DNS-serveradressen\\\" en voer jouw AdGuard Home serveradressen in.\",\n  \"install_saved\": \"Succesvol opgeslagen\",\n  \"install_settings_all_interfaces\": \"Alle interfaces\",\n  \"install_settings_dns\": \"DNS-server\",\n  \"install_settings_dns_desc\": \"Je moet jouw apparaten of router configureren om de DNS-server te gebruiken op de volgende adressen:\",\n  \"install_settings_interface_link\": \"De webinterface van AdGuard Home admin is beschikbaar op de volgende adressen:\",\n  \"install_settings_listen\": \"Luister interface\",\n  \"install_settings_port\": \"Poort\",\n  \"install_settings_title\": \"Admin webinterface\",\n  \"install_static_configure\": \"AdGuard Home heeft vastgesteld dat er een dynamisch IP-adres <0>{{ip}}</0> wordt gebruikt. Wil je dit als je statische adres gebruiken?\",\n  \"install_static_error\": \"AdGuard Home kan dit niet automatisch configureren op deze netwerkinterface. Zoek een instructie om dit handmatig te doen.\",\n  \"install_static_ok\": \"Goed nieuws! Het statische IP-adres was al geconfigureerd\",\n  \"install_step\": \"Stap\",\n  \"install_submit_desc\": \"De installatieprocedure is voltooid en je bent klaar om AdGuard Home te gebruiken.\",\n  \"install_submit_title\": \"Gefeliciteerd!\",\n  \"install_welcome_desc\": \"AdGuard Home is een netwerk DNS-server die advertenties en trackers blokkeert. Het doel is om jou controle te geven over je gehele netwerk en al je apparaten, en er hoeft geen client-side programma te worden gebruikt.\",\n  \"install_welcome_title\": \"Welkom bij AdGuard Home!\",\n  \"interval_24_hour\": \"24 uur\",\n  \"interval_6_hour\": \"6 uren\",\n  \"interval_days\": \"{{count}} dagen\",\n  \"interval_days_plural\": \"{{count}} dagen\",\n  \"interval_hours\": \"{{count}} uur\",\n  \"interval_hours_plural\": \"{{count}} uren\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP adres\",\n  \"known_tracker\": \"Bekende volger\",\n  \"last_rule_in_allowlist\": \"Kan deze client niet weigeren omdat het uitsluiten van de regel \\\"{{disallowed_rule}}\\\" de lijst \\\"Toegestane clients\\\" zal UITSCHAKELEN.\",\n  \"last_time_updated_table_header\": \"Laatste update\",\n  \"list_confirm_delete\": \"Weet je zeker dat je deze lijst wilt verwijderen?\",\n  \"list_label\": \"Lijst\",\n  \"list_updated\": \"{{count}} lijst geüpdatet\",\n  \"list_updated_plural\": \"{{count}} lijsten geüpdatet\",\n  \"list_url_table_header\": \"URL lijst\",\n  \"load_balancing\": \"Volume balanceren\",\n  \"load_balancing_desc\": \"Voer zoekopdrachten uit op één upstream-server tegelijk.<br/>AdGuard Home gebruikt een gewogen willekeurig algoritme om servers te selecteren met het laagste aantal mislukte zoekopdrachten en de laagste gemiddelde opzoektijd.\",\n  \"loading_table_status\": \"Laden...\",\n  \"local_ptr_default_resolver\": \"Standaard gebruikt AdGuard Home de volgende omgekeerde DNS-resolvers: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS-servers die door AdGuard Home worden gebruikt voor privé PTR-, SOA- en NS-verzoeken. Een verzoek wordt als privé beschouwd als het vraagt om een ARPA-domein dat een subnet binnen privé-IP-bereiken bevat (zoals \\\"192.168.12.34\\\") en afkomstig is van een client met een privé-IP-adres. Indien niet ingesteld, zullen de standaard DNS-resolvers van je besturingssysteem worden gebruikt, behalve de AdGuard Home IP-adressen.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home kon voor dit systeem geen geschikte private omgekeerde DNS-resolvers bepalen.\",\n  \"local_ptr_placeholder\": \"Voer één IP-adres per regel in\",\n  \"local_ptr_title\": \"Private omgekeerde DNS-servers\",\n  \"location\": \"Locatie\",\n  \"log_and_stats_section_label\": \"Aanvragenlogboek en statistieken\",\n  \"lower_range_start_error\": \"Moet lager zijn dan begin reeks\",\n  \"main_settings\": \"Algemene instellingen\",\n  \"make_static\": \"Statisch maken\",\n  \"manual_update\": \"<a>Volg deze stappen</a> om handmatig bij te werken.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"maandag\",\n  \"monday_short\": \"ma\",\n  \"name\": \"Naam\",\n  \"name_table_header\": \"Naam\",\n  \"netname\": \"Netwerk naam\",\n  \"network\": \"Netwerk\",\n  \"new_allowlist\": \"Nieuwe toelatingslijst\",\n  \"new_blocklist\": \"Nieuwe blokkeerlijst\",\n  \"next\": \"Volgende\",\n  \"next_btn\": \"Volgende\",\n  \"no_blocklist_added\": \"Geen blokkeerlijsten toegevoegd\",\n  \"no_clients_found\": \"Geen gebruikers gevonden\",\n  \"no_domains_found\": \"Geen domeinen gevonden\",\n  \"no_logs_found\": \"Geen logboeken gevonden\",\n  \"no_servers_specified\": \"Geen servers gespecificeerd\",\n  \"no_upstreams_data_found\": \"Geen upstreams-gegevens gevonden\",\n  \"no_whitelist_added\": \"Geen toelatingslijsten toegevoegd\",\n  \"nothing_found\": \"Niets gevonden\",\n  \"null_ip\": \"Nul IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Aantal geblokkeerde DNS aanvragen door advertentie blokkering en hosts blokkeerlijsten\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Aantal geblokkeerde 18+ websites\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Aantal geblokkeerde DNS aanvragen door AdGuard browsing beveiligingsmodule\",\n  \"number_of_dns_query_days\": \"Aantal verwerkte DNS aanvragen van de laatste {{count}} dag\",\n  \"number_of_dns_query_days_plural\": \"Aantal verwerkte DNS aanvragen van de laatste {{count}} dagen\",\n  \"number_of_dns_query_hours\": \"Het aantal DNS-verzoeken dat het afgelopen {{count}} uur is verwerkt\",\n  \"number_of_dns_query_hours_plural\": \"Het aantal DNS-verzoeken dat de afgelopen {{count}} uren is verwerkt\",\n  \"number_of_dns_query_to_safe_search\": \"Aantal DNS aanvragen in zoekmachines dmv geforceerd veilig zoeken\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"Uit\",\n  \"on\": \"Aan\",\n  \"open_dashboard\": \"Open Dashboard\",\n  \"orgname\": \"Naam organisatie\",\n  \"original_response\": \"Oorspronkelijke reactie\",\n  \"out_of_range_error\": \"Moet buiten bereik zijn \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Pagina\",\n  \"parallel_requests\": \"Parallelle verzoeken\",\n  \"parental_control\": \"Ouderlijk toezicht\",\n  \"password_label\": \"Wachtwoord\",\n  \"password_placeholder\": \"Voer wachtwoord in\",\n  \"plain_dns\": \"Gewone DNS\",\n  \"port_53_faq_link\": \"Poort 53 wordt vaak gebruikt door services als DNSStubListener- of de systeem DNS-resolver. Lees a.u.b. <0>deze instructie</0> hoe dit is op te lossen.\",\n  \"previous_btn\": \"Vorige\",\n  \"privacy_policy\": \"Privacybeleid\",\n  \"processing_update\": \"Even geduld, AdGuard Home wordt bijgewerkt\",\n  \"protection_section_label\": \"Bescherming\",\n  \"protocol\": \"Protocol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Query log\",\n  \"query_log_clear\": \"Querylogboeken wissen\",\n  \"query_log_cleared\": \"Het querylogboek is succesvol gewist\",\n  \"query_log_configuration\": \"Logbestanden instellingen\",\n  \"query_log_confirm_clear\": \"Weet je zeker dat je het hele querylogboek wilt wissen?\",\n  \"query_log_disabled\": \"Het query logboek is uitgeschakeld en kan worden geconfigureerd in de <0>instellingen</0>\",\n  \"query_log_enable\": \"Log bestanden inschakelen\",\n  \"query_log_filtered\": \"Gefilterd door {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Query logs rotatie\",\n  \"query_log_retention_confirm\": \"Weet u zeker dat u de rotatie van het querylogboek wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren\",\n  \"query_log_strict_search\": \"Gebruik dubbele aanhalingstekens voor strikt zoeken\",\n  \"query_log_updated\": \"Het query logboek is succesvol bijgewerkt\",\n  \"rate_limit\": \"Ratio limiet\",\n  \"rate_limit_desc\": \"Het aantal verzoeken per seconde toegelaten per toestel. 0 betekent onbeperkt.\",\n  \"rate_limit_subnet_len_ipv4\": \"Lengte subnetvoorvoegsel voor IPv4-adressen\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Lengte subnetvoorvoegsel voor IPv4-adressen die worden gebruikt voor snelheidsbeperking. De standaardwaarde is 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"De lengte van het IPv4-subnetvoorvoegsel moet tussen 0 en 32 liggen\",\n  \"rate_limit_subnet_len_ipv6\": \"Lengte subnetvoorvoegsel voor IPv6-adressen\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Lengte subnetvoorvoegsel voor IPv6-adressen die worden gebruikt voor snelheidsbeperking. De standaardwaarde is 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"De lengte van het IPv6-subnetvoorvoegsel moet tussen 0 en 128 liggen\",\n  \"rate_limit_whitelist\": \"Toelatingslijst voor snelheidsbeperking\",\n  \"rate_limit_whitelist_desc\": \"IP-adressen uitgesloten van snelheidsbeperking\",\n  \"rate_limit_whitelist_placeholder\": \"Voer één IP-adres per regel in\",\n  \"refresh_btn\": \"Verversen\",\n  \"refresh_statics\": \"Ververs statistieken\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Rapporteer een probleem\",\n  \"request_details\": \"Verzoekdetails\",\n  \"request_table_header\": \"Verzoek\",\n  \"requests_count\": \"Verzoek teller\",\n  \"reset_settings\": \"Reset Instellingen\",\n  \"resolve_clients_desc\": \"Indien ingeschakeld, zal AdGuard Home proberen om IP-adressen van apparaten te converteren in hun hostnamen door PTR-verzoeken te sturen naar overeenkomstige resolvers (privé-DNS-servers voor lokale apparaten, upstream-server voor apparaten met een openbaar IP-adres).\",\n  \"resolve_clients_title\": \"Omzetten van hostnamen van clients inschakelen\",\n  \"response_code\": \"Reactiecode\",\n  \"response_details\": \"Antwoorddetails\",\n  \"response_table_header\": \"Antwoord\",\n  \"response_time\": \"Responsetijd\",\n  \"rewrite_A\": \"<0>A</0>: speciale waarde, <0>A</0> records uit de upstream bewaren\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: speciale waarde, <0>AAAA</0> records uit de upstream bewaren\",\n  \"rewrite_add\": \"DNS-herschrijving toevoegen\",\n  \"rewrite_added\": \"DNS-herschrijving voor \\\"{{key}}\\\" met succes toegevoegd\",\n  \"rewrite_applied\": \"Herschrijf regel toegepast\",\n  \"rewrite_confirm_delete\": \"Weet je zeker dat je DNS-herschrijving \\\"{{key}}\\\" wilt verwijderen?\",\n  \"rewrite_deleted\": \"DNS-herschrijving voor \\\"{{key}}\\\" met succes verwijderd\",\n  \"rewrite_desc\": \"Hiermee kunt u eenvoudig aangepaste DNS-antwoorden configureren voor een specifieke domeinnaam.\",\n  \"rewrite_domain_name\": \"Domeinnaam: een CNAME record toevoegen\",\n  \"rewrite_edit\": \"DNS-herschrijven bewerken\",\n  \"rewrite_hosts_applied\": \"Geherdefinieerd door de filterregel van de host\",\n  \"rewrite_ip_address\": \"IP adres: gebruik dit IP in een A of AAAA antwoord\",\n  \"rewrite_not_found\": \"Geen DNS-herschrijving gevonden\",\n  \"rewrite_settings_updated\": \"Instellingen voor DNS-herschrijven bijgewerkt\",\n  \"rewrite_updated\": \"DNS-herschrijven succesvol bijgewerkt\",\n  \"rewrites_disabled_table_header\": \"Herschrijvingen zijn uitgeschakeld\",\n  \"rewrites_enabled_table_header\": \"Herschrijvingen zijn ingeschakeld\",\n  \"rewritten\": \"Herschreven\",\n  \"rows_table_footer_text\": \"rijen\",\n  \"rule_added_to_custom_filtering_toast\": \"Regel toegevoegd aan de aangepaste filterregels: {{rule}}\",\n  \"rule_label\": \"Regel(s)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regel verwijderd uit de aangepaste filterregels: {{rule}}\",\n  \"rules_count_table_header\": \"Aantal regels\",\n  \"safe_browsing\": \"Veilig browsen\",\n  \"safe_search\": \"Veilig zoeken\",\n  \"saturday\": \"zaterdag\",\n  \"saturday_short\": \"za\",\n  \"save_btn\": \"Opslaan\",\n  \"save_config\": \"Configuratie opslaan\",\n  \"schedule_add\": \"Schema toevoegen\",\n  \"schedule_current_timezone\": \"Huidige tijdzone: {{value}}\",\n  \"schedule_desc\": \"Inactiviteitsperioden instellen voor geblokkeerde services\",\n  \"schedule_edit\": \"Schema bewerken\",\n  \"schedule_from\": \"Van\",\n  \"schedule_invalid_select\": \"Begintijd moet vóór eindtijd liggen\",\n  \"schedule_modal_description\": \"Dit schema vervangt alle bestaande schema's voor dezelfde dag van de week. Elke dag van de week kan slechts één inactiviteitsperiode hebben.\",\n  \"schedule_modal_time_off\": \"Geen serviceblokkering:\",\n  \"schedule_new\": \"Nieuw schema\",\n  \"schedule_remove\": \"Schema verwijderen\",\n  \"schedule_save\": \"Schema opslaan\",\n  \"schedule_select_days\": \"Selecteer dagen\",\n  \"schedule_services\": \"Serviceblokkering onderbreken\",\n  \"schedule_services_desc\": \"Het pauzeschema van het serviceblokkeringsfilter configureren\",\n  \"schedule_services_desc_client\": \"Het pauzeschema van het serviceblokkeringsfilter voor deze client configureren\",\n  \"schedule_time_all_day\": \"De hele dag\",\n  \"schedule_timezone\": \"Selecteer een tijdzone\",\n  \"schedule_to\": \"tot\",\n  \"served_from_cache_label\": \"Geleverd vanuit cache\",\n  \"service_name\": \"Naam service\",\n  \"set_static_ip\": \"Stel een statisch IP-adres in\",\n  \"settings\": \"Instellingen\",\n  \"settings_custom\": \"Aangepast\",\n  \"settings_global\": \"Globaal\",\n  \"setup_config_to_enable_dhcp_server\": \"Configuratie instellen om DHCP-server in te schakelen\",\n  \"setup_dns_notice\": \"Om <1>DNS-via-HTTPS</1> of <1>DNS-via-TLS</1> te gebruiken, moet je <0>Versleuteling configureren</0> in de AdGuard Home instellingen.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-via-TLS:</0> Gebruik <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-via-HTTPS:</0> Gebruik <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_3\": \"<0>Hou er rekening mee dat het beveiligde DNS protocol alleen beschikbaar is voor Android 9. U moet dus extra software installeren voor andere besturingssystemen.</0><0>Hier is een lijst van te gebruiken software.</0>\",\n  \"setup_dns_privacy_4\": \"Op een iOS 14 of macOS Big Sur apparaat kan je een speciaal '.mobileconfig'-bestand downloaden dat <highlight>DNS-via-HTTPS</highlight> of <highlight>DNS-via-TLS</highlight> servers aan de DNS-instellingen toevoegt.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 ondersteunt systeem-eigen DNS-via-TLS. Om het te configureren, ga naar Instellingen → Netwerk & internet → Geavanceerd → Privé-DNS en voer daar je domeinnaam in.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard voor Android</0>ondersteunt<1>DNS-via-HTTPS </1>en<1>DNS-via-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0> Intra </0> voegt <1> DNS-via-HTTPS</1> ondersteuning toe aan Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS en macOS configuratie\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> ondersteunt <1> DNS-via-HTTPS </1>, maar om het te configureren op jouw eigen server moet er een <2> DNS-stempel </2> gegenereerd worden.\",\n  \"setup_dns_privacy_ios_2\": \"<0> AdGuard voor iOS </0> ondersteunt de instellingen <1> DNS-via-HTTPS </1> en <1> DNS-via-TLS </1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home kan op elk platform een ​​veilige DNS-client zijn.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> ondersteunt alle bekende beveiligde DNS-protocollen.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> ondersteunt <1>DNS-via-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> ondersteunt <1>DNS-via-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"U vindt meer implementaties <0> hier </0> en <1> hier </1>.\",\n  \"setup_dns_privacy_other_title\": \"Overig gebruik\",\n  \"setup_guide\": \"Installatie gids\",\n  \"show_all_filter_type\": \"Alles weergeven\",\n  \"show_blocked_responses\": \"Geblokkeerd\",\n  \"show_filtered_type\": \"Gefilterde weergeven\",\n  \"show_processed_responses\": \"Verwerkt\",\n  \"show_whitelisted_responses\": \"Op toelatingslijst\",\n  \"sign_in\": \"Aanmelden\",\n  \"sign_out\": \"Afmelden\",\n  \"source_label\": \"Bron\",\n  \"static_ip\": \"Statisch IP-adres\",\n  \"static_ip_desc\": \"AdGuard Home is een server en heeft daarom een statisch IP-adres nodig om goed te kunnen functioneren, anders kan uw router op een bepaald moment een ander IP-adres aan dit apparaat toewijzen.\",\n  \"statistics_clear\": \"Statistieken wissen\",\n  \"statistics_clear_confirm\": \"Alle statistieken werkelijk wissen?\",\n  \"statistics_cleared\": \"Statistieken succesvol gewist\",\n  \"statistics_configuration\": \"Statistieken configuratie\",\n  \"statistics_enable\": \"Statistieken inschakelen\",\n  \"statistics_retention\": \"Statistieken retentie\",\n  \"statistics_retention_confirm\": \"Weet u zeker dat u de bewaartermijn van de statistieken wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren\",\n  \"statistics_retention_desc\": \"Als je de intervalwaarde vermindert, zullen sommige gegevens verloren gaan\",\n  \"stats_adult\": \"Geblokkeerde 18+ websites\",\n  \"stats_disabled\": \"Statistieken zijn uitgeschakeld. Je kunt ze inschakelen op de <0>instellingen pagina</0>.\",\n  \"stats_disabled_short\": \"Statistieken zijn uitgeschakeld\",\n  \"stats_malware_phishing\": \"Geblokkeerde malware/phishing\",\n  \"stats_params\": \"Statistieken configuratie\",\n  \"stats_query_domain\": \"Meest bezochte domeinen\",\n  \"subnet_error\": \"Adressen moeten in één subnet vallen\",\n  \"sunday\": \"zondag\",\n  \"sunday_short\": \"zo\",\n  \"system_host_files\": \"Systeem host-bestanden\",\n  \"table_client\": \"Gebruiker\",\n  \"table_name\": \"Naam\",\n  \"tags_desc\": \"Je kunt labels selecteren die overeenkomen met de client. Labels kunnen worden opgenomen in de filterregels om ze \\n nauwkeuriger toe te passen. <0>Meer informatie</0>.\",\n  \"tags_title\": \"Labels\",\n  \"test_upstream_btn\": \"Test upstream\",\n  \"theme_auto\": \"Automatisch\",\n  \"theme_auto_desc\": \"Automatisch (op basis van het kleurenschema van jouw apparaat)\",\n  \"theme_dark\": \"Donker\",\n  \"theme_dark_desc\": \"Donker thema\",\n  \"theme_light\": \"Licht\",\n  \"theme_light_desc\": \"Licht thema\",\n  \"thursday\": \"donderdag\",\n  \"thursday_short\": \"do\",\n  \"time_table_header\": \"Tijd\",\n  \"top_blocked_domains\": \"Top geblokkeerde domeinen\",\n  \"top_clients\": \"Top gebruikers\",\n  \"top_upstreams\": \"Top upstreams\",\n  \"topline_expired_certificate\": \"Jouw SSL-certificaat is vervallen. Werk de <0>encryptie-instellingen</0> bij.\",\n  \"topline_expiring_certificate\": \"Jouw SSL-certificaat vervalt binnenkort. Werk de <0>encryptie-instellingen</0> bij.\",\n  \"tracker_source\": \"Bron volger\",\n  \"try_again\": \"Probeer opnieuw\",\n  \"ttl_cache_validation\": \"Minimale waarde TTL-cache moet kleiner dan of gelijk zijn aan de maximale waarde\",\n  \"tuesday\": \"dinsdag\",\n  \"tuesday_short\": \"di\",\n  \"type_table_header\": \"Type\",\n  \"unavailable_dhcp\": \"DHCP is niet beschikbaar\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home kan geen DHCP-server draaien op uw OS\",\n  \"unblock\": \"Deblokkeren\",\n  \"unblock_all\": \"Alles deblokkeren\",\n  \"unblock_for_this_client_only\": \"Alleen voor deze cliënt deblokkeren\",\n  \"unknown_filter\": \"Onbekend filter {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home{{version}} is nu beschikbaar! <0>klik hier</0> voor meer info.\",\n  \"update_failed\": \"Automatisch bijwerken is mislukt. <a>Volg deze stappen</a> om handmatig bij te werken.\",\n  \"update_now\": \"Nu bijwerken\",\n  \"updated_custom_filtering_toast\": \"Aangepaste regels succesvol opgeslagen\",\n  \"updated_save_search_toast\": \"Safe Search-instellingen bijgewerkt\",\n  \"updated_upstream_dns_toast\": \"Upstream-servers succesvol opgeslagen\",\n  \"updates_checked\": \"Een nieuwe versie van AdGuard Home is beschikbaar\\n\",\n  \"updates_version_equal\": \"AdGuard Home is actueel\",\n  \"upstream\": \"Upstream\",\n  \"upstream_dns\": \"Upstream DNS-servers\",\n  \"upstream_dns_cache_configuration\": \"Upstream DNS-cacheconfiguratie\",\n  \"upstream_dns_client_desc\": \"Indien je dit veld leeglaat zal AdGuard Home de servers welke zijn ingesteld in de <0>DNS instellingen</0> gebruiken.\",\n  \"upstream_dns_configured_in_file\": \"Geconfigureerd in {{path}}\",\n  \"upstream_dns_help\": \"Een server-adres per regel invoeren. <a>Meer informatie</a> over het configureren van upstream DNS-servers.\",\n  \"upstream_parallel\": \"Parallelle verzoeken gebruiken om te versnellen door gelijktijdig verzoeken te sturen naar alle upstream servers.\",\n  \"upstream_timeout\": \"Upstream time-out\",\n  \"upstream_timeout_desc\": \"Geeft het aantal seconden aan dat moet worden gewacht op een reactie van de upstream-server\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Gebruik AdGuardBrowsing Security web service\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home controleert of het domein in de blokkeerlijst voorkomt dmv Browsing Security web service. Dit gebeurt dmv een privacy vriendelijk API verzoek:een korte prefix van de domein naam met SHA256 hash wordt verzonden naar de server.\",\n  \"use_adguard_parental\": \"Gebruik AdGuard Ouderlijk toezicht web service\",\n  \"use_adguard_parental_hint\": \"AdGuard Home controleert of het domein 18+ content bevat. Dit gebeurt dmv dezelfde privacy vriendelijke API als de Browsing Security web service.\",\n  \"use_private_ptr_resolvers_desc\": \"PTR-, SOA- en NS-verzoeken voor ARPA-domeinen die privé-IP-adressen bevatten oplossen via privé-upstreamservers, DHCP, /etc/hosts, enz. Indien uitgeschakeld, zal AdGuard Home op al dergelijke verzoeken reageren met NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Private omgekeerde DNS-resolvers gebruiken\",\n  \"use_saved_key\": \"De eerder opgeslagen sleutel gebruiken\",\n  \"username_label\": \"Gebruikersnaam\",\n  \"username_placeholder\": \"Voer gebruikersnaam in\",\n  \"validated_with_dnssec\": \"Gevalideerd met DNSSEC\",\n  \"version\": \"Versie\",\n  \"version_request_error\": \"Updatecontrole mislukt. Controleer je internetverbinding.\",\n  \"wednesday\": \"woensdag\",\n  \"wednesday_short\": \"wo\",\n  \"whois\": \"Whois\"\n}\n"
  },
  {
    "path": "client/src/__locales/no.json",
    "content": "{\n  \"access_allowed_desc\": \"En liste over CIDR- eller IP-adresser. Dersom dette er satt opp, vil AdGuard Home kun akseptere forespørsler fra disse IP-adressene.\",\n  \"access_allowed_title\": \"Tillatte klienter\",\n  \"access_blocked_desc\": \"Ikke forveksle dette med filtre. AdGuard Home vil nekte å behandle DNS-forespørsler som har disse domenene, og disse forespørslene dukker ikke engang opp i forespørselsloggen. Du kan spesifisere nøyaktige domene navn, jokertegn, eller URL-filterregler, f.eks. «example.org», «*.example.log» eller «||example.org^» derav.\",\n  \"access_blocked_title\": \"Blokkerte domener\",\n  \"access_desc\": \"Her kan du sette opp tilgangsregler for AdGuard Home-DNS-tjeneren.\",\n  \"access_disallowed_desc\": \"En liste over CIDR- eller IP-adresser. Dersom dette er satt opp, vil AdGuard Home avslå forespørsler fra disse IP-adressene.\",\n  \"access_disallowed_title\": \"Klienter som skal avvises\",\n  \"access_settings_saved\": \"Tilgangsinnstillingene ble vellykket lagret\",\n  \"access_title\": \"Tilgangsinnstillinger\",\n  \"actions_table_header\": \"Handlinger\",\n  \"add_allowlist\": \"Legg til hviteliste\",\n  \"add_blocklist\": \"Legg til blokkeringsliste\",\n  \"add_custom_list\": \"Legg til en selvvalgt liste\",\n  \"add_persistent_client\": \"Legg til som vedvarende klient\",\n  \"address\": \"Adresse\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home vil droppe alle DNS-forespørsler fra denne klienten.\",\n  \"all_lists_up_to_date_toast\": \"Alle listene er allerede oppdatert\",\n  \"all_queries\": \"Alle forespørsler\",\n  \"allow_this_client\": \"Tillat denne klienten\",\n  \"allowed\": \"Unntak\",\n  \"anonymize_client_ip\": \"Anonymiser klient-IP-en\",\n  \"anonymize_client_ip_desc\": \"Ikke lagre den fulle IP-adressen til klienten i loggføringer eller statistikker\",\n  \"anonymizer_notification\": \"<0>Merk:</0> IP-anonymisering er aktivert. Du kan deaktivere det i <1>Generelle innstillinger</1>.\",\n  \"answer\": \"Svar\",\n  \"apply_btn\": \"Benytt\",\n  \"auto_clients_desc\": \"Informasjon om IP-adresser til enheter som bruker eller kan bruke AdGuard Home. Denne informasjonen er samlet inn fra flere kilder, inkludert vertsfiler, omvendt DNS, etc.\",\n  \"auto_clients_title\": \"Klienter (kjørende)\",\n  \"autofix_warning_list\": \"Den vil utføre disse handlingene: <0>Skru av systemets DNSStubListener</0> <0>Sette DNS-tjeneradressen til 127.0.0.1</0> <0>Bytte ut det symbolske lenkemålet til /etc/resolv.conf med /run/systemd/resolve/resolv.conf</0> <0>Stoppe DNSStubListener (gjeninnlast 'systemd-resolved'-tjenesten)</0>\",\n  \"autofix_warning_result\": \"Som følge av det vil alle DNS-forespørsler fra systemet ditt bli behandlet av AdGuard Home som standard.\",\n  \"autofix_warning_text\": \"Hvis du klikker på «Fiks», vil AdGuard Home sette opp systemet ditt til å bruke 'AdGuard Home'-DNS-tjeneren.\",\n  \"average_processing_time\": \"Gjennomsnittlig behandlingstid\",\n  \"average_processing_time_hint\": \"Gjennomsnittstid for behandling av DNS-forespørsler i millisekunder\",\n  \"average_upstream_response_time\": \"Gjennomsnittlig responstid fra oppstrømsserver\",\n  \"back\": \"Tilbake\",\n  \"block\": \"Blokker\",\n  \"block_all\": \"Blokker alt\",\n  \"block_domain_use_filters_and_hosts\": \"Blokker domener ved hjelp av filtre, «hosts»-filer, og rå domener\",\n  \"block_for_this_client_only\": \"Blokker kun for denne klienten\",\n  \"block_services\": \"Blokker spesifikke tjenester\",\n  \"blocked_adult_websites\": \"Blokkerte voksennettsteder\",\n  \"blocked_by\": \"<0>Blokkert av filtre</0>\",\n  \"blocked_by_cname_or_ip\": \"Blokkert av CNAME eller IP\",\n  \"blocked_by_response\": \"Blokkert av responsens CNAME eller IP\",\n  \"blocked_response_ttl\": \"Blokkerte svars TTL\",\n  \"blocked_response_ttl_desc\": \"Angir hvor mange sekunder klientene skal cache et filtrert svar\",\n  \"blocked_safebrowsing\": \"Blokkert av barnevennlig nettlesing\",\n  \"blocked_service\": \"Blokkert tjeneste\",\n  \"blocked_services\": \"Blokkerte tjenester\",\n  \"blocked_services_desc\": \"Gjør det mulig å blokkere populære nettsteder og tjenester med letthet.\",\n  \"blocked_services_global\": \"Bruk de globalt blokkerte tjenestene\",\n  \"blocked_services_saved\": \"Tjenesteblokkeringene ble vellykket lagret\",\n  \"blocked_threats\": \"Blokkerte trusler\",\n  \"blocking_ipv4\": \"IPv4-blokkering\",\n  \"blocking_ipv4_desc\": \"IP-adressen som det skal svares med for blokkerte A-forespørsler\",\n  \"blocking_ipv6\": \"IPv6-blokkering\",\n  \"blocking_ipv6_desc\": \"IP-adressen som det skal svares med for blokkerte AAAA-forespørsler\",\n  \"blocking_mode\": \"Blokkeringsmodus\",\n  \"blocking_mode_custom_ip\": \"Tilpasset IP: Svar med en manuelt valgt IP-adresse\",\n  \"blocking_mode_default\": \"Standard: Svar med null-IP-adresse (0.0.0.0 for A; :: for AAAA) når den blokkeres av adblock-aktige oppføringer; svar med IP-adressen som er spesifisert i oppføringen når den blokkeres av /etc/hosts-typeoppføringer\",\n  \"blocking_mode_null_ip\": \"Null IP: Svar med en 0-IP-adresse (0.0.0.0 for A; :: for AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Svar med NXDOMAIN-koden\",\n  \"blocking_mode_refused\": \"REFUSED: Svar med REFUSED-koden\",\n  \"blocklist\": \"Blokkeringsliste\",\n  \"bootstrap_dns\": \"Bootstrap-DNS-tjenere\",\n  \"bootstrap_dns_desc\": \"IP-adresser til DNS-servere som brukes til å løse IP-adresser til DoH/DoT-løsere du spesifiserer som oppstrøms. Kommentarer er ikke tillatt.\",\n  \"cache_cleared\": \"DNS-bufferet ble vellykket tømt\",\n  \"cache_enabled\": \"Aktiver cache\",\n  \"cache_enabled_desc\": \"Lagre DNS-svar lokalt.\",\n  \"cache_optimistic\": \"Optimistisk mellomlagring\",\n  \"cache_optimistic_desc\": \"Få AdGuard Home til å svare fra hurtigbufferen selv når oppføringene er utløpt, og prøv også å oppfriske dem.\",\n  \"cache_size\": \"Mellomlagerstørrelse\",\n  \"cache_size_desc\": \"Størrelse på DNS-cache (i bytes).\",\n  \"cache_size_validation\": \"Bufferstørrelsen må være større enn null når den er aktivert.\",\n  \"cache_ttl_max_override\": \"Overstyr maksimallevetiden\",\n  \"cache_ttl_max_override_desc\": \"Velg en maks-levetidsverdi (i sekunder) for oppføringer i DNS-mellomlageret\",\n  \"cache_ttl_min_override\": \"Overstyr minimumslevetiden\",\n  \"cache_ttl_min_override_desc\": \"Overstyr korte levetidsverdier (i sekunder) som mottas fra oppstrømstjeneren under mellomlagring av DNS-responser\",\n  \"cancel_btn\": \"Avbryt\",\n  \"category_label\": \"Kategori\",\n  \"check\": \"Sjekk\",\n  \"check_client_id\": \"Klientidentifikator (ClientID eller IP-adresse)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Sjekk om domenenavnet er filtrert\",\n  \"check_dhcp_servers\": \"Se etter DHCP-tjenere\",\n  \"check_dns_record\": \"Velg type DNS-post\",\n  \"check_enter_client_id\": \"Skriv inn klientidentifikator\",\n  \"check_hostname\": \"Vertsnavn eller domenenavn\",\n  \"check_ip\": \"IP-adresser: {{ip}}\",\n  \"check_not_found\": \"Ikke funnet i filterlistene dine\",\n  \"check_reason\": \"Årsak: {{reason}}\",\n  \"check_service\": \"Tjenestenavn: {{service}}\",\n  \"check_title\": \"Sjekk filtreringen\",\n  \"check_updates_btn\": \"Se etter oppdateringer\",\n  \"check_updates_now\": \"Se etter oppdateringer nå\",\n  \"choose_allowlist\": \"Velg hvitelister\",\n  \"choose_blocklist\": \"Velg blokkeringslister\",\n  \"choose_from_list\": \"Velg fra listen\",\n  \"city\": \"By\",\n  \"clear_cache\": \"Tøm cache\",\n  \"click_to_view_queries\": \"Klikk for å vise forespørsler\",\n  \"client_add\": \"Legg til klient\",\n  \"client_added\": \"Klienten «{{key}}» ble vellykket lagt til\",\n  \"client_blocked\": \"Klienten «{{ip}}» ble vellykket blokkert\",\n  \"client_confirm_block\": \"Er du sikker på at du vil blokkere klienten «{{ip}}»?\",\n  \"client_confirm_delete\": \"Er du sikker på at du vil slette klienten «{{key}}»?\",\n  \"client_confirm_unblock\": \"Er du sikker på at du vil oppheve blokkeringen av klienten «{{ip}}»?\",\n  \"client_deleted\": \"Klienten «{{key}}» ble vellykket slettet\",\n  \"client_details\": \"Klientdetaljer\",\n  \"client_edit\": \"Rediger klienten\",\n  \"client_global_settings\": \"Bruk de globale innstillingene\",\n  \"client_id\": \"Klient-ID\",\n  \"client_id_desc\": \"Forskjellige klienter kan identifiserer med en spesiell klient-ID. <a>Her</a> kan du lære mer om hvordan man identifiserer klienter.\",\n  \"client_id_placeholder\": \"Skriv inn klient-ID\",\n  \"client_identifier\": \"Identifikator\",\n  \"client_identifier_desc\": \"Klienter kan bli identifisert gjennom IP-adressen, CIDR, MAC-adressen, eller en spesiell klient-ID (kan også brukes for DoT/DoH/DoQ). <0>Her</0> kan du lære mer om å identifisere klienter.\",\n  \"client_name\": \"Klient {{id}}\",\n  \"client_new\": \"Ny klient\",\n  \"client_settings\": \"Klientinnstillinger\",\n  \"client_table_header\": \"Klient\",\n  \"client_unblocked\": \"Opphevingen av blokkeringen av klienten «{{ip}}» var vellykket\",\n  \"client_updated\": \"Klienten «{{key}}» ble vellykket oppdatert\",\n  \"clients_desc\": \"Konfigurer enheter som er koblet til AdGuard Home\",\n  \"clients_not_found\": \"Ingen klienter ble funnet\",\n  \"clients_title\": \"Klienter\",\n  \"compact\": \"Kompakt\",\n  \"config_successfully_saved\": \"Oppsettet ble vellykket lagret\",\n  \"configure\": \"Sett opp\",\n  \"confirm_dns_cache_clear\": \"Er du sikker på at du vil tømme DNS-bufferet?\",\n  \"confirm_static_ip\": \"AdGuard Home vil sette opp {{ip}} til å bli din statiske IP-adresse. Vil du fortsette?\",\n  \"copyright\": \"Opphavsrett\",\n  \"country\": \"Land\",\n  \"custom_filter_rules\": \"Selvvalgte filtreringsregler\",\n  \"custom_filter_rules_hint\": \"Skriv inn én oppføring per linje. Du kan bruke adblock-oppføringer, «hosts»-filsyntaks, eller rå domener.\",\n  \"custom_filtering_rules\": \"Selvvalgte filtreringsoppføringer\",\n  \"custom_ip\": \"Tilpasset IP\",\n  \"custom_retention_input\": \"Angi oppbevaring i timer\",\n  \"custom_rotation_input\": \"Angi rotasjon i timer\",\n  \"dashboard\": \"Kontrollsenter\",\n  \"date\": \"Dato\",\n  \"default\": \"Standardmodus\",\n  \"delete_confirm\": \"Er du sikker på at du vil slette «{{key}}»?\",\n  \"delete_table_action\": \"Slett\",\n  \"descr\": \"Beskrivelse\",\n  \"details\": \"Detaljer\",\n  \"dhcp_add_static_lease\": \"Legg til statisk leieavtale\",\n  \"dhcp_config_saved\": \"Lagret DHCP-tjeneroppsettet\",\n  \"dhcp_description\": \"Dersom ruteren din ikke har DHCP-innstillinger, kan du bruke AdGuard Home sin egen innebygde DHCP-tjener.\",\n  \"dhcp_disable\": \"Skru av DHCP-tjeneren\",\n  \"dhcp_dynamic_ip_found\": \"Systemet ditt bruker et oppsett med dynamisk IP-adresse for grensesnittet <0>{{interfaceName}}</0>. For å kunne bruke DHCP-tjeneren, må en statisk IP-adresse ha blitt satt opp. Din nåværende IP-adresse er <0>{{ipAddress}}</0>. Vi vil automatisk gjøre denne IP-adressen statisk hvis du trykker på «Skru på DHCP»-knappen.\",\n  \"dhcp_edit_static_lease\": \"Rediger statisk leieavtale\",\n  \"dhcp_enable\": \"Skru på DHCP-tjeneren\",\n  \"dhcp_error\": \"Vi klarte ikke å fastslå om det er en annen DHCP-tjener i nettverket ditt eller ikke.\",\n  \"dhcp_form_gateway_input\": \"Gateway-IP\",\n  \"dhcp_form_lease_input\": \"Leieavtalenes varighet\",\n  \"dhcp_form_lease_title\": \"DHCP-leieavtalevarighet (i sekunder)\",\n  \"dhcp_form_range_end\": \"Rekkeviddeslutt\",\n  \"dhcp_form_range_start\": \"Rekkeviddestart\",\n  \"dhcp_form_range_title\": \"Spennvidden til IP-adressene\",\n  \"dhcp_form_subnet_input\": \"Nettverksmaske\",\n  \"dhcp_found\": \"En aktiv DHCP-tjener ble oppdaget i nettverket. Det er ikke trygt å bruke den innebygde DHCP-tjeneren.\",\n  \"dhcp_hardware_address\": \"Maskinvareadresse\",\n  \"dhcp_interface_select\": \"Velg DHCP-grensesnitt\",\n  \"dhcp_ip_addresses\": \"IP-adresser\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4-innstillinger\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6-innstillinger\",\n  \"dhcp_lease_added\": \"Den statiske leieavtalen «{{key}}» ble vellykket lagt til\",\n  \"dhcp_lease_deleted\": \"Den statiske leieavtalen «{{key}}» ble vellykket lagt slettet\",\n  \"dhcp_lease_updated\": \"Den statiske leieavtalen «{{key}}» ble vellykket oppdatert\",\n  \"dhcp_leases\": \"DHCP-leieavtaler\",\n  \"dhcp_leases_not_found\": \"Ingen DHCP-leieavtaler ble funnet\",\n  \"dhcp_new_static_lease\": \"Ny statisk leieavtale\",\n  \"dhcp_not_found\": \"Det er trygt å skru på den innebygde DHCP-tjeneren - vi kunne ikke finne noen aktive DHCP-tjenere i nettverket. Men vi oppfordrer deg til å dobbeltsjekke manuelt, siden vår automatiske test ikke gir 100% sikre svar ennå.\",\n  \"dhcp_reset\": \"Er du sikker på at du vil tilbakestille DHCP-oppsettet?\",\n  \"dhcp_reset_leases\": \"Tilbakestill alle leieavtaler\",\n  \"dhcp_reset_leases_confirm\": \"Er du sikker på at du vil tilbakestille alle leieavtaler?\",\n  \"dhcp_reset_leases_success\": \"DHCP-leieavtaler tilbakestilt med suksess\",\n  \"dhcp_settings\": \"DHCP-innstillinger\",\n  \"dhcp_static_ip_error\": \"For å kunne bruke DHCP-tjeneren, må det være satt en statisk IP-adresse. Vi klarte ikke å finne ut om dette nettverksgrensesnittet har blitt satt opp med en statisk IP-adresse. Vennligst sett opp en statisk IP-adresse manuelt.\",\n  \"dhcp_static_leases\": \"Statiske DHCP-leieavtaler\",\n  \"dhcp_static_leases_not_found\": \"Ingen statiske DHCP-leieavtaler ble funnet\",\n  \"dhcp_table_expires\": \"Utløper\",\n  \"dhcp_table_hostname\": \"Vertsnavn\",\n  \"dhcp_title\": \"DHCP-tjener (eksperimentell!)\",\n  \"dhcp_warning\": \"Hvis du vil aktivere DHCP-tjeneren likevel, så sørg for at det ikke er noen andre aktive DHCP-tjenere i nettverket ditt. Ellers kan det knekke internettilgangen til tilkoblede enheter!\",\n  \"disable_for_hours\": \"For {{count}} time\",\n  \"disable_for_hours_plural\": \"For {{count}} timer\",\n  \"disable_for_minutes\": \"For {{count}} minutt\",\n  \"disable_for_minutes_plural\": \"For {{count}} minutter\",\n  \"disable_for_seconds\": \"For {{count}} sekund\",\n  \"disable_for_seconds_plural\": \"For {{count}} sekunder\",\n  \"disable_ipv6\": \"Skru av IPv6\",\n  \"disable_ipv6_desc\": \"Slipp alle DNS-spørringer for IPv6-adresser (type AAAA) og fjern IPv6-hint fra HTTPS-svar.\",\n  \"disable_notify_for_hours\": \"Deaktiver beskyttelse i {{count}} time\",\n  \"disable_notify_for_hours_plural\": \"Skru av beskyttelse for {{count}} timer\",\n  \"disable_notify_for_minutes\": \"Deaktiver beskyttelse i {{count}} minutt\",\n  \"disable_notify_for_minutes_plural\": \"Deaktiver beskyttelsen for {{count}} minutter\",\n  \"disable_notify_for_seconds\": \"Deaktiver beskyttelse i {{count}} sekund\",\n  \"disable_notify_for_seconds_plural\": \"Deaktiver beskyttelse for {{count}} sekunder\",\n  \"disable_notify_until_tomorrow\": \"Deaktiver beskyttelsen til i morgen\",\n  \"disable_protection\": \"Skru av beskyttelse\",\n  \"disable_rewrites\": \"Deaktiver omskrivningsregler\",\n  \"disable_until_tomorrow\": \"Frem til i morgen\",\n  \"disabled\": \"Skrudd av\",\n  \"disabled_dhcp\": \"DHCP-tjeneren ble skrudd av\",\n  \"disabled_filtering_toast\": \"Skrudde av filtrering\",\n  \"disabled_parental_toast\": \"Skrudde av foreldrekontroll\",\n  \"disabled_protection\": \"Beskyttelsen ble skrudd av\",\n  \"disabled_safe_browsing_toast\": \"Skrudde av barnevennlig nettlesing\",\n  \"disabled_safe_search_toast\": \"Skrudde av barnevennlige søk\",\n  \"disallow_this_client\": \"Ikke tillat denne klienten\",\n  \"dns_addresses\": \"DNS-adresser\",\n  \"dns_allowlists\": \"DNS-hvitelister\",\n  \"dns_allowlists_desc\": \"Domener fra DNS-hvitelistene vil bli sluppet gjennom, selv hvis de er i noen av blokkeringslistene.\",\n  \"dns_blocklists\": \"DNS-blokkeringslister\",\n  \"dns_blocklists_desc\": \"AdGuard Home vil blokkere domener som samsvarer med blokkeringslistene.\",\n  \"dns_cache_config\": \"DNS-mellomlageroppsett\",\n  \"dns_cache_config_desc\": \"Her kan du justere DNS-mellomlageret\",\n  \"dns_cache_size\": \"DNS-bufferstørrelse (i byte)\",\n  \"dns_config\": \"DNS-tjeneroppsett\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS-privatliv\",\n  \"dns_providers\": \"Her er en <0>liste over kjente DNS-leverandører</0> som du kan velge blant.\",\n  \"dns_query\": \"DNS-forespørsler\",\n  \"dns_rewrites\": \"DNS-omdirigeringer\",\n  \"dns_settings\": \"DNS-innstillinger\",\n  \"dns_start\": \"DNS-tjeneren starter opp\",\n  \"dns_status_error\": \"Feil ved sjekk av DNS-tjenerstatusen\",\n  \"dns_test_not_ok_toast\": \"Tjeneren «{{key}}» kunne ikke brukes, vennligst dobbeltsjekk at du har skrevet den riktig\",\n  \"dns_test_ok_toast\": \"De spesifiserte DNS-tjenerne fungerer riktig\",\n  \"dns_test_parsing_error_toast\": \"Seksjon {{section}}: linje {{line}}: kunne ikke brukes, vennligst sjekk at du har skrevet det riktig\",\n  \"dns_test_warning_toast\": \"Oppstrøms \\\"{{key}}\\\" svarer ikke på testforespørselene og fungerer kanskje ikke riktig\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Skru på DNSSEC\",\n  \"dnssec_enable_desc\": \"Fest på DNSSEC-flagg til utgående DNS-forespørsler og sjekk resultatet (En DNS-oppstrømstjener med DNSSEC-støtte er påkrevd)\",\n  \"domain\": \"Domene\",\n  \"domain_desc\": \"Skriv inn domenenavnet eller jokertegnet som du vil skal skrives om.\",\n  \"domain_name_table_header\": \"Domenenavn\",\n  \"domain_or_client\": \"Domene eller klient\",\n  \"down\": \"Nedstrøm\",\n  \"download_mobileconfig\": \"Last ned oppsettsfil\",\n  \"download_mobileconfig_doh\": \"Last ned .mobileconfig for DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Last ned .mobileconfig for DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Rediger hviteliste\",\n  \"edit_blocklist\": \"Rediger blokkeringsliste\",\n  \"edit_table_action\": \"Rediger\",\n  \"edns_cs_desc\": \"Hvis det er skrudd på, vil AdGuard Home sende klientenes undernett til DNS-tjenerne.\",\n  \"edns_enable\": \"Aktiver EDNS-klientundernett\",\n  \"edns_use_custom_ip\": \"Bruk tilpasset IP for EDNS\",\n  \"edns_use_custom_ip_desc\": \"Tillat å bruke tilpasset IP for EDNS\",\n  \"elapsed\": \"Utløpt\",\n  \"empty_response_status\": \"Tomt innhold\",\n  \"enable_protection\": \"Skru på beskyttelse\",\n  \"enable_protection_timer\": \"Beskyttelse vil være aktivert i {{time}}\",\n  \"enable_rewrites\": \"Aktiver omskrivningsregler\",\n  \"enable_upstream_dns_cache\": \"Aktiver DNS-caching for denne klientens tilpassede upstream-konfigurasjon\",\n  \"enabled_dhcp\": \"DHCP-tjeneren ble skrudd på\",\n  \"enabled_filtering_toast\": \"Skrudde på filtrering\",\n  \"enabled_parental_toast\": \"Skrudde på foreldrekontroll\",\n  \"enabled_protection\": \"Beskyttelse ble skrudd på\",\n  \"enabled_safe_browsing_toast\": \"Skrudde på barnevennlig nettlesing\",\n  \"enabled_save_search_toast\": \"Skrudde på barnevennlige søk\",\n  \"enabled_table_header\": \"Skrudd på\",\n  \"encryption_certificate_path\": \"Filbanen til sertifikatet\",\n  \"encryption_certificates\": \"Sertifikater\",\n  \"encryption_certificates_desc\": \"For å bruke kryptering, må du skrive inn et gyldig SSL-sertifikatkjede for domenet ditt. Du kan få et gratis sertifikat hos <0>{{link}}</0>, eller kjøpe et fra en av de troverdige sertifikatsautoritetene.\",\n  \"encryption_certificates_input\": \"Kopier / lim inn dine PEM-kodede sertifikater her.\",\n  \"encryption_certificates_source_content\": \"Lim inn innholdet til sertifikatet\",\n  \"encryption_certificates_source_path\": \"Bestem en filbane for sertifikater\",\n  \"encryption_chain_invalid\": \"Sertifikatskjeden er ugyldig\",\n  \"encryption_chain_valid\": \"Sertifikatskjeden er gyldig\",\n  \"encryption_config_saved\": \"Krypteringsoppsettet ble lagret\",\n  \"encryption_desc\": \"Krypteringsstøtte (HTTPS/TLS) for både DNS og admin-nettgrensesnittet\",\n  \"encryption_doq\": \"DNS-over-QUIC-port\",\n  \"encryption_doq_desc\": \"Dersom denne porten er satt opp, vil AdGuard Home kjøre en DNS-over-QUIC-tjener på denne porten. \",\n  \"encryption_dot\": \"'DNS-over-TLS'-port\",\n  \"encryption_dot_desc\": \"Dersom denne porten er satt opp, vil AdGuard Home kjøre en 'DNS-over-TLS'-tjener på denne porten.\",\n  \"encryption_enable\": \"Skru på kryptering (HTTPS, DNS-over-HTTPS, og DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Dersom kryptering er skrudd på, vil AdGuard Home sitt admingrensesnitt virke over HTTPS, og DNS-tjeneren vil lytte etter forespørseler over DNS-over-HTTPS og DNS-over-TLS.\",\n  \"encryption_expire\": \"Utløper\",\n  \"encryption_hostnames\": \"Vertsnavn\",\n  \"encryption_https\": \"HTTPS-port\",\n  \"encryption_https_desc\": \"Dersom HTTPS-porten er satt opp, vil AdGuard Home sitt admin-grensesnitt være tilgjengelig gjennom HTTPS, og vil også sørge for DNS-over-HTTPS på «/dns-query»-plasseringen.\",\n  \"encryption_issuer\": \"Utsteder\",\n  \"encryption_key\": \"Privat nøkkel\",\n  \"encryption_key_input\": \"Kopier / lim inn ditt sertifikats PEM-kodede private nøkkel her.\",\n  \"encryption_key_invalid\": \"Dette er en ugyldig {{type}}-type privat nøkkel\",\n  \"encryption_key_source_content\": \"Lim inn innholdet til den private nøkkelen\",\n  \"encryption_key_source_path\": \"Bestem en privat nøkkelfilsti\",\n  \"encryption_key_valid\": \"Dette er en gyldig {{type}}-type privat nøkkel\",\n  \"encryption_plain_dns_desc\": \"Vanlig DNS er aktivert som standard. Du kan deaktivere dette for å tvinge alle enheter til å bruke kryptert DNS. For å gjøre dette må du aktivere minst ett kryptert DNS-protokoll\",\n  \"encryption_plain_dns_enable\": \"Aktiver vanlig DNS\",\n  \"encryption_plain_dns_error\": \"For å deaktivere vanlig DNS, aktiver minst ett kryptert DNS-protokoll\",\n  \"encryption_private_key_path\": \"Filbanen til den private nøkkelen\",\n  \"encryption_redirect\": \"Automatisk omdiriger til HTTPS\",\n  \"encryption_redirect_desc\": \"Dersom dette er valgt, vil AdGuard Home automatisk omdirigere deg fra HTTP til HTTPS-adresser.\",\n  \"encryption_reset\": \"Er du sikker på at du vil tilbakestille krypteringsinnstillingene?\",\n  \"encryption_server\": \"Tjenerens navn\",\n  \"encryption_server_desc\": \"Hvis angitt, oppdager AdGuard Home klient-IDer, svarer på DDR-spørringer og utfører ytterligere tilkoblingsvalideringer. Hvis ikke angitt, er disse funksjonene deaktivert. Må samsvare med ett av DNS-navnene i sertifikatet.\",\n  \"encryption_server_enter\": \"Skriv inn domenenavnet ditt\",\n  \"encryption_settings\": \"Krypteringsinnstillinger\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Tema\",\n  \"encryption_title\": \"Kryptering\",\n  \"encryption_warning\": \"Advarsel\",\n  \"enforce_safe_search\": \"Påtving barnevennlige søk\",\n  \"enforce_save_search_hint\": \"AdGuard Home vil håndheve \\\"Safe Search\\\" i følgende søkemotorer: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Påtvungede barnevennlige søk\",\n  \"enter_cache_size\": \"Skriv inn mellomlagerstørrelse (i bytes)\",\n  \"enter_cache_ttl_max_override\": \"Skriv inn maksimallevetiden (i sekunder)\",\n  \"enter_cache_ttl_min_override\": \"Skriv inn minimumslevetiden (i sekunder)\",\n  \"enter_name_hint\": \"Skriv inn navn\",\n  \"enter_url_or_path_hint\": \"Skriv inn listens URL eller fulle filbane\",\n  \"enter_valid_allowlist\": \"Skriv inn en gyldig nettadresse til hvitelisten.\",\n  \"enter_valid_blocklist\": \"Skriv inn en gyldig nettadresse til blokkeringslisten.\",\n  \"error_details\": \"Feildetaljer\",\n  \"example_comment\": \"! Her er det en kommentar\",\n  \"example_comment_hash\": \"# Også en kommentar\",\n  \"example_comment_meaning\": \"bare en kommentar\",\n  \"example_meaning_filter_block\": \"blokker tilgang til 'example.org'-domenet og alle dens underdomener\",\n  \"example_meaning_filter_whitelist\": \"opphev blokkeringen av 'example.org'-domenet og alle dens underdomener\",\n  \"example_meaning_host_block\": \"AdGuard Home vil nå videresende 'example.org'-domenet (men ikke dens underdomener) til 127.0.0.1.\",\n  \"example_multiple_upstreams_reserved\": \"flere upstreams <0>for spesifikke domener</0>;\",\n  \"example_regex_meaning\": \"blokker tilgang til domener som samsvarer med den valgte ordinære oppføringen\",\n  \"example_rewrite_domain\": \"omskriv svarene til kun dette domenenavnet.\",\n  \"example_rewrite_wildcard\": \"omskriv svarene til alle <0>example.org</0>-underdomener.\",\n  \"example_upstream_comment\": \"Du kan spesifisere en kommentar\",\n  \"example_upstream_doh\": \"kryptert <0>DNS-over-HTTPS</0>\",\n  \"example_upstream_doh3\": \"kryptert DNS-over-HTTPS med tvungen <0>HTTP/3</0> og uten fallback til HTTP/2 eller lavere;\",\n  \"example_upstream_doq\": \"kryptert <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"kryptert <0>DNS-over-TLS</0>\",\n  \"example_upstream_regular\": \"vanlig DNS (over UDP)\",\n  \"example_upstream_regular_port\": \"vanlig DNS (over UDP, med port);\",\n  \"example_upstream_reserved\": \"Du kan bestemme en oppstrøms-DNS <0>for et spesifikt domene(r)</0>\",\n  \"example_upstream_sdns\": \"du kan bruke <0>DNS-stempler</0> med <1>DNSCrypt</1> eller <2>DNS-over-HTTPS</2>-behandlere\",\n  \"example_upstream_tcp\": \"vanlig DNS (over TCP)\",\n  \"example_upstream_tcp_hostname\": \"vanlig DNS (over TCP, vertsnavn);\",\n  \"example_upstream_tcp_port\": \"vanlig DNS (over TCP, med port);\",\n  \"example_upstream_udp\": \"vanlig DNS (over UDP, vertsnavn);\",\n  \"examples_title\": \"Eksempler\",\n  \"fallback_dns_desc\": \"Liste over reserve-DNS-servere som brukes når oppstrøms DNS-servere ikke svarer. Syntaksen er den samme som i hovedoppstrømsfeltet ovenfor.\",\n  \"fallback_dns_placeholder\": \"Angi én reserve-DNS-server per linje\",\n  \"fallback_dns_title\": \"Reserve DNS-servere\",\n  \"faq\": \"OSS\",\n  \"fastest_addr\": \"Raskeste IP-adresse\",\n  \"fastest_addr_desc\": \"Vent på svar fra <b>alle</b> DNS-serverne, mål TCP-tilkoblingshastigheten for hver server, og returner IP-adressen til serveren med den raskeste tilkoblingshastigheten.<br/>Denne modusen kan betydelig bremse DNS-forespørslene, hvis en eller flere oppstrømsservere ikke svarer. Sørg for at oppstrømsserverne dine er stabile og at oppstrøms tidsavbruddet er lavt.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"Filteret har blitt vellykket lagt til\",\n  \"filter_allowlist\": \"ADVARSEL: Denne handlingen vil også ekskludere regelen \\\"{{disallowed_rule}}\\\" fra listen over tillatte klienter.\",\n  \"filter_category_general\": \"Generelt\",\n  \"filter_category_general_desc\": \"Lister som blokkerer sporing og reklamer på de fleste enheter\",\n  \"filter_category_other\": \"Andre\",\n  \"filter_category_other_desc\": \"Andre blokkeringslister\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Lister som fokuserer på regionale reklamer og sporingstjenere\",\n  \"filter_category_security\": \"Sikkerhet\",\n  \"filter_category_security_desc\": \"Lister som spesialiserer seg på å blokkere skadevare-, phishing- eller svindeldomener\",\n  \"filter_removed_successfully\": \"Listen ble vellykket fjernet\",\n  \"filter_updated\": \"Listen ble vellykket oppdatert\",\n  \"filtered\": \"Filtrert\",\n  \"filtered_custom_rules\": \"Filtrert av Selvvalgte filtreringsoppføringer\",\n  \"filtering_rules_learn_more\": \"<0>Lær mer</0> om å lage dine egne filterlister for AdGuard Home.\",\n  \"filters\": \"Filtre\",\n  \"filters_and_hosts_hint\": \"AdGuard Home forstår grunnleggende adblock-oppføringer, «hosts»-filsyntaks, og domenelister.\",\n  \"filters_block_toggle_hint\": \"Du kan sette opp blokkeringsoppføringer i <a>Filtre</a>-innstillingene.\",\n  \"filters_configuration\": \"Oppsett av filtre\",\n  \"filters_enable\": \"Skru på filtre\",\n  \"filters_interval\": \"Filteroppdateringsvanlighet\",\n  \"fix\": \"Fiks\",\n  \"for_last_days\": \"for den siste {{count}} dagen\",\n  \"for_last_days_plural\": \"de siste {{count}} dagene\",\n  \"for_last_hours\": \"for den siste {{count}} timen\",\n  \"for_last_hours_plural\": \"for de siste {{count}} timene\",\n  \"forgot_password\": \"Har du glemt passordet?\",\n  \"forgot_password_desc\": \"Vennligst følg <0>disse trinnene</0> for å lage et nytt passord til brukerkontoen din.\",\n  \"form_add_id\": \"Legg til identifikator\",\n  \"form_answer\": \"Skriv inn IP-adresse eller domenenavn\",\n  \"form_client_name\": \"Skriv inn klientnavnet\",\n  \"form_domain\": \"Skriv inn domene\",\n  \"form_enter_blocked_response_ttl\": \"Skriv inn TTL for blokkerte svar (sekunder)\",\n  \"form_enter_host\": \"Legg til et domenenavn\",\n  \"form_enter_hostname\": \"Skriv inn vertsnavnet\",\n  \"form_enter_id\": \"Skriv inn identifikator\",\n  \"form_enter_ip\": \"Skriv inn IP\",\n  \"form_enter_mac\": \"Skriv inn MAC\",\n  \"form_enter_rate_limit\": \"Skriv inn forespørselsfrekvensgrense\",\n  \"form_enter_rate_limit_subnet_len\": \"Oppgi subnet prefixlengde for ratebegrensning\",\n  \"form_enter_subnet_ip\": \"Skriv inn en IP-adresse i undernettet «{{cidr}}»\",\n  \"form_enter_upstream_timeout\": \"Skriv inn oppstrømsserverens tidsavbrudd varighet i sekunder\",\n  \"form_error_answer_format\": \"Ugyldig svarformat\",\n  \"form_error_client_id_format\": \"Ugyldig ID-klientformat\",\n  \"form_error_domain_format\": \"Ugyldig domeneformat\",\n  \"form_error_equal\": \"Burde ikke være de samme\",\n  \"form_error_gateway_ip\": \"Leie kan ikke ha IP-adresse til gateway\",\n  \"form_error_ip4_format\": \"Ugyldig IPv4-format\",\n  \"form_error_ip4_gateway_format\": \"Ugyldig IPv4-adresse for gatewayen\",\n  \"form_error_ip6_format\": \"Ugyldig IPv6-format\",\n  \"form_error_ip_format\": \"Ugyldig IPv4-format\",\n  \"form_error_mac_format\": \"Ugyldig MAC-format\",\n  \"form_error_password\": \"Passordet samsvarer ikke\",\n  \"form_error_password_length\": \"Passordet må være {{min}} til {{max}} tegn langt\",\n  \"form_error_port\": \"Skriv inn en gyldig portverdi\",\n  \"form_error_port_range\": \"Skriv inn et portnummer i området 80-65535\",\n  \"form_error_port_unsafe\": \"Denne porten er ikke trygg\",\n  \"form_error_positive\": \"Må være høyere enn 0\",\n  \"form_error_required\": \"Påkrevd felt\",\n  \"form_error_server_name\": \"Ugyldig tjenernavn\",\n  \"form_error_subnet\": \"Undernettet «{{cidr}}» inneholder ikke IP-adressen «{{ip}}»\",\n  \"form_error_url_format\": \"Ugyldig URL-format\",\n  \"form_error_url_or_path_format\": \"Listens URL eller fulle filbane er ugyldig\",\n  \"form_select_tags\": \"Velg klientstempler\",\n  \"found_in_known_domain_db\": \"Funnet i databasen over kjente domener.\",\n  \"friday\": \"Fredag\",\n  \"friday_short\": \"fre\",\n  \"gateway_or_subnet_invalid\": \"Ugyldig undernettmaske\",\n  \"general_settings\": \"Generelle innstillinger\",\n  \"general_statistics\": \"Generelle statistikker\",\n  \"get_started\": \"Kom i gang\",\n  \"greater_range_start_error\": \"Må være høyere enn rekkeviddens start\",\n  \"homepage\": \"Hjemmeside\",\n  \"host_whitelisted\": \"Domenet er hvitelistet\",\n  \"ignore_domains\": \"Ignorerte domener (separate med linjeskift)\",\n  \"ignore_domains_desc_query\": \"Forespørslene som samsvarer med disse reglene, skrives ikke til loggen for forespørslene\",\n  \"ignore_domains_desc_stats\": \"Forespørslene som samsvarer med disse reglene, skrives ikke til statistikken\",\n  \"ignore_domains_title\": \"Ignorerte domener\",\n  \"ignore_query_log\": \"Ignorer denne klienten i spørringsloggen\",\n  \"ignore_statistics\": \"Ignorer denne klienten i statistikken\",\n  \"install_auth_confirm\": \"Bekreft passord\",\n  \"install_auth_desc\": \"Det er høyst anbefalt å sette opp passordautentisering på ditt AdGuard Home-adminnettgrensesnitt. Selv om du velger å bare gjøre den tilgjengelig på ditt lokale nettverk, er det fortsatt viktig å beskytte den fra ubegrenset tilgang.\",\n  \"install_auth_password\": \"Passord\",\n  \"install_auth_password_enter\": \"Skriv inn passord\",\n  \"install_auth_title\": \"Autentisering\",\n  \"install_auth_username\": \"Brukernavn\",\n  \"install_auth_username_enter\": \"Skriv inn brukernavn\",\n  \"install_devices_address\": \"AdGuard Home-DNS-tjeneren lytter til de følgende adressene\",\n  \"install_devices_android_list_1\": \"Fra Android-startskjermen, trykk på «Innstillinger».\",\n  \"install_devices_android_list_2\": \"Velg «Wi-Fi» i menyen. Skjermen som lister opp alle de tilgjengelige nettverkene vil bli vist (det er umulig å velge selvvalgte DNS-adresser for mobiltilkoblinger uten en DNS-endringsapp).\",\n  \"install_devices_android_list_3\": \"Langtrykk på nettverket du er koblet til, og så trykk «Endre nettverket».\",\n  \"install_devices_android_list_4\": \"På noen enheter, vil du måtte huke av boksen for Avansert for se flere innstillinger. For å justere dine Android-DNS-innstillinger, vil du måtte endre IP-innstillingene fra DHCP til Statisk.\",\n  \"install_devices_android_list_5\": \"Endre de forvalgte 'DNS 1' og 'DNS 2'-verdiene til din AdGuard Home-tjeners adresser.\",\n  \"install_devices_desc\": \"For å begynne å bruke AdGuard Home, må du sette opp enhetene dine til å bruke den.\",\n  \"install_devices_ios_list_1\": \"Fra startskjermen, trykk på «Innstillinger».\",\n  \"install_devices_ios_list_2\": \"Velg Wi-Fi i den venstre menyen (det er umulig å sette opp DNS for mobildata-nettverk).\",\n  \"install_devices_ios_list_3\": \"Trykk på navnet til det nettverket som er aktivt for øyeblikket.\",\n  \"install_devices_ios_list_4\": \"I DNS-feltet, skriv inn din AdGuard Home-tjeners adresser.\",\n  \"install_devices_macos_list_1\": \"Klikk på Apple-ikonet og gå til Systeminnstillinger.\",\n  \"install_devices_macos_list_2\": \"Klikk på «Nettverk».\",\n  \"install_devices_macos_list_3\": \"Velg den første tilkoblingen i listen din, og klikk på «Avansert».\",\n  \"install_devices_macos_list_4\": \"Velg DNS-fanen og skriv inn din AdGuard Home-tjeners adresser der.\",\n  \"install_devices_router\": \"Ruter\",\n  \"install_devices_router_desc\": \"Dette oppsettet vil automatisk dekke alle enhetene som er koblet til hjemmeruteren din, og du vil ikke måtte sette opp hver av dem manuelt.\",\n  \"install_devices_router_list_1\": \"Åpne innstillingene til ruteren din. Vanligvis kan du få tilgang til den på nettleseren din gjennom en URL (f.eks. http://192.168.0.1/ eller http://192.168.1.1/). Du kan bli spurt om å skrive inn passordet ditt. Hvis du ikke husker det, kan du som oftest tilbakestille passordet ditt ved å trykke på knapp på selve ruteren. Noen rutere krever et spesifikt program, som i så fall er ment å allerede ha blitt installert på din PC/mobil.\",\n  \"install_devices_router_list_2\": \"Finn DHCP-/DNS-innstillingene. Se etter DNS-bokstavene ved siden av et felt som tillater to eller tre sett med sifre, som hver er delt opp i fire grupper på 1-3 sifre.\",\n  \"install_devices_router_list_3\": \"Skriv inn din AdGuard Home-tjeners adresser her.\",\n  \"install_devices_router_list_4\": \"På noen rutertyper, f.eks. Altibox sine hjemmesentraler, kan man ikke velge en selvvalgt DNS-tjener. I så fall kan det hjelpe på saken om du setter opp AdGuard Home som en <0>DHCP-tjener</0>. Alternativt, burde du se i bruksanvisningen til din spesifikke rutermodell om hvordan man tilpasser DNS-tjenerne.\",\n  \"install_devices_title\": \"Sett opp enhetene dine\",\n  \"install_devices_windows_list_1\": \"Åpne «Kontrollpanel» gjennom Start-menyen eller et Windows-søk.\",\n  \"install_devices_windows_list_2\": \"Gå til «Nettverk og internett»-kategorien, og så til «Nettverks- og delingssenter».\",\n  \"install_devices_windows_list_3\": \"På den venstre siden av skjermen, finn «Endre innstillinger for nettverkskort» og klikk på den.\",\n  \"install_devices_windows_list_4\": \"Velg din aktive tilkobling, høyreklikk på den, og velg «Egenskaper».\",\n  \"install_devices_windows_list_5\": \"Finn «Internet Protocol versjon 4 (TCP/IP)» i listen, velg den, og så klikk på «Egenskaper» igjen.\",\n  \"install_devices_windows_list_6\": \"Velg «Bruk følgende DNS-serveradresser» og så skriv inn din AdGuard Home-tjeners adresser.\",\n  \"install_saved\": \"Lagringen var vellykket\",\n  \"install_settings_all_interfaces\": \"Alle grensesnitt\",\n  \"install_settings_dns\": \"DNS-tjener\",\n  \"install_settings_dns_desc\": \"Du vil måtte sette opp enhetene eller ruteren din(e) til å bruke DNS-tjeneren på disse adressene:\",\n  \"install_settings_interface_link\": \"Ditt AdGuard Home-admin-nettgrensesnitt vil være tilgjengelig på de følgende adressene:\",\n  \"install_settings_listen\": \"Lytt til grensesnitt\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Admin-nettgrensesnitt\",\n  \"install_static_configure\": \"Vi har oppdaget at det brukes en dynamisk IP-adresse — <0>{{ip}}</0>. Vil du bruke det som din statiske adresse?\",\n  \"install_static_error\": \"AdGuard Home kan ikke sette opp automatisk i dette nettverksgrensesnitt. Vennligst let opp anvisningen for hvordan man gjør det manuellt.\",\n  \"install_static_ok\": \"Gode nyheter! Den statiske IP-adressen er allerede satt opp\",\n  \"install_step\": \"Trinn\",\n  \"install_submit_desc\": \"Oppsettsprosedyren er ferdig, og du er klar til å begynne å bruke AdGuard Home.\",\n  \"install_submit_title\": \"Gratulerer!\",\n  \"install_welcome_desc\": \"AdGuard Home er en nettverksdekkende reklame-og-sporings-blokkerende DNS-tjener. Formålet dens er å la deg styre hele nettverket ditt og alle dine enheter, og den krever ikke at klientene bruker spesifikke programmer.\",\n  \"install_welcome_title\": \"Velkommen til AdGuard Home!\",\n  \"interval_24_hour\": \"24 timer\",\n  \"interval_6_hour\": \"6 timer\",\n  \"interval_days\": \"{{count}} dag\",\n  \"interval_days_plural\": \"{{count}} dager\",\n  \"interval_hours\": \"{{count}} time\",\n  \"interval_hours_plural\": \"{{count}} timer\",\n  \"ip\": \"IP-adresse\",\n  \"ip_address\": \"IP-adresse\",\n  \"known_tracker\": \"Kjent sporer\",\n  \"last_rule_in_allowlist\": \"Kan ikke nekte denne klienten fordi utelukkelse av regelen \\\"{{disallowed_rule}}\\\" vil deaktivere listen over \\\"Tillatte klienter\\\".\",\n  \"last_time_updated_table_header\": \"Senest oppdatert\",\n  \"list_confirm_delete\": \"Er du sikker på at du vil slette denne listen?\",\n  \"list_label\": \"Liste\",\n  \"list_updated\": \"{{count}} liste oppdatert\",\n  \"list_updated_plural\": \"{{count}} lister oppdatert\",\n  \"list_url_table_header\": \"Listens nettadresse\",\n  \"load_balancing\": \"Pågangstrykk-utjevning\",\n  \"load_balancing_desc\": \"Kjør en forespørsel mot én oppstrømsserver om gangen.<br/>AdGuard Home bruker en vektet tilfeldig algoritme for å velge servere med det laveste antallet mislykkede oppslag og det laveste gjennomsnittlige oppslagstiden.\",\n  \"loading_table_status\": \"Laster inn …\",\n  \"local_ptr_default_resolver\": \"Som standard, bruker AdGuard Home følgende revers-DNS-oppletere: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS-servere brukt av AdGuard Home for private PTR-, SOA- og NS-forespørsel. En forespørsel anses som privat hvis den ber om et ARPA-domene som inneholder et subnett innenfor private IP-områder (som \\\"192.168.12.34\\\") og kommer fra en klient med en privat IP-adresse. Hvis det ikke er angitt, vil de standard DNS-oppløserne for ditt operativsystem bli brukt, unntatt for AdGuard Home IP-adressene.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home klarte ikke å finne egnede private revers-DNS-oppletere for dette systemet.\",\n  \"local_ptr_placeholder\": \"Skriv inn én IP-adresse per linje\",\n  \"local_ptr_title\": \"Private DNS-tjenere\",\n  \"location\": \"Posisjon\",\n  \"log_and_stats_section_label\": \"Spørringslogg og statistikk\",\n  \"lower_range_start_error\": \"Må være lavere enn rekkeviddens start\",\n  \"main_settings\": \"Hovedinnstillinger\",\n  \"make_static\": \"Gjør statisk\",\n  \"manual_update\": \"Vennligst <a>følg disse trinnene</a> for manuell oppdatering.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Mandag\",\n  \"monday_short\": \"man\",\n  \"name\": \"Navn\",\n  \"name_table_header\": \"Navn\",\n  \"netname\": \"Nettverksnavn\",\n  \"network\": \"Network\",\n  \"new_allowlist\": \"Ny hviteliste\",\n  \"new_blocklist\": \"Ny blokkeringsliste\",\n  \"next\": \"Neste\",\n  \"next_btn\": \"Neste\",\n  \"no_blocklist_added\": \"Ingen blokkeringslister er lagt til\",\n  \"no_clients_found\": \"Ingen klienter ble funnet\",\n  \"no_domains_found\": \"Ingen domener ble funnet\",\n  \"no_logs_found\": \"Ingen loggføringer ble funnet\",\n  \"no_servers_specified\": \"Ingen tjenere er spesifisert\",\n  \"no_upstreams_data_found\": \"Ingen oppstrøms servere data funnet\",\n  \"no_whitelist_added\": \"Ingen hvitelister er lagt til\",\n  \"nothing_found\": \"Ingenting ble funnet\",\n  \"null_ip\": \"Null-IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Antall DNS-forespørsler som ble blokkert av adblock-filtre, hosts-lister, og domene-lister\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Antall voksennettsteder som ble blokkert\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Antall DNS-forespørsler som ble blokkert av AdGuard sin nettlesersikkerhetsmodul\",\n  \"number_of_dns_query_days\": \"Antall DNS-spørringer behandlet for de siste {{count}} dagene\",\n  \"number_of_dns_query_days_plural\": \"Antall DNS-forespørsler som ble behandlet de siste {{count}} dagene\",\n  \"number_of_dns_query_hours\": \"Antall DNS-forespøringer behandlet for den siste {{count}} timen\",\n  \"number_of_dns_query_hours_plural\": \"Antall DNS-forespørsel som ble behandlet de siste {{count}} timene\",\n  \"number_of_dns_query_to_safe_search\": \"Antall DNS-forespørsler til søkemotorer der \\\"Safe Search\\\" ble fremtvunget\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"AV\",\n  \"on\": \"PÅ\",\n  \"open_dashboard\": \"Åpne kontrollsenteret\",\n  \"orgname\": \"Firmanavn\",\n  \"original_response\": \"Opprinnelig svar\",\n  \"out_of_range_error\": \"Må være utenfor rekkevidden \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Side\",\n  \"parallel_requests\": \"Parallelle forespørsler\",\n  \"parental_control\": \"Foreldrekontroll\",\n  \"password_label\": \"Passord\",\n  \"password_placeholder\": \"Skriv inn passord\",\n  \"plain_dns\": \"Ordinær DNS\",\n  \"port_53_faq_link\": \"Port 53 er ofte opptatt av «DNSStubListener»- eller «systemd-resolved»-tjenestene. Vennligst les <0>denne instruksjonen</0> om hvordan man løser dette.\",\n  \"previous_btn\": \"Forrige\",\n  \"privacy_policy\": \"Personvernretningslinjer\",\n  \"processing_update\": \"Vennligst vent, AdGuard Home blir oppdatert\",\n  \"protection_section_label\": \"Beskyttelse\",\n  \"protocol\": \"Protokoll\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Forespørselslogg\",\n  \"query_log_clear\": \"Tøm forespørselsloggene\",\n  \"query_log_cleared\": \"Forespørselsloggen ble vellykket slettet\",\n  \"query_log_configuration\": \"Loggføringskonfigurasjon\",\n  \"query_log_confirm_clear\": \"Er du sikker på at du vil slette hele forespørselsloggen?\",\n  \"query_log_disabled\": \"Forespørselsloggen er skrudd av og kan bli satt opp i <0>innstillingene</0>\",\n  \"query_log_enable\": \"Skru på loggføring\",\n  \"query_log_filtered\": \"Filtrert av {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotasjon av forespørselsloggføringene\",\n  \"query_log_retention_confirm\": \"Er du sikker på at du vil endre hvor lenge forespørselsloggføringene skal beholdes? Hvis du reduserer den interne verdien, vil noe av dataene gå tapt\",\n  \"query_log_strict_search\": \"Bruk anførselstegn for strenge søk\",\n  \"query_log_updated\": \"Forespørselsloggen ble vellykket oppdatert\",\n  \"rate_limit\": \"Forespørselsfrekvensgrense\",\n  \"rate_limit_desc\": \"Antallet forespørsler per sekund som én enkelt klient har lov til å be om (0: ubegrenset)\",\n  \"rate_limit_subnet_len_ipv4\": \"Lengde på subnettprefiks for IPv4-adresser\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Subnet prefixlengde for IPv4-adresser som brukes til ratebegrensning. Standard er 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4 subnet prefixlengde bør være mellom 0 og 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Subnet prefixlengde for IPv6-adresser\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Subnet prefixlengde for IPv6-adresser som brukes til ratebegrensning. Standard er 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6 subnet prefixlengde bør være mellom 0 og 128\",\n  \"rate_limit_whitelist\": \"Ratebegrensnings tillatelsesliste\",\n  \"rate_limit_whitelist_desc\": \"IP-adresser unntatt fra ratebegrensning\",\n  \"rate_limit_whitelist_placeholder\": \"Skriv inn én IP-adresse per linje\",\n  \"refresh_btn\": \"Oppfrisk\",\n  \"refresh_statics\": \"Oppfrisk statistikkene\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Send inn feilrapport\",\n  \"request_details\": \"Detaljer over forespørsel\",\n  \"request_table_header\": \"Forespørsel\",\n  \"requests_count\": \"Antall forespørsler\",\n  \"reset_settings\": \"Tilbakestill innstillinger\",\n  \"resolve_clients_desc\": \"Hvis aktivert, vil AdGuard Home forsøke å automatisk løse klienters vertsnavn fra deres IP-adresser ved å sende en PTR-forespørsel til en tilsvarende resolver (privat DNS-server for lokale klienter, upstream-server for klienter med offentlig IP-adresse).\",\n  \"resolve_clients_title\": \"Skru på revers-oppleting av klienters IP-adresser\",\n  \"response_code\": \"Svarkode\",\n  \"response_details\": \"Svardetaljer\",\n  \"response_table_header\": \"Respons\",\n  \"response_time\": \"Responstid\",\n  \"rewrite_A\": \"<0>A</0>: spesialverdi, behold <0>A</0>-statutter fra oppstrømstjeneren\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: spesialverdi, behold <0>AAAA</0>-statutter fra oppstrømstjeneren\",\n  \"rewrite_add\": \"Legg til DNS-omdirigering\",\n  \"rewrite_added\": \"DNS-omdirigeringen for «{{key}}» ble vellykket lagt til\",\n  \"rewrite_applied\": \"Benyttet omdirigeringsregelen\",\n  \"rewrite_confirm_delete\": \"Er du sikker på at du vil slette DNS-omdirigeringen for «{{key}}»?\",\n  \"rewrite_deleted\": \"DNS-omdirigeringen for «{{key}}» ble vellykket slettet\",\n  \"rewrite_desc\": \"Lar deg enkelt konfigurere selvvalgte DNS-tilbakemeldinger for et spesifikt domenenavn.\",\n  \"rewrite_domain_name\": \"Domenenavn: Legg til en CNAME-statutt\",\n  \"rewrite_edit\": \"Rediger DNS-omskriving\",\n  \"rewrite_hosts_applied\": \"Omskrevet av 'hosts'-oppføringen\",\n  \"rewrite_ip_address\": \"IP-adresse: Bruk denne IP-en i en A- eller AAAA-respons\",\n  \"rewrite_not_found\": \"Ingen DNS-omdirigeringer ble funnet\",\n  \"rewrite_settings_updated\": \"DNS-omskrivingsinnstillingene er oppdatert\",\n  \"rewrite_updated\": \"DNS-omskriving ble oppdatert\",\n  \"rewrites_disabled_table_header\": \"Omskrivinger er deaktivert\",\n  \"rewrites_enabled_table_header\": \"Omskrivinger er aktivert\",\n  \"rewritten\": \"Omskrevet\",\n  \"rows_table_footer_text\": \"rekker\",\n  \"rule_added_to_custom_filtering_toast\": \"Oppføringen ble lagt til i de selvvalgte filtreringsreglene: {{rule}}\",\n  \"rule_label\": \"Oppføring\",\n  \"rule_removed_from_custom_filtering_toast\": \"Oppføringen ble fjernet fra de selvvalgte filtreringsreglene: {{rule}}\",\n  \"rules_count_table_header\": \"Antall oppføringer\",\n  \"safe_browsing\": \"Sikker surfing\",\n  \"safe_search\": \"Trygge søk\",\n  \"saturday\": \"Lørdag\",\n  \"saturday_short\": \"lør\",\n  \"save_btn\": \"Lagre\",\n  \"save_config\": \"Lagre oppsettet\",\n  \"schedule_add\": \"Legg til tidsplan\",\n  \"schedule_current_timezone\": \"Gjeldende tidssone: {{value}}\",\n  \"schedule_desc\": \"Angi inaktivitetsperioder for blokkerte tjenester\",\n  \"schedule_edit\": \"Endre tidsplan\",\n  \"schedule_from\": \"Fra\",\n  \"schedule_invalid_select\": \"Starttid må være før sluttid\",\n  \"schedule_modal_description\": \"Denne tidsplanen vil erstatte alle eksisterende tidsplaner for samme ukedag. Hver dag i uken kan bare ha én inaktivitetsperiode.\",\n  \"schedule_modal_time_off\": \"Ingen tjenesteblokkering:\",\n  \"schedule_new\": \"Ny tidsplan\",\n  \"schedule_remove\": \"Fjern tidsplanen\",\n  \"schedule_save\": \"Lagre tidsplan\",\n  \"schedule_select_days\": \"Velg dager\",\n  \"schedule_services\": \"Sett blokkering av tjenesten på pause\",\n  \"schedule_services_desc\": \"Konfigurer pauseplanen for tjenesteblokkeringsfilteret\",\n  \"schedule_services_desc_client\": \"Konfigurer pauseplanen for tjenesteblokkeringsfilteret for denne klienten\",\n  \"schedule_time_all_day\": \"Hele dagen\",\n  \"schedule_timezone\": \"Velg en tidssone\",\n  \"schedule_to\": \"Til\",\n  \"served_from_cache_label\": \"Formidlet fra mellomlageret\",\n  \"service_name\": \"Tjenestenavn\",\n  \"set_static_ip\": \"Velg en statisk IP-adresse\",\n  \"settings\": \"Innstillinger\",\n  \"settings_custom\": \"Tilpasset\",\n  \"settings_global\": \"Global\",\n  \"setup_config_to_enable_dhcp_server\": \"Oppsett for å skru på DHCP-tjeneren\",\n  \"setup_dns_notice\": \"For å benytte <1>DNS-over-HTTPS</1> eller <1>DNS-over-TLS</1>, må du <0>sette opp Kryptering</0> i AdGuard Home-innstillingene.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Benytt <1>{{address}}</1>-strengen.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Benytt <1>{{address}}</1>-strengen.\",\n  \"setup_dns_privacy_3\": \"<0>Her er en liste over programvarer du kan bruke.</0>\",\n  \"setup_dns_privacy_4\": \"På en iOS 14 eller macOS Big Sur-enhet kan du laste ned en spesiell '.mobileconfig'-fil som legger til<highlight>DNS-over-HTTPS</highlight>- eller<highlight>DNS-over-TLS</highlight>-tjenere til DNS-innstillingene.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 har innebygd støtte for DNS-over-TLS. For å sette det opp, gå til Innstillinger → Nettverk og internett → Avansert → Privat DNS, og skriv inn domenenavnet ditt der.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0> støtter <1>DNS-over-HTTPS</1> og <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> legger til <1>DNS-over-HTTPS</1>-støtte i Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS- og macOS-oppsett\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> støtter <1>DNS-over-HTTPS</1>, men for å sette det opp til å bruke din egen tjener, vil du måtte generere et <2>DNS-stempel</2> for det.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard for iOS</0> støtter <1>DNS-over-HTTPS</1>- og <1>DNS-over-TLS</1>-oppsett.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home i seg selv kan brukes som en sikker DNS-klient for enhver plattform.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> støtter alle kjente sikre DNS-protokoller.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> støtter <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> støtter <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Du finner flere implementeringer <0>her</0> og <1>her</1>.\",\n  \"setup_dns_privacy_other_title\": \"Andre implementeringer\",\n  \"setup_guide\": \"Oppsettsveiledning\",\n  \"show_all_filter_type\": \"Vis alle\",\n  \"show_blocked_responses\": \"Blokkért\",\n  \"show_filtered_type\": \"Vis kun filtrerte\",\n  \"show_processed_responses\": \"Bearbeidet\",\n  \"show_whitelisted_responses\": \"Hvitelistet\",\n  \"sign_in\": \"Logg på\",\n  \"sign_out\": \"Logg av\",\n  \"source_label\": \"Kilde\",\n  \"static_ip\": \"Statisk IP-adresse\",\n  \"static_ip_desc\": \"AdGuard Home er en tjener, så den trenger en statisk IP-adresse for å fungere ordentlig. Hvis ikke, kan ruteren din en dag kan tilegne en annen IP-adresse til denne enheten.\",\n  \"statistics_clear\": \" Tøm statistikkene\",\n  \"statistics_clear_confirm\": \"Er du sikker på at du vil slette statistikkene?\",\n  \"statistics_cleared\": \"Statistikkene ble vellykket tømt\",\n  \"statistics_configuration\": \"Statistikk-oppsett\",\n  \"statistics_enable\": \"Skru på statistikker\",\n  \"statistics_retention\": \"Statistikkbeholding\",\n  \"statistics_retention_confirm\": \"Er du sikker på at du vil endre hvor lenge statistikkene skal beholdes? Hvis du reduserer den interne verdien, vil noe av dataene gå tapt\",\n  \"statistics_retention_desc\": \"Hvis du reduserer intervallverdien, vil noen av dataene gå tapt\",\n  \"stats_adult\": \"Blokkerte voksennettsteder\",\n  \"stats_disabled\": \"Statistikkene har blitt skrudd av. Du kan skru den på fra <0>innstillingssiden</0>.\",\n  \"stats_disabled_short\": \"Statistikkene har blitt skrudd av\",\n  \"stats_malware_phishing\": \"Blokkert skadevare/phishing\",\n  \"stats_params\": \"Statistikk-oppsett\",\n  \"stats_query_domain\": \"Mest forespurte domener\",\n  \"subnet_error\": \"Adresser må være i et subnett\",\n  \"sunday\": \"Søndag\",\n  \"sunday_short\": \"søn\",\n  \"system_host_files\": \"System-'hosts'-filer\",\n  \"table_client\": \"Klient\",\n  \"table_name\": \"Navn\",\n  \"tags_desc\": \"Du kan velge stemplene som passer til klienten. Stempler kan bli inkludert i filtreringsoppføringene, og lar deg benytte dem mer nøyaktig. <0>Lær mer</0>\",\n  \"tags_title\": \"Stempler\",\n  \"test_upstream_btn\": \"Test oppstrømstilkoblinger\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Auto (basert på fargeoppsettet til enheten din)\",\n  \"theme_dark\": \"Mørkt tema\",\n  \"theme_dark_desc\": \"Mørkt tema\",\n  \"theme_light\": \"Lyst tema\",\n  \"theme_light_desc\": \"Lyst tema\",\n  \"thursday\": \"Torsdag\",\n  \"thursday_short\": \"tor\",\n  \"time_table_header\": \"Tidspunkt\",\n  \"top_blocked_domains\": \"Mest blokkerte domener\",\n  \"top_clients\": \"Vanligste klienter\",\n  \"top_upstreams\": \"Topp oppstrøms servere\",\n  \"topline_expired_certificate\": \"SSL-sertifikatet har utløpt. Oppdater <0>Krypteringsinnstillinger</0>.\",\n  \"topline_expiring_certificate\": \"Ditt SSL-sertifikat er i ferd med å utløpe. Oppdater <0>Krypteringsinnstillinger</0>.\",\n  \"tracker_source\": \"Sporerkilde\",\n  \"try_again\": \"Prøv på nytt\",\n  \"ttl_cache_validation\": \"Minimums-mellomlagringslevetidsverdien må være mindre enn eller det samme som maksverdien\",\n  \"tuesday\": \"Tirsdag\",\n  \"tuesday_short\": \"tir\",\n  \"type_table_header\": \"Type\",\n  \"unavailable_dhcp\": \"DHCP er utilgjengelig\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home kan ikke kjøre en DHCP-tjener på ditt OS\",\n  \"unblock\": \"Tillat\",\n  \"unblock_all\": \"Tillat alt\",\n  \"unblock_for_this_client_only\": \"Opphev blokkering kun for denne enheten\",\n  \"unknown_filter\": \"Ukjent filter {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} er nå tilgjengelig! <0>Klikk her</0> for mere informasjon.\",\n  \"update_failed\": \"Auto-oppdatering mislyktes. Vennligst <a>følg trinnene</a> for manuelle oppdateringer.\",\n  \"update_now\": \"Oppdater nå\",\n  \"updated_custom_filtering_toast\": \"Oppdaterte de selvvalgte filtreringsreglene\",\n  \"updated_save_search_toast\": \"Innstillinger for sikker søk oppdatert\",\n  \"updated_upstream_dns_toast\": \"Oppdaterte oppstrøms-DNS-tjenerne\",\n  \"updates_checked\": \"En ny versjon av AdGuard Home er tilgjengelig\",\n  \"updates_version_equal\": \"AdGuard Home er fullt oppdatert\",\n  \"upstream\": \"Oppstrøms server\",\n  \"upstream_dns\": \"Oppstrøms-DNS-tjenere\",\n  \"upstream_dns_cache_configuration\": \"Konfigurasjon av upstream DNS-cache\",\n  \"upstream_dns_client_desc\": \"Hvis dette feltet holdes tomt, vil AdGuard Home bruke tjenerne som er satt opp i <0>DNS-innstillingene</0>.\",\n  \"upstream_dns_configured_in_file\": \"Satt opp i {{path}}\",\n  \"upstream_dns_help\": \"Skriv inn én tjeneradresse per linje. <a>Lær mer</a> om å konfigurere oppstrøms-DNS-tjenere.\",\n  \"upstream_parallel\": \"Bruk parallele forespørsler for å få oppfarten på behandlinger, ved å forespørre til alle oppstrømstjenerne samtidig\",\n  \"upstream_timeout\": \"Oppstrøms tidsavbrudd\",\n  \"upstream_timeout_desc\": \"Spesifiserer antallet sekunder å vente på svar fra oppstrømsserveren\",\n  \"upstreams\": \"Oppstrømstjenere\",\n  \"use_adguard_browsing_sec\": \"Benytt AdGuard sin nettlesersikkerhetstjeneste\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home vil sjekke om domenet har blitt svartelistet av nettlesersikkerhetstjenesten. Den vil bruke en privatlivsvennlig søke-API til å utføre sjekken: kun en kort prefiks av domenenavnet med SHA256-salting blir sendt til tjeneren.\",\n  \"use_adguard_parental\": \"Benytt AdGuard sin foreldrekontroll-nettjeneste\",\n  \"use_adguard_parental_hint\": \"AdGuard Home vil sjekke om domenet inneholder erotisk materiale. Den benytter den samme privatlivsvennlige API-en som nettlesersikkerhetstjenesten.\",\n  \"use_private_ptr_resolvers_desc\": \"Løs PTR-, SOA- og NS-forespørslene for ARPA-domener som inneholder private IP-adresser gjennom private oppstrømsservere, DHCP, /etc/hosts osv. Hvis deaktivert, vil AdGuard Home svare på alle slike forespørsel med NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Bruk private omvendte DNS-løsere\",\n  \"use_saved_key\": \"Bruk den tidligere lagrede nøkkelen\",\n  \"username_label\": \"Brukernavn\",\n  \"username_placeholder\": \"Skriv inn brukernavn\",\n  \"validated_with_dnssec\": \"Validert med DNSSEC\",\n  \"version\": \"Versjon\",\n  \"version_request_error\": \"Oppdateringssjekken mislyktes. Vennligst sjekk internettforbindelsen din.\",\n  \"wednesday\": \"Onsdag\",\n  \"wednesday_short\": \"ons\",\n  \"whois\": \"Whois\"\n}\n"
  },
  {
    "path": "client/src/__locales/pl.json",
    "content": "{\n  \"access_allowed_desc\": \"Lista identyfikatorów CIDR, adresów IP lub <a>identyfikatorów klienta</a>. Jeśli ta lista zawiera wpisy, AdGuard Home zaakceptuje żądania tylko od tych klientów.\",\n  \"access_allowed_title\": \"Dozwoleni klienci\",\n  \"access_blocked_desc\": \"Nie należy ich mylić z filtrami. AdGuard Home usuwa zapytania DNS pasujące do tych domen, a zapytania te nie pojawiają się nawet w dzienniku zapytań. Możesz określić dokładne nazwy domen, symbole wieloznaczne lub reguły filtrowania adresów URL, np. \\\"example.org\\\", \\\"*.example.org\\\" lub \\\"||example.org^\\\".\",\n  \"access_blocked_title\": \"Niedozwolone domeny\",\n  \"access_desc\": \"Tutaj możesz skonfigurować reguły dostępu dla serwera DNS AdGuard Home\",\n  \"access_disallowed_desc\": \"Lista identyfikatorów CIDR, adresów IP lub <a>identyfikatorów klienta</a>. Jeśli ta lista zawiera wpisy, AdGuard Home odrzuci żądania od tych klientów. To pole jest ignorowane, jeśli istnieją wpisy w Dozwolonych klientach.\",\n  \"access_disallowed_title\": \"Niedozwoleni klienci\",\n  \"access_settings_saved\": \"Ustawienia dostępu zostały pomyślnie zapisane\",\n  \"access_title\": \"Ustawienia dostępu\",\n  \"actions_table_header\": \"Akcje\",\n  \"add_allowlist\": \"Dodaj listę dozwolonych\",\n  \"add_blocklist\": \"Dodaj listę zablokowanych\",\n  \"add_custom_list\": \"Dodaj listę niestandardową\",\n  \"add_persistent_client\": \"Dodaj do zapisanych klientów\",\n  \"address\": \"Adres\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home odrzuci zapytanie DNS od tego klienta.\",\n  \"all_lists_up_to_date_toast\": \"Wszystkie listy są już aktualne\",\n  \"all_queries\": \"Wszystkie zapytania\",\n  \"allow_this_client\": \"Pozwól temu klientowi\",\n  \"allowed\": \"Dozwolone\",\n  \"anonymize_client_ip\": \"Anonimizuj adres IP klienta\",\n  \"anonymize_client_ip_desc\": \"Nie zapisuj pełnego adresu IP w dziennikach i statystykach\",\n  \"anonymizer_notification\": \"<0>Uwaga:</0> Anonimizacja IP jest włączona. Możesz ją wyłączyć w <1>Ustawieniach ogólnych</1>.\",\n  \"answer\": \"Odpowiedź\",\n  \"apply_btn\": \"Zastosuj\",\n  \"auto_clients_desc\": \"Informacje o adresach IP urządzeń korzystających lub mogących korzystać z AdGuard Home. Te informacje są gromadzone z wielu źródeł takich jak pliki hosta, odwrotna translacja DNS, itp.\",\n  \"auto_clients_title\": \"Uruchomieni klienci\",\n  \"autofix_warning_list\": \"Wykona następujące zadania: <0>Dezaktywuj system DNSStubListener</0> <0>Ustaw adres serwera DNS na 127.0.0.1</0> <0>Zamień symboliczny cel łącza z /etc/resolv.conf na /run/systemd/resolve/resolv.conf</0> <0>Zatrzymaj DNSStubListener (przeładuj usługę systemową)</0>\",\n  \"autofix_warning_result\": \"W rezultacie wszystkie żądania DNS z Twojego systemu będą domyślnie przetwarzane przez AdGuardHome.\",\n  \"autofix_warning_text\": \"Jeśli klikniesz „Napraw”, AdGuardHome skonfiguruje system do korzystania z serwera DNS AdGuardHome.\",\n  \"average_processing_time\": \"Średni czas przetwarzania\",\n  \"average_processing_time_hint\": \"Średni czas przetwarzania żądania DNS liczony w milisekundach\",\n  \"average_upstream_response_time\": \"Średni czas odpowiedzi serwera nadrzędnego\",\n  \"back\": \"Wróć\",\n  \"block\": \"Zablokuj\",\n  \"block_all\": \"Zablokuj wszystko\",\n  \"block_domain_use_filters_and_hosts\": \"Zablokuj domeny za pomocą filtrów i plików host\",\n  \"block_for_this_client_only\": \"Zablokuj tylko tego klienta\",\n  \"block_services\": \"Zablokuj określone usługi\",\n  \"blocked_adult_websites\": \"Zablokowane przez Kontrolę rodzicielską\",\n  \"blocked_by\": \"<0>Zablokowane przez filtry</0>\",\n  \"blocked_by_cname_or_ip\": \"Zablokowany przez rekord CNAME lub adres IP\",\n  \"blocked_by_response\": \"W odpowiedzi zablokowany przez CNAME lub IP\",\n  \"blocked_response_ttl\": \"TTL zablokowanej odpowiedzi\",\n  \"blocked_response_ttl_desc\": \"Określa, przez ile sekund klienci powinni buforować przefiltrowaną odpowiedź\",\n  \"blocked_safebrowsing\": \"Zablokowane przez Bezpieczne przeglądanie\",\n  \"blocked_service\": \"Zablokowana usługa\",\n  \"blocked_services\": \"Zablokowane usługi\",\n  \"blocked_services_desc\": \"Pozwala szybko zablokować popularne witryny i usługi.\",\n  \"blocked_services_global\": \"Użyj globalnych zablokowanych usług\",\n  \"blocked_services_saved\": \"Zablokowane usługi zostały pomyślnie zapisane\",\n  \"blocked_threats\": \"Zablokowane zagrożenia\",\n  \"blocking_ipv4\": \"Blokowanie IPv4\",\n  \"blocking_ipv4_desc\": \"Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania A\",\n  \"blocking_ipv6\": \"Blokowanie IPv6\",\n  \"blocking_ipv6_desc\": \"Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania AAAA\",\n  \"blocking_mode\": \"Tryb blokowania\",\n  \"blocking_mode_custom_ip\": \"Niestandardowy adres IP: Odpowiedz ręcznie ustawionym adresem IP\",\n  \"blocking_mode_default\": \"Domyślna: Odpowiedz z zerowym adresem IP (0.0.0.0 dla A; :: dla AAAA) po zablokowaniu przez regułę Adblock; odpowiedź adresem IP wpisanym w regule, jeśli jest blokowany przez regułę w stylu /etc/hosts\",\n  \"blocking_mode_null_ip\": \"Null IP: Odpowiedz z zerowym adresem IP (0.0.0.0 dla A; :: dla AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Odpowiedz kodem NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: Odpowiedz kodem REFUSED\",\n  \"blocklist\": \"Lista zablokowanych\",\n  \"bootstrap_dns\": \"Serwery DNS Bootstrap\",\n  \"bootstrap_dns_desc\": \"Adresy IP serwerów DNS używanych do rozpoznawania adresów IP programów rozpoznawania nazw DoH/DoT określonych jako nadrzędne. Komentarze są niedozwolone.\",\n  \"cache_cleared\": \"Pamięć podręczna DNS została pomyślnie wyczyszczona\",\n  \"cache_enabled\": \"Aktywuj pamięć podręczną\",\n  \"cache_enabled_desc\": \"Przechowuj odpowiedź DNS lokalnie.\",\n  \"cache_optimistic\": \"Optymistyczne buforowanie\",\n  \"cache_optimistic_desc\": \"Spraw, aby AdGuard Home odpowiadał z pamięci podręcznej, nawet gdy wpisy wygasły, a także spróbuj je odświeżyć.\",\n  \"cache_size\": \"Rozmiar pamięci podręcznej\",\n  \"cache_size_desc\": \"Rozmiar pamięci podręcznej DNS (w bajtach).\",\n  \"cache_size_validation\": \"Rozmiar pamięci podręcznej cache musi być większy od zera, gdy jest włączona.\",\n  \"cache_ttl_max_override\": \"Nadpisz maksymalną wartość TTL\",\n  \"cache_ttl_max_override_desc\": \"Ustaw maksymalną wartość czasu życia (w sekundach) dla wpisów w pamięci podręcznej DNS.\",\n  \"cache_ttl_min_override\": \"Nadpisz minimalną wartość TTL\",\n  \"cache_ttl_min_override_desc\": \"Przedłuż najkrótszą wartość TTL (w sekundach) otrzymaną od serwera wychodzącego podczas buforowania odpowiedzi DNS.\",\n  \"cancel_btn\": \"Anuluj\",\n  \"category_label\": \"Kategoria\",\n  \"check\": \"Sprawdź\",\n  \"check_client_id\": \"Identyfikator klienta (ClientID lub Adres IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Sprawdź, czy nazwa hosta jest filtrowana.\",\n  \"check_dhcp_servers\": \"Sprawdź serwery DHCP\",\n  \"check_dns_record\": \"Wybierz typ rekordu DNS\",\n  \"check_enter_client_id\": \"Wprowadź identyfikator klienta\",\n  \"check_hostname\": \"Nazwa hosta lub nazwa domeny\",\n  \"check_ip\": \"Adresy IP: {{ip}}\",\n  \"check_not_found\": \"Nie znaleziono na Twoich listach filtrów\",\n  \"check_reason\": \"Powód: {{reason}}\",\n  \"check_service\": \"Nazwa usługi: {{service}}\",\n  \"check_title\": \"Sprawdź filtrowanie\",\n  \"check_updates_btn\": \"Sprawdź aktualizacje\",\n  \"check_updates_now\": \"Sprawdź aktualizacje teraz\",\n  \"choose_allowlist\": \"Wybierz listy dozwolonych\",\n  \"choose_blocklist\": \"Wybierz listy zablokowanych\",\n  \"choose_from_list\": \"Wybierz z listy\",\n  \"city\": \"Miasto\",\n  \"clear_cache\": \"Wyczyść pamięć podręczną\",\n  \"click_to_view_queries\": \"Kliknij, aby wyświetlić zapytania\",\n  \"client_add\": \"Dodaj klienta\",\n  \"client_added\": \"Klient \\\"{{key}}\\\" został pomyślnie dodany\",\n  \"client_blocked\": \"Klient \\\"{{ip}}\\\" został pomyślnie zablokowany\",\n  \"client_confirm_block\": \"Czy na pewno chcesz zablokować klienta \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Czy na pewno chcesz usunąć klienta \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Czy na pewno chcesz odblokować klienta \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Klient \\\"{{key}}\\\" został pomyślnie usunięty\",\n  \"client_details\": \"Szczegóły klienta\",\n  \"client_edit\": \"Edytuj klienta\",\n  \"client_global_settings\": \"Użyj ustawień globalnych\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Klienci mogą być identyfikowani przez ClientID. Dowiedz się więcej o tym, jak identyfikować klientów <a>tutaj</a>.\",\n  \"client_id_placeholder\": \"Wpisz ClientID\",\n  \"client_identifier\": \"Identyfikator\",\n  \"client_identifier_desc\": \"Klienci mogą być identyfikowani na podstawie ich adresu IP, CIDR, adresu MAC lub ClientID (może być używany do DoT/DoH/DoQ). Dowiedz się więcej o tym, jak identyfikować klientów <0>tutaj</0>.\",\n  \"client_name\": \"Klient {{id}}\",\n  \"client_new\": \"Nowy klient\",\n  \"client_settings\": \"Ustawienia klienta\",\n  \"client_table_header\": \"Klient\",\n  \"client_unblocked\": \"Klient \\\"{{ip}}\\\" został pomyślnie odblokowany\",\n  \"client_updated\": \"Klient \\\"{{key}}\\\" został pomyślnie zaktualizowany\",\n  \"clients_desc\": \"Skonfiguruj trwałe rekordy klienta dla urządzeń podłączonych do AdGuard Home\",\n  \"clients_not_found\": \"Nie znaleziono klientów\",\n  \"clients_title\": \"Trwali klienci\",\n  \"compact\": \"Kompaktowy\",\n  \"config_successfully_saved\": \"Konfiguracja została pomyślnie zapisana\",\n  \"configure\": \"Skonfiguruj\",\n  \"confirm_dns_cache_clear\": \"Czy na pewno chcesz wyczyścić pamięć podręczną DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home skonfiguruje {{ip}} aby był Twoim statycznym adresem IP. Czy chcesz kontynuować?\",\n  \"copyright\": \"Prawo autorskie\",\n  \"country\": \"Kraj\",\n  \"custom_filter_rules\": \"Niestandardowe reguły filtrowania\",\n  \"custom_filter_rules_hint\": \"Wpisz jedną regułę w jednej linii. Możesz użyć reguł adblock lub składni plików hostów.\",\n  \"custom_filtering_rules\": \"Niestandardowe reguły filtrowania\",\n  \"custom_ip\": \"Niestandardowy adres IP\",\n  \"custom_retention_input\": \"Wprowadź retencję w godzinach\",\n  \"custom_rotation_input\": \"Wprowadź rotację w godzinach\",\n  \"dashboard\": \"Panel kontrolny\",\n  \"date\": \"Data\",\n  \"default\": \"Domyślny\",\n  \"delete_confirm\": \"Czy na pewno chcesz usunąć \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Usuń\",\n  \"descr\": \"Opis\",\n  \"details\": \"Szczegóły\",\n  \"dhcp_add_static_lease\": \"Dodaj dzierżawę statyczną\",\n  \"dhcp_config_saved\": \"Konfiguracja DHCP została pomyślnie zapisana\",\n  \"dhcp_description\": \"Jeśli router nie zapewnia ustawień DHCP, możesz użyć wbudowanego serwera DHCP AdGuard.\",\n  \"dhcp_disable\": \"Wyłącz serwer DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Twój system używa dynamicznej konfiguracji adresu IP dla interfejsu <0>{{interfaceName}}</0>. Aby można było korzystać z serwera DHCP, należy ustawić statyczny adres IP. Twój obecny adres IP to <0>{{ipAddress}}</0>. AdGuard Home automatycznie ustawi ten adres IP jako statyczny, jeśli naciśniesz przycisk \\\"Włącz serwer DHCP\\\".\",\n  \"dhcp_edit_static_lease\": \"Edytuj dzierżawę statyczną\",\n  \"dhcp_enable\": \"Włącz serwer DHCP\",\n  \"dhcp_error\": \"AdGuard Home nie mógł określić, czy w sieci jest inny aktywny serwer DHCP\",\n  \"dhcp_form_gateway_input\": \"Adres IP bramy\",\n  \"dhcp_form_lease_input\": \"Czas trwania dzierżawy\",\n  \"dhcp_form_lease_title\": \"Czas dzierżawy DHCP (w sekundach)\",\n  \"dhcp_form_range_end\": \"Koniec zakresu\",\n  \"dhcp_form_range_start\": \"Początek zakresu\",\n  \"dhcp_form_range_title\": \"Zakres adresów IP\",\n  \"dhcp_form_subnet_input\": \"Maska podsieci\",\n  \"dhcp_found\": \"W sieci został znaleziony aktywny serwer DHCP. Włączenie wbudowanego serwera DHCP nie jest bezpieczne.\",\n  \"dhcp_hardware_address\": \"Adres sprzętowy\",\n  \"dhcp_interface_select\": \"Wybierz interfejs DHCP\",\n  \"dhcp_ip_addresses\": \"Adresy IP\",\n  \"dhcp_ipv4_settings\": \"Ustawienia serwera DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Ustawienia serwera DHCP IPv6\",\n  \"dhcp_lease_added\": \"Dzierżawa statyczna \\\"{{key}}\\\" pomyślnie dodana\",\n  \"dhcp_lease_deleted\": \"Dzierżawa statyczna \\\"{{key}}\\\" pomyślnie usunięta\",\n  \"dhcp_lease_updated\": \"Dzierżawa statyczna \\\"{{key}}\\\" pomyślnie zaktualizowana\",\n  \"dhcp_leases\": \"Dzierżawa DHCP\",\n  \"dhcp_leases_not_found\": \"Nie znaleziono dzierżaw DHCP\",\n  \"dhcp_new_static_lease\": \"Nowa dzierżawa statyczna\",\n  \"dhcp_not_found\": \"Włączenie wbudowanego serwera DHCP jest bezpieczne, ponieważ AdGuard Home nie znalazł żadnych aktywnych serwerów DHCP w sieci. Powinieneś jednak ponownie sprawdzić to ręcznie, ponieważ automatyczne sondowanie nie daje obecnie 100% gwarancji.\",\n  \"dhcp_reset\": \"Czy na pewno chcesz zresetować konfigurację DHCP?\",\n  \"dhcp_reset_leases\": \"Zresetuj wszystkie dzierżawy\",\n  \"dhcp_reset_leases_confirm\": \"Czy na pewno chcesz zresetować wszystkie dzierżawy?\",\n  \"dhcp_reset_leases_success\": \"Pomyślnie zresetowano dzierżawy DHCP\",\n  \"dhcp_settings\": \"Ustawienia DHCP\",\n  \"dhcp_static_ip_error\": \"Aby korzystać z serwera DHCP musi być ustawiony statyczny adres IP. AdGuard Home nie udało się ustalić, czy ten interfejs sieciowy jest skonfigurowany przy użyciu statycznego adresu IP. Proszę ustawić statyczny adres IP ręcznie.\",\n  \"dhcp_static_leases\": \"Dzierżawy statyczne DHCP\",\n  \"dhcp_static_leases_not_found\": \"Nie znaleziono statycznych dzierżaw DHCP\",\n  \"dhcp_table_expires\": \"Wygasa\",\n  \"dhcp_table_hostname\": \"Nazwa hosta\",\n  \"dhcp_title\": \"Serwer DHCP \",\n  \"dhcp_warning\": \"Jeśli mimo wszystko chcesz włączyć serwer DHCP, upewnij się, że w Twojej sieci nie ma innego aktywnego serwera DHCP, ponieważ może to spowodować przerwanie łączności z Internetem dla urządzeń w sieci!\",\n  \"disable_for_hours\": \"Na {{count}} godzinę\",\n  \"disable_for_hours_plural\": \"Na {{count}} godziny\",\n  \"disable_for_minutes\": \"Na {{count}} minutę\",\n  \"disable_for_minutes_plural\": \"Na {{count}} minut\",\n  \"disable_for_seconds\": \"Na {{count}} sekundę\",\n  \"disable_for_seconds_plural\": \"Na {{count}} sekund\",\n  \"disable_ipv6\": \"Wyłącz rozwiązywanie adresów IPv6\",\n  \"disable_ipv6_desc\": \"Ignorować wszystkie zapytania DNS dotyczące adresów IPv6 (typ AAAA) i usuwać dane IPv6 z odpowiedzi HTTPS.\",\n  \"disable_notify_for_hours\": \"Wyłącz ochronę na {{count}} godzinę\",\n  \"disable_notify_for_hours_plural\": \"Wyłącz ochronę na {{count}} godziny\",\n  \"disable_notify_for_minutes\": \"Wyłącz ochronę na {{count}} minutę\",\n  \"disable_notify_for_minutes_plural\": \"Wyłącz ochronę na {{count}} minut\",\n  \"disable_notify_for_seconds\": \"Wyłącz ochronę na {{count}} sekundę\",\n  \"disable_notify_for_seconds_plural\": \"Wyłącz ochronę na {{count}} sekund\",\n  \"disable_notify_until_tomorrow\": \"Wyłącz ochronę do jutra\",\n  \"disable_protection\": \"Wyłącz ochronę\",\n  \"disable_rewrites\": \"Wyłącz reguły przepisywania\",\n  \"disable_until_tomorrow\": \"Do jutra\",\n  \"disabled\": \"Wyłączone\",\n  \"disabled_dhcp\": \"Serwer DHCP wyłączony\",\n  \"disabled_filtering_toast\": \"Wyłączone filtrowanie\",\n  \"disabled_parental_toast\": \"Wyłączona Kontrola Rodzicielska\",\n  \"disabled_protection\": \"Ochrona wyłączona \",\n  \"disabled_safe_browsing_toast\": \"Wyłączone Bezpieczne przeglądanie\",\n  \"disabled_safe_search_toast\": \"Wyłączone bezpieczne wyszukiwanie\",\n  \"disallow_this_client\": \"Odrzuć tego klienta\",\n  \"dns_addresses\": \"Adresy DNS\",\n  \"dns_allowlists\": \"Listy dozwolonych DNS\",\n  \"dns_allowlists_desc\": \"Domeny z listy dozwolonych DNS będą dozwolone, nawet jeśli znajdują się na jednej z zablokowanych list.\",\n  \"dns_blocklists\": \"Listy zablokowanych DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home zablokuje domeny pasujące do listy zablokowanych.\",\n  \"dns_cache_config\": \"Konfiguracja pamięci podręcznej DNS\",\n  \"dns_cache_config_desc\": \"Tutaj możesz skonfigurować pamięć podręczną DNS\",\n  \"dns_cache_size\": \"Rozmiar pamięci podręcznej DNS, w bajtach\",\n  \"dns_config\": \"Konfiguracja serwera DNS\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"Prywatny DNS\",\n  \"dns_providers\": \"Oto lista <0>znanych dostawców DNS</0> do wyboru.\",\n  \"dns_query\": \"Zapytania DNS\",\n  \"dns_rewrites\": \"Przepisywanie DNS\",\n  \"dns_settings\": \"Ustawienia DNS\",\n  \"dns_start\": \"Serwer DNS uruchamia się\",\n  \"dns_status_error\": \"Błąd podczas sprawdzania stanu serwera DNS\",\n  \"dns_test_not_ok_toast\": \"Serwer \\\"{{key}}\\\": nie może być użyte, sprawdź, czy zapisano go poprawnie\",\n  \"dns_test_ok_toast\": \"Określone serwery DNS działają poprawnie\",\n  \"dns_test_parsing_error_toast\": \"Sekcja {{section}}: linia {{line}}: nie może być użyte, sprawdź, czy zapisano go poprawnie\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" nie odpowiada na zapytania testowe i może nie działać prawidłowo\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Włącz DNSSEC\",\n  \"dnssec_enable_desc\": \"Ustaw flagę DNSSEC w wychodzących zapytaniach DNS i sprawdź wynik (wymagany jest resolver z obsługą DNSSEC).\",\n  \"domain\": \"Domena\",\n  \"domain_desc\": \"Wpisz nazwę domeny lub symbol wieloznaczny, który chcesz przepisać.\",\n  \"domain_name_table_header\": \"Nazwa domeny\",\n  \"domain_or_client\": \"Domena lub klient\",\n  \"down\": \"Utrata połączenia\",\n  \"download_mobileconfig\": \"Pobierz plik konfiguracyjny\",\n  \"download_mobileconfig_doh\": \"Pobierz plik .mobileconfig dla DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Pobierz plik .mobileconfig dla DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Edytuj listę dozwolonych\",\n  \"edit_blocklist\": \"Edytuj listę zablokowanych\",\n  \"edit_table_action\": \"Edytuj\",\n  \"edns_cs_desc\": \"Dodaj opcję podsieci klienta EDNS (ECS) do żądań nadrzędnych i rejestruj wartości wysyłane przez klientów w dzienniku zapytań.\",\n  \"edns_enable\": \"Włącz podsieć klienta EDNS\",\n  \"edns_use_custom_ip\": \"Użyj niestandardowego adresu IP dla EDNS\",\n  \"edns_use_custom_ip_desc\": \"Zezwól na użycie niestandardowego adresu IP dla EDNS\",\n  \"elapsed\": \"Upłynęło\",\n  \"empty_response_status\": \"Pusty\",\n  \"enable_protection\": \"Włącz ochronę\",\n  \"enable_protection_timer\": \"Ochrona zostanie włączona za {{time}}\",\n  \"enable_rewrites\": \"Włącz reguły przepisywania\",\n  \"enable_upstream_dns_cache\": \"Włącz pamięć podręczną dla niestandardowej konfiguracji serwera upstream tego klienta\",\n  \"enabled_dhcp\": \"Serwer DHCP włączony\",\n  \"enabled_filtering_toast\": \"Włączone filtrowanie\",\n  \"enabled_parental_toast\": \"Włączona Kontrola Rodzicielska\",\n  \"enabled_protection\": \"Ochrona włączona \",\n  \"enabled_safe_browsing_toast\": \"Włączone Bezpieczne przeglądanie\",\n  \"enabled_save_search_toast\": \"Włączone bezpieczne wyszukiwanie\",\n  \"enabled_table_header\": \"Włączone\",\n  \"encryption_certificate_path\": \"Ścieżka certyfikatu\",\n  \"encryption_certificates\": \"Certyfikaty\",\n  \"encryption_certificates_desc\": \"Aby korzystać z szyfrowania, musisz podać prawidłowy łańcuch certyfikatów SSL dla swojej domeny. Możesz uzyskać bezpłatny certyfikat na  <0>{{link}}</0> lub możesz go kupić od jednego z zaufanych urzędów certyfikacji.\",\n  \"encryption_certificates_input\": \"Kopiuj/wklej tutaj swoje zakodowane certyfikaty PEM.\",\n  \"encryption_certificates_source_content\": \"Wklej zawartość certyfikatów\",\n  \"encryption_certificates_source_path\": \"Ustaw ścieżkę do pliku certyfikatów\",\n  \"encryption_chain_invalid\": \"Łańcuch certyfikatu jest nieprawidłowy\",\n  \"encryption_chain_valid\": \"Łańcuch certyfikatów jest prawidłowy\",\n  \"encryption_config_saved\": \"Konfiguracja szyfrowania została zapisana\",\n  \"encryption_desc\": \"Obsługa szyfrowania (HTTPS/TLS) dla interfejsu sieciowego DNS i administratora\",\n  \"encryption_doq\": \"Port DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"Jeśli ten port jest skonfigurowany, AdGuard Home uruchomi serwer DNS-over-QUIC na tym porcie.\",\n  \"encryption_dot\": \"Port DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Jeśli ten port jest skonfigurowany, AdGuard Home uruchomi serwer DNS-over-TLS na tym porcie.\",\n  \"encryption_enable\": \"Włącz szyfrowanie (HTTPS, DNS-over-HTTPS i DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Jeśli szyfrowanie jest włączone, interfejs administracyjny AdGuard Home będzie działał przez HTTPS, a serwer DNS będzie nasłuchiwał żądań przez DNS-over-HTTPS i DNS-over-TLS.\",\n  \"encryption_expire\": \"Wygasa\",\n  \"encryption_hostnames\": \"Nazwy hostów\",\n  \"encryption_https\": \"Port HTTPS\",\n  \"encryption_https_desc\": \"Jeśli port HTTPS jest skonfigurowany, interfejs administratora AdGuard Home będzie dostępny za pośrednictwem protokołu HTTPS i zapewni DNS przez HTTPS w lokalizacji zapytania '/dns-query'.\",\n  \"encryption_issuer\": \"Zgłaszający\",\n  \"encryption_key\": \"Klucz prywatny\",\n  \"encryption_key_input\": \"Tutaj kopiuj/wklej klucze prywatne zakodowane w PEM do swojego certyfikatu.\",\n  \"encryption_key_invalid\": \"Nieprawidłowy {{type}} klucz prywatny\",\n  \"encryption_key_source_content\": \"Wklej zawartość klucza prywatnego\",\n  \"encryption_key_source_path\": \"Ustaw ścieżkę pliku klucza prywatnego\",\n  \"encryption_key_valid\": \"Poprawny {{type}} klucz prywatny\",\n  \"encryption_plain_dns_desc\": \"Zwykły DNS jest domyślnie włączony. Możesz go wyłączyć, aby zmusić wszystkie urządzenia do korzystania z szyfrowanego DNS. Aby to zrobić, musisz włączyć co najmniej jeden szyfrowany protokół DNS\",\n  \"encryption_plain_dns_enable\": \"Włącz zwykły DNS\",\n  \"encryption_plain_dns_error\": \"Aby wyłączyć zwykły DNS, włącz co najmniej jeden szyfrowany protokół DNS\",\n  \"encryption_private_key_path\": \"Ścieżka klucza prywatnego\",\n  \"encryption_redirect\": \"Przekieruj automatycznie do HTTPS\",\n  \"encryption_redirect_desc\": \"Jeśli zaznaczone, AdGuard Home automatycznie przekieruje Cię z adresów HTTP na HTTPS.\",\n  \"encryption_reset\": \"Czy na pewno chcesz zresetować ustawienia szyfrowania?\",\n  \"encryption_server\": \"Nazwa serwera\",\n  \"encryption_server_desc\": \"Jeśli jest ustawiony, AdGuard Home wykrywa ClientID, odpowiada na zapytania DDR i wykonuje dodatkowe walidacje połączeń. Jeśli nie jest ustawiony, funkcje te są wyłączone. Musi odpowiadać jednej z nazw DNS w certyfikacie.\",\n  \"encryption_server_enter\": \"Wpisz swoją nazwę domeny\",\n  \"encryption_settings\": \"Ustawienia szyfrowania\",\n  \"encryption_status\": \"Stan\",\n  \"encryption_subject\": \"Temat\",\n  \"encryption_title\": \"Szyfrowanie\",\n  \"encryption_warning\": \"Ostrzeżenie\",\n  \"enforce_safe_search\": \"Użyj bezpiecznego wyszukiwania\",\n  \"enforce_save_search_hint\": \"AdGuard Home wymusza bezpieczne wyszukiwanie w następujących wyszukiwarkach: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Wymuszone bezpieczne wyszukiwanie\",\n  \"enter_cache_size\": \"Wpisz rozmiar pamięci podręcznej (w bajtach)\",\n  \"enter_cache_ttl_max_override\": \"Wpisz maksymalną wartość TTL (w sekundach)\",\n  \"enter_cache_ttl_min_override\": \"Wpisz minimalną wartość TTL (w sekundach)\",\n  \"enter_name_hint\": \"Wpisz nazwę\",\n  \"enter_url_or_path_hint\": \"Wpisz adres URL lub bezwzględną ścieżkę listy\",\n  \"enter_valid_allowlist\": \"Wpisz prawidłowy adres URL do listy dozwolonych.\",\n  \"enter_valid_blocklist\": \"Wpisz prawidłowy adres URL do listy zablokowanych.\",\n  \"error_details\": \"Szczegóły błędu\",\n  \"example_comment\": \"! Tutaj jest komentarz.\",\n  \"example_comment_hash\": \"# Również komentarz.\",\n  \"example_comment_meaning\": \"komentarz;\",\n  \"example_meaning_filter_block\": \"zablokuj dostęp do domeny example.org i wszystkich jej subdomen;\",\n  \"example_meaning_filter_whitelist\": \"odblokuj dostęp do domeny example.org i wszystkich jej subdomen;\",\n  \"example_meaning_host_block\": \"odpowiedz 127.0.0.1 na example.org (ale nie dla jego subdomen);\",\n  \"example_multiple_upstreams_reserved\": \"wiele serwerów nadrzędnych <0>dla konkretnej domeny</0>;\",\n  \"example_regex_meaning\": \"zablokuj dostęp do domen pasujących do określonego wyrażenia regularnego.\",\n  \"example_rewrite_domain\": \"przepisz odpowiedzi tylko dla tej nazwy domeny.\",\n  \"example_rewrite_wildcard\": \"przepisz odpowiedzi dla wszystkich subdomen <0>example.org</0>.\",\n  \"example_upstream_comment\": \"komentarz.\",\n  \"example_upstream_doh\": \"zaszyfrowany <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"szyfrowany DNS-over-HTTPS z wymuszonym <0>HTTP/3</0> i nie ma powrotu do HTTP/2 lub niższego;\",\n  \"example_upstream_doq\": \"zaszyfrowany <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"zaszyfrowany <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"normalny DNS (przez UDP);\",\n  \"example_upstream_regular_port\": \"zwykły DNS (przez UDP, z portem);\",\n  \"example_upstream_reserved\": \"upstream <0>dla określonych domen</0>;\",\n  \"example_upstream_sdns\": \"<0>Stempel DNS</0> dla resolwerów <1>DNSCrypt</1> lub <2>DNS-over-HTTPS</2>;\",\n  \"example_upstream_tcp\": \"zwykły DNS (przez TCP);\",\n  \"example_upstream_tcp_hostname\": \"zwykły DNS (przez TCP, nazwa hosta);\",\n  \"example_upstream_tcp_port\": \"zwykły DNS (przez TCP, z portem);\",\n  \"example_upstream_udp\": \"zwykły DNS (przez UDP, nazwa hosta);\",\n  \"examples_title\": \"Przykłady\",\n  \"fallback_dns_desc\": \"Lista rezerwowych serwerów DNS używanych, gdy nadrzędne serwery DNS nie odpowiadają. Składnia jest taka sama jak w głównym polu powyżej.\",\n  \"fallback_dns_placeholder\": \"Wprowadź jeden rezerwowy serwer DNS w każdym wierszu\",\n  \"fallback_dns_title\": \"Rezerwowe serwery DNS\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Najszybszy adres IP\",\n  \"fastest_addr_desc\": \"Poczekaj na odpowiedzi od <b>wszystkich</b> serwerów DNS, zmierz prędkość połączenia TCP dla każdego serwera i zwróć adres IP serwera z najszybszym połączeniem.<br/>Ten tryb może znacznie spowolnić zapytania DNS, jeśli jeden lub więcej serwerów nadrzędnych nie odpowiada. Upewnij się, że Twoje serwery nadrzędne są stabilne, a czas oczekiwania na odpowiedź jest krótki.\",\n  \"filter\": \"Filtr\",\n  \"filter_added_successfully\": \"Lista została pomyślnie dodana\",\n  \"filter_allowlist\": \"OSTRZEŻENIE: To działanie spowoduje również wykluczenie reguły \\\"{{disallowed_rule}}\\\" z listy dozwolonych klientów.\",\n  \"filter_category_general\": \"Ogólne\",\n  \"filter_category_general_desc\": \"Listy, które blokują skrypty śledzące i reklamy na większości urządzeń\",\n  \"filter_category_other\": \"Inne\",\n  \"filter_category_other_desc\": \"Inne listy zablokowanych\",\n  \"filter_category_regional\": \"Regionalne\",\n  \"filter_category_regional_desc\": \"Listy, które koncentrują się na reklamach regionalnych i serwerach ze skryptami śledzącymi\",\n  \"filter_category_security\": \"Bezpieczeństwo\",\n  \"filter_category_security_desc\": \"Listy zaprojektowane specjalnie w celu blokowania złośliwych, phishingowych i oszukańczych domen\",\n  \"filter_removed_successfully\": \"Lista została usunięta\",\n  \"filter_updated\": \"Filtr został pomyślnie zaktualizowany\",\n  \"filtered\": \"Filtrowane\",\n  \"filtered_custom_rules\": \"Filtrowane według niestandardowych reguł filtrowania\",\n  \"filtering_rules_learn_more\": \"<0>Dowiedz się więcej</0> o tworzeniu własnych list blokowania hostów.\",\n  \"filters\": \"Filtry\",\n  \"filters_and_hosts_hint\": \"AdGuard Home rozumie podstawowe reguły adblocka i składnię plików hostów.\",\n  \"filters_block_toggle_hint\": \"Możesz skonfigurować reguły blokowania w ustawieniach <a>Filtry</a>.\",\n  \"filters_configuration\": \"Konfiguracja filtrów\",\n  \"filters_enable\": \"Włącz filtry\",\n  \"filters_interval\": \"Aktualizuj filtry co\",\n  \"fix\": \"Napraw\",\n  \"for_last_days\": \"za ostatni {{count}} dzień\",\n  \"for_last_days_plural\": \"z ostatnich {{count}} dni\",\n  \"for_last_hours\": \"w ciągu ostatniej {{count}} godziny\",\n  \"for_last_hours_plural\": \"w ciągu ostatnich {{count}} godzin\",\n  \"forgot_password\": \"Zapomniano hasła?\",\n  \"forgot_password_desc\": \"Wykonaj <0>te kroki</0>, aby utworzyć nowe hasło do konta użytkownika.\",\n  \"form_add_id\": \"Dodaj identyfikator\",\n  \"form_answer\": \"Wpisz adres IP lub nazwę domeny\",\n  \"form_client_name\": \"Wpisz nazwę klienta\",\n  \"form_domain\": \"Wpisz nazwę domeny lub symbol wieloznaczny\",\n  \"form_enter_blocked_response_ttl\": \"Wprowadź TTL zablokowanej odpowiedzi (sekundy)\",\n  \"form_enter_host\": \"Wpisz nazwę hosta\",\n  \"form_enter_hostname\": \"Wpisz nazwę hosta\",\n  \"form_enter_id\": \"Wpisz identyfikator\",\n  \"form_enter_ip\": \"Wpisz adres IP\",\n  \"form_enter_mac\": \"Wpisz adres MAC\",\n  \"form_enter_rate_limit\": \"Wpisz limit ilościowy\",\n  \"form_enter_rate_limit_subnet_len\": \"Wprowadź długość prefiksu podsieci dla ograniczenia prędkości\",\n  \"form_enter_subnet_ip\": \"Wprowadź adres IP w podsieci \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Wprowadź czas oczekiwania na odpowiedź od serwera nadrzędnego w sekundach\",\n  \"form_error_answer_format\": \"Nieprawidłowy format odpowiedzi\",\n  \"form_error_client_id_format\": \"ClientID musi zawierać tylko cyfry, małe litery i myślniki\",\n  \"form_error_domain_format\": \"Niepoprawny format domeny\",\n  \"form_error_equal\": \"Nie mogą być równe\",\n  \"form_error_gateway_ip\": \"Lease nie może mieć adresu IP bramy\",\n  \"form_error_ip4_format\": \"Nieprawidłowy adres IPv4\",\n  \"form_error_ip4_gateway_format\": \"Nieprawidłowy adres IPv4 bramy\",\n  \"form_error_ip6_format\": \"Nieprawidłowy adres IPv6\",\n  \"form_error_ip_format\": \"Nieprawidłowy adres IP\",\n  \"form_error_mac_format\": \"Nieprawidłowy adres MAC\",\n  \"form_error_password\": \"Niezgodne hasło\",\n  \"form_error_password_length\": \"Hasło musi zawierać od {{min}} do {{max}} znaków\",\n  \"form_error_port\": \"Wprowadź prawidłowy numer portu\",\n  \"form_error_port_range\": \"Wpisz numer portu z zakresu 80-65535\",\n  \"form_error_port_unsafe\": \"Niebezpieczny port\",\n  \"form_error_positive\": \"Musi być większa niż 0\",\n  \"form_error_required\": \"Pole wymagane\",\n  \"form_error_server_name\": \"Nieprawidłowa nazwa serwera\",\n  \"form_error_subnet\": \"Podsieć \\\"{{cidr}}\\\" nie zawiera adresu IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Nieprawidłowy format URL\",\n  \"form_error_url_or_path_format\": \"Nieprawidłowy adres URL lub bezwzględna ścieżka listy\",\n  \"form_select_tags\": \"Wybierz tagi klienta\",\n  \"found_in_known_domain_db\": \"Znaleziono w bazie danych znanych domen.\",\n  \"friday\": \"Piątek\",\n  \"friday_short\": \"Pt\",\n  \"gateway_or_subnet_invalid\": \"Nieprawidłowa maska podsieci\",\n  \"general_settings\": \"Ustawienia główne\",\n  \"general_statistics\": \"Ogólne statystyki\",\n  \"get_started\": \"Zaczynamy\",\n  \"greater_range_start_error\": \"Musi być większy niż początek zakresu\",\n  \"homepage\": \"Strona główna\",\n  \"host_whitelisted\": \"Host znajduje się na białej liście\",\n  \"ignore_domains\": \"Ignorowane domeny (każda w nowym wierszu)\",\n  \"ignore_domains_desc_query\": \"Zapytania pasujące do tych reguł nie są zapisywane w dzienniku zapytań\",\n  \"ignore_domains_desc_stats\": \"Zapytania pasujące do tych reguł nie są zapisywane w statystykach\",\n  \"ignore_domains_title\": \"Ignorowane domeny\",\n  \"ignore_query_log\": \"Zignoruj tego klienta w dzienniku zapytań\",\n  \"ignore_statistics\": \"Ignoruj tego klienta w statystykach\",\n  \"install_auth_confirm\": \"Potwierdź hasło\",\n  \"install_auth_desc\": \"Należy skonfigurować uwierzytelnianie hasłem do interfejsu internetowego administratora AdGuard Home. Nawet jeśli AdGuard Home jest dostępny tylko w sieci lokalnej, nadal ważne jest, aby chronić go przed nieograniczonym dostępem.\",\n  \"install_auth_password\": \"Hasło\",\n  \"install_auth_password_enter\": \"Wpisz hasło\",\n  \"install_auth_title\": \"Uwierzytelnianie\",\n  \"install_auth_username\": \"Nazwa użytkownika\",\n  \"install_auth_username_enter\": \"Wpisz nazwę użytkownika\",\n  \"install_devices_address\": \"Serwer DNS AdGuard Home używa następujących adresów\",\n  \"install_devices_android_list_1\": \"Na ekranie głównym Menu systemu Android stuknij Ustawienia.\",\n  \"install_devices_android_list_2\": \"Dotknij Wi-Fi w menu. Zostanie wyświetlony ekran z listą wszystkich dostępnych sieci (nie można ustawić niestandardowego DNS dla połączenia komórkowego).\",\n  \"install_devices_android_list_3\": \"Długo naciśnij sieć, do której jesteś podłączony, i dotknij Modyfikuj sieć.\",\n  \"install_devices_android_list_4\": \"W przypadku niektórych urządzeń może być konieczne zaznaczenie pola Zaawansowane, aby wyświetlić dalsze ustawienia. Aby dostosować ustawienia DNS Android, musisz zmienić ustawienia IP z DHCP na Statyczny.\",\n  \"install_devices_android_list_5\": \"Zmień wartości DNS 1 i DNS 2 na adresy serwerów AdGuard Home.\",\n  \"install_devices_desc\": \"Aby usługa AdGuard Home mogła zacząć działać, musisz skonfigurować urządzenia, aby z niej korzystać.\",\n  \"install_devices_ios_list_1\": \"Na ekranie głównym stuknij Ustawienia.\",\n  \"install_devices_ios_list_2\": \"Wybierz Wi-Fi w lewym menu (nie można skonfigurować DNS dla sieci komórkowych).\",\n  \"install_devices_ios_list_3\": \"Stuknij w nazwę aktualnie aktywnej sieci.\",\n  \"install_devices_ios_list_4\": \"W polu DNS wpisz adresy serwerów AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Kliknij ikonę Apple i przejdź do Preferencje systemowe.\",\n  \"install_devices_macos_list_2\": \"Kliknij Sieć.\",\n  \"install_devices_macos_list_3\": \"Wybierz pierwsze połączenie z listy i kliknij Zaawansowane.\",\n  \"install_devices_macos_list_4\": \"Wybierz kartę DNS i wprowadź adresy serwerów AdGuard Home.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Ta konfiguracja automatycznie obejmuje wszystkie urządzenia podłączone do routera domowego, bez konieczności ręcznego konfigurowania każdego z nich.\",\n  \"install_devices_router_list_1\": \"Otwórz preferencje routera. Zazwyczaj można uzyskać do nich dostęp z przeglądarki za pośrednictwem adresu URL, takiego jak http://192.168.0.1/ lub http://192.168.1.1/. Możesz zostać poproszony o podanie hasła. Jeśli go nie pamiętasz, często można zresetować hasło przez naciśnięcie przycisku na samym routerze, ale należy pamiętać, że jeśli ta procedura jest wybrana, prawdopodobnie stracisz całą konfigurację routera. Jeśli Twój router wymaga aplikacji do jego konfiguracji, zainstaluj ją na swoim telefonie lub komputerze i użyj jej, aby uzyskać dostęp do ustawień routera.\",\n  \"install_devices_router_list_2\": \"Znajdź ustawienia DHCP/DNS. Poszukaj skrótu DNS obok pola, które pozwala wstawić dwa lub trzy zestawy liczb, z których każdy jest podzielony na cztery grupy z jedną do trzech cyfr.\",\n  \"install_devices_router_list_3\": \"Wpisz adresy swojego serwera AdGuard Home.\",\n  \"install_devices_router_list_4\": \"Na niektórych typach routerów nie można skonfigurować własnego serwera DNS. W takim przypadku pomocne może być skonfigurowanie AdGuard Home jako <0>serwera DHCP</0>. W przeciwnym razie należy sprawdzić w instrukcji obsługi routera, jak dostosować serwery DNS do konkretnego modelu routera.\",\n  \"install_devices_title\": \"Skonfiguruj swoje urządzenia\",\n  \"install_devices_windows_list_1\": \"Otwórz panel Ustawienia w menu Start lub w Windows.\",\n  \"install_devices_windows_list_2\": \"Przejdź do kategorii Sieć i Internet, a następnie do Centrum sieci i udostępniania.\",\n  \"install_devices_windows_list_3\": \"W lewym panelu kliknij \\\"Zmień ustawienia adaptera\\\".\",\n  \"install_devices_windows_list_4\": \"Kliknij prawym przyciskiem myszy aktywne połączenie i wybierz Właściwości.\",\n  \"install_devices_windows_list_5\": \"Znajdź na liście \\\"Protokół internetowy w wersji 4 (TCP/IPv4)\\\" (lub w przypadku IPv6 \\\"Protokół internetowy w wersji 6 (TCP/IPv6)\\\"), zaznacz go i ponownie kliknij Właściwości.\",\n  \"install_devices_windows_list_6\": \"Wybierz opcję \\\"Użyj następujących adresów serwerów DNS\\\" i wprowadź adresy serwerów AdGuard Home.\",\n  \"install_saved\": \"Pomyślnie zapisany\",\n  \"install_settings_all_interfaces\": \"Wszystkie interfejsy\",\n  \"install_settings_dns\": \"Serwer DNS\",\n  \"install_settings_dns_desc\": \"Konieczne będzie skonfigurowanie urządzenia lub routera do korzystania z serwera DNS pod następującymi adresami:\",\n  \"install_settings_interface_link\": \"Twój interfejs www AdGuard Home Admin będzie dostępny pod następującymi adresami:\",\n  \"install_settings_listen\": \"Interfejs sieciowy\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Interfejs internetowy administratora\",\n  \"install_static_configure\": \"AdGuard Home wykrył, że używany jest dynamiczny adres IP <0>{{ip}}</0>. Czy chcesz, aby został on ustawiony jako adres statyczny?\",\n  \"install_static_error\": \"AdGuard Home nie może skonfigurować go automatycznie dla tego interfejsu sieciowego. Poszukaj instrukcji, jak to zrobić ręcznie.\",\n  \"install_static_ok\": \"Dobre wieści! Statyczny adres IP jest już skonfigurowany\",\n  \"install_step\": \"Krok\",\n  \"install_submit_desc\": \"Procedura konfiguracji została zakończona i możesz teraz rozpocząć korzystanie z AdGuard Home.\",\n  \"install_submit_title\": \"Gratulacje!\",\n  \"install_welcome_desc\": \"AdGuard Home to w pełni funkcjonalny serwer DNS do blokowania reklam i śledzenia. Jego celem jest kontrolowanie całej sieci i wszystkich urządzeń, bez konieczności korzystania z jakiegokolwiek programu po stronie klienta.\",\n  \"install_welcome_title\": \"Witaj w AdGuard Home!\",\n  \"interval_24_hour\": \"24 godziny\",\n  \"interval_6_hour\": \"6 godzin\",\n  \"interval_days\": \"{{count}} dni\",\n  \"interval_days_plural\": \"{{count}} dni\",\n  \"interval_hours\": \"{{count}} godzina\",\n  \"interval_hours_plural\": \"{{count}} godziny\",\n  \"ip\": \"Adres IP\",\n  \"ip_address\": \"Adres IP\",\n  \"known_tracker\": \"Znany element śledzący\",\n  \"last_rule_in_allowlist\": \"Nie można odrzucić tego klienta, ponieważ wykluczenie reguły \\\"{{disallowed_rule}}\\\" spowoduje WYŁĄCZENIE listy „Dozwolonych klientów”.\",\n  \"last_time_updated_table_header\": \"Ostatnia aktualizacja\",\n  \"list_confirm_delete\": \"Czy na pewno chcesz usunąć tę listę?\",\n  \"list_label\": \"Lista\",\n  \"list_updated\": \"{{count}} lista zaktualizowana\",\n  \"list_updated_plural\": \"{{count}} list zaktualizowanych\",\n  \"list_url_table_header\": \"Adres URL listy\",\n  \"load_balancing\": \"Równoważenie obciążenia\",\n  \"load_balancing_desc\": \"Zapytaj jeden serwer nadrzędny na raz.<br/>AdGuard Home używa ważonego, losowego algorytmu do wybierania serwerów z najmniejszą liczbą nieudanych wyszukiwań i najniższym uśrednionym czasem wyszukiwania.\",\n  \"loading_table_status\": \"Wczytuję...\",\n  \"local_ptr_default_resolver\": \"Domyślnie AdGuard Home używa następujących odwrotnych resolwerów DNS: {{ip}}.\",\n  \"local_ptr_desc\": \"Serwery DNS używane przez AdGuard Home do prywatnych żądań PTR, SOA i NS. Żądanie jest uważane za prywatne, jeśli prosi o domenę ARPA zawierającą podsieć w prywatnym zakresie adresów IP (np. „192.168.12.34”) i pochodzi od klienta z prywatnym adresem IP. Jeśli nie zostanie ustawione, zostaną użyte domyślne programy rozpoznawania nazw DNS Twojego systemu operacyjnego, z wyjątkiem domowych adresów IP AdGuard.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home nie mógł określić odpowiednich prywatnych resolwerów DNS dla tego systemu.\",\n  \"local_ptr_placeholder\": \"Wprowadź po jednym adresie IP w każdym wierszu\",\n  \"local_ptr_title\": \"Prywatne odwrotne serwery DNS\",\n  \"location\": \"Lokalizacja\",\n  \"log_and_stats_section_label\": \"Dziennik zapytań i statystyki\",\n  \"lower_range_start_error\": \"Musi być niższy niż początek zakresu\",\n  \"main_settings\": \"Ustawienia główne\",\n  \"make_static\": \"Ustaw adres statyczny\",\n  \"manual_update\": \"Proszę <a>wykonać te czynności</a>, aby zaktualizować ręcznie.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Poniedziałek\",\n  \"monday_short\": \"Pon\",\n  \"name\": \"Nazwa\",\n  \"name_table_header\": \"Nazwa\",\n  \"netname\": \"Nazwa sieci\",\n  \"network\": \"Sieć\",\n  \"new_allowlist\": \"Nowa lista dozwolonych\",\n  \"new_blocklist\": \"Nowa lista zablokowanych\",\n  \"next\": \"Dalej\",\n  \"next_btn\": \"Następny\",\n  \"no_blocklist_added\": \"Nie dodano list zablokowanych\",\n  \"no_clients_found\": \"Nie znaleziono klientów\",\n  \"no_domains_found\": \"Nie znaleziono domen\",\n  \"no_logs_found\": \"Nie znaleziono logów\",\n  \"no_servers_specified\": \"Nie określono serwerów\",\n  \"no_upstreams_data_found\": \"Brak danych dotyczących serwerów nadrzędnych\",\n  \"no_whitelist_added\": \"Nie dodano list dozwolonych\",\n  \"nothing_found\": \"Nic nie znaleziono\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Liczba żądań DNS zablokowanych przez filtry blokowania reklam i listy zablokowanych hostów\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Liczba zablokowanych witryn dla dorosłych\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Liczba żądań DNS zablokowanych przez moduł Bezpiecznego przeglądania AdGuard\",\n  \"number_of_dns_query_days\": \"Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} dni\",\n  \"number_of_dns_query_days_plural\": \"Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} dni\",\n  \"number_of_dns_query_hours\": \"Liczba przetworzonych zapytań DNS w ciągu ostatniej {{count}} godziny\",\n  \"number_of_dns_query_hours_plural\": \"Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} godzin\",\n  \"number_of_dns_query_to_safe_search\": \"Liczba żądań DNS kierowanych do wyszukiwarek, dla których wymuszono Bezpieczne wyszukiwanie\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"WYŁĄCZONY\",\n  \"on\": \"WŁĄCZONY\",\n  \"open_dashboard\": \"Otwórz panel sterowania\",\n  \"orgname\": \"Nazwa firmy\",\n  \"original_response\": \"Oryginalna odpowiedź\",\n  \"out_of_range_error\": \"Musi być spoza zakresu \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Strona\",\n  \"parallel_requests\": \"Równoległe żądania\",\n  \"parental_control\": \"Kontrola rodzicielska\",\n  \"password_label\": \"Hasło\",\n  \"password_placeholder\": \"Wpisz hasło\",\n  \"plain_dns\": \"Zwykły DNS\",\n  \"port_53_faq_link\": \"Port 53 jest często zajęty przez usługi \\\"DNSStubListener\\\" lub \\\"systemd-resolved\\\". Przeczytaj <0>tę instrukcję</0> jak to rozwiązać.\",\n  \"previous_btn\": \"Poprzedni\",\n  \"privacy_policy\": \"Polityka Prywatności\",\n  \"processing_update\": \"Poczekaj, trwa aktualizacja AdGuard Home\",\n  \"protection_section_label\": \"Ochrona\",\n  \"protocol\": \"Protokół\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Dziennik zapytań\",\n  \"query_log_clear\": \"Wyczyść dzienniki zapytań\",\n  \"query_log_cleared\": \"Dziennik zapytań został pomyślnie wyczyszczony\",\n  \"query_log_configuration\": \"Konfiguracja dzienników\",\n  \"query_log_confirm_clear\": \"Czy na pewno chcesz wyczyścić cały dziennik zapytań?\",\n  \"query_log_disabled\": \"Dziennik zapytań jest wyłączony i można go skonfigurować w <0>ustawieniach</0>\",\n  \"query_log_enable\": \"Włącz dziennik\",\n  \"query_log_filtered\": \"Filtrowane przez {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotacja dzienników zapytań\",\n  \"query_log_retention_confirm\": \"Czy na pewno chcesz zmienić rotację dziennika zapytań? Jeśli zmniejszysz wartość interwału, niektóre dane zostaną utracone\",\n  \"query_log_strict_search\": \"Używaj podwójnych cudzysłowów do ścisłego wyszukiwania\",\n  \"query_log_updated\": \"Dziennik zapytań został zaktualizowany\",\n  \"rate_limit\": \"Limit ilościowy\",\n  \"rate_limit_desc\": \"Liczba żądań na sekundę dozwolona na klienta. Ustawienie wartości 0 oznacza brak ograniczeń.\",\n  \"rate_limit_subnet_len_ipv4\": \"Długość maski podsieci dla adresów IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Długość maski podsieci dla adresów IPv4 używanych do ograniczania prędkości. Domyślnie jest to 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Długość maski podsieci IPv4 powinna wynosić od 0 do 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Długość prefiksu podsieci dla adresów IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Długość prefiksu podsieci dla adresów IPv6 używanych do ograniczania szybkości. Domyślnie jest to 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Długość prefiksu podsieci IPv6 powinna wynosić od 0 do 128\",\n  \"rate_limit_whitelist\": \"Lista zezwoleń ograniczających prędkość\",\n  \"rate_limit_whitelist_desc\": \"Adresy IP wykluczone z ograniczania prędkości\",\n  \"rate_limit_whitelist_placeholder\": \"Wprowadź po jednym adresie IP w każdym wierszu\",\n  \"refresh_btn\": \"Odśwież\",\n  \"refresh_statics\": \"Odśwież statystyki\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Zgłoś problem\",\n  \"request_details\": \"Szczegóły żądania\",\n  \"request_table_header\": \"Żądanie\",\n  \"requests_count\": \"Licznik żądań\",\n  \"reset_settings\": \"Resetowanie ustawień\",\n  \"resolve_clients_desc\": \"Odwróć adresy IP klientów na ich nazwy hostów, wysyłając zapytania PTR do odpowiednich programów tłumaczących (prywatne serwery DNS dla klientów lokalnych, serwery nadrzędne dla klientów z publicznymi adresami IP).\",\n  \"resolve_clients_title\": \"Włącz odwrotne rozpoznawanie adresów IP klientów\",\n  \"response_code\": \"Kod odpowiedzi\",\n  \"response_details\": \"Szczegóły odpowiedzi\",\n  \"response_table_header\": \"Odpowiedź \",\n  \"response_time\": \"Czas odpowiedzi\",\n  \"rewrite_A\": \"<0>A</0>: wartość specjalna, zachowaj rekord <0>A</0> z głównego serwera DNS\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: wartość specjalna, zachowaj rekord <0>AAAA</0> z głównego serwera DNS\",\n  \"rewrite_add\": \"Dodaj przepisywanie DNS\",\n  \"rewrite_added\": \"Pomyślnie dodano przepisanie DNS dla „{{key}}”\",\n  \"rewrite_applied\": \"Przepisano regułę\",\n  \"rewrite_confirm_delete\": \"Czy na pewno chcesz usunąć przepisywanie DNS dla „{{key}}”?\",\n  \"rewrite_deleted\": \"Przepisanie DNS dla „{{key}}” zostało pomyślnie usunięte\",\n  \"rewrite_desc\": \"Pozwala łatwo skonfigurować niestandardową odpowiedź DNS dla określonej nazwy domeny.\",\n  \"rewrite_domain_name\": \"Nazwa domeny: dodaj rekord CNAME\",\n  \"rewrite_edit\": \"Edytuj przepisywanie DNS\",\n  \"rewrite_hosts_applied\": \"Przepisana reguła w pliku hosts\",\n  \"rewrite_ip_address\": \"Adres IP: użyj tego adresu IP w odpowiedzi A lub AAAA\",\n  \"rewrite_not_found\": \"Nie znaleziono przepisywania DNS\",\n  \"rewrite_settings_updated\": \"Ustawienia przepisywania DNS zostały pomyślnie zaktualizowane\",\n  \"rewrite_updated\": \"Pomyślnie zaktualizowano przepisywanie DNS\",\n  \"rewrites_disabled_table_header\": \"Przepisywanie jest wyłączone\",\n  \"rewrites_enabled_table_header\": \"Przepisywanie jest włączone\",\n  \"rewritten\": \"Przepisane\",\n  \"rows_table_footer_text\": \"wierszy\",\n  \"rule_added_to_custom_filtering_toast\": \"Reguła dodana do niestandardowych reguł filtrowania: {{rule}}\",\n  \"rule_label\": \"Reguła(y)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Reguła usunięta z niestandardowych reguł filtrowania: {{rule}}\",\n  \"rules_count_table_header\": \"Liczba reguł\",\n  \"safe_browsing\": \"Bezpieczne przeglądanie\",\n  \"safe_search\": \"Bezpieczne wyszukiwanie\",\n  \"saturday\": \"Sobota\",\n  \"saturday_short\": \"Sob\",\n  \"save_btn\": \"Zapisz\",\n  \"save_config\": \"Zapisz konfigurację\",\n  \"schedule_add\": \"Dodaj harmonogram\",\n  \"schedule_current_timezone\": \"Aktualna strefa czasowa: {{value}}\",\n  \"schedule_desc\": \"Ustawianie okresów bezczynności dla zablokowanych serwisów\",\n  \"schedule_edit\": \"Edytuj harmonogram\",\n  \"schedule_from\": \"Od\",\n  \"schedule_invalid_select\": \"Czas rozpoczęcia musi być przed czasem zakończenia\",\n  \"schedule_modal_description\": \"Ten harmonogram zastąpi wszystkie istniejące harmonogramy na ten sam dzień tygodnia. Każdy dzień tygodnia może mieć tylko jeden okres bezczynności.\",\n  \"schedule_modal_time_off\": \"Blokowanie serwisu jest wyłączone:\",\n  \"schedule_new\": \"Nowy harmonogram\",\n  \"schedule_remove\": \"Usuń harmonogram\",\n  \"schedule_save\": \"Zapisz harmonogram\",\n  \"schedule_select_days\": \"Wybierz dni\",\n  \"schedule_services\": \"Wstrzymanie blokowania serwisów\",\n  \"schedule_services_desc\": \"Ustawianie harmonogramu wstrzymywania filtru blokowania serwisów\",\n  \"schedule_services_desc_client\": \"Ustawianie harmonogramu wstrzymywania filtru blokowania serwisów dla tego klienta\",\n  \"schedule_time_all_day\": \"Cały dzień\",\n  \"schedule_timezone\": \"Wybierz strefę czasową\",\n  \"schedule_to\": \"Do\",\n  \"served_from_cache_label\": \"Podano z pamięci podręcznej\",\n  \"service_name\": \"Nazwa usługi\",\n  \"set_static_ip\": \"Ustaw statyczny adres IP\",\n  \"settings\": \"Ustawienia\",\n  \"settings_custom\": \"Własne\",\n  \"settings_global\": \"Globalny\",\n  \"setup_config_to_enable_dhcp_server\": \"Konfiguracja ustawień w celu włączenia serwera DHCP\",\n  \"setup_dns_notice\": \"Aby skorzystać z <1>DNS-over-HTTPS</1> lub <1>DNS-over-TLS</1>, musisz w ustawieniach AdGuard Home <0>skonfigurować szyfrowanie</0>.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Skorzystaj z adresu <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Skorzystaj z adresu <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Oto lista oprogramowania, którego możesz użyć.</0>\",\n  \"setup_dns_privacy_4\": \"Na urządzeniu iOS 14 lub macOS Big Sur możesz pobrać specjalny plik '.mobileconfig', który dodaje serwery <highlight>DNS-over-HTTPS</highlight> lub <highlight>DNS-over-TLS</highlight> do ustawień DNS.\",\n  \"setup_dns_privacy_android_1\": \"System Android 9 obsługuje natywnie DNS-over-TLS. Aby go skonfigurować, przejdź do Ustawienia → Sieć i Internet → Zaawansowane → Prywatny DNS i wpisz tam swoją nazwę domeny.\",\n  \"setup_dns_privacy_android_2\": \"Aplikacja <0>AdGuard dla Androida</0> obsługuje <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"Aplikacja <0>Intra</0> dodaje obsługę <1>DNS-over-HTTPS</1> dla Androida.\",\n  \"setup_dns_privacy_ioc_mac\": \"Konfiguracja iOS i macOS\",\n  \"setup_dns_privacy_ios_1\": \"Aplikacja <0>DNSCloak</0> obsługuje <1>DNS-over-HTTPS</1>, ale musisz wygenerować znacznik, aby skonfigurować go do używania własnego serwera <2>DNS Stamp</2>.\",\n  \"setup_dns_privacy_ios_2\": \"Aplikacja <0>AdGuard dla iOS</0> obsługuje <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"Sam AdGuard Home może być bezpiecznym klientem DNS na dowolnej platformie.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> obsługuje wszystkie znane bezpieczne protokoły DNS.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> obsługuje <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> obsługuje <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Znajdziesz więcej implementacji <0>tutaj</0> i <1>tutaj</1>.\",\n  \"setup_dns_privacy_other_title\": \"Inne implementacje\",\n  \"setup_guide\": \"Przewodnik instalacji\",\n  \"show_all_filter_type\": \"Pokaż wszystko\",\n  \"show_blocked_responses\": \"Zablokowane\",\n  \"show_filtered_type\": \"Pokaż filtrowane\",\n  \"show_processed_responses\": \"Przetworzono\",\n  \"show_whitelisted_responses\": \"Biała lista\",\n  \"sign_in\": \"Zaloguj się\",\n  \"sign_out\": \"Wyloguj się\",\n  \"source_label\": \"Źródło\",\n  \"static_ip\": \"Statyczny adres IP\",\n  \"static_ip_desc\": \"AdGuard Home to serwer, więc do poprawnego działania potrzebuje statycznego adresu IP. W przeciwnym razie router może przypisać temu urządzeniu inny adres IP.\",\n  \"statistics_clear\": \"Wyczyść statystyki\",\n  \"statistics_clear_confirm\": \"Czy na pewno chcesz wyczyścić statystyki?\",\n  \"statistics_cleared\": \"Statystyki zostały pomyślnie wyczyszczone\",\n  \"statistics_configuration\": \"Konfiguracja statystyk\",\n  \"statistics_enable\": \"Włącz statystyki\",\n  \"statistics_retention\": \"Przechowywanie statystyk\",\n  \"statistics_retention_confirm\": \"Czy chcesz zmienić sposób przechowania statystyk? Jeżeli obniżysz wartość interwału, niektóre dane będą utracone\",\n  \"statistics_retention_desc\": \"Jeśli zmniejszysz wartość interwału, niektóre dane zostaną utracone\",\n  \"stats_adult\": \"Zablokowane witryny dla dorosłych\",\n  \"stats_disabled\": \"Statystyki zostały wyłączone. Można je włączyć na <0>stronie ustawień</0>.\",\n  \"stats_disabled_short\": \"Statystyki zostały wyłączone\",\n  \"stats_malware_phishing\": \"Zablokowane złośliwe oprogramowanie/phishing\",\n  \"stats_params\": \"Konfiguracja statystyk\",\n  \"stats_query_domain\": \"Najczęściej wyszukiwane domeny\",\n  \"subnet_error\": \"Adresy muszą należeć do jednej podsieci\",\n  \"sunday\": \"Niedziela\",\n  \"sunday_short\": \"Ndz\",\n  \"system_host_files\": \"Pliki hosts systemu\",\n  \"table_client\": \"Klient\",\n  \"table_name\": \"Nazwa\",\n  \"tags_desc\": \"Możesz wybrać tagi, które odpowiadają klientowi. Uwzględnij tagi w regułach filtrowania, aby zastosować je dokładniej. <0>Dowiedz się więcej</0>.\",\n  \"tags_title\": \"Tagi\",\n  \"test_upstream_btn\": \"Test głównych serwerów DNS\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Automatycznie (na podstawie schematu kolorów Twojego urządzenia)\",\n  \"theme_dark\": \"Ciemny\",\n  \"theme_dark_desc\": \"Ciemny motyw\",\n  \"theme_light\": \"Jasny\",\n  \"theme_light_desc\": \"Jasny motyw\",\n  \"thursday\": \"Czwartek\",\n  \"thursday_short\": \"Czw\",\n  \"time_table_header\": \"Czas\",\n  \"top_blocked_domains\": \"Najpopularniejsze zablokowane domeny\",\n  \"top_clients\": \"Główni klienci\",\n  \"top_upstreams\": \"Często żądane serwery nadrzędne\",\n  \"topline_expired_certificate\": \"Twój certyfikat SSL wygasł. Zaktualizuj <0>Ustawienia szyfrowania</0>.\",\n  \"topline_expiring_certificate\": \"Twój certyfikat SSL wkrótce wygaśnie. Zaktualizuj <0>Ustawienia szyfrowania</0>.\",\n  \"tracker_source\": \"Źródło skryptu śledzącego\",\n  \"try_again\": \"Spróbuj ponownie\",\n  \"ttl_cache_validation\": \"Minimalne nadpisanie pamięci podręcznej TTL musi być mniejsze lub równe maksimum\",\n  \"tuesday\": \"Wtorek\",\n  \"tuesday_short\": \"Wt\",\n  \"type_table_header\": \"Typ\",\n  \"unavailable_dhcp\": \"Serwer DHCP jest niedostępny\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home nie może uruchomić serwera DHCP na Twoim systemie operacyjnym\",\n  \"unblock\": \"Odblokuj\",\n  \"unblock_all\": \"Odblokuj wszystko\",\n  \"unblock_for_this_client_only\": \"Odblokuj tylko tego klienta\",\n  \"unknown_filter\": \"Nieznany filtr {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} jest już dostępny! <0>Kliknij tutaj</0> aby uzyskać więcej informacji.\",\n  \"update_failed\": \"Automatyczna aktualizacja nie powiodła się. Proszę <a>wykonaj kroki</a> aby zaktualizować ręcznie.\",\n  \"update_now\": \"Aktualizuj teraz\",\n  \"updated_custom_filtering_toast\": \"Reguły niestandardowe zapisane pomyślnie\",\n  \"updated_save_search_toast\": \"Zaktualizowano ustawienia bezpiecznego wyszukiwania\",\n  \"updated_upstream_dns_toast\": \"Serwery nadrzędne zostały pomyślnie zapisane\",\n  \"updates_checked\": \"Dostępna jest nowa wersja programu AdGuard Home\\n\",\n  \"updates_version_equal\": \"AdGuard Home jest aktualny\",\n  \"upstream\": \"Serwer nadrzędny\",\n  \"upstream_dns\": \"Główne serwery DNS\",\n  \"upstream_dns_cache_configuration\": \"Konfiguracja pamięci podręcznej upstream serwerów DNS\",\n  \"upstream_dns_client_desc\": \"Jeśli to pole pozostanie puste, AdGuard Home użyje serwerów skonfigurowanych w <0>Ustawieniach DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Skonfigurowano w {{path}}\",\n  \"upstream_dns_help\": \"Wprowadź po jednym adresie serwera w każdym wierszu. <a>Dowiedz się więcej</a> o konfigurowaniu nadrzędnych serwerów DNS.\",\n  \"upstream_parallel\": \"Użyj zapytań równoległych, aby przyspieszyć rozwiązywanie przez jednoczesne wysyłanie zapytań do wszystkich serwerów nadrzędnych.\",\n  \"upstream_timeout\": \"Czas oczekiwania na odpowiedź\",\n  \"upstream_timeout_desc\": \"Określa liczbę sekund oczekiwania na odpowiedź od serwera nadrzędnego\",\n  \"upstreams\": \"Główne serwery DNS\",\n  \"use_adguard_browsing_sec\": \"Użyj usługi sieciowej Bezpieczne Przeglądanie AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home sprawdzi, czy domena jest zablokowana przez usługę bezpiecznego przeglądania. Do przeprowadzenia kontroli użyje przyjaznego dla prywatności interfejsu API wyszukiwania: na serwer wysyłany jest tylko krótki prefiks skrótu nazwy domeny SHA256.\",\n  \"use_adguard_parental\": \"Użyj usługi Kontrola Rodzicielska AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home sprawdzi, czy domena zawiera materiały dla dorosłych. Używa tego samego interfejsu API przyjaznego prywatności, co usługa sieciowa Bezpieczne Przeglądanie. \",\n  \"use_private_ptr_resolvers_desc\": \"Rozwiązuj żądania PTR, SOA i NS dla domen ARPA zawierających prywatne adresy IP za pośrednictwem prywatnych serwerów nadrzędnych, DHCP, /etc/hosts itp. Jeśli ta opcja jest wyłączona, AdGuard Home będzie odpowiadać na wszystkie takie żądania za pomocą NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Użyj prywatnych odwrotnych resolwerów DNS\",\n  \"use_saved_key\": \"Użyj wcześniej zapisanego klucza\",\n  \"username_label\": \"Nazwa użytkownika\",\n  \"username_placeholder\": \"Wpisz nazwę użytkownika\",\n  \"validated_with_dnssec\": \"Zweryfikowany przez DNSSEC\",\n  \"version\": \"wersja\",\n  \"version_request_error\": \"Sprawdzanie aktualizacji zakończone niepowodzeniem. Sprawdź swoje połączenie z internetem.\",\n  \"wednesday\": \"Środa\",\n  \"wednesday_short\": \"Śro\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/pt-br.json",
    "content": "{\n  \"access_allowed_desc\": \"Uma lista de CIDRs, endereços IP ou <a>IDs de cliente</a>. Se esta lista tiver entradas, o AdGuard Home do aceitará solicitações apenas desses clientes.\",\n  \"access_allowed_title\": \"Clientes permitidos\",\n  \"access_blocked_desc\": \"Não deve ser confundido com filtros. O AdGuard Home elimina as consultas DNS que correspondem a esses domínios, e essas consultas nem aparecem no registro de consultas. Você pode especificar nomes de domínio exatos, caracteres curinga ou regras de filtro de URL, por exemplo \\\"exemplo.org\\\", \\\"*.exemplo.org\\\", ou \\\"||exemplo.org^\\\" correspondentemente.\",\n  \"access_blocked_title\": \"Domínios bloqueados\",\n  \"access_desc\": \"Aqui você pode configurar as regras de acesso para o servidores de DNS do AdGuard Home\",\n  \"access_disallowed_desc\": \"Uma lista de CIDRs, endereços IP ou <a>IDs de cliente</a>. Se essa lista tiver entradas, o AdGuard Home descartará as solicitações desses clientes. Este campo é ignorado se houver entradas em clientes permitidos.\",\n  \"access_disallowed_title\": \"Clientes não permitidos\",\n  \"access_settings_saved\": \"Configurações de acesso foram salvas com sucesso\",\n  \"access_title\": \"Configurações de acessos\",\n  \"actions_table_header\": \"Ações\",\n  \"add_allowlist\": \"Adicionar lista de permissões\",\n  \"add_blocklist\": \"Adicionar lista de bloqueio\",\n  \"add_custom_list\": \"Adicionar uma lista personalizada\",\n  \"add_persistent_client\": \"Adicionar como cliente persistente\",\n  \"address\": \"Endereço\",\n  \"adg_will_drop_dns_queries\": \"O AdGuard Home descartará todas as consultas DNS deste cliente.\",\n  \"all_lists_up_to_date_toast\": \"Todas as listas já estão atualizadas\",\n  \"all_queries\": \"Todas as consultas\",\n  \"allow_this_client\": \"Permitir este cliente\",\n  \"allowed\": \"Permitido\",\n  \"anonymize_client_ip\": \"Tornar anônimo o IP do cliente\",\n  \"anonymize_client_ip_desc\": \"Não salva o endereço de IP completo do cliente em registros ou estatísticas\",\n  \"anonymizer_notification\": \"<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-lo em <1>Configurações gerais</1>.\",\n  \"answer\": \"Resposta\",\n  \"apply_btn\": \"Aplicar\",\n  \"auto_clients_desc\": \"Informações sobre endereços IP de dispositivos que usam ou podem usar o AdGuard Home. Essas informações são coletadas de várias fontes, incluindo arquivos de hosts, DNS reverso, etc.\",\n  \"auto_clients_title\": \"Clientes ativos\",\n  \"autofix_warning_list\": \"Ele irá realizar estas tarefas: <0>Desativar sistema DNSStubListener</0> <0>Definir endereço do servidor DNS para 127.0.0.1</0> <0>Substituir o alvo simbólico do link /etc/resolv.conf para /run/systemd/resolv.conf</0> <0>Parar DNSStubListener (recarregar serviço resolvido pelo sistema)</0>\",\n  \"autofix_warning_result\": \"Como resultado, todos as solicitações DNS do seu sistema serão processadas pelo AdGuard Home por padrão.\",\n  \"autofix_warning_text\": \"Se clicar em \\\"Corrigir\\\", o AdGuardHome irá configurar o seu sistema para utilizar o servidor DNS do AdGuardHome.\",\n  \"average_processing_time\": \"Tempo médio de processamento\",\n  \"average_processing_time_hint\": \"Tempo médio em milissegundos no processamento de uma solicitação DNS\",\n  \"average_upstream_response_time\": \"Tempo médio de resposta upstream\",\n  \"back\": \"Voltar\",\n  \"block\": \"Bloquear\",\n  \"block_all\": \"Bloquear tudo\",\n  \"block_domain_use_filters_and_hosts\": \"Bloquear domínios usando arquivos de filtros e hosts\",\n  \"block_for_this_client_only\": \"Bloquear apenas para este cliente\",\n  \"block_services\": \"Bloquear serviços específicos\",\n  \"blocked_adult_websites\": \"Bloqueado pelo controle parental\",\n  \"blocked_by\": \"<0>Bloqueador por filtros</0>\",\n  \"blocked_by_cname_or_ip\": \"Bloqueado por CNAME ou IP\",\n  \"blocked_by_response\": \"Bloqueado por CNAME ou IP na resposta\",\n  \"blocked_response_ttl\": \"Resposta bloqueada TTL\",\n  \"blocked_response_ttl_desc\": \"Especifica por quantos segundos os clientes devem armazenar em cache uma resposta filtrada\",\n  \"blocked_safebrowsing\": \"Bloqueado pela navegação segura\",\n  \"blocked_service\": \"Serviço bloqueado\",\n  \"blocked_services\": \"Serviços bloqueados\",\n  \"blocked_services_desc\": \"Permite o bloqueio rápido de sites e serviços populares.\",\n  \"blocked_services_global\": \"Usar serviços bloqueados globais\",\n  \"blocked_services_saved\": \"Serviços bloqueados salvos com sucesso\",\n  \"blocked_threats\": \"Ameaças bloqueadas\",\n  \"blocking_ipv4\": \"Bloqueando IPv4\",\n  \"blocking_ipv4_desc\": \"Endereço de IP a ser retornado para uma solicitação bloqueada\",\n  \"blocking_ipv6\": \"Bloqueando IPv6\",\n  \"blocking_ipv6_desc\": \"Endereço de IP a ser retornado para uma solicitação AAAA bloqueada\",\n  \"blocking_mode\": \"Modo de bloqueio\",\n  \"blocking_mode_custom_ip\": \"IP personalizado: Responder com um endereço IP definido manualmente\",\n  \"blocking_mode_default\": \"Padrão: Responder com zero endereço IP (0.0.0.0 para A; :: para AAAA) quando bloqueado pela regra de estilo Adblock; responde com o endereço IP especificado na regra quando bloqueado pela regra /etc/hosts-style\",\n  \"blocking_mode_null_ip\": \"IP nulo: Responder com endereço IP zero (0.0.0.0 para A; :: para AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Responder com o código NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: responder com o código REFUSED\",\n  \"blocklist\": \"Lista de bloqueio\",\n  \"bootstrap_dns\": \"Servidores DNS de inicialização\",\n  \"bootstrap_dns_desc\": \"Endereços IP de servidores DNS usados para resolver endereços IP dos resolvedores DoH/DoT que você especifica como upstreams. Comentários não são permitidos.\",\n  \"cache_cleared\": \"Cache DNS limpo com sucesso\",\n  \"cache_enabled\": \"Ativar cache\",\n  \"cache_enabled_desc\": \"Armazenar as respostas DNS localmente.\",\n  \"cache_optimistic\": \"Cache otimista\",\n  \"cache_optimistic_desc\": \"Faz o AdGuard Home responder a partir do cache mesmo quando as entradas expirarem e também tenta atualizá-las.\",\n  \"cache_size\": \"Tamanho do cache\",\n  \"cache_size_desc\": \"Tamanho do cache DNS (em bytes).\",\n  \"cache_size_validation\": \"O tamanho do cache deve ser maior que zero quando ativado.\",\n  \"cache_ttl_max_override\": \"Sobrepor o TTL máximo\",\n  \"cache_ttl_max_override_desc\": \"Defina um valor máximo de tempo de vida (segundos) para entradas no cache DNS.\",\n  \"cache_ttl_min_override\": \"Sobrepor o TTL mínimo\",\n  \"cache_ttl_min_override_desc\": \"Prolongue os valores de curta duração (segundos) recebidos do servidor primário ao armazenar em cache as respostas DNS.\",\n  \"cancel_btn\": \"Cancelar\",\n  \"category_label\": \"Categoria\",\n  \"check\": \"Verificar\",\n  \"check_client_id\": \"Identificador do cliente (ClienteID ou endereço de IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Verificar se um nome do host está sendo filtrado.\",\n  \"check_dhcp_servers\": \"Verificar por servidores DHCP\",\n  \"check_dns_record\": \"Selecione o tipo de registro DNS\",\n  \"check_enter_client_id\": \"Insira o identificador do cliente\",\n  \"check_hostname\": \"Nome do anfitrião ou nome de domínio\",\n  \"check_ip\": \"Endereços de IP: {{ip}}\",\n  \"check_not_found\": \"Não encontrado em suas listas de filtros\",\n  \"check_reason\": \"Motivo: {{reason}}\",\n  \"check_service\": \"Nome do serviço: {{service}}\",\n  \"check_title\": \"Verifique a filtragem\",\n  \"check_updates_btn\": \"Verificar atualizações\",\n  \"check_updates_now\": \"Verificar atualizações\",\n  \"choose_allowlist\": \"Escolher as listas de permissões\",\n  \"choose_blocklist\": \"Escolher as listasde bloqueio\",\n  \"choose_from_list\": \"Escolha na lista\",\n  \"city\": \"Cidade\",\n  \"clear_cache\": \"Limpar cache\",\n  \"click_to_view_queries\": \"Clique para ver as consultas\",\n  \"client_add\": \"Adicionar cliente\",\n  \"client_added\": \"Cliente \\\"{{key}}\\\" adicionado com sucesso\",\n  \"client_blocked\": \"Cliente \\\"{{ip}}\\\" foi bloqueado com sucesso\",\n  \"client_confirm_block\": \"Você tem certeza de que deseja bloquear o cliente \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Você tem certeza de que deseja excluir o cliente \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Você tem certeza de que deseja desbloquear o cliente \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Cliente \\\"{{key}}\\\" excluído com sucesso\",\n  \"client_details\": \"Detalhes do cliente\",\n  \"client_edit\": \"Editar cliente\",\n  \"client_global_settings\": \"Usar configurações global\",\n  \"client_id\": \"ID do cliente\",\n  \"client_id_desc\": \"Os clientes podem ser identificados por um ID de cliente especial. Saiba mais como identificar clientes <a>aqui</a>.\",\n  \"client_id_placeholder\": \"Digite o ID do cliente\",\n  \"client_identifier\": \"Identificador\",\n  \"client_identifier_desc\": \"Os clientes podem ser identificados pelo endereço IP, CIDR, Endereço MAC ou um ID de cliente especial (pode ser usado para DoT/DoH/DoQ). Saiba mais sobre como identificar clientes <0>aqui</0>.\",\n  \"client_name\": \"Cliente {{id}}\",\n  \"client_new\": \"Novo cliente\",\n  \"client_settings\": \"Configurações do cliente\",\n  \"client_table_header\": \"Cliente\",\n  \"client_unblocked\": \"Cliente \\\"{{ip}}\\\" foi desbloqueado com sucesso\",\n  \"client_updated\": \"Cliente \\\"{{key}}\\\" atualizado com sucesso\",\n  \"clients_desc\": \"Configure registros de cliente persistentes para dispositivos conectados ao AdGuard Home\",\n  \"clients_not_found\": \"Nenhum cliente foi encontrado\",\n  \"clients_title\": \"Clientes persistentes\",\n  \"compact\": \"Compacto\",\n  \"config_successfully_saved\": \"Configuração salva com sucesso\",\n  \"configure\": \"Configurar\",\n  \"confirm_dns_cache_clear\": \"Tem certeza de que deseja limpar o cache DNS?\",\n  \"confirm_static_ip\": \"O AdGuard Home irá configurar {{ip}} para ser seu endereço IP estático. Deseja continuar?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"País\",\n  \"custom_filter_rules\": \"Regras de filtragem personalizadas\",\n  \"custom_filter_rules_hint\": \"Digite uma regra por linha. Você pode usar regras de bloqueio de anúncios ou a sintaxe de arquivos de hosts.\",\n  \"custom_filtering_rules\": \"Regras de filtragem personalizadas\",\n  \"custom_ip\": \"IP personalizado\",\n  \"custom_retention_input\": \"Insira a retenção em horas\",\n  \"custom_rotation_input\": \"Insira a rotação em horas\",\n  \"dashboard\": \"Painel\",\n  \"date\": \"Data\",\n  \"default\": \"Padrão\",\n  \"delete_confirm\": \"Você tem certeza de que deseja excluir \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Excluir\",\n  \"descr\": \"Descrição\",\n  \"details\": \"Detalhes\",\n  \"dhcp_add_static_lease\": \"Adicionar nova concessão estática\",\n  \"dhcp_config_saved\": \"Configurações DHCP salvas com sucesso\",\n  \"dhcp_description\": \"Se o seu roteador não fornecer configurações de DHCP, você poderá usar o servidor DHCP integrado do AdGuard.\",\n  \"dhcp_disable\": \"Desativar servidor DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Seu sistema usa a configuração de endereço IP dinâmico para a interface <0>{{interfaceName}}</0>. Para usar o servidor DHCP, você deve definir um endereço de IP estático. Seu endereço IP atual é <0> {{ipAddress}} </ 0>. AdGuard Home irá definir automaticamente este endereço IP como estático se você pressionar o botão \\\"Ativar servidor DHCP\\\".\",\n  \"dhcp_edit_static_lease\": \"Editar concessão estática\",\n  \"dhcp_enable\": \"Ativar servidor DHCP\",\n  \"dhcp_error\": \"O AdGuard Home não conseguiu determinar se há outro servidor DHCP ativo na rede\",\n  \"dhcp_form_gateway_input\": \"IP do gateway\",\n  \"dhcp_form_lease_input\": \"Duração da concessão\",\n  \"dhcp_form_lease_title\": \"Tempo de concessão do DHCP (em segundos)\",\n  \"dhcp_form_range_end\": \"Final da faixa\",\n  \"dhcp_form_range_start\": \"Início da faixa\",\n  \"dhcp_form_range_title\": \"Faixa de endereços IP\",\n  \"dhcp_form_subnet_input\": \"Máscara de sub-rede\",\n  \"dhcp_found\": \"Um servidor DHCP ativo foi encontrado na rede. Não é seguro ativar o servidor DHCP incorporado.\",\n  \"dhcp_hardware_address\": \"Endereço de hardware\",\n  \"dhcp_interface_select\": \"Selecione a interface DHCP\",\n  \"dhcp_ip_addresses\": \"Endereço de IP\",\n  \"dhcp_ipv4_settings\": \"Configurações DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Configurações DHCP IPv6\",\n  \"dhcp_lease_added\": \"Concessão estática \\\"{{key}}\\\" adicionada com sucesso\",\n  \"dhcp_lease_deleted\": \"Concessão estática \\\"{{key}}\\\" excluída com sucesso\",\n  \"dhcp_lease_updated\": \"Concessão estática \\\"{{key}}\\\" atualizada com sucesso\",\n  \"dhcp_leases\": \"Concessões DHCP\",\n  \"dhcp_leases_not_found\": \"Nenhuma concessão DHCP encontrada\",\n  \"dhcp_new_static_lease\": \"Nova concessão estática\",\n  \"dhcp_not_found\": \"É seguro ativar o servidor DHCP integrado porque o AdGuard Home não encontrou nenhum servidor DHCP ativo na rede. No entanto, você deve verificar isso manualmente, pois a verificação automática atualmente não oferece 100% de garantia.\",\n  \"dhcp_reset\": \"Você tem certeza de que deseja redefinir a configuração DHCP?\",\n  \"dhcp_reset_leases\": \"Redefinir todas as concessões\",\n  \"dhcp_reset_leases_confirm\": \"Tem certeza de que deseja redefinir todas as concessões?\",\n  \"dhcp_reset_leases_success\": \"Concessões de DHCP redefinidas com sucesso\",\n  \"dhcp_settings\": \"Configurações de DHCP\",\n  \"dhcp_static_ip_error\": \"Para usar o servidor DHCP, você deve definir um endereço IP estático. AdGuard Home não conseguiu determinar se essa interface de rede está configurada usando o endereço de IP estático. Por favor, defina um endereço IP estático manualmente.\",\n  \"dhcp_static_leases\": \"Concessões de DHCP estático\",\n  \"dhcp_static_leases_not_found\": \"Nenhuma concessão DHCP estática foi encontrada\",\n  \"dhcp_table_expires\": \"Expira\",\n  \"dhcp_table_hostname\": \"Nome do servidor\",\n  \"dhcp_title\": \"Servidor DHCP (experimental)\",\n  \"dhcp_warning\": \"Se você quiser ativar o servidor DHCP de qualquer maneira, certifique-se de que não haja outro servidor DHCP ativo em sua rede, pois isso pode quebrar a conectividade com a Internet para dispositivos na rede!\",\n  \"disable_for_hours\": \"Por {{count}} hora\",\n  \"disable_for_hours_plural\": \"Por {{count}} horas\",\n  \"disable_for_minutes\": \"Por {{count}} minuto\",\n  \"disable_for_minutes_plural\": \"Por {{count}} minutos\",\n  \"disable_for_seconds\": \"Por {{count}} segundo\",\n  \"disable_for_seconds_plural\": \"Por {{count}} segundos\",\n  \"disable_ipv6\": \"Desativar resolução de endereços IPv6\",\n  \"disable_ipv6_desc\": \"Descarta todas as consultas DNS para endereços IPv6 (tipo AAAA) e remove dicas de IPv6 das respostas HTTPS.\",\n  \"disable_notify_for_hours\": \"Desativar proteção por {{count}} hora\",\n  \"disable_notify_for_hours_plural\": \"Desativar proteção por {{count}} horas\",\n  \"disable_notify_for_minutes\": \"Desativar proteção por {{count}} minuto\",\n  \"disable_notify_for_minutes_plural\": \"Desativar proteção por {{count}} minutos\",\n  \"disable_notify_for_seconds\": \"Desativar proteção por {{count}} segundo\",\n  \"disable_notify_for_seconds_plural\": \"Desativar proteção por {{count}} segundos\",\n  \"disable_notify_until_tomorrow\": \"Desativar a proteção até amanhã\",\n  \"disable_protection\": \"Desativar proteção\",\n  \"disable_rewrites\": \"Desativar regras de reescrita\",\n  \"disable_until_tomorrow\": \"Até amanhã\",\n  \"disabled\": \"Desativado\",\n  \"disabled_dhcp\": \"Servidor DHCP desativado\",\n  \"disabled_filtering_toast\": \"Filtragem desativada\",\n  \"disabled_parental_toast\": \"Controle parental desativado\",\n  \"disabled_protection\": \"Proteção desativada\",\n  \"disabled_safe_browsing_toast\": \"Navegação segura desativada\",\n  \"disabled_safe_search_toast\": \"Pesquisa segura desativada\",\n  \"disallow_this_client\": \"Não permitir este cliente\",\n  \"dns_addresses\": \"Endereços DNS\",\n  \"dns_allowlists\": \"Listas de permissões de DNS\",\n  \"dns_allowlists_desc\": \"Os domínios das listas de permissões de DNS serão permitidos mesmo que estejam em qualquer uma das listas de bloqueio.\",\n  \"dns_blocklists\": \"Listas de bloqueio de DNS\",\n  \"dns_blocklists_desc\": \"O AdGuard Home bloqueará domínios que correspondam às listas de bloqueio.\",\n  \"dns_cache_config\": \"Configuração de cache DNS\",\n  \"dns_cache_config_desc\": \"Aqui você pode configurar o cache do DNS\",\n  \"dns_cache_size\": \"Tamanho do cache do DNS, em bytes\",\n  \"dns_config\": \"Configuração do servidor DNS\",\n  \"dns_over_https\": \"DNS-sobre-HTTPS\",\n  \"dns_over_quic\": \"DNS-sobre-QUIC\",\n  \"dns_over_tls\": \"DNS-sobre-TLS\",\n  \"dns_privacy\": \"Privacidade de DNS\",\n  \"dns_providers\": \"Aqui está uma <0>lista de provedores de DNS conhecidos</0> para escolher.\",\n  \"dns_query\": \"Consultas de DNS\",\n  \"dns_rewrites\": \"Reescritas de DNS\",\n  \"dns_settings\": \"Configurações de DNS\",\n  \"dns_start\": \"O servidor DNS está iniciando\",\n  \"dns_status_error\": \"Ocorreu um erro ao verificar o status do servidor DNS\",\n  \"dns_test_not_ok_toast\": \"O servidor \\\"{{key}}\\\": não pôde ser utilizado. Por favor, verifique se você escreveu corretamente\",\n  \"dns_test_ok_toast\": \"Os servidores DNS especificados estão funcionando corretamente\",\n  \"dns_test_parsing_error_toast\": \"A seção {{section}}: linha {{line}}: não pôde ser usada. Verifique se foi escrita corretamente\",\n  \"dns_test_warning_toast\": \"Servidor DNS primário \\\"{{key}}\\\" não responde aos Solicitações de teste e pode não funcionar corretamente\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Ativar DNSSEC\",\n  \"dnssec_enable_desc\": \"Definir a marcação DNSSEC nas consultas de DNS em andamento e verificar o resultado (é necessário um resolvedor DNSSEC ativado).\",\n  \"domain\": \"Domínio\",\n  \"domain_desc\": \"Digite o nome do domínio ou wildcard que pretende reescrever.\",\n  \"domain_name_table_header\": \"Nome de domínio\",\n  \"domain_or_client\": \"Domínio ou cliente\",\n  \"down\": \"Caiu\",\n  \"download_mobileconfig\": \"Baixar arquivo de configuração\",\n  \"download_mobileconfig_doh\": \"BAixar .mobileconfig para DNS-sobre-HTTPS\",\n  \"download_mobileconfig_dot\": \"BAixar .mobileconfig para DNS-sobre-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Editar lista de permissões\",\n  \"edit_blocklist\": \"Editar lista de bloqueio\",\n  \"edit_table_action\": \"Editar\",\n  \"edns_cs_desc\": \"Adicione a opção de sub-rede de cliente EDNS (ECS) às solicitações de servidor DNS primário e registre os valores enviados pelos clientes no registro de consulta.\",\n  \"edns_enable\": \"Ativar a sub-rede do cliente EDNS\",\n  \"edns_use_custom_ip\": \"Usar IP personalizado para EDNS\",\n  \"edns_use_custom_ip_desc\": \"Permitir o uso de IP personalizado para EDNS\",\n  \"elapsed\": \"Tempo decorrido\",\n  \"empty_response_status\": \"Vazio\",\n  \"enable_protection\": \"Ativar proteção\",\n  \"enable_protection_timer\": \"A proteção será ativada em {{time}}\",\n  \"enable_rewrites\": \"Ativar regras de reescrita\",\n  \"enable_upstream_dns_cache\": \"Ativar o armazenamento em cache do DNS para a configuração de upstream personalizada deste cliente\",\n  \"enabled_dhcp\": \"Servidor DHCP ativado\",\n  \"enabled_filtering_toast\": \"Filtragem ativada\",\n  \"enabled_parental_toast\": \"Controle parental ativado\",\n  \"enabled_protection\": \"Proteção ativada\",\n  \"enabled_safe_browsing_toast\": \"Navegação segura ativada\",\n  \"enabled_save_search_toast\": \"Pesquisa segura ativada\",\n  \"enabled_table_header\": \"Ativado\",\n  \"encryption_certificate_path\": \"Caminho do certificado\",\n  \"encryption_certificates\": \"Certificados\",\n  \"encryption_certificates_desc\": \"Para usar criptografia, você precisa fornecer uma cadeia de certificados SSL válida para seu domínio. Você pode obter um certificado gratuito em <0> {{link}}</0> ou pode comprá-lo de uma das autoridades de certificação confiáveis.\",\n  \"encryption_certificates_input\": \"Copie/cole aqui seu certificado codificado em PEM.\",\n  \"encryption_certificates_source_content\": \"Colar o conteúdo dos certificados\",\n  \"encryption_certificates_source_path\": \"Definir o caminho do arquivo de certificados\",\n  \"encryption_chain_invalid\": \"A cadeia de certificado é inválida\",\n  \"encryption_chain_valid\": \"Cadeia de chave válida\",\n  \"encryption_config_saved\": \"Configuração de criptografia salva\",\n  \"encryption_desc\": \"Suporte a criptografia (HTTPS/QUIC/TLS) para DNS e interface de administração web\",\n  \"encryption_doq\": \"Porta DNS-sobre-QUIC\",\n  \"encryption_doq_desc\": \"Se esta porta estiver configurada, o AdGuard Home executará um servidor DNS-sobre-QUIC nesta porta. \",\n  \"encryption_dot\": \"Porta DNS-sobre-TLS\",\n  \"encryption_dot_desc\": \"Se essa porta estiver configurada, o AdGuard Home irá executar o servidor DNS-sobre- TSL nesta porta.\",\n  \"encryption_enable\": \"Ativar criptografia (HTTPS, DNS-sobre-HTTPS e DNS-sobre-TLS)\",\n  \"encryption_enable_desc\": \"Se a criptografia estiver ativada, a interface administrativa do AdGuard Home funcionará em HTTPS, o servidor DNS irá capturar as solicitações por meio do DNS-sobre-HTTPS e DNS-sobre-TLS.\",\n  \"encryption_expire\": \"Expira\",\n  \"encryption_hostnames\": \"Nomes dos servidores\",\n  \"encryption_https\": \"Porta HTTPS\",\n  \"encryption_https_desc\": \"Se a porta HTTPS estiver configurada, a interface administrativa do AdGuard Home será acessível via HTTPS e também fornecerá o DNS-sobre-HTTPS no local '/dns-query'.\",\n  \"encryption_issuer\": \"Emissor\",\n  \"encryption_key\": \"Chave privada\",\n  \"encryption_key_input\": \"Copie/cole aqui a chave privada codificada em PEM para seu certificado.\",\n  \"encryption_key_invalid\": \"Esta é uma chave privada {{type}} inválida\",\n  \"encryption_key_source_content\": \"Colar o conteúdo da chave privada\",\n  \"encryption_key_source_path\": \"Definir um caminho do arquivo de chave privada\",\n  \"encryption_key_valid\": \"Esta é uma chave privada {{type}} válida\",\n  \"encryption_plain_dns_desc\": \"O DNS simples (sem criptografia) está ativado por padrão. Você pode desativá-lo para forçar todos os dispositivos a usar DNS criptografado. Para fazer isso, você deve ativar pelo menos um protocolo DNS criptografado\",\n  \"encryption_plain_dns_enable\": \"Ativar DNS simples (sem criptografia)\",\n  \"encryption_plain_dns_error\": \"Para desativar o DNS simples, ative pelo menos um protocolo DNS criptografado\",\n  \"encryption_private_key_path\": \"Caminho da chave privada\",\n  \"encryption_redirect\": \"Redirecionar automaticamente para HTTPS\",\n  \"encryption_redirect_desc\": \"Se marcado, o AdGuard Home irá redirecionar automaticamente os endereços HTTP para HTTPS.\",\n  \"encryption_reset\": \"Você tem certeza de que deseja redefinir a configuração de criptografia?\",\n  \"encryption_server\": \"Nome do servidor\",\n  \"encryption_server_desc\": \"Se definido, AdGuard Home detecta ClientIDs, responde a consultas DDR, e executa validações de ligações adicionais. Se não estiver definido, estas características são desactivadas. Devem corresponder a um dos Nomes DNS no certificado.\",\n  \"encryption_server_enter\": \"Digite seu nome de domínio\",\n  \"encryption_settings\": \"Configurações de criptografia\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Assunto\",\n  \"encryption_title\": \"Criptografia\",\n  \"encryption_warning\": \"Aviso\",\n  \"enforce_safe_search\": \"Usar pesquisa segura\",\n  \"enforce_save_search_hint\": \"O AdGuard Home forcará a pesquisa segura nos seguintes motores de busca: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Forçar pesquisa segura\",\n  \"enter_cache_size\": \"Digite o tamanho do cache (bytes)\",\n  \"enter_cache_ttl_max_override\": \"Digite o TTL máximo (segundos)\",\n  \"enter_cache_ttl_min_override\": \"Digite o TTL máximo (segundos)\",\n  \"enter_name_hint\": \"Digite o nome\",\n  \"enter_url_or_path_hint\": \"Digite a URL ou o local da lista\",\n  \"enter_valid_allowlist\": \"Digite uma URL válida para a lista de permissões.\",\n  \"enter_valid_blocklist\": \"Digite um URL válido para a lista de bloqueio.\",\n  \"error_details\": \"Detalhes do erro\",\n  \"example_comment\": \"! Aqui vai um comentário.\",\n  \"example_comment_hash\": \"# Também um comentário.\",\n  \"example_comment_meaning\": \"apenas um comentário;\",\n  \"example_meaning_filter_block\": \"bloqueia o acesso ao exemplo.org e a todos os seus subdomínios;\",\n  \"example_meaning_filter_whitelist\": \"desbloqueia o acesso ao exemplo.org e a todos os seus subdomínios;\",\n  \"example_meaning_host_block\": \"responde o endereço 127.0.0.1 para o exemplo.org (exceto seus subdomínios);\",\n  \"example_multiple_upstreams_reserved\": \"múltiplos upstreams <0>para domínios específicos</0>;\",\n  \"example_regex_meaning\": \"bloqueia o acesso aos domínios que correspondem à expressão regular especificada.\",\n  \"example_rewrite_domain\": \"reescrever respostas apenas para este nome de domínio.\",\n  \"example_rewrite_wildcard\": \"reescrever respostas para todos subdomínios <0>exemplo.org</0>.\",\n  \"example_upstream_comment\": \"um comentário.\",\n  \"example_upstream_doh\": \"<0>DNS-sobre-HTTPS</0> criptografado;\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS criptografado com <0>HTTP/3 forçado</0> e sem fallback para HTTP/2 ou inferior;\",\n  \"example_upstream_doq\": \"<0>DNS-sobre-QUIC</0> criptografado;\",\n  \"example_upstream_dot\": \"<0>DNS-sobre-TLS</0> criptografado;\",\n  \"example_upstream_regular\": \"dNS regular (através do UDP);\",\n  \"example_upstream_regular_port\": \"DNS normal (através do UDP, com porta);\",\n  \"example_upstream_reserved\": \"um DNS primário <0>para o domínios especificos</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> para o <1>DNSCrypt</1> ou usar os resolvedores <2>DNS-sobre-HTTPS</2>;\",\n  \"example_upstream_tcp\": \"DNS regular (através do TCP);\",\n  \"example_upstream_tcp_hostname\": \"DNS normal (através do TCP, nome do servidor);\",\n  \"example_upstream_tcp_port\": \"dNS normal (através do TCP, com porta);\",\n  \"example_upstream_udp\": \"DNS normal (através do UDP, nome do servidor);\",\n  \"examples_title\": \"Exemplos\",\n  \"fallback_dns_desc\": \"Lista de servidores DNS Fallback usados quando os servidores DNS primários não estão respondendo. A sintaxe é a mesma dos campos de servidores principais na seção acima.\",\n  \"fallback_dns_placeholder\": \"Insira um servidor DNS fallback por linha\",\n  \"fallback_dns_title\": \"Servidores DNS Fallback\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Endereço de IP mais rápido\",\n  \"fastest_addr_desc\": \"Aguarde as respostas de <b>todos</b> os servidores DNS, meça a velocidade da conexão TCP para cada servidor e retorne o endereço de IP do servidor com a velocidade de conexão mais rápida.<br/>Esse modo pode retardar significativamente as consultas de DNS, se um ou mais servidores DNS primários não estiverem respondendo. Certifique-se de que seus servidores DNS primários sejam estáveis e que seu tempo de espera para DNS seja baixo.\",\n  \"filter\": \"Filtro\",\n  \"filter_added_successfully\": \"O filtro foi adicionado com sucesso\",\n  \"filter_allowlist\": \"AVISO: Esta ação também excluirá a regra \\\"{{disallowed_rule}}\\\" da lista de clientes permitidos.\",\n  \"filter_category_general\": \"Geral\",\n  \"filter_category_general_desc\": \"Listas que bloqueiam o rastreamento e a publicidade na maioria dos dispositivos\",\n  \"filter_category_other\": \"Outro\",\n  \"filter_category_other_desc\": \"Outras listas de bloqueio\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Listas focadas em anúncios regionais e servidores de rastreamento\",\n  \"filter_category_security\": \"Segurança\",\n  \"filter_category_security_desc\": \"Listas projetadas especificamente em bloquear domínios maliciosos, de phishing e fraude\",\n  \"filter_removed_successfully\": \"A lista foi removida com sucesso\",\n  \"filter_updated\": \"O filtro atualizado com sucesso\",\n  \"filtered\": \"Filtrado\",\n  \"filtered_custom_rules\": \"Filtrado pelas regras de filtragem personalizadas\",\n  \"filtering_rules_learn_more\": \"<0>Saiba mais</0> sobre como criar as suas próprias listas negras de servidores.\",\n  \"filters\": \"Filtros\",\n  \"filters_and_hosts_hint\": \"O AdGuard Home entende regras básicas de bloqueio de anúncios e a sintaxe de arquivos de hosts.\",\n  \"filters_block_toggle_hint\": \"Você pode configurar as regras de bloqueio nas configurações de <a>Filtros</a>.\",\n  \"filters_configuration\": \"Configuração de filtros\",\n  \"filters_enable\": \"Ativar filtros\",\n  \"filters_interval\": \"Intervalo de atualização de filtro\",\n  \"fix\": \"Corrigido\",\n  \"for_last_days\": \"nos últimos {{count}} dias\",\n  \"for_last_days_plural\": \"nos últimos {{count}} dias\",\n  \"for_last_hours\": \"na última {{count}} hora\",\n  \"for_last_hours_plural\": \"nas últimas {{count}} horas\",\n  \"forgot_password\": \"Esqueceu sua senha?\",\n  \"forgot_password_desc\": \"Por favor, siga <0>estes passos</0> para criar uma nova senha para a sua conta.\",\n  \"form_add_id\": \"Adicionar identificador\",\n  \"form_answer\": \"Digite o endereço de IP ou nome de domínio\",\n  \"form_client_name\": \"Digite o nome do cliente\",\n  \"form_domain\": \"Digite o nome do domínio ou wildcard\",\n  \"form_enter_blocked_response_ttl\": \"Insira o TTL da resposta bloqueada (segundos)\",\n  \"form_enter_host\": \"Digite o nome do host\",\n  \"form_enter_hostname\": \"Digite o hostname\",\n  \"form_enter_id\": \"Inserir identificador\",\n  \"form_enter_ip\": \"Digite o endereço de IP\",\n  \"form_enter_mac\": \"Digite o endereço MAC\",\n  \"form_enter_rate_limit\": \"Insira a velocidade limite\",\n  \"form_enter_rate_limit_subnet_len\": \"Insira o comprimento do prefixo da sub-rede para limitação de taxa\",\n  \"form_enter_subnet_ip\": \"Digite um endereço IP na sub-rede \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Insira a duração do tempo limite do servidor upstream em segundos\",\n  \"form_error_answer_format\": \"Formato de resposta inválido\",\n  \"form_error_client_id_format\": \"O ID do cliente deve conter apenas números, letras minúsculas e hifens\",\n  \"form_error_domain_format\": \"Formato de domínio inválido\",\n  \"form_error_equal\": \"Não deve ser igual\",\n  \"form_error_gateway_ip\": \"A concessão não pode ter o endereço IP do gateway\",\n  \"form_error_ip4_format\": \"Endereço de IPv4 inválido\",\n  \"form_error_ip4_gateway_format\": \"Endereço IPv4 de gateway inválido\",\n  \"form_error_ip6_format\": \"Endereço de IPv6 inválido\",\n  \"form_error_ip_format\": \"Endereço de IP inválido\",\n  \"form_error_mac_format\": \"Endereço de MAC inválido\",\n  \"form_error_password\": \"Senhas não coincidem\",\n  \"form_error_password_length\": \"A senha deve ter entre {{min}} e {{max}} caracteres\",\n  \"form_error_port\": \"Digite um numero de porta válida\",\n  \"form_error_port_range\": \"Digite um número de porta entre 80 e 65535\",\n  \"form_error_port_unsafe\": \"Porta não é segura\",\n  \"form_error_positive\": \"Deve ser maior que 0\",\n  \"form_error_required\": \"Campo obrigatório\",\n  \"form_error_server_name\": \"Nome de servidor inválido\",\n  \"form_error_subnet\": \"A sub-rede \\\"{{cidr}}\\\" não contém o endereço IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Formato da URL inválida\",\n  \"form_error_url_or_path_format\": \"URL ou local da lista inválida\",\n  \"form_select_tags\": \"Selecione as tags do cliente\",\n  \"found_in_known_domain_db\": \"Encontrado no banco de dados de domínios conhecidos.\",\n  \"friday\": \"Sexta-feira\",\n  \"friday_short\": \"Sex\",\n  \"gateway_or_subnet_invalid\": \"Máscara de sub-rede inválida\",\n  \"general_settings\": \"Configurações gerais\",\n  \"general_statistics\": \"Estatísticas gerais\",\n  \"get_started\": \"Começar\",\n  \"greater_range_start_error\": \"Deve ser maior que o início do intervalo\",\n  \"homepage\": \"Página inicial\",\n  \"host_whitelisted\": \"O host está na lista branca\",\n  \"ignore_domains\": \"Domínios ignorados (separados por nova linha)\",\n  \"ignore_domains_desc_query\": \"As consultas que correspondem a essas regras não são gravadas no registro de consultas\",\n  \"ignore_domains_desc_stats\": \"As consultas que correspondem a essas regras não são gravadas nas estatísticas\",\n  \"ignore_domains_title\": \"Domínios ignorados\",\n  \"ignore_query_log\": \"Ignorar este cliente no registo de consultas\",\n  \"ignore_statistics\": \"Ignorar este cliente nas estatísticas\",\n  \"install_auth_confirm\": \"Confirmar senha\",\n  \"install_auth_desc\": \"A autenticação de senha para a interface da web de administrador do AdGuard Home deve ser configurada. Mesmo que o AdGuard Home esteja acessível apenas em sua rede local, ainda é importante protegê-la de acesso irrestrito.\",\n  \"install_auth_password\": \"Senha\",\n  \"install_auth_password_enter\": \"Digite a senha\",\n  \"install_auth_title\": \"Autenticação\",\n  \"install_auth_username\": \"Nome de usuário\",\n  \"install_auth_username_enter\": \"Digite o nome de usuário\",\n  \"install_devices_address\": \"O servidor de DNS do AdGuard Home está capturando os seguintes endereços\",\n  \"install_devices_android_list_1\": \"Na tela inicial do menu Android, toque em Configurações.\",\n  \"install_devices_android_list_2\": \"Toque em Wi-Fi. A tela listando todas as redes será exibida (não é possível configurar DNS personalizado para uma conexão de dados móveis)\",\n  \"install_devices_android_list_3\": \"Pressione prolongadamente a rede para a qual você está conectado e toque em Modificar rede\",\n  \"install_devices_android_list_4\": \"Em alguns dispositivos, talvez seja necessário marcar a caixa Avançado para ver as outras configurações. Para ajustar suas configurações de DNS do Android, você precisará alternar as configurações de IP de DHCP para Estático.\",\n  \"install_devices_android_list_5\": \"Altere os valores DNS 1 e DNS 2 para os endereços de servidores do AdGuard Home.\",\n  \"install_devices_desc\": \"Para que o AdGuard Home comece a funcionar, você precisa configurar seus dispositivos para usá-lo.\",\n  \"install_devices_ios_list_1\": \"Na tela incial, toque em Ajustes.\",\n  \"install_devices_ios_list_2\": \"Selecione Wi-Fi no menu esquerdo (não é possível configurar o DNS em conexões de dados móveis).\",\n  \"install_devices_ios_list_3\": \"Toque no nome da rede atualmente ativa.\",\n  \"install_devices_ios_list_4\": \"No campo DNS, digite os endereços dos servidores do AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Clique no ícone da Apple e depois em Preferências do Sistema.\",\n  \"install_devices_macos_list_2\": \"Clique em Rede.\",\n  \"install_devices_macos_list_3\": \"Selecione a primeira conexão da lista e clique em Avançado.\",\n  \"install_devices_macos_list_4\": \"Selecione a guia DNS e digite os endereços dos servidores do AdGuard Home.\",\n  \"install_devices_router\": \"Roteador\",\n  \"install_devices_router_desc\": \"Esta configuração cobre automaticamente todos os dispositivos conectados ao seu roteador doméstico, não há necessidade de configurar cada um deles manualmente.\",\n  \"install_devices_router_list_1\": \"Abra as preferências do seu roteador. Normalmente, você pode acessá-lo de seu navegador por meio de um URL, como http://192.168.0.1/ ou http://192.168.1.1/. Você pode ser solicitado a inserir uma senha. Se você não se lembrar, muitas vezes você pode redefinir a senha pressionando um botão no próprio roteador, mas esteja ciente de que se esse procedimento for escolhido, você provavelmente perderá toda a configuração do roteador. Se o seu roteador requer um aplicativo para configurá-lo, instale o aplicativo no seu telefone ou PC e use-o para acessar as configurações do roteador.\",\n  \"install_devices_router_list_2\": \"Encontre as Configurações de DNS. Procure as letras DNS ao lado de um campo que permite dois ou três conjuntos de números, cada um dividido em quatro grupos de um a três números.\",\n  \"install_devices_router_list_3\": \"Digite aqui seu servidor do AdGuard Home.\",\n  \"install_devices_router_list_4\": \"Em alguns tipos de roteador, um servidor DNS personalizado não pode ser configurado. Nesse caso, configurar o AdGuard Home como um <0>Servidor DHCP</0> pode ajudar. Caso contrário, você deve verificar o manual do roteador sobre como personalizar os servidores DNS em seu modelo de roteador específico.\",\n  \"install_devices_title\": \"Configure seus dispositivos\",\n  \"install_devices_windows_list_1\": \"Abra o Painel de Controle pelo Menu Iniciar ou pela Pesquisa do Windows.\",\n  \"install_devices_windows_list_2\": \"Entre na categoria Rede e Internet e depois clique em Central de Rede e Compartilhamento.\",\n  \"install_devices_windows_list_3\": \"No painel esquerdo, clique em \\\"Alterar configurações do adaptador\\\".\",\n  \"install_devices_windows_list_4\": \"Clique com o botão direito do mouse em sua conexão ativa e selecione Propriedades.\",\n  \"install_devices_windows_list_5\": \"Procure na lista por \\\"Internet Protocol Version 4 (TCP/IP)\\\" (ou por IPv6, \\\"Internet Protocol Version 6 (TCP/IPv6)\\\"), selecione e clique em Propriedades novamente.\",\n  \"install_devices_windows_list_6\": \"Marque \\\"usar os seguintes endereços de servidor DNS\\\" e digite os endereços do servidores do AdGuard Home.\",\n  \"install_saved\": \"Salvo com sucesso\",\n  \"install_settings_all_interfaces\": \"Todas interfaces\",\n  \"install_settings_dns\": \"Servidor DNS\",\n  \"install_settings_dns_desc\": \"Você precisa configurar seu dispositivo ou roteador para usar o servidor DNS nos seguintes endereços:\",\n  \"install_settings_interface_link\": \"A interface web de administrador do AdGuard estará disponível nos seguintes endereços:\",\n  \"install_settings_listen\": \"Interface de escuta\",\n  \"install_settings_port\": \"Porta\",\n  \"install_settings_title\": \"Interface web de administrador\",\n  \"install_static_configure\": \"O AdGuard Home detectou que o endereço IP dinâmico <0>{{ip}}</0> está sendo usado. Você deseja que seja definido como seu endereço estático?\",\n  \"install_static_error\": \"O AdGuard Home não pode configurar automaticamente para esta interface de rede. Por favor, procure uma instrução sobre como fazer isso manualmente.\",\n  \"install_static_ok\": \"Boas notícias! O endereço de IP estático já está configurado\",\n  \"install_step\": \"Passo\",\n  \"install_submit_desc\": \"O procedimento de configuração está concluído e agora você está pronto para começar a usar o AdGuard Home.\",\n  \"install_submit_title\": \"Parabéns!\",\n  \"install_welcome_desc\": \"O AdGuard Home é um servidor de DNS para bloqueio de anúncios e rastreamento em toda a rede. Sua finalidade é permitir que você controle toda a sua rede e seus dispositivos sem precisar ter um programa instalado.\",\n  \"install_welcome_title\": \"Bem-vindo(a) ao AdGuard Home!\",\n  \"interval_24_hour\": \"24 horas\",\n  \"interval_6_hour\": \"6 horas\",\n  \"interval_days\": \"{{count}} dias\",\n  \"interval_days_plural\": \"{{count}} dias\",\n  \"interval_hours\": \"{{count}} hora\",\n  \"interval_hours_plural\": \"{{count}} horas\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Endereço de IP\",\n  \"known_tracker\": \"Rastreador conhecido\",\n  \"last_rule_in_allowlist\": \"Não é possível desautorizar este cliente porque excluir a regra \\\"{{disallowed_rule}}\\\" DESATIVARÁ a lista de \\\"Clientes permitidos\\\".\",\n  \"last_time_updated_table_header\": \"Última atualização\",\n  \"list_confirm_delete\": \"Você tem certeza de que deseja excluir essa lista?\",\n  \"list_label\": \"Lista\",\n  \"list_updated\": \"{{count}} lista atualizada\",\n  \"list_updated_plural\": \"{{count}} listas atualizadas\",\n  \"list_url_table_header\": \"URL da lista\",\n  \"load_balancing\": \"Balanceamento de carga\",\n  \"load_balancing_desc\": \"Consulte um servidor upstream por vez.<br/>O AdGuard Home usa um algoritmo aleatório ponderado para selecionar servidores com o menor número de falhas e o menor tempo médio de consulta.\",\n  \"loading_table_status\": \"Carregando\",\n  \"local_ptr_default_resolver\": \"Por padrão, o AdGuard Home usa os seguintes resolvedores de DNS reverso: {{ip}}.\",\n  \"local_ptr_desc\": \"Os servidores DNS que o AdGuard Home utiliza para consultas privadas de PTR, SOA e NS. A solicitação é considerada privada se solicitar um domínio ARPA contendo uma sub-rede dentro de intervalos de IP privados, por exemplo \\\"192.168.12.34\\\", e vier de um cliente com endereço privado. Se não for definido, o AdGuard Home usará os endereços dos resolvedores DNS padrão do seu sistema operacional, exceto os endereços do próprio AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"A página inicial do AdGuard não conseguiu determinar resolvedores DNS reversos privados adequados para este sistema.\",\n  \"local_ptr_placeholder\": \"Insira um endereço IP por linha\",\n  \"local_ptr_title\": \"Servidores DNS reversos privados\",\n  \"location\": \"Localização\",\n  \"log_and_stats_section_label\": \"Registro de consultas e estatísticas\",\n  \"lower_range_start_error\": \"Deve ser inferior ao início do intervalo\",\n  \"main_settings\": \"Configurações principais\",\n  \"make_static\": \"Tornar estático\",\n  \"manual_update\": \"Por favor, <a>siga estes passos</a> para atualizar manualmente.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Segunda-feira\",\n  \"monday_short\": \"Seg\",\n  \"name\": \"Nome\",\n  \"name_table_header\": \"Nome\",\n  \"netname\": \"Nome da rede\",\n  \"network\": \"Rede\",\n  \"new_allowlist\": \"Nova lista de permissão\",\n  \"new_blocklist\": \"Nova lista de bloqueio\",\n  \"next\": \"Próximo\",\n  \"next_btn\": \"Próximo\",\n  \"no_blocklist_added\": \"Nenhuma lista de bloqueio adicionada\",\n  \"no_clients_found\": \"Nenhuma cliente encontrado\",\n  \"no_domains_found\": \"Nenhum domínio encontrado\",\n  \"no_logs_found\": \"Nenhum registro encontrado\",\n  \"no_servers_specified\": \"Nenhum servidor especificado\",\n  \"no_upstreams_data_found\": \"Nenhum dado de servidor DNS primário encontrado\",\n  \"no_whitelist_added\": \"Nenhuma lista de permissões foi adicionada\",\n  \"nothing_found\": \"Nada encontrado\",\n  \"null_ip\": \"IP nulo\",\n  \"number_of_dns_query_blocked_24_hours\": \"Várias solicitações DNS bloqueadas por filtros de bloqueio de anúncios e listas de bloqueio de hosts\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"O número de sites adultos bloqueados\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Várias solicitações de DNS bloqueadas pelo módulo de segurança da navegação do AdGuard\",\n  \"number_of_dns_query_days\": \"O número de consultas DNS processadas nos últimos {{count}} dias\",\n  \"number_of_dns_query_days_plural\": \"Número de consultas DNS processadas nos últimos {{count}} dias\",\n  \"number_of_dns_query_hours\": \"Número de consultas DNS processadas durante a última {{count}} hora\",\n  \"number_of_dns_query_hours_plural\": \"Número de consultas DNS processadas durante as últimas {{count}} horas\",\n  \"number_of_dns_query_to_safe_search\": \"O número de solicitações de DNS para mecanismos de pesquisa para os quais a pesquisa segura foi aplicada\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"Desligado\",\n  \"on\": \"Ligado\",\n  \"open_dashboard\": \"Abrir painel\",\n  \"orgname\": \"Nome da organização\",\n  \"original_response\": \"Resposta original\",\n  \"out_of_range_error\": \"Deve estar fora do intervalo \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Página\",\n  \"parallel_requests\": \"Solicitações paralelas\",\n  \"parental_control\": \"Controle parental\",\n  \"password_label\": \"Senha\",\n  \"password_placeholder\": \"Digite a senha\",\n  \"plain_dns\": \"DNS simples\",\n  \"port_53_faq_link\": \"A porta 53 é frequentemente ocupada por serviços \\\"DNSStubListener\\\" ou \\\"systemd-resolved\\\". Por favor leia <0>essa instrução</0> para resolver isso.\",\n  \"previous_btn\": \"Anterior\",\n  \"privacy_policy\": \"Política de privacidade\",\n  \"processing_update\": \"Por favor, aguarde enquanto o AdGuard Home está sendo atualizado\",\n  \"protection_section_label\": \"Proteção\",\n  \"protocol\": \"Protocolo\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Registro de consultas\",\n  \"query_log_clear\": \"Limpar registros de consulta\",\n  \"query_log_cleared\": \"O registro de consulta foi limpo com sucesso\",\n  \"query_log_configuration\": \"Configuração de registros\",\n  \"query_log_confirm_clear\": \"Você tem certeza que deseja limpar o registro de consulta?\",\n  \"query_log_disabled\": \"O registro de consulta está desativado e pode ser configurado em <0>configurações</0>\",\n  \"query_log_enable\": \"Ativar registro\",\n  \"query_log_filtered\": \"Filtrado por {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotação de registros de consulta\",\n  \"query_log_retention_confirm\": \"Tem a certeza de que quer alterar a rotação do registo de consulta? Se diminuir o valor do intervalo, alguns dados serão perdidos\",\n  \"query_log_strict_search\": \"Use aspas duplas para uma pesquisa mais criteriosa\",\n  \"query_log_updated\": \"O registro da consulta foi atualizado com sucesso\",\n  \"rate_limit\": \"Velocidade limite\",\n  \"rate_limit_desc\": \"O número de solicitações por segundo permitidas por cliente. Definir como 0 significa que não há limite.\",\n  \"rate_limit_subnet_len_ipv4\": \"Comprimento do prefixo de sub-rede para endereços IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Comprimento do prefixo de sub-rede para endereços IPv4 usados para limitação de velocidade. O padrão é 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"O comprimento do prefixo da sub-rede IPv4 deve estar entre 0 e 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Comprimento do prefixo de sub-rede para endereços IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Comprimento do prefixo de sub-rede para endereços IPv6 usados para limitação de velocidade. O padrão é 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"O comprimento do prefixo da sub-rede IPv6 deve estar entre 0 e 128\",\n  \"rate_limit_whitelist\": \"Lista de permissões de limitação de velocidade\",\n  \"rate_limit_whitelist_desc\": \"Endereços IP excluídos da limitação de velocidade\",\n  \"rate_limit_whitelist_placeholder\": \"Insira um endereço IP por linha\",\n  \"refresh_btn\": \"Atualizar\",\n  \"refresh_statics\": \"Atualizar estatísticas\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Reportar um problema\",\n  \"request_details\": \"Detalhes da solicitação\",\n  \"request_table_header\": \"Solicitação\",\n  \"requests_count\": \"Contagem de solicitações\",\n  \"reset_settings\": \"Redefinir configurações\",\n  \"resolve_clients_desc\": \"Resolva reversamente os endereços IP dos clientes em seus nomes de host, enviando consultas PTR aos resolvedores correspondentes (servidores DNS privados para clientes locais, servidores upstream para clientes com endereços IP públicos).\",\n  \"resolve_clients_title\": \"Ativar resolução reversa de endereços IP de clientes\",\n  \"response_code\": \"Código de resposta\",\n  \"response_details\": \"Detalhes da resposta\",\n  \"response_table_header\": \"Resposta\",\n  \"response_time\": \"Tempo de resposta\",\n  \"rewrite_A\": \"<0>A</0>: valor especial, mantenha <0>A</0> nos registros do upstream\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: valor especial, mantenha <0>AAAA</0> nos registros do servidor DNS primário\",\n  \"rewrite_add\": \"Adicionar reescrita de DNS\",\n  \"rewrite_added\": \"Reescrita de DNS para \\\"{{key}}\\\" adicionada com sucesso\",\n  \"rewrite_applied\": \"Regra de reescrita aplicada\",\n  \"rewrite_confirm_delete\": \"Você tem certeza de que deseja excluir a reescrita de DNS para \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"Reescrita de DNS para \\\"{{key}}\\\" excluída com sucesso\",\n  \"rewrite_desc\": \"Permite configurar uma resposta personalizada do DNS para um nome de domínio específico.\",\n  \"rewrite_domain_name\": \"Nome de domínio: adicione um registro CNAME\",\n  \"rewrite_edit\": \"Editar reconfiguração de DNS\",\n  \"rewrite_hosts_applied\": \"Reescrito pela regra do arquivo de hosts\",\n  \"rewrite_ip_address\": \"Endereço IP: use esse IP em uma resposta A ou AAAA\",\n  \"rewrite_not_found\": \"Nenhuma reescrita de DNS foi encontrada\",\n  \"rewrite_settings_updated\": \"Configurações de reescrita de DNS atualizadas com sucesso\",\n  \"rewrite_updated\": \"Reconfiguração de DNS atualizada com êxito\",\n  \"rewrites_disabled_table_header\": \"Reescritas desativadas\",\n  \"rewrites_enabled_table_header\": \"Reescritas ativadas\",\n  \"rewritten\": \"Reescrito\",\n  \"rows_table_footer_text\": \"linhas\",\n  \"rule_added_to_custom_filtering_toast\": \"Regra adicionada às regras de filtragem personalizadas: {{rule}}\",\n  \"rule_label\": \"Regra(s)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regra removida das regras de filtragem personalizadas: {{rule}}\",\n  \"rules_count_table_header\": \"Quantidade de regras\",\n  \"safe_browsing\": \"Navegação segura\",\n  \"safe_search\": \"Pesquisa segura\",\n  \"saturday\": \"Sábado\",\n  \"saturday_short\": \"Sab\",\n  \"save_btn\": \"Salvar\",\n  \"save_config\": \"Salvar configuração\",\n  \"schedule_add\": \"Adicionar agendamento\",\n  \"schedule_current_timezone\": \"Fuso horário atual: {{value}}\",\n  \"schedule_desc\": \"Define períodos de inatividade para serviços bloqueados\",\n  \"schedule_edit\": \"Editar agendamento\",\n  \"schedule_from\": \"De\",\n  \"schedule_invalid_select\": \"O horário de início deve ser antes do horário de término\",\n  \"schedule_modal_description\": \"Este agendamento substituirá qualquer agendamento existente para o mesmo dia da semana. Cada dia da semana pode ter apenas um período de inatividade.\",\n  \"schedule_modal_time_off\": \"Sem bloqueio de serviço:\",\n  \"schedule_new\": \"Novo agendamento\",\n  \"schedule_remove\": \"Remover agendamento\",\n  \"schedule_save\": \"Salvar agendamento\",\n  \"schedule_select_days\": \"Selecionar dias\",\n  \"schedule_services\": \"Pausar bloqueio de serviço\",\n  \"schedule_services_desc\": \"Configura o agendamento de pausa do filtro de bloqueio de serviço\",\n  \"schedule_services_desc_client\": \"Configura o agendamento de pausa do filtro de bloqueio de serviço para este cliente\",\n  \"schedule_time_all_day\": \"O dia todo\",\n  \"schedule_timezone\": \"Selecione um fuso horário\",\n  \"schedule_to\": \"Para\",\n  \"served_from_cache_label\": \"Servido a partir do cache\",\n  \"service_name\": \"Nome do serviço\",\n  \"set_static_ip\": \"Definir um endereço de IP estático\",\n  \"settings\": \"Configurações\",\n  \"settings_custom\": \"Personalizado\",\n  \"settings_global\": \"Global\",\n  \"setup_config_to_enable_dhcp_server\": \"Configure a configuração para ativar o servidor DHCP\",\n  \"setup_dns_notice\": \"Para usar o <1>DNS-sobre-HTTPS</1> ou <1>DNS-sobre-TLS</1>, você precisa <0>configurar a criptografia</0> nas configurações do AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-sobre-TLS:</0> Use <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-sobre-HTTPS:</0> Use <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_3\": \"<0>Aqui está uma lista de softwares que você pode usar.</0>\",\n  \"setup_dns_privacy_4\": \"Em um dispositivo iOS 14 ou macOS Big Sur, você pode baixar o arquivo especial '.mobileconfig' que adiciona os servidores <highlight>DNS-sobre-HTTPS</highlight> ou <highlight>DNS-sobre-TLS</highlight> nas configurações de DNS.\",\n  \"setup_dns_privacy_android_1\": \"O Android 9 suporta o DNS-sobre-TLS de forma nativa. Para configurá-lo, vá para Configurações → Rede e internet → Avançado → DNS privado e digite seu nome de domínio lá.\",\n  \"setup_dns_privacy_android_2\": \"O <0>AdGuard para Android</0> suporta <1>DNS-sobre-HTTPS</1> e <1>DNS-sobre-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> adiciona o suporte <1>DNS-sobre-HTTPS</1> para o Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"configuração para iOS e macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> suporta <1>DNS-sobre-HTTPS</1>, mas para configurá-lo para usar seu próprio servidor, você precisará gerar um <2>DNS Stamp</2>.\",\n  \"setup_dns_privacy_ios_2\": \"O <0>AdGuard para iOS</0> suporta a configuração do <1>DNS-sobre-HTTPS</1> e <1>DNS-sobre-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"O próprio AdGuard Home pode ser usado como um cliente DNS seguro em qualquer plataforma.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> suporta todos os protocolos de DNS seguros conhecidos.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> suporta <1>DNS-sobre-HTTPS</1>\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> suporta <1>DNS-sobre-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Você encontrará mais implementações <0>aqui</0> e <1>aqui</1>.\",\n  \"setup_dns_privacy_other_title\": \"Outras implementações\",\n  \"setup_guide\": \"Guia de configuração\",\n  \"show_all_filter_type\": \"Mostrar todos\",\n  \"show_blocked_responses\": \"Bloqueado\",\n  \"show_filtered_type\": \"Mostrar filtrados\",\n  \"show_processed_responses\": \"Processado\",\n  \"show_whitelisted_responses\": \"Na lista branca\",\n  \"sign_in\": \"Entrar\",\n  \"sign_out\": \"Encerrar sessão\",\n  \"source_label\": \"Fonte\",\n  \"static_ip\": \"Endereço de IP estático\",\n  \"static_ip_desc\": \"O AdGuard Home é um servidor, portanto, ele precisa de um endereço de IP estático para funcionar corretamente. Caso contrário, em algum momento, seu roteador poderá atribuir um novo endereço de IP neste dispositivo.\",\n  \"statistics_clear\": \" Limpar estatísticas\",\n  \"statistics_clear_confirm\": \"Você tem certeza de que deseja limpar as estatísticas?\",\n  \"statistics_cleared\": \"As estatísticas foram limpas com sucesso\",\n  \"statistics_configuration\": \"Configurações de estatísticas\",\n  \"statistics_enable\": \"Ativar estatísticas\",\n  \"statistics_retention\": \"Permanência das estatísticas\",\n  \"statistics_retention_confirm\": \"Você tem certeza que quer alterar o arquivamento das estatísticas? Se diminuir o valor do intervalo, alguns dados serão perdidos\",\n  \"statistics_retention_desc\": \"Se você diminuir o valor do intervalo, alguns dados serão perdidos\",\n  \"stats_adult\": \"Bloqueado sites adultos\",\n  \"stats_disabled\": \"As estatísticas foram desativadas. Você pode ligá-las através da <0>página de configurações</0>.\",\n  \"stats_disabled_short\": \"As estatísticas foram desativadas\",\n  \"stats_malware_phishing\": \"Bloqueado malware/phishing\",\n  \"stats_params\": \"Configuração de estatísticas\",\n  \"stats_query_domain\": \"Principais domínios consultados\",\n  \"subnet_error\": \"Endereços devem estar em uma sub-rede\",\n  \"sunday\": \"Domingo\",\n  \"sunday_short\": \"Dom\",\n  \"system_host_files\": \"Arquivos hosts do sistema\",\n  \"table_client\": \"Cliente\",\n  \"table_name\": \"Nome\",\n  \"tags_desc\": \"Você pode selecionar tags que correspondam ao cliente. Inclua tags nas regras de filtragem para aplicá-las com mais precisão. <0>Saber mais</0>.\",\n  \"tags_title\": \"Marcadores\",\n  \"test_upstream_btn\": \"Testar DNS primário\",\n  \"theme_auto\": \"Automático\",\n  \"theme_auto_desc\": \"Automático (com base no esquema de cores do seu dispositivo)\",\n  \"theme_dark\": \"Escuro\",\n  \"theme_dark_desc\": \"Tema escuro\",\n  \"theme_light\": \"Claro\",\n  \"theme_light_desc\": \"Tema claro\",\n  \"thursday\": \"Quinta-feira\",\n  \"thursday_short\": \"Qui\",\n  \"time_table_header\": \"Data\",\n  \"top_blocked_domains\": \"Principais domínios bloqueados\",\n  \"top_clients\": \"Principais clientes\",\n  \"top_upstreams\": \"Melhores servidores DNS primários\",\n  \"topline_expired_certificate\": \"Seu certificado SSL está expirado. Atualize suas <0>configurações de criptografia</0>\",\n  \"topline_expiring_certificate\": \"Seu certificado SSL está prestes a expirar. Atualize suas <0>configurações de criptografia</]0>\",\n  \"tracker_source\": \"Fonte do rastreador\",\n  \"try_again\": \"Tente novamente\",\n  \"ttl_cache_validation\": \"O substituto mínimo de cache TTL deve ser menor ou igual ao máximo\",\n  \"tuesday\": \"Terça-feira\",\n  \"tuesday_short\": \"Ter\",\n  \"type_table_header\": \"Tipo\",\n  \"unavailable_dhcp\": \"DHCP não está disponível\",\n  \"unavailable_dhcp_desc\": \"O AdGuard Home não pode executar um servidor DHCP em seu sistema operacional\",\n  \"unblock\": \"Desbloquear\",\n  \"unblock_all\": \"Desbloquear todos\",\n  \"unblock_for_this_client_only\": \"Desbloquear apenas para este cliente\",\n  \"unknown_filter\": \"Filtro desconhecido {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} está disponível!<0>Clique aqui</0> para mais informações.\",\n  \"update_failed\": \"A atualização automática falhou. Por favor, <a>siga estes passos</a> para atualizar manualmente.\",\n  \"update_now\": \"Atualizar agora\",\n  \"updated_custom_filtering_toast\": \"Regras personalizadas salvas com sucesso\",\n  \"updated_save_search_toast\": \"Configurações de Pesquisa Segura atualizadas\",\n  \"updated_upstream_dns_toast\": \"Servidores DNS primário salvos com sucesso\",\n  \"updates_checked\": \"Uma nova versão do AdGuard Home está disponível\\n\",\n  \"updates_version_equal\": \"O AdGuard Home está atualizado.\",\n  \"upstream\": \"Servidor DNS primário\",\n  \"upstream_dns\": \"Servidores DNS primário\",\n  \"upstream_dns_cache_configuration\": \"Configuração do cache de DNS upstream\",\n  \"upstream_dns_client_desc\": \"Se você mantiver este campo vazio, o AdGuard Home usará os servidores configurados nas configurações <0>DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Configurado em {{path}}\",\n  \"upstream_dns_help\": \"Insira o endereço de servidor, um por linha. <a>Saber mais</a> sobre a configuração de servidores DNS primários.\",\n  \"upstream_parallel\": \"Usar consultas paralelas para acelerar a resolução consultando simultaneamente todos os servidores DNS primário\",\n  \"upstream_timeout\": \"Tempo limite de upstream\",\n  \"upstream_timeout_desc\": \"Especifica o número de segundos para esperar por uma resposta do servidor upstream\",\n  \"upstreams\": \"DNS primário\",\n  \"use_adguard_browsing_sec\": \"Usar o serviço de segurança da navegação do AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"O AdGuard Home irá verificar se o domínio está bloqueado pelo serviço de segurança da navegação. Ele usará a API de pesquisa de privacidade para executar a verificação: apenas um prefixo curto do hash do nome de domínio SHA256 é enviado para o servidor.\",\n  \"use_adguard_parental\": \"Usar o serviço de controle parental do AdGuard\",\n  \"use_adguard_parental_hint\": \"O AdGuard Home irá verificar se o domínio contém conteúdo adulto. Ele usa a mesma API amigável de privacidade que o serviço de segurança da navegação.\",\n  \"use_private_ptr_resolvers_desc\": \"Resolver solicitações PTR, SOA e NS para domínios ARPA contendo endereços privados usando servidores upstream privados, DHCP, /etc/hosts e assim por diante. Se desativado, o AdGuard Home responde a todas essas consultas com NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Usar resolvedores DNS reversos privados\",\n  \"use_saved_key\": \"Use a chave salva anteriormente\",\n  \"username_label\": \"Nome de usuário\",\n  \"username_placeholder\": \"Digite o nome de usuário\",\n  \"validated_with_dnssec\": \"Validado com DNSSEC\",\n  \"version\": \"Versão\",\n  \"version_request_error\": \"A verificação de atualização falhou. Por favor, verifique sua conexão com a internet.\",\n  \"wednesday\": \"Quarta-feira\",\n  \"wednesday_short\": \"Quar\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/pt-pt.json",
    "content": "{\n  \"access_allowed_desc\": \"Uma lista de CIDRs, endereços IP ou <a>IDs de cliente</a>. Se esta lista tiver entradas, o AdGuard Home do aceitará solicitações apenas desses clientes.\",\n  \"access_allowed_title\": \"Clientes permitidos\",\n  \"access_blocked_desc\": \"Não deve ser confundido com filtros. O AdGuard Home elimina as consultas DNS que correspondem a esses domínios, e essas consultas nem aparecem no registo de consultas. Você pode especificar nomes de domínio exatos, caracteres curinga ou regras de filtro de URL, por exemplo \\\"exemplo.org\\\", \\\"*.exemplo.org\\\", ou \\\"||exemplo.org^\\\" correspondentemente.\",\n  \"access_blocked_title\": \"Domínios bloqueados\",\n  \"access_desc\": \"Aqui pode configurar as regras de acesso para o servidores de DNS do AdGuard Home\",\n  \"access_disallowed_desc\": \"Uma lista de CIDRs, endereços IP ou <a>IDs de cliente</a>. Se essa lista tiver entradas, o AdGuard Home descartará as solicitações desses clientes. Este campo é ignorado se houver entradas em clientes permitidos.\",\n  \"access_disallowed_title\": \"Clientes não permitidos\",\n  \"access_settings_saved\": \"Definições de acesso foram guardadas com sucesso\",\n  \"access_title\": \"Definições de acesso\",\n  \"actions_table_header\": \"Acções\",\n  \"add_allowlist\": \"Adicionar lista de permissões\",\n  \"add_blocklist\": \"Adicionar lista de bloqueio\",\n  \"add_custom_list\": \"Adicionar uma lista personalizada\",\n  \"add_persistent_client\": \"Adicionar como cliente persistente\",\n  \"address\": \"Endereço\",\n  \"adg_will_drop_dns_queries\": \"O AdGuard Home descartará todas as consultas DNS deste cliente.\",\n  \"all_lists_up_to_date_toast\": \"Todas as listas já estão atualizadas\",\n  \"all_queries\": \"Todas as consultas\",\n  \"allow_this_client\": \"Permitir este cliente\",\n  \"allowed\": \"Permitido\",\n  \"anonymize_client_ip\": \"Tornar anónimo o IP do cliente\",\n  \"anonymize_client_ip_desc\": \"Não gurda o endereço de IP completo do cliente em registo ou estatísticas\",\n  \"anonymizer_notification\": \"<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-la em <1>Definições gerais</1>.\",\n  \"answer\": \"Resposta\",\n  \"apply_btn\": \"Aplicar\",\n  \"auto_clients_desc\": \"Informações sobre endereços IP de dispositivos que estão a utilizar ou podem utilizar o AdGuard Home. Estas informações são recolhidas a partir de várias fontes, incluindo ficheiros hosts, DNS reverso etc.\",\n  \"auto_clients_title\": \"Clientes ativos\",\n  \"autofix_warning_list\": \"Irá realizar estas tarefas: <0>Desativar sistema DNSStubListener</0> <0>Definir endereço do servidor DNS para 127.0.0.1</0> <0>Substituir o alvo simbólico do link /etc/resolv.conf para /run/systemd/resolv.conf</0> <0>Parar DNSStubListener (recarregar serviço resolvido pelo sistema)</0>\",\n  \"autofix_warning_result\": \"Como resultado, todos as solicitações DNS do seu sistema serão processadas pelo AdGuard Home por predefinição.\",\n  \"autofix_warning_text\": \"Se clicar em \\\"Corrigir\\\", o AdGuardHome irá configurar o seu sistema para utilizar o servidor DNS do AdGuardHome.\",\n  \"average_processing_time\": \"Tempo médio de processamento\",\n  \"average_processing_time_hint\": \"Tempo médio em milissegundos no processamento de uma solicitação DNS\",\n  \"average_upstream_response_time\": \"Tempo médio de resposta upstream\",\n  \"back\": \"Retroceder\",\n  \"block\": \"Bloquear\",\n  \"block_all\": \"Bloquear todos\",\n  \"block_domain_use_filters_and_hosts\": \"Bloquear domínios usando ficheiros de filtros e hosts\",\n  \"block_for_this_client_only\": \"Bloquear apenas para este cliente\",\n  \"block_services\": \"Bloquear serviços específicos\",\n  \"blocked_adult_websites\": \"Bloqueado pelo controlo parental\",\n  \"blocked_by\": \"<0>Bloqueado por filtros</0>\",\n  \"blocked_by_cname_or_ip\": \"Bloqueado por CNAME ou IP\",\n  \"blocked_by_response\": \"Bloqueado por CNAME ou IP em resposta\",\n  \"blocked_response_ttl\": \"Resposta bloqueada TTL\",\n  \"blocked_response_ttl_desc\": \"Especifica por quantos segundos os clientes devem armazenar em cache uma resposta filtrada\",\n  \"blocked_safebrowsing\": \"Bloqueado pela navegação segura\",\n  \"blocked_service\": \"Serviço bloqueado\",\n  \"blocked_services\": \"Serviços bloqueados\",\n  \"blocked_services_desc\": \"Permite o bloqueio rápido de sítios e serviços populares.\",\n  \"blocked_services_global\": \"Usar serviços bloqueados globais\",\n  \"blocked_services_saved\": \"Serviços bloqueados guardados com sucesso\",\n  \"blocked_threats\": \"Ameaças bloqueadas\",\n  \"blocking_ipv4\": \"A bloquear IPv4\",\n  \"blocking_ipv4_desc\": \"Endereço IP a ser devolvido para uma solicitação A bloqueada\",\n  \"blocking_ipv6\": \"A bloquear IPv6\",\n  \"blocking_ipv6_desc\": \"Endereço IP a ser devolvido para uma solicitação AAAA bloqueada\",\n  \"blocking_mode\": \"Modo de bloqueio\",\n  \"blocking_mode_custom_ip\": \"IP personalizado: Responder com um endereço IP definido manualmente\",\n  \"blocking_mode_default\": \"Predefinido: Responder com zero endereço IP (0.0.0.0 para A; :: para AAAA) quando bloqueado pela regra de estilo Adblock; responde com o endereço IP especificado na regra quando bloqueado pela regra /etc/hosts-style\",\n  \"blocking_mode_null_ip\": \"IP nulo: Responder com endereço IP zero (0.0.0.0 para A; :: para AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Responder com o código NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: responder com o código REFUSED\",\n  \"blocklist\": \"Lista de bloqueio\",\n  \"bootstrap_dns\": \"Servidores DNS de arranque\",\n  \"bootstrap_dns_desc\": \"Endereços IP de servidores DNS usados para resolver endereços IP dos resolvedores DoH/DoT que você especifica como upstreams. Comentários não são permitidos.\",\n  \"cache_cleared\": \"O cache DNS foi apagado com sucesso\",\n  \"cache_enabled\": \"Ativar cache\",\n  \"cache_enabled_desc\": \"Armazene as respostas DNS localmente.\",\n  \"cache_optimistic\": \"Cache otimista\",\n  \"cache_optimistic_desc\": \"Faz o AdGuard Home responder a partir do cache mesmo quando as entradas expirarem e também tenta atualizá-las.\",\n  \"cache_size\": \"Tamanho do cache\",\n  \"cache_size_desc\": \"Tamanho do cache do DNS (em bytes).\",\n  \"cache_size_validation\": \"O tamanho do cache deve ser maior que zero quando ativado.\",\n  \"cache_ttl_max_override\": \"Sobrepor o TTL máximo\",\n  \"cache_ttl_max_override_desc\": \"Defina um valor máximo de tempo de vida (segundos) para entradas no cache DNS.\",\n  \"cache_ttl_min_override\": \"Sobrepor o TTL mínimo\",\n  \"cache_ttl_min_override_desc\": \"Prolongue os valores de curta duração (segundos) recebidos do servidor primário ao armazenar em cache as respostas DNS.\",\n  \"cancel_btn\": \"Cancelar\",\n  \"category_label\": \"Categoria\",\n  \"check\": \"Verificar\",\n  \"check_client_id\": \"Identificador do cliente (ClientID ou endereço IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Verificar se um nome do host está sendo filtrado.\",\n  \"check_dhcp_servers\": \"Verificar por servidores DHCP\",\n  \"check_dns_record\": \"Selecione o tipo de registro DNS\",\n  \"check_enter_client_id\": \"Insira o identificador do cliente\",\n  \"check_hostname\": \"Nome do hospedeiro ou nome de domínio\",\n  \"check_ip\": \"Endereços de IP: {{ip}}\",\n  \"check_not_found\": \"Não encontrado nas tuas listas de filtros\",\n  \"check_reason\": \"Motivo: {{reason}}\",\n  \"check_service\": \"Nome do serviço: {{service}}\",\n  \"check_title\": \"Verifique a filtragem\",\n  \"check_updates_btn\": \"Verificar atualizações\",\n  \"check_updates_now\": \"Verificar atualizações\",\n  \"choose_allowlist\": \"Escolher as listas de permissões\",\n  \"choose_blocklist\": \"Escolher as listas de bloqueio\",\n  \"choose_from_list\": \"Escolher na lista\",\n  \"city\": \"Cidade\",\n  \"clear_cache\": \"Limpar cache\",\n  \"click_to_view_queries\": \"Clique para ver as consultas\",\n  \"client_add\": \"Adicionar cliente\",\n  \"client_added\": \"Cliente \\\"{{key}}\\\" adicionado com sucesso\",\n  \"client_blocked\": \"Cliente \\\"{{ip}}\\\" foi bloqueado com sucesso\",\n  \"client_confirm_block\": \"Você tem certeza de que deseja bloquear o cliente \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Tem a certeza de que deseja excluir o cliente \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Você tem certeza de que deseja desbloquear o cliente \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Cliente \\\"{{key}}\\\" excluído com sucesso\",\n  \"client_details\": \"Detalhes do cliente\",\n  \"client_edit\": \"Editar cliente\",\n  \"client_global_settings\": \"Usar definições globais\",\n  \"client_id\": \"ID do cliente\",\n  \"client_id_desc\": \"Os clientes podem ser identificados por um ID de cliente especial. Saiba mais como identificar clientes <a>aqui</a>.\",\n  \"client_id_placeholder\": \"Insira o ID do cliente\",\n  \"client_identifier\": \"Identificador\",\n  \"client_identifier_desc\": \"Os clientes podem ser identificados pelo endereço IP, CIDR, Endereço MAC ou um ID de cliente especial (pode ser usado para DoT/DoH/DoQ). Saiba mais sobre como identificar clientes <0>aqui</0>.\",\n  \"client_name\": \"Cliente {{id}}\",\n  \"client_new\": \"Novo cliente\",\n  \"client_settings\": \"Definições do cliente\",\n  \"client_table_header\": \"Cliente\",\n  \"client_unblocked\": \"Cliente \\\"{{ip}}\\\" foi desbloqueado com sucesso\",\n  \"client_updated\": \"Cliente \\\"{{key}}\\\" atualizado com sucesso\",\n  \"clients_desc\": \"Configure registos de cliente persistentes para dispositivos conectados ao AdGuard Home\",\n  \"clients_not_found\": \"Nenhum cliente foi encontrado\",\n  \"clients_title\": \"Clientes persistentes\",\n  \"compact\": \"Compacto\",\n  \"config_successfully_saved\": \"Definição guardada com sucesso\",\n  \"configure\": \"Configurar\",\n  \"confirm_dns_cache_clear\": \"Tem certeza de que quer limpar a cache DNS?\",\n  \"confirm_static_ip\": \"O AdGuard Home irá configurar {{ip}} para ser seu endereço IP estático. Deseja continuar?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"País\",\n  \"custom_filter_rules\": \"Regras de filtragem personalizadas\",\n  \"custom_filter_rules_hint\": \"Insira uma regra por linha. Pode usar regras de bloqueio de anúncios ou a sintaxe de ficheiros de hosts.\",\n  \"custom_filtering_rules\": \"Regras de filtragem personalizadas\",\n  \"custom_ip\": \"IP Personalizado\",\n  \"custom_retention_input\": \"Insira a retenção em horas\",\n  \"custom_rotation_input\": \"Insira a rotação em horas\",\n  \"dashboard\": \"Painel\",\n  \"date\": \"Data\",\n  \"default\": \"Predefinido\",\n  \"delete_confirm\": \"Tem a certeza de que deseja excluir \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Apagar\",\n  \"descr\": \"Descrição\",\n  \"details\": \"Detalhes\",\n  \"dhcp_add_static_lease\": \"Adicionar nova concessão estática\",\n  \"dhcp_config_saved\": \"Definições DHCP guardadas com sucesso\",\n  \"dhcp_description\": \"Se o seu router não fornecer configurações de DHCP, poderá usar o servidor DHCP integrado do AdGuard.\",\n  \"dhcp_disable\": \"Desativar servidor DHCP\",\n  \"dhcp_dynamic_ip_found\": \"O seu sistema usa a configuração de endereço IP dinâmico para a interface <0>{{interfaceName}}</0>. Para usar o servidor DHCP, deve definir um endereço de IP estático. O seu endereço IP atual é <0> {{ipAddress}} </ 0>. AdGuard Home irá definir automaticamente este endereço IP como estático se pressionar o botão \\\"Ativar servidor DHCP\\\".\",\n  \"dhcp_edit_static_lease\": \"Editar concessão estática\",\n  \"dhcp_enable\": \"Ativar servidor DHCP\",\n  \"dhcp_error\": \"O AdGuard Home não conseguiu determinar se há noutro servidor DHCP ativo na rede\",\n  \"dhcp_form_gateway_input\": \"IP do gateway\",\n  \"dhcp_form_lease_input\": \"Duração da concessão\",\n  \"dhcp_form_lease_title\": \"Tempo de concessão do DHCP (em segundos)\",\n  \"dhcp_form_range_end\": \"Final da faixa\",\n  \"dhcp_form_range_start\": \"Início da faixa\",\n  \"dhcp_form_range_title\": \"Faixa de endereços IP\",\n  \"dhcp_form_subnet_input\": \"Máscara de sub-rede\",\n  \"dhcp_found\": \"Um servidor DHCP ativo foi encontrado na rede. Não é seguro ativar o servidor DHCP incorporado.\",\n  \"dhcp_hardware_address\": \"Endereço de hardware\",\n  \"dhcp_interface_select\": \"Selecione a interface DHCP\",\n  \"dhcp_ip_addresses\": \"Endereços de IP\",\n  \"dhcp_ipv4_settings\": \"Definições DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Definições DHCP IPv6\",\n  \"dhcp_lease_added\": \"Concessão estática \\\"{{key}}\\\" adicionada com sucesso\",\n  \"dhcp_lease_deleted\": \"Concessão estática \\\"{{key}}\\\" excluída com sucesso\",\n  \"dhcp_lease_updated\": \"Concessão estática \\\"{{key}}\\\" atualizada com sucesso\",\n  \"dhcp_leases\": \"Concessões DHCP\",\n  \"dhcp_leases_not_found\": \"Nenhuma concessão DHCP encontrada\",\n  \"dhcp_new_static_lease\": \"Nova concessão estática\",\n  \"dhcp_not_found\": \"É seguro ativar o servidor DHCP integrado porque o AdGuard Home não encontrou nenhum servidor DHCP ativo na rede. No entanto, você deve verificar isso manualmente, pois a verificação automática atualmente não oferece 100% de garantia.\",\n  \"dhcp_reset\": \"Tem a certeza de que deseja repor a definição de DHCP?\",\n  \"dhcp_reset_leases\": \"Repor todas as concessões\",\n  \"dhcp_reset_leases_confirm\": \"Tem certeza de que deseja repor todas as concessões?\",\n  \"dhcp_reset_leases_success\": \"Concessões de DHCP repostas com sucesso\",\n  \"dhcp_settings\": \"Definições de DHCP\",\n  \"dhcp_static_ip_error\": \"Para usar o servidor DHCP, deve definir um endereço IP estático. AdGuard Home não conseguiu determinar se essa interface de rede está configurada usando o endereço de IP estático. Por favor, defina um endereço IP estático manualmente.\",\n  \"dhcp_static_leases\": \"Concessões de DHCP estático\",\n  \"dhcp_static_leases_not_found\": \"Nenhuma concessão DHCP estática foi encontrada\",\n  \"dhcp_table_expires\": \"Expira\",\n  \"dhcp_table_hostname\": \"Nome do servidor\",\n  \"dhcp_title\": \"Servidor DHCP (experimental)\",\n  \"dhcp_warning\": \"Se tu quiser ativar o servidor DHCP de qualquer maneira, certifique-se de que não haja outro servidor DHCP ativo em tua rede, pois isso pode quebrar a conectividade com a Internet para dispositivos na rede!\",\n  \"disable_for_hours\": \"Por {{count}} hora\",\n  \"disable_for_hours_plural\": \"Por {{count}} horas\",\n  \"disable_for_minutes\": \"Por {{count}} minuto\",\n  \"disable_for_minutes_plural\": \"Por {{count}} minutos\",\n  \"disable_for_seconds\": \"Por {{count}} segundo\",\n  \"disable_for_seconds_plural\": \"Por {{count}} segundos\",\n  \"disable_ipv6\": \"Desativar resolução de endereços IPv6\",\n  \"disable_ipv6_desc\": \"Descarte todas as consultas DNS para endereços IPv6 (tipo AAAA) e remova as dicas IPv6 das respostas HTTPS.\",\n  \"disable_notify_for_hours\": \"Desativar proteção por {{count}} hora\",\n  \"disable_notify_for_hours_plural\": \"Desativar proteção por {{count}} horas\",\n  \"disable_notify_for_minutes\": \"Desativar proteção por {{count}} minuto\",\n  \"disable_notify_for_minutes_plural\": \"Desativar proteção por {{count}} minutos\",\n  \"disable_notify_for_seconds\": \"Desativar proteção por {{count}} segundo\",\n  \"disable_notify_for_seconds_plural\": \"Desativar proteção por {{count}} segundos\",\n  \"disable_notify_until_tomorrow\": \"Desativar a proteção até amanhã\",\n  \"disable_protection\": \"Desativar proteção\",\n  \"disable_rewrites\": \"Desativar regras de reescrita\",\n  \"disable_until_tomorrow\": \"Até amanhã\",\n  \"disabled\": \"Desativado\",\n  \"disabled_dhcp\": \"Servidor DHCP desativado\",\n  \"disabled_filtering_toast\": \"Filtragem desativada\",\n  \"disabled_parental_toast\": \"Controlo parental desativado\",\n  \"disabled_protection\": \"Desativar proteção\",\n  \"disabled_safe_browsing_toast\": \"Navegação segura desativada\",\n  \"disabled_safe_search_toast\": \"Pesquisa segura desativada\",\n  \"disallow_this_client\": \"Não permitir este cliente\",\n  \"dns_addresses\": \"Endereços DNS\",\n  \"dns_allowlists\": \"Listas de permissões de DNS\",\n  \"dns_allowlists_desc\": \"Os domínios das listas de permissões de DNS serão permitidos mesmo que estejam em qualquer uma das listas de bloqueio.\",\n  \"dns_blocklists\": \"Lista de bloqueio de DNS\",\n  \"dns_blocklists_desc\": \"O AdGuard Home bloqueará domínios que correspondam às listas de bloqueio.\",\n  \"dns_cache_config\": \"Definição de cache DNS\",\n  \"dns_cache_config_desc\": \"Aqui você pode configurar o cache do DNS\",\n  \"dns_cache_size\": \"Tamanho da cache DNS, em bytes\",\n  \"dns_config\": \"Definição do servidor DNS\",\n  \"dns_over_https\": \"DNS-sobre-HTTPS\",\n  \"dns_over_quic\": \"DNS-sobre-QUIC\",\n  \"dns_over_tls\": \"DNS-sobre-TLS\",\n  \"dns_privacy\": \"Privacidade de DNS\",\n  \"dns_providers\": \"Aqui está uma <0>lista de provedores de DNS conhecidos</0> para escolher.\",\n  \"dns_query\": \"Consultas de DNS\",\n  \"dns_rewrites\": \"Reescritas de DNS\",\n  \"dns_settings\": \"Definições de DNS\",\n  \"dns_start\": \"O servidor DNS está a iniciar\",\n  \"dns_status_error\": \"Ocorreu um erro ao verificar o estado do servidor DNS\",\n  \"dns_test_not_ok_toast\": \"O servidor \\\"{{key}}\\\": não pôde ser utilizado. Por favor, verifique se o escreveu corretamente\",\n  \"dns_test_ok_toast\": \"Os servidores DNS especificados estão a funcionar corretamente\",\n  \"dns_test_parsing_error_toast\": \"A seção {{section}}: linha {{line}}: não pôde ser usada. Verifique se foi escrita corretamente\",\n  \"dns_test_warning_toast\": \"Servidor DNS primário \\\"{{key}}\\\" não responde aos solicitações de teste e pode não funcionar corretamente\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Ativar DNSSEC\",\n  \"dnssec_enable_desc\": \"Definir a marcação DNSSEC nas consultas de DNS em andamento e verificar o resultado (é necessário um resolvedor DNSSEC ativado).\",\n  \"domain\": \"Domínio\",\n  \"domain_desc\": \"Insere o nome do domínio para ser reescrito.\",\n  \"domain_name_table_header\": \"Nome do domínio\",\n  \"domain_or_client\": \"Domínio ou cliente\",\n  \"down\": \"Caiu\",\n  \"download_mobileconfig\": \"Transferir ficheiro de configuração\",\n  \"download_mobileconfig_doh\": \"Transferir .mobileconfig para DNS-sobre-HTTPS\",\n  \"download_mobileconfig_dot\": \"Transferir .mobileconfig para DNS-sobre-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Editar lista de permissões\",\n  \"edit_blocklist\": \"Editar lista de bloqueio\",\n  \"edit_table_action\": \"Editar\",\n  \"edns_cs_desc\": \"Adicione a opção de sub-rede de cliente EDNS (ECS) às solicitações de servidor DNS primário e registre os valores enviados pelos clientes no registo de consulta.\",\n  \"edns_enable\": \"Ativar a sub-rede do cliente EDNS\",\n  \"edns_use_custom_ip\": \"Usar IP personalizado para EDNS\",\n  \"edns_use_custom_ip_desc\": \"Permitir a utilização de IP personalizado para EDNS\",\n  \"elapsed\": \"Tempo decorrido\",\n  \"empty_response_status\": \"Vazio\",\n  \"enable_protection\": \"Ativar proteção\",\n  \"enable_protection_timer\": \"A proteção será habilitada em {{time}}\",\n  \"enable_rewrites\": \"Ativar regras de reescrita\",\n  \"enable_upstream_dns_cache\": \"Ativar o armazenamento em cache do DNS para a configuração de upstream personalizada deste cliente\",\n  \"enabled_dhcp\": \"Servidor DHCP ativado\",\n  \"enabled_filtering_toast\": \"Filtragem ativada\",\n  \"enabled_parental_toast\": \"Controlo parental ativado\",\n  \"enabled_protection\": \"Ativar proteção\",\n  \"enabled_safe_browsing_toast\": \"Navegação segura ativada\",\n  \"enabled_save_search_toast\": \"Pesquisa segura ativada\",\n  \"enabled_table_header\": \"Ativado\",\n  \"encryption_certificate_path\": \"Caminho do certificado\",\n  \"encryption_certificates\": \"Certificados\",\n  \"encryption_certificates_desc\": \"Para usar criptografia, precisa de fornecer uma cadeia de certificados SSL válida para o seu domínio. Pode obter um certificado gratuito em <0> {{link}}</0> ou pode comprá-lo numa das autoridades de certificação confiáveis.\",\n  \"encryption_certificates_input\": \"Copie/cole aqui o seu certificado codificado em PEM.\",\n  \"encryption_certificates_source_content\": \"Colar o conteúdo dos certificados\",\n  \"encryption_certificates_source_path\": \"Definir um caminho do ficheiro de certificados\",\n  \"encryption_chain_invalid\": \"A cadeia de certificado é inválida\",\n  \"encryption_chain_valid\": \"Cadeia de certificado válida\",\n  \"encryption_config_saved\": \"Definição de criptografia guardada\",\n  \"encryption_desc\": \"Suporta a criptografia (HTTPS/QUIC/TLS) para DNS e interface de administração web\",\n  \"encryption_doq\": \"Porta DNS-sobre-QUIC\",\n  \"encryption_doq_desc\": \"Se esta porta estiver configurada, o AdGuard Home executará um servidor DNS-sobre-QUIC nesta porta. \",\n  \"encryption_dot\": \"Porta DNS-sobre-TLS\",\n  \"encryption_dot_desc\": \"Se essa porta estiver configurada, o AdGuard Home irá executar o servidor DNS-sobre- TSL nesta porta.\",\n  \"encryption_enable\": \"Ativar criptografia (HTTPS, DNS-sobre-HTTPS e DNS-sobre-TLS)\",\n  \"encryption_enable_desc\": \"Se a criptografia estiver ativada, a interface administrativa do AdGuard Home funcionará em HTTPS, o servidor DNS irá capturar as solicitações por meio do DNS-sobre-HTTPS e DNS-sobre-TLS.\",\n  \"encryption_expire\": \"Expira\",\n  \"encryption_hostnames\": \"Nomes dos servidores\",\n  \"encryption_https\": \"Porta HTTPS\",\n  \"encryption_https_desc\": \"Se a porta HTTPS estiver configurada, a interface administrativa do AdGuard Home será acessível via HTTPS e também fornecerá o DNS-sobre-HTTPS no local '/dns-query'.\",\n  \"encryption_issuer\": \"Emissor\",\n  \"encryption_key\": \"Chave privada\",\n  \"encryption_key_input\": \"Copie/cole aqui a chave privada codificada em PEM para o seu certificado.\",\n  \"encryption_key_invalid\": \"Esta é uma chave privada {{type}} inválida\",\n  \"encryption_key_source_content\": \"Colar o conteúdo da chave privada\",\n  \"encryption_key_source_path\": \"Definir um caminho para o ficheiro de chave privada\",\n  \"encryption_key_valid\": \"Esta é uma chave privada {{type}} válida\",\n  \"encryption_plain_dns_desc\": \"O DNS simples (sem criptografia) está ativado por padrão. Pode desativá-lo para forçar todos os dispositivos a usar DNS criptografado. Para isso, deve ativar pelo menos um protocolo DNS criptografado\",\n  \"encryption_plain_dns_enable\": \"Habilitar DNS simples (sem criptografia)\",\n  \"encryption_plain_dns_error\": \"Para desabilitar o DNS simples, habilite pelo menos um protocolo DNS criptografado\",\n  \"encryption_private_key_path\": \"Caminho da chave privada\",\n  \"encryption_redirect\": \"Redirecionar automaticamente para HTTPS\",\n  \"encryption_redirect_desc\": \"Se marcado, o AdGuard Home irá redirecionar automaticamente os endereços HTTP para HTTPS.\",\n  \"encryption_reset\": \"Tem a certeza de que deseja repor a definição de criptografia?\",\n  \"encryption_server\": \"Nome do servidor\",\n  \"encryption_server_desc\": \"Se definido, AdGuard Home detecta ClientIDs, responde a consultas DDR, e executa validações de ligações adicionais. Se não estiver definido, estas características são desactivadas. Devem corresponder a um dos Nomes DNS no certificado.\",\n  \"encryption_server_enter\": \"Insira o seu nome de domínio\",\n  \"encryption_settings\": \"Definições de criptografia\",\n  \"encryption_status\": \"Estado\",\n  \"encryption_subject\": \"Assunto\",\n  \"encryption_title\": \"Encriptação\",\n  \"encryption_warning\": \"Cuidado\",\n  \"enforce_safe_search\": \"Usar pesquisa segura\",\n  \"enforce_save_search_hint\": \"O AdGuard Home aplicará pesquisa segura nos seguintes motores de busca: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Forçar pesquisa segura\",\n  \"enter_cache_size\": \"Digite o tamanho do cache (bytes)\",\n  \"enter_cache_ttl_max_override\": \"Digite o TTL máximo (segundos)\",\n  \"enter_cache_ttl_min_override\": \"Digite o TTL máximo (segundos)\",\n  \"enter_name_hint\": \"Insira o nome\",\n  \"enter_url_or_path_hint\": \"Digite a URL ou o local da lista\",\n  \"enter_valid_allowlist\": \"Digite uma URL válida para a lista de permissões.\",\n  \"enter_valid_blocklist\": \"Digite uma URL válida para a lista de bloqueio.\",\n  \"error_details\": \"Detalhes do erro\",\n  \"example_comment\": \"! Aqui vai um comentário.\",\n  \"example_comment_hash\": \"# Também um comentário.\",\n  \"example_comment_meaning\": \"apenas um comentário;\",\n  \"example_meaning_filter_block\": \"bloqueia o acesso ao exemplo.org e a todos os seus subdomínios;\",\n  \"example_meaning_filter_whitelist\": \"desbloqueia o acesso ao exemplo.org e a todos os seus subdomínios;\",\n  \"example_meaning_host_block\": \"retorna o endereço 127.0.0.1 para o exemplo.org (exceto seus subdomínios);\",\n  \"example_multiple_upstreams_reserved\": \"múltiplos upstreams <0>para domínios específicos</0>;\",\n  \"example_regex_meaning\": \"bloquear o acesso aos domínios que correspondam à expressão regular especificada.\",\n  \"example_rewrite_domain\": \"reescrever resposta apenas para este domínio.\",\n  \"example_rewrite_wildcard\": \"reescrever resposta para todos <0>example.org</0> sub-domínios.\",\n  \"example_upstream_comment\": \"um comentário.\",\n  \"example_upstream_doh\": \"<0>DNS-sobre-HTTPS</0> criptografado;\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS encriptado com <0>HTTP/3</0> forçado e sem retorno para HTTP/2 ou inferior;\",\n  \"example_upstream_doq\": \"<0>DNS-sobre-QUIC</0> criptografado;\",\n  \"example_upstream_dot\": \"<0>DNS-sobre-TLS</0> criptografado;\",\n  \"example_upstream_regular\": \"DNS regular (através do UDP)\",\n  \"example_upstream_regular_port\": \"DNS normal (através do UDP, com porta);\",\n  \"example_upstream_reserved\": \"Podes especificar o DNS primário <0>para domínio(s) especifico(s)</0>\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> para o <1>DNSCrypt</1> ou usar os resolvedores <2>DNS-sobre-HTTPS</2>;\",\n  \"example_upstream_tcp\": \"DNS regular (através do TCP);\",\n  \"example_upstream_tcp_hostname\": \"DNS normal (através do TCP, nome do servidor);\",\n  \"example_upstream_tcp_port\": \"dNS normal (através do TCP, com porta);\",\n  \"example_upstream_udp\": \"DNS normal (através do UDP, nome do servidor);\",\n  \"examples_title\": \"Exemplos\",\n  \"fallback_dns_desc\": \"Lista de servidores DNS de fallback usados quando os servidores DNS upstream não estão respondendo. A sintaxe é a mesma do campo principal de upstreams acima.\",\n  \"fallback_dns_placeholder\": \"Insira um servidor DNS de fallback por linha\",\n  \"fallback_dns_title\": \"Servidores DNS de fallback\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Endereço de IP mais rápido\",\n  \"fastest_addr_desc\": \"Aguarda por respostas de <b>todos</b> os servidores DNS, mede a velocidade da ligação TCP para cada servidor e devolva o endereço IP do servidor com a velocidade de ligação mais rápida.<br/>Este modo pode abrandar significativamente as consultas DNS, se um ou mais servidores upstream não estiverem a responder. Certifique-se de que os seus servidores upstream são estáveis e que o tempo esgotado de upstream é baixo.\",\n  \"filter\": \"Filtro\",\n  \"filter_added_successfully\": \"O filtro foi adicionado com sucesso\",\n  \"filter_allowlist\": \"AVISO: Esta ação também excluirá a regra \\\"{{disallowed_rule}}\\\" da lista de clientes permitidos.\",\n  \"filter_category_general\": \"Geral\",\n  \"filter_category_general_desc\": \"Listas que bloqueiam o monitorização e a publicidade na maioria dos dispositivos\",\n  \"filter_category_other\": \"Noutro\",\n  \"filter_category_other_desc\": \"Outras listas de bloqueio\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Listas focadas em anúncios regionais e servidores de monitorização\",\n  \"filter_category_security\": \"Segurança\",\n  \"filter_category_security_desc\": \"Listas projetadas especificamente em bloquear domínios maliciosos, de phishing e fraude\",\n  \"filter_removed_successfully\": \"A lista foi removida com sucesso\",\n  \"filter_updated\": \"O filtro atualizado com sucesso\",\n  \"filtered\": \"Filtrado\",\n  \"filtered_custom_rules\": \"Filtrado pelas regras de filtragem personalizadas\",\n  \"filtering_rules_learn_more\": \"<0>Saiba mais</0>sobre como criar as suas próprias listas negras de servidores.\",\n  \"filters\": \"Filtros\",\n  \"filters_and_hosts_hint\": \"O AdGuard Home entende regras básicas de bloqueio de anúncios e a sintaxe de ficheiros de hosts.\",\n  \"filters_block_toggle_hint\": \"Pode configurar as regras de bloqueio nas configurações de <a>Filtros</a>.\",\n  \"filters_configuration\": \"Definição dos filtros\",\n  \"filters_enable\": \"Ativar filtros\",\n  \"filters_interval\": \"Intervalo de atualização de filtro\",\n  \"fix\": \"Corrigido\",\n  \"for_last_days\": \"nos últimos {{count}} dias\",\n  \"for_last_days_plural\": \"nos últimos {{count}} dias\",\n  \"for_last_hours\": \"na última {{count}} hora\",\n  \"for_last_hours_plural\": \"nas últimas {{count}} horas\",\n  \"forgot_password\": \"Não se lembra da palavra-passe?\",\n  \"forgot_password_desc\": \"Siga <0>estes passos</0> para criar uma nova palavra-passe para a sua conta de utilizador.\",\n  \"form_add_id\": \"Adicionar identificador\",\n  \"form_answer\": \"Insira o endereço de IP ou nome de domínio\",\n  \"form_client_name\": \"Insira o nome do cliente\",\n  \"form_domain\": \"Inserir domínio\",\n  \"form_enter_blocked_response_ttl\": \"Insira o TTL da resposta bloqueada (segundos)\",\n  \"form_enter_host\": \"Insira o hostname\",\n  \"form_enter_hostname\": \"Insira o hostname\",\n  \"form_enter_id\": \"Inserir identificador\",\n  \"form_enter_ip\": \"Insira IP\",\n  \"form_enter_mac\": \"Insira o endereço MAC\",\n  \"form_enter_rate_limit\": \"Insira o limite de velocidade\",\n  \"form_enter_rate_limit_subnet_len\": \"Introduza o comprimento do prefixo da sub-rede para limitação da velocidade\",\n  \"form_enter_subnet_ip\": \"Digite um endereço IP na sub-rede \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Insira a duração do tempo esgotado do servidor upstream em segundos\",\n  \"form_error_answer_format\": \"Formato de resposta inválido\",\n  \"form_error_client_id_format\": \"O ID do cliente deve conter apenas números, letras minúsculas e hifens\",\n  \"form_error_domain_format\": \"Formato de domínio inválido\",\n  \"form_error_equal\": \"Não deve ser igual\",\n  \"form_error_gateway_ip\": \"A concessão não pode ter o endereço IP do gateway\",\n  \"form_error_ip4_format\": \"Endereço de IPv4 inválido\",\n  \"form_error_ip4_gateway_format\": \"Endereço IPv4 de gateway inválido\",\n  \"form_error_ip6_format\": \"Endereço de IPv6 inválido\",\n  \"form_error_ip_format\": \"Endereço de email inválido\",\n  \"form_error_mac_format\": \"Endereço de MAC inválido\",\n  \"form_error_password\": \"As palavras-passe não coincidem\",\n  \"form_error_password_length\": \"A palavra-passe deve ter {{min}} a {{max}} caracteres\",\n  \"form_error_port\": \"Insira um número de porta válida\",\n  \"form_error_port_range\": \"Digite um numero de porta entre 80 e 65535\",\n  \"form_error_port_unsafe\": \"Porta não é segura\",\n  \"form_error_positive\": \"Deve ser maior que 0\",\n  \"form_error_required\": \"Campo obrigatório\",\n  \"form_error_server_name\": \"Nome de servidor inválido\",\n  \"form_error_subnet\": \"A sub-rede \\\"{{cidr}}\\\" não contém o endereço IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Formato da URL inválida\",\n  \"form_error_url_or_path_format\": \"URL ou local da lista inválida\",\n  \"form_select_tags\": \"Selecione as tags do cliente\",\n  \"found_in_known_domain_db\": \"Encontrado no banco de dados de domínios conhecido.\",\n  \"friday\": \"Sexta-feira\",\n  \"friday_short\": \"Sexta\",\n  \"gateway_or_subnet_invalid\": \"Máscara de sub-rede inválida\",\n  \"general_settings\": \"Definições gerais\",\n  \"general_statistics\": \"Estatísticas gerais\",\n  \"get_started\": \"Vamos Começar\",\n  \"greater_range_start_error\": \"Deve ser maior que o início do intervalo\",\n  \"homepage\": \"Página inicial\",\n  \"host_whitelisted\": \"O host está na lista branca\",\n  \"ignore_domains\": \"Domínios ignorados (separados por nova linha)\",\n  \"ignore_domains_desc_query\": \"As consultas que correspondem a essas regras não são gravadas no registo de consultas\",\n  \"ignore_domains_desc_stats\": \"As consultas que correspondem a essas regras não são gravadas nas estatísticas\",\n  \"ignore_domains_title\": \"Domínios ignorados\",\n  \"ignore_query_log\": \"Ignorar este cliente no log de consulta\",\n  \"ignore_statistics\": \"Ignorar este cliente nas estatísticas\",\n  \"install_auth_confirm\": \"Confirmar palavra-passe\",\n  \"install_auth_desc\": \"A autenticação de palavra-passe para a interface da web de administrador do AdGuard Home deve ser configurada. Mesmo que o AdGuard Home esteja acessível apenas em sua rede local, ainda é importante protegê-la de acesso irrestrito.\",\n  \"install_auth_password\": \"Palavra-passe\",\n  \"install_auth_password_enter\": \"Insira palavra-passe\",\n  \"install_auth_title\": \"Autenticação\",\n  \"install_auth_username\": \"Nome do utilizador\",\n  \"install_auth_username_enter\": \"Insira o nome de utilizador\",\n  \"install_devices_address\": \"O servidor de DNS do AdGuard Home está a capturar os seguintes endereços\",\n  \"install_devices_android_list_1\": \"No painel inicial do menu Android, toque em Definições.\",\n  \"install_devices_android_list_2\": \"Toque em Wi-Fi no menu. O painel com todas as redes será exibida (não é possível configurar DNS personalizado para uma conexão de dados móveis).\",\n  \"install_devices_android_list_3\": \"Pressione prolongadamente a rede à qual está ligado e toque em Modificar Rede.\",\n  \"install_devices_android_list_4\": \"Toque em Wi-Fi no menu. O painel com todas as redes será exibida (não é possível configurar DNS personalizado para uma conexão de dados móveis).\",\n  \"install_devices_android_list_5\": \"Altere os valores DNS 1 e DNS 2 para os endereços de servidores do AdGuard Home.\",\n  \"install_devices_desc\": \"Para que o AdGuard Home comece a funcionar, precisa de configurar os seus dispositivos para o poder usar.\",\n  \"install_devices_ios_list_1\": \"No painel inicial, toque em Definições.\",\n  \"install_devices_ios_list_2\": \"Selecione Wi-Fi no menu esquerdo (não é possível configurar o DNS em conexões de dados móveis).\",\n  \"install_devices_ios_list_3\": \"Toque no nome da rede atualmente ativa.\",\n  \"install_devices_ios_list_4\": \"No campo DNS, digite os endereços dos servidores do AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Clique no ícone da Apple e depois em Preferências do Sistema.\",\n  \"install_devices_macos_list_2\": \"Clique em Rede.\",\n  \"install_devices_macos_list_3\": \"Selecione a primeira ligação da lista e clique em Avançado.\",\n  \"install_devices_macos_list_4\": \"Selecione a guia DNS e insira os endereços dos servidores do AdGuard Home.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Esta configuração cobre automaticamente todos os dispositivos conectados ao seu router doméstico, sem a necessidade de configurar cada um deles manualmente.\",\n  \"install_devices_router_list_1\": \"Abra as preferências do seu router. Normalmente, tu podes acessá-lo de teu navegador por meio de um URL, como http://192.168.0.1/ ou http://192.168.1.1/. Tu podes ser solicitado a inserir uma palavra-passe. Se tu não se lembrar, muitas vezes tu podes repor a palavra-passe pressionando um botão no próprio roteador, mas esteja ciente de que se esse procedimento for escolhido, tu provavelmente perderás toda a definição do router. Se o teu router requer uma aplicação para configurá-lo, instale a aplicação no seu telefone ou PC e use-o para acessar as definições do router.\",\n  \"install_devices_router_list_2\": \"Encontre as configurações de DNS. Procure as letras DNS ao lado de um campo que permite dois ou três conjuntos de números, cada um dividido em quatro grupos de um a três números.\",\n  \"install_devices_router_list_3\": \"Insira aqui seu servidor do AdGuard Home.\",\n  \"install_devices_router_list_4\": \"Em alguns tipos de router, um servidor DNS personalizado não pode ser configurado. Nesse caso, configurar o AdGuard Home como um <0>Servidor DHCP</0> pode ajudar. Caso contrário, tu deves verificar o manual do router sobre como personalizar os servidores DNS no seu modelo de router específico.\",\n  \"install_devices_title\": \"Configure os seus dispositivos\",\n  \"install_devices_windows_list_1\": \"Abra o Painel de Controlo através do Menu Iniciar ou pela Pesquisa do Windows.\",\n  \"install_devices_windows_list_2\": \"Entre na categoria Rede e Internet e depois clique em Central de Rede e Partilha.\",\n  \"install_devices_windows_list_3\": \"No painel esquerdo, clique em \\\"Alterar configurações do adaptador\\\".\",\n  \"install_devices_windows_list_4\": \"Clique com o botão direito do mouse em sua conexão ativa e selecione Propriedades.\",\n  \"install_devices_windows_list_5\": \"Procure na lista por \\\"Internet Protocol Version 4 (TCP/IP)\\\" (ou por IPv6, \\\"Internet Protocol Version 6 (TCP/IPv6)\\\"), selecione e clique em Propriedades novamente.\",\n  \"install_devices_windows_list_6\": \"Marque \\\"Usar os seguintes endereços de servidor DNS\\\" e insira os endereços do servidores do AdGuard Home.\",\n  \"install_saved\": \"Guardado com sucesso\",\n  \"install_settings_all_interfaces\": \"Todas as interfaces\",\n  \"install_settings_dns\": \"Servidor DNS\",\n  \"install_settings_dns_desc\": \"Precisa de configurar o seu dispositivo ou router para usar o servidor DNS nos seguintes endereços:\",\n  \"install_settings_interface_link\": \"A interface web de administrador do AdGuard estará disponível nos seguintes endereços:\",\n  \"install_settings_listen\": \"Interface de escuta\",\n  \"install_settings_port\": \"Porta\",\n  \"install_settings_title\": \"Interface web de administrador\",\n  \"install_static_configure\": \"O AdGuard Home detectou que o endereço IP dinâmico <0>{{ip}}</0> está sendo usado. Tu desejas que seja definido como teu endereço estático?\",\n  \"install_static_error\": \"O AdGuard Home não pode configurar automaticamente para esta interface de rede. Por favor, procure uma instrução sobre como fazer isso manualmente.\",\n  \"install_static_ok\": \"Boas notícias! O endereço de IP estático já está configurado\",\n  \"install_step\": \"Passo\",\n  \"install_submit_desc\": \"O procedimento de configuração está concluído e agora você está pronto para começar a usar o AdGuard Home.\",\n  \"install_submit_title\": \"Parabéns!\",\n  \"install_welcome_desc\": \"O AdGuard Home é um servidor de DNS para bloqueio de anúncios e monitorização em toda a rede. A sua finalidade é permitir que controle toda a sua rede e os seus dispositivos sem precisar de ter um programa instalado.\",\n  \"install_welcome_title\": \"Bem-vindo ao AdGuard Home!\",\n  \"interval_24_hour\": \"24 horas\",\n  \"interval_6_hour\": \"6 horas\",\n  \"interval_days\": \"{{count}} dias\",\n  \"interval_days_plural\": \"{{count}} dias\",\n  \"interval_hours\": \"{{count}} hora\",\n  \"interval_hours_plural\": \"{{count}} horas\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Endereço de IP\",\n  \"known_tracker\": \"Rastreador conhecido\",\n  \"last_rule_in_allowlist\": \"Não é possível desautorizar este cliente porque excluir a regra \\\"{{disallowed_rule}}\\\" DESATIVARÁ a lista de \\\"Clientes permitidos\\\".\",\n  \"last_time_updated_table_header\": \"Última atualização\",\n  \"list_confirm_delete\": \"Você tem certeza de que deseja excluir essa lista?\",\n  \"list_label\": \"Lista\",\n  \"list_updated\": \"{{count}} lista atualizada\",\n  \"list_updated_plural\": \"{{count}} listas atualizadas\",\n  \"list_url_table_header\": \"URL da lista\",\n  \"load_balancing\": \"Balanceamento de carga\",\n  \"load_balancing_desc\": \"Consulta um servidor upstream de cada vez. <br/>O AdGuard Home usa um algoritmo aleatório ponderado para selecionar servidores com o menor número de pesquisas falhadas e o menor tempo médio de pesquisa.\",\n  \"loading_table_status\": \"A carregar...\",\n  \"local_ptr_default_resolver\": \"Por predefinição, o AdGuard Home usa os seguintes resolvedores de DNS reverso: {{ip}}.\",\n  \"local_ptr_desc\": \"Os servidores DNS que o AdGuard Home utiliza para consultas privadas de PTR, SOA e NS. A solicitação é considerada privada se solicitar um domínio ARPA contendo uma sub-rede dentro de intervalos de IP privados, por exemplo \\\"192.168.12.34\\\", e vier de um cliente com endereço privado. Se não for definido, o AdGuard Home usará os endereços dos resolvedores DNS padrão do seu sistema operacional, exceto os endereços do próprio AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"A página inicial do AdGuard não conseguiu determinar resolvedores DNS reversos privados adequados para este sistema.\",\n  \"local_ptr_placeholder\": \"Insira um endereço IP por linha\",\n  \"local_ptr_title\": \"Servidores DNS reversos privados\",\n  \"location\": \"Localização\",\n  \"log_and_stats_section_label\": \"Log de consulta e estatísticas\",\n  \"lower_range_start_error\": \"Deve ser inferior ao início do intervalo\",\n  \"main_settings\": \"Definições principais\",\n  \"make_static\": \"Tornar estático\",\n  \"manual_update\": \"Por favor, <a>siga estes passos</a> para atualizar manualmente.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Segunda-feira\",\n  \"monday_short\": \"Seg\",\n  \"name\": \"Nome\",\n  \"name_table_header\": \"Nome\",\n  \"netname\": \"Nome da rede\",\n  \"network\": \"Rede\",\n  \"new_allowlist\": \"Nova lista de permissões\",\n  \"new_blocklist\": \"Nova lista de bloqueio\",\n  \"next\": \"Seguinte\",\n  \"next_btn\": \"Seguinte\",\n  \"no_blocklist_added\": \"Nenhuma lista de bloqueio foi adicionada\",\n  \"no_clients_found\": \"Nenhum cliente foi encontrado\",\n  \"no_domains_found\": \"Não foram encontrados domínios\",\n  \"no_logs_found\": \"Nenhum registo encontrado\",\n  \"no_servers_specified\": \"Nenhum servidor especificado\",\n  \"no_upstreams_data_found\": \"Nenhum dado de servidor DNS primário encontrado\",\n  \"no_whitelist_added\": \"Nenhuma lista de permissões foi adicionada\",\n  \"nothing_found\": \"Nada encontrado\",\n  \"null_ip\": \"IP nulo\",\n  \"number_of_dns_query_blocked_24_hours\": \"Várias solicitações DNS bloqueadas por filtros de bloqueio de anúncios e listas de bloqueio de hosts\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"O número de sítios adultos bloqueados\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Várias solicitações de DNS bloqueadas pelo módulo de segurança da navegação do AdGuard\",\n  \"number_of_dns_query_days\": \"Número de consultas DNS processadas durante los últimos {{count}} días\",\n  \"number_of_dns_query_days_plural\": \"Número de consultas DNS processadas durante os últimos {{count}} dias\",\n  \"number_of_dns_query_hours\": \"Número de consultas DNS processadas durante a última {{count}} hora\",\n  \"number_of_dns_query_hours_plural\": \"Número de consultas DNS processadas durante as últimas {{count}} horas\",\n  \"number_of_dns_query_to_safe_search\": \"O número de solicitações de DNS para motores de busca para os quais a pesquisa segura foi aplicada\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"DESLIGADO\",\n  \"on\": \"LIGADO\",\n  \"open_dashboard\": \"Abrir Painel\",\n  \"orgname\": \"Nome da organização\",\n  \"original_response\": \"Resposta original\",\n  \"out_of_range_error\": \"Deve estar fora do intervalo \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Página\",\n  \"parallel_requests\": \"Solicitações paralelas\",\n  \"parental_control\": \"Controlo parental\",\n  \"password_label\": \"Palavra-passe\",\n  \"password_placeholder\": \"Insira palavra-passe\",\n  \"plain_dns\": \"DNS simples\",\n  \"port_53_faq_link\": \"A porta 53 é frequentemente ocupada por serviços \\\"DNSStubListener\\\" ou \\\"systemd-resolved\\\". Por favor leia <0>essa instrução</0> para resolver isso.\",\n  \"previous_btn\": \"Anterior\",\n  \"privacy_policy\": \"Política de privacidade\",\n  \"processing_update\": \"Por favor espere, o AdGuard Home está a atualizar-se\",\n  \"protection_section_label\": \"Proteção\",\n  \"protocol\": \"Protocolo\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Registo de consultas\",\n  \"query_log_clear\": \"Limpar registos de consulta\",\n  \"query_log_cleared\": \"O registo de consulta foi limpo com sucesso\",\n  \"query_log_configuration\": \"Definições do registo\",\n  \"query_log_confirm_clear\": \"Tem a certeza de que deseja limpar todo o registo de consulta?\",\n  \"query_log_disabled\": \"O registo de consulta está desativado e pode ser configurado em <0>definições</0>\",\n  \"query_log_enable\": \"Ativar registo\",\n  \"query_log_filtered\": \"Filtrado por {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Rotação de registros de consulta\",\n  \"query_log_retention_confirm\": \"Tem a certeza de que quer alterar a rotação do registo de consulta? Se diminuir o valor do intervalo, alguns dados serão perdidos\",\n  \"query_log_strict_search\": \"Usar aspas duplas para uma pesquisa rigorosa\",\n  \"query_log_updated\": \"O registo da consulta foi atualizado com sucesso\",\n  \"rate_limit\": \"Limite de velocidade\",\n  \"rate_limit_desc\": \"O número de solicitações por segundo permitido por cliente. Configurando para 0 significa sem limite.\",\n  \"rate_limit_subnet_len_ipv4\": \"Comprimento do prefixo de sub-rede para endereços IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Comprimento do prefixo de sub-rede para endereços IPv4 usados para limitação de velocidade. O padrão é 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"O comprimento do prefixo da sub-rede IPv4 deve estar entre 0 e 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Comprimento do prefixo de sub-rede para endereços IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Comprimento do prefixo de sub-rede para endereços IPv6 usados para limitação de velocidade. O padrão é 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"O comprimento do prefixo da sub-rede IPv6 deve situar-se entre 0 e 128\",\n  \"rate_limit_whitelist\": \"Lista de permissões de limitação de velocidade\",\n  \"rate_limit_whitelist_desc\": \"Endereços IP excluídos da limitação de velocidade\",\n  \"rate_limit_whitelist_placeholder\": \"Insira um endereço IP por linha\",\n  \"refresh_btn\": \"Atualizar\",\n  \"refresh_statics\": \"Atualizar estatísticas\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Comunicar um problema\",\n  \"request_details\": \"Detalhes da solicitação\",\n  \"request_table_header\": \"Solicitação\",\n  \"requests_count\": \"Contagem de solicitações\",\n  \"reset_settings\": \"Repor definições\",\n  \"resolve_clients_desc\": \"Resolva reversamente os endereços IP dos clientes em seus nomes de host, enviando consultas PTR aos resolvedores correspondentes (servidores DNS privados para clientes locais, servidores upstream para clientes com endereços IP públicos).\",\n  \"resolve_clients_title\": \"Ativar resolução reversa de endereços IP de clientes\",\n  \"response_code\": \"Código de resposta\",\n  \"response_details\": \"Detalhes da resposta\",\n  \"response_table_header\": \"Resposta\",\n  \"response_time\": \"Tempo de resposta\",\n  \"rewrite_A\": \"<0>A</0>: valor especial, mantenha <0>A</0> nos registos do upstream\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: valor especial, mantenha <0>AAAA</0> nos registos do servidor DNS primário\",\n  \"rewrite_add\": \"Adicionar reescrita de DNS\",\n  \"rewrite_added\": \"Reescrita de DNS para \\\"{{key}}\\\" adicionada com sucesso\",\n  \"rewrite_applied\": \"Regra de reescrita aplicada\",\n  \"rewrite_confirm_delete\": \"Tem a certeza de que deseja excluir a reescrita de DNS para \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"Reescrita de DNS para \\\"{{key}}\\\" excluída com sucesso\",\n  \"rewrite_desc\": \"Permite configurar uma resposta personalizada do DNS para um nome de domínio específico.\",\n  \"rewrite_domain_name\": \"Nome de domínio: adicione um registo CNAME\",\n  \"rewrite_edit\": \"Editar reedição de DNS\",\n  \"rewrite_hosts_applied\": \"Reescrito pela regra do ficheiro de hosts\",\n  \"rewrite_ip_address\": \"Endereço IP: use esse IP em uma resposta A ou AAAA\",\n  \"rewrite_not_found\": \"Nenhuma reescrita de DNS foi encontrada\",\n  \"rewrite_settings_updated\": \"Definições de reescrita de DNS actualizadas com sucesso\",\n  \"rewrite_updated\": \"Reedição de DNS atualizada com sucesso\",\n  \"rewrites_disabled_table_header\": \"As reescritas estão desativadas\",\n  \"rewrites_enabled_table_header\": \"As reescritas estão ativadas\",\n  \"rewritten\": \"Reescrito\",\n  \"rows_table_footer_text\": \"linhas\",\n  \"rule_added_to_custom_filtering_toast\": \"Regra adicionada às regras de filtragem personalizadas: {{rule}}\",\n  \"rule_label\": \"Regra(s)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regra removida das regras de filtragem personalizadas: {{rule}}\",\n  \"rules_count_table_header\": \"Total de Regras\",\n  \"safe_browsing\": \"Navegação segura\",\n  \"safe_search\": \"Pesquisa segura\",\n  \"saturday\": \"Sábado\",\n  \"saturday_short\": \"Sábado\",\n  \"save_btn\": \"Guardar\",\n  \"save_config\": \"Guardar definição\",\n  \"schedule_add\": \"Adicionar agendamento\",\n  \"schedule_current_timezone\": \"Fuso horário atual: {{value}}\",\n  \"schedule_desc\": \"Defina períodos de inatividade para serviços bloqueados\",\n  \"schedule_edit\": \"Editar agendamento\",\n  \"schedule_from\": \"De\",\n  \"schedule_invalid_select\": \"O horário de início deve ser antes do horário de término\",\n  \"schedule_modal_description\": \"Este horário substituirá quaisquer horários existentes para o mesmo dia da semana. Cada dia da semana só pode ter um período de inatividade.\",\n  \"schedule_modal_time_off\": \"Sem bloqueio de serviço:\",\n  \"schedule_new\": \"Novo agendamento\",\n  \"schedule_remove\": \"Remover agendamento\",\n  \"schedule_save\": \"Salvar agendamento\",\n  \"schedule_select_days\": \"Selecione os dias\",\n  \"schedule_services\": \"Pausar bloqueio de serviço\",\n  \"schedule_services_desc\": \"Configure o agendamento de pausa do filtro de bloqueio de serviço\",\n  \"schedule_services_desc_client\": \"Configure o agendamento de pausa do filtro de bloqueio de serviço para este cliente\",\n  \"schedule_time_all_day\": \"O dia todo\",\n  \"schedule_timezone\": \"Selecione um fuso horário\",\n  \"schedule_to\": \"Para\",\n  \"served_from_cache_label\": \"Servido a partir do cache\",\n  \"service_name\": \"Nome do serviço\",\n  \"set_static_ip\": \"Definir um endereço de IP estático\",\n  \"settings\": \"Definições\",\n  \"settings_custom\": \"Personalizar\",\n  \"settings_global\": \"Global\",\n  \"setup_config_to_enable_dhcp_server\": \"Defina a configuração para ativar o servidor DHCP\",\n  \"setup_dns_notice\": \"Para usar o <1>DNS-sobre-HTTPS</1> ou <1>DNS-sobre-TLS</1>, precisa de <0>configurar a criptografia</0> nas configurações do AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-sobre-TLS:</0> Use <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-sobre-HTTPS:</0> Use <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_3\": \"<0>Aqui está uma lista de softwares que você pode usar.</0>\",\n  \"setup_dns_privacy_4\": \"Em um dispositivo iOS 14 ou macOS Big Sur, você pode transferir o ficheiro especial '.mobileconfig' que adiciona os servidores <highlight>DNS-sobre-HTTPS</highlight> ou <highlight>DNS-sobre-TLS</highlight> nas definições de DNS.\",\n  \"setup_dns_privacy_android_1\": \"O Android 9 suporta o DNS-sobre-TLS de forma nativa. Para o configurar, vá a Definições → Rede e internet → Avançado → DNS privado e digite o seu nome de domínio.\",\n  \"setup_dns_privacy_android_2\": \"O <0>AdGuard para Android</0> suporta <1>DNS-sobre-HTTPS</1> e <1>DNS-sobre-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> adiciona o suporte <1>DNS-sobre-HTTPS</1> para o Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"configuração para iOS e macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> suporta <1>DNS-sobre-HTTPS</1>, mas para o configurar para usar o seu próprio servidor, precisará de gerar um <2>DNS Stamp</2>.\",\n  \"setup_dns_privacy_ios_2\": \"O <0>AdGuard para iOS</0> suporta a definição do <1>DNS-sobre-HTTPS</1> e <1>DNS-sobre-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"O próprio AdGuard Home pode ser usado como um cliente DNS seguro em qualquer plataforma.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> suporta todos os protocolos de DNS seguros conhecidos.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> suporta <1>DNS-sobre-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> suporta <1>DNS-sobre-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Encontrará mais implementações <0>aqui</0> e <1>aqui</1>.\",\n  \"setup_dns_privacy_other_title\": \"Outras implementações\",\n  \"setup_guide\": \"Guia de instalação\",\n  \"show_all_filter_type\": \"Mostrar todos\",\n  \"show_blocked_responses\": \"Bloqueado\",\n  \"show_filtered_type\": \"Mostrar filtrados\",\n  \"show_processed_responses\": \"Processado\",\n  \"show_whitelisted_responses\": \"Na lista branca\",\n  \"sign_in\": \"Iniciar sessão\",\n  \"sign_out\": \"Sair\",\n  \"source_label\": \"Fonte\",\n  \"static_ip\": \"Endereço de IP estático\",\n  \"static_ip_desc\": \"O AdGuard Home é um servidor, portanto, ele precisa de um endereço de IP estático para funcionar corretamente. Caso contrário, em algum momento, seu router poderá atribuir um novo endereço de IP neste dispositivo.\",\n  \"statistics_clear\": \"Limpar estatísticas\",\n  \"statistics_clear_confirm\": \"Tem a certeza de que deseja limpar as estatísticas?\",\n  \"statistics_cleared\": \"As estatísticas foram apagadas com sucesso\",\n  \"statistics_configuration\": \"Definição das estatísticas\",\n  \"statistics_enable\": \"Ativar estatísticas\",\n  \"statistics_retention\": \"Retenção de estatísticas\",\n  \"statistics_retention_confirm\": \"Tem a certeza que quer alterar a retenção de estatísticas? Se diminuir o valor do intervalo, alguns dados serão perdidos\",\n  \"statistics_retention_desc\": \"Se diminuir o valor do intervalo, alguns dados serão perdidos\",\n  \"stats_adult\": \"Sítios adultos bloqueados\",\n  \"stats_disabled\": \"As estatísticas foram desativadas. Você pode ligá-las através da <0>página de definições</0>.\",\n  \"stats_disabled_short\": \"As estatísticas foram desativadas\",\n  \"stats_malware_phishing\": \"Malware/phishing bloqueados\",\n  \"stats_params\": \"Definição de estatísticas\",\n  \"stats_query_domain\": \"Principais domínios consultados\",\n  \"subnet_error\": \"Os endereços devem estar numa sub-rede\",\n  \"sunday\": \"Domingo\",\n  \"sunday_short\": \"Domingo\",\n  \"system_host_files\": \"Arquivos hosts do sistema\",\n  \"table_client\": \"Cliente\",\n  \"table_name\": \"Nome\",\n  \"tags_desc\": \"Você pode selecionar tags que correspondam ao cliente. Inclua tags nas regras de filtragem para aplicá-las com mais precisão. <0>Saber mais</0>.\",\n  \"tags_title\": \"Etiquetas\",\n  \"test_upstream_btn\": \"Testar DNS primário\",\n  \"theme_auto\": \"Automático\",\n  \"theme_auto_desc\": \"Automático (com base no esquema de cores do seu dispositivo)\",\n  \"theme_dark\": \"Escuro\",\n  \"theme_dark_desc\": \"Tema escuro\",\n  \"theme_light\": \"Claro\",\n  \"theme_light_desc\": \"Tema claro\",\n  \"thursday\": \"Quinta-feira\",\n  \"thursday_short\": \"Quinta\",\n  \"time_table_header\": \"Data\",\n  \"top_blocked_domains\": \"Principais domínios bloqueados\",\n  \"top_clients\": \"Principais clientes\",\n  \"top_upstreams\": \"Melhores servidores DNS primários\",\n  \"topline_expired_certificate\": \"O seu certificado SSL está expirado. Atualize as suas <0>definições de criptografia</0>.\",\n  \"topline_expiring_certificate\": \"O seu certificado SSL está prestes a expirar. Atualize as suas <0>definições de criptografia</0>.\",\n  \"tracker_source\": \"Fonte do rastreador\",\n  \"try_again\": \"Tente novamente\",\n  \"ttl_cache_validation\": \"O substituto mínimo de cache TTL deve ser menor ou igual ao máximo\",\n  \"tuesday\": \"Terça-feira\",\n  \"tuesday_short\": \"Terça\",\n  \"type_table_header\": \"Tipo\",\n  \"unavailable_dhcp\": \"DHCP não está disponível\",\n  \"unavailable_dhcp_desc\": \"O AdGuard Home não pode executar um servidor DHCP em seu sistema operacional\",\n  \"unblock\": \"Desbloquear\",\n  \"unblock_all\": \"Desbloquear todos\",\n  \"unblock_for_this_client_only\": \"Desbloquear apenas para este cliente\",\n  \"unknown_filter\": \"Filtro desconhecido {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} está disponível!<0>Clique aqui</0> para mais informações.\",\n  \"update_failed\": \"A atualização automática falhou. Por favor, <a>siga estes passos</a> para atualizar manualmente.\",\n  \"update_now\": \"Atualizar agora\",\n  \"updated_custom_filtering_toast\": \"Regras personalizadas guardadas com sucesso\",\n  \"updated_save_search_toast\": \"Configurações de pesquisa segura actualizadas\",\n  \"updated_upstream_dns_toast\": \"Servidores DNS primário guardados com sucesso\",\n  \"updates_checked\": \"Uma nova versão do AdGuard Home está disponível\\n\",\n  \"updates_version_equal\": \"O AdGuard Home está atualizado\",\n  \"upstream\": \"Servidor DNS primário\",\n  \"upstream_dns\": \"Servidores DNS primário\",\n  \"upstream_dns_cache_configuration\": \"Configuração da cache do DNS upstream\",\n  \"upstream_dns_client_desc\": \"Se mantiver esse campo vazio, o AdGuard Home usará os servidores configurados nas <0>Definições de DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Configurado em {{path}}\",\n  \"upstream_dns_help\": \"Insira um endereço de servidor, um por linha. <a>Saber mais</a> sobre a definição de servidores DNS primários.\",\n  \"upstream_parallel\": \"Usar consultas paralelas para acelerar a resolução consultando simultaneamente todos os servidores DNS\",\n  \"upstream_timeout\": \"Tempo esgotado de upstream\",\n  \"upstream_timeout_desc\": \"Especifica o número de segundos a aguardar por uma resposta do servidor upstream\",\n  \"upstreams\": \"DNS primário\",\n  \"use_adguard_browsing_sec\": \"Usar o serviço de segurança da navegação do AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"O AdGuard Home irá verificar se o domínio está bloqueado na segurança da navegação. Usará a API de pesquisa de privacidade para executar a verificação: apenas um prefixo curto do hash do nome de domínio SHA256 é enviado para o servidor.\",\n  \"use_adguard_parental\": \"Usar o serviço de controlo parental do AdGuard\",\n  \"use_adguard_parental_hint\": \"O AdGuard Home irá verificar se o domínio contém conteúdo adulto. Usa a mesma API amigável de privacidade que o serviço de segurança da navegação.\",\n  \"use_private_ptr_resolvers_desc\": \"Resolver solicitações PTR, SOA e NS para domínios ARPA contendo endereços privados usando servidores upstream privados, DHCP, /etc/hosts e assim por diante. Se desativado, o AdGuard Home responde a todas essas consultas com NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Usar resolvedores DNS reversos privados\",\n  \"use_saved_key\": \"Use a chave guardada anteriormente\",\n  \"username_label\": \"Nome do utilizador\",\n  \"username_placeholder\": \"Insira o nome de utilizador\",\n  \"validated_with_dnssec\": \"Validado com DNSSEC\",\n  \"version\": \"Versão\",\n  \"version_request_error\": \"A verificação de atualização falhou. Verifique a sua ligação à internet.\",\n  \"wednesday\": \"Quarta-feira\",\n  \"wednesday_short\": \"Quarta\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/ro.json",
    "content": "{\n  \"access_allowed_desc\": \"O listă de CIDR-uri, adrese IP sau <a>ClientID-uri</a>. Dacă această listă are intrări, AdGuard Home va accepta cereri numai de la acești clienți.\",\n  \"access_allowed_title\": \"Clienți autorizați\",\n  \"access_blocked_desc\": \"A nu se confunda cu filtrele. AdGuard Home respinge cererile DNS pentru aceste domenii, iar aceste cereri nici măcar nu apar în jurnalul de solicitări. Puteți specifica nume exacte de domenii, metacaractere sau reguli de filtrare URL, cum ar fi \\\"example.org\\\", \\\"*.exemple.org\\\" sau \\\"||example.org^\\\" în mod corespunzător.\",\n  \"access_blocked_title\": \"Domenii blocate\",\n  \"access_desc\": \"Aici puteți configura regulile de acces pentru serverul DNS AdGuard Home\",\n  \"access_disallowed_desc\": \"O listă de CIDR-uri, adrese IP sau <a>ClientID-uri</a>. Dacă această listă are intrări, AdGuard Home va renunța la cererile de la acești clienți. Acest câmp este ignorat dacă există intrări în „Clienți permiși”.\",\n  \"access_disallowed_title\": \"Clienți neautorizați\",\n  \"access_settings_saved\": \"Setările de acces au fost salvate cu succes\",\n  \"access_title\": \"Setări de acces\",\n  \"actions_table_header\": \"Acțiuni\",\n  \"add_allowlist\": \"Adăugați autorizare\",\n  \"add_blocklist\": \"Adăugați blocaj\",\n  \"add_custom_list\": \"Adăugați propria listă\",\n  \"add_persistent_client\": \"Adăugați ca client persistent\",\n  \"address\": \"Adresă\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home va renunța la toate interogările DNS de la acest client.\",\n  \"all_lists_up_to_date_toast\": \"Toate listele sunt deja la zi\",\n  \"all_queries\": \"Toate interogările\",\n  \"allow_this_client\": \"Permiteți acest client\",\n  \"allowed\": \"Permise\",\n  \"anonymize_client_ip\": \"Anonimizare client IP\",\n  \"anonymize_client_ip_desc\": \"Nu salvați adresa IP completă a clientului în jurnale și statistici\",\n  \"anonymizer_notification\": \"<0>Nota:</0> Anonimizarea IP este activată. Puteți să o dezactivați în <1>Setări generale</1>.\",\n  \"answer\": \"Răspuns\",\n  \"apply_btn\": \"Aplică\",\n  \"auto_clients_desc\": \"Informații despre adresele IP ale dispozitivelor care utilizează sau pot utiliza AdGuard Home. Aceste informații sunt colectate din mai multe surse, inclusiv din fișiere hosts, DNS inversat etc.\",\n  \"auto_clients_title\": \"Clienți runtime\",\n  \"autofix_warning_list\": \"Va efectua aceste sarcini: <0>Dezactivare sistem DNSStubListener</0> <0>Setare adresă server DNS la 127.0.0.1</0> <0>Înlocuire link simbolic țintă /etc/resolv.conf cu /run/systemd/resolve/resolv.conf</0> <0>Oprire DNSStubListener (reîncărcare servici rezolvat prin sistem)</0>\",\n  \"autofix_warning_result\": \"Ca urmare, toate cererile DNS ale sistemul dvs. vor fi procesate în mod implicit de AdGuardHome.\",\n  \"autofix_warning_text\": \"Dacă clicați pe \\\"Fix\\\", AdGuardHome va configura sistemul dvs. pentru a utiliza serverul DNS AdGuardHome.\",\n  \"average_processing_time\": \"Timpul mediu de procesare\",\n  \"average_processing_time_hint\": \"Timp mediu în milisecunde la procesarea unei cereri DNS\",\n  \"average_upstream_response_time\": \"Timpul mediu de răspuns al serverului în amonte\",\n  \"back\": \"Înapoi\",\n  \"block\": \"Blocați\",\n  \"block_all\": \"Blocați tot\",\n  \"block_domain_use_filters_and_hosts\": \"Blocați domenii folosind filtre și fișiere hosts\",\n  \"block_for_this_client_only\": \"Blocați numai pentru acest client\",\n  \"block_services\": \"Blochează anumite servicii\",\n  \"blocked_adult_websites\": \"Site-uri pentru adulți blocate\",\n  \"blocked_by\": \"<0>Blocate de Filtre</0>\",\n  \"blocked_by_cname_or_ip\": \"Blocat de CNAME sau IP\",\n  \"blocked_by_response\": \"Blocat de CNAME sau IP ca răspuns\",\n  \"blocked_response_ttl\": \"Răspuns blocat TTL\",\n  \"blocked_response_ttl_desc\": \"Specifică pentru câte secunde trebuie să memoreze clienții un răspuns filtrat\",\n  \"blocked_safebrowsing\": \"Blocat de Navigarea în siguranță\",\n  \"blocked_service\": \"Serviciu blocat\",\n  \"blocked_services\": \"Servicii blocate\",\n  \"blocked_services_desc\": \"Permite blocarea rapidă a site-urilor și serviciilor populare.\",\n  \"blocked_services_global\": \"Folosiți servicii blocate globale\",\n  \"blocked_services_saved\": \"Serviciile blocate au fost salvate cu succes\",\n  \"blocked_threats\": \"Amenințări blocate\",\n  \"blocking_ipv4\": \"Blocarea IPv4\",\n  \"blocking_ipv4_desc\": \"Adresa IP de returnat pentru o cerere A de blocare\",\n  \"blocking_ipv6\": \"Blocarea IPv6\",\n  \"blocking_ipv6_desc\": \"Adresa IP de returnat pentru o cerere AAAA de blocare\",\n  \"blocking_mode\": \"Modul de blocare\",\n  \"blocking_mode_custom_ip\": \"IP personalizat: răspunde cu o adresă IP setată manual\",\n  \"blocking_mode_default\": \"Implicit: Răspunde cu adresa IP (0.0.0.0 for A; :: pentru AAAA) când sunt blocate de regulă tip Adblock; răspunde cu adresa IP specificată în regulă când sunt blocate de regula tip /etc/hosts\",\n  \"blocking_mode_null_ip\": \"IP nul: răspunde cu o adresă IP zero (0.0.0.0 pentru A; :: pentru AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Răspunde cu codul NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUZAT: Răspunde cu codul REFUZAT\",\n  \"blocklist\": \"Lista de blocări\",\n  \"bootstrap_dns\": \"Serverele DNS Bootstrap\",\n  \"bootstrap_dns_desc\": \"Adresele IP ale serverelor DNS utilizate pentru a rezolva adresele IP ale soluțiilor DoH/DoT pe care le specificați ca fiind în amonte. Comentariile nu sunt permise.\",\n  \"cache_cleared\": \"Cache-ul DNS a fost golit cu succes\",\n  \"cache_enabled\": \"Activați memoria cache\",\n  \"cache_enabled_desc\": \"Stocați răspunsurile DNS local.\",\n  \"cache_optimistic\": \"Caching optimistic\",\n  \"cache_optimistic_desc\": \"Face ca AdGuard Home să răspundă din cache chiar și atunci când intrările au expirate și de asemenea, încearcă să le reîmprospăteze.\",\n  \"cache_size\": \"Mărime cache\",\n  \"cache_size_desc\": \"Mărime cache DNS (în octeți).\",\n  \"cache_size_validation\": \"Dimensiunea memoriei cache trebuie să fie mai mare decât zero atunci când este activată.\",\n  \"cache_ttl_max_override\": \"Suprascrieți maximum TTL\",\n  \"cache_ttl_max_override_desc\": \"Setează o valoare maximă a timpului-de-viață (secunde) pentru intrările din memoria cache DNS.\",\n  \"cache_ttl_min_override\": \"Suprascrieți minimum TTL\",\n  \"cache_ttl_min_override_desc\": \"Extinde valorile timp-de-viață scurte (secunde) primite de la serverul din amonte la stocarea în cache a răspunsurilor DNS.\",\n  \"cancel_btn\": \"Anulare\",\n  \"category_label\": \"Categorie\",\n  \"check\": \"Verificați\",\n  \"check_client_id\": \"Identificator client (ClientID sau adresă IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Verifică dacă numele de host este filtrat.\",\n  \"check_dhcp_servers\": \"Căutați servere DHCP\",\n  \"check_dns_record\": \"Selectați tipul de înregistrare DNS\",\n  \"check_enter_client_id\": \"Introduceți identificatorul clientului\",\n  \"check_hostname\": \"Nume gazdă sau nume de domeniu\",\n  \"check_ip\": \"Adrese IP: {{ip}}\",\n  \"check_not_found\": \"Nu se găsește în listele de filtre\",\n  \"check_reason\": \"Cauza: {{reason}}\",\n  \"check_service\": \"Nume servici: {{service}}\",\n  \"check_title\": \"Verificați filtrarea\",\n  \"check_updates_btn\": \"Caută actualizări\",\n  \"check_updates_now\": \"Verificați actualizările acum\",\n  \"choose_allowlist\": \"Selectați liste de autorizări\",\n  \"choose_blocklist\": \"Alegeți liste de blocări\",\n  \"choose_from_list\": \"Alege din listă\",\n  \"city\": \"Oraș\",\n  \"clear_cache\": \"Goliți memoria cache\",\n  \"click_to_view_queries\": \"Clicați pentru a vizualiza interogări\",\n  \"client_add\": \"Adăugați client\",\n  \"client_added\": \"Clientul \\\"{{key}}\\\" a fost adăugat cu succes\",\n  \"client_blocked\": \"Clientul \\\"{{ip}}\\\" blocat cu succes\",\n  \"client_confirm_block\": \"Sunteți sigur că doriți să blocați clientul \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Sunteți sigur că doriți să ștergeți clientul \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Sunteți sigur că doriți să deblocați clientul \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Clientul \\\"{{key}}\\\" a fost șters cu succes\",\n  \"client_details\": \"Detalii client\",\n  \"client_edit\": \"Editare client\",\n  \"client_global_settings\": \"Folosiți setări globale\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Clienții pot fi identificați prin ClientID. Aflați mai multe despre cum să identificați clienții <a>aici</a>.\",\n  \"client_id_placeholder\": \"Introduceți un ClientID\",\n  \"client_identifier\": \"Identificator\",\n  \"client_identifier_desc\": \"Clienții pot fi identificați prin adresa lor IP, CIDR, adresa MAC sau ClientID (poate fi utilizat pentru DoT/DoH/DoQ). Aflați mai multe despre cum să identificați clienții <0>aici</0>.\",\n  \"client_name\": \"Client {{id}}\",\n  \"client_new\": \"Client nou\",\n  \"client_settings\": \"Setări client\",\n  \"client_table_header\": \"Client\",\n  \"client_unblocked\": \"Clientul \\\"{{ip}}\\\" deblocat cu succes\",\n  \"client_updated\": \"Clientul \\\"{{key}}\\\" a fost adus la zi cu succes\",\n  \"clients_desc\": \"Configurează înregistrările persistente ale clienților pentru dispozitivele conectate la AdGuard Home\",\n  \"clients_not_found\": \"Nu au fost găsiți clienți\",\n  \"clients_title\": \"Clienți persistenți\",\n  \"compact\": \"Compact\",\n  \"config_successfully_saved\": \"Configurarea a fost salvată cu succes\",\n  \"configure\": \"Configurați\",\n  \"confirm_dns_cache_clear\": \"Sunteți sigur că doriți să ștergeți memoria cache DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home va configura {{ip}} ca adresa dvs. IP statică. Doriți să continuați?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"Țara\",\n  \"custom_filter_rules\": \"Reguli de filtrare personalizate\",\n  \"custom_filter_rules_hint\": \"Introduceți o regulă pe linie. Puteți utiliza reguli de blocare sau sintaxa de fișiere hosts.\",\n  \"custom_filtering_rules\": \"Reguli filtrare personale\",\n  \"custom_ip\": \"IP personalizat\",\n  \"custom_retention_input\": \"Introduceți reținerea în ore\",\n  \"custom_rotation_input\": \"Introduceți rotația în ore\",\n  \"dashboard\": \"Tablou de bord\",\n  \"date\": \"Data\",\n  \"default\": \"Implicit\",\n  \"delete_confirm\": \"Sunteți sigur că doriți să ștergeți \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Șterge\",\n  \"descr\": \"Descriere\",\n  \"details\": \"Detalii\",\n  \"dhcp_add_static_lease\": \"Adăugați închiriere statică\",\n  \"dhcp_config_saved\": \"Configurare DHCP salvată cu succes\",\n  \"dhcp_description\": \"Dacă routerul dvs. nu furnizează setări DHCP, puteți utiliza serverul DHCP încorporat AdGuard.\",\n  \"dhcp_disable\": \"Dezactivați serverul DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Sistemul dvs. folosește configurația dinamică a adreselor IP pentru interfața <0>{{interfaceName}}</0>. Pentru a utiliza serverul DHCP, trebuie setată o adresă IP statică. Adresa IP curentă este <0>{{ipAddress}}</0>. AdGuard Home o va configura automat ca adresă IP statică, dacă apăsați butonul \\\"Activați serverul DHCP\\\".\",\n  \"dhcp_edit_static_lease\": \"Editați închiriere statică\",\n  \"dhcp_enable\": \"Activați serverul DHCP\",\n  \"dhcp_error\": \"AdGuard Home nu a putut determina dacă există un alt server DHCP activ în rețea\",\n  \"dhcp_form_gateway_input\": \"IP Gateway\",\n  \"dhcp_form_lease_input\": \"Durata locației\",\n  \"dhcp_form_lease_title\": \"Timp de închidere DHCP (în secunde)\",\n  \"dhcp_form_range_end\": \"Sfârșit interval\",\n  \"dhcp_form_range_start\": \"Start interval\",\n  \"dhcp_form_range_title\": \"Interval de adrese IP\",\n  \"dhcp_form_subnet_input\": \"Mască subnet\",\n  \"dhcp_found\": \"În rețea se găsește un server DHCP activ. Nu este sigur să activați serverul DHCP încorporat.\",\n  \"dhcp_hardware_address\": \"Adresa mașinii\",\n  \"dhcp_interface_select\": \"Selectați interfața DHCP\",\n  \"dhcp_ip_addresses\": \"Adrese IP\",\n  \"dhcp_ipv4_settings\": \"Setări DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Setări DHCP IPv6\",\n  \"dhcp_lease_added\": \"\\\"{{key}}\\\" statică închiriată adăugată cu succes\",\n  \"dhcp_lease_deleted\": \"\\\"{{key}}\\\" statică închiriată eliminată cu succes\",\n  \"dhcp_lease_updated\": \"\\\"{{key}}\\\" statică închiriată actualizată cu succes\",\n  \"dhcp_leases\": \"DHCP închiriate\",\n  \"dhcp_leases_not_found\": \"Nu s-au găsit DHCP închiriate\",\n  \"dhcp_new_static_lease\": \"Închiriere statică nouă\",\n  \"dhcp_not_found\": \"Este sigur să activați serverul DHCP încorporat deoarece AdGuard Home nu a găsit niciun server DHCP activ în rețea. Cu toate acestea, ar trebui să verificați din nou manual, deoarece sondarea automată nu oferă în prezent o garanție de 100%.\",\n  \"dhcp_reset\": \"Sigur doriți să resetați configurația DHCP?\",\n  \"dhcp_reset_leases\": \"Resetați toate închirierile\",\n  \"dhcp_reset_leases_confirm\": \"Sigur doriți să resetați toate închirierile?\",\n  \"dhcp_reset_leases_success\": \"Închirierile DHCP au fost resetate cu succes\",\n  \"dhcp_settings\": \"Setări DHCP\",\n  \"dhcp_static_ip_error\": \"Pentru a utiliza serverul DHCP, trebuie setată o adresă IP statică. AdGuard Home nu a reușit să determine dacă această interfață de rețea este configurată utilizând o adresă IP statică. Setați manual o adresă IP statică.\",\n  \"dhcp_static_leases\": \"DHCP statice închiriate\",\n  \"dhcp_static_leases_not_found\": \"Nu s-au găsit închirieri statice DHCP\",\n  \"dhcp_table_expires\": \"Expiră\",\n  \"dhcp_table_hostname\": \"Hostname\",\n  \"dhcp_title\": \"Server DHCP (experimental!)\",\n  \"dhcp_warning\": \"Dacă doriți să activați serverul DHCP oricum, asigurați-vă că nu există nici un alt server DHCP activ în rețeaua dvs., deoarece acest lucru poate rupe conectivitatea la Internet a dispozitivelor din rețea!\",\n  \"disable_for_hours\": \"Timp de {{count}} oră\",\n  \"disable_for_hours_plural\": \"Timp de {{count}} ore\",\n  \"disable_for_minutes\": \"Timp de {{count}} minut\",\n  \"disable_for_minutes_plural\": \"Timp de {{count}} minute\",\n  \"disable_for_seconds\": \"Timp de {{count}} secundă\",\n  \"disable_for_seconds_plural\": \"Timp de {{count}} secunde\",\n  \"disable_ipv6\": \"Dezactivați rezolvarea adreselor IPv6\",\n  \"disable_ipv6_desc\": \"Renunțați la toate interogările DNS pentru adresele IPv6 (tip AAAA) și eliminați indicațiile IPv6 din răspunsurile HTTPS.\",\n  \"disable_notify_for_hours\": \"Dezactivează protecția timp de {{count}} oră\",\n  \"disable_notify_for_hours_plural\": \"Dezactivați protecția timp de {{count}} ore\",\n  \"disable_notify_for_minutes\": \"Dezactivați protecția timp de {{count}} minut\",\n  \"disable_notify_for_minutes_plural\": \"Dezactivați protecția timp de {{count}} minute\",\n  \"disable_notify_for_seconds\": \"Dezactivați protecția timp de {{count}} secundă\",\n  \"disable_notify_for_seconds_plural\": \"Dezactivați protecția timp de {{count}} secunde\",\n  \"disable_notify_until_tomorrow\": \"Dezactivează protecția până mâine\",\n  \"disable_protection\": \"Dezactivați protecția\",\n  \"disable_rewrites\": \"Dezactivați regulile de rescriere\",\n  \"disable_until_tomorrow\": \"Până mâine\",\n  \"disabled\": \"Dezactivat\",\n  \"disabled_dhcp\": \"Server DHCP dezactivat\",\n  \"disabled_filtering_toast\": \"Filtrare dezactivată\",\n  \"disabled_parental_toast\": \"Control Parental dezactivat\",\n  \"disabled_protection\": \"Protecție dezactivată\",\n  \"disabled_safe_browsing_toast\": \"Navigare în siguranță dezactivată\",\n  \"disabled_safe_search_toast\": \"Căutare protejată dezactivată\",\n  \"disallow_this_client\": \"Nu permiteți acest client\",\n  \"dns_addresses\": \"Adrese DNS\",\n  \"dns_allowlists\": \"Listă de autorizări DNS\",\n  \"dns_allowlists_desc\": \"Domeniile din listele de autorizări DNS vor fi permise chiar dacă se află în oricare dintre listele de blocări.\",\n  \"dns_blocklists\": \"Liste de blocări DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home blochează domenii incluse în liste de blocări.\",\n  \"dns_cache_config\": \"Configurare cache DNS\",\n  \"dns_cache_config_desc\": \"Aici puteți configura cache-ul DNS\",\n  \"dns_cache_size\": \"Dimensiunea cache-ului DNS, în octeți\",\n  \"dns_config\": \"Configurația serverului DNS\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"Confidențialitate DNS\",\n  \"dns_providers\": \"Iată o <0>listă de furnizori DNS cunoscuți</0> ce pot fi aleși.\",\n  \"dns_query\": \"Interogări DNS\",\n  \"dns_rewrites\": \"Rescrieri DNS\",\n  \"dns_settings\": \"Setări DNS\",\n  \"dns_start\": \"Serverul DNS demarează\",\n  \"dns_status_error\": \"Eroare la verificarea stării serverului DNS\",\n  \"dns_test_not_ok_toast\": \"Serverul \\\"{{key}}\\\": nu a putut fi utilizat, verificați dacă l-ați scris corect\",\n  \"dns_test_ok_toast\": \"Serverele DNS specificate funcționează corect\",\n  \"dns_test_parsing_error_toast\": \"Secțiune {{section}}: linie {{line}}: nu a putut fi folosit, vă rugăm să verificați dacă l-ați scris corect\",\n  \"dns_test_warning_toast\": \"„{{key}}” în amonte nu răspunde la solicitările de testare și s-ar putea să nu funcționeze corect\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Activați DNSSEC\",\n  \"dnssec_enable_desc\": \"Activați semnalul DNSSEC în interogările DNS de ieșire și verificați rezultatul (este necesar un rezolvator compatibil DNSSEC).\",\n  \"domain\": \"Domeniu\",\n  \"domain_desc\": \"Introduceți un nume de domeniu sau wildcard care doriți să fie rescris.\",\n  \"domain_name_table_header\": \"Nume domeniu\",\n  \"domain_or_client\": \"Domeniu sau client\",\n  \"down\": \"Down\",\n  \"download_mobileconfig\": \"Descărcați fișierul de configurare\",\n  \"download_mobileconfig_doh\": \"Descărcați .mobileconfig pentru DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Descărcați .mobileconfig pentru DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Editare autorizare\",\n  \"edit_blocklist\": \"Editare blocare\",\n  \"edit_table_action\": \"Editare\",\n  \"edns_cs_desc\": \"Adaugă opțiunea EDNS Client Subnet (ECS) la solicitările în amonte și înregistrează valorile trimise de clienți în jurnalul de interogare.\",\n  \"edns_enable\": \"Activați subrețeaua de clienți EDNS\",\n  \"edns_use_custom_ip\": \"Utilizați IP personalizat pentru EDNS\",\n  \"edns_use_custom_ip_desc\": \"Permiteți utilizarea IP-ului personalizat pentru EDNS\",\n  \"elapsed\": \"Scurs\",\n  \"empty_response_status\": \"Gol\",\n  \"enable_protection\": \"Activați protecția\",\n  \"enable_protection_timer\": \"Protecția va fi activată în {{time}}\",\n  \"enable_rewrites\": \"Activați regulile de rescriere\",\n  \"enable_upstream_dns_cache\": \"Activați memoria cache DNS pentru configurația personalizată în amonte a acestui client\",\n  \"enabled_dhcp\": \"Server DHCP activat\",\n  \"enabled_filtering_toast\": \"Filtrare activată\",\n  \"enabled_parental_toast\": \"Control Parental activat\",\n  \"enabled_protection\": \"Protecție activată\",\n  \"enabled_safe_browsing_toast\": \"Navigare în siguranță activată\",\n  \"enabled_save_search_toast\": \"Căutare protejată activată\",\n  \"enabled_table_header\": \"Activat\",\n  \"encryption_certificate_path\": \"Locația certificatului\",\n  \"encryption_certificates\": \"Certificate\",\n  \"encryption_certificates_desc\": \"Pentru a utiliza criptarea, trebuie furnizate o serie de certificate SSL valabile pentru domeniul dvs.. Puteți obține un certificat gratuit pe <0>{{link}}</0> sau îl puteți cumpăra de la una din Autoritățile Certificate de încredere.\",\n  \"encryption_certificates_input\": \"Copiați/lipiți certificatele dvs. PEM-codate aici.\",\n  \"encryption_certificates_source_content\": \"Lipiți conținutul certificatelor\",\n  \"encryption_certificates_source_path\": \"Precizați locația certificatelor\",\n  \"encryption_chain_invalid\": \"Lanț de certificate invalid\",\n  \"encryption_chain_valid\": \"Lanț de certificate valid\",\n  \"encryption_config_saved\": \"Configurația de criptare salvată\",\n  \"encryption_desc\": \"Suport pentru criptare (HTTPS/TLS) atât pentru DNS, cât și pentru interfața web de administrare\",\n  \"encryption_doq\": \"Portul DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"Dacă este configurat acest port, AdGuard Home va rula un server DNS-over-QUIC pe acest port.\",\n  \"encryption_dot\": \"Port DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Dacă acest port este configurat, AdGuard Home va rula un server DNS-over-TLS pe acest port.\",\n  \"encryption_enable\": \"Activați criptarea (HTTPS, DNS-over-HTTPS, și DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Dacă este activată criptarea, interfața administrator AdGuard Home va lucra peste HTTPS, și serverul DNS va asculta pentru cereri peste DNS-over-HTTPS și DNS-over-TLS.\",\n  \"encryption_expire\": \"Expiră\",\n  \"encryption_hostnames\": \"Nume de host\",\n  \"encryption_https\": \"Port HTTPS\",\n  \"encryption_https_desc\": \"Dacă portul HTTPS este configurat, interfața administrator AdGuard Home va fi accesibilă prin HTTPS și va oferi de asemenea DNS-over-HTTPS în locația '/DNS-query'.\",\n  \"encryption_issuer\": \"Emitent\",\n  \"encryption_key\": \"Cheie privată\",\n  \"encryption_key_input\": \"Copiați/lipiți cheia dvs. privată PEM-codată pentru certificatul dvs. aici.\",\n  \"encryption_key_invalid\": \"Aceasta este o cheie privată {{type}} invalidă\",\n  \"encryption_key_source_content\": \"Lipiți conținutul cheii private\",\n  \"encryption_key_source_path\": \"Precizați o cale către un fișier cu cheie privată\",\n  \"encryption_key_valid\": \"Aceasta este o cheie privată {{type}} validă\",\n  \"encryption_plain_dns_desc\": \"DNS simplu este activat în mod implicit. Îl puteți dezactiva pentru a forța toate dispozitivele să utilizeze DNS criptat. Pentru a face acest lucru, trebuie să activați cel puțin un protocol DNS criptat\",\n  \"encryption_plain_dns_enable\": \"Activați DNS simplu\",\n  \"encryption_plain_dns_error\": \"Pentru a dezactiva DNS simplu, activați cel puțin un protocol DNS criptat\",\n  \"encryption_private_key_path\": \"Locația cheii private\",\n  \"encryption_redirect\": \"Redirecționați automat la HTTPS\",\n  \"encryption_redirect_desc\": \"Dacă este bifat, AdGuard Home vă va redirecționa automat de la adrese HTTP la HTTPS.\",\n  \"encryption_reset\": \"Sunteți sigur că doriți să resetați setările de criptare?\",\n  \"encryption_server\": \"Nume de server\",\n  \"encryption_server_desc\": \"Dacă este setat, AdGuard Home detectează ID-urile de client, răspunde la interogările DDR și efectuează validări suplimentare ale conexiunii. Dacă nu este setat, aceste caracteristici sunt dezactivate. Trebuie să corespundă cu unul dintre numele DNS din certificat.\",\n  \"encryption_server_enter\": \"Introduceți numele domeniului\",\n  \"encryption_settings\": \"Setări de criptare\",\n  \"encryption_status\": \"Statut\",\n  \"encryption_subject\": \"Obiect\",\n  \"encryption_title\": \"Criptare\",\n  \"encryption_warning\": \"Atenție\",\n  \"enforce_safe_search\": \"Folosiți Căutarea Sigură\",\n  \"enforce_save_search_hint\": \"AdGuard Home va impune căutarea sigură în următoarele motoare de căutare: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Căutare protejată întărită\",\n  \"enter_cache_size\": \"Introduceți mărimea cache-ului (bytes)\",\n  \"enter_cache_ttl_max_override\": \"Introduceți maximum TTL (secunde)\",\n  \"enter_cache_ttl_min_override\": \"Introduceți minimum TTL (secunde)\",\n  \"enter_name_hint\": \"Introduceți numele\",\n  \"enter_url_or_path_hint\": \"Introduceți un URL sau o cale absolută a listei\",\n  \"enter_valid_allowlist\": \"Introduceți un URL valid pentru autorizare.\",\n  \"enter_valid_blocklist\": \"Introduceți un URL valid pentru blocare.\",\n  \"error_details\": \"Detalii eroare\",\n  \"example_comment\": \"! Aici urmează un comentariu.\",\n  \"example_comment_hash\": \"# De asemenea, un comentariu.\",\n  \"example_comment_meaning\": \"doar un comentariu;\",\n  \"example_meaning_filter_block\": \"blochează accesul la domeniul exemplu.org și la toate subdomeniile sale;\",\n  \"example_meaning_filter_whitelist\": \"deblochează accesul la domeniul exemplu.org și la toate subdomeniile sale;\",\n  \"example_meaning_host_block\": \"răspunde cu 127.0.0.1 pentru domeniul exemplu.org (dar nu și pentru subdomeniile sale);\",\n  \"example_multiple_upstreams_reserved\": \"mai mulți servere în amonte <0>pentru domenii specifice</0>;\",\n  \"example_regex_meaning\": \"blochează accesul la domeniile care corespund expresiei regulate specificate.\",\n  \"example_rewrite_domain\": \"rescrie răspunsuri numai pentru acest nume de domeniu.\",\n  \"example_rewrite_wildcard\": \"rescrie răspunsuri pentru toate subdomeniile <0>exemplu.org</0>.\",\n  \"example_upstream_comment\": \"un comentariu.\",\n  \"example_upstream_doh\": \"<0>DNS-over-HTTPS</0> criptat;\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS criptat cu <0>HTTP/3</0> forțat și fără revenire la HTTP/2 sau inferior;\",\n  \"example_upstream_doq\": \"criptat <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"<0>DNS-over-TLS</0> criptat;\",\n  \"example_upstream_regular\": \"DNS clasic (over UDP);\",\n  \"example_upstream_regular_port\": \"DNS obișnuit (over UDP, cu port);\",\n  \"example_upstream_reserved\": \"un flux în amonte <0>pentru domenii specifice</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> pentru <1>DNSCrypt</1> sau rezolvatori <2>DNS-over-HTTPS</2>;\",\n  \"example_upstream_tcp\": \"DNS clasic (over TCP);\",\n  \"example_upstream_tcp_hostname\": \"DNS obișnuit (over TCP, nume de gazdă);\",\n  \"example_upstream_tcp_port\": \"DNS obișnuit (over TCP, cu port);\",\n  \"example_upstream_udp\": \"DNS obișnuit (over UDP, nume de gazdă);\",\n  \"examples_title\": \"Exemple\",\n  \"fallback_dns_desc\": \"Lista serverelor DNS de rezervă utilizate atunci când serverele DNS din amonte nu răspund. Sintaxa este aceeași ca în câmpul principal din amonte de mai sus.\",\n  \"fallback_dns_placeholder\": \"Introduceți un server DNS de rezervă pe linie\",\n  \"fallback_dns_title\": \"Servere DNS de rezervă\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Cea mai rapidă adresă IP\",\n  \"fastest_addr_desc\": \"Așteptați răspunsuri de la <b>toate</b> serverele DNS, măsurați viteza de conexiune TCP pentru fiecare server și returnați adresa IP a serverului cu cea mai rapidă viteză de conexiune.<br/>Această modul poate încetini semnificativ interogările DNS, dacă unul sau mai multe servere în amonte nu răspund. Asigurați-vă că serverele dumneavoastră în amonte sunt stabile și că timpul de așteptare pentru serverele în amonte este scăzut.\",\n  \"filter\": \"Filtru\",\n  \"filter_added_successfully\": \"Filtrul a fost adăugat cu succes\",\n  \"filter_allowlist\": \"AVERTISMENT: Această acțiune va exclude și regula „{{disallowed_rule}}” din lista de clienți permiși.\",\n  \"filter_category_general\": \"General\",\n  \"filter_category_general_desc\": \"Liste care blochează urmărirea și publicitatea pe majoritatea aparatelor\",\n  \"filter_category_other\": \"Altele\",\n  \"filter_category_other_desc\": \"Alte liste de blocări\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Liste focalizate pe reclame regionale și servere de urmărire\",\n  \"filter_category_security\": \"Securitate\",\n  \"filter_category_security_desc\": \"Listele concepute special pentru a bloca domenii rău intenționate, phishing și înșelătorie\",\n  \"filter_removed_successfully\": \"Lista a fost eliminată cu succes\",\n  \"filter_updated\": \"Filtrul a fost actualizat cu succes\",\n  \"filtered\": \"Filtrate\",\n  \"filtered_custom_rules\": \"Filtrat prin reguli de filtrare personalizate\",\n  \"filtering_rules_learn_more\": \"<0>Aflați mai multe</0> despre crearea propriilor liste hosts.\",\n  \"filters\": \"Filtre\",\n  \"filters_and_hosts_hint\": \"AdGuard Home înțelege regulile de bază de blocare cât și sintaxa fișierelor hosts.\",\n  \"filters_block_toggle_hint\": \"Puteți configura regulile de blocare în setările <a>Filtre</a>.\",\n  \"filters_configuration\": \"Configurația filtrelor\",\n  \"filters_enable\": \"Activați filtrele\",\n  \"filters_interval\": \"Intervalul de actualizare a filtrului\",\n  \"fix\": \"Fix\",\n  \"for_last_days\": \"în ultima {{count}} zi\",\n  \"for_last_days_plural\": \"pentru ultimele {{count}} zile\",\n  \"for_last_hours\": \"în ultima {{count}} oră\",\n  \"for_last_hours_plural\": \"în ultimele {{count}} ore\",\n  \"forgot_password\": \"Ați uitat parola?\",\n  \"forgot_password_desc\": \"Vă rugăm să urmați <0>aceste etape</0> pentru a crea o nouă parolă pentru contul de utilizator.\",\n  \"form_add_id\": \"Adăugați identificator\",\n  \"form_answer\": \"Introduceți adresa IP sau numele de domeniu\",\n  \"form_client_name\": \"Introduceți nume client\",\n  \"form_domain\": \"Introduceți un nume de domeniu sau wildcard\",\n  \"form_enter_blocked_response_ttl\": \"Introduceți răspunsul blocat TTL (secunde)\",\n  \"form_enter_host\": \"Introduceți un nume de host\",\n  \"form_enter_hostname\": \"Introduceți hostname\",\n  \"form_enter_id\": \"Introduceți identificator\",\n  \"form_enter_ip\": \"Introduceți IP\",\n  \"form_enter_mac\": \"Introduceți MAC\",\n  \"form_enter_rate_limit\": \"Introduceți limita ratei\",\n  \"form_enter_rate_limit_subnet_len\": \"Introduceți lungimea prefixului de subrețea pentru limitarea ratei\",\n  \"form_enter_subnet_ip\": \"Introduceți o adresă IP în subrețeaua „{{cidr}}”\",\n  \"form_enter_upstream_timeout\": \"Introduceți durata de timp de așteptare a serverului în amonte în secunde\",\n  \"form_error_answer_format\": \"Format de răspuns invalid\",\n  \"form_error_client_id_format\": \"ClientID-ul trebuie să conțină numai numere, litere minuscule și cratime\",\n  \"form_error_domain_format\": \"Format de domeniu invalid\",\n  \"form_error_equal\": \"Nu trebuie să fie egale\",\n  \"form_error_gateway_ip\": \"Locația nu poate avea adresa IP a gateway-ului\",\n  \"form_error_ip4_format\": \"Adresă IPv4 nevalidă\",\n  \"form_error_ip4_gateway_format\": \"Adresă IPv4 nevalidă a gateway-ului\",\n  \"form_error_ip6_format\": \"Adresa IPv6 nevalidă\",\n  \"form_error_ip_format\": \"Adresă IP nevalidă\",\n  \"form_error_mac_format\": \"Adresă MAC nevalidă\",\n  \"form_error_password\": \"Parolele nu corespund\",\n  \"form_error_password_length\": \"Parola trebuie să aibă între {{min}} și {{max}} caractere\",\n  \"form_error_port\": \"Introduceți un număr de port valid\",\n  \"form_error_port_range\": \"Introduceți valoarea portului între 80-65535\",\n  \"form_error_port_unsafe\": \"Port nesigur\",\n  \"form_error_positive\": \"Trebuie să fie mai mare de 0\",\n  \"form_error_required\": \"Câmp obligatoriu\",\n  \"form_error_server_name\": \"Nume de server nevalid\",\n  \"form_error_subnet\": \"Subrețeaua „{{cidr}}” nu conține adresa IP „{{ip}}”\",\n  \"form_error_url_format\": \"Format URL nevalid\",\n  \"form_error_url_or_path_format\": \"URL nevalabil sau calea absolută a listei\",\n  \"form_select_tags\": \"Selectați etichete client\",\n  \"found_in_known_domain_db\": \"Găsit în baza de date de domenii cunoscută.\",\n  \"friday\": \"Vineri\",\n  \"friday_short\": \"vi\",\n  \"gateway_or_subnet_invalid\": \"Mască de subrețea nevalidă\",\n  \"general_settings\": \"Setări Generale\",\n  \"general_statistics\": \"Statistici generale\",\n  \"get_started\": \"Să începem\",\n  \"greater_range_start_error\": \"Trebuie să fie mai mare decât începutul intervalului\",\n  \"homepage\": \"Homepage\",\n  \"host_whitelisted\": \"Numele de host este în lista albă\",\n  \"ignore_domains\": \"Domenii ignorate (separate prin linie nouă)\",\n  \"ignore_domains_desc_query\": \"Interogările care corespund acestor reguli nu sunt scrise în jurnalul de interogări\",\n  \"ignore_domains_desc_stats\": \"Interogările care corespund acestor reguli nu sunt scrise în statistici\",\n  \"ignore_domains_title\": \"Domenii ignorate\",\n  \"ignore_query_log\": \"Ignorați acest client în jurnalul de interogări\",\n  \"ignore_statistics\": \"Ignorați acest client în statistici\",\n  \"install_auth_confirm\": \"Confirmați parola\",\n  \"install_auth_desc\": \"Trebuie configurată autentificarea cu parolă la interfața web AdGuard Home admin. Chiar dacă AdGuard Home este accesibil numai în rețeaua locală, este important să îl protejați de accesul fără restricții.\",\n  \"install_auth_password\": \"Parola\",\n  \"install_auth_password_enter\": \"Introduceți parola\",\n  \"install_auth_title\": \"Autentificare\",\n  \"install_auth_username\": \"Nume utilizator\",\n  \"install_auth_username_enter\": \"Introduceți nume utilizator\",\n  \"install_devices_address\": \"Serverul DNS AdGuard Home ascultă pe următoarele adrese\",\n  \"install_devices_android_list_1\": \"Din ecranul principal al Meniului Android, tapați Setări.\",\n  \"install_devices_android_list_2\": \"Tapați Wi-Fi din meniu. Ecranul cu toate rețelele disponibile va fi afișat (este imposibil să setați DNS personalizat pentru conexiunea mobilă).\",\n  \"install_devices_android_list_3\": \"Apăsați lung pe rețeaua la care sunteți conectat și tapați Modificare Rețea.\",\n  \"install_devices_android_list_4\": \"Pe unele aparate, poate fi necesar să bifați caseta Advanced pentru a vedea setările adiționale. Pentru a ajusta setările DNS Android, va trebui să comutați setările IP de la DHCP la Static.\",\n  \"install_devices_android_list_5\": \"Schimbați valorile DNS 1 și DNS 2 la adresele serverului dvs. AdGuard Home.\",\n  \"install_devices_desc\": \"Pentru a începe să utilizați AdGuard Home, trebuie să configurați aparatele.\",\n  \"install_devices_ios_list_1\": \"Din ecranul de start, tapați Setări.\",\n  \"install_devices_ios_list_2\": \"Alegeți Wi-Fi în meniul din stânga (este imposibil să configurați DNS pentru rețelele mobile).\",\n  \"install_devices_ios_list_3\": \"Apăsați pe numele rețelei active în prezent.\",\n  \"install_devices_ios_list_4\": \"În câmpul DNS, introduceți adresele serverului dvs. AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Faceți clic pe pictograma „Apple” și accesați „Preferințe de sistem”.\",\n  \"install_devices_macos_list_2\": \"Faceți clic pe „Rețea”.\",\n  \"install_devices_macos_list_3\": \"Selectați prima conexiune din listă și clicați pe Avansat.\",\n  \"install_devices_macos_list_4\": \"Selectați fila DNS și introduceți adresele serverului dvs. AdGuard Home.\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Această configurare acoperă automat toate dispozitivele conectate la routerul de acasă, nu este nevoie să le configurați manual.\",\n  \"install_devices_router_list_1\": \"Deschideți preferințele routerului dvs. De obicei, îl puteți accesa din browser printr-o adresă URL cum ar fi http://192.168.0.1/ sau http://192.168.1.1/. Vi se poate cere să introduceți o parolă. Dacă nu v-o amintiți, adesea puteți reseta parola apăsând un buton de pe routerul propriu-zis, dar fiți conștienți, că prin acest procedeu puteți pierde întreaga configurație a routerului. \\nDacă routerul dvs. necesită o aplicație pentru configurare, instalați aplicația pe telefon sau pe PC și utilizați-o pentru a accesa setările routerului.\",\n  \"install_devices_router_list_2\": \"Găsiți setările DHCP/DNS. Căutați literele DNS lângă un câmp care să permită două sau trei seturi de numere, fiecare împărțit în patru grupuri de una până la trei cifre.\",\n  \"install_devices_router_list_3\": \"Introduceți adresele serverului dvs. AdGuard Home aici.\",\n  \"install_devices_router_list_4\": \"Unele tipuri de routere, nu permit configurarea unui server DNS personalizat. În acest caz, configurarea AdGuard Home ca un <0>server DHCP</0>vă poate ajuta. Dacă nu, ar trebui verificat manualul routerului dvs. specific, ca să aflați cum se pot personaliza serverele DNS.\",\n  \"install_devices_title\": \"Configurați aparatele dvs\",\n  \"install_devices_windows_list_1\": \"Deschideți panoul de control prin meniul Start sau căutare Windows.\",\n  \"install_devices_windows_list_2\": \"Accesați categoria \\\"Rețea și Internet\\\", apoi la \\\"Centrul de Rețea și Partajare\\\".\",\n  \"install_devices_windows_list_3\": \"În panoul din stânga, faceți clic pe „Modificare setări adaptor”.\",\n  \"install_devices_windows_list_4\": \"Faceți clic dreapta pe conexiunea activă și selectați „Proprietăți”.\",\n  \"install_devices_windows_list_5\": \"Găsiți Internet Protocol Versiunea 4 (TCP/IPv4) din listă, selectați-l și apoi clicați din nou pe Proprietăți.\",\n  \"install_devices_windows_list_6\": \"Alegeți „Utilizați următoarele adrese de server DNS” și introduceți adresele serverului dvs. AdGuard Home.\",\n  \"install_saved\": \"Salvat cu succes\",\n  \"install_settings_all_interfaces\": \"Toate interfețele\",\n  \"install_settings_dns\": \"Server DNS\",\n  \"install_settings_dns_desc\": \"Va trebui să configurați aparatele sau routerul pentru a utiliza serverul DNS pe următoarele adrese:\",\n  \"install_settings_interface_link\": \"Interfața dvs. de administrare AdGuard Home va fi disponibilă pe următoarele adrese:\",\n  \"install_settings_listen\": \"Interfață de ascultare\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Interfață administrator web\",\n  \"install_static_configure\": \"AdGuard Home a detectat că se folosește adresa IP dinamică <0>{{ip}}</0>. Doriți ca aceasta să fie setată ca adresă statică?\",\n  \"install_static_error\": \"AdGuard Home nu o poate configura automat pentru această interfață de rețea. Vă rugăm să căutați instrucțiuni despre cum să faceți acest lucru manual.\",\n  \"install_static_ok\": \"Vești bune! Adresa IP statică este deja configurată\",\n  \"install_step\": \"Etapa\",\n  \"install_submit_desc\": \"Procedura de configurare este finalizată și acum sunteți gata să începeți să utilizați AdGuard Home.\",\n  \"install_submit_title\": \"Felicitări!\",\n  \"install_welcome_desc\": \"AdGuard Home este un server DNS care blochează reclame și trackere la nivel de rețea. Scopul său este de a vă da controlul pe întreaga rețea și toate aparatele dvs. și fără un program din partea clientului.\",\n  \"install_welcome_title\": \"Bun venit la AdGuard Home!\",\n  \"interval_24_hour\": \"24 ore\",\n  \"interval_6_hour\": \"6 ore\",\n  \"interval_days\": \"{{count}} zi\",\n  \"interval_days_plural\": \"{{count}} zile\",\n  \"interval_hours\": \"{{count}} oră\",\n  \"interval_hours_plural\": \"{{count}} ore\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Adresa IP\",\n  \"known_tracker\": \"Tracker cunoscut\",\n  \"last_rule_in_allowlist\": \"Acest client nu poate fi exclus deoarece excluderea regulii „{{disallowed_rule}}” va DEZACTIVA lista „Clienți acceptați”.\",\n  \"last_time_updated_table_header\": \"Ultima aducere la zi\",\n  \"list_confirm_delete\": \"Sigur doriți să ștergeți această listă?\",\n  \"list_label\": \"Listă\",\n  \"list_updated\": \"{{count}} listă actualizată\",\n  \"list_updated_plural\": \"{{count}} liste actualizate\",\n  \"list_url_table_header\": \"Lista URL\",\n  \"load_balancing\": \"Echilibrare-sarcini\",\n  \"load_balancing_desc\": \"Interogați câte un server în amonte la un moment dat.<br/>AdGuard Home folosește un algoritm de randomizare ponderat pentru a selecta servere cu cel mai mic număr de căutări nereușite și cel mai mic timp mediu de căutare.\",\n  \"loading_table_status\": \"Se încarcă...\",\n  \"local_ptr_default_resolver\": \"În mod implicit, AdGuard Home utilizează următorii rezolvatori DNS inverși: {{ip}}.\",\n  \"local_ptr_desc\": \"Serverele DNS utilizate de AdGuard Home pentru solicitările private PTR, SOA și NS. O solicitare este considerată privată dacă solicită un domeniu ARPA care conține o subrețea în interiorul intervalelor IP private (cum ar fi \\\"192.168.12.34\\\") și provine de la un client cu o adresă IP privată. Dacă nu este setat, vor fi folosite resolverele DNS implicite ale sistemului dvs. de operare, cu excepția adreselor IP AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home nu a putut determina rezolvatorii DNS privați adecvați pentru acest sistem.\",\n  \"local_ptr_placeholder\": \"Introduceți o adresă IP per linie\",\n  \"local_ptr_title\": \"Servere DNS inverse private\",\n  \"location\": \"Locația\",\n  \"log_and_stats_section_label\": \"Jurnal de interogări și statistici\",\n  \"lower_range_start_error\": \"Trebuie să fie mai mică decât începutul intervalului\",\n  \"main_settings\": \"Setări principale\",\n  \"make_static\": \"Faceți static\",\n  \"manual_update\": \"Vă rugăm <a>să urmați etapele următoare</a> pentru a actualiza manual.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Luni\",\n  \"monday_short\": \"lu\",\n  \"name\": \"Nume\",\n  \"name_table_header\": \"Nume\",\n  \"netname\": \"Numele rețelei\",\n  \"network\": \"Rețea\",\n  \"new_allowlist\": \"Nouă autorizare\",\n  \"new_blocklist\": \"Nouă blocare\",\n  \"next\": \"Următor\",\n  \"next_btn\": \"Următor\",\n  \"no_blocklist_added\": \"Listă blocări goală\",\n  \"no_clients_found\": \"Nu au fost găsiți clienți\",\n  \"no_domains_found\": \"Nu s-au găsit domenii\",\n  \"no_logs_found\": \"Niciun jurnal găsit\",\n  \"no_servers_specified\": \"Nu sunt specificate servere\",\n  \"no_upstreams_data_found\": \"Nu există date despre serverele din amonte\",\n  \"no_whitelist_added\": \"Nu s-au adăugat autorizări\",\n  \"nothing_found\": \"Nimic găsit\",\n  \"null_ip\": \"IP nul\",\n  \"number_of_dns_query_blocked_24_hours\": \"Numărul de interogări DNS blocate de filtrele adblock și lista de blocări din hosts\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Numărul de site-uri pentru adulți blocate\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Numărul de interogări DNS blocate de modulul de securitate de navigare AdGuard\",\n  \"number_of_dns_query_days\": \"Numărul de interogări DNS procesate în ultima {{count}} zi\",\n  \"number_of_dns_query_days_plural\": \"Numărul de interogări DNS procesate în ultimele {{count}} zile\",\n  \"number_of_dns_query_hours\": \"Numărul de interogări DNS procesate în ultima {{count}} oră\",\n  \"number_of_dns_query_hours_plural\": \"Numărul de interogări DNS procesate în ultimele {{count}} ore\",\n  \"number_of_dns_query_to_safe_search\": \"Numărul de interogări DNS pe motoarele de căutare pentru care a fost impusă Căutarea Sigură\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"OFF\",\n  \"on\": \"ON\",\n  \"open_dashboard\": \"Deschideți Tabloul de bord\",\n  \"orgname\": \"Numele organizației\",\n  \"original_response\": \"Răspuns original\",\n  \"out_of_range_error\": \"Trebuie să fie în afara intervalului „{{start}}”-„{{end}}”\",\n  \"page_table_footer_text\": \"Pagina\",\n  \"parallel_requests\": \"Solicitări paralele\",\n  \"parental_control\": \"Control Parental\",\n  \"password_label\": \"Parola\",\n  \"password_placeholder\": \"Introduceți parola\",\n  \"plain_dns\": \"DNS simplu\",\n  \"port_53_faq_link\": \"Portul 53 este adesea ocupat de serviciile \\\"DNSStubListener\\\" sau \\\"systemd-resolved\\\". Vă rugăm să citiți <0>această instrucțiune</0> despre cum să rezolvați aceasta.\",\n  \"previous_btn\": \"Anterior\",\n  \"privacy_policy\": \"Politică confidențialitate\",\n  \"processing_update\": \"Vă rugăm să așteptați, AdGuard Home se actualizează...\",\n  \"protection_section_label\": \"Protecție\",\n  \"protocol\": \"Protocol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Jurnal interogări\",\n  \"query_log_clear\": \"Curăță jurnalele\",\n  \"query_log_cleared\": \"Jurnalul de interogare a fost șters cu succes\",\n  \"query_log_configuration\": \"Configurația jurnalelor\",\n  \"query_log_confirm_clear\": \"Sunteți sigur că doriți să ștergeți întregul jurnal de interogări?\",\n  \"query_log_disabled\": \"Jurnalul de interogare este dezactivat și poate fi configurat în <0>setări</0>\",\n  \"query_log_enable\": \"Activați jurnal\",\n  \"query_log_filtered\": \"Filtrat de {{filter}}\",\n  \"query_log_response_status\": \"Statut: {{value}}\",\n  \"query_log_retention\": \"Interogarea jurnalelor de rotație\",\n  \"query_log_retention_confirm\": \"Sigur doriți să modificați rotația jurnalului de interogări? Dacă micșorați valoarea intervalului, unele date se vor pierde\",\n  \"query_log_strict_search\": \"Utilizați ghilimele duble pentru căutare strictă\",\n  \"query_log_updated\": \"Jurnalul de solicitări a fost actualizat cu succes\",\n  \"rate_limit\": \"Limita ratei\",\n  \"rate_limit_desc\": \"Numărul de interogări pe secundă permise pe client. Setarea la 0 înseamnă că nu există limită.\",\n  \"rate_limit_subnet_len_ipv4\": \"Lungimea prefixului de subrețea pentru adrese IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Lungimea prefixului de subrețea pentru adresele IPv4 utilizate pentru limitarea ratei. Valoarea implicită este 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Lungimea prefixului de subrețea IPv4 ar trebui să fie între 0 și 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Lungimea prefixului de subrețea pentru adrese IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Lungimea prefixului de subrețea pentru adresele IPv6 utilizate pentru limitarea ratei. Valoarea implicită este 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Lungimea prefixului de subrețea IPv6 ar trebui să fie între 0 și 128\",\n  \"rate_limit_whitelist\": \"Lista permisă pentru limitarea ratei\",\n  \"rate_limit_whitelist_desc\": \"Adresele IP excluse de la limitarea ratei\",\n  \"rate_limit_whitelist_placeholder\": \"Introduceți o adresă IP per linie\",\n  \"refresh_btn\": \"Actualizare\",\n  \"refresh_statics\": \"Actualizare statistici\",\n  \"refused\": \"REFUZAT\",\n  \"report_an_issue\": \"Raportați o problemă\",\n  \"request_details\": \"Detalii solicitare\",\n  \"request_table_header\": \"Solicitare\",\n  \"requests_count\": \"Cont interogări\",\n  \"reset_settings\": \"Resetare setări\",\n  \"resolve_clients_desc\": \"Rezolvă invers adresele IP ale clienților în numele lor de gazde prin trimiterea interogărilor PTR la rezolvatorii corespunzători (servere DNS private pentru clienți locali, servere în amonte pentru clienți cu adrese IP publice).\",\n  \"resolve_clients_title\": \"Permiteți rezolvarea inversa a adreselor IP ale clienților\",\n  \"response_code\": \"Cod de răspuns\",\n  \"response_details\": \"Detalii răspuns\",\n  \"response_table_header\": \"Răspuns\",\n  \"response_time\": \"Timp de răspuns\",\n  \"rewrite_A\": \"<0>A</0>: valoare specială, păstrați <0>A</0> înregistrări din amonte\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: valoare specială, păstrați <0>AAAA</0> înregistrări din amonte\",\n  \"rewrite_add\": \"Adăugați rescriere DNS\",\n  \"rewrite_added\": \"Rescriere DNS pentru \\\"{{key}}\\\" adăugată cu succes\",\n  \"rewrite_applied\": \"Regula de rescriere s-a aplicat\",\n  \"rewrite_confirm_delete\": \"Sunteți sigur că doriți să ștergeți rescrierea DNS pentru \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"Rescriere DNS pentru \\\"{{key}}\\\" ștearsă cu succes\",\n  \"rewrite_desc\": \"Permite configurarea cu ușurință a răspunsului personalizat DNS pentru un nume de domeniu specific.\",\n  \"rewrite_domain_name\": \"Nume de domeniu: adăugați o înregistrare CNAME\",\n  \"rewrite_edit\": \"Editați rescrierea DNS\",\n  \"rewrite_hosts_applied\": \"Rescrisă de regula fișierului hosts\",\n  \"rewrite_ip_address\": \"Adresa IP: utilizați acest IP într-un răspuns A sau AAAA\",\n  \"rewrite_not_found\": \"Nu s-au găsit rescrieri DNS\",\n  \"rewrite_settings_updated\": \"Setările de rescriere DNS au fost actualizate cu succes\",\n  \"rewrite_updated\": \"DNS rescrie actualizat cu succes\",\n  \"rewrites_disabled_table_header\": \"Rescrierile sunt dezactivate\",\n  \"rewrites_enabled_table_header\": \"Rescrierile sunt activate\",\n  \"rewritten\": \"Rescrise\",\n  \"rows_table_footer_text\": \"linii\",\n  \"rule_added_to_custom_filtering_toast\": \"Regulă adăugată la regulile de filtrare personalizate: {{rule}}\",\n  \"rule_label\": \"Regulă(reguli)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regulă scoasă din regullei personalizate de filtrare: {{rule}}\",\n  \"rules_count_table_header\": \"Număr de reguli\",\n  \"safe_browsing\": \"Navigare în siguranță\",\n  \"safe_search\": \"Căutarea sigură\",\n  \"saturday\": \"Sâmbătă\",\n  \"saturday_short\": \"sa\",\n  \"save_btn\": \"Salvați\",\n  \"save_config\": \"Salvare configurare\",\n  \"schedule_add\": \"Adăugați program\",\n  \"schedule_current_timezone\": \"Fus orar curent: {{value}}\",\n  \"schedule_desc\": \"Setați perioade de inactivitate pentru serviciile blocate\",\n  \"schedule_edit\": \"Editare program\",\n  \"schedule_from\": \"De\",\n  \"schedule_invalid_select\": \"Ora de început trebuie să fie înaintea orei de sfârșit\",\n  \"schedule_modal_description\": \"Acest program va înlocui orice program existent pentru aceeași zi a săptămânii. Fiecare zi a săptămânii poate avea o singură perioadă de inactivitate.\",\n  \"schedule_modal_time_off\": \"Fără blocare a serviciului:\",\n  \"schedule_new\": \"Program nou\",\n  \"schedule_remove\": \"Înlăturați programul\",\n  \"schedule_save\": \"Salvați programul\",\n  \"schedule_select_days\": \"Selectați zile\",\n  \"schedule_services\": \"Întrerupeți blocarea serviciului\",\n  \"schedule_services_desc\": \"Configurați programul de pauză al filtrului de blocare a serviciului\",\n  \"schedule_services_desc_client\": \"Configurați programul de pauză al filtrului de blocare a serviciului pentru acest client\",\n  \"schedule_time_all_day\": \"Toată ziua\",\n  \"schedule_timezone\": \"Selectați un fus orar\",\n  \"schedule_to\": \"Până\",\n  \"served_from_cache_label\": \"Furnizat din cache\",\n  \"service_name\": \"Numele serviciului\",\n  \"set_static_ip\": \"Setați o adresă IP statică\",\n  \"settings\": \"Setări\",\n  \"settings_custom\": \"Personalizat\",\n  \"settings_global\": \"General\",\n  \"setup_config_to_enable_dhcp_server\": \"Setați configurația pentru a activa serverul DHCP\",\n  \"setup_dns_notice\": \"Pentru a utiliza <1>DNS-over-HTTPS</1> sau <1>DNS-over-TLS</1>, trebuie să <0>configurați Criptarea</0> în setările AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Folosiți stringul <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Folosiți stringul <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>lată o listă de software pe care le puteți utiliza.</0>\",\n  \"setup_dns_privacy_4\": \"Pe un dispozitiv iOS 14 sau macOS Big Sur puteți descărca fișierul special '.mobileconfig' care adaugă servere <highlight>DNS-over-HTTPS</highlight> sau <highlight>DNS-over-TLS</highlight> la setările DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 acceptă DNS-over-TLS nativ. Pentru a-l configura, accesați Setări → Rețea și internet → Advanced → Private DNS și introduceți numele de domeniu acolo.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard pentru Android</0> acceptă <1>DNS-over-HTTPS</1> și <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> adaugă <1>DNS-over-HTTPS</1> suport pentru Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Configurarea iOS și macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> acceptă <1>DNS-over-HTTPS</1>, dar pentru a-l configura pentru a utiliza propriul server, va trebui să generați un <2>DNS Stamp</2> pentru aceasta.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard pentru iOS</0> acceptă instalarea <1>DNS-over-HTTPS</1> și <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home poate fi un client DNS sigur pe orice platformă.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> acceptă toate protocoalele DNS securizate cunoscute.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> acceptă <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> acceptă <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Veți găsi mai multe implementări <0>aici</0> și <1>aici</1>.\",\n  \"setup_dns_privacy_other_title\": \"Alte implementări\",\n  \"setup_guide\": \"Ghid de instalare\",\n  \"show_all_filter_type\": \"Arată tot\",\n  \"show_blocked_responses\": \"Blocat\",\n  \"show_filtered_type\": \"Arată cele filtrate\",\n  \"show_processed_responses\": \"Tratat\",\n  \"show_whitelisted_responses\": \"Autorizate\",\n  \"sign_in\": \"Conectare\",\n  \"sign_out\": \"Deconectare\",\n  \"source_label\": \"Sursă\",\n  \"static_ip\": \"Adresa IP Statică\",\n  \"static_ip_desc\": \"AdGuard Home este un server, deci are nevoie de o adresă IP statică pentru a funcționa corect. Altfel, routerul dvs. poate eventual să atribuie o adresă IP diferită acestui dispozitiv.\",\n  \"statistics_clear\": \" Șterge statisticile\",\n  \"statistics_clear_confirm\": \"Sunteți sigur că doriți să ștergeți statisticile?\",\n  \"statistics_cleared\": \"Statisticile au fost șterse cu succes\",\n  \"statistics_configuration\": \"Configurația statisticilor\",\n  \"statistics_enable\": \"Activați statisticile\",\n  \"statistics_retention\": \"Păstrarea statisticilor\",\n  \"statistics_retention_confirm\": \"Sunteți sigur că doriți să schimbați păstrarea statisticilor? Dacă reduceți valoarea intervalului, unele date vor fi pierdute\",\n  \"statistics_retention_desc\": \"Dacă reduceți valoarea intervalului, unele date vor fi pierdute\",\n  \"stats_adult\": \"Site-uri cu conținut adult blocate\",\n  \"stats_disabled\": \"Statisticile au fost dezactivate. Puteți să le porniți din <0>pagina de setări</0>.\",\n  \"stats_disabled_short\": \"Statisticile au fost dezactivate\",\n  \"stats_malware_phishing\": \"Malware/phishing blocate\",\n  \"stats_params\": \"Configurația statisticilor\",\n  \"stats_query_domain\": \"Domeniile cele mai căutate\",\n  \"subnet_error\": \"Adresele trebuie să fie în aceeași subrețea\",\n  \"sunday\": \"Duminică\",\n  \"sunday_short\": \"du\",\n  \"system_host_files\": \"Fișiere de sistem hosts\",\n  \"table_client\": \"Client\",\n  \"table_name\": \"Nume\",\n  \"tags_desc\": \"Puteți selecta etichetele care corespund clientului. Includeți etichete în regulile de filtrare pentru a le aplica mai precis. <0>Aflați mai multe</0>.\",\n  \"tags_title\": \"Etichete\",\n  \"test_upstream_btn\": \"Testați upstreams\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Auto (pe baza schemei de culori a dispozitivului dvs.)\",\n  \"theme_dark\": \"Sombră\",\n  \"theme_dark_desc\": \"Temă întunecată\",\n  \"theme_light\": \"Luminoasă\",\n  \"theme_light_desc\": \"Temă luminoasă\",\n  \"thursday\": \"Joi\",\n  \"thursday_short\": \"jo\",\n  \"time_table_header\": \"Ora\",\n  \"top_blocked_domains\": \"Domeniile blocate cel mai des\",\n  \"top_clients\": \"Clienți de top\",\n  \"top_upstreams\": \"Top servere în amonte\",\n  \"topline_expired_certificate\": \"Certificatul dvs. SSL a expirat. Actualizați <0>Setările de criptare</0>.\",\n  \"topline_expiring_certificate\": \"Certificatul dvs. SSL este pe cale să expire. Actualizați <0>Setările de criptare</0>.\",\n  \"tracker_source\": \"Sursă tracker\",\n  \"try_again\": \"Încercați din nou\",\n  \"ttl_cache_validation\": \"Valoarea TTL cache minimă trebuie să fie mai mică sau egală cu valoarea maximă\",\n  \"tuesday\": \"Marți\",\n  \"tuesday_short\": \"ma\",\n  \"type_table_header\": \"Tip\",\n  \"unavailable_dhcp\": \"DHCP este indisponibil\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home nu poate rula un server DHCP pe OS-ul dvs.\",\n  \"unblock\": \"Deblocați\",\n  \"unblock_all\": \"Deblocați tot\",\n  \"unblock_for_this_client_only\": \"Deblocați numai pentru acest client\",\n  \"unknown_filter\": \"Filtru necunoscut {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} este disponibil! <0>Clicați aici</0> pentru mai multe informații.\",\n  \"update_failed\": \"Auto-actualizarea a eșuat. Vă rugăm să <a>urmați aceste etape</a> pentru a actualiza manual.\",\n  \"update_now\": \"Actualizați acum\",\n  \"updated_custom_filtering_toast\": \"Regulile personalizate au fost salvate cu succes\",\n  \"updated_save_search_toast\": \"Setări Căutare sigură actualizate\",\n  \"updated_upstream_dns_toast\": \"Serverele din amonte au fost salvate cu succes\",\n  \"updates_checked\": \"Este disponibilă o nouă versiune de AdGuard Home\\n\",\n  \"updates_version_equal\": \"AdGuard Home este la zi\",\n  \"upstream\": \"Server în amonte\",\n  \"upstream_dns\": \"Servere DNS în amonte\",\n  \"upstream_dns_cache_configuration\": \"Configurarea cache-ului DNS în amonte\",\n  \"upstream_dns_client_desc\": \"Dacă mențineți acest câmp gol, AdGuard Home va folosi serverele configurate în <0>setările DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Configurat în {{path}}\",\n  \"upstream_dns_help\": \"Introduceți o adresă de server pe linie. <a>Aflați mai multe</a> despre configurarea serverelor DNS în amonte.\",\n  \"upstream_parallel\": \"Folosiți interogări paralele pentru a accelera rezolvarea, interogând simultan toate serverele în amonte.\",\n  \"upstream_timeout\": \"Durata de așteptare a răspunsurilor de la serverele upstream\",\n  \"upstream_timeout_desc\": \"Specifica numărul de secunde de așteptat pentru un răspuns de la serverul în amonte\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Utilizați serviciul Navigarea în Securitate AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home va verifica dacă domeniul este blocat de serviciul web de securitate de navigare. Pentru acesta, va utiliza un API de căutare discret: numai un prefix scurt al hash-ului SHA256 al numelui de domeniu este trimis la server.\",\n  \"use_adguard_parental\": \"Utilizați Controlul Parental AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home va verifica pentru conținut adult pe domeniu. Utilizează același API discret ca cel utilizat de serviciul de securitate de navigare.\",\n  \"use_private_ptr_resolvers_desc\": \"Rezolvați cererile PTR, SOA și NS pentru domeniile ARPA care conțin adrese IP private prin intermediul serverelor upstream private, DHCP, /etc/hosts etc. Dacă este dezactivat, AdGuard Home va răspunde la toate aceste cereri cu NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Utilizați rezolvatori DNS inverși privați\",\n  \"use_saved_key\": \"Folosiți cheia salvată anterior\",\n  \"username_label\": \"Nume utilizator\",\n  \"username_placeholder\": \"Introduceți nume utilizator\",\n  \"validated_with_dnssec\": \"Validat cu DNSSEC\",\n  \"version\": \"Versiune\",\n  \"version_request_error\": \"Verificarea actualizării nu a reușit. Verificați conexiunea internet.\",\n  \"wednesday\": \"Miercuri\",\n  \"wednesday_short\": \"mi\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/ru.json",
    "content": "{\n  \"access_allowed_desc\": \"Список CIDR, IP-адресов или <a>ClientID</a>. Если в списке есть записи, AdGuard Home будет принимать запросы только от этих клиентов.\",\n  \"access_allowed_title\": \"Разрешённые клиенты\",\n  \"access_blocked_desc\": \"Не путать с фильтрами. AdGuard Home будет игнорировать DNS-запросы с этими доменами. Здесь вы можете уточнить точные имена доменов, шаблоны, правила URL-фильтрации, например, «example.org», «*.example.org» или «||example.org».\",\n  \"access_blocked_title\": \"Неразрешённые домены\",\n  \"access_desc\": \"Здесь вы можете настроить правила доступа к DNS-серверу AdGuard Home\",\n  \"access_disallowed_desc\": \"Список CIDR, IP-адресов или <a>ClientID</a>. Если в списке есть записи, AdGuard Home будет игнорировать запросы от этих клиентов. Это поле игнорируется, если список разрешённых клиентов содержит записи.\",\n  \"access_disallowed_title\": \"Запрещённые клиенты\",\n  \"access_settings_saved\": \"Настройки доступа успешно сохранены\",\n  \"access_title\": \"Настройки доступа\",\n  \"actions_table_header\": \"Действия\",\n  \"add_allowlist\": \"Добавить белый список\",\n  \"add_blocklist\": \"Добавить чёрный список\",\n  \"add_custom_list\": \"Добавить свой список\",\n  \"add_persistent_client\": \"Добавить в сохранённые клиенты\",\n  \"address\": \"Адрес\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home сбросит все DNS-запросы от этого клиента.\",\n  \"all_lists_up_to_date_toast\": \"Все списки уже обновлены\",\n  \"all_queries\": \"Все запросы\",\n  \"allow_this_client\": \"Разрешить доступ клиенту\",\n  \"allowed\": \"Разрешённые\",\n  \"anonymize_client_ip\": \"Анонимизировать IP-адрес клиента\",\n  \"anonymize_client_ip_desc\": \"Не сохранять полный IP-адрес клиента в журналах и статистике\",\n  \"anonymizer_notification\": \"<0>Внимание:</0> включена анонимизация IP-адресов. Вы можете отключить её в разделе <1>Основные настройки</1>.\",\n  \"answer\": \"Ответ\",\n  \"apply_btn\": \"Применить\",\n  \"auto_clients_desc\": \"Информация об IP-адресах устройств, которые используют или могут использовать AdGuard Home. Эта информация собирается из нескольких источников, включая файлы hosts, обратный DNS и так далее.\",\n  \"auto_clients_title\": \"Клиенты (runtime)\",\n  \"autofix_warning_list\": \"Будут выполняться следующие задачи: <0>Деактивировать системный DNSStubListener</0> <0>Установить адрес сервера DNS на 127.0.0.1</0> <0>Создать символическую ссылку /etc/resolv.conf на /run/systemd/resolve/resolv.conf</0> <0>Остановить DNSStubListener (перезагрузить системную службу)</0>.\",\n  \"autofix_warning_result\": \"В результате все DNS-запросы от вашей системы будут по умолчанию обрабатываться AdGuard Home.\\n\",\n  \"autofix_warning_text\": \"При нажатии «Исправить» AdGuard Home настроит вашу систему на использование DNS-сервера AdGuard Home.\",\n  \"average_processing_time\": \"Среднее время обработки запроса\",\n  \"average_processing_time_hint\": \"Среднее время для обработки запроса DNS  в миллисекундах\",\n  \"average_upstream_response_time\": \"Среднее время ответа upstream-сервера\",\n  \"back\": \"Назад\",\n  \"block\": \"Заблокировать\",\n  \"block_all\": \"Заблокировать все\",\n  \"block_domain_use_filters_and_hosts\": \"Блокировать домены с использованием фильтров и файлов hosts\",\n  \"block_for_this_client_only\": \"Заблокировать только для этого клиента\",\n  \"block_services\": \"Выбрать заблокированные сервисы\",\n  \"blocked_adult_websites\": \"Заблокировано Родительским контролем\",\n  \"blocked_by\": \"<0>Заблокировано фильтрами</0>\",\n  \"blocked_by_cname_or_ip\": \"Заблокировано с помощью CNAME или IP\",\n  \"blocked_by_response\": \"Заблокировано по CNAME или IP в ответе\",\n  \"blocked_response_ttl\": \"TTL заблокированного ответа\",\n  \"blocked_response_ttl_desc\": \"Указывает, в течение скольких секунд клиенты должны кешировать отфильтрованный ответ\",\n  \"blocked_safebrowsing\": \"Заблокировано согласно базе данных Safe Browsing\",\n  \"blocked_service\": \"Заблокированный сервис\",\n  \"blocked_services\": \"Заблокированные сервисы\",\n  \"blocked_services_desc\": \"Позволяет быстро заблокировать популярные сайты и сервисы.\",\n  \"blocked_services_global\": \"Использовать глобальные заблокированные сервисы\",\n  \"blocked_services_saved\": \"Заблокированные сервисы успешно сохранены\",\n  \"blocked_threats\": \"Заблокировано угроз\",\n  \"blocking_ipv4\": \"Блокировка IPv4\",\n  \"blocking_ipv4_desc\": \"IP-адрес, возвращаемый при блокировке A-запроса\",\n  \"blocking_ipv6\": \"Блокировка IPv6\",\n  \"blocking_ipv6_desc\": \"IP-адрес, возвращаемый при блокировке AAAA-запроса\",\n  \"blocking_mode\": \"Режим блокировки\",\n  \"blocking_mode_custom_ip\": \"Пользовательский IP: Отвечает с вручную настроенным IP-адресом\",\n  \"blocking_mode_default\": \"Стандартный: Отвечает с нулевым IP-адресом, (0.0.0.0 для A; :: для AAAA) когда заблокировано правилом в стиле Adblock; отвечает с IP-адресом, указанным в правиле, когда заблокировано правилом в стиле файлов hosts\",\n  \"blocking_mode_null_ip\": \"Нулевой IP: Отвечает с нулевым IP-адресом (0.0.0.0 для A; :: для AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Отвечает с кодом NXDOMAIN\\n\",\n  \"blocking_mode_refused\": \"REFUSED: Отвечает с кодом REFUSED\",\n  \"blocklist\": \"Чёрный список\",\n  \"bootstrap_dns\": \"Bootstrap DNS-серверы\",\n  \"bootstrap_dns_desc\": \"IP-адреса DNS-серверов, используемых для поиска IP-адресов DoH/DoT upstream-серверов, которые вы указали. Комментарии не допускаются.\",\n  \"cache_cleared\": \"Кеш DNS успешно очищен\",\n  \"cache_enabled\": \"Включить кеш\",\n  \"cache_enabled_desc\": \"Сохранять локально ответы DNS.\",\n  \"cache_optimistic\": \"Оптимистическое кеширование\",\n  \"cache_optimistic_desc\": \"AdGuard Home будет отвечать из кеша, даже если ответы в нём неактуальны, и попытается обновить их.\",\n  \"cache_size\": \"Размер кеша\",\n  \"cache_size_desc\": \"Размер кеша DNS (в байтах).\",\n  \"cache_size_validation\": \"Если кеш включен, его размер должен быть больше нуля.\",\n  \"cache_ttl_max_override\": \"Переопределить максимальный TTL\",\n  \"cache_ttl_max_override_desc\": \"Установить максимальное TTL-значение (в секундах) для записей в DNS-кеше.\",\n  \"cache_ttl_min_override\": \"Переопределить минимальный TTL\",\n  \"cache_ttl_min_override_desc\": \"Расширить короткие TTL-значения (в секундах), полученные с upstream-сервера при кешировании DNS-ответов.\",\n  \"cancel_btn\": \"Отмена\",\n  \"category_label\": \"Категория\",\n  \"check\": \"Проверить\",\n  \"check_client_id\": \"Идентификатор клиента (ClientID или IP-адрес)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Проверить фильтрацию имени хоста.\",\n  \"check_dhcp_servers\": \"Проверить DHCP-серверы\",\n  \"check_dns_record\": \"Выберите тип DNS-записи\",\n  \"check_enter_client_id\": \"Введите идентификатор клиента\",\n  \"check_hostname\": \"Имя хоста или домена\",\n  \"check_ip\": \"IP-адреса: {{ip}}\",\n  \"check_not_found\": \"Не найдено в вашем списке фильтров\",\n  \"check_reason\": \"Причина: {{reason}}\",\n  \"check_service\": \"Название сервиса: {{service}}\",\n  \"check_title\": \"Проверить фильтрацию\",\n  \"check_updates_btn\": \"Проверить обновления\",\n  \"check_updates_now\": \"Проверить обновления\",\n  \"choose_allowlist\": \"Выберите списки разрешённых\",\n  \"choose_blocklist\": \"Выберите списки блокировки\",\n  \"choose_from_list\": \"Выбрать из списка\",\n  \"city\": \"Город\",\n  \"clear_cache\": \"Очистить кеш\",\n  \"click_to_view_queries\": \"Нажмите, чтобы просмотреть запросы\",\n  \"client_add\": \"Добавить клиента\",\n  \"client_added\": \"Клиент «{{key}}» успешно добавлен\",\n  \"client_blocked\": \"Клиент «{{ip}}» успешно заблокирован\",\n  \"client_confirm_block\": \"Вы уверены, что хотите заблокировать клиента «{{ip}}»?\",\n  \"client_confirm_delete\": \"Вы уверены, что хотите удалить клиента «{{key}}»?\",\n  \"client_confirm_unblock\": \"Вы уверены, что хотите разблокировать клиента «{{ip}}»?\",\n  \"client_deleted\": \"Клиент «{{key}}» успешно удалён\",\n  \"client_details\": \"Информация о клиенте\",\n  \"client_edit\": \"Редактировать клиента\",\n  \"client_global_settings\": \"Использовать глобальные настройки\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Клиенты могут идентифицироваться по ClientID. <a>Здесь</a> вы можете узнать больше об идентификации клиентов.\",\n  \"client_id_placeholder\": \"Введите ClientID\",\n  \"client_identifier\": \"Идентификатор\",\n  \"client_identifier_desc\": \"Клиенты могут быть идентифицированы по IP-адресу, CIDR, MAC-адресу или ClientID (можно использовать для DoT/DoH/DoQ). <0>Здесь</0> вы можете узнать больше об идентификации клиентов.\",\n  \"client_name\": \"Клиент {{id}}\",\n  \"client_new\": \"Новый клиент\",\n  \"client_settings\": \"Настройки клиентов\",\n  \"client_table_header\": \"Клиент\",\n  \"client_unblocked\": \"Клиент «{{ip}}» успешно разблокирован\",\n  \"client_updated\": \"Клиент «{{key}}» успешно обновлён\",\n  \"clients_desc\": \"Настройте устройства, использующие AdGuard Home\",\n  \"clients_not_found\": \"Клиентов не найдено\",\n  \"clients_title\": \"Сохранённые клиенты\",\n  \"compact\": \"Компактный\",\n  \"config_successfully_saved\": \"Конфигурация успешно сохранена\",\n  \"configure\": \"Настроить\",\n  \"confirm_dns_cache_clear\": \"Вы уверены, что хотите очистить кеш DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home настроит {{ip}} в качестве вашего статического IP-адреса. Хотите продолжить?\",\n  \"copyright\": \"Все права защищены\",\n  \"country\": \"Страна\",\n  \"custom_filter_rules\": \"Пользовательские правила фильтрации\",\n  \"custom_filter_rules_hint\": \"Вводите по одному правилу на строчку. Вы можете использовать правила блокировки или синтаксис файлов hosts.\",\n  \"custom_filtering_rules\": \"Пользовательские правила фильтрации\",\n  \"custom_ip\": \"Свой IP\",\n  \"custom_retention_input\": \"Введите срок хранения в часах\",\n  \"custom_rotation_input\": \"Введите частоту ротации в часах\",\n  \"dashboard\": \"Панель управления\",\n  \"date\": \"Дата\",\n  \"default\": \"Стандартный\",\n  \"delete_confirm\": \"Вы уверены, что хотите удалить «{{key}}»?\",\n  \"delete_table_action\": \"Удалить\",\n  \"descr\": \"Описание\",\n  \"details\": \"Детали\",\n  \"dhcp_add_static_lease\": \"Добавить статическую аренду\",\n  \"dhcp_config_saved\": \"Конфигурация DHCP-сервера успешно сохранена\",\n  \"dhcp_description\": \"Если ваш роутер не предоставляет настройки DHCP, вы можете использовать собственный встроенный DHCP-сервер AdGuard.\",\n  \"dhcp_disable\": \"Отключить DHCP-сервер\",\n  \"dhcp_dynamic_ip_found\": \"Ваша система использует динамический IP-адрес для интерфейса <0>{{interfaceName}}</0>. Чтобы использовать DHCP-сервер, необходимо установить статический IP-адрес. Ваш текущий IP-адрес – <0>{{ipAddress}}</0>. Мы автоматически установим его как статический, если вы нажмёте кнопку «Включить DHCP-сервер».\",\n  \"dhcp_edit_static_lease\": \"Редактирование статической аренды\",\n  \"dhcp_enable\": \"Включить DHCP-сервер\",\n  \"dhcp_error\": \"AdGuard Home не смог определить присутствие других DHCP-серверов в сети\",\n  \"dhcp_form_gateway_input\": \"IP-адрес шлюза\",\n  \"dhcp_form_lease_input\": \"Срок аренды\",\n  \"dhcp_form_lease_title\": \"Время аренды DHCP (в секундах)\",\n  \"dhcp_form_range_end\": \"Конец диапазона\",\n  \"dhcp_form_range_start\": \"Начало диапазона\",\n  \"dhcp_form_range_title\": \"Диапазон IP-адресов\",\n  \"dhcp_form_subnet_input\": \"Маска подсети\",\n  \"dhcp_found\": \"Некоторые активные DHCP-серверы найдены в сети. Включение встроенного DHCP-сервера небезопасно.\",\n  \"dhcp_hardware_address\": \"Аппаратный адрес\",\n  \"dhcp_interface_select\": \"Выбрать интерфейс DHCP\",\n  \"dhcp_ip_addresses\": \"IP-адреса\",\n  \"dhcp_ipv4_settings\": \"Настройки DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Настройки DHCP IPv6\",\n  \"dhcp_lease_added\": \"Статическая аренда «{{key}}» успешно добавлена\",\n  \"dhcp_lease_deleted\": \"Статическая аренда «{{key}}» успешно удалена\",\n  \"dhcp_lease_updated\": \"Статическая аренда «{{key}}» успешно обновлена\",\n  \"dhcp_leases\": \"Аренда DHCP\",\n  \"dhcp_leases_not_found\": \"Аренда DHCP не обнаружена\",\n  \"dhcp_new_static_lease\": \"Новая статическая аренда\",\n  \"dhcp_not_found\": \"Можно безопасно включить DHCP-сервер, так как другие активные DHCP-серверы в сети не найдены. Однако, рекомендуется перепроверить их отсутствие вручную, так как автоматическая проверка не даёт 100% гарантии.\",\n  \"dhcp_reset\": \"Вы уверены, что хотите сбросить настройки DHCP?\",\n  \"dhcp_reset_leases\": \"Сбросить все аренды\",\n  \"dhcp_reset_leases_confirm\": \"Вы уверены, что хотите удалить все аренды?\",\n  \"dhcp_reset_leases_success\": \"Аренды DHCP успешно удалены\",\n  \"dhcp_settings\": \"Настройки DHCP\",\n  \"dhcp_static_ip_error\": \"Чтобы использовать DHCP-сервер, должен быть установлен статический IP-адрес. AdGuard Home не смог определить, использует ли этот сетевой интерфейс статический IP-адрес. Пожалуйста, установите его вручную.\",\n  \"dhcp_static_leases\": \"Статические аренды DHCP\",\n  \"dhcp_static_leases_not_found\": \"Не найдено статических аренд DHCP\",\n  \"dhcp_table_expires\": \"Истекает\",\n  \"dhcp_table_hostname\": \"Имя хоста\",\n  \"dhcp_title\": \"DHCP-сервер (экспериментальный!)\",\n  \"dhcp_warning\": \"Если вы всё равно хотите включить DHCP-сервер, убедитесь, что в сети больше нет активных DHCP-серверов. Иначе это может сломать доступ в сеть для подключённых устройств!\",\n  \"disable_for_hours\": \"На {{count}} час\",\n  \"disable_for_hours_plural\": \"На {{count}} часов\",\n  \"disable_for_minutes\": \"На {{count}} минуту\",\n  \"disable_for_minutes_plural\": \"На {{count}} минут\",\n  \"disable_for_seconds\": \"На {{count}} секунд\",\n  \"disable_for_seconds_plural\": \"На {{count}} секунд\",\n  \"disable_ipv6\": \"Отключить обработку IPv6-адресов\",\n  \"disable_ipv6_desc\": \"Игнорировать все DNS-запросы адресов IPv6 (тип AAAA) и удалять IPv6-данные из ответов типа HTTPS.\",\n  \"disable_notify_for_hours\": \"Отключить защиту на {{count}} час\",\n  \"disable_notify_for_hours_plural\": \"Отключить защиту на {{count}} часов\",\n  \"disable_notify_for_minutes\": \"Отключить защиту на {{count}} минуту\",\n  \"disable_notify_for_minutes_plural\": \"Отключить защиту на {{count}} минут\",\n  \"disable_notify_for_seconds\": \"Отключить защиту на {{count}} секунд\",\n  \"disable_notify_for_seconds_plural\": \"Отключить защиту на {{count}} секунд\",\n  \"disable_notify_until_tomorrow\": \"Отключить защиту до завтра\",\n  \"disable_protection\": \"Отключить защиту\",\n  \"disable_rewrites\": \"Отключить правила перезаписи\",\n  \"disable_until_tomorrow\": \"До завтра\",\n  \"disabled\": \"Выключено\",\n  \"disabled_dhcp\": \"DHCP-сервер отключён\",\n  \"disabled_filtering_toast\": \"Фильтрация выкл.\",\n  \"disabled_parental_toast\": \"Родительский контроль выкл.\",\n  \"disabled_protection\": \"Защита выкл.\",\n  \"disabled_safe_browsing_toast\": \"Антифишинг отключён\",\n  \"disabled_safe_search_toast\": \"Безопасный поиск выкл.\",\n  \"disallow_this_client\": \"Запретить доступ клиенту\",\n  \"dns_addresses\": \"Адреса DNS\",\n  \"dns_allowlists\": \"Белые списки DNS\",\n  \"dns_allowlists_desc\": \"Домены из белых списков DNS будут разрешены, даже если они находятся в любом из чёрных списков.\",\n  \"dns_blocklists\": \"Чёрные списки DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home будет блокировать домены из чёрных списков.\",\n  \"dns_cache_config\": \"Настройка кеша DNS\",\n  \"dns_cache_config_desc\": \"Здесь можно настроить кеш DNS\",\n  \"dns_cache_size\": \"Размер DNS-кеша в байтах\",\n  \"dns_config\": \"Настройки DNS-сервера\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"Зашифрованный DNS\",\n  \"dns_providers\": \"<0>Список известных DNS-провайдеров</0> на выбор.\",\n  \"dns_query\": \"DNS-запросы\",\n  \"dns_rewrites\": \"Перезапись DNS-запросов\",\n  \"dns_settings\": \"Настройки DNS\",\n  \"dns_start\": \"DNS-сервер запускается\",\n  \"dns_status_error\": \"Ошибка при получении состояния DNS-сервера\",\n  \"dns_test_not_ok_toast\": \"Сервер «{{key}}»: невозможно использовать, проверьте правильность написания\",\n  \"dns_test_ok_toast\": \"Указанные серверы DNS работают корректно\",\n  \"dns_test_parsing_error_toast\": \"Раздел {{section}}: строка {{line}}: невозможно использовать, проверьте правильность написания\",\n  \"dns_test_warning_toast\": \"Upstream «{{key}}» не отвечает на тестовые запросы и может работать некорректно\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Включить DNSSEC\",\n  \"dnssec_enable_desc\": \"Установите флаг DNSSEC в исходящих DNS-запросах и проверьте результат (требуется резолвер с поддержкой DNSSEC).\",\n  \"domain\": \"Домен\",\n  \"domain_desc\": \"Введите имя или маску домена, который вы хотите перенаправить.\",\n  \"domain_name_table_header\": \"Домен\",\n  \"domain_or_client\": \"Домен или клиент\",\n  \"down\": \"Вниз\",\n  \"download_mobileconfig\": \"Загрузить файл конфигурации\",\n  \"download_mobileconfig_doh\": \"Скачать .mobileconfig для DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Скачать .mobileconfig для DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Редактировать белый список\",\n  \"edit_blocklist\": \"Редактировать чёрный список\",\n  \"edit_table_action\": \"Редактировать\",\n  \"edns_cs_desc\": \"Добавлять опцию EDNS Client Subnet (ECS) к запросам к upstream-серверам, а также записывать присланные клиентами значения в журнал.\",\n  \"edns_enable\": \"Включить отправку EDNS Client Subnet\",\n  \"edns_use_custom_ip\": \"Использовать указанный IP для EDNS\",\n  \"edns_use_custom_ip_desc\": \"Разрешить использовать собственный IP для EDNS\",\n  \"elapsed\": \"Затрачено\",\n  \"empty_response_status\": \"Пусто\",\n  \"enable_protection\": \"Включить защиту\",\n  \"enable_protection_timer\": \"Защита будет включена в {{time}}\",\n  \"enable_rewrites\": \"Включить правила перезаписи\",\n  \"enable_upstream_dns_cache\": \"Включить кеширование для пользовательской конфигурации upstream-серверов этого клиента\",\n  \"enabled_dhcp\": \"DHCP-сервер включён\",\n  \"enabled_filtering_toast\": \"Фильтрация вкл.\",\n  \"enabled_parental_toast\": \"Родительский контроль вкл.\",\n  \"enabled_protection\": \"Защита вкл.\",\n  \"enabled_safe_browsing_toast\": \"Антифишинг включён\",\n  \"enabled_save_search_toast\": \"Безопасный поиск вкл.\",\n  \"enabled_table_header\": \"Вкл.\",\n  \"encryption_certificate_path\": \"Путь к сертификату\",\n  \"encryption_certificates\": \"Сертификаты\",\n  \"encryption_certificates_desc\": \"Для использования шифрования вам необходимо предоставить корректную цепочку SSL-сертификатов для вашего домена. Вы можете получить бесплатный сертификат на <0>{{link}}</0> или вы можете купить его у одного из доверенных Центров Сертификации.\",\n  \"encryption_certificates_input\": \"Скопируйте сюда сертификаты в PEM-кодировке.\",\n  \"encryption_certificates_source_content\": \"Вставить содержимое сертификатов\",\n  \"encryption_certificates_source_path\": \"Указать путь к файлу сертификатов\",\n  \"encryption_chain_invalid\": \"Цепочка сертификатов не прошла проверку\",\n  \"encryption_chain_valid\": \"Цепочка сертификатов прошла проверку\",\n  \"encryption_config_saved\": \"Настройки шифрования сохранены\",\n  \"encryption_desc\": \"Поддержка шифрования (HTTPS/QUIC/TLS) для DNS и веб-интерфейса администрирования\",\n  \"encryption_doq\": \"Порт DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"Если этот порт настроен, AdGuard Home запустит сервер DNS-over-QUIC на этом порте.\",\n  \"encryption_dot\": \"Порт DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Если этот порт настроен, AdGuard Home запустит DNS-over-TLS-сервер на этому порту.\",\n  \"encryption_enable\": \"Включить шифрование (HTTPS, DNS-over-HTTPS и DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Если шифрование включено, веб-интерфейс AdGuard Home будет работать по HTTPS, а DNS-сервер будет также работать по DNS-over-HTTPS и DNS-over-TLS.\",\n  \"encryption_expire\": \"Истекает\",\n  \"encryption_hostnames\": \"Имена хостов\",\n  \"encryption_https\": \"Порт HTTPS\",\n  \"encryption_https_desc\": \"Если порт HTTPS настроен, веб-интерфейс администрирования AdGuard Home будет доступен через HTTPS, а также DNS-over-HTTPS сервер будет доступен по пути '/dns-query'.\",\n  \"encryption_issuer\": \"Издатель\",\n  \"encryption_key\": \"Приватный ключ\",\n  \"encryption_key_input\": \"Скопируйте сюда приватный ключ в PEM-кодировке.\",\n  \"encryption_key_invalid\": \"Некорректный {{type}} приватный ключ\",\n  \"encryption_key_source_content\": \"Вставить содержимое закрытого ключа\",\n  \"encryption_key_source_path\": \"Задайте путь к файлу приватного ключа\",\n  \"encryption_key_valid\": \"Корректный {{type}} приватный ключ\",\n  \"encryption_plain_dns_desc\": \"Незашифрованный DNS включён по умолчанию. Вы можете отключить его, чтобы заставить все устройства использовать зашифрованный DNS. Для этого необходимо включить хотя бы один зашифрованный протокол DNS\",\n  \"encryption_plain_dns_enable\": \"Включить незашифрованный DNS\",\n  \"encryption_plain_dns_error\": \"Чтобы отключить незашифрованный DNS, включите хотя бы один зашифрованный протокол DNS\",\n  \"encryption_private_key_path\": \"Путь к закрытому ключу\",\n  \"encryption_redirect\": \"Автоматически перенаправлять на HTTPS\",\n  \"encryption_redirect_desc\": \"Если включено, AdGuard Home будет автоматически перенаправлять вас с HTTP на HTTPS адрес.\",\n  \"encryption_reset\": \"Вы уверены, что хотите сбросить настройки шифрования?\",\n  \"encryption_server\": \"Имя сервера\",\n  \"encryption_server_desc\": \"Если задано, AdGuard Home распознаёт ClientID, отвечает на DDR-запросы, и дополнительно проверяет соединения. Если не задано, этот функционал отключён. Должно соответствовать одному из параметров DNS Names в сертификате.\",\n  \"encryption_server_enter\": \"Введите ваше доменное имя\",\n  \"encryption_settings\": \"Настройки шифрования\",\n  \"encryption_status\": \"Статус\",\n  \"encryption_subject\": \"Субъект\",\n  \"encryption_title\": \"Шифрование\",\n  \"encryption_warning\": \"Предупреждение\",\n  \"enforce_safe_search\": \"Включить безопасный поиск\",\n  \"enforce_save_search_hint\": \"AdGuard Home будет обеспечивать безопасный поиск в следующих поисковых системах: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Применён безопасный поиск\",\n  \"enter_cache_size\": \"Введите размер кеша (в байтах)\",\n  \"enter_cache_ttl_max_override\": \"Введите максимальный TTL (в секундах)\",\n  \"enter_cache_ttl_min_override\": \"Введите минимальный TTL (в секундах)\",\n  \"enter_name_hint\": \"Введите имя\",\n  \"enter_url_or_path_hint\": \"Введите URL-адрес или абсолютный путь к списку\",\n  \"enter_valid_allowlist\": \"Добавьте действующий URL-адрес в белый список.\",\n  \"enter_valid_blocklist\": \"Добавьте действующий URL-адрес в чёрный список.\",\n  \"error_details\": \"Детализация ошибки\",\n  \"example_comment\": \"! Так можно добавлять комментарии.\",\n  \"example_comment_hash\": \"# И вот так тоже.\",\n  \"example_comment_meaning\": \"комментарий;\",\n  \"example_meaning_filter_block\": \"заблокировать доступ к домену example.org и всем его поддоменам;\",\n  \"example_meaning_filter_whitelist\": \"разблокировать доступ к домену example.org и всем его поддоменам;\",\n  \"example_meaning_host_block\": \"отвечать адресом 127.0.0.1 для домена example.org (но не для его поддоменов);\",\n  \"example_multiple_upstreams_reserved\": \"несколько DNS-серверов <0>для конкретных доменов</0>;\",\n  \"example_regex_meaning\": \"блокировать доступ к доменам, соответствующим заданному регулярному выражению.\",\n  \"example_rewrite_domain\": \"переписывать ответы только для этого домена.\",\n  \"example_rewrite_wildcard\": \"переписывать ответы для всех поддоменов <0>example.org</0>.\",\n  \"example_upstream_comment\": \"комментарий.\",\n  \"example_upstream_doh\": \"зашифрованный <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"зашифрованный DNS-over-HTTPS с принудительным <0>HTTP/3</0> без отката к HTTP/2 или ниже;\",\n  \"example_upstream_doq\": \"зашифрован <0>DNS-over-QUIC</0>\",\n  \"example_upstream_dot\": \"зашифрованный <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"обычный DNS (поверх UDP);\",\n  \"example_upstream_regular_port\": \"обычный DNS (поверх UDP, с портом);\",\n  \"example_upstream_reserved\": \"DNS-сервер <0>для конкретных доменов</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> для <1>DNSCrypt</1> или <2>DNS-over-HTTPS</2> серверов;\",\n  \"example_upstream_tcp\": \"обычный DNS (поверх TCP);\",\n  \"example_upstream_tcp_hostname\": \"обычный DNS (поверх TCP, с именем хоста);\",\n  \"example_upstream_tcp_port\": \"обычный DNS (поверх TCP, с портом);\",\n  \"example_upstream_udp\": \"обычный DNS (поверх UDP, с именем хоста);\",\n  \"examples_title\": \"Примеры\",\n  \"fallback_dns_desc\": \"Список резервных DNS-серверов, используемых в тех случаях, когда вышестоящие DNS-серверы недоступны. Синтаксис такой же, как и в поле Upstream DNS-серверы выше.\",\n  \"fallback_dns_placeholder\": \"Введите один резервный DNS-сервер в каждой строке\",\n  \"fallback_dns_title\": \"Резервные DNS-серверы\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Самый быстрый IP-адрес\",\n  \"fastest_addr_desc\": \"Дождаться ответов от <b>всех</b> DNS-серверов, измерить скорость TCP-соединения для каждого сервера и вернуть IP-адрес сервера с самой высокой скоростью соединения.<br/>Этот режим может значительно замедлить выполнение DNS-запросов, если один или несколько серверов не отвечают. Убедитесь, что ваши серверы работают стабильно, а время ожидания серверов мало.\",\n  \"filter\": \"Фильтр\",\n  \"filter_added_successfully\": \"Список успешно добавлен\",\n  \"filter_allowlist\": \"ВНИМАНИЕ: Это действие также исключит правило «{{disallowed_rule}}» из списка разрешённых клиентов.\",\n  \"filter_category_general\": \"Общие\",\n  \"filter_category_general_desc\": \"Списки, которые блокируют отслеживание и рекламу на большинстве устройств\",\n  \"filter_category_other\": \"Другие\",\n  \"filter_category_other_desc\": \"Другие списки блокировки\",\n  \"filter_category_regional\": \"Региональные\",\n  \"filter_category_regional_desc\": \"Списки, которые фокусируются на региональной рекламе и серверах отслеживания\",\n  \"filter_category_security\": \"Безопасность\",\n  \"filter_category_security_desc\": \"Списки, созданные специально для блокировки вредоносных, фишинговых и мошеннических доменов\",\n  \"filter_removed_successfully\": \"Список успешно удалён\",\n  \"filter_updated\": \"Список успешно обновлён\",\n  \"filtered\": \"Отфильтрованные\",\n  \"filtered_custom_rules\": \"Отфильтрованы с помощью пользовательских правил фильтрации\",\n  \"filtering_rules_learn_more\": \"<0>Узнайте больше</0> о создании собственных списков блокировки хостов.\",\n  \"filters\": \"Фильтры\",\n  \"filters_and_hosts_hint\": \"AdGuard Home распознаёт базовые правила блокировки и синтаксис файлов hosts.\",\n  \"filters_block_toggle_hint\": \"Вы можете настроить правила блокировки в <a>«Фильтрах»</a>.\",\n  \"filters_configuration\": \"Настройка фильтров\",\n  \"filters_enable\": \"Включить фильтры\",\n  \"filters_interval\": \"Интервал обновления фильтров\",\n  \"fix\": \"Исправить\",\n  \"for_last_days\": \"за последний {{count}} день\",\n  \"for_last_days_plural\": \"за последние {{count}} дней\",\n  \"for_last_hours\": \"за последний {{count}} час\",\n  \"for_last_hours_plural\": \"за последние {{count}} часов\",\n  \"forgot_password\": \"Забыли пароль?\",\n  \"forgot_password_desc\": \"Пожалуйста, выполните <0>эти действия</0> для создания нового пароля к вашему аккаунту.\",\n  \"form_add_id\": \"Добавить идентификатор\",\n  \"form_answer\": \"Введите IP адрес или домен\",\n  \"form_client_name\": \"Введите имя клиента\",\n  \"form_domain\": \"Введите имя или маску домена\",\n  \"form_enter_blocked_response_ttl\": \"Введите TTL заблокированного ответа (в секундах)\",\n  \"form_enter_host\": \"Введите имя хоста\",\n  \"form_enter_hostname\": \"Введите имя хоста\",\n  \"form_enter_id\": \"Введите идентификатор\",\n  \"form_enter_ip\": \"Введите IP\",\n  \"form_enter_mac\": \"Введите MAC\",\n  \"form_enter_rate_limit\": \"Введите rate limit\",\n  \"form_enter_rate_limit_subnet_len\": \"Введите длину префикса подсети для ограничения скорости\",\n  \"form_enter_subnet_ip\": \"Введите IP-адрес в подсети «{{cidr}}»\",\n  \"form_enter_upstream_timeout\": \"Введите время ожидания для upstream-сервера в секундах\",\n  \"form_error_answer_format\": \"Некорректный ответ\",\n  \"form_error_client_id_format\": \"ClientID может содержать только цифры, строчные латинские буквы и дефисы\",\n  \"form_error_domain_format\": \"Некорректный домен\",\n  \"form_error_equal\": \"Не должны быть равны\",\n  \"form_error_gateway_ip\": \"Аренда не может иметь IP-адрес шлюза\",\n  \"form_error_ip4_format\": \"Некорректный IPv4-адрес\",\n  \"form_error_ip4_gateway_format\": \"Некорректный IPv4-адрес шлюза\",\n  \"form_error_ip6_format\": \"Некорректный IPv6-адрес\",\n  \"form_error_ip_format\": \"Некорректный IP-адрес\",\n  \"form_error_mac_format\": \"Некорректный MAC-адрес\",\n  \"form_error_password\": \"Пароли не совпадают\",\n  \"form_error_password_length\": \"Пароль должен содержать от {{min}} до {{max}} символов\",\n  \"form_error_port\": \"Введите корректный порт\",\n  \"form_error_port_range\": \"Введите номер порта из интервала 80-65535\",\n  \"form_error_port_unsafe\": \"Небезопасный порт\",\n  \"form_error_positive\": \"Должно быть больше 0\",\n  \"form_error_required\": \"Обязательное поле\",\n  \"form_error_server_name\": \"Некорректное имя сервера\",\n  \"form_error_subnet\": \"Подсеть «{{cidr}}» не содержит IP-адрес «{{ip}}»\",\n  \"form_error_url_format\": \"Неверный формат URL\",\n  \"form_error_url_or_path_format\": \"Неверный URL или абсолютный путь к списку\",\n  \"form_select_tags\": \"Выбрать теги клиента\",\n  \"found_in_known_domain_db\": \"Найден в базе известных доменов.\",\n  \"friday\": \"Пятница\",\n  \"friday_short\": \"Пт\",\n  \"gateway_or_subnet_invalid\": \"Некорректная маска подсети\",\n  \"general_settings\": \"Основные настройки\",\n  \"general_statistics\": \"Общая статистика\",\n  \"get_started\": \"Поехали\",\n  \"greater_range_start_error\": \"Должно быть больше начала диапазона\",\n  \"homepage\": \"Главная\",\n  \"host_whitelisted\": \"Хост занесён в белый список\",\n  \"ignore_domains\": \"Игнорируемые домены (построчно)\",\n  \"ignore_domains_desc_query\": \"Запросы, соответствующие этим правилам, не записываются в журнал запросов\",\n  \"ignore_domains_desc_stats\": \"Запросы, соответствующие этим правилам, не записываются в статистику\",\n  \"ignore_domains_title\": \"Игнорируемые домены\",\n  \"ignore_query_log\": \"Игнорировать этого клиента в журнале запросов\",\n  \"ignore_statistics\": \"Игнорировать этого клиента в статистике\",\n  \"install_auth_confirm\": \"Подтвердить пароль\",\n  \"install_auth_desc\": \"Должна быть настроена аутентификация паролем для веб-интерфейса AdGuard Home. Даже если он доступен только в вашей локальной сети, важно защитить его от неограниченного доступа.\",\n  \"install_auth_password\": \"Пароль\",\n  \"install_auth_password_enter\": \"Введите пароль\",\n  \"install_auth_title\": \"Авторизация\",\n  \"install_auth_username\": \"Имя пользователя\",\n  \"install_auth_username_enter\": \"Введите имя пользователя\",\n  \"install_devices_address\": \"DNS-сервер AdGuard Home доступен по следующим адресам\",\n  \"install_devices_android_list_1\": \"В меню управления нажмите иконку «Настройки».\",\n  \"install_devices_android_list_2\": \"Выберите пункт «Wi-Fi». Появится экран со списком доступных сетей (настройка DNS недоступна для мобильных сетей).\",\n  \"install_devices_android_list_3\": \"Долгим нажатием по текущей сети вызовите меню, в котором нажмите «Изменить сеть».\",\n  \"install_devices_android_list_4\": \"На некоторых устройствах может потребоваться нажать «Расширенные настройки». Чтобы получить возможность изменять настройки DNS, вам потребуется переключить «Настройки IP» на «Пользовательские».\",\n  \"install_devices_android_list_5\": \"Замените заданные значения DNS 1 и DNS 2 на адреса серверов AdGuard Home.\",\n  \"install_devices_desc\": \"Чтобы использовать AdGuard Home, настройте ваши устройства на его использование.\",\n  \"install_devices_ios_list_1\": \"Войдите в меню настроек устройства.\",\n  \"install_devices_ios_list_2\": \"Выберите пункт «Wi-Fi» (для мобильных сетей ручная настройка DNS невозможна).\",\n  \"install_devices_ios_list_3\": \"Нажмите на название сети, к которой устройство подключено в данный момент.\",\n  \"install_devices_ios_list_4\": \"В поле «DNS» введите введите адреса AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Кликните на иконку Apple и перейдите в «Системные настройки».\",\n  \"install_devices_macos_list_2\": \"Кликните на иконку «Сеть».\",\n  \"install_devices_macos_list_3\": \"Выберите первое подключение в списке и нажмите кнопку «Дополнительно».\",\n  \"install_devices_macos_list_4\": \"Выберите вкладку «DNS» и добавьте адреса AdGuard Home.\",\n  \"install_devices_router\": \"Роутер\",\n  \"install_devices_router_desc\": \"Эта настройка покроет все устройства, подключённые к вашему домашнему роутеру, и вам не нужно будет настраивать каждое вручную.\",\n  \"install_devices_router_list_1\": \"Откройте настройки вашего роутера. Обычно вы можете открыть их в вашем браузере, например, http://192.168.0.1/ или http://192.168.1.1/. Вас могут попросить ввести пароль. Если вы не помните его, пароль часто можно сбросить, нажав на кнопку на самом роутере, но помните, что эта процедура может привести к потере всей конфигурации роутера. Если вашему роутеру необходимо приложение для настройки, установите его на свой телефон или ПК и воспользуйтесь им для настройки роутера.\",\n  \"install_devices_router_list_2\": \"Найдите настройки DHCP или DNS. Найдите буквы «DNS» рядом с текстовым полем, в которое можно ввести два или три ряда цифр, разделённых на 4 группы от одной до трёх цифр.\",\n  \"install_devices_router_list_3\": \"Введите туда адрес вашего AdGuard Home.\",\n  \"install_devices_router_list_4\": \"Вы не можете установить собственный DNS-сервер на некоторых типах маршрутизаторов. В этом случае может помочь настройка AdGuard Home в качестве <0>DHCP-сервера</0>. В противном случае вам следует обратиться к руководству по настройке DNS-серверов для вашей конкретной модели маршрутизатора.\",\n  \"install_devices_title\": \"Настройте ваши устройства\",\n  \"install_devices_windows_list_1\": \"Откройте Панель управления через меню «Пуск» или через поиск Windows.\",\n  \"install_devices_windows_list_2\": \"Перейдите в «Сеть и интернет», а затем в «Центр управления сетями и общим доступом»\",\n  \"install_devices_windows_list_3\": \"В левой панели кликните на «Изменение параметров адаптера».\",\n  \"install_devices_windows_list_4\": \"Кликните на ваше активное подключение правой кнопкой мыши и выберите «Свойства».\",\n  \"install_devices_windows_list_5\": \"Найдите в списке пункт «IP версии 4 (TCP/IPv4)» (или «IP версии 6 (TCP/IPv6)» для IPv6), выделите его и затем снова нажмите «Свойства».\",\n  \"install_devices_windows_list_6\": \"Выберите «Использовать следующие адреса DNS-серверов» и введите адреса серверов AdGuard Home.\",\n  \"install_saved\": \"Успешно сохранено\",\n  \"install_settings_all_interfaces\": \"Все интерфейсы\",\n  \"install_settings_dns\": \"DNS-сервер\",\n  \"install_settings_dns_desc\": \"Вам будет нужно настроить свои устройства или роутер на использование DNS-сервера на одном из следующих адресов:\",\n  \"install_settings_interface_link\": \"Ваш веб-интерфейс администрирования AdGuard Home будет доступен по следующим адресам:\",\n  \"install_settings_listen\": \"Сетевой интерфейс\",\n  \"install_settings_port\": \"Порт\",\n  \"install_settings_title\": \"Веб-интерфейс администрирования\",\n  \"install_static_configure\": \"Мы обнаружили использование динамического IP-адреса — <0>{{ip}}</0>. Хотите использовать его в качестве статического адреса?\",\n  \"install_static_error\": \"AdGuard Home не может автоматически настроить его для этого сетевого интерфейса. Пожалуйста, посмотрите инструкцию о том, как это сделать вручную.\",\n  \"install_static_ok\": \"Хорошие новости! Ваш статический IP-адрес уже настроен\",\n  \"install_step\": \"Шаг\",\n  \"install_submit_desc\": \"Настройка завершена, AdGuard Home готов к использованию.\",\n  \"install_submit_title\": \"Поздравляем!\",\n  \"install_welcome_desc\": \"AdGuard Home – это DNS-сервер, блокирующий рекламу и трекинг. Его цель – дать вам возможность контролировать всю вашу сеть и все подключённые устройства. Он не требует установки клиентских программ.\",\n  \"install_welcome_title\": \"Добро пожаловать в AdGuard Home!\",\n  \"interval_24_hour\": \"24 часа\",\n  \"interval_6_hour\": \"6 часов\",\n  \"interval_days\": \"{{count}} день\",\n  \"interval_days_plural\": \"{{count}} дней\",\n  \"interval_hours\": \"{{count}} час\",\n  \"interval_hours_plural\": \"{{count}} часов\",\n  \"ip\": \"IP-адрес\",\n  \"ip_address\": \"IP-адрес\",\n  \"known_tracker\": \"Известный трекер\",\n  \"last_rule_in_allowlist\": \"Нельзя заблокировать этого клиента, так как исключение правила «{{disallowed_rule}}» ОТКЛЮЧИТ режим белого списка.\",\n  \"last_time_updated_table_header\": \"Последнее обновление\",\n  \"list_confirm_delete\": \"Вы уверены, что хотите удалить этот список?\",\n  \"list_label\": \"Список\",\n  \"list_updated\": \"Обновлён {{count}} список\",\n  \"list_updated_plural\": \"Обновлено списков: {{count}}\",\n  \"list_url_table_header\": \"URL-адрес списка\",\n  \"load_balancing\": \"Распределение нагрузки\\n\",\n  \"load_balancing_desc\": \"Запрашивать по одному upstream-серверу.<br/>AdGuard Home использует алгоритм случайной выборки с учётом веса для выбора серверов с наименьшим количеством неудачных запросов и наименьшим средним временем выполнения запроса.\",\n  \"loading_table_status\": \"Загрузка…\",\n  \"local_ptr_default_resolver\": \"По умолчанию AdGuard Home использует следующие обратные DNS-резолверы: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS-серверы, которые AdGuard Home использует для локальных PTR, SOA и NS-запросов. Запрос считается локальным, если он запрашивает информацию об ARPA-домене, подсеть которого в локальном IP-диапазоне (например, «192.168.12.34»), и если при этом запрос пришел от клиента с локальным адресом. Если значение не установлено, AdGuard Home использует адреса DNS-серверы по умолчанию в вашей ОС, за исключением адресов самого AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home не смог определить подходящие приватные обратные DNS-резолверы для этой системы.\",\n  \"local_ptr_placeholder\": \"Введите по одному адресу на строчку\",\n  \"local_ptr_title\": \"Приватные серверы для обратного DNS\",\n  \"location\": \"Местоположение\",\n  \"log_and_stats_section_label\": \"Журнал запросов и статистика\",\n  \"lower_range_start_error\": \"Должно быть меньше начала диапазона\",\n  \"main_settings\": \"Основные настройки\",\n  \"make_static\": \"Сделать статической\",\n  \"manual_update\": \"Пожалуйста, <a>следуйте инструкции</a> для обновления вручную.\",\n  \"milliseconds_abbreviation\": \"мс\",\n  \"monday\": \"Понедельник\",\n  \"monday_short\": \"Пн\",\n  \"name\": \"Имя\",\n  \"name_table_header\": \"Имя\",\n  \"netname\": \"Название сети\",\n  \"network\": \"Сеть\",\n  \"new_allowlist\": \"Новый белый список\",\n  \"new_blocklist\": \"Новый чёрный список\",\n  \"next\": \"Далее\",\n  \"next_btn\": \"Далее\",\n  \"no_blocklist_added\": \"Чёрные списки не добавлены\",\n  \"no_clients_found\": \"Клиентов не найдено\",\n  \"no_domains_found\": \"Домены не найдены\",\n  \"no_logs_found\": \"Логи не найдены\",\n  \"no_servers_specified\": \"Нет указанных серверов\",\n  \"no_upstreams_data_found\": \"Нет данных об upstream-серверах\",\n  \"no_whitelist_added\": \"Белые списки не добавлены\",\n  \"nothing_found\": \"Ничего не найдено\",\n  \"null_ip\": \"Нулевой IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Количество DNS-запросов, заблокированных фильтрами и блок-списками\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Количество заблокированных «сайтов для взрослых»\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard\",\n  \"number_of_dns_query_days\": \"Количество DNS-запросов за последний {{count}} день\",\n  \"number_of_dns_query_days_plural\": \"Количество DNS запросов, обработанных за последние {{count}} дней\",\n  \"number_of_dns_query_hours\": \"Количество DNS-запросов, обработанных за последний {{count}} час\",\n  \"number_of_dns_query_hours_plural\": \"Количество DNS-запросов, обработанных за последние {{count}} часов\",\n  \"number_of_dns_query_to_safe_search\": \"Количество запросов DNS для поисковых систем, для которых был применён Безопасный поиск\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"Выкл\",\n  \"on\": \"Вкл\",\n  \"open_dashboard\": \"Открыть Панель управления\",\n  \"orgname\": \"Название организации\",\n  \"original_response\": \"Первоначальный ответ\",\n  \"out_of_range_error\": \"Должно быть вне диапазона «{{start}}»-«{{end}}»\",\n  \"page_table_footer_text\": \"Страница\",\n  \"parallel_requests\": \"Параллельные запросы\",\n  \"parental_control\": \"Родительский контроль\",\n  \"password_label\": \"Пароль\",\n  \"password_placeholder\": \"Введите пароль\",\n  \"plain_dns\": \"Нешифрованный DNS\",\n  \"port_53_faq_link\": \"Порт 53 часто занят службами «DNSStubListener» или «systemd-resolved». Ознакомьтесь с <0>инструкцией</0> о том, как это разрешить.\",\n  \"previous_btn\": \"Назад\",\n  \"privacy_policy\": \"Политика конфиденциальности\",\n  \"processing_update\": \"Пожалуйста, подождите, AdGuard Home обновляется\",\n  \"protection_section_label\": \"Защита\",\n  \"protocol\": \"Протокол\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Журнал\",\n  \"query_log_clear\": \"Очистить журнал запросов\",\n  \"query_log_cleared\": \"Журнал запросов успешно очищен\",\n  \"query_log_configuration\": \"Настройка журнала\",\n  \"query_log_confirm_clear\": \"Вы уверены, что хотите очистить весь журнал запросов?\",\n  \"query_log_disabled\": \"Журнал запросов выключен, его можно включить в <0>настройках</0>\",\n  \"query_log_enable\": \"Включить журнал\",\n  \"query_log_filtered\": \"Отфильтровано с помощью {{filter}}\",\n  \"query_log_response_status\": \"Статус: {{value}}\",\n  \"query_log_retention\": \"Частота ротации журнала запросов\",\n  \"query_log_retention_confirm\": \"Вы уверены, что хотите изменить частоту ротации журнала запросов? При сокращении срока данные могут быть утеряны\",\n  \"query_log_strict_search\": \"Используйте двойные кавычки для строгого поиска\",\n  \"query_log_updated\": \"Журнал запросов успешно обновлён\",\n  \"rate_limit\": \"Rate limit\",\n  \"rate_limit_desc\": \"Ограничение на количество запросов в секунду для каждого клиента (0 — неограниченно).\",\n  \"rate_limit_subnet_len_ipv4\": \"Длина префикса подсети для IPv4-адресов\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Длина префикса подсети для IPv4-адресов, используемых для ограничения скорости. По умолчанию 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Длина префикса IPv4-подсетей должна составлять от 0 до 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Длина префикса подсети для IPv6-адресов\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Длина префикса подсети для IPv6-адресов, используемых для ограничения скорости. По умолчанию 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Длина префикса IPv6-подсетей должна составлять от 0 до 128\",\n  \"rate_limit_whitelist\": \"Белый список ограничения скорости\",\n  \"rate_limit_whitelist_desc\": \"IP-адреса, на которые не распространяется ограничение скорости\",\n  \"rate_limit_whitelist_placeholder\": \"Введите по одному адресу на строчку\",\n  \"refresh_btn\": \"Обновить\",\n  \"refresh_statics\": \"Обновить статистику\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Сообщить о проблеме\",\n  \"request_details\": \"Детали запроса\",\n  \"request_table_header\": \"Запрос\",\n  \"requests_count\": \"Количество запросов\",\n  \"reset_settings\": \"Сбросить настройки\",\n  \"resolve_clients_desc\": \"Определять доменные имена клиентов через PTR-запросы к соответствующим серверам (приватные DNS-серверы для локальных клиентов, upstream-серверы для клиентов с публичным IP-адресом).\",\n  \"resolve_clients_title\": \"Включить запрашивание доменных имён для IP-адресов клиентов\",\n  \"response_code\": \"Код ответа\",\n  \"response_details\": \"Детали ответа\",\n  \"response_table_header\": \"Ответ\",\n  \"response_time\": \"Время ответа\",\n  \"rewrite_A\": \"<0>A</0>: специальное значение, хранить записи <0>A</0> с upstream-сервера\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: специальное значение, хранить записи <0>AAAA</0> с upstream-сервера\",\n  \"rewrite_add\": \"Добавить правило перезаписи DNS-запросов\",\n  \"rewrite_added\": \"Правило перезаписи DNS-запросов для «{{key}}» успешно добавлено\",\n  \"rewrite_applied\": \"Применено правило перезаписи\",\n  \"rewrite_confirm_delete\": \"Вы уверены, что хотите удалить правило перезаписи DNS-запросов для «{{key}}»?\",\n  \"rewrite_deleted\": \"Правило перезаписи DNS-запросов для «{{key}}» успешно удалено\",\n  \"rewrite_desc\": \"Позволяет легко настроить пользовательский DNS-ответ для определеннного домена.\",\n  \"rewrite_domain_name\": \"Доменное имя: добавить запись CNAME\",\n  \"rewrite_edit\": \"Редактировать правило перезаписи DNS-запросов\",\n  \"rewrite_hosts_applied\": \"Переписано по правилу файла hosts\",\n  \"rewrite_ip_address\": \"IP-адрес: использовать этот IP для А или АААА ответов\",\n  \"rewrite_not_found\": \"Не найдено правил перезаписи DNS-запросов\",\n  \"rewrite_settings_updated\": \"Настройки перезаписи DNS-запросов успешно обновлены\",\n  \"rewrite_updated\": \"Правило перезаписи DNS-запросов успешно обновлено\",\n  \"rewrites_disabled_table_header\": \"Перезапись отключена\",\n  \"rewrites_enabled_table_header\": \"Перезапись включена\",\n  \"rewritten\": \"Перезаписан\",\n  \"rows_table_footer_text\": \"строк\",\n  \"rule_added_to_custom_filtering_toast\": \"Пользовательское правило добавлено: {{rule}}\",\n  \"rule_label\": \"Правило(-а)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Пользовательское правило удалено: {{rule}}\",\n  \"rules_count_table_header\": \"Количество правил:\",\n  \"safe_browsing\": \"Безопасный интернет\",\n  \"safe_search\": \"Безопасный поиск\",\n  \"saturday\": \"Суббота\",\n  \"saturday_short\": \"Сб\",\n  \"save_btn\": \"Сохранить\",\n  \"save_config\": \"Сохранить конфигурацию\",\n  \"schedule_add\": \"Добавить расписание\",\n  \"schedule_current_timezone\": \"Текущий часовой пояс: {{value}}\",\n  \"schedule_desc\": \"Установка периодов паузы блокировки сервисов\",\n  \"schedule_edit\": \"Изменить расписание\",\n  \"schedule_from\": \"С\",\n  \"schedule_invalid_select\": \"Время начала должно быть до времени окончания\",\n  \"schedule_modal_description\": \"Это расписание заменит все существующие расписания для того же дня недели. Каждый день недели может иметь только один период паузы блокировки.\",\n  \"schedule_modal_time_off\": \"Блокировка сервисов отключена:\",\n  \"schedule_new\": \"Новое расписание\",\n  \"schedule_remove\": \"Удалить расписание\",\n  \"schedule_save\": \"Сохранить расписание\",\n  \"schedule_select_days\": \"Выбрать дни\",\n  \"schedule_services\": \"Пауза блокировки сервисов\",\n  \"schedule_services_desc\": \"Настройка расписания паузы фильтра блокировки сервисов\",\n  \"schedule_services_desc_client\": \"Настройка расписания паузы фильтра блокировки сервисов для данного клиента\",\n  \"schedule_time_all_day\": \"Весь день\",\n  \"schedule_timezone\": \"Выберите часовой пояс\",\n  \"schedule_to\": \"До\",\n  \"served_from_cache_label\": \"Получено из кеша\",\n  \"service_name\": \"Имя сервиса\",\n  \"set_static_ip\": \"Установить статический IP-адрес\",\n  \"settings\": \"Настройки\",\n  \"settings_custom\": \"Свои\",\n  \"settings_global\": \"Глобальные\",\n  \"setup_config_to_enable_dhcp_server\": \"Настроить конфигурацию для включения DHCP-сервера\",\n  \"setup_dns_notice\": \"Чтобы использовать <1>DNS-over-HTTPS</1> или <1>DNS-over-TLS</1>, вам нужно <0>настроить шифрование</0> в настройках AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Используйте строку <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Используйте строку <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Вот список ПО, которое вы можете использовать.</0>\",\n  \"setup_dns_privacy_4\": \"На устройствах с iOS 14 и macOS Big Sur вы можете скачать специальный файл '.mobileconfig', который добавляет <highlight>DNS-over-HTTPS</highlight> или <highlight>DNS-over-TLS</highlight> серверы в настройки DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 нативно поддерживает DNS-over-TLS. Для настройки, перейдите в Настройки → Сеть и Интернет → Дополнительно → Персональный DNS сервер, и введите туда ваше доменное имя.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard для Android</0> поддерживает <1>DNS-over-HTTPS</1> и <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> добавляет поддержка <1>DNS-over-HTTPS</1> на Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Конфигурация для iOS и macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> поддерживает <1>DNS-over-HTTPS</1>, но для настройки его, вам будет нужно сгенерировать для него <2>DNS-отпечаток</2>.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard для iOS</0> поддерживает <1>DNS-over-HTTPS</1> и <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home сам может быть клиентом зашифрованного DNS на любой платформе.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> поддерживает все известные зашифрованные DNS-протоколы.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> поддерживает <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> поддерживает <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Вы можете найти ещё варианты <0>тут</0> и <1>тут</1>.\",\n  \"setup_dns_privacy_other_title\": \"Другие решения\",\n  \"setup_guide\": \"Инструкция по настройке\",\n  \"show_all_filter_type\": \"Показать все\",\n  \"show_blocked_responses\": \"Заблокирован\",\n  \"show_filtered_type\": \"Показать отфильтрованные\",\n  \"show_processed_responses\": \"Обработан\",\n  \"show_whitelisted_responses\": \"Разрешённые\",\n  \"sign_in\": \"Войти\",\n  \"sign_out\": \"Выйти\",\n  \"source_label\": \"Источник\",\n  \"static_ip\": \"Статический IP-адрес\",\n  \"static_ip_desc\": \"AdGuard Home является сервером, поэтому для корректной работы ему необходим статический IP-адрес. В противном случае, в какой-то момент ваш роутер может присвоить этому устройству другой IP-адрес.\",\n  \"statistics_clear\": \"Очистить статистику\",\n  \"statistics_clear_confirm\": \"Вы уверены, что хотите очистить статистику?\",\n  \"statistics_cleared\": \"Статистика успешно очищена\",\n  \"statistics_configuration\": \"Конфигурация статистики\",\n  \"statistics_enable\": \"Включить статистику\",\n  \"statistics_retention\": \"Сохранение статистики\",\n  \"statistics_retention_confirm\": \"Вы уверены, что хотите изменить срок хранения статистики? При сокращении интервала данные могут быть утеряны\",\n  \"statistics_retention_desc\": \"Если вы уменьшите значение интервала, некоторые данные могут быть потеряны\",\n  \"stats_adult\": \"Заблокированные «взрослые» сайты\",\n  \"stats_disabled\": \"Статистика отключена. Вы можете включить её на <0>странице настроек</0>.\",\n  \"stats_disabled_short\": \"Статистика отключена\",\n  \"stats_malware_phishing\": \"Заблокированные вредоносные и фишинговые сайты\",\n  \"stats_params\": \"Конфигурация статистики\",\n  \"stats_query_domain\": \"Часто запрашиваемые домены\",\n  \"subnet_error\": \"Адреса должны быть внутри одной подсети\",\n  \"sunday\": \"Воскресенье\",\n  \"sunday_short\": \"Вс\",\n  \"system_host_files\": \"Системные hosts-файлы\",\n  \"table_client\": \"Клиент\",\n  \"table_name\": \"Имя\",\n  \"tags_desc\": \"Вы можете выбрать теги, которые соответствуют клиенту. Теги могут быть включены в правила фильтрации, чтобы применять их более точно. <0>Узнать больше</0>.\",\n  \"tags_title\": \"Теги\",\n  \"test_upstream_btn\": \"Тест upstream серверов\",\n  \"theme_auto\": \"Авто\",\n  \"theme_auto_desc\": \"Авто (на основе цветовой схемы вашего устройства)\",\n  \"theme_dark\": \"Тёмная\",\n  \"theme_dark_desc\": \"Тёмная тема\",\n  \"theme_light\": \"Светлая\",\n  \"theme_light_desc\": \"Светлая тема\",\n  \"thursday\": \"Четверг\",\n  \"thursday_short\": \"Чт\",\n  \"time_table_header\": \"Время\",\n  \"top_blocked_domains\": \"Часто блокируемые домены\",\n  \"top_clients\": \"Частые клиенты\",\n  \"top_upstreams\": \"Часто запрашиваемые upstream-серверы\",\n  \"topline_expired_certificate\": \"Ваш SSL-сертификат истёк. Обновите <0>Настройки шифрования</0>.\",\n  \"topline_expiring_certificate\": \"Ваш SSL-сертификат скоро истекает. Обновите <0>Настройки шифрования</0>.\",\n  \"tracker_source\": \"Источник трекинга\",\n  \"try_again\": \"Попробовать ещё раз\",\n  \"ttl_cache_validation\": \"Значение для переопределения минимального TTL должно быть меньше или равно значению для переопределения максимального\",\n  \"tuesday\": \"Вторник\",\n  \"tuesday_short\": \"Вт\",\n  \"type_table_header\": \"Тип\",\n  \"unavailable_dhcp\": \"DHCP недоступен\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home не может запустить DHCP-сервер на вашей ОС\",\n  \"unblock\": \"Разблокировать\",\n  \"unblock_all\": \"Разблокировать все\",\n  \"unblock_for_this_client_only\": \"Разблокировать только для этого клиента\",\n  \"unknown_filter\": \"Неизвестный фильтр {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} уже доступна! <0>Нажмите сюда</0>, чтобы узнать больше.\",\n  \"update_failed\": \"Ошибка авто-обновления. Пожалуйста, <a>следуйте инструкции</a> для обновления вручную.\",\n  \"update_now\": \"Обновить сейчас\",\n  \"updated_custom_filtering_toast\": \"Пользовательские правила успешно сохранены\",\n  \"updated_save_search_toast\": \"Настройки безопасного поиска обновлены\",\n  \"updated_upstream_dns_toast\": \"DNS-серверы успешно обновлены\",\n  \"updates_checked\": \"Доступна новая версия AdGuard Home\",\n  \"updates_version_equal\": \"Версия AdGuard Home актуальна\",\n  \"upstream\": \"Upstream-сервер\",\n  \"upstream_dns\": \"Upstream DNS-серверы\",\n  \"upstream_dns_cache_configuration\": \"Конфигурация кеша upstream DNS-серверов\",\n  \"upstream_dns_client_desc\": \"Если оставить поле пустым, AdGuard Home будет обращаться к серверам, указанным в <0>настройках DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Настроен в {{path}}\",\n  \"upstream_dns_help\": \"Введите адреса серверов по одному на строке. <a>Узнать больше</a> о настройке upstream DNS-серверов.\",\n  \"upstream_parallel\": \"Использовать параллельные запросы ко всем серверам одновременно для ускорения обработки запроса.\",\n  \"upstream_timeout\": \"Время ожидания ответов от upstream-серверов\",\n  \"upstream_timeout_desc\": \"Длительность ожидания ответа от upstream-серверов в секундах\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Включить Безопасную навигацию AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home проверит, включён ли домен в веб-службу безопасности браузера. Он будет использовать API, чтобы выполнить проверку: на сервер отправляется только короткий префикс имени домена SHA256.\",\n  \"use_adguard_parental\": \"Включить модуль Родительского контроля AdGuard \",\n  \"use_adguard_parental_hint\": \"AdGuard Home проверит, содержит ли домен материалы 18+. Он использует тот же API для обеспечения конфиденциальности, что и веб-служба безопасности браузера.\",\n  \"use_private_ptr_resolvers_desc\": \"Посылать PTR, SOA и NS-запросы для ARPA-доменов, содержащих локальные адреса, с помощью указанных upstream-серверов, DHCP, /etc/hosts и так далее. Если отключено, AdGuard Home отвечает NXDOMAIN на все подобные запросы.\",\n  \"use_private_ptr_resolvers_title\": \"Использовать приватные обратные DNS-резолверы\",\n  \"use_saved_key\": \"Использовать сохранённый ранее ключ\",\n  \"username_label\": \"Имя пользователя\",\n  \"username_placeholder\": \"Введите имя пользователя\",\n  \"validated_with_dnssec\": \"Подтверждено с помощью DNSSEC\",\n  \"version\": \"версия\",\n  \"version_request_error\": \"Ошибка при проверке наличия обновлений. Проверьте ваше интернет-соединение.\",\n  \"wednesday\": \"Среда\",\n  \"wednesday_short\": \"Ср\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/si-lk.json",
    "content": "{\n  \"access_allowed_desc\": \"අන.ජා.(CIDR), අ.ජා.කෙ. ලිපින හෝ <a>අනුග්‍රාහක හැඳු.</a> ලේඛනයකි. මෙහි නිවේශිත ඇත්නම්, ඇඩ්ගාර්ඩ් හෝම් එම අනුග්‍රාහක වලින් පමණක් ඉල්ලීම් පිළිගනු ඇත.\",\n  \"access_allowed_title\": \"ඉඩ දෙන අනුග්‍රාහක\",\n  \"access_blocked_desc\": \"මෙය පෙරලුවලට වඩා නොවේ. AdGuard Home, මෙම වසමට සමාන DNS විමසුම් වලින් තොරව ඉවත් කරයි, සහ මෙම විමසුම් විමසුම් ලොග් එකේද එක් නොවේ. ඔබ නිශ්චිත ආකාරයට වසමන්, වර්ජ්ජ නීති, හෝ URL විමසුම් නීති සංඛ්‍යාගත කිරීමට කැමැත්ත දක්වන ලෙසද ඔබට හැක.\",\n  \"access_blocked_title\": \"ඉඩ නොදෙන වසම්\",\n  \"access_desc\": \"මෙහි දී ඔබට ඇඩ්ගාර්ඩ් හෝම් ව.නා.ප. සේවාදායකයට ප්‍රවේශ වී‌‌‌‌මේ නීති වින්‍යාසගත කිරීමට හැකිය\",\n  \"access_disallowed_desc\": \"අන.ජා.(CIDR), අ.ජා.කෙ. ලිපින හෝ <a>අනුග්‍රාහක හැඳු.</a> ලේඛනයකි. මෙහි නිවේශිත ඇත්නම්, ඇඩ්ගාර්ඩ් හෝම් එම අනුග්‍රාහක වලින් ඉල්ලීම් අත්හරිනු ඇත. ඉඩ දෙන අනුග්‍රාහක කොටසේ නිවේශිත තිබේ නම්, මෙම ක්‍ෂේත්‍රය නොසලකා හරිනු ඇත.\",\n  \"access_disallowed_title\": \"ඉඩ නොදෙන අනුග්‍රාහක\",\n  \"access_settings_saved\": \"ප්‍රවේශ වීමේ සැකසුම් සාර්ථකව සුරකින ලදි\",\n  \"access_title\": \"ප්‍රවේශ සැකසුම්\",\n  \"actions_table_header\": \"ක්‍රියාමාර්ග\",\n  \"add_allowlist\": \"ඉඩ දීමේ ලැයිස්තුවක් එකතු කරන්න\",\n  \"add_blocklist\": \"අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුවක් එකතු කරන්න\",\n  \"add_custom_list\": \"අභිරුචි ලැයිස්තුවක්\",\n  \"add_persistent_client\": \"නිබඳිත පාරිභෝගිකයා ලෙස එකතු කරන්න\",\n  \"address\": \"ලිපිනය\",\n  \"adg_will_drop_dns_queries\": \"ඇඩ්ගාර්ඩ් හෝම් මෙම අනුග්‍රාහකයේ සියළුම ව.නා.ප. විමසුම් අතහැර දමනු ඇත.\",\n  \"all_lists_up_to_date_toast\": \"සියළුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි\",\n  \"all_queries\": \"සියළුම විමසුම්\",\n  \"allow_this_client\": \"මෙම අනුග්‍රාහකයට ඉඩ දෙන්න\",\n  \"allowed\": \"ඉඩ දී ඇත\",\n  \"anonymize_client_ip\": \"අනුග්‍රාහකයෙහි අ.ජා.කෙ. (IP) නිර්නාමික කරන්න\",\n  \"anonymize_client_ip_desc\": \"සටහන් සහ සංඛ්‍යාලේඛන තුළ අනුග්‍රාහකයේ පූර්ණ අ.ජා.කෙ. ලිපිනය සුරකින්න එපා\",\n  \"anonymizer_notification\": \"<0>සටහන:</0> අ.ජා.කෙ. නිර්නාමිකකරණය සබලයි. ඔබට එය <1>පොදු සැකසුම්</1> හරහා අබල කිරීමට හැකිය .\",\n  \"answer\": \"උත්තරය\",\n  \"apply_btn\": \"යොදන්න\",\n  \"auto_clients_desc\": \"ඇඩ්ගාර්ඩ් හෝම් භාවිතා කරන හෝ භාවිතයට ඉඩ තිබෙන උපාංගවල අ.ජා.කෙ. (IP) ලිපින පිළිබඳ තොරතුරු. මෙම තොරතුරු සත්කාරක ගොනු, ප්‍රතිවර්ත ව.නා.ප. ආදී මූලාශ්‍ර කිහිපයකින් රැස් කෙරේ.\",\n  \"auto_clients_title\": \"ධාවනකාල අනුග්‍රාහක\",\n  \"autofix_warning_list\": \"මෙම කාර්යාල කරනු ඇත: <0>සැ ව.නා.ප. ඡේදකය නශ්කරන්න</0> <0>ව.නා.ප. සේවාදායක ලිපිනය 127.0.0.1 සඳහා සකසන්න.</0> <0>/etc/resolv.conf යන ගැඹුරු සම්පූර්ණය මාරු කරන්න /run/systemd/resolve/resolv.conf සඳහා </0> <0>සැ වෙ.නා.ප. ඡේදකය නශ්කරන්න (systemd-resolved සේවාව නැවත පුරෙන්න)</0>\",\n  \"autofix_warning_result\": \"ප්‍රතිඵලයක් ලෙස ඔබගේ පද්ධතියෙන් ලැබෙන සියළුම ව.නා.ප. ඉල්ලීම් මූලිකවම ඇඩ්ගාර්ඩ් හෝම් විසින් සකසනු ඇත.\",\n  \"autofix_warning_text\": \"ඔබ \\\"නිරාකරණය\\\" යන්න එබුවහොත්, ඔබගේ පද්ධතිය ඇඩ්ගාර්ඩ් හෝම් ව.නා.ප. සේවාදායකය භාවිතයට වින්‍යාසගත කෙරෙනු ඇත.\",\n  \"average_processing_time\": \"සාමාන්‍ය සැකසුම් කාලය\",\n  \"average_processing_time_hint\": \"ව.නා.ප. ඉල්ලීමක් සැකසීමේ සාමාන්‍ය කාලය මිලි තත්පර වලින්\",\n  \"average_upstream_response_time\": \"උඩුගත කරන සේවාදායකයේ සාමාන්‍ය පිළිතුරු කාලය\",\n  \"back\": \"ආපසු\",\n  \"block\": \"අවහිර\",\n  \"block_all\": \"සියල්ල අවහිර\",\n  \"block_domain_use_filters_and_hosts\": \"පෙරහන් හා සත්කාරක ගොනු භාවිතයෙන් වසම් අවහිර කරන්න\",\n  \"block_for_this_client_only\": \"මෙම අනුග්‍රාහකයට අවහිර කරන්න\",\n  \"block_services\": \"නිශ්චිත සේවා අවහිර කරන්න\",\n  \"blocked_adult_websites\": \"දෙමාපිය පාලනය මගින් අවහිර කළ\",\n  \"blocked_by\": \"<0>පෙරහන් මගින් අවහිර කළ</0>\",\n  \"blocked_by_cname_or_ip\": \"අන්. නාමයක් (CNAME) හෝ අ.ජා.කෙ. මගින් අවහිර කර ඇත\",\n  \"blocked_by_response\": \"ප්‍රතිචාරය අන්. නාමයක් (CNAME) හෝ අ.ජා.කෙ. මගින් අවහිර කර ඇත\",\n  \"blocked_response_ttl\": \"ප්‍රතිචාරය මගින් අවහිර කර ඇති TTL\",\n  \"blocked_response_ttl_desc\": \"සු filtr ම කළ ලිපිනයක් මහිහි කළයක් සකස් කරයිාරුවන්\",\n  \"blocked_safebrowsing\": \"ආරක්‍ෂිත පිරික්සුම මගින් අවහිර කළ\",\n  \"blocked_service\": \"අවහිර කළ සේවාව\",\n  \"blocked_services\": \"අවහිර කළ සේවා\",\n  \"blocked_services_desc\": \"ජනප්‍රිය අඩවි සහ සේවා ඉක්මනින් අවහිර කිරීමට ඉඩ දෙයි.\",\n  \"blocked_services_global\": \"ගෝලීය අවහිර කළ සේවා භාවිතා කරන්න\",\n  \"blocked_services_saved\": \"අවහිර කළ සේවා සාර්ථකව සුරකින ලදි\",\n  \"blocked_threats\": \"අවහිර කළ තර්ජන\",\n  \"blocking_ipv4\": \"IPv4 අවහිර කිරීම\",\n  \"blocking_ipv4_desc\": \"අවහිර කළ A ඉල්ලීමක් සඳහා ආපසු එවිය යුතු අ.ජා.කෙ. (IP) ලිපිනය\",\n  \"blocking_ipv6\": \"IPv6 අවහිර කිරීම\",\n  \"blocking_ipv6_desc\": \"අවහිර කළ AAAA ඉල්ලීමක් සඳහා ආපසු එවිය යුතු අ.ජා.කෙ. (IP) ලිපිනය\",\n  \"blocking_mode\": \"අවහිර කරන ආකාරය\",\n  \"blocking_mode_custom_ip\": \"අභිරුචි අන්තර්ජාල කෙටුම්පත: අතින් සැකසූ අ.ජා. කෙ. ලිපිනයක් සමඟ ප්‍රතිචාර දක්වයි\",\n  \"blocking_mode_default\": \"පොදු: දැන්වීම් අවහිර කරන ආකාරයේ නීතියක් මගින් අවහිර කළ විට REFUSED සමඟ ප්‍රතිචාර දක්වයි; /etc/host-style ආකාරයේ නීතියක් මගින් අවහිර කළ විට නීතියේ දක්වා ඇති අ.ජා.කෙ. ලිපිනය සමඟ ප්‍රතිචාර දක්වයි\",\n  \"blocking_mode_null_ip\": \"අභිශූන්‍යය අ.ජා.කෙ.: ශුන්‍ය අ.ජා.කෙ. ලිපිනය සමඟ ප්‍රතිචාර දක්වයි (A සඳහා 0.0.0.0; AAAA සඳහා ::)\",\n  \"blocking_mode_nxdomain\": \"නොපවතින වසම: NXDOMAIN කේතය සමඟ ප්‍රතිචාර දක්වයි\",\n  \"blocking_mode_refused\": \"REFUSED: REFUSED කේතය සමඟ ප්‍රතිචාර දක්වයි\",\n  \"blocklist\": \"අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව\",\n  \"bootstrap_dns\": \"Bootstrap DNS සේවාදායකය\",\n  \"bootstrap_dns_desc\": \"DNS සේවාදායකයේ IP ලිපින, ඔබ උඩුගත සේවා මෙන්ම DoH/DoT විසඳුම් IP ලිපින නිර්දන් කරයි. মন্তব্য සිද්ධ නොවේ.\",\n  \"cache_cleared\": \"ව.නා.ප. නිහිතය හිස් කෙරිණි\",\n  \"cache_optimistic\": \"සර්වශුභවාදී නිහිතගතය\",\n  \"cache_optimistic_desc\": \"නිවේශිත කල් ඉකුත් වූ විට පවා ඇඩ්ගාර්ඩ් හෝම් නිහිතයෙන් ප්‍රතිචාර දැක්වීමට සලස්වයි එමෙන්ම ඒවා නැවත නැවුම් කිරීමට ද උත්සාහ කරයි.\",\n  \"cache_size\": \"නිහිතයෙහි ප්‍රමාණය\",\n  \"cache_size_desc\": \"ව.නා.ප. නිහිතයේ ප්‍රමාණය (බයිට). නිහිතය අබල කිරීමට, 0 ලෙස තබන්න.\",\n  \"cache_ttl_max_override\": \"උපරිම පව. කා. අභිබවන්න\",\n  \"cache_ttl_max_override_desc\": \"ව.නා.ප. නිහිතයෙහි නිවේශිත සඳහා උපරිම පවත්නා කාලයක අගයක් (තත්.) සකසන්න.\",\n  \"cache_ttl_min_override\": \"අවම පව. කා. අභිබවන්න\",\n  \"cache_ttl_min_override_desc\": \"DNS ප්‍රතිචාර කේෂ කිරීමේදී උඩුගත සේවාදායකයෙන් ලබා ගන්නා කෙටි පවත්නා කාලය (තත්පර) වටිනාකම් දිගු කරන්න.\",\n  \"cancel_btn\": \"අවලංගු\",\n  \"category_label\": \"ප්‍රවර්ගය\",\n  \"check\": \"පරීක්‍ෂාව\",\n  \"check_client_id\": \"අනුග්‍රාහකයේ හඳුන්වනය (හැඳු. හෝ අ.ජා.කෙ. ලිපිනය)\",\n  \"check_cname\": \"අන්. නාමය (CNAME): {{cname}}\",\n  \"check_desc\": \"සත්කාරක නාමය පෙරෙනවා දැයි පරීක්‍ෂා කරන්න.\",\n  \"check_dhcp_servers\": \"ග.ධා.වි.කෙ. සේවාදායක පරීක්‍ෂා කරන්න\",\n  \"check_dns_record\": \"ව.නා.ප. වාර්තා වර්ගය තෝරන්න\",\n  \"check_enter_client_id\": \"අනුග්‍රාහකයේ හඳුන්වනය ලියන්න\",\n  \"check_hostname\": \"සත්කාරක හෝ වසම් නාමය\",\n  \"check_ip\": \"අ.ජා.කෙ. ලිපින: {{ip}}\",\n  \"check_not_found\": \"ඔබගේ පෙරහන් ලැයිස්තු තුළ නැත\",\n  \"check_reason\": \"හේතුව: {{reason}}\",\n  \"check_service\": \"සේවාවෙහි නම: {{service}}\",\n  \"check_title\": \"පෙරීම පරීක්‍ෂා කරන්න\",\n  \"check_updates_btn\": \"යාවත්කාල පරීක්‍ෂා කරන්න\",\n  \"check_updates_now\": \"දැන් යාවත්කාල පරීක්‍ෂා කරන්න\",\n  \"choose_allowlist\": \"ඉඩ දීමේ ලැයිස්තු තෝරන්න\",\n  \"choose_blocklist\": \"අවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න\",\n  \"choose_from_list\": \"ලැයිස්තුවෙන් තෝරන්න\",\n  \"city\": \"නගරය\",\n  \"clear_cache\": \"නිහිතය මකන්න\",\n  \"click_to_view_queries\": \"විමසුම් බැලීමට ඔබන්න\",\n  \"client_add\": \"අනුග්‍රාහකයක් එකතු කරන්න\",\n  \"client_added\": \"\\\"{{key}}\\\" අනුග්‍රාහකය සාර්ථකව එකතු කෙරිණි\",\n  \"client_blocked\": \"අනුග්‍රාහකය \\\"{{ip}}\\\" සාර්ථකව අවහිර කෙරිණි\",\n  \"client_confirm_block\": \"{{ip}} අනුග්‍රාහකය අවහිර කිරීමට ඇවැසි බව ඔබට විශ්වාසද?\",\n  \"client_confirm_delete\": \"\\\"{{key}}\\\" අනුග්‍රාහකය ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?\",\n  \"client_confirm_unblock\": \"{{ip}} අනුග්‍රාහකය අනවහිර කිරීමට ඇවැසි බව ඔබට විශ්වාසද?\",\n  \"client_deleted\": \"\\\"{{key}}\\\" අනුග්‍රාහකය සාර්ථකව ඉවත් කෙරිණි\",\n  \"client_details\": \"අනුග්‍රාහකයේ විස්තර\",\n  \"client_edit\": \"අනුග්‍රාහකය සංස්කරණය\",\n  \"client_global_settings\": \"ගෝලීය සැකසුම් භාවිතා කරන්න\",\n  \"client_id\": \"අනුග්‍රාහකයේ හැඳු.\",\n  \"client_id_desc\": \"අනුග්‍රාහක හැඳු. මගින් අනුග්‍රාහක හඳුනාගත හැකිය. කෙසේදැයි <a>මෙතැනින්</a> දැන ගන්න.\",\n  \"client_id_placeholder\": \"අනුග්‍රාහකයක හැඳු. යොදන්න\",\n  \"client_identifier\": \"හඳුන්වනය\",\n  \"client_identifier_desc\": \"අ.ජා.කෙ. (IP) ලිපින, අන.ජා. (CIDR), මා.ප්‍ර.පා. (MAC) ලිපින හෝ අනුග්‍රාහක හැඳුනුමක් (DoT/DoH/DoQ සඳහා භාවිතා කළ හැකිය) මගින් අනුග්‍රාහක හඳුනාගත හැකිය. අනුග්‍රාහක හඳුනා ගන්නේ කෙසේද යන්න පිළිබඳව <0>මෙතැනින්</0> තව දැනගන්න.\",\n  \"client_name\": \"අනුග්‍රාහකය {{id}}\",\n  \"client_new\": \"නව අනුග්‍රාහකය\",\n  \"client_settings\": \"අනුග්‍රාහකයේ සැකසුම්\",\n  \"client_table_header\": \"අනුග්‍රාහකය\",\n  \"client_unblocked\": \"අනුග්‍රාහකය \\\"{{ip}}\\\" සාර්ථකව අනවහිර කෙරිණි\",\n  \"client_updated\": \"\\\"{{key}}\\\" අනුග්‍රාහකය සාර්ථකව යාවත්කාල කෙරිණි\",\n  \"clients_desc\": \"ඇඩ්ගාර්ඩ් හෝම් වෙත සම්බන්ධිත උපාංග සඳහා නිබැඳි අනුග්‍රාහක වාර්තා වින්‍යාසගත කරන්න\",\n  \"clients_not_found\": \"අනුග්‍රාහක හමු නොවිණි\",\n  \"clients_title\": \"නිබැඳි අනුග්‍රාහක\",\n  \"compact\": \"සංක්ෂිප්ත\",\n  \"config_successfully_saved\": \"වින්‍යාසය සාර්ථකව සුරකින ලදි\",\n  \"configure\": \"වින්‍යාසගත කරන්න\",\n  \"confirm_dns_cache_clear\": \"ඔබට ව.නා.ප. නිහිතය හිස් කිරීමට වුවමනාද?\",\n  \"confirm_static_ip\": \"ඇඩ්ගාර්ඩ් හෝම් ඔබගේ ස්ථිතික අ.ජා.කෙ. (IP) ලිපිනය ලෙස {{ip}} වින්‍යාසගත කරනු ඇත. ඔබට ඉදිරියට යාමට අවශ්‍යද?\",\n  \"copyright\": \"ප්‍රකාශන හිමිකම\",\n  \"country\": \"රට\",\n  \"custom_filter_rules\": \"අභිරුචි පෙරීමේ නීති\",\n  \"custom_filter_rules_hint\": \"පේළියකට එක් නීතියක් බැගින් ඇතුල් කරන්න. ඔබට දැන්වීම් අවහිර කිරීමේ නීති හෝ සත්කාරක ගොනු පද ගැලපුම් භාවිතා කිරීමට හැකිය.\",\n  \"custom_filtering_rules\": \"අභිරුචි පෙරීමේ නීති\",\n  \"custom_ip\": \"අභිරුචි අ.ජා.කෙ.\",\n  \"custom_retention_input\": \"රඳවා ගැනීම පැය වලින්\",\n  \"custom_rotation_input\": \"රඳවා ගැනීම පැය වලින්\",\n  \"dashboard\": \"උපකරණ පුවරුව\",\n  \"date\": \"දිනය\",\n  \"default\": \"සුපුරුදු\",\n  \"delete_confirm\": \"\\\"{{key}}\\\" මකා දැමීමට අවශ්‍ය බව ඔබට විශ්වාසද?\",\n  \"delete_table_action\": \"මකන්න\",\n  \"descr\": \"සවිස්තරය\",\n  \"details\": \"විස්තර\",\n  \"dhcp_add_static_lease\": \"ස්ථිර කල්පැවරීමක් යොදන්න\",\n  \"dhcp_config_saved\": \"ග.ධා.වි.කෙ. වින්‍යාසය සාර්ථකව සුරකින ලදි\",\n  \"dhcp_description\": \"ඔබගේ මාර්ගකාරකය ග.ධා.වි.කෙ. (DHCP) සැකසුම් ලබා නොදෙන්නේ නම්, ඔබට ඇඩ්ගාර්ඩ් තිළෑලි ග.ධා.වි.කෙ. සේවාදායකය භාවිතා කිරීමට හැකිය.\",\n  \"dhcp_disable\": \"ග.ධා.වි.කෙ. සේවාදායකය අබල කරන්න\",\n  \"dhcp_dynamic_ip_found\": \"ඔබගේ පද්ධතිය <0>{{interfaceName}}</0> අතුරු මුහුණත සඳහා ගතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපින වින්‍යාසය භාවිතා කරයි. ග.ධා.වි.කෙ. සේවාදායකය භාවිතා කිරීම සඳහා ස්ථිතික අ.ජා. කෙ. ලිපිනයක් සැකසිය යුතුය. ඔබගේ වර්තමාන අ.ජා. කෙ. ලිපිනය <0>{{ipAddress}}</0> වේ. ඔබ \\\"ග.ධා.වි.කෙ. සබල කරන්න\\\" බොත්තම එබුවහොත් ඇඩ්ගාර්ඩ් හෝම් ස්වයංක්‍රීයව මෙම අ.ජා. කෙ. ලිපිනය ස්ථිතික ලෙස සකසනු ඇත.\",\n  \"dhcp_edit_static_lease\": \"ස්ථිර කල්පැවරීම සංස්කරණය කරන්න\",\n  \"dhcp_enable\": \"ග.ධා.වි.කෙ. සේවාදායකය සබල කරන්න\",\n  \"dhcp_error\": \"ජාලයේ තවත් ක්‍රියාත්මක ග.ධා.වි.කෙ. සේවාදායකයක් තිබේද යන්න නිශ්චය කළ නොහැකි විය\",\n  \"dhcp_form_gateway_input\": \"සේවා ප්‍රවර්ධක දොරටු IP අංකය\",\n  \"dhcp_form_lease_input\": \"කල්පැවරීමේ පරාසය\",\n  \"dhcp_form_lease_title\": \"ග.ධා.වි.කෙ. කල්පැවරීම (තත්. වලින්)\",\n  \"dhcp_form_range_end\": \"පරාසය අවසානය\",\n  \"dhcp_form_range_start\": \"පරාසය ආරම්භය\",\n  \"dhcp_form_range_title\": \"අ.ජා.කෙ. (IP) ලිපින පරාසය\",\n  \"dhcp_form_subnet_input\": \"උපජාල කාණ්ඩය\",\n  \"dhcp_found\": \"ක්‍රියාත්මක ග.ධා.වි.කෙ සේවාදායකයක් ජාලය තුළ හමු විය. තිළෑලි ග.ධා.වි.කෙ සේවාදායකය සබල කිරීම ආරක්‍ෂිත නොවේ.\",\n  \"dhcp_hardware_address\": \"දෘඩාංග ලිපිනය\",\n  \"dhcp_interface_select\": \"ග.ධා.වි.කෙ. අතුරුමුහුණත තෝරන්න\",\n  \"dhcp_ip_addresses\": \"අ.ජා.කෙ. (IP) ලිපින\",\n  \"dhcp_ipv4_settings\": \"ග.ධා.වි.කෙ. IPv4 සැකසුම්\",\n  \"dhcp_ipv6_settings\": \"ග.ධා.වි.කෙ. IPv6 සැකසුම්\",\n  \"dhcp_lease_added\": \"\\\"{{key}}\\\" ස්ථිර කල්පැවරීම එකතු කෙරිණි\",\n  \"dhcp_lease_deleted\": \"\\\"{{key}}\\\" ස්ථිර කල්පැවරීම මකා දැමිණි\",\n  \"dhcp_lease_updated\": \"ස්ථිර කල්පැවරීම \\\"{{key}}\\\" සාර්ථකව යාවත්කාල කර ඇත\",\n  \"dhcp_leases\": \"ග.ධා.වි.කෙ. කල්පැවරීම\",\n  \"dhcp_leases_not_found\": \"ග.ධා.වි.කෙ. කල්පැවරීම් නැත\",\n  \"dhcp_new_static_lease\": \"නව ස්ථිර කල්පැවරීම\",\n  \"dhcp_not_found\": \"ඇඩ්ගාර්ඩ් හෝම් සඳහා ජාලයෙහි කිසිදු ක්‍රියාත්මක ග.ධා.වි.කෙ. සේවාදායකයක් හමු නොවූ නිසා තිළෑලි සේවාදායකය සබල කිරීම ආරක්‍ෂිත වේ. කෙසේ වෙතත්, ස්වයංක්‍රීය ඒෂණය ඉතා නිවැරදි නොවිය හැකි බැවින් ඔබ එය අතින් නැවත පරීක්‍ෂා කළ යුතුය.\",\n  \"dhcp_reset\": \"ග.ධා.වි.කෙ. වින්‍යාසය යළි පිහිටුවීමට අවශ්‍ය බව ඔබට විශ්වාස ද?\",\n  \"dhcp_reset_leases\": \"කල්පැවරීම් යළි සකසන්න\",\n  \"dhcp_reset_leases_confirm\": \"සියළුම කල්පැවරීම් යළි සැකසීමට වුවමනා ද?\",\n  \"dhcp_reset_leases_success\": \"ග.ධා.වි.කෙ. කල්පැවරීම් යළි සැකසිණි\",\n  \"dhcp_settings\": \"ග.ධා.වි.කෙ. සැකසුම්\",\n  \"dhcp_static_ip_error\": \"ග.ධා.වි.කෙ. සේවාදායකය භාවිතා කිරීම සඳහා ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනයක් සැකසිය යුතුය. මෙම ජාල අතුරුමුහුණත ස්ථිතික අ.ජා. කෙ. ලිපිනයක් භාවිතයෙන් වින්‍යාසගත කර තිබේද යන්න තීරණය කිරීමට ඇඩ්ගාර්ඩ් හෝම් අසමත් විය. කරුණාකර ස්ථිතික අ.ජා. කෙ. ලිපිනයක් අතින් සකසන්න.\",\n  \"dhcp_static_leases\": \"ස්ථිර ග.ධා.වි.කෙ. කල්පැවරීම\",\n  \"dhcp_static_leases_not_found\": \"ග.ධා.වි.කෙ. ස්ථිර කල්පැවරීම් නැත\",\n  \"dhcp_table_expires\": \"කල් ඉකුත් වීම\",\n  \"dhcp_table_hostname\": \"සත්කාරක නාමය\",\n  \"dhcp_title\": \"ග.ධා.වි.කෙ. සේවාදායකය (පර්යේෂණාත්මක!)\",\n  \"dhcp_warning\": \"ඔබට කෙසේ හෝ ග.ධා.වි.කෙ. සේවාදායකය සබල කිරීමට අවශ්‍ය නම්, ඔබගේ ජාලයේ වෙනත් ක්‍රියාකාරී ග.ධා.වි.කෙ. සේවාදායකයක් නැති බව තහවුරු කරගන්න. මෙය සම්බන්ධිත උපාංග සඳහා අන්තර්ජාලය බිඳ දැමිය හැකිය!\",\n  \"disable_for_hours\": \"පැය {{count}} ක්\",\n  \"disable_for_hours_plural\": \"පැය {{count}} ක්\",\n  \"disable_for_minutes\": \"විනාඩි {{count}} ක්\",\n  \"disable_for_minutes_plural\": \"විනාඩි {{count}} ක්\",\n  \"disable_for_seconds\": \"තත්පර {{count}} ක්\",\n  \"disable_for_seconds_plural\": \"තත්පර {{count}} ක්\",\n  \"disable_ipv6\": \"IPv6 ලිපින විසඳීම අබල කරන්න\",\n  \"disable_ipv6_desc\": \"IPv6 ලිපින (AAAA වර්ගය) සඳහා වන සියළුම ව.නා.ප. විමසුම් අතහැර දමයි. HTTPS ප්‍රතිචාර වලින් IPv6 ඉඟි ඉවත් කරයි.\",\n  \"disable_notify_for_hours\": \"පැය {{count}} කට රැකවරණය අබල කරන්න\",\n  \"disable_notify_for_hours_plural\": \"පැය {{count}} කට රැකවරණය අබල කරන්න\",\n  \"disable_notify_for_minutes\": \"විනාඩි {{count}} කට රැකවරණය අබල කරන්න\",\n  \"disable_notify_for_minutes_plural\": \"විනාඩි {{count}} කට රැකවරණය අබල කරන්න\",\n  \"disable_notify_for_seconds\": \"තත්. {{count}} කට රැකවරණය අබල කරන්න\",\n  \"disable_notify_for_seconds_plural\": \"තත්. {{count}} කට රැකවරණය අබල කරන්න\",\n  \"disable_notify_until_tomorrow\": \"හෙට වනතුරු රැකවරණය අබල කරන්න\",\n  \"disable_protection\": \"රැකවරණය අබල කරන්න\",\n  \"disable_until_tomorrow\": \"හෙට වනතුරු\",\n  \"disabled\": \"අබල කර ඇත\",\n  \"disabled_dhcp\": \"ග.ධා.වි.කෙ. සේවාදායකය අබල කෙරිණි\",\n  \"disabled_filtering_toast\": \"පෙරීම අබල කෙරිණි\",\n  \"disabled_parental_toast\": \"දෙමාපිය පාලනය අබල කෙරිණි\",\n  \"disabled_protection\": \"රැකවරණය අබල කෙරිණි\",\n  \"disabled_safe_browsing_toast\": \"ආරක්‍ෂිත පිරික්සුම අබල කෙරිණි\",\n  \"disabled_safe_search_toast\": \"ආරක්‍ෂිත සෙවුම අබල කෙරිණි\",\n  \"disallow_this_client\": \"මෙම අනුග්‍රාහකයට ඉඩ නොදෙන්න\",\n  \"dns_addresses\": \"ව.නා.ප. ලිපින\",\n  \"dns_allowlists\": \"ව.නා.ප. ඉඩ දීමේ ලැයිස්තු\",\n  \"dns_allowlists_desc\": \"ඉඩ දීමේ ව.නා.ප. ලැයිස්තුවල වසම් කිසියම් අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුවක අඩංගු වුවද එය නොසලකා හැර ඉඩ දෙනු ලැබේ.\",\n  \"dns_blocklists\": \"ව.නා.ප. අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු\",\n  \"dns_blocklists_desc\": \"ඇඩ්ගාර්ඩ් හෝම් විසින් අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු වලට ගැළපෙන වසම් අවහිර කරනු ඇත.\",\n  \"dns_cache_config\": \"ව.නා.ප. නිහිත වින්‍යාසය\",\n  \"dns_cache_config_desc\": \"මෙතැන ඔබට ව.නා.ප. නිහිතය වින්‍යාසගත කළ හැකිය\",\n  \"dns_cache_size\": \"ව.නා.ප. නිහිතයේ ප්‍රමාණය (බයිට වලින්)\",\n  \"dns_config\": \"ව.නා.ප. සේවාදායක වින්‍යාසය\",\n  \"dns_over_https\": \"HTTPS-මගින්-ව.නා.ප.\",\n  \"dns_over_quic\": \"QUIC-මගින්-ව.නා.ප.\",\n  \"dns_over_tls\": \"TLS-මගින්-ව.නා.ප.\",\n  \"dns_privacy\": \"ව.නා.ප. රහස්‍යතා\",\n  \"dns_providers\": \"මෙහි තෝරා ගැනීමට <0>දැනුවත් ව.නා.ප. සපයන්නන්ගේ ලැයිස්තුවක්</0> ඇත.\",\n  \"dns_query\": \"ව.නා.ප. (DNS) විමසුම්\",\n  \"dns_rewrites\": \"ව.නා.ප. නැවත ලිවීම්\",\n  \"dns_settings\": \"ව.නා.ප. සැකසුම්\",\n  \"dns_start\": \"ව.නා.ප. සේවාදායකය ආරම්භ වෙමින්\",\n  \"dns_status_error\": \"ව.නා.ප. සේවාදායකයේ තත්‍වය පරීක්‍ෂා කිරීමේ දෝෂයකි\",\n  \"dns_test_not_ok_toast\": \"\\\"{{key}}\\\" සේවාදායක(ය): භාවිතා කිරීමට නොහැකි විය, ඔබ එය නිවැරදිව ලියා ඇතිදැයි පරීක්‍ෂා කරන්න\",\n  \"dns_test_ok_toast\": \"සඳහන් කළ ව.නා.ප. සේවාදායක නිවැරදිව ක්‍රියා කරයි\",\n  \"dns_test_parsing_error_toast\": \"කොටස {{section}}: වාක්‍ය {{line}}: භාවිතා කළ නොහැක,කරුණාකර ඔබට එය නිවැරදිව සම්මත කර ඇති දැයි පරීක්‍ෂා කරන්න\",\n  \"dns_test_warning_toast\": \"උඩුගත \\\"{{key}}\\\" ටෙස්ට් ඉල්ලීම්වලට ප්‍රතිචාර නොදෙයි හා සරිලන ලෙස ක්‍රියා නොකරනු ඇත\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"DNSSEC සබල කරන්න\",\n  \"dnssec_enable_desc\": \"DNSSEC යුග්ම තීරු සෙවනේ සබැඳියෙන් පිළිබඳ අධ්‍යනය හා ප්‍රතිඵලය පරික්ෂා කරන්න (DNSSEC- සබල කළ ප්‍රතිලේඛකය අවශ්‍යයි).\",\n  \"domain\": \"වසම\",\n  \"domain_desc\": \"නැවත අලුත් කල යුතු වසම නම හෝ තරු සලකුණ යොදන්න.\",\n  \"domain_name_table_header\": \"වසම් නාමය\",\n  \"domain_or_client\": \"වසම හෝ අනුග්‍රාහකය\",\n  \"down\": \"බිඳ වැටී\",\n  \"download_mobileconfig\": \"වින්‍යාසගත ගොනුව බාගන්න\",\n  \"download_mobileconfig_doh\": \"HTTPS-මගින්-ව.නා.ප. සඳහා .ජංගමවින්‍යාසය බාගන්න\",\n  \"download_mobileconfig_dot\": \"TLS-මගින්-ව.නා.ප. සඳහා .ජංගමවින්‍යාසය බාගන්න\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"ඉඩ දීමේ ලැයිස්තුව සංස්කරණය\",\n  \"edit_blocklist\": \"අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව සංස්කරණය\",\n  \"edit_table_action\": \"සංස්කරණය කරන්න\",\n  \"edns_cs_desc\": \"උඩුගත විමසුම් සඳහා EDNS Client Subnet විකල්පය (ECS) එකතු කරන්න සහ විමසුම් ලොගයේ සේවාදායකයින් විසින් යැව්ම කළ වටිනාකම් ලොග් කරන්න.\",\n  \"edns_enable\": \"EDNS අනුග්‍රාහක අනුජාලය සබල කරන්න\",\n  \"edns_use_custom_ip\": \"EDNS සඳහා අභිරුචි අ.ජා.කෙ. යොදාගන්න\",\n  \"edns_use_custom_ip_desc\": \"EDNS සඳහා අභිරුචි අ.ජා.කෙ. භාවිතයට ඉඩදෙන්න\",\n  \"elapsed\": \"ගත වූ කාලය\",\n  \"empty_response_status\": \"හිස්\",\n  \"enable_protection\": \"රැකවරණය සබල කරන්න\",\n  \"enable_protection_timer\": \"{{time}} න් රැකවරණය සබල කෙරේ\",\n  \"enable_upstream_dns_cache\": \"මෙම ගනුකරුගේ අභිරුචි උඩුගත වින්‍යාසය සඳහා ව.නා.ප. නිහිතය සක්‍රිය කරන්න\",\n  \"enabled_dhcp\": \"ග.ධා.වි.කෙ. සේවාදායකය සබල කෙරිණි\",\n  \"enabled_filtering_toast\": \"පෙරීම සබල කෙරිණි\",\n  \"enabled_parental_toast\": \"දෙමාපිය පාලනය සබල කෙරිණි\",\n  \"enabled_protection\": \"රැකවරණය සබල කෙරිණි\",\n  \"enabled_safe_browsing_toast\": \"ආරක්‍ෂිත පිරික්සුම සබල කෙරිණි\",\n  \"enabled_save_search_toast\": \"ආරක්‍ෂිත සෙවුම සබල කෙරිණි\",\n  \"enabled_table_header\": \"සබලයි\",\n  \"encryption_certificate_path\": \"සහතිකයේ මාර්ගය\",\n  \"encryption_certificates\": \"සහතික\",\n  \"encryption_certificates_desc\": \"සංකේතනය භාවිතයට, ඔබගේ වසම සඳහා වලංගු SSL සහතික දාමයක් සැපයිය යුතුය. <0>{{link}}</0> වෙතින් නොමිලේ සහතිකයක් ලබා ගැනීමට හැකිය හෝ විශ්වාසදායක සහතික අධිකාරියකින් මිලදී ගන්න.\",\n  \"encryption_certificates_input\": \"ඔබගේ PEM-කේතනය කළ සහතික පිටපත් කර මෙහි අලවන්න.\",\n  \"encryption_certificates_source_content\": \"සහතිකවල අන්තර්ගත අලවන්න\",\n  \"encryption_certificates_source_path\": \"සහතික ගොනු‌ව‌ක මාර්ගය සකසන්න\",\n  \"encryption_chain_invalid\": \"සහතික දාමය වලංගු නොවේ\",\n  \"encryption_chain_valid\": \"සහතික දාමය වලංගු ය\",\n  \"encryption_config_saved\": \"සංකේතන වින්‍යාසය සුරකින ලදි\",\n  \"encryption_desc\": \"සංකේතනය (HTTPS/QUIC/TLS) සඳහා ව.නා.ප. සහ පරිපාලක වියමන අතුරු මුහුණත සහය දක්වයි\",\n  \"encryption_doq\": \"QUIC-මගින්-ව.නා.ප. තොට\",\n  \"encryption_doq_desc\": \"මෙම තොට වින්‍යාසගත නම්, ඇඩ්ගාර්ඩ් හෝම් විසින් මෙම තොට හරහා QUIC-මගින්-ව.නා.ප. සේවාදායකයක් ධාවනය කෙරේ.\",\n  \"encryption_dot\": \"TLS-මගින්-ව.නා.ප. තොට\",\n  \"encryption_dot_desc\": \"මෙම තොට වින්‍යාසගත නම්, ඇඩ්ගාර්ඩ් හෝම් විසින් මෙම කවුළුව හරහා TLS-මගින්-ව.නා.ප. සේවාදායකයක් ධාවනය කෙරේ.\",\n  \"encryption_enable\": \"සංකේතනය සබල කරන්න (HTTPS, HTTPS-මගින්-ව.නා.ප. සහ TLS-මගින්-ව.නා.ප.)\",\n  \"encryption_enable_desc\": \"සංකේතනය සබල කළ විට, ඇඩ්ගාර්ඩ් හෝම් පරිපාලක අතුරුමුහුණත HTTPS හරහා ක්‍රියා කරන අතර ව.නා.ප. සේවාදායකය HTTPS-මගින්-ව.නා.ප. සහ TLS-මගින්-ව.නා.ප. හරහා ලැබෙන ඉල්ලීම් වලට සවන් දෙනු ඇත.\",\n  \"encryption_expire\": \"කල් ඉකුත් වීම\",\n  \"encryption_hostnames\": \"සත්කාරක නාම\",\n  \"encryption_https\": \"HTTPS තොට\",\n  \"encryption_https_desc\": \"HTTPS තොට වින්‍යාසගත නම්, ඇඩ්ගාර්ඩ් හෝම් පරිපාලක අතුරුමුහුණතට HTTPS හරහා ප්‍රවේශ විය හැකි අතර එය '/dns-query' ස්ථානයේ HTTPS-මගින්-ව.නා.ප. ද ලබා දෙනු ඇත.\",\n  \"encryption_issuer\": \"නිකුත් කරන්නා\",\n  \"encryption_key\": \"පුද්ගලික යතුර\",\n  \"encryption_key_input\": \"ඔබගේ සහතිකය සඳහා PEM-කේතනය කළ පුද්ගලික යතුර පිටපත් කර මෙහි අලවන්න.\",\n  \"encryption_key_invalid\": \"මෙය වලංගු නොවන {{type}} පුද්ගලික යතුරකි\",\n  \"encryption_key_source_content\": \"පුද්ගලික යතු‌රෙහි අන්තර්ගත අලවන්න\",\n  \"encryption_key_source_path\": \"පුද්ගලික යතුරක ගොනු මාර්ගය සකසන්න\",\n  \"encryption_key_valid\": \"මෙය වලංගු {{type}} පුද්ගලික යතුරකි\",\n  \"encryption_plain_dns_desc\": \"ප්ලේන් DNS ඇත්තෙන්ම සක්‍රීය වේ. ඔබට එය අබල කිරීමෙන් සියලුම උපාංගයන් සිංහලෙන් DNS භාවිතා කිරීමට හෝ කරන්න. එය සඳහා, අත්‍යාවශ්‍යය වශයෙන් මින් කිහිපයක් සක්‍රීය කළ යුතුය.\",\n  \"encryption_plain_dns_enable\": \"ප්ලේන් DNS සබල කරන්න\",\n  \"encryption_plain_dns_error\": \"ප්ලේන් DNS අබල කිරීමට, අනිවාර්ය වශයෙන් කුමක් හෝ සංකේතාත්මක DNS කෙටුම්පතක් සක්‍රීය කරන්න.\",\n  \"encryption_private_key_path\": \"පුද්ගලික යතුරෙහි මාර්ගය\",\n  \"encryption_redirect\": \"ස්වයංක්‍රීයව HTTPS වෙත හරවා යවන්න\",\n  \"encryption_redirect_desc\": \"සබල කර ඇත්නම්, ඇඩ්ගාර්ඩ් හෝම් ඔබව ස්වයංක්‍රීයව HTTP වෙතින් HTTPS ලිපින වෙත හරවා යවනු ඇත.\",\n  \"encryption_reset\": \"සංකේතාංකන සැකසුම් යළි පිහිටුවීමට අවශ්‍ය බව ඔබට විශ්වාස ද?\",\n  \"encryption_server\": \"සේවාදායක‌‌‌‌යේ නම\",\n  \"encryption_server_desc\": \"සකසා ඇත්නම්, ඇඩ්ගාර්ඩ් හෝම් විසින් අනුග්‍රාහක හැඳුනුම් හඳුනා ගැනෙයි, සෘ.ද.ඉ. (DDR) විමසුම් වලට ප්‍රතිචාර දක්වයි, සහ අතිරේක සම්බන්ධතා වලංගුකරණය සිදු කරයි. නොඑසේ නම්, මෙම විශේෂාංග අබලව පවතී. සහතිකයේ අඩංගු ව.නා.ප. නම් වලින් එකකට ගැළපිය යුතුය.\",\n  \"encryption_server_enter\": \"වසමේ නම ඇතුල් කරන්න\",\n  \"encryption_settings\": \"සංකේතන සැකසුම්\",\n  \"encryption_status\": \"තත්‍වය\",\n  \"encryption_subject\": \"මාතෘකාව\",\n  \"encryption_title\": \"සංකේතනය\",\n  \"encryption_warning\": \"අවවාදයයි\",\n  \"enforce_safe_search\": \"ආරක්‍ෂිත සෙවුම භාවිතා කරන්න\",\n  \"enforce_save_search_hint\": \"ඇඩ්ගාර්ඩ් හෝම් පහත සෙවුම් යන්ත්‍ර තුළ ආරක්‍ෂිත සෙවුම බලාත්මක කරනු ඇත: ගූගල්, යූටියුබ්, බින්ග්, ඩක්ඩක්ගෝ, එකොසියා, යාන්ඩෙක්ස් සහ පික්සාබේ.\",\n  \"enforced_save_search\": \"ආරක්‍ෂිත සෙවීම බලාත්මක කළ\",\n  \"enter_cache_size\": \"ව.නා.ප. නිහිතයෙහි ප්‍රමාණය යොදන්න (බයිට)\",\n  \"enter_cache_ttl_max_override\": \"උපරිම පව. කා. (TTL) ඇතුල් කරන්න\",\n  \"enter_cache_ttl_min_override\": \"අවම පව. කා. (TTL) ඇතුල් කරන්න\",\n  \"enter_name_hint\": \"නම ඇතුල් කරන්න\",\n  \"enter_url_or_path_hint\": \"ලැයිස්තුවක ඒ.ස.නි.(URL) හෝ ස්ථීර මාර්ගයක් ඇතුල් කරන්න\",\n  \"enter_valid_allowlist\": \"ඉඩ දීමේ ලැයිස්තුවට වලංගු ඒ.ස.නි.(URL) ලිපිනයක් ඇතුල් කරන්න.\",\n  \"enter_valid_blocklist\": \"අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුවට වලංගු ඒ.ස.නි.(URL) ලිපිනයක් ඇතුල් කරන්න.\",\n  \"error_details\": \"දෝෂ විස්තර\",\n  \"example_comment\": \"! මෙතැන අදහස් දැක්වීමක්.\",\n  \"example_comment_hash\": \"# එසේම අදහස් දැක්වීමක්.\",\n  \"example_comment_meaning\": \"අදහසක්;\",\n  \"example_meaning_filter_block\": \"උදාහරණය.ලංකා වසමට සහ එහි සියළුම උප වසම් වලට ප්‍රවේශය අවහිර කරයි;\",\n  \"example_meaning_filter_whitelist\": \"උදාහරණය.ලංකා වසමට සහ එහි සියළුම උප වසම් වලට ප්‍රවේශය අනවහිර කරයි;\",\n  \"example_meaning_host_block\": \"උදාහරණය.ලංකා වසම සඳහා 127.0.0.1 සමඟ ප්‍රතිචාර දක්වයි (නමුත් එහි උප ලිපින සඳහා නොවේ);\",\n  \"example_multiple_upstreams_reserved\": \"නිශ්චිත වසම් <0>සඳහා බහු උඩුගං</0>;\",\n  \"example_regex_meaning\": \"නිශ්චිතව දක්වා ඇති නිත්‍ය වාක්‍යවිධියට ගැළපෙන වසම් වෙත ප්‍රවේශය අවහිර කරයි.\",\n  \"example_rewrite_domain\": \"මෙම වසම් නාමය සඳහා පමණක් ප්‍රතිචාර නැවත ලියයි.\",\n  \"example_rewrite_wildcard\": \"<0>උදාහරණය.ලංකා</0> සහ එහි සියළුම උප වසම් සඳහා ප්‍රතිචාර නැවත ලියයි.\",\n  \"example_upstream_comment\": \"අදහසක්.\",\n  \"example_upstream_doh\": \"සංකේතිත <0>HTTPS-මගින්-ව.නා.ප.</0>;\",\n  \"example_upstream_doh3\": \"සිනිළි <0>HTTP/3</0> ප්‍රධාන විකල්පය හා HTTP/2 හෝ ඔස්සේ භාවිත නොකරන DNS-over-HTTPS;\",\n  \"example_upstream_doq\": \"සංකේතිත <0>QUIC-මගින්-ව.නා.ප.</0>;\",\n  \"example_upstream_dot\": \"සංකේතිත <0>TLS-මගින්-ව.නා.ප.</0>;\",\n  \"example_upstream_regular\": \"සාමාන්‍ය ව.නා.ප. (UDP හරහා);\",\n  \"example_upstream_regular_port\": \"සාමාන්‍ය ව.නා.ප. (UDP හරහා, තොට සමඟ);\",\n  \"example_upstream_reserved\": \"ක්‍රමවේද ගෙන එන <0>නිශ්චිත වසම</0> සඳහා;\",\n  \"example_upstream_sdns\": \"<1>DNSCrypt</1> හෝ <2>HTTPS-මගින්-ව.නා.ප.</2> පිළිවිසඳු සඳහා <0>ව.නා.ප. මුද්දර</0>;\",\n  \"example_upstream_tcp\": \"සාමාන්‍ය ව.නා.ප. (TCP/ස.පා.කෙ. හරහා);\",\n  \"example_upstream_tcp_hostname\": \"සාමාන්‍ය ව.නා.ප. (ස.පා.කෙ., සත්කාරක-නම හරහා);\",\n  \"example_upstream_tcp_port\": \"සාමාන්‍ය ව.නා.ප. (TCP හරහා, තොට සමඟ);\",\n  \"example_upstream_udp\": \"සාමාන්‍ය ව.නා.ප. (UDP, සත්කාරක-නම හරහා);\",\n  \"examples_title\": \"උදාහරණ\",\n  \"fallback_dns_desc\": \"උඩුගත DNS සේවාදායක සක්‍රිය නොවීමේදී භාවිතා කරන සමාන්‍ය DNS සේවාදායකවල ලැයිස්තුව. මෙම සංකේතය ඉහත ප්‍රධාන උඩුගත සේවාදායක ක්ෂේත්‍රයේ සමඟ ඒකසමාන වේ.\",\n  \"fallback_dns_placeholder\": \"ඒක පිරික්සුම් සමාන්‍ය DNS සේවාදායකයකු සෑදීම\",\n  \"fallback_dns_title\": \"සමාන්‍ය DNS සේවාදායක\",\n  \"faq\": \"නිති පැණ\",\n  \"fastest_addr\": \"වේගවත්ම අන්තර්ජාල කෙටුම්පතක (IP) ලිපිනය\",\n  \"fastest_addr_desc\": \"උඩුගත කරන සියලු DNS සේවාදායකයන්ගෙන් පිළිතුරු ලබා ගැනීමට කාලය ගණනය කරයි. TCP සම්බන්ධතාවයේ වේගය මනිමින් ඉක්මන්සම සේවාදායකයේ IP ලිපිනය පිළිතුරු ලෙස ලබා දෙයි.<br/>එක් හෝ කීපයකුත් උඩුගත කරන සේවාදායක පොළඹීම නොමැතිනම්, මෙම ක්‍රමය DNS විමසුම් දැක්මට කාර්යක්ෂම ලෙස මෝඩින්ට පත්වේ. ඔබගේ උඩුගත කරන සේවාදායක සවිකරනු ලබන බව සහ කාල සීමාව පහත් බවට හැකි මොහොතේම නිතිකාලෙන් පරීක්ෂා කරන්න.\",\n  \"filter\": \"පෙරහන\",\n  \"filter_added_successfully\": \"පෙරහන සාර්ථකව එකතු කෙරිණි\",\n  \"filter_allowlist\": \"අවවාදයයි: මෙම ක්‍රියාමාර්ගය ඉඩලත් අනුග්‍රාහක ලැයිස්තු වලින් ද \\\"{{disallowed_rule}}\\\" නීතිය බැහැර කරයි.\",\n  \"filter_category_general\": \"පොදු\",\n  \"filter_category_general_desc\": \"බොහෝ උපාංගවල ලුහුබැඳීම් සහ දැන්වීම් අවහිර කරන ලැයිස්තු\",\n  \"filter_category_other\": \"වෙනත්\",\n  \"filter_category_other_desc\": \"වෙනත් අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු\",\n  \"filter_category_regional\": \"ප්‍රාදේශීය\",\n  \"filter_category_regional_desc\": \"ප්‍රාදේශීය දැන්වීම් සහ ලුහුබැඳීමේ සේවාදායක කෙරෙහි අවධානය යොමු කරන ලැයිස්තු\",\n  \"filter_category_security\": \"ආරක්‍ෂණ\",\n  \"filter_category_security_desc\": \"ද්වේෂසහගත, තතුබෑම් සහ වංචනික වසම් අවහිර කිරීමට නිර්මාණය කළ ලැයිස්තු\",\n  \"filter_removed_successfully\": \"ලැයිස්තුව සාර්ථකව ඉවත් කෙරිණි\",\n  \"filter_updated\": \"ලැයිස්තුව සාර්ථකව යාවත්කාල කෙරිණි\",\n  \"filtered\": \"පෙරහන් කරන ලද\",\n  \"filtered_custom_rules\": \"අභිරුචි පෙරීමේ නීති මගින් පෙරහන් කරන ලදි\",\n  \"filtering_rules_learn_more\": \"ඔබගේ ම සත්කාරක ලැයිස්තු සෑදීම පිළිබඳව <0>තව දැනගන්න</0>.\",\n  \"filters\": \"පෙරහන්\",\n  \"filters_and_hosts_hint\": \"ඇඩ්ගාර්ඩ් හෝම් මූලික දැන්වීම් වාරණ නීති සහ සත්කාරක ගොනු පද ගැලපුම් තේරුම් ගනී.\",\n  \"filters_block_toggle_hint\": \"ඔබට අවහිර කිරීමේ නීති <a>පෙරහන්</a> තුළ පිහිටුවිය හැකිය.\",\n  \"filters_configuration\": \"පෙරහන් වින්‍යාසය\",\n  \"filters_enable\": \"පෙරහන් සබල කරන්න\",\n  \"filters_interval\": \"පෙරහන් යාවත්කාල කාල පරතරය\",\n  \"fix\": \"නිරාකරණය\",\n  \"for_last_days\": \"පසුගිය දවස් {{count}} සඳහා\",\n  \"for_last_days_plural\": \"පසුගිය දවස් {{count}} සඳහා\",\n  \"for_last_hours\": \"අවසන් {{count}} ත්‍රාසයේ සදහා\",\n  \"for_last_hours_plural\": \"අවසන් {{count}} පැයක් සදහා\",\n  \"forgot_password\": \"මුරපදය අමතක වුණා ද?\",\n  \"forgot_password_desc\": \"ඔබගේ පරිශ්‍රීලක ගිණුම සඳහා නව මුරපදයක් සෑදීමට කරුණාකර <0>මෙම පියවර</0> අනුගමනය කරන්න.\",\n  \"form_add_id\": \"හඳුන්වනයක් එකතු කරන්න\",\n  \"form_answer\": \"අ.ජා.කෙ. (IP) ලිපිනය ‌හෝ වසම ඇතුල් කරන්න \",\n  \"form_client_name\": \"අනුග්‍රාහකයේ නම ඇතුල් කරන්න\",\n  \"form_domain\": \"වසම නම හෝ තරු සලකුණ යොදන්න\",\n  \"form_enter_blocked_response_ttl\": \"අසැක කරන මගුලකෙන් කාලය ඇතුලත් කරන්න (තත්පර)\",\n  \"form_enter_host\": \"සත්කාරක නාමයක් යොදන්න\",\n  \"form_enter_hostname\": \"සත්කාරක නාමය යොදන්න\",\n  \"form_enter_id\": \"හඳුන්වනය ඇතුල් කරන්න\",\n  \"form_enter_ip\": \"අ.ජා.කෙ. (IP) ඇතුල් කරන්න\",\n  \"form_enter_mac\": \"මා.ප්‍ර.පා. (MAC) යොදන්න\",\n  \"form_enter_rate_limit\": \"අනුපාත සීමාව ඇතුල් කරන්න\",\n  \"form_enter_rate_limit_subnet_len\": \"අවම සීමා කිරීමට අඩවියේ ආකලන අකුණු දිග ඇතුළත් කරන්න\",\n  \"form_enter_subnet_ip\": \"\\\"{{cidr}}\\\" අනුජාලයෙහි අ.ජා.කෙ. ලිපිනයක් යොදන්න.\",\n  \"form_enter_upstream_timeout\": \"උඩුගත කරන සේවාදායකයේ කාල සීමාව තෝරන්න.\",\n  \"form_error_answer_format\": \"වලංගු නොවන උත්තර ආකෘතියකි\",\n  \"form_error_client_id_format\": \"අනුග්‍රාහකයේ හැඳු. වලංගු නොවේ\",\n  \"form_error_domain_format\": \"වලංගු නොවන වසම් ආකෘතියකි\",\n  \"form_error_equal\": \"සමාන නොවිය යුතුය\",\n  \"form_error_gateway_ip\": \"කලාවලට ගේට්වේ IP ලිපිනයක් නොඑන්න\",\n  \"form_error_ip4_format\": \"IPv4 ලිපිනය වලංගු නොවේ\",\n  \"form_error_ip4_gateway_format\": \"ගේට්වේ ගැලිමයේ වලංගු නොවන IPv4 ලිපිනය\",\n  \"form_error_ip6_format\": \"වලංගු නොවන IPv6 ලිපිනයකි\",\n  \"form_error_ip_format\": \"අ.ජා.කෙ. (IP) ලිපිනය වලංගු නොවේ\",\n  \"form_error_mac_format\": \"මා.ප්‍ර.පා. ලිපිනය වලංගු නොවේ\",\n  \"form_error_password\": \"මුරපදය නොගැළපේ\",\n  \"form_error_password_length\": \"මුරපදය අකුරු {{min}} සහ {{value}} ක් අතර විය යුතුය\",\n  \"form_error_port\": \"වලංගු තොටක අගයක් යොදන්න\",\n  \"form_error_port_range\": \"80-65535 පරාසය හි තොටක අගයක් ඇතුල් කරන්න\",\n  \"form_error_port_unsafe\": \"මෙය අනාරක්‍ෂිත තොටකි\",\n  \"form_error_positive\": \"0 ට වඩා වැඩි විය යුතුය\",\n  \"form_error_required\": \"ඇවැසි ක්‍ෂේත්‍රයකි\",\n  \"form_error_server_name\": \"සේවාදායකයේ නම වලංගු නොවේ\",\n  \"form_error_subnet\": \"\\\"{{cidr}}\\\" අනුජාලය හි \\\"{{ip}}\\\" අ.ජා.කෙ. ලිපිනය අඩංගු නොවේ\",\n  \"form_error_url_format\": \"වලංගු නොවන ඒ.ස.නි.(URL) ආකෘතියකි\",\n  \"form_error_url_or_path_format\": \"වලංගු නොවන ඒ.ස.නි.(URL) හෝ ස්ථීර මාර්ගයකි\",\n  \"form_select_tags\": \"අනුග්‍රාහක අනන්‍යන තෝරන්න\",\n  \"found_in_known_domain_db\": \"දැනුවත් වසම් දත්ත ගබඩාවේ හමු විය.\",\n  \"friday\": \"සිකුරාදා\",\n  \"friday_short\": \"සිකු\",\n  \"gateway_or_subnet_invalid\": \"උපජාල කාණ්ඩය වලංගු නොවේ\",\n  \"general_settings\": \"පොදු සැකසුම්\",\n  \"general_statistics\": \"පොදු සංඛ්‍යාලේඛන\",\n  \"get_started\": \"පටන් ගන්න\",\n  \"greater_range_start_error\": \"පරාසය ආරම්භයට වඩා වැඩි විය යුතුය\",\n  \"homepage\": \"මුල්  පිටුව\",\n  \"host_whitelisted\": \"සත්කාරකයට ඉඩ දී ඇත\",\n  \"ignore_domains\": \"නොසලකන වසම් (පේළියකට එක බැගින්)\",\n  \"ignore_domains_desc_query\": \"විමසුම් සටහනට මෙම නීති වලට ගැළපෙන විමසුම් නොලියැවෙයි\",\n  \"ignore_domains_desc_stats\": \"මෙම නීති වලට ගැළපෙන විමසුම් සංඛ්‍යාලේඛනයට නොලියැවෙයි\",\n  \"ignore_domains_title\": \"නොසලකන වසම්\",\n  \"ignore_query_log\": \"විමසුම් සටහනට මෙම අනුග්‍රාහකය යොදන්න එපා\",\n  \"ignore_statistics\": \"සංඛ්‍යාලේඛනයට මෙම අනුග්‍රාහකය යොදන්න එපා\",\n  \"install_auth_confirm\": \"මුරපදය තහවුරු කරන්න\",\n  \"install_auth_desc\": \"ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලන වියමන අතුරු මුහුණතට මුරපද සත්‍යාපනය වින්‍යාසගත කළ යුතුය. ඔබගේ ස්ථානීය ජාල‌යෙන් පමණක් ප්‍රවේශ වීමට හැකි වුවද, එය තව දුරටත් සීමා රහිත ප්‍රවේශයකින් ආරක්‍ෂා කර ගැනීම වැදගත් ය.\",\n  \"install_auth_password\": \"මුරපදය\",\n  \"install_auth_password_enter\": \"මුරපදය ඇතුල් කරන්න\",\n  \"install_auth_title\": \"සත්‍යාපනය\",\n  \"install_auth_username\": \"පරිශ්‍රීලක නාමය\",\n  \"install_auth_username_enter\": \"පරිශ්‍රීලක නාමය යොදන්න\",\n  \"install_devices_address\": \"ඇඩ්ගාර්ඩ් හෝම් ව.නා.ප. සේවාදායකය පහත ලිපිනයන්ට සවන් දෙමින් පවතී\",\n  \"install_devices_android_list_1\": \"ඇන්ඩ්‍රොයිඩ් මුල් තිරයෙන්, සැකසුම් මත තට්ටු කරන්න.\",\n  \"install_devices_android_list_2\": \"වට්ටෝරුවෙහි වයි-ෆයි මත තට්ටු කරන්න. පවතින සියළුම ජාල ලේඛන ගතවී තිබෙන තිරය පෙන්වනු ඇත (ජංගම සම්බන්ධතාවය සඳහා අභිරුචි ව.නා.ප. සැකසීමට නොහැකිය).\",\n  \"install_devices_android_list_3\": \"සම්බන්ධිත ජාලය මත මද වේලාවක් ඔබාගෙන ඉන්න, ඉන්පසුව ජාලය වෙනස් කිරීම මත තට්ටු කරන්න.\",\n  \"install_devices_android_list_4\": \"ඔබට සමහර උපාංගවල සියළුම සැකසුම් බැලීමට \\\"වැඩිදුර\\\" සඳහා වූ කොටුව සලකුණු කිරීමට අවශ්‍ය විය හැකිය. එමෙන්ම ග.ධා.වි.කෙ. (DHCP) සිට ස්ථිතික වෙත අ.ජා.කෙ. (IP) සැකසුම් මාරු කිරීමෙන් ඔබගේ ඇන්ඩ්‍රොයිඩ් ව.නා.ප. (DNS) සැකසුම් වෙනස් කිරීමට හැකිය.\",\n  \"install_devices_android_list_5\": \"ව.නා.ප. 1 සහ ව.නා.ප. 2 පිහිටුවීම් අගයන් ඔබගේ ඇඩ්ගාර්ඩ් හෝම් සේවාදායක ලිපින වලට වෙනස් කරන්න.\",\n  \"install_devices_desc\": \"ඇඩ්ගාර්ඩ් හෝම් භාවිතා කිරීමට, ඔබගේ උපාංග එය පරිශ්‍රීලනයට වින්‍යාසගත කළ යුතුය.\",\n  \"install_devices_ios_list_1\": \"මුල් තිරයෙන්, සැකසුම් මත තට්ටු කරන්න.\",\n  \"install_devices_ios_list_2\": \"වම්පස වට්ටෝරුවෙන් වයි-ෆයි තෝරන්න (ජංගම දුරකථන සඳහා ව.නා.ප. වින්‍යාසගත කිරීමට නොහැකිය).\",\n  \"install_devices_ios_list_3\": \"දැනට සක්‍රිය ජාලයේ නම මත තට්ටු කරන්න.\",\n  \"install_devices_ios_list_4\": \"ව.නා.ප. (DNS) ක්‍ෂේත්‍රය තුළ ඔබගේ ඇඩ්ගාර්ඩ් හෝම් සේවාදායක ලිපින ඇතුල් කරන්න.\",\n  \"install_devices_macos_list_1\": \"ඇපල් නිරූපකය එබීමෙන් පසු පද්ධතියේ අභිප්‍රේත වෙත යන්න.\",\n  \"install_devices_macos_list_2\": \"ජාලය මත ඔබන්න.\",\n  \"install_devices_macos_list_3\": \"ඔබගේ ලැයිස්තුවේ පළමු සම්බන්ධතාවය තෝරා වැඩිදුර යන්න ඔබන්න.\",\n  \"install_devices_macos_list_4\": \"ව.නා.ප. (DNS) තීරුව තෝරා ඔබගේ ඇඩ්ගාර්ඩ් හෝම් සේවාදායක ලිපින ඇතුල් කරන්න.\",\n  \"install_devices_router\": \"මාර්ගකාරකය\",\n  \"install_devices_router_desc\": \"මෙම පිහිටුම ඔබගේ නිවසේ මාර්ගකාරකයට සම්බන්ධිත සියළුම උපාංග ස්වයංක්‍රීයව ආවරණය කරන අතර ඔබට ඒ සෑම එකක්ම අතින් වින්‍යාසගත කිරීමට අවශ්‍ය නොවේ.\",\n  \"install_devices_router_list_1\": \"ඔබගේ මාර්ගකාරකයෙහි අභිප්‍රේත විවෘත කරන්න. සාමාන්‍යයෙන්, එය ඔබගේ අතිරික්සුවෙන් ඒ.ස.නි.(URL) ක් හරහා (http://192.168.0.1/ හෝ http://192.168.1.1/ වැනි) ප්‍රවේශ වීමට හැකිය. මුරපදය ඇතුල් කිරීමට සිදු විය හැකි නමුත් එය මතක නැතිනම් බොහෝ විට මාර්ගකාරකයේ බොත්තමක් එබීමෙන් මුරපදය නැවත සැකසීමට හැකිය. නමුත් මෙම ක්‍රියා පටිපාටිය තෝරා ගන්නේ නම්, බොහෝ විට ඔබගේ මාර්ගකාරකයේ සමස්ථ වින්‍යාසය අහිමි වනු ඇති බව මතක තබා ගන්න. මෙය පිහිටුවීමට ඔබගේ මාර්ගකාරකයට යෙදුමක් වුවමනා නම්, කරුණාකර එය ඔබගේ පරිගණකයේ හෝ දුරකථනයේ ස්ථාපනය කර මාර්ගකාරකයේ සැකසුම් වෙත ප්‍රවේශ වීමට භාවිතා කරන්න.\",\n  \"install_devices_router_list_2\": \"ග.ධා.වි.කෙ. (DHCP)/ ව.නා.ප. (DNS) සැකසුම් සොයා ගන්න. අංක කට්ටල දෙකකට හෝ තුනකට ඉඩ දෙන ක්‍ෂේත්‍රයක් අසල ඇති ව.නා.ප. අකුරු බලන්න, සෑම එකක්ම ඉලක්කම් එකේ සිට තුන දක්වා කාණ්ඩ හතරකට බෙදා ඇත.\",\n  \"install_devices_router_list_3\": \"ඔබගේ ඇඩ්ගාර්ඩ් හෝම් සේවාදායක ලිපින එහි ඇතුල් කරන්න.\",\n  \"install_devices_router_list_4\": \"සමහර මාර්ගකාරක වර්ගවල අභිරුචි ව.නා.ප. සේවාදායකයක් සැකසීමට නොහැකිය. මෙම අවස්ථාවේ දී ඇඩ්ගාර්ඩ් හෝම් <0>ග.ධා.වි.කෙ. සේවාදායකයක්</0> ලෙස පිහිටුවන්නේ නම් එය විසඳුමක් වනු ඇත. එසේ නැතිනම්, ඔබගේ විශේෂිත මාර්ගකාරකයේ අත්පොත පරීක්‍ෂා කර අභිරුචි ව.නා.ප. සේවාදායක යොදන ආකාරය සොයා ගත  යුතුය.\",\n  \"install_devices_title\": \"ඔබගේ උපාංග වින්‍යාසගත කරන්න\",\n  \"install_devices_windows_list_1\": \"පාලන වට්ටෝරුව හෝ වින්ඩෝස් සෙවුම හරහා පාලන මඬල අරින්න.\",\n  \"install_devices_windows_list_2\": \"ජාල සහ අන්තර්ජාල ප්‍රවර්ගයට ගොස් පසුව ජාල සහ බෙදාගැනීමේ මධ්‍යස්ථානය වෙත යන්න.\",\n  \"install_devices_windows_list_3\": \"වම් තීරුවෙහි \\\"උපයුක්තක‌‌‌යෙහි සැකසුම් වෙනස් කිරීම\\\" ඔබන්න.\",\n  \"install_devices_windows_list_4\": \"ඔබගේ සක්‍රිය සම්බන්ධතාවය මත දකුණින් ඔබා ගුණාංග තෝරන්න.\",\n  \"install_devices_windows_list_5\": \"ලැයිස්තුවෙන් \\\"අන්තර්ජාල කෙටුම්පත් අනුවාදය 4 (TCP/IPv4)\\\" (හෝ, IPv6 සඳහා, \\\"අන්තර්ජාල කෙටුම්පත් අනුවාදය 6 (TCP/IPv6)\\\") සොයාගෙන එය තෝරා ඉන්පසු ගුණාංග මත නැවත ඔබන්න.\",\n  \"install_devices_windows_list_6\": \"'පහත සඳහන් ව.නා.ප. සේවාදායක ලිපින භාවිතා කරන්න' යන්න තෝරා ඔබගේ ඇඩ්ගාර්ඩ් හෝම් සේවාදායක ලිපින ඇතුල් කරන්න.\",\n  \"install_saved\": \"සාර්ථකව සුරකින ලදි\",\n  \"install_settings_all_interfaces\": \"සියළුම අතුරුමුහුණත්\",\n  \"install_settings_dns\": \"ව.නා.ප. සේවාදායකය\",\n  \"install_settings_dns_desc\": \"ව.නා.ප. සේවාදායකය පහත ලිපිනවල භාවිතා කිරීම සඳහා ඔබගේ උපාංග හෝ මාර්ගකාරකය වින්‍යාසගත කිරීමට අවශ්‍ය වනු ඇත:\",\n  \"install_settings_interface_link\": \"ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වියමන අතුරු මුහුණතට පහත ලිපින වලින් ප්‍රවේශ වීමට හැකිය:\",\n  \"install_settings_listen\": \"සවන් දෙන අතුරු මුහුණත\",\n  \"install_settings_port\": \"තොට\",\n  \"install_settings_title\": \"පරිපාලක වියමන අතුරු මුහුණත\",\n  \"install_static_configure\": \"ගතික අ.ජා.කෙ. (IP) ලිපිනයක් භාවිතා කරන බව ඇඩ්ගාර්ඩ් හෝම් හඳුනාගෙන ඇත - <0>{{ip}}</0>. එය ඔබගේ ස්ථිතික ලිපිනය ලෙස භාවිතා කිරීමට අවශ්‍යද?\",\n  \"install_static_error\": \"මෙම ජාල අතුරුමුහුණත සඳහා ඇඩ්ගාර්ඩ් හෝම් හට එය ස්වයංක්‍රීයව වින්‍යාසගත කිරීමට නොහැකිය. මෙය අතින් කරන්නේ කෙසේද යන්න පිළිබඳ උපදෙස් සොයා ගන්න.\",\n  \"install_static_ok\": \"සුභ තොරතුරක්! ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනය දැනටමත් වින්‍යාසගත කර ඇත\",\n  \"install_step\": \"පියවර\",\n  \"install_submit_desc\": \"පිහිටුවීමේ ක්‍රියා පටිපාටිය අවසන් වී ඇති අතර ඔබ දැන් ඇඩ්ගාර්ඩ් හෝම් භාවිතා කිරීමට සූදානම් ය.\",\n  \"install_submit_title\": \"සුභ පැතුම්!\",\n  \"install_welcome_desc\": \"ඇඩ්ගාර්ඩ් හෝම් යනු ජාලය පුරා දැන්වීම් සහ ලුහුබැඳීම අවහිර කරන ව.නා.ප. සේවාදායකයකි. ඔබගේ සමස්ත ජාලය සහ සියළුම උපාංග පාලනයට ඉඩ සලසා දීම එහි පරමාර්ථය යි, එයට අනුග්‍රාහක පාර්ශ්ව වැඩසටහනක් වුවමනා නොවේ.\",\n  \"install_welcome_title\": \"ඇඩ්ගාර්ඩ් හෝම් වෙත සාදරයෙන් පිළිගනිමු!\",\n  \"interval_24_hour\": \"පැය 24\",\n  \"interval_6_hour\": \"පැය 6\",\n  \"interval_days\": \"දවස් {{count}}\",\n  \"interval_days_plural\": \"දවස් {{count}}\",\n  \"interval_hours\": \"පැය {{count}}\",\n  \"interval_hours_plural\": \"පැය {{count}}\",\n  \"ip\": \"අ.ජා.කෙ. (IP)\",\n  \"ip_address\": \"අ.ජා.කෙ. ලිපිනය\",\n  \"known_tracker\": \"දැනුවත් ලුහුබැඳීමකි\",\n  \"last_rule_in_allowlist\": \"\\\"{{disallowed_rule}}\\\" නීතිය බැහැර කිරීම \\\"ඉඩලත් අනුග්‍රාහක\\\" ලේඛනය අබල කරන බැවින් මෙම අනුග්‍රාහකය ඉඩ නොදීමට නොහැකිය.\",\n  \"last_time_updated_table_header\": \"අවසන් යාවත්කාල වීම\",\n  \"list_confirm_delete\": \"මෙම ලැයිස්තුව ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාස ද?\",\n  \"list_label\": \"ලැයිස්තුව\",\n  \"list_updated\": \"ලැයිස්තු {{count}} ක් යාවත්කාල කෙරිණි\",\n  \"list_updated_plural\": \"ලැයිස්තු {{count}} ක් යාවත්කාල කෙරිණි\",\n  \"list_url_table_header\": \"ඒ.ස.නි.(URL) ලැයිස්තුව\",\n  \"load_balancing\": \"ධාරිතාව තුලනය\",\n  \"load_balancing_desc\": \"එක වරකට එක උඩුගත කරන සේවාදායකයක් විමසුම් කරන්න.<br/>AdGuard Home උඩුගත කරන සේවාදායකයන් ගණනාව සහ සාමාන්‍ය විමසුම් කාලය පහත් කළ වන සේවාදායකය විශේෂිත ලෙස තෝරා ගැනීමට ඇස්තමේන්තු රැඳවුම් ප්‍රොසිජර් ක්‍රමයක් භාවිතා කරයි.\",\n  \"loading_table_status\": \"පූරණය ‌වෙමින්...\",\n  \"local_ptr_default_resolver\": \"පෙරනිමි පරිදි, ඇඩ්ගාර්ඩ් හෝම් පහත ප්‍රතිවර්ත ව.නා.ප. පිළිවිසඳු භාවිතා කරයි: {{ip}}.\",\n  \"local_ptr_desc\": \"AdGuard Home හි පුද්ගලික PTR, SOA සහ NS ඉල්ලීම් සඳහා භාවිතායේ පවතින වා.නා.ප. සේවාදාකයන්. ඉල්ලීමක් පුද්ගලික ලෙස සැලකෙයි එය පුද්ගලික IP පරාසයන් (\\\"192.168.12.34\\\" වැනි) ඇතුළුව ඇති ARPA වාසමානේ යාමක් සඳහා ඉල්ලීමක් යැවී එය පුද්ගලික IP ලිපිනයක් ඇති පාරිභෝගිකයකු ලැබේ. එය සකස් නොකිරීමේදී, ඔබේ මෙහෙයුම් පද්ධතියේ දේශපාලනකරණ සේවාදාකයන් භාවිතා කරයි, AdGuard Home IP ලිපිනයන් හැර.\",\n  \"local_ptr_no_default_resolver\": \"ඇඩ්ගාර්ඩ් හෝම් හට මෙම පද්ධතිය සඳහා සුදුසු පුද්ගලික ප්‍රතිවර්ත ව.නා.ප. පිළිවිසඳු නිශ්චය කරගත නොහැකි විය.\",\n  \"local_ptr_placeholder\": \"පේළියකට අ.ජා.කෙ. ලිපිනය බැගින් ලියන්න\",\n  \"local_ptr_title\": \"පෞද්ගලික ප්‍රතිවර්ත ව.නා.ප. සේවාදායක\",\n  \"location\": \"ස්ථානය\",\n  \"log_and_stats_section_label\": \"විමසුම් සටහන හා සංඛ්‍යාලේඛන\",\n  \"lower_range_start_error\": \"පරාසය ආරම්භයට වඩා අඩු විය යුතුය\",\n  \"main_settings\": \"ප්‍රධාන සැකසුම්\",\n  \"make_static\": \"ස්ථිතික කරන්න\",\n  \"manual_update\": \"අතින් යාවත්කාල කිරීමට <a>මෙම පියවර</a> අනුගමනය කරන්න.\",\n  \"milliseconds_abbreviation\": \"මිලි තත්.\",\n  \"monday\": \"සඳුදා\",\n  \"monday_short\": \"සඳුදා\",\n  \"name\": \"නම\",\n  \"name_table_header\": \"නම\",\n  \"netname\": \"ජාල‌යේ  නම\",\n  \"network\": \"ජාලය\",\n  \"new_allowlist\": \"නව ඉඩ දීමේ ලැයිස්තුව\",\n  \"new_blocklist\": \"නව අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව\",\n  \"next\": \"ඊළඟ\",\n  \"next_btn\": \"ඊළඟ\",\n  \"no_blocklist_added\": \"අවහිර කිරීමේ ලැයිස්තු එකතු කර නැත\",\n  \"no_clients_found\": \"අනුග්‍රාහක හමු නොවිණි\",\n  \"no_domains_found\": \"වසම් කිසිවක් හමු නොවිණි\",\n  \"no_logs_found\": \"සටහන් හමු නොවිණි\",\n  \"no_servers_specified\": \"සේවාදායක කිසිවක් නිශ්චිතව දක්වා නැත\",\n  \"no_upstreams_data_found\": \"උඩුගතදත්ත සොයාගැනීමක් නැත\",\n  \"no_whitelist_added\": \"ඉඩ දීමේ ලැයිස්තු එකතු කර නැත\",\n  \"nothing_found\": \"කිසිවක් හමු නොවිණි\",\n  \"null_ip\": \"අභිශූන්‍යය අ.ජා.කෙ.\",\n  \"number_of_dns_query_blocked_24_hours\": \"දැන්වීම් වාරණ පෙරහන් සහ සත්කාරක වාරණ ලැයිස්තු මගින් අවහිර කළ ව.නා.ප. ඉල්ලීම් ගණන\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"අවහිර කළ වැඩිහිටි වියමන අඩවි ගණන\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්‍ෂණ ඒකකය මගින් අවහිර කළ ව.නා.ප. ඉල්ලීම් ගණන\",\n  \"number_of_dns_query_days\": \"පසුගිය දවස් {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන\",\n  \"number_of_dns_query_days_plural\": \"පසුගිය දවස් {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන\",\n  \"number_of_dns_query_hours\": \"පසුගිය පැය {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන\",\n  \"number_of_dns_query_hours_plural\": \"පසුගිය පැය {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන\",\n  \"number_of_dns_query_to_safe_search\": \"ආරක්‍ෂිත සෙවීම බලාත්මක කළ සෙවුම් යන්ත්‍ර සඳහා ව.නා.ප. ඉල්ලීම් ගණන\",\n  \"nxdomain\": \"නොපවතින වසම\",\n  \"off\": \"අක්‍රියයි\",\n  \"on\": \"සක්‍රියයි\",\n  \"open_dashboard\": \"උපකරණ පුවරුව අරින්න\",\n  \"orgname\": \"සංවිධානයේ නම\",\n  \"original_response\": \"මුල් ප්‍රතිචාරය\",\n  \"out_of_range_error\": \"\\\"{{start}}\\\"-\\\"{{end}}\\\" පරාසයෙන් පිට විය යුතුය\",\n  \"page_table_footer_text\": \"පිටුව\",\n  \"parallel_requests\": \"සමාන්තර ඉල්ලීම්\",\n  \"parental_control\": \"දෙමාපිය පාලනය\",\n  \"password_label\": \"මුරපදය\",\n  \"password_placeholder\": \"මුරපදය ඇතුල් කරන්න\",\n  \"plain_dns\": \"සරල ව.නා.ප.\",\n  \"port_53_faq_link\": \"53 වන තොට බොහෝ විට \\\"DNSStubListener\\\" හෝ \\\"systemd-resolved\\\" සේවා භාවිතයට ගනු ලැබේ. කරුණාකර මෙය විසඳන්නේ කෙසේද යන්න පිළිබඳ <0>මෙම උපදෙස්</0> කියවන්න.\",\n  \"previous_btn\": \"පෙර\",\n  \"privacy_policy\": \"රහස්‍යතා ප්‍රතිපත්තිය\",\n  \"processing_update\": \"රැඳී සිටින්න, ඇඩ්ගාර්ඩ් හෝම් යාවත්කාල වෙමින්\",\n  \"protection_section_label\": \"රැකවරණය\",\n  \"protocol\": \"කෙටුම්පත\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"විමසුම් සටහන\",\n  \"query_log_clear\": \"විමසුම් සටහන් හිස් කරන්න\",\n  \"query_log_cleared\": \"විමසුම් සටහන සාර්ථකව හිස් කර ඇත\",\n  \"query_log_configuration\": \"සටහන් වින්‍යාසය\",\n  \"query_log_confirm_clear\": \"සමස්ථ විමසුම් සටහන හිස් කිරීමට ඇවැසි බව ඔබට විශ්වාසද?\",\n  \"query_log_disabled\": \"විමසුම් සටහන අබල කර ඇති අතර එය <0>සැකසුම්</0> තුළ වින්‍යාසගත කළ හැකිය\",\n  \"query_log_enable\": \"සටහන සබල කරන්න\",\n  \"query_log_filtered\": \"{{filter}} මගින් පෙරිණි\",\n  \"query_log_response_status\": \"තත්‍වය: {{value}}\",\n  \"query_log_retention\": \"විමසුම් සටහන් රැඳවීම\",\n  \"query_log_retention_confirm\": \"විමසුම් සටහන රඳවා තබා ගැනීම වෙනස් කිරීමට වුවමනා ද? ඔබ කාල පරතරයෙහි අගය අඩු කළහොත් සමහර දත්ත නැති වී යනු ඇත\",\n  \"query_log_strict_search\": \"ඉතා නිවැරදිව සෙවීමට ද්විත්ව උද්ධෘතය භාවිතා කරන්න\",\n  \"query_log_updated\": \"විමසුම් සටහන සාර්ථකව යාවත්කාල කෙරිණි\",\n  \"rate_limit\": \"අනුපාත සීමාව\",\n  \"rate_limit_desc\": \"එක් අනුග්‍රාහකයකට ඉඩ දී ඇති තත්පරයට ඉල්ලීම් ගණන. එය 0 ලෙස සැකසීම යනුවෙන් අදහස් කරන්නේ සීමාවක් නැති බවයි.\",\n  \"rate_limit_subnet_len_ipv4\": \"IPv4 ලිපින සදහා උපසර්න පෙරහන් දිග\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"සමත් කරණය සීමා කරයින් සදහන් කිරීම සඳහා, IPv4 ලිපින සඳහා උපසර්න පෙරහන් දිග. පෙරහනේ පදනම 24 වේ\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4 අඩවියේ ප්‍රතිමා අකුණු දිග 0 සහ 32 අතර විය යුතුය\",\n  \"rate_limit_subnet_len_ipv6\": \"IPv6 ලිපින සඳහා අඩවිය අකුණු දිග\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"අඩවිය අකුණු දිගක් සමඟ අවම සීමා කිරීමට භාවිතා කරන IPv6 ලිපින සඳහා. ප්‍රතිමන් 56 වනු ඇත\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6 අඩවියේ ප්‍රතිමා අකුණු දිග 0 සහ 128 අතර විය යුතුය\",\n  \"rate_limit_whitelist\": \"අනුපාත සීමා ඉඩදෙන ලැයිස්තුව\",\n  \"rate_limit_whitelist_desc\": \"අ.ජා.කෙ. ලිපින (IP) අනුපාත සීමාවෙන් බැහැර කරයි.\",\n  \"rate_limit_whitelist_placeholder\": \"පේළියකට අ.ජා.කෙ. ලිපිනය බැගින් ලියන්න\",\n  \"refresh_btn\": \"නැවුම් කරන්න\",\n  \"refresh_statics\": \"සංඛ්‍යාලේඛන නැවුම් කරන්න\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"ගැටලුවක් වාර්තා කරන්න\",\n  \"request_details\": \"ඉල්ලීමෙහි විස්තර\",\n  \"request_table_header\": \"ඉල්ලීම\",\n  \"requests_count\": \"ඉල්ලීම් ගණන\",\n  \"reset_settings\": \"සැකසුම් යළි පිහිටුවන්න\",\n  \"resolve_clients_desc\": \"පාරිභෝගිකයන්ගේ IP නාමාවලිය ආපසු ආශ්‍රිත කිරීමට PTR විමසුම් යවා මාදිලීන්ට අනුව සම්බන්ධතා අහඹු විකල්ප සඳහා යොදා ගන්න (ගෙන් සේවාදායකය සඳහා ප්‍රායෝගික DNS සේවාදායකයන්, පොදු IP ලිපිනයක් ඇති ගෙන් සේවාදායකය).\",\n  \"resolve_clients_title\": \"අනුග්‍රාහකවල අ.ජා.කෙ. ලිපින ප්‍රතිවර්ත විසඳීම සබල කරන්න\",\n  \"response_code\": \"ප්‍රතිචාර කේතය\",\n  \"response_details\": \"ප්‍රතිචාරයෙහි විස්තර\",\n  \"response_table_header\": \"ප්‍රතිචාරය\",\n  \"response_time\": \"ප්‍රතිචාර කාලය\",\n  \"rewrite_A\": \"<0>A</0>: විශේෂ අගය, උඩුගත කිරීමෙන් <0>A</0> වාර්තා තබන්න\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: විශේෂ අගය, උඩුගත කිරීමෙන් <0>AAAA</0> වාර්තා තබන්න\",\n  \"rewrite_add\": \"ව.නා.ප. නැවත ලිවීමක් යොදන්න\",\n  \"rewrite_added\": \"\\\"{{key}}\\\" සඳහා ව.නා.ප. නැවත ලිවීම සාර්ථකව එකතු කෙරිණි\",\n  \"rewrite_applied\": \"නැවත ලිවීමේ නීතිය යොදා ඇත\",\n  \"rewrite_confirm_delete\": \"\\\"{{key}}\\\" සඳහා ව.නා.ප. නැවත ලිවීම ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?\",\n  \"rewrite_deleted\": \"\\\"{{key}}\\\" සඳහා ව.නා.ප. නැවත ලිවීම ඉවත් කෙරිණි\",\n  \"rewrite_desc\": \"නිශ්චිත වසම් නාමයක් සඳහා අභිරුචි ව.නා.ප. ප්‍රතිචාර පහසුවෙන් වින්‍යාසගත කිරීමට ඉඩ දෙයි.\",\n  \"rewrite_domain_name\": \"වසම් නාමය: අන්වර්ථ නාමයක් (CNAME) එක් කරන්න\",\n  \"rewrite_edit\": \"ව.නා.ප. නැවත ලිවීම සංස්කරණය\",\n  \"rewrite_hosts_applied\": \"සත්කාරක ගොනුවක නීතියකින් නැවත ලියා ඇත\",\n  \"rewrite_ip_address\": \"අ.ජා.කෙ. ලිපිනය: A හෝ AAAA ප්‍රතිචාරයකට අ.ජා.කෙ. ලිපිනයක් යොදන්න\",\n  \"rewrite_not_found\": \"ව.නා.ප. නැවත ලිවීම් හමු නොවිණි\",\n  \"rewrite_updated\": \"ව.නා.ප. නැවත ලිවීම සාර්ථකව යාවත්කාලීන කෙරිණි\",\n  \"rewritten\": \"නැවත ලියන ලද\",\n  \"rows_table_footer_text\": \"පේළි\",\n  \"rule_added_to_custom_filtering_toast\": \"අභිරුචි පෙරීමේ නීති තුළට මෙම නීතිය එකතු කෙරිණි: {{rule}}\",\n  \"rule_label\": \"නීති(ය)\",\n  \"rule_removed_from_custom_filtering_toast\": \"අභිරුචි පෙරීමේ නීති තුළින් නීතියක් ඉවත් කෙරිණි: {{rule}}\",\n  \"rules_count_table_header\": \"නීති ගණන\",\n  \"safe_browsing\": \"ආරක්‍ෂිත පිරික්සුම\",\n  \"safe_search\": \"ආරක්‍ෂිත සෙවීම\",\n  \"saturday\": \"සෙනසුරාදා\",\n  \"saturday_short\": \"සෙන\",\n  \"save_btn\": \"සුරකින්න\",\n  \"save_config\": \"වින්‍යාසය සුරකින්න\",\n  \"schedule_add\": \"කාලසටහන එක් කරන්න\",\n  \"schedule_current_timezone\": \"වත්මන් වේලා කලාපය: {{value}}\",\n  \"schedule_desc\": \"අවහිර සේවා සඳහා අක්‍රිය කාල සීමා සකසන්න\",\n  \"schedule_edit\": \"කාලසටහන සංස්කරණය කරන්න\",\n  \"schedule_from\": \"සිට\",\n  \"schedule_invalid_select\": \"ආරම්භක වේලාව අවසන් වේලාවට කලින් විය යුතුය\",\n  \"schedule_modal_description\": \"මෙම කාලසටහන සතියේ එකම දිනය සඳහා පවතින කාලසටහන් ප්‍රතිස්ථාපනය කරයි. සතියේ සෑම දිනකම තිබිය හැක්කේ එක් අක්‍රිය කාල සීමාවක් පමණි.\",\n  \"schedule_modal_time_off\": \"සේවා අවහිර නැත:\",\n  \"schedule_new\": \"නව කාලසටහන\",\n  \"schedule_remove\": \"කාලසටහන ඉවත් කරන්න\",\n  \"schedule_save\": \"කාලසටහන සුරකින්න\",\n  \"schedule_select_days\": \"දවස් තෝරන්න\",\n  \"schedule_services\": \"සේවා අවහිර විරාමය\",\n  \"schedule_services_desc\": \"සේවා අවහිර විරාමය සඳහා කාලසටහන සකසන්න\",\n  \"schedule_services_desc_client\": \"මෙම සේවා අවහිර විරාමය සඳහා කාලසටහන සකසන්න\",\n  \"schedule_time_all_day\": \"දවස පුරාම\",\n  \"schedule_timezone\": \"වේලා කලාපයක් තෝරන්න\",\n  \"schedule_to\": \"දක්වා\",\n  \"served_from_cache_label\": \"නිහිතයෙන් සැපයිණි\",\n  \"service_name\": \"සේවාවේ නම\",\n  \"set_static_ip\": \"ස්ථිතික අ.ජා.කෙ. (IP) ලිපිනයක් සකසන්න\",\n  \"settings\": \"සැකසුම්\",\n  \"settings_custom\": \"අභිරුචි\",\n  \"settings_global\": \"ගෝලීය\",\n  \"setup_config_to_enable_dhcp_server\": \"ග.ධා.වි.කෙ. සේවාදායකය සබල කිරීමට වින්‍යාසය පිහිටුවන්න\",\n  \"setup_dns_notice\": \"ඔබට <1>HTTPS-මගින්-ව.නා.ප.</1> හෝ <1>DNS-මගින්-ව.නා.ප.</1> භාවිතයට ඇඩ්ගාර්ඩ් හෝම් සැකසුම් තුළ <0>සංකේතනය වින්‍යාසගත</0> කළ යුතුය.\",\n  \"setup_dns_privacy_1\": \"<0>TLS-මගින්-ව.නා.ප.</0> සඳහා <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>HTTPS-මගින්-ව.නා.ප.</0> සඳහා <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>මෙහි ඔබට භාවිතා කළ හැකි මෘදුකාංග ලැයිස්තුවක් ඇත.</0>\",\n  \"setup_dns_privacy_4\": \"iOS 14 හෝ macOS Big Sur උපාංගයක ඔබට විශේෂ '.mobileconfig' ගොනුවක් බාගන්නට හැකි අතර එය DNS සැකසුම් සඳහා <highlight>DNS-over-HTTPS</highlight> හෝ <highlight>DNS-over-TLS</highlight> සේවාදායකයන් එක් කරයි.\",\n  \"setup_dns_privacy_android_1\": \"TLS-මගින්-ව.නා.ප සහාය සමගම ඇන්ඩ්‍රොයිඩ් 9 පැමිණේ. එය වින්‍යාස කිරීමට, සැකසුම් → ජාලය හා අන්තර්ජාලය → වැඩිදුර → පෞද්. ව.නා.ප. වෙත ගොස් එහි ඔබගේ වසමේ නම යොදන්න.\",\n  \"setup_dns_privacy_android_2\": \"<1>HTTPS-මගින්-ව.නා.ප.</1> හා <1>TLS-මගින්-ව.නා.ප.</1> සඳහා <0>ඇන්ඩ්‍රොයිඩ් සඳහා ඇඩ්ගාර්ඩ්</0> සහාය දක්වයි.\",\n  \"setup_dns_privacy_android_3\": \"<0>ඉන්ට්‍රා</0> විසින් <1>HTTPS-මගින්-ව.නා.ප</1> සහාය ඇන්ඩ්‍රොයිඩ් සඳහා එකතු කරයි.\",\n  \"setup_dns_privacy_ioc_mac\": \"අයිඕඑස් සහ මැක්ඕඑස් වින්‍යාසය\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> <1>DNS-over-HTTPS</1> අනුකූල වේ, නමුත් ඔබේම සේවාදායකය භාවිතා කිරීමට සකස් කරන්න, ඔබට ඒ සඳහා <2>DNS Stamp</2> හි සෑදීමට අවශ්‍ය වේ.\",\n  \"setup_dns_privacy_ios_2\": \"<1>HTTPS-මගින්-ව.නා.ප.</1> හා <1>TLS-මගින්-ව.නා.ප.</1> සඳහා <0>අයිඕඑස් සඳහා ඇඩ්ගාර්ඩ්</0> සහාය දක්වයි.\",\n  \"setup_dns_privacy_other_1\": \"ඇඩ්ගාර්ඩ් හෝම් මෘදුකාංගයට ඕනෑම වේදිකාවක ආරක්‍ෂිත ව.නා.ප. අනුග්‍රාහකයක් ලෙස ක්‍රියාත්මක වීමට ද හැකිය.\",\n  \"setup_dns_privacy_other_2\": \"<0>ව.නා.ප. ප්‍රතියුක්තය</0> දන්නා සියළුම ආරක්‍ෂිත ව.නා.ප. කෙටුම්පත් සඳහා සහාය දක්වයි.\",\n  \"setup_dns_privacy_other_3\": \"<1>HTTPS-මගින්-ව.නා.ප.</1> සඳහා <0>dnscrypt-ප්‍රතියුක්තය</0> සහාය දක්වයි.\",\n  \"setup_dns_privacy_other_4\": \"<1>HTTPS-මගින්-ව.නා.ප.</1> සඳහා <0>මොසිල්ලා ෆයර්ෆොක්ස්</0> සහාය දක්වයි.\",\n  \"setup_dns_privacy_other_5\": \"<0>මෙහි</0> සහ <1>මෙහි</1> තවත් ක්‍රියාවට නැංවූ දෑ ඔබට හමුවනු ඇත.\",\n  \"setup_dns_privacy_other_title\": \"වෙනත් ක්‍රියාවට නැංවූ දෑ\",\n  \"setup_guide\": \"පිහිටුවීමේ මාර්ගෝපදේශය\",\n  \"show_all_filter_type\": \"සියල්ල පෙන්වන්න\",\n  \"show_blocked_responses\": \"අවහිර කර ඇත\",\n  \"show_filtered_type\": \"පෙරූ දෑ පෙන්වන්න\",\n  \"show_processed_responses\": \"සකසා ඇත\",\n  \"show_whitelisted_responses\": \"ඉඩ දී ඇත\",\n  \"sign_in\": \"පුරන්න\",\n  \"sign_out\": \"වරන්න\",\n  \"source_label\": \"මූලාශ්‍රය\",\n  \"static_ip\": \"ස්ථිතික අ.ජා. කෙ. ලිපිනය\",\n  \"static_ip_desc\": \"ඇඩ්ගාර්ඩ් හෝම් යනු සේවාදායකයක් බැවින් එය නිසි ලෙස ක්‍රියා කිරීමට ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනයක් ඇවැසිය. එසේ නැතිනම්, යම් අවස්ථාවක දී ඔබගේ මාර්ගකාරකය මෙම උපාංගයට වෙනත් අ.ජා. කෙ. ලිපිනයක් ලබා දීමට ඉඩ තිබේ.\",\n  \"statistics_clear\": \"සංඛ්‍යාලේඛන හිස් කරන්න\",\n  \"statistics_clear_confirm\": \"සංඛ්‍යාලේඛන ඉවත් කිරීමට වුවමනා ද?\",\n  \"statistics_cleared\": \"සංඛ්‍යාලේඛන සාර්ථකව හිස් කෙරිණි\",\n  \"statistics_configuration\": \"සංඛ්‍යාලේඛන වින්‍යාසය\",\n  \"statistics_enable\": \"සංඛ්‍යාලේඛන සබල කරන්න\",\n  \"statistics_retention\": \"සංඛ්‍යාලේඛන රඳවා තබා ගැනීම\",\n  \"statistics_retention_confirm\": \"සංඛ්‍යාලේඛන රඳවා තබා ගැනීම වෙනස් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද? ඔබ කාල පරතරයෙහි අගය අඩු කළහොත් සමහර දත්ත නැති වී යනු ඇත\",\n  \"statistics_retention_desc\": \"ඔබ කාල පරතරය අඩු කළහොත් සමහර දත්ත නැති වනු ඇත\",\n  \"stats_adult\": \"අවහිර කළ වැඩිහිටි වියමන අඩවි\",\n  \"stats_disabled\": \"සංඛ්‍යාලේඛන අබල කර ඇත. එය <0>සැකසුම් පිටුවෙන්</0> සබල කළ හැකිය.\",\n  \"stats_disabled_short\": \"සංඛ්‍යාලේඛන අබල කර ඇත\",\n  \"stats_malware_phishing\": \"අවහිර කළ ද්වේශාංග/තතුබෑම්\",\n  \"stats_params\": \"සංඛ්‍යාලේඛන වින්‍යාසය\",\n  \"stats_query_domain\": \"ප්‍රචලිත විමසන ලද වසම්\",\n  \"subnet_error\": \"ලිපින එක් අනුජාලයක තිබිය යුතුය\",\n  \"sunday\": \"ඉරිදා\",\n  \"sunday_short\": \"ඉරිදා\",\n  \"system_host_files\": \"පද්ධතියේ සත්කාරක ගොනු\",\n  \"table_client\": \"අනුග්‍රාහකය\",\n  \"table_name\": \"නම\",\n  \"tags_desc\": \"අනුග්‍රාහකයට අනුරූප වන අනන්‍යන ඔබට තේරීමට හැකිය. ඒවා වඩාත් නිවැරදිව යෙදීමට \\nඅනන්‍යන පෙරහන් නීති වලට ඇතුළත් කරන්න. <0>තව දැන ගන්න</0>.\",\n  \"tags_title\": \"අනන්‍යන\",\n  \"test_upstream_btn\": \"අත්හදා බලන්න\",\n  \"theme_auto\": \"ස්වයං\",\n  \"theme_auto_desc\": \"ස්වයං (උපාංගයේ වර්ණ පරිපාටිය මත පදනම්ව)\",\n  \"theme_dark\": \"අඳුරු\",\n  \"theme_dark_desc\": \"අඳුරු තේමාව\",\n  \"theme_light\": \"දීප්ත\",\n  \"theme_light_desc\": \"දීප්ත තේමාව\",\n  \"thursday\": \"බ්‍රහස්පතින්දා\",\n  \"thursday_short\": \"බ්‍රහස්\",\n  \"time_table_header\": \"වේලාව\",\n  \"top_blocked_domains\": \"ප්‍රචලිත අවහිර කළ වසම්\",\n  \"top_clients\": \"ප්‍රචලිත අනුග්‍රාහක\",\n  \"top_upstreams\": \"නිෂ්පාදන සම්පුර්ණ\",\n  \"topline_expired_certificate\": \"ඔබගේ SSL සහතිකය කල් ඉකුත් වී ඇත. <0>සංකේතන සැකසුම්</0> යාවත්කාල කරන්න.\",\n  \"topline_expiring_certificate\": \"ඔබගේ SSL සහතිකය කල් ඉකුත්වීමට ආසන්න වී ඇත. <0>සංකේතන සැකසුම්</0> යාවත්කාල කරන්න.\",\n  \"tracker_source\": \"ලුහුබැඳීම් මූලාශ්‍රය\",\n  \"try_again\": \"යළි උත්සාහය\",\n  \"ttl_cache_validation\": \"නිහිතයෙහි පාගාගෙන යන අවම පව. කා. (TTL) උපරිමයට වඩා අඩු හෝ සමාන විය යුතුය\",\n  \"tuesday\": \"අඟහරුවාදා\",\n  \"tuesday_short\": \"අඟහ\",\n  \"type_table_header\": \"වර්ගය\",\n  \"unavailable_dhcp\": \"ග.ධා.වි.කෙ. නැත\",\n  \"unavailable_dhcp_desc\": \"ඇඩ්ගාර්ඩ් හෝම් හට ඔබගේ මෙහෙයුම් පද්ධතියේ ග.ධා.වි.කෙ. සේවාදායකයක් ධාවනය කිරීමට නොහැකිය\",\n  \"unblock\": \"අනවහිර\",\n  \"unblock_all\": \"සියල්ල අනවහිර\",\n  \"unblock_for_this_client_only\": \"මෙම අනුග්‍රාහකයට පමණක් අනවහිර කරන්න\",\n  \"unknown_filter\": \"{{filterId}} නොදන්නා පෙරහනකි\",\n  \"update_announcement\": \"ඇඩ්ගාර්ඩ් හෝම් {{version}} දැන් ලබා ගත හැකිය! වැඩි විස්තර සඳහා <0>මෙය ඔබන්න</0>.\",\n  \"update_failed\": \"ස්වයං යාවත්කාලය අසමත් විය. අතින් යාවත්කාල කිරීමට කරුණාකර <a>පියවර අනුගමනය කරන්න</a>.\",\n  \"update_now\": \"යාවත්කාල කරන්න\",\n  \"updated_custom_filtering_toast\": \"අභිරුචි නීති සාර්ථකව සුරකින ලදි\",\n  \"updated_save_search_toast\": \"ආරක්‍ෂිත සෙවුමේ සැකසුම් යාවත්කාල විය\",\n  \"updated_upstream_dns_toast\": \"උඩුගත කරන සේවාදායක සාර්ථකව සුරකින ලදි\",\n  \"updates_checked\": \"ඇඩ්ගාර්ඩ් හෝම් හි නව අනුවාදයක් තිබේ\",\n  \"updates_version_equal\": \"ඇඩ්ගාර්ඩ් හෝම් යාවත්කාලීනයි\",\n  \"upstream\": \"උඩුගත\",\n  \"upstream_dns\": \"Upstream ව.නා.ප. සේවාදායක\",\n  \"upstream_dns_cache_configuration\": \"උඩුගත ව.නා.ප. නිහිත වින්‍යාසය\",\n  \"upstream_dns_client_desc\": \"ඔබ මෙම ක්‍ෂේත්‍රය හිස්ව තබා ගන්නේ නම්, <0>ව.නා.ප. සැකසුම්</0> හි වින්‍යාසගත කර ඇති සේවාදායක ඇඩ්ගාර්ඩ් හෝම් විසින් භාවිතා කරනු ඇත.\",\n  \"upstream_dns_configured_in_file\": \"{{path}} හි වින්‍යාසගත කර ඇත\",\n  \"upstream_dns_help\": \"පේළියකට එක් සේවාදායක ලිපිනය බැගින් ඇතුල් කරන්න. upstream ව.නා.ප. (DNS) \\n සේවාදායක වින්‍යාසගත කිරීම ගැන <a>තව දැනගන්න</a>.\",\n  \"upstream_parallel\": \"සමාන්තර විමසුම් යොදා ගනිමින් සියලුම උඩුගත කරන සේවාදායක එකවර විමසුම් කිරීම මගින් විමසුම් වේගය වැඩිදියුණු කර ගත හැක.\",\n  \"upstream_timeout\": \"උඩුගත කරන සේවාදායකයේ කාල සීමාව\",\n  \"upstream_timeout_desc\": \"උඩුගත කරන සේවාදායකයෙන් පිළිතුරු පෑමට ආරක්ෂිතව රැඳී සිටින තත්ත්වය ඉවසන විනාඩි ගණන විශේෂිත කරයි\",\n  \"upstreams\": \"උඩුගත ධාරාව\",\n  \"use_adguard_browsing_sec\": \"ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්‍ෂණ වියමන සේවාව භාවිතා කරන්න\",\n  \"use_adguard_browsing_sec_hint\": \"ඇඩ්ගාර්ඩ් හෝම් විසින් පිරික්සුම් ආරක්‍ෂණ වියමන සේවාව මගින් වසම අවහිර කර ඇත්දැයි පරීක්‍ෂා කරයි. එය සිදු කිරීමට රහස්‍යතා-හිතකාමී බැලීමේ යෙ.ක්‍ර.මු. භාවිතා කෙරේ: වසමේ කෙටි උපසර්ගයක SHA256 පූරකයක් පමණක් සේවාදායකය වෙත යවනු ලැබේ.\",\n  \"use_adguard_parental\": \"ඇඩ්ගාර්ඩ් දෙමාපිය පාලන වියමන සේවාව භාවිතා කරන්න\",\n  \"use_adguard_parental_hint\": \"වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්‍ෂා කරනු ඇත. එය පිරික්සුම් ආරක්‍ෂණ වියමන සේවාව මෙන් රහස්‍යතා හිතකාමී යෙ.ක්‍ර. අ.මු. (API) භාවිතා කරයි.\",\n  \"use_private_ptr_resolvers_desc\": \"පුද්ගලික IP ලිපිනයන් අඩංගු ARPA වාසමානේ සඳහා PTR, SOA සහ NS ඉල්ලීම් විසඳා ගැනීම සඳහා පුද්ගලික පෝෂක සේවාදායකය, DHCP, /etc/hosts ආදී व्यापक මාර්ග ආශ්‍රිත විසඳන්න. අබල කරන්නේ නම්, AdGuard Home සියලුම එවැනි ඉල්ලීම් සඳහා NXDOMAIN වශයෙන් පිළිතුරු දෙනු ඇත.\",\n  \"use_private_ptr_resolvers_title\": \"පෞද්. ප්‍රතිවර්ත ව.නා.ප. පිළිවිසඳු භාවිතය\",\n  \"use_saved_key\": \"පෙර සුරැකි යතුර භාවිතා කරන්න\",\n  \"username_label\": \"පරිශ්‍රීලක නාමය\",\n  \"username_placeholder\": \"පරිශ්‍රීලක නාමය යොදන්න\",\n  \"validated_with_dnssec\": \"DNSSEC සමඟ වලංගු කෙරිණි\",\n  \"version\": \"අනුවාදය\",\n  \"version_request_error\": \"යාවත්කාලීන පරීක්‍ෂාවට අසමත් විය. ඔබගේ අන්තර්ජාල සම්බන්ධතාවය පරීක්‍ෂා කරන්න.\",\n  \"wednesday\": \"බදාදා\",\n  \"wednesday_short\": \"බදාදා\",\n  \"whois\": \"Whois\"\n}\n"
  },
  {
    "path": "client/src/__locales/sk.json",
    "content": "{\n  \"access_allowed_desc\": \"Zoznam CIDR, IP adries alebo <a>ClientID</a>. Ak tento zoznam obsahuje položky, AdGuard Home bude akceptovať dopyty iba od týchto klientov.\",\n  \"access_allowed_title\": \"Povolení klienti\",\n  \"access_blocked_desc\": \"Nesmie byť zamieňaná s filtrami. AdGuard Home zruší DNS dopyty, ktoré sa zhodujú s týmito doménami, a tieto dopyty sa nezobrazia ani v denníku dopytov. Môžete určiť presné názvy domén, zástupné znaky alebo pravidlá filtrácie URL adries, napr. \\\"example.org\\\", \\\"*.example.org\\\" alebo ||example.org^\\\" zodpovedajúcim spôsobom.\",\n  \"access_blocked_title\": \"Nepovolené domény\",\n  \"access_desc\": \"Tu môžete konfigurovať pravidlá prístupu pre server DNS AdGuard Home.\",\n  \"access_disallowed_desc\": \"Zoznam CIDR, IP adries alebo <a>ClientID</a>. Ak tento zoznam obsahuje položky, AdGuard Home zruší dopyty od týchto klientov. Toto pole sa ignoruje, ak sú v poli Povolení klienti položky.\",\n  \"access_disallowed_title\": \"Nepovolení klienti\",\n  \"access_settings_saved\": \"Nastavenia prístupu úspešne uložené\",\n  \"access_title\": \"Nastavenia prístupu\",\n  \"actions_table_header\": \"Akcie\",\n  \"add_allowlist\": \"Pridať zoznam povolených DNS\",\n  \"add_blocklist\": \"Pridať zoznam blokovaných DNS\",\n  \"add_custom_list\": \"Pridať vlastný zoznam\",\n  \"add_persistent_client\": \"Pridať ako trvalého klienta\",\n  \"address\": \"Adresa\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home zruší všetky DNS dopyty od tohto klienta.\",\n  \"all_lists_up_to_date_toast\": \"Všetky zoznamy sú už aktuálne\",\n  \"all_queries\": \"Všetky dopyty\",\n  \"allow_this_client\": \"Povoliť tohto klienta\",\n  \"allowed\": \"Povolené\",\n  \"anonymize_client_ip\": \"Anonymizujte IP klienta\",\n  \"anonymize_client_ip_desc\": \"Neukladať úplnú IP adresu klienta do protokolov a štatistík\",\n  \"anonymizer_notification\": \"<0>Poznámka:</0> Anonymizácia IP je zapnutá. Môžete ju vypnúť vo <1>Všeobecných nastaveniach</1>.\",\n  \"answer\": \"Odpoveď\",\n  \"apply_btn\": \"Použiť\",\n  \"auto_clients_desc\": \"Informácie o IP adresách zariadení, ktoré používajú alebo môžu používať AdGuard Home. Tieto informácie sa získavajú z viacerých zdrojov vrátane súborov hosts, reverzného DNS atď.\",\n  \"auto_clients_title\": \"Runtime klienti\",\n  \"autofix_warning_list\": \"Bude vykonávať tieto úlohy: <0>Deaktivovať systém DNSStubListener</0> <0>Nastaviť adresu servera DNS na 127.0.0.1</0> <0>Nahradiť cieľový symbolický odkaz /etc/resolv.conf na /run/systemd/resolve/resolv.conf</0> <0>Zastaviť službu DNSStubListener (znova načítať službu systemd-resolved)</0>\",\n  \"autofix_warning_result\": \"Výsledkom bude, že všetky DNS dopyty z Vášho systému budú štandardne spracované službou AdGuard Home.\",\n  \"autofix_warning_text\": \"Ak kliknete na „Opraviť“, AdGuardHome nakonfiguruje Váš systém tak, aby používal DNS server AdGuardHome.\",\n  \"average_processing_time\": \"Priemerný čas spracovania\",\n  \"average_processing_time_hint\": \"Priemerný čas spracovania DNS dopytu v milisekundách\",\n  \"average_upstream_response_time\": \"Priemerný čas odozvy upstreamu\",\n  \"back\": \"Naspäť\",\n  \"block\": \"Blokovať\",\n  \"block_all\": \"Blokovať všetko\",\n  \"block_domain_use_filters_and_hosts\": \"Blokovať domény pomocou filtrov a zoznamov adries\",\n  \"block_for_this_client_only\": \"Blokovať len pre tohto klienta\",\n  \"block_services\": \"Blokovať vybrané služby\",\n  \"blocked_adult_websites\": \"Zablokované Rodičovskou kontrolou\",\n  \"blocked_by\": \"<0>Blokované filtrami</0>\",\n  \"blocked_by_cname_or_ip\": \"Blokované pomocou CNAME alebo IP\",\n  \"blocked_by_response\": \"Blokované pomocou CNAME alebo IP v odpovedi\",\n  \"blocked_response_ttl\": \"Blokovaná odozva TTL\",\n  \"blocked_response_ttl_desc\": \"Určuje, na koľko sekúnd by mali klienti uložiť filtrovanú odozvu do vyrovnávacej pamäte\",\n  \"blocked_safebrowsing\": \"Zablokované modulom Bezpečné prehliadanie\",\n  \"blocked_service\": \"Blokované služby\",\n  \"blocked_services\": \"Blokované služby\",\n  \"blocked_services_desc\": \"Umožňuje rýchlo blokovať populárne stránky a služby.\",\n  \"blocked_services_global\": \"Použiť globálne blokované služby\",\n  \"blocked_services_saved\": \"Blokované služby boli úspešne uložené\",\n  \"blocked_threats\": \"Zablokované hrozby\",\n  \"blocking_ipv4\": \"Blokovanie IPv4\",\n  \"blocking_ipv4_desc\": \"IP adresa, ktorá sa má vrátiť v prípade blokovanej žiadosti A\",\n  \"blocking_ipv6\": \"Blokovanie IPv6\",\n  \"blocking_ipv6_desc\": \"IP adresa, ktorá sa má vrátiť v prípade blokovanej žiadosti AAAA\",\n  \"blocking_mode\": \"Spôsob blokovania\",\n  \"blocking_mode_custom_ip\": \"Vlastná IP adresa: Odpovedzte s manuálne nastavenou IP adresou\",\n  \"blocking_mode_default\": \"Predvolené: Odpovedať nulovou adresou IP (0,0.0.0 pre A; :: pre AAAA), keď je blokovaná pravidlom v štýle Adblock; odpovedať IP adresou uvedenou v pravidle, keď je blokovaná pravidlom v štýle /etc/hosts\",\n  \"blocking_mode_null_ip\": \"Null IP: Odpoveď s nulovou IP adresou (0.0.0.0 pre A; :: pre AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Odpovedať kódom NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: Odpovedať kódom REFUSED\",\n  \"blocklist\": \"Zoznam blokovaní\",\n  \"bootstrap_dns\": \"Bootstrap DNS servery\",\n  \"bootstrap_dns_desc\": \"IP adresy serverov DNS používaných na rozlíšenie IP adries prekladačov DoH/DoT, ktoré zadáte ako upstream. Komentáre nie sú povolené.\",\n  \"cache_cleared\": \"Vyrovnávacia pamäť DNS bola úspešne vymazaná\",\n  \"cache_enabled\": \"Povoliť vyrovnávaciu pamäť\",\n  \"cache_enabled_desc\": \"Ukladať DNS odpovede lokálne.\",\n  \"cache_optimistic\": \"Optimistické nastavenie\",\n  \"cache_optimistic_desc\": \"Nechajte AdGuard Home odpovedať z vyrovnávacej pamäte, aj keď už platnosť položiek skončila, a tiež sa pokúste ich obnoviť.\",\n  \"cache_size\": \"Veľkosť cache\",\n  \"cache_size_desc\": \"Veľkosť vyrovnávacej pamäte DNS (v bajtoch).\",\n  \"cache_size_validation\": \"Veľkosť vyrovnávacej pamäte musí byť po povolení väčšia ako nula.\",\n  \"cache_ttl_max_override\": \"Prepísať maximálne TTL\",\n  \"cache_ttl_max_override_desc\": \"Nastaví maximálnu hodnotu TTL (v sekundách) pre záznamy v DNS cache pamäti.\",\n  \"cache_ttl_min_override\": \"Prepísať minimálne TTL\",\n  \"cache_ttl_min_override_desc\": \"Predĺži krátke hodnoty TTL (v sekundách) prijaté od servera typu upstream pri ukladaní odpovedí DNS do cache pamäte.\",\n  \"cancel_btn\": \"Zrušiť\",\n  \"category_label\": \"Kategória\",\n  \"check\": \"Kontrola\",\n  \"check_client_id\": \"Identifikátor klienta (ClientID alebo IP adresa)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Skontrolujte, či je názov hostiteľa filtrovaný.\",\n  \"check_dhcp_servers\": \"Skontrolovať DHCP servery\",\n  \"check_dns_record\": \"Vyberte typ DNS záznamu\",\n  \"check_enter_client_id\": \"Zadajte identifikátor klienta\",\n  \"check_hostname\": \"Názov hostiteľa alebo názov domény\",\n  \"check_ip\": \"IP adresy: {{ip}}\",\n  \"check_not_found\": \"Nenašlo sa vo Vašom zozname filtrov\",\n  \"check_reason\": \"Dôvod: {{reason}}\",\n  \"check_service\": \"Meno služby: {{service}}\",\n  \"check_title\": \"Skontrolujte filtráciu\",\n  \"check_updates_btn\": \"Skontrolovať aktualizácie\",\n  \"check_updates_now\": \"Skontrolovať aktualizácie teraz\",\n  \"choose_allowlist\": \"Vybrať povolený zoznam\",\n  \"choose_blocklist\": \"Vybrať blokovací zoznam\",\n  \"choose_from_list\": \"Vybrať zo zoznamu\",\n  \"city\": \"Mesto\",\n  \"clear_cache\": \"Vymazať vyrovnávaciu pamäť\",\n  \"click_to_view_queries\": \"Kliknite pre zobrazenie dopytov\",\n  \"client_add\": \"Pridať klienta\",\n  \"client_added\": \"\\\"{{key}}\\\" klienta bol úspešne pridaný\",\n  \"client_blocked\": \"Klient \\\"{{ip}}\\\" úspešne zablokovaný\",\n  \"client_confirm_block\": \"Naozaj chcete zablokovať klienta \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Naozaj chcete vymazať \\\"{{key}}\\\" klienta?\",\n  \"client_confirm_unblock\": \"Naozaj chcete odblokovať klienta \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"\\\"{{key}}\\\" klienta bol úspešne vymazaný\",\n  \"client_details\": \"Podrobnosti klienta\",\n  \"client_edit\": \"Upraviť klienta\",\n  \"client_global_settings\": \"Použiť globálne nastavenia\",\n  \"client_id\": \"ID klienta\",\n  \"client_id_desc\": \"Klientov možno identifikovať podľa ClientID. Viac informácií o tom, ako identifikovať klientov, nájdete <a>tu</a>.\",\n  \"client_id_placeholder\": \"Zadať ID klienta\",\n  \"client_identifier\": \"Identifikátor\",\n  \"client_identifier_desc\": \"Klientov možno identifikovať podľa ich IP adresy, CIDR, MAC adresy alebo ClientID (možno použiť pre DoT/DoH/DoQ). Viac informácií o tom, ako identifikovať klientov, nájdete <0>tu</0>.\",\n  \"client_name\": \"Klient {{id}}\",\n  \"client_new\": \"Nový klient\",\n  \"client_settings\": \"Nastavenie klienta\",\n  \"client_table_header\": \"Klient\",\n  \"client_unblocked\": \"Klient \\\"{{ip}}\\\" úspešne odblokovaný\",\n  \"client_updated\": \"\\\"{{key}}\\\" klienta bol úspešne aktualizovaný\",\n  \"clients_desc\": \"Nakonfigurujte trvalé záznamy klientov pre zariadenia pripojené k AdGuard Home\",\n  \"clients_not_found\": \"Nebol nájdený žiaden klient\",\n  \"clients_title\": \"Permanentní klienti\",\n  \"compact\": \"Kompaktný\",\n  \"config_successfully_saved\": \"Konfigurácia bola úspešne uložená\",\n  \"configure\": \"Konfigurovať\",\n  \"confirm_dns_cache_clear\": \"Naozaj chcete vymazať vyrovnávaciu pamäť DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home nakonfiguruje {{ip}} ako statickú IP adresu. Chcete pokračovať?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"Krajina\",\n  \"custom_filter_rules\": \"Vlastné filtračné pravidlá\",\n  \"custom_filter_rules_hint\": \"Zadajte na každý riadok jedno pravidlo. Môžete použiť buď adblock pravidlá alebo syntax host súborov.\",\n  \"custom_filtering_rules\": \"Vlastné filtračné pravidlá\",\n  \"custom_ip\": \"Vlastná IP adresa\",\n  \"custom_retention_input\": \"Zadajte retenciu v hodinách\",\n  \"custom_rotation_input\": \"Zadajte rotáciu v hodinách\",\n  \"dashboard\": \"Riadiaci panel\",\n  \"date\": \"Dátum\",\n  \"default\": \"Predvolené\",\n  \"delete_confirm\": \"Naozaj chcete vymazať \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Vymazať\",\n  \"descr\": \"Popis\",\n  \"details\": \"Podrobnosti\",\n  \"dhcp_add_static_lease\": \"Pridať statický prenájom\",\n  \"dhcp_config_saved\": \"Konfigurácia DHCP servera bola úspešne uložená\",\n  \"dhcp_description\": \"Ak Váš smerovač neposkytuje možnosť nastaviť DHCP, môžete použiť vlastný zabudovaný DHCP server AdGuard.\",\n  \"dhcp_disable\": \"Vypnúť DHCP server\",\n  \"dhcp_dynamic_ip_found\": \"Váš systém používa pre rozhranie <0>{{interfaceName}}</0> dynamickú konfiguráciu IP adresy. Aby bolo možné používať DHCP server, musí byť nastavená statická IP adresa. Vaša aktuálna IP adresa je <0>{{ipAddress}}</0>. ak Ak stlačíte tlačidlo \\\"Povoliť DHCP server\\\", AdGuard Home automaticky nastaví túto IP adresu ako statickú.\",\n  \"dhcp_edit_static_lease\": \"Upraviť statický prenájom\",\n  \"dhcp_enable\": \"Zapnúť DHCP server\",\n  \"dhcp_error\": \"AdGuard Home nevie určiť, či je v sieti iný aktívny DHCP server\",\n  \"dhcp_form_gateway_input\": \"IP brána\",\n  \"dhcp_form_lease_input\": \"Trvanie prenájmu\",\n  \"dhcp_form_lease_title\": \"Doba prenájmu DHCP (v sekundách)\",\n  \"dhcp_form_range_end\": \"Koniec rozsahu\",\n  \"dhcp_form_range_start\": \"Začiatok rozsahu\",\n  \"dhcp_form_range_title\": \"Rozsah IP adries\",\n  \"dhcp_form_subnet_input\": \"Maska podsiete\",\n  \"dhcp_found\": \"V sieti bol nájdený aktívny DHCP server. Nie je bezpečné povoliť vstavaný DHCP server.\",\n  \"dhcp_hardware_address\": \"Hardware adresa\",\n  \"dhcp_interface_select\": \"Zvoľte DHCP rozhranie\",\n  \"dhcp_ip_addresses\": \"IP adresy\",\n  \"dhcp_ipv4_settings\": \"Nastavenia DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Nastavenia DHCP IPv6\",\n  \"dhcp_lease_added\": \"Statický  \\\"{{key}}\\\" prenájmu bol úspešne pridaný\",\n  \"dhcp_lease_deleted\": \"Statický \\\"{{key}}\\\" prenájmu bol úspešne vymazaný\",\n  \"dhcp_lease_updated\": \"Statický prenájom \\\"{{key}}\\\" bol úspešne aktualizovaný\",\n  \"dhcp_leases\": \"DHCP prenájom\",\n  \"dhcp_leases_not_found\": \"Neboli nájdené žiadne DHCP prenájmy\",\n  \"dhcp_new_static_lease\": \"Nový statický prenájom\",\n  \"dhcp_not_found\": \"Je bezpečné zapnúť vstavaný DHCP server - AdGuard Home v sieti nenašiel žiadne aktívne DHCP servery. Odporúčame Vám však, aby ste ho znova manuálne skontrolovali, pretože náš automatický test momentálne neposkytuje 100% záruku.\",\n  \"dhcp_reset\": \"Naozaj chcete vymazať DHCP konfiguráciu?\",\n  \"dhcp_reset_leases\": \"Resetovať všetky prenájmy\",\n  \"dhcp_reset_leases_confirm\": \"Naozaj chcete resetovať všetky prenájmy?\",\n  \"dhcp_reset_leases_success\": \"DHCP prenájmy boli úspešne resetované\",\n  \"dhcp_settings\": \"Nastavenia DHCP\",\n  \"dhcp_static_ip_error\": \"Aby bolo možné používať DHCP server, musí byť nastavená statická IP adresa. AdGuard Home nedokázal určiť, či je toto sieťové rozhranie nakonfigurované pomocou statickej adresy IP. Nastavte statickú IP adresu manuálne.\",\n  \"dhcp_static_leases\": \"DHCP statické prenájmy\",\n  \"dhcp_static_leases_not_found\": \"Nebol nájdený žiadny statický DHCP prenájom\",\n  \"dhcp_table_expires\": \"Vyprší\",\n  \"dhcp_table_hostname\": \"Meno hostiteľa\",\n  \"dhcp_title\": \"DHCP server (experimentálne!)\",\n  \"dhcp_warning\": \"Ak chcete server DHCP napriek tomu zapnúť, uistite sa, že v sieti nie je žiadny iný aktívny DHCP server. V opačnom prípade sa môže prerušiť internet pre už pripojené zariadenia!\",\n  \"disable_for_hours\": \"Na {{count}} hodinu\",\n  \"disable_for_hours_plural\": \"Na {{count}} hodín\",\n  \"disable_for_minutes\": \"Na {{count}} minútu\",\n  \"disable_for_minutes_plural\": \"Na {{count}} minút\",\n  \"disable_for_seconds\": \"Na {{count}} sekundu\",\n  \"disable_for_seconds_plural\": \"Na {{count}} sekúnd\",\n  \"disable_ipv6\": \"Vypnúť rozlišovanie IPv6 adries\",\n  \"disable_ipv6_desc\": \"Ignorovať všetky dotazy DNS na adresy IPv6 (typ AAAA) a odstrániť IPv6 údaje z HTTPS odpovedí.\",\n  \"disable_notify_for_hours\": \"Vypnite ochranu na {{count}} hodinu\",\n  \"disable_notify_for_hours_plural\": \"Vypnite ochranu na {{count}} hodín\",\n  \"disable_notify_for_minutes\": \"Vypnite ochranu na {{count}} minútu\",\n  \"disable_notify_for_minutes_plural\": \"Vypnite ochranu na {{count}} minút\",\n  \"disable_notify_for_seconds\": \"Vypnite ochranu na {{count}} sekundu\",\n  \"disable_notify_for_seconds_plural\": \"Vypnite ochranu na {{count}} sekúnd\",\n  \"disable_notify_until_tomorrow\": \"Vypnúť ochranu do zajtra\",\n  \"disable_protection\": \"Vypnúť ochranu\",\n  \"disable_rewrites\": \"Vypnúť pravidlá prepisovania\",\n  \"disable_until_tomorrow\": \"Do zajtra\",\n  \"disabled\": \"Vypnuté\",\n  \"disabled_dhcp\": \"DHCP server vypnutý\",\n  \"disabled_filtering_toast\": \"Vypnutá filtrácia\",\n  \"disabled_parental_toast\": \"Vypnutá Rodičovská kontrola\",\n  \"disabled_protection\": \"Ochrana vypnutá\",\n  \"disabled_safe_browsing_toast\": \"Bezpečné prehliadanie vypnuté\",\n  \"disabled_safe_search_toast\": \"Vypnuté Bezpečné vyhľadávanie\",\n  \"disallow_this_client\": \"Zablokovať tohto klienta\",\n  \"dns_addresses\": \"DNS adresy\",\n  \"dns_allowlists\": \"Zoznam povolených DNS\",\n  \"dns_allowlists_desc\": \"Domény zo zoznamu povolených DNS budú povolené, aj keď sa nachádzajú v niektorom zo zoznamov blokovaných DNS.\",\n  \"dns_blocklists\": \"Zoznam blokovaných DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home bude blokovať domény obsiahnuté v zozname blokovaných DNS.\",\n  \"dns_cache_config\": \"Konfigurácia DNS cache\",\n  \"dns_cache_config_desc\": \"Tu môžete nakonfigurovať DNS cache\",\n  \"dns_cache_size\": \"Veľkosť cache pamäte DNS v bajtoch\",\n  \"dns_config\": \"Konfigurácia DNS servera\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS súkromie\",\n  \"dns_providers\": \"Tu je <0>zoznam známych poskytovateľov DNS</0>, z ktorého si vyberiete.\",\n  \"dns_query\": \"DNS dopyty\",\n  \"dns_rewrites\": \"DNS prepisovanie\",\n  \"dns_settings\": \"Nastavenia DNS\",\n  \"dns_start\": \"Spúšťa sa DNS server\",\n  \"dns_status_error\": \"Chyba pri zisťovaní stavu DNS servera\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": nemohol byť použitý, skontrolujte, či ste ho správne napísali\",\n  \"dns_test_ok_toast\": \"Špecifikované DNS servery pracujú korektne\",\n  \"dns_test_parsing_error_toast\": \"Sekcia {{section}}: riadok {{line}}: nepodarilo sa použiť, skontrolujte, či ste ho napísali správne\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" neodpovedá na testovacie dopyty a nemusí fungovať správne\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Zapnúť DNSSEC\",\n  \"dnssec_enable_desc\": \"Nastavuje príznak DNSSEC v odchádzajúcich DNS dopytoch a skontrolujte výsledok (vyžaduje sa prekladač s povoleným DNSSEC).\",\n  \"domain\": \"Doména\",\n  \"domain_desc\": \"Zadajte meno domény alebo zástupný znak, ktorý chcete prepísať.\",\n  \"domain_name_table_header\": \"Meno domény\",\n  \"domain_or_client\": \"Doména alebo klient\",\n  \"down\": \"Nadol\",\n  \"download_mobileconfig\": \"Stiahnuť konfiguračný súbor\",\n  \"download_mobileconfig_doh\": \"Prevziať .mobileconfig pre DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Prevziať .mobileconfig pre DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Upraviť zoznam povolených DNS\",\n  \"edit_blocklist\": \"Upraviť zoznam blokovaných DNS\",\n  \"edit_table_action\": \"Upraviť\",\n  \"edns_cs_desc\": \"Pridáva možnosť EDNS Client Subnet (ECS) do upstream dopytov a zapíše hodnoty odoslané klientami do denníka dopytov.\",\n  \"edns_enable\": \"Povoliť klientsku podsiete EDNS\",\n  \"edns_use_custom_ip\": \"Použiť vlastnú IP adresu pre EDNS\",\n  \"edns_use_custom_ip_desc\": \"Povoliť používanie vlastnej IP adresy pre EDNS\",\n  \"elapsed\": \"Uplynuté\",\n  \"empty_response_status\": \"Vyčistiť\",\n  \"enable_protection\": \"Zapnúť ochranu\",\n  \"enable_protection_timer\": \"Ochrana bude zapnutá o {{time}}\",\n  \"enable_rewrites\": \"Zapnúť pravidlá prepisovania\",\n  \"enable_upstream_dns_cache\": \"Zapnúť ukladanie DNS do cache pamäte pre vlastnú konfiguráciu odosielania tohto klienta\",\n  \"enabled_dhcp\": \"DHCP server zapnutý\",\n  \"enabled_filtering_toast\": \"Zapnutá filtrácia\",\n  \"enabled_parental_toast\": \"Zapnutá Rodičovská kontrola\",\n  \"enabled_protection\": \"Ochrana zapnutá\",\n  \"enabled_safe_browsing_toast\": \"Bezpečné prehliadanie zapnuté\",\n  \"enabled_save_search_toast\": \"Zapnuté Bezpečné vyhľadávanie\",\n  \"enabled_table_header\": \"Zapnuté\",\n  \"encryption_certificate_path\": \"Cesta k certifikátu\",\n  \"encryption_certificates\": \"Certifikáty\",\n  \"encryption_certificates_desc\": \"Ak chcete používať šifrovanie, musíte pre svoju doménu poskytnúť platný reťazec certifikátov SSL. Certifikát môžete získať bezplatne na adrese <0>{{link}}</0> alebo si ho môžete kúpiť od jedného z dôveryhodných certifikačných orgánov.\",\n  \"encryption_certificates_input\": \"Skopírujte alebo prilepte sem certifikáty vo formáte PEM.\",\n  \"encryption_certificates_source_content\": \"Vložte obsah certifikátu\",\n  \"encryption_certificates_source_path\": \"Nastavte cestu k súboru s certifikátom\",\n  \"encryption_chain_invalid\": \"Certifikačný reťazec je neplatný\",\n  \"encryption_chain_valid\": \"Certifikačný reťazec je platný\",\n  \"encryption_config_saved\": \"Konfigurácia šifrovania uložená\",\n  \"encryption_desc\": \"Podpora šifrovania (HTTPS/TLS) pre webové rozhranie DNS aj administrátora\",\n  \"encryption_doq\": \"Port DNS-cez-QUIC\",\n  \"encryption_doq_desc\": \"Ak je tento port nakonfigurovaný, AdGuard Home na tomto porte spustí server DNS-over-QUIC. \",\n  \"encryption_dot\": \"Port DNS-cez-TLS\",\n  \"encryption_dot_desc\": \"Ak je tento port nakonfigurovaný, AdGuard Home bude na tomto porte spúšťať DNS-cez-TLS server.\",\n  \"encryption_enable\": \"Zapnite šifrovanie (HTTPS, DNS-cez-HTTPS a DNS-cez-TLS)\",\n  \"encryption_enable_desc\": \"Ak je šifrovanie zapnuté, AdGuard Home administrátorské rozhranie bude pracovať cez HTTPS a DNS server bude počúvať dopyty cez DNS-cez-HTTPS a DNS-cez-TLS.\",\n  \"encryption_expire\": \"Vyprší\",\n  \"encryption_hostnames\": \"Názvy hostiteľov\",\n  \"encryption_https\": \"HTTPS port\",\n  \"encryption_https_desc\": \"Ak je nakonfigurovaný HTTPS port, AdGuard Home administrátorské rozhranie bude prístupné cez HTTPS a bude tiež poskytovať DNS-cez-HTTPS na '/dns-query'.\",\n  \"encryption_issuer\": \"Vydavateľ\",\n  \"encryption_key\": \"Súkromný kľúč\",\n  \"encryption_key_input\": \"Skopírujte a prilepte sem svoj súkromný kľúč vo formáte PEM pre Váš certifikát.\",\n  \"encryption_key_invalid\": \"Toto je neplatný {{type}} súkromný kľúč\",\n  \"encryption_key_source_content\": \"Vložte obsah privátneho kľúča\",\n  \"encryption_key_source_path\": \"Nastavenie cesty k súboru súkromného kľúča\",\n  \"encryption_key_valid\": \"Toto je platný {{type}} súkromný kľúč\",\n  \"encryption_plain_dns_desc\": \"Jednoduchý DNS je predvolene zapnutý. Môžete ho vypnúť, aby ste prinútili všetky zariadenia používať šifrovaný DNS. Ak to chcete urobiť, musíte zapnúť aspoň jeden šifrovaný DNS protokol\",\n  \"encryption_plain_dns_enable\": \"Zapnúť jednoduchý DNS\",\n  \"encryption_plain_dns_error\": \"Ak chcete vypnúť jednoduchý DNS protokol, zapnite aspoň jeden šifrovaný DNS protokol\",\n  \"encryption_private_key_path\": \"Cesta k súkromného kľúču\",\n  \"encryption_redirect\": \"Automaticky presmerovať na HTTPS\",\n  \"encryption_redirect_desc\": \"Ak je táto možnosť začiarknutá, služba AdGuard Home Vás automaticky presmeruje z adresy HTTP na adresy HTTPS.\",\n  \"encryption_reset\": \"Naozaj chcete obnoviť nastavenia šifrovania?\",\n  \"encryption_server\": \"Meno servera\",\n  \"encryption_server_desc\": \"Ak je nastavené, AdGuard Home zisťuje ClientID, odpovedá na dotazy DDR a vykonáva ďalšie overenia pripojenia. Ak nie je nastavená, tieto funkcie sú vypnuté. Musí sa zhodovať s jedným z názvov DNS v certifikáte.\",\n  \"encryption_server_enter\": \"Zadajte meno Vašej domény\",\n  \"encryption_settings\": \"Nastavenia šifrovania\",\n  \"encryption_status\": \"Stav\",\n  \"encryption_subject\": \"Predmet\",\n  \"encryption_title\": \"Šifrovanie\",\n  \"encryption_warning\": \"Varovanie\",\n  \"enforce_safe_search\": \"Používať bezpečné vyhľadávanie\",\n  \"enforce_save_search_hint\": \"AdGuard Home vynúti bezpečné vyhľadávanie v nasledujúcich vyhľadávačoch: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Vynútené bezpečné vyhľadávanie\",\n  \"enter_cache_size\": \"Zadať veľkosť cache (v bajtoch)\",\n  \"enter_cache_ttl_max_override\": \"Zadať maximálne TTL (v sekundách)\",\n  \"enter_cache_ttl_min_override\": \"Zadať minimálne TTL (v sekundách)\",\n  \"enter_name_hint\": \"Zadajte meno\",\n  \"enter_url_or_path_hint\": \"Zadajte URL adresu alebo absolútnu adresu zoznamu\",\n  \"enter_valid_allowlist\": \"Zadajte platnú URL adresu do zoznamu povolených DNS.\",\n  \"enter_valid_blocklist\": \"Zadajte platnú URL adresu do zoznamu blokovaných DNS.\",\n  \"error_details\": \"Podrobnosti chyby\",\n  \"example_comment\": \"! Sem sa pridáva komentár.\",\n  \"example_comment_hash\": \"# Tiež komentár.\",\n  \"example_comment_meaning\": \"len komentár;\",\n  \"example_meaning_filter_block\": \"zablokovať prístup k doméne example.org a všetkým jej subdoménam;\",\n  \"example_meaning_filter_whitelist\": \"odblokovať prístup k doméne example.org a všetkým jej subdoménam;\",\n  \"example_meaning_host_block\": \"vrátiť IP adresu 127.0.0.1 pre doménu example.org (ale nie pre jej subdomény);\",\n  \"example_multiple_upstreams_reserved\": \"viaceré upstreamy pre <0>konkrétne domény</0>;\",\n  \"example_regex_meaning\": \"zablokovať prístup k doménam zodpovedajúcim zadanému regulárnemu výrazu.\",\n  \"example_rewrite_domain\": \"prepísať odpovede iba pre toto meno domény.\",\n  \"example_rewrite_wildcard\": \"prepísať odpovede pre všetky subdomény <0>example.org</0>.\",\n  \"example_upstream_comment\": \"komentár.\",\n  \"example_upstream_doh\": \"šifrované <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"šifrované DNS-over-HTTPS s vynúteným <0>HTTP/3</0> a bez spätného prechodu na HTTP/2 alebo nižšie;\",\n  \"example_upstream_doq\": \"šifrované <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"šifrované <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"obyčajná DNS (cez UDP);\",\n  \"example_upstream_regular_port\": \"bežný DNS (cez UDP, s portom);\",\n  \"example_upstream_reserved\": \"upstream <0>pre konkrétne domény</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS pečiatky</0> pre <1>DNSCrypt</1> alebo <2>DNS-over-HTTPS</2> rezolvery;\",\n  \"example_upstream_tcp\": \"obyčajná DNS (cez TCP);\",\n  \"example_upstream_tcp_hostname\": \"štandardné DNS (cez TCP, hostname);\",\n  \"example_upstream_tcp_port\": \"bežný DNS (cez TCP, s portom);\",\n  \"example_upstream_udp\": \"štandardné DNS (cez UDP, hostname);\",\n  \"examples_title\": \"Príklady\",\n  \"fallback_dns_desc\": \"Zoznam záložných serverov DNS, ktoré sa používajú, keď nadradený servery DNS neodpovedajú. Syntax je rovnaká ako v hlavnom poli vyššie.\",\n  \"fallback_dns_placeholder\": \"Zadajte jeden záložný server DNS na riadok\",\n  \"fallback_dns_title\": \"Záložné servery DNS\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Najrýchlejšia IP adresa\",\n  \"fastest_addr_desc\": \"Čaká na odpovede od <b>všetkých</b> DNS serverov, zmeria rýchlosť pripojenia TCP pre každý server a vráti adresu IP servera s najväčšou rýchlosťou pripojenia.<br/>Tento režim môže výrazne spomaliť DNS dopyty, ak jeden alebo viac upstream serverov neodpovedá. Uistite sa, že Vaše upstream servery sú stabilné a upstream upstream je nízky.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"Filter bol úspešne pridaný\",\n  \"filter_allowlist\": \"UPOZORNENIE: Táto akcia tiež vylúči pravidlo \\\"\\\"{{disallowed_rule}}\\\"\\\" zo zoznamu povolených klientov.\",\n  \"filter_category_general\": \"Všeobecné\",\n  \"filter_category_general_desc\": \"Zoznamy, ktoré blokujú sledovanie a reklamu na väčšine zariadení\",\n  \"filter_category_other\": \"Iné\",\n  \"filter_category_other_desc\": \"Iné blokovacie zoznamy\",\n  \"filter_category_regional\": \"Regionálne\",\n  \"filter_category_regional_desc\": \"Zoznamy zamerané na regionálne reklamy a sledovacie servery\",\n  \"filter_category_security\": \"Bezpečnosť\",\n  \"filter_category_security_desc\": \"Zoznamy určené špeciálne na blokovanie škodlivých, phishingových a podvodníckych domén\",\n  \"filter_removed_successfully\": \"Zoznam bol úspešne odstránený\",\n  \"filter_updated\": \"Filter bol úspešne aktualizovaný\",\n  \"filtered\": \"Filtrované\",\n  \"filtered_custom_rules\": \"Filtrované podľa vlastných filtračných pravidiel\",\n  \"filtering_rules_learn_more\": \"<0>Dozvedieť sa viac</0> o tvorbe vlastných zoznamov hostiteľov.\",\n  \"filters\": \"Filtre\",\n  \"filters_and_hosts_hint\": \"AdGuard Home pozná základné pravidlá adblock a syntax hosts súborov.\",\n  \"filters_block_toggle_hint\": \"Pravidlá blokovania môžete nastaviť v nastaveniach <a>Filtre</a>.\",\n  \"filters_configuration\": \"Konfigurácia filtrov\",\n  \"filters_enable\": \"Zapnúť filtre\",\n  \"filters_interval\": \"Interval aktualizácie filtrov\",\n  \"fix\": \"Opraviť\",\n  \"for_last_days\": \"za posledný {{count}} deň\",\n  \"for_last_days_plural\": \"za posledných {{count}} dní\",\n  \"for_last_hours\": \"za poslednú {{count}} hodinu\",\n  \"for_last_hours_plural\": \"za posledné {{count}} hodiny|za posledných {{count}} hodín\",\n  \"forgot_password\": \"Zabudnuté heslo?\",\n  \"forgot_password_desc\": \"Postupujte podľa <0>týchto krokov</0> a vytvorte nové heslo pre svoj používateľský účet.\",\n  \"form_add_id\": \"Pridajte identifikátor\",\n  \"form_answer\": \"Zadajte IP adresu alebo meno domény\",\n  \"form_client_name\": \"Zadajte meno klienta\",\n  \"form_domain\": \"Zadajte meno domény alebo zástupný znak\",\n  \"form_enter_blocked_response_ttl\": \"Zadajte TTL blokovanej odozve (sekundy)\",\n  \"form_enter_host\": \"Zadajte meno hostiteľa\",\n  \"form_enter_hostname\": \"Zadajte meno hostiteľa\",\n  \"form_enter_id\": \"Zadajte identifikátor\",\n  \"form_enter_ip\": \"Zadajte IP adresu\",\n  \"form_enter_mac\": \"Zadajte MAC adresu\",\n  \"form_enter_rate_limit\": \"Zadajte rýchlostný limit\",\n  \"form_enter_rate_limit_subnet_len\": \"Zadajte dĺžku prefixu podsiete pre obmedzenie rýchlosti\",\n  \"form_enter_subnet_ip\": \"Zadajte IP adresu do podsiete \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Zadajte trvanie časového limitu upstream servera v sekundách\",\n  \"form_error_answer_format\": \"Neplatný formát odpovede\",\n  \"form_error_client_id_format\": \"ID klienta musí obsahovať iba čísla, malé písmená a spojovníky\",\n  \"form_error_domain_format\": \"Neplatný formát domény\",\n  \"form_error_equal\": \"Nesmie byť rovnaká\",\n  \"form_error_gateway_ip\": \"Prenájom nemôže mať IP adresu brány\",\n  \"form_error_ip4_format\": \"Neplatná IPv4 adresa\",\n  \"form_error_ip4_gateway_format\": \"Neplatná IPv4 adresa brány\",\n  \"form_error_ip6_format\": \"Neplatná IPv6 adresa\",\n  \"form_error_ip_format\": \"Neplatná IP adresa\",\n  \"form_error_mac_format\": \"Neplatná MAC adresa\",\n  \"form_error_password\": \"Heslo sa nezhoduje\",\n  \"form_error_password_length\": \"Heslo musí mať od {{min}} do {{max}} znakov\",\n  \"form_error_port\": \"Zadajte platné číslo portu\",\n  \"form_error_port_range\": \"Zadajte číslo portu v rozsahu 80-65535\",\n  \"form_error_port_unsafe\": \"Nezabezpečený port\",\n  \"form_error_positive\": \"Musí byť väčšie ako 0\",\n  \"form_error_required\": \"Povinná položka.\",\n  \"form_error_server_name\": \"Neplatné meno servera\",\n  \"form_error_subnet\": \"Podsieť \\\"{{cidr}}\\\" neobsahuje IP adresu \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Neplatný URL formát\",\n  \"form_error_url_or_path_format\": \"Neplatná URL adresa alebo absolútna adresa zoznamu\",\n  \"form_select_tags\": \"Zvoľte tagy klienta\",\n  \"found_in_known_domain_db\": \"Nájdené v databáze známych domén.\",\n  \"friday\": \"Piatok\",\n  \"friday_short\": \"Pia\",\n  \"gateway_or_subnet_invalid\": \"Maska podsiete je neplatná\",\n  \"general_settings\": \"Všeobecné nastavenia\",\n  \"general_statistics\": \"Všeobecná štatistika\",\n  \"get_started\": \"Začíname\",\n  \"greater_range_start_error\": \"Musí byť väčšie ako začiatok rozsahu\",\n  \"homepage\": \"Domovská stránka\",\n  \"host_whitelisted\": \"Hostiteľ je na bielej listine\",\n  \"ignore_domains\": \"Ignorované domény (oddelené novým riadkom)\",\n  \"ignore_domains_desc_query\": \"Dopyty zodpovedajúce týmto pravidlám sa nezapisujú do denníka dopytov\",\n  \"ignore_domains_desc_stats\": \"Dopyty zodpovedajúce týmto pravidlám sa nezapisujú do štatistík\",\n  \"ignore_domains_title\": \"Ignorované domény\",\n  \"ignore_query_log\": \"Ignorovať tohto klienta v denníku dopytov\",\n  \"ignore_statistics\": \"Ignorovanie tohto klienta v štatistikách\",\n  \"install_auth_confirm\": \"Potvrdenie hesla\",\n  \"install_auth_desc\": \"Je potrebné nakonfigurovať autentifikáciu heslom do administrátorského webového rozhrania AdGuard Home. Aj keď je AdGuard Home prístupný iba vo Vašej lokálnej sieti, je stále dôležité chrániť ho pred neobmedzeným prístupom.\",\n  \"install_auth_password\": \"Heslo\",\n  \"install_auth_password_enter\": \"Zadajte heslo\",\n  \"install_auth_title\": \"Overenie identity\",\n  \"install_auth_username\": \"Meno používateľa\",\n  \"install_auth_username_enter\": \"Zadajte meno používateľa\",\n  \"install_devices_address\": \"DNS server AdGuard Home používa nasledujúce adresy\",\n  \"install_devices_android_list_1\": \"Na domovskej obrazovke ponuky Android klepnite na Nastavenia.\",\n  \"install_devices_android_list_2\": \"V ponuke klepnite na položku Wi-Fi. Zobrazí sa obrazovka so zoznamom všetkých dostupných sietí (nie je možné nastaviť vlastný DNS pre mobilné pripojenie).\",\n  \"install_devices_android_list_3\": \"Dlho stlačte sieť, ku ktorej ste pripojení, a klepnite na Modifikovať sieť.\",\n  \"install_devices_android_list_4\": \"Na niektorých zariadeniach možno budete musieť skontrolovať pole Pokročilé a zobraziť ďalšie nastavenia. Ak chcete upraviť nastavenia DNS systému Android, budete musieť prepnúť nastavenia IP z DHCP na Statické.\",\n  \"install_devices_android_list_5\": \"Zmeňte nastavené hodnoty DNS 1 a DNS 2 na adresy serverov AdGuard Home.\",\n  \"install_devices_desc\": \"Ak chcete začať používať službu AdGuard Home, musíte najskôr nakonfigurovať Vaše zariadenia.\",\n  \"install_devices_ios_list_1\": \"Na domácej obrazovke ťuknite na položku Nastavenia.\",\n  \"install_devices_ios_list_2\": \"V ľavej ponuke vyberte Wi-Fi (nie je možné nakonfigurovať DNS pre mobilné siete).\",\n  \"install_devices_ios_list_3\": \"Klepnite na meno aktuálne aktívnej siete.\",\n  \"install_devices_ios_list_4\": \"Do poľa DNS zadajte adresy Vašich AdGuard Home serverov.\",\n  \"install_devices_macos_list_1\": \"Kliknite na ikonu Apple a prejdite na Predvoľby systému.\",\n  \"install_devices_macos_list_2\": \"Kliknite na Sieť.\",\n  \"install_devices_macos_list_3\": \"Zvoľte prvé pripojenie vo Vašom zozname a kliknite na Pokročilé.\",\n  \"install_devices_macos_list_4\": \"Vyberte kartu DNS a zadajte adresy Vašich AdGuard Home serverov.\",\n  \"install_devices_router\": \"Smerovač\",\n  \"install_devices_router_desc\": \"Toto nastavenie automaticky pokrýva všetky zariadenia pripojené k Vášmu domácemu smerovaču a nebudete ich musieť konfigurovať manuálne.\",\n  \"install_devices_router_list_1\": \"Otvorte predvoľby Vášho smerovača. Zvyčajne ho môžete získať z Vášho prehliadača prostredníctvom URL adresy, ako napr. http://192.168.0.1/ alebo http://192.168.1.1/. Možno bude potrebné zadať heslo. Ak si ho nepamätáte, môžete často resetovať heslo stlačením tlačidla na samotnom smerovači, uvedomte si však, že ak sa zvolíte tento postup, pravdepodobne stratíte celú konfiguráciu smerovača. Ak Váš smerovač vyžaduje na nastavenie vlastnú aplikáciu, nainštalujte si ju do telefónu alebo počítača a použite ju na prístup k nastaveniam smerovača.\",\n  \"install_devices_router_list_2\": \"Nájdite nastavenia DHCP/DNS. Hľadajte skratku DNS vedľa poľa, ktoré umožňuje vložiť dve alebo tri sady čísel, každé rozdelené do štyroch skupín s jedným až tromi číslicami.\",\n  \"install_devices_router_list_3\": \"Zadajte tam adresy Vášho AdGuard Home servera.\",\n  \"install_devices_router_list_4\": \"Na niektorých typoch smerovačov nemôžete nastaviť vlastný DNS server. V takom prípade môže pomôcť, ak nastavíte AdGuard Home ako <0>DHCP server</0>. V opačnom prípade by ste mali vyhľadať príručku, ako prispôsobiť DNS servery konkrétnemu modelu smerovača.\",\n  \"install_devices_title\": \"Konfigurujte Vaše zariadenia\",\n  \"install_devices_windows_list_1\": \"Otvorte panel Nastavenia cez menu Štart alebo vyhľadávanie Windows.\",\n  \"install_devices_windows_list_2\": \"Prejdite do kategórie Sieť a internet a potom do Centra sietí a zdieľania.\",\n  \"install_devices_windows_list_3\": \"Na ľavom paneli kliknite na „Zmeniť nastavenia adaptéra“.\",\n  \"install_devices_windows_list_4\": \"Kliknite pravým tlačidlom myši na aktívne pripojenie a vyberte Vlastnosti.\",\n  \"install_devices_windows_list_5\": \"Nájdite v zozname položku \\\"Internet Protocol verzia 4 (TCP/IPv4)\\\" (alebo pre IPv6, \\\"Internet Protocol verzia 6 (TCP/IPv6)\\\"), vyberte ju a potom znova kliknite na Vlastnosti.\",\n  \"install_devices_windows_list_6\": \"Zvoľte \\\"Použiť nasledujúce adresy DNS servera\\\" a zadajte adresy domáceho AdGuard servera.\",\n  \"install_saved\": \"Úspešne uložené\",\n  \"install_settings_all_interfaces\": \"Všetky rozhrania\",\n  \"install_settings_dns\": \"DNS server\",\n  \"install_settings_dns_desc\": \"Budete musieť konfigurovať Vaše zariadenia alebo smerovač, aby používali DNS server na nasledujúcich adresách:\",\n  \"install_settings_interface_link\": \"Vaše administrátorské webové rozhranie AdGuard Home  bude k dispozícii na týchto adresách:\",\n  \"install_settings_listen\": \"Sieťové rozhranie\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Administrátorské webové rozhranie\",\n  \"install_static_configure\": \"AdGuard Home zistil, že sa používa dynamická IP adresa <0>{{ip}}</0>. Chcete ju použiť ako svoju statickú adresu?\",\n  \"install_static_error\": \"AdGuard Home ho nemôže automaticky nakonfigurovať pre toto sieťové rozhranie. Vyhľadajte návod, ako to urobiť manuálne.\",\n  \"install_static_ok\": \"Dobré správy! Statická IP adresa je už nakonfigurovaná\",\n  \"install_step\": \"Krok\",\n  \"install_submit_desc\": \"Proces nastavenia je dokončený a ste pripravený začať používať AdGuard Home.\",\n  \"install_submit_title\": \"Gratulujeme!\",\n  \"install_welcome_desc\": \"Doména AdGuard Home je celosieťový DNS server pre blokovanie reklám a sledovačov. Jeho cieľom je, aby ste ovládali celú Vašu sieť a všetky Vaše zariadenia, pričom sa nevyžaduje použitie akéhokoľvek programu na strane klienta.\",\n  \"install_welcome_title\": \"Vitajte na stránkach AdGuard Home!\",\n  \"interval_24_hour\": \"24 hodín\",\n  \"interval_6_hour\": \"6 hodín\",\n  \"interval_days\": \"{{count}} deň\",\n  \"interval_days_plural\": \"{{count}} dní\",\n  \"interval_hours\": \"{{count}} hodina\",\n  \"interval_hours_plural\": \"{{count}} hodín\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP adresa\",\n  \"known_tracker\": \"Známy sledovač\",\n  \"last_rule_in_allowlist\": \"Nemôžete zakázať tohto klienta, pretože vylúčenie pravidla \\\"{{disallowed_rule}}\\\" zakáže zoznam \\\"povolených klientov\\\".\",\n  \"last_time_updated_table_header\": \"Posledná aktualizácia\",\n  \"list_confirm_delete\": \"Naozaj chcete vymazať tento zoznam?\",\n  \"list_label\": \"Zoznam\",\n  \"list_updated\": \"{{count}} zoznam aktualizovaný\",\n  \"list_updated_plural\": \"{{count}} zoznamov aktualizovaných\",\n  \"list_url_table_header\": \"Zoznam URL adries\",\n  \"load_balancing\": \"Vyrovnávanie záťaže\",\n  \"load_balancing_desc\": \"Dopytuje sa súčasne len jeden upstream server.<br/>AdGuard Home používa vážený náhodný algoritmus na výber serverov s najnižším počtom neúspešných vyhľadávaní a najnižším priemerným časom vyhľadávania.\",\n  \"loading_table_status\": \"Načítavam...\",\n  \"local_ptr_default_resolver\": \"V predvolenom nastavení používa AdGuard Home nasledujúce reverzné DNS prekladače: {{ip}}.\",\n  \"local_ptr_desc\": \"Servery DNS, ktoré používa AdGuard Home na súkromné dopyty PTR, SOA a NS. Dopyt sa považuje za súkromný, ak požaduje doménu ARPA obsahujúcu podsieť v rozsahu súkromnej IP adresy (napríklad „192.168.12.34“) a pochádza od klienta so súkromnou IP adresou. Ak nie je nastavené, použijú sa predvolené DNS resolvery Vášho operačného systému, okrem AdGuard Home IP adries.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home nemohol určiť vhodné súkromné reverzné DNS prekladače pre tento systém.\",\n  \"local_ptr_placeholder\": \"Na každý riadok zadajte IP adresu jedného servera\",\n  \"local_ptr_title\": \"Súkromné reverzné DNS servery\",\n  \"location\": \"Poloha\",\n  \"log_and_stats_section_label\": \"Protokol dopytov a štatistiky\",\n  \"lower_range_start_error\": \"Musí byť nižšie ako začiatok rozsahu\",\n  \"main_settings\": \"Hlavné nastavenia\",\n  \"make_static\": \"Vytvárať štatistiku\",\n  \"manual_update\": \"Pre manuálnu aktualizáciu prosím <a>sledujte tento postup</a>.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Pondelok\",\n  \"monday_short\": \"Pon\",\n  \"name\": \"Meno\",\n  \"name_table_header\": \"Meno\",\n  \"netname\": \"Meno siete\",\n  \"network\": \"Sieť\",\n  \"new_allowlist\": \"Nový zoznam povolených DNS\",\n  \"new_blocklist\": \"Nový zoznam blokovaných DNS\",\n  \"next\": \"Ďalej\",\n  \"next_btn\": \"Ďalšie\",\n  \"no_blocklist_added\": \"Nebol pridaný žiaden zoznam blokovaných DNS\",\n  \"no_clients_found\": \"Neboli nájdení žiadni klienti\",\n  \"no_domains_found\": \"Žiadna doména nebola nájdená\",\n  \"no_logs_found\": \"Neboli nájdené žiadne denníky\",\n  \"no_servers_specified\": \"Neboli špecifikované žiadne servery\",\n  \"no_upstreams_data_found\": \"Nenašli sa žiadne údaje o upstream serveroch\",\n  \"no_whitelist_added\": \"Nebol pridaný žiaden zoznam povolených DNS\",\n  \"nothing_found\": \"Nič sa nenašlo\",\n  \"null_ip\": \"Nulová IP adresa\",\n  \"number_of_dns_query_blocked_24_hours\": \"Počet DNS dopytov zablokovaných filtrami reklamy a zoznamami blokovaných hostov\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Počet zablokovaných stránok pre dospelých\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Počet DNS dopytov zablokovaných AdGuard modulom Bezpečné prehliadanie\",\n  \"number_of_dns_query_days\": \"Počet DNS dopytov spracovaných za posledný {{count}} deň\",\n  \"number_of_dns_query_days_plural\": \"Počet DNS dopytov spracovaných za posledných {{count}} dní\",\n  \"number_of_dns_query_hours\": \"Počet DNS dopytov spracovaných za poslednú {{count}} hodinu\",\n  \"number_of_dns_query_hours_plural\": \"Počet DNS dopytov spracovaných za posledné {{count}} hodiny)|Počet DNS dopytov spracovaných za posledných {{count}} hodín\",\n  \"number_of_dns_query_to_safe_search\": \"Počet DNS dopytov na vyhľadávače, pri ktorých bolo vynútené bezpečné vyhľadávanie\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"VYP.\",\n  \"on\": \"ZAP.\",\n  \"open_dashboard\": \"Otvoriť riadiaci panel\",\n  \"orgname\": \"Meno organizácie\",\n  \"original_response\": \"Pôvodná odozva\",\n  \"out_of_range_error\": \"Musí byť mimo rozsahu \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Stránka\",\n  \"parallel_requests\": \"Paralelné dopyty\",\n  \"parental_control\": \"Rodičovská kontrola\",\n  \"password_label\": \"Heslo\",\n  \"password_placeholder\": \"Zadajte heslo\",\n  \"plain_dns\": \"Obyčajné DNS\",\n  \"port_53_faq_link\": \"Port 53 je často obsadený službami \\\"DNSStubListener\\\" alebo \\\"systemd-resolved\\\". Prečítajte si <0>tento návod</0> o tom, ako to vyriešiť.\",\n  \"previous_btn\": \"Predošlé\",\n  \"privacy_policy\": \"Pravidlá ochrany súkromia\",\n  \"processing_update\": \"Čakajte prosím, AdGuard Home sa aktualizuje\",\n  \"protection_section_label\": \"Ochrana\",\n  \"protocol\": \"Protokol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Denník dopytov\",\n  \"query_log_clear\": \"Vymazať denníky dopytov\",\n  \"query_log_cleared\": \"Denník dopytov bol úspešne vymazaný\",\n  \"query_log_configuration\": \"Konfigurácia denníka\",\n  \"query_log_confirm_clear\": \"Naozaj chcete vymazať celý denník dopytov?\",\n  \"query_log_disabled\": \"Protokol dopytov je vypnutý a možno ho nakonfigurovať v <0>nastaveniach</0>\",\n  \"query_log_enable\": \"Zapnúť denník\",\n  \"query_log_filtered\": \"Vyfiltrované pomocou {{filter}}\",\n  \"query_log_response_status\": \"Stav: {{value}}\",\n  \"query_log_retention\": \"Rotácia denníkov dopytov\",\n  \"query_log_retention_confirm\": \"Naozaj chcete zmeniť rotáciu denníka dopytov? Ak znížite hodnotu intervalu, niektoré údaje sa stratia\",\n  \"query_log_strict_search\": \"Na prísne vyhľadávanie použite dvojité úvodzovky\",\n  \"query_log_updated\": \"Denník dopytov bol úspešne aktualizovaný\",\n  \"rate_limit\": \"Rýchlostný limit\",\n  \"rate_limit_desc\": \"Počet požiadaviek za sekundu, ktoré môže jeden klient vykonať. Nastavenie na hodnotu 0 znamená neobmedzene.\",\n  \"rate_limit_subnet_len_ipv4\": \"Dĺžka prefixu podsiete pre adresy IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Dĺžka prefixu podsiete pre adresy IPv4 používané na obmedzenie rýchlosti. Predvolená hodnota je 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Dĺžka prefixu podsiete IPv4 musí byť od 0 do 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Dĺžka prefixu podsiete pre adresy IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Dĺžka prefixu podsiete pre adresy IPv6 používané na obmedzenie rýchlosti. Predvolená hodnota je 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Dĺžka prefixu podsiete IPv6 musí byť od 0 do 128\",\n  \"rate_limit_whitelist\": \"Zoznam povolení obmedzujúcich rýchlosť\",\n  \"rate_limit_whitelist_desc\": \"IP adresy vylúčené z obmedzenia rýchlosti\",\n  \"rate_limit_whitelist_placeholder\": \"Na každý riadok zadajte IP adresu jedného servera\",\n  \"refresh_btn\": \"Obnoviť\",\n  \"refresh_statics\": \"Obnoviť štatistiku\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Nahlásiť problém\",\n  \"request_details\": \"Podrobnosti dopytu\",\n  \"request_table_header\": \"Dopyt\",\n  \"requests_count\": \"Počet dopytov\",\n  \"reset_settings\": \"Obnoviť nastavenia\",\n  \"resolve_clients_desc\": \"Reverzne rozlišuje adresy IP klientov na ich názvy hostiteľov odosielaním PTR dopytov príslušným prekladačom (súkromné DNS servery pre miestnych klientov, servery typu upstream pre klientov s verejnými IP adresami).\",\n  \"resolve_clients_title\": \"Povoliť spätný preklad IP adries klientov\",\n  \"response_code\": \"Kód odozvy\",\n  \"response_details\": \"Podrobnosti odpovede\",\n  \"response_table_header\": \"Odozva\",\n  \"response_time\": \"Čas odozvy\",\n  \"rewrite_A\": \"<0>A</0>: špeciálna hodnota, uchovávajte záznamy <0>A</0> z upstream\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: špeciálna hodnota, uchovávajte záznamy <0>AAAA</0> z upstream\",\n  \"rewrite_add\": \"Pridať DNS prepísanie\",\n  \"rewrite_added\": \"DNS prepísanie pre \\\"{{key}}\\\" bolo úspešne pridané\",\n  \"rewrite_applied\": \"Použilo sa pravidlo prepisovania\",\n  \"rewrite_confirm_delete\": \"Naozaj chcete odstrániť prepísanie DNS pre \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"DNS prepísanie pre \\\"{{key}}\\\" bolo úspešne vymazané\",\n  \"rewrite_desc\": \"Umožňuje ľahko nakonfigurovať vlastnú odpoveď DNS pre konkrétne meno domény.\",\n  \"rewrite_domain_name\": \"Meno domény: pridajte záznam CNAME\",\n  \"rewrite_edit\": \"Upraviť prepísanie DNS\",\n  \"rewrite_hosts_applied\": \"Prepísané pravidlom súboru hostiteľov\",\n  \"rewrite_ip_address\": \"IP adresa: použite túto IP v odpovedi A alebo AAAA\",\n  \"rewrite_not_found\": \"Neboli nájdené žiadne DNS prepísania\",\n  \"rewrite_settings_updated\": \"Úspešná aktualizácia nastavení prepisovania DNS\",\n  \"rewrite_updated\": \"Prepísanie DNS bolo úspešne aktualizované\",\n  \"rewrites_disabled_table_header\": \"Prepisovanie je vypnuté\",\n  \"rewrites_enabled_table_header\": \"Prepisovanie je zapnuté\",\n  \"rewritten\": \"Prepísané\",\n  \"rows_table_footer_text\": \"riadky\",\n  \"rule_added_to_custom_filtering_toast\": \"Pravidlo pridané do vlastných filtračných pravidiel: {{rule}}\",\n  \"rule_label\": \"Pravidlo (pravidlá)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Pravidlo odstránené z vlastných filtračných pravidiel: {{rule}}\",\n  \"rules_count_table_header\": \"Počet pravidiel\",\n  \"safe_browsing\": \"Bezpečné prehliadanie\",\n  \"safe_search\": \"Bezpečné vyhľadávanie\",\n  \"saturday\": \"Sobota\",\n  \"saturday_short\": \"Sob\",\n  \"save_btn\": \"Uložiť\",\n  \"save_config\": \"Uložiť konfiguráciu\",\n  \"schedule_add\": \"Pridať časový plán\",\n  \"schedule_current_timezone\": \"Aktuálne časové pásmo: {{value}}\",\n  \"schedule_desc\": \"Nastavenie doby nečinnosti pre blokované služby\",\n  \"schedule_edit\": \"Upraviť časový plán\",\n  \"schedule_from\": \"Od\",\n  \"schedule_invalid_select\": \"Čas začiatku musí byť pred časom ukončenia\",\n  \"schedule_modal_description\": \"Tento plán nahradí všetky existujúce plány na rovnaký deň v týždni. Každý deň v týždni môže mať iba jedno obdobie nečinnosti.\",\n  \"schedule_modal_time_off\": \"Žiadne blokovanie služby:\",\n  \"schedule_new\": \"Nový časový plán\",\n  \"schedule_remove\": \"Odstrániť časový plán\",\n  \"schedule_save\": \"Uložiť časový plán\",\n  \"schedule_select_days\": \"Zvoliť dni\",\n  \"schedule_services\": \"Pozastavenie blokovania služby\",\n  \"schedule_services_desc\": \"Konfigurácia plánu pozastavenia filtra blokovania služieb\",\n  \"schedule_services_desc_client\": \"Konfigurácia plánu pozastavenia filtra blokovania služieb pre tohto klienta\",\n  \"schedule_time_all_day\": \"Celý deň\",\n  \"schedule_timezone\": \"Vyberte časové pásmo\",\n  \"schedule_to\": \"Do\",\n  \"served_from_cache_label\": \"Prevzaté z cache pamäte\",\n  \"service_name\": \"Názov služby\",\n  \"set_static_ip\": \"Nastaviť statickú IP adresu\",\n  \"settings\": \"Nastavenia\",\n  \"settings_custom\": \"Vlastné\",\n  \"settings_global\": \"Globálne\",\n  \"setup_config_to_enable_dhcp_server\": \"K zapnutiu DHCP servera je potrebné nastaviť konfiguráciu\",\n  \"setup_dns_notice\": \"Pre použitie <1>DNS-over-HTTPS</1> alebo <1>DNS-over-TLS</1>, potrebujete v nastaveniach AdGuard Home <0>nakonfigurovať šifrovanie</0>.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Použiť <1>{{address}}</1> reťazec.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Použiť <1>{{address}}</1> reťazec.\",\n  \"setup_dns_privacy_3\": \"<0>Tu je zoznam softvéru, ktorý môžete použiť.</0>\",\n  \"setup_dns_privacy_4\": \"Na zariadení so systémom iOS 14 alebo macOS Big Sur si môžete stiahnuť špeciálny súbor „.mobileconfig“, ktorý do nastavení DNS pridáva servery <highlight> DNS-over-HTTPS </highlight> alebo <highlight> DNS-over-TLS </highlight>.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 podporuje DNS-over-TLS natívne. Ak ho chcete konfigurovať, prejdite na Nastavenia → Sieť a internet → Pokročilé → Súkromné DNS a zadajte tam meno Vašej domény.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard pre Android</0> podporuje <1>DNS-over-HTTPS</1> a <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> pridáva <1>DNS-over-HTTPS</1> podporu pre Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Konfigurácia iOS a macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> podporuje funkciu <1>DNS-over-HTTPS</1>, ale aby ste ju mohli nakonfigurovať na používanie vlastného servera, musíte kvôli tomu vygenerovať značku <2>DNS Stamp</2>.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard pre  iOS</0> podporuje <1>DNS-over-HTTPS</1> a <1>DNS-over-TLS</1> nastavenie.\",\n  \"setup_dns_privacy_other_1\": \"Samotný AdGuard Home môže byť bezpečným DNS klientom na ľubovoľnej platforme.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> podporuje všetky známe bezpečné DNS protokoly.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> podporuje <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> podporuje <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Viac implementácií nájdete <0>tu</0> a <1>tu</1>.\",\n  \"setup_dns_privacy_other_title\": \"Ostatné implementácie\",\n  \"setup_guide\": \"Sprievodca nastavením\",\n  \"show_all_filter_type\": \"Zobraziť všetko\",\n  \"show_blocked_responses\": \"Zablokované\",\n  \"show_filtered_type\": \"Zobraziť filtrované\",\n  \"show_processed_responses\": \"Spracované\",\n  \"show_whitelisted_responses\": \"Obsiahnuté v bielej listine\",\n  \"sign_in\": \"Prihlásiť sa\",\n  \"sign_out\": \"Odhlásiť sa\",\n  \"source_label\": \"Zdroj\",\n  \"static_ip\": \"Statická IP adresa\",\n  \"static_ip_desc\": \"AdGuard Home je server, takže na správne fungovanie potrebuje statickú IP adresu. V opačnom prípade môže smerovač tomuto zariadeniu prideliť inú IP adresu.\",\n  \"statistics_clear\": \"Vynulovať štatistiku\",\n  \"statistics_clear_confirm\": \"Naozaj chcete vynulovať štatistiku?\",\n  \"statistics_cleared\": \"Štatistika bola úspešne vynulovaná\",\n  \"statistics_configuration\": \"Konfigurácia štatistiky\",\n  \"statistics_enable\": \"Zapnúť štatistiku\",\n  \"statistics_retention\": \"Štatistika za obdobie\",\n  \"statistics_retention_confirm\": \"Naozaj chcete zmeniť uchovávanie štatistík? Ak znížite hodnotu intervalu, niektoré údaje sa stratia\",\n  \"statistics_retention_desc\": \"Ak znížite hodnotu intervalu, niektoré údaje sa stratia\",\n  \"stats_adult\": \"Blokovaná stránka pre dospelých\",\n  \"stats_disabled\": \"Štatistiky boli vypnuté. Môžete ich zapnúť na <0>stránke nastavení</0>.\",\n  \"stats_disabled_short\": \"Štatistiky boli vypnuté\",\n  \"stats_malware_phishing\": \"Blokovaný škodlivý kód/pokus o podvod\",\n  \"stats_params\": \"Konfigurácia štatistiky\",\n  \"stats_query_domain\": \"Najčastejšie dopytované domény\",\n  \"subnet_error\": \"Adresy musia byť v spoločnej podsieti\",\n  \"sunday\": \"Nedeľa\",\n  \"sunday_short\": \"Ned\",\n  \"system_host_files\": \"Systémové súbory hosts\",\n  \"table_client\": \"Klient\",\n  \"table_name\": \"Meno\",\n  \"tags_desc\": \"Môžete vybrať značky, ktoré zodpovedajú klientovi. Zahrňte značky do pravidiel filtrácie, aby ste ich použili presnejšie. <0>Viac informácií</0>.\",\n  \"tags_title\": \"Tagy\",\n  \"test_upstream_btn\": \"Test upstreamov\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Automaticky (na základe farebnej schémy Vášho zariadenia)\",\n  \"theme_dark\": \"Tmavá\",\n  \"theme_dark_desc\": \"Tmavá téma\",\n  \"theme_light\": \"Svetlá\",\n  \"theme_light_desc\": \"Svetlá téma\",\n  \"thursday\": \"Štvrtok\",\n  \"thursday_short\": \"Štr\",\n  \"time_table_header\": \"Čas\",\n  \"top_blocked_domains\": \"Najčastejšie zablokované domény\",\n  \"top_clients\": \"Najčastejší klienti\",\n  \"top_upstreams\": \"Často požadované upstream servery\",\n  \"topline_expired_certificate\": \"Váš SSL certifikát vypršal. Aktualizujte <0>Nastavenia šifrovania</0>.\",\n  \"topline_expiring_certificate\": \"Váš SSL certifikát čoskoro vyprší. Aktualizujte <0>Nastavenia šifrovania</0>.\",\n  \"tracker_source\": \"Zdroj sledovania\",\n  \"try_again\": \"Skúste znova\",\n  \"ttl_cache_validation\": \"Minimálna hodnota TTL cache musí byť menšia alebo rovná maximálnej hodnote\",\n  \"tuesday\": \"Utorok\",\n  \"tuesday_short\": \"Uto\",\n  \"type_table_header\": \"Typ\",\n  \"unavailable_dhcp\": \"DHCP nie je dostupné\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home nemôže vo Vašom OS prevádzkovať DHCP server\",\n  \"unblock\": \"Odblokovať\",\n  \"unblock_all\": \"Odblokovať všetko\",\n  \"unblock_for_this_client_only\": \"Odblokovať len pre tohto klienta\",\n  \"unknown_filter\": \"Neznámy filter {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} je teraz k dispozícii! <0>Viac informácií nájdete tu</0>.\",\n  \"update_failed\": \"Automatická aktualizácia zlyhala. Prosím <a>sledujte postup</a> pre manuálnu aktualizáciu.\",\n  \"update_now\": \"Aktualizovať teraz\",\n  \"updated_custom_filtering_toast\": \"Vlastné pravidlá boli úspešne uložené\",\n  \"updated_save_search_toast\": \"Nastavenia Bezpečného vyhľadávania boli aktualizované\",\n  \"updated_upstream_dns_toast\": \"Upstream servery boli úspešne uložené\",\n  \"updates_checked\": \"K dispozícii je nová verzia aplikácie AdGuard Home\\n\",\n  \"updates_version_equal\": \"AdGuard Home je aktuálny\",\n  \"upstream\": \"Upstream server\",\n  \"upstream_dns\": \"Upstream DNS servery\",\n  \"upstream_dns_cache_configuration\": \"Konfigurácia cache pamäte DNS pre upstream\",\n  \"upstream_dns_client_desc\": \"Ak ponecháte toto pole prázdne, AdGuard Home použije servery nakonfigurované v <0>nastaveniach DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Konfigurované v {{path}}\",\n  \"upstream_dns_help\": \"Zadajte jednu adresu server na každý riadok. <a>Získajte viac informácií</a> o konfigurácii upstream DNS serverov.\",\n  \"upstream_parallel\": \"Používať paralelné dopyty na zrýchlenie súčasným dopytovaním všetkých upstream serverov súčasne.\",\n  \"upstream_timeout\": \"Časový limit pre upstream\",\n  \"upstream_timeout_desc\": \"Určuje počet sekúnd čakania na odpoveď z upstream servera\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Použiť  AdGuard službu Bezpečného prehliadania\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home skontroluje, či je doména blokovaná službou Bezpečného prehliadania. Na vykonanie kontroly použije API vyhľadávania priateľské k ochrane súkromia: na server je poslaná iba krátka predpona názvu domény SHA256 hash.\",\n  \"use_adguard_parental\": \"Použiť AdGuard službu Rodičovská kontrola\",\n  \"use_adguard_parental_hint\": \"AdGuard Home skontroluje, či doména obsahuje materiály pre dospelých. Používa rovnaké API priateľské k ochrane osobných údajov ako služba Bezpečného prehliadania.\",\n  \"use_private_ptr_resolvers_desc\": \"Riešenie dopytov PTR, SOA a NS pre domény ARPA obsahujúce súkromné IP adresy prostredníctvom súkromných upstream serverov, DHCP, /etc/hosts atď. Ak je vypnuté, AdGuard Home bude na všetky takéto dopyty odpovedať pomocou NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Použiť súkromné reverzné DNS resolvery\",\n  \"use_saved_key\": \"Použiť predtým uložený kľúč\",\n  \"username_label\": \"Meno používateľa\",\n  \"username_placeholder\": \"Zadajte meno používateľa\",\n  \"validated_with_dnssec\": \"Overené pomocou DNSSEC\",\n  \"version\": \"Verzia\",\n  \"version_request_error\": \"Kontrola aktualizácie zlyhala. Skontrolujte svoje internetové pripojenie.\",\n  \"wednesday\": \"Streda\",\n  \"wednesday_short\": \"Str\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/sl.json",
    "content": "{\n  \"access_allowed_desc\": \"Seznam CIDR-jev, naslovov IP ali <a>ID-jev odjemalcev</a>. Če ta seznam vsebuje vnose, bo AdGuard Home sprejel zahteve samo teh odjemalcev.\",\n  \"access_allowed_title\": \"Dovoljeni odjemalci\",\n  \"access_blocked_desc\": \"Ne gre zamenjati s filtri. AdGuard Home spusti poizvedbe DNS, ki se ujemajo s temi domenami, in te poizvedbe se niti ne pojavijo v dnevniku poizvedb. Določite lahko natančna imena domen, nadomestne znake ali pravila filtriranja URL-jev, npr. ustrezno \\\"example.org\\\", \\\"*.example.org\\\" ali \\\"|| example.org ^\\\".\",\n  \"access_blocked_title\": \"Prepovedane domene\",\n  \"access_desc\": \"Tukaj lahko nastavite pravila dostopa strežnika DNS AdGuard Home\",\n  \"access_disallowed_desc\": \"Seznam CIDR-jev, naslovov IP ali <a>ID-jev odjemalcev</a>. Če ta seznam vsebuje vnose, bo AdGuard Home zavrnil zahteve teh odjemalcev. To polje je prezrto, če so vnosi v dovoljenih odjemalcih.\",\n  \"access_disallowed_title\": \"Zavrnjeni odjemalci\",\n  \"access_settings_saved\": \"Nastavitve dostopa so uspešno shranjene\",\n  \"access_title\": \"Nastavitve dostopa\",\n  \"actions_table_header\": \"Akcij\",\n  \"add_allowlist\": \"Dodaj seznam dovoljenih\",\n  \"add_blocklist\": \"Dodaj seznam nedovoljenih\",\n  \"add_custom_list\": \"Dodaj seznam po meri\",\n  \"add_persistent_client\": \"Dodaj kot vztrajnega odjemalca\",\n  \"address\": \"Naslov\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home bo izpustil vse poizvedbe DNS iz tega odjemalca.\",\n  \"all_lists_up_to_date_toast\": \"Vsi seznami so že posodobljeni\",\n  \"all_queries\": \"Vse poizvedbe\",\n  \"allow_this_client\": \"Dovoli tega odjemalca\",\n  \"allowed\": \"Dovoljeno\",\n  \"anonymize_client_ip\": \"Anonimiziraj odjemalca IP\",\n  \"anonymize_client_ip_desc\": \"Ne shrani celotnega naslova IP odjemalca v dnevnikih ali statistiki\",\n  \"anonymizer_notification\": \"<0>Opomba:</0> Anonimizacija IP je omogočena. Onemogočite ga lahko v <1>Splošnih nastavitvah</1>.\",\n  \"answer\": \"Odgovor\",\n  \"apply_btn\": \"Uporabi\",\n  \"auto_clients_desc\": \"Informacije o naslovih IP naprav, ki uporabljajo ali bi lahko uporabljale AdGuard Home. Te informacije so zbrane iz več virov, vključno z datotekami gostiteljev, povratnim DNS-jem itd.\",\n  \"auto_clients_title\": \"Odjemalci izvajanja\",\n  \"autofix_warning_list\": \"To bo izvedlo naslednja opravila: <0>Deaktiviraj sistemski DNSStubListener</0> <0>Nastavi naslov strežnika DNS na 127.0.0.1</0> <0>Zamenjaj cilj simbolične povezave /etc/resolv.conf with /run/systemd/resolve/resolv.conf</0> <0>Zaustavi DNSStubListener (znova naloži storitev systemd-resolved)\",\n  \"autofix_warning_result\": \"Kot rezultat, bo vse zahteve DNS iz vašega sistema privzeto obdelal AdGuard Home.\",\n  \"autofix_warning_text\": \"Če kliknete 'Popravi', bo AdGuardHome konfiguriral vaš sistem za uporabo strežnika AdGuardHome DNS.\",\n  \"average_processing_time\": \"Povprečni čas obdelave\",\n  \"average_processing_time_hint\": \"Povprečni čas v milisekundah pri obdelavi zahteve DNS\",\n  \"average_upstream_response_time\": \"Povprečni gorvodni odzivni čas\",\n  \"back\": \"Nazaj\",\n  \"block\": \"Onemogoči\",\n  \"block_all\": \"Onemogoči vse\",\n  \"block_domain_use_filters_and_hosts\": \"Onemogoči domene s filtri in gostiteljskimi datotekami\",\n  \"block_for_this_client_only\": \"Onemogoči samo za tega odjemalca\",\n  \"block_services\": \"Onemogoči določene storitve\",\n  \"blocked_adult_websites\": \"Onemogočeno s Starševskim nadzorom\",\n  \"blocked_by\": \"<0>Onemogočeno s filtri</0>\",\n  \"blocked_by_cname_or_ip\": \"Onemogočeno s CNAME ali IP naslovom\",\n  \"blocked_by_response\": \"Onemogočeno s CNAME ali IP v odgovoru\",\n  \"blocked_response_ttl\": \"Zaviran odziv TTL\",\n  \"blocked_response_ttl_desc\": \"Določa, koliko sekund naj odjemalci predpomnijo filtrirane odgovore\",\n  \"blocked_safebrowsing\": \"Onemogočeno z 'Varnim brskanjem'\",\n  \"blocked_service\": \"Onemogočena storitev\",\n  \"blocked_services\": \"Onemogočene storitve\",\n  \"blocked_services_desc\": \"Omogoča hitro onemogočanje priljubljenih spletnih strani in storitev.\",\n  \"blocked_services_global\": \"Uporabi splošne onemogočene storitve\",\n  \"blocked_services_saved\": \"Onemogočene storitve so uspešno shranjene\",\n  \"blocked_threats\": \"Onemogočeno groženj\",\n  \"blocking_ipv4\": \"Onemogočanje IPv4\",\n  \"blocking_ipv4_desc\": \"IP naslov, ki mora biti vrnjen za onemogočeno zahtevo A\",\n  \"blocking_ipv6\": \"Onemogočanje IPv6\",\n  \"blocking_ipv6_desc\": \"IP naslov, ki mora biti vrnjen za onemogočeno zahtevo AAAA\",\n  \"blocking_mode\": \"Način zaviranja\",\n  \"blocking_mode_custom_ip\": \"IP po meri: Odziv z ročno nastavljenim naslovom IP\",\n  \"blocking_mode_default\": \"Privzeto: odgovori z ničelnim naslovom IP (0.0.0.0 za A; :: za AAAA), ko je onemogočen s pravilom v slogu Adblocka; odgovor z naslovom IP, določenim v pravilu, ko je onemogočen s pravilom /etc/hosts\",\n  \"blocking_mode_null_ip\": \"Prazen IP: Odziv z ničelnim naslovom IP (0.0.0.0 za A; :: za AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Odziv s kodo NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: Odziv s kodo REFUSED\",\n  \"blocklist\": \"Seznam nedovoljenih\",\n  \"bootstrap_dns\": \"Zagonski DNS strežniki\",\n  \"bootstrap_dns_desc\": \"Naslovi IP strežnikov DNS, ki se uporabljajo za razreševanje naslovov IP razreševalcev DoH/DoT, ki jih določite kot navzgor. Komentarji niso dovoljeni.\",\n  \"cache_cleared\": \"Predpomnilnik DNS je bil uspešno počiščen\",\n  \"cache_enabled\": \"Omogoči predpomnilnik\",\n  \"cache_enabled_desc\": \"Shranite odgovore DNS lokalno.\",\n  \"cache_optimistic\": \"Optimistično predpomnjenje\",\n  \"cache_optimistic_desc\": \"Poskrbi, da se AdGuard Home odzove iz predpomnilnika, tudi ko vnosi potečejo, in jih tudi poskusi osvežiti.\",\n  \"cache_size\": \"Velikost predpomnilnika\",\n  \"cache_size_desc\": \"Velikost predpomnilnika DNS (v bajtih).\",\n  \"cache_size_validation\": \"Velikost predpomnilnika mora biti večja od nič, ko je omogočeno.\",\n  \"cache_ttl_max_override\": \"Preglasi največji TTL\",\n  \"cache_ttl_max_override_desc\": \"Nastavite največjo vrednost življenjske dobe (sekunde) za vnose v predpomnilniku DNS.\",\n  \"cache_ttl_min_override\": \"Preglasi najmanjši TTL\",\n  \"cache_ttl_min_override_desc\": \"Podaljšajte kratke življenjske vrednosti (sekunde), prejete od gorvodnega strežnika pri predpomnjenju odgovorov DNS.\",\n  \"cancel_btn\": \"Prekliči\",\n  \"category_label\": \"Kategorija\",\n  \"check\": \"Preveri\",\n  \"check_client_id\": \"Identifikator odjemalca (ClientID ali naslov IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Preverite, ali je ime gostitelja filtrirano.\",\n  \"check_dhcp_servers\": \"Preveri strežnike DHCP\",\n  \"check_dns_record\": \"Izberite vrsto DNS zapisa\",\n  \"check_enter_client_id\": \"Vnesite identifikator odjemalca\",\n  \"check_hostname\": \"Ime gostitelja ali ime domene\",\n  \"check_ip\": \"IP naslovi: {{ip}}\",\n  \"check_not_found\": \"Ni najdeno na vašem seznamu filtrov\",\n  \"check_reason\": \"Razlog: {{reason}}\",\n  \"check_service\": \"Ime storitve: {{service}}\",\n  \"check_title\": \"Preveri filtriranje\",\n  \"check_updates_btn\": \"Preveri obstoj posodobitev\",\n  \"check_updates_now\": \"Preveri obstoj posodobitev zdaj\",\n  \"choose_allowlist\": \"Izberite sezname dovoljenih\",\n  \"choose_blocklist\": \"Izberite sezname za zaviranje\",\n  \"choose_from_list\": \"Izberi s seznama\",\n  \"city\": \"Mesto\",\n  \"clear_cache\": \"Počisti predpomnilnik\",\n  \"click_to_view_queries\": \"Kliknite za prikaz poizvedb\",\n  \"client_add\": \"Dodaj odjemalca\",\n  \"client_added\": \"Odjemalec \\\"{{key}}\\\" je bil uspešno dodan\",\n  \"client_blocked\": \"Odjemalec \\\"{{ip}}\\\" je uspešno onemogočen\",\n  \"client_confirm_block\": \"Ali ste prepričani, da želite onemogočiti odjemalca \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Ali ste prepričani, da želite izbrisati odjemalca \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Ali ste prepričani, da želite omogočiti odjemalca \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Odjemalec \\\"{{key}}\\\" je bil uspešno izbrisan\",\n  \"client_details\": \"Podatki o odjemalcu\",\n  \"client_edit\": \"Uredi odjemalca\",\n  \"client_global_settings\": \"Uporabi splošne nastavitve\",\n  \"client_id\": \"ID odjemalca\",\n  \"client_id_desc\": \"Odjemalce je mogoče identificirati s ClientID. Več o tem, kako prepoznati odjemalce, preberite <a>tukaj</a>.\",\n  \"client_id_placeholder\": \"Vnesite ID odjemalca\",\n  \"client_identifier\": \"Identifikator\",\n  \"client_identifier_desc\": \"Odjemalce je mogoče prepoznati po naslovu IP, CIDR, naslovu MAC ali ID-ju (lahko se uporablja za DoT/DoH/DoQ). <0>Tukaj</0> lahko izveste več o prepoznavanju odjemalcev.\",\n  \"client_name\": \"Odjemalec {{id}}\",\n  \"client_new\": \"Nov odjemalec\",\n  \"client_settings\": \"Nastavitve odjemalca\",\n  \"client_table_header\": \"Odjemalec\",\n  \"client_unblocked\": \"Odjemalec \\\"{{ip}}\\\" je uspešno omogočen\",\n  \"client_updated\": \"Odjemalec \\\"{{key}}\\\" je bil uspešno posodobljen\",\n  \"clients_desc\": \"Nastavite trajne zapise odjemalca za povezane naprave z AdGuard Home\",\n  \"clients_not_found\": \"Odjemalcev ni bilo mogoče najti\",\n  \"clients_title\": \"Trajni odjemalci\",\n  \"compact\": \"Stisni\",\n  \"config_successfully_saved\": \"Nastavitve so uspešno shranjene\",\n  \"configure\": \"Konfiguriraj\",\n  \"confirm_dns_cache_clear\": \"Ali ste prepričani, da želite počistiti predpomnilnik DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home bo konfiguriral {{ip}}, da bo postal vas statičen IP naslov. Ali želite nadaljevati?\",\n  \"copyright\": \"Avtorske pravice\",\n  \"country\": \"Dežela\",\n  \"custom_filter_rules\": \"Pravila filtriranja po meri\",\n  \"custom_filter_rules_hint\": \"V vrstico vnesite eno pravilo. Uporabite lahko pravila zaviranja oglasov ali sintakso gostiteljskih datotek.\",\n  \"custom_filtering_rules\": \"Pravila filtriranja po meri\",\n  \"custom_ip\": \"IP po meri\",\n  \"custom_retention_input\": \"Vnesite zadrževanje v urah\",\n  \"custom_rotation_input\": \"Vnesite rotacijo v urah\",\n  \"dashboard\": \"Nadzorna plošča\",\n  \"date\": \"Datum\",\n  \"default\": \"Privzeto\",\n  \"delete_confirm\": \"Ali ste prepričani, da želite izbrisati \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Izbriši\",\n  \"descr\": \"Opis\",\n  \"details\": \"Podrobnosti\",\n  \"dhcp_add_static_lease\": \"Dodaj statičen najem\",\n  \"dhcp_config_saved\": \"Nastavitve DHCP so bile uspešno shranjena\",\n  \"dhcp_description\": \"Če vaš usmerjevalnik ne nudi nastavitev DHCP, lahko uporabite AdGuardov vgrajen DHCP strežnik.\",\n  \"dhcp_disable\": \"Onemogoči strežnik DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Vaš sistem uporablja dinamično nastavitev naslova IP kartice <0>{{interfaceName}}</0>. Za uporabo strežnika DHCP morate nastaviti statični naslov IP. Vaš trenutni naslov IP je<0>{{ipAddress}}</0>. AdGuard Home bo samodejno nastavil ta naslov IP kot statičen, če pritisnete gumb 'Omogoči strežnik DHCP'.\",\n  \"dhcp_edit_static_lease\": \"Uredi statični najem\",\n  \"dhcp_enable\": \"Omogoči strežnik DHCP\",\n  \"dhcp_error\": \"AdGuard Home ni mogel ugotoviti, ali je v omrežju še en aktivni strežnik DHCP\",\n  \"dhcp_form_gateway_input\": \"IP prehoda\",\n  \"dhcp_form_lease_input\": \"Trajanje najema\",\n  \"dhcp_form_lease_title\": \"Čas najema DHCP (v sekundah)\",\n  \"dhcp_form_range_end\": \"Konec razpona\",\n  \"dhcp_form_range_start\": \"Začetek razpona\",\n  \"dhcp_form_range_title\": \"Razpon naslovov IP\",\n  \"dhcp_form_subnet_input\": \"Maska podomrežja\",\n  \"dhcp_found\": \"V omrežju je bil najden aktivni DHCP strežnik. Vgrajenega AdGuardovega DHCP strežnika ni varno vključiti.\",\n  \"dhcp_hardware_address\": \"Naslov strojne opreme\",\n  \"dhcp_interface_select\": \"Izberite DHCP vmesnik\",\n  \"dhcp_ip_addresses\": \"IP naslovi\",\n  \"dhcp_ipv4_settings\": \"Nastavitve DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Nastavitve DHCP IPv6\",\n  \"dhcp_lease_added\": \"Statičen najem \\\"{{key}}\\\" je uspešno dodan\",\n  \"dhcp_lease_deleted\": \"Statičen najem \\\"{{key}}\\\" je uspešno izbrisan\",\n  \"dhcp_lease_updated\": \"Statični najem \\\"{{key}}\\\" je uspešno posodobljen\",\n  \"dhcp_leases\": \"Najemi DHCP\",\n  \"dhcp_leases_not_found\": \"Ni najdenih najemov DHCP\",\n  \"dhcp_new_static_lease\": \"Nov statični najem\",\n  \"dhcp_not_found\": \"Varno je omogočiti vgrajeni strežnik DHCP, ker AdGuard Home v omrežju ni našel nobenega aktivnega strežnika DHCP. Vendar morate to ponovno preveriti ročno, saj samodejno sondiranje trenutno ne zagotavlja 100-odstotnega jamstva.\",\n  \"dhcp_reset\": \"Ali ste prepričani, da želite ponastaviti nastavitve DHCP?\",\n  \"dhcp_reset_leases\": \"Ponastavi vse najeme\",\n  \"dhcp_reset_leases_confirm\": \"Ali ste prepričani, da želite ponastaviti vse najeme?\",\n  \"dhcp_reset_leases_success\": \"Najemi DHCP so bili uspešno ponastavljeni\",\n  \"dhcp_settings\": \"Nastavitve DHCP\",\n  \"dhcp_static_ip_error\": \"Za uporabo strežnika DHCP mora biti nastavljen statični naslov IP. AdGuard Home ni uspel ugotoviti, ali je ta omrežni vmesnik nastavljen s statičnim naslovom IP. Prosimo, nastavite statični naslov IP ročno.\",\n  \"dhcp_static_leases\": \"DHCP statični najemi\",\n  \"dhcp_static_leases_not_found\": \"Ni najdenih statičnih najemov DHCP\",\n  \"dhcp_table_expires\": \"Poteče\",\n  \"dhcp_table_hostname\": \"Ime gostitelja\",\n  \"dhcp_title\": \"Strežnik DHCP (poskusno!)\",\n  \"dhcp_warning\": \"Če želite vseeno omogočiti strežnik DHCP, se prepričajte, da v vašem omrežju ni nobenega drugega aktivnega strežnika DHCP, saj lahko to prekine internetno povezljivost naprav v omrežju!\",\n  \"disable_for_hours\": \"Za {{count}} uro\",\n  \"disable_for_hours_plural\": \"Za {{count}} ur\",\n  \"disable_for_minutes\": \"Za {{count}} minuto\",\n  \"disable_for_minutes_plural\": \"Za {{count}} minut\",\n  \"disable_for_seconds\": \"Za {{count}} sekundo\",\n  \"disable_for_seconds_plural\": \"Za {{count}} sekund\",\n  \"disable_ipv6\": \"Onemogoči reševanje naslovov IPv6\",\n  \"disable_ipv6_desc\": \"Spustite vse poizvedbe DNS za naslove IPv6 (tip AAAA) in odstranite namige IPv6 iz odgovorov HTTPS.\",\n  \"disable_notify_for_hours\": \"Onemogoči zaščito za {{count}} uro\",\n  \"disable_notify_for_hours_plural\": \"Onemogoči zaščito za {{count}} ur\",\n  \"disable_notify_for_minutes\": \"Onemogoči zaščito za {{count}} minuto\",\n  \"disable_notify_for_minutes_plural\": \"Onemogoči zaščito za {{count}} minut\",\n  \"disable_notify_for_seconds\": \"Onemogoči zaščito za {{count}} sekundo\",\n  \"disable_notify_for_seconds_plural\": \"Onemogoči zaščito za {{count}} sekund\",\n  \"disable_notify_until_tomorrow\": \"Onemogoči zaščito do jutri\",\n  \"disable_protection\": \"Onemogoči zaščito\",\n  \"disable_rewrites\": \"Onemogoči pravila prepisovanja\",\n  \"disable_until_tomorrow\": \"Do jutri\",\n  \"disabled\": \"Onemogočeno\",\n  \"disabled_dhcp\": \"Strežnik DHCP je onemogočen\",\n  \"disabled_filtering_toast\": \"Onemogočeno filtriranje\",\n  \"disabled_parental_toast\": \"Onemogočen starševski nadzor\",\n  \"disabled_protection\": \"Zaščita je onemogočena\",\n  \"disabled_safe_browsing_toast\": \"Onemogočeno varno brskanje\",\n  \"disabled_safe_search_toast\": \"Onemogočeno Varno iskanje\",\n  \"disallow_this_client\": \"Onemogoči tega odjemalca\",\n  \"dns_addresses\": \"DNS naslovi\",\n  \"dns_allowlists\": \"Seznam dovoljenih DNS\",\n  \"dns_allowlists_desc\": \"Domene i dovoljenih seznamov DNS bodo dovoljene, tudi če so na katerem koli od seznamov nedovoljenih.\",\n  \"dns_blocklists\": \"Seznam nedovoljenih DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home bo onemogočil domene, ki ustrezajo seznamom.\",\n  \"dns_cache_config\": \"Konfiguracija strežnika DNS\",\n  \"dns_cache_config_desc\": \"Tu lahko nastavite predpomnilnik DNS\",\n  \"dns_cache_size\": \"Velikost predpomnilnika DNS, v bajtih\",\n  \"dns_config\": \"Konfiguracija strežnika DNS\",\n  \"dns_over_https\": \"DNS-prek-HTTPS\",\n  \"dns_over_quic\": \"DNS-prek-QIUC\",\n  \"dns_over_tls\": \"DNS-prek-TLS\",\n  \"dns_privacy\": \"Zasebnost DNS\",\n  \"dns_providers\": \"Tukaj je  <0>seznam znanih ponudnikov DNS</0>, med katerimi lahko izbirate.\",\n  \"dns_query\": \"Poizvedbe DNS\",\n  \"dns_rewrites\": \"Prepisovanja NDS\",\n  \"dns_settings\": \"Nastavitve DNS\",\n  \"dns_start\": \"Zaganja se strežnik DNS\",\n  \"dns_status_error\": \"Napaka pri preverjanju stanja strežnika DNS\",\n  \"dns_test_not_ok_toast\": \"Ni mogoče uporabiti: strežnika \\\"{{key}}\\\". Preverite, ali ste ga pravilno napisali\",\n  \"dns_test_ok_toast\": \"Navedeni strežniki DNS delujejo pravilno\",\n  \"dns_test_parsing_error_toast\": \"Razdelek {{section}}: vrstica {{line}}: ni bilo mogoče uporabiti, preverite, ali ste ga pravilno zapisali\",\n  \"dns_test_warning_toast\": \"Upstream \\\"{{key}}\\\" se ne odziva na testne zahteve in morda ne deluje pravilno\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Omogoči DNSSEC\",\n  \"dnssec_enable_desc\": \"V prihajajočih poizvedbah DNS nastavite zastavico DNSSEC in preverite rezultat (potreben je razreševalnik z omogočenim DNSSEC).\",\n  \"domain\": \"Domena\",\n  \"domain_desc\": \"Vnesite ime domene ali nadomestni znak, ki ga želite prepisati.\",\n  \"domain_name_table_header\": \"Ime domene\",\n  \"domain_or_client\": \"Domena ali odjemalec\",\n  \"down\": \"Navzdol\",\n  \"download_mobileconfig\": \"Prenesi nastavitveno datoteko\",\n  \"download_mobileconfig_doh\": \"Prenos .mobileconfig za DNS-preko-HTTPS\",\n  \"download_mobileconfig_dot\": \"Prenos .mobileconfig za DNS-preko-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Uredi seznam dovoljenih\",\n  \"edit_blocklist\": \"Uredi seznam nedovoljenih\",\n  \"edit_table_action\": \"Uredi\",\n  \"edns_cs_desc\": \"Dodaj možnost podomrežja odjemalca EDNS (ECS) zahtevam v gorvodnem toku in zabeleži vrednosti, ki jih pošljejo odjemalci, v dnevnik poizvedb.\",\n  \"edns_enable\": \"Omogoči odjemalsko podomrežje EDNS\",\n  \"edns_use_custom_ip\": \"Uporabi IP po meri za EDNS\",\n  \"edns_use_custom_ip_desc\": \"Dovoli uporabo naslova IP po meri za EDNS\",\n  \"elapsed\": \"Potekla\",\n  \"empty_response_status\": \"Prazno\",\n  \"enable_protection\": \"Omogoči zaščito\",\n  \"enable_protection_timer\": \"Zaščita bo omogočena ob {{time}}\",\n  \"enable_rewrites\": \"Omogoči pravila prepisovanja\",\n  \"enable_upstream_dns_cache\": \"Omogoči predpomnjenje nastavitev gorvodnega DNS po meri tega odjemalca\",\n  \"enabled_dhcp\": \"Strežnik DHCP je omogočen\",\n  \"enabled_filtering_toast\": \"Omogočeno filtriranje\",\n  \"enabled_parental_toast\": \"Omogočen starševski nadzor\",\n  \"enabled_protection\": \"Zaščita je omogočena\",\n  \"enabled_safe_browsing_toast\": \"Omogočeno varno brskanje\",\n  \"enabled_save_search_toast\": \"Omogočeno Varno iskanje\",\n  \"enabled_table_header\": \"Omogočeno\",\n  \"encryption_certificate_path\": \"Pot digitalnega potrdila\",\n  \"encryption_certificates\": \"Digitalna potrdila\",\n  \"encryption_certificates_desc\": \"Za uporabo šifriranja morate za svojo domeno zagotoviti veljavno verigo potrdil SSL. Brezplačno digitalno potrdilo lahko dobite na <0>{{link}}</0> ali pa ga kupite pri enem od   zaupanja vrednih overiteljev.\\n\\n\",\n  \"encryption_certificates_input\": \"Tukaj kopirajte/prilepite PEM šifrirana digitalna potrdila.\",\n  \"encryption_certificates_source_content\": \"Prilepi vsebino digitalnih potrdil\",\n  \"encryption_certificates_source_path\": \"Nastavi pot datoteke digitalnih potrdil\",\n  \"encryption_chain_invalid\": \"Veriga digitalih potrdil ni veljavna\",\n  \"encryption_chain_valid\": \"Veriga digitalih potrdil je veljavna\",\n  \"encryption_config_saved\": \"Nastavitve šifriranja so shranjene\",\n  \"encryption_desc\": \"Podpora za šifriranje (HTTPS/TLS) za DNS in skrbniški spletni vmesnik\",\n  \"encryption_doq\": \"DNS-prek-vrat QUIC\",\n  \"encryption_doq_desc\": \"Če so nastavljena ta vrata bo AdGuard Home na teh vratih zagnal strežnik DNS-prek-QUIC. \",\n  \"encryption_dot\": \"Vrata DNS-prek-TLS\",\n  \"encryption_dot_desc\": \"Če so ta vrata konfigurirana, bo AdGuard Home na teh vratih zagnal DNS-prek-TLS strežnika.\",\n  \"encryption_enable\": \"Omogoči šifriranje (HTTPS, DNS-prek-HTTPS in DNS-prek-TLS)\",\n  \"encryption_enable_desc\": \"Če je omogočeno šifriranje, bo skrbniški vmesnik AdGuard Home deloval prek HTTPS, strežnik DNS pa bo poslušal zahteve prek DNS-prek-HTTPS in DNS-prek-TLS.\",\n  \"encryption_expire\": \"Poteče\",\n  \"encryption_hostnames\": \"Imena gostiteljev\",\n  \"encryption_https\": \"Vrata HTTPS\",\n  \"encryption_https_desc\": \"Če so vrata HTTPS konfigurirana, bo skrbniški vmesnik AdGuard Home dostopen prek protokola HTTPS, prav tako pa bo zagotovil DNS-prek-HTTPS na mestu '/dns-query'.\",\n  \"encryption_issuer\": \"Izdajatelj\",\n  \"encryption_key\": \"Zasebni ključ\",\n  \"encryption_key_input\": \"Tukaj kopirajte/prilepite PEM-kodiran zasebni ključ za vaše digitalno potrdilo.\",\n  \"encryption_key_invalid\": \"To je neveljaven zasebni ključ {{type}}\",\n  \"encryption_key_source_content\": \"Prilepi vsebino zasebnega ključa\",\n  \"encryption_key_source_path\": \"Nastavi pot do datoteke zasebnega ključa\",\n  \"encryption_key_valid\": \"To je veljaven zasebni ključ {{type}}\",\n  \"encryption_plain_dns_desc\": \"Navaden DNS je privzeto omogočen. Lahko ga onemogočite, da vse naprave prisilite k uporabi šifriranega DNS-ja. Če želite to narediti, morate omogočiti vsaj en šifriran protokol DNS\",\n  \"encryption_plain_dns_enable\": \"Omogoči navaden DNS\",\n  \"encryption_plain_dns_error\": \"Da onemogočite navaden DNS, omogočite vsaj en šifriran protokol DNS\",\n  \"encryption_private_key_path\": \"Pot zasebnega ključa\",\n  \"encryption_redirect\": \"Samodejno preusmeri na HTTPS\",\n  \"encryption_redirect_desc\": \"Če je označeno, vas bo AdGuard Home samodejno preusmeril iz naslovov HTTP na naslove HTTPS.\",\n  \"encryption_reset\": \"Ali ste prepričani, da želite ponastaviti nastavitve šifriranja?\",\n  \"encryption_server\": \"Ime strežnika\",\n  \"encryption_server_desc\": \"Če je nastavljeno, AdGuard Home zazna ClientID-je, odgovori na poizvedbe DDR in izvede dodatna preverjanja povezave. Če ni nastavljeno, so te funkcije onemogočene. Ujemati se mora z enim od imen DNS v potrdilu.\",\n  \"encryption_server_enter\": \"Vnesite ime vaše domene\",\n  \"encryption_settings\": \"Nastavitve šifriranja\",\n  \"encryption_status\": \"Stanje\",\n  \"encryption_subject\": \"Predmet\",\n  \"encryption_title\": \"Šifriranje\",\n  \"encryption_warning\": \"Opozorilo\",\n  \"enforce_safe_search\": \"Uporabi Varno iskanje\",\n  \"enforce_save_search_hint\": \"AdGuard Home bo izvajal varno iskanje v naslednjih iskalnikih: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Prisilno varno iskanje\",\n  \"enter_cache_size\": \"Vnesite velikost predpomnilnika (v bajtih)\",\n  \"enter_cache_ttl_max_override\": \"Vnesite največji TTL (v sekundah)\",\n  \"enter_cache_ttl_min_override\": \"Vnesite najmanjši TTL (v sekundah)\",\n  \"enter_name_hint\": \"Vnesite ime\",\n  \"enter_url_or_path_hint\": \"Vnesite URL ali absolutno pot seznama\",\n  \"enter_valid_allowlist\": \"Vnesite veljaven URL naslov seznama dovoljenih.\",\n  \"enter_valid_blocklist\": \"Vnesite veljaven URL naslov seznama nedovoljenih.\",\n  \"error_details\": \"Podrobnosti o napaki\",\n  \"example_comment\": \"! Tukaj je komentar.\",\n  \"example_comment_hash\": \"# Tudi komentar.\",\n  \"example_comment_meaning\": \"samo komentar;\",\n  \"example_meaning_filter_block\": \"onemogoči dostop do domene example.org in vseh njenih poddomen;\",\n  \"example_meaning_filter_whitelist\": \"omogoči dostop do domene example.org in vseh njenih poddomen;\",\n  \"example_meaning_host_block\": \"odgovori z 127.0.0.1 na primer.org (vendar ne za njegove poddomene);\",\n  \"example_multiple_upstreams_reserved\": \"več gorvodnih <0>za določene domene</0>;\",\n  \"example_regex_meaning\": \"onemogoča dostop do domen, ki se ujemajo z določenim regularnim izrazom.\",\n  \"example_rewrite_domain\": \"prepiše odgovore samo za to ime domene.\",\n  \"example_rewrite_wildcard\": \"prepiše odgovore za vse poddomene <0>example.org</0>.\",\n  \"example_upstream_comment\": \"komentar.\",\n  \"example_upstream_doh\": \"šifriran <0>DNS-prek-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"šifriran DNS prek HTTPS s prisilnim <0>HTTP/3</0> in brez povratne možnosti za HTTP/2 ali nižjim;\",\n  \"example_upstream_doq\": \"šifriran <0>DNS-prek-QUIC</0>;\",\n  \"example_upstream_dot\": \"šifriran <0>DNS-prek-TLS</0>;\",\n  \"example_upstream_regular\": \"redni DNS (nad UDP);\",\n  \"example_upstream_regular_port\": \"redni DNS (nad UDP, z vrati);\",\n  \"example_upstream_reserved\": \"gorvodni <0>za določene domene</0>;\",\n  \"example_upstream_sdns\": \"lahko uporabite <0>DNS Žige</0> za reševalce <1>DNSCrypt</1> ali <2>DNS-prek-HTTPS</2>;\",\n  \"example_upstream_tcp\": \"redni DNS (nad TCP);\",\n  \"example_upstream_tcp_hostname\": \"redni DNS (nad TCP, ime gostitelja);\",\n  \"example_upstream_tcp_port\": \"redni DNS (nad TCP, z vrati);\",\n  \"example_upstream_udp\": \"redni DNS (nad UDP, ime gostitelja);\",\n  \"examples_title\": \"Primeri\",\n  \"fallback_dns_desc\": \"Seznam rezervnih strežnikov DNS, ki se uporabljajo, ko se gorvodni strežniki DNS ne odzivajo. Sintaksa je enaka kot v zgornjem gorvodnem polju.\",\n  \"fallback_dns_placeholder\": \"Vnesite en rezervni strežnik DNS na vrstico\",\n  \"fallback_dns_title\": \"Rezervni strežniki DNS\",\n  \"faq\": \"Pogosta vprašanja in odgovori (FAQ)\",\n  \"fastest_addr\": \"Najhitrejši IP naslov\",\n  \"fastest_addr_desc\": \"Počakajte na odgovore od <b>vseh</b> DNS strežnikov, izmerite hitrost TCP povezave za vsak strežnik in vrnite naslov IP strežnika z najhitrejšo povezavo.<br/>Ta način lahko znatno upočasni poizvedbe DNS, če eden ali več gorvodnih strežnikov ne odgovarja. Prepričajte se, da so vaši gorvodni strežniki stabilni in da je vaš gorvodni časovni zahtevek nizek.\",\n  \"filter\": \"Filtriraj\",\n  \"filter_added_successfully\": \"Seznam je bil uspešno dodan\",\n  \"filter_allowlist\": \"OPOZORILO: S to akcijo bo pravilo \\\"{{disallowed_rule}}\\\" izključeno s seznama dovoljenih odjemalcev.\",\n  \"filter_category_general\": \"Splošno\",\n  \"filter_category_general_desc\": \"Seznami, ki zavirajo sledenje in oglaševanje na večini naprav\",\n  \"filter_category_other\": \"Drugo\",\n  \"filter_category_other_desc\": \"Drugi seznami za zaviranje\",\n  \"filter_category_regional\": \"Področno\",\n  \"filter_category_regional_desc\": \"Seznami, ki so osredotočeni na področne oglase in strežnike za sledenje\",\n  \"filter_category_security\": \"Varnost\",\n  \"filter_category_security_desc\": \"Seznami, posebej zasnovani za onemogočanje zlonamernih domen, domen z lažnim predstavljanjem in prevarami\",\n  \"filter_removed_successfully\": \"Seznam je bil uspešno odstranjen\",\n  \"filter_updated\": \"Filter je bil uspešno posodobljen\",\n  \"filtered\": \"Filtrirano\",\n  \"filtered_custom_rules\": \"Filtrirano s pravili filtriranja po meri\",\n  \"filtering_rules_learn_more\": \"<0>Več o</0> ustvarjanju lastnih seznamov gostiteljev.\",\n  \"filters\": \"Filtri\",\n  \"filters_and_hosts_hint\": \"AdGuard Home razume osnovna pravila zaviranja oglasov in sintakso datotek gostiteljev.\",\n  \"filters_block_toggle_hint\": \"Pravila zaviranja lahko nastavite v nastavitvah <a>Filtri</a>.\",\n  \"filters_configuration\": \"Nastavitve filtrov\",\n  \"filters_enable\": \"Omogoči filtre\",\n  \"filters_interval\": \"Interval posodabljanja filtrov\",\n  \"fix\": \"Popravi\",\n  \"for_last_days\": \"zadnjega {{count}} dne\",\n  \"for_last_days_plural\": \"zadnjih {{count}} dni\",\n  \"for_last_hours\": \"za zadnjo {{count}} uro\",\n  \"for_last_hours_plural\": \"za zadnjih {{count}} ur\",\n  \"forgot_password\": \"Izgubljeno geslo?\",\n  \"forgot_password_desc\": \"Prosimo, sledite <0>tem korakom</0>, da ustvarite novogeslo za uporabniški računa.\",\n  \"form_add_id\": \"Dodaj identifikatorja\",\n  \"form_answer\": \"Vnesite IP naslov ali ime domene\",\n  \"form_client_name\": \"Vnesite ime odjemalca\",\n  \"form_domain\": \"Vnesite domeno ali nadomestni znak\",\n  \"form_enter_blocked_response_ttl\": \"Vnesite TTL zaviranega odgovora (sekunde)\",\n  \"form_enter_host\": \"Vnesite ime gostitelja\",\n  \"form_enter_hostname\": \"Vnesite ime gostitelja\",\n  \"form_enter_id\": \"Vnesi identifikatorja\",\n  \"form_enter_ip\": \"Vnesite IP\",\n  \"form_enter_mac\": \"Vnesite MAC\",\n  \"form_enter_rate_limit\": \"Vnesite omejitev hitrosti\",\n  \"form_enter_rate_limit_subnet_len\": \"Vnesite dolžino predpone podomrežja za omejitev hitrosti\",\n  \"form_enter_subnet_ip\": \"V podomrežje \\\"{{cidr}}\\\" vnesite naslov IP\",\n  \"form_enter_upstream_timeout\": \"Vnesite čas čakanja za upstream strežnik v sekundah\",\n  \"form_error_answer_format\": \"Neveljavna oblika odgovora\",\n  \"form_error_client_id_format\": \"ID odjemalca mora vsebovati samo številke, male črke in vezaje\",\n  \"form_error_domain_format\": \"Neveljavna oblika domene\",\n  \"form_error_equal\": \"Ne sme biti enako\",\n  \"form_error_gateway_ip\": \"Najem ne more imeti naslova IP prehoda\",\n  \"form_error_ip4_format\": \"Neveljaven naslov IPv4.\",\n  \"form_error_ip4_gateway_format\": \"Neveljaven naslov IPv4 prehoda\",\n  \"form_error_ip6_format\": \"Neveljaven naslov IPv6\",\n  \"form_error_ip_format\": \"Neveljaven naslov IP\",\n  \"form_error_mac_format\": \"Neveljaven naslov MAC\",\n  \"form_error_password\": \"Geslo se ne ujema\",\n  \"form_error_password_length\": \"Geslo mora vsebovati od {{min}} do {{max}} znakov\",\n  \"form_error_port\": \"Vnesite veljavno številko vrat\",\n  \"form_error_port_range\": \"Vnesite številko vrat v razponu med 80-65535\",\n  \"form_error_port_unsafe\": \"Nevarna vrata\",\n  \"form_error_positive\": \"Mora biti večja od 0\",\n  \"form_error_required\": \"Zahtevano polje.\",\n  \"form_error_server_name\": \"Neveljavno ime strežnika\",\n  \"form_error_subnet\": \"Podomrežje \\\"{{cidr}}\\\" ne vsebuje naslova IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Neveljavna oblika URL naslova\",\n  \"form_error_url_or_path_format\": \"Neveljaven URL ali absolutna pot seznama\",\n  \"form_select_tags\": \"Izberite odjemalske oznake\",\n  \"found_in_known_domain_db\": \"Najdeno v zbirki podatkov znanih domen.\",\n  \"friday\": \"Petek\",\n  \"friday_short\": \"Pet\",\n  \"gateway_or_subnet_invalid\": \"Maska podomrežja ni veljavna\",\n  \"general_settings\": \"Splošne nastavitve\",\n  \"general_statistics\": \"Splošna statistika\",\n  \"get_started\": \"Začnimo\",\n  \"greater_range_start_error\": \"Mora biti večji od začetka razpona\",\n  \"homepage\": \"Domača stran\",\n  \"host_whitelisted\": \"Gostitelj je na seznamu dovoljenih\",\n  \"ignore_domains\": \"Prezrte domene (ločene z novo vrstico)\",\n  \"ignore_domains_desc_query\": \"Poizvedbe, ki ustrezajo tem pravilom, se ne zapišejo v dnevnik poizvedb\",\n  \"ignore_domains_desc_stats\": \"Poizvedbe, ki ustrezajo tem pravilom, se ne zapišejo v statistiko\",\n  \"ignore_domains_title\": \"Prezrte domene\",\n  \"ignore_query_log\": \"Ignorirajte tega odjemalca v dnevniku poizvedb\",\n  \"ignore_statistics\": \"Ignoriranje tega odjemalca v statistiki\",\n  \"install_auth_confirm\": \"Potrdite geslo\",\n  \"install_auth_desc\": \"Nastavljeno mora biti preverjanje pristnosti gesla za skrbniški spletni vmesnik AdGuard Home. Tudi če je AdGuard Home dostopen samo v vašem lokalnem omrežju, je še vedno pomembno, da ga zaščitite pred neomejenim dostopom.\",\n  \"install_auth_password\": \"Geslo\",\n  \"install_auth_password_enter\": \"Vnesite geslo\",\n  \"install_auth_title\": \"Preverjanje pristnosti\",\n  \"install_auth_username\": \"Uporabniško ime\",\n  \"install_auth_username_enter\": \"Vnesite uporabniško ime\",\n  \"install_devices_address\": \"AdGuard Home strežnik DNS posluša naslednje naslove\",\n  \"install_devices_android_list_1\": \"Na začetnem zaslonu menija Android tapnite 'Nastavitve'.\",\n  \"install_devices_android_list_2\": \"V meniju tapnite na 'Wi-Fi'. Prikazal se bo seznam vseh razpoložljivih omrežij (nemogoče je nastaviti DNS po meri za mobilno povezavo).\",\n  \"install_devices_android_list_3\": \"Dolgo pritisnite na omrežje, s katerim ste povezani, in tapnite 'Spremeni omrežje'.\",\n  \"install_devices_android_list_4\": \"Na nekaterih napravah boste morda morali potrditi polje 'Napredno', za prikaz dodatnih nastavitev. Če želite prilagoditi nastavitve DNS za Android, morate nastavitve IP preklopiti z DHCP na Statični.\",\n  \"install_devices_android_list_5\": \"Spremeni nastavitev vrednosti DNS 1 in DNS 2 na naslove strežnikov AdGuard Home.\",\n  \"install_devices_desc\": \"Če želite, da AdGuard Home začne delovati, morate konfigurirati vaše naprave, da jih bo uporabljal.\",\n  \"install_devices_ios_list_1\": \"Na začetnem zaslonu izberite Nastavitve.\",\n  \"install_devices_ios_list_2\": \"V levem meniju izberite Wi-Fi (nemogoče je konfigurirati DNS za mobilna omrežja).\",\n  \"install_devices_ios_list_3\": \"Tapnite na ime trenutno aktivnega omrežja.\",\n  \"install_devices_ios_list_4\": \"V polje DNS vnesite vaše naslove AdGuard Home strežnika.\",\n  \"install_devices_macos_list_1\": \"Kkliknite ikono Apple in pojdite na Sistemske nastavitve.\",\n  \"install_devices_macos_list_2\": \"Kliknite na 'Omrežje'.\",\n  \"install_devices_macos_list_3\": \"Izberite prvo povezavo na seznamu in kliknite na 'Napredno'.\",\n  \"install_devices_macos_list_4\": \"Izberite zavihek DNS in vnesite vaše naslove AdGuard Home strežnika.\",\n  \"install_devices_router\": \"Usmerjevalnik\",\n  \"install_devices_router_desc\": \"Ta namestitev samodejno pokriva vse naprave, povezane z vašim domačim usmerjevalnikom, zato vam jih ni treba ročno nastaviti.\",\n  \"install_devices_router_list_1\": \"Odprite nastavitve vašega usmerjevalnika. Običajno lahko imate dostop do njega iz brskalnika prek URL naslova, kot je http://192.168.0.1/ ali http://192.168.1.1/. Morda boste pozvani, da vnesete geslo. Če se tega ne spomnite, lahko geslo pogosto ponastavite s pritiskom na gumb na samem usmerjevalniku, vendar se zavedajte, da če izberete ta postopek, boste verjetno izgubili celotne nastavitve usmerjevalnika. Nekateri usmerjevalniki zahtevajo posebno aplikacijo, ki mora v tem primeru biti že nameščena v vašem računalniku ali telefonu.\",\n  \"install_devices_router_list_2\": \"Poiščite nastavitve DHCP/DNS. Poiščite črke DNS poleg polja, ki dovoljuje dva ali tri naborov številk, pri čemer je vsaka razdeljena na štiri skupine z enim do tremi števili.\",\n  \"install_devices_router_list_3\": \"Tam vnesite svoje naslove strežnikov AdGuard Home.\",\n  \"install_devices_router_list_4\": \"Pri nekaterih vrstah usmerjevalnikov strežnika DNS po meri ni mogoče nastaviti. V tem primeru vam lahko pomaga nastavitev AdGuard Home kot <0>strežnika DHCP</0>. V nasprotnem primeru bi morali v priročniku usmerjevalnika preveriti, kako prilagodite strežnike DNS na vašem določenem modelu usmerjevalnika.\",\n  \"install_devices_title\": \"Konfigurirajte svoje naprave\",\n  \"install_devices_windows_list_1\": \"Odprite 'Nadzorno ploščo' prek menija 'Začetek' ali 'Iskanja v sistemu Windows'.\",\n  \"install_devices_windows_list_2\": \"Pojdite v 'Omrežje' in 'Kategorija interneta' in nato v 'Omrežje' in 'Središče za skupno rabo'.\",\n  \"install_devices_windows_list_3\": \"V levem podoknu kliknite 'Spremeni nastavitve kartice'\\\".\",\n  \"install_devices_windows_list_4\": \"Z desno tipko miške kliknite svojo aktivno povezavo in izberite Lastnosti.\",\n  \"install_devices_windows_list_5\": \"Na seznamu poiščite 'Internet protokol različica 4 (TCP/IPv4)' (ali, za IPv6, 'Internet protokol različica 6 (TCP/IPv6)'), jo izberite in nato še enkrat kliknite 'Lastnosti'.\",\n  \"install_devices_windows_list_6\": \"Izberite 'Uporabi naslednje naslove DNS strežnikov' in vnesite vaše naslove strežnika AdGuard Home.\",\n  \"install_saved\": \"Shranjeno uspešno\",\n  \"install_settings_all_interfaces\": \"Vsi vmesniki\",\n  \"install_settings_dns\": \"DNS strežnik\",\n  \"install_settings_dns_desc\": \"Vaše naprave ali usmerjevalnik boste morali konfigurirati za uporabo strežnika DNS na naslednjih naslovih:\",\n  \"install_settings_interface_link\": \"Vaš AdGuard Home Skrbniški spletni vmesnik bo na voljo na naslednjih naslovih:\",\n  \"install_settings_listen\": \"Poslušaj vmesnik\",\n  \"install_settings_port\": \"Vrata\",\n  \"install_settings_title\": \"Skrbniški spletni vmesnik\",\n  \"install_static_configure\": \"AdGuard Home je zaznal, da se uporablja dinamični naslov IP <0>{{ip}}</0>. Ali želite, da je nastavljen kot vaš statični naslov?\",\n  \"install_static_error\": \"AdGuard Home tega omrežnega vmesnika ne more samodejno konfigurirati. Poiščite navodila, kako to storiti ročno.\",\n  \"install_static_ok\": \"Dobra novica! Statičen IP naslov je že konfiguriran\",\n  \"install_step\": \"Korak\",\n  \"install_submit_desc\": \"Postopek namestitve je končan in zdaj ste pripravljeni na uporabo AdGuard Home.\",\n  \"install_submit_title\": \"Čestitamo!\",\n  \"install_welcome_desc\": \"AdGuard Home je omrežni strežnik DNS, ki zavira oglase in sledilce v celotnem omrežju. Njegov namen je omogočanje nadzora nad celotnim omrežjem in vsemi vašimi napravami in ne zahteva uporabo odjemalskega programa.\",\n  \"install_welcome_title\": \"Dobrodošli v AdGuard Home!\",\n  \"interval_24_hour\": \"24 ur\",\n  \"interval_6_hour\": \"6 ur\",\n  \"interval_days\": \"{{count}} dan\",\n  \"interval_days_plural\": \"{{count}} dni\",\n  \"interval_hours\": \"{{count}} ur\",\n  \"interval_hours_plural\": \"{{count}} ur\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP naslov\",\n  \"known_tracker\": \"Znan sledilec\",\n  \"last_rule_in_allowlist\": \"Tega odjemalca ni mogoče onemogočiti, ker izključitev pravila \\\"{{disallowed_rule}}\\\" bo ONEMOGOČILO seznam 'Dovoljeni odjemalci'.\",\n  \"last_time_updated_table_header\": \"Zadnjič posodobljeno\",\n  \"list_confirm_delete\": \"Ali ste prepričani, da želite izbrisati ta seznam?\",\n  \"list_label\": \"Seznam\",\n  \"list_updated\": \"{{count}} posodobljen seznam\",\n  \"list_updated_plural\": \"{{count}} posodobljenih seznamov\",\n  \"list_url_table_header\": \"Seznam URL naslovov\",\n  \"load_balancing\": \"Uravnavanje obremenitve\",\n  \"load_balancing_desc\": \"Poizvedujte po enem strežniku navzgor.<br/>AdGuard Home uporablja tehtan naključni algoritem za izbiro strežnikov z najmanjšim številom neuspešnih iskanj in najnižjim povprečnim časom iskanja.\",\n  \"loading_table_status\": \"Nalaganje...\",\n  \"local_ptr_default_resolver\": \"AdGuard Home privzeto uporablja te povratne razreševalnike DNS: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS strežniki, ki jih uporablja AdGuard Home za zasebne PTR, SOA in NS poizvedbe. Poizvedba se smatra za zasebno, če zahteva ARPA domeno, ki vsebuje podmrežje znotraj zasebnih IP razponov (kot je \\\"192.168.12.34\\\") in prihaja od odjemalca z zasebnim naslovom IP. Če ni nastavljeno, se bodo uporabili privzeti razreševalci DNS vašega operacijskega sistema, razen naslovov IP AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home ni mogel določiti ustreznih zasebnih povratnih reševalcev DNS za ta sistem.\",\n  \"local_ptr_placeholder\": \"Vnesite en naslov IP na vrstico\",\n  \"local_ptr_title\": \"Zasebni povratni strežniki DNS\",\n  \"location\": \"Lokacija\",\n  \"log_and_stats_section_label\": \"Dnevnik poizvedb in statistika\",\n  \"lower_range_start_error\": \"Mora biti manjši od začetka razpona\",\n  \"main_settings\": \"Glavne nastavitve\",\n  \"make_static\": \"Naredi statično\",\n  \"manual_update\": \"Za ročno posodobitev <a>sledite tem korakom</a>.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Ponedeljek\",\n  \"monday_short\": \"Pon\",\n  \"name\": \"Ime\",\n  \"name_table_header\": \"Ime\",\n  \"netname\": \"Ime omrežja\",\n  \"network\": \"Omrežje\",\n  \"new_allowlist\": \"Nov seznam dovoljenih\",\n  \"new_blocklist\": \"Nov seznam nedovoljenih\",\n  \"next\": \"Naprej\",\n  \"next_btn\": \"Naslednja\",\n  \"no_blocklist_added\": \"Ni dodanih nobenih seznamov nedovoljenih\",\n  \"no_clients_found\": \"Ni najdenih odjemalcev\",\n  \"no_domains_found\": \"Ni najdenih domen\",\n  \"no_logs_found\": \"Ni najdenih dnevnikov\",\n  \"no_servers_specified\": \"Ni določenih strežnikov\",\n  \"no_upstreams_data_found\": \"Ni podatkov o gorvodnih strežnikih\",\n  \"no_whitelist_added\": \"Ni dodanih nobenih dovoljenih seznamov\",\n  \"nothing_found\": \"Nič ni bilo najdeno\",\n  \"null_ip\": \"Prazen IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Število zahtev DNS, ki so jih onemogočili filtri za zaviranje oglasov in seznami nedovoljenih, gostiteljev\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Število onemogočenih spletnih strani za odrasle\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Število zahtev DNS, ki jih je blokiral AdGuard zaščitni modul brskanja\",\n  \"number_of_dns_query_days\": \"Število obdelanih poizvedb DNS v zadnjem {{count}} dnevu\",\n  \"number_of_dns_query_days_plural\": \"Število obdelanih poizvedb DNS v zadnjih {{count}} dneh\",\n  \"number_of_dns_query_hours\": \"Število poizvedb DNS, obdelanih v zadnji {{count}} uri\",\n  \"number_of_dns_query_hours_plural\": \"Število poizvedb DNS, obdelanih v zadnjih {{count}} urah\",\n  \"number_of_dns_query_to_safe_search\": \"Število zahtev DNS za iskalnike, za katere je bilo uveljavljeno varno iskanje\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"IZK\",\n  \"on\": \"VKL\",\n  \"open_dashboard\": \"Odpri nadzorno ploščo\",\n  \"orgname\": \"Ime organizacije\",\n  \"original_response\": \"Izviren odgovor\",\n  \"out_of_range_error\": \"Mora biti izven razpona \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Stran\",\n  \"parallel_requests\": \"Vzporedne zahteve\",\n  \"parental_control\": \"Starševski nadzor\",\n  \"password_label\": \"Geslo\",\n  \"password_placeholder\": \"Vnesite geslo\",\n  \"plain_dns\": \"Navadni DNS\",\n  \"port_53_faq_link\": \"Vrata 53 pogosto zasedajo storitve 'DNSStubListener' ali 'Sistemsko razrešene storitve'. Preberite <0>to navodilo</0> o tem, kako to rešiti.\",\n  \"previous_btn\": \"Prejšnja\",\n  \"privacy_policy\": \"Politika zasebnosti\",\n  \"processing_update\": \"Prosimo, počakajte. AdGuard Home se posodablja!\",\n  \"protection_section_label\": \"Zaščita\",\n  \"protocol\": \"Protokol\",\n  \"punycode\": \"Slaba koda\",\n  \"query_log\": \"Dnevnik poizvedb\",\n  \"query_log_clear\": \"Počisti dnevnike poizvedb\",\n  \"query_log_cleared\": \"Dnevnik poizvedb je uspešno izbrisan\",\n  \"query_log_configuration\": \"Konfiguracija dnevnikov\",\n  \"query_log_confirm_clear\": \"Ali ste prepričani, da želite počistiti celoten dnevnik poizvedb?\",\n  \"query_log_disabled\": \"Dnevnik poizvedb je onemogočen in ga je mogoče konfigurirati v <0>nastavitvah</0>\",\n  \"query_log_enable\": \"Omogoči dnevni\",\n  \"query_log_filtered\": \"Filtriran z {{filter}}\",\n  \"query_log_response_status\": \"Stanje: {{value}}\",\n  \"query_log_retention\": \"Rotacija dnevnikov poizvedb\",\n  \"query_log_retention_confirm\": \"Ali ste prepričani, da želite spremeniti rotacijo dnevnika poizvedb? Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni\",\n  \"query_log_strict_search\": \"Za strogo iskanje uporabite dvojne narekovaje\",\n  \"query_log_updated\": \"Dnevnik poizvedb je bil uspešno posodobljen\",\n  \"rate_limit\": \"Omejitev hitrosti\",\n  \"rate_limit_desc\": \"Dovoljeno število zahtev na sekundo na odjemalca. Nastavitev na 0 pomeni brez omejitve.\",\n  \"rate_limit_subnet_len_ipv4\": \"Dolžina predpone podomrežja za naslove IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Dolžina predpone podomrežja za naslove IPv4, ki se uporabljajo za omejevanje hitrosti. Privzeto je 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Dolžina predpone podomrežja IPv4 mora biti med 0 in 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Dolžina predpone podomrežja za naslove IPv4\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Dolžina predpone podomrežja za naslove IPv6, ki se uporabljajo za omejevanje hitrosti. Privzeta vrednost je 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Dolžina podomrežne predpone IPv6 mora biti med 0 in 128\",\n  \"rate_limit_whitelist\": \"Seznam dovoljenih za omejevanje hitrosti\",\n  \"rate_limit_whitelist_desc\": \"Naslovi IP so izključeni iz omejitve hitrosti\",\n  \"rate_limit_whitelist_placeholder\": \"Vnesite en naslov IP na vrstico\",\n  \"refresh_btn\": \"Osveži\",\n  \"refresh_statics\": \"Osveži statistiko\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Prijavi težavo\",\n  \"request_details\": \"Podrobnosti o zahtevi\",\n  \"request_table_header\": \"Zahteva\",\n  \"requests_count\": \"Štavilo zahtev\",\n  \"reset_settings\": \"Ponastavi nastavitve\",\n  \"resolve_clients_desc\": \"Povratno razrešite naslove IP odjemalcev v njihova gostiteljska imena, tako da pošljete poizvedbe PTR ustreznim razreševalcem (zasebni strežniki DNS za lokalne odjemalce, gorvodni strežniki za odjemalce z javnimi naslovi IP).\",\n  \"resolve_clients_title\": \"Omogoči obratno reševanje naslovov IP gostiteljev\",\n  \"response_code\": \"Koda odziva\",\n  \"response_details\": \"Podrobnosti o odzivu\",\n  \"response_table_header\": \"Odgovor\",\n  \"response_time\": \"Odzivni čas\",\n  \"rewrite_A\": \">A</0>: posebna vrednost, obdrži <0>A</0> zapise iz gorvodnega toka\",\n  \"rewrite_AAAA\": \">A</0>: posebna vrednost, obdrži <0>AAAA</0> zapise iz gorvodnega toka\",\n  \"rewrite_add\": \"Dodaj prepisovanje DNS\",\n  \"rewrite_added\": \"Uspešno je dodano DNS prepisovanje za \\\"{{key}}\\\"\",\n  \"rewrite_applied\": \"Uporabljeno Pravilo za prepisovanje\",\n  \"rewrite_confirm_delete\": \"Ali ste prepričani, da želite izbrisati prepisovanje DNS za \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"Uspešno je izbrisano DNS prepisovanje za \\\"{{key}}\\\"\",\n  \"rewrite_desc\": \"Omogoča enostavno konfiguriranje odgovora DNS po meri za določeno ime domene.\",\n  \"rewrite_domain_name\": \"Ime domene: dodaj CNAME zapis\",\n  \"rewrite_edit\": \"Urejanje prepisa DNS\",\n  \"rewrite_hosts_applied\": \"Prepisano s pravilom gostiteljske datoteke\",\n  \"rewrite_ip_address\": \"IP naslov: uporabi ta IP v odzivu A ali AAAA\",\n  \"rewrite_not_found\": \"Ni bilo najdenih prepisovanj DNS\",\n  \"rewrite_settings_updated\": \"Nastavitve prepisovanja DNS so bile uspešno posodobljene\",\n  \"rewrite_updated\": \"DNS prepisovanje uspešno posodobljen\",\n  \"rewrites_disabled_table_header\": \"Prepisovanje je onemogočeno\",\n  \"rewrites_enabled_table_header\": \"Prepisovanje je omogočeno\",\n  \"rewritten\": \"Znova napisano\",\n  \"rows_table_footer_text\": \"vrstic\",\n  \"rule_added_to_custom_filtering_toast\": \"Pravilo je dodano pravilom filtriranja po meri: {{rule}}\",\n  \"rule_label\": \"Pravila\",\n  \"rule_removed_from_custom_filtering_toast\": \"Pravilo je odstranjeno iz pravil filtriranja po meri: {{rule}}\",\n  \"rules_count_table_header\": \"Število pravil\",\n  \"safe_browsing\": \"Varno brskanje\",\n  \"safe_search\": \"Varno iskanje\",\n  \"saturday\": \"Sobota\",\n  \"saturday_short\": \"Sob\",\n  \"save_btn\": \"Shrani\",\n  \"save_config\": \"Shrani nastavitve\",\n  \"schedule_add\": \"Dodaj rokovnik\",\n  \"schedule_current_timezone\": \"Trenutni časovni pas: {{value}}\",\n  \"schedule_desc\": \"Nastavite obdobja nedejavnosti onemogočenih storitev\",\n  \"schedule_edit\": \"Uredi rokovnik\",\n  \"schedule_from\": \"Od\",\n  \"schedule_invalid_select\": \"Začetni čas mora biti pred končnim časom\",\n  \"schedule_modal_description\": \"Ta rokovnik bo nadomestil vse obstoječe rokovnike za isti dan v tednu. Vsak dan v tednu ima lahko samo eno obdobje neaktivnosti.\",\n  \"schedule_modal_time_off\": \"Brez onemogočanja storitve:\",\n  \"schedule_new\": \"Nov rokovnik\",\n  \"schedule_remove\": \"Odstrani rokovnik\",\n  \"schedule_save\": \"Shrani rokovnik\",\n  \"schedule_select_days\": \"Izberite dneve\",\n  \"schedule_services\": \"Začasno ustavi onemogočanje storitve\",\n  \"schedule_services_desc\": \"Nastavite rokovnik premora filtra za onemogočanje storitev\",\n  \"schedule_services_desc_client\": \"Nastavite rokovnik premora filtra za onemogočanje storitev za tega odjemalca\",\n  \"schedule_time_all_day\": \"Ves dan\",\n  \"schedule_timezone\": \"Izberite časovni pas\",\n  \"schedule_to\": \"Do\",\n  \"served_from_cache_label\": \"Dostavljeno iz predpomnilnika\",\n  \"service_name\": \"Ime storitve\",\n  \"set_static_ip\": \"Nastavi statičen IP naslov\",\n  \"settings\": \"Nastavitve\",\n  \"settings_custom\": \"Po meri\",\n  \"settings_global\": \"Splošno\",\n  \"setup_config_to_enable_dhcp_server\": \"Nastavite nastavitve, da omogočite strežnik DHCP\",\n  \"setup_dns_notice\": \"Za uporabo <1>DNS-prek-HTTPS</1> ali <1>DNS-prek-TLS</1>, morate <0>konfigurirati šifriranje</0> v nastavitvah AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-prek-TLS:</0> Uporabite niz <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-prek-HTTPS:</0> Uporabite niz <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Tu je seznam programov, ki jo lahko uporabite.</0>\",\n  \"setup_dns_privacy_4\": \"Na napravo iOS 14 ali macOS Big Sur lahko prenesete posebno datoteko '.mobileconfig', ki v nastavitve DNS doda strežnike <highlight>DNS-preko-HTTPS</highlight> ali <highlight>DNS-preko-TLS</highlight>.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 izvirno podpira DNS-prek-TLS. Če ga želite konfigurirati, pojdite v Nastavitve → Omrežje in internet → Napredno → Zasebni DNS, in tam vnesite svoje ime domene.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard za Android</0> podpira <1>DNS-prek-HTTPS</1> in <1>DNS-prek-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> doda podporo <1>DNS-prek-HTTPS</1> za Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Nastavitve iOS in macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> podpira <1>DNS-prek-HTTPS</1> vendar za njegovo konfiguracijo, da bo uporabljal svoj strežnik, morate zanj ustvariti <2>DNS Stamp</2>.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard za iOS</0> podpira nastavitev <1>DNS-prek-HTTPS</1> in <1>DNS-prek-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"Sam AdGuard Home je lahko varen odjemalec DNS na kateri koli platformi.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> podpira vse znane varne protokole DNS.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> podpira <1>DNS-prek-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> podpira <1>DNS-prek-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Našli boste več izvedb <0>tukaj</0> in <1>tukaj</1>.\",\n  \"setup_dns_privacy_other_title\": \"Druge izvedbe\",\n  \"setup_guide\": \"Navodila za nastavitev\",\n  \"show_all_filter_type\": \"Prikaži vse\",\n  \"show_blocked_responses\": \"Onemogočen\",\n  \"show_filtered_type\": \"Prikaži filtrirane\",\n  \"show_processed_responses\": \"Obdelana\",\n  \"show_whitelisted_responses\": \"Na seznamu dovoljenih\",\n  \"sign_in\": \"Vpis\",\n  \"sign_out\": \"Izpis\",\n  \"source_label\": \"Vir\",\n  \"static_ip\": \"Statičen IP naslov\",\n  \"static_ip_desc\": \"AdGuard Home je strežnik, zato za pravilno delovanje potrebuje statičen IP naslov. V nasprotnem primeru lahko vaš usmerjevalnik tej napravi v nekem trenutku dodeli drug IP naslov.\",\n  \"statistics_clear\": \" Počisti statistiko\",\n  \"statistics_clear_confirm\": \"Ali ste prepričani, da želite počistiti statistiko?\",\n  \"statistics_cleared\": \"Statistika je bila uspešno počiščena\",\n  \"statistics_configuration\": \"Nastavitve statistike\",\n  \"statistics_enable\": \"Omogoči statistiko\",\n  \"statistics_retention\": \"Statistika zadrževanja\",\n  \"statistics_retention_confirm\": \"Ali ste prepričani, da želite spremeniti zadrževanje statistike? Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni\",\n  \"statistics_retention_desc\": \"Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni\",\n  \"stats_adult\": \"Onemogočeno spletnih strani za odrasle\",\n  \"stats_disabled\": \"Statistika je onemogočena. Vklopite ga lahko na <0>strani z nastavitvami</0>.\",\n  \"stats_disabled_short\": \"Statistika je bila onemogočena\",\n  \"stats_malware_phishing\": \"Onemogočeno zlonamernih programov/lažnih predstavljanj\",\n  \"stats_params\": \"Nastavitve statistike\",\n  \"stats_query_domain\": \"Najbolj poizvedovane domene\",\n  \"subnet_error\": \"Naslovi morajo biti v enem podomrežju\",\n  \"sunday\": \"Nedelja\",\n  \"sunday_short\": \"Ned\",\n  \"system_host_files\": \"Sistemske gostiteljske datooteke\",\n  \"table_client\": \"Odjemalec\",\n  \"table_name\": \"Ime\",\n  \"tags_desc\": \"Izberete lahko oznake, ki ustrezajo odjemalcu. Oznake lahko vključite v pravila filtriranja in vam omogočajo, da jih natančneje uporabite. <0>Več o tem</0>.\",\n  \"tags_title\": \"Oznake\",\n  \"test_upstream_btn\": \"Preizkusi upstreame\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Samodejno (glede na barvno shemo vaše naprave)\",\n  \"theme_dark\": \"Temna tema\",\n  \"theme_dark_desc\": \"Temna tema\",\n  \"theme_light\": \"Svetla tema\",\n  \"theme_light_desc\": \"Svetla tema\",\n  \"thursday\": \"Četrtek\",\n  \"thursday_short\": \"Čet\",\n  \"time_table_header\": \"Čas\",\n  \"top_blocked_domains\": \"Najbolj zavirane domene\",\n  \"top_clients\": \"Najpogostejši odjemalci\",\n  \"top_upstreams\": \"Pogosto zahtevani gorvodni strežniki\",\n  \"topline_expired_certificate\": \"Vaše digitalno potrdilo SSL je poteklo. Posodobi <0>Nastavitve šifriranja</0>.\",\n  \"topline_expiring_certificate\": \"Vaš e digitalno potrdilo SSL bo kmalu poteklol. Posodobite <0>Nastavitve šifriranja</0>.\",\n  \"tracker_source\": \"Vir sledilca\",\n  \"try_again\": \"Poskusi ponovno\",\n  \"ttl_cache_validation\": \"Najmanjša preglasitev TTL predpomnilnika mora biti manjša ali enaka najvišji\",\n  \"tuesday\": \"Torek\",\n  \"tuesday_short\": \"Tor\",\n  \"type_table_header\": \"Vrsta\",\n  \"unavailable_dhcp\": \"DHCP ni na voljo\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home ne more zagnati strežnika DHCP v vašem operacijskem sistemu\",\n  \"unblock\": \"Omogoči\",\n  \"unblock_all\": \"Omogoči vse\",\n  \"unblock_for_this_client_only\": \"Omogoči samo za tega odjemalca\",\n  \"unknown_filter\": \"Neznan filter {{filterId}}\",\n  \"update_announcement\": \"Zdaj je na voljo AdGuard Home {{version}}! <0>Klinite tukaj</0> za več informacij.\",\n  \"update_failed\": \"Samodejna posodobitev ni uspela. Prosimo <a>sledite korakom</a>, da ročno posodobite.\",\n  \"update_now\": \"Posodobi zdaj\",\n  \"updated_custom_filtering_toast\": \"Pravila po meri so uspešno shranjena\",\n  \"updated_save_search_toast\": \"Nastavitve varnega iskanja so posodobljene\",\n  \"updated_upstream_dns_toast\": \"Gorvodni trežniki so uspešno shranjeni\",\n  \"updates_checked\": \"Na voljo je nova različica programa AdGuard Home\\n\",\n  \"updates_version_equal\": \"AdGuard Home je posodobljen\",\n  \"upstream\": \"Gorvodni strežnik\",\n  \"upstream_dns\": \"Zagonski DNS strežniki\",\n  \"upstream_dns_cache_configuration\": \"Nastavitve predpomnilnika gorvodnega DNS\",\n  \"upstream_dns_client_desc\": \"Če pustite to polje prazno, bo AdGuard Home uporabil strežnike, konfigurirane v <0>nastavitvah DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Nastavljen v {{path}}\",\n  \"upstream_dns_help\": \"Vnesite naslove strežnikov, enega na vrstico. <a>Več o tem</a> o konfiguriranju zgornjih strežnikov DNS.\",\n  \"upstream_parallel\": \"Uporabite vzporedne zahteve za pospešitev reševanja s hkratnim poizvedovanjem vseh gorvodnih strežnikov.\",\n  \"upstream_timeout\": \"Čas čakanja na odzive strežnikov upstream\",\n  \"upstream_timeout_desc\": \"Določa število sekund, ki jih je treba počakati na odgovor od strežnika\",\n  \"upstreams\": \"Tokovi navzgor\",\n  \"use_adguard_browsing_sec\": \"Uporabi AdGuardovo spletno storitev 'Varnost brskanja'\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home bo preveril ali je domena onemogočena s spletno storitivijo 'Varnost brskanja'ovoljenih. Za izvedbo preverjanja bo uporabil API za iskanje, ki je prijazen do zasebnosti: strežniku se pošlje le kratka predpona zgoščenke domenskega imena SHA256.\",\n  \"use_adguard_parental\": \"Uporabi AdGuardovo spletno storitev 'Starševski nadzor'\",\n  \"use_adguard_parental_hint\": \"AdGuard Home bo preveril, če domena vsebuje  vsebine za odrasle. Uporablja enako, za zasebnost prijazen API, kot spletno storitev za varnost brskanja.\",\n  \"use_private_ptr_resolvers_desc\": \"Razreši PTR, SOA in NS poizvedbe za ARPA domene, ki vsebujejo zasebne IP naslove, prek zasebnih gorvodnih strežnikov, DHCP, /etc/hosts itd. Če je onemogočeno, bo AdGuard Home na vse takšne poizvedbe odgovoril z NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Uporabi zasebne povratne razreševalnike rDNS\",\n  \"use_saved_key\": \"Uporabi prej shranjeni ključ\",\n  \"username_label\": \"Uporabniško ime\",\n  \"username_placeholder\": \"Vnesite uporabniško ime\",\n  \"validated_with_dnssec\": \"Potrjen z DNSSEC\",\n  \"version\": \"različica\",\n  \"version_request_error\": \"Posodobitev ni uspela. Preverite vašo internetno povezavo.\",\n  \"wednesday\": \"Sreda\",\n  \"wednesday_short\": \"Sre\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/sr-cs.json",
    "content": "{\n  \"access_allowed_desc\": \"Spisak CIDR, IP adresa ili <a>ClientIDs</a>. Ako ova lista ima stavke, AdGuard Home će prihvatiti zahteve samo ovih klijenata.\",\n  \"access_allowed_title\": \"Dozvoljeni klijenti\",\n  \"access_blocked_desc\": \"Da ne bude zabune sa filterima. AdGuard Home odustaje od DNS upita koji se podudaraju sa ovim domenima, a ovi upiti se čak i ne pojavljuju u evidenciji upita. Možete da navedete tačna imena domena, džoker znakove ili pravila URL filtera, npr. \\\"example.org\\\", \\\"*.example.org\\\" ili \\\"|| example.org^\\\" dopisno.\",\n  \"access_blocked_title\": \"Blokirani domeni\",\n  \"access_desc\": \"Ovde možete konfigurisati pravila pristupa za AdGuard Home DNS server\",\n  \"access_disallowed_desc\": \"Spisak CIDR, IP adresa ili <a>ClientIDs</a>. Ako ova lista ima stavke, AdGuard Home će otpustiti zahteve ovih klijenata. Ovo polje se zanemaruje ako postoje stavke u dozvoljenim klijentima.\",\n  \"access_disallowed_title\": \"Zabranjeni klijenti\",\n  \"access_settings_saved\": \"Postavke pristupa su uspešno sačuvane\",\n  \"access_title\": \"Postavke pristupa\",\n  \"actions_table_header\": \"Radnje\",\n  \"add_allowlist\": \"Dodaj listu dozvoljenih\",\n  \"add_blocklist\": \"Dodaj blok listu\",\n  \"add_custom_list\": \"Dodaj prilagođenu listu\",\n  \"add_persistent_client\": \"Dodati u sačuvane klijente\",\n  \"address\": \"Adresa\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home će odbacivati sve DNS unose od ovog klijenta.\",\n  \"all_lists_up_to_date_toast\": \"Sve liste su već ažurirane\",\n  \"all_queries\": \"Svi zahtevi\",\n  \"allow_this_client\": \"Dozvoli ovaj klijent\",\n  \"allowed\": \"Dozvoljeno\",\n  \"anonymize_client_ip\": \"Anonimizuj IP klijenta\",\n  \"anonymize_client_ip_desc\": \"Ne čuvaj punu IP adresu klijenta u dnevnicima i statistikama\",\n  \"anonymizer_notification\": \"<0>Nota:</0> IP prepoznavanje je omogućeno. Možete ga onemogućiti u opštim <1>postavkama</1>.\",\n  \"answer\": \"Odgovor\",\n  \"apply_btn\": \"Primeni\",\n  \"auto_clients_desc\": \"Podaci o klijentima koji koriste AdGuard Home, ali nisu sačuvani u konfiguraciji\",\n  \"auto_clients_title\": \"Klijenti (runtime)\",\n  \"autofix_warning_list\": \"To će izvršiti sledeće zadatke: <0>Deaktiviranje system DNSStubListener</0> <0>Set DNS server address to 127.0.0.1</0> <0>Replace symbolic link target of /etc/resolv.conf to /run/systemd/resolve/resolv.conf</0> <0>Stop DNSStubListener (reload systemd-resolved service)</0>\",\n  \"autofix_warning_result\": \"Kao rezultat, svi DNS zahtevi sa vašeg sistema će biti obrađeni od AdGuardHome.\",\n  \"autofix_warning_text\": \"Ako kliknete \\\"Popravi\\\", AdGuardHome će konfigurisati vaš sistem da koristi AdGuardHome DNS server.\",\n  \"average_processing_time\": \"Prosečno vreme obrade\",\n  \"average_processing_time_hint\": \"Prosečno vreme u milisekundama za obradu DNS zahteva\",\n  \"average_upstream_response_time\": \"Prosečno vreme odziva upstream-servera\",\n  \"back\": \"Nazad\",\n  \"block\": \"Blokiraj\",\n  \"block_all\": \"Blokiraj sve\",\n  \"block_domain_use_filters_and_hosts\": \"Blokiraj domene koristeći filtere i hosts datoteke\",\n  \"block_for_this_client_only\": \"Blokiraj samo za ovaj klijent\",\n  \"block_services\": \"Blokiraj određene usluge\",\n  \"blocked_adult_websites\": \"Blokiraj sajtove za odrasle\",\n  \"blocked_by\": \"<0>blokirano od filtera</0>\",\n  \"blocked_by_cname_or_ip\": \"Blokirano od CNAME ili IP\",\n  \"blocked_by_response\": \"Blokirano od CNAME ili IP u odgovoru\",\n  \"blocked_response_ttl\": \"TTL blokiranog odgovora\",\n  \"blocked_response_ttl_desc\": \"Određuje koliko sekundi klijenti treba da keširaju filtrirani odgovor\",\n  \"blocked_safebrowsing\": \"Blokiralo bezbedno pregledanje\",\n  \"blocked_service\": \"Blokirana usluga\",\n  \"blocked_services\": \"Blokiraj usluge\",\n  \"blocked_services_desc\": \"Dozvoljava vam da brzo blokirate popularne sajtove i usluge.\",\n  \"blocked_services_global\": \"Koristi globalne blokirane usluge\",\n  \"blocked_services_saved\": \"Blokirane usluge su uspešno sačuvane\",\n  \"blocked_threats\": \"Blokiranih pretnji\",\n  \"blocking_ipv4\": \"Blokiranje IPv4\",\n  \"blocking_ipv4_desc\": \"IP adresa koja će biti vraćena za blokirane zahteve\",\n  \"blocking_ipv6\": \"Blokiranje IPv6\",\n  \"blocking_ipv6_desc\": \"IP adresa koja će biti vraćena za blokirane AAAA zahteve\",\n  \"blocking_mode\": \"Način blokiranja\",\n  \"blocking_mode_custom_ip\": \"Prilagođeni IP: Odgovara sa ručno podešenom IP adresom\",\n  \"blocking_mode_default\": \"Podrazumevano: Odgovara sa REFUSED kada je blokirano od Adblock-style pravila; odgovara sa IP adresom koja je određena u pravilu kada je blokiran od /etc/hosts-style pravila\",\n  \"blocking_mode_null_ip\": \"Null IP: Odgovara sa zero IP adresom (0.0.0.0 za A; :: za AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Odgovara sa NXDOMAIN kodom\",\n  \"blocking_mode_refused\": \"Odbijeno: Odgovor sa kodom odbijanja\",\n  \"blocklist\": \"Lista blokiranih\",\n  \"bootstrap_dns\": \"Bootstrap DNS serveri\",\n  \"bootstrap_dns_desc\": \"IP adrese DNS servera koje se koriste za rešavanje IP adresa DoH/DoT razrešivača koje navodite kao uzvodne. Komentari nisu dozvoljeni.\",\n  \"cache_cleared\": \"DNS keš je uspešno očišćen\",\n  \"cache_enabled\": \"Omogući keširanje\",\n  \"cache_enabled_desc\": \"Čuvajte DNS odgovore lokalno.\",\n  \"cache_optimistic\": \"Optimistično keširanje\",\n  \"cache_optimistic_desc\": \"Neka AdGuard Home odgovara iz predmemorije čak i kada su unosi istekli pa pokušaj da ih osvežiš.\",\n  \"cache_size\": \"Veličina predmemorije\",\n  \"cache_size_desc\": \"Veličina DNS keša (u bajtovima).\",\n  \"cache_size_validation\": \"Veličina keša mora biti veća od nule kada je omogućena.\",\n  \"cache_ttl_max_override\": \"Prepiši najveći TTL\",\n  \"cache_ttl_max_override_desc\": \"Prepiši TTL vrednost (maksimum) dobijen od apstrim servera.\",\n  \"cache_ttl_min_override\": \"Prepiši najmanji TTL\",\n  \"cache_ttl_min_override_desc\": \"Proširivanje kratkih vrednosti vremena na život (sekundi) primljenih sa uzvodnog servera prilikom keširanje DNS odgovora.\",\n  \"cancel_btn\": \"Otkaži\",\n  \"category_label\": \"Kategorija\",\n  \"check\": \"Proveri\",\n  \"check_client_id\": \"Identifikator klijenta (ClientID ili IP adresa)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Proverite da li je host filtriran.\",\n  \"check_dhcp_servers\": \"Proveri DHCP servere\",\n  \"check_dns_record\": \"Izaberite tip DNS zapisa\",\n  \"check_enter_client_id\": \"Unesite identifikator klijenta\",\n  \"check_hostname\": \"Ime domaćina ili ime domena\",\n  \"check_ip\": \"IP adrese: {{ip}}\",\n  \"check_not_found\": \"Nije pronađeno na vašoj listi filtera\",\n  \"check_reason\": \"Razlog: {{reason}}\",\n  \"check_service\": \"Ime usluge: {{service}}\",\n  \"check_title\": \"Proverite filtriranje\",\n  \"check_updates_btn\": \"Proveri ažuriranja\",\n  \"check_updates_now\": \"Proveri da li postoje ispravke\",\n  \"choose_allowlist\": \"Izaberite liste dozvoljenih\",\n  \"choose_blocklist\": \"Izaberite liste blokiranja\",\n  \"choose_from_list\": \"Izaberite sa liste\",\n  \"city\": \"Grad\",\n  \"clear_cache\": \"Obriši keš memoriju\",\n  \"click_to_view_queries\": \"Kliknite da pogledate zahteve\",\n  \"client_add\": \"Dodaj klijent\",\n  \"client_added\": \"Klijent \\\"{{key}}\\\" uspešno dodat\",\n  \"client_blocked\": \"Klijent \\\"{{ip}}\\\" uspešno blokiran\",\n  \"client_confirm_block\": \"Jeste li sigurni da želite da blokirate klijent \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Jeste li sigurni da želite da izbrišete klijenta \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Jeste li sigurni da želite da odblokirate klijent \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Klijent \\\"{{key}}\\\" uspešno izbrisan\",\n  \"client_details\": \"Pojedinosti klijenta\",\n  \"client_edit\": \"Izmeni klijent\",\n  \"client_global_settings\": \"Koristi globalne postavke\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Različiti klijenti mogu biti prepoznati posebnim ClientID. <a>Ovde</a> možete saznati više o tome kako da prepoznate klijente.\",\n  \"client_id_placeholder\": \"Unesite ClientID\",\n  \"client_identifier\": \"Identifikator\",\n  \"client_identifier_desc\": \"Klijenti se mogu identifikovati po IP adresi, CIDR-u, MAC adresi ili ClientID (može se koristiti za DoT/DoH/DoQ). Saznajte više o tome kako <0>da identifikujete klijente</0>.\",\n  \"client_name\": \"Klijent {{id}}\",\n  \"client_new\": \"Novi klijent\",\n  \"client_settings\": \"Postavke klijenta\",\n  \"client_table_header\": \"Klijent\",\n  \"client_unblocked\": \"Klijent \\\"{{ip}}\\\" uspešno odblokiran\",\n  \"client_updated\": \"Klijent \\\"{{key}}\\\" uspešno ažuriran\",\n  \"clients_desc\": \"Konfigurisanje stalnih klijenata za uređaje povezane sa AdGuard Home\",\n  \"clients_not_found\": \"Nema pronađenih klijenata\",\n  \"clients_title\": \"Uporni klijenti\",\n  \"compact\": \"Kompaktno\",\n  \"config_successfully_saved\": \"Konfiguracija je uspešno sačuvana\",\n  \"configure\": \"Konfiguriši\",\n  \"confirm_dns_cache_clear\": \"Želite li zaista da obrišite DNS keš?\",\n  \"confirm_static_ip\": \"AdGuard Home će konfigurisati {{ip}} da bude vaša statička IP adresa. Želite li da nastavite?\",\n  \"copyright\": \"Autorska prava\",\n  \"country\": \"Zemlja\",\n  \"custom_filter_rules\": \"Prilagođena pravila filtriranja\",\n  \"custom_filter_rules_hint\": \"Unesite jedno pravilo po redu. Možete koristiti pravila blokatora reklama ili sintaksu hosts datoteke.\",\n  \"custom_filtering_rules\": \"Prilagođena pravila filtriranja\",\n  \"custom_ip\": \"Prilagođeni IP\",\n  \"custom_retention_input\": \"Unesite zadržavanje u časovima\",\n  \"custom_rotation_input\": \"Unesite rotaciju u časovima\",\n  \"dashboard\": \"Kontrolna tabla\",\n  \"date\": \"Datum\",\n  \"default\": \"Podrazumevano\",\n  \"delete_confirm\": \"Jeste li sigurni da želite da izbrišete \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Izbriši\",\n  \"descr\": \"Opis\",\n  \"details\": \"Detalji\",\n  \"dhcp_add_static_lease\": \"Dodaj statičko iznajmljivanje\",\n  \"dhcp_config_saved\": \"Sačuvaj DHCP konfiguraciju servera\",\n  \"dhcp_description\": \"Ako vaš ruter nema DHCP postavke, možete koristiti AdGuard' ugrađen DHCP server.\",\n  \"dhcp_disable\": \"Isključi DHCP server\",\n  \"dhcp_dynamic_ip_found\": \"Vaš sistem koristi dinamičku IP adresu za okruženje <0>{{interfaceName}}</0>. Kako biste koristili DHCP server, morate podesiti statičku IP adresu. Vaša trenutna IP adresa je <0>{{ipAddress}}</0>. Automatski ćemo podesiti ovu IP adresu kao statičku ako pritisnete Uključi DHCP dugme.\",\n  \"dhcp_edit_static_lease\": \"Uređivanje statičkog iznajmljivanja\",\n  \"dhcp_enable\": \"Uključi DHCP server\",\n  \"dhcp_error\": \"AdGuard Home nije mogao da utvrdi da li postoji još jedan aktivni DHCP server na mreži\",\n  \"dhcp_form_gateway_input\": \"IP mrežnog prolaza\",\n  \"dhcp_form_lease_input\": \"Trajanje iznajmljivanja\",\n  \"dhcp_form_lease_title\": \"DHCP vreme pozajmljivanja (u sekundama)\",\n  \"dhcp_form_range_end\": \"Kraj opsega\",\n  \"dhcp_form_range_start\": \"Početak opsega\",\n  \"dhcp_form_range_title\": \"Opseg IP adresa\",\n  \"dhcp_form_subnet_input\": \"Subnet mask\",\n  \"dhcp_found\": \"Pronađen je aktivan DHCP server na mreži. Nije bezbedno da uključite ugrađeni DHCP server.\",\n  \"dhcp_hardware_address\": \"Adresa hardvera\",\n  \"dhcp_interface_select\": \"Izaberite DHCP okruženje\",\n  \"dhcp_ip_addresses\": \"IP adrese\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 postavke\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 postavke\",\n  \"dhcp_lease_added\": \"Statičko iznajmljivanje \\\"{{key}}\\\" uspešno dodato\",\n  \"dhcp_lease_deleted\": \"Statičko iznajmljivanje lease \\\"{{key}}\\\" uspešno izbrisano\",\n  \"dhcp_lease_updated\": \"Statičko iznajmljivanje \\\"{{key}}\\\" ažurirano\",\n  \"dhcp_leases\": \"DHCP pozajmljivanja\",\n  \"dhcp_leases_not_found\": \"DHCP pozajmljivanja nisu pronađena\",\n  \"dhcp_new_static_lease\": \"Novo statičko iznajmljivanje\",\n  \"dhcp_not_found\": \"Bezbedno je da uključite ugrađeni DHCP server. Nismo pronašli nijedan aktivan DHCP server na mreži. međutim, ohrabrujemo vas da to ponovo proverite ručno, jer naš automatski test trenutno nije 100% pouzdan.\",\n  \"dhcp_reset\": \"Jeste li sigurni da želite da resetujete DHCP konfiguraciju?\",\n  \"dhcp_reset_leases\": \"Resetuj sva unajmljivanja\",\n  \"dhcp_reset_leases_confirm\": \"Jeste li sigurni da želite da resetujete sva pozajmljivanja?\",\n  \"dhcp_reset_leases_success\": \"DHCP unajmljivanja uspešno resetovana\",\n  \"dhcp_settings\": \"DHCP postavke\",\n  \"dhcp_static_ip_error\": \"Da biste koristili DHCP server, morate postaviti statičnu IP adresu. AdGuard Home nije uspeo da utvrdi da li je ovaj mrežni interfejs konfigurisan pomoću statične IP adrese. Postavite statičnu IP adresu ručno.\",\n  \"dhcp_static_leases\": \"DHCP statička pozajmljivanja\",\n  \"dhcp_static_leases_not_found\": \"Nisu pronađena statička DHCP iznajmljivanja\",\n  \"dhcp_table_expires\": \"Ističe\",\n  \"dhcp_table_hostname\": \"Ime hosta\",\n  \"dhcp_title\": \"DHCP server (eksperimentalno!)\",\n  \"dhcp_warning\": \"Ako ipak želite da omogućite DHCP server, uverite se da u mreži ne postoji drugi aktivni DHCP server, jer to može da prekine Internet vezu za uređaje na mreži!\",\n  \"disable_for_hours\": \"Za {{count}} sat\",\n  \"disable_for_hours_plural\": \"Za {{count}} sati\",\n  \"disable_for_minutes\": \"Za {{count}} minut\",\n  \"disable_for_minutes_plural\": \"Za {{count}} minuta\",\n  \"disable_for_seconds\": \"Za {{count}} sekund\",\n  \"disable_for_seconds_plural\": \"Za {{count}} sekundi\",\n  \"disable_ipv6\": \"Onemogući rešavanje IPv6 adresa\",\n  \"disable_ipv6_desc\": \"Ignorisanje svih DNS upite za IPv6 adrese (tip AAAA) i uklanjanje IPv6 podataka iz HTTPS odgovora.\",\n  \"disable_notify_for_hours\": \"Isključi zaštitu na {{count}} sat\",\n  \"disable_notify_for_hours_plural\": \"Isključi zaštitu na {{count}} sati\",\n  \"disable_notify_for_minutes\": \"Isključi zaštitu na {{count}} minut\",\n  \"disable_notify_for_minutes_plural\": \"Isključi zaštitu na {{count}} minuta\",\n  \"disable_notify_for_seconds\": \"Isključi zaštitu na {{count}} sekund\",\n  \"disable_notify_for_seconds_plural\": \"Isključi zaštitu na {{count}} sekundi\",\n  \"disable_notify_until_tomorrow\": \"Isključi zaštitu do sutra\",\n  \"disable_protection\": \"Isključi zaštitu\",\n  \"disable_rewrites\": \"Onemogući pravila prepisivanja\",\n  \"disable_until_tomorrow\": \"Do sutra\",\n  \"disabled\": \"Isključeno\",\n  \"disabled_dhcp\": \"DHCP server isključen\",\n  \"disabled_filtering_toast\": \"Isključeno filtriranje\",\n  \"disabled_parental_toast\": \"Isključena roditeljska kontrola\",\n  \"disabled_protection\": \"Isključena zaštita\",\n  \"disabled_safe_browsing_toast\": \"Isključeno sigurno pregledanje\",\n  \"disabled_safe_search_toast\": \"Isključena sigurna pretraga\",\n  \"disallow_this_client\": \"Zabrani ovaj klijent\",\n  \"dns_addresses\": \"DNS adrese\",\n  \"dns_allowlists\": \"DNS dozvoljene liste\",\n  \"dns_allowlists_desc\": \"Domeni sa liste dozvoljenih će uvek biti dozvoljeni, čak iako se neki od njih nalazi na blok listi.\",\n  \"dns_blocklists\": \"DNs blok liste\",\n  \"dns_blocklists_desc\": \"AdGuard Home će blokirati domene koji se poklapaju sa blok listama.\",\n  \"dns_cache_config\": \"Konfigurisanje DNS predmemorije\",\n  \"dns_cache_config_desc\": \"Ovde možete konfigurisati DNS predmemoriju\",\n  \"dns_cache_size\": \"Veličina DNS keša, u bajtovima\",\n  \"dns_config\": \"Konfiguracija DNS servera\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS privatnost\",\n  \"dns_providers\": \"Ovo je a <0>lista poznatih DNS dobavljača</0> sa koje možete da izaberete.\",\n  \"dns_query\": \"DNS zahtevi\",\n  \"dns_rewrites\": \"DNS prepisivanja\",\n  \"dns_settings\": \"DNS postavke\",\n  \"dns_start\": \"DNS server se pokreće\",\n  \"dns_status_error\": \"Greška pri proveri statusa DNS servera\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": se ne može koristiti. Proverite da li ste ga ispravno uneli\",\n  \"dns_test_ok_toast\": \"Dati DNS serveri rade ispravno\",\n  \"dns_test_parsing_error_toast\": \"Odeljak {{section}}: linija {{line}}: ne može se koristiti, molimo proverite da li ste ga ispravno napisali\",\n  \"dns_test_warning_toast\": \"Apstrim \\\"{{key}}\\\" ne odgovara na zahteve za testiranje i možda neće raditi kako treba\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Uključi DNSSEC\",\n  \"dnssec_enable_desc\": \"Postavlja DNSSEC zastavicu u odlaznim DNS zahtevima i proverava rezultat (DNSSEC rešavač je potreban).\",\n  \"domain\": \"Domen\",\n  \"domain_desc\": \"Unesite domen ili džoker koji želite da prepišete.\",\n  \"domain_name_table_header\": \"Ime domena\",\n  \"domain_or_client\": \"Domen ili klijent\",\n  \"down\": \"Dole\",\n  \"download_mobileconfig\": \"Preuzmi konfiguracionu datoteku\",\n  \"download_mobileconfig_doh\": \"Preuzimanja\",\n  \"download_mobileconfig_dot\": \"Preuzmi .mobileconfig za DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Uredi listu dozvoljenih\",\n  \"edit_blocklist\": \"Uredi blok listu\",\n  \"edit_table_action\": \"Izmeni\",\n  \"edns_cs_desc\": \"Dodajte opciju podmreži EDNS klijenta (ECS) uzvodnim zahtevima i evidentirajte vrednosti koje klijenti šalju u evidenciji upita.\",\n  \"edns_enable\": \"Uključi EDNS Client Subnet\",\n  \"edns_use_custom_ip\": \"Koristi prilagođeni IP za EDNS\",\n  \"edns_use_custom_ip_desc\": \"Dozvoli korišćenje prilagođenog IP-a za EDNS\",\n  \"elapsed\": \"Proteklo\",\n  \"empty_response_status\": \"Prazno\",\n  \"enable_protection\": \"Uključi zaštitu\",\n  \"enable_protection_timer\": \"Zaštita će biti uključena u {{time}}\",\n  \"enable_rewrites\": \"Omogući pravila prepisivanja\",\n  \"enable_upstream_dns_cache\": \"Uključite keširanje za korisničku konfiguraciju upstream servera ovog klijenta\",\n  \"enabled_dhcp\": \"DHCP server uključen\",\n  \"enabled_filtering_toast\": \"Uključeno filtriranje\",\n  \"enabled_parental_toast\": \"Uključena roditeljska kontrola\",\n  \"enabled_protection\": \"Uključena zaštita\",\n  \"enabled_safe_browsing_toast\": \"Uključeno sigurno pretraživanje\",\n  \"enabled_save_search_toast\": \"Uključeno sigurno pretraživanje\",\n  \"enabled_table_header\": \"Uključeno\",\n  \"encryption_certificate_path\": \"Putanja sertifikata\",\n  \"encryption_certificates\": \"Sertifikati\",\n  \"encryption_certificates_desc\": \"Da biste koristili šifrovanje, morate obezbediti važeći lanac SSL sertifikata za vaš domen. Besplatan sertifikat možete nabaviti na <0>{{link}}</0> ili ga možete kupiti od nekog od pouzdanih izdavalaca sertifikata.\",\n  \"encryption_certificates_input\": \"Kopirajte/nalepite vaše PEM-kodirane sertifikate ovde.\",\n  \"encryption_certificates_source_content\": \"Nalepite sadržaj sertifikata\",\n  \"encryption_certificates_source_path\": \"Postavi putanju do datoteke sertifikata\",\n  \"encryption_chain_invalid\": \"Lanac sertifikata je nevažeći\",\n  \"encryption_chain_valid\": \"Lanac sertifikata je važeći\",\n  \"encryption_config_saved\": \"Konfiguracija šifrovanja je sačuvana\",\n  \"encryption_desc\": \"Šifrovanje (HTTPS/QUIC/TLS) podrška za oba DNS i administratorsko okruženje\",\n  \"encryption_doq\": \"DNS-over-QUIC port\",\n  \"encryption_doq_desc\": \"Ako je ovaj port konfigurisan, AdGuard Home će pokrenuti DNS-over-QUIC server na tom portu.\",\n  \"encryption_dot\": \"DNS-over-TLS port\",\n  \"encryption_dot_desc\": \"Ako je ovaj port konfigurisan, AdGuard Home će pokretati DNS-over-TLS server na ovom portu.\",\n  \"encryption_enable\": \"Uključi šifrovanje (HTTPS, DNS-over-HTTPS, i DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Ako je šifrovanje uključeno, AdGuard Home administratorsko okruženje će raditi preko HTTPS, i DNS server će slušati zahteve preko DNS-over-HTTPS i DNS-over-TLS.\",\n  \"encryption_expire\": \"Ističe\",\n  \"encryption_hostnames\": \"Imena hostova\",\n  \"encryption_https\": \"HTTPS port\",\n  \"encryption_https_desc\": \"Ako je HTTPS port konfigurisan, AdGuard Home administratorskom okruženju će se moći pristupati preko HTTPS, a to će takođe omogućiti DNS-over-HTTPS na '/dns-query' lokaciji.\",\n  \"encryption_issuer\": \"Izdavač\",\n  \"encryption_key\": \"Privatni ključ\",\n  \"encryption_key_input\": \"Kopirajte/nalepite vaš PEM-kodirani privatni ključ za vaš sertifikat ovde.\",\n  \"encryption_key_invalid\": \"Ovo je nevažeći {{type}} privatni ključ\",\n  \"encryption_key_source_content\": \"Nalepi sadržaj privatnog ključa\",\n  \"encryption_key_source_path\": \"Podesi putanju do datoteke privatnog ključa\",\n  \"encryption_key_valid\": \"Ovo je važeći {{type}} privatni ključ\",\n  \"encryption_plain_dns_desc\": \"Plain DNS je podrazumevano omogućen. Možete ga onemogućiti da biste primorali sve uređaje da koriste šifrovani DNS. Da biste to uradili, potrebno je da omogućite bar jedan šifrovani DNS protokol\",\n  \"encryption_plain_dns_enable\": \"Omogući plain DNS\",\n  \"encryption_plain_dns_error\": \"Da biste onemogućili običan DNS, omogućite najmanje jedan šifrovani DNS protokol\",\n  \"encryption_private_key_path\": \"Putanja privatnog ključa\",\n  \"encryption_redirect\": \"Automatski preusmeri na HTTPS\",\n  \"encryption_redirect_desc\": \"Ako je označeno, AdGuard Home će vas automatski preusmeravati sa HTTP na HTTPS adrese.\",\n  \"encryption_reset\": \"Jeste li sigurni da želite dda resetujete postavke šifrovanja?\",\n  \"encryption_server\": \"Ime servera\",\n  \"encryption_server_desc\": \"Ako je podešen, AdGuard Home otkriva ID-ove klijenta, odgovara na DDR upite i izvršava dodatne provere valjanosti veze. Ako se ne postave, ove funkcije su onemogućene. Mora se podudarati sa DNS imenima u certifikatu.\",\n  \"encryption_server_enter\": \"Unesite vaše ime domena\",\n  \"encryption_settings\": \"Postavke šifrovanja\",\n  \"encryption_status\": \"Stanje\",\n  \"encryption_subject\": \"Predmet\",\n  \"encryption_title\": \"Šifrovanje\",\n  \"encryption_warning\": \"Upozorenje\",\n  \"enforce_safe_search\": \"Nametni sigurno pretraživanje\",\n  \"enforce_save_search_hint\": \"AdGuard Home će sprovesti sigurnu pretragu u sledećim pretraživačima: Google, IouTube, Bing, DuckDuckGo, Ecosia, Iandek, Pikabai.\",\n  \"enforced_save_search\": \"Nametni sigurno pretraživanje\",\n  \"enter_cache_size\": \"Unesite veličinu predmemorije\",\n  \"enter_cache_ttl_max_override\": \"Unesite najveći TTL\",\n  \"enter_cache_ttl_min_override\": \"Unesite najmanji TTL\",\n  \"enter_name_hint\": \"Unesite ime\",\n  \"enter_url_or_path_hint\": \"Unesite URL ili apsolutnu putanju liste\",\n  \"enter_valid_allowlist\": \"Unesite važeći URL do liste dozvoljenih.\",\n  \"enter_valid_blocklist\": \"Unesite važeći URL do blok liste.\",\n  \"error_details\": \"Detalji greške\",\n  \"example_comment\": \"! Ovde ide komentar.\",\n  \"example_comment_hash\": \"# Takođe komentar.\",\n  \"example_comment_meaning\": \"samo komentar;\",\n  \"example_meaning_filter_block\": \"blokirajte pristup ka primer.org i svim njegovim poddomenima;\",\n  \"example_meaning_filter_whitelist\": \"dozvolite pristup ka primer.org i svim njegovim poddomenima;\",\n  \"example_meaning_host_block\": \"vratiti adresu 127.0.0.1 za primer.org (ali ne i za njegove poddomene);\",\n  \"example_multiple_upstreams_reserved\": \"nekoliko DNS servera <0>za određene domene</0>;\",\n  \"example_regex_meaning\": \"blokiranje pristupa domenima koji odgovaraju određenom uobičajenom izrazu.\",\n  \"example_rewrite_domain\": \"prepiši odgovore samo za ovaj domen.\",\n  \"example_rewrite_wildcard\": \"prepiši odgovore za sve poddomene na <0>example.org</0>.\",\n  \"example_upstream_comment\": \"komentar.\",\n  \"example_upstream_doh\": \"šifrovano <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"šifrovani DNS-over-HTTPS sa prinudnim <0>HTTP/3</0> bez povratka na HTTP/2 ili ispod;\",\n  \"example_upstream_doq\": \"šifrovano <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"šifrovano <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"uobičajeno DNS (preko UDP);\",\n  \"example_upstream_regular_port\": \"uobičajen DNS (preko UDP, sa portom);\",\n  \"example_upstream_reserved\": \"upstream <0>za određene domene</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS brojeve</0> za <1>DNSCrypt</1> ili <2>DNS-over-HTTPS</2> razrešivače;\",\n  \"example_upstream_tcp\": \"uobičajeni DNS (preko TCP);\",\n  \"example_upstream_tcp_hostname\": \"uobičajen DNS (preko TCP, imena domaćina);\",\n  \"example_upstream_tcp_port\": \"uobičajen DNS (preko TCP, sa portom);\",\n  \"example_upstream_udp\": \"uobičajen DNS (preko UDP, imena domaćina);\",\n  \"examples_title\": \"Primeri\",\n  \"fallback_dns_desc\": \"Lista povratnih DNS servera koji se koriste kada se uzvodni DNS serveri ne odaziva. Sintaksa je ista kao u glavnom uzvodnom polju iznad.\",\n  \"fallback_dns_placeholder\": \"Unesite jedan povratni DNS server po liniji\",\n  \"fallback_dns_title\": \"Odstupajući DNS serveri\",\n  \"faq\": \"ČPP\",\n  \"fastest_addr\": \"Najbrža IP adresa\",\n  \"fastest_addr_desc\": \"Sačekajte odgovore od <b>Sve</b> DNS serveri, izmerite brzinu TCP veze za svaki server i vratite IP adresu servera sa najvećom brzinom veze.<br/>Ovaj režim može značajno usporiti DNS upite, ako jedan ili više uzvodnih servera ne reaguju. Uverite se da su vaši uzvodni serveri stabilni i da je vaš uzvodni timeout nizak.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"Filter je uspešno dodat\",\n  \"filter_allowlist\": \"UPOZORENJE: Ova radnja će takođe izuzeti pravilo \\\"{{disallowed_rule}}\\\" sa the spiska dozvoljenih klijenata.\",\n  \"filter_category_general\": \"Opšte\",\n  \"filter_category_general_desc\": \"Lista koja blokira praćenja i reklame na većini uređaja\",\n  \"filter_category_other\": \"Ostalo\",\n  \"filter_category_other_desc\": \"Ostale liste blokiranja\",\n  \"filter_category_regional\": \"Region\",\n  \"filter_category_regional_desc\": \"Lista koja se usredsređuje na regionalne reklame i servere praćenja\",\n  \"filter_category_security\": \"Bezbednost\",\n  \"filter_category_security_desc\": \"Lista specijalizovana za blokiranje štetnog softvera, štetnih i fišing domena\",\n  \"filter_removed_successfully\": \"Lista je uspešno uklonjena\",\n  \"filter_updated\": \"Filter je uspešno ažuriran\",\n  \"filtered\": \"Filtrirano\",\n  \"filtered_custom_rules\": \"Filtrirano od strane prilagođenog pravila\",\n  \"filtering_rules_learn_more\": \"<0>Saznajte više</0> o stvaranju vaše lične blokliste hostova.\",\n  \"filters\": \"Filteri\",\n  \"filters_and_hosts_hint\": \"AdGuard Home razume osnovna pravila blokiranja reklama i sintaksu hosts datoteke.\",\n  \"filters_block_toggle_hint\": \"Možete postaviti pravila blokiranja u <a>Filters</a> postavkama.\",\n  \"filters_configuration\": \"Konfiguracija filtera\",\n  \"filters_enable\": \"Uključi filtere\",\n  \"filters_interval\": \"Interval ažuriranja filtera\",\n  \"fix\": \"Popravi\",\n  \"for_last_days\": \"u poslednjih {{count}} dana\",\n  \"for_last_days_plural\": \"u poslednjih {{count}} dana\",\n  \"for_last_hours\": \"u poslednjih {{count}} sat\",\n  \"for_last_hours_plural\": \"u poslednjih {{count}} sati\",\n  \"forgot_password\": \"Zaboravili ste lozinku?\",\n  \"forgot_password_desc\": \"Ispratite <0>ove korake</0> za stvaranje nove lozinke za vaš korisnički nalog.\",\n  \"form_add_id\": \"Dodaj identifikator\",\n  \"form_answer\": \"Unesite IP adresu ili domen\",\n  \"form_client_name\": \"Unesite ime klijenta\",\n  \"form_domain\": \"Unesite domen\",\n  \"form_enter_blocked_response_ttl\": \"Unesite TTL blokiranog odgovora (sekunde)\",\n  \"form_enter_host\": \"Unesite host\",\n  \"form_enter_hostname\": \"Unesite ime hosta\",\n  \"form_enter_id\": \"Unesite identifikator\",\n  \"form_enter_ip\": \"Unesite IP\",\n  \"form_enter_mac\": \"Unesite MAC\",\n  \"form_enter_rate_limit\": \"Unesite ograničenje brzine\",\n  \"form_enter_rate_limit_subnet_len\": \"Unesite dužinu prefixa podmreže da biste ograničili brzinu\",\n  \"form_enter_subnet_ip\": \"Unesite IP adresu subnet \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Unesite trajanje timeout-a upstream servera u sekundama\",\n  \"form_error_answer_format\": \"Nevažeći format odgovora\",\n  \"form_error_client_id_format\": \"ClientID mora da sadrži samo brojeve, malim slovima i crticama\",\n  \"form_error_domain_format\": \"Nevažeći format domena\",\n  \"form_error_equal\": \"Ne smije biti jednako\",\n  \"form_error_gateway_ip\": \"Zakup ne može imati IP adresu mrežnog prolaza\",\n  \"form_error_ip4_format\": \"Nevažeća IPv4 adresa\",\n  \"form_error_ip4_gateway_format\": \"Nevažeća IPv4 addresa prozala\",\n  \"form_error_ip6_format\": \"Nevažeća IPv6 adresa\",\n  \"form_error_ip_format\": \"Nevažeća IP adresa\",\n  \"form_error_mac_format\": \"Nevažeća MAC adresa\",\n  \"form_error_password\": \"Lozinke se ne podudaraju\",\n  \"form_error_password_length\": \"Lozinka mora imati od {{min}} do {{max}} znakova\",\n  \"form_error_port\": \"Unesite važeći broj porta\",\n  \"form_error_port_range\": \"Unesite vrednost porta u opsegu od 80-65535\",\n  \"form_error_port_unsafe\": \"Nije siguran port\",\n  \"form_error_positive\": \"Mora biti veće od 0\",\n  \"form_error_required\": \"Obavezno polje\",\n  \"form_error_server_name\": \"Nevažeće ime servera\",\n  \"form_error_subnet\": \"Subnet \\\"{{cidr}}\\\" ne sadrži IP adresu \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Nevažeći format URL-a\",\n  \"form_error_url_or_path_format\": \"URL ili apsolutna putanja do liste nije valjana\",\n  \"form_select_tags\": \"Izaberite oznake klijenta\",\n  \"found_in_known_domain_db\": \"Pronađeno u poznatim bazama podataka domena.\",\n  \"friday\": \"Petak\",\n  \"friday_short\": \"Pet\",\n  \"gateway_or_subnet_invalid\": \"Subnet mask nevažeća\",\n  \"general_settings\": \"Opšte postavke\",\n  \"general_statistics\": \"Opšte statistike\",\n  \"get_started\": \"Počnimo\",\n  \"greater_range_start_error\": \"Mora biti veće od početnog opsega\",\n  \"homepage\": \"Početna stranica\",\n  \"host_whitelisted\": \"Host je na beloj listi\",\n  \"ignore_domains\": \"Zanemari domene (razdvojene novom linijom)\",\n  \"ignore_domains_desc_query\": \"Upiti koji se podudaraju sa ovim pravilima nisu upisani u evidenciju upita\",\n  \"ignore_domains_desc_stats\": \"Upiti koji se podudaraju sa ovim pravilima nisu upisani u statistiku\",\n  \"ignore_domains_title\": \"Zanemareni domeni\",\n  \"ignore_query_log\": \"Zanemari ovog klijenta u evidenciji upita\",\n  \"ignore_statistics\": \"Zanemari ovog klijenta u statističkim podacima\",\n  \"install_auth_confirm\": \"Potvrdite lozinku\",\n  \"install_auth_desc\": \"Preporučujemo vam da konfigurišete autentifikaciju lozinkom za vaše AdGuard Home administratorsko okruženje. Čak iako mu je moguće pristupiti samo iz vaše lokalne mreže,, i dalje je važno da ga zaštitite od neograničenog pristupa.\",\n  \"install_auth_password\": \"Lozinka\",\n  \"install_auth_password_enter\": \"Unesite lozinku\",\n  \"install_auth_title\": \"Autentifikacija\",\n  \"install_auth_username\": \"Korisničko ime\",\n  \"install_auth_username_enter\": \"Unesite korisničko ime\",\n  \"install_devices_address\": \"AdGuard Home DNS server sluša na sledećim adresama\",\n  \"install_devices_android_list_1\": \"Sa Android početnog ekrana, dodirnite Postavke.\",\n  \"install_devices_android_list_2\": \"Dodirnite Wi-Fi. Pojaviće se ekran sa svim dostupnim mrežama. Nije moguće da podesite prilagođeni DNS za mobilne veze).\",\n  \"install_devices_android_list_3\": \"Dugo pritisnite na mrežu na koju ste povezani, pa dodirnite Izmeni mrežu.\",\n  \"install_devices_android_list_4\": \"Na nekim uređajima će možda biti potrebno da označite kućicu za napredne opcije kako bi videli dalje postavke. Da biste prilagodili vaše Android DNS postavke, prebacite IP postavke sa DHCP na statičke.\",\n  \"install_devices_android_list_5\": \"Promenite DNS 1 i DNS 2 vrednosti na adrese vašeg AdGuard Home servera.\",\n  \"install_devices_desc\": \"Za početak korišćenja AdGuard Home, potrebno je da konfigurišete vaše uređaje da ga koriste.\",\n  \"install_devices_ios_list_1\": \"Sa početnog ekrana, dodirnite postavke.\",\n  \"install_devices_ios_list_2\": \"U levom meniju izaberite Wi-Fi. Nije moguće da konfigurišete DNS za mobilne mreže).\",\n  \"install_devices_ios_list_3\": \"Dodirnite ime trenutno aktivne mreže.\",\n  \"install_devices_ios_list_4\": \"U DNS polje unesite adrese vašeg AdGuard Home servera.\",\n  \"install_devices_macos_list_1\": \"Kliknite na ikonicu jabuke pa otiđite na postavke sistema.\",\n  \"install_devices_macos_list_2\": \"Kliknite na mrežu.\",\n  \"install_devices_macos_list_3\": \"Izaberite prvu vezu sa liste pa kliknite na više opcija.\",\n  \"install_devices_macos_list_4\": \"Izaberite karticu DNS pa tu unesite adrese vašeg AdGuard Home servera.\",\n  \"install_devices_router\": \"Ruter\",\n  \"install_devices_router_desc\": \"Ovo postavljanje će automatski pokriti sve uređaje koji su povezani na vaš kućni ruter pa nećete morati da konfigurišete svaki uređaj posebno.\",\n  \"install_devices_router_list_1\": \"Otvorite željene postavke mrežne skretnice. Obično mu možete pristupiti iz pregledača putem URL adrese, kao što su http://192.168.0.1/ ili http://192.168.1.1/. Od vas će možda biti zatraženo da unesete lozinku. Ako je se ne sećate, često možete da poništite lozinku pritiskom na dugme na samoj mrežnoj skretnici, ali imajte na umu da ćete, ako se ova procedura izabere, verovatno izgubiti celu konfiguraciju rutera. Ako ruter zahteva aplikaciju za podešavanje, instalirajte aplikaciju na telefonu ili računaru i koristite je za pristup postavkama rutera.\",\n  \"install_devices_router_list_2\": \"Pronađite DHCP ili DNS postavke. Potražite DNS slova pored polja koje dozvoljava dve ili tri skupine brojeva, a svaka može da sadrži četiri grupe od jedne do tri cifre.\",\n  \"install_devices_router_list_3\": \"Tamo unesite adrese AdGuard home servera.\",\n  \"install_devices_router_list_4\": \"Na nekim tipovima mrežnih skretnica nije moguće podesiti prilagođeni DNS server. U tom slučaju, podešavanje AdGuard Home-a kao <0>DHCP servera</0> može da pomogne. U suprotnom, trebalo bi da proverite uputstvo mrežne skretnice o prilagođavanju DNS servera na određenom modelu rutera.\",\n  \"install_devices_title\": \"Konfigurišite vaše uređaje\",\n  \"install_devices_windows_list_1\": \"Otvorite kontrolnu tablu iz startnog menija ili kroz Windows pretragu.\",\n  \"install_devices_windows_list_2\": \"Otvorite kategoriju mreža i internet a onda otiđite u centar za mrežu i deljenje.\",\n  \"install_devices_windows_list_3\": \"Na levoj tabli kliknite na dugme \\\"Promeni postavke adaptera\\\".\",\n  \"install_devices_windows_list_4\": \"Kliknite desnim tasterom miša na aktivnu vezu i izaberite stavku Svojstva.\",\n  \"install_devices_windows_list_5\": \"Na listi pronađite Internet Protokol verzija 4 (TCP/IP) (ili, za IPv6, \\\"Internet Protocol Version 6 (TCP/IPv6)\\\"), izaberite ga pa kliknite ponovo na Svojstva.\",\n  \"install_devices_windows_list_6\": \"Izaberite \\\"Koristi sledeće adrese DNS servera\\\" pa unesite vaše adrese AdGuard Home servera.\",\n  \"install_saved\": \"Uspešno sačuvano\",\n  \"install_settings_all_interfaces\": \"Sva okruženja\",\n  \"install_settings_dns\": \"DNS server\",\n  \"install_settings_dns_desc\": \"Potrebno je da konfigurišete vaše uređaje ili ruter da koristi DNS server sa sledećim adresama:\",\n  \"install_settings_interface_link\": \"Vaše AdGuard Home administratorsko web okruženje će biti dostupno na sledećim adresama:\",\n  \"install_settings_listen\": \"Okruženje slušanja\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Administratorsko web okruženje\",\n  \"install_static_configure\": \"Otkrili smo da se koristi dinamička IP adresa — <0>{{ip}}</0>. Želite li da je koristite kao vašu statičku adresu?\",\n  \"install_static_error\": \"AdGuard Home se ne može automatski konfigurisati za ovo mrežno okruženje. Pogledajte uputstvo kako da to ručno uradite.\",\n  \"install_static_ok\": \"Dobre vesti! Statička IP adresa je već konfigurisana\",\n  \"install_step\": \"Korak\",\n  \"install_submit_desc\": \"Postavljanje je završeno i sada ste spremni da započnete sa korišćenjem AdGuard Home.\",\n  \"install_submit_title\": \"Čestitamo!\",\n  \"install_welcome_desc\": \"AdGuard Home je mrežni DNS server, blokator reklama i praćenja. Dopušta vam da kontrolišete svoju čitavu mrežu i sve vaše uređaje i ne zahteva korišćenje nikakvog klijentskog programa.\",\n  \"install_welcome_title\": \"Dobrodošli u AdGuard home!\",\n  \"interval_24_hour\": \"24 časa\",\n  \"interval_6_hour\": \"6 časa\",\n  \"interval_days\": \"{{count}} dan\",\n  \"interval_days_plural\": \"{{count}} dana\",\n  \"interval_hours\": \"{{count}} čas\",\n  \"interval_hours_plural\": \"{{count}} časova\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP adresa\",\n  \"known_tracker\": \"Poznato praćenje\",\n  \"last_rule_in_allowlist\": \"Ne mogu da zabranim ovog klijenta zato što će izuzimanje pravila \\\"{{disallowed_rule}}\\\" onemogućiti \\\"dozvoljene klijente\\\".\",\n  \"last_time_updated_table_header\": \"Poslednji put ažurirano\",\n  \"list_confirm_delete\": \"Jeste li sigurni da želite da izbrišete ovu listu?\",\n  \"list_label\": \"Lista\",\n  \"list_updated\": \"{{count}} lista ažurirana\",\n  \"list_updated_plural\": \"{{count}} lista ažurirano\",\n  \"list_url_table_header\": \"URL do liste\",\n  \"load_balancing\": \"Load-balancing\",\n  \"load_balancing_desc\": \"Upitajte jedan uzvodni server u isto vreme.<br/>AdGuard Home koristi ponderisani slučajni algoritam za odabir servera sa najmanjim brojem neuspelih pretraga i najnižim prosečnim vremenom pretrage.\",\n  \"loading_table_status\": \"Učitavanje...\",\n  \"local_ptr_default_resolver\": \"Podrazumevano, AdGuard Home koristi sledeće obrnute DNS razrešivače: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS serveri koje koristi AdGuard Home za privatne PTR, SOA i NS zahteve. Zahtev se smatra privatnim ako traži ARPA domen koji sadrži podmrežu unutar privatnih IP opsega (kao što je \\\"192.168.12.34\\\") i dolazi od klijenta sa privatnom IP adresom. Ako nije podešeno, podrazumevani DNS resolveri vašeg operativnog sistema će se koristiti, osim za AdGuard Home IP adrese.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home ne može da odredi pogodne privatne obrnute DNS razrešivače za ovaj sistem.\",\n  \"local_ptr_placeholder\": \"Unesite jednu IP adresu servera po redu\",\n  \"local_ptr_title\": \"Private reverse DNS serveri\",\n  \"location\": \"Lokacija\",\n  \"log_and_stats_section_label\": \"Evidencija upita i statistika\",\n  \"lower_range_start_error\": \"Mora biti manje od početnog opsega\",\n  \"main_settings\": \"Glavne postavke\",\n  \"make_static\": \"Učini statičnim\",\n  \"manual_update\": \"Molimo vas <a>pratite korake</a> za ručno ažuriranje.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Ponedeljak\",\n  \"monday_short\": \"Pon\",\n  \"name\": \"Ime\",\n  \"name_table_header\": \"Ime\",\n  \"netname\": \"Ime mreže\",\n  \"network\": \"Mreža\",\n  \"new_allowlist\": \"Nova lista dozvoljenih\",\n  \"new_blocklist\": \"Nova blok lista\",\n  \"next\": \"Dalje\",\n  \"next_btn\": \"Sledeće\",\n  \"no_blocklist_added\": \"Blok liste nisu dodate\",\n  \"no_clients_found\": \"Nema pronađenih klijenata\",\n  \"no_domains_found\": \"Domeni nisu pronađeni\",\n  \"no_logs_found\": \"Dnevnici nisu pronađeni\",\n  \"no_servers_specified\": \"Serveri nisu određeni\",\n  \"no_upstreams_data_found\": \"Nema podataka o upstream serverima\",\n  \"no_whitelist_added\": \"Liste dozvoljenih nisu dodate\",\n  \"nothing_found\": \"Ništa nije pronađeno\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Broj DNS zahteva blokiranih od filtera blokatora reklama i blok liste hostova\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Broj blokiranih sajtova za odrasle\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Broj DNS zahteva blokiranih od AdGuard-ovog podprograma za bezbedno pregledanje\",\n  \"number_of_dns_query_days\": \"Broj obrađenih DNS unosa u poslednjih {{count}} dan\",\n  \"number_of_dns_query_days_plural\": \"Broj obrađenih DNS unosa u poslednjih {{count}} dana\",\n  \"number_of_dns_query_hours\": \"Broj obrađenih DNS unosa u poslednji {{count}} sat\",\n  \"number_of_dns_query_hours_plural\": \"Broj obrađenih DNS unosa u poslednjih {{count}} sati\",\n  \"number_of_dns_query_to_safe_search\": \"Broj DNS zahteva ka pretraživačima za koje je nametnuto sigurno pretraživanje\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"Isključeno\",\n  \"on\": \"Uključeno\",\n  \"open_dashboard\": \"Otvori kontrolnu tablu\",\n  \"orgname\": \"Ime organizacije\",\n  \"original_response\": \"Izvorni odgovor\",\n  \"out_of_range_error\": \"Mora biti izvan opsega \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Stranica\",\n  \"parallel_requests\": \"Paralelni zahtevi\",\n  \"parental_control\": \"Roditeljska kontrola\",\n  \"password_label\": \"Lozinka\",\n  \"password_placeholder\": \"Unesite lozinku\",\n  \"plain_dns\": \"Plain DNS\",\n  \"port_53_faq_link\": \"Port 53 je najčešće zauzet od \\\"DNSStubListener\\\" ili \\\"systemd-resolved\\\" usluga. Pročitajte <0>ovo uputstvo</0> kako da to rešite.\",\n  \"previous_btn\": \"Prethodno\",\n  \"privacy_policy\": \"Politika privatnosti\",\n  \"processing_update\": \"Molimo sačekajte. AdGuard Home se ažurira\",\n  \"protection_section_label\": \"Zaštita\",\n  \"protocol\": \"Protokol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Dnevnik zahteva\",\n  \"query_log_clear\": \"Očisti dnevnike unosa\",\n  \"query_log_cleared\": \"Dnevnik unosa je uspešno očišćen\",\n  \"query_log_configuration\": \"Konfiguracija dnevnika\",\n  \"query_log_confirm_clear\": \"Jeste li sigurni da želite da očistite ceo dnevnik unosa?\",\n  \"query_log_disabled\": \"Dnevnik unosa je isključen ali se može konfigurisati u <0>postavkama</0>\",\n  \"query_log_enable\": \"Uključi dnevnik\",\n  \"query_log_filtered\": \"Filtrirano od {{filter}}\",\n  \"query_log_response_status\": \"Stanje: {{value}}\",\n  \"query_log_retention\": \"Rotacija evidencija upita\",\n  \"query_log_retention_confirm\": \"Želite li zaista da promenite rotaciju evidencije upita? Ako smanjite vrednost intervala, neki podaci će biti izgubljeni\",\n  \"query_log_strict_search\": \"Koristi duple navodnike za striktnu pretragu\",\n  \"query_log_updated\": \"Dnevnik zapisa je uspešno ažuriran\",\n  \"rate_limit\": \"Ograničenje brzine\",\n  \"rate_limit_desc\": \"Broj zahteva u sekundi dozvoljen po klijentu. Postavljanje na 0 znači da nema ograničenja.\",\n  \"rate_limit_subnet_len_ipv4\": \"Dužina prefixa podmreže za IPv4 adrese\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Dužina prefixa podmreže za IPv4 adrese koje se koriste za ograničavanje brzine. Podrazumevano je 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Dužina prefixa IPv4 podmreže treba da bude između 0 i 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Dužina prefixa podmreže za IPv6 adrese\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Dužina prefixa podmreže za IPv6 adrese koje se koriste za ograničavanje brzine. Podrazumevano je 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Dužina prefixa IPv6 podmreže treba da bude između 0 i 128\",\n  \"rate_limit_whitelist\": \"Lista dozvoljenih lista za ograničavanje brzine\",\n  \"rate_limit_whitelist_desc\": \"IP adrese koje nisu obuhvaćene ograničenjem brzine\",\n  \"rate_limit_whitelist_placeholder\": \"Unesite jednu IP adresu servera po redu\",\n  \"refresh_btn\": \"Osveži\",\n  \"refresh_statics\": \"Osveži statistike\",\n  \"refused\": \"Odbijeno\",\n  \"report_an_issue\": \"Prijavi poteškoću\",\n  \"request_details\": \"Pojedinosti zahteva\",\n  \"request_table_header\": \"Zahtev\",\n  \"requests_count\": \"Broj zahteva\",\n  \"reset_settings\": \"Vrati postavke na podrazumevano\",\n  \"resolve_clients_desc\": \"Obrnuto razrešite IP adrese klijenata u njihova imena domaćina slanjem PTR upita odgovarajućim razrešivačima (privatni DNS serveri za lokalne klijente, uzvodni serveri za klijente sa javnim IP adresama).\",\n  \"resolve_clients_title\": \"Uključi obrnuto razrešavanje klijentskih IP adresa\",\n  \"response_code\": \"Kod odgovora\",\n  \"response_details\": \"Pojedinosti odgovora\",\n  \"response_table_header\": \"Odgovor\",\n  \"response_time\": \"Vreme odziva\",\n  \"rewrite_A\": \"<0>A</0>: posebna vrednost, zadrži <0>A</0> records iz apstrima\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: posebna vrednost, zadržite <0>AAAA</0> records iz apstrima\",\n  \"rewrite_add\": \"Dodaj DNS prepisivanje\",\n  \"rewrite_added\": \"DNS prepisivanje za \\\"{{key}}\\\" je uspešno dodato\",\n  \"rewrite_applied\": \"Primenjeno pravilo prepisivanja\",\n  \"rewrite_confirm_delete\": \"Jeste li sigurni da želite da izbrišete DNS prepisivanje za \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"DNS prepisivanje za \\\"{{key}}\\\" uspešno izbrisano\",\n  \"rewrite_desc\": \"Dozvoljava da jednostavno konfigurišete prilagođeni DNS odgovor za određeni domen.\",\n  \"rewrite_domain_name\": \"Ime domena: dodajte CNAME zapis\",\n  \"rewrite_edit\": \"Uređivanje DNS prepravke\",\n  \"rewrite_hosts_applied\": \"Prepisano od pravila hosts datoteke\",\n  \"rewrite_ip_address\": \"IP adresa: kkoristite ovaj IP u A ili AAAA odgovoru\",\n  \"rewrite_not_found\": \"DNS prepisivanja nisu pronađena\",\n  \"rewrite_settings_updated\": \"Podešavanja DNS prepisivanja uspešno ažurirana\",\n  \"rewrite_updated\": \"DNS ponovo napisao uspešno ažuriran\",\n  \"rewrites_disabled_table_header\": \"Prepisivanja su onemogućena\",\n  \"rewrites_enabled_table_header\": \"Prepisivanja su omogućena\",\n  \"rewritten\": \"Prepisano\",\n  \"rows_table_footer_text\": \"redovi\",\n  \"rule_added_to_custom_filtering_toast\": \"Pravilo dodato u prilagođena pravila filtriranja: {{rule}}\",\n  \"rule_label\": \"Pravilo(-a)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Pravilo uklonjeno iz prilagođenih pravila filtriranja: {{rule}}\",\n  \"rules_count_table_header\": \"Broj pravila\",\n  \"safe_browsing\": \"Sigurno pregledanje\",\n  \"safe_search\": \"Sigurna pretraga\",\n  \"saturday\": \"Subota\",\n  \"saturday_short\": \"Sub\",\n  \"save_btn\": \"Sačuvaj\",\n  \"save_config\": \"Sačuvaj konfiguraciju\",\n  \"schedule_add\": \"Dodaj raspored\",\n  \"schedule_current_timezone\": \"Trenutna vremenska zona: {{value}}\",\n  \"schedule_desc\": \"Podešavanje perioda neaktivnosti za blokirane usluge\",\n  \"schedule_edit\": \"Uredi raspored\",\n  \"schedule_from\": \"Od\",\n  \"schedule_invalid_select\": \"Vreme početka mora biti pre vremena završetka\",\n  \"schedule_modal_description\": \"Ovaj raspored će zameniti sve postojeće rasporede za isti dan u sedmici. Svaki dan u sedmici može imati samo jedan period neaktivnosti.\",\n  \"schedule_modal_time_off\": \"Nema blokiranja usluge:\",\n  \"schedule_new\": \"Novi raspored\",\n  \"schedule_remove\": \"Ukloni raspored\",\n  \"schedule_save\": \"Sačuvaj raspored\",\n  \"schedule_select_days\": \"Izaberite dane\",\n  \"schedule_services\": \"Pauziranje blokiranja usluge\",\n  \"schedule_services_desc\": \"Konfigurisanje rasporeda pauziranja filtera za blokiranje usluga\",\n  \"schedule_services_desc_client\": \"Konfigurišite raspored pauziranja filtera za blokiranje usluga za ovog klijenta\",\n  \"schedule_time_all_day\": \"Ceo dan\",\n  \"schedule_timezone\": \"Izaberite vremensku zonu\",\n  \"schedule_to\": \"Do\",\n  \"served_from_cache_label\": \"Posluženo iz pred memorije\",\n  \"service_name\": \"Ime usluge\",\n  \"set_static_ip\": \"Postavite statičku IP adresu\",\n  \"settings\": \"Postavke\",\n  \"settings_custom\": \"Prilagođeno\",\n  \"settings_global\": \"Globalno\",\n  \"setup_config_to_enable_dhcp_server\": \"Podesite konfiguraciju kako biste omogućili DHCP server\",\n  \"setup_dns_notice\": \"Kako biste koristili <1>DNS-over-HTTPS</1> ili <1>DNS-over-TLS</1>, potrebno je da <0>konfigurišete šifrovanje</0> u AdGuard Home postavkama.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> koristi <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> koristi <1>{{address}}</1> string.\",\n  \"setup_dns_privacy_3\": \"<0>Ovde je lista softvera koje možete koristiti.</0>\",\n  \"setup_dns_privacy_4\": \"Na iOS 14 ili macOS Big Sur uređaju možete preuzeti posebnu '.mobileconfig' datotteku koja dodaje <highlight>DNS-over-HTTPS</highlight> ili <highlight>DNS-over-TLS</highlight> servere u DNS postavke.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 podržava DNS-over-TLS. Za konfiguraciju, idite u postavke → mreža i internet → Napredno → Privatni DNS i tamo unesite ime vašeg domena.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0> podržava <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> dodaje <1>DNS-over-HTTPS</1> podršku za Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS i macOS konfiguracija\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> podržava <1>DNS-over-HTTPS</1>, ali da biste mogli da ga konfigurišete da koristi vaš lični server, biće potrebno da generišete a <2>DNS Stamp</2> za njega.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard za iOS</0> podržava <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard home može biti bezbedan DNS server na bilo kojoj platformi.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> podržava sve poznate bezbedne DNS protokole.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> podržava <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> podržava <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Više implementacija ćete pronaći <0>ovde</0> i <1>ovde</1>.\",\n  \"setup_dns_privacy_other_title\": \"Druge implementacije\",\n  \"setup_guide\": \"Uputstvo za podešavanje\",\n  \"show_all_filter_type\": \"Pokaži sve\",\n  \"show_blocked_responses\": \"Blokirano\",\n  \"show_filtered_type\": \"Pokaži filtrirano\",\n  \"show_processed_responses\": \"Obrađeno\",\n  \"show_whitelisted_responses\": \"Na beloj listi\",\n  \"sign_in\": \"Prijavi se\",\n  \"sign_out\": \"Odjavi se\",\n  \"source_label\": \"Izvor\",\n  \"static_ip\": \"Statička IP adresa\",\n  \"static_ip_desc\": \"AdGuard Home je server pa mu je zbog toga potrebna statička IP aadresa kako bi ispravno radio. Ako je nema, u nekim slučajevima, vaš ruter može dodeliti drugu IP adresu ovom uređaju.\",\n  \"statistics_clear\": \" Očisti statistiku\",\n  \"statistics_clear_confirm\": \"Jeste li sigurni da želite da očistite statistiku?\",\n  \"statistics_cleared\": \"Statistika je uspešno očišćena\",\n  \"statistics_configuration\": \"Konfiguracija statistike\",\n  \"statistics_enable\": \"Uključi statistiku\",\n  \"statistics_retention\": \"Zadržavanje statistike\",\n  \"statistics_retention_confirm\": \"Jeste li sigurni da želite da promenite zadržavanje statistike? Ako smanjite vrednost intervala, neki podaci će biti izgubljeni\",\n  \"statistics_retention_desc\": \"Ako smanjite vrednost intervala, neki podaci će biti izgubljeni\",\n  \"stats_adult\": \"Blokiraj sajtove za odrasle\",\n  \"stats_disabled\": \"Statistika je isključena. Možete ga uključiti sa stranice <0>sa postavkama</0>.\",\n  \"stats_disabled_short\": \"Statistika je isključena\",\n  \"stats_malware_phishing\": \"Blokiraj štetan softver i fišing\",\n  \"stats_params\": \"Konfiguracija statistike\",\n  \"stats_query_domain\": \"Najčešće unošeni domeni\",\n  \"subnet_error\": \"Asrese moraju biti u jednoj subnet\",\n  \"sunday\": \"Nedelja\",\n  \"sunday_short\": \"Ned\",\n  \"system_host_files\": \"System hosts datoteke\",\n  \"table_client\": \"Klijent\",\n  \"table_name\": \"Ime\",\n  \"tags_desc\": \"Možete izabrati oznake koje odgovaraju klijentu. Uključite oznake u pravila filtriranja da biste ih preciznije primenili. <0>Saznajte više</0>.\",\n  \"tags_title\": \"Oznake\",\n  \"test_upstream_btn\": \"Testiraj upstreams\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Automatski (na osnovu šeme boja uređaja)\",\n  \"theme_dark\": \"Tamna tema\",\n  \"theme_dark_desc\": \"Tamna tema\",\n  \"theme_light\": \"Svetla tema\",\n  \"theme_light_desc\": \"Svetla tema\",\n  \"thursday\": \"Četvrtak\",\n  \"thursday_short\": \"Čet\",\n  \"time_table_header\": \"Vreme\",\n  \"top_blocked_domains\": \"Najčešće blokirani domeni\",\n  \"top_clients\": \"Najčešći klijenti\",\n  \"top_upstreams\": \"Često traženi upstream serveri\",\n  \"topline_expired_certificate\": \"Vaš SSL sertifikat je istekao. Ažurirajte <0>postavke šifrovanja</0>.\",\n  \"topline_expiring_certificate\": \"Vaš SSL sertifikat uskoro ističe. Ažurirajte <0>postavke šifrovanja</0>.\",\n  \"tracker_source\": \"Izvor praćenja\",\n  \"try_again\": \"Pokušaj ponovo\",\n  \"ttl_cache_validation\": \"Minimalna TTL vrednost mora biti manja ili jednaka najvišij vrednosti\",\n  \"tuesday\": \"Utorak\",\n  \"tuesday_short\": \"Uto\",\n  \"type_table_header\": \"Vrsta\",\n  \"unavailable_dhcp\": \"DHCP nije dostupan\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home Ne može da pokrene DHCP na vašem OS\",\n  \"unblock\": \"Odblokiraj\",\n  \"unblock_all\": \"Odblokiraj sve\",\n  \"unblock_for_this_client_only\": \"Odblokiraj samo za ovaj klijent\",\n  \"unknown_filter\": \"Nepoznat filter {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} je sada dostupan! <0>Kliknite ovde</0> za više informacija.\",\n  \"update_failed\": \"Automatsko ažuriranje nije uspelo. Molimo vas <a>pratite korake</a> za ručno ažuriranje.\",\n  \"update_now\": \"Ažuriraj sada\",\n  \"updated_custom_filtering_toast\": \"Prilagođena pravila su uspešno sačuvana\",\n  \"updated_save_search_toast\": \"Ažurirane postavke bezbedne pretrage\",\n  \"updated_upstream_dns_toast\": \"Upstream serveri su uspešno sačuvani\",\n  \"updates_checked\": \"Dostupna je nova verzija AdGuard Home-a\",\n  \"updates_version_equal\": \"AdGuard Home je ažuriran na najnoviju verziju\",\n  \"upstream\": \"Upstream-server\",\n  \"upstream_dns\": \"Upstream DNS serveri\",\n  \"upstream_dns_cache_configuration\": \"Konfiguracija keša upstream DNS servera\",\n  \"upstream_dns_client_desc\": \"AKo ovo polje ostavite prazno, AdGuard Home će koristiti servere konfigurisane u <0>DNS postavkama</0>.\",\n  \"upstream_dns_configured_in_file\": \"Konfiguriši u {{path}}\",\n  \"upstream_dns_help\": \"Unesite adrese servera, jednu po redu. <a>Saznajte više</a> o konfigurisanju upstream DNS servera.\",\n  \"upstream_parallel\": \"Koristite paralelne upite da biste ubrzali rešavanje tako što ćete istovremeno ispitati sve uzvodne servere.\",\n  \"upstream_timeout\": \"Upravljački timeout\",\n  \"upstream_timeout_desc\": \"Određuje broj sekundi čekanja na odgovor od uzvodnog servera\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Koristi AdGuard-ovu uslugu bezbednog pregledanja\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home će proveriti da li je domen blokiran od strane usluge za bezbednosno pregledanje. Koristiće prijateljski API privatni pregled da izvrši proveru. Samo će se kratak prefiks domena SHA256 hash poslati na server.\",\n  \"use_adguard_parental\": \"Koristi AdGuard-ovu uslugu roditeljske kontrole\",\n  \"use_adguard_parental_hint\": \"AdGuard Home će proveriti da li domen sadrži sadržaj za odrasle. Koristi se isti privatni prijateljski API kao i kod usluge bezbednog pregledanja.\",\n  \"use_private_ptr_resolvers_desc\": \"Rešavanje PTR, SOA i NS zahteva za ARPA domene koji sadrže privatne IP adrese preko privatnih uzvodnih servera, DHCP, /etc/hosts, itd. Ako je onemogućen, AdGuard Home će odgovoriti na sve takve zahteve sa NKSDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Koristi privatne obrnute razrešivače\",\n  \"use_saved_key\": \"Koristi prethodno sačuvan ključ\",\n  \"username_label\": \"Korisničko ime\",\n  \"username_placeholder\": \"Unesite korisničko ime\",\n  \"validated_with_dnssec\": \"Potvrđeno sa DNSSEC\",\n  \"version\": \"Verzija\",\n  \"version_request_error\": \"Provera ažuriranja nije uspela. Proverite svoju vezu sa internetom.\",\n  \"wednesday\": \"Sreda\",\n  \"wednesday_short\": \"Sre\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/sv.json",
    "content": "{\n  \"access_allowed_desc\": \"En lista över CIDR, IP-adresser eller <a>ClientID</a>. Om den här listan har poster accepterar AdGuard Home endast förfrågningar från dessa clienter.\",\n  \"access_allowed_title\": \"Tillåtna klienter\",\n  \"access_blocked_desc\": \"Ej att förväxla med filter. AdGuard Home kastar DNS-frågor som matchar dessa domäner, och dessa frågor visas inte ens i frågeloggen. Du kan ange exakta domännamn, jokertecken eller URL-filterregler, t.ex. \\\"example.org\\\", \\\"*.example.org\\\" eller \\\"||example.org^\\\" på motsvarande sätt.\",\n  \"access_blocked_title\": \"Blockerade domäner\",\n  \"access_desc\": \"Här kan du konfigurera åtkomstregler för AdGuard Homes DNS-server\",\n  \"access_disallowed_desc\": \"En lista över CIDR, IP-adresser eller <a>ClientID</a>. Om den här listan har poster kommer AdGuard Home att ta bort förfrågningar från dessa klienter. Detta fält ignoreras om det finns poster i Tillåtna klienter.\",\n  \"access_disallowed_title\": \"Otillåtna klienter\",\n  \"access_settings_saved\": \"Åtkomstinställningar sparade\",\n  \"access_title\": \"Åtkomstinställningar\",\n  \"actions_table_header\": \"Åtgärder\",\n  \"add_allowlist\": \"Lägg till frilista\",\n  \"add_blocklist\": \"Lägg till blockeringslista\",\n  \"add_custom_list\": \"Lägg till en anpassad lista\",\n  \"add_persistent_client\": \"Lägg till som beständig klient\",\n  \"address\": \"Adress\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home kommer att kasta alla DNS-frågor från den här klienten.\",\n  \"all_lists_up_to_date_toast\": \"Alla listor är redan uppdaterade\",\n  \"all_queries\": \"Alla förfrågningar\",\n  \"allow_this_client\": \"Tillåt den här klienten\",\n  \"allowed\": \"Vitlistade\",\n  \"anonymize_client_ip\": \"Anonymisera klientens IP\",\n  \"anonymize_client_ip_desc\": \"Spara inte klientens fullständiga IP-adress i loggar och statistik\",\n  \"anonymizer_notification\": \"<0>Observera:</0> IP-anonymisering är aktiverad. Du kan inaktivera den i <1>Allmänna inställningar</1>.\",\n  \"answer\": \"Svar\",\n  \"apply_btn\": \"Tillämpa\",\n  \"auto_clients_desc\": \"Information om IP-adresser för enheter som använder eller kan använda AdGuard Home. Denna information samlas in från flera källor, inklusive värdfiler, omvänd DNS, etc.\",\n  \"auto_clients_title\": \"Klienter (körtid)\",\n  \"autofix_warning_list\": \"Den kommer att utföra följande uppgifter: <0>Avaktivera system DNSStubListener</0> <0>Sätt DNS serveradress till 127.0.0.1</0> <0>Ersätt symboliskt länkmål för /etc/resolv.conf med /run/systemd /resolve/resolv.conf</0> <0>Stoppa DNSStubListener (ladda om systemd-resolved tjänst)</0>\",\n  \"autofix_warning_result\": \"Som ett resultat kommer alla DNS-förfrågningar från ditt system att behandlas av AdGuard Home som standard.\",\n  \"autofix_warning_text\": \"Om du klickar på \\\"Fix\\\" kommer AdGuard Home att konfigurera ditt system för att använda AdGuard Home DNS server.\",\n  \"average_processing_time\": \"Genomsnittlig processtid\",\n  \"average_processing_time_hint\": \"Genomsnittlig processtid i millisekunder för DNS-förfrågning\",\n  \"average_upstream_response_time\": \"Genomsnittlig svarstid uppströmsserver\",\n  \"back\": \"Tiilbaka\",\n  \"block\": \"Blockera\",\n  \"block_all\": \"Blockera alla\",\n  \"block_domain_use_filters_and_hosts\": \"Blockera domäner med filter- och värdfiler\",\n  \"block_for_this_client_only\": \"Blockera endast för denna klient\",\n  \"block_services\": \"Blockera specifika tjänster\",\n  \"blocked_adult_websites\": \"Blockerad av Föräldrakontroll\",\n  \"blocked_by\": \"<0>Blockerat av filter</0>\",\n  \"blocked_by_cname_or_ip\": \"Blockerad av CNAME eller IP\",\n  \"blocked_by_response\": \"Blockerad av CNAME eller IP i svaret\",\n  \"blocked_response_ttl\": \"TTL för blockerat svar\",\n  \"blocked_response_ttl_desc\": \"Anger hur många sekunder klienterna ska cache ett filtrerat svar\",\n  \"blocked_safebrowsing\": \"Blockerad av Säker webbsökning\",\n  \"blocked_service\": \"Blockerad tjänst\",\n  \"blocked_services\": \"Blockerade tjänster\",\n  \"blocked_services_desc\": \"Gör det möjligt att snabbt blockera populära webbplatser och tjänster.\",\n  \"blocked_services_global\": \"Använd globalt blockerade tjänster\",\n  \"blocked_services_saved\": \"Blockerade tjänster har sparats\",\n  \"blocked_threats\": \"Blockerade hot\",\n  \"blocking_ipv4\": \"Blockera IPv4\",\n  \"blocking_ipv4_desc\": \"IP adress som ska returneras för en blockerad A förfrågan\",\n  \"blocking_ipv6\": \"Blockera IPv6\",\n  \"blocking_ipv6_desc\": \"IP adress som ska returneras för en blockerad AAAA förfrågan\",\n  \"blocking_mode\": \"Blockeringsläge\",\n  \"blocking_mode_custom_ip\": \"Anpassad IP: Svara med en manuellt inställd IP adress\",\n  \"blocking_mode_default\": \"Standard: Svara med noll IP-adress (0.0.0.0 för A; :: för AAAA) när det blockeras av regel i Adblock-stil; svara med IP-adressen som anges i regeln när den blockeras av regel i /etc/hosts-stil\",\n  \"blocking_mode_null_ip\": \"Null IP: Svara med noll IP adress (0.0.0.0 för A; :: för AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Svara med NXDOMAIN kod\",\n  \"blocking_mode_refused\": \"REFUSED: Svara med REFUSED kod\",\n  \"blocklist\": \"Blocklista\",\n  \"bootstrap_dns\": \"Bootstrap-DNS-servrar\",\n  \"bootstrap_dns_desc\": \"IP-adresser för DNS-servrar som används för att lösa IP-adresser för de DoH/DoT-resolvers som du anger som uppströms. Kommentarer är inte tillåtna.\",\n  \"cache_cleared\": \"DNS-cacheminnet har rensats\",\n  \"cache_enabled\": \"Aktivera cache\",\n  \"cache_enabled_desc\": \"Lagra DNS-svar lokalt.\",\n  \"cache_optimistic\": \"Optimistisk cachning\",\n  \"cache_optimistic_desc\": \"Få AdGuard Home att svara från cachen även när posterna har gått ut och försök även uppdatera dem.\",\n  \"cache_size\": \"Cachestorlek\",\n  \"cache_size_desc\": \"DNS cachestorlek (i byte).\",\n  \"cache_size_validation\": \"Cache-storleken måste vara större än noll när den är aktiverad.\",\n  \"cache_ttl_max_override\": \"Åsidosätt maximal TTL\",\n  \"cache_ttl_max_override_desc\": \"Ställ in ett maximalt värde för time-to-live (sekunder) för poster i DNS cachen.\",\n  \"cache_ttl_min_override\": \"Åsidosätt minsta TTL\",\n  \"cache_ttl_min_override_desc\": \"Förläng värden för korta time-to-live värden (sekunder) som tas emot från uppströms server när DNS svar cachelagras.\",\n  \"cancel_btn\": \"Avbryt\",\n  \"category_label\": \"Kategori\",\n  \"check\": \"Kontrollera\",\n  \"check_client_id\": \"Klientidentifierare (ClientID eller IP-adress)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Kontrollera om värdnamnet är filtrerat.\",\n  \"check_dhcp_servers\": \"Letar efter DHCP-servrar\",\n  \"check_dns_record\": \"Välj typ av DNS-post\",\n  \"check_enter_client_id\": \"Ange klientidentifierare\",\n  \"check_hostname\": \"Värdnamn eller domännamn\",\n  \"check_ip\": \"IP-adresser: {{ip}}\",\n  \"check_not_found\": \"Hittades inte i dina filterlistor\",\n  \"check_reason\": \"Anledning: {{reason}}\",\n  \"check_service\": \"Service namn: {{service}}\",\n  \"check_title\": \"Kontrollera filtreringen\",\n  \"check_updates_btn\": \"Sök efter uppdateringar\",\n  \"check_updates_now\": \"Sök efter uppdateringar nu\",\n  \"choose_allowlist\": \"Välj frilistor\",\n  \"choose_blocklist\": \"Välj blockeringslistor\",\n  \"choose_from_list\": \"Välj från listan\",\n  \"city\": \"Stad\",\n  \"clear_cache\": \"Rensa cache\",\n  \"click_to_view_queries\": \"Klicka för att se förfrågningar\",\n  \"client_add\": \"Lägg till klient\",\n  \"client_added\": \"Klient \\\"{{key}}\\\" har lagts till\",\n  \"client_blocked\": \"Klienten \\\"{{ip}}\\\" har blockerats\",\n  \"client_confirm_block\": \"Är du säker på att du vill blockera klienten \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"Är du säker på att du vill ta bort klient \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"Är du säker på att du vill avblockera klienten \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"Klient \\\"{{key}}\\\" har raderats\",\n  \"client_details\": \"Klient information\",\n  \"client_edit\": \"Redigera klient\",\n  \"client_global_settings\": \"Använda globala inställningar\",\n  \"client_id\": \"Klient ID\",\n  \"client_id_desc\": \"Olika klienter kan identifieras med ett speciellt klient ID. <a>Här</a> kan du lära dig mer om hur du identifierar klienter.\",\n  \"client_id_placeholder\": \"Ange klient ID\",\n  \"client_identifier\": \"Identifikator\",\n  \"client_identifier_desc\": \"Klienter kan identifieras med IP-adressen, CIDR, MAC-adressen eller ett ClientID (kan användas för DoT/DoH/DoQ). <0>Här</0> kan du lära dig mer om hur du identifierar klienter.\",\n  \"client_name\": \"Klient {{id}}\",\n  \"client_new\": \"Ny klient\",\n  \"client_settings\": \"Klientinställningar\",\n  \"client_table_header\": \"Klient\",\n  \"client_unblocked\": \"Klienten \\\"{{ip}}\\\" har avblockerats\",\n  \"client_updated\": \"Klient \\\"{{key}}\\\" har uppdaterats\",\n  \"clients_desc\": \"Konfigurera beständiga klientposter för enheter som är anslutna till AdGuard Home\",\n  \"clients_not_found\": \"Inga klienter hittade\",\n  \"clients_title\": \"Uthålliga klienter\",\n  \"compact\": \"Kompakt\",\n  \"config_successfully_saved\": \"Konfigurationen har sparats\",\n  \"configure\": \"Konfigurera\",\n  \"confirm_dns_cache_clear\": \"Är du säker på att du vill rensa DNS-cache?\",\n  \"confirm_static_ip\": \"AdGuard Home kommer att konfigurera {{ip}} för att vara din statiska IP adress. Vill du fortsätta?\",\n  \"copyright\": \"Copyright\",\n  \"country\": \"Land\",\n  \"custom_filter_rules\": \"Egna filterregler\",\n  \"custom_filter_rules_hint\": \"Skriv en regel per rad. Du kan använda antingen annonsblockeringsregler eller värdfilssyntax.\",\n  \"custom_filtering_rules\": \"Egna filterregler\",\n  \"custom_ip\": \"Eget IP\",\n  \"custom_retention_input\": \"Ange retention i timmar\",\n  \"custom_rotation_input\": \"Ange rotation i timmar\",\n  \"dashboard\": \"Kontrollpanel\",\n  \"date\": \"Datum\",\n  \"default\": \"Standard\",\n  \"delete_confirm\": \"Är du säker på att du vill ta bort \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"Radera\",\n  \"descr\": \"Beskrivning\",\n  \"details\": \"Detaljer\",\n  \"dhcp_add_static_lease\": \"Lägg till statisk lease\",\n  \"dhcp_config_saved\": \"DHCP-konfigurationen har sparats\",\n  \"dhcp_description\": \"Om din router inte har inställningar för DHCP kan du använda AdGuards inbyggda server.\",\n  \"dhcp_disable\": \"Avaktivera DHCP-server\",\n  \"dhcp_dynamic_ip_found\": \"Din enhet använder en dynamisk IP-adress för gränssnittet <0>{{interfaceName}}</0>. För att kunna använda DHCP-servern behövs en statisk IP-adress. Din nuvarande IP-adress är <0>{{ipAddress}}</0>. AdGuard Home kommer automatiskt att göra denna IP-adress statisk om du trycker på knappen \\\"Aktivera DHCP\\\".\",\n  \"dhcp_edit_static_lease\": \"Redigera statiskt lease\",\n  \"dhcp_enable\": \"Aktivera DHCP.-server\",\n  \"dhcp_error\": \"Vi kunde inte avgöra om det finns en till DHCP-server på nätverket.\",\n  \"dhcp_form_gateway_input\": \"Gateway-IP\",\n  \"dhcp_form_lease_input\": \"Leasetid\",\n  \"dhcp_form_lease_title\": \"DHCP-leasetid (i sekunder)\",\n  \"dhcp_form_range_end\": \"Gränsslut\",\n  \"dhcp_form_range_start\": \"Startgräns\",\n  \"dhcp_form_range_title\": \"IP-adressgränser\",\n  \"dhcp_form_subnet_input\": \"Subnetmask\",\n  \"dhcp_found\": \"Några aktiva DHCP-servar upptäcktes. Det är inte säkert att aktivera inbyggda DHCP-servrar.\",\n  \"dhcp_hardware_address\": \"Hårdvaruadress\",\n  \"dhcp_interface_select\": \"Välj DHCP-gränssnitt\",\n  \"dhcp_ip_addresses\": \"IP-adresser\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 inställningar\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 inställningar\",\n  \"dhcp_lease_added\": \"Statisk lease \\\"{{key}}\\\" har lagts till\",\n  \"dhcp_lease_deleted\": \"Statisk lease \\\"{{key}}\\\" har raderats\",\n  \"dhcp_lease_updated\": \"Statiskt lease \\\"{{key}}\\\" har uppdaterats\",\n  \"dhcp_leases\": \"DHCP-lease\",\n  \"dhcp_leases_not_found\": \"Ingen DHCP-lease hittad\",\n  \"dhcp_new_static_lease\": \"Ny statisk lease\",\n  \"dhcp_not_found\": \"Det är säkert att aktivera den inbyggda DHCP-servern eftersom AdGuard Home inte hittade några aktiva DHCP-servrar i nätverket. Du bör dock kontrollera det igen manuellt eftersom den automatiska sökningenn efter DHCP-servrar inte ger 100 % garanti.\",\n  \"dhcp_reset\": \"Är du säker på att du vill ta bort DHCP inställningarna?\",\n  \"dhcp_reset_leases\": \"Återställ alla leasingavtal\",\n  \"dhcp_reset_leases_confirm\": \"Är du säker på att du vill ta bort alla leasingavtal?\",\n  \"dhcp_reset_leases_success\": \"DHCP-leasing har återställts\",\n  \"dhcp_settings\": \"DHCP-inställningar\",\n  \"dhcp_static_ip_error\": \"För att kunna använda en DHCP-server måste det finnas en statisk IP-adress. AdGuard Home kunde inte avgöra om nätverksgränssnittet är konfigurerat med en statisk IP-adress. Ställ in en statisk IP-adress manuellt.\",\n  \"dhcp_static_leases\": \"Statiska DHCP-leases\",\n  \"dhcp_static_leases_not_found\": \"Inga statiska DHCP-leases hittade\",\n  \"dhcp_table_expires\": \"Utgår\",\n  \"dhcp_table_hostname\": \"Värdnamn\",\n  \"dhcp_title\": \"DHCP-server (experimentell)\",\n  \"dhcp_warning\": \"Om du vill använda den inbyggda DHCP servern ändå, se till att det inte finns några andra aktiva DHCP servrar. Annars kan den störa internetanslutningen för anslutna enheter!\",\n  \"disable_for_hours\": \"I {{count}} timme\",\n  \"disable_for_hours_plural\": \"I {{count}} timmar\",\n  \"disable_for_minutes\": \"I {{count}} minut\",\n  \"disable_for_minutes_plural\": \"I {{count}} minuter\",\n  \"disable_for_seconds\": \"I {{count}} sekund\",\n  \"disable_for_seconds_plural\": \"I {{count}} sekunder\",\n  \"disable_ipv6\": \"Inaktivera upplösning av IPv6 adresser\",\n  \"disable_ipv6_desc\": \"Släpp alla DNS-frågor för IPv6-adresser (typ AAAA) och ta bort IPv6-tips från HTTPS-svar.\",\n  \"disable_notify_for_hours\": \"Inaktivera skyddet i {{count}} timme\",\n  \"disable_notify_for_hours_plural\": \"Inaktivera skyddet i {{count}} timmar\",\n  \"disable_notify_for_minutes\": \"Inaktivera skyddet i {{count}} minut\",\n  \"disable_notify_for_minutes_plural\": \"Inaktivera skyddet i {{count}} minuter\",\n  \"disable_notify_for_seconds\": \"Inaktivera skyddet i {{count}} sekund\",\n  \"disable_notify_for_seconds_plural\": \"Inaktivera skyddet i {{count}} sekunder\",\n  \"disable_notify_until_tomorrow\": \"Inaktivera skyddet tills imorgon\",\n  \"disable_protection\": \"Koppla bort skydd\",\n  \"disable_rewrites\": \"Inaktivera omskrivningsregler\",\n  \"disable_until_tomorrow\": \"Tills imorgon\",\n  \"disabled\": \"Avaktiverad\",\n  \"disabled_dhcp\": \"Dhcp-server avaktiverad\",\n  \"disabled_filtering_toast\": \"Filtrering bortkopplad\",\n  \"disabled_parental_toast\": \"Föräldrakontroll bortkopplat\",\n  \"disabled_protection\": \"Kopplade bort skydd\",\n  \"disabled_safe_browsing_toast\": \"Säker surfning inaktiverad\",\n  \"disabled_safe_search_toast\": \"Säker webbsökning bortkopplat\",\n  \"disallow_this_client\": \"Tillåt inte den här klienten\",\n  \"dns_addresses\": \"DNS-adresser\",\n  \"dns_allowlists\": \"DNS frilistor\",\n  \"dns_allowlists_desc\": \"Domäner från DNS frilistor kommer att tillåtas även om de finns i någon av blockeringslistorna.\",\n  \"dns_blocklists\": \"DNS blockeringslistor\",\n  \"dns_blocklists_desc\": \"AdGuard Home kommer att blockera domäner som matchar blockeringslistorna.\",\n  \"dns_cache_config\": \"DNS cache konfiguration\",\n  \"dns_cache_config_desc\": \"Här kan du konfigurera DNS cache\",\n  \"dns_cache_size\": \"DNS-cachestorlek, i byte\",\n  \"dns_config\": \"DNS server konfiguration\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS-Integritet\",\n  \"dns_providers\": \"Här är en <0>lista över kända DNS-leverantörer</0> att välja från.\",\n  \"dns_query\": \"DNS-förfrågningar\",\n  \"dns_rewrites\": \"DNS omskrivningar\",\n  \"dns_settings\": \"DNS-inställningar\",\n  \"dns_start\": \"DNS servern startar\",\n  \"dns_status_error\": \"Fel vid kontroll av DNS serverns status\",\n  \"dns_test_not_ok_toast\": \"Server \\\"{{key}}\\\": kunde inte användas. Var snäll och kolla att du skrivit in rätt\",\n  \"dns_test_ok_toast\": \"Angivna DNS servrar fungerar korrekt\",\n  \"dns_test_parsing_error_toast\": \"Avsnitt {{section}}: rad {{line}}: kunde inte användas, kontrollera att du har skrivit det korrekt\",\n  \"dns_test_warning_toast\": \"Uppströms \\\"{{key}}\\\" svarar inte på testförfrågningar och kanske inte fungerar korrekt\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Aktivera DNSSEC\",\n  \"dnssec_enable_desc\": \"Ställ in DNSSEC flagga i de utgående DNS frågorna och kontrollera resultatet (DNSSEC-aktiverad upplösare krävs).\",\n  \"domain\": \"Domän\",\n  \"domain_desc\": \"Ange domännamnet eller jokertecken som du vill ska skrivas om.\",\n  \"domain_name_table_header\": \"Domännamn\",\n  \"domain_or_client\": \"Domän eller klient\",\n  \"down\": \"Ner\",\n  \"download_mobileconfig\": \"Ladda ner konfigurationsfil\",\n  \"download_mobileconfig_doh\": \"Ladda ner .mobileconfig för DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Ladda ner .mobileconfig för DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Redigera frilista\",\n  \"edit_blocklist\": \"Redigera blockeringslista\",\n  \"edit_table_action\": \"Redigera\",\n  \"edns_cs_desc\": \"Skicka klienternas subnät till DNS servrarna.\",\n  \"edns_enable\": \"Aktivera EDNS-klient subnät\",\n  \"edns_use_custom_ip\": \"Använd anpassad IP för EDNS\",\n  \"edns_use_custom_ip_desc\": \"Tillåt att använda anpassad IP för EDNS\",\n  \"elapsed\": \"Förfluten tid\",\n  \"empty_response_status\": \"Tomt\",\n  \"enable_protection\": \"Koppla på skydd\",\n  \"enable_protection_timer\": \"Skyddet kommer att aktiveras i {{time}}\",\n  \"enable_rewrites\": \"Aktivera omskrivningsregler\",\n  \"enable_upstream_dns_cache\": \"Aktivera DNS-cachelagring för den här klientens anpassade uppströmskonfiguration\",\n  \"enabled_dhcp\": \"DHCP-server aktiverad\",\n  \"enabled_filtering_toast\": \"Filtrering inkopplad\",\n  \"enabled_parental_toast\": \"Föräldrakontroll inkopplat\",\n  \"enabled_protection\": \"Kopplade på skydd\",\n  \"enabled_safe_browsing_toast\": \"Säker surfning aktiverat\",\n  \"enabled_save_search_toast\": \"Säker webbsökning inkopplat\",\n  \"enabled_table_header\": \"Inkopplat\",\n  \"encryption_certificate_path\": \"Certifikatsökväg\",\n  \"encryption_certificates\": \"Certifikat\",\n  \"encryption_certificates_desc\": \"För att använda kryptering måste du ange ett giltigt SSL-certifikat för din domän. Du kan skaffa ett certifikat gratis på <0>{{link}}</0> eller köpa ett från någon av de godkända certifikatutfärdare.\",\n  \"encryption_certificates_input\": \"Kopiera/klistra in dina PEM-kodade certifikat här.\",\n  \"encryption_certificates_source_content\": \"Klistra in certifikatets innehåll\",\n  \"encryption_certificates_source_path\": \"Ange sökväg för certifikatfilen\",\n  \"encryption_chain_invalid\": \"Certifikatkedjan är ogiltig\",\n  \"encryption_chain_valid\": \"Certifikatkedjan är giltig\",\n  \"encryption_config_saved\": \"Krypteringsinställningar sparade\",\n  \"encryption_desc\": \"Krypteringsstöd (HTTPS/TLS) för både DNS och adminwebbgränssnitt.\",\n  \"encryption_doq\": \"DNS-over-QUIC port\",\n  \"encryption_doq_desc\": \"Om denna port är konfigurerad kommer AdGuard Home att köra en DNS-over-QUIC-server på denna port. \",\n  \"encryption_dot\": \"DNS-över-TLS port\",\n  \"encryption_dot_desc\": \"Om den här porten ställs in kommer AdGuard Home att använda DNS-over-TLS-server på porten.\",\n  \"encryption_enable\": \"Aktivera kryptering (HTTPS, DNS-över-HTTPS och DNS-över-TLS)\",\n  \"encryption_enable_desc\": \"Om kryptering är aktiverat kommer administratörsgränssnittet till AdGuard Home att köras över HTTPS och DNS-servern kommer att lyssna på förfrågningar över DNS-over-HTTPS och DNS-over-TLS.\",\n  \"encryption_expire\": \"Utgår\",\n  \"encryption_hostnames\": \"Värdnamn\",\n  \"encryption_https\": \"HTTPS-port\",\n  \"encryption_https_desc\": \"Om en HTTPS-port är inställd kommer gränssnittet till AdGuard Home administrering att kunna nås via HTTPS och kommer också att erbjuda DNS-over-HTTPS på '/dns-query' plats.\",\n  \"encryption_issuer\": \"Utgivare\",\n  \"encryption_key\": \"Privat nyckel\",\n  \"encryption_key_input\": \"Kopiera/klistra in din PEM-kodade privata nyckel för ditt certifikat här.\",\n  \"encryption_key_invalid\": \"Det här är en ogiltig {{type}} privat nyckel\",\n  \"encryption_key_source_content\": \"Klistra in den privata nyckelns innehåll\",\n  \"encryption_key_source_path\": \"Ställ in en sökväg till en privat nyckelfil\",\n  \"encryption_key_valid\": \"Det här är en giltig {{type}} privat nyckel\",\n  \"encryption_plain_dns_desc\": \"Vanlig DNS är aktiverad som standard. Du kan inaktivera den för att tvinga alla enheter att använda krypterad DNS. För att göra detta måste du aktivera minst ett krypterat DNS-protokoll\",\n  \"encryption_plain_dns_enable\": \"Aktivera vanlig DNS\",\n  \"encryption_plain_dns_error\": \"För att inaktivera vanlig DNS, aktivera minst ett krypterat DNS-protokoll\",\n  \"encryption_private_key_path\": \"Privat nyckel sökväg\",\n  \"encryption_redirect\": \"Omdirigera till HTTPS automatiskt\",\n  \"encryption_redirect_desc\": \"Om bockad kommer AdGuard Home automatiskt att omdirigera dig från HTTP till HTTPS-adresser.\",\n  \"encryption_reset\": \"Är du säker på att du vill återställa krypteringsinställningarna?\",\n  \"encryption_server\": \"Servernamn\",\n  \"encryption_server_desc\": \"För att använda HTTPS behöver du skriva in servernamnet som stämmer överens med ditt SSL-certifikat.\",\n  \"encryption_server_enter\": \"Skriv in ditt domännamn\",\n  \"encryption_settings\": \"Krypteringsinställningar\",\n  \"encryption_status\": \"Status\",\n  \"encryption_subject\": \"Subjekt\",\n  \"encryption_title\": \"Kryptering\",\n  \"encryption_warning\": \"Varning\",\n  \"enforce_safe_search\": \"Använd SafeSearch\",\n  \"enforce_save_search_hint\": \"AdGuard Home kommer tvinga säker surf på följande sökmotorer: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Genomdrev SafeSearch\",\n  \"enter_cache_size\": \"Ange cachestorlek (byte)\",\n  \"enter_cache_ttl_max_override\": \"Ange maximal TTL (sekunder)\",\n  \"enter_cache_ttl_min_override\": \"Ange minsta TTL (sekunder)\",\n  \"enter_name_hint\": \"Skriv in namn\",\n  \"enter_url_or_path_hint\": \"Ange en URL eller en absolut sökväg till listan\",\n  \"enter_valid_allowlist\": \"Ange en giltig URL till frilistan.\",\n  \"enter_valid_blocklist\": \"Ange en giltig URL till blockeringslistan.\",\n  \"error_details\": \"Felinformation\",\n  \"example_comment\": \"! Här kommer en kommentar\",\n  \"example_comment_hash\": \"# Också en kommentar\",\n  \"example_comment_meaning\": \"Endast en kommentar\",\n  \"example_meaning_filter_block\": \"blockera åtkomst till domän example.org domain och alla dess subdomäner\",\n  \"example_meaning_filter_whitelist\": \"avblockera åtkomst till domän example.org domain och alla dess subdomäner\",\n  \"example_meaning_host_block\": \"AdGuard Home kommer nu att returnera adress 127.0.0.1 för domänexemplet example.org (dock utan dess subdomäner).\",\n  \"example_multiple_upstreams_reserved\": \"flera uppströmsservrar <0>för specifika domäner</0>;\",\n  \"example_regex_meaning\": \"blockera åtkomst till domäner som matchar det angivna uttrycket\",\n  \"example_rewrite_domain\": \"skriv bara om svar för detta domännamn.\",\n  \"example_rewrite_wildcard\": \"skriv om svar för alla <0>example.org</0> subdomäner.\",\n  \"example_upstream_comment\": \"en kommentar.\",\n  \"example_upstream_doh\": \"krypterat <0>DNS-over-HTTPS</0>\",\n  \"example_upstream_doh3\": \"krypterad DNS-över-HTTPS med påtvingad <0>HTTP/3</0> och ingen reserv till HTTP/2 eller lägre;\",\n  \"example_upstream_doq\": \"krypterat <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"krypterat <0>DNS-over-TLS</0>\",\n  \"example_upstream_regular\": \"vanlig DNS (över UDP)\",\n  \"example_upstream_regular_port\": \"vanlig DNS (via UDP, med port);\",\n  \"example_upstream_reserved\": \"uppström <0>för en specifik domän</0>;\",\n  \"example_upstream_sdns\": \"Du kan använda <0>DNS-stamps</0> för <1>DNSCrypt</1> eller <2>DNS-over-HTTPS</2>-resolvers\",\n  \"example_upstream_tcp\": \"vanlig DNS (över UDP)\",\n  \"example_upstream_tcp_hostname\": \"vanlig DNS (över TCP, värdnamn);\",\n  \"example_upstream_tcp_port\": \"vanlig DNS (via TCP, med port);\",\n  \"example_upstream_udp\": \"vanlig DNS (över UDP, värdnamn);\",\n  \"examples_title\": \"Exempel\",\n  \"fallback_dns_desc\": \"Lista över reserv-DNS-servrar som används när uppströms DNS-servrar inte svarar. Syntaxen är densamma som i huvuduppströmsfältet ovan.\",\n  \"fallback_dns_placeholder\": \"Ange en reserv-DNS-server per rad\",\n  \"fallback_dns_title\": \"Reserv DNS-servrar\",\n  \"faq\": \"FAQ\",\n  \"fastest_addr\": \"Snabbaste IP adressen\",\n  \"fastest_addr_desc\": \"Vänta på svar från <b>alla</b> DNS-servrar, mät TCP-anslutningshastigheten för varje server och returnera IP-adressen till servern med den snabbaste anslutningshastigheten.<br/>Detta läge kan avsevärt sakta ner DNS-frågor om en eller flera uppströmsservrar inte svarar. Se till att dina uppströmsservrar är stabila och att din uppströms timeout är låg.\",\n  \"filter\": \"Filter\",\n  \"filter_added_successfully\": \"Listan har lagts till\",\n  \"filter_allowlist\": \"VARNING: Denna åtgärd kommer också att utesluta regeln \\\"{{disallowed_rule}}\\\" från listan över tillåtna klienter.\",\n  \"filter_category_general\": \"Allmänt\",\n  \"filter_category_general_desc\": \"Listor som blockerar spårning och reklam på de flesta enheterna\",\n  \"filter_category_other\": \"Övrigt\",\n  \"filter_category_other_desc\": \"Andra blockeringslistor\",\n  \"filter_category_regional\": \"Regional\",\n  \"filter_category_regional_desc\": \"Listor som fokuserar på regionala annonser och spårningsservrar\",\n  \"filter_category_security\": \"Säkerhet\",\n  \"filter_category_security_desc\": \"Listor utformade specifikt för att blockera skadliga domäner, nätfiske och bluffdomäner\",\n  \"filter_removed_successfully\": \"Listan har tagits bort\",\n  \"filter_updated\": \"Listan har uppdaterats\",\n  \"filtered\": \"Filtrerad\",\n  \"filtered_custom_rules\": \"Filtrerat efter anpassade filtreringsregler\",\n  \"filtering_rules_learn_more\": \"<0>Mer info</0> om att skapa dina egna blockeringslistor för värdar.\",\n  \"filters\": \"Filter\",\n  \"filters_and_hosts_hint\": \"AdGuard tillämpar grundläggande annonsblockeringsregler och värdfiltersyntaxer\",\n  \"filters_block_toggle_hint\": \"Du kan ställa in egna blockerings regler i <a>Filterinställningar</a>.\",\n  \"filters_configuration\": \"Filterinställningar\",\n  \"filters_enable\": \"Aktivera filter\",\n  \"filters_interval\": \"Filteruppdateringsintervall\",\n  \"fix\": \"Fixa\",\n  \"for_last_days\": \"för den senaste {{count}} dagen\",\n  \"for_last_days_plural\": \"för de senaste {{count}} dagarna\",\n  \"for_last_hours\": \"för den senaste {{count}} timme\",\n  \"for_last_hours_plural\": \"för de senaste {{count}} timmar\",\n  \"forgot_password\": \"Glömt lösenord?\",\n  \"forgot_password_desc\": \"Följ <0>dessa steg</0> för att skapa ett nytt lösenord till ditt konto.\",\n  \"form_add_id\": \"Lägg till identifierare\",\n  \"form_answer\": \"Ange IP adress eller domännamn\",\n  \"form_client_name\": \"Ange klientnamn\",\n  \"form_domain\": \"Ange domännamn eller jokertecken\",\n  \"form_enter_blocked_response_ttl\": \"Ange TTL för blockerat svar (sekunder)\",\n  \"form_enter_host\": \"Ange ett värdnamn\",\n  \"form_enter_hostname\": \"Skriv in värdnamn\",\n  \"form_enter_id\": \"Ange identifierare\",\n  \"form_enter_ip\": \"Skriv in IP\",\n  \"form_enter_mac\": \"Skriv in MAC\",\n  \"form_enter_rate_limit\": \"Ange förfrågnings gräns\",\n  \"form_enter_rate_limit_subnet_len\": \"Ange subnätprefixlängd för hastighetsbegränsning\",\n  \"form_enter_subnet_ip\": \"Ange en IP adress i subnätet \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Ange uppströmsserverns timeout-längd i sekunder\",\n  \"form_error_answer_format\": \"Ogiltigt svarsformat\",\n  \"form_error_client_id_format\": \"Ogiltigt klient-ID\",\n  \"form_error_domain_format\": \"Ogiltigt domänformat\",\n  \"form_error_equal\": \"Får inte vara samma\",\n  \"form_error_gateway_ip\": \"Lease kan inte ha IP-adressen för gatewayen\",\n  \"form_error_ip4_format\": \"Ogiltig IPv4-adress\",\n  \"form_error_ip4_gateway_format\": \"Ogiltig IPv4 adress för gatewayen\",\n  \"form_error_ip6_format\": \"Ogiltig IPv6-adress\",\n  \"form_error_ip_format\": \"Ogiltig IP-adress\",\n  \"form_error_mac_format\": \"Ogiltig MAC-adress\",\n  \"form_error_password\": \"Lösenorden överensstämmer inte\",\n  \"form_error_password_length\": \"Lösenordet måste vara {{min}} till {{max}} tecken långt\",\n  \"form_error_port\": \"Skriv in ett giltigt portnummer\",\n  \"form_error_port_range\": \"Ange ett portnummer inom värdena 80-65535\",\n  \"form_error_port_unsafe\": \"Osäker port\",\n  \"form_error_positive\": \"Måste vara större än noll\",\n  \"form_error_required\": \"Obligatoriskt fält\",\n  \"form_error_server_name\": \"Ogiltigt servernamn\",\n  \"form_error_subnet\": \"Subnätet \\\"{{cidr}}\\\" innehåller inte IP-adressen \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"Ogiltigt URL-format\",\n  \"form_error_url_or_path_format\": \"Ogiltig URL eller absolut sökväg till listan\",\n  \"form_select_tags\": \"Välj klienttaggar\",\n  \"found_in_known_domain_db\": \"Hittad i domändatabas.\",\n  \"friday\": \"Fredag\",\n  \"friday_short\": \"Fre\",\n  \"gateway_or_subnet_invalid\": \"Subnätmask ogiltig\",\n  \"general_settings\": \"Allmänna inställningar\",\n  \"general_statistics\": \"Allmän statistik\",\n  \"get_started\": \"Kom igång\",\n  \"greater_range_start_error\": \"Måste vara högre än starten på intervallet\",\n  \"homepage\": \"Hemsida\",\n  \"host_whitelisted\": \"Värden är tillåten\",\n  \"ignore_domains\": \"Ignorerade domäner (avgränsade med ny rad)\",\n  \"ignore_domains_desc_query\": \"Förfrågningar som matchar dessa regler skrivs inte till förfrågningsloggen\",\n  \"ignore_domains_desc_stats\": \"Förfrågningar som matchar dessa regler skrivs inte till statistiken\",\n  \"ignore_domains_title\": \"Ignorerade domäner\",\n  \"ignore_query_log\": \"Ignorera den här klienten i frågeloggen\",\n  \"ignore_statistics\": \"Ignorera denna kund i statistiken\",\n  \"install_auth_confirm\": \"Bekräfta lösenord\",\n  \"install_auth_desc\": \"Lösenordsautentisering till ditt AdGuard Home administratörsgränssnitt måste konfigureras. Även om AdGuard Home bara är tillgängligt i ditt lokala nätverk är det fortfarande viktigt att skydda det från obegränsad åtkomst.\",\n  \"install_auth_password\": \"Lösenord\",\n  \"install_auth_password_enter\": \"Skriv in lösenord\",\n  \"install_auth_title\": \"Autentisering\",\n  \"install_auth_username\": \"Användarnamn\",\n  \"install_auth_username_enter\": \"Ange användarnamn\",\n  \"install_devices_address\": \"AdGuard Home DNS-server täcker följande adresser\",\n  \"install_devices_android_list_1\": \"Välj Inställningar från Androids hemknapp\",\n  \"install_devices_android_list_2\": \"Tryck på Nätverk och Internet, Wi-Fi. Alla tillgängliga nätverk visas i en lista (det går inte all välja egen DNS på mobilnätverk.\",\n  \"install_devices_android_list_3\": \"Håll ner på nätverksnamnet som du är ansluten till och välj Ändra nätverk.\",\n  \"install_devices_android_list_4\": \"På en del enheter kan du behöva välja Avancerat för att komma åt ytterligare inställningar. För att ändra på DNS-inställningar måste du byta IP-inställning från DHCP till Statisk. På Android Pie väljs Privat DNS på Nätverk och internet.\",\n  \"install_devices_android_list_5\": \"Ändra DNS 1 och DNS 2 värdena till serveradresserna för din AdGuard Home.\",\n  \"install_devices_desc\": \"För att kunna använda AdGuard Home måste du ställa in dina enheter för att utnyttja den.\",\n  \"install_devices_ios_list_1\": \"Tryck Inställningar från hemskärmen.\",\n  \"install_devices_ios_list_2\": \"Välj Wi_Fi på den vänstra menyn (det går inte att ställa in egen DNS för mobila nätverk).\",\n  \"install_devices_ios_list_3\": \"Tryck på namnet på den aktiva anslutningen.\",\n  \"install_devices_ios_list_4\": \"Skriv in AdGuard Homes serveradresser i DNS-fälten.\",\n  \"install_devices_macos_list_1\": \"Klicka på Apple-ikonen och välj Systemalternativ.\",\n  \"install_devices_macos_list_2\": \"Klicka på Nätverk.\",\n  \"install_devices_macos_list_3\": \"Välj den första anslutningen i listan och klicka på Avancerat.\",\n  \"install_devices_macos_list_4\": \"Klicka på DNS-fliken och skriv in AdGuard Homes serveradresser\",\n  \"install_devices_router\": \"Router\",\n  \"install_devices_router_desc\": \"Den här anpassningen kommer att automatiskt täcka in alla de enheter som är anslutna till din hemmarouter och du behöver därför inte konfigurera var och en individuellt.\",\n  \"install_devices_router_list_1\": \"Öppna inställningarna för din router. Vanligtvis kan du komma åt den från din webbläsare via en URL, som http://192.168.0.1/ eller http://192.168.1.1/. Du kan bli ombedd att ange ett lösenord. Om du inte kommer ihåg det kan du ofta återställa lösenordet genom att trycka på en knapp på själva routern, men var medveten om att om denna procedur väljs kommer du förmodligen att förlora hela routerkonfigurationen. Om din router kräver en app för att konfigurera den, installera appen på din telefon eller dator och använd den för att komma åt routerns inställningar.\",\n  \"install_devices_router_list_2\": \"Leta upp DHCP/DNS-inställningarna. Titta efter DNS-tecken intill ett fält med två eller tre uppsättningar siffror, var och en uppdelade i grupper om fyra med en eller tre siffror.\",\n  \"install_devices_router_list_3\": \"Ange serveradressen till ditt AdGuard Home.\",\n  \"install_devices_router_list_4\": \"På vissa routertyper kan en anpassad DNS server inte konfigureras. I så fall kan det hjälpa att konfigurera AdGuard Home som en <0>DHCP server</0>. Annars bör du kontrollera routermanualen om hur du anpassar DNS servrar på din specifika routermodell.\",\n  \"install_devices_title\": \"Ställ in dina enheter\",\n  \"install_devices_windows_list_1\": \"Öppna Kontrollpanelen via Start eller Windows Sök.\",\n  \"install_devices_windows_list_2\": \"Välj Nätverks och delningscenter, Nätverk och Internet.\",\n  \"install_devices_windows_list_3\": \"På vänster sida av skärmen hittar du \\\"Ändra adapterinställningar\\\" och klicka på den.\",\n  \"install_devices_windows_list_4\": \"Markera din aktiva anslutning. Högerklicka på den och välj Egenskaper.\",\n  \"install_devices_windows_list_5\": \"Hitta \\\"Internet Protocol Version 4 (TCP/IPv4)\\\" (eller, för IPv6, \\\"Internet Protocol Version 6 (TCP/IPv6)\\\") i listan, välj den och klicka sedan på Egenskaper igen.\",\n  \"install_devices_windows_list_6\": \"Välj \\\"Använd följande DNS-serveradresser\\\" och ange dina AdGuard Home-serveradresser.\",\n  \"install_saved\": \"Sparat utan fel\",\n  \"install_settings_all_interfaces\": \"Alla gränssnitt\",\n  \"install_settings_dns\": \"DNS-server\",\n  \"install_settings_dns_desc\": \"Du behöver ställa in dina enheter eller din router för att använda DNS-server på följande adresser.\",\n  \"install_settings_interface_link\": \"Din administratörssida för AdGuard Home finns på följande adresser:\",\n  \"install_settings_listen\": \"Övervakningsgränssnitt\",\n  \"install_settings_port\": \"Port\",\n  \"install_settings_title\": \"Administratörens webbgränssnitt\",\n  \"install_static_configure\": \"AdGuard Home har upptäckt att den dynamiska IP adressen <0>{{ip}}</0> används. Vill du att den ska ställas in som din statiska adress?\",\n  \"install_static_error\": \"AdGuard Home kan inte konfigurera det automatiskt för detta nätverksgränssnitt. Vänligen leta efter en instruktion om hur du gör detta manuellt.\",\n  \"install_static_ok\": \"Goda nyheter! Den statiska IP adressen är redan konfigurerad\",\n  \"install_step\": \"Steg\",\n  \"install_submit_desc\": \"Installationen är klar och du kan börja använda AdGuard Home.\",\n  \"install_submit_title\": \"Grattis!\",\n  \"install_welcome_desc\": \"AdGuard Home är en DNS-server för nätverkstäckande annons- och spårningsblockering. Dess syfte är att de dig kontroll över hela nätverket och alla dina enheter, utan behov av att använda klientbaserade program.\",\n  \"install_welcome_title\": \"Välkommen till AdGuard Home!\",\n  \"interval_24_hour\": \"24 timmar\",\n  \"interval_6_hour\": \"6 timmar\",\n  \"interval_days\": \"{{count}} dag\",\n  \"interval_days_plural\": \"{{count}} dagar\",\n  \"interval_hours\": \"{{count}} timme\",\n  \"interval_hours_plural\": \"{{count}} timmar\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP-adress\",\n  \"known_tracker\": \"Känd spårare\",\n  \"last_rule_in_allowlist\": \"Det går inte att avvisa den här klienten eftersom att utesluta regeln \\\"{{disallowed_rule}}\\\" kommer att INAKTIVERA listan \\\"Tillåtna klienter\\\".\",\n  \"last_time_updated_table_header\": \"Uppdaterades senast\",\n  \"list_confirm_delete\": \"Är du säker på att du vill ta bort den här listan?\",\n  \"list_label\": \"Lista\",\n  \"list_updated\": \"{{count}} listan uppdaterad\",\n  \"list_updated_plural\": \"{{count}} listor uppdaterade\",\n  \"list_url_table_header\": \"Lista URL\",\n  \"load_balancing\": \"Lastbalansering\",\n  \"load_balancing_desc\": \"Fråga en uppströmsserver åt gången.<br/>AdGuard Home använder en viktad slumpmässig algoritm för att välja servrar med det lägsta antalet misslyckade uppslagningar och den lägsta genomsnittliga uppslagningstiden.\",\n  \"loading_table_status\": \"Läser in...\",\n  \"local_ptr_default_resolver\": \"Som standard använder AdGuard Home följande omvända DNS upplösare: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS-servrar som används av AdGuard Home för privata PTR-, SOA- och NS-förfrågningar. En begäran anses vara privat om den frågar efter en ARPA-domän som innehåller ett subnät inom privata IP-intervallerna (t.ex. \\\"192.168.12.34\\\") och kommer från en klient med en privat IP-adress. Om det inte är inställt kommer standard DNS-resolvers för ditt operativsystem att användas, förutom AdGuard Home IP-adresserna.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home kunde inte fastställa lämpliga privata omvända DNS upplösare för detta system.\",\n  \"local_ptr_placeholder\": \"Ange en IP-adress per rad\",\n  \"local_ptr_title\": \"Privata omvända DNS-servrar\",\n  \"location\": \"Plats\",\n  \"log_and_stats_section_label\": \"Förfrågningslogg och statistik\",\n  \"lower_range_start_error\": \"Måste vara lägre än starten på intervallet\",\n  \"main_settings\": \"Huvudinställningar\",\n  \"make_static\": \"Gör statisk\",\n  \"manual_update\": \"Vänligen <a>följ dessa steg</a> för att uppdatera manuellt.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Måndag\",\n  \"monday_short\": \"Mån\",\n  \"name\": \"Namn\",\n  \"name_table_header\": \"Namn\",\n  \"netname\": \"Nätverksnamn\",\n  \"network\": \"Nätverk\",\n  \"new_allowlist\": \"Ny frilista\",\n  \"new_blocklist\": \"Ny blockeringslista\",\n  \"next\": \"Nästa\",\n  \"next_btn\": \"Nästa\",\n  \"no_blocklist_added\": \"Inga blocklistor har lagts till\",\n  \"no_clients_found\": \"Inga klienter hittade\",\n  \"no_domains_found\": \"Inga domäner hittade\",\n  \"no_logs_found\": \"Inga logga funna\",\n  \"no_servers_specified\": \"Inga servrar angivna\",\n  \"no_upstreams_data_found\": \"Inga uppströmsdata hittades\",\n  \"no_whitelist_added\": \"Inga frilistor har lagts till\",\n  \"nothing_found\": \"Ingenting hittades\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Antalet DNS-förfrågningar som blockerades av annonsfilter och värdens blockeringsklistor\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Antalet vuxensajter som blockerats\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Antalet DNS-förfrågningar som blockerades av AdGuards modul för surfsäkerhet\",\n  \"number_of_dns_query_days\": \"Antalet DNS-förfrågningar som utfördes under senaste {{count}} dagen\",\n  \"number_of_dns_query_days_plural\": \"Ett antal DNS förfrågningar utfördes under de senaste {{count}} dagarna\",\n  \"number_of_dns_query_hours\": \"Ett antal DNS förfrågningar utfördes för den senaste {{count}} timme\",\n  \"number_of_dns_query_hours_plural\": \"Ett antal DNS förfrågningar utfördes för den senaste {{count}} timmar\",\n  \"number_of_dns_query_to_safe_search\": \"Antalet DNS-förfrågningar till sökmotorer för vilka SafeSearch genomdrevs\",\n  \"nxdomain\": \"NXDOMÄN\",\n  \"off\": \"AV\",\n  \"on\": \"PÅ\",\n  \"open_dashboard\": \"Öppna Kontrollbordet\",\n  \"orgname\": \"Organisationsnamn\",\n  \"original_response\": \"Ursprungligt svar\",\n  \"out_of_range_error\": \"Måste vara utanför intervallet \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Sida\",\n  \"parallel_requests\": \"Parallella förfrågningar\",\n  \"parental_control\": \"Föräldrakontroll\",\n  \"password_label\": \"Lösenord\",\n  \"password_placeholder\": \"Skriv in lösenord\",\n  \"plain_dns\": \"Vanlig DNS\",\n  \"port_53_faq_link\": \"Port 53 är ofta upptagen av \\\"DNSStubListener\\\" eller \\\"systemd-resolved\\\" tjänster. Läs <0>denna instruktion</0> om hur du löser detta.\",\n  \"previous_btn\": \"Föregående\",\n  \"privacy_policy\": \"Integritetspolicy\",\n  \"processing_update\": \"Vänta, AdGuard Home uppdateras\",\n  \"protection_section_label\": \"Skydd\",\n  \"protocol\": \"Protokoll\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Förfrågningslogg\",\n  \"query_log_clear\": \"Rensa förfrågningsloggar\",\n  \"query_log_cleared\": \"Förfrågningsloggen har rensats\",\n  \"query_log_configuration\": \"Logginställningar\",\n  \"query_log_confirm_clear\": \"Är du säker på att du vill rensa hela förfrågningsloggen?\",\n  \"query_log_disabled\": \"Förfrågningsloggen är avaktiverad och kan konfigureras i <0>inställningar</0>\",\n  \"query_log_enable\": \"Aktivera logg\",\n  \"query_log_filtered\": \"Filtrerat av {{filter}}\",\n  \"query_log_response_status\": \"Status: {{value}}\",\n  \"query_log_retention\": \"Förfrågningsloggars retentionstid\",\n  \"query_log_retention_confirm\": \"Är du säker på att du vill ändra förfrågningsloggars retentionstid? Om du minskar intervallet kommer viss data att gå förlorad\",\n  \"query_log_strict_search\": \"Använd dubbla citattecken för strikt sökning\",\n  \"query_log_updated\": \"Förfrågningsloggen har uppdaterats\",\n  \"rate_limit\": \"Förfrågnings gräns\",\n  \"rate_limit_desc\": \"Antalet förfrågningar per sekund som tillåts per klient. Att sätta den till 0 innebär ingen gräns.\",\n  \"rate_limit_subnet_len_ipv4\": \"Prefixlängd för subnät för IPv4-adresser\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Subnätprefixlängd för IPv4-adresser som används för hastighetsbegränsning. Standard är 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4-subnätets prefixlängd ska vara mellan 0 och 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Prefixlängd för subnät för IPv6-adresser\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Subnätprefixlängd för IPv6-adresser som används för hastighetsbegränsning. Standard är 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6-subnätets prefixlängd ska vara mellan 0 och 128\",\n  \"rate_limit_whitelist\": \"Vitlista för hastighetsgränser\",\n  \"rate_limit_whitelist_desc\": \"IP-adresser uteslutna från hastighetsbegränsning\",\n  \"rate_limit_whitelist_placeholder\": \"Ange en IP-adress per rad\",\n  \"refresh_btn\": \"Läs in igen\",\n  \"refresh_statics\": \"Uppdatera statistik\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Rapportera ett problem\",\n  \"request_details\": \"Förfrågningsdetaljer\",\n  \"request_table_header\": \"Förfrågning\",\n  \"requests_count\": \"Förfrågningsantal\",\n  \"reset_settings\": \"Återställ inställningar\",\n  \"resolve_clients_desc\": \"Lös upp klienternas värdnamn med omvänt uppslag av klienternas IP-adresser genom att skicka PTR-frågor till motsvarande upplösare (privata DNS-servrar för lokala klienter, uppströmsservrar för klienter med offentliga IP-adresser).\",\n  \"resolve_clients_title\": \"Aktivera omvänd upplösning av klienters IP-adresser\",\n  \"response_code\": \"Svarskod\",\n  \"response_details\": \"Svarsdetaljer\",\n  \"response_table_header\": \"Svar\",\n  \"response_time\": \"Svarstid\",\n  \"rewrite_A\": \"<0>A</0>: specialvärde, behåll <0>A</0> poster från uppströms\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: specialvärde, behåll <0>AAAA</0> poster från uppströms\",\n  \"rewrite_add\": \"Lägg till DNS omskrivning\",\n  \"rewrite_added\": \"DNS-omskrivning för \\\"{{key}}\\\" lyckad\",\n  \"rewrite_applied\": \"Omskrivningsregeln tillämpas\",\n  \"rewrite_confirm_delete\": \"Är du säker på att du vill ta bort DNS-omskrivningen för \\\"{{key}}\\\"?\",\n  \"rewrite_deleted\": \"DNS-omskrivning för \\\"{{key}}\\\" har tagits bort\",\n  \"rewrite_desc\": \"Gör det enkelt att konfigurera anpassat DNS svar för ett specifikt domännamn.\",\n  \"rewrite_domain_name\": \"Domännamn: lägg till en CNAME post\",\n  \"rewrite_edit\": \"Redigera DNS-omskrivning\",\n  \"rewrite_hosts_applied\": \"Omskriven av värd fil regel\",\n  \"rewrite_ip_address\": \"IP adress: använd denna IP i ett A- eller AAAA-svar\",\n  \"rewrite_not_found\": \"Inga DNS omskrivningar hittades\",\n  \"rewrite_settings_updated\": \"Inställningarna för DNS-omskrivning har uppdaterats\",\n  \"rewrite_updated\": \"DNS-omskrivning har uppdaterats\",\n  \"rewrites_disabled_table_header\": \"Omskrivningar är inaktiverade\",\n  \"rewrites_enabled_table_header\": \"Omskrivningar är aktiverade\",\n  \"rewritten\": \"Omskriven\",\n  \"rows_table_footer_text\": \"rader\",\n  \"rule_added_to_custom_filtering_toast\": \"Regel tillagd till de egna filterreglerna: {{rule}}\",\n  \"rule_label\": \"Regel(er)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Regel borttagen från de egna filterreglerna: {{rule}}\",\n  \"rules_count_table_header\": \"Regelantal\",\n  \"safe_browsing\": \"Säker surfning\",\n  \"safe_search\": \"Säker sökning\",\n  \"saturday\": \"Lördag\",\n  \"saturday_short\": \"Lör\",\n  \"save_btn\": \"Spara\",\n  \"save_config\": \"Spara konfiguration\",\n  \"schedule_add\": \"Lägg till schema\",\n  \"schedule_current_timezone\": \"Aktuell tidszon: {{value}}\",\n  \"schedule_desc\": \"Ange inaktivitetsperioder för blockerade tjänster\",\n  \"schedule_edit\": \"Redigera schema\",\n  \"schedule_from\": \"Från\",\n  \"schedule_invalid_select\": \"Starttid måste vara före sluttid\",\n  \"schedule_modal_description\": \"Detta schema ersätter alla befintliga scheman för samma veckodag. Varje veckodag kan bara ha en inaktivitetsperiod.\",\n  \"schedule_modal_time_off\": \"Ingen blockering av tjänster:\",\n  \"schedule_new\": \"Nytt schema\",\n  \"schedule_remove\": \"Ta bort schema\",\n  \"schedule_save\": \"Spara schema\",\n  \"schedule_select_days\": \"Välj dagar\",\n  \"schedule_services\": \"Pausa blockering av tjänst\",\n  \"schedule_services_desc\": \"Konfigurera pausschemat för det tjänsteblockerande filtret\",\n  \"schedule_services_desc_client\": \"Konfigurera pausschemat för det tjänsteblockerande filtret för den här klienten\",\n  \"schedule_time_all_day\": \"Hela dagen\",\n  \"schedule_timezone\": \"Välj en tidszon\",\n  \"schedule_to\": \"Till\",\n  \"served_from_cache_label\": \"Levererat från cache\",\n  \"service_name\": \"Service namn\",\n  \"set_static_ip\": \"Ställ in en statisk IP adress\",\n  \"settings\": \"Inställningar\",\n  \"settings_custom\": \"Anpassade\",\n  \"settings_global\": \"Global\",\n  \"setup_config_to_enable_dhcp_server\": \"Ställ in konfiguration för att aktivera DHCP-server\",\n  \"setup_dns_notice\": \"För att kunna använda <1>DNS-över-HTTPS</1> eller <1>DNS-över-TLS</1>, behöver du <0>konfigurera Kryptering</0> i AdGuard Home-inställningar.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-över-TLS:</0> Använd: <1>{{address}}</1>\",\n  \"setup_dns_privacy_2\": \"<0>DNS-över-HTTPS:</0> Använd: <1>{{address}}</1>\",\n  \"setup_dns_privacy_3\": \"<0>Här är en lista över program du kan använda.</0>\",\n  \"setup_dns_privacy_4\": \"På en iOS 14 eller macOS Big Sur enhet kan du ladda ner en speciell '.mobileconfig' fil som lägger till <highlight>DNS-over-HTTPS</highlight> eller <highlight>DNS-over-TLS</highlight>-servrar till DNS inställningarna.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 har inbyggt stöd för DNS-över-TLS. Konfigurera och uppge domännamn under Inställningar → Nätverk & Internet → Avancerat → Privat DNS.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard för Android</0> stödjer <1>DNS-över-HTTPS</1> samt <1>DNS-över-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> lägger till stöd för <1>DNS-ÖVER-HTTPS</1> till Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS och macOS konfiguration\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> stödjer <1>DNS-ÖVER-HTTPS</1>, men för konfigurering krävs att du använder dig egen server. Du behöver generera en <2>DNS-Stämpel</2> till programmet.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard för iOS</0> stödjer <1>DNS-över-HTTPS</1> samt <1>DNS-över-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home kan själv vara en säker DNS-klient på alla plattformar.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> stödjer alla bekräftat säkra DNS-protokoll.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> stödjer <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> stödjer <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Du kan hitta fler implementeringar <0>här</0> och <1>här</1>.\",\n  \"setup_dns_privacy_other_title\": \"Andra implementeringar\",\n  \"setup_guide\": \"Installationsguide\",\n  \"show_all_filter_type\": \"Visa alla\",\n  \"show_blocked_responses\": \"Blockerade\",\n  \"show_filtered_type\": \"Visa filtrerade\",\n  \"show_processed_responses\": \"Utförda\",\n  \"show_whitelisted_responses\": \"Vitlistade\",\n  \"sign_in\": \"Logga in\",\n  \"sign_out\": \"Logga ut\",\n  \"source_label\": \"Källa\",\n  \"static_ip\": \"Statisk IP adress\",\n  \"static_ip_desc\": \"AdGuard Home är en server så den behöver en statisk IP-adress för att fungera korrekt. Annars kan din router vid något tillfälle tilldela en annan IP-adress till den här enheten.\",\n  \"statistics_clear\": \"Rensa statistik\",\n  \"statistics_clear_confirm\": \"Är du säker på att du vill radera statistiken?\",\n  \"statistics_cleared\": \"Statistiken har rensats\",\n  \"statistics_configuration\": \"Statistikkonfiguration\",\n  \"statistics_enable\": \"Aktivera statistik\",\n  \"statistics_retention\": \"Bevarande av statistik\",\n  \"statistics_retention_confirm\": \"Är du säker på att du vill ändra retentionstiden för statistik? Om du minskar intervallet kommer viss data att gå förlorad\",\n  \"statistics_retention_desc\": \"Om du minskar intervallet kommer viss data att gå förlorad\",\n  \"stats_adult\": \"Blockerade vuxensajter\",\n  \"stats_disabled\": \"Statistiken har inaktiverats. Du kan aktivera det från <0>inställningssidan</0>.\",\n  \"stats_disabled_short\": \"Statistiken har inaktiverats\",\n  \"stats_malware_phishing\": \"Blockerad skadekod/phishing\",\n  \"stats_params\": \"Statistikkonfiguration\",\n  \"stats_query_domain\": \"Mest eftersökta domäner\",\n  \"subnet_error\": \"Adresser måste finnas i ett subnät\",\n  \"sunday\": \"Söndag\",\n  \"sunday_short\": \"Sön\",\n  \"system_host_files\": \"Systemfiler\",\n  \"table_client\": \"Klient\",\n  \"table_name\": \"Namn\",\n  \"tags_desc\": \"Du kan välja de taggar som motsvarar klienten. Inkludera taggar i filtreringsregler för att tillämpa dem mer exakt. <0>Läs mer</0>.\",\n  \"tags_title\": \"Taggar\",\n  \"test_upstream_btn\": \"Testa uppströmmar\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_desc\": \"Auto (baserat på färgschemat på din enhet)\",\n  \"theme_dark\": \"Mörkt\",\n  \"theme_dark_desc\": \"Mörkt tema\",\n  \"theme_light\": \"Ljust\",\n  \"theme_light_desc\": \"Ljust tema\",\n  \"thursday\": \"Torsdag\",\n  \"thursday_short\": \"Tor\",\n  \"time_table_header\": \"Tid\",\n  \"top_blocked_domains\": \"Flest blockerade domäner\",\n  \"top_clients\": \"Toppklienter\",\n  \"top_upstreams\": \"Topp uppströmsservrar\",\n  \"topline_expired_certificate\": \"Ditt SSL-certifikat har gått ut. Uppdatera <0>Krypteringsinställningar</0>-\",\n  \"topline_expiring_certificate\": \"Ditt SSL-certifikat håller på att gå ut. <0>Krypteringsinställningar</0>.\",\n  \"tracker_source\": \"Spårningskälla\",\n  \"try_again\": \"Försök igen\",\n  \"ttl_cache_validation\": \"Minsta cache TTL-värde måste vara mindre än eller lika med maxvärdet\",\n  \"tuesday\": \"Tisdag\",\n  \"tuesday_short\": \"Tis\",\n  \"type_table_header\": \"Typ\",\n  \"unavailable_dhcp\": \"DHCP är inte tillgängligt\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home kan inte köra en DHCP-server på ditt operativsystem\",\n  \"unblock\": \"Avblockera\",\n  \"unblock_all\": \"Avblockera alla\",\n  \"unblock_for_this_client_only\": \"Avblockera endast för denna klient\",\n  \"unknown_filter\": \"Okänt filter {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} är nu tillgänglig! <0>Klicka här</0> för mer information.\",\n  \"update_failed\": \"Automatisk uppdatering misslyckad. Var god <a>följ stegen</a> för att uppdatera manuellt.\",\n  \"update_now\": \"Uppdatera nu\",\n  \"updated_custom_filtering_toast\": \"Anpassade filterregler sparade\",\n  \"updated_save_search_toast\": \"Inställningarna för Säker sökning uppdaterade\",\n  \"updated_upstream_dns_toast\": \"Sparade uppströms dns-servrar\",\n  \"updates_checked\": \"En ny version av AdGuard Home är tillgänglig\\n\",\n  \"updates_version_equal\": \"AdGuard Home är uppdaterat\",\n  \"upstream\": \"Uppströms server\",\n  \"upstream_dns\": \"Upstream DNS-servrar\",\n  \"upstream_dns_cache_configuration\": \"Konfiguration av uppströms DNS-cache\",\n  \"upstream_dns_client_desc\": \"Om detta fält är tomt kommer AdGuard Home att använda de servrar som konfigurerats i <0>DNS inställningarna</0>.\",\n  \"upstream_dns_configured_in_file\": \"Konfigurerad i {{path}}\",\n  \"upstream_dns_help\": \"Ange en serveradress per rad. <a>Läs mer</a> om att konfigurera uppströms DNS-servrar.\",\n  \"upstream_parallel\": \"Använd parallella förfrågningar för att snabba upp dessa genom att fråga alla uppströmsservrar samtidigt.\",\n  \"upstream_timeout\": \"Uppströms timeout\",\n  \"upstream_timeout_desc\": \"Anger antalet sekunder att vänta på ett svar från uppströmsservern\",\n  \"upstreams\": \"Uppströms\",\n  \"use_adguard_browsing_sec\": \"Använd AdGuards webbservice för surfsäkerhet\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home kommer att kontrollera om en domän är blockerad av webbservicen surfsäkerhet. Med en integritetsvänlig metod görs en API-lookup för att kontrollera: endast ett kort prefix i domännamnet SHA256 hash skickas till servern.\",\n  \"use_adguard_parental\": \"Använda AdGuards webbservice för föräldrakontroll\",\n  \"use_adguard_parental_hint\": \"AdGuard Home kommer att kontrollera domäner för innehåll av vuxenmaterial . Samma integritetsvänliga metod för  API-lookup som tillämpas i webbservicens surfsäkerhet används.\",\n  \"use_private_ptr_resolvers_desc\": \"Lös PTR-, SOA- och NS-förfrågningar för ARPA-domäner som innehåller privata IP-adresser genom privata uppströmsservrar, DHCP, /etc/hosts, etc. Om det är inaktiverat kommer AdGuard Home att svara på alla sådana förfrågningar med NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Använd privata omvända DNS upplösare\",\n  \"use_saved_key\": \"Använd den tidigare sparade nyckeln\",\n  \"username_label\": \"Användarnamn\",\n  \"username_placeholder\": \"Skriv in användarnamn\",\n  \"validated_with_dnssec\": \"Validerad med DNSSEC\",\n  \"version\": \"version\",\n  \"version_request_error\": \"Uppdateringskontroll misslyckades. Kontrollera din internetanslutning.\",\n  \"wednesday\": \"Onsdag\",\n  \"wednesday_short\": \"Ons\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/th.json",
    "content": "{\n  \"access_allowed_desc\": \"รายการ CIDR หรือที่อยู่ IP หากมีการตั้งค่าไว้ AdGuard Home จะยอมรับคำขอจากที่อยู่ IP เหล่านี้เท่านั้น\",\n  \"access_allowed_title\": \"ลูกค้าที่ได้รับอนุญาต\",\n  \"access_blocked_desc\": \"อย่าสับสนกับตัวกรอง AdGuard Home จะลบคำขอ DNS ที่ตรงกับโดเมนเหล่านี้ และคำขอเหล่านี้จะไม่ปรากฏในบันทึกคำขอด้วยซ้ำ คุณสามารถระบุชื่อโดเมน ไวล์ดการ์ด หรือกฎตัวกรอง URL ได้ เช่น \\\"example.org\\\" \\\"*.example.org\\\" หรือ \\\"||example.org^\\\" ตามลำดับ\",\n  \"access_blocked_title\": \"โดเมนที่ถูกปิดกั้น\",\n  \"access_desc\": \"ที่นี่คุณสามารถกำหนดค่ากฎการเข้าถึงสำหรับเซิร์ฟเวอร์ AdGuard Home DNS\",\n  \"access_disallowed_desc\": \"รายการ CIDR หรือที่อยู่ IP ถ้าหากมีการตั้งค่าไว้ AdGuard Home จะยกเลิกคำขอจากที่อยู่ IP เหล่านี้ หากมีรายการใน Allowed clients จะไม่มีการพิจารณาฟิลด์นี้.\",\n  \"access_disallowed_title\": \"ลูกค้าไม่ได้รับอนุญาต\",\n  \"access_settings_saved\": \"บันทึกการตั้งค่าการเข้าถึงเรียบร้อยแล้ว\",\n  \"access_title\": \"เข้าถึงการตั้งค่า\",\n  \"actions_table_header\": \"การกระทำ\",\n  \"add_allowlist\": \"ไม่มีรายการอนุญาต\",\n  \"add_blocklist\": \"เพิ่มรายการบล็อก\",\n  \"add_custom_list\": \"เพิ่มรายการที่กำหนดเอง\",\n  \"add_persistent_client\": \"เพิ่มเป็นไคลเอนต์ถาวร\",\n  \"address\": \"ที่อยู่\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home จะลบคำขอ DNS ทั้งหมดจากไคลเอนต์นี้\",\n  \"all_lists_up_to_date_toast\": \"รายการทั้งหมดเป็นข้อมูลล่าสุดอยู่แล้ว\",\n  \"all_queries\": \"ทุกการสอบถาม\",\n  \"allow_this_client\": \"อนุญาตไคลเอ็นต์นี้\",\n  \"allowed\": \"รายการที่อนุญาต\",\n  \"anonymize_client_ip\": \"ปิดบังเลข ip ของเครื่องลูกข่าย\",\n  \"anonymize_client_ip_desc\": \"อย่าบันทึกที่อยู่ IP แบบเต็มของไคลเอนต์ในบันทึกและสถิติ\",\n  \"anonymizer_notification\": \"<0>หมายเหตุ:</0> เปิดใช้งานการไม่ระบุตัวตนของ IP แล้ว คุณสามารถปิดใช้งานได้ใน <1>การตั้งค่าทั่วไป</1> -\",\n  \"answer\": \"คำตอบ\",\n  \"apply_btn\": \"นำไปใช้\",\n  \"auto_clients_desc\": \"ข้อมูลเกี่ยวกับที่อยู่ IP ของอุปกรณ์ที่ใช้หรืออาจใช้ AdGuard Home ข้อมูลนี้ถูกรวบรวมจากหลายแหล่งรวมถึงไฟล์โฮสต์, DNS ย้อนกลับ ฯลฯ\",\n  \"auto_clients_title\": \"เครื่อง (runtime)\",\n  \"autofix_warning_list\": \"มันจะทำงานเหล่านี้: <0>ปิดการใช้งานระบบ DNSStubListener</0> <0>ตั้งที่อยู่เซิร์ฟเวอร์ DNS เป็น 127.0.0.1</0> <0>แทนที่เป้าหมายลิงก์สัญลักษณ์ของ /etc/resolv.conf เป็น /run/systemd/resolve/resolv.conf</0> <0>หยุด DNSStubListener (โหลดบริการแก้ไขระบบซ้ำ)</0>\",\n  \"autofix_warning_result\": \"ดังนั้น AdGuardHome จะประมวลผลคำขอ DNS ทั้งหมดจากระบบของคุณตามค่าเริ่มต้น\",\n  \"autofix_warning_text\": \"หากคุณคลิก \\\"แก้ไข\\\" AdGuardHome จะกำหนดค่าระบบของคุณเพื่อใช้เซิร์ฟเวอร์ AdGuardHome\",\n  \"average_processing_time\": \"เวลาประมวลผลโดยเฉลี่ย\",\n  \"average_processing_time_hint\": \"เวลาเฉลี่ยเป็นมิลลิวินาทีในการประมวลผลคำขอ DNS\",\n  \"average_upstream_response_time\": \"เวลาเฉลี่ยในการตอบสนองของพอร์ต\",\n  \"back\": \"กลับ\",\n  \"block\": \"ปิดกั้น\",\n  \"block_all\": \"ปิดกั้นทั้งหมด\",\n  \"block_domain_use_filters_and_hosts\": \"ปิดกั้นโดเมนโดยใช้ตัวกรองและไฟล์โฮสต์\",\n  \"block_for_this_client_only\": \"ปิดกั้นสำหรับไคลเอ็นต์นี้เท่านั้น\",\n  \"block_services\": \"ปิดกั้นบริการเฉพาะ\",\n  \"blocked_adult_websites\": \"ถูกปิดกั้นโดยการควบคุมของผู้ปกครอง\",\n  \"blocked_by\": \"<0>ถูกปิดกั้นโดยตัวกรอง</0>\",\n  \"blocked_by_cname_or_ip\": \"บล็อกโดย CNAME หรือ IP\",\n  \"blocked_by_response\": \"ปิดกั้นโดย CNAME หรือ IP ในการตอบกลับ\",\n  \"blocked_response_ttl\": \"การตอบกลับที่ถูกปิดกั้น TTL\",\n  \"blocked_response_ttl_desc\": \"ระบุว่าไคลเอนต์ควรแคชการตอบสนองที่ผ่านการกรองเป็นเวลากี่วินาที\",\n  \"blocked_safebrowsing\": \"ถูกบล็อกโดยการค้นหาที่ปลอดภัย\",\n  \"blocked_service\": \"ปิดกั้นบริการ\",\n  \"blocked_services\": \"ปิดกั้นบริการ\",\n  \"blocked_services_desc\": \"อนุญาตให้บล็อกเว็บไซต์และบริการยอดนิยมได้อย่างรวดเร็ว\",\n  \"blocked_services_global\": \"ใช้บริการที่ถูกบล็อกทั่วโลก\",\n  \"blocked_services_saved\": \"บันทึกบริการที่ถูกปิดกั้นเรียบร้อยแล้ว\",\n  \"blocked_threats\": \"ภัยคุกคามที่ถูกบล็อก\",\n  \"blocking_ipv4\": \"ปิดกั้น IPv4\",\n  \"blocking_ipv4_desc\": \"ที่อยู่ IP ที่จะส่งคืนสำหรับคำขอที่ถูกปิดกั้น\",\n  \"blocking_ipv6\": \"ปิดกั้น IPv6\",\n  \"blocking_ipv6_desc\": \"ที่อยู่ IP ที่จะส่งคืนสำหรับคำขอ AAAA ที่ถูกปิดกั้น\",\n  \"blocking_mode\": \"โหมดการปิดกั้น\",\n  \"blocking_mode_custom_ip\": \"IP ที่กำหนดเอง: ตอบกลับด้วยที่อยู่ IP ที่ตั้งค่าด้วยตนเอง\",\n  \"blocking_mode_default\": \"ปฏิเสธ: ตอบสนองด้วย REFUSED เมื่อถูกปิดกั้นโดยกฎสไตล์ปิดกั้นโฆษณา; ตอบกลับด้วยที่อยู่ IP ที่ระบุในกฎเมื่อถูกปิดกั้นโดยกฎ /etc/hosts-style\",\n  \"blocking_mode_null_ip\": \"Null IP: ตอบกลับด้วยที่อยู่เลขศูนย์ IP (0.0.0.0 สำหรับ A; :: สำหรับ AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: ตอบสนองด้วยรหัส NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: ตอบกลับด้วยรหัส REFUSED\",\n  \"blocklist\": \"บัญชีดำของระบบ\",\n  \"bootstrap_dns\": \"Bootstrap เซิร์ฟเวอร์ DNS\",\n  \"bootstrap_dns_desc\": \"เซิร์ฟเวอร์ Bootstrap DNS ใช้เพื่อแก้ไขที่อยู่ IP ของตัวแก้ไข DoH / DoT ที่คุณระบุว่าเป็น upstreams\",\n  \"cache_cleared\": \"ล้างแคช DNS สำเร็จแล้ว\",\n  \"cache_enabled\": \"เปิดใช้งานแคช\",\n  \"cache_enabled_desc\": \"จัดเก็บคำตอบ DNS ไว้ในเครื่องโดยตรง\",\n  \"cache_optimistic\": \"การเก็บข้อมูลแบบมีความหวัง\",\n  \"cache_optimistic_desc\": \"ทำให้ AdGuard Home ตอบกลับจากแคช แม้เมื่อข้อมูลหมดอายุแล้ว และยังลองรีเฟรชข้อมูลด้วย.\",\n  \"cache_size\": \"ขนาดแคช\",\n  \"cache_size_desc\": \"ขนาดแคช DNS (เป็นไบต์)\",\n  \"cache_size_validation\": \"ขนาดแคชจะต้องมากกว่าศูนย์เมื่อเปิดใช้งาน\",\n  \"cache_ttl_max_override\": \"ไม่มีการกำหนดค่า TTL สูงสุด\",\n  \"cache_ttl_max_override_desc\": \"ตั้งค่าค่าระยะเวลาการมีชีวิตสูงสุด (วินาที) สำหรับคำแนะนำในแคช DNS\",\n  \"cache_ttl_min_override\": \"ไม่มีการกำหนดค่า TTL ขั้นต่ำ\",\n  \"cache_ttl_min_override_desc\": \"ขยายค่าเวลาที่สั้น (วินาที) ที่ได้รับจากเซิร์ฟเวอร์ upstream เมื่อแคชตอบสนอง DNS\",\n  \"cancel_btn\": \"ยกเลิก\",\n  \"category_label\": \"ประเภท\",\n  \"check\": \"ตรวจสอบ\",\n  \"check_client_id\": \"ตัวระบุไคลเอนต์ (ClientID หรือ ที่อยู่ IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"ตรวจสอบว่าชื่อโฮสต์ถูกกรอง\",\n  \"check_dhcp_servers\": \"ตรวจสอบ DHCP servers\",\n  \"check_dns_record\": \"เลือกประเภทระเบียน DNS\",\n  \"check_enter_client_id\": \"ป้อนตัวระบุคล้าย\",\n  \"check_hostname\": \"ชื่อโฮสต์หรือชื่อโดเมน\",\n  \"check_ip\": \"IP addresses: {{ip}}\",\n  \"check_not_found\": \"ไม่พบในรายการตัวกรองของคุณ\",\n  \"check_reason\": \"เหตุผล: {{reason}}\",\n  \"check_service\": \"ชื่อบริการ: {{service}}\",\n  \"check_title\": \"ตรวจสอบการกรอง\",\n  \"check_updates_btn\": \"ตรวจสอบการปรับปรุง\",\n  \"check_updates_now\": \"ตรวจสอบการปรับปรุง\",\n  \"choose_allowlist\": \"เลือกรายการอนุญาต\",\n  \"choose_blocklist\": \"Choose blocklists\",\n  \"choose_from_list\": \"เลือกจากรายการ\",\n  \"city\": \"เมือง\",\n  \"clear_cache\": \"ล้างแคช\",\n  \"click_to_view_queries\": \"คลิกเพื่อดูคำถาม\",\n  \"client_add\": \"เพิ่มเครื่องลูกข่าย\",\n  \"client_added\": \"เครื่อง \\\"{{key}}\\\" เพิ่มเรียบร้อยแล้ว\",\n  \"client_blocked\": \"บล็อกเครื่อง \\\"{{ip}}\\\" สำเร็จแล้ว\",\n  \"client_confirm_block\": \"คุณแน่ใจนะว่าจะบล็อกเครื่อง \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"คุณแน่ใจนะว่าจะลบเครื่อง \\\"{{key}}\\\"?\",\n  \"client_confirm_unblock\": \"คุณแน่ใจนะว่าจะยกเลิกบล็อกเครื่อง \\\"{{ip}}\\\"?\",\n  \"client_deleted\": \"เครื่อง \\\"{{key}}\\\" ลบเรียบร้อยแล้ว\",\n  \"client_details\": \"รายละเอียดลูกข่าย\",\n  \"client_edit\": \"แก้ไขเครื่องลูกข่าย\",\n  \"client_global_settings\": \"ใช้การตั้งค่าทั่วโลก\",\n  \"client_id\": \"รหัสลูกค้า\",\n  \"client_id_desc\": \"ลูกค้าที่แตกต่างกันสามารถระบุได้ด้วยรหัสลูกค้าพิเศษ <a>ที่นี่</a> คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับวิธีระบุลูกค้า\",\n  \"client_id_placeholder\": \"ป้อนรหัสลูกค้า\",\n  \"client_identifier\": \"ตรวจสอบโดย\",\n  \"client_identifier_desc\": \"ลูกค้าสามารถระบุได้โดยที่อยู่ IP, CIDR, ที่อยู่ MAC โปรดทราบว่าการใช้ MAC เป็นตัวระบุเป็นไปได้ก็ต่อเมื่อ AdGuard Home เป็น <0>เซิร์ฟเวอร์ DHCP</0> ด้วย\",\n  \"client_name\": \"ลูกค้า {{id}}\",\n  \"client_new\": \"สร้างเครื่องลูกข่าย\",\n  \"client_settings\": \"การตั้งค่าไคลเอนต์\",\n  \"client_table_header\": \"เครื่องลูกข่าย\",\n  \"client_unblocked\": \"ยกเลิกบล็อกเครื่อง \\\"{{ip}}\\\" สำเร็จแล้ว\",\n  \"client_updated\": \"อัปเดตเครื่อง \\\"{{key}}\\\" สำเร็จแล้ว\",\n  \"clients_desc\": \"ตั้งค่าอุปกรณ์ที่เชื่อมต่อกับ AdGuard Home\",\n  \"clients_not_found\": \"ไม่มีเครื่องลูกข่าย\",\n  \"clients_title\": \"เครื่องลูกข่าย\",\n  \"compact\": \"กะทัดรัด\",\n  \"config_successfully_saved\": \"บันทึกการตั้งค่าเรีบยร้อยแล้ว\",\n  \"configure\": \"กำหนดค่า\",\n  \"confirm_dns_cache_clear\": \"คุณแน่ใจหรือไม่ว่าต้องการล้างแคช DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home จะกำหนด {{ip}} เป็นที่อยู่ IP คงที่ของคุณ คุณต้องการดำเนินการต่อหรือไม่?\",\n  \"copyright\": \"ลิขสิทธิ์\",\n  \"country\": \"ประเทศ\",\n  \"custom_filter_rules\": \"กฎการกรองที่กำหนดเอง\",\n  \"custom_filter_rules_hint\": \"ป้อนหนึ่งกฎในหนึ่งบรรทัด คุณสามารถใช้กฎปิดกั้นโฆษณาหรือโฮสต์ไฟล์ไวยากรณ์\",\n  \"custom_filtering_rules\": \"รายการของกฎการกรอง\",\n  \"custom_ip\": \"IP กำหนดเอง\",\n  \"custom_retention_input\": \"ป้อนการเก็บรักษาเป็นชั่วโมง\",\n  \"custom_rotation_input\": \"ป้อนการหมุนรอบเป็นชั่วโมง\",\n  \"dashboard\": \"แผงควบคุม\",\n  \"date\": \"วันที่\",\n  \"default\": \"ค่าเริ่มต้น\",\n  \"delete_confirm\": \"คุณแน่ใจหรือว่าต้องการลบ \\\"{{key}}\\\"?\",\n  \"delete_table_action\": \"ลบ\",\n  \"descr\": \"คำอธิบาย\",\n  \"details\": \"รายละเอียด\",\n  \"dhcp_add_static_lease\": \"เพิ่มสัญญาเช่าคงที่\",\n  \"dhcp_config_saved\": \"บันทึกการกำหนดค่า DHCP สำเร็จแล้ว\",\n  \"dhcp_description\": \"ถ้าหากเราเตอร์ของคุณไม่รองรับการตั้งค่า DHCP คุณสามารถใช้ ADGuard's ทำ DHCP server ได้\",\n  \"dhcp_disable\": \"ปิด DHCP server\",\n  \"dhcp_dynamic_ip_found\": \"ระบบของคุณใช้การกำหนดค่าที่อยู่ IP แบบไดนามิกสำหรับอินเทอร์เฟซ <0>{{interfaceName}}</0> ในการใช้เซิร์ฟเวอร์ DHCP จะต้องตั้งค่าที่อยู่ IP แบบคงที่ ที่อยู่ IP ปัจจุบันของคุณคือ <0>{{ipAddress}}</0> เราจะตั้งค่าที่อยู่ IP นี้เป็นแบบคงที่โดยอัตโนมัติหากคุณกดปุ่มเปิดใช้งาน DHCP\",\n  \"dhcp_edit_static_lease\": \"แก้ไขสัญญาเช่าคงที่\",\n  \"dhcp_enable\": \"เปิด DHCP server\",\n  \"dhcp_error\": \"เราไม่สามารถระบุได้ว่ามีเซิร์ฟเวอร์ DHCP อื่นในเครือข่ายหรือไม่\",\n  \"dhcp_form_gateway_input\": \"IP ของเกตเวย์\",\n  \"dhcp_form_lease_input\": \"ระยะเวลาการเช่า\",\n  \"dhcp_form_lease_title\": \"เวลาเช่า DHCP (เป็นวินาที)\",\n  \"dhcp_form_range_end\": \"ช่วงสิ้นสุด\",\n  \"dhcp_form_range_start\": \"ช่วงเริ่มต้น\",\n  \"dhcp_form_range_title\": \"ช่วงของที่อยู่ IP\",\n  \"dhcp_form_subnet_input\": \"ซับเน็ตมาสก์\",\n  \"dhcp_found\": \"พบเซิร์ฟเวอร์ DHCP ที่ใช้งานอยู่ในเครือข่าย ไม่ปลอดภัยที่จะเปิดใช้งานเซิร์ฟเวอร์ DHCP ในตัว\",\n  \"dhcp_hardware_address\": \"ที่อยู่ฮาร์ดแวร์\",\n  \"dhcp_interface_select\": \"เลือกอินเตอร์เฟส DHCP\",\n  \"dhcp_ip_addresses\": \"ที่อยู่ IP\",\n  \"dhcp_ipv4_settings\": \"การตั้งค่า DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"การตั้งค่า DHCP IPv6\",\n  \"dhcp_lease_added\": \"เพิ่มสัญญาเช่าคงที่ \\\"{{key}}\\\" สำเร็จแล้ว\",\n  \"dhcp_lease_deleted\": \"ลบสัญญาเช่าคงที่ \\\"{{key}}\\\" สำเร็จแล้ว\",\n  \"dhcp_lease_updated\": \"อัปเดตสัญญาเช่าคงที่ \\\"{{key}}\\\" สำเร็จแล้ว\",\n  \"dhcp_leases\": \"สัญญาเช่า DHCP\",\n  \"dhcp_leases_not_found\": \"ไม่พบสัญญาเช่า DHCP\",\n  \"dhcp_new_static_lease\": \"เช่าใหม่คงที่\",\n  \"dhcp_not_found\": \"มีความปลอดภัยในการเปิดใช้งานเซิร์ฟเวอร์ DHCP ในตัว - เราไม่พบเซิร์ฟเวอร์ DHCP ที่ใช้งานอยู่ในเครือข่าย อย่างไรก็ตามเราขอแนะนำให้คุณตรวจสอบด้วยตนเองอีกครั้งเนื่องจากการทดสอบอัตโนมัติของเราไม่ได้รับประกัน 100%\",\n  \"dhcp_reset\": \"คุณแน่ใจหรือว่าต้องการรีเซ็ตการกำหนดค่า DHCP?\",\n  \"dhcp_reset_leases\": \"รีเซ็ตสัญญาเช่าทั้งหมด\",\n  \"dhcp_reset_leases_confirm\": \"คุณแน่ใจหรือว่าต้องการรีเซ็ตสัญญาเช่าทั้งหมด?\",\n  \"dhcp_reset_leases_success\": \"รีเซ็ตสัญญาเช่า DHCP สำเร็จแล้ว\",\n  \"dhcp_settings\": \"การตั้งค่า DHCP\",\n  \"dhcp_static_ip_error\": \"ในการใช้เซิร์ฟเวอร์ DHCP จะต้องตั้งค่าที่อยู่ IP แบบคงที่ เราไม่สามารถระบุได้ว่ามีการกำหนดค่าอินเทอร์เฟซเครือข่ายนี้โดยใช้ที่อยู่ IP แบบคงที่หรือไม่ โปรดตั้งค่าที่อยู่ IP แบบคงที่ด้วยตนเอง\",\n  \"dhcp_static_leases\": \"DHCP แบบกำหนด\",\n  \"dhcp_static_leases_not_found\": \"ไม่พบสัญญาเช่า DHCP แบบคงที่\",\n  \"dhcp_table_expires\": \"วันที่หมดอายุ\",\n  \"dhcp_table_hostname\": \"ชื่อโฮสต์\",\n  \"dhcp_title\": \"DHCP server (ยังไม่สมบูรณ์)\",\n  \"dhcp_warning\": \"หากคุณต้องการเปิดใช้งานเซิร์ฟเวอร์ DHCP ตรวจสอบให้แน่ใจว่าไม่มีเซิร์ฟเวอร์ DHCP ที่ใช้งานอยู่ในเครือข่ายของคุณ มิฉะนั้นจะทำให้อินเทอร์เน็ตสำหรับอุปกรณ์ที่เชื่อมต่อมีปัญหาได้!\",\n  \"disable_for_hours\": \"เป็นเวลา {{count}} ชั่วโมง\",\n  \"disable_for_hours_plural\": \"เป็นเวลา {{count}} ชั่วโมง\",\n  \"disable_for_minutes\": \"เป็นเวลา {{count}} นาที\",\n  \"disable_for_minutes_plural\": \"เป็นเวลา {{count}} นาที\",\n  \"disable_for_seconds\": \"เป็นเวลา {{count}} วินาที\",\n  \"disable_for_seconds_plural\": \"เป็นเวลา {{count}} วินาที\",\n  \"disable_ipv6\": \"ปิดใช้งาน IPv6\",\n  \"disable_ipv6_desc\": \"หากเปิดใช้งานคุณสมบัตินี้การสืบค้น DNS ทั้งหมดสำหรับที่อยู่ IPv6 (ประเภท AAAA) จะถูกทิ้ง\",\n  \"disable_notify_for_hours\": \"ปิดใช้งานการป้องกันเป็นเวลา {{count}} ชั่วโมง\",\n  \"disable_notify_for_hours_plural\": \"ปิดใช้งานการป้องกันเป็นเวลา {{count}} ชั่วโมง\",\n  \"disable_notify_for_minutes\": \"ปิดใช้งานการป้องกันเป็นเวลา {{count}} นาที\",\n  \"disable_notify_for_minutes_plural\": \"ปิดใช้งานการป้องกันเป็นเวลา {{count}} นาที\",\n  \"disable_notify_for_seconds\": \"ปิดใช้งานการป้องกันเป็นเวลา {{count}} วินาที\",\n  \"disable_notify_for_seconds_plural\": \"ปิดใช้งานการป้องกันเป็นเวลา {{count}} วินาที\",\n  \"disable_notify_until_tomorrow\": \"ปิดการป้องกันจนถึงพรุ่งนี้\",\n  \"disable_protection\": \"ปิดใช้งานการป้องกัน\",\n  \"disable_rewrites\": \"ปิดใช้งานกฎการเขียนใหม่\",\n  \"disable_until_tomorrow\": \"จนกว่าจะถึงวันพรุ่งนี้\",\n  \"disabled\": \"ปิดใช้งาน\",\n  \"disabled_dhcp\": \"ปิดการใช้งาน DHCP server แล้ว\",\n  \"disabled_filtering_toast\": \"ปิดใช้งานการกรอง\",\n  \"disabled_parental_toast\": \"ปิดใช้งานการควบคุมโดยผู้ปกครอง\",\n  \"disabled_protection\": \"ปิดใช้งานการป้องกันแล้ว\",\n  \"disabled_safe_browsing_toast\": \"ปิดใช้งานการเรียกดูอย่างปลอดภัย\",\n  \"disabled_safe_search_toast\": \"ปิดใช้งานการค้นหาที่ปลอดภัย\",\n  \"disallow_this_client\": \"ไม่อนุญาตไคลเอ็นต์นี้\",\n  \"dns_addresses\": \"ที่อยู่ DNS\",\n  \"dns_allowlists\": \"รายการอนุญาต DNS\",\n  \"dns_allowlists_desc\": \"โดเมนจาก DNS allowlists จะได้รับอนุญาตแม้ว่าจะอยู่ในรายการบล็อคก็ตาม\",\n  \"dns_blocklists\": \"รายการบล็อค DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home จะบล็อกโดเมนที่ตรงกับรายการบล็อค\",\n  \"dns_cache_config\": \"การกำหนดค่าแคช DNS\",\n  \"dns_cache_config_desc\": \"คุณสามารถกำหนดค่า DNS cache ได้ที่นี่\",\n  \"dns_cache_size\": \"ขนาดแคช DNS, เป็นไบต์\",\n  \"dns_config\": \"การกำหนดค่าเซิร์ฟเวอร์ DNS\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"ความเป็นส่วนตัวของ DNS\",\n  \"dns_providers\": \"นี่คือรายการ <0>ของผู้ให้บริการ DNS ที่เป็นที่รู้จัก</0> ให้เลือก\",\n  \"dns_query\": \"การค้นหา DNS\",\n  \"dns_rewrites\": \"การเขียน DNS ใหม่\",\n  \"dns_settings\": \"การตั้งค่า DNS\",\n  \"dns_start\": \"เซิร์ฟเวอร์ DNS เริ่มทำงาน\",\n  \"dns_status_error\": \"เกิดข้อผิดพลาดในการตรวจสอบสถานะเซิร์ฟเวอร์ DNS\",\n  \"dns_test_not_ok_toast\": \"เซิร์ฟเวอร์ \\\"{{key}}\\\": ไม่สามารถใช้งานได้ โปรดตรวจสอบว่าคุณเขียนถูกต้อง\",\n  \"dns_test_ok_toast\": \"เซิร์ฟเวอร์ DNS ที่ระบุทำงานอย่างถูกต้อง\",\n  \"dns_test_parsing_error_toast\": \"ส่วน {{section}}: บรรทัด {{line}}: ไม่สามารถใช้งานได้ กรุณาตรวจสอบว่าคุณเขียนถูกต้องแล้ว\",\n  \"dns_test_warning_toast\": \"อัปสตรีม \\\"{{key}}\\\" ไม่ตอบสนองต่อคำขอทดสอบและอาจไม่ทำงานอย่างถูกต้อง\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"เปิดใช้ DNSSEC\",\n  \"dnssec_enable_desc\": \"ตั้งค่าสถานะ DNSSEC ในการสืบค้น DNS ขาออกและตรวจสอบผลลัพธ์ (ต้องใช้ตัวแก้ไขที่เปิดใช้ DNSSEC)\",\n  \"domain\": \"โดเมน\",\n  \"domain_desc\": \"ป้อนชื่อโดเมนหรือไวด์การ์ดที่คุณต้องการเขียนใหม่\",\n  \"domain_name_table_header\": \"ชื่อโดเมน\",\n  \"domain_or_client\": \"โดเมนหรือไคลเอนต์\",\n  \"down\": \"ดับ\",\n  \"download_mobileconfig\": \"ดาวน์โหลดไฟล์กำหนดค่า\",\n  \"download_mobileconfig_doh\": \"ดาวน์โหลด .mobileconfig สำหรับ DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"ดาวน์โหลด .mobileconfig สำหรับ DNS-over-TLS\",\n  \"ecs\": \"อีซีเอส\",\n  \"edit_allowlist\": \"แก้ไขรายการอนุญาต\",\n  \"edit_blocklist\": \"แก้ไขรายการบล็อก\",\n  \"edit_table_action\": \"แก้ไข\",\n  \"edns_cs_desc\": \"เพิ่มตัวเลือก EDNS Client Subnet (ECS) ลงในคำร้องขอไปยัง upstream และบันทึกค่าที่ส่งโดยไคลเอนต์ในบันทึกคำขอ.\",\n  \"edns_enable\": \"เปิดใช้งานซับเน็ตไคลเอ็นต์ EDNS\",\n  \"edns_use_custom_ip\": \"ใช้ IP ที่กำหนดเองสำหรับ EDNS\",\n  \"edns_use_custom_ip_desc\": \"อนุญาตให้ใช้ IP ที่กำหนดเองสำหรับ EDNS\",\n  \"elapsed\": \"ระยะเวลา\",\n  \"empty_response_status\": \"ว่างเปล่า\",\n  \"enable_protection\": \"เปิดใช้งานการป้องกัน\",\n  \"enable_protection_timer\": \"การป้องกันจะเปิดใช้งานใน {{time}}\",\n  \"enable_rewrites\": \"เปิดใช้งานกฎการเขียนใหม่\",\n  \"enable_upstream_dns_cache\": \"เปิดใช้งานการแคช DNS สำหรับการกำหนดค่าสำหรับลูกค้ารายนี้\",\n  \"enabled_dhcp\": \"เปิดการใช้งาน DHCP server แล้ว\",\n  \"enabled_filtering_toast\": \"เปิดใช้งานการกรอง\",\n  \"enabled_parental_toast\": \"เปิดการใช้งานควบคุมโดยผู้ปกครอง\",\n  \"enabled_protection\": \"เปิดใช้งานการป้องกันแล้ว\",\n  \"enabled_safe_browsing_toast\": \"เปิดใช้งานการเรียกดูอย่างปลอดภัย\",\n  \"enabled_save_search_toast\": \"เปิดใช้งานการค้นหาที่ปลอดภัย\",\n  \"enabled_table_header\": \"เปิดใช้งาน\",\n  \"encryption_certificate_path\": \"เส้นทางใบรับรอง\",\n  \"encryption_certificates\": \"ใบรับรอง\",\n  \"encryption_certificates_desc\": \"ในการใช้การเข้ารหัสคุณต้องระบุเชนใบรับรอง SSL ที่ถูกต้องสำหรับโดเมนของคุณ คุณสามารถรับใบรับรองฟรีได้ที่ <0>{{link}}</0> หรือคุณสามารถซื้อได้จากหนึ่งในผู้ออกใบรับรองที่เชื่อถือได้\",\n  \"encryption_certificates_input\": \"คัดลอก/วางใบรับรองที่เข้ารหัส PEM ของคุณที่นี่\",\n  \"encryption_certificates_source_content\": \"วางเนื้อหา certificates \",\n  \"encryption_certificates_source_path\": \"ตั้งค่าเส้นทาง certificates \",\n  \"encryption_chain_invalid\": \"ใบรับรองไม่มีความน่าเชื่อถือ\",\n  \"encryption_chain_valid\": \"ใบรับรองมีความน่าเชื่อถือ\",\n  \"encryption_config_saved\": \"บันทึกการตั้งค่าเข้ารหัสเรียบร้อยแล้ว\",\n  \"encryption_desc\": \"การเข้ารหัส (HTTPS/QUIC/TLS) รองรับทั้ง DNS และหน้าเว็บแอดมิน\",\n  \"encryption_doq\": \"พอร์ต DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"หากพอร์ตนี้ถูกกำหนดค่า AdGuard Home จะทำงานเซิร์ฟเวอร์ DNS-over-QUIC ที่พอร์ตนี้ และอาจจะยังไม่เชื่อถือได้ นอกจากนี้ยังมีผู้ใช้ที่ไม่มากนักที่รองรับในขณะนี้\",\n  \"encryption_dot\": \"พอร์ต DNS-over-TLS\",\n  \"encryption_dot_desc\": \"หากมีการกำหนดค่าพอร์ตนี้ AdGuard Home จะเรียกใช้เซิร์ฟเวอร์ DNS-over-TLS ในพอร์ตนี้\",\n  \"encryption_enable\": \"เปิดการเข้ารหัส (HTTPS, DNS-over-HTTPS, และ DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"หากเปิดใช้งานการเข้ารหัสอินเทอร์เฟซผู้ดูแลระบบของ AdGuard Home จะทำงานผ่าน HTTPS และเซิร์ฟเวอร์ DNS จะรับฟังคำร้องขอผ่านทาง DNS-over-HTTPS และ DNS-over-TLS\",\n  \"encryption_expire\": \"หมดอายุ\",\n  \"encryption_hostnames\": \"ชื่อโฮส\",\n  \"encryption_https\": \"พอร์ท HTTPS\",\n  \"encryption_https_desc\": \"หากมีการกำหนดค่าพอร์ต HTTPS ส่วนติดต่อผู้ดูแลระบบของ AdGuard Home จะสามารถเข้าถึงได้ผ่าน HTTPS และจะให้ DNS-over-HTTPS ในตำแหน่ง '/dns-query'\",\n  \"encryption_issuer\": \"ผู้ออกใบรับรอง:\",\n  \"encryption_key\": \"รหัสส่วนตัว (Private key)\",\n  \"encryption_key_input\": \"คัดลอก/วาง PEM-encoded private key ของคุณตรงนี้\",\n  \"encryption_key_invalid\": \"นี่เป็นคีย์ส่วนตัว {{type}} ที่ไม่ถูกต้อง\",\n  \"encryption_key_source_content\": \"วางเนื้อหาคีย์ส่วนตัว\",\n  \"encryption_key_source_path\": \"ตั้งค่าเส้นทางไฟล์กุญแจส่วนตัว\",\n  \"encryption_key_valid\": \"นี่เป็นคีย์ส่วนตัว {{type}} ที่ถูกต้อง\",\n  \"encryption_plain_dns_desc\": \"DNS แบบธรรมดาจะเปิดใช้งานตามค่าเริ่มต้น คุณสามารถปิดใช้งานเพื่อบังคับให้อุปกรณ์ทั้งหมดใช้ DNS ที่เข้ารหัสได้ หากต้องการทำเช่นนี้ คุณต้องเปิดใช้งานโปรโตคอล DNS ที่เข้ารหัสอย่างน้อยหนึ่งโปรโตคอล\",\n  \"encryption_plain_dns_enable\": \"เปิดใช้งาน DNS ธรรมดา\",\n  \"encryption_plain_dns_error\": \"หากต้องการปิดใช้งาน DNS ธรรมดา ให้เปิดใช้งานโปรโตคอล DNS ที่เข้ารหัสอย่างน้อยหนึ่งรายการ\",\n  \"encryption_private_key_path\": \"เส้นทางกุญแจส่วนตัว\",\n  \"encryption_redirect\": \"ไปเส้นทาง HTTPS อัตโนมัติ\",\n  \"encryption_redirect_desc\": \"หากเลือกตัวเลือกนี้ AdGuard Home จะเปลี่ยนเส้นทางคุณจากที่อยู่ HTTP ไปยัง HTTPS โดยอัตโนมัติ\",\n  \"encryption_reset\": \"คุณแน่ใจนะว่าจะล้างค่าการเข้ารหัส?\",\n  \"encryption_server\": \"ชื่อเซิร์ฟเวอร์\",\n  \"encryption_server_desc\": \"ถ้าตั้งค่าแล้ว AdGuard Home จะตรวจจับ ClientIDs ตอบสนองต่อการค้นหา DDR และทำการตรวจสอบการเชื่อมต่อเพิ่มเติม ถ้าไม่ได้ตั้งค่า ฟีเจอร์เหล่านี้จะถูกปิดใช้งาน ต้องตรงกับหนึ่งใน DNS Names ในใบรับรอง\",\n  \"encryption_server_enter\": \"ป้อนชื่อโดเมน\",\n  \"encryption_settings\": \"การตั้งค่าการเข้ารหัส\",\n  \"encryption_status\": \"สถานะ\",\n  \"encryption_subject\": \"เรื่อง:\",\n  \"encryption_title\": \"การเข้ารหัส\",\n  \"encryption_warning\": \"คำเตือน\",\n  \"enforce_safe_search\": \"บังคับใช้การค้นหาที่ปลอดภัย\",\n  \"enforce_save_search_hint\": \"AdGuard Home จะบังคับใช้การค้นหาที่ปลอดภัยในเครื่องมือค้นหาต่อไปนี้: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay\",\n  \"enforced_save_search\": \"บังคับใช้การค้นหาที่ปลอดภัย\",\n  \"enter_cache_size\": \"ป้อนขนาดแคช (ไบต์)\",\n  \"enter_cache_ttl_max_override\": \"ป้อน TTL สูงสุด (วินาที)\",\n  \"enter_cache_ttl_min_override\": \"ป้อน TTL ขั้นต่ำ (วินาที)\",\n  \"enter_name_hint\": \"ป้อนชื่อ\",\n  \"enter_url_or_path_hint\": \"ป้อน URL หรือเส้นทางสัมบูรณ์ของรายการ\",\n  \"enter_valid_allowlist\": \"ป้อน URL ที่ถูกต้องไปยังรายการที่อนุญาต\",\n  \"enter_valid_blocklist\": \"ป้อน URL ที่ถูกต้องไปยังรายการปิดกั้น\",\n  \"error_details\": \"รายละเอียดข้อผิดพลาด\",\n  \"example_comment\": \"! นี่คือความคิดเห็น\",\n  \"example_comment_hash\": \"# นอกจากนี้ยังมีความคิดเห็น\",\n  \"example_comment_meaning\": \"เพียงความคิดเห็น\",\n  \"example_meaning_filter_block\": \"ปิดกั้นการเข้าถึงโดเมน example.org และโดเมนย่อยทั้งหมด\",\n  \"example_meaning_filter_whitelist\": \"เลิกปิดกั้นการเข้าถึงโดเมน example.org และโดเมนย่อยทั้งหมด\",\n  \"example_meaning_host_block\": \"ตอนนี้ AdGuard Home จะส่งคืนที่อยู่ 127.0.0.1 สำหรับโดเมน example.org (แต่ไม่ใช่โดเมนย่อย)\",\n  \"example_multiple_upstreams_reserved\": \"หลาย upstream <0>สำหรับโดเมนเฉพาะ</0>;\",\n  \"example_regex_meaning\": \"ปิดกั้นการเข้าถึงโดเมนที่ตรงกับนิพจน์ทั่วไปที่ระบุ\",\n  \"example_rewrite_domain\": \"เขียนคำตอบซ้ำสำหรับชื่อโดเมนนี้เท่านั้น\",\n  \"example_rewrite_wildcard\": \"เขียนคำตอบใหม่ทั้งหมดสำหรับ <0>example.org</0> โดเมนย่อย\",\n  \"example_upstream_comment\": \"คุณสามารถระบุความคิดเห็นได้\",\n  \"example_upstream_doh\": \"เข้ารหัส <0>DNS-over-HTTPS</0> แล้ว\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS ถูกเข้ารหัสพร้อมแรงบีบ <0>HTTP/3</0> และไม่มีการย้อนกลับไปยัง HTTP/2 หรือต่ำกว่า;\",\n  \"example_upstream_doq\": \"เข้ารหัส <0> DNS-over-QUIC </0>\",\n  \"example_upstream_dot\": \"encrypted <0>DNS-over-TLS</0> แล้ว\",\n  \"example_upstream_regular\": \"DNS ปกติ (มากกว่า UDP)\",\n  \"example_upstream_regular_port\": \"DNS ปกติ (ผ่าน UDP, พร้อมพอร์ต);\",\n  \"example_upstream_reserved\": \"คุณสามารถระบุ DNS อัปสตรีม <0>สำหรับโดเมนเฉพาะ</0>\",\n  \"example_upstream_sdns\": \"คุณสามรถใช้ <0>DNS Stamps</0> กับ <1>DNSCrypt</1> หรือ <2>DNS-over-HTTPS</2> ตัวแก้ปัญหา\",\n  \"example_upstream_tcp\": \"dNS ปกติ (ผ่าน TCP)\",\n  \"example_upstream_tcp_hostname\": \"DNS ปกติ (ผ่าน TCP, ชื่อโฮสต์);\",\n  \"example_upstream_tcp_port\": \"DNS ปกติ (ผ่าน TCP, พร้อมพอร์ต);\",\n  \"example_upstream_udp\": \"DNS ปกติ (ผ่าน UDP, ชื่อโฮสต์);\",\n  \"examples_title\": \"ตัวอย่าง\",\n  \"fallback_dns_desc\": \"รายการเซิร์ฟเวอร์ DNS สำรองที่ใช้เมื่อเซิร์ฟเวอร์ DNS ต้นทางไม่ตอบสนอง ซินแท็กซ์เหมือนกับในฟิลด์เซิร์ฟเวอร์ต้นทางหลักข้างต้น.\",\n  \"fallback_dns_placeholder\": \"ป้อนเซิร์ฟเวอร์ DNS สำรองหนึ่งตัวต่อหนึ่งบรรทัด\",\n  \"fallback_dns_title\": \"เซิร์ฟเวอร์ DNS สำรอง\",\n  \"faq\": \"คำถามที่พบบ่อย\",\n  \"fastest_addr\": \"ที่อยู่ IP ที่เร็วที่สุด\",\n  \"fastest_addr_desc\": \"รอการตอบกลับจาก <b>ทั้งหมด</b> เซิร์ฟเวอร์ DNS, วัดความเร็วการเชื่อมต่อ TCP สำหรับแต่ละเซิร์ฟเวอร์, และคืนค่าที่อยู่ IP ของเซิร์ฟเวอร์ที่มีความเร็วการเชื่อมต่อเร็วที่สุด.<br/>โหมดนี้อาจทำให้การค้นหา DNS ช้าลงอย่างมีนัยสำคัญหากหนึ่งหรือหลายเซิร์ฟเวอร์ต้นทางไม่ได้ตอบกลับ ตรวจสอบให้แน่ใจว่าเซิร์ฟเวอร์ต้นทางของคุณมีเสถียรภาพและเวลาตอบกลับของคุณต่ำ.\",\n  \"filter\": \"ตัวกรอง\",\n  \"filter_added_successfully\": \"ตัวกรองเพิ่มเรียบร้อยแล้ว\",\n  \"filter_allowlist\": \"คำเตือน: การดำเนินการนี้จะยกเว้นกฎ \\\"{{disallowed_rule}}\\\" จากรายการไคลเอนต์ที่อนุญาตด้วย\",\n  \"filter_category_general\": \"ทั่วไป\",\n  \"filter_category_general_desc\": \"รายการที่ปิดกั้นการติดตามและโฆษณาบนอุปกรณ์ส่วนใหญ่\",\n  \"filter_category_other\": \"อื่น ๆ\",\n  \"filter_category_other_desc\": \"รายการปิดกั้นอื่น ๆ\",\n  \"filter_category_regional\": \"ภูมิภาค\",\n  \"filter_category_regional_desc\": \"รายการที่เน้นโฆษณาระดับภูมิภาคและเซิร์ฟเวอร์ตัวติดตาม\",\n  \"filter_category_security\": \"ความปลอดภัย\",\n  \"filter_category_security_desc\": \"รายการที่ออกแบบมาเพื่อปิดกั้นโดเมนที่เป็นอันตราย ฟิชชิ่ง และโดเมนหลอกลวง\",\n  \"filter_removed_successfully\": \"รายการถูกลบเรียบร้อยแล้ว\",\n  \"filter_updated\": \"อัปเดตตัวกรองสำเร็จแล้ว\",\n  \"filtered\": \"ถูกกรอง\",\n  \"filtered_custom_rules\": \"ถูกกรองโดยกฎการกรองที่กำหนดเอง\",\n  \"filtering_rules_learn_more\": \"<0>เรียนรู้เพิ่มเติม</0> เกี่ยวกับการสร้างรายการปิดกั้นโฮสต์ของคุณเอง\",\n  \"filters\": \"ตัวกรอง\",\n  \"filters_and_hosts_hint\": \"AdGuard Home เข้าใจกฎปิดกั้นโฆษณาพื้นฐานและโฮสต์ไฟล์ไวยากรณ์\",\n  \"filters_block_toggle_hint\": \"คุณสามารถตั้งค่ากฎการปิดกั้นในการตั้งค่า<a>ตัวกรอง</a>\",\n  \"filters_configuration\": \"การกำหนดค่าตัวกรอง\",\n  \"filters_enable\": \"เปิดใช้งานตัวกรอง\",\n  \"filters_interval\": \"ช่วงเวลาการอัปเดตตัวกรอง\",\n  \"fix\": \"ซ่อม\",\n  \"for_last_days\": \"สำหรับ {{count}} วันสุดท้าย\",\n  \"for_last_days_plural\": \"สำหรับ {{count}} วันล่าสุด\",\n  \"for_last_hours\": \"สำหรับ {{count}} ชั่วโมงล่าสุด\",\n  \"for_last_hours_plural\": \"สำหรับ {{count}} ชั่วโมงล่าสุด\",\n  \"forgot_password\": \"ลืมรหัสผ่าน?\",\n  \"forgot_password_desc\": \"โปรดปฏิบัติตาม <0>ขั้นตอนเหล่านี้</0> เพื่อสร้างรหัสผ่านใหม่สำหรับบัญชีผู้ใช้ของคุณ\",\n  \"form_add_id\": \"เพิ่มตัวระบุ\",\n  \"form_answer\": \"ป้อนชื่อโดเมนหรือ IP\",\n  \"form_client_name\": \"กรอกชื่อเครื่องลูกข่าย\",\n  \"form_domain\": \"ป้อนชื่อโดเมน\",\n  \"form_enter_blocked_response_ttl\": \"ป้อนการตอบกลับที่ถูกบล็อค TTL (วินาที)\",\n  \"form_enter_host\": \"ป้อนชื่อโฮสต์\",\n  \"form_enter_hostname\": \"ป้อนชื่อโฮสต์\",\n  \"form_enter_id\": \"ป้อนตัวระบุ\",\n  \"form_enter_ip\": \"กรอก IP\",\n  \"form_enter_mac\": \"กรอก MAC\",\n  \"form_enter_rate_limit\": \"ป้อนขีดจำกัดอัตรา\",\n  \"form_enter_rate_limit_subnet_len\": \"ป้อนความยาวคำนำหน้าซับเน็ตเพื่อจำกัดอัตรา\",\n  \"form_enter_subnet_ip\": \"กรอกที่อยู่ IP ในซับเน็ต \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"ป้อนระยะเวลา timeout ของเซิร์ฟเวอร์ต้นทางเป็นวินาที\",\n  \"form_error_answer_format\": \"รูปแบบคำตอบไม่ถูกต้อง\",\n  \"form_error_client_id_format\": \"รูปแบบ ID ลูกค้าไม่ถูกต้อง\",\n  \"form_error_domain_format\": \"รูปแบบ Domain ไม่ถูกต้อง\",\n  \"form_error_equal\": \"ไม่ควรตรงกัน\",\n  \"form_error_gateway_ip\": \"สัญญาเช่าไม่สามารถมีที่อยู่ IP ของเกตเวย์ได้\",\n  \"form_error_ip4_format\": \"รูปแบบ IPv4 ไม่ถูกต้อง\",\n  \"form_error_ip4_gateway_format\": \"ที่อยู่ IPv4 ของเกตเวย์ไม่ถูกต้อง\",\n  \"form_error_ip6_format\": \"รูปแบบ IPv6 ไม่ถูกต้อง\",\n  \"form_error_ip_format\": \"รูปแบบ IP ไม่ถูกต้อง\",\n  \"form_error_mac_format\": \"รูปแบบ MAC ไม่ถูกต้อง\",\n  \"form_error_password\": \"รหัสผ่านไม่ตรงกัน\",\n  \"form_error_password_length\": \"รหัสผ่านต้องมีความยาว {{min}} ถึง {{max}} อักขระ\",\n  \"form_error_port\": \"ป้อนค่าพอร์ตที่ถูกต้อง\",\n  \"form_error_port_range\": \"ป้อนค่าพอร์ตในช่วง 80-65535\",\n  \"form_error_port_unsafe\": \"เป็นพอร์ทที่ไม่ปลอดภัย\",\n  \"form_error_positive\": \"ต้องมากกว่า 0\",\n  \"form_error_required\": \"ช่องที่ต้องกรอก\",\n  \"form_error_server_name\": \"ชื่อเซิร์ฟเวอร์ไม่ถูกต้อง\",\n  \"form_error_subnet\": \"ซับเน็ต \\\"{{cidr}}\\\" ไม่มีที่อยู่ IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"รูปแบบ URL ไม่ถูกต้อง\",\n  \"form_error_url_or_path_format\": \"URL ไม่ถูกต้องหรือเส้นทางสัมบูรณ์ของรายการ\",\n  \"form_select_tags\": \"เลือกแท็กเครื่อง\",\n  \"found_in_known_domain_db\": \"พบในฐานข้อมูลโดเมนที่รู้จัก\",\n  \"friday\": \"ศุกร์\",\n  \"friday_short\": \"ศุกร์\",\n  \"gateway_or_subnet_invalid\": \"หน้ากากซับเน็ตไม่ถูกต้อง\",\n  \"general_settings\": \"การตั้งค่าทั่วไป\",\n  \"general_statistics\": \"สถิติทั่วไป\",\n  \"get_started\": \"เริ่มต้นการใช้งาน\",\n  \"greater_range_start_error\": \"ต้องมากกว่าช่วงเริ่มต้น\",\n  \"homepage\": \"หน้าหลัก\",\n  \"host_whitelisted\": \"โฮสต์ได้รับอนุญาต\",\n  \"ignore_domains\": \"โดเมนที่ถูกละเว้น (แยกด้วยการขึ้นบรรทัดใหม่)\",\n  \"ignore_domains_desc_query\": \"คำค้นที่ตรงกับกฎเหล่านี้จะไม่ถูกเขียนลงในบันทึกคำค้น\",\n  \"ignore_domains_desc_stats\": \"คำค้นที่ตรงกับกฎเหล่านี้จะไม่ถูกเขียนลงในสถิติ\",\n  \"ignore_domains_title\": \"โดเมนที่ถูกละเว้น\",\n  \"ignore_query_log\": \"ละเว้นไคลเอนต์นี้ในบันทึกการค้นหา\",\n  \"ignore_statistics\": \"ละเว้นไคลเอนต์นี้ในสถิติ\",\n  \"install_auth_confirm\": \"ยืนยันรหัสผ่าน\",\n  \"install_auth_desc\": \"ขอแนะนำอย่างยิ่งให้กำหนดค่าการตรวจสอบรหัสผ่านให้กับส่วนต่อประสานเว็บผู้ดูแลระบบ AdGuard Home ของคุณ แม้ว่ามันจะสามารถเข้าถึงได้เฉพาะในเครือข่ายท้องถิ่นของคุณก็ยังคงเป็นสิ่งสำคัญที่จะปกป้องมันจากการเข้าถึงที่ไม่จำกัด\",\n  \"install_auth_password\": \"รหัสผ่าน\",\n  \"install_auth_password_enter\": \"กรอกรหัสผ่าน\",\n  \"install_auth_title\": \"การตรวจสอบสิทธิ์\",\n  \"install_auth_username\": \"ชื่อผู้ใช้\",\n  \"install_auth_username_enter\": \"กรอกชื่อผู้ใช้\",\n  \"install_devices_address\": \"เซิร์ฟเวอร์ DNS ของ AdGuard Home กำลังรับฟังตามที่อยู่ต่อไปนี้\",\n  \"install_devices_android_list_1\": \"เข้าหน้าเมนู(บางรุ่นจะมีตรงแท็บการแจ้งเตือน) เลือกการตั้งค่า\",\n  \"install_devices_android_list_2\": \"เลือกเมนู Wi-Fi แล้วค้นหา Wi-Fi ที่จะเชื่อมต่อ (ไม่สารถตั้งค่ากับเน็ตมือถือได้)\",\n  \"install_devices_android_list_3\": \"แตะชื่อWi-Fi ที่จะเชื่อมต่อค้างไว้(บางรุ่นให้เลื่อนจอลงไปล่างสุด) เลือกการตั้งค่าเพิ่มเติม\",\n  \"install_devices_android_list_4\": \"ในอุปกรณ์บางอย่างคุณอาจต้องทำเครื่องหมายในช่องสำหรับขั้นสูงเพื่อดูการตั้งค่าเพิ่มเติม หากต้องการปรับการตั้งค่า Android DNS ของคุณคุณจะต้องเปลี่ยนการตั้งค่า IP จาก DHCP เป็นแบบคงที่\",\n  \"install_devices_android_list_5\": \"เปลี่ยนการตั้งค่า DNS ที่ 1 และค่า DNS 2 ถึงที่อยู่เซิร์ฟเวอร์ AdGuard Home ของคุณ\",\n  \"install_devices_desc\": \"ในการเริ่มใช้งาน AdGuard Home คุณต้องกำหนดค่าอุปกรณ์ของคุณเพื่อใช้งาน\",\n  \"install_devices_ios_list_1\": \"เลือกการตั้งค่า\",\n  \"install_devices_ios_list_2\": \"เลือก Wi-Fi ด้านซ้าย (ไม่สามรถใช้งานได้กับดาต้ามือถือ)\",\n  \"install_devices_ios_list_3\": \"เลือกชื่อที่จะเชื่อมต่อ\",\n  \"install_devices_ios_list_4\": \"กรอก DNS AdGuard Home Server ลงไปในช่อง\",\n  \"install_devices_macos_list_1\": \"คลิกโลโก้แอปเปิ้ลแล้วกด System Preferences\",\n  \"install_devices_macos_list_2\": \"คลิก  Network\",\n  \"install_devices_macos_list_3\": \"เลือกการเชื่อมต่อแล้วคลิก Advanced\",\n  \"install_devices_macos_list_4\": \"ค้นหาแท็บ DNS แล้วกรอกหมาเลย AdGuard Home\",\n  \"install_devices_router\": \"เราเตอร์\",\n  \"install_devices_router_desc\": \"การตั้งค่านี้จะครอบคลุมอุปกรณ์ทั้งหมดที่เชื่อมต่อกับเราเตอร์ที่บ้านของคุณโดยอัตโนมัติและคุณไม่จำเป็นต้องกำหนดค่าแต่ละอุปกรณ์ด้วยตนเอง\",\n  \"install_devices_router_list_1\": \"เปิดการตั้งค่าสำหรับเราเตอร์ของคุณ โดยปกติแล้วคุณสามารถเข้าถึงได้จากเบราว์เซอร์ของคุณผ่าน URL (เช่น http://192.168.0.1/ หรือ http://192.168.1.1/) คุณอาจถูกขอให้ป้อนรหัสผ่าน หากคุณจำไม่ได้คุณสามารถรีเซ็ตรหัสผ่านได้บ่อยครั้งโดยกดปุ่มบนเราเตอร์เอง อย่างไรก็ตามคุณควรทราบว่าหากเลือกขั้นตอนนี้คุณอาจสูญเสียการกำหนดค่าของเราเตอร์ทั้งหมด หากเราเตอร์ของคุณต้องการแอปในการตั้งค่า กรุณาติดตั้งแอปบนโทรศัพท์หรือคอมพิวเตอร์ของคุณแล้วใช้เพื่อเข้าถึงการตั้งค่าของเราเตอร์\",\n  \"install_devices_router_list_2\": \"ค้นหาการตั้งค่า DHCP/DNS ค้นหาตัวอักษร DNS ที่อยู่ถัดจากช่องที่อนุญาตให้มีตัวเลขสองหรือสามชุดโดยแต่ละกลุ่มแบ่งออกเป็นสี่กลุ่มหนึ่งถึงสามหลัก\",\n  \"install_devices_router_list_3\": \"ป้อนที่อยู่เซิร์ฟเวอร์ AdGuard Home ของคุณที่นั่น\",\n  \"install_devices_router_list_4\": \"ในเราเตอร์บางประเภท ไม่สามารถตั้งค่าเซิร์ฟเวอร์ DNS แบบกำหนดเองได้ ในกรณีนั้น ให้ตั้งค่า AdGuard Home เป็นเซิร์ฟเวอร์ DHCP</0> อาจช่วยได้ มิฉะนั้น คุณควรตรวจสอบคู่มือเราเตอร์เพื่อดูวิธีปรับแต่งเซิร์ฟเวอร์ DNS ในรุ่นเราเตอร์ของคุณโดยเฉพาะ\",\n  \"install_devices_title\": \"กำหนดค่าอุปกรณ์ของคุณ\",\n  \"install_devices_windows_list_1\": \"เปิด  Control Panel โดยใช้ Start menu หรือ Windows search\",\n  \"install_devices_windows_list_2\": \"ไปที่หมวด  Network and Internet แล้วเลือก Network and Sharing Center\",\n  \"install_devices_windows_list_3\": \"ทางด้านซ้ายจะมีคำว่า  Change adapter settings ให้กดเข้าไป\",\n  \"install_devices_windows_list_4\": \"เลือกการเชื่อมต่อที่ใช้งานอยู่ คลิกขวาแล้วเลือก Properties\",\n  \"install_devices_windows_list_5\": \"ค้นหา Internet Protocol Version 4 (TCP/IP)  แล้วคลิก  Properties  อีกครั้ง\",\n  \"install_devices_windows_list_6\": \"เลือก \\\"ใช้ที่อยู่เซิร์ฟเวอร์ DNS ต่อไปนี้\\\" และป้อนที่อยู่เซิร์ฟเวอร์ AdGuard Home ของคุณ\",\n  \"install_saved\": \"บันทึกเรียบร้อยแล้ว\",\n  \"install_settings_all_interfaces\": \"อินเทอร์เฟซทั้งหมด\",\n  \"install_settings_dns\": \"เซิรฟ์เวอร์ DNS\",\n  \"install_settings_dns_desc\": \"คุณจะต้องกำหนดค่าอุปกรณ์หรือเราเตอร์ของคุณเพื่อใช้เซิร์ฟเวอร์ DNS ตามที่อยู่ต่อไปนี้:\",\n  \"install_settings_interface_link\": \"เว็บอินเตอร์เฟสผู้ดูแลระบบ AdGuard Home ของคุณจะพร้อมใช้งานตามที่อยู่ต่อไปนี้:\",\n  \"install_settings_listen\": \"รูปแบบการดักจับ\",\n  \"install_settings_port\": \"พอร์ต\",\n  \"install_settings_title\": \"รูปแบบเว็บสำหรับผู้ดูแล\",\n  \"install_static_configure\": \"AdGuard Home ตรวจพบว่าใช้ที่อยู่ IP แบบไดนามิก <0>{{ip}}</0> คุณต้องการให้ตั้งเป็นที่อยู่ IP คงที่ของคุณหรือไม่?\",\n  \"install_static_error\": \"AdGuard Home ไม่สามารถกำหนดค่าโดยอัตโนมัติสำหรับอินเตอร์เฟซเครือข่ายนี้ได้ กรุณาหาเอกสารในการทำขั้นตอนนี้ด้วยตนเอง.\",\n  \"install_static_ok\": \"ข่าวดี! ที่อยู่ IP คงที่ได้ถูกกำหนดค่าแล้ว\",\n  \"install_step\": \"ขั้นตอน\",\n  \"install_submit_desc\": \"ขั้นตอนการตั้งค่าเสร็จสิ้นและคุณพร้อมที่จะเริ่มใช้งาน AdGuard Home\",\n  \"install_submit_title\": \"ยินดีด้วย!\",\n  \"install_welcome_desc\": \"AdGuard Home เป็นเซิร์ฟเวอร์ DNS ปิดกั้นโฆษณาและติดตามทั่วทั้งเครือข่าย วัตถุประสงค์คือเพื่อให้คุณควบคุมเครือข่ายทั้งหมดและอุปกรณ์ทั้งหมดของคุณและไม่จำเป็นต้องใช้โปรแกรมฝั่งไคลเอ็นต์\",\n  \"install_welcome_title\": \"ยินดีต้อนรับสู่ AdGuard Home\",\n  \"interval_24_hour\": \"24 ชั่วโมง\",\n  \"interval_6_hour\": \"6 ชั่วโมง\",\n  \"interval_days\": \"{{count}} วัน\",\n  \"interval_days_plural\": \"{{count}} วัน\",\n  \"interval_hours\": \"{{count}} ชั่วโมง\",\n  \"interval_hours_plural\": \"{{count}} ชั่วโมง\",\n  \"ip\": \"ไอพี\",\n  \"ip_address\": \"IP addresses\",\n  \"known_tracker\": \"ตัวติดตามที่รู้จัก\",\n  \"last_rule_in_allowlist\": \"ไม่สามารถไม่อนุญาตไคลเอนต์นี้ได้เนื่องจากการยกเว้นกฎ \\\"{{disallowed_rule}}\\\" จะปิดการใช้งานรายการ \\\"ไคลเอนต์ที่อนุญาต\\\"\",\n  \"last_time_updated_table_header\": \"ปรับปรุงครั้งล่าสุด\",\n  \"list_confirm_delete\": \"แน่ใจไหมว่าต้องการลบรายการนี้?\",\n  \"list_label\": \"รายการ\",\n  \"list_updated\": \"{{count}} รายการได้รับการอัปเดต\",\n  \"list_updated_plural\": \"{{count}} รายการอัปเดต\",\n  \"list_url_table_header\": \"รายการ URL\",\n  \"load_balancing\": \"โหลดบาลานซ์\",\n  \"load_balancing_desc\": \"สอบถามเซิร์ฟเวอร์ต้นทางทีละตัว<br/>AdGuard Home ใช้อัลกอริธึมการสุ่มแบบถ่วงน้ำหนักเพื่อเลือกเซิร์ฟเวอร์ที่มีจำนวนการค้นหาที่ล้มเหลวต่ำที่สุดและเวลาการค้นหาที่ต่ำที่สุด\",\n  \"loading_table_status\": \"กำลังโหลด...\",\n  \"local_ptr_default_resolver\": \"ตามค่าเริ่มต้น AdGuard Home ใช้ resolver DNS ตรงข้ามต่อไปนี้: {{ip}}\",\n  \"local_ptr_desc\": \"เซิร์ฟเวอร์ DNS ที่ AdGuard Home ใช้สำหรับคำขอ PTR, SOA และ NS ส่วนตัว คำขอจะถูกพิจารณาว่าเป็นส่วนตัวหากขอโดเมน ARPA ที่มีซับเน็ตภายในช่วง IP ส่วนตัว (เช่น \\\"192.168.12.34\\\") และมาจากไคลเอนต์ที่มีที่อยู่ IP ส่วนตัว หากไม่ตั้งค่า จะใช้ DNS resolver เริ่มต้นของระบบปฏิบัติการของคุณ ยกเว้นที่อยู่ IP ของ AdGuard Home\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home ไม่สามารถกำหนด DNS resolver ส่วนตัวที่เหมาะสมสำหรับระบบนี้ได้\",\n  \"local_ptr_placeholder\": \"ป้อนที่อยู่เซิร์ฟเวอร์หนึ่งรายการต่อบรรทัด\",\n  \"local_ptr_title\": \"เซิร์ฟเวอร์ DNS ส่วนตัว\",\n  \"location\": \"ตำแหน่ง\",\n  \"log_and_stats_section_label\": \"บันทึกการสอบถามและสถิติ\",\n  \"lower_range_start_error\": \"ต้องน้อยกว่าช่วงเริ่มต้น\",\n  \"main_settings\": \"ตั้งค่าหลัก\",\n  \"make_static\": \"ทำให้คงที่\",\n  \"manual_update\": \"อัปเดทล้มเหลว กรุณา <a> ทำตามขั้นตอน </a> เพื่ออัพเดทด้วยตนเอง\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"วันจันทร์\",\n  \"monday_short\": \"จันทร์\",\n  \"name\": \"ชื่อ\",\n  \"name_table_header\": \"ชื่อ\",\n  \"netname\": \"ชื่อเครือข่าย\",\n  \"network\": \"เครือข่าย\",\n  \"new_allowlist\": \"รายการอนุญาตใหม่\",\n  \"new_blocklist\": \"รายการบล็อคใหม่\",\n  \"next\": \"ถัดไป\",\n  \"next_btn\": \"ถัดไป\",\n  \"no_blocklist_added\": \"ไม่มีรายการบล็อกเพิ่ม\",\n  \"no_clients_found\": \"ไม่มีเครื่องลูกข่าย\",\n  \"no_domains_found\": \"ไม่พบโดเมน\",\n  \"no_logs_found\": \"ไม่มีประวัติ\",\n  \"no_servers_specified\": \"ไม่ได้ระบุเซิร์ฟเวอร์\",\n  \"no_upstreams_data_found\": \"ไม่พบข้อมูลเซิร์ฟเวอร์ต้นทาง\",\n  \"no_whitelist_added\": \"ไม่มีการเพิ่มรายการอนุญาต\",\n  \"nothing_found\": \"ไม่พบอะไร\",\n  \"null_ip\": \"IP ว่าง\",\n  \"number_of_dns_query_blocked_24_hours\": \"จำนวนคำขอ DNS ที่ถูกปิดกั้นโดยตัวกรองปิดกั้นและโฮสต์รายการปิดกั้น\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"มีการปิดกั้นเว็บไซต์สำหรับผู้ใหญ่จำนวนหนึ่ง\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"คำขอ DNS จำนวนหนึ่งถูกปิดกั้นโดยโมดูลความปลอดภัยการเรียกดู AdGuard\",\n  \"number_of_dns_query_days\": \"จำนวนการสืบค้น DNS ที่ประมวลผลสำหรับ {{count}} วันล่าสุด\",\n  \"number_of_dns_query_days_plural\": \"จำนวนการสืบค้น DNS ที่ดำเนินการในช่วง {{count}} วันล่าสุด\",\n  \"number_of_dns_query_hours\": \"จำนวนการสืบค้น DNS ที่ดำเนินการในช่วง {{count}} ชั่วโมงล่าสุด\",\n  \"number_of_dns_query_hours_plural\": \"จำนวนการสืบค้น DNS ที่ดำเนินการในช่วง {{count}} ชั่วโมงล่าสุด\",\n  \"number_of_dns_query_to_safe_search\": \"จำนวนคำขอ DNS ไปยังเครื่องมือค้นหาที่บังคับใช้การค้นหาปลอดภัย\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"ปิด\",\n  \"on\": \"เปิด\",\n  \"open_dashboard\": \"เปิดหน้าควบคุม\",\n  \"orgname\": \"ชื่อองค์กร\",\n  \"original_response\": \"ตอบกลับเดิม\",\n  \"out_of_range_error\": \"ต้องออกจากช่วง \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"หน้า\",\n  \"parallel_requests\": \"คำขอแบบคู่ขนาน\",\n  \"parental_control\": \"ควบคุมโดยผู้ปกครอง\",\n  \"password_label\": \"รหัสผ่าน\",\n  \"password_placeholder\": \"ใส่รหัสผ่าน\",\n  \"plain_dns\": \"DNS ธรรมดา\",\n  \"port_53_faq_link\": \"พอร์ต 53 มักถูกครอบครองโดยบริการ \\\"DNSStubListener\\\" หรือ \\\"systemd-resolved\\\" โปรดอ่าน <0>คำแนะนำนี้</0> เกี่ยวกับวิธีแก้ไขปัญหานี้\",\n  \"previous_btn\": \"ก่อนหน้า\",\n  \"privacy_policy\": \"นโยบายความเป็นส่วนตัว\",\n  \"processing_update\": \"รอซักครู่ AdGuard Home กำลังอัปเดท\",\n  \"protection_section_label\": \"การป้องกัน\",\n  \"protocol\": \"โปรโตคอล\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"บันทึกการสืบค้น\",\n  \"query_log_clear\": \"ล้างบันทึกการสืบค้น\",\n  \"query_log_cleared\": \"บันทึกการใช้งานได้รับการล้างเรียบร้อยแล้ว\",\n  \"query_log_configuration\": \"บันทึกการกำหนดค่า\",\n  \"query_log_confirm_clear\": \"คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?\",\n  \"query_log_disabled\": \"บันทึกแบบสอบถามถูกปิดใช้งานและสามารถกำหนดค่าใน <0>การตั้งค่า</0>\",\n  \"query_log_enable\": \"เปิดใช้งานบันทึก\",\n  \"query_log_filtered\": \"กรองโดย {{filter}}\",\n  \"query_log_response_status\": \"สถานะ: {{value}}\",\n  \"query_log_retention\": \"การหมุนเวียนบันทึกคำขอ\",\n  \"query_log_retention_confirm\": \"คุณแน่ใจหรือไม่ว่าต้องการเปลี่ยนการเก็บข้อมูลบันทึกแบบสอบถาม? หากคุณลดค่าช่วงเวลา ข้อมูลบางอย่างจะหายไป\",\n  \"query_log_strict_search\": \"ใช้เครื่องหมายคำพูดคู่เพื่อการค้นหาที่จำกัด\",\n  \"query_log_updated\": \"อัปเดตบันทึกการสืบค้นสำเร็จแล้ว\",\n  \"rate_limit\": \"จำกัดอัตรา\",\n  \"rate_limit_desc\": \"จำนวนการร้องขอต่อวินาทีที่อนุญาตให้ไคลเอนต์เดียวทำ (0: ไม่จำกัดจำนวน)\",\n  \"rate_limit_subnet_len_ipv4\": \"ความยาวของคำนำหน้าซับเน็ตสำหรับที่อยู่ IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"ความยาวพรีฟิกซ์ซับเน็ตสำหรับที่อยู่ IPv4 ที่ใช้สำหรับการจำกัดอัตรา ค่าเริ่มต้นคือ 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"ความยาวพรีฟิกซ์ซับเน็ต IPv4 ควรอยู่ระหว่าง 0 ถึง 32\",\n  \"rate_limit_subnet_len_ipv6\": \"ความยาวของคำนำหน้าซับเน็ตสำหรับที่อยู่ IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"ความยาวพรีฟิกซ์ซับเน็ตสำหรับที่อยู่ IPv6 ที่ใช้สำหรับการจำกัดอัตรา ค่าเริ่มต้นคือ 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"ความยาวพรีฟิกซ์ซับเน็ต IPv6 ควรอยู่ระหว่าง 0 ถึง 128\",\n  \"rate_limit_whitelist\": \"รายการอนุญาตจำกัดอัตรา\",\n  \"rate_limit_whitelist_desc\": \"ที่อยู่ IP ที่ถูกยกเว้นจากการจำกัดอัตรา\",\n  \"rate_limit_whitelist_placeholder\": \"ป้อนที่อยู่ IP หนึ่งรายการต่อบรรทัด\",\n  \"refresh_btn\": \"รีเฟรช\",\n  \"refresh_statics\": \"รีเฟรชสถิติ\",\n  \"refused\": \"ปฏิเสธ\",\n  \"report_an_issue\": \"รายงานปัญหา\",\n  \"request_details\": \"รายละเอียดคำขอ\",\n  \"request_table_header\": \"คำขอ\",\n  \"requests_count\": \"จำนวนคำขอ\",\n  \"reset_settings\": \"รีเซ็ตการตั้งค่า\",\n  \"resolve_clients_desc\": \"หากเปิดใช้งาน AdGuard Home จะพยายามแก้ไขที่อยู่ IP ของไคลเอ็นต์กลับเป็นชื่อโฮสต์โดยการส่งแบบสอบถาม PTR ไปยังตัวแก้ไขที่เกี่ยวข้อง (เซิร์ฟเวอร์ DNS ส่วนตัวสำหรับไคลเอนต์ในเครื่องเซิร์ฟเวอร์ต้นน้ำสำหรับไคลเอนต์ที่มีที่อยู่ IP สาธารณะ)\",\n  \"resolve_clients_title\": \"เปิดใช้งานการแก้ไขย้อนกลับของที่อยู่ IP ของไคลเอ็นต์\",\n  \"response_code\": \"รหัสตอบกลับ\",\n  \"response_details\": \"รายละเอียดการตอบกลับ\",\n  \"response_table_header\": \"การตอบสนอง\",\n  \"response_time\": \"เวลาในการตอบสนอง\",\n  \"rewrite_A\": \"<0>A</0>: ค่าเฉพาะ, เก็บ <0>A</0> บันทึกจาก upstream\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: ค่าเฉพาะ, เก็บ <0>AAAA</0> บันทึกจาก upstream\",\n  \"rewrite_add\": \"เพิ่ม DNS rewrite\",\n  \"rewrite_added\": \"เพิ่มการเขียน DNS ใหม่สำหรับ \\\"{{key}}\\\" สำเร็จแล้ว\",\n  \"rewrite_applied\": \"กฎการเขียนใหม่ถูกนำมาใช้\",\n  \"rewrite_confirm_delete\": \"คุณแน่ใจหรือไม่ว่าต้องการลบการเขียน DNS ใหม่สำหรับ \\\"{{key}}\\\"\",\n  \"rewrite_deleted\": \"ลบการเขียน DNS ใหม่สำหรับ \\\"{{key}}\\\" สำเร็จแล้ว\",\n  \"rewrite_desc\": \"ช่วยให้สามารถกำหนดค่าการตอบสนอง DNS แบบกำหนดเองสำหรับชื่อโดเมนเฉพาะได้อย่างง่ายดาย\",\n  \"rewrite_domain_name\": \"ชื่อโดเมน: เพิ่มระเบียน CNAME\",\n  \"rewrite_edit\": \"แก้ไขการเขียน DNS ใหม่\",\n  \"rewrite_hosts_applied\": \"เขียนใหม่โดยกฎไฟล์โฮสต์\",\n  \"rewrite_ip_address\": \"ที่อยู่ IP: ใช้ IP นี้ในการตอบสนอง A หรือ AAAA\",\n  \"rewrite_not_found\": \"ไม่พบการเขียน DNS ใหม่\",\n  \"rewrite_settings_updated\": \"การตั้งค่าเขียนทับ DNS อัปเดตเรียบร้อยแล้ว\",\n  \"rewrite_updated\": \"อัปเดตการเขียน DNS ใหม่สำเร็จแล้ว\",\n  \"rewrites_disabled_table_header\": \"ปิดใช้งานการเขียนทับแล้ว\",\n  \"rewrites_enabled_table_header\": \"เปิดใช้งานการเขียนทับแล้ว\",\n  \"rewritten\": \"เขียนใหม่\",\n  \"rows_table_footer_text\": \"ตาราง\",\n  \"rule_added_to_custom_filtering_toast\": \"เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว {{rule}}\",\n  \"rule_label\": \"กฎ\",\n  \"rule_removed_from_custom_filtering_toast\": \"ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว {{rule}}\",\n  \"rules_count_table_header\": \"กฎการนับ\",\n  \"safe_browsing\": \"ท่องเว็บอย่างปลอดภัย\",\n  \"safe_search\": \"ค้นหาอย่างปลอดภัย\",\n  \"saturday\": \"เสาร์\",\n  \"saturday_short\": \"เสาร์\",\n  \"save_btn\": \"บันทึก\",\n  \"save_config\": \"บันทึกการตั้งค่า\",\n  \"schedule_add\": \"เพิ่มตารางเวลา\",\n  \"schedule_current_timezone\": \"เขตเวลาปัจจุบัน: {{value}}\",\n  \"schedule_desc\": \"ตั้งค่าระยะเวลาที่ไม่มีกิจกรรมสำหรับบริการที่ถูกบล็อค\",\n  \"schedule_edit\": \"แก้ไขตารางเวลา\",\n  \"schedule_from\": \"จาก\",\n  \"schedule_invalid_select\": \"เวลาเริ่มต้นต้องอยู่ก่อนเวลาสิ้นสุด\",\n  \"schedule_modal_description\": \"ตารางนี้จะมาแทนที่ตารางที่มีอยู่สำหรับวันเดียวกันของสัปดาห์ แต่ละวันของสัปดาห์สามารถมีช่วงที่ไม่มีกิจกรรมได้เพียงช่วงเดียวเท่านั้น\",\n  \"schedule_modal_time_off\": \"ไม่มีการบล็อคบริการ:\",\n  \"schedule_new\": \"ตารางใหม่\",\n  \"schedule_remove\": \"ลบตารางเวลา\",\n  \"schedule_save\": \"บันทึกตารางเวลา\",\n  \"schedule_select_days\": \"เลือกวัน\",\n  \"schedule_services\": \"หยุดการบล็อคบริการ\",\n  \"schedule_services_desc\": \"กำหนดค่ากำหนดการหยุดชั่วคราวของตัวกรองการบล็อคบริการ\",\n  \"schedule_services_desc_client\": \"กำหนดค่ากำหนดการหยุดชั่วคราวของตัวกรองการบล็อคบริการสำหรับไคลเอนต์นี้\",\n  \"schedule_time_all_day\": \"ตลอดทั้งวัน\",\n  \"schedule_timezone\": \"เลือกโซนเวลา\",\n  \"schedule_to\": \"ถึง\",\n  \"served_from_cache_label\": \"เสิร์ฟจากแคช\",\n  \"service_name\": \"ชื่อบริการ\",\n  \"set_static_ip\": \"ตั้งค่าที่อยู่ IP แบบคงที่\",\n  \"settings\": \"การตั้งค่า\",\n  \"settings_custom\": \"กำหนดเอง\",\n  \"settings_global\": \"ทั่วโลก\",\n  \"setup_config_to_enable_dhcp_server\": \"ตั้งค่าคอนฟิกเพื่อเปิดใช้งานเซิร์ฟเวอร์ DHCP\",\n  \"setup_dns_notice\": \"เพื่อใช้ <1>DNS-over-HTTPS</1> หรือ <1>DNS-over-TLS</1> คุณต้อง<0>กำหนดค่าการเข้ารหัส</0> ในการตั้งค่า AdGuard Home\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS</0> ใช้ที่อยู่ <1>{{address}}</1>\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS</0> ใช้ที่อยู่ <1>{{address}}</1>\",\n  \"setup_dns_privacy_3\": \"<0>นี่คือรายการซอฟต์แวร์ที่คุณสามารถใช้ได้</0>\",\n  \"setup_dns_privacy_4\": \"บนอุปกรณ์ iOS 14 หรือ macOS Big Sur คุณสามารถดาวน์โหลดไฟล์พิเศษ '.mobileconfig' ที่จะเพิ่มเซิร์ฟเวอร์ <highlight>DNS-over-HTTPS</highlight> หรือ <highlight>DNS-over-TLS</highlight> ลงในการตั้งค่า DNS\",\n  \"setup_dns_privacy_android_1\": \"Android 9 รองรับ DNS-over-TLS โดยตรง หากต้องการกำหนดค่า ให้ไปที่การตั้งค่า → เครือข่ายและอินเทอร์เน็ต → ขั้นสูง → DNS ส่วนตัว และป้อนชื่อโดเมนของคุณที่นั่น\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard สำหรับ Android</0> รองรับ <1>DNS-over-HTTPS</1> และ <1>DNS-over-TLS</1> -\",\n  \"setup_dns_privacy_android_3\": \"<0>ภายใน</0> เพิ่ม <1>DNS-over-HTTPS</1> รองรับระบบ Android\",\n  \"setup_dns_privacy_ioc_mac\": \"การกำหนดค่า iOS และ macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNS โคลก</0> รองรับ <1>DNS-over-HTTPS</1> แต่เพื่อกำหนดค่าให้ใช้เซิร์ฟเวอร์ของคุณเอง คุณจะต้องสร้าง <2>DNS Stamp</2> สำหรับมัน\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard สำหรับ iOS</0> รองรับ <1>DNS-over-HTTPS</1> และ <1>DNS-over-TLS</1> การตั้งค่า\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home จะส่ง DNS ที่ปลอดภัยทุกเครื่อทุกระบบ\\n\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> รองรับโปรโตคอล DNS ที่ปลอดภัยที่รู้จักทั้งหมด\",\n  \"setup_dns_privacy_other_3\": \"<0>พร็อกซี DNScrypt</0> รองรับ <1>DNS-over-HTTPS</1> -\",\n  \"setup_dns_privacy_other_4\": \"<0>โมซิลล่า ไฟร์ฟอกซ์</0> รองรับ <1>DNS-over-HTTPS</1> -\",\n  \"setup_dns_privacy_other_5\": \"คุณจะพบการใช้งานเพิ่มเติมได้<0>ที่นี่</0> และ <1>ที่นี่</1> -\",\n  \"setup_dns_privacy_other_title\": \"การใช้งานอื่น ๆ\",\n  \"setup_guide\": \"วิธีการตั้งค่า\",\n  \"show_all_filter_type\": \"แสดงทั้งหมด\",\n  \"show_blocked_responses\": \"ปิดกั้นแล้ว\",\n  \"show_filtered_type\": \"แสดงเฉพาะที่กรองแล้ว\",\n  \"show_processed_responses\": \"ประมวลผลแล้ว\",\n  \"show_whitelisted_responses\": \"รายการที่อนุญาต\",\n  \"sign_in\": \"ลงชื่อเข้าใช้\",\n  \"sign_out\": \"ออกจากระบบ\",\n  \"source_label\": \"ที่มา\",\n  \"static_ip\": \"ที่อยู่ IP แบบคงที่\",\n  \"static_ip_desc\": \"AdGuard Home เป็นเซิร์ฟเวอร์ดังนั้นจึงต้องการที่อยู่ IP แบบคงที่เพื่อให้ทำงานได้อย่างถูกต้อง มิฉะนั้นในบางครั้งเราเตอร์ของคุณอาจกำหนดที่อยู่ IP อื่นให้กับอุปกรณ์นี้\",\n  \"statistics_clear\": \" ล้างค่าสถิติ\",\n  \"statistics_clear_confirm\": \"คุณแน่ใจหรือไม่ว่าต้องการล้างสถิติ?\",\n  \"statistics_cleared\": \"สถิติได้ถูกล้างเรียบร้อยแล้ว\",\n  \"statistics_configuration\": \"การกำหนดค่าสถิติ\",\n  \"statistics_enable\": \"เปิดใช้งานสถิติ\",\n  \"statistics_retention\": \"การเก็บรักษาสถิติ\",\n  \"statistics_retention_confirm\": \"คุณแน่ใจหรือไม่ว่าต้องการเปลี่ยนการเก็บรักษาสถิติ? หากคุณลดค่าช่วงเวลา ข้อมูลบางอย่างจะหายไป\",\n  \"statistics_retention_desc\": \"หากคุณลดค่าช่วงเวลาข้อมูลบางอย่างจะหายไป\",\n  \"stats_adult\": \"ปิดกั้นเว็บไซต์สำหรับผู้ใหญ่แล้ว\",\n  \"stats_disabled\": \"สถิติถูกปิดใช้งานแล้ว คุณสามารถเปิดใช้งานได้จาก <0>หน้าการตั้งค่า</0>.\",\n  \"stats_disabled_short\": \"สถิติถูกปิดใช้งานแล้ว\",\n  \"stats_malware_phishing\": \"ปิดกั้นมัลแวร์/ฟิชชิ่ง แล้ว\",\n  \"stats_params\": \"การกำหนดค่าสถิติ\",\n  \"stats_query_domain\": \"โดเมนที่เข้าบ่อยสุด\",\n  \"subnet_error\": \"ที่อยู่ต้องอยู่ในซับเน็ตเดียวกัน\",\n  \"sunday\": \"วันอาทิตย์\",\n  \"sunday_short\": \"อาทิตย์\",\n  \"system_host_files\": \"ไฟล์โฮสต์ระบบ\",\n  \"table_client\": \"เครื่องลูกข่าย\",\n  \"table_name\": \"ชื่อ\",\n  \"tags_desc\": \"คุณสามารถเลือกแท็กที่สอดคล้องกับลูกค้า แท็กสามารถรวมอยู่ในกฎการกรองและอนุญาตให้คุณใช้งานได้อย่างถูกต้องมากขึ้น <0>เรียนรู้เพิ่มเติม</0>\",\n  \"tags_title\": \"แท็ก\",\n  \"test_upstream_btn\": \"ทดสอบต้นทาง\",\n  \"theme_auto\": \"ออโต้\",\n  \"theme_auto_desc\": \"อัตโนมัติ (ขึ้นอยู่กับโทนสีของอุปกรณ์ของคุณ)\",\n  \"theme_dark\": \"โหมดมืด\",\n  \"theme_dark_desc\": \"ธีมสีเข้ม\",\n  \"theme_light\": \"โหมดสว่าง\",\n  \"theme_light_desc\": \"ธีมสีอ่อน\",\n  \"thursday\": \"พฤหัสบดี\",\n  \"thursday_short\": \"พฤหัส\",\n  \"time_table_header\": \"เวลา\",\n  \"top_blocked_domains\": \"โดเมนที่ถูกปิดกั้นมากที่สุด\",\n  \"top_clients\": \"ลูกข่ายที่ใช้งานบ่อยสุด\",\n  \"top_upstreams\": \"เซิร์ฟเวอร์ต้นทางที่ดีที่สุด\",\n  \"topline_expired_certificate\": \"ใบรับรอง SSL ของคุณหมดอายุแล้ว กรุณาอัปเดท <0>การตั้งค่าเข้ารหัส</0>.\",\n  \"topline_expiring_certificate\": \"ใบรับรอง SSL ของคุณกำลังจะหมดอายุ กรุณาอัปเดท <0>การตั้งค่าเข้ารหัส</0>.\",\n  \"tracker_source\": \"แหล่งที่มาของตัวติดตาม\",\n  \"try_again\": \"ลองอีกครั้ง\",\n  \"ttl_cache_validation\": \"ค่าการเขียนทับ TTL ของแคชขั้นต่ำจะต้องน้อยกว่าหรือเท่ากับค่าสูงสุด\",\n  \"tuesday\": \"อังคาร\",\n  \"tuesday_short\": \"อังคาร\",\n  \"type_table_header\": \"ประเภท\",\n  \"unavailable_dhcp\": \"DHCP ไม่พร้อมใช้งาน\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home ไม่สามารถทำงาน DHCP server  บนระบบปฎิบัติการของ Server คุณ\",\n  \"unblock\": \"เลิกปิดกั้น\",\n  \"unblock_all\": \"ปลดล็อคทั้งหมด\",\n  \"unblock_for_this_client_only\": \"เลิกปิดกั้นสำหรับไคลเอนต์นี้เท่านั้น\",\n  \"unknown_filter\": \"ตัวกรองที่ไม่รู้จัก {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} พร้อมแล้ว <0>กดตรงนี้</0> สำหรับข้อมูลเพิ่มเติม\",\n  \"update_failed\": \"อัปเดทล้มเหลว กรุณา <a> ทำตามขั้นตอน </a> เพื่ออัพเดทด้วยตนเอง\",\n  \"update_now\": \"อัปเดตตอนนี้\",\n  \"updated_custom_filtering_toast\": \"อัปเดตกฎการกรองที่กำหนดเอง\",\n  \"updated_save_search_toast\": \"การตั้งค่าการค้นหาที่ปลอดภัยได้รับการปรับปรุงแล้ว\",\n  \"updated_upstream_dns_toast\": \"อัปเดตเซิร์ฟเวอร์ DNS ต้นทาง\",\n  \"updates_checked\": \"มีรุ่นใหม่ของ AdGuard Home พร้อมใช้งาน\",\n  \"updates_version_equal\": \"AdGuard Home เป็นตัวล่าสุดแล้ว\",\n  \"upstream\": \"เซิร์ฟเวอร์ต้นทาง\",\n  \"upstream_dns\": \"เซิร์ฟเวอร์ DNS ต้นทาง\",\n  \"upstream_dns_cache_configuration\": \"การกำหนดค่าแคช DNS สำหรับเซิร์ฟเวอร์ต้นทาง\",\n  \"upstream_dns_client_desc\": \"หากคุณเว้นช่องนี้ว่างไว้ AdGuard Home จะใช้เซิร์ฟเวอร์ที่กำหนดค่าใน <0>การตั้งค่า DNS</0>\",\n  \"upstream_dns_configured_in_file\": \"กำหนดค่าใน {{path}}\",\n  \"upstream_dns_help\": \"ป้อนที่อยู่เซิร์ฟเวอร์หนึ่งรายการต่อบรรทัด <a>เรียนรู้เพิ่มเติม</a> เกี่ยวกับการกำหนดค่าเซิร์ฟเวอร์ DNS ต้นทาง\",\n  \"upstream_parallel\": \"ใช้การสืบค้นแบบขนานเพื่อเพิ่มความเร็วในการแก้ไขโดยการสอบถามเซิร์ฟเวอร์ upstream ทั้งหมดพร้อมกัน\",\n  \"upstream_timeout\": \"ระยะเวลาหมดอายุของต้นทาง\",\n  \"upstream_timeout_desc\": \"ระบุจำนวนวินาทีที่ต้องรอการตอบกลับจากเซิร์ฟเวอร์ต้นทาง\",\n  \"upstreams\": \"ต้นทาง\",\n  \"use_adguard_browsing_sec\": \"ใช้บริการเว็บการรักษาความปลอดภัยการเรียกดู AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home จะตรวจสอบว่าโดเมนอยู่ในรายการที่ไม่อนุญาตโดยเว็บเซอร์วิสความปลอดภัยการสืบค้นหรือไม่ จะใช้ API การค้นหาที่เป็นมิตรกับข้อมูลส่วนบุคคลเพื่อทำการตรวจสอบ: มีการส่งคำนำหน้าสั้น ๆ ของชื่อโดเมน SHA256 แฮชไปยังเซิร์ฟเวอร์\",\n  \"use_adguard_parental\": \"ใช้บริการเว็บการควบคุมโดยผู้ปกครองของ AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home จะตรวจสอบว่าโดเมนมีเนื้อหาสำหรับผู้ใหญ่หรือไม่ มันใช้ API ความเป็นส่วนตัวเช่นเดียวกับบริการเว็บการรักษาความปลอดภัยการท่องเว็บ\",\n  \"use_private_ptr_resolvers_desc\": \"แก้ไขคำขอ PTR, SOA และ NS สำหรับโดเมน ARPA ที่มีที่อยู่ IP ส่วนตัว ผ่านเซิร์ฟเวอร์ต้นทางส่วนตัว, DHCP, /etc/hosts เป็นต้น หากปิดการใช้งาน AdGuard Home จะตอบสนองต่อคำขอทั้งหมดดังกล่าวด้วย NXDOMAIN\",\n  \"use_private_ptr_resolvers_title\": \"ใช้ DNS resolver ส่วนตัวสำหรับ DNS ตรงข้าม\",\n  \"use_saved_key\": \"ใช้คีย์ที่บันทึกไว้ก่อนหน้านี้\",\n  \"username_label\": \"ชื่อผู้ใช้\",\n  \"username_placeholder\": \"ป้อนชื่อผู้ใช้\",\n  \"validated_with_dnssec\": \"ตรวจสอบกับ DNSSEC\",\n  \"version\": \"รุ่น\",\n  \"version_request_error\": \"การตรวจสอบการอัปเดตล้มเหลว โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ\",\n  \"wednesday\": \"พุธ\",\n  \"wednesday_short\": \"พุธ\",\n  \"whois\": \"Whois\"\n}\n"
  },
  {
    "path": "client/src/__locales/tr.json",
    "content": "{\n  \"access_allowed_desc\": \"CIDR'lerin, IP adreslerinin veya <a>ClientIDs</a> listesi. Bu listede girişler varsa, AdGuard Home yalnızca bu istemcilerden gelen istekleri kabul eder.\",\n  \"access_allowed_title\": \"İzin verilen istemciler\",\n  \"access_blocked_desc\": \"Bu işlem filtrelerle ilgili değildir. AdGuard Home, bu alan adlarından gelen DNS sorgularını yanıtsız bırakır ve bu sorgular sorgu günlüğünde görünmez. Tam alan adlarını, joker karakterleri veya URL filtre kurallarını belirtebilirsiniz, örn. \\\"example.org\\\", \\\"*.example.org\\\" veya \\\"||example.org^\\\".\",\n  \"access_blocked_title\": \"İzin verilmeyen alan adları\",\n  \"access_desc\": \"AdGuard Home DNS sunucusu için erişim kuralları buradan yapılandırılabilir\",\n  \"access_disallowed_desc\": \"CIDR'lerin, IP adreslerinin veya <a>ClientIDs</a> listesi. Bu listede girişler varsa, AdGuard Home bu istemcilerden gelen istekleri kabul etmez. İzin verilen istemcilerde girişler varsa, bu alan yok sayılır.\",\n  \"access_disallowed_title\": \"İzin verilmeyen istemciler\",\n  \"access_settings_saved\": \"Erişim ayarları başarıyla kaydedildi!\",\n  \"access_title\": \"Erişim ayarları\",\n  \"actions_table_header\": \"Eylemler\",\n  \"add_allowlist\": \"İzin listesi ekle\",\n  \"add_blocklist\": \"Engel listesi ekle\",\n  \"add_custom_list\": \"Özel liste ekle\",\n  \"add_persistent_client\": \"Kalıcı istemci olarak ekle\",\n  \"address\": \"Adres\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home, bu istemciden gelen tüm DNS sorgularını yok sayar.\",\n  \"all_lists_up_to_date_toast\": \"Tüm listeler güncel\",\n  \"all_queries\": \"Tüm sorgular\",\n  \"allow_this_client\": \"Bu istemciye izin ver\",\n  \"allowed\": \"İzin verilen\",\n  \"anonymize_client_ip\": \"İstemcinin IP adresini gizle\",\n  \"anonymize_client_ip_desc\": \"İstemcinin tam IP adresini günlüklere veya istatistiklere kaydetmez\",\n  \"anonymizer_notification\": \"<0>Not:</0> IP gizleme etkinleştirildi. Bunu <1>Genel ayarlardan</1> devre dışı bırakabilirsiniz.\",\n  \"answer\": \"Yanıt\",\n  \"apply_btn\": \"Uygula\",\n  \"auto_clients_desc\": \"AdGuard Home'u kullanan veya kullanabilecek cihazların IP adresleri hakkında bilgiler. Bu bilgiler, ana bilgisayar dosyaları, ters DNS sorguları ve çeşitli diğer kaynaklardan toplanmaktadır.\",\n  \"auto_clients_title\": \"Çalışma zamanı istemcileri\",\n  \"autofix_warning_list\": \"Bu görevleri gerçekleştirir: <0>Sistem DNSStubListener'ı devre dışı bırakın</0> <0>DNS sunucusu adresini 127.0.0.1 olarak ayarlayın</0> <0>/etc/resolv.conf'un sembolik bağlantı hedefini /run/systemd/resolve/resolv.conf ile değiştirin<0> <0>DNSStubListener'ı durdurun (systemd çözümlenmiş hizmeti yeniden yükleyin)</0>\",\n  \"autofix_warning_result\": \"Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuard Home tarafından işlenecektir.\",\n  \"autofix_warning_text\": \"\\\"Düzelt\\\" seçeneğine tıklarsanız, AdGuard Home, sisteminizi AdGuard Home DNS sunucusunu kullanacak şekilde yapılandırır.\",\n  \"average_processing_time\": \"Ortalama işlem süresi\",\n  \"average_processing_time_hint\": \"Bir DNS isteğinin milisaniye cinsinden ortalama işlem süresi\",\n  \"average_upstream_response_time\": \"Ortalama üst kaynak yanıt süresi\",\n  \"back\": \"Geri\",\n  \"block\": \"Engelle\",\n  \"block_all\": \"Tümünü engelle\",\n  \"block_domain_use_filters_and_hosts\": \"Filtre ve ana bilgisayar dosyalarını kullanarak alan adlarını engelle\",\n  \"block_for_this_client_only\": \"Yalnızca bu istemci için engelle\",\n  \"block_services\": \"Belirli hizmetleri engelle\",\n  \"blocked_adult_websites\": \"Ebeveyn Denetimi tarafından engellendi\",\n  \"blocked_by\": \"<0>Filtreler tarafından engellenen</0>\",\n  \"blocked_by_cname_or_ip\": \"CNAME veya IP tarafından engellendi\",\n  \"blocked_by_response\": \"Yanıt olarak CNAME veya IP tarafından engellendi\",\n  \"blocked_response_ttl\": \"Engellenen yanıtın geçerlilik süresi\",\n  \"blocked_response_ttl_desc\": \"İstemcilerin filtrelenmiş bir yanıtı kaç saniye boyunca önbellekte tutması gerektiğini belirtir\",\n  \"blocked_safebrowsing\": \"Güvenli Gezinti tarafından engellendi\",\n  \"blocked_service\": \"Hizmet engellendi\",\n  \"blocked_services\": \"Engellenen hizmetler\",\n  \"blocked_services_desc\": \"Popüler siteleri ve hizmetleri hızlı bir şekilde engellemenizi sağlar.\",\n  \"blocked_services_global\": \"Genel olarak engellenen hizmetleri kullan\",\n  \"blocked_services_saved\": \"Engellenen hizmetler başarıyla kaydedildi\",\n  \"blocked_threats\": \"Tehdit engellendi\",\n  \"blocking_ipv4\": \"IPv4 engelleme\",\n  \"blocking_ipv4_desc\": \"Engellenen bir A isteği için geri döndürülecek IP adresi\",\n  \"blocking_ipv6\": \"IPv6 engelleme\",\n  \"blocking_ipv6_desc\": \"Engellenen bir AAAA isteği için geri döndürülecek IP adresi\",\n  \"blocking_mode\": \"Engelleme modu\",\n  \"blocking_mode_custom_ip\": \"Özel IP: Elle ayarlanmış IP adresiyle yanıt verin\",\n  \"blocking_mode_default\": \"Varsayılan: Reklam engelleyici tarzı kural tarafından engellendiğinde sıfır IP adresiyle (A için 0.0.0.0; AAAA için ::) yanıt verir; /etc/hosts tarzı kural tarafından engellendiğinde, kuralda belirtilen IP adresiyle yanıt verir\",\n  \"blocking_mode_null_ip\": \"Boş IP: Sıfır IP adresiyle yanıt verin (A için 0.0.0.0; AAAA için ::)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: NXDOMAIN koduyla yanıt verin\",\n  \"blocking_mode_refused\": \"REFUSED: REFUSED koduyla yanıt verin\",\n  \"blocklist\": \"Engel listesi\",\n  \"bootstrap_dns\": \"Ön yükleme DNS sunucuları\",\n  \"bootstrap_dns_desc\": \"Üst kaynak olarak belirttiğiniz DoH/DoT çözümleyicilerin IP adreslerini çözümlemek için kullanılan DNS sunucularının IP adresleri. Yorumlara izin verilmez.\",\n  \"cache_cleared\": \"DNS önbelleği başarıyla temizlendi\",\n  \"cache_enabled\": \"Önbelleği etkinleştir\",\n  \"cache_enabled_desc\": \"DNS yanıtlarını yerel olarak depolayın.\",\n  \"cache_optimistic\": \"İyimser önbelleğe alma\",\n  \"cache_optimistic_desc\": \"AdGuard Home, yanıtların süresi dolduğunda bile önbellekten yanıt vermesini sağlar ve bu yanıtları yenilemeyi dener.\",\n  \"cache_size\": \"Önbellek boyutu\",\n  \"cache_size_desc\": \"DNS önbellek boyutu (bayt cinsinden).\",\n  \"cache_size_validation\": \"Etkinleştirildiğinde önbellek boyutu sıfırdan büyük olmalıdır.\",\n  \"cache_ttl_max_override\": \"En fazla kullanım süresini geçersiz kıl\",\n  \"cache_ttl_max_override_desc\": \"DNS önbelleğindeki girişler için en fazla kullanım süresi değerini saniye türünden belirler.\",\n  \"cache_ttl_min_override\": \"En az kullanım süresini geçersiz kıl\",\n  \"cache_ttl_min_override_desc\": \"DNS yanıtlarını önbelleğe alırken üst sunucudan alınan kullanım süresi değerini saniye türünden uzatır.\",\n  \"cancel_btn\": \"İptal\",\n  \"category_label\": \"Kategori\",\n  \"check\": \"Denetle\",\n  \"check_client_id\": \"İstemci tanımlayıcısı (ClientID veya IP adresi)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Ana makine adının filtreleme durumunu denetler.\",\n  \"check_dhcp_servers\": \"DHCP sunucularını denetle\",\n  \"check_dns_record\": \"DNS kayıt türünü seçin\",\n  \"check_enter_client_id\": \"İstemci tanımlayıcısı girin\",\n  \"check_hostname\": \"Ana makine adı veya alan adı\",\n  \"check_ip\": \"IP adresleri: {{ip}}\",\n  \"check_not_found\": \"Filtre listelerinizde bulunamadı\",\n  \"check_reason\": \"Sebep: {{reason}}\",\n  \"check_service\": \"Hizmet adı: {{service}}\",\n  \"check_title\": \"Filtrelemeyi denetleyin\",\n  \"check_updates_btn\": \"Güncellemeleri denetle\",\n  \"check_updates_now\": \"Güncellemeleri şimdi denetle\",\n  \"choose_allowlist\": \"İzin listelerini seçin\",\n  \"choose_blocklist\": \"Engel listelerini seçin\",\n  \"choose_from_list\": \"Listeden seç\",\n  \"city\": \"Şehir\",\n  \"clear_cache\": \"Önbelleği temizle\",\n  \"click_to_view_queries\": \"Sorguları görmek için tıklayın\",\n  \"client_add\": \"İstemci Ekle\",\n  \"client_added\": \"\\\"{{key}}\\\" istemcisi başarıyla eklendi\",\n  \"client_blocked\": \"\\\"{{ip}}\\\" istemcisi başarıyla engellendi\",\n  \"client_confirm_block\": \"\\\"{{ip}}\\\" istemcisini engellemek istediğinizden emin misiniz?\",\n  \"client_confirm_delete\": \"\\\"{{key}}\\\" istemcisini silmek istediğinizden emin misiniz?\",\n  \"client_confirm_unblock\": \"\\\"{{ip}}\\\" istemcisinin engellemesini kaldırmak istediğinizden emin misiniz?\",\n  \"client_deleted\": \"\\\"{{key}}\\\" istemcisi başarıyla silindi\",\n  \"client_details\": \"İstemci ayrıntıları\",\n  \"client_edit\": \"İstemciyi Düzenle\",\n  \"client_global_settings\": \"Genel ayarları kullan\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"İstemciler, ClientID ile tanımlanabilir. İstemcileri nasıl tanımlayacağınız hakkında daha fazla bilgiye <a>buradan</a> ulaşabilirsiniz.\",\n  \"client_id_placeholder\": \"ClientID girin\",\n  \"client_identifier\": \"Tanımlayıcı\",\n  \"client_identifier_desc\": \"İstemciler, IP adresi, CIDR, MAC adresi veya ClientID (DoT/DoH/DoQ için kullanılabilir) ile tanımlanabilir. İstemcileri nasıl tanımlayacağınız hakkında daha fazla bilgiye <0>buradan</0> ulaşabilirsiniz.\",\n  \"client_name\": \"İstemci {{id}}\",\n  \"client_new\": \"Yeni İstemci\",\n  \"client_settings\": \"İstemci ayarları\",\n  \"client_table_header\": \"İstemci\",\n  \"client_unblocked\": \"\\\"{{ip}}\\\" istemcisinin engeli başarıyla kaldırıldı\",\n  \"client_updated\": \"\\\"{{key}}\\\" istemcisi başarıyla güncellendi\",\n  \"clients_desc\": \"AdGuard Home'a bağlı cihazlar için kalıcı istemci kayıtlarını yapılandırır\",\n  \"clients_not_found\": \"İstemci bulunamadı\",\n  \"clients_title\": \"Kalıcı istemciler\",\n  \"compact\": \"Sık\",\n  \"config_successfully_saved\": \"Yapılandırma başarıyla kaydedildi\",\n  \"configure\": \"Yapılandır\",\n  \"confirm_dns_cache_clear\": \"DNS önbelleğini temizlemek istediğinizden emin misiniz?\",\n  \"confirm_static_ip\": \"AdGuard Home, {{ip}} adresini sabit IP adresiniz olacak şekilde yapılandırır. Devam etmek istiyor musunuz?\",\n  \"copyright\": \"Telif Hakkı\",\n  \"country\": \"Ülke\",\n  \"custom_filter_rules\": \"Özel filtreleme kuralları\",\n  \"custom_filter_rules_hint\": \"Her satıra bir kural girin. Reklam engelleme kuralı veya hosts dosyası söz dizimi kullanabilirsiniz.\",\n  \"custom_filtering_rules\": \"Özel filtreleme kuralları\",\n  \"custom_ip\": \"Özel IP\",\n  \"custom_retention_input\": \"Saklama süresini saat olarak girin\",\n  \"custom_rotation_input\": \"Döngüyü saat cinsinden girin\",\n  \"dashboard\": \"Pano\",\n  \"date\": \"Tarih\",\n  \"default\": \"Varsayılan\",\n  \"delete_confirm\": \"\\\"{{key}}\\\" öğesini silmek istediğinizden emin misiniz?\",\n  \"delete_table_action\": \"Sil\",\n  \"descr\": \"Açıklama\",\n  \"details\": \"Ayrıntılar\",\n  \"dhcp_add_static_lease\": \"Sabit kiralama ekle\",\n  \"dhcp_config_saved\": \"DHCP yapılandırması başarıyla kaydedildi\",\n  \"dhcp_description\": \"Yönlendiriciniz DHCP ayarlarını sağlamıyorsa, AdGuard'ın yerleşik DHCP sunucusunu kullanabilirsiniz.\",\n  \"dhcp_disable\": \"DHCP sunucusunu devre dışı bırak\",\n  \"dhcp_dynamic_ip_found\": \"Sisteminiz, <0>{{interfaceName}}</0> arayüzü için değişebilen IP adresi yapılandırması kullanıyor. DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. Geçerli olan IP adresiniz <0>{{ipAddress}}</0>. \\\"DHCP sunucusunu etkinleştir\\\" düğmesine basarsanız, AdGuard Home bu IP adresini otomatik bir şekilde sabit olarak ayarlar.\",\n  \"dhcp_edit_static_lease\": \"Statik kiralamayı düzenle\",\n  \"dhcp_enable\": \"DHCP sunucusunu etkinleştir\",\n  \"dhcp_error\": \"AdGuard Home, ağda başka bir etkin DHCP sunucusu olup olmadığını belirleyemedi\",\n  \"dhcp_form_gateway_input\": \"Ağ geçidi IP\",\n  \"dhcp_form_lease_input\": \"Kira süresi\",\n  \"dhcp_form_lease_title\": \"DHCP kiralama süresi (saniye cinsinden)\",\n  \"dhcp_form_range_end\": \"Bitiş aralığı\",\n  \"dhcp_form_range_start\": \"Başlangıç aralığı\",\n  \"dhcp_form_range_title\": \"IP adresi aralığı\",\n  \"dhcp_form_subnet_input\": \"Alt ağ maskesi\",\n  \"dhcp_found\": \"Ağ üzerinde aktif bir DHCP sunucusu bulundu. Yerleşik DHCP sunucusunu etkinleştirmek güvenli olmayacaktır.\",\n  \"dhcp_hardware_address\": \"Donanım adresi\",\n  \"dhcp_interface_select\": \"DHCP arayüzünü seç\",\n  \"dhcp_ip_addresses\": \"IP adresleri\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 Ayarları\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 Ayarları\",\n  \"dhcp_lease_added\": \"Sabit kiralama \\\"{{key}}\\\" başarıyla eklendi\",\n  \"dhcp_lease_deleted\": \"Sabit kiralama \\\"{{key}}\\\" başarıyla silindi\",\n  \"dhcp_lease_updated\": \"Statik kiralama \\\"{{key}}\\\" başarıyla güncellendi\",\n  \"dhcp_leases\": \"DHCP kiralamaları\",\n  \"dhcp_leases_not_found\": \"DHCP kiralaması bulunamadı\",\n  \"dhcp_new_static_lease\": \"Yeni sabit kiralama\",\n  \"dhcp_not_found\": \"AdGuard Home, ağda herhangi bir aktif DHCP sunucusu bulamadığı için yerleşik DHCP sunucusunu etkinleştirmek güvenlidir. Ancak, otomatik ayarlama şu anda %100 garanti sağlamadığından bunu elle yeniden kontrol etmelisiniz.\",\n  \"dhcp_reset\": \"DHCP yapılandırmasını sıfırlamak istediğinizden emin misiniz?\",\n  \"dhcp_reset_leases\": \"Tüm kiralamaları sıfırla\",\n  \"dhcp_reset_leases_confirm\": \"Tüm kiralamaları sıfırlamak istediğinizden emin misiniz?\",\n  \"dhcp_reset_leases_success\": \"DHCP kiralamaları başarıyla sıfırlandı\",\n  \"dhcp_settings\": \"DHCP ayarları\",\n  \"dhcp_static_ip_error\": \"DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. AdGuard Home, bu ağ arayüzünün sabit bir IP adresi kullanılarak yapılandırılıp yapılandırılmadığını belirleyemedi. Lütfen sabit IP adresini elle ayarlayın.\",\n  \"dhcp_static_leases\": \"Sabit DHCP kiralamaları\",\n  \"dhcp_static_leases_not_found\": \"Sabit DHCP kiralaması bulunamadı\",\n  \"dhcp_table_expires\": \"Bitiş tarihi\",\n  \"dhcp_table_hostname\": \"Ana makine Adı\",\n  \"dhcp_title\": \"DHCP sunucusu (deneysel!)\",\n  \"dhcp_warning\": \"DHCP sunucusunu yine de etkinleştirmek istiyorsanız, ağınızda başka bir aktif DHCP sunucusu olmadığından emin olun, aksi takdirde ağa bağlı cihazların internet bağlantısı kesilebilir!\",\n  \"disable_for_hours\": \"{{count}} saat için\",\n  \"disable_for_hours_plural\": \"{{count}} saat için\",\n  \"disable_for_minutes\": \"{{count}} dakika için\",\n  \"disable_for_minutes_plural\": \"{{count}} dakika için\",\n  \"disable_for_seconds\": \"{{count}} saniye için\",\n  \"disable_for_seconds_plural\": \"{{count}} saniye için\",\n  \"disable_ipv6\": \"IPv6 adreslerinin çözümlenmesini devre dışı bırak\",\n  \"disable_ipv6_desc\": \"IPv6 adresleri için tüm DNS sorgularını yanıtsız bırakır (AAAA yazar) ve HTTPS yanıtlarından IPv6 ipuçlarını kaldırır.\",\n  \"disable_notify_for_hours\": \"Korumayı {{count}} saatliğine devre dışı bırak\",\n  \"disable_notify_for_hours_plural\": \"Korumayı {{count}} saatliğine devre dışı bırak\",\n  \"disable_notify_for_minutes\": \"Korumayı {{count}} dakikalığına devre dışı bırak\",\n  \"disable_notify_for_minutes_plural\": \"Korumayı {{count}} dakikalığına devre dışı bırak\",\n  \"disable_notify_for_seconds\": \"Korumayı {{count}} saniyeliğine devre dışı bırak\",\n  \"disable_notify_for_seconds_plural\": \"Korumayı {{count}} saniyeliğine devre dışı bırak\",\n  \"disable_notify_until_tomorrow\": \"Korumayı yarına kadar devre dışı bırak\",\n  \"disable_protection\": \"Korumayı devre dışı bırak\",\n  \"disable_rewrites\": \"Yeniden yazma kurallarını devre dışı bırak\",\n  \"disable_until_tomorrow\": \"Yarına kadar\",\n  \"disabled\": \"Devre dışı\",\n  \"disabled_dhcp\": \"DHCP sunucusu devre dışı bırakıldı\",\n  \"disabled_filtering_toast\": \"Filtreleme devre dışı\",\n  \"disabled_parental_toast\": \"Ebeveyn Denetimi devre dışı bırakıldı\",\n  \"disabled_protection\": \"Koruma devre dışı bırakıldı\",\n  \"disabled_safe_browsing_toast\": \"Güvenli Gezinti devre dışı bırakıldı\",\n  \"disabled_safe_search_toast\": \"Güvenli Arama devre dışı bırakıldı\",\n  \"disallow_this_client\": \"Bu istemciye izin verme\",\n  \"dns_addresses\": \"DNS adresleri\",\n  \"dns_allowlists\": \"DNS izin listeleri\",\n  \"dns_allowlists_desc\": \"DNS izin listesindeki alan adlarına, engel listesinde olsa bile izin verilecektir.\",\n  \"dns_blocklists\": \"DNS engel listeleri\",\n  \"dns_blocklists_desc\": \"AdGuard Home, engel listeleriyle eşleşen alan adlarını engeller.\",\n  \"dns_cache_config\": \"DNS önbellek yapılandırması\",\n  \"dns_cache_config_desc\": \"Burada DNS önbelleğini yapılandırabilirsiniz\",\n  \"dns_cache_size\": \"DNS önbellek boyutu, bayt cinsinden\",\n  \"dns_config\": \"DNS sunucu yapılandırması\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS Gizliliği\",\n  \"dns_providers\": \"Aralarından seçim yapabileceğiniz, bilinen <0>DNS sağlayıcıların listesi</0>.\",\n  \"dns_query\": \"DNS Sorguları\",\n  \"dns_rewrites\": \"DNS yeniden yazımları\",\n  \"dns_settings\": \"DNS ayarları\",\n  \"dns_start\": \"DNS sunucusu başlatılıyor\",\n  \"dns_status_error\": \"DNS sunucusunun durumu denetlenirken hata oluştu\",\n  \"dns_test_not_ok_toast\": \"Sunucu \\\"{{key}}\\\": kullanılamıyor, lütfen doğru yazdığınızdan emin olun\",\n  \"dns_test_ok_toast\": \"Belirtilen DNS sunucuları düzgün çalışıyor\",\n  \"dns_test_parsing_error_toast\": \"{{section}} bölümü: {{line}}. satır: kullanılamadı, lütfen doğru yazdığınızı kontrol edin\",\n  \"dns_test_warning_toast\": \"Üst kaynak \\\"{{key}}\\\", test isteklerine yanıt vermiyor ve düzgün çalışmayabilir\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"DNSSEC'i etkinleştir\",\n  \"dnssec_enable_desc\": \"Giden DNS sorguları için DNSSEC işaretini etkinleştirir ve sonucu denetler (DNSSEC özellikli çözümleyici gereklidir).\",\n  \"domain\": \"Alan adı\",\n  \"domain_desc\": \"Yeniden yazılmasını istediğiniz alan adını veya joker karakteri girin.\",\n  \"domain_name_table_header\": \"Alan adı\",\n  \"domain_or_client\": \"Alan adı veya istemci\",\n  \"down\": \"Kapalı\",\n  \"download_mobileconfig\": \"Yapılandırma dosyasını indir\",\n  \"download_mobileconfig_doh\": \"DNS-over-HTTPS için .mobileconfig dosyasını indir\",\n  \"download_mobileconfig_dot\": \"DNS-over-TLS için .mobileconfig dosyasını indir\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"İzin listesini düzenle\",\n  \"edit_blocklist\": \"Engel listesini düzenle\",\n  \"edit_table_action\": \"Düzenle\",\n  \"edns_cs_desc\": \"Üst sunucu isteklerine ECS (EDNS İstemci Alt Ağı) seçeneğini ekler ve istemciler tarafından gönderilen değerleri sorgu günlüğünde kaydeder.\",\n  \"edns_enable\": \"EDNS istemci alt ağını etkinleştir\",\n  \"edns_use_custom_ip\": \"EDNS için özel IP kullan\",\n  \"edns_use_custom_ip_desc\": \"EDNS için özel IP kullanımına izin ver\",\n  \"elapsed\": \"Geçen süre\",\n  \"empty_response_status\": \"Boş\",\n  \"enable_protection\": \"Korumayı etkinleştir\",\n  \"enable_protection_timer\": \"Koruma {{time}} içinde etkinleştirilecektir\",\n  \"enable_rewrites\": \"Yeniden yazma kurallarını etkinleştir\",\n  \"enable_upstream_dns_cache\": \"Bu istemcinin özel üst kaynak yapılandırması için DNS önbelleğini etkinleştir\",\n  \"enabled_dhcp\": \"DHCP sunucusu etkinleştirildi\",\n  \"enabled_filtering_toast\": \"Filtreleme etkin\",\n  \"enabled_parental_toast\": \"Ebeveyn Denetimi etkinleştirildi\",\n  \"enabled_protection\": \"Koruma etkinleştirildi\",\n  \"enabled_safe_browsing_toast\": \"Güvenli Gezinti etkinleştirildi\",\n  \"enabled_save_search_toast\": \"Güvenli Arama etkinleştirildi\",\n  \"enabled_table_header\": \"Etkin\",\n  \"encryption_certificate_path\": \"Sertifika dosya yolu\",\n  \"encryption_certificates\": \"Sertifikalar\",\n  \"encryption_certificates_desc\": \"Şifrelemeyi kullanmak için alan adınıza geçerli bir SSL sertifika zinciri sağlamanız gerekir. <0>{{link}}</0> adresinden ücretsiz bir sertifika alabilir veya güvenilir Sertifika Yetkililerinden satın alabilirsiniz.\",\n  \"encryption_certificates_input\": \"PEM biçimindeki sertifikalarınızı kopyalayıp buraya yapıştırın.\",\n  \"encryption_certificates_source_content\": \"Sertifika içeriğini yapıştır\",\n  \"encryption_certificates_source_path\": \"Bir sertifika dosyası yolu ayarlayın\",\n  \"encryption_chain_invalid\": \"Sertifika zinciri geçersiz\",\n  \"encryption_chain_valid\": \"Sertifika zinciri geçerli\",\n  \"encryption_config_saved\": \"Şifreleme yapılandırması kaydedildi\",\n  \"encryption_desc\": \"DNS ve yönetici web arayüzü için şifreleme (HTTPS/TLS) desteği\",\n  \"encryption_doq\": \"DNS-over-QUIC bağlantı noktası\",\n  \"encryption_doq_desc\": \"Bu bağlantı noktası yapılandırılırsa, AdGuard Home, DNS-over-QUIC sunucusunu bu bağlantı noktası üzerinden çalıştırır.\",\n  \"encryption_dot\": \"DNS-over-TLS bağlantı noktası\",\n  \"encryption_dot_desc\": \"Bu bağlantı noktası yapılandırılırsa, AdGuard Home, DNS-over-TLS sunucusunu bu bağlantı noktası üzerinden çalıştırır.\",\n  \"encryption_enable\": \"Şifrelemeyi etkinleştir (HTTPS, DNS-over-HTTPS ve DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Şifrelemeyi etkinleştirirseniz, AdGuard Home yönetici arayüzü HTTPS üzerinden çalışır ve DNS sunucusu, DNS-over-HTTPS ve DNS-over-TLS üzerinden gelen istekleri dinler.\",\n  \"encryption_expire\": \"Bitiş tarihi\",\n  \"encryption_hostnames\": \"Ana makine adları\",\n  \"encryption_https\": \"HTTPS bağlantı noktası\",\n  \"encryption_https_desc\": \"HTTPS bağlantı noktası yapılandırılırsa, AdGuard Home yönetici arayüzüne HTTPS aracılığıyla erişilebilir olacak ve ayrıca '/dns-query' üzerinden DNS-over-HTTPS bağlantısı sağlar.\",\n  \"encryption_issuer\": \"Sağlayan\",\n  \"encryption_key\": \"Özel anahtar\",\n  \"encryption_key_input\": \"Sertifikanızın PEM biçimli özel anahtarını kopyalayıp buraya yapıştırın.\",\n  \"encryption_key_invalid\": \"Bu geçersiz bir {{type}} özel anahtarıdır\",\n  \"encryption_key_source_content\": \"Özel anahtar içeriğini yapıştır\",\n  \"encryption_key_source_path\": \"Özel bir anahtar dosyası yolu belirle\",\n  \"encryption_key_valid\": \"Bu geçerli bir {{type}} özel anahtarıdır\",\n  \"encryption_plain_dns_desc\": \"Düz DNS varsayılan olarak etkindir. Tüm cihazları şifrelenmiş DNS kullanmaya zorlamak için bunu devre dışı bırakabilirsiniz. Bunu yapmak için en az bir şifrelenmiş DNS protokolünü etkinleştirmeniz gerekir\",\n  \"encryption_plain_dns_enable\": \"Düz DNS'i etkinleştir\",\n  \"encryption_plain_dns_error\": \"Düz DNS'i devre dışı bırakmak için en az bir şifrelenmiş DNS protokolünü etkinleştirin\",\n  \"encryption_private_key_path\": \"Özel anahtar dosya yolu\",\n  \"encryption_redirect\": \"HTTPS'e otomatik olarak yönlendir\",\n  \"encryption_redirect_desc\": \"İşaretlenirse, AdGuard Home sizi otomatik olarak HTTP adresinden HTTPS adreslerine yönlendirir.\",\n  \"encryption_reset\": \"Şifreleme ayarlarını sıfırlamak istediğinizden emin misiniz?\",\n  \"encryption_server\": \"Sunucu adı\",\n  \"encryption_server_desc\": \"Ayarlanırsa, AdGuard Home ClientID'leri tespit eder, DDR sorgularına yanıt verir ve ek bağlantı doğrulamaları gerçekleştirir. Ayarlanmazsa, bu özellikler devre dışı bırakılır. Sertifikadaki DNS Adlarından biriyle eşleşmelidir.\",\n  \"encryption_server_enter\": \"Alan adınızı girin\",\n  \"encryption_settings\": \"Şifreleme ayarları\",\n  \"encryption_status\": \"Durum\",\n  \"encryption_subject\": \"Konu\",\n  \"encryption_title\": \"Şifreleme\",\n  \"encryption_warning\": \"Uyarı\",\n  \"enforce_safe_search\": \"Güvenli aramayı kullan\",\n  \"enforce_save_search_hint\": \"AdGuard Home, şu arama motorlarında güvenli aramayı uygular: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Uygulanan güvenli arama\",\n  \"enter_cache_size\": \"Önbellek boyutunu bayt türünden girin\",\n  \"enter_cache_ttl_max_override\": \"En fazla kullanım süresini saniye olarak girin\",\n  \"enter_cache_ttl_min_override\": \"En az kullanım süresini saniye olarak girin\",\n  \"enter_name_hint\": \"Ad girin\",\n  \"enter_url_or_path_hint\": \"Listenin URL'sini veya dosya yolunu girin\",\n  \"enter_valid_allowlist\": \"İzin listesine geçerli bir URL girin.\",\n  \"enter_valid_blocklist\": \"Engel listesine geçerli bir URL girin.\",\n  \"error_details\": \"Hata ayrıntıları\",\n  \"example_comment\": \"! Buraya bir yorum gelir.\",\n  \"example_comment_hash\": \"# Ayrıca bir yorum.\",\n  \"example_comment_meaning\": \"sadece bir yorum;\",\n  \"example_meaning_filter_block\": \"example.org'a ve tüm alt alanlarına erişimi engeller;\",\n  \"example_meaning_filter_whitelist\": \"example.org'a ve tüm alt alanlarına erişimin engelini kaldırır;\",\n  \"example_meaning_host_block\": \"example.org için 127.0.0.1 ile yanıt verin (ancak alt alanları için değil);\",\n  \"example_multiple_upstreams_reserved\": \"<0>belirli alan adları için</0> birden fazla üst kaynak;\",\n  \"example_regex_meaning\": \"belirtilen düzenli ifadelerle eşleşen alan adlarına erişimi engelle.\",\n  \"example_rewrite_domain\": \"yanıtları yalnızca bu alan adı için yeniden yazar.\",\n  \"example_rewrite_wildcard\": \"tüm <0>example.org</0> yanıtları alt alan adları için yeniden yazar.\",\n  \"example_upstream_comment\": \"bir yorum.\",\n  \"example_upstream_doh\": \"şifrelenmiş <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"<0>HTTP/3</0> uygulanmış ve HTTP/2 veya aşağısı için yedek olmayan şifrelenmiş DNS-over-HTTPS;\",\n  \"example_upstream_doq\": \"şifrelenmiş <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"şifrelenmiş <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"normal DNS (UDP üzerinden);\",\n  \"example_upstream_regular_port\": \"normal DNS (UDP üzerinden, bağlantı noktası ile);\",\n  \"example_upstream_reserved\": \"<0>belirli alan adları için</0> bir üst kaynak;\",\n  \"example_upstream_sdns\": \"<1>DNSCrypt</1> veya <2>DNS-over-HTTPS</2> çözümleyicileri için <0>DNS Damgaları</0>;\",\n  \"example_upstream_tcp\": \"normal DNS (TCP üzerinden);\",\n  \"example_upstream_tcp_hostname\": \"normal DNS (TCP üzerinden, ana makine adı);\",\n  \"example_upstream_tcp_port\": \"normal DNS (TCP üzerinden, bağlantı noktası ile);\",\n  \"example_upstream_udp\": \"normal DNS (UDP üzerinden, ana makine adı);\",\n  \"examples_title\": \"Örnekler\",\n  \"fallback_dns_desc\": \"Yukarı akış DNS sunucuları yanıt vermediğinde kullanılan yedek DNS sunucularının listesi. Söz dizimi yukarıdaki ana üst kaynak alanıyla aynıdır.\",\n  \"fallback_dns_placeholder\": \"Her satıra bir yedek DNS sunucusu girin\",\n  \"fallback_dns_title\": \"Yedek DNS sunucuları\",\n  \"faq\": \"SSS\",\n  \"fastest_addr\": \"En hızlı IP adresi\",\n  \"fastest_addr_desc\": \"<b>Tüm</b> DNS sunucularından yanıt bekler, her sunucu için TCP bağlantı hızını ölçer ve en hızlı bağlantı hızına sahip sunucunun IP adresini döndürür.<br/>Bu yapılandırma, bir veya daha fazla üst kaynak sunucusu yanıt vermediğinde, DNS sorgularını önemli ölçüde yavaşlatabilir. Üst kaynak sunucularınızın kararlı olduğundan ve üst kaynak zaman aşım sürenizin düşük olduğundan emin olun.\",\n  \"filter\": \"Filtre\",\n  \"filter_added_successfully\": \"Liste başarıyla eklendi\",\n  \"filter_allowlist\": \"UYARI: Bu işlem ayrıca \\\"{{disallowed_rule}}\\\" kuralını izin verilen istemciler listesinden hariç tutar.\",\n  \"filter_category_general\": \"Genel\",\n  \"filter_category_general_desc\": \"Çoğu cihazda izlemeyi ve reklamları engelleyen listeler\",\n  \"filter_category_other\": \"Diğer\",\n  \"filter_category_other_desc\": \"Diğer engel listeleri\",\n  \"filter_category_regional\": \"Bölgesel\",\n  \"filter_category_regional_desc\": \"Bölgesel reklamlara ve izleme sunucularına odaklanan listeler\",\n  \"filter_category_security\": \"Güvenlik\",\n  \"filter_category_security_desc\": \"Kötü amaçlı, kimlik avı ve dolandırıcılık alan adlarını engellemek için özel olarak tasarlanmış listeler\",\n  \"filter_removed_successfully\": \"Liste başarıyla kaldırıldı\",\n  \"filter_updated\": \"Liste başarıyla güncellendi\",\n  \"filtered\": \"Filtrelendi\",\n  \"filtered_custom_rules\": \"Özel filtreleme kuralları tarafından filtrelendi\",\n  \"filtering_rules_learn_more\": \"Kendi hosts listelerinizi oluşturma hakkında <0>daha fazla bilgi edinin</0>.\",\n  \"filters\": \"Filtreler\",\n  \"filters_and_hosts_hint\": \"AdGuard Home, temel reklam engelleme kurallarını ve hosts dosyalarının söz dizimini anlar.\",\n  \"filters_block_toggle_hint\": \"<a>Filtreler</a> ayarlarında engelleme kuralları oluşturabilirsiniz.\",\n  \"filters_configuration\": \"Filtre yapılandırması\",\n  \"filters_enable\": \"Filtreleri etkinleştir\",\n  \"filters_interval\": \"Filtre güncelleme sıklığı\",\n  \"fix\": \"Düzelt\",\n  \"for_last_days\": \"son {{count}} gün için\",\n  \"for_last_days_plural\": \"son {{count}} gün için\",\n  \"for_last_hours\": \"son {{count}} saat için\",\n  \"for_last_hours_plural\": \"son {{count}} saat için\",\n  \"forgot_password\": \"Parolanızı mı unuttunuz?\",\n  \"forgot_password_desc\": \"Kullanıcı hesabınız için yeni bir parola oluşturmak istiyorsanız lütfen <0>bu adımları</0> uygulayın.\",\n  \"form_add_id\": \"Tanımlayıcı ekle\",\n  \"form_answer\": \"IP adresi veya alan adı girin\",\n  \"form_client_name\": \"İstemci adını girin\",\n  \"form_domain\": \"Alan adı veya joker karakter girin\",\n  \"form_enter_blocked_response_ttl\": \"Engellenen yanıt kullanım süresini girin (saniye)\",\n  \"form_enter_host\": \"Ana makine adı girin\",\n  \"form_enter_hostname\": \"Ana makine adı girin\",\n  \"form_enter_id\": \"Tanımlayıcı girin\",\n  \"form_enter_ip\": \"IP girin\",\n  \"form_enter_mac\": \"MAC adresi girin\",\n  \"form_enter_rate_limit\": \"Sıklık limitini girin\",\n  \"form_enter_rate_limit_subnet_len\": \"Hız sınırlaması için alt ağ önek uzunluğunu girin\",\n  \"form_enter_subnet_ip\": \"\\\"{{cidr}}\\\" alt ağına bir IP adresi girin\",\n  \"form_enter_upstream_timeout\": \"Üst kaynak sunucusu zaman aşımı süresini saniye cinsinden girin\",\n  \"form_error_answer_format\": \"Geçersiz yanıt biçimi\",\n  \"form_error_client_id_format\": \"ClientID yalnızca sayılar, küçük harfler ve kısa çizgiler içermelidir\",\n  \"form_error_domain_format\": \"Geçersiz alan adı biçimi\",\n  \"form_error_equal\": \"Aynı olmamalıdır\",\n  \"form_error_gateway_ip\": \"Kiralama, ağ geçidinin IP adresiyle aynı olamaz\",\n  \"form_error_ip4_format\": \"IPv4 adresi geçersiz\",\n  \"form_error_ip4_gateway_format\": \"Ağ geçidi IPv4 adresi geçersiz\",\n  \"form_error_ip6_format\": \"IPv6 adresi geçersiz\",\n  \"form_error_ip_format\": \"IP adresi geçersiz\",\n  \"form_error_mac_format\": \"MAC adresi geçersiz\",\n  \"form_error_password\": \"Parolalar uyuşmuyor\",\n  \"form_error_password_length\": \"Parola {{min}} ila {{max}} karakter uzunluğunda olmalıdır\",\n  \"form_error_port\": \"Geçerli bir bağlantı noktası değeri girin\",\n  \"form_error_port_range\": \"80-65535 aralığında geçerli bir bağlantı noktası değeri girin\",\n  \"form_error_port_unsafe\": \"Güvenli olmayan bağlantı noktası\",\n  \"form_error_positive\": \"0'dan büyük olmalıdır\",\n  \"form_error_required\": \"Gerekli alan\",\n  \"form_error_server_name\": \"Sunucu adı geçersiz\",\n  \"form_error_subnet\": \"\\\"{{cidr}}\\\" alt ağı, \\\"{{ip}}\\\" IP adresini içermiyor\",\n  \"form_error_url_format\": \"URL biçimi geçersiz\",\n  \"form_error_url_or_path_format\": \"Listenin URL'si veya dosya konumu geçersiz\",\n  \"form_select_tags\": \"İstemci etiketlerini seçin\",\n  \"found_in_known_domain_db\": \"Bilinen alan adları veri tabanında bulundu.\",\n  \"friday\": \"Cuma\",\n  \"friday_short\": \"Cum\",\n  \"gateway_or_subnet_invalid\": \"Geçersiz alt ağ maskesi\",\n  \"general_settings\": \"Genel ayarlar\",\n  \"general_statistics\": \"Genel istatistikler\",\n  \"get_started\": \"Başla\",\n  \"greater_range_start_error\": \"Başlangıç aralığından daha büyük olmalıdır\",\n  \"homepage\": \"Ana Sayfa\",\n  \"host_whitelisted\": \"Ana makineye izin verildi\",\n  \"ignore_domains\": \"Yok sayılan alan adları (yeni satırla ayrılmış)\",\n  \"ignore_domains_desc_query\": \"Bu kurallarla eşleşen sorgular sorgu günlüğüne yazılmaz\",\n  \"ignore_domains_desc_stats\": \"Bu kurallarla eşleşen sorgular istatistiklere yazılmaz\",\n  \"ignore_domains_title\": \"Yok sayılan alan adları\",\n  \"ignore_query_log\": \"Sorgu günlüğünde bu istemciyi gösterme\",\n  \"ignore_statistics\": \"İstatistiklerde bu istemciyi gösterme\",\n  \"install_auth_confirm\": \"Parolayı onayla\",\n  \"install_auth_desc\": \"AdGuard Home yönetici web arayüzüne parola ile kimlik doğrulama yapılandırılmalıdır. AdGuard Home yalnızca yerel ağınızdan erişilebilir olsa bile, yine de yetkisiz erişime karşı korunması önemlidir.\",\n  \"install_auth_password\": \"Parola\",\n  \"install_auth_password_enter\": \"Parola girin\",\n  \"install_auth_title\": \"Kimlik Doğrulama\",\n  \"install_auth_username\": \"Kullanıcı adı\",\n  \"install_auth_username_enter\": \"Kullanıcı adı girin\",\n  \"install_devices_address\": \"AdGuard Home DNS sunucusu aşağıdaki adresleri dinliyor\",\n  \"install_devices_android_list_1\": \"Android Menüsü ana ekranından Ayarlar'a dokunun.\",\n  \"install_devices_android_list_2\": \"Menüde bulunan Wi-Fi öğesine dokunun. Mevcut tüm ağlar listelenecektir (mobil ağlar için özel DNS sunucusu ayarlanamaz).\",\n  \"install_devices_android_list_3\": \"Bağlı olduğunuz ağın üzerine basılı tutun ve Ağı Değiştir'e dokunun.\",\n  \"install_devices_android_list_4\": \"Bazı cihazlarda, diğer ayarları görmek için \\\"Gelişmiş\\\" seçeneğini seçmeniz gerekebilir. Android DNS ayarlarınızı yapmak için IP ayarlarını DHCP modundan Statik moda değiştirmeniz gerekir.\",\n  \"install_devices_android_list_5\": \"DNS 1 ve DNS 2 değerlerini AdGuard Home sunucunuzun adresleriyle değiştirin.\",\n  \"install_devices_desc\": \"AdGuard Home'u kullanmaya başlamak için, cihazlarınızı onu kullanacak şekilde yapılandırmanız gerekir.\",\n  \"install_devices_ios_list_1\": \"Ana ekrandan Ayarlar'a dokunun.\",\n  \"install_devices_ios_list_2\": \"Sol menüde bulunan Wi-Fi bölümüne girin (mobil ağlar için özel DNS sunucusu ayarlanamaz).\",\n  \"install_devices_ios_list_3\": \"O anda aktif olan ağın adına dokunun.\",\n  \"install_devices_ios_list_4\": \"DNS alanına AdGuard Home sunucunuzun adreslerini girin.\",\n  \"install_devices_macos_list_1\": \"Apple simgesine tıklayın ve Sistem Tercihleri öğesine gidin.\",\n  \"install_devices_macos_list_2\": \"Ağ öğesine tıklayın.\",\n  \"install_devices_macos_list_3\": \"Listedeki ilk bağlantıyı seçin ve Gelişmiş öğesine tıklayın.\",\n  \"install_devices_macos_list_4\": \"DNS sekmesini seçin ve AdGuard Home sunucunuzun adreslerini girin.\",\n  \"install_devices_router\": \"Yönlendirici\",\n  \"install_devices_router_desc\": \"Bu kurulum, ev yönlendiricinize bağlı tüm cihazları otomatik olarak kapsar ve her birini elle yapılandırmanıza gerek yoktur.\",\n  \"install_devices_router_list_1\": \"Yönlendiricinizin ayarlarına gidin. Genellikle, tarayıcınızdan http://192.168.0.1/ veya http://192.168.1.1/ gibi bir URL üzerinden erişebilirsiniz. Giriş yaparken bir parola girmeniz istenebilir. Parolanızı hatırlamıyorsanız, genellikle yönlendiricinin üzerindeki bir düğmeye basarak parolayı sıfırlayabilirsiniz, ancak bu işlemi seçerseniz yönlendiricinin tüm yapılandırmasını kaybedebileceğinizi unutmayın. Yönlendiricinizin kurulumu için bir uygulama gerekiyorsa, lütfen uygulamayı telefonunuza veya bilgisayarınıza yükleyin ve yönlendiricinin ayarlarına erişmek için bu uygulamayı kullanın.\",\n  \"install_devices_router_list_2\": \"DHCP/DNS ayarlarını bulun. DNS satırlarını arayın, genelde iki veya üç tanedir, üç rakam girilebilen dört ayrı grup içeren satırdır.\",\n  \"install_devices_router_list_3\": \"AdGuard Home sunucu adreslerinizi oraya girin.\",\n  \"install_devices_router_list_4\": \"Bazı yönlendirici türlerinde özel bir DNS sunucusu yapılandırılamaz. Bu durumda, AdGuard Home'u bir <0>DHCP sunucusu</0> olarak yapılandırmak yardımcı olabilir. Aksi takdirde, yönlendirici modelinizde DNS sunucularını nasıl özelleştireceğinizi öğrenmek için yönlendirici kılavuzunu kontrol etmelisiniz.\",\n  \"install_devices_title\": \"Cihazlarınızı yapılandırın\",\n  \"install_devices_windows_list_1\": \"Başlat menüsünden veya Windows araması aracılığıyla Denetim Masası'nı açın.\",\n  \"install_devices_windows_list_2\": \"Ağ ve İnternet kategorisine girin ve ardından Ağ ve Paylaşım Merkezi'ne girin.\",\n  \"install_devices_windows_list_3\": \"Panelin solunda \\\"Bağdaştırıcı ayarlarını değiştirin\\\" öğesine tıklayın.\",\n  \"install_devices_windows_list_4\": \"Kullandığınız aktif bağlantının üzerine sağ tıklayın ve Özellikler öğesine tıklayın.\",\n  \"install_devices_windows_list_5\": \"Listede \\\"İnternet Protokolü Sürüm 4 (TCP/IPv4)\\\" (veya IPv6 için \\\"İnternet Protokolü Sürüm 6 (TCP/IPv6)\\\") öğesini bulun, seçin ve ardından tekrar Özellikler öğesine tıklayın.\",\n  \"install_devices_windows_list_6\": \"\\\"Aşağıdaki DNS sunucu adreslerini kullan\\\" seçeneğini seçin ve ardından AdGuard Home sunucunuzun adreslerini girin.\",\n  \"install_saved\": \"Başarıyla kaydedildi\",\n  \"install_settings_all_interfaces\": \"Tüm arayüzler\",\n  \"install_settings_dns\": \"DNS sunucusu\",\n  \"install_settings_dns_desc\": \"Cihazlarınızı veya yönlendiricinizi aşağıdaki adreslerdeki DNS sunucusunu kullanacak şekilde yapılandırmanız gerekir:\",\n  \"install_settings_interface_link\": \"AdGuard Home yönetici web arayüzüne aşağıdaki adreslerden erişebilirsiniz:\",\n  \"install_settings_listen\": \"Dinleme arayüzü\",\n  \"install_settings_port\": \"Bağlantı noktası\",\n  \"install_settings_title\": \"Yönetici Web Arayüzü\",\n  \"install_static_configure\": \"AdGuard Home, <0>{{ip}}</0> sabit IP adresinin kullanıldığını tespit etti. Sabit adresiniz olarak ayarlanmasını istiyor musunuz?\",\n  \"install_static_error\": \"AdGuard Home, bu ağ arayüzü için otomatik olarak yapılandırılamıyor. Lütfen bunu elle nasıl yapacağınızla ilgili talimatlara bakın.\",\n  \"install_static_ok\": \"İyi haber! Sabit IP adresi zaten yapılandırılmış\",\n  \"install_step\": \"Adım\",\n  \"install_submit_desc\": \"Kurulum işlemi tamamlandı ve artık AdGuard Home'u kullanmaya hazırsınız.\",\n  \"install_submit_title\": \"Tebrikler!\",\n  \"install_welcome_desc\": \"AdGuard Home, ağ genelinde reklam ve izleyici engelleyen bir DNS sunucusudur. Tüm ağınızı ve cihazlarınızı kontrol etmenizi sağlar ve istemci tarafında ek bir yazılım kullanmanıza gerek duymaz.\",\n  \"install_welcome_title\": \"AdGuard Home'a hoş geldiniz!\",\n  \"interval_24_hour\": \"24 saat\",\n  \"interval_6_hour\": \"6 saat\",\n  \"interval_days\": \"{{count}} gün\",\n  \"interval_days_plural\": \"{{count}} gün\",\n  \"interval_hours\": \"{{count}} saat\",\n  \"interval_hours_plural\": \"{{count}} saat\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP adresi\",\n  \"known_tracker\": \"Bilinen izleyici\",\n  \"last_rule_in_allowlist\": \"\\\"{{disallowed_rule}}\\\" kuralı hariç tutulduğunda \\\"İzin verilen istemciler\\\" listesi DEVRE DIŞI bırakılacağı için bu istemciye izin verilemez.\",\n  \"last_time_updated_table_header\": \"Son güncelleme zamanı\",\n  \"list_confirm_delete\": \"Bu listeyi silmek istediğinizden emin misiniz?\",\n  \"list_label\": \"Liste\",\n  \"list_updated\": \"{{count}} liste güncellendi\",\n  \"list_updated_plural\": \"{{count}} liste güncellendi\",\n  \"list_url_table_header\": \"Liste URL'si\",\n  \"load_balancing\": \"Yük dengeleme\",\n  \"load_balancing_desc\": \"Üst kaynak sunucuları aynı anda sorgulanır.<br/>AdGuard Home, en düşük başarısız sorgu sayısına ve en düşük ortalama sorgu süresine sahip sunucuları seçmek için ağırlıklı rastgele algoritma kullanır.\",\n  \"loading_table_status\": \"Yükleniyor...\",\n  \"local_ptr_default_resolver\": \"AdGuard Home, varsayılan olarak aşağıdaki ters DNS çözümleyicilerini kullanır: {{ip}}.\",\n  \"local_ptr_desc\": \"AdGuard Home tarafından özel PTR, SOA ve NS istekleri için kullanılan DNS sunucuları. Bir istek, özel IP aralıklarında (örneğin \\\"192.168.12.34\\\" gibi) bir alt ağ içeren bir ARPA alanı soruyorsa ve özel bir IP adresine sahip bir istemciden geliyorsa özel kabul edilir. Ayarlanmadığı durumda AdGuard Home IP adresleri hariç, işletim sisteminizin varsayılan DNS çözümleyicileri kullanılır.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home, bu sistem için uygun olan özel ters DNS çözümleyicilerini belirleyemedi.\",\n  \"local_ptr_placeholder\": \"Her satıra bir IP adresi girin\",\n  \"local_ptr_title\": \"Özel ters DNS sunucuları\",\n  \"location\": \"Konum\",\n  \"log_and_stats_section_label\": \"Sorgu günlüğü ve istatistikler\",\n  \"lower_range_start_error\": \"Başlangıç aralığından daha düşük olmalıdır\",\n  \"main_settings\": \"Ana ayarlar\",\n  \"make_static\": \"Statik yap\",\n  \"manual_update\": \"Elle güncellemek için lütfen <a>bu adımları uygulayın</a>.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Pazartesi\",\n  \"monday_short\": \"Pzt\",\n  \"name\": \"Adı\",\n  \"name_table_header\": \"Adı\",\n  \"netname\": \"Ağ adı\",\n  \"network\": \"Ağ\",\n  \"new_allowlist\": \"Yeni izin listesi\",\n  \"new_blocklist\": \"Yeni engel listesi\",\n  \"next\": \"Sonraki\",\n  \"next_btn\": \"Sonraki\",\n  \"no_blocklist_added\": \"Engel listesi eklenmedi\",\n  \"no_clients_found\": \"İstemci bulunamadı\",\n  \"no_domains_found\": \"Alan adı bulunamadı\",\n  \"no_logs_found\": \"Günlük bulunamadı\",\n  \"no_servers_specified\": \"Sunucu belirtilmedi\",\n  \"no_upstreams_data_found\": \"Üst kaynak verisi bulunamadı\",\n  \"no_whitelist_added\": \"İzin listesi eklenmedi\",\n  \"nothing_found\": \"Hiçbir şey bulunamadı\",\n  \"null_ip\": \"Boş IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Reklam engelleme filtreleri ve hosts engel listeleri tarafından engellenen DNS isteklerinin sayısı\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Engellenen yetişkin içerikli sitelerin sayısı\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"AdGuard gezinti koruması modülü tarafından engellenen DNS isteklerinin sayısı\",\n  \"number_of_dns_query_days\": \"Son {{count}} gün içinde işlenen DNS sorgularının sayısı\",\n  \"number_of_dns_query_days_plural\": \"Son {{count}} gün içinde işlenen DNS sorgularının sayısı\",\n  \"number_of_dns_query_hours\": \"Son {{count}} saat içinde işlenen DNS sorgularının sayısı\",\n  \"number_of_dns_query_hours_plural\": \"Son {{count}} saat içinde işlenen DNS sorgularının sayısı\",\n  \"number_of_dns_query_to_safe_search\": \"Güvenli Aramanın uygulandığı arama motorlarına gönderilen DNS isteklerinin sayısı\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"KAPALI\",\n  \"on\": \"AÇIK\",\n  \"open_dashboard\": \"Panoyu Aç\",\n  \"orgname\": \"Kuruluş adı\",\n  \"original_response\": \"Gerçek yanıt\",\n  \"out_of_range_error\": \"\\\"{{start}}\\\"-\\\"{{end}}\\\" aralığının dışında olmalıdır\",\n  \"page_table_footer_text\": \"Sayfa\",\n  \"parallel_requests\": \"Eş zamanlı sorgu\",\n  \"parental_control\": \"Ebeveyn Denetimi\",\n  \"password_label\": \"Parola\",\n  \"password_placeholder\": \"Parolayı girin\",\n  \"plain_dns\": \"Düz DNS\",\n  \"port_53_faq_link\": \"53 numaralı bağlantı noktası genellikle \\\"DNSStubListener\\\" veya \\\"systemd-resolved\\\" hizmetleri tarafından kullanılır. Bu sorunun nasıl çözüleceğine ilişkin lütfen <0>bu talimatı</0> okuyun.\",\n  \"previous_btn\": \"Önceki\",\n  \"privacy_policy\": \"Gizlilik Politikası\",\n  \"processing_update\": \"Lütfen bekleyin, AdGuard Home güncelleniyor\",\n  \"protection_section_label\": \"Koruma\",\n  \"protocol\": \"Protokol\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Sorgu Günlüğü\",\n  \"query_log_clear\": \"Sorgu günlüklerini temizle\",\n  \"query_log_cleared\": \"Sorgu günlüğü başarıyla temizlendi\",\n  \"query_log_configuration\": \"Günlük yapılandırması\",\n  \"query_log_confirm_clear\": \"Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?\",\n  \"query_log_disabled\": \"Sorgu günlüğü devre dışı bırakıldı, bunu <0>ayarlar</0> kısmından yapılandırılabilirsiniz\",\n  \"query_log_enable\": \"Günlüğü etkinleştir\",\n  \"query_log_filtered\": \"{{filter}} tarafından filtrelendi\",\n  \"query_log_response_status\": \"Durum: {{value}}\",\n  \"query_log_retention\": \"Sorgu günlüğü döngüsü\",\n  \"query_log_retention_confirm\": \"Sorgu günlüğü döngüsünü değiştirmek istediğinizden emin misiniz? Aralık değerini düşürürseniz, bazı veriler kaybolacaktır\",\n  \"query_log_strict_search\": \"Tam arama için çift tırnak işareti kullanın\",\n  \"query_log_updated\": \"Sorgu günlüğü başarıyla güncellendi\",\n  \"rate_limit\": \"Sıklık limiti\",\n  \"rate_limit_desc\": \"İstemci başına izin verilen saniyedeki istek sayısı. 0 olarak ayarlamak, sınır olmadığı anlamına gelir.\",\n  \"rate_limit_subnet_len_ipv4\": \"IPv4 adresleri için alt ağ önek uzunluğu\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Hız sınırlaması için kullanılan IPv4 adreslerinin alt ağ önek uzunluğu. Varsayılan 24'tür\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4 alt ağ önek uzunluğu 0 ile 32 arasında olmalıdır\",\n  \"rate_limit_subnet_len_ipv6\": \"IPv6 adresleri için alt ağ önek uzunluğu\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Hız sınırlaması için kullanılan IPv6 adreslerinin alt ağ önek uzunluğu. Varsayılan 56'tür\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6 alt ağ önek uzunluğu 0 ile 128 arasında olmalıdır\",\n  \"rate_limit_whitelist\": \"Hız sınırlama izin listesi\",\n  \"rate_limit_whitelist_desc\": \"Hız sınırlamasından hariç tutulan IP adresleri\",\n  \"rate_limit_whitelist_placeholder\": \"Her satıra bir IP adresi girin\",\n  \"refresh_btn\": \"Yenile\",\n  \"refresh_statics\": \"İstatistikleri yenile\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Bir sorun bildir\",\n  \"request_details\": \"İstek ayrıntıları\",\n  \"request_table_header\": \"İstek\",\n  \"requests_count\": \"İstek sayısı\",\n  \"reset_settings\": \"Ayarları sıfırla\",\n  \"resolve_clients_desc\": \"Belirtilen çözümleyicilere (yerel istemciler için özel DNS sunucuları, genel IP adresi olan istemciler için üst kaynak sunucuları) PTR sorguları göndererek istemcilerin IP adreslerinin ana makine adlarına tersine çözülmesini sağlar.\",\n  \"resolve_clients_title\": \"İstemcilerin IP adreslerinin ters çözümlenmesini etkinleştir\",\n  \"response_code\": \"Yanıt kodu\",\n  \"response_details\": \"Yanıt ayrıntıları\",\n  \"response_table_header\": \"Yanıt\",\n  \"response_time\": \"Yanıt süresi\",\n  \"rewrite_A\": \"<0>A</0>: özel değer, üst kaynak sunucudan gelen <0>A</0> kayıtlarını tutar\",\n  \"rewrite_AAAA\": \"<0>AAA</0>: özel değer, üst sunucudan gelen <0>AAA</0> kayıtlarını tutar\",\n  \"rewrite_add\": \"DNS yeniden yazımı ekle\",\n  \"rewrite_added\": \"\\\"{{key}}\\\" için DNS yeniden yazımı başarıyla eklendi\",\n  \"rewrite_applied\": \"Yeniden yazım kuralı uygulandı\",\n  \"rewrite_confirm_delete\": \"\\\"{{key}}\\\" için DNS yeniden yazımını silmek istediğinize emin misiniz?\",\n  \"rewrite_deleted\": \"\\\"{{key}}\\\" için DNS yeniden yazımı başarıyla silindi\",\n  \"rewrite_desc\": \"Belirli bir alan adı için özel DNS yanıtını kolayca yapılandırmanızı sağlar.\",\n  \"rewrite_domain_name\": \"Alan adı: bir CNAME kaydı ekler\",\n  \"rewrite_edit\": \"DNS yeniden yazmayı düzenle\",\n  \"rewrite_hosts_applied\": \"Hosts dosyası kuralı tarafından yeniden yazıldı\",\n  \"rewrite_ip_address\": \"IP adresi: bu IP'yi A veya AAAA yanıtında kullanır\",\n  \"rewrite_not_found\": \"DNS yeniden yazımı bulunamadı\",\n  \"rewrite_settings_updated\": \"DNS yeniden yazma ayarları başarıyla güncellendi\",\n  \"rewrite_updated\": \"DNS yeniden yazma başarıyla güncellendi\",\n  \"rewrites_disabled_table_header\": \"Yeniden yazmalar devre dışı\",\n  \"rewrites_enabled_table_header\": \"Yeniden yazmalar etkinleştirildi\",\n  \"rewritten\": \"Yeniden yazıldı\",\n  \"rows_table_footer_text\": \"satır\",\n  \"rule_added_to_custom_filtering_toast\": \"Özel filtreleme kurallarına eklendi: {{rule}}\",\n  \"rule_label\": \"Kural\",\n  \"rule_removed_from_custom_filtering_toast\": \"Özel filtreleme kurallarından kaldırıldı: {{rule}}\",\n  \"rules_count_table_header\": \"Kural sayısı\",\n  \"safe_browsing\": \"Güvenli Gezinti\",\n  \"safe_search\": \"Güvenli Arama\",\n  \"saturday\": \"Cumartesi\",\n  \"saturday_short\": \"Cmt\",\n  \"save_btn\": \"Kaydet\",\n  \"save_config\": \"Yapılandırmayı kaydet\",\n  \"schedule_add\": \"Plan ekle\",\n  \"schedule_current_timezone\": \"Şu anki saat dilimi: {{value}}\",\n  \"schedule_desc\": \"Engellenen hizmetler için duraklatma zamanı ayarlayın\",\n  \"schedule_edit\": \"Planı düzenle\",\n  \"schedule_from\": \"Başlangıç\",\n  \"schedule_invalid_select\": \"Başlangıç zamanı, bitiş zamanından önce olmalıdır\",\n  \"schedule_modal_description\": \"Bu plan, haftanın aynı günü için mevcut planların yerini alır. Haftanın her gününde yalnızca bir duraklatma zamanı olabilir.\",\n  \"schedule_modal_time_off\": \"Hizmet engelleme yok:\",\n  \"schedule_new\": \"Yeni plan\",\n  \"schedule_remove\": \"Planı kaldır\",\n  \"schedule_save\": \"Planı kaydet\",\n  \"schedule_select_days\": \"Günleri seçin\",\n  \"schedule_services\": \"Hizmet engellemeyi duraklat\",\n  \"schedule_services_desc\": \"Hizmet engelleme filtresinin duraklatma planını yapılandırın\",\n  \"schedule_services_desc_client\": \"Bu istemci için hizmet engelleme filtresinin duraklatma planını yapılandırın\",\n  \"schedule_time_all_day\": \"Tüm gün\",\n  \"schedule_timezone\": \"Saat dilimi seçin\",\n  \"schedule_to\": \"Bitiş\",\n  \"served_from_cache_label\": \"Önbellekten kullanıldı\",\n  \"service_name\": \"Hizmet adı\",\n  \"set_static_ip\": \"Sabit IP adresi olarak ayarla\",\n  \"settings\": \"Ayarlar\",\n  \"settings_custom\": \"Özel\",\n  \"settings_global\": \"Genel\",\n  \"setup_config_to_enable_dhcp_server\": \"DHCP sunucusunu etkinleştirmek için kurulum yapılandırması\",\n  \"setup_dns_notice\": \"<1>DNS-over-HTTPS</1> veya <1>DNS-over-TLS</1> protokolünü kullanmak için AdGuard Home üzerinde <0>Şifreleme ayarları</0> bölümünden ayarları yapmanız gerekir.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> <1>{{address}}</1> dizesini kullan.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> <1>{{address}}</1> dizesini kullan.\",\n  \"setup_dns_privacy_3\": \"<0>İşte, kullanabileceğiniz yazılımların bir listesi.</0>\",\n  \"setup_dns_privacy_4\": \"Bir iOS 14 veya macOS Big Sur cihazında, DNS ayarlarına <highlight>DNS-over-HTTPS</highlight> veya <highlight>DNS-over-TLS</highlight> sunucuları ekleyen özel '.mobileconfig' dosyasını indirebilirsiniz.\",\n  \"setup_dns_privacy_android_1\": \"Android 9, yerel olarak DNS-over-TLS protokolünü destekler. Yapılandırmak için Ayarlar → Ağ ve İnternet → Gelişmiş → Özel DNS öğesine gidin ve alan adınızı girin.\",\n  \"setup_dns_privacy_android_2\": \"<0>Android için AdGuard</0>, <1>DNS-over-HTTPS</1> ve <1>DNS-over-TLS</1> protokolünü destekler.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> Android'e <1>DNS-over-HTTPS</1> protokol desteğini ekler.\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS ve macOS yapılandırması\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0>, <1>DNS-over-HTTPS</1> protokolünü destekler, ancak kendi sunucunuzu kullanacak şekilde yapılandırmak için bir <2>DNS Damgası</2> oluşturmanız gerekir.\",\n  \"setup_dns_privacy_ios_2\": \"<0>iOS için AdGuard</0>, <1>DNS-over-HTTPS</1> ve <1>DNS-over-TLS</1> protokolünü destekler.\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home, herhangi bir platformda güvenli bir DNS istemcisi olabilir.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0>, bilinen tüm güvenli DNS protokollerini destekler.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0>, <1>DNS-over-HTTPS</1> protokolünü destekler.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0>, <1>DNS-over-HTTPS</1> protokolünü destekler.\",\n  \"setup_dns_privacy_other_5\": \"<0>Burada</0> ve <1>burada</1> daha fazla kullanım alanı bulabilirsiniz.\",\n  \"setup_dns_privacy_other_title\": \"Diğer kullanım alanları\",\n  \"setup_guide\": \"Kurulum Rehberi\",\n  \"show_all_filter_type\": \"Tümünü göster\",\n  \"show_blocked_responses\": \"Engellendi\",\n  \"show_filtered_type\": \"Filtrelenenleri göster\",\n  \"show_processed_responses\": \"İşlendi\",\n  \"show_whitelisted_responses\": \"İzin verilen\",\n  \"sign_in\": \"Giriş yap\",\n  \"sign_out\": \"Çıkış yap\",\n  \"source_label\": \"Kaynak\",\n  \"static_ip\": \"Sabit IP adresi\",\n  \"static_ip_desc\": \"AdGuard Home bir sunucudur, bu nedenle düzgün çalışabilmesi için sabit bir IP adresine ihtiyaç duyar. Aksi takdirde, yönlendiriciniz bu cihaza farklı bir IP adresi atayabilir.\",\n  \"statistics_clear\": \" İstatistikleri temizle\",\n  \"statistics_clear_confirm\": \"İstatistikleri temizlemek istediğinizden emin misiniz?\",\n  \"statistics_cleared\": \"İstatistikler başarıyla temizlendi\",\n  \"statistics_configuration\": \"İstatistik yapılandırması\",\n  \"statistics_enable\": \"İstatistikleri etkinleştir\",\n  \"statistics_retention\": \"İstatistikleri sakla\",\n  \"statistics_retention_confirm\": \"İstatistik saklama süresini değiştirmek istediğinizden emin misiniz? Aralık değerini azaltırsanız, bazı veriler kaybolacaktır\",\n  \"statistics_retention_desc\": \"Zaman değerini azaltırsanız, bazı veriler kaybolacaktır\",\n  \"stats_adult\": \"Engellenen yetişkin içerikli siteler\",\n  \"stats_disabled\": \"İstatistikler devre dışı bırakıldı. Bunu, <0>ayarlar sayfasından</0> etkinleştirebilirsiniz.\",\n  \"stats_disabled_short\": \"İstatistikler devre dışı bırakıldı\",\n  \"stats_malware_phishing\": \"Engellenen kötü amaçlı yazılım ve kimlik avı\",\n  \"stats_params\": \"İstatistik yapılandırması\",\n  \"stats_query_domain\": \"Başlıca sorgulanan alan adları\",\n  \"subnet_error\": \"Adresler bir alt ağda olmalıdır\",\n  \"sunday\": \"Pazar\",\n  \"sunday_short\": \"Paz\",\n  \"system_host_files\": \"Sistem hosts dosyaları\",\n  \"table_client\": \"İstemci\",\n  \"table_name\": \"Ad\",\n  \"tags_desc\": \"İstemciyi tanımlayan etiketleri seçebilirsiniz. Etiketleri filtreleme kurallarına ekleyerek filtrelemeyi daha etkin bir şekilde uygulayabilirsiniz. <0>Daha fazla bilgi edinin</0>.\",\n  \"tags_title\": \"Etiketler\",\n  \"test_upstream_btn\": \"Üst kaynakları test et\",\n  \"theme_auto\": \"Otomatik\",\n  \"theme_auto_desc\": \"Otomatik (cihazınızın renk düzenine göre)\",\n  \"theme_dark\": \"Koyu\",\n  \"theme_dark_desc\": \"Koyu tema\",\n  \"theme_light\": \"Açık\",\n  \"theme_light_desc\": \"Açık tema\",\n  \"thursday\": \"Perşembe\",\n  \"thursday_short\": \"Per\",\n  \"time_table_header\": \"Süre\",\n  \"top_blocked_domains\": \"Başlıca engellenen alan adları\",\n  \"top_clients\": \"Başlıca istemciler\",\n  \"top_upstreams\": \"Başlıca üst kaynaklar\",\n  \"topline_expired_certificate\": \"SSL sertifikanızın süresi sona erdi. <0>Şifreleme ayarlarını</0> güncelleyin.\",\n  \"topline_expiring_certificate\": \"SSL sertifikanızın süresi sona üzere. <0>Şifreleme ayarlarını</0> güncelleyin.\",\n  \"tracker_source\": \"İzleyici kaynağı\",\n  \"try_again\": \"Tekrar dene\",\n  \"ttl_cache_validation\": \"Minimum önbellek kullanım süresi geçersiz kılma, maksimum değerden küçük veya ona eşit olmalıdır\",\n  \"tuesday\": \"Salı\",\n  \"tuesday_short\": \"Sal\",\n  \"type_table_header\": \"Tür\",\n  \"unavailable_dhcp\": \"DHCP kullanılamıyor\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home, işletim sisteminizde DHCP sunucusu çalıştıramıyor\",\n  \"unblock\": \"Engeli kaldır\",\n  \"unblock_all\": \"Tüm engellemeyi kaldır\",\n  \"unblock_for_this_client_only\": \"Yalnızca bu istemci için engellemeyi kaldır\",\n  \"unknown_filter\": \"Bilinmeyen filtre {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home'un {{version}} sürümü mevcut! Daha fazla bilgi için <0>buraya tıklayın</0>.\",\n  \"update_failed\": \"Otomatik güncellenemedi. Elle güncellemek için lütfen <a>bu adımları izleyin</a>.\",\n  \"update_now\": \"Şimdi güncelle\",\n  \"updated_custom_filtering_toast\": \"Özel kurallar başarıyla kaydedildi\",\n  \"updated_save_search_toast\": \"Güvenli Arama ayarları güncellendi\",\n  \"updated_upstream_dns_toast\": \"Üst sunucular başarıyla kaydedildi\",\n  \"updates_checked\": \"AdGuard Home'un yeni bir sürümü mevcut\",\n  \"updates_version_equal\": \"AdGuard Home güncel\",\n  \"upstream\": \"Üst kaynak\",\n  \"upstream_dns\": \"Üst kaynak DNS sunucusu\",\n  \"upstream_dns_cache_configuration\": \"Üst kaynak DNS önbellek yapılandırması\",\n  \"upstream_dns_client_desc\": \"Bu alanı boş bırakırsanız, AdGuard Home, <0>DNS ayarlarında</0> yapılandırılan sunucuları kullanır.\",\n  \"upstream_dns_configured_in_file\": \"{{path}} dosyasında yapılandırıldı\",\n  \"upstream_dns_help\": \"Her satıra bir sunucu adresi girin. Üst DNS sunucularını yapılandırma hakkında <a>daha fazla bilgi edinin</a>.\",\n  \"upstream_parallel\": \"Tüm üst kaynak sunucuları aynı anda sorgulayarak çözümlemeyi hızlandırır.\",\n  \"upstream_timeout\": \"Üst kaynak zaman aşımı\",\n  \"upstream_timeout_desc\": \"Üst kaynak sunucusundan yanıt almak için kaç saniye bekleneceğini belirtir\",\n  \"upstreams\": \"Üst kaynak\",\n  \"use_adguard_browsing_sec\": \"AdGuard gezinti koruması web hizmetini kullan\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home, alan adının gezinti koruması web hizmeti tarafından engellenip engellenmediğini kontrol eder. Kontrolü gerçekleştirmek için gizlilik dostu arama API'sini kullanır: sunucuya yalnızca SHA256 karma alan adının kısa bir ön eki gönderilir.\",\n  \"use_adguard_parental\": \"AdGuard ebeveyn denetimi web hizmetini kullan\",\n  \"use_adguard_parental_hint\": \"AdGuard Home, alan adının yetişkin içerik bulundurup bulundurmadığını kontrol eder. Gezinti koruması web hizmeti ile kullandığımız aynı gizlilik dostu API'yi kullanır.\",\n  \"use_private_ptr_resolvers_desc\": \"Özel üst kaynak sunucuları, DHCP, /etc/hosts, vb. aracılığıyla özel IP adresleri içeren ARPA alan adları için PTR, SOA ve NS isteklerini çözümleyin. Devre dışı bırakılırsa, AdGuard Home bu tür tüm isteklere NXDOMAIN ile yanıt verir.\",\n  \"use_private_ptr_resolvers_title\": \"Özel ters DNS çözümleyicileri kullan\",\n  \"use_saved_key\": \"Önceden kaydedilmiş anahtarı kullan\",\n  \"username_label\": \"Kullanıcı adı\",\n  \"username_placeholder\": \"Kullanıcı adını girin\",\n  \"validated_with_dnssec\": \"DNSSEC ile doğrulandı\",\n  \"version\": \"Sürüm\",\n  \"version_request_error\": \"Güncelleme denetlenemedi. Lütfen internet bağlantınızı kontrol edin.\",\n  \"wednesday\": \"Çarşamba\",\n  \"wednesday_short\": \"Çar\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/uk.json",
    "content": "{\n  \"access_allowed_desc\": \"Перелік CIDR, IP-адрес та <a>ClientIDs</a>. Якщо налаштовано, AdGuard Home прийматиме запити лише від цих клієнтів.\",\n  \"access_allowed_title\": \"Дозволені клієнти\",\n  \"access_blocked_desc\": \"Не плутайте з фільтрами. AdGuard Home буде ігнорувати DNS-запити з цими доменами, такі запити навіть не будуть записані до журналу. Ви можете вказати точні доменні імена, замінні знаки та правила фільтрування URL-адрес, наприклад, «example.org», «*.example.org» або «||example.org^» відповідно.\",\n  \"access_blocked_title\": \"Заборонені домени\",\n  \"access_desc\": \"Тут ви можете налаштувати правила доступу для DNS-сервера AdGuard Home\",\n  \"access_disallowed_desc\": \"Перелік CIDR, IP-адрес та <a>ClientIDs</a>. Якщо налаштовано, AdGuard Home буде скасовувати запити від цих клієнтів. Проте якщо налаштовано список Дозволених клієнтів, то це поле проігнорується.\",\n  \"access_disallowed_title\": \"Заборонені клієнти\",\n  \"access_settings_saved\": \"Налаштування доступу успішно збережено\",\n  \"access_title\": \"Налаштування доступу\",\n  \"actions_table_header\": \"Дії\",\n  \"add_allowlist\": \"Додати список дозволів\",\n  \"add_blocklist\": \"Додати список блокування\",\n  \"add_custom_list\": \"Додати власний список\",\n  \"add_persistent_client\": \"Додати в збережені клієнти\",\n  \"address\": \"Адреса\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home буде видаляти всі запити DNS із цього клієнта.\",\n  \"all_lists_up_to_date_toast\": \"Всі списки вже оновлені\",\n  \"all_queries\": \"Усі запити\",\n  \"allow_this_client\": \"Дозволити цей клієнт\",\n  \"allowed\": \"Дозволено\",\n  \"anonymize_client_ip\": \"Анонімізація IP-адреси клієнта\",\n  \"anonymize_client_ip_desc\": \"Не зберігати повну IP-адресу клієнта в журналах і статистиці\",\n  \"anonymizer_notification\": \"<0>Примітка:</0> IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування</1> .\",\n  \"answer\": \"Відповідь\",\n  \"apply_btn\": \"Застосувати\",\n  \"auto_clients_desc\": \"Інформація про IP-адреси пристроїв, які використовують або можуть використовувати AdGuard Home. Ця інформація збирається з кількох джерел, зокрема з файлів hosts, зворотного DNS тощо.\",\n  \"auto_clients_title\": \"Runtime-клієнти\",\n  \"autofix_warning_list\": \"Будуть виконані такі дії: <0>Деактивація системи DNSStubListener</0> <0>Зміна адреси DNS-сервера на «127.0.0.1»</0> <0>Заміна символічного посилання «/etc/resolv.conf» на «/run/systemd/resolve/resolv.conf»</0> <0>Зупинка DNSStubListener (перезапуск системної служби systemd-resolved)</0>\",\n  \"autofix_warning_result\": \"В результаті буде усталено, що усі DNS-запити вашої системи будуть опрацьовані AdGuard Home.\",\n  \"autofix_warning_text\": \"Якщо ви натиснете «Виправити», AdGuard Home налаштує вашу систему на використання DNS-сервера AdGuard Home.\",\n  \"average_processing_time\": \"Середній час обробки\",\n  \"average_processing_time_hint\": \"Середній час обробки DNS запиту в мілісекундах\",\n  \"average_upstream_response_time\": \"Середній час відгуку upstream-сервера\",\n  \"back\": \"Назад\",\n  \"block\": \"Заборонити\",\n  \"block_all\": \"Блокувати все\",\n  \"block_domain_use_filters_and_hosts\": \"Блокування доменів за допомогою фільтрів та hosts-файлів\",\n  \"block_for_this_client_only\": \"Заборонити тільки цей клієнт\",\n  \"block_services\": \"Блокувати конкретні сервіси\",\n  \"blocked_adult_websites\": \"Заблоковано «Батьківським контролем»\",\n  \"blocked_by\": \"<0>Заблоковано фільтрами</0>\",\n  \"blocked_by_cname_or_ip\": \"Заблоковано по CNAME або IP\",\n  \"blocked_by_response\": \"У відповідь заблоковано по CNAME або IP\",\n  \"blocked_response_ttl\": \"TTL заблокованої відповіді\",\n  \"blocked_response_ttl_desc\": \"Вказує, скільки секунд клієнти повинні кешувати відфільтровану відповідь\",\n  \"blocked_safebrowsing\": \"Заблоковано модулем «Безпека перегляду»\",\n  \"blocked_service\": \"Заблокований сервіс\",\n  \"blocked_services\": \"Заблоковані сервіси\",\n  \"blocked_services_desc\": \"Дозволяє швидко блокувати популярні сайти та сервіси.\",\n  \"blocked_services_global\": \"Використовувати глобально заблоковані сервіси\",\n  \"blocked_services_saved\": \"Заблоковані сервіси успішно збережено\",\n  \"blocked_threats\": \"Заблоковано загроз\",\n  \"blocking_ipv4\": \"Блокування IPv4\",\n  \"blocking_ipv4_desc\": \"IP-адреса, яку потрібно видати для заблокованого A запиту\",\n  \"blocking_ipv6\": \"Блокування IPv6\",\n  \"blocking_ipv6_desc\": \"IP-адреса, яку потрібно видати для заблокованого АААА запиту\",\n  \"blocking_mode\": \"Режим блокування\",\n  \"blocking_mode_custom_ip\": \"Спеціальна IP-адреса: Відповісти із вручну встановленою IP-адресою\",\n  \"blocking_mode_default\": \"Усталено: відповідь із нульовою IP-адресою (0.0.0.0 для A; :: для AAAA), якщо заблоковано правилом у Adblock-стилі; відповідь зазначеною у правилі IP-адресою, якщо заблокувано правилом у hosts-стилі\",\n  \"blocking_mode_null_ip\": \"Нульовий IP: Відповісти з нульовою IP-адресою (0.0.0.0 для A; :: для AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Відповісти з кодом NXDOMAIN\",\n  \"blocking_mode_refused\": \"ВІДМОВЛЕНО: Відповісти з кодом ВІДМОВЛЕНО\",\n  \"blocklist\": \"Список блокування\",\n  \"bootstrap_dns\": \"Bootstrap DNS-сервери\",\n  \"bootstrap_dns_desc\": \"IP-адреси DNS-серверів, які використовуються для визначення IP-адрес DoH/DoT-розпізнавачів, які ви вказуєте як висхідні. Коментарі заборонені.\",\n  \"cache_cleared\": \"Кеш DNS успішно очищено\",\n  \"cache_enabled\": \"Увімкнути кеш\",\n  \"cache_enabled_desc\": \"Зберігати відповіді DNS локально.\",\n  \"cache_optimistic\": \"Оптимістичне кешування\",\n  \"cache_optimistic_desc\": \"AdGuard Home буде відповідати з кешу, навіть якщо відповіді в ньому застарілі, а також спробує оновити їх.\",\n  \"cache_size\": \"Розмір кешу\",\n  \"cache_size_desc\": \"Розмір кешу DNS (у байтах).\",\n  \"cache_size_validation\": \"Розмір кешу має бути більшим за нуль, коли цю функцію увімкнуто.\",\n  \"cache_ttl_max_override\": \"Змінити максимальний TTL\",\n  \"cache_ttl_max_override_desc\": \"Встановіть максимальне TTL-значення (в секундах) для записів у DNS-кеші.\",\n  \"cache_ttl_min_override\": \"Змінити мінімальний TTL\",\n  \"cache_ttl_min_override_desc\": \"Збільшити малі TTL-значення (в секундах), отримані від основного сервера під час кешування DNS-відповідей.\",\n  \"cancel_btn\": \"Скасувати\",\n  \"category_label\": \"Категорія\",\n  \"check\": \"Перевірити\",\n  \"check_client_id\": \"Ідентифікатор клієнта (ClientID або IP-адреса)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Перевірити чи фільтрується назва вузла.\",\n  \"check_dhcp_servers\": \"Перевірити DHCP-сервери\",\n  \"check_dns_record\": \"Виберіть тип DNS запису\",\n  \"check_enter_client_id\": \"Введіть ідентифікатор клієнта\",\n  \"check_hostname\": \"Ім'я хоста або доменне ім'я\",\n  \"check_ip\": \"IP адреси: {{ip}}\",\n  \"check_not_found\": \"Не знайдено у ваших списках фільтрів\",\n  \"check_reason\": \"Причина: {{reason}}\",\n  \"check_service\": \"Назва сервісу: {{service}}\",\n  \"check_title\": \"Перевірити фільтрування\",\n  \"check_updates_btn\": \"Перевірити оновлення\",\n  \"check_updates_now\": \"Перевірити наявність оновлень\",\n  \"choose_allowlist\": \"Виберіть списки дозволів\",\n  \"choose_blocklist\": \"Виберіть списки блокування\",\n  \"choose_from_list\": \"Виберіть зі списку\",\n  \"city\": \"Місто\",\n  \"clear_cache\": \"Очистити кеш\",\n  \"click_to_view_queries\": \"Клацніть, щоб переглянути запити\",\n  \"client_add\": \"Додати Клієнта\",\n  \"client_added\": \"Клієнта «{{key}}» успішно додано\",\n  \"client_blocked\": \"Клієнта «{{ip}}» успішно заблоковано\",\n  \"client_confirm_block\": \"Ви впевнені, що хочете заблокувати клієнта «{{ip}}»?\",\n  \"client_confirm_delete\": \"Ви впевнені, що хочете видалити клієнта «{{key}}»?\",\n  \"client_confirm_unblock\": \"Ви впевнені, що хочете розблокувати клієнт «{{ip}}»?\",\n  \"client_deleted\": \"Клієнта «{{key}}» успішно видалено\",\n  \"client_details\": \"Подробиці про клієнта\",\n  \"client_edit\": \"Редагувати Клієнта\",\n  \"client_global_settings\": \"Використати загальні налаштування\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Різні клієнти можуть бути розпізнані завдяки ClientID. <a>Докладніше про ідентифікацію клієнтів</a>.\",\n  \"client_id_placeholder\": \"Введіть ClientID\",\n  \"client_identifier\": \"Ідентифікатор\",\n  \"client_identifier_desc\": \"Клієнтів можна ідентифікувати за IP- чи MAC-адресами, CIDR або ж за спеціальним клієнтським ідентифікатором ClientID (можливий для DoT, DoH та DoQ). <0>Докладніше про ідентифікацію клієнтів</0>.\",\n  \"client_name\": \"Клієнт {{id}}\",\n  \"client_new\": \"Новий Клієнт\",\n  \"client_settings\": \"Налаштування клієнта\",\n  \"client_table_header\": \"Клієнт\",\n  \"client_unblocked\": \"Клієнта «{{ip}}» успішно розблоковано\",\n  \"client_updated\": \"Клієнта «{{key}}» успішно оновлено\",\n  \"clients_desc\": \"Налаштуйте пристрої, які підʼєднано до AdGuard Home\",\n  \"clients_not_found\": \"Клієнтів не знайдено\",\n  \"clients_title\": \"Постійні клієнти\",\n  \"compact\": \"Стисло\",\n  \"config_successfully_saved\": \"Конфігурацію успішно збережено\",\n  \"configure\": \"Налаштувати\",\n  \"confirm_dns_cache_clear\": \"Ви впевнені, що бажаєте очистити кеш DNS?\",\n  \"confirm_static_ip\": \"AdGuard Home налаштує {{ip}} як вашу статичну IP-адресу. Ви хочете продовжити?\",\n  \"copyright\": \"Авторське право\",\n  \"country\": \"Країна\",\n  \"custom_filter_rules\": \"Власні правила фільтрування\",\n  \"custom_filter_rules_hint\": \"Вводьте одне правило на рядок. Ви можете використовувати правила блокування чи синтаксис файлів hosts.\",\n  \"custom_filtering_rules\": \"Власні правила фільтрування\",\n  \"custom_ip\": \"Власний IP\",\n  \"custom_retention_input\": \"Введіть час в годинах\",\n  \"custom_rotation_input\": \"Введіть час в годинах\",\n  \"dashboard\": \"Панель керування\",\n  \"date\": \"Дата\",\n  \"default\": \"Усталено\",\n  \"delete_confirm\": \"Ви дійсно хочете видалити «{{key}}»?\",\n  \"delete_table_action\": \"Видалити\",\n  \"descr\": \"Опис\",\n  \"details\": \"Подробиці\",\n  \"dhcp_add_static_lease\": \"Додати статичну оренду\",\n  \"dhcp_config_saved\": \"Конфігурацію DHCP-сервера успішно збережено\",\n  \"dhcp_description\": \"Якщо ваш роутер не пропонує налаштування DHCP, ви можете використати власний вбудований DHCP-сервер AdGuard.\",\n  \"dhcp_disable\": \"Вимкнути DHCP-сервер\",\n  \"dhcp_dynamic_ip_found\": \"Ваша система використовує конфігурацію з динамічною IP-адресою для інтерфейсу <0>{{interfaceName}}</0>. Для використання DHCP-сервера необхідно встановити статичну IP-адресу. Ваша поточна IP-адреса <0>{{ipAddress}}</0>. Ми автоматично встановимо цю IP-адресу як статичну, якщо ви натиснете кнопку «Увімкнути DHCP-сервер».\",\n  \"dhcp_edit_static_lease\": \"Редагувати статичну оренду\",\n  \"dhcp_enable\": \"Увімкнути DHCP-сервер\",\n  \"dhcp_error\": \"AdGuard Home не зміг визначити, чи є в мережі інший DHCP-сервер\",\n  \"dhcp_form_gateway_input\": \"IP-адреса шлюзу\",\n  \"dhcp_form_lease_input\": \"Тривалість оренди\",\n  \"dhcp_form_lease_title\": \"Час оренди DHCP (в секундах)\",\n  \"dhcp_form_range_end\": \"Кінець діапазону\",\n  \"dhcp_form_range_start\": \"Початок діапазону\",\n  \"dhcp_form_range_title\": \"Діапазон IP-адрес\",\n  \"dhcp_form_subnet_input\": \"Маска підмережі\",\n  \"dhcp_found\": \"Не знайдено DHCP-сервера в мережі. Вмикати вбудований DHCP-сервер небезпечно.\",\n  \"dhcp_hardware_address\": \"Апаратна адреса\",\n  \"dhcp_interface_select\": \"Вибрати DHCP-інтерфейс\",\n  \"dhcp_ip_addresses\": \"IP-адреси\",\n  \"dhcp_ipv4_settings\": \"Налаштування DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Налаштування DHCP IPv6\",\n  \"dhcp_lease_added\": \"Статичну оренду «{{key}}» успішно додано\",\n  \"dhcp_lease_deleted\": \"Статичну оренду «{{key}}» успішно видалено\",\n  \"dhcp_lease_updated\": \"Статичну оренду «{{key}}» успішно оновлено\",\n  \"dhcp_leases\": \"Оренда DHCP\",\n  \"dhcp_leases_not_found\": \"Оренду DHCP не знайдено\",\n  \"dhcp_new_static_lease\": \"Нова статична оренда\",\n  \"dhcp_not_found\": \"Можна безпечно увімкнути вбудований DHCP-сервер — ми не знайшли жодного активного DHCP-сервера в мережі. Однак, ми радимо вам ще раз перевірити вручну, тому що наш автоматичний тест наразі не дає 100% гарантії.\",\n  \"dhcp_reset\": \"Ви дійсно хочете скинути DHCP-конфігурацію?\",\n  \"dhcp_reset_leases\": \"Скинути всі аренди\",\n  \"dhcp_reset_leases_confirm\": \"Ви дійсно хочете скинути усі аренди?\",\n  \"dhcp_reset_leases_success\": \"Оренду DHCP успішно скинуто\",\n  \"dhcp_settings\": \"Налаштування DHCP\",\n  \"dhcp_static_ip_error\": \"Для використання DHCP-сервера необхідно встановити статичну IP-адресу. Нам не вдалося визначити, чи цей мережевий інтерфейс налаштовано для використання статичної IP-адреси. Встановіть статичну IP-адресу вручну.\",\n  \"dhcp_static_leases\": \"Статичні оренди DHCP\",\n  \"dhcp_static_leases_not_found\": \"Не знайдено статичних оренд DHCP\",\n  \"dhcp_table_expires\": \"Закінчується\",\n  \"dhcp_table_hostname\": \"Назва вузла\",\n  \"dhcp_title\": \"DHCP-сервер (експериментальний!)\",\n  \"dhcp_warning\": \"Якщо ви однаково хочете увімкнути DHCP-сервер, переконайтеся, що у вашій мережі немає інших активних DHCP-серверів. Інакше, це може порушити роботу інтернету на підʼєднаних пристроях!\",\n  \"disable_for_hours\": \"На {{count}} годину\",\n  \"disable_for_hours_plural\": \"На {{count}} годин\",\n  \"disable_for_minutes\": \"На {{count}} хвилину\",\n  \"disable_for_minutes_plural\": \"На {{count}} хвилин\",\n  \"disable_for_seconds\": \"На {{count}} секунду\",\n  \"disable_for_seconds_plural\": \"На {{count}} секунд\",\n  \"disable_ipv6\": \"Вимкнути вирішення IPv6-адрес\",\n  \"disable_ipv6_desc\": \"Ігнорувати всі DNS-запити адрес IPv6 (тип AAAA) та видаляти IPv6-дані з відповідей типу HTTPS.\",\n  \"disable_notify_for_hours\": \"Вимкнення захисту на {{count}} годину\",\n  \"disable_notify_for_hours_plural\": \"Вимкнення захисту на {{count}} годин\",\n  \"disable_notify_for_minutes\": \"Вимкнення захисту на {{count}} хвилину\",\n  \"disable_notify_for_minutes_plural\": \"Вимкнення захисту на {{count}} хвилин\",\n  \"disable_notify_for_seconds\": \"Вимкнення захисту на {{count}} секунду\",\n  \"disable_notify_for_seconds_plural\": \"Вимкнення захисту на {{count}} секунд\",\n  \"disable_notify_until_tomorrow\": \"Вимкнути захист до завтра\",\n  \"disable_protection\": \"Вимкнути захист\",\n  \"disable_rewrites\": \"Вимкнути правила перезапису\",\n  \"disable_until_tomorrow\": \"До завтра\",\n  \"disabled\": \"Вимкнено\",\n  \"disabled_dhcp\": \"DHCP-сервер вимкнено\",\n  \"disabled_filtering_toast\": \"Фільтрування вимкнено\",\n  \"disabled_parental_toast\": \"«Батьківський контроль» вимкнено\",\n  \"disabled_protection\": \"Захист вимкнено\",\n  \"disabled_safe_browsing_toast\": \"Безпечний перегляд вимкнено\",\n  \"disabled_safe_search_toast\": \"Безпечний пошук вимкнено\",\n  \"disallow_this_client\": \"Заборонити цього клієнта\",\n  \"dns_addresses\": \"DNS-адреси\",\n  \"dns_allowlists\": \"Списки дозволів DNS\",\n  \"dns_allowlists_desc\": \"Домени зі списків дозволів DNS будуть дозволятися, навіть якщо вони знаходяться в будь-якому зі списків блокування.\",\n  \"dns_blocklists\": \"Список блокування DNS\",\n  \"dns_blocklists_desc\": \"AdGuard Home блокуватиме домени зі списків блокування.\",\n  \"dns_cache_config\": \"Конфігурація кешу DNS\",\n  \"dns_cache_config_desc\": \"Тут ви можете налаштувати DNS-кеш\",\n  \"dns_cache_size\": \"Розмір кешу DNS, у байтах\",\n  \"dns_config\": \"Конфігурація DNS-сервера\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"Конфіденційність DNS\",\n  \"dns_providers\": \"<0>Список відомих DNS-провайдерів</0> на вибір.\",\n  \"dns_query\": \"DNS-запити\",\n  \"dns_rewrites\": \"DNS перезаписи\",\n  \"dns_settings\": \"Налаштування DNS\",\n  \"dns_start\": \"DNS-сервер запускається\",\n  \"dns_status_error\": \"Помилка перевірки стану DNS-сервера\",\n  \"dns_test_not_ok_toast\": \"Сервер «{{key}}»: неможливо використати. Перевірте правильність введення\",\n  \"dns_test_ok_toast\": \"Вказані DNS сервери працюють правильно\",\n  \"dns_test_parsing_error_toast\": \"Розділ {{section}}: рядок {{line}}: неможливо використати. Перевірте правильність введення\",\n  \"dns_test_warning_toast\": \"Upstream «{{key}}» не відповідає на тестові запити та може працювати не правильно\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Увімкнути DNSSEC\",\n  \"dnssec_enable_desc\": \"Увімкнути DNSSEC для вихідних DNS-запитів та перевірити результат (потрібен вирішувач з підтримкою DNSSEC).\",\n  \"domain\": \"Домен\",\n  \"domain_desc\": \"Введіть доменне ім’я або підстановний знак, який потрібно переписати.\",\n  \"domain_name_table_header\": \"Назва домену\",\n  \"domain_or_client\": \"Домен чи клієнт\",\n  \"down\": \"Недоступний\",\n  \"download_mobileconfig\": \"Завантажити файл конфігурації\",\n  \"download_mobileconfig_doh\": \"Завантажити .mobileconfig для DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Завантажити .mobileconfig для DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Змінити список дозволів\",\n  \"edit_blocklist\": \"Змінити список блокування\",\n  \"edit_table_action\": \"Редагувати\",\n  \"edns_cs_desc\": \"Додавати параметр EDNS Client Subnet (ECS) до запитів до upstream-серверів, а також записувати в журнал значення, що надсилаються клієнтами.\",\n  \"edns_enable\": \"Увімкнути відправку EDNS Client Subnet\",\n  \"edns_use_custom_ip\": \"Використання користувацької IP-адреси для EDNS\",\n  \"edns_use_custom_ip_desc\": \"Дозволити використовувати користувацьку IP-адресу для EDNS\",\n  \"elapsed\": \"Витрачений час\",\n  \"empty_response_status\": \"Порожньо\",\n  \"enable_protection\": \"Увімкнути захист\",\n  \"enable_protection_timer\": \"Захист буде ввімкнено о {{time}}\",\n  \"enable_rewrites\": \"Увімкнути правила перезапису\",\n  \"enable_upstream_dns_cache\": \"Увімкнути кешування для користувацької конфігурації upstream-серверів цього клієнта\",\n  \"enabled_dhcp\": \"DHCP-сервер увімкнено\",\n  \"enabled_filtering_toast\": \"Фільтрування увімкнено\",\n  \"enabled_parental_toast\": \"«Батьківський контроль» увімкнено\",\n  \"enabled_protection\": \"Захист увімкнено\",\n  \"enabled_safe_browsing_toast\": \"Безпечний перегляд увімкнено\",\n  \"enabled_save_search_toast\": \"Безпечний пошук увімкнено\",\n  \"enabled_table_header\": \"Увімкнено\",\n  \"encryption_certificate_path\": \"Шлях до сертифіката\",\n  \"encryption_certificates\": \"Сертифікати\",\n  \"encryption_certificates_desc\": \"Для використання шифрування потрібно надати дійсний ланцюжок сертифікатів SSL для вашого домену. Ви можете отримати безплатний сертифікат на <0>{{link}}</0> або придбати його в одному з надійних Центрів Сертифікації.\",\n  \"encryption_certificates_input\": \"Скопіюйте/вставте сюди свої кодовані PEM сертифікати.\",\n  \"encryption_certificates_source_content\": \"Вставити вміст сертифікату\",\n  \"encryption_certificates_source_path\": \"Вказати шлях до сертифікату\",\n  \"encryption_chain_invalid\": \"Ланцюжок довіри сертифікатів не дійсний\",\n  \"encryption_chain_valid\": \"Ланцюжок довіри сертифікатів дійсний\",\n  \"encryption_config_saved\": \"Конфігурацію шифрування збережено\",\n  \"encryption_desc\": \"Підтримка шифрування (HTTPS/TLS) як для DNS, так і для вебінтерфейсу адміністратора\",\n  \"encryption_doq\": \"Порт DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"Якщо цей порт налаштовано, AdGuard Home запустить на ньому сервер DNS-over-QUIC.\",\n  \"encryption_dot\": \"Порт DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Якщо цей порт налаштовано, AdGuard Home запустить на цьому порту сервер DNS-over-TLS.\",\n  \"encryption_enable\": \"Увімкнути шифрування (HTTPS, DNS-over-HTTPS і DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Якщо ввімкнено шифрування, інтерфейс адміністратора AdGuard Home буде працювати через HTTPS, а DNS-сервер буде прослуховувати запити через DNS-over-HTTPS і DNS-over-TLS.\",\n  \"encryption_expire\": \"Закінчується\",\n  \"encryption_hostnames\": \"Назви вузлів\",\n  \"encryption_https\": \"Порт HTTPS\",\n  \"encryption_https_desc\": \"Якщо HTTPS-порт налаштовано, інтерфейс адміністратора AdGuard Home буде доступний через HTTPS, а також сервер DNS-over-HTTPS буде доступний за адресою '/dns-query'.\",\n  \"encryption_issuer\": \"Видавець\",\n  \"encryption_key\": \"Приватний ключ\",\n  \"encryption_key_input\": \"Скопіюйте/вставте сюди свій приватний ключ кодований PEM для вашого сертифіката.\",\n  \"encryption_key_invalid\": \"Недійсний {{type}} приватний ключ\",\n  \"encryption_key_source_content\": \"Вставити вміст приватного ключа\",\n  \"encryption_key_source_path\": \"Вказати шлях до файлу приватного ключа\",\n  \"encryption_key_valid\": \"Дійсний {{type}} приватний ключ\",\n  \"encryption_plain_dns_desc\": \"Звичайний DNS усталено увімкнений. Ви можете вимкнути його, щоб змусити всі пристрої використовувати зашифрований DNS. Для цього необхідно увімкнути хоча б один зашифрований протокол DNS\",\n  \"encryption_plain_dns_enable\": \"Увімкнути звичайний DNS\",\n  \"encryption_plain_dns_error\": \"Щоб вимкнути звичайний DNS, увімкніть принаймні один зашифрований протокол DNS\",\n  \"encryption_private_key_path\": \"Шлях до приватного ключа\",\n  \"encryption_redirect\": \"Автоматично перенаправляти на HTTPS\",\n  \"encryption_redirect_desc\": \"Якщо встановлено, AdGuard Home автоматично перенаправить вас з HTTP на адреси HTTPS.\",\n  \"encryption_reset\": \"Ви впевнені, що хочете скинути налаштування шифрування?\",\n  \"encryption_server\": \"Назва сервера\",\n  \"encryption_server_desc\": \"Якщо встановлено, AdGuard Home розпізнає ClientID, відповідає на DDR-запити та додатково перевіряє з'єднання. Якщо не встановлено, то цей функціонал вимкнено. Мусить відповідати одному з параметрів DNS Names в сертифікаті.\",\n  \"encryption_server_enter\": \"Введіть ваше доменне ім'я\",\n  \"encryption_settings\": \"Налаштування шифрування\",\n  \"encryption_status\": \"Статус\",\n  \"encryption_subject\": \"Обє'кт\",\n  \"encryption_title\": \"Шифрування\",\n  \"encryption_warning\": \"Попередження\",\n  \"enforce_safe_search\": \"Використовувати Безпечний пошук\",\n  \"enforce_save_search_hint\": \"AdGuard Home забезпечить безпечний пошук у таких пошукових системах: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Примусовий безпечний пошук\",\n  \"enter_cache_size\": \"Введіть розмір кешу (байт)\",\n  \"enter_cache_ttl_max_override\": \"Введіть максимальний TTL (в секундах)\",\n  \"enter_cache_ttl_min_override\": \"Введіть мінімальний TTL (в секундах)\",\n  \"enter_name_hint\": \"Введіть назву\",\n  \"enter_url_or_path_hint\": \"Уведіть URL-адресу чи абсолютний шлях до списку\",\n  \"enter_valid_allowlist\": \"Введіть дійсну URL-адресу в список дозволів.\",\n  \"enter_valid_blocklist\": \"Введіть дійсну URL-адресу в список блокування.\",\n  \"error_details\": \"Подробиці помилки\",\n  \"example_comment\": \"! Так можна додавати коментар.\",\n  \"example_comment_hash\": \"# Також коментар.\",\n  \"example_comment_meaning\": \"просто коментар;\",\n  \"example_meaning_filter_block\": \"блокувати доступ до домену example.org та всіх його піддоменів;\",\n  \"example_meaning_filter_whitelist\": \"розблоковвати доступ до домену example.org та всіх його піддоменів;\",\n  \"example_meaning_host_block\": \"повертати адресу 127.0.0.1 для домену example.org, але не його піддоменів;\",\n  \"example_multiple_upstreams_reserved\": \"кілька DNS-серверів <0>для конкретних доменів</0>;\",\n  \"example_regex_meaning\": \"блокувати доступ до доменів, що відповідають вказаному регулярному виразу.\",\n  \"example_rewrite_domain\": \"перепишіть відповіді лише для цього доменного імені.\",\n  \"example_rewrite_wildcard\": \"перепишіть відповіді для всіх субдоменів <0>example.org</0>.\",\n  \"example_upstream_comment\": \"коментар.\",\n  \"example_upstream_doh\": \"зашифрований <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"зашифрований DNS через HTTPS із примусовим <0>HTTP/3</0> і без повернення до HTTP/2 або нижче;\",\n  \"example_upstream_doq\": \"зашифрований <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"зашифрований <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"звичайний DNS (через UDP);\",\n  \"example_upstream_regular_port\": \"звичайний DNS (поверх UDP, з портом);\",\n  \"example_upstream_reserved\": \"DNS-сервер <0>для певних доменів</0>;\",\n  \"example_upstream_sdns\": \"<0>DNS Stamps</0> для <1>DNSCrypt-</1> або <2>DNS-over-HTTPS-</2>вирішувачів;\",\n  \"example_upstream_tcp\": \"звичайний DNS (через TCP);\",\n  \"example_upstream_tcp_hostname\": \"звичайний DNS (поверх TCP, з назвою вузла);\",\n  \"example_upstream_tcp_port\": \"звичайний DNS (поверх TCP, з портом);\",\n  \"example_upstream_udp\": \"звичайний DNS (поверх UDP, з назвою вузла);\",\n  \"examples_title\": \"Зразки\",\n  \"fallback_dns_desc\": \"Список резервних DNS-серверів, які використовуються, коли upstream DNS-сервери не відповідають. Синтаксис такий самий, як і в полі upstream сервера вище.\",\n  \"fallback_dns_placeholder\": \"Вводьте один резервний DNS-сервер на рядок\",\n  \"fallback_dns_title\": \"Резервні DNS-сервери\",\n  \"faq\": \"Часті питання\",\n  \"fastest_addr\": \"Найшвидша IP-адреса\",\n  \"fastest_addr_desc\": \"Чекайте на відповіді від <b>усіх</b> DNS серверів, вимірюйте швидкість TCP з'єднання для кожного сервера та поверніть IP-адресу сервера з найшвидшою швидкістю з'єднання.<br/>Цей режим може суттєво уповільнити DNS запити, якщо один або декілька upstream серверів не відповідають. Переконайтеся, що ваші upstream сервери стабільні, а тайм-аут низький.\",\n  \"filter\": \"Фільтр\",\n  \"filter_added_successfully\": \"Фільтр успішно додано\",\n  \"filter_allowlist\": \"ПОПЕРЕДЖЕННЯ: Таким чином ви також виключите правило «{{disallowed_rule}}» зі списку дозволених клієнтів.\",\n  \"filter_category_general\": \"Загальні\",\n  \"filter_category_general_desc\": \"Списки, які блокують відстеження та рекламу на більшості пристроїв\",\n  \"filter_category_other\": \"Інші\",\n  \"filter_category_other_desc\": \"Інші списки блокувань\",\n  \"filter_category_regional\": \"Регіональні\",\n  \"filter_category_regional_desc\": \"Списки, орієнтовані на регіональні оголошення та сервери відстеження\",\n  \"filter_category_security\": \"Безпека\",\n  \"filter_category_security_desc\": \"Фільтри, які спеціалізуються на блокуванні зловмисних програм, фішингу та шахрайських доменів\",\n  \"filter_removed_successfully\": \"Фільтр успішно видалено\",\n  \"filter_updated\": \"Фільтр успішно оновлено\",\n  \"filtered\": \"Відфільтровано\",\n  \"filtered_custom_rules\": \"Відфільтровано завдяки власним правилам фільтрування\",\n  \"filtering_rules_learn_more\": \"<0>Як створити власні списки блокування</0>.\",\n  \"filters\": \"Фільтри\",\n  \"filters_and_hosts_hint\": \"AdGuard Home розуміє основні правила блокування і синтаксис файлів hosts.\",\n  \"filters_block_toggle_hint\": \"Ви можете налаштувати правила блокування в розділі <a>Фільтри</a>.\",\n  \"filters_configuration\": \"Конфігурація фільтрів\",\n  \"filters_enable\": \"Увімкнути фільтри\",\n  \"filters_interval\": \"Інтервал оновлення фільтрів\",\n  \"fix\": \"Виправити\",\n  \"for_last_days\": \"за останній {{count}} день\",\n  \"for_last_days_plural\": \"за останні {{count}} днів\",\n  \"for_last_hours\": \"за останню {{count}} годину\",\n  \"for_last_hours_plural\": \"за останні {{count}} годин\",\n  \"forgot_password\": \"Забули пароль?\",\n  \"forgot_password_desc\": \"Виконайте <0>ці кроки</0>, щоб створити новий пароль для свого імені користувача.\",\n  \"form_add_id\": \"Додати ідентифікатор\",\n  \"form_answer\": \"Введіть IP-адресу або доменне ім'я\",\n  \"form_client_name\": \"Введіть ім'я клієнта\",\n  \"form_domain\": \"Введіть доменне ім’я або підстановний знак\",\n  \"form_enter_blocked_response_ttl\": \"Введіть TTL заблокованої відповіді (секунди)\",\n  \"form_enter_host\": \"Введіть назву вузла\",\n  \"form_enter_hostname\": \"Введіть назву вузла\",\n  \"form_enter_id\": \"Введіть ідентифікатор\",\n  \"form_enter_ip\": \"Введіть IP\",\n  \"form_enter_mac\": \"Введіть MAC\",\n  \"form_enter_rate_limit\": \"Уведіть обмеження швидкості\",\n  \"form_enter_rate_limit_subnet_len\": \"Введіть довжину префікса підмережі для обмеження швидкості\",\n  \"form_enter_subnet_ip\": \"Введіть IP-адресу в підмережі «{{cidr}}»\",\n  \"form_enter_upstream_timeout\": \"Введіть тривалість тайм-ауту upstream сервера в секундах\",\n  \"form_error_answer_format\": \"Неправильний формат відповіді\",\n  \"form_error_client_id_format\": \"ID клієнта має містити лише цифри, малі букви та дефіси\",\n  \"form_error_domain_format\": \"Неправильний формат домену\",\n  \"form_error_equal\": \"Мають бути різні значення\",\n  \"form_error_gateway_ip\": \"Оренда не може мати IP-адресу шлюзу\",\n  \"form_error_ip4_format\": \"Неправильна IPv4-адреса\",\n  \"form_error_ip4_gateway_format\": \"Неправильна IPv4-адреса шлюзу\",\n  \"form_error_ip6_format\": \"Неправильна IPv6-адреса\",\n  \"form_error_ip_format\": \"Неправильна IP-адреса\",\n  \"form_error_mac_format\": \"Неправильна MAC-адреса\",\n  \"form_error_password\": \"Паролі не збігаються\",\n  \"form_error_password_length\": \"Пароль має містити від {{min}} до {{max}} символів\",\n  \"form_error_port\": \"Уведіть правильне значення порту\",\n  \"form_error_port_range\": \"Введіть значення порту в діапазоні 80−65535\",\n  \"form_error_port_unsafe\": \"Небезпечний порт\",\n  \"form_error_positive\": \"Повинно бути більше за 0\",\n  \"form_error_required\": \"Обов'язкове поле\",\n  \"form_error_server_name\": \"Неправильна назва сервера\",\n  \"form_error_subnet\": \"Підмережа «{{cidr}}» не містить IP-адресу «{{ip}}»\",\n  \"form_error_url_format\": \"Неправильний формат URL\",\n  \"form_error_url_or_path_format\": \"Неправильна URL-адреса або абсолютний шлях до списку\",\n  \"form_select_tags\": \"Виберіть теги клієнта\",\n  \"found_in_known_domain_db\": \"Знайдений у базі даних відомих доменів.\",\n  \"friday\": \"П'ятниця\",\n  \"friday_short\": \"ПТ\",\n  \"gateway_or_subnet_invalid\": \"Неправильна маска підмережі\",\n  \"general_settings\": \"Загальні налаштування\",\n  \"general_statistics\": \"Загальна статистика\",\n  \"get_started\": \"Розпочати\",\n  \"greater_range_start_error\": \"Має бути більшим за початкову адресу\",\n  \"homepage\": \"Домашня сторінка\",\n  \"host_whitelisted\": \"Вузол додано до списку дозволів\",\n  \"ignore_domains\": \"Ігноровані домени (по одному на рядок)\",\n  \"ignore_domains_desc_query\": \"Запити, які відповідають цим правилам, не записуються до журналу запитів\",\n  \"ignore_domains_desc_stats\": \"Запити, які відповідають цим правилам, в статистику не пишуться\",\n  \"ignore_domains_title\": \"Ігноровані домени\",\n  \"ignore_query_log\": \"Ігнорувати цей клієнт у журналі запитів\",\n  \"ignore_statistics\": \"Ігноруйте цей клієнт в статистиці\",\n  \"install_auth_confirm\": \"Підтвердьте пароль\",\n  \"install_auth_desc\": \"Необхідно налаштувати автентифікацію паролем для вебінтерфейсу AdGuard Home. Навіть якщо він доступний лише у вашій локальній мережі, важливо захистити його від необмеженого доступу.\",\n  \"install_auth_password\": \"Пароль\",\n  \"install_auth_password_enter\": \"Введіть пароль\",\n  \"install_auth_title\": \"Авторизація\",\n  \"install_auth_username\": \"Ім'я користувача\",\n  \"install_auth_username_enter\": \"Уведіть ім'я користувача\",\n  \"install_devices_address\": \"DNS-сервер AdGuard Home прослуховує наступні адреси\",\n  \"install_devices_android_list_1\": \"На головному екрані меню Android торкніться Налаштування.\",\n  \"install_devices_android_list_2\": \"У меню торкніться Wi-Fi. З'явиться екран із переліком усіх доступних мереж (неможливо встановити власний DNS для мобільного з'єднання).\",\n  \"install_devices_android_list_3\": \"Довго натисніть на мережу, до якої ви приєднані, та торкніться «Змінити мережу».\",\n  \"install_devices_android_list_4\": \"На деяких пристроях вам може знадобитися встановити прапорець Додатково, щоб побачити подальші налаштування. Щоб відредагувати налаштування DNS для Android, вам потрібно буде переключити налаштування IP з DHCP на статичні.\",\n  \"install_devices_android_list_5\": \"Змініть встановлені значення DNS 1 і DNS 2 на адреси вашого домашнього сервера AdGuard.\",\n  \"install_devices_desc\": \"Щоби розпочати використовувати AdGuard Home, вам потрібно налаштувати ваші пристої для його використання.\",\n  \"install_devices_ios_list_1\": \"На головному екрані торкніться Налаштування.\",\n  \"install_devices_ios_list_2\": \"Виберіть Wi-Fi у меню ліворуч (неможливо налаштувати DNS для мобільних мереж).\",\n  \"install_devices_ios_list_3\": \"Натисніть на назву поточної активної мережі.\",\n  \"install_devices_ios_list_4\": \"У полі DNS введіть адреси вашого сервера AdGuard Home.\",\n  \"install_devices_macos_list_1\": \"Клацніть на піктограму Apple і перейдіть до Системних налаштувань.\",\n  \"install_devices_macos_list_2\": \"Виберіть «Мережа».\",\n  \"install_devices_macos_list_3\": \"Виберіть перше з'єднання зі списку та натисніть кнопку Додатково.\",\n  \"install_devices_macos_list_4\": \"Виберіть вкладку DNS і введіть адреси сервера AdGuard Home.\",\n  \"install_devices_router\": \"Роутер\",\n  \"install_devices_router_desc\": \"Це налаштування буде автоматично охоплювати всі пристрої, що підʼєднано до домашнього маршрутизатора. Вам не потрібно буде налаштовувати кожен з них вручну.\",\n  \"install_devices_router_list_1\": \"Відкрийте налаштування маршрутизатора. Зазвичай ви можете отримати до нього доступ із браузера за допомогою URL-адреси, наприклад, http://192.168.0.1/ або http://192.168.1.1/. Можливо, треба буде ввести пароль. Якщо ви його не знаєте, часто можна скинути пароль, натиснувши кнопку на самому маршрутизаторі. Для деяких маршрутизаторів потрібна спеціальна програма, яка в такому випадку повинна бути вже встановлена на вашому комп’ютері чи телефоні.\",\n  \"install_devices_router_list_2\": \"Знайдіть налаштування DHCP/DNS. Шукайте літери DNS поруч із полем, в яке можна ввести два або три набори чисел, кожен з яких розбитий на чотири групи від однієї до трьох цифр.\",\n  \"install_devices_router_list_3\": \"Введіть туди адреси вашого домашнього сервера AdGuard.\",\n  \"install_devices_router_list_4\": \"Ви не можете встановити власний DNS-сервер на деяких типах маршрутизаторів. У цьому разі вам може допомогти налаштування AdGuard Home в якості <0>DHCP-сервера</0>. В іншому разі вам потрібно знайти інструкцію щодо налаштування DNS-сервера для вашої конкретної моделі маршрутизатора.\",\n  \"install_devices_title\": \"Налаштуйте ваші пристрої\",\n  \"install_devices_windows_list_1\": \"Відкрийте Панель керування через меню «Пуск» або пошук Windows.\",\n  \"install_devices_windows_list_2\": \"Перейдіть до категорії Мережа й Інтернет, а потім до Центру мереж і спільного доступу.\",\n  \"install_devices_windows_list_3\": \"Зліва на екрані натисніть на «Змінити налаштування адаптера».\",\n  \"install_devices_windows_list_4\": \"Клацніть на активному з'єднанні правою кнопкою миші та виберіть «Властивості».\",\n  \"install_devices_windows_list_5\": \"Знайдіть у списку пункт «Internet Protocol Version 4 (TCP/IPv4)» або «Internet Protocol Version 6 (TCP/IPv6)», виберіть його та натисніть кнопку Властивості ще раз.\",\n  \"install_devices_windows_list_6\": \"Виберіть «Використовувати наступні адреси DNS-серверів» та введіть адреси вашого сервера AdGuard Home.\",\n  \"install_saved\": \"Збережено успішно\",\n  \"install_settings_all_interfaces\": \"Усі інтерфейси\",\n  \"install_settings_dns\": \"DNS-сервер\",\n  \"install_settings_dns_desc\": \"Вам потрібно буде налаштувати свої пристрої або маршрутизатор для використання DNS-сервера за такими адресами:\",\n  \"install_settings_interface_link\": \"Вебінтерфейс адміністратора AdGuard Home буде доступний за такими адресами:\",\n  \"install_settings_listen\": \"Мережевий інтерфейс\",\n  \"install_settings_port\": \"Порт\",\n  \"install_settings_title\": \"Вебінтерфейс адміністратора\",\n  \"install_static_configure\": \"AdGuard Home виявив, що використовується динамічна IP-адреса — <0>{{ip}}</0>. Ви хочете встановити її як свою статичну адресу?\",\n  \"install_static_error\": \"AdGuard Home не може налаштувати його автоматично для цього мережевого інтерфейсу. Будь ласка, шукайте інструкції як це зробити вручну.\",\n  \"install_static_ok\": \"Гарні новини! Статична IP-адреса вже налаштована\",\n  \"install_step\": \"Крок\",\n  \"install_submit_desc\": \"Процедура налаштування завершена і тепер все готово, аби почати користуватися AdGuard Home.\",\n  \"install_submit_title\": \"Вітаємо!\",\n  \"install_welcome_desc\": \"AdGuard Home — це мережевий DNS-сервер, що блокує рекламу та відстеження. Його мета — надати вам контроль над усією мережею та всіма пристроями в ній без потреби використання програми на стороні клієнта.\",\n  \"install_welcome_title\": \"Вітаємо в AdGuard Home!\",\n  \"interval_24_hour\": \"24 години\",\n  \"interval_6_hour\": \"6 годин\",\n  \"interval_days\": \"{{count}} день\",\n  \"interval_days_plural\": \"{{count}} дні(в)\",\n  \"interval_hours\": \"{{count}} година\",\n  \"interval_hours_plural\": \"{{count}} годин(и)\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP-адреса\",\n  \"known_tracker\": \"Відомі трекери\",\n  \"last_rule_in_allowlist\": \"Неможливо заблокувати цього клієнта, тому що правило «{{disallowed_rule}}» ВИМКНЕ режим списку дозволів.\",\n  \"last_time_updated_table_header\": \"Востаннє оновлено\",\n  \"list_confirm_delete\": \"Ви впевнені, що хочете видалити цей список?\",\n  \"list_label\": \"Список\",\n  \"list_updated\": \"{{count}} список оновлено\",\n  \"list_updated_plural\": \"{{count}} списки оновлено\",\n  \"list_url_table_header\": \"URL списку\",\n  \"load_balancing\": \"Балансування навантаження\",\n  \"load_balancing_desc\": \"Виконуйте запити по одному upstream серверу за раз.<br/>AdGuard Home використовує зважений випадковий алгоритм, щоб вибрати сервери з найменшою кількістю невдалих пошуків і найменшим середнім часом пошуку.\",\n  \"loading_table_status\": \"Завантаження...\",\n  \"local_ptr_default_resolver\": \"Стандартно AdGuard Home користується такими зворотними DNS-вирішувачами: {{ip}}.\",\n  \"local_ptr_desc\": \"DNS-сервери, які AdGuard Home використовує для приватних запитів PTR, SOA та NS. Запит вважається приватним, якщо він запитує домен ARPA, що містить підмережу в межах приватних діапазонів IP (наприклад, «192.168.12.34») і надходить від клієнта з приватною IP-адресою. Якщо не встановлено, використовуватимуться стандартні DNS-перетворювачі вашої ОС, за винятком домашніх IP-адрес AdGuard.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home не зміг визначити приватні зворотні DNS-вирішувачі, які підійшли б для цієї системи.\",\n  \"local_ptr_placeholder\": \"Вводьте одну адресу на рядок\",\n  \"local_ptr_title\": \"Приватні сервери для зворотного DNS\",\n  \"location\": \"Місцезнаходження\",\n  \"log_and_stats_section_label\": \"Журнал запитів і статистика\",\n  \"lower_range_start_error\": \"Має бути меншим за початкову адресу\",\n  \"main_settings\": \"Головні налаштування\",\n  \"make_static\": \"Зробити статичним\",\n  \"manual_update\": \"Щоб оновити самостійно, <a>виконайте ці кроки</a>.\",\n  \"milliseconds_abbreviation\": \"мс\",\n  \"monday\": \"Понеділок\",\n  \"monday_short\": \"ПН\",\n  \"name\": \"Ім'я\",\n  \"name_table_header\": \"Назва\",\n  \"netname\": \"Назва мережі\",\n  \"network\": \"Мережа\",\n  \"new_allowlist\": \"Новий список дозволів\",\n  \"new_blocklist\": \"Новий список блокування\",\n  \"next\": \"Наступні\",\n  \"next_btn\": \"Далі\",\n  \"no_blocklist_added\": \"Списків блокування не додано\",\n  \"no_clients_found\": \"Клієнтів не знайдено\",\n  \"no_domains_found\": \"Не знайдено жодного домену\",\n  \"no_logs_found\": \"Немає записів\",\n  \"no_servers_specified\": \"Сервери не вказано\",\n  \"no_upstreams_data_found\": \"Немає даних про upstream-сервери\",\n  \"no_whitelist_added\": \"Списків дозволів не додано\",\n  \"nothing_found\": \"Нічого не знайдено...\",\n  \"null_ip\": \"Нульовий IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"Кількість DNS-запитів, заблокованих фільтрами і списками блокування hosts\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Кількість заблокованих вебсайтів для дорослих\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Кількість DNS-запитів, заблокованих модулем «Безпека перегляду» AdGuard\",\n  \"number_of_dns_query_days\": \"Кількість DNS-запитів, оброблених за останні {{count}} дні\",\n  \"number_of_dns_query_days_plural\": \"Кількість DNS-запитів, оброблених за останні {{count}} днів\",\n  \"number_of_dns_query_hours\": \"Кількість DNS-запитів, оброблених за останню {{count}} годину\",\n  \"number_of_dns_query_hours_plural\": \"Кількість DNS-запитів, оброблених за останні {{count}} годин\",\n  \"number_of_dns_query_to_safe_search\": \"Кількість DNS-запитів до пошукових систем, для яких примусово застосований безпечний пошук\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"ВИМК\",\n  \"on\": \"УВІМК\",\n  \"open_dashboard\": \"Відкрити інформаційну панель\",\n  \"orgname\": \"Назва організації\",\n  \"original_response\": \"Оригінальна відповідь\",\n  \"out_of_range_error\": \"Не повинна бути в діапазоні «{{start}}»−«{{end}}»\",\n  \"page_table_footer_text\": \"Сторінка\",\n  \"parallel_requests\": \"Паралельні запити\",\n  \"parental_control\": \"Батьківський контроль\",\n  \"password_label\": \"Пароль\",\n  \"password_placeholder\": \"Введіть пароль\",\n  \"plain_dns\": \"Звичайний DNS\",\n  \"port_53_faq_link\": \"Порт 53 часто зайнятий службами «DNSStubListener» або «systemd-resolved». <0>Як це вирішити</0>.\",\n  \"previous_btn\": \"Назад\",\n  \"privacy_policy\": \"Політика конфіденційності\",\n  \"processing_update\": \"Зачекайте будь ласка, AdGuard Home оновлюється\",\n  \"protection_section_label\": \"Захист\",\n  \"protocol\": \"Протокол\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Журнал запитів\",\n  \"query_log_clear\": \"Очистити журнал запитів\",\n  \"query_log_cleared\": \"Журнал запитів успішно очищено\",\n  \"query_log_configuration\": \"Конфігурація журналу\",\n  \"query_log_confirm_clear\": \"Ви впевнені, що хочете цілком очистити журнал запитів?\",\n  \"query_log_disabled\": \"Журнал запитів вимкнений. Конфігурацію можна змінити в <0>налаштуваннях</0>\",\n  \"query_log_enable\": \"Увімкнути журнал\",\n  \"query_log_filtered\": \"Фільтровано з {{filter}}\",\n  \"query_log_response_status\": \"Стан: {{value}}\",\n  \"query_log_retention\": \"Час зберігання журналу\",\n  \"query_log_retention_confirm\": \"Ви дійсно хочете змінити час зберігання журналу? Якщо ви зменшите значення, деякі дані будуть втрачені\",\n  \"query_log_strict_search\": \"Використовуйте подвійні лапки для точного пошуку\",\n  \"query_log_updated\": \"Журнал запитів успішно оновлено\",\n  \"rate_limit\": \"Обмеження швидкості\",\n  \"rate_limit_desc\": \"Кількість запитів в секунду, які може робити один клієнт. Встановлене значення «0» означатиме необмежену кількість.\",\n  \"rate_limit_subnet_len_ipv4\": \"Довжина префікса підмережі для адрес IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Довжина префікса підмережі для адрес IPv4, які використовуються для обмеження швидкості. Типовим значенням є 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Довжина префікса підмережі IPv4 має бути від 0 до 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Довжина префікса підмережі для адрес IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Довжина префікса підмережі для адрес IPv6, які використовуються для обмеження швидкості. Типовим значенням є 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Довжина префікса підмережі IPv6 має бути від 0 до 128\",\n  \"rate_limit_whitelist\": \"Список дозволених обмежень швидкості\",\n  \"rate_limit_whitelist_desc\": \"IP-адреси, на які не поширюється обмеження швидкості\",\n  \"rate_limit_whitelist_placeholder\": \"Вводьте одну адресу на рядок\",\n  \"refresh_btn\": \"Оновити\",\n  \"refresh_statics\": \"Оновити статистику\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Повідомити про проблему\",\n  \"request_details\": \"Деталі запиту\",\n  \"request_table_header\": \"Запит\",\n  \"requests_count\": \"Кількість запитів\",\n  \"reset_settings\": \"Скинути налаштування\",\n  \"resolve_clients_desc\": \"Визначати доменні імена клієнтів за допомогою PTR-запитів до відповідних серверів — приватних DNS-серверів для локальних клієнтів та upstream-серверів для клієнтів з публічними IP-адресами.\",\n  \"resolve_clients_title\": \"Увімкнути зворотне вирішення IP-адрес клієнтів\",\n  \"response_code\": \"Код відповіді\",\n  \"response_details\": \"Деталі відповіді\",\n  \"response_table_header\": \"Відповідь\",\n  \"response_time\": \"Час відгуку\",\n  \"rewrite_A\": \"<0>A</0>: спеціальне значення, зберігайте <0>A</0> записи із вищого сервера\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: спеціальне значення, зберігайте <0>AAAA</0> записи із вищого сервера\",\n  \"rewrite_add\": \"Додати перезапис DNS\",\n  \"rewrite_added\": \"Перезапис DNS для «{{key}}» успішно додано\",\n  \"rewrite_applied\": \"Застосовано правило перезапису\",\n  \"rewrite_confirm_delete\": \"Ви впевнені, що хочете видалити перезапис DNS для «{{key}}»?\",\n  \"rewrite_deleted\": \"Перезапис DNS для «{{key}}» успішно видалено\",\n  \"rewrite_desc\": \"Дозволяє легко налаштувати власну відповідь DNS для певного доменного імені.\",\n  \"rewrite_domain_name\": \"Доменне ім’я: додайте запис CNAME\",\n  \"rewrite_edit\": \"Редагувати перезапис DNS\",\n  \"rewrite_hosts_applied\": \"Перезаписано правилом hosts-файлу\",\n  \"rewrite_ip_address\": \"IP-адреса: використайте цю IP-адресу у відповіді A або AAAA\",\n  \"rewrite_not_found\": \"Перезаписів DNS не знайдено\",\n  \"rewrite_settings_updated\": \"Налаштування перезапису DNS успішно оновлено\",\n  \"rewrite_updated\": \"Перезапис DNS успішно оновлено\",\n  \"rewrites_disabled_table_header\": \"Перезаписи вимкнено\",\n  \"rewrites_enabled_table_header\": \"Перезаписи ввімкнено\",\n  \"rewritten\": \"Перезаписано\",\n  \"rows_table_footer_text\": \"рядків\",\n  \"rule_added_to_custom_filtering_toast\": \"Правило додано до власних правил фільтрування: {{rule}}\",\n  \"rule_label\": \"Правило(-а)\",\n  \"rule_removed_from_custom_filtering_toast\": \"Правило вилучено з власних правил фільтрування: {{rule}}\",\n  \"rules_count_table_header\": \"Кількість правил\",\n  \"safe_browsing\": \"Безпечний перегляд\",\n  \"safe_search\": \"Безпечний пошук\",\n  \"saturday\": \"Субота\",\n  \"saturday_short\": \"СБ\",\n  \"save_btn\": \"Зберегти\",\n  \"save_config\": \"Зберегти конфігурацію\",\n  \"schedule_add\": \"Додати розклад\",\n  \"schedule_current_timezone\": \"Поточний часовий пояс: {{value}}\",\n  \"schedule_desc\": \"Установка періодів паузи блокування сервісів\",\n  \"schedule_edit\": \"Редагувати розклад\",\n  \"schedule_from\": \"З\",\n  \"schedule_invalid_select\": \"Час початку має бути завчасно закінчення\",\n  \"schedule_modal_description\": \"Цей розклад замінить усі наявні розклади на той самий день тижня. Кожен день тижня може мати тільки один період бездіяльності.\",\n  \"schedule_modal_time_off\": \"Вимкнення блокування сервісів:\",\n  \"schedule_new\": \"Новий розклад\",\n  \"schedule_remove\": \"Видалити розклад\",\n  \"schedule_save\": \"Зберегти розклад\",\n  \"schedule_select_days\": \"Вибрати дні\",\n  \"schedule_services\": \"Пауза блокування сервісів\",\n  \"schedule_services_desc\": \"Налаштування розкладу паузи фільтра блокування сервісів\",\n  \"schedule_services_desc_client\": \"Налаштування розкладу паузи фільтра блокування сервісів для даного клієнта\",\n  \"schedule_time_all_day\": \"Увесь день\",\n  \"schedule_timezone\": \"Вибрати часовий пояс\",\n  \"schedule_to\": \"До\",\n  \"served_from_cache_label\": \"Отримано з кешу\",\n  \"service_name\": \"Назва сервісу\",\n  \"set_static_ip\": \"Встановити статичну IP-адресу\",\n  \"settings\": \"Налаштування\",\n  \"settings_custom\": \"Власні\",\n  \"settings_global\": \"Загальні\",\n  \"setup_config_to_enable_dhcp_server\": \"Налаштуйте конфігурацію для увімкнення DHCP-сервера\",\n  \"setup_dns_notice\": \"Для використання <1>DNS-over-HTTPS</1> або <1>DNS-over-TLS</1>, вам потрібно <0>налаштувати Шифрування</0> в налаштуваннях AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS: </0>Використайте рядок <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Використайте рядок <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Ось перелік програмного забезпечення, яке можете використати.</0>\",\n  \"setup_dns_privacy_4\": \"На пристрої iOS 14 або macOS Big Sur ви можете завантажити спеціальний файл .mobileconfig, який додасть до налаштувань DNS сервери <highlight>DNS-over-HTTPS</highlight> або <highlight>DNS-over-TLS</highlight>.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 підтримує DNS-over-TLS. Щоб його налаштувати, перейдіть у Налаштування → Мережа та Інтернет → Додатково → Приватний DNS і введіть там свій домен.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard для Android</0> підтримує <1>DNS-over-HTTPS</1> і <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> додає підтримку <1>DNS-over-HTTPS</1> для Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Конфігурація для iOS та macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> підтримує <1>DNS-over-HTTPS</1>, але для того, щоб налаштувати його на використання власного сервера, вам потрібно буде створити для нього <2>штамп DNS</2>.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard для iOS</0> підтримує налаштування <1>DNS over-HTTPS</1> і <1>DNS over over TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"Сам AdGuard Home може слугувати захищеним клієнтом DNS на будь-якій платформі.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> підтримує всі відомі захищені протоколи DNS.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> підтримує <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> підтримує <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Ви знайдете більше реалізацій <0>тут</0> та <1>тут</1>.\",\n  \"setup_dns_privacy_other_title\": \"Інші реалізації\",\n  \"setup_guide\": \"Посібник з налаштування\",\n  \"show_all_filter_type\": \"Показати все\",\n  \"show_blocked_responses\": \"Заблоковані\",\n  \"show_filtered_type\": \"Показати фільтровані\",\n  \"show_processed_responses\": \"Оброблені\",\n  \"show_whitelisted_responses\": \"Дозволені\",\n  \"sign_in\": \"Увійти\",\n  \"sign_out\": \"Вийти\",\n  \"source_label\": \"Джерело\",\n  \"static_ip\": \"Статична IP-адреса\",\n  \"static_ip_desc\": \"AdGuard Home - це сервер, тому йому потрібна статична IP-адреса для нормальної роботи. В іншому випадку, в певний момент, ваш маршрутизатор може призначити іншу IP-адресу цьому пристрою.\",\n  \"statistics_clear\": \"Очистити статистику\",\n  \"statistics_clear_confirm\": \"Ви впевнені, що хочете очистити статистику?\",\n  \"statistics_cleared\": \"Статистику успішно очищено\",\n  \"statistics_configuration\": \"Налаштування статистики\",\n  \"statistics_enable\": \"Увімкнути статистику\",\n  \"statistics_retention\": \"Збереження статистики\",\n  \"statistics_retention_confirm\": \"Ви впевнені, що хочете змінити тривалість статистики? Якщо зменшити значення інтервалу, деякі дані будуть втрачені\",\n  \"statistics_retention_desc\": \"Якщо зменшити значення інтервалу, деякі дані будуть втрачені\",\n  \"stats_adult\": \"Заблоковано вебсайтів для дорослих\",\n  \"stats_disabled\": \"Статистику вимкнено. Ви можете увімкнути її на <0>сторінці налаштувань</0>.\",\n  \"stats_disabled_short\": \"Статистику вимкнено\",\n  \"stats_malware_phishing\": \"Заблоковано зловмисних/шахрайських програм\",\n  \"stats_params\": \"Налаштування статистики\",\n  \"stats_query_domain\": \"Найчастіші запити доменів\",\n  \"subnet_error\": \"Адреси повинні бути в одній підмережі\",\n  \"sunday\": \"Неділя\",\n  \"sunday_short\": \"НД\",\n  \"system_host_files\": \"Системні hosts-файли\",\n  \"table_client\": \"Клієнт\",\n  \"table_name\": \"Назва\",\n  \"tags_desc\": \"Ви можете вибрати теги, які відповідають клієнту. Теги можна використати в правилах фільтрування, щоб точніше застосовувати їх. <0>Докладніше</0>.\",\n  \"tags_title\": \"Теги\",\n  \"test_upstream_btn\": \"Перевірити сервери\",\n  \"theme_auto\": \"Авто\",\n  \"theme_auto_desc\": \"Автоматична (на основі теми вашого пристрою)\",\n  \"theme_dark\": \"Темна\",\n  \"theme_dark_desc\": \"Темна тема\",\n  \"theme_light\": \"Світла\",\n  \"theme_light_desc\": \"Світла тема\",\n  \"thursday\": \"Четвер\",\n  \"thursday_short\": \"ЧТ\",\n  \"time_table_header\": \"Час\",\n  \"top_blocked_domains\": \"Найчастіше блоковані домени\",\n  \"top_clients\": \"Найактивніші клієнти\",\n  \"top_upstreams\": \"Часто запитувані upstream-сервери\",\n  \"topline_expired_certificate\": \"Термін дії вашого сертифіката SSL закінчився. Оновіть <0>Налаштування шифрування</0>.\",\n  \"topline_expiring_certificate\": \"Ваш сертифікат SSL скоро закінчиться. Оновіть <0>Налаштування шифрування</0>.\",\n  \"tracker_source\": \"Джерело відстежувача\",\n  \"try_again\": \"Спробувати знову\",\n  \"ttl_cache_validation\": \"Мінімальне TTL-значення має бути меншим або рівним максимальному значенню\",\n  \"tuesday\": \"Вівторок\",\n  \"tuesday_short\": \"ВТ\",\n  \"type_table_header\": \"Тип\",\n  \"unavailable_dhcp\": \"DHCP недоступний\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home не може запустити DHCP-сервер у вашій ОС\",\n  \"unblock\": \"Дозволити\",\n  \"unblock_all\": \"Розблокувати все\",\n  \"unblock_for_this_client_only\": \"Дозволити тільки цей клієнт\",\n  \"unknown_filter\": \"Невідомий фільтр {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} тепер доступний! <0>Докладніше</0>.\",\n  \"update_failed\": \"Помилка автоматичного оновлення. Будь ласка, <a>виконайте ці кроки</a> аби оновити вручну.\",\n  \"update_now\": \"Оновити зараз\",\n  \"updated_custom_filtering_toast\": \"Власні правила фільтрування успішно збережено\",\n  \"updated_save_search_toast\": \"Налаштування Безпечного пошуку оновлено\",\n  \"updated_upstream_dns_toast\": \"DNS-сервери успішно збережено\",\n  \"updates_checked\": \"Доступна нова версія AdGuard Home\",\n  \"updates_version_equal\": \"AdGuard Home останньої версії\",\n  \"upstream\": \"Upstream-сервер\",\n  \"upstream_dns\": \"Upstream DNS-сервери\",\n  \"upstream_dns_cache_configuration\": \"Конфігурація кешу upstream DNS-серверів\",\n  \"upstream_dns_client_desc\": \"Якщо це поле залишатиметься порожнім, AdGuard Home використовуватиме сервери, вказані в <0>налаштуваннях DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Налаштовано в {{path}}\",\n  \"upstream_dns_help\": \"Введіть адреси серверів по одній на рядок. <a>Докладніше</a> про налаштування DNS-серверів.\",\n  \"upstream_parallel\": \"Використовувати паралельні запити, щоб пришвидшити вирішення одночасною чергою всіх оригінальних серверів.\",\n  \"upstream_timeout\": \"Час вийшов для upstream\",\n  \"upstream_timeout_desc\": \"Визначає кількість секунд, які потрібно чекати на відповідь від upstream сервера\",\n  \"upstreams\": \"Upstreams\",\n  \"use_adguard_browsing_sec\": \"Використовувати вебслужбу «Безпека перегляду» AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home перевірятиме, чи підлягає домен блокуванню завдяки вебслужбі «Безпека перегляду». Для перевірки буде використано безпечний API — на сервер надсилається лише короткий префікс хешу SHA256 доменного імені.\",\n  \"use_adguard_parental\": \"Використовувати вебслужбу «Батьківський контроль» AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home перевірить, чи містить домен матеріали для дорослих. Буде використано той же безпечний API, що й для «Безпеки перегляду» AdGuard.\",\n  \"use_private_ptr_resolvers_desc\": \"Розвʼязувати запити PTR, SOA та NS для доменів ARPA, що містять приватні IP-адреси, через приватні вихідні сервери, DHCP, /etc/hosts тощо. Якщо вимкнено, AdGuard Home відповідатиме на всі такі запити з NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Використовувати приватні зворотні DNS-резолвери\",\n  \"use_saved_key\": \"Використати раніше збережений ключ\",\n  \"username_label\": \"Ім'я користувача\",\n  \"username_placeholder\": \"Уведіть ім'я користувача\",\n  \"validated_with_dnssec\": \"Засвідчено DNSSEC\",\n  \"version\": \"Версія\",\n  \"version_request_error\": \"Не вдалося перевірити оновлення. Будь ласка, перевірте з'єднання з інтернетом.\",\n  \"wednesday\": \"Середа\",\n  \"wednesday_short\": \"СР\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/vi.json",
    "content": "{\n  \"access_allowed_desc\": \"Danh sách CIDR, địa chỉ IP hoặc <a>ClientID</a>. Nếu danh sách này có các mục nhập, AdGuard Home sẽ chỉ chấp nhận yêu cầu từ những khách hàng này.\",\n  \"access_allowed_title\": \"Máy chủ được phép\",\n  \"access_blocked_desc\": \"Đừng nhầm lẫn điều này với các bộ lọc. AdGuard Home sẽ bỏ các truy vấn DNS với các tên miền này trong câu hỏi của truy vấn.\",\n  \"access_blocked_title\": \"Tên miền bị chặn\",\n  \"access_desc\": \"Tại đây bạn có thể định cấu hình quy tắc truy cập cho máy chủ AdGuard Home DNS\",\n  \"access_disallowed_desc\": \"Danh sách CIDR, địa chỉ IP hoặc <a>ClientID</a>. Nếu danh sách này có các mục nhập, AdGuard Home sẽ loại bỏ các yêu cầu từ những khách hàng này. Trường này bị bỏ qua nếu có các mục nhập trong máy khách Được phép.\",\n  \"access_disallowed_title\": \"Máy chủ không được phép\",\n  \"access_settings_saved\": \"Cài đặt truy cập đã lưu thành công\",\n  \"access_title\": \"Cài đặt truy cập\",\n  \"actions_table_header\": \"Thao tác\",\n  \"add_allowlist\": \"Thêm danh sách\",\n  \"add_blocklist\": \"Thêm danh sách\",\n  \"add_custom_list\": \"Thêm bộ lọc tùy chọn\",\n  \"add_persistent_client\": \"Thêm làm ứng dụng khách liên tục\",\n  \"address\": \"địa chỉ\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home sẽ loại bỏ tất cả các truy vấn DNS từ ứng dụng khách này.\",\n  \"all_lists_up_to_date_toast\": \"Tất cả danh sách đã ở phiên bản mới nhất\",\n  \"all_queries\": \"Tất cả truy vấn\",\n  \"allow_this_client\": \"Cho phép ứng dụng khách này\",\n  \"allowed\": \"Được phép\",\n  \"anonymize_client_ip\": \"Ẩn danh IP khách\",\n  \"anonymize_client_ip_desc\": \"Không lưu địa chỉ IP đầy đủ của khách hàng trong nhật ký và thống kê\",\n  \"anonymizer_notification\": \"<0> Lưu ý:</0> Tính năng ẩn danh IP được bật. Bạn có thể tắt nó trong <1> Cài đặt chung</1>.\",\n  \"answer\": \"Trả lời\",\n  \"apply_btn\": \"Áp dụng\",\n  \"auto_clients_desc\": \"Thông tin về địa chỉ IP của thiết bị đang sử dụng hoặc có thể sử dụng AdGuard Home. Thông tin này được thu thập từ nhiều nguồn, bao gồm tệp máy chủ, DNS ngược, v.v.\",\n  \"auto_clients_title\": \"Máy khách (thời gian chạy)\",\n  \"autofix_warning_list\": \"Nó sẽ thực hiện các tác vụ sau: <0> Hủy kích hoạt hệ thống DNSStubListener </0> <0> Đặt địa chỉ máy chủ DNS thành 127.0.0.1 </0> <0> Thay thế mục tiêu liên kết tượng trưng của /etc/resolv.conf bằng / run / systemd /resolve/resolv.conf </0> <0> Dừng DNSStubListener (tải lại dịch vụ do hệ thống phân giải) </0>\",\n  \"autofix_warning_result\": \"Do đó, tất cả các yêu cầu DNS từ hệ thống của bạn sẽ được AdGuard Home xử lý theo mặc định.\",\n  \"autofix_warning_text\": \"Nếu bạn nhấp vào \\\"Khắc phục\\\", AdGuard Home sẽ định cấu hình hệ thống của bạn để sử dụng máy chủ DNS của AdGuard Home.\",\n  \"average_processing_time\": \"Thời gian xử lý trung bình\",\n  \"average_processing_time_hint\": \"Thời gian trung bình cho một yêu cầu DNS tính bằng mili giây\",\n  \"average_upstream_response_time\": \"Thời gian phản hồi trung bình từ máy chủ thượng nguồn\",\n  \"back\": \"Quay lại\",\n  \"block\": \"Chặn\",\n  \"block_all\": \"Chặn tất cả\",\n  \"block_domain_use_filters_and_hosts\": \"Chặn tên miền sử dụng các bộ lọc và file hosts\",\n  \"block_for_this_client_only\": \"Chỉ chặn ứng dụng khách này\",\n  \"block_services\": \"Chặn các dịch vụ cụ thể\",\n  \"blocked_adult_websites\": \"Bị chặn bởi Quản lý của Phụ huynh\",\n  \"blocked_by\": \"<0>Chặn bởi Bộ lọc</0>\",\n  \"blocked_by_cname_or_ip\": \"Đã bị chặn bởi CNAME hoặc IP\",\n  \"blocked_by_response\": \"Chặn bởi CNAME hoặc địa IP ở phản hồi\",\n  \"blocked_response_ttl\": \"Chặn phản hồi TTL\",\n  \"blocked_response_ttl_desc\": \"Chỉ định trong bao nhiêu giây máy khách sẽ lưu vào bộ đệm một phản hồi đã được lọc\",\n  \"blocked_safebrowsing\": \"Chặn bởi Safebrowsing\",\n  \"blocked_service\": \"Dịch vụ bị chặn\",\n  \"blocked_services\": \"Dịch vụ bị chặn\",\n  \"blocked_services_desc\": \"Cho phép nhanh chóng chặn các trang web và dịch vụ phổ biến.\",\n  \"blocked_services_global\": \"Sử dụng các dịch vụ bị chặn toàn cầu\",\n  \"blocked_services_saved\": \"Dịch vụ bị chặn đã lưu thành công\",\n  \"blocked_threats\": \"Mối nguy hiểm đã chặn\",\n  \"blocking_ipv4\": \"Chặn IPv4\",\n  \"blocking_ipv4_desc\": \"Địa chỉ IP được trả lại cho một yêu cầu A bị chặn\",\n  \"blocking_ipv6\": \"Chặn IPv6\",\n  \"blocking_ipv6_desc\": \"Địa chỉ IP được trả lại cho một yêu cầu AAA bị chặn\",\n  \"blocking_mode\": \"Chế độ chặn\",\n  \"blocking_mode_custom_ip\": \"IP tùy chỉnh: Phản hồi với địa chỉ IP đã được tiết lập\",\n  \"blocking_mode_default\": \"Mặc định: Trả lời với NXDOMAIN khi bị chặn bởi quy tắc kiểu Adblock; phản hồi với địa chỉ IP được chỉ định trong quy tắc khi bị chặn bởi quy tắc / etc / hosts-style\",\n  \"blocking_mode_null_ip\": \"Null IP: Trả lời bằng không địa chỉ IP (0.0.0.0 cho A; :: cho AAAA)\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN: Phản hổi với mã NXDOMAIN\",\n  \"blocking_mode_refused\": \"REFUSED: Trả lời bằng mã REFUSED\",\n  \"blocklist\": \"Danh sách chặn\",\n  \"bootstrap_dns\": \"Máy chủ DNS Bootstrap\",\n  \"bootstrap_dns_desc\": \"Địa chỉ IP của máy chủ DNS được sử dụng để phân giải địa chỉ IP của trình phân giải DoH/DoT mà bạn chỉ định làm thượng nguồn. Bình luận không được phép.\",\n  \"cache_cleared\": \"Đã xóa thành công bộ đệm DNS\",\n  \"cache_enabled\": \"Bật bộ nhớ đệm\",\n  \"cache_enabled_desc\": \"Lưu trữ phản hồi DNS cục bộ.\",\n  \"cache_optimistic\": \"Bộ nhớ đệm lạc quan\",\n  \"cache_optimistic_desc\": \"Làm cho AdGuard Home phản hồi từ bộ nhớ cache ngay cả khi các mục nhập đã hết hạn và cố gắng làm mới chúng.\",\n  \"cache_size\": \"Kích thước cache\",\n  \"cache_size_desc\": \"Kích thước cache DNS (bytes).\",\n  \"cache_size_validation\": \"Kích thước bộ nhớ đệm phải lớn hơn 0 khi được bật.\",\n  \"cache_ttl_max_override\": \"Ghi đè TTL tối đa\",\n  \"cache_ttl_max_override_desc\": \"Đặt giá trị thời gian tồn tại tối đa (giây) cho các mục nhập trong bộ nhớ cache DNS.\",\n  \"cache_ttl_min_override\": \"Ghi đè TTL tối thiểu\",\n  \"cache_ttl_min_override_desc\": \"Mở rộng giá trị thời gian tồn tại ngắn (giây) nhận được từ máy chủ ngược dòng khi phản hồi DNS vào bộ nhớ đệm.\",\n  \"cancel_btn\": \"Huỷ\",\n  \"category_label\": \"Thể loại\",\n  \"check\": \"Kiểm tra\",\n  \"check_client_id\": \"Định danh khách hàng (ClientID hoặc Địa chỉ IP)\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"Kiểm tra xem tên miền có tồn tại trong các bộ lọc không.\",\n  \"check_dhcp_servers\": \"Kiểm tra máy chủ DHCP\",\n  \"check_dns_record\": \"Chọn loại bản ghi DNS\",\n  \"check_enter_client_id\": \"Nhập định danh khách hàng\",\n  \"check_hostname\": \"Tên máy chủ hoặc tên miền\",\n  \"check_ip\": \"Địa chỉ IP: {{ip}}\",\n  \"check_not_found\": \"Không tìm thấy trong danh sách bộ lọc của bạn\",\n  \"check_reason\": \"Lý do: {{reason}}\",\n  \"check_service\": \"Tên dịch vụ: {{service}}\",\n  \"check_title\": \"Kiểm tra bộ lọc\",\n  \"check_updates_btn\": \"Kiểm tra cập nhật\",\n  \"check_updates_now\": \"Kiểm tra cập nhật ngay bây giờ\",\n  \"choose_allowlist\": \"Chọn danh sách cho phép\",\n  \"choose_blocklist\": \"Chọn danh sách chặn\",\n  \"choose_from_list\": \"Chọn từ danh sách\",\n  \"city\": \"Thành phố\",\n  \"clear_cache\": \"Xóa bộ nhớ cache\",\n  \"click_to_view_queries\": \"Nhấp để xem truy xuất\",\n  \"client_add\": \"Thêm Máy Khách\",\n  \"client_added\": \"Máy khách \\\"{{key}}\\\" đã thêm thành công\",\n  \"client_blocked\": \"Đã chặn người dùng {{ip}}\",\n  \"client_confirm_block\": \"Bạn có muốn chặn người dùng {{ip}}?\",\n  \"client_confirm_delete\": \"Bạn có chắc chắn muốn xóa máy khách \\\"{{key}}\\\" không?\",\n  \"client_confirm_unblock\": \"Bạn có muốn bỏ chặn người dùng {{ip}}?\",\n  \"client_deleted\": \"Máy khách \\\"{{key}}\\\" đã xóa thành công\",\n  \"client_details\": \"Thông tin máy khách\",\n  \"client_edit\": \"Chỉnh Sửa Máy Khách\",\n  \"client_global_settings\": \"Sử dụng cài đặt toàn cầu\",\n  \"client_id\": \"ClientID\",\n  \"client_id_desc\": \"Khách hàng có thể được xác định bằng ClientID. Tìm hiểu thêm về cách xác định khách hàng <a> tại đây </a>.\",\n  \"client_id_placeholder\": \"Nhập một ClientID\",\n  \"client_identifier\": \"Định danh\",\n  \"client_identifier_desc\": \"Khách hàng có thể được xác định bằng địa chỉ IP, CIDR, địa chỉ MAC hoặc ClientID (có thể được sử dụng cho DoT / DoH / DoQ). Tìm hiểu thêm về cách xác định khách hàng <0>tại đây</0>.\",\n  \"client_name\": \"Khách hàng {{id}}\",\n  \"client_new\": \"Máy Khách Mới\",\n  \"client_settings\": \"Cài đặt thiết bị\",\n  \"client_table_header\": \"Người dùng\",\n  \"client_unblocked\": \"Đã bỏ chặn người dùng {{ip}}\",\n  \"client_updated\": \"Máy khách \\\"{{key}}\\\" đã cập nhật thành công\",\n  \"clients_desc\": \"Định cấu hình hồ sơ khách hàng liên tục cho các thiết bị được kết nối với AdGuard Home\",\n  \"clients_not_found\": \"Không tìm thấy máy khách\",\n  \"clients_title\": \"Khách hàng lâu dài\",\n  \"compact\": \"Thu gọn\",\n  \"config_successfully_saved\": \"Cấu hình được lưu thành công\",\n  \"configure\": \"Cấu hình\",\n  \"confirm_dns_cache_clear\": \"Bạn có chắc chắn muốn xóa bộ đệm ẩn DNS không?\",\n  \"confirm_static_ip\": \"AdGuard Home sẽ lấy {{ip}} làm địa chỉ IP tĩnh. Bạn có muốn tiếp tục?\",\n  \"copyright\": \"Bản quyền\",\n  \"country\": \"Quốc gia\",\n  \"custom_filter_rules\": \"Quy tắc lọc tuỳ chỉnh\",\n  \"custom_filter_rules_hint\": \"Nhập mỗi quy tắc 1 dòng. Có thể sử dụng quy tắc chặn quảng cáo hoặc cú pháp file host\",\n  \"custom_filtering_rules\": \"Bộ lọc tùy chỉnh\",\n  \"custom_ip\": \"IP tuỳ chỉnh\",\n  \"custom_retention_input\": \"Nhập thời gian giữ lại theo giờ\",\n  \"custom_rotation_input\": \"Nhập chu kỳ theo giờ\",\n  \"dashboard\": \"Tổng quan\",\n  \"date\": \"Ngày\",\n  \"default\": \"Mặc định\",\n  \"delete_confirm\": \"Bạn có chắc chắn muốn xóa \\\"{{key}}\\\" không?\",\n  \"delete_table_action\": \"Xoá\",\n  \"descr\": \"Mô tả\",\n  \"details\": \"Chi tiết\",\n  \"dhcp_add_static_lease\": \"Thêm thuê tĩnh\",\n  \"dhcp_config_saved\": \"Đã lưu cấu hình máy chủ DHCP\",\n  \"dhcp_description\": \"Nếu bộ định tuyến không trợ cài đặt DHCP, bạn có thể dùng máy chủ DHCP dựng sẵn của AdGuard\",\n  \"dhcp_disable\": \"Tắt máy chủ DHCP\",\n  \"dhcp_dynamic_ip_found\": \"Hệ thống của bạn sử dụng cấu hình địa chỉ IP động cho giao diện <0>{{interfaceName}}</0>. Để sử dụng máy chủ DHCP, phải đặt địa chỉ IP tĩnh. Địa chỉ IP hiện tại của bạn là <0>{{ipAddress}}</0>. Chúng tôi sẽ tự động đặt địa chỉ IP này thành tĩnh nếu bạn nhấn nút Bật DHCP.\",\n  \"dhcp_edit_static_lease\": \"Chỉnh sửa hợp đồng thuê tĩnh\",\n  \"dhcp_enable\": \"Bật máy chủ DHCP\",\n  \"dhcp_error\": \"Chúng tôi không thể xác định liệu có một máy chủ DHCP khác trong mạng hay không\",\n  \"dhcp_form_gateway_input\": \"Cổng IP\",\n  \"dhcp_form_lease_input\": \"Thời hạn thuê\",\n  \"dhcp_form_lease_title\": \"Thời gian thuê DHCP (tính bằng giây)\",\n  \"dhcp_form_range_end\": \"IP kết thúc\",\n  \"dhcp_form_range_start\": \"Phạm vi bắt đầu\",\n  \"dhcp_form_range_title\": \"Phạm vi của địa chỉ IP\",\n  \"dhcp_form_subnet_input\": \"Mặt nạ mạng con\",\n  \"dhcp_found\": \"Đã tìm thấy máy chủ DHCP trong mạng. Có thể có rủi ro nếu kích hoạt máy chủ DHCP dựng sẵn\",\n  \"dhcp_hardware_address\": \"Địa chỉ phần cứng\",\n  \"dhcp_interface_select\": \"Chọn một card mạng\",\n  \"dhcp_ip_addresses\": \"Các địa chỉ IP\",\n  \"dhcp_ipv4_settings\": \"Cài đặt DHCP IPv4\",\n  \"dhcp_ipv6_settings\": \"Cài đặt DHCP IPv6\",\n  \"dhcp_lease_added\": \"Cho thuê tĩnh \\\"{{key}}\\\" đã được thêm thành công\",\n  \"dhcp_lease_deleted\": \"Cho thuê tĩnh \\\"{{key}}\\\" đã xóa thành công\",\n  \"dhcp_lease_updated\": \"Cho thuê tĩnh \\\"{{key}}\\\" được cập nhật thành công\",\n  \"dhcp_leases\": \"Thuê DHCP\",\n  \"dhcp_leases_not_found\": \"Không tìm thấy DHCP cho thuê\",\n  \"dhcp_new_static_lease\": \"Cho thuê tĩnh mới\",\n  \"dhcp_not_found\": \"Không  có máy chủ DHCP nào được tìm thấy trong mạng. Có thể bật máy chủ DHCP một cách an toàn\",\n  \"dhcp_reset\": \"Bạn có chắc chắn muốn đặt lại thiết lập DHCP?\",\n  \"dhcp_reset_leases\": \"Đặt lại tất cả các hợp đồng thuê\",\n  \"dhcp_reset_leases_confirm\": \"Bạn có chắc chắn muốn đặt lại tất cả các hợp đồng thuê không?\",\n  \"dhcp_reset_leases_success\": \"DHCP cho thuê đã đặt lại thành công\",\n  \"dhcp_settings\": \"Cài đặt DHCP\",\n  \"dhcp_static_ip_error\": \"Để sử dụng máy chủ DHCP, phải đặt địa chỉ IP tĩnh. Chúng tôi không thể xác định xem giao diện mạng này có được cấu hình bằng địa chỉ IP tĩnh hay không. Vui lòng đặt địa chỉ IP tĩnh theo cách thủ công.\",\n  \"dhcp_static_leases\": \"Thuê DHCP tĩnh\",\n  \"dhcp_static_leases_not_found\": \"Không tìm thấy DHCP cho thuê tĩnh\",\n  \"dhcp_table_expires\": \"Hết hạn\",\n  \"dhcp_table_hostname\": \"Tên máy chủ\",\n  \"dhcp_title\": \"Máy chủ DHCP (thử nghiệm!)\",\n  \"dhcp_warning\": \"Nếu bạn vẫn muốn bật máy chủ DHCP, hãy đảm bảo rằng không có máy chủ DHCP hoạt động nào khác trong mạng của bạn. Nếu không, nó có thể phá vỡ Internet cho các thiết bị được kết nối!\",\n  \"disable_for_hours\": \"Trong {{count}} giờ\",\n  \"disable_for_hours_plural\": \"Trong {{count}} giờ\",\n  \"disable_for_minutes\": \"Trong {{count}} phút\",\n  \"disable_for_minutes_plural\": \"Trong {{count}} phút\",\n  \"disable_for_seconds\": \"Trong {{count}} giây\",\n  \"disable_for_seconds_plural\": \"Trong {{count}} giây\",\n  \"disable_ipv6\": \"Tắt IPv6\",\n  \"disable_ipv6_desc\": \"Bỏ tất cả truy vấn DNS cho địa chỉ IPv6 (loại AAAA) và xóa gợi ý IPv6 khỏi phản hồi HTTPS.\",\n  \"disable_notify_for_hours\": \"Tắt bảo vệ trong {{count}} giờ\",\n  \"disable_notify_for_hours_plural\": \"Tắt bảo vệ trong {{count}} giờ\",\n  \"disable_notify_for_minutes\": \"Tắt bảo vệ trong {{count}} phút\",\n  \"disable_notify_for_minutes_plural\": \"Tắt bảo vệ trong {{count}} phút\",\n  \"disable_notify_for_seconds\": \"Tắt bảo vệ trong {{count}} giây\",\n  \"disable_notify_for_seconds_plural\": \"Tắt bảo vệ trong {{count}} giây\",\n  \"disable_notify_until_tomorrow\": \"Vô hiệu hóa bảo vệ cho đến ngày mai\",\n  \"disable_protection\": \"Tắt bảo vệ\",\n  \"disable_rewrites\": \"Tắt quy tắc viết lại\",\n  \"disable_until_tomorrow\": \"Cho đến ngày mai\",\n  \"disabled\": \"Đã vô hiệu\",\n  \"disabled_dhcp\": \"Máy chủ DHCP đã tắt\",\n  \"disabled_filtering_toast\": \"Đã tắt chặn quảng cáo\",\n  \"disabled_parental_toast\": \"Đã tắt quản lý của phụ huynh\",\n  \"disabled_protection\": \"Đã tắt bảo vệ\",\n  \"disabled_safe_browsing_toast\": \"Đã tắt bảo vệ duyệt web\",\n  \"disabled_safe_search_toast\": \"Đã tắt tìm kiếm an toàn\",\n  \"disallow_this_client\": \"Không cho phép client này\",\n  \"dns_addresses\": \"Địa chỉ DNS\",\n  \"dns_allowlists\": \"Danh sách cho phép\",\n  \"dns_allowlists_desc\": \"Tên miền nằm trong danh sách cho phép sẽ không bị chặn cho dù nó có nằm trong bất kì danh sách bị chặn nào.\",\n  \"dns_blocklists\": \"Danh sách chặn\",\n  \"dns_blocklists_desc\": \"AdGuard Home sẽ chặn tên miền nằm trong danh sách bị chặn.\",\n  \"dns_cache_config\": \"Cấu hình cache DNS\",\n  \"dns_cache_config_desc\": \"Bạn có thể cấu hình cache cho DNS tại đây\",\n  \"dns_cache_size\": \"Kích thước bộ nhớ cache DNS, tính bằng byte\",\n  \"dns_config\": \"Thiết lập máy chủ DNS\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS Riêng Tư\",\n  \"dns_providers\": \"Dưới đây là một <0>danh sách của các nhà cung cấp DNS đã biết</0> để lựa chọn.\",\n  \"dns_query\": \"Truy vấn DNS\",\n  \"dns_rewrites\": \"DNS viết lại\",\n  \"dns_settings\": \"Cài đặt DNS\",\n  \"dns_start\": \"Máy chủ DNS đang khởi động\",\n  \"dns_status_error\": \"Có lỗi khi kiểm tra trạng thái máy chủ DNS\",\n  \"dns_test_not_ok_toast\": \"Máy chủ \\\"{{key}}\\\"': không thể sử dụng, vui lòng kiểm tra lại\",\n  \"dns_test_ok_toast\": \"Máy chủ DNS có thể sử dụng\",\n  \"dns_test_parsing_error_toast\": \"Phần {{section}}: dòng {{line}}: không thể sử dụng được, vui lòng kiểm tra xem bạn đã viết đúng chưa\",\n  \"dns_test_warning_toast\": \"Ngược lại \\\"{{key}}\\\" không phản hồi các yêu cầu kiểm tra và có thể không hoạt động bình thường\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"Bật DNSSEC\",\n  \"dnssec_enable_desc\": \"Cắm mốc DNSSEC trong các truy vấn DNS sắp tới và kiểm tra kết quả (buộc phải có trình sửa lỗi hỗ trợ DNSSEC)\",\n  \"domain\": \"Tên miền\",\n  \"domain_desc\": \"Nhập tên miền hoặc ký tự đại diện mà bạn muốn được viết lại.\",\n  \"domain_name_table_header\": \"Tên miền\",\n  \"domain_or_client\": \"Tên miền hoặc khách hàng\",\n  \"down\": \"Xuống\",\n  \"download_mobileconfig\": \"Tải xuống tệp cấu hình\",\n  \"download_mobileconfig_doh\": \"Tải xuống .mobileconfig cho DNS-over-HTTPS\",\n  \"download_mobileconfig_dot\": \"Tải xuống .mobileconfig cho DNS-over-TLS\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"Chỉnh sửa danh sách cho phép\",\n  \"edit_blocklist\": \"Chỉnh sửa danh sách chặn\",\n  \"edit_table_action\": \"Chỉnh sửa\",\n  \"edns_cs_desc\": \"Thêm tùy chọn EDNS Client Subnet (ECS) vào các yêu cầu ngược dòng và ghi lại các giá trị được gửi bởi các máy khách trong nhật ký truy vấn.\",\n  \"edns_enable\": \"Bật mạng con EDNS Client\",\n  \"edns_use_custom_ip\": \"Sử dụng địa chỉ IP tùy chỉnh cho EDNS\",\n  \"edns_use_custom_ip_desc\": \"Cho phép sử dụng địa chỉ IP tùy chỉnh cho EDNS\",\n  \"elapsed\": \"Đã tốn\",\n  \"empty_response_status\": \"Trống\",\n  \"enable_protection\": \"Bật bảo vệ\",\n  \"enable_protection_timer\": \"Bảo vệ sẽ được bật trong {{time}}\",\n  \"enable_rewrites\": \"Bật quy tắc viết lại\",\n  \"enable_upstream_dns_cache\": \"Bật bộ nhớ cache cho cấu hình ngược dòng của máy chủ upstream của khách hàng này\",\n  \"enabled_dhcp\": \"Máy chủ DHCP đã kích hoạt\",\n  \"enabled_filtering_toast\": \"Đã bật chặn quảng cáo\",\n  \"enabled_parental_toast\": \"Đã bật quản lý của phụ huynh\",\n  \"enabled_protection\": \"Đã bật bảo vệ\",\n  \"enabled_safe_browsing_toast\": \"Đã bật bảo vệ duyệt web\",\n  \"enabled_save_search_toast\": \"Đã bật tìm kiếm an toàn\",\n  \"enabled_table_header\": \"Kích hoạt\",\n  \"encryption_certificate_path\": \"Đường dẫn chứng chỉ\",\n  \"encryption_certificates\": \"Chứng chỉ\",\n  \"encryption_certificates_desc\": \"Để sử dụng mã hóa, bạn cần cung cấp chuỗi chứng chỉ SSL hợp lệ cho miền của mình. Bạn có thể nhận chứng chỉ miễn phí trên <0>{{link}}</0> hoặc bạn có thể mua chứng chỉ từ một trong các Cơ Quan Chứng Nhận tin cậy.\",\n  \"encryption_certificates_input\": \"Sao chép/dán chứng chỉ được mã hóa PEM của bạn tại đây.\",\n  \"encryption_certificates_source_content\": \"Dán nội dung chứng chỉ\",\n  \"encryption_certificates_source_path\": \"Đặt đường dẫn tệp chứng chỉ\",\n  \"encryption_chain_invalid\": \"Chứng chỉ không hợp lệ\",\n  \"encryption_chain_valid\": \"Chứng chỉ hợp lệ\",\n  \"encryption_config_saved\": \"Đã lưu cấu hình mã hóa\",\n  \"encryption_desc\": \"Hỗ trợ mã hóa (HTTPS/QUIC/TLS) cho cả giao diện web quản trị viên và DNS\",\n  \"encryption_doq\": \"Cổng DNS-over-QUIC\",\n  \"encryption_doq_desc\": \"Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS qua QUIC trên cổng này. \",\n  \"encryption_dot\": \"Cổng DNS-over-TLS\",\n  \"encryption_dot_desc\": \"Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS-over-TLS trên cổng này.\",\n  \"encryption_enable\": \"Kích Hoạt Mã Hóa (HTTPS, DNS-over-HTTPS và DNS-over-TLS)\",\n  \"encryption_enable_desc\": \"Nếu mã hóa được bật, giao diện quản trị viên AdGuard Home sẽ hoạt động trên HTTPS và máy chủ DNS sẽ lắng nghe các yêu cầu qua DNS-over-HTTPS và DNS-over-TLS.\",\n  \"encryption_expire\": \"Hết hạn\",\n  \"encryption_hostnames\": \"Tên máy chủ\",\n  \"encryption_https\": \"Cổng HTTPS\",\n  \"encryption_https_desc\": \"Nếu cổng HTTPS được định cấu hình, giao diện quản trị viên AdGuard Home sẽ có thể truy cập thông qua HTTPS và nó cũng sẽ cung cấp DNS-over-HTTPS trên vị trí '/dns-query'.\",\n  \"encryption_issuer\": \"Phát hành\",\n  \"encryption_key\": \"Khóa riêng\",\n  \"encryption_key_input\": \"Sao chép/dán khóa riêng được mã hóa PEM cho chứng chỉ của bạn tại đây.\",\n  \"encryption_key_invalid\": \"Khóa riêng {{type}} không hợp lệ\",\n  \"encryption_key_source_content\": \"Dán nội dung khóa riêng\",\n  \"encryption_key_source_path\": \"Đặt đường dẫn tệp khóa riêng\",\n  \"encryption_key_valid\": \"Khóa riêng {{type}} hợp lệ\",\n  \"encryption_plain_dns_desc\": \"DNS đơn giản được bật theo mặc định. Bạn có thể vô hiệu hóa nó để buộc tất cả các thiết bị sử dụng DNS được mã hóa. Để thực hiện việc này, bạn phải kích hoạt ít nhất một giao thức DNS được mã hóa\",\n  \"encryption_plain_dns_enable\": \"Kích hoạt DNS đơn giản\",\n  \"encryption_plain_dns_error\": \"Để tắt DNS đơn giản, hãy bật ít nhất một giao thức DNS được mã hóa\",\n  \"encryption_private_key_path\": \"Đường dẫn khóa riêng\",\n  \"encryption_redirect\": \"Tự động chuyển hướng đến HTTPS\",\n  \"encryption_redirect_desc\": \"Nếu được chọn, AdGuard Home sẽ tự động chuyển hướng bạn từ địa chỉ HTTP sang địa chỉ HTTPS.\",\n  \"encryption_reset\": \"Bạn có chắc chắn muốn đặt lại cài đặt mã hóa?\",\n  \"encryption_server\": \"Tên máy chủ\",\n  \"encryption_server_desc\": \"Nếu được đặt, AdGuard Home sẽ phát hiện ClientID, phản hồi các truy vấn DDR và thực hiện xác thực kết nối bổ sung. Nếu không được đặt, các tính năng này sẽ bị vô hiệu hóa. Phải khớp với một trong các Tên DNS trong chứng chỉ.\",\n  \"encryption_server_enter\": \"Nhập tên miền của bạn\",\n  \"encryption_settings\": \"Cài đặt mã hóa\",\n  \"encryption_status\": \"Trạng thái\",\n  \"encryption_subject\": \"Chủ đề\",\n  \"encryption_title\": \"Mã hóa\",\n  \"encryption_warning\": \"Cảnh báo\",\n  \"enforce_safe_search\": \"Bắt buộc tìm kiếm an toàn\",\n  \"enforce_save_search_hint\": \"AdGuard Home sẽ thực thi tìm kiếm an toàn trong các công cụ tìm kiếm sau: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.\",\n  \"enforced_save_search\": \"Bắt buộc tìm kiếm an toàn\",\n  \"enter_cache_size\": \"Nhập kích thước bộ nhớ cache (byte)\",\n  \"enter_cache_ttl_max_override\": \"Nhập TTL tối đa (giây)\",\n  \"enter_cache_ttl_min_override\": \"Nhập TTL tối thiểu (giây)\",\n  \"enter_name_hint\": \"Nhập tên\",\n  \"enter_url_or_path_hint\": \"Nhập địa chỉ hoặc đường dẫn tới danh sách\",\n  \"enter_valid_allowlist\": \"Điề địa chỉ URL của danh sách cho phép.\",\n  \"enter_valid_blocklist\": \"Điền địa chỉ URL của danh sách chặn.\",\n  \"error_details\": \"Chi tiết lỗi\",\n  \"example_comment\": \"! Đây là một chú thích.\",\n  \"example_comment_hash\": \"# Cũng là một chú thích.\",\n  \"example_comment_meaning\": \"chỉ là một chú thích;\",\n  \"example_meaning_filter_block\": \"chặn truy cập tới tên miền example.org và tất cả tên miền con;\",\n  \"example_meaning_filter_whitelist\": \"không chặn truy cập tới tên miền example.org và tất cả tên miền con;\",\n  \"example_meaning_host_block\": \"hồi địa chỉ IP 127.0.0.1 cho tên miền example.org (không áp dụng tên miền con);\",\n  \"example_multiple_upstreams_reserved\": \"nhiều máy chủ thượng nguồn <0>cho các miền cụ thể</0>;\",\n  \"example_regex_meaning\": \"chặn quyền truy cập vào các miền khớp với biểu thức chính được quy định.\",\n  \"example_rewrite_domain\": \"chỉ viết lại phản hồi cho tên miền này.\",\n  \"example_rewrite_wildcard\": \"viết lại câu trả lời cho tất cả các tên miền phụ <0> example.org </0>.\",\n  \"example_upstream_comment\": \"một lời bình luận.\",\n  \"example_upstream_doh\": \"được mã hoá <0>DNS-over-HTTPS</0>;\",\n  \"example_upstream_doh3\": \"DNS-over-HTTPS được mã hóa với <0>HTTP/3</0> bắt buộc và không có dự phòng cho HTTP/2 trở xuống;\",\n  \"example_upstream_doq\": \"được mã hoá <0>DNS-over-QUIC</0>;\",\n  \"example_upstream_dot\": \"được mã hoá <0>DNS-over-TLS</0>;\",\n  \"example_upstream_regular\": \"DNS thông thường (dùng UDP);\",\n  \"example_upstream_regular_port\": \"DNS thông thường (qua UDP, với cổng);\",\n  \"example_upstream_reserved\": \"ngược dòng <0>cho các miền cụ thể</0>;\",\n  \"example_upstream_sdns\": \"bạn có thể sử dụng <0>DNS Stamps</0> for <1>DNSCrypt</1> hoặc <2>DNS-over-HTTPS</2> \",\n  \"example_upstream_tcp\": \"DNS thông thường(dùng TCP);\",\n  \"example_upstream_tcp_hostname\": \"DNS thông thường (qua TCP, tên máy chủ);\",\n  \"example_upstream_tcp_port\": \"DNS thông thường (qua TCP, với cổng);\",\n  \"example_upstream_udp\": \"DNS thông thường (qua UDP, tên máy chủ);\",\n  \"examples_title\": \"Ví dụ\",\n  \"fallback_dns_desc\": \"Danh sách máy chủ DNS dự phòng được sử dụng khi máy chủ DNS ngược tuyến không phản hồi. Cú pháp tương tự như trong trường ngược dòng chính ở trên.\",\n  \"fallback_dns_placeholder\": \"Nhập một máy chủ DNS dự phòng trên mỗi dòng\",\n  \"fallback_dns_title\": \"Máy chủ DNS dự phòng\",\n  \"faq\": \"Hỏi đáp\",\n  \"fastest_addr\": \"Địa chỉ IP nhanh nhất\",\n  \"fastest_addr_desc\": \"Chờ phản hồi từ <b>tất cả</b> các máy chủ DNS, đo tốc độ kết nối TCP cho từng máy chủ, và trả về địa chỉ IP của máy chủ có tốc độ kết nối nhanh nhất.<br/>Chế độ này có thể làm chậm đáng kể các truy vấn DNS, nếu một hoặc nhiều máy chủ thượng nguồn không phản hồi. Hãy đảm bảo rằng các máy chủ thượng nguồn của bạn ổn định và thời gian hết hạn đến máy chủ thượng nguồn của bạn thấp.\",\n  \"filter\": \"Bộ lọc\",\n  \"filter_added_successfully\": \"Thêm bộ lọc thành công\",\n  \"filter_allowlist\": \"CẢNH BÁO: Hành động này cũng sẽ loại trừ quy tắc \\\"{{disallowed_rule}}\\\" khỏi danh sách các ứng dụng khách được phép.\",\n  \"filter_category_general\": \"Chung\",\n  \"filter_category_general_desc\": \"Bộ lọc chặn quảng cáo và theo dõi cho hầu hết các thiết bị\",\n  \"filter_category_other\": \"Khác\",\n  \"filter_category_other_desc\": \"Bộ lọc chặn khác\",\n  \"filter_category_regional\": \"Khu vực\",\n  \"filter_category_regional_desc\": \"Bộ lọc tập trung vào từng khu vực\",\n  \"filter_category_security\": \"Bảo mật\",\n  \"filter_category_security_desc\": \"Bộ lọc chuyên biệt chặn tên miền chứa mã độc và lừa đảo\",\n  \"filter_removed_successfully\": \"Xóa bộ lọc thành công\",\n  \"filter_updated\": \"Cập nhật bộ lọc thành công\",\n  \"filtered\": \"Đã lọc\",\n  \"filtered_custom_rules\": \"Được lọc bởi các quy tắc lọc tùy chỉnh\",\n  \"filtering_rules_learn_more\": \"<0>Tìm hiểu thêm</0> về việc tạo danh sách chặn máy chủ của riêng bạn.\",\n  \"filters\": \"Bộ lọc\",\n  \"filters_and_hosts_hint\": \"AdGuard home hiểu các quy tắc chặn quảng cáo đơn giản và cú pháp file hosts\",\n  \"filters_block_toggle_hint\": \"Bạn có thể thiết lập quy tắc chặn tại cài đặt <a>Bộ lọc</a>.\",\n  \"filters_configuration\": \"Cấu hình bộ lọc\",\n  \"filters_enable\": \"Kích hoạt bộ lọc\",\n  \"filters_interval\": \"Khoảng thời gian cập nhật bộ lọc\",\n  \"fix\": \"Sửa\",\n  \"for_last_days\": \"trong {{count}} ngày qua\",\n  \"for_last_days_plural\": \"trong {{count}} ngày qua\",\n  \"for_last_hours\": \"trong {{count}} giờ qua\",\n  \"for_last_hours_plural\": \"trong {{count}} giờ qua\",\n  \"forgot_password\": \"Quên mật khẩu?\",\n  \"forgot_password_desc\": \"Vui lòng làm theo <0>các bước này</0> để tạo mật khẩu mới cho tài khoản người dùng của bạn.\",\n  \"form_add_id\": \"Thêm định danh\",\n  \"form_answer\": \"Nhập địa chỉ IP hoặc tên miền\",\n  \"form_client_name\": \"Nhập tên máy khách\",\n  \"form_domain\": \"Nhập tên miền\",\n  \"form_enter_blocked_response_ttl\": \"Nhập phản hồi bị chặn TTL (giây)\",\n  \"form_enter_host\": \"Nhập tên máy chủ\",\n  \"form_enter_hostname\": \"Nhập tên máy chủ\",\n  \"form_enter_id\": \"Nhập định danh\",\n  \"form_enter_ip\": \"Nhập IP\",\n  \"form_enter_mac\": \"Nhập MAC\",\n  \"form_enter_rate_limit\": \"Nhập giới hạn yêu cầu\",\n  \"form_enter_rate_limit_subnet_len\": \"Nhập độ dài tiền tố mạng con để giới hạn tốc độ\",\n  \"form_enter_subnet_ip\": \"Nhập địa chỉ IP vào mạng con \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"Nhập thời gian chờ máy chủ ngược dòng tính bằng giây\",\n  \"form_error_answer_format\": \"Định dạng câu trả lời không hợp lệ\",\n  \"form_error_client_id_format\": \"ClientID chỉ được chứa số, chữ thường và dấu gạch nối\",\n  \"form_error_domain_format\": \"Định dạng tên miền không hợp lệ\",\n  \"form_error_equal\": \"Không nên bằng nhau\",\n  \"form_error_gateway_ip\": \"Cho thuê không thể có địa chỉ IP của cổng\",\n  \"form_error_ip4_format\": \"Địa chỉ IPv4 không hợp lệ\",\n  \"form_error_ip4_gateway_format\": \"Địa chỉ IPv4 không hợp lệ của cổng kết nối\",\n  \"form_error_ip6_format\": \"Địa chỉ IPv6 không hợp lệ\",\n  \"form_error_ip_format\": \"Địa chỉ IP không hợp lệ\",\n  \"form_error_mac_format\": \"Địa chỉ MAC không hợp lệ\",\n  \"form_error_password\": \"Mật khẩu không khớp\",\n  \"form_error_password_length\": \"Mật khẩu phải dài từ {{min}} đến {{max}} ký tự\",\n  \"form_error_port\": \"Nhập số cổng hợp lệ\",\n  \"form_error_port_range\": \"Nhập giá trị cổng trong phạm vi 80-65535\",\n  \"form_error_port_unsafe\": \"Đây là một cổng không an toàn\",\n  \"form_error_positive\": \"Phải lớn hơn 0\",\n  \"form_error_required\": \"Trường bắt buộc\",\n  \"form_error_server_name\": \"Tên máy chủ không hợp lệ\",\n  \"form_error_subnet\": \"Mạng con \\\"{{cidr}}\\\" không chứa địa chỉ IP \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"ĐỊnh dạng URL không hợp lệ\",\n  \"form_error_url_or_path_format\": \"Định dạng URL hoặc đường dẫn tới danh sách không hợp lệ\",\n  \"form_select_tags\": \"Chọn thẻ khách hàng\",\n  \"found_in_known_domain_db\": \"Tìm thấy trong cơ sở dữ liệu tên miền\",\n  \"friday\": \"Thứ sáu\",\n  \"friday_short\": \"Thứ 6\",\n  \"gateway_or_subnet_invalid\": \"Mặt nạ mạng con không hợp lệ\",\n  \"general_settings\": \"Cài đặt chung\",\n  \"general_statistics\": \"Thống kê chung\",\n  \"get_started\": \"Bắt Đầu\",\n  \"greater_range_start_error\": \"Phải lớn hơn khoảng bắt đầu\",\n  \"homepage\": \"Trang chủ\",\n  \"host_whitelisted\": \"Trang đã được thêm vào danh sách được cho phép\",\n  \"ignore_domains\": \"Các miền bị bỏ qua (cách nhau bởi dòng mới)\",\n  \"ignore_domains_desc_query\": \"Các truy vấn khớp với các quy tắc này không được ghi vào nhật ký truy vấn\",\n  \"ignore_domains_desc_stats\": \"Các truy vấn phù hợp với các quy tắc này không được ghi vào số liệu thống kê\",\n  \"ignore_domains_title\": \"Các miền bị bỏ qua\",\n  \"ignore_query_log\": \"Bỏ qua máy khách này trong nhật ký truy vấn\",\n  \"ignore_statistics\": \"Bỏ qua máy khách này trong thống kê\",\n  \"install_auth_confirm\": \"Xác nhận mật khẩu\",\n  \"install_auth_desc\": \"Xác thực mật khẩu cho giao diện web quản trị AdGuard Home của bạn phải được định cấu hình. Ngay cả khi AdGuard Home chỉ có thể truy cập được trong mạng cục bộ của bạn, điều quan trọng vẫn là bảo vệ nó khỏi quyền truy cập không hạn chế.\",\n  \"install_auth_password\": \"Mật khẩu\",\n  \"install_auth_password_enter\": \"Nhập mật khẩu\",\n  \"install_auth_title\": \"Xác thực\",\n  \"install_auth_username\": \"Tên đăng nhập\",\n  \"install_auth_username_enter\": \"Nhập tên đăng nhập\",\n  \"install_devices_address\": \"Máy chủ DNS của AdGuard Home đang lắng nghe các địa chỉ sau\",\n  \"install_devices_android_list_1\": \"Từ màn hình chính của Trình Đơn Android, chạm Cài đặt.\",\n  \"install_devices_android_list_2\": \"Nhấp Wi-Fi trên trình đơn. Màn hình liệt kê tất cả các mạng khả dụng sẽ được hiển thị (không thể đặt DNS tùy chỉnh cho kết nối di động).\",\n  \"install_devices_android_list_3\": \"Nhấn và giữ mạng mà bạn đã kết nối và chạm Sửa Đổi Mạng.\",\n  \"install_devices_android_list_4\": \"Trên một số thiết bị, bạn có thể cần chọn hộp Nâng cao để xem thêm cài đặt. Để điều chỉnh cài đặt DNS Android của bạn, bạn sẽ cần chuyển cài đặt IP từ DHCP sang Tĩnh.\",\n  \"install_devices_android_list_5\": \"Thay đổi giá trị DNS 1 và DNS 2 thành địa chỉ máy chủ AdGuard Home của bạn.\",\n  \"install_devices_desc\": \"Để bắt đầu sử dụng AdGuard Home, bạn cần định cấu hình thiết bị của mình để sử dụng nó.\",\n  \"install_devices_ios_list_1\": \"Từ màn hình chính, chạm Cài đặt.\",\n  \"install_devices_ios_list_2\": \"Chọn Wi-Fi trong trình đơn bên trái (không thể định cấu hình DNS cho mạng di động).\",\n  \"install_devices_ios_list_3\": \"Chạm vào tên của mạng hiện đang hoạt động.\",\n  \"install_devices_ios_list_4\": \"Trong trường DNS nhập địa chỉ máy chủ AdGuard Home của bạn.\",\n  \"install_devices_macos_list_1\": \"Nhấp vào biểu tượng Apple và đi đến Tùy Chọn Hệ Thống.\",\n  \"install_devices_macos_list_2\": \"Nhấp vào Mạng.\",\n  \"install_devices_macos_list_3\": \"Chọn kết nối đầu tiên trong danh sách của bạn và nhấp vào Nâng cao.\",\n  \"install_devices_macos_list_4\": \"Chọn thẻ DNS và nhập địa chỉ máy chủ AdGuard Home của bạn.\",\n  \"install_devices_router\": \"Bộ định tuyến\",\n  \"install_devices_router_desc\": \"Thiết lập này sẽ tự động bao gồm tất cả các thiết bị được kết nối với bộ định tuyến gia đình của bạn và bạn sẽ không cần phải định cấu hình từng thiết bị theo cách thủ công.\",\n  \"install_devices_router_list_1\": \"Mở tùy chọn cho bộ định tuyến của bạn. Thông thường, bạn có thể truy cập nó từ trình duyệt của mình thông qua một URL, chẳng hạn như http://192.168.0.1/ hoặc http://192.168.1.1/. Bạn có thể được nhắc nhập mật khẩu. Nếu bạn không nhớ nó, bạn có thể đặt lại mật khẩu bằng cách nhấn nút khởi động lại trên chính bộ định tuyến, nhưng lưu ý rằng nếu khởi động lại, bạn có thể sẽ mất toàn bộ cấu hình bộ định tuyến. Một số bộ định tuyến sẽ yêu cầu đăng nhập từ một ứng dụng cụ thể đã được cài đặt trên máy tính hoặc điện thoại của bạn.\",\n  \"install_devices_router_list_2\": \"Tìm cài đặt DHCP/DNS. Tìm các chữ cái DNS bên cạnh một trường cho phép hai hoặc ba bộ số, mỗi bộ được chia thành bốn nhóm từ một đến ba chữ số.\",\n  \"install_devices_router_list_3\": \"Nhập địa chỉ máy chủ AdGuard Home của bạn ở đó.\",\n  \"install_devices_router_list_4\": \"Bạn không thể đặt máy chủ DNS tùy chỉnh trên một số loại bộ định tuyến. Trong trường hợp này, có thể hữu ích nếu bạn thiết lập AdGuard Home làm <0> máy chủ DHCP </0>. Nếu không, bạn nên tìm kiếm hướng dẫn về cách tùy chỉnh máy chủ DNS cho kiểu bộ định tuyến cụ thể của mình.\",\n  \"install_devices_title\": \"Định cấu hình thiết bị của bạn\",\n  \"install_devices_windows_list_1\": \"Mở Control Panel thông qua Trình đơn Bắt đầu hoặc Tìm kiếm Windows.\",\n  \"install_devices_windows_list_2\": \"Chuyển đến danh mục Mạng và Internet, sau đó đến Trung tâm Mạng và Chia sẻ.\",\n  \"install_devices_windows_list_3\": \"Ở bên trái màn hình, tìm Thay đổi cài đặt bộ điều hợp và nhấp vào nó.\",\n  \"install_devices_windows_list_4\": \"Chọn kết nối hoạt động của bạn, nhấp chuột phải vào nó và chọn Thuộc tính.\",\n  \"install_devices_windows_list_5\": \"Tìm Giao Thức Internet Phiên Bản 4 (TCP/IP) trong danh sách, chọn nó và sau đó nhấp vào Thuộc tính một lần nữa.\",\n  \"install_devices_windows_list_6\": \"Chọn Sử dụng các địa chỉ máy chủ DNS sau và nhập địa chỉ máy chủ AdGuard Home của bạn.\",\n  \"install_saved\": \"Lưu thành công\",\n  \"install_settings_all_interfaces\": \"Tất cả các giao diện\",\n  \"install_settings_dns\": \"Máy chủ DNS\",\n  \"install_settings_dns_desc\": \"Bạn sẽ cần định cấu hình thiết bị hoặc bộ định tuyến của mình để sử dụng máy chủ DNS trên các địa chỉ sau:\",\n  \"install_settings_interface_link\": \"Giao diện web quản trị viên AdGuard Home của bạn sẽ có sẵn trên các địa chỉ sau:\",\n  \"install_settings_listen\": \"Giao diện nghe\",\n  \"install_settings_port\": \"Cổng\",\n  \"install_settings_title\": \"Giao Diện Web Quản Trị\",\n  \"install_static_configure\": \"Chúng tôi đã phát hiện thấy rằng một địa chỉ IP động được sử dụng - <0> {{ip}} </0>. Bạn có muốn sử dụng nó làm địa chỉ tĩnh của mình không?\",\n  \"install_static_error\": \"AdGuard Home không thể cấu hình tự động cho giao diện mạng này. Vui lòng tìm hướng dẫn về cách thực hiện việc này theo cách thủ công.\",\n  \"install_static_ok\": \"Địa chỉ IP tĩnh đã được thiết lập\",\n  \"install_step\": \"Bước\",\n  \"install_submit_desc\": \"Quy trình thiết lập đã kết thúc và bạn đã sẵn sàng bắt đầu sử dụng AdGuard Home.\",\n  \"install_submit_title\": \"Xin chúc mừng!\",\n  \"install_welcome_desc\": \"AdGuard Home là một máy chủ DNS chặn quảng cáo và theo dõi trên toàn mạng. Mục đích của nó là cho phép bạn kiểm soát toàn bộ mạng và tất cả các thiết bị của mình và không yêu cầu sử dụng chương trình phía máy khách.\",\n  \"install_welcome_title\": \"Chào mừng bạn đến với AdGuard Home!\",\n  \"interval_24_hour\": \"24 giờ\",\n  \"interval_6_hour\": \"6 tiếng\",\n  \"interval_days\": \"{{count}} ngày\",\n  \"interval_days_plural\": \"{{count}} ngày\",\n  \"interval_hours\": \"{{count}} giờ\",\n  \"interval_hours_plural\": \"{{count}} giờ\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"Địa chỉ IP\",\n  \"known_tracker\": \"Theo dõi đã biết\",\n  \"last_rule_in_allowlist\": \"Không thể không cho phép ứng dụng khách này vì việc loại trừ quy tắc \\\"{{disallowed_rule}}\\\" sẽ TẮT danh sách \\\"Ứng dụng khách được phép\\\".\",\n  \"last_time_updated_table_header\": \"Cập nhật lần cuối\",\n  \"list_confirm_delete\": \"Bạn có muốn xóa bộ lọc này?\",\n  \"list_label\": \"Danh sách\",\n  \"list_updated\": \"Đã cập nhật {{count}} bộ lọc\",\n  \"list_updated_plural\": \"Đã cập nhật {{count}} bộ lọc\",\n  \"list_url_table_header\": \"URL bộ lọc\",\n  \"load_balancing\": \"Cân bằng tải\",\n  \"load_balancing_desc\": \"Truy vấn một máy chủ thượng nguồn tại một thời điểm.<br/>AdGuard Home sử dụng thuật toán ngẫu nhiên có trọng số để chọn máy chủ có số lần tìm kiếm không thành công thấp nhất và thời gian tìm kiếm trung bình thấp nhất.\",\n  \"loading_table_status\": \"Đang tải...\",\n  \"local_ptr_default_resolver\": \"Theo mặc định, AdGuard Home sử dụng các hệ thống phân giải tên miền ngược sau: {{ip}}.\",\n  \"local_ptr_desc\": \"Máy chủ DNS được AdGuard Home sử dụng cho các yêu cầu PTR, SOA và NS riêng tư. Một yêu cầu được coi là riêng tư nếu nó yêu cầu một miền ARPA chứa một mạng con trong phạm vi IP riêng tư (chẳng hạn như \\\"192.168.12.34\\\") và đến từ một máy khách có địa chỉ IP riêng tư. Nếu không được thiết lập, các trình phân giải DNS mặc định của hệ điều hành của bạn sẽ được sử dụng, ngoại trừ các địa chỉ IP của AdGuard Home.\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home không thể xác định hệ thống phân giải tên miền ngược riêng phù hợp cho hệ thống này.\",\n  \"local_ptr_placeholder\": \"Nhập một địa chỉ IP trên mỗi dòng\",\n  \"local_ptr_title\": \"Máy chủ DNS riêng tư\",\n  \"location\": \"Vị trí\",\n  \"log_and_stats_section_label\": \"Nhật ký truy vấn và thống kê\",\n  \"lower_range_start_error\": \"Phải thấp hơn khởi động phạm vi\",\n  \"main_settings\": \"Cài đặt chính\",\n  \"make_static\": \"Chuyển sang tĩnh\",\n  \"manual_update\": \"Vui lòng <a>làm theo các bước này</a> để cập nhật thủ công.\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"Thứ hai\",\n  \"monday_short\": \"Thứ 2\",\n  \"name\": \"Tên\",\n  \"name_table_header\": \"Tên\",\n  \"netname\": \"Tên mạng\",\n  \"network\": \"Mạng\",\n  \"new_allowlist\": \"Danh sách cho phép mới\",\n  \"new_blocklist\": \"Danh sách chặn mới\",\n  \"next\": \"Tiếp\",\n  \"next_btn\": \"Trang sau\",\n  \"no_blocklist_added\": \"Chưa có danh sách chặn được thêm vào\",\n  \"no_clients_found\": \"Không có người dùng\",\n  \"no_domains_found\": \"Không có tên miền nào\",\n  \"no_logs_found\": \"Không có lịch sử truy vấn\",\n  \"no_servers_specified\": \"Không có máy chủ nào được liệt kê\",\n  \"no_upstreams_data_found\": \"Không tìm thấy dữ liệu máy chủ ngược dòng\",\n  \"no_whitelist_added\": \"Chưa có danh sách cho phép được thêm vào\",\n  \"nothing_found\": \"Không tìm thấy\",\n  \"null_ip\": \"Địa chỉ IP rỗng\",\n  \"number_of_dns_query_blocked_24_hours\": \"Số yêu cầu DNS bị chặn bởi bộ lọc quảng cáo và danh sách chặn host\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"Số trang web người lớn đã chặn\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"Số yêu cầu DNS bị chặn bởi chế độ bảo vệ duyệt web AdGuard\",\n  \"number_of_dns_query_days\": \"Một số truy vấn DNS được xử lý trong {{count}} ngày qua\",\n  \"number_of_dns_query_days_plural\": \"Một số truy vấn DNS được xử lý trong {{count}} ngày qua\",\n  \"number_of_dns_query_hours\": \"Một số truy vấn DNS được xử lý trong {{count}} giờ qua\",\n  \"number_of_dns_query_hours_plural\": \"Một số truy vấn DNS được xử lý trong {{count}} giờ qua\",\n  \"number_of_dns_query_to_safe_search\": \"Số yêu cầu DNS tới công cụ tìm kiếm đã chuyển thành tìm kiếm an toàn\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"Đang tắt\",\n  \"on\": \"Đang bật\",\n  \"open_dashboard\": \"Mở bảng điều khiển\",\n  \"orgname\": \"Tên tổ chức\",\n  \"original_response\": \"Phản hồi gốc\",\n  \"out_of_range_error\": \"Phải nằm ngoài phạm vi \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"Trang\",\n  \"parallel_requests\": \"Yêu cầu song song\",\n  \"parental_control\": \"Quản lý của phụ huynh\",\n  \"password_label\": \"Mật khẩu\",\n  \"password_placeholder\": \"Nhập mật khẩu\",\n  \"plain_dns\": \"DNS thuần\",\n  \"port_53_faq_link\": \"Cổng 53 thường được sử dụng \\\"DNSStubListener\\\" hoặc \\\"systemd-resolved\\\". Vui lòng đọc <0>hướng dẫn</0> để giải quyết vấn đề này.\",\n  \"previous_btn\": \"Trước\",\n  \"privacy_policy\": \"Chính sách riêng tư\",\n  \"processing_update\": \"Xin vui lòng chờ, AdGuard Home đang được cập nhật\",\n  \"protection_section_label\": \"Sự bảo vệ\",\n  \"protocol\": \"Giao thức\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"Lịch sử truy vấn\",\n  \"query_log_clear\": \"Xóa nhật ký truy vấn\",\n  \"query_log_cleared\": \"Nhật ký truy vấn đã được xóa thành công\",\n  \"query_log_configuration\": \"Cấu hình nhật ký\",\n  \"query_log_confirm_clear\": \"Bạn có chắc chắn muốn xóa toàn bộ nhật ký truy vấn không?\",\n  \"query_log_disabled\": \"Nhật ký truy vấn bị vô hiệu hóa và có thể được định cấu hình trong <0>cài đặt</ 0>\",\n  \"query_log_enable\": \"Bật nhật ký\",\n  \"query_log_filtered\": \"Được lọc bởi {{filter}}\",\n  \"query_log_response_status\": \"Trạng thái: {{value}}\",\n  \"query_log_retention\": \"Xoay vòng nhật ký truy vấn\",\n  \"query_log_retention_confirm\": \"Bạn có chắc chắn muốn thay đổi xoay vòng nhật ký truy vấn không? Nếu bạn giảm giá trị khoảng thời gian, một số dữ liệu sẽ bị mất\",\n  \"query_log_strict_search\": \"Sử dụng dấu ngoặc kép để tìm kiếm nghiêm ngặt\",\n  \"query_log_updated\": \"Cập nhật thành công nhật kí truy xuất\",\n  \"rate_limit\": \"Giới hạn yêu cầu\",\n  \"rate_limit_desc\": \"Số lượng yêu cầu mỗi giây mà một khách hàng được phép thực hiện (0: không giới hạn)\",\n  \"rate_limit_subnet_len_ipv4\": \"Độ dài tiền tố mạng con cho địa chỉ IPv4\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"Độ dài tiền tố mạng con cho các địa chỉ IPv4 được sử dụng để giới hạn tốc độ. Mặc định là 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"Độ dài tiền tố mạng con IPv4 phải nằm trong khoảng từ 0 đến 32\",\n  \"rate_limit_subnet_len_ipv6\": \"Độ dài tiền tố mạng con cho địa chỉ IPv6\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"Độ dài tiền tố mạng con cho các địa chỉ IPv6 được sử dụng để giới hạn tốc độ. Mặc định là 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"Độ dài tiền tố mạng con IPv6 phải nằm trong khoảng từ 0 đến 128\",\n  \"rate_limit_whitelist\": \"Danh sách cho phép giới hạn tỷ lệ\",\n  \"rate_limit_whitelist_desc\": \"Địa chỉ IP bị loại trừ khỏi giới hạn tốc độ\",\n  \"rate_limit_whitelist_placeholder\": \"Nhập một địa chỉ IP trên mỗi dòng\",\n  \"refresh_btn\": \"Làm mới\",\n  \"refresh_statics\": \"Làm mới thống kê\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"Báo lỗi\",\n  \"request_details\": \"Chi tiết về yêu cầu\",\n  \"request_table_header\": \"Yêu cầu\",\n  \"requests_count\": \"Số lần yêu cầu\",\n  \"reset_settings\": \"Đặt lại cài đặt\",\n  \"resolve_clients_desc\": \"Nếu được bật, AdGuard Home sẽ cố gắng phân giải ngược lại địa chỉ IP của khách hàng thành tên máy chủ của họ bằng cách gửi các truy vấn PTR tới trình phân giải tương ứng (máy chủ DNS riêng cho máy khách cục bộ, máy chủ ngược dòng cho máy khách có địa chỉ IP công cộng).\",\n  \"resolve_clients_title\": \"Kích hoạt cho phép phân giải ngược về địa chỉ IP của máy khách\",\n  \"response_code\": \"Mã phản hồi\",\n  \"response_details\": \"Chi tiết về phản hồi\",\n  \"response_table_header\": \"Phản hồi\",\n  \"response_time\": \"Thời gian đáp ứng\",\n  \"rewrite_A\": \"<0>A</0>: giá trị đặc biệt, giữ bản ghi <0>A</0> từ nguồn\",\n  \"rewrite_AAAA\": \"<0>A</0>: giá trị đặc biệt, giữ bản ghi <0>A</0> từ nguồn\",\n  \"rewrite_add\": \"Thêm DNS viết lại\",\n  \"rewrite_added\": \"DNS viết lại cho \\\"{{key}}\\\" đã thêm thành công\",\n  \"rewrite_applied\": \"Đã áp dụng quy tắc Viết lại\",\n  \"rewrite_confirm_delete\": \"Bạn có chắc chắn muốn xóa DNS viết lại cho \\\"{{key}}\\\" không?\",\n  \"rewrite_deleted\": \"DNS viết lại cho \\\"{{key}}\\\" đã xóa thành công\",\n  \"rewrite_desc\": \"Cho phép dễ dàng định cấu hình tùy chỉnh DNS phản hồi cho một tên miền cụ thể.\",\n  \"rewrite_domain_name\": \"Tên miền: thêm bản ghi CNAME\",\n  \"rewrite_edit\": \"Chỉnh sửa viết lại DNS\",\n  \"rewrite_hosts_applied\": \"Viết lại bởi quy tắc tệp máy chủ\",\n  \"rewrite_ip_address\": \"Địa chỉ IP: sử dụng IP này trong phản hồi A hoặc AAAA\",\n  \"rewrite_not_found\": \"Không tìm thấy DNS viết lại\",\n  \"rewrite_settings_updated\": \"Cài đặt viết lại DNS được cập nhật thành công\",\n  \"rewrite_updated\": \"Viết lại DNS được cập nhật thành công\",\n  \"rewrites_disabled_table_header\": \"Viết lại bị vô hiệu hóa\",\n  \"rewrites_enabled_table_header\": \"Viết lại được kích hoạt\",\n  \"rewritten\": \"Đã viết lại\",\n  \"rows_table_footer_text\": \"hàng\",\n  \"rule_added_to_custom_filtering_toast\": \"Quy tắc đã được thêm vào quy tắc lọc tuỳ chỉnh: {{rule}}\",\n  \"rule_label\": \"Quy tắc\",\n  \"rule_removed_from_custom_filtering_toast\": \"Quy tắc đã được xoá khỏi quy tắc lọc tuỳ chỉnh {{rule}}\",\n  \"rules_count_table_header\": \"Số quy tắc\",\n  \"safe_browsing\": \"Duyệt web an toàn\",\n  \"safe_search\": \"Tìm kiếm an toàn\",\n  \"saturday\": \"Thứ bảy\",\n  \"saturday_short\": \"Thứ 7\",\n  \"save_btn\": \"Lưu\",\n  \"save_config\": \"Lưu thiết lập\",\n  \"schedule_add\": \"Thêm lịch trình\",\n  \"schedule_current_timezone\": \"Múi giờ hiện tại: {{value}}\",\n  \"schedule_desc\": \"Đặt khoảng thời gian không hoạt động cho các dịch vụ bị chặn\",\n  \"schedule_edit\": \"Chỉnh sửa lịch trình\",\n  \"schedule_from\": \"Từ\",\n  \"schedule_invalid_select\": \"Thời gian bắt đầu phải trước thời gian kết thúc\",\n  \"schedule_modal_description\": \"Lịch trình này sẽ thay thế mọi lịch trình hiện có cho cùng một ngày trong tuần. Mỗi ngày trong tuần chỉ có thể có một khoảng thời gian không hoạt động.\",\n  \"schedule_modal_time_off\": \"Chặn dịch vụ bị vô hiệu hóa:\",\n  \"schedule_new\": \"Kế hoạch mới\",\n  \"schedule_remove\": \"Xóa lịch biểu\",\n  \"schedule_save\": \"Lưu lịch trình\",\n  \"schedule_select_days\": \"Chọn ngày\",\n  \"schedule_services\": \"Tạm dừng chặn dịch vụ\",\n  \"schedule_services_desc\": \"Định cấu hình lịch tạm dừng của bộ lọc chặn dịch vụ\",\n  \"schedule_services_desc_client\": \"Định cấu hình lịch tạm dừng của bộ lọc chặn dịch vụ cho máy khách này\",\n  \"schedule_time_all_day\": \"Cả ngày\",\n  \"schedule_timezone\": \"Chọn múi giờ\",\n  \"schedule_to\": \"Đến\",\n  \"served_from_cache_label\": \"Được phục vụ từ bộ nhớ đệm\",\n  \"service_name\": \"Tên dịch vụ\",\n  \"set_static_ip\": \"Thiết lập địa chỉ IP tĩnh\",\n  \"settings\": \"Cài đặt\",\n  \"settings_custom\": \"Tùy chỉnh\",\n  \"settings_global\": \"Toàn cầu\",\n  \"setup_config_to_enable_dhcp_server\": \"Thiết lập cấu hình để bật máy chủ DHCP\",\n  \"setup_dns_notice\": \"Để sử dụng <1>DNS-over-HTTPS</1> hoặc <1>DNS-over-TLS</1>, bạn cần <0>định cấu hình Mã hóa</0> trong cài đặt AdGuard Home.\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> Sử dụng chuỗi <1>{{address}}</1>.\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> Sử dụng chuỗi <1>{{address}}</1>.\",\n  \"setup_dns_privacy_3\": \"<0>Đây là danh sách phần mềm bạn có thể sử dụng.</0>\",\n  \"setup_dns_privacy_4\": \"Trên thiết bị chạy iOS 14 hoặc macOS Big Sur bạn có thể tải tệp '.mobileconfig' đặc biệt có chứa máy chủ <highlight>DNS-over-HTTPS</highlight> hoặc <highlight>DNS-over-TLS</highlight> trong thiết lập DNS.\",\n  \"setup_dns_privacy_android_1\": \"Android 9 hỗ trợ DNS trên TLS nguyên bản. Để định cấu hình, hãy đi tới Cài đặt → Mạng & internet → Nâng cao → DNS Riêng Tư và nhập tên miền của bạn vào đó.\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0> hỗ trợ <1>DNS-over-HTTPS</1> và <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> thêm <1>DNS-over-HTTPS</1> hỗ trợ cho Android.\",\n  \"setup_dns_privacy_ioc_mac\": \"Cấu hình iOS và macOS\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> hỗ trợ <1>DNS-over-HTTPS</1>, nhưng để định cấu hình nó để sử dụng máy chủ của riêng bạn, bạn sẽ cần phải tạo một <2>DNS Stamp</2> cho nó.\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard for iOS</0> hỗ trợ thiết lập <1>DNS-over-HTTPS</1> và <1>DNS-over-TLS</1>.\",\n  \"setup_dns_privacy_other_1\": \"Bản thân AdGuard Home có thể là máy khách DNS an toàn trên mọi nền tảng.\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> hỗ trợ tất cả các giao thức DNS bảo mật đã biết.\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> hỗ trợ <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> hỗ trợ <1>DNS-over-HTTPS</1>.\",\n  \"setup_dns_privacy_other_5\": \"Bạn sẽ tìm thấy nhiều triển khai hơn <0>tại đây</0> và <1>tại đây</1>.\",\n  \"setup_dns_privacy_other_title\": \"Triển khai khác\",\n  \"setup_guide\": \"Hướng dẫn thiết lập\",\n  \"show_all_filter_type\": \"Hiện tất cả\",\n  \"show_blocked_responses\": \"Bị chặn\",\n  \"show_filtered_type\": \"Chỉ hiện đã lọc\",\n  \"show_processed_responses\": \"Đã xử lý\",\n  \"show_whitelisted_responses\": \"Đã thêm vào danh sách cho phép\",\n  \"sign_in\": \"Đăng nhập\",\n  \"sign_out\": \"Đăng xuất\",\n  \"source_label\": \"Nguồn\",\n  \"static_ip\": \"Địa chỉ IP tĩnh\",\n  \"static_ip_desc\": \"AdGuard Home là một máy chủ nên nó cần một địa chỉ IP tĩnh để hoạt động bình thường. Nếu không, tại một số thời điểm, bộ định tuyến của bạn có thể gán một địa chỉ IP khác cho thiết bị này.\",\n  \"statistics_clear\": \"Xoá thống kê\",\n  \"statistics_clear_confirm\": \"Bạn có chắc chắn muốn xóa số liệu thống kê?\",\n  \"statistics_cleared\": \"Xoá thống kê thành công\",\n  \"statistics_configuration\": \"Cấu hình thống kê\",\n  \"statistics_enable\": \"Bật thống kê\",\n  \"statistics_retention\": \"Duy trì thống kê\",\n  \"statistics_retention_confirm\": \"Bạn có chắc chắn muốn thay đổi lưu giữ số liệu thống kê? Nếu bạn giảm giá trị khoảng, một số dữ liệu sẽ bị mất\",\n  \"statistics_retention_desc\": \"Nếu bạn giảm giá trị khoảng, một số dữ liệu sẽ bị mất\",\n  \"stats_adult\": \"Website người lớn đã chặn\",\n  \"stats_disabled\": \"Số liệu thống kê đã bị vô hiệu hóa. Bạn có thể bật nó từ <0> trang cài đặt </0>.\",\n  \"stats_disabled_short\": \"Số liệu thống kê đã bị vô hiệu hóa\",\n  \"stats_malware_phishing\": \"Mã độc/lừa đảo đã chặn\",\n  \"stats_params\": \"Cấu hình thống kê\",\n  \"stats_query_domain\": \"Tên miền truy vấn nhiều\",\n  \"subnet_error\": \"Địa chỉ phải nằm trong một mạng con\",\n  \"sunday\": \"Chủ nhật\",\n  \"sunday_short\": \"Chủ nhật\",\n  \"system_host_files\": \"Hệ thống lưu trữ tệp\",\n  \"table_client\": \"Máy khách\",\n  \"table_name\": \"Tên\",\n  \"tags_desc\": \"Bạn có thể chọn các thẻ tương ứng với máy khách. Bao gồm các thẻ trong các quy tắc lọc để áp dụng chúng chính xác hơn. <0>Tìm hiểu thêm</0>.\",\n  \"tags_title\": \"Thẻ\",\n  \"test_upstream_btn\": \"Kiểm tra\",\n  \"theme_auto\": \"Tự động\",\n  \"theme_auto_desc\": \"Tự động (dựa trên chủ đề màu của thiết bị của bạn)\",\n  \"theme_dark\": \"Dark theme\",\n  \"theme_dark_desc\": \"Chủ đề tối\",\n  \"theme_light\": \"Light theme\",\n  \"theme_light_desc\": \"Chủ đề sáng\",\n  \"thursday\": \"Thứ năm\",\n  \"thursday_short\": \"Thứ 5\",\n  \"time_table_header\": \"Thời gian\",\n  \"top_blocked_domains\": \"Tên miền chặn nhiều\",\n  \"top_clients\": \"Người dùng hàng đầu\",\n  \"top_upstreams\": \"Máy chủ thượng nguồn hàng đầu\",\n  \"topline_expired_certificate\": \"Chứng chỉ SSL của bạn đã hết hạn. Cập nhật <0>Cài đặt mã hóa</0>.\",\n  \"topline_expiring_certificate\": \"Chứng chỉ SSL của bạn sắp hết hạn. Cập nhật <0>Cài đặt mã hóa</0>.\",\n  \"tracker_source\": \"Nguồn theo dõi\",\n  \"try_again\": \"Hãy thử lại\",\n  \"ttl_cache_validation\": \"Giá trị TTL trong bộ nhớ cache tối thiểu phải nhỏ hơn hoặc bằng giá trị lớn nhất\",\n  \"tuesday\": \"Thứ ba\",\n  \"tuesday_short\": \"Thứ 3\",\n  \"type_table_header\": \"Loại\",\n  \"unavailable_dhcp\": \"DHCP không khả dụng\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home không thể chạy máy chủ DHCP trên hệ điều hành của bạn\",\n  \"unblock\": \"Bỏ chặn\",\n  \"unblock_all\": \"Bỏ chặn tất cả\",\n  \"unblock_for_this_client_only\": \"Chỉ hủy chặn ứng dụng khách này\",\n  \"unknown_filter\": \"Bộ lọc không rõ {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} hiện có sẵn! <0>Chạm vào đây</0> để biết thêm thông tin.\",\n  \"update_failed\": \"Tự động cập nhật thất bại. Vui lòng <a>làm theo các bước</a> để cập nhật thủ công.\",\n  \"update_now\": \"Cập nhật ngay\",\n  \"updated_custom_filtering_toast\": \"Đã cập nhật quy tắc lọc tuỳ chỉnh\",\n  \"updated_save_search_toast\": \"Cài đặt Tìm kiếm an toàn đã được cập nhật\",\n  \"updated_upstream_dns_toast\": \"Các máy chủ thượng nguồn đã được lưu thành công\",\n  \"updates_checked\": \"Phiên bản mới của AdGuard Home có sẵn\",\n  \"updates_version_equal\": \"AdGuard Home đã được cập nhật\",\n  \"upstream\": \"Máy chủ thượng nguồn\",\n  \"upstream_dns\": \"Máy chủ DNS tìm kiếm\",\n  \"upstream_dns_cache_configuration\": \"Cấu hình bộ nhớ đệm upstream của các máy chủ DNS\",\n  \"upstream_dns_client_desc\": \"Nếu để trống trường này, AdGuardHome sẽ sử dụng nhũng máy chủ được cấu hình ở <0>Cấu hình DNS</0>.\",\n  \"upstream_dns_configured_in_file\": \"Cấu hình tại {{path}}\",\n  \"upstream_dns_help\": \"Nhập địa chỉ máy chủ một trên mỗi dòng. <a>Tìm hiểu thêm</a> về cách định cấu hình máy chủ DNS ngược dòng.\",\n  \"upstream_parallel\": \"Sử dụng truy vấn song song để tăng tốc độ giải quyết bằng cách truy vấn đồng thời tất cả các máy chủ ngược tuyến\",\n  \"upstream_timeout\": \"Thời gian chờ ngược dòng\",\n  \"upstream_timeout_desc\": \"Xác định số giây chờ để nhận phản hồi từ máy chủ thượng nguồn\",\n  \"upstreams\": \"Nguồn\",\n  \"use_adguard_browsing_sec\": \"Sử dụng dịch vụ bảo vệ duyệt web AdGuard\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home sẽ kiểm tra tên miền với dịch vụ bảo vệ duyệt web. Tính năng sử dụng một API thân thiện với quyền riêng tư: chỉ một phần ngắn tiền tố mã băm SHA256 được gửi đến máy chủ\",\n  \"use_adguard_parental\": \"Sử dụng dịch vụ quản lý của phụ huynh AdGuard\",\n  \"use_adguard_parental_hint\": \"AdGuard Home sẽ kiểm tra nếu tên miền chứa từ khoá người lớn. Tính năng sử dụng API thân thiện với quyền riêng tư tương tự với dịch vụ bảo vệ duyệt web\",\n  \"use_private_ptr_resolvers_desc\": \"Giải quyết các yêu cầu PTR, SOA và NS cho các miền ARPA chứa địa chỉ IP riêng thông qua máy chủ thượng nguồn riêng, DHCP, /etc/hosts, v. v. Nếu bị vô hiệu hóa, AdGuard Home sẽ phản hồi tất cả các yêu cầu đó bằng NXDOMAIN.\",\n  \"use_private_ptr_resolvers_title\": \"Sử dụng trình rDNS riêng tư\",\n  \"use_saved_key\": \"Sử dụng khóa đã lưu trước đó\",\n  \"username_label\": \"Tên đăng nhập\",\n  \"username_placeholder\": \"Nhập tên đăng nhập\",\n  \"validated_with_dnssec\": \"Xác thực bỏi DNSSEC\",\n  \"version\": \"phiên bản\",\n  \"version_request_error\": \"Cập nhật không thành công. Hãy kiểm tra kết nối internet của bạn.\",\n  \"wednesday\": \"Thứ Tư\",\n  \"wednesday_short\": \"Thứ 4\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/zh-cn.json",
    "content": "{\n  \"access_allowed_desc\": \"CIDR、IP 地址或<a>客户端 ID</a> 的列表。如已配置，则 AdGuard Home 将仅接受来自这些客户端的请求。\",\n  \"access_allowed_title\": \"允许的客户端\",\n  \"access_blocked_desc\": \"不要将此功能与过滤器混淆。AdGuard Home 将排除匹配这些域名的 DNS 查询，并且这些查询将不会在查询日志中显示。在此可以明确指定域名、通配符（wildcard）和网址过滤的规则，例如 \\\"example.org\\\"、\\\"*.example.org\\\" 或 \\\"||example.org^\\\"。\",\n  \"access_blocked_title\": \"不允许的域名\",\n  \"access_desc\": \"您可以在此处配置 AdGuard Home 的 DNS 服务器的访问规则\",\n  \"access_disallowed_desc\": \"CIDR、IP 地址或<a>客户端 ID</a> 的列表。如果已配置，则 AdGuard Home 将丢弃来自这些客户端的请求。如果允许的客户端已配置，此字段将会被忽略。\",\n  \"access_disallowed_title\": \"不允许的客户端\",\n  \"access_settings_saved\": \"访问设置保存成功\",\n  \"access_title\": \"访问设置\",\n  \"actions_table_header\": \"操作\",\n  \"add_allowlist\": \"添加白名单\",\n  \"add_blocklist\": \"添加黑名单\",\n  \"add_custom_list\": \"添加一个自定义列表\",\n  \"add_persistent_client\": \"添加为持久客户端\",\n  \"address\": \"地址\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home 会丢弃所有来自此客户端的 DNS 查询。\",\n  \"all_lists_up_to_date_toast\": \"所有列表都是最新的\",\n  \"all_queries\": \"所有查询记录\",\n  \"allow_this_client\": \"允许这个客户端\",\n  \"allowed\": \"允许项\",\n  \"anonymize_client_ip\": \"匿名化客户端 IP\",\n  \"anonymize_client_ip_desc\": \"不要在日志和统计信息中保存客户端的完整 IP 地址\",\n  \"anonymizer_notification\": \"<0>注意：</0> IP 匿名化已启用。您可以在<1>常规设置</1>中禁用它。\",\n  \"answer\": \"应答\",\n  \"apply_btn\": \"应用\",\n  \"auto_clients_desc\": \"有关正在使用或可能使用 AdGuard Home 的设备的 IP 地址的信息。此信息是从多个来源收集的，包括 hosts 文件、反向 DNS 等。\",\n  \"auto_clients_title\": \"客户端（运行时间）\",\n  \"autofix_warning_list\": \"其将会进行如下工作：<0>停用系统DNSStubListener</0><0>设置DNS服务器地址为127.0.0.1</0><0>将/etc/resolv.conf的符号链接目标替换为/run/systemd/resolv/resolv.conf</0><0>停止DNSStubListener（重新加载系统解析服务）</0>\",\n  \"autofix_warning_result\": \"因此，默认情况下所有来自系统的 DNS 请求都将由 AdGuard Home 处理。\",\n  \"autofix_warning_text\": \"若您单击「修复」，AdGuard Home 将会配置您的系统以使用 AdGuard Home 的 DNS 服务器。\",\n  \"average_processing_time\": \"平均处理时间\",\n  \"average_processing_time_hint\": \"处理 DNS 请求的平均时间（毫秒）\",\n  \"average_upstream_response_time\": \"上游服务器的平均响应时间\",\n  \"back\": \"返回\",\n  \"block\": \"拦截\",\n  \"block_all\": \"阻止所有\",\n  \"block_domain_use_filters_and_hosts\": \"使用过滤器和 Hosts 文件以拦截指定域名\",\n  \"block_for_this_client_only\": \"仅对此客户端拦截\",\n  \"block_services\": \"阻止特定服务\",\n  \"blocked_adult_websites\": \"被家长控制阻止\",\n  \"blocked_by\": \"<0>已被过滤器拦截</0>\",\n  \"blocked_by_cname_or_ip\": \"按 CNAME 或 IP 拦截\",\n  \"blocked_by_response\": \"因响应的 CNAME 或 IP 被屏蔽\",\n  \"blocked_response_ttl\": \"屏蔽的 TTL 应答\",\n  \"blocked_response_ttl_desc\": \"指定客户端应缓存已过滤响应的秒数\",\n  \"blocked_safebrowsing\": \"被安全浏览阻止\",\n  \"blocked_service\": \"已阻止的服务\",\n  \"blocked_services\": \"已阻止的服务\",\n  \"blocked_services_desc\": \"允许快速地阻止热门网站和服务。\",\n  \"blocked_services_global\": \"使用全局已阻止服务设置\",\n  \"blocked_services_saved\": \"已阻止服务的设置保存成功\",\n  \"blocked_threats\": \"拦截的威胁\",\n  \"blocking_ipv4\": \"拦截 IPv4\",\n  \"blocking_ipv4_desc\": \"拦截 A 记录请求返回的 IP 地址\",\n  \"blocking_ipv6\": \"拦截 IPv6\",\n  \"blocking_ipv6_desc\": \"拦截 AAAA 记录请求返回的 IP 地址\",\n  \"blocking_mode\": \"拦截模式\",\n  \"blocking_mode_custom_ip\": \"自定义 IP：以手动设置的 IP 地址响应\",\n  \"blocking_mode_default\": \"默认：被 Adblock 规则拦截时反应为零 IP 地址（A记录：0.0.0.0；AAAA记录：::）；被 /etc/hosts 规则拦截时反应为规则中指定 IP 地址\",\n  \"blocking_mode_null_ip\": \"空 IP：以零 IP 地址响应（A 记录 0.0.0.0；AAAA 记录 ::）\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN：以 NXDOMAIN 码响应\",\n  \"blocking_mode_refused\": \"REFUSED：以 REFUSED 码响应请求\",\n  \"blocklist\": \"黑名单\",\n  \"bootstrap_dns\": \"Bootstrap DNS 服务器\",\n  \"bootstrap_dns_desc\": \"DNS 服务器的 IP 地址，用于解析指定为上游的 DoH/DoT 解析器的 IP 地址。不允许添加注释。\",\n  \"cache_cleared\": \"已成功清除 DNS 缓存\",\n  \"cache_enabled\": \"启用缓存\",\n  \"cache_enabled_desc\": \"在本地存储 DNS 响应。\",\n  \"cache_optimistic\": \"乐观缓存\",\n  \"cache_optimistic_desc\": \"即使条目已过期，也让 AdGuard Home 从缓存中响应，并尝试刷新它们。\",\n  \"cache_size\": \"缓存大小\",\n  \"cache_size_desc\": \"DNS 缓存大小（单位：字节）\",\n  \"cache_size_validation\": \"启用时，缓存大小必须大于 0。\",\n  \"cache_ttl_max_override\": \"覆盖最大 TTL 值\",\n  \"cache_ttl_max_override_desc\": \"设定 DNS 缓存条目的最大 TTL 值（秒）。\",\n  \"cache_ttl_min_override\": \"覆盖最小 TTL 值\",\n  \"cache_ttl_min_override_desc\": \"缓存 DNS 响应时，延长从上游服务器接收到的 TTL 值 (秒)。\",\n  \"cancel_btn\": \"取消\",\n  \"category_label\": \"类别\",\n  \"check\": \"检查\",\n  \"check_client_id\": \"客户端标识符（ClientID 或 IP 地址）\",\n  \"check_cname\": \"CNAME: {{cname}}\",\n  \"check_desc\": \"检查主机名是否被过滤。\",\n  \"check_dhcp_servers\": \"检查 DHCP 服务器\",\n  \"check_dns_record\": \"选择 DNS 记录类型\",\n  \"check_enter_client_id\": \"输入客户端标识符\",\n  \"check_hostname\": \"主机名或域名\",\n  \"check_ip\": \"IP地址：{{ip}}\",\n  \"check_not_found\": \"未在您的筛选列表中找到\",\n  \"check_reason\": \"原因：{{reason}}\",\n  \"check_service\": \"服务名称：{{service}}\",\n  \"check_title\": \"检查过滤\",\n  \"check_updates_btn\": \"检查更新\",\n  \"check_updates_now\": \"立即检查更新\",\n  \"choose_allowlist\": \"选择白名单\",\n  \"choose_blocklist\": \"选择黑名单\",\n  \"choose_from_list\": \"从列表中选择\",\n  \"city\": \"城市\",\n  \"clear_cache\": \"清除缓存\",\n  \"click_to_view_queries\": \"点击查看查询\",\n  \"client_add\": \"添加客户端\",\n  \"client_added\": \"客户端 \\\"{{key}}\\\" 添加成功\",\n  \"client_blocked\": \"客户端 \\\"{{ip}}\\\" 被成功拦截\",\n  \"client_confirm_block\": \"确定要阻止客户端 \\\"{{ip}}\\\"?\",\n  \"client_confirm_delete\": \"您确定要删除客户端 \\\"{{key}}\\\"？\",\n  \"client_confirm_unblock\": \"您确定要解除对客户端 \\\"{{ip}}\\\" 的封锁吗？\",\n  \"client_deleted\": \"客户端 \\\"{{key}}\\\" 删除成功\",\n  \"client_details\": \"客户端详情\",\n  \"client_edit\": \"编辑客户端\",\n  \"client_global_settings\": \"使用全局设置\",\n  \"client_id\": \"客户端 ID\",\n  \"client_id_desc\": \"可根据一个特殊的客户端 ID 识别不同客户端。在<a>这里</a>您可以了解到更多关于如何识别客户端的信息。\",\n  \"client_id_placeholder\": \"输入客户端 ID\",\n  \"client_identifier\": \"标识符\",\n  \"client_identifier_desc\": \"客户端可通过 IP 、MAC 地址、CIDR 或客户端 ID（可用于 DoT/DoH/DoQ）被识别。<0>这里</0>您可多了解如何识别客户端。\",\n  \"client_name\": \"客户端 {{id}}\",\n  \"client_new\": \"新建客户端\",\n  \"client_settings\": \"客户端设置\",\n  \"client_table_header\": \"客户端\",\n  \"client_unblocked\": \"成功解除对 \\\"{{ip}}\\\" 客户端的封锁\",\n  \"client_updated\": \"客户端 \\\"{{key}}\\\" 更新成功\",\n  \"clients_desc\": \"配置已连接到 AdGuard Home 的设备的持久客户端记录\",\n  \"clients_not_found\": \"未找到客户端\",\n  \"clients_title\": \"持久客户端\",\n  \"compact\": \"紧凑\",\n  \"config_successfully_saved\": \"配置保存成功\",\n  \"configure\": \"配置\",\n  \"confirm_dns_cache_clear\": \"您确定要清除 DNS 缓存吗？\",\n  \"confirm_static_ip\": \"AdGuard Home 将把 {{ip}} 配置为静态 IP 地址。您想要继续吗？\",\n  \"copyright\": \"版权\",\n  \"country\": \"国家\",\n  \"custom_filter_rules\": \"自定义过滤器规则\",\n  \"custom_filter_rules_hint\": \"请确保每行只输入一条规则。你可以输入符合 adblock 语法或 Hosts 语法的规则。\",\n  \"custom_filtering_rules\": \"自定义过滤规则\",\n  \"custom_ip\": \"自定义 IP\",\n  \"custom_retention_input\": \"输入保留时间（小时）\",\n  \"custom_rotation_input\": \"输入旋转时间（小时）\",\n  \"dashboard\": \"仪表盘\",\n  \"date\": \"日期\",\n  \"default\": \"默认\",\n  \"delete_confirm\": \"您确定要删除 \\\"{{key}}\\\"？\",\n  \"delete_table_action\": \"删除\",\n  \"descr\": \"描述\",\n  \"details\": \"详细信息\",\n  \"dhcp_add_static_lease\": \"添加静态租约\",\n  \"dhcp_config_saved\": \"已成功保存 DHCP 服务器配置\",\n  \"dhcp_description\": \"如果你的路由器没有提供 DHCP （动态主机配置协议）设置，你可以使用 AdGuard 内置的 DHCP 服务器。\",\n  \"dhcp_disable\": \"停用 DHCP 服务器\",\n  \"dhcp_dynamic_ip_found\": \"您的系统对网络接口 <0>{{interfaceName}}</0> 使用了动态 IP 地址配置。要使用 DHCP 服务器，则必须对此网络接口使用静态 IP 地址配置。此网络接口当前的 IP 地址为 <0>{{ipAddress}}</0>。如您点击「启用 DHCP 服务器」按钮，AdGuard Home 将自动修改此网络接口以使用静态 IP 地址。\",\n  \"dhcp_edit_static_lease\": \"编辑静态租约\",\n  \"dhcp_enable\": \"启用 DHCP 服务器\",\n  \"dhcp_error\": \"AdGuard Home 无法确定在当前网络中是否存在其它 DHCP 服务器\",\n  \"dhcp_form_gateway_input\": \"网关 IP\",\n  \"dhcp_form_lease_input\": \"租期\",\n  \"dhcp_form_lease_title\": \"DHCP 租约时间（秒）\",\n  \"dhcp_form_range_end\": \"结束 IP 地址\",\n  \"dhcp_form_range_start\": \"起始 IP 地址\",\n  \"dhcp_form_range_title\": \"IP 地址范围\",\n  \"dhcp_form_subnet_input\": \"子网掩码\",\n  \"dhcp_found\": \"在当前网络中检测到 DHCP 服务器。如果启用内置的 DHCP 服务器可能不安全。\",\n  \"dhcp_hardware_address\": \"硬件地址\",\n  \"dhcp_interface_select\": \"选择 DHCP 接口\",\n  \"dhcp_ip_addresses\": \"IP 地址\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4设置\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6设置\",\n  \"dhcp_lease_added\": \"静态租约 \\\"{{key}}\\\" 已成功添加\",\n  \"dhcp_lease_deleted\": \"静态租约 \\\"{{key}}\\\" 已成功删除\",\n  \"dhcp_lease_updated\": \"静态租约 “{{key}}” 已成功更新\",\n  \"dhcp_leases\": \"DHCP 租约\",\n  \"dhcp_leases_not_found\": \"未找到 DHCP 租约\",\n  \"dhcp_new_static_lease\": \"新建静态租约\",\n  \"dhcp_not_found\": \"您可以安全地启用内置 DHCP 服务器。在当前网络中 AdGuard Home 未检测到任何起作用的 DHCP 服务器。然而，我们推荐您以手动方式重新检测，因为当前我们的自动检测不能确保 100% 准确。\",\n  \"dhcp_reset\": \"您确定要重置 DHCP 设定吗？\",\n  \"dhcp_reset_leases\": \"重置所有租约\",\n  \"dhcp_reset_leases_confirm\": \"您确定您想重置所有租约吗？\",\n  \"dhcp_reset_leases_success\": \"成功重置了 DHCP 租约\",\n  \"dhcp_settings\": \"DHCP 设置\",\n  \"dhcp_static_ip_error\": \"要使用 DHCP 服务器，则必须设置静态 IP 地址。AdGuard Home 无法确定此网络接口是否已被配置为使用静态 IP 地址。请手动为此网络接口设置静态 IP 地址。\",\n  \"dhcp_static_leases\": \"DHCP 静态租约\",\n  \"dhcp_static_leases_not_found\": \"未找到 DHCP 静态租约\",\n  \"dhcp_table_expires\": \"到期\",\n  \"dhcp_table_hostname\": \"主机名\",\n  \"dhcp_title\": \"DHCP 服务器（实验性）\",\n  \"dhcp_warning\": \"如果用户想要启用内置的 DHCP 服务器，请确保在当前网络中没有其它起作用的 DHCP 服务器。否则，此操作可能会中断已连接设备的网络连接！\",\n  \"disable_for_hours\": \"{{count}} 小时\",\n  \"disable_for_hours_plural\": \"{{count}} 小时\",\n  \"disable_for_minutes\": \"{{count}} 分钟\",\n  \"disable_for_minutes_plural\": \"{{count}} 分钟\",\n  \"disable_for_seconds\": \"{{count}} 秒\",\n  \"disable_for_seconds_plural\": \"{{count}} 秒\",\n  \"disable_ipv6\": \"禁用 IPv6 地址的解析\",\n  \"disable_ipv6_desc\": \"丢弃对 IPv6 地址（类型 AAAA）的所有 DNS 查询，并从 HTTPS 响应中删除 IPv6 相关的信息。\",\n  \"disable_notify_for_hours\": \"禁用保护 {{count}} 小时\",\n  \"disable_notify_for_hours_plural\": \"禁用保护 {{count}} 小时\",\n  \"disable_notify_for_minutes\": \"禁用保护 {{count}} 分钟\",\n  \"disable_notify_for_minutes_plural\": \"禁用保护 {{count}} 分钟\",\n  \"disable_notify_for_seconds\": \"禁用保护 {{count}} 秒\",\n  \"disable_notify_for_seconds_plural\": \"禁用保护 {{count}} 秒\",\n  \"disable_notify_until_tomorrow\": \"禁用保护直到明天\",\n  \"disable_protection\": \"禁用保护\",\n  \"disable_rewrites\": \"关闭重写规则\",\n  \"disable_until_tomorrow\": \"直到明天\",\n  \"disabled\": \"已禁用\",\n  \"disabled_dhcp\": \"DHCP 服务器已禁用\",\n  \"disabled_filtering_toast\": \"过滤器已禁用\",\n  \"disabled_parental_toast\": \"家长控制已禁用\",\n  \"disabled_protection\": \"保护已禁用\",\n  \"disabled_safe_browsing_toast\": \"安全浏览已禁用\",\n  \"disabled_safe_search_toast\": \"安全搜索已禁用\",\n  \"disallow_this_client\": \"不允许这个客户端\",\n  \"dns_addresses\": \"DNS 地址\",\n  \"dns_allowlists\": \"DNS 白名单\",\n  \"dns_allowlists_desc\": \"来自 DNS 白名单的域名将被允许，即使它们位于任意黑名单中也是如此。\",\n  \"dns_blocklists\": \"DNS 黑名单\",\n  \"dns_blocklists_desc\": \"AdGuard Home 将阻止 DNS 黑名单里的域名\",\n  \"dns_cache_config\": \"DNS 缓存配置\",\n  \"dns_cache_config_desc\": \"您可以在此处配置 DNS 缓存\",\n  \"dns_cache_size\": \"DNS 缓存大小，单位：字节\",\n  \"dns_config\": \"DNS 服务配置\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS 隐私\",\n  \"dns_providers\": \"此为可从中选择的<0>已知 DNS 提供商列表</0>。\",\n  \"dns_query\": \"DNS 查询\",\n  \"dns_rewrites\": \"DNS 重写\",\n  \"dns_settings\": \"DNS 设置\",\n  \"dns_start\": \"正在启动DNS服务\",\n  \"dns_status_error\": \"检查 DNS 服务器状态时出错\",\n  \"dns_test_not_ok_toast\": \"服务器 \\\"{{key}}\\\"：无法使用，请检查你输入的是否正确\",\n  \"dns_test_ok_toast\": \"指定的 DNS 服务器现已正常运行\",\n  \"dns_test_parsing_error_toast\": \"第 {{section}} 节：第 {{line}} 行：无法使用，请检查您输入的是否正确\",\n  \"dns_test_warning_toast\": \"上游 “{{key}}” 不响应测试请求，可能无法正常工作\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"启用 DNSSEC\",\n  \"dnssec_enable_desc\": \"在发出 DNS 查询中设置 DNSSEC 标志并检查结果（需要启用 DNSSEC 的解析器）。\",\n  \"domain\": \"域名\",\n  \"domain_desc\": \"输入您要重写的域名或通配符。\",\n  \"domain_name_table_header\": \"域名\",\n  \"domain_or_client\": \"域名或客户端\",\n  \"down\": \"下移\",\n  \"download_mobileconfig\": \"下载配置文件\",\n  \"download_mobileconfig_doh\": \"下载适用于 DNS-over-HTTPS 的 .mobileconfig\",\n  \"download_mobileconfig_dot\": \"下载适用于 DNS-over-TLS 的 .mobileconfig\",\n  \"ecs\": \"ECS\",\n  \"edit_allowlist\": \"编辑白名单\",\n  \"edit_blocklist\": \"编辑黑名单\",\n  \"edit_table_action\": \"编辑\",\n  \"edns_cs_desc\": \"在上游请求中加入 EDNS 客户端子网（“EDNS Client Subnet”，即 ECS）选项，并在查询日志中记录客户端发送的数值。\",\n  \"edns_enable\": \"启用 EDNS 客户端子网\",\n  \"edns_use_custom_ip\": \"为 EDNS 使用自定义 IP\",\n  \"edns_use_custom_ip_desc\": \"允许为 EDNS 使用自定义 IP\",\n  \"elapsed\": \"耗时\",\n  \"empty_response_status\": \"空\",\n  \"enable_protection\": \"启用保护\",\n  \"enable_protection_timer\": \"保护将于 {{time}} 启用\",\n  \"enable_rewrites\": \"启用重写规则\",\n  \"enable_upstream_dns_cache\": \"为该客户端的自定义上游配置启用 DNS 缓存\",\n  \"enabled_dhcp\": \"DHCP 服务器已启用\",\n  \"enabled_filtering_toast\": \"过滤器已启用\",\n  \"enabled_parental_toast\": \"家长控制已启用\",\n  \"enabled_protection\": \"保护已启用\",\n  \"enabled_safe_browsing_toast\": \"安全浏览已启用\",\n  \"enabled_save_search_toast\": \"安全搜索已启用\",\n  \"enabled_table_header\": \"已启用\",\n  \"encryption_certificate_path\": \"证书路径\",\n  \"encryption_certificates\": \"证书\",\n  \"encryption_certificates_desc\": \"为了使用加密，您需要为域提供有效的 SSL 证书链。您可以在 <0>{{link}}</0> 上获得免费证书，也可以从受信任的证书颁发机构购买证书。\",\n  \"encryption_certificates_input\": \"将您以 PEM 格式编码的证书复制粘贴到此处。\",\n  \"encryption_certificates_source_content\": \"粘贴证书内容\",\n  \"encryption_certificates_source_path\": \"设置证书路径\",\n  \"encryption_chain_invalid\": \"证书链无效\",\n  \"encryption_chain_valid\": \"证书链有效\",\n  \"encryption_config_saved\": \"加密配置已保存\",\n  \"encryption_desc\": \"为 DNS 与网页管理界面启用加密（HTTPS/QUIC/TLS）\",\n  \"encryption_doq\": \"DNS-over-QUIC 端口\",\n  \"encryption_doq_desc\": \"如果配置了此端口，AdGuard Home 将在此端口上运行一个 DNS-over-QUIC 服务器。\",\n  \"encryption_dot\": \"DNS-over-TLS 端口\",\n  \"encryption_dot_desc\": \"如果配置了此端口，AdGuard Home 将在此端口上运行一个 DNS-over-TLS 服务器。\",\n  \"encryption_enable\": \"启用加密（HTTPS、DNS-over-HTTPS、DNS-over-TLS）\",\n  \"encryption_enable_desc\": \"如果启用加密选项，AdGuard Home 的网页管理界面将通过 HTTPS 连接访问，同时 DNS 服务器将监听通过 DNS-over-HTTPS 与 DNS-over-TLS 发送的请求。\",\n  \"encryption_expire\": \"有效期\",\n  \"encryption_hostnames\": \"主机名\",\n  \"encryption_https\": \"HTTPS 端口\",\n  \"encryption_https_desc\": \"如果配置了 HTTPS 端口，AdGuard Home 管理界面将可以通过 HTTPS 访问，它还将在在 '/dns-query' 位置提供 DNS-over-HTTPS 。\",\n  \"encryption_issuer\": \"颁发者\",\n  \"encryption_key\": \"私钥\",\n  \"encryption_key_input\": \"将您以 PEM 格式编码的证书私钥复制粘贴到此处。\",\n  \"encryption_key_invalid\": \"该 {{type}} 私钥无效\",\n  \"encryption_key_source_content\": \"粘贴私钥内容\",\n  \"encryption_key_source_path\": \"设置私钥文件路径\",\n  \"encryption_key_valid\": \"该 {{type}} 私钥有效\",\n  \"encryption_plain_dns_desc\": \"默认情况下启用无加密 DNS。用户可以禁用它，强制所有设备使用加密 DNS。为此，必须至少启用一个加密 DNS 协议\",\n  \"encryption_plain_dns_enable\": \"启用无加密 DNS\",\n  \"encryption_plain_dns_error\": \"要禁用无加密 DNS，请至少启用一个加密 DNS 协议\",\n  \"encryption_private_key_path\": \"私钥路径\",\n  \"encryption_redirect\": \"HTTPS 自动重定向\",\n  \"encryption_redirect_desc\": \"如果勾选此选项，AdGuard Home 将自动将您从 HTTP 重定向到 HTTPS 地址。\",\n  \"encryption_reset\": \"您确定想要重置加密设置？\",\n  \"encryption_server\": \"服务器名称\",\n  \"encryption_server_desc\": \"设置后，AdGuard Home 检测客户端标识号，对 DDR 查询作出反应，以及进一步检查连接。在没有设置的情况下，该功能被禁用。必须与证书里的一个 DNS 名称相匹配。\",\n  \"encryption_server_enter\": \"输入您的域名\",\n  \"encryption_settings\": \"加密设置\",\n  \"encryption_status\": \"状态\",\n  \"encryption_subject\": \"使用者\",\n  \"encryption_title\": \"加密\",\n  \"encryption_warning\": \"警告\",\n  \"enforce_safe_search\": \"使用安全搜索\",\n  \"enforce_save_search_hint\": \"AdGuard Home 将会在下列搜索引擎中强制启用安全搜索：Google、YouTube、Bing、DuckDuckGo、Ecosia、Yandex、Pixabay。\",\n  \"enforced_save_search\": \"强制安全搜索\",\n  \"enter_cache_size\": \"输入缓存大小（字节）\",\n  \"enter_cache_ttl_max_override\": \"输入最大 TTL 值（秒）\",\n  \"enter_cache_ttl_min_override\": \"输入最小 TTL 值（秒）\",\n  \"enter_name_hint\": \"输入名称\",\n  \"enter_url_or_path_hint\": \"请输入URL或列表的绝对路径\",\n  \"enter_valid_allowlist\": \"输入有效的白名单 URL\",\n  \"enter_valid_blocklist\": \"输入有效的黑名单 URL\",\n  \"error_details\": \"详细错误信息\",\n  \"example_comment\": \"! 这是一行注释。\",\n  \"example_comment_hash\": \"# 这也是一行注释。\",\n  \"example_comment_meaning\": \"只是一条注释；\",\n  \"example_meaning_filter_block\": \"阻止 example.org 域名及其所有子域名；\",\n  \"example_meaning_filter_whitelist\": \"解除对 example.org 及其所有子域名的拦截；\",\n  \"example_meaning_host_block\": \"对 example.org（不包括它的子域名）以 127.0.0.1 作为响应；\",\n  \"example_multiple_upstreams_reserved\": \"<0>特定域名</0>的多个上游服务器；\",\n  \"example_regex_meaning\": \"阻止访问与指定的正则表达式匹配的域名。\",\n  \"example_rewrite_domain\": \"仅重写此域名的响应。\",\n  \"example_rewrite_wildcard\": \"重写所有<0>example.org</0> 子域的响应。\",\n  \"example_upstream_comment\": \"注释。\",\n  \"example_upstream_doh\": \"加密 <0>DNS-over-HTTPS</0>；\",\n  \"example_upstream_doh3\": \"带有强制 <0>HTTP/3</0> 的加密 DNS-over-HTTPS，不会回退到 HTTP/2 或更低版本;\",\n  \"example_upstream_doq\": \"加密 <0>DNS-over-QUIC</0>\",\n  \"example_upstream_dot\": \"加密 <0>DNS-over-TLS</0>；\",\n  \"example_upstream_regular\": \"常规 DNS（基于 UDP）；\",\n  \"example_upstream_regular_port\": \"常规 DNS（通过 UDP，带端口）；\",\n  \"example_upstream_reserved\": \"指定为<0>特定域名</0>的上游服务器；\",\n  \"example_upstream_sdns\": \"<1>DNSCrypt</1> 的 <0>DNS Stamps</0> 或者 <2>DNS-over-HTTPS</2> 解析器；\",\n  \"example_upstream_tcp\": \"常规 DNS（基于 TCP ）；\",\n  \"example_upstream_tcp_hostname\": \"常规 DNS（通过 TCP、主机名）；\",\n  \"example_upstream_tcp_port\": \"常规 DNS（通过 TCP，带端口）；\",\n  \"example_upstream_udp\": \"常规 DNS（通过 UDP、主机名）；\",\n  \"examples_title\": \"示例\",\n  \"fallback_dns_desc\": \"当上游 DNS 服务器没有响应时使用的后备 DNS 服务器列表。语法与上面的「主要上游」字段相同。\",\n  \"fallback_dns_placeholder\": \"每行输入一个后备 DNS 服务器\",\n  \"fallback_dns_title\": \"后备 DNS 服务器\",\n  \"faq\": \"常见问题\",\n  \"fastest_addr\": \"最快的 IP 地址\",\n  \"fastest_addr_desc\": \"等待<b>所有</b> DNS 服务器的响应，测量每个服务器的 TCP 连接速度，并返回连接速度最快的服务器的 IP 地址。<br/>如果一个或多个上游服务器没有响应，此模式会显著减慢 DNS 查询速度。确保您的上游服务器稳定且上游超时时间短。\",\n  \"filter\": \"过滤器\",\n  \"filter_added_successfully\": \"已成功添加过滤器\",\n  \"filter_allowlist\": \"警告：此操作将把规则 \\\"{{disallowed_rule}}\\\" 排除在允许客户端的列表之外。\",\n  \"filter_category_general\": \"常规\",\n  \"filter_category_general_desc\": \"在大多数设备上阻止跟踪和广告的列表\",\n  \"filter_category_other\": \"其它\",\n  \"filter_category_other_desc\": \"其他黑名单\",\n  \"filter_category_regional\": \"区域\",\n  \"filter_category_regional_desc\": \"专注于区域广告和跟踪服务器的列表\",\n  \"filter_category_security\": \"安全\",\n  \"filter_category_security_desc\": \"专用于拦截恶意软件、钓鱼或欺诈域名的列表\",\n  \"filter_removed_successfully\": \"已成功删除该列表\",\n  \"filter_updated\": \"成功更新过滤器\",\n  \"filtered\": \"已过滤\",\n  \"filtered_custom_rules\": \"被自定义过滤规则过滤\",\n  \"filtering_rules_learn_more\": \"<0>了解更多</0>关于创建自己的 hosts 清单。\",\n  \"filters\": \"过滤器\",\n  \"filters_and_hosts_hint\": \"AdGuard Home 可以解析基础的 adblock 规则和 Hosts 语法。\",\n  \"filters_block_toggle_hint\": \"你可以在 <a>过滤器</a> 设置中添加过滤规则。\",\n  \"filters_configuration\": \"过滤器配置\",\n  \"filters_enable\": \"启用过滤器\",\n  \"filters_interval\": \"过滤器更新间隔\",\n  \"fix\": \"修复\",\n  \"for_last_days\": \"最近 {{count}} 天\",\n  \"for_last_days_plural\": \"最近 {{count}} 天\",\n  \"for_last_hours\": \"最近 {{count}} 小时\",\n  \"for_last_hours_plural\": \"最近 {{count}} 小时\",\n  \"forgot_password\": \"忘记密码？\",\n  \"forgot_password_desc\": \"请遵从<0>这些步骤</0>来为你的用户账号创建一个新密码\",\n  \"form_add_id\": \"添加标识符\",\n  \"form_answer\": \"输入 IP 地址或域名\",\n  \"form_client_name\": \"输入客户端名称\",\n  \"form_domain\": \"输入域\",\n  \"form_enter_blocked_response_ttl\": \"输入拦截的 TTL 应答（秒）\",\n  \"form_enter_host\": \"输入主机名称\",\n  \"form_enter_hostname\": \"输入主机名称\",\n  \"form_enter_id\": \"输入标识符\",\n  \"form_enter_ip\": \"输入 IP\",\n  \"form_enter_mac\": \"输入 MAC\",\n  \"form_enter_rate_limit\": \"输入限制速率\",\n  \"form_enter_rate_limit_subnet_len\": \"输入用于速率限制的子网前缀长度\",\n  \"form_enter_subnet_ip\": \"输入一个 IP 地址，其须位于子网 \\\"{{cidr}}\\\"\",\n  \"form_enter_upstream_timeout\": \"输入上游服务器超时时间（以秒为单位）\",\n  \"form_error_answer_format\": \"无效的响应格式\",\n  \"form_error_client_id_format\": \"客户端 ID 必须只包含数字、小写字母和连字符\",\n  \"form_error_domain_format\": \"无效的域格式\",\n  \"form_error_equal\": \"不可相同\",\n  \"form_error_gateway_ip\": \"租约期限不能有网关的 IP 地址\",\n  \"form_error_ip4_format\": \"无效的 IPv4 地址\",\n  \"form_error_ip4_gateway_format\": \"网关 IPv4 地址无效\",\n  \"form_error_ip6_format\": \"无效的 IPv6 地址\",\n  \"form_error_ip_format\": \"无效的 IP 地址\",\n  \"form_error_mac_format\": \"无效的 MAC 地址\",\n  \"form_error_password\": \"密码不匹配\",\n  \"form_error_password_length\": \"密码长度必须为 {{min}} 到 {{max}} 个字符\",\n  \"form_error_port\": \"输入有效的端口值\",\n  \"form_error_port_range\": \"输入 80 - 65535 范围内的端口值\",\n  \"form_error_port_unsafe\": \"这是一个不安全的端口\",\n  \"form_error_positive\": \"必须大于 0\",\n  \"form_error_required\": \"必填字段\",\n  \"form_error_server_name\": \"无效的服务器名\",\n  \"form_error_subnet\": \"子网 \\\"{{cidr}}\\\" 不包含 IP 地址 \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"无效的 URL 格式\",\n  \"form_error_url_or_path_format\": \"无效的 URL 或列表的绝对路径\",\n  \"form_select_tags\": \"选择客户端标签\",\n  \"found_in_known_domain_db\": \"成功在已知域名数据库中查询到\",\n  \"friday\": \"星期五\",\n  \"friday_short\": \"周五\",\n  \"gateway_or_subnet_invalid\": \"子网掩码无效\",\n  \"general_settings\": \"常规设置\",\n  \"general_statistics\": \"概况统计\",\n  \"get_started\": \"开始配置\",\n  \"greater_range_start_error\": \"必须大于范围起始值\",\n  \"homepage\": \"主页\",\n  \"host_whitelisted\": \"主机在白名单内\",\n  \"ignore_domains\": \"忽略的域名（以换行符分隔）\",\n  \"ignore_domains_desc_query\": \"匹配这些规则的查询不在查询日志\",\n  \"ignore_domains_desc_stats\": \"匹配这些规则的查询不在统计信息\",\n  \"ignore_domains_title\": \"被忽略的域名\",\n  \"ignore_query_log\": \"在查询日志中忽略此客户端\",\n  \"ignore_statistics\": \"在统计数据中忽略此客户端\",\n  \"install_auth_confirm\": \"确认密码\",\n  \"install_auth_desc\": \"需要对 AdGuard Home 的网页管理界面配置密码认证。即使 AdGuard Home 只能通过本地网络访问，为它添加访问限制依旧十分重要。\",\n  \"install_auth_password\": \"密码\",\n  \"install_auth_password_enter\": \"输入密码\",\n  \"install_auth_title\": \"身份认证\",\n  \"install_auth_username\": \"用户名\",\n  \"install_auth_username_enter\": \"输入用户名\",\n  \"install_devices_address\": \"AdGuard Home DNS 服务器正在监听以下地址\",\n  \"install_devices_android_list_1\": \"在安卓主屏幕菜单中点击设置。\",\n  \"install_devices_android_list_2\": \"点击菜单上的「无线局域网」选项。在屏幕上将列出所有可用的网络（蜂窝移动网络不支持修改 DNS ）。\",\n  \"install_devices_android_list_3\": \"长按当前已连接的网络，然后点击「修改网络设置」。\",\n  \"install_devices_android_list_4\": \"在某些设备上，您可能需要选中「高级」复选框以查看进一步的设置。您可能需要调整您安卓设备的 DNS 设置，或是需要将 IP 设置从 DHCP 切换到静态。\",\n  \"install_devices_android_list_5\": \"将 DNS 1 和 DNS 2 的值改为您的 AdGuard Home 服务器地址。\",\n  \"install_devices_desc\": \"为保证 AdGuard Home 可以开始正常工作，您需要在设备上对其进行配置。\",\n  \"install_devices_ios_list_1\": \"从主屏幕中点击「设置」。\",\n  \"install_devices_ios_list_2\": \"从左侧目录中选择「无线局域网」（移动数据网络环境下不支持修改 DNS ）。\",\n  \"install_devices_ios_list_3\": \"点击当前已连接网络的名称。\",\n  \"install_devices_ios_list_4\": \"在 DNS 字段中输入您的 AdGuard Home 服务器地址。\",\n  \"install_devices_macos_list_1\": \"点击苹果图标，进入「系统首选项」。\",\n  \"install_devices_macos_list_2\": \"点击「网络」。\",\n  \"install_devices_macos_list_3\": \"选择在列表中的第一个连接，并点击「高级」。\",\n  \"install_devices_macos_list_4\": \"选择「DNS」选项卡，并输入您的 AdGuard Home 服务器地址。\",\n  \"install_devices_router\": \"路由器\",\n  \"install_devices_router_desc\": \"此设置将自动覆盖连接到您的家庭路由器的所有设备，您不需要手动配置它们。\",\n  \"install_devices_router_list_1\": \"打开您的路由器配置界面。通常情况下，您可以通过浏览器访问地址（如 http://192.168.0.1/ 或 http://192.168.1.1 ）。打开后您可能需要输入密码以进入配置界面。如果您不记得密码，通常可以通过路由器上的重置按钮来重设密码。但是，请注意，如您进行此操作，您最可能会失去所有路由器的配置。如果您的路由器需要通过特定的应用进行这一操作，请将相关应用程序安装到您的手机或计算机上并使用它设置您的路由器。\",\n  \"install_devices_router_list_2\": \"找到路由器的 DHCP/DNS 设置页面。您会在 DNS 这一单词旁边找到两到三行允许输入的输入框，每一行输入框分为四组，每组允许输入一到三个数字。\",\n  \"install_devices_router_list_3\": \"请在此处输入您的 AdGuard Home 服务器地址。\",\n  \"install_devices_router_list_4\": \"在某些类型的路由器上无法设置自定义 DNS 服务器。在此情况下将 AdGuard Home 设置为 <0>DHCP 服务器</0>，可能会有所帮助。否则您应该查找如何根据特定路由器型号设置 DNS 服务器的使用手册。\",\n  \"install_devices_title\": \"配置您的设备\",\n  \"install_devices_windows_list_1\": \"通过开始菜单或 Windows 搜索功能打开控制面板。\",\n  \"install_devices_windows_list_2\": \"点击进入「网络和 Internet」后，再次点击进入「网络和共享中心」\",\n  \"install_devices_windows_list_3\": \"在窗口的左侧点击「更改适配器设置」。\",\n  \"install_devices_windows_list_4\": \"选择您正在连接的网络设备，右击它并选择「属性”」。\",\n  \"install_devices_windows_list_5\": \"在列表中找到「Internet 协议版本 4 (TCP/IPv4)」，选择并再次点击「属性」。\",\n  \"install_devices_windows_list_6\": \"选择「使用下面的 DNS 服务器地址」，并输入您的 AdGuard Home 服务器地址。\",\n  \"install_saved\": \"保存成功\",\n  \"install_settings_all_interfaces\": \"所有接口\",\n  \"install_settings_dns\": \"DNS 服务器\",\n  \"install_settings_dns_desc\": \"您将需要使用以下地址来设置您的设备或路由器的 DNS 服务器：\",\n  \"install_settings_interface_link\": \"您可以通过以下地址访问您的 AdGuard Home 网页管理界面：\",\n  \"install_settings_listen\": \"监听接口\",\n  \"install_settings_port\": \"端口\",\n  \"install_settings_title\": \"网页管理界面\",\n  \"install_static_configure\": \"AdGuard Home 检测到一个动态 IP 地址 <0>{{ip}}</0> 被使用。您想把它作为您的静态地址吗？\",\n  \"install_static_error\": \"AdGuard Home 无法为这个网络接口自动配置它。请参阅如何手动完成此操作的说明。\",\n  \"install_static_ok\": \"好消息！静态 IP 地址已经配置\",\n  \"install_step\": \"步骤\",\n  \"install_submit_desc\": \"安装过程已经完成，您可以开始使用 AdGuard Home 了。\",\n  \"install_submit_title\": \"恭喜您！\",\n  \"install_welcome_desc\": \"AdGuard Home 是一个可在特定网络范围内拦截所有广告和跟踪器的 DNS 服务器。它的目的是让您控制整个网络和您的所有设备，且不需要使用任何客户端程序。\",\n  \"install_welcome_title\": \"欢迎使用 AdGuard Home！\",\n  \"interval_24_hour\": \"24 小时\",\n  \"interval_6_hour\": \"6 小时\",\n  \"interval_days\": \"{{count}} 天\",\n  \"interval_days_plural\": \"{{count}} 天\",\n  \"interval_hours\": \"{{count}} 小时\",\n  \"interval_hours_plural\": \"{{count}} 小时\",\n  \"ip\": \"IP 地址\",\n  \"ip_address\": \"IP 地址\",\n  \"known_tracker\": \"已知跟踪器\",\n  \"last_rule_in_allowlist\": \"无法禁止此客户端，因为排除 “{{disallowed_rule}}” 规则将禁用“允许客户端”的列表。\",\n  \"last_time_updated_table_header\": \"上次更新时间\",\n  \"list_confirm_delete\": \"您确定要删除此列表吗？\",\n  \"list_label\": \"列表\",\n  \"list_updated\": \"{{count}} 列表已更新\",\n  \"list_updated_plural\": \"{{count}} 条列表已更新\",\n  \"list_url_table_header\": \"清单网址\",\n  \"load_balancing\": \"负载均衡\",\n  \"load_balancing_desc\": \"一次查询一台上游服务器。<br/>AdGuard Home 使用加权随机算法来选择具有最少失败查找和最低平均查找时间的服务器。\",\n  \"loading_table_status\": \"加载中……\",\n  \"local_ptr_default_resolver\": \"AdGuard Home 默认使用下列反向 DNS 解析器: {{ip}}\",\n  \"local_ptr_desc\": \"AdGuard Home 用于私人 PTR、SOA 和 NS 请求的 DNS 服务器。如果请求的 ARPA 域名包含私有 IP 范围内的子网（如 \\\"192.168.12.34\\\"），且请求来自具有私有 IP 地址的客户端，该请求被视为私有请求。如果未设置，将使用操作系统的默认 DNS 解析器，AdGuard Home IP 地址除外。\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home 无法为这个系统确定合适的私人反向 DNS 解析器。\",\n  \"local_ptr_placeholder\": \"每行输入一个 IP 地址\",\n  \"local_ptr_title\": \"私人反向 DNS 服务器\",\n  \"location\": \"地址\",\n  \"log_and_stats_section_label\": \"查询日志和统计数据\",\n  \"lower_range_start_error\": \"必须小于范围起始值\",\n  \"main_settings\": \"主要设置\",\n  \"make_static\": \"静态化\",\n  \"manual_update\": \"请跟随<a>此步骤</a>以进行手动更新。\",\n  \"milliseconds_abbreviation\": \"毫秒\",\n  \"monday\": \"星期一\",\n  \"monday_short\": \"周一\",\n  \"name\": \"名称\",\n  \"name_table_header\": \"名称\",\n  \"netname\": \"网络名称\",\n  \"network\": \"网络\",\n  \"new_allowlist\": \"新增白名单\",\n  \"new_blocklist\": \"新封锁清单\",\n  \"next\": \"下一步\",\n  \"next_btn\": \"下一页\",\n  \"no_blocklist_added\": \"未添加黑名单\",\n  \"no_clients_found\": \"未找到客户端\",\n  \"no_domains_found\": \"未找到域名\",\n  \"no_logs_found\": \"未找到日志\",\n  \"no_servers_specified\": \"未找到指定的服务器\",\n  \"no_upstreams_data_found\": \"未找到上游服务器数据\",\n  \"no_whitelist_added\": \"未添加白名单\",\n  \"nothing_found\": \"没找到\",\n  \"null_ip\": \"空 IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"被广告过滤器和 Hosts 黑名单阻止的 DNS 请求总数\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"被阻止的成人网站总数\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"被 AdGuard 安全浏览模块阻止的 DNS 请求总数\",\n  \"number_of_dns_query_days\": \"过去 {{count}} 天内处理的 DNS 查询总数\",\n  \"number_of_dns_query_days_plural\": \"在过去的 {{count}} 天内处理了多少个 DNS 查询\",\n  \"number_of_dns_query_hours\": \"最近 {{count}} 小时内处理的 DNS 查询次数\",\n  \"number_of_dns_query_hours_plural\": \"最近 {{count}} 小时内处理的 DNS 查询次数\",\n  \"number_of_dns_query_to_safe_search\": \"启用强制安全搜索后对搜索引擎的 DNS 请求总数\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"禁用中\",\n  \"on\": \"启用中\",\n  \"open_dashboard\": \"打开仪表盘\",\n  \"orgname\": \"机构名称\",\n  \"original_response\": \"原始响应\",\n  \"out_of_range_error\": \"必定超出了范围 \\\"{{start}}\\\"-\\\"{{end}}\\\"\",\n  \"page_table_footer_text\": \"页\",\n  \"parallel_requests\": \"并行请求\",\n  \"parental_control\": \"家长控制\",\n  \"password_label\": \"密码\",\n  \"password_placeholder\": \"输入密码\",\n  \"plain_dns\": \"无加密 DNS\",\n  \"port_53_faq_link\": \"53端口常被 DNSStubListener 或 systemdn 解析的服务占用。请阅读<0>这份关于如何解决这一问题的说明</0>。\",\n  \"previous_btn\": \"上一页\",\n  \"privacy_policy\": \"隐私政策\",\n  \"processing_update\": \"正在更新 AdGuard Home，请稍侯\",\n  \"protection_section_label\": \"防护\",\n  \"protocol\": \"协议\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"查询日志\",\n  \"query_log_clear\": \"清除查询日志\",\n  \"query_log_cleared\": \"查询日志已成功清除\",\n  \"query_log_configuration\": \"日志配置\",\n  \"query_log_confirm_clear\": \"确定想要清除全部查询日志吗？\",\n  \"query_log_disabled\": \"查询日志已禁用，在<0>这些设置</0>中能配置它们\",\n  \"query_log_enable\": \"启用日志\",\n  \"query_log_filtered\": \"被 {{filter}} 过滤\",\n  \"query_log_response_status\": \"状态： {{value}}\",\n  \"query_log_retention\": \"查询日志保留时间\",\n  \"query_log_retention_confirm\": \"确定要更改查询记录保留时间吗？如果减少时间间隔数值，可能会丢失某些数据\",\n  \"query_log_strict_search\": \"使用双引号进行精确搜索\",\n  \"query_log_updated\": \"已成功更新查询日志\",\n  \"rate_limit\": \"速度限制\",\n  \"rate_limit_desc\": \"每个客户端每秒钟查询次数的限制。设置为 0 意味着不限制。\",\n  \"rate_limit_subnet_len_ipv4\": \"IPv4 地址子网前缀长度\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"用于速率限制的 IPv4 地址子网前缀长度。默认为 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4 子网前缀长度应介于 0 到 32 之间\",\n  \"rate_limit_subnet_len_ipv6\": \"IPv6 地址子网前缀长度\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"用于速率限制的 IPv6 地址子网前缀长度。默认为 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6 子网前缀长度应介于 0 到 128 之间\",\n  \"rate_limit_whitelist\": \"速率限制白名单\",\n  \"rate_limit_whitelist_desc\": \"排除在速率限制之外的 IP 地址\",\n  \"rate_limit_whitelist_placeholder\": \"每行输入一个 IP 地址\",\n  \"refresh_btn\": \"刷新\",\n  \"refresh_statics\": \"刷新统计数据\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"问题反馈\",\n  \"request_details\": \"请求详情\",\n  \"request_table_header\": \"请求\",\n  \"requests_count\": \"请求数\",\n  \"reset_settings\": \"重置设置\",\n  \"resolve_clients_desc\": \"通过发送 PTR 查询到对应的解析器 (本地客户端的私人 DNS 服务器，公共 IP 客户端的上游服务器) 将 IP 地址反向解析成其客户端主机名。\",\n  \"resolve_clients_title\": \"启用客户端的 IP 地址的反向解析\",\n  \"response_code\": \"响应代码\",\n  \"response_details\": \"响应细节\",\n  \"response_table_header\": \"响应\",\n  \"response_time\": \"响应时间\",\n  \"rewrite_A\": \"<0>A</0>：特殊值，保持来自上游的<0>A</0>记录\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>：特殊值，保持来自上游的<0>AAAA</0>记录\",\n  \"rewrite_add\": \"添加 DNS 重写\",\n  \"rewrite_added\": \"已成功添加 \\\"{{key}}\\\" 的 DNS 重写\",\n  \"rewrite_applied\": \" 重定向规则已应用\",\n  \"rewrite_confirm_delete\": \"您确定要删除 \\\"{{key}}\\\" 的 DNS 重写？\",\n  \"rewrite_deleted\": \"已成功删除 \\\"{{key}}\\\" 的 DNS 重写\",\n  \"rewrite_desc\": \"可以轻松地为特定域名配置自定义 DNS 响应。\",\n  \"rewrite_domain_name\": \"域名：添加一个 CNAME 记录\",\n  \"rewrite_edit\": \"编辑 DNS 重写\",\n  \"rewrite_hosts_applied\": \"根据 hosts 文件规则已被重写\",\n  \"rewrite_ip_address\": \"IP 地址：在 A 或 AAAA 响应中使用这个 IP\",\n  \"rewrite_not_found\": \"未找到 DNS 重写\",\n  \"rewrite_settings_updated\": \"DNS 重写设置更新成功\",\n  \"rewrite_updated\": \"DNS 重写已成功更新\",\n  \"rewrites_disabled_table_header\": \"重写已停用\",\n  \"rewrites_enabled_table_header\": \"重写已启用\",\n  \"rewritten\": \"重写项\",\n  \"rows_table_footer_text\": \"行\",\n  \"rule_added_to_custom_filtering_toast\": \"规则已添加到自定义过滤规则列表中 {{rule}}\",\n  \"rule_label\": \"规则\",\n  \"rule_removed_from_custom_filtering_toast\": \"规则已从自定义过滤规则列表中移除 {{rule}}\",\n  \"rules_count_table_header\": \"规则数\",\n  \"safe_browsing\": \"安全浏览\",\n  \"safe_search\": \"安全搜索\",\n  \"saturday\": \"星期六\",\n  \"saturday_short\": \"周六\",\n  \"save_btn\": \"保存\",\n  \"save_config\": \"保存配置\",\n  \"schedule_add\": \"添加时间表\",\n  \"schedule_current_timezone\": \"当前时区：{{value}}\",\n  \"schedule_desc\": \"安排暂停拦截服务的时间段\",\n  \"schedule_edit\": \"编辑时间表\",\n  \"schedule_from\": \"从\",\n  \"schedule_invalid_select\": \"开始时间必须在结束时间之前\",\n  \"schedule_modal_description\": \"本时间表将要替换现有同一天内所有时间表。一个星期内只可以有一段不活跃期。\",\n  \"schedule_modal_time_off\": \"无服务拦截：\",\n  \"schedule_new\": \"新时间表\",\n  \"schedule_remove\": \"移除时间表\",\n  \"schedule_save\": \"保存时间表\",\n  \"schedule_select_days\": \"选择日\",\n  \"schedule_services\": \"暂停服务拦截\",\n  \"schedule_services_desc\": \"配置「服务拦截过滤器」的暂停计划\",\n  \"schedule_services_desc_client\": \"为此客户端配置「服务拦截过滤器」的暂停计划\",\n  \"schedule_time_all_day\": \"全天\",\n  \"schedule_timezone\": \"选择时区\",\n  \"schedule_to\": \"到\",\n  \"served_from_cache_label\": \"从缓存中\",\n  \"service_name\": \"服务名称\",\n  \"set_static_ip\": \"设置一个静态 IP\",\n  \"settings\": \"设置\",\n  \"settings_custom\": \"自定义\",\n  \"settings_global\": \"全局\",\n  \"setup_config_to_enable_dhcp_server\": \"设置配置以启用 DHCP 服务器\",\n  \"setup_dns_notice\": \"为了使用 <1>DNS-over-HTTPS</1> 或者 <1>DNS-over-TLS</1> ，您需要在 AdGuard Home 设置中 <0>配置加密</0> 。\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS:</0> 使用 <1>{{address}}</1> 字符串。\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS:</0> 使用 <1>{{address}}</1> 字符串。\",\n  \"setup_dns_privacy_3\": \"<0>以下是您可以使用软件的列表</0>\",\n  \"setup_dns_privacy_4\": \"在 iOS 14 或 macOS Big Sur 设备上，您可以下载特定的 '.mobileconfig' 文件。此文件将<highlight>DNS-over-HTTPS</highlight> 或 <highlight>DNS-over-TLS</highlight> 服务器添加于 DNS 设置。\",\n  \"setup_dns_privacy_android_1\": \"Android 9 原生支持 DNS-over-TLS。 要进行配置，请转到 设置 → 网络和互联网 → 高级 → 私有 DNS，然后在那里输入您的域名。\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0> 支持 <1>DNS-over-HTTPS</1> 和 <1>DNS-over-TLS</1>。\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> 为 Android 提供了 <1>DNS-over-HTTPS</1> 的支持。\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS 和 macOS 配置\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> 支持 <1>DNS-over-HTTPS</1> ，但为了设置使用您自己的服务器，您需要为了它生成一个 <2>DNS Stamp</2> 。\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard for iOS</0> 支持 <1>DNS-over-HTTPS</1> 和 <1>DNS-over-TLS</1>。\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home 本身可以作为任何平台上的安全 DNS 客户端。\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> 支持所有已知的安全 DNS 协议。\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> 支持 <1>DNS-over-HTTPS</1>。\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> 支持 <1>DNS-over-HTTPS</1>。\",\n  \"setup_dns_privacy_other_5\": \"您可以从 <0>这里</0> 和 <1>这里</1> 找到更多的实施方案。\",\n  \"setup_dns_privacy_other_title\": \"其他实施方案\",\n  \"setup_guide\": \"设置指导\",\n  \"show_all_filter_type\": \"显示所有\",\n  \"show_blocked_responses\": \"已拦截\",\n  \"show_filtered_type\": \"显示被拦截的\",\n  \"show_processed_responses\": \"已处理\",\n  \"show_whitelisted_responses\": \"已列入白名单\",\n  \"sign_in\": \"登入\",\n  \"sign_out\": \"登出\",\n  \"source_label\": \"源\",\n  \"static_ip\": \"静态IP地址\",\n  \"static_ip_desc\": \"AdGuard Home 是一个服务器，所以它需要一个静态 IP 地址才能正常工作。否则，在某些情况下，路由器可能会给这个设备分配一个不同的 IP 地址。\",\n  \"statistics_clear\": \" 清除统计数据\",\n  \"statistics_clear_confirm\": \"确定要清除统计数据？\",\n  \"statistics_cleared\": \"统计数据已成功清除\",\n  \"statistics_configuration\": \"统计配置\",\n  \"statistics_enable\": \"启用统计数据\",\n  \"statistics_retention\": \"统计保留\",\n  \"statistics_retention_confirm\": \"您确定要更改统计记录保留时间吗？ 如果您减少间隔时间的值， 某些数据可能会丢失。\",\n  \"statistics_retention_desc\": \"如果您减少该间隔的数值， 某些数据可能会丢失\",\n  \"stats_adult\": \"被拦截的成人网站\",\n  \"stats_disabled\": \"已禁用统计数据。您可以从<0>设置页面</0>打开它。\",\n  \"stats_disabled_short\": \"已禁用统计数据\",\n  \"stats_malware_phishing\": \"被拦截的恶意/钓鱼网站\",\n  \"stats_params\": \"统计配置\",\n  \"stats_query_domain\": \"请求域名排行\",\n  \"subnet_error\": \"地址必须在一个子网内\",\n  \"sunday\": \"星期日\",\n  \"sunday_short\": \"周日\",\n  \"system_host_files\": \"系统主机文件\",\n  \"table_client\": \"客户端\",\n  \"table_name\": \"名称\",\n  \"tags_desc\": \"您可以选择与客户端对应的标记。标签可以包含在过滤规则中，并允许您更准确地应用它们。<0>了解更多</0>。\",\n  \"tags_title\": \"标签\",\n  \"test_upstream_btn\": \"测试上游\",\n  \"theme_auto\": \"自动\",\n  \"theme_auto_desc\": \"自动（基于设备的配色方案）\",\n  \"theme_dark\": \"深色主题\",\n  \"theme_dark_desc\": \"暗黑主题\",\n  \"theme_light\": \"浅色主题\",\n  \"theme_light_desc\": \"浅色主题\",\n  \"thursday\": \"星期四\",\n  \"thursday_short\": \"周四\",\n  \"time_table_header\": \"时间\",\n  \"top_blocked_domains\": \"被拦截域名排行\",\n  \"top_clients\": \"客户端排行\",\n  \"top_upstreams\": \"经常请求的上游服务器\",\n  \"topline_expired_certificate\": \"您的 SSL 证书已过期。请更新 <0>加密设置</0> 。\",\n  \"topline_expiring_certificate\": \"您的 SSL 证书即将过期。请更新 <0>加密设置</0> 。\",\n  \"tracker_source\": \"追踪器来源\",\n  \"try_again\": \"重试\",\n  \"ttl_cache_validation\": \"最小缓存 TTL 值必须小于或等于最大值\",\n  \"tuesday\": \"星期二\",\n  \"tuesday_short\": \"周二\",\n  \"type_table_header\": \"类型\",\n  \"unavailable_dhcp\": \"DHCP 无法使用\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home 无法在您的操作系统上运行 DHCP 服务器\",\n  \"unblock\": \"放行\",\n  \"unblock_all\": \"允许所有\",\n  \"unblock_for_this_client_only\": \"仅解除对此客户端的拦截\",\n  \"unknown_filter\": \"未知过滤器 {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} 现已发布！ <0>点击此处</0>以获取详细信息。\",\n  \"update_failed\": \"自动更新失败。请<a>跟随这些步骤</a>以手动更新。\",\n  \"update_now\": \"立即更新\",\n  \"updated_custom_filtering_toast\": \"自定义规则保存成功\",\n  \"updated_save_search_toast\": \"安全搜索设置更新成功\",\n  \"updated_upstream_dns_toast\": \"上游服务器保存成功\",\n  \"updates_checked\": \"AdGuard Home 的新版本现在可用\",\n  \"updates_version_equal\": \"AdGuard Home已经是最新版本\",\n  \"upstream\": \"上游服务器\",\n  \"upstream_dns\": \"上游 DNS 服务器\",\n  \"upstream_dns_cache_configuration\": \"上游 DNS 缓存配置\",\n  \"upstream_dns_client_desc\": \"如果将此字段留空，AdGuard Home 将使用在<0>DNS设置</0>中配置的服务器。\",\n  \"upstream_dns_configured_in_file\": \"配置路径{{path}}\",\n  \"upstream_dns_help\": \"每行输入一个服务器地址。<a>了解更多</a>关于配置上游 DNS 服务器的内容\",\n  \"upstream_parallel\": \"使用并行请求以同时查询所有上游服务器来加快解析速度。\",\n  \"upstream_timeout\": \"上游超时\",\n  \"upstream_timeout_desc\": \"指定等待上游服务器响应的秒数\",\n  \"upstreams\": \"上游服务器\",\n  \"use_adguard_browsing_sec\": \"使用 AdGuard【浏览安全】网页服务\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home 将检查域名是否被浏览安全服务阻止。它将使用隐私性更强的检索 API 来执行检查，只有以 SHA256 为短前缀的域名会被发送到服务器。\",\n  \"use_adguard_parental\": \"使用 AdGuard 【家长控制】服务\",\n  \"use_adguard_parental_hint\": \"AdGuard Home 将使用与浏览安全服务相同的隐私性强的 API 来检查域名指向的网站是否包含成人内容。\",\n  \"use_private_ptr_resolvers_desc\": \"使用私有上游服务器、DHCP、/etc/hosts 等解决包含私有 IP 地址的 ARPA 域名的 PTR、SOA 和 NS 请求。如果禁用，AdGuard Home 将以 NXDOMAIN 回应所有此类请求。\",\n  \"use_private_ptr_resolvers_title\": \"使用私人反向 DNS 解析器\",\n  \"use_saved_key\": \"使用之前保存的密钥\",\n  \"username_label\": \"用户名\",\n  \"username_placeholder\": \"输入用户名\",\n  \"validated_with_dnssec\": \"通过 DNSSEC 验证\",\n  \"version\": \"版本\",\n  \"version_request_error\": \"检查更新失败。请检查互联网连接。\",\n  \"wednesday\": \"星期三\",\n  \"wednesday_short\": \"周三\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales/zh-hk.json",
    "content": "{\n  \"access_allowed_desc\": \"輸入 CIDR 或 IP 位址格式的清單，設定後 AdGuard Home 將僅接受設定的 IP 位址查詢請求。\",\n  \"access_allowed_title\": \"用戶端白名單\",\n  \"access_blocked_desc\": \"請不要與過濾器搞混，AdGuard Home 將對這些網域執行過濾檢查後丟棄 DNS 請求。您可以輸入特定網域名稱來使用設定，或使用萬用字元，例如：「example.org」、「*.example.org」或「||example.org^」。\",\n  \"access_blocked_title\": \"網域黑名單\",\n  \"access_desc\": \"您可以在這裡設定 AdGuard Home DNS 伺服器存取規則。\",\n  \"access_disallowed_desc\": \"輸入 CIDR 或 IP 位址格式的清單，設定後 AdGuard Home 將拒絕設定的 IP 位址查詢請求。\",\n  \"access_disallowed_title\": \"用戶端黑名單\",\n  \"access_settings_saved\": \"存取設定已儲存\",\n  \"access_title\": \"存取設定\",\n  \"actions_table_header\": \"動作\",\n  \"add_allowlist\": \"新增白名單\",\n  \"add_blocklist\": \"新增黑名單\",\n  \"add_custom_list\": \"新增自訂清單\",\n  \"add_persistent_client\": \"加入到用戶端\",\n  \"address\": \"位址\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home 將停止回應此用戶端的所有 DNS 查詢。\",\n  \"all_lists_up_to_date_toast\": \"所有清單已更新至最新\",\n  \"all_queries\": \"所有查詢\",\n  \"allow_this_client\": \"允許此用戶端\",\n  \"allowed\": \"已允許\",\n  \"anonymize_client_ip\": \"將用戶端 IP 匿名化\",\n  \"anonymize_client_ip_desc\": \"不要將用戶端完整 IP 位址儲存在記錄檔與統計資料\",\n  \"anonymizer_notification\": \"<0>注意</0>: 已啟用 IP 去識別化。您可以在<1>一般設定</1>中停用它。\",\n  \"answer\": \"回應\",\n  \"apply_btn\": \"套用\",\n  \"auto_clients_desc\": \"未設定但有連接過 AdGuard Home 的用戶端\",\n  \"auto_clients_title\": \"用戶端（連接時間）\",\n  \"autofix_warning_list\": \"它將執行這些任務：<0>停用系統 DNSStubListener</0> <0>將 DNS 設定為 127.0.0.1</0> <0>更換軟連結將 /etc/resolv.conf 為 /run/systemd/resolve/resolv.conf</0> <0>停止 DNSStubListener（重新載入 systemd-resolved）</0>\",\n  \"autofix_warning_result\": \"就結論來說 DNS 請求預設由本機的 AdGuard Home 處理。\",\n  \"autofix_warning_text\": \"如果您點擊「修復」，AdGuard Home 將更改您的系統 DNS 設定更改為 AdGuard Home DNS 伺服器\",\n  \"average_processing_time\": \"平均的處理時間\",\n  \"average_processing_time_hint\": \"處理 DNS 請求的平均時間(毫秒)\",\n  \"average_upstream_response_time\": \"平均上游伺服器回應時間\",\n  \"back\": \"返回\",\n  \"block\": \"封鎖\",\n  \"block_all\": \"封鎖全部\",\n  \"block_domain_use_filters_and_hosts\": \"使用過濾器與 hosts 檔案阻擋網域查詢\",\n  \"block_for_this_client_only\": \"僅封鎖此用戶端\",\n  \"block_services\": \"封鎖特定服務\",\n  \"blocked_adult_websites\": \"已封鎖的成人網站\",\n  \"blocked_by\": \"<0>被過濾器封鎖</0>\",\n  \"blocked_by_cname_or_ip\": \"使用 CNAME 或 IP 封鎖\",\n  \"blocked_by_response\": \"回應時被 CNAME 或 IP 封鎖\",\n  \"blocked_response_ttl\": \"阻塞響應 TTL\",\n  \"blocked_response_ttl_desc\": \"指定客戶端應快取過濾回應的秒數\",\n  \"blocked_safebrowsing\": \"被安全瀏覽封鎖\",\n  \"blocked_service\": \"封鎖服務\",\n  \"blocked_services\": \"已封鎖服務\",\n  \"blocked_services_desc\": \"用來快速封鎖熱門網站或服務\",\n  \"blocked_services_global\": \"使用全域封鎖服務\",\n  \"blocked_services_saved\": \"已成功封鎖服務\",\n  \"blocked_threats\": \"已封鎖的威脅\",\n  \"blocking_ipv4\": \"封鎖 IPv4\",\n  \"blocking_ipv4_desc\": \"回覆指定 IPv4 位址給被封鎖的網域的 A 紀錄查詢\",\n  \"blocking_ipv6\": \"封鎖 IPv6\",\n  \"blocking_ipv6_desc\": \"回覆指定 IPv6 位址給被封鎖的網域的 AAAA 紀錄查詢\",\n  \"blocking_mode\": \"封鎖模式\",\n  \"blocking_mode_custom_ip\": \"自訂 IP 位址：回應一個自訂的 IP 位址\",\n  \"blocking_mode_default\": \"預設：被 Adblock 規則封鎖時回應零值的 IP 位址（A 紀錄回應 0.0.0.0 ，AAAA 紀錄回應 ::）；被 /etc/hosts 規則封鎖時回應規則中指定 IP 位址\",\n  \"blocking_mode_null_ip\": \"Null IP：回應零值的 IP 位址（A 紀錄回應 0.0.0.0 ，AAAA 紀錄回應 ::）\",\n  \"blocking_mode_nxdomain\": \"NXDOMAIN：回應 NXDOMAIN 狀態碼\",\n  \"blocking_mode_refused\": \"REFUSED：以 REFUSED 碼回應\",\n  \"blocklist\": \"封鎖清單\",\n  \"bootstrap_dns\": \"引導（Boostrap） DNS 伺服器\",\n  \"bootstrap_dns_desc\": \"Bootstrap DNS 伺服器用於解析您所設定的上游 DoH/DoT 解析器的 IP 地址\",\n  \"cache_cleared\": \"DNS 快取成功清除\",\n  \"cache_enabled\": \"啟用快取\",\n  \"cache_enabled_desc\": \"在本機儲存 DNS 回應。\",\n  \"cache_optimistic\": \"優化的\",\n  \"cache_optimistic_desc\": \"即使條目已過期，也讓 AdGuard Home 從快取中回應，並嘗試刷新它們。\",\n  \"cache_size\": \"快取大小\",\n  \"cache_size_desc\": \"DNS 快取大小（位元組）。若要停用快取，請設為 0。\",\n  \"cache_size_validation\": \"啟用時，快取大小必須大於零。\",\n  \"cache_ttl_max_override\": \"覆寫最大 TTL 值\",\n  \"cache_ttl_max_override_desc\": \"設定 DNS 快取條目的最大 TTL 值（秒）\",\n  \"cache_ttl_min_override\": \"覆寫最小 TTL 值\",\n  \"cache_ttl_min_override_desc\": \"快取 DNS 回應時，延長從上游伺服器收到的 TTL 值 (秒）\",\n  \"cancel_btn\": \"取消\",\n  \"category_label\": \"類別\",\n  \"check\": \"檢查\",\n  \"check_client_id\": \"用戶端識別碼（ClientID 或 IP 位址）\",\n  \"check_cname\": \"CNAME：{{cname}}\",\n  \"check_desc\": \"檢查網域是否被封鎖\",\n  \"check_dhcp_servers\": \"檢查 DHCP 伺服器\",\n  \"check_dns_record\": \"選擇 DNS 記錄類型\",\n  \"check_enter_client_id\": \"輸入用戶識別碼\",\n  \"check_hostname\": \"主機名稱或域名\",\n  \"check_ip\": \"IP 位址：{{ip}}\",\n  \"check_not_found\": \"未在您的過濾清單中找到\",\n  \"check_reason\": \"原因：{{reason}}\",\n  \"check_service\": \"服務名稱：{{service}}\",\n  \"check_title\": \"過濾檢查\",\n  \"check_updates_btn\": \"檢查更新\",\n  \"check_updates_now\": \"立即檢查更新\",\n  \"choose_allowlist\": \"選擇允許清單\",\n  \"choose_blocklist\": \"選擇封鎖清單\",\n  \"choose_from_list\": \"從清單中選取\",\n  \"city\": \"城市\",\n  \"clear_cache\": \"清除快取\",\n  \"click_to_view_queries\": \"按一下以檢視查詢結果\",\n  \"client_add\": \"新增用戶端\",\n  \"client_added\": \"已新增「{{key}}」\",\n  \"client_blocked\": \"已封鎖「{{ip}}」\",\n  \"client_confirm_block\": \"您確定要封鎖「{{ip}}」用戶端？\",\n  \"client_confirm_delete\": \"您確定要刪除「{{key}}」用戶端嗎？\",\n  \"client_confirm_unblock\": \"您確定要將「{{ip}}」解除封鎖嗎？\",\n  \"client_deleted\": \"已刪除「{{key}}」\",\n  \"client_details\": \"用戶端詳細資料\",\n  \"client_edit\": \"編輯用戶端\",\n  \"client_global_settings\": \"使用全域設定\",\n  \"client_id\": \"用戶端 ID\",\n  \"client_id_desc\": \"可通過建立不同用戶端 ID 來辨識不同裝置。您可以在<a>這裡</a>進一步了解如何辨識用戶端。\",\n  \"client_id_placeholder\": \"輸入用戶端 ID\",\n  \"client_identifier\": \"識別碼\",\n  \"client_identifier_desc\": \"可通過 IP 地址、CIDR、MAC 地址來辨識使用者裝置，或使用個別客戶端 ID （可用於 DoT/DoH/DoQ）。<0>點擊這裡</0>進一步了解關於辨識使用者裝置。\",\n  \"client_name\": \"客戶端 {{id}}\",\n  \"client_new\": \"設定新用戶端\",\n  \"client_settings\": \"用戶端設定\",\n  \"client_table_header\": \"用戶端\",\n  \"client_unblocked\": \"已解除封鎖「{{ip}}」\",\n  \"client_updated\": \"已更新「{{key}}」\",\n  \"clients_desc\": \"對已連接到 AdGuard Home 的裝置進行設定\",\n  \"clients_not_found\": \"找不到用戶端\",\n  \"clients_title\": \"用戶端\",\n  \"compact\": \"精簡\",\n  \"config_successfully_saved\": \"已儲存設定\",\n  \"configure\": \"設定\",\n  \"confirm_dns_cache_clear\": \"您確定要清除 DNS 快取嗎？\",\n  \"confirm_static_ip\": \"AdGuard Home 將使用 {{ip}} 作為靜態 IP。要繼續處理？\",\n  \"copyright\": \"版權\",\n  \"country\": \"國家\",\n  \"custom_filter_rules\": \"自訂過濾規則\",\n  \"custom_filter_rules_hint\": \"一行一條規則。您可以使用「adblock」語法或「hosts檔案」的語法。\",\n  \"custom_filtering_rules\": \"自訂過濾規則\",\n  \"custom_ip\": \"自訂 IP 位址\",\n  \"custom_retention_input\": \"輸入保存時長（單位：小時）\",\n  \"custom_rotation_input\": \"請輸入輪替週期（單位：小時）\",\n  \"dashboard\": \"儀表板\",\n  \"date\": \"日期\",\n  \"default\": \"預設\",\n  \"delete_confirm\": \"您確定要刪除「{{key}}」嗎？\",\n  \"delete_table_action\": \"刪除\",\n  \"descr\": \"描述\",\n  \"details\": \"詳細資料\",\n  \"dhcp_add_static_lease\": \"新增靜態租用\",\n  \"dhcp_config_saved\": \"DHCP 設定已儲存\",\n  \"dhcp_description\": \"如果你的路由器沒有提供 DHCP 設定，您可以使用 AdGuard 內建的 DHCP 伺服器。\",\n  \"dhcp_disable\": \"關閉 DHCP 伺服器\",\n  \"dhcp_dynamic_ip_found\": \"您的網路介面  <0>{{interfaceName}}</0> 正在使用動態 IP，要使用 DHCP 伺服器必須指定靜態 IP 給 AdGuard。\\n目前您的 IP 位址  <0>{{ipAddress}}</0>，啟用 DHCP 後此 IP 將自動設定為靜態 IP 位址。\",\n  \"dhcp_edit_static_lease\": \"編輯靜態租約\",\n  \"dhcp_enable\": \"開啟 DHCP 伺服器\",\n  \"dhcp_error\": \"無法偵測到同一網路下使否有其他 DHCP 伺服器。\",\n  \"dhcp_form_gateway_input\": \"閘道 IP 位址\",\n  \"dhcp_form_lease_input\": \"租用時間長度\",\n  \"dhcp_form_lease_title\": \"DHCP 租用時間（以秒為單位）\",\n  \"dhcp_form_range_end\": \"範圍結束\",\n  \"dhcp_form_range_start\": \"範圍開始\",\n  \"dhcp_form_range_title\": \"IP 位址範圍\",\n  \"dhcp_form_subnet_input\": \"子網路遮罩\",\n  \"dhcp_found\": \"在目前網段中有正在運作的 DHCP 伺服器，開啟內建 DHCP 伺服器是不安全的。\",\n  \"dhcp_hardware_address\": \"硬體位址\",\n  \"dhcp_interface_select\": \"選擇 DHCP 使用的網路介面\",\n  \"dhcp_ip_addresses\": \"IP 位址\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 設定\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 設定\",\n  \"dhcp_lease_added\": \"靜態租用 \\\"{{key}}\\\" 已新增成功\",\n  \"dhcp_lease_deleted\": \"靜態租用 \\\"{{key}}\\\" 已刪除成功\",\n  \"dhcp_lease_updated\": \"靜態租約 \\\"{{key}}\\\" 已成功更新\",\n  \"dhcp_leases\": \"DHCP 租用\",\n  \"dhcp_leases_not_found\": \"找不到 DHCP 租約\",\n  \"dhcp_new_static_lease\": \"新增靜態租用\",\n  \"dhcp_not_found\": \"您可以安全地啟用內建 DHCP 伺服器 - 在目前網路中沒有找到任何有效的 DHCP 伺服器。但我們依舊建議您手動再次檢查，因為目前我們的自動檢測並不能確定 100% 準確\",\n  \"dhcp_reset\": \"您確定要重設 DHCP 設定嗎？\",\n  \"dhcp_reset_leases\": \"重置所有 DHCP 租約\",\n  \"dhcp_reset_leases_confirm\": \"您確定要重設所有 DHCP 租約嗎？\",\n  \"dhcp_reset_leases_success\": \"重置 DHCP 租約成功\",\n  \"dhcp_settings\": \"DHCP 設定\",\n  \"dhcp_static_ip_error\": \"使用 DHCP 伺服器必須先指定靜態 IP 位置給 AdGuard。無法偵測到有效的靜態 IP 設定，請先手動設定。\",\n  \"dhcp_static_leases\": \"DHCP 靜態租用\",\n  \"dhcp_static_leases_not_found\": \"找不到 DHCP 靜態租用\",\n  \"dhcp_table_expires\": \"到期\",\n  \"dhcp_table_hostname\": \"主機名稱\",\n  \"dhcp_title\": \"DHCP 伺服器（實驗性功能！）\",\n  \"dhcp_warning\": \"如果無論如何您都想要啟動 AdGuard 內建 DHCP 伺服器，請先確保同一網路下沒有正在運作的 DHCP 伺服器，否則很有可能會破壞其他已連線至網際網路的裝置。\",\n  \"disable_for_hours\": \"{{count}} 小時\",\n  \"disable_for_hours_plural\": \"{{count}} 小時\",\n  \"disable_for_minutes\": \"{{count}} 分鐘\",\n  \"disable_for_minutes_plural\": \"{{count}} 分鐘\",\n  \"disable_for_seconds\": \"{{count}} 秒\",\n  \"disable_for_seconds_plural\": \"{{count}} 秒\",\n  \"disable_ipv6\": \"停止解析 IPv6 位址\",\n  \"disable_ipv6_desc\": \"開啟此功能後，將捨棄所有對 IPv6 位址（AAAA）的查詢。\",\n  \"disable_notify_for_hours\": \"暫停防護 {{count}} 小時\",\n  \"disable_notify_for_hours_plural\": \"停用保護 {{count}} 小時\",\n  \"disable_notify_for_minutes\": \"暫停防護 {{count}} 分鐘\",\n  \"disable_notify_for_minutes_plural\": \"暫停防護 {{count}} 分鐘\",\n  \"disable_notify_for_seconds\": \"暫停防護 {{count}} 秒\",\n  \"disable_notify_for_seconds_plural\": \"暫停防護 {{count}} 秒\",\n  \"disable_notify_until_tomorrow\": \"停用保護直至明天\",\n  \"disable_protection\": \"停用防護\",\n  \"disable_until_tomorrow\": \"直到明天\",\n  \"disabled\": \"已停用\",\n  \"disabled_dhcp\": \"DHCP 伺服器已關閉\",\n  \"disabled_filtering_toast\": \"已停用過濾\",\n  \"disabled_parental_toast\": \"已停用家長監護\",\n  \"disabled_protection\": \"已停用防護\",\n  \"disabled_safe_browsing_toast\": \"已停用安全瀏覽\",\n  \"disabled_safe_search_toast\": \"已停用安全搜尋\",\n  \"disallow_this_client\": \"不允許此用戶端\",\n  \"dns_addresses\": \"DNS 位址\",\n  \"dns_allowlists\": \"DNS 白名單\",\n  \"dns_allowlists_desc\": \"在白名單內的網域無論如何都會被允許，即使他在其他黑名單內也一樣。\",\n  \"dns_blocklists\": \"DNS 黑名單\",\n  \"dns_blocklists_desc\": \"AdGuard Home 會對符合規則的查詢進行封鎖。\",\n  \"dns_cache_config\": \"DNS 快取設定\",\n  \"dns_cache_config_desc\": \"在這裡您可以設定 DNS 快取\",\n  \"dns_cache_size\": \"DNS 快取大小（bytes）\",\n  \"dns_config\": \"DNS 伺服器設定\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS 隱私\",\n  \"dns_providers\": \"下列為常見的<0> DNS 伺服器</0>。\",\n  \"dns_query\": \"DNS 查詢\",\n  \"dns_rewrites\": \"DNS 覆寫\",\n  \"dns_settings\": \"DNS 設定\",\n  \"dns_start\": \"DNS 伺服器正在啟動\",\n  \"dns_status_error\": \"檢查 DNS 伺服器狀態錯誤\",\n  \"dns_test_not_ok_toast\": \"DNS 設定中的 \\\"{{key}}\\\" 出現錯誤，請確認是否正確輸入\",\n  \"dns_test_ok_toast\": \"設定中的 DNS 上游運作正常\",\n  \"dns_test_parsing_error_toast\": \"在 {{section}} 部分中：第 {{line}} 行：無法使用，請檢查您是否有正確地填寫\",\n  \"dns_test_warning_toast\": \"上游伺服器 \\\"{{key}}\\\" 沒有回應測試請求，可能無法正常運作\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"啟用 DNSSEC\",\n  \"dnssec_enable_desc\": \"在連出 DNS 查詢結果中加入 DNSSEC 旗幟並檢查結果（必須開啟 DNSSEC-enabled 解析器）\",\n  \"domain\": \"網域\",\n  \"domain_desc\": \"輸入您想要覆寫的網域或 wildcard 字元。\",\n  \"domain_name_table_header\": \"域名\",\n  \"domain_or_client\": \"網域或用戶端\",\n  \"down\": \"離線\",\n  \"download_mobileconfig\": \"下載描述檔\",\n  \"download_mobileconfig_doh\": \"下載適用於 DNS-over-HTTPS 的 .mobileconfig\",\n  \"download_mobileconfig_dot\": \"下載適用於 DNS-over-TLS 的 .mobileconfig\",\n  \"ecs\": \"EDNS 子網\",\n  \"edit_allowlist\": \"編輯白名單\",\n  \"edit_blocklist\": \"編輯黑名單\",\n  \"edit_table_action\": \"編輯\",\n  \"edns_cs_desc\": \"傳送用戶端的子網路給 DNS 伺服器。\",\n  \"edns_enable\": \"啟用 EDNS Client Subnet\",\n  \"edns_use_custom_ip\": \"使用自訂 EDNS IP\",\n  \"edns_use_custom_ip_desc\": \"允許使用自訂 EDNS IP\",\n  \"elapsed\": \"已耗用\",\n  \"empty_response_status\": \"空白\",\n  \"enable_protection\": \"開啟保護\",\n  \"enable_protection_timer\": \"保護功能將在 {{time}} 啟用\",\n  \"enable_upstream_dns_cache\": \"為此客戶端的自訂上游設定啟用 DNS 快取\",\n  \"enabled_dhcp\": \"DHCP 伺服器已啟動\",\n  \"enabled_filtering_toast\": \"已啟用過濾\",\n  \"enabled_parental_toast\": \"已啟用家長監護\",\n  \"enabled_protection\": \"已開啟保護\",\n  \"enabled_safe_browsing_toast\": \"已啟用安全瀏覽\",\n  \"enabled_save_search_toast\": \"已啟用安全搜尋\",\n  \"enabled_table_header\": \"啟用\",\n  \"encryption_certificate_path\": \"憑證路徑\",\n  \"encryption_certificates\": \"憑證\",\n  \"encryption_certificates_desc\": \"要使用加密連線，必須擁有一個有效的 SSL 憑證對應您的網域。您可以從<0>{{link}}</0>取得免費的 SSL 憑證或從受信任的 SSL 憑證簽發機構購買。\",\n  \"encryption_certificates_input\": \"在這裡複製/貼上您的 PEM 憑證。\",\n  \"encryption_certificates_source_content\": \"貼上憑證內容\",\n  \"encryption_certificates_source_path\": \"設定憑證檔案路徑\",\n  \"encryption_chain_invalid\": \"憑證連結無效\",\n  \"encryption_chain_valid\": \"憑證鏈結有效\",\n  \"encryption_config_saved\": \"加密設定已儲存\",\n  \"encryption_desc\": \"加密（HTTPS/TLS）提供給 DNS 和「管理介面網頁介面」兩者\",\n  \"encryption_doq\": \"DNS-over-QUIC 連接埠\",\n  \"encryption_doq_desc\": \"若設定此連接埠，AdGuard Home 將在此連接埠上運行 DNS-over-QUIC 服務。此功能還是實驗性功能，可能並不可靠。此外目前還沒有太多客戶端支援。\",\n  \"encryption_dot\": \"DNS-over-TLS 連接埠\",\n  \"encryption_dot_desc\": \"如果已設定此連接埠，AdGuard Home 將啟動 DNS-over-TLS 伺服器來監聽請求。\",\n  \"encryption_enable\": \"開啟加密（HTTPS、DNS-over-HTTPS 和 DNS-over-TLS）\",\n  \"encryption_enable_desc\": \"如果加密開啟 AdGuard Home 網頁管理介面將使用 HTTPS 提供存取，DNS 伺服器也提供 DNS-over-HTTPS 和 DNS-over-TLS 查詢請求。\",\n  \"encryption_expire\": \"到期\",\n  \"encryption_hostnames\": \"主機名稱\",\n  \"encryption_https\": \"HTTPS 連接埠\",\n  \"encryption_https_desc\": \"如果已設定 HTTPS，AdGuard Home 網頁管理介面將會使用 HTTPS 來存取，且「/dns-query」也提供 DNS-over-HTTPS 查詢。\",\n  \"encryption_issuer\": \"簽發者\",\n  \"encryption_key\": \"私密金鑰\",\n  \"encryption_key_input\": \"在這裡複製/貼上您的 PEM 憑證。\",\n  \"encryption_key_invalid\": \"{{type}} 私密金鑰無效\",\n  \"encryption_key_source_content\": \"貼上私鑰內容\",\n  \"encryption_key_source_path\": \"設定私鑰路徑\",\n  \"encryption_key_valid\": \"{{type}} 私密金鑰有效\",\n  \"encryption_plain_dns_desc\": \"預設情況下已啟用一般 DNS。您可以將其停用以強制所有裝置使用加密 DNS。要執行此操作，您必須啟用至少一個加密的 DNS 協定。\",\n  \"encryption_plain_dns_enable\": \"啟用一般 DNS\",\n  \"encryption_plain_dns_error\": \"若要停用一般 DNS，請啟用至少一個加密的 DNS 協定\",\n  \"encryption_private_key_path\": \"私鑰路徑\",\n  \"encryption_redirect\": \"自動重新導向到 HTTPS\",\n  \"encryption_redirect_desc\": \"如果啟用，AdGuard Home 將會自動導向 HTTP 到 HTTPS。\",\n  \"encryption_reset\": \"您確定要重設加密設定嗎？\",\n  \"encryption_server\": \"伺服器名稱\",\n  \"encryption_server_desc\": \"要使用 HTTPS，您必須輸入與您 SSL 憑證相符的伺服器名稱。\",\n  \"encryption_server_enter\": \"輸入您的網域名稱\",\n  \"encryption_settings\": \"加密設定\",\n  \"encryption_status\": \"狀態\",\n  \"encryption_subject\": \"主體\",\n  \"encryption_title\": \"加密\",\n  \"encryption_warning\": \"警告\",\n  \"enforce_safe_search\": \"強制使用安全搜尋\",\n  \"enforce_save_search_hint\": \"AdGuard Home 將在下列的搜尋引擎：Google、YouTube、Bing、DuckDuckGo、Ecosia、Yandex 和 Pixabay 中強制執行安全搜尋。\",\n  \"enforced_save_search\": \"強制使用安全搜尋\",\n  \"enter_cache_size\": \"輸入快取大小（bytes）\",\n  \"enter_cache_ttl_max_override\": \"輸入最大 TTL 值（秒）\",\n  \"enter_cache_ttl_min_override\": \"輸入最小 TTL 值（秒）\",\n  \"enter_name_hint\": \"輸入名稱\",\n  \"enter_url_or_path_hint\": \"請在列表中輸入 URL 網址或絕對路徑\",\n  \"enter_valid_allowlist\": \"輸入有效的白名單 URL 網址\",\n  \"enter_valid_blocklist\": \"輸入有效的黑名單 URL 網址\",\n  \"error_details\": \"錯誤詳細資料\",\n  \"example_comment\": \"! Here goes a comment\",\n  \"example_comment_hash\": \"# Also a comment\",\n  \"example_comment_meaning\": \"註解\",\n  \"example_meaning_filter_block\": \"封鎖對 example.org 網域及其所有子網域的存取\",\n  \"example_meaning_filter_whitelist\": \"解除對 example.org 網域及其所有子網域存取封鎖\",\n  \"example_meaning_host_block\": \"AdGuard Home 將會對 example.org （不包含子網域）查詢回應 127.0.0.1。\",\n  \"example_multiple_upstreams_reserved\": \"多個上游 <0>for 特定網域</0>;\",\n  \"example_regex_meaning\": \"使用正規表示式（Regular Expression）來阻止對應的網域查詢\",\n  \"example_rewrite_domain\": \"DNS 覆寫只套用在這個域名。\",\n  \"example_rewrite_wildcard\": \"DNS 覆寫會套用在 <0>example.org</0> 及所有子域名。\",\n  \"example_upstream_comment\": \"您可以指定註解\",\n  \"example_upstream_doh\": \"<0>DNS-over-HTTPS</0>（流量加密）\",\n  \"example_upstream_doh3\": \"使 DNS-over-HTTPS 強制使用 <0>HTTP/3</0> ，並禁止使用後備 HTTP/2 或更低版本；\",\n  \"example_upstream_doq\": \"加密 <0>DNS-over-QUIC</0>\",\n  \"example_upstream_dot\": \"<0>DNS-over-TLS</0>（流量加密）\",\n  \"example_upstream_regular\": \"一般 DNS（透過 UDP）\",\n  \"example_upstream_regular_port\": \"一般 DNS（透過 UDP，連接埠）\",\n  \"example_upstream_reserved\": \"您可以<0>指定網域</0>使用特定 DNS 查詢\",\n  \"example_upstream_sdns\": \"您可以使透過 <0>DNS Stamps</0> 來解析  <1>DNSCrypt</1> 或 <2>DNS-over-HTTPS</2>\",\n  \"example_upstream_tcp\": \"一般 DNS（透過 TCP）\",\n  \"example_upstream_tcp_hostname\": \"一般 DNS（透過 TCP，主機名稱）\",\n  \"example_upstream_tcp_port\": \"一般 DNS（透過 TCP，連接埠）\",\n  \"example_upstream_udp\": \"一般 DNS（透過 UDP，主機名稱）\",\n  \"examples_title\": \"範例\",\n  \"fallback_dns_desc\": \"備用 DNS 伺服器列表：於主要 DNS 伺服器沒有回應時使用。語法與主要 DNS 伺服器設定欄位相同。\",\n  \"fallback_dns_placeholder\": \"每行輸入一個備用 DNS 伺服器\",\n  \"fallback_dns_title\": \"備用 DNS 伺服器\",\n  \"faq\": \"常見問題\",\n  \"fastest_addr\": \"Fastest IP 位址\",\n  \"fastest_addr_desc\": \"等待<b>所有</b> DNS 伺服器的回應，測量每個伺服器的 TCP 連線速度，並返回連線速度最快的伺服器的 IP 位址。<br/>如果一個或多個上游伺服器沒有回應，此模式會顯著減慢 DNS 查詢速度。確保您的上游伺服器穩定且上游超時時間短。\",\n  \"filter\": \"過濾器\",\n  \"filter_added_successfully\": \"已成功新增清單\",\n  \"filter_allowlist\": \"警告：此操作同時會將規則 \\\"{{disallowed_rule}}\\\" 從允許的客戶端清單中排除。\",\n  \"filter_category_general\": \"一般\",\n  \"filter_category_general_desc\": \"封鎖大多數裝置的廣告與追蹤器清單\",\n  \"filter_category_other\": \"其他\",\n  \"filter_category_other_desc\": \"其他封鎖清單\",\n  \"filter_category_regional\": \"區域性\",\n  \"filter_category_regional_desc\": \"針對地區性廣告與追蹤器伺服器的封鎖清單\",\n  \"filter_category_security\": \"安全性\",\n  \"filter_category_security_desc\": \"針對惡意軟體、網路釣魚或詐騙網域的封鎖清單\",\n  \"filter_removed_successfully\": \"已成功移除清單\",\n  \"filter_updated\": \"已成功更新清單\",\n  \"filtered\": \"已過濾\",\n  \"filtered_custom_rules\": \"已套用自訂規則\",\n  \"filtering_rules_learn_more\": \"<0>進一步了解</0>如何創建自己的「hosts 檔案」\",\n  \"filters\": \"過濾器\",\n  \"filters_and_hosts_hint\": \"AdGuard Home 接受「adblock」以及「host檔案」語法。\",\n  \"filters_block_toggle_hint\": \"您可在<a>過濾器</a>設定中設定封鎖規則。\",\n  \"filters_configuration\": \"過濾器設定\",\n  \"filters_enable\": \"開啟過濾器\",\n  \"filters_interval\": \"過濾器更新頻率\",\n  \"fix\": \"修正\",\n  \"for_last_days\": \"最近 {{count}} 天內\",\n  \"for_last_days_plural\": \"最近 {{count}} 天內\",\n  \"for_last_hours\": \"在過去 {{count}} 小時\",\n  \"for_last_hours_plural\": \"在過去 {{count}} 小時裡\",\n  \"forgot_password\": \"忘記密碼？\",\n  \"forgot_password_desc\": \"請依照<0>步驟</0>來為您的帳號建立新密碼。\",\n  \"form_add_id\": \"新增識別碼\",\n  \"form_answer\": \"輸入 IP 或網域名稱\",\n  \"form_client_name\": \"輸入用戶端名稱\",\n  \"form_domain\": \"輸入網域名稱或使用 wildcard 字元。\",\n  \"form_enter_blocked_response_ttl\": \"輸入已封鎖的回應 TTL（秒）\",\n  \"form_enter_host\": \"輸入網域\",\n  \"form_enter_hostname\": \"請輸入主機名稱\",\n  \"form_enter_id\": \"輸入識別碼\",\n  \"form_enter_ip\": \"輸入 IP\",\n  \"form_enter_mac\": \"輸入 MAC 地址\",\n  \"form_enter_rate_limit\": \"輸入速率限制\",\n  \"form_enter_rate_limit_subnet_len\": \"輸入速率限制的子網路前綴長度\",\n  \"form_enter_subnet_ip\": \"在子網路 \\\"{{cidr}}\\\" 中輸入一個 IP 位址\",\n  \"form_enter_upstream_timeout\": \"輸入上游伺服器超時持續時間（以秒為單位）\",\n  \"form_error_answer_format\": \"回應格式無效\",\n  \"form_error_client_id_format\": \"無效的「客戶端 ID」格式\",\n  \"form_error_domain_format\": \"網域格式無效\",\n  \"form_error_equal\": \"不可相同\",\n  \"form_error_gateway_ip\": \"租約不能使用閘道器的 IP 位址\",\n  \"form_error_ip4_format\": \"無效的 IPv4 格式\",\n  \"form_error_ip4_gateway_format\": \"閘道的 IPv4 位址無效\",\n  \"form_error_ip6_format\": \"無效的 IPv6 格式\",\n  \"form_error_ip_format\": \"無效的 IP 格式\",\n  \"form_error_mac_format\": \"無效的 「MAC 位址」格式\",\n  \"form_error_password\": \"密碼不相符\",\n  \"form_error_password_length\": \"密碼必須至少 {{value}} 個字元長度\",\n  \"form_error_port\": \"輸入有效的連接埠\",\n  \"form_error_port_range\": \"輸入範圍 80-65535 中的值\",\n  \"form_error_port_unsafe\": \"這個連接埠不安全\",\n  \"form_error_positive\": \"數值必須大於 0\",\n  \"form_error_required\": \"必要欄位\",\n  \"form_error_server_name\": \"無效伺服器名稱\",\n  \"form_error_subnet\": \"子網路 \\\"{{cidr}}\\\" 不包含 IP 位址 \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"無效的 URL 網址格式\",\n  \"form_error_url_or_path_format\": \"列表中含有的 URL 網址或絕對路徑\",\n  \"form_select_tags\": \"選擇用戶端標籤\",\n  \"found_in_known_domain_db\": \"在已知網域資料庫中找到。\",\n  \"friday\": \"星期五\",\n  \"friday_short\": \"週五\",\n  \"gateway_or_subnet_invalid\": \"無效子網路\",\n  \"general_settings\": \"一般設定\",\n  \"general_statistics\": \"一般統計資料\",\n  \"get_started\": \"開始設定\",\n  \"greater_range_start_error\": \"必須大於起始值\",\n  \"homepage\": \"首頁\",\n  \"host_whitelisted\": \"主機已列入白名單\",\n  \"ignore_domains\": \"已忽略網域（每行一個）\",\n  \"ignore_domains_desc_query\": \"符合這些規則的查詢不會被寫入查詢記錄中\",\n  \"ignore_domains_desc_stats\": \"符合這些規則的查詢不會被計入統計資料中\",\n  \"ignore_domains_title\": \"已忽略網域\",\n  \"ignore_query_log\": \"在查詢日誌中忽略此客戶端\",\n  \"ignore_statistics\": \"在統計資料中忽略此客戶端\",\n  \"install_auth_confirm\": \"確認密碼\",\n  \"install_auth_desc\": \"強烈建議為 AdGuard Home 管理介面設定驗證密碼，即使管理介面僅能從本地區域網路連接，不過設定密碼保護仍然很重要。\",\n  \"install_auth_password\": \"密碼\",\n  \"install_auth_password_enter\": \"輸入密碼\",\n  \"install_auth_title\": \"驗證\",\n  \"install_auth_username\": \"使用者名稱\",\n  \"install_auth_username_enter\": \"輸入用戶名\",\n  \"install_devices_address\": \"AdGuard Home DNS 伺服器正在監聽以下位址\",\n  \"install_devices_android_list_1\": \"在 Android 主選單中點選設定。\",\n  \"install_devices_android_list_2\": \"在 Wi-Fi 選單中會列出所有可用的網路（在行動網路時無法使用自訂 DNS）。\",\n  \"install_devices_android_list_3\": \"長按您正在使用的網路，接著點選修改網路。\",\n  \"install_devices_android_list_4\": \"在某些裝置上您需要勾選進階方塊才能接著設定。要設定自訂 DNS 必須先將 IP 設定從 DHCP 改為靜態 IP。\",\n  \"install_devices_android_list_5\": \"將 DNS 1 和 DNS 2 更改成您的 AdGuard Home 伺服器位址。\",\n  \"install_devices_desc\": \"要開始使用 AdGuard Home，您需要設定好裝置才能使用。\",\n  \"install_devices_ios_list_1\": \"從主畫面中，點選設定。\",\n  \"install_devices_ios_list_2\": \"在左側選擇 Wi-Fi（在行動網路時無法使用自訂 DNS）。\",\n  \"install_devices_ios_list_3\": \"點選連線中的網路\",\n  \"install_devices_ios_list_4\": \"在 DNS 欄位中輸入您的 AdGuard Home 伺服器位址。\",\n  \"install_devices_macos_list_1\": \"點擊左上角的 Apple Icon，接著點擊「系統偏好設定」。\",\n  \"install_devices_macos_list_2\": \"點擊「網路」。\",\n  \"install_devices_macos_list_3\": \"選擇清單中第一個連線接著點選「進階設定」。\",\n  \"install_devices_macos_list_4\": \"選擇 DNS 分頁，接著輸入您的 AdGuard Home 伺服器位址。\",\n  \"install_devices_router\": \"路由器\",\n  \"install_devices_router_desc\": \"使用此設定後，所有連接家中路由器的裝置都會自動套用，無須在每台裝置上個別設定。\",\n  \"install_devices_router_list_1\": \"開啟您的路由器設定。通常可透過瀏覽器開啟（http://192.168.0.1/ 或 http://192.168.1.1）。接著您可能會被要求驗證登入，如果忘記密碼可以按壓路由器的 REST 重設按鈕來重設。部分路由器可能需要安裝特定應用程式，在這種情況下應該已經安裝在您的電腦或手機上。\",\n  \"install_devices_router_list_2\": \"找到 DHCP/DNS 設定。允許兩到三組數字的欄位旁邊尋找 DNS 字串，每組數字分為四組，每組一到三位數。\",\n  \"install_devices_router_list_3\": \"請在那邊輸入您的 AdGuard Home 伺服器位址。\",\n  \"install_devices_router_list_4\": \"您無法於某些類型的路由器上設定自訂的 DNS 伺服器。在這種情況下，如果您設置 AdGuard Home 作為 <0>DHCP</0> 伺服器，其可能有所幫助。否則，您應搜尋有關如何為您的特定路由器型號自訂 DNS 伺服器之用法說明。\",\n  \"install_devices_title\": \"設定您的裝置\",\n  \"install_devices_windows_list_1\": \"在「開始列」或「Windows 搜尋」開啟控制台。\",\n  \"install_devices_windows_list_2\": \"點擊「網路和網際網路」，接著點選「網路和共用中心」。\",\n  \"install_devices_windows_list_3\": \"在畫面左側點擊「變更介面卡設定」。\",\n  \"install_devices_windows_list_4\": \"對著您正在使用的連線點擊右鍵，選擇「內容」。\",\n  \"install_devices_windows_list_5\": \"選擇清單中的「網際網路通訊協定第 4 版（TCP/IPv4）」，再點擊「內容」。\",\n  \"install_devices_windows_list_6\": \"點擊「使用下列的 DNS 伺服器位址」，接著輸入您的 AdGuard Home 伺服器位址。\",\n  \"install_saved\": \"成功儲存\",\n  \"install_settings_all_interfaces\": \"所有介面\",\n  \"install_settings_dns\": \"DNS 伺服器\",\n  \"install_settings_dns_desc\": \"您需要將您的裝置或路由器設定以下的 IP 位址為 DNS 伺服器：\",\n  \"install_settings_interface_link\": \"您可以從以下 IP 位址來訪問 AdGuard Home 管理介面：\",\n  \"install_settings_listen\": \"監聽介面\",\n  \"install_settings_port\": \"連接埠\",\n  \"install_settings_title\": \"管理介面\",\n  \"install_static_configure\": \"我們偵測到 <0>{{ip}}</0> 動態 IP 已被使用。您想要將它當作靜態 IP 使用嗎？\",\n  \"install_static_error\": \"AdGuard Home 無法在這個網路介面上執行自動設定。請尋找有關如何手動更改設定的說明。\",\n  \"install_static_ok\": \"好消息！靜態 IP 位址設定完成了\",\n  \"install_step\": \"步驟\",\n  \"install_submit_desc\": \"安裝步驟已完成，現在您已經可以開始使用 AdGuard Home\",\n  \"install_submit_title\": \"恭喜！\",\n  \"install_welcome_desc\": \"AdGuard Home 是個封鎖全網路廣告和追蹤器封鎖的 DNS 伺服器。用來控制您自己的整個網路以及裝置，而且並不需要在裝置安裝程式。\",\n  \"install_welcome_title\": \"歡迎使用 AdGuard Home！\",\n  \"interval_24_hour\": \"24 小時\",\n  \"interval_6_hour\": \"6 小時\",\n  \"interval_days\": \"{{count}} 天\",\n  \"interval_days_plural\": \"{{count}} 天\",\n  \"interval_hours\": \"{{count}} 小時\",\n  \"interval_hours_plural\": \"{{count}} 小時\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP 位址\",\n  \"known_tracker\": \"已知追蹤器\",\n  \"last_rule_in_allowlist\": \"無法停用此客戶端，因為排除規則「{{disallowed_rule}}」會導致「允許的用戶端」清單停用。\",\n  \"last_time_updated_table_header\": \"上次更新時間\",\n  \"list_confirm_delete\": \"您確定要刪除這個清單嗎？\",\n  \"list_label\": \"清單\",\n  \"list_updated\": \"已更新 {{count}} 個清單\",\n  \"list_updated_plural\": \"已更新 {{count}} 個清單\",\n  \"list_url_table_header\": \"清單 URL 網址\",\n  \"load_balancing\": \"負載平衡\",\n  \"load_balancing_desc\": \"一次查詢一台上游伺服器。<br/>AdGuard Home 使用加權隨機演算法來選擇具有最少失敗查詢和最低平均查詢時間的伺服器。\",\n  \"loading_table_status\": \"正在載入...\",\n  \"local_ptr_default_resolver\": \"AdGuard Home 預設使用以下作為 DNS 反解器：{{ip}}\",\n  \"local_ptr_desc\": \"AdGuard Home 使用的 DNS 伺服器用於私人 PTR、SOA 和 NS 請求。如果請求要求包含私有 IP 範圍內的子網域的 ARPA 網域（例如 \\\"192.168.12.34\\\"），並來自具有私人 IP 位址的用戶端，該請求被視為私人。如果未設定，將使用您的作業系統的預設 DNS 解析器，但不包括 AdGuard Home 的 IP 位址。\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home 無法為此系統確定適合私有反解析器。\",\n  \"local_ptr_placeholder\": \"每行輸入一個伺服器位址\",\n  \"local_ptr_title\": \"私人 DNS 伺服器\",\n  \"location\": \"位置\",\n  \"log_and_stats_section_label\": \"查詢日誌與統計資料\",\n  \"lower_range_start_error\": \"必須小於起始值\",\n  \"main_settings\": \"主要設定\",\n  \"make_static\": \"新增為靜態\",\n  \"manual_update\": \"請嘗試依照<a>下列步驟</a>來手動更新。\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"星期一\",\n  \"monday_short\": \"週一\",\n  \"name\": \"名稱\",\n  \"name_table_header\": \"名稱\",\n  \"netname\": \"網路名稱\",\n  \"network\": \"網路\",\n  \"new_allowlist\": \"新增白名單\",\n  \"new_blocklist\": \"新增黑名單\",\n  \"next\": \"下一步\",\n  \"next_btn\": \"下一頁\",\n  \"no_blocklist_added\": \"沒有新增的黑名單\",\n  \"no_clients_found\": \"找不到用戶端\",\n  \"no_domains_found\": \"找不到網域\",\n  \"no_logs_found\": \"找不到記錄\",\n  \"no_servers_specified\": \"沒有指定的伺服器\",\n  \"no_upstreams_data_found\": \"找不到上游數據\",\n  \"no_whitelist_added\": \"沒有新增的白名單\",\n  \"nothing_found\": \"沒有結果\",\n  \"null_ip\": \"Null IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"已被廣告過濾器與主機黑名單封鎖 DNS 查詢總數\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"已封鎖成人網站總數\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"已被 AdGuard 瀏覽安全模組封鎖的 DNS 查詢總數\",\n  \"number_of_dns_query_days\": \"過去 {{count}} 天內 DNS 查詢總數\",\n  \"number_of_dns_query_days_plural\": \"過去 {{count}} 天內 DNS 查詢總數\",\n  \"number_of_dns_query_hours\": \"過去 {{count}} 小時處理的 DNS 查詢數量\",\n  \"number_of_dns_query_hours_plural\": \"過去 {{count}} 小時處理的 DNS 查詢數量\",\n  \"number_of_dns_query_to_safe_search\": \"已強制使用安全搜尋總數\",\n  \"nxdomain\": \"NXDOMAIN\",\n  \"off\": \"未運作\",\n  \"on\": \"運作中\",\n  \"open_dashboard\": \"開啟儀表板\",\n  \"orgname\": \"組織名稱\",\n  \"original_response\": \"原始回應\",\n  \"out_of_range_error\": \"必須介於 \\\"{{start}}\\\" - \\\"{{end}}\\\" 範圍之外\",\n  \"page_table_footer_text\": \"頁\",\n  \"parallel_requests\": \"平行處理\",\n  \"parental_control\": \"家長監護\",\n  \"password_label\": \"密碼\",\n  \"password_placeholder\": \"輸入密碼\",\n  \"plain_dns\": \"一般未加密 DNS\",\n  \"port_53_faq_link\": \"連接埠 53 經常被「DNSStubListener」或「systemd-resolved」服務佔用。請閱讀下列有關解決<0>這個問題</0>的說明\",\n  \"previous_btn\": \"上一頁\",\n  \"privacy_policy\": \"隱私政策\",\n  \"processing_update\": \"請稍候，AdGuard Home 正在更新\",\n  \"protection_section_label\": \"保護\",\n  \"protocol\": \"協定\",\n  \"punycode\": \"Punycode\",\n  \"query_log\": \"查詢記錄\",\n  \"query_log_clear\": \"清除查詢記錄\",\n  \"query_log_cleared\": \"已清除查詢記錄\",\n  \"query_log_configuration\": \"記錄檔設定\",\n  \"query_log_confirm_clear\": \"您確定要清除整個查詢記錄嗎？\",\n  \"query_log_disabled\": \"查詢記錄未開啟，可以在<0>設定</0>中開啟\",\n  \"query_log_enable\": \"開啟記錄\",\n  \"query_log_filtered\": \"被 {{filter}} 過濾\",\n  \"query_log_response_status\": \"狀態：{{value}}\",\n  \"query_log_retention\": \"查詢記錄保留時間\",\n  \"query_log_retention_confirm\": \"您確定要更改記錄檔保存期限嗎？如果您縮短期限部分資料可能將會遺失\",\n  \"query_log_strict_search\": \"使用雙引號來強調搜尋結果\",\n  \"query_log_updated\": \"已成功更新查詢記錄\",\n  \"rate_limit\": \"速率限制\",\n  \"rate_limit_desc\": \"限制單一裝置每秒發出的查詢次數（設定為 0 即表示無限制）\",\n  \"rate_limit_subnet_len_ipv4\": \"IPv4 位址的子網路前綴長度\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"用於速率限制的 IPv4 位址的子網路前綴長度。 預設為 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4 子網路前綴長度應介於 0 到 32 之間\",\n  \"rate_limit_subnet_len_ipv6\": \"IPv6 位址的子網路前綴長度\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"用於速率限制的 IPv6 位址的子網路前綴長度。 預設為 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6 子網路前綴長度應介於 0 到 128 之間\",\n  \"rate_limit_whitelist\": \"速率限制白名單\",\n  \"rate_limit_whitelist_desc\": \"排除在速率限制之外的 IP 位址\",\n  \"rate_limit_whitelist_placeholder\": \"每行輸入一個 IP 地址\",\n  \"refresh_btn\": \"重新整理\",\n  \"refresh_statics\": \"重新整理統計資料\",\n  \"refused\": \"REFUSED\",\n  \"report_an_issue\": \"回報問題\",\n  \"request_details\": \"請求詳細資料\",\n  \"request_table_header\": \"請求\",\n  \"requests_count\": \"查詢次數\",\n  \"reset_settings\": \"重設設定\",\n  \"resolve_clients_desc\": \"透過相應的伺服器傳送 PTR 查詢（本機用戶端使用私人 DNS 伺服器，公共 IP 使用上游伺服器），將客戶端的 IP 反解為主機名稱。\",\n  \"resolve_clients_title\": \"啟用用戶端的 IP 位址的反向解析\",\n  \"response_code\": \"回應代碼\",\n  \"response_details\": \"回應詳細資料\",\n  \"response_table_header\": \"回應\",\n  \"response_time\": \"回應時間\",\n  \"rewrite_A\": \"<0>A</0>: 特殊值，將上游查詢結果覆寫 <0>A</0> 紀錄\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>: 特殊值，將上游查詢結果覆寫 <0>AAAA</0> 紀錄\",\n  \"rewrite_add\": \"新增 DNS 覆寫\",\n  \"rewrite_added\": \"「{{key}}」的 DNS 覆寫新增成功\",\n  \"rewrite_applied\": \"已套用 DNS 覆寫規則\",\n  \"rewrite_confirm_delete\": \"您確定要刪除 \\\"{{key}}\\\" 的 DNS 覆寫？\",\n  \"rewrite_deleted\": \"「{{key}}」的 DNS 覆寫刪除成功\",\n  \"rewrite_desc\": \"提供簡單的方式對特定網域自訂 DNS 回應。\",\n  \"rewrite_domain_name\": \"網域名稱：新增一筆 CNAME 紀錄\",\n  \"rewrite_edit\": \"編輯 DNS 覆寫\",\n  \"rewrite_hosts_applied\": \"由「hosts 檔案」覆寫\",\n  \"rewrite_ip_address\": \"IP 位址：使用 A 或 AAAA 紀錄回應\",\n  \"rewrite_not_found\": \"找不到 DNS 覆寫\",\n  \"rewrite_updated\": \"已更新 DNS 覆寫\",\n  \"rewritten\": \"已覆寫\",\n  \"rows_table_footer_text\": \"列\",\n  \"rule_added_to_custom_filtering_toast\": \"已新增至自訂規則中：{{rule}}\",\n  \"rule_label\": \"規則\",\n  \"rule_removed_from_custom_filtering_toast\": \"已從自訂過濾規則中移除：{{rule}}\",\n  \"rules_count_table_header\": \"規則總數\",\n  \"safe_browsing\": \"安全瀏覽\",\n  \"safe_search\": \"安全搜尋\",\n  \"saturday\": \"星期六\",\n  \"saturday_short\": \"週六\",\n  \"save_btn\": \"儲存\",\n  \"save_config\": \"儲存設定\",\n  \"schedule_add\": \"新增排程\",\n  \"schedule_current_timezone\": \"目前時區：{{value}}\",\n  \"schedule_desc\": \"設定已封鎖服務的閒置時段\",\n  \"schedule_edit\": \"編輯排程\",\n  \"schedule_from\": \"從\",\n  \"schedule_invalid_select\": \"開始時間必須在結束時間之前\",\n  \"schedule_modal_description\": \"這個排程將會取代同一星期中所有現有的排程。每一天只能有一個閒置時段。\",\n  \"schedule_modal_time_off\": \"沒有封鎖服務：\",\n  \"schedule_new\": \"新排程\",\n  \"schedule_remove\": \"移除排程\",\n  \"schedule_save\": \"儲存排程\",\n  \"schedule_select_days\": \"選擇天數\",\n  \"schedule_services\": \"暫停服務封鎖\",\n  \"schedule_services_desc\": \"設定服務封鎖過濾器的暫停排程\",\n  \"schedule_services_desc_client\": \"針對此用戶端，設定服務阻擋的暫停排程\",\n  \"schedule_time_all_day\": \"全天\",\n  \"schedule_timezone\": \"選擇時區\",\n  \"schedule_to\": \"至\",\n  \"served_from_cache_label\": \"由快取回應\",\n  \"service_name\": \"服務名稱\",\n  \"set_static_ip\": \"設定一組靜態 IP 位址\",\n  \"settings\": \"設定\",\n  \"settings_custom\": \"自訂\",\n  \"settings_global\": \"全域\",\n  \"setup_config_to_enable_dhcp_server\": \"建立設定檔來使用 DHCP 伺服器\",\n  \"setup_dns_notice\": \"要使用  <1>DNS-over-HTTPS</1> 或 <1>DNS-over-TLS</1>，您必須先在 AdGuard Home 完成 <0>加密設定</0>。\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS：</0>使用 <1>{{address}}</1>。\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS：</0>使用 <1>{{address}}</1>。\",\n  \"setup_dns_privacy_3\": \"<0>以下是您可以使用軟體的列表</0>\",\n  \"setup_dns_privacy_4\": \"在 iOS 14 或 macOS Big Sur 裝置上，您可以下載特定的 '.mobileconfig' 檔案。此檔案將<highlight>DNS-over-HTTPS</highlight> 或 <highlight>DNS-over-TLS</highlight> 伺服器新增至 DNS 設定。\",\n  \"setup_dns_privacy_android_1\": \"Android 9 原生支援 DNS-over-TLS。前往「設定」→「網路 & 網際網路」→「進階」→「私人 DNS」設定。\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0> 支援  <1>DNS-over-HTTPS</1> 與 <1>DNS-over-TLS</1>。\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> 對 Android 新增支援 <1>DNS-over-HTTPS</1>。\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS 與 macOS 描述檔\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> 支援 <1>DNS-over-HTTPS</1>，若要使用您必須先產生 <2>DNS Stamp</2>。\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard for iOS</0> 支援 <1>DNS-over-HTTPS</1> 與 <1>DNS-over-TLS</1> 設定。\",\n  \"setup_dns_privacy_other_1\": \"AdGuard Home 本身在任何平台都是安全的 DNS 用戶端。\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> 支援所有加密 DNS 協定。\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> 支援 <1>DNS-over-HTTPS</1>。\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> 支援 <1>DNS-over-HTTPS</1>。\",\n  \"setup_dns_privacy_other_5\": \"您可以在<0>這裏</0>與<1>這裏</1>找到更多實作軟體。\",\n  \"setup_dns_privacy_other_title\": \"其他實作方式\",\n  \"setup_guide\": \"安裝導覽\",\n  \"show_all_filter_type\": \"顯示全部\",\n  \"show_blocked_responses\": \"已封鎖\",\n  \"show_filtered_type\": \"僅顯示已過濾\",\n  \"show_processed_responses\": \"已處理\",\n  \"show_whitelisted_responses\": \"已加入允許清單\",\n  \"sign_in\": \"登入\",\n  \"sign_out\": \"登出\",\n  \"source_label\": \"來源\",\n  \"static_ip\": \"靜態 IP 位址\",\n  \"static_ip_desc\": \"由於 AdGuard Home 是個伺服器，因此需要一組靜態 IP 位址使它正常運作。否則在某些時候您的路由器可能會發派不同的 IP 位址給 AdGuard Home。\",\n  \"statistics_clear\": \" 清除統計資料\",\n  \"statistics_clear_confirm\": \"您確定要清除統計資料嗎？\",\n  \"statistics_cleared\": \"已清除統計資料\",\n  \"statistics_configuration\": \"統計資料設定\",\n  \"statistics_enable\": \"啟用統計數據\",\n  \"statistics_retention\": \"統計資料保留時間\",\n  \"statistics_retention_confirm\": \"您確定要更改統計資料保存時間嗎？如果您縮短期限部分資料可能將會遺失\",\n  \"statistics_retention_desc\": \"如果您縮短期限部分資料可能將會遺失\",\n  \"stats_adult\": \"已封鎖成人網站\",\n  \"stats_disabled\": \"已禁用統計資料。您可以從<0>設定頁面</0>打開它。\",\n  \"stats_disabled_short\": \"已禁用統計資料\",\n  \"stats_malware_phishing\": \"已封鎖惡意軟體/網路釣魚\",\n  \"stats_params\": \"統計資料設定\",\n  \"stats_query_domain\": \"熱門查詢網域排行\",\n  \"subnet_error\": \"地址必須在同一個子網路中\",\n  \"sunday\": \"星期日\",\n  \"sunday_short\": \"週日\",\n  \"system_host_files\": \"系統 hosts 檔案\",\n  \"table_client\": \"用戶端\",\n  \"table_name\": \"名稱\",\n  \"tags_desc\": \"可在此指定用戶端的標籤。標籤可包含在過濾規則內，並且在指定上更為精確。\\n<0>進一步了解</0>\",\n  \"tags_title\": \"標籤\",\n  \"test_upstream_btn\": \"測試上游 DNS\",\n  \"theme_auto\": \"自動\",\n  \"theme_auto_desc\": \"自動（根據裝置調整）\",\n  \"theme_dark\": \"深色\",\n  \"theme_dark_desc\": \"深色主題\",\n  \"theme_light\": \"明亮\",\n  \"theme_light_desc\": \"淺色主題\",\n  \"thursday\": \"星期四\",\n  \"thursday_short\": \"週四\",\n  \"time_table_header\": \"時間\",\n  \"top_blocked_domains\": \"熱門封鎖網域排行\",\n  \"top_clients\": \"熱門用戶端排行\",\n  \"top_upstreams\": \"熱門上游伺服器\",\n  \"topline_expired_certificate\": \"您的 SSL 憑證已到期。請前往<0>加密設定</0>更新。\",\n  \"topline_expiring_certificate\": \"您的 SSL 憑證即將到期。請前往<0>加密設定</0>更新。\",\n  \"tracker_source\": \"追蹤器來源\",\n  \"try_again\": \"再試一次\",\n  \"ttl_cache_validation\": \"最小快取 TTL 值必須小於或等於最大值\",\n  \"tuesday\": \"星期二\",\n  \"tuesday_short\": \"週二\",\n  \"type_table_header\": \"類型\",\n  \"unavailable_dhcp\": \"DHCP 無法使用\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home 無法在您的作業系統上運行 DHCP 伺服器\",\n  \"unblock\": \"解除封鎖\",\n  \"unblock_all\": \"全部解除封鎖\",\n  \"unblock_for_this_client_only\": \"僅解除封鎖此用戶端\",\n  \"unknown_filter\": \"未知過濾器 {{filterId}}\",\n  \"update_announcement\": \"有新版的 AdGuard Home {{version}} 可供更新！詳細資訊請<0>點擊這裡</0>。\",\n  \"update_failed\": \"自動更新發生錯誤。請嘗試依照<a>以下步驟</a> 來手動更新。\",\n  \"update_now\": \"立即更新\",\n  \"updated_custom_filtering_toast\": \"自訂過濾規則已更新\",\n  \"updated_save_search_toast\": \"已更新安全搜尋設定\",\n  \"updated_upstream_dns_toast\": \"已更新上游 DNS 伺服器\",\n  \"updates_checked\": \"檢查更新成功\",\n  \"updates_version_equal\": \"AdGuard Home 是最新的版本\",\n  \"upstream\": \"上游伺服器\",\n  \"upstream_dns\": \"上游 DNS 伺服器\",\n  \"upstream_dns_cache_configuration\": \"上游 DNS 快取設定\",\n  \"upstream_dns_client_desc\": \"如果您將此欄位留白，AdGuard Home 將使用 <0>DNS 設定</0> 內的設定的 DNS 伺服器。\",\n  \"upstream_dns_configured_in_file\": \"設定在 {{path}}\",\n  \"upstream_dns_help\": \"每行輸入一個伺服器位址。<a>了解更多</a>有關設定上游 DNS 伺服器的內容\",\n  \"upstream_parallel\": \"使用平行查詢，同時查詢所有上游伺服器來加速解析結果\",\n  \"upstream_timeout\": \"上游超時\",\n  \"upstream_timeout_desc\": \"指定等待來自此上游伺服器回應的秒數\",\n  \"upstreams\": \"上游\",\n  \"use_adguard_browsing_sec\": \"使用 AdGuard 瀏覽安全網路服務\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home 將比對查詢網域是否在瀏覽安全服務黑名單內。AdGuard Home 選擇使用尊重個人隱私的 API 進行比對，先透過 SHA256 將網域編碼，取前置字串傳送到伺服器進行比對。\",\n  \"use_adguard_parental\": \"使用 AdGuard 家長監護功能\",\n  \"use_adguard_parental_hint\": \"AdGuard Home 將比對查詢網域是否含有成人內容。它使用與 AdGuard 瀏覽安全一樣的尊重個人隱私的 API 來進行檢查。\",\n  \"use_private_ptr_resolvers_desc\": \"透過私有上游伺服器、DHCP 或 /etc/hosts 等管道，解析含有私有 IP 位址的 ARPA 網域的 PTR、SOA 與 NS 請求。若停用此功能，AdGuard Home 將以 NXDOMAIN 回應所有相關請求。\",\n  \"use_private_ptr_resolvers_title\": \"使用私人 DNS 反解器\",\n  \"use_saved_key\": \"使用先前儲存的鍵\",\n  \"username_label\": \"使用者名稱\",\n  \"username_placeholder\": \"輸入使用者名稱\",\n  \"validated_with_dnssec\": \"DNSSEC 驗證有效\",\n  \"version\": \"版本\",\n  \"version_request_error\": \"更新檢查失敗。請檢查您的網絡連線。\",\n  \"wednesday\": \"星期三\",\n  \"wednesday_short\": \"週三\",\n  \"whois\": \"Whois\"\n}\n"
  },
  {
    "path": "client/src/__locales/zh-tw.json",
    "content": "{\n  \"access_allowed_desc\": \"無類別網域間路由（CIDRs）、IP 位址或<a>用戶端 IDs</a> 之清單。如果此清單有項目，AdGuard Home 將接受僅來自這些用戶端的請求。\",\n  \"access_allowed_title\": \"被允許的用戶端\",\n  \"access_blocked_desc\": \"不要把這個和過濾器混淆。AdGuard Home 排除與這些網域相符的 DNS 查詢，且這些查詢甚至不會出現在查詢記錄中。您可相應地明確指定確切的域名、萬用字元（wildcard）或網址過濾器的規則，例如，\\\"example.org\\\"、\\\"*.example.org\\\" 或 \\\"||example.org^\\\"。\",\n  \"access_blocked_title\": \"未被允許的網域\",\n  \"access_desc\": \"於此您可配置用於 AdGuard Home DNS 伺服器之存取規則\",\n  \"access_disallowed_desc\": \"無類別網域間路由（CIDRs）、IP 位址或<a>用戶端 IDs</a> 之清單。如果此清單有項目，AdGuard Home 將排除來自這些用戶端的請求。如果在已允許的用戶端中有項目，此欄位被忽略。\",\n  \"access_disallowed_title\": \"未被允許的用戶端\",\n  \"access_settings_saved\": \"存取設定被成功地儲存\",\n  \"access_title\": \"存取設定\",\n  \"actions_table_header\": \"動作\",\n  \"add_allowlist\": \"新增允許清單\",\n  \"add_blocklist\": \"新增封鎖清單\",\n  \"add_custom_list\": \"新增一個自訂的清單\",\n  \"add_persistent_client\": \"新增為永久性客戶端\",\n  \"address\": \"位址\",\n  \"adg_will_drop_dns_queries\": \"AdGuard Home 將持續排除來自此用戶端之所有的 DNS 查詢。\",\n  \"all_lists_up_to_date_toast\": \"所有的清單已是最新的\",\n  \"all_queries\": \"所有的查詢\",\n  \"allow_this_client\": \"允許此用戶端\",\n  \"allowed\": \"已允許的\",\n  \"anonymize_client_ip\": \"將用戶端 IP 匿名\",\n  \"anonymize_client_ip_desc\": \"不要儲存用戶端之完整的 IP 位址到記錄或統計資料裡\",\n  \"anonymizer_notification\": \"<0>注意：</0>IP 匿名功能已開啟。您可在<1>一般設定</1>中關閉。\",\n  \"answer\": \"回應\",\n  \"apply_btn\": \"套用\",\n  \"auto_clients_desc\": \"AdGuard Home 使用或可能使用的裝置的 IP 地址資訊。這些資訊來自多個來源，包括主機檔案、反向 DNS 等。\",\n  \"auto_clients_title\": \"執行時期用戶端\",\n  \"autofix_warning_list\": \"它將執行這些任務：<0>撤銷系統 DNSStubListener</0> <0>設定 DNS 伺服器位址為 127.0.0.1</0> <0>用 /run/systemd/resolve/resolv.conf 取代 /etc/resolv.conf 的符號連結目標</0> <0>停止 DNSStubListener（重新載入 systemd-resolved 服務）</0>\",\n  \"autofix_warning_result\": \"因此，預設下，來自您的系統之所有的 DNS 請求將被 AdGuard Home 處理。\",\n  \"autofix_warning_text\": \"如果您點擊\\\"修復\\\"，AdGuard Home 將配置您的系統使用 AdGuard Home DNS 伺服器。\",\n  \"average_processing_time\": \"平均的處理時間\",\n  \"average_processing_time_hint\": \"在處理一項 DNS 請求時以毫秒（ms）計的平均時間\",\n  \"average_upstream_response_time\": \"平均的上游回應時間\",\n  \"back\": \"返回\",\n  \"block\": \"封鎖\",\n  \"block_all\": \"封鎖全部\",\n  \"block_domain_use_filters_and_hosts\": \"透過過濾器和主機檔案封鎖網域\",\n  \"block_for_this_client_only\": \"僅對此用戶端封鎖\",\n  \"block_services\": \"封鎖特定的服務\",\n  \"blocked_adult_websites\": \"被家長控制封鎖\",\n  \"blocked_by\": \"<0>被過濾器封鎖</0>\",\n  \"blocked_by_cname_or_ip\": \"被正規名稱（CNAME）或 IP 封鎖\",\n  \"blocked_by_response\": \"在回應過程中被正規名稱（CNAME）或 IP 封鎖\",\n  \"blocked_response_ttl\": \"已封鎖的回應之存活時間（TTL）\",\n  \"blocked_response_ttl_desc\": \"對用戶端應快取受過濾的回應，指定多少秒數\",\n  \"blocked_safebrowsing\": \"被安全瀏覽封鎖\",\n  \"blocked_service\": \"已封鎖的服務\",\n  \"blocked_services\": \"已封鎖的服務\",\n  \"blocked_services_desc\": \"允許立即封鎖熱門的網站和服務。\",\n  \"blocked_services_global\": \"使用全域已封鎖的服務\",\n  \"blocked_services_saved\": \"已封鎖的服務被成功地儲存\",\n  \"blocked_threats\": \"已封鎖的威脅\",\n  \"blocking_ipv4\": \"封鎖 IPv4\",\n  \"blocking_ipv4_desc\": \"要被返回給已封鎖的 A 請求之 IP 位址\",\n  \"blocking_ipv6\": \"封鎖 IPv6\",\n  \"blocking_ipv6_desc\": \"要被返回給已封鎖的 AAAA 請求之 IP 位址\",\n  \"blocking_mode\": \"封鎖模式\",\n  \"blocking_mode_custom_ip\": \"自訂的 IP：以一組手動地被設定的 IP 位址回覆\",\n  \"blocking_mode_default\": \"預設：當被 AdBlock 樣式的規則封鎖時，以零值 IP 位址（0.0.0.0 供 A；:: 供 AAAA）回覆；當被 /etc/hosts 樣式的規則封鎖時，以在該規則中之已明確指定的 IP 位址回覆\",\n  \"blocking_mode_null_ip\": \"無效的 IP：以零值 IP 位址（0.0.0.0 供 A；:: 供 AAAA）回覆\",\n  \"blocking_mode_nxdomain\": \"不存在的網域（NXDOMAIN）：以 NXDOMAIN 碼回覆\",\n  \"blocking_mode_refused\": \"已拒絕（REFUSED）：以 REFUSED 碼回覆\",\n  \"blocklist\": \"封鎖清單\",\n  \"bootstrap_dns\": \"自我啟動（Bootstrap）DNS 伺服器\",\n  \"bootstrap_dns_desc\": \"DNS 伺服器的 IP 位址，用於解析您指定為上游伺服器的 DoH/DoT 解析器的 IP 位址。不允許註釋。\",\n  \"cache_cleared\": \"DNS 快取被成功地清除\",\n  \"cache_enabled\": \"啟用快取\",\n  \"cache_enabled_desc\": \"在本機儲存 DNS 回應。\",\n  \"cache_optimistic\": \"樂觀快取\",\n  \"cache_optimistic_desc\": \"即使當項目為已到期的，從快取使 AdGuard Home 回覆，並還嘗試重新整理它們。\",\n  \"cache_size\": \"快取大小\",\n  \"cache_size_desc\": \"DNS 快取大小（以位元組）\",\n  \"cache_size_validation\": \"啟用時，快取大小必須大於 0。\",\n  \"cache_ttl_max_override\": \"覆寫最大的存活時間（TTL）\",\n  \"cache_ttl_max_override_desc\": \"設定最大的存活時間數值（秒）供在 DNS 快取中的項目。\",\n  \"cache_ttl_min_override\": \"覆寫最小的存活時間（TTL）\",\n  \"cache_ttl_min_override_desc\": \"當快取 DNS 回應時，延長從上游的伺服器收到的短存活時間數值（秒）。\",\n  \"cancel_btn\": \"取消\",\n  \"category_label\": \"類別\",\n  \"check\": \"檢查\",\n  \"check_client_id\": \"用戶端識別碼（ClientID 或 IP 位址）\",\n  \"check_cname\": \"正規名稱（CNAME）：{{cname}}\",\n  \"check_desc\": \"檢查主機名稱是否被過濾。\",\n  \"check_dhcp_servers\": \"檢查動態主機設定協定（DHCP）伺服器\",\n  \"check_dns_record\": \"選擇 DNS 記錄類型\",\n  \"check_enter_client_id\": \"輸入用戶識別碼\",\n  \"check_hostname\": \"主機名稱或域名\",\n  \"check_ip\": \"IP 位址：{{ip}}\",\n  \"check_not_found\": \"未在您的過濾器清單中被找到\",\n  \"check_reason\": \"原因：{{reason}}\",\n  \"check_service\": \"服務名稱：{{service}}\",\n  \"check_title\": \"檢查該過濾\",\n  \"check_updates_btn\": \"檢查更新\",\n  \"check_updates_now\": \"立即檢查更新\",\n  \"choose_allowlist\": \"選擇允許清單\",\n  \"choose_blocklist\": \"選擇封鎖清單\",\n  \"choose_from_list\": \"從該清單中選擇\",\n  \"city\": \"城市\",\n  \"clear_cache\": \"清除快取\",\n  \"click_to_view_queries\": \"點擊以檢視查詢\",\n  \"client_add\": \"新增用戶端\",\n  \"client_added\": \"用戶端 \\\"{{key}}\\\" 被成功地加入\",\n  \"client_blocked\": \"用戶端 \\\"{{ip}}\\\" 被成功地封鎖\",\n  \"client_confirm_block\": \"您確定您想要封鎖該用戶端 \\\"{{ip}}\\\" 嗎？\",\n  \"client_confirm_delete\": \"您確定您想要刪除用戶端 \\\"{{key}}\\\" 嗎？\",\n  \"client_confirm_unblock\": \"您確定您想要解除封鎖該用戶端 \\\"{{ip}}\\\" 嗎？\",\n  \"client_deleted\": \"用戶端 \\\"{{key}}\\\" 被成功地刪除\",\n  \"client_details\": \"用戶端細節\",\n  \"client_edit\": \"編輯用戶端\",\n  \"client_global_settings\": \"使用全域的設定\",\n  \"client_id\": \"用戶端 ID\",\n  \"client_id_desc\": \"用戶端可根據用戶端 ID 被識別。<a>於此</a>，了解更多關於如何識別用戶端。\",\n  \"client_id_placeholder\": \"輸入用戶端 ID\",\n  \"client_identifier\": \"識別碼\",\n  \"client_identifier_desc\": \"用戶端可根據它們的 IP 位址、無類別網域間路由（CIDR）、媒體存取控制（MAC）位址或用戶端 ID（可被用於 DoT/DoH/DoQ）被識別。<0>於此</0>，了解更多關於如何識別用戶端。\",\n  \"client_name\": \"用戶端 {{id}}\",\n  \"client_new\": \"新的用戶端\",\n  \"client_settings\": \"用戶端設定\",\n  \"client_table_header\": \"用戶端\",\n  \"client_unblocked\": \"用戶端 \\\"{{ip}}\\\" 被成功地解除封鎖\",\n  \"client_updated\": \"用戶端 \\\"{{key}}\\\" 被成功地更新\",\n  \"clients_desc\": \"配置關於被連線到 AdGuard Home 的裝置之持續性用戶端記錄\",\n  \"clients_not_found\": \"無已發現之用戶端\",\n  \"clients_title\": \"持續性用戶端\",\n  \"compact\": \"精簡的\",\n  \"config_successfully_saved\": \"配置被成功地儲存\",\n  \"configure\": \"配置\",\n  \"confirm_dns_cache_clear\": \"您確定您想要清除 DNS 快取嗎？\",\n  \"confirm_static_ip\": \"AdGuard Home 將配置 {{ip}} 為您的靜態 IP 位址。您想要繼續嗎？\",\n  \"copyright\": \"版權\",\n  \"country\": \"國家\",\n  \"custom_filter_rules\": \"自訂的過濾規則\",\n  \"custom_filter_rules_hint\": \"於一行上輸入一項規則。您可使用廣告封鎖規則或主機檔案語法。\",\n  \"custom_filtering_rules\": \"自訂的過濾規則\",\n  \"custom_ip\": \"自訂的 IP\",\n  \"custom_retention_input\": \"輸入保留時間（小時）\",\n  \"custom_rotation_input\": \"輸入旋轉時間（小時）\",\n  \"dashboard\": \"儀表板\",\n  \"date\": \"日期\",\n  \"default\": \"預設\",\n  \"delete_confirm\": \"您確定您想要刪除 \\\"{{key}}\\\" 嗎？\",\n  \"delete_table_action\": \"刪除\",\n  \"descr\": \"說明\",\n  \"details\": \"細節\",\n  \"dhcp_add_static_lease\": \"新增靜態租約\",\n  \"dhcp_config_saved\": \"動態主機設定協定（DHCP）配置被成功地儲存\",\n  \"dhcp_description\": \"如果您的路由器未提供動態主機設定協定（DHCP）設定，您可使用 AdGuard 自身內建的 DHCP 伺服器。\",\n  \"dhcp_disable\": \"停用 DHCP 伺服器\",\n  \"dhcp_dynamic_ip_found\": \"您的系統使用動態 IP 位址配置供介面 <0>{{interfaceName}}</0>。為了使用動態主機設定協定（DHCP）伺服器，靜態 IP 位址必須被設定。您現行的 IP 位址為 <0>{{ipAddress}}</0>。如果您按\\\"啟用 DHCP 伺服器\\\" 按鈕，AdGuard Home 將自動地設定此 IP 位址作為靜態。\",\n  \"dhcp_edit_static_lease\": \"編輯靜態租約\",\n  \"dhcp_enable\": \"啟用動態主機設定協定（DHCP）伺服器\",\n  \"dhcp_error\": \"AdGuard Home 無法確定於該網路上是否有另外現行的動態主機設定協定（DHCP）伺服器\",\n  \"dhcp_form_gateway_input\": \"閘道 IP\",\n  \"dhcp_form_lease_input\": \"租約期間\",\n  \"dhcp_form_lease_title\": \"動態主機設定協定（DHCP）租約時間（以秒數）\",\n  \"dhcp_form_range_end\": \"結束範圍\",\n  \"dhcp_form_range_start\": \"起始範圍\",\n  \"dhcp_form_range_title\": \"IP 位址範圍\",\n  \"dhcp_form_subnet_input\": \"子網路遮罩\",\n  \"dhcp_found\": \"於該網路上，一個現行的動態主機設定協定（DHCP）伺服器被發現。啟用內建的 DHCP 伺服器為不安全的。\",\n  \"dhcp_hardware_address\": \"硬體位址\",\n  \"dhcp_interface_select\": \"選擇動態主機設定協定（DHCP）介面\",\n  \"dhcp_ip_addresses\": \"IP 位址\",\n  \"dhcp_ipv4_settings\": \"DHCP IPv4 設定\",\n  \"dhcp_ipv6_settings\": \"DHCP IPv6 設定\",\n  \"dhcp_lease_added\": \"靜態租約 \\\"{{key}}\\\" 被成功地加入\",\n  \"dhcp_lease_deleted\": \"靜態租約 \\\"{{key}}\\\" 被成功地刪除\",\n  \"dhcp_lease_updated\": \"「{{key}}」的靜態租約已成功更新\",\n  \"dhcp_leases\": \"動態主機設定協定（DHCP）租約\",\n  \"dhcp_leases_not_found\": \"無已發現之動態主機設定協定（DHCP）租約\",\n  \"dhcp_new_static_lease\": \"新的靜態租約\",\n  \"dhcp_not_found\": \"因為 AdGuard Home 於該網路上未發現任何現行的 DHCP 伺服器，啟用內建的動態主機設定協定（DHCP）伺服器為安全的。然而，您應手動地重新檢查那個，因為自動的探查目前不予 100％ 保證。\",\n  \"dhcp_reset\": \"您確定您想要重置動態主機設定協定（DHCP）配置嗎？\",\n  \"dhcp_reset_leases\": \"重置所有的租約\",\n  \"dhcp_reset_leases_confirm\": \"您確定您想要重置所有的租約嗎？\",\n  \"dhcp_reset_leases_success\": \"動態主機設定協定（DHCP）租約被成功地重置\",\n  \"dhcp_settings\": \"動態主機設定協定（DHCP）設定\",\n  \"dhcp_static_ip_error\": \"為了使用動態主機設定協定（DHCP）伺服器，靜態 IP 位址必須被設定。AdGuard Home 未能確定此網路介面是否被配置使用靜態 IP 位址。請手動地設定靜態 IP 位址。\",\n  \"dhcp_static_leases\": \"動態主機設定協定（DHCP）靜態租約\",\n  \"dhcp_static_leases_not_found\": \"無已發現之動態主機設定協定（DHCP）靜態租約\",\n  \"dhcp_table_expires\": \"到期\",\n  \"dhcp_table_hostname\": \"主機名稱\",\n  \"dhcp_title\": \"動態主機設定協定（DHCP）伺服器（實驗性的！）\",\n  \"dhcp_warning\": \"如果您無論如何想要啟用動態主機設定協定（DHCP）伺服器，確保在您的網路中無其它現行的 DHCP 伺服器，因為對於該網路上的裝置，這可能破壞其網際網路連線！\",\n  \"disable_for_hours\": \"{{count}} 小時\",\n  \"disable_for_hours_plural\": \"{{count}} 小時\",\n  \"disable_for_minutes\": \"{{count}} 分鐘\",\n  \"disable_for_minutes_plural\": \"{{count}} 分鐘\",\n  \"disable_for_seconds\": \"{{count}} 秒\",\n  \"disable_for_seconds_plural\": \"{{count}} 秒\",\n  \"disable_ipv6\": \"停用 IPv6 位址解析\",\n  \"disable_ipv6_desc\": \"停止所有對於 IPv6 位址（類型 AAAA）的 DNS 查詢，並從 HTTPS 回應中移除 IPv6 的提示。\",\n  \"disable_notify_for_hours\": \"計 {{count}} 小時停用防護\",\n  \"disable_notify_for_hours_plural\": \"計 {{count}} 小時停用防護\",\n  \"disable_notify_for_minutes\": \"計 {{count}} 分鐘停用防護\",\n  \"disable_notify_for_minutes_plural\": \"計 {{count}} 分鐘停用防護\",\n  \"disable_notify_for_seconds\": \"計 {{count}} 秒停用防護\",\n  \"disable_notify_for_seconds_plural\": \"計 {{count}} 秒停用防護\",\n  \"disable_notify_until_tomorrow\": \"停用防護直到明天\",\n  \"disable_protection\": \"停用防護\",\n  \"disable_rewrites\": \"關閉重寫規則\",\n  \"disable_until_tomorrow\": \"直到明天\",\n  \"disabled\": \"已停用\",\n  \"disabled_dhcp\": \"DHCP 伺服器已停用\",\n  \"disabled_filtering_toast\": \"已停用過濾\",\n  \"disabled_parental_toast\": \"已停用家長控制\",\n  \"disabled_protection\": \"已停用防護\",\n  \"disabled_safe_browsing_toast\": \"已停用安全瀏覽\",\n  \"disabled_safe_search_toast\": \"已停用安全搜尋\",\n  \"disallow_this_client\": \"不允許此用戶端\",\n  \"dns_addresses\": \"DNS 位址\",\n  \"dns_allowlists\": \"DNS 允許清單\",\n  \"dns_allowlists_desc\": \"即使來自 DNS 允許清單的網域在任何的封鎖清單中，它們將被允許。\",\n  \"dns_blocklists\": \"DNS 封鎖清單\",\n  \"dns_blocklists_desc\": \"AdGuard Home 將阻擋與封鎖清單相符的網域。\",\n  \"dns_cache_config\": \"DNS 快取配置\",\n  \"dns_cache_config_desc\": \"於此您可配置 DNS 快取\",\n  \"dns_cache_size\": \"DNS 快取大小，單位：位元\",\n  \"dns_config\": \"DNS 伺服器配置\",\n  \"dns_over_https\": \"DNS-over-HTTPS\",\n  \"dns_over_quic\": \"DNS-over-QUIC\",\n  \"dns_over_tls\": \"DNS-over-TLS\",\n  \"dns_privacy\": \"DNS 隱私\",\n  \"dns_providers\": \"這裡是一個從中選擇之<0>已知的 DNS 供應商之清單</0>。\",\n  \"dns_query\": \"DNS 查詢\",\n  \"dns_rewrites\": \"DNS 改寫\",\n  \"dns_settings\": \"DNS 設定\",\n  \"dns_start\": \"DNS 伺服器正在啟動\",\n  \"dns_status_error\": \"檢查 DNS 伺服器狀態出錯\",\n  \"dns_test_not_ok_toast\": \"伺服器 \\\"{{key}}\\\"：無法被使用，請檢查您已正確地填寫它\",\n  \"dns_test_ok_toast\": \"已明確指定的 DNS 伺服器正在正確地運作\",\n  \"dns_test_parsing_error_toast\": \"第 {{section}} 節：第 {{line}} 行：無法使用，請檢查您輸入的是否正確\",\n  \"dns_test_warning_toast\": \"上游 “{{key}}” 不回應測試請求，可能無法正常工作\",\n  \"dnscrypt\": \"DNSCrypt\",\n  \"dnssec_enable\": \"啟用網域名稱系統安全性擴充功能（DNSSEC）\",\n  \"dnssec_enable_desc\": \"在發出的 DNS 查詢中設定 DNSSEC 標記並檢查該結果（已啟用 DNSSEC 的解析器是必須的）。\",\n  \"domain\": \"網域\",\n  \"domain_desc\": \"輸入您想要被改寫的域名或萬用字元（wildcard）。\",\n  \"domain_name_table_header\": \"域名\",\n  \"domain_or_client\": \"網域或用戶端\",\n  \"down\": \"停止運作的\",\n  \"download_mobileconfig\": \"下載配置檔案\",\n  \"download_mobileconfig_doh\": \"下載用於 DNS-over-HTTPS 的 .mobileconfig\",\n  \"download_mobileconfig_dot\": \"下載用於 DNS-over-TLS 的 .mobileconfig\",\n  \"ecs\": \"對於 DNS 的擴充機制（EDNS）用戶端子網路\",\n  \"edit_allowlist\": \"編輯允許清單\",\n  \"edit_blocklist\": \"編輯封鎖清單\",\n  \"edit_table_action\": \"編輯\",\n  \"edns_cs_desc\": \"新增對於 DNS 的擴充機制（EDNS）用戶端子網路選項到上游的請求，並在查詢記錄中記錄由用戶端傳送的數值。\",\n  \"edns_enable\": \"啟用對於 DNS 的擴充機制（EDNS）用戶端子網路\",\n  \"edns_use_custom_ip\": \"為 EDNS 使用自訂的 IP\",\n  \"edns_use_custom_ip_desc\": \"允許為 EDNS 使用自訂的 IP\",\n  \"elapsed\": \"已經過\",\n  \"empty_response_status\": \"空無的\",\n  \"enable_protection\": \"啟用防護\",\n  \"enable_protection_timer\": \"防護將於 {{time}} 啟用\",\n  \"enable_rewrites\": \"啟用重寫規則\",\n  \"enable_upstream_dns_cache\": \"啟用本用戶端自訂上游配置的 DNS 快取\",\n  \"enabled_dhcp\": \"動態主機設定協定（DHCP）伺服器被啟用\",\n  \"enabled_filtering_toast\": \"已啟用過濾\",\n  \"enabled_parental_toast\": \"已啟用家長控制\",\n  \"enabled_protection\": \"已啟用防護\",\n  \"enabled_safe_browsing_toast\": \"已啟用安全瀏覽\",\n  \"enabled_save_search_toast\": \"已啟用安全搜尋\",\n  \"enabled_table_header\": \"已啟用\",\n  \"encryption_certificate_path\": \"憑證路徑\",\n  \"encryption_certificates\": \"憑證\",\n  \"encryption_certificates_desc\": \"為了使用加密，您需要提供有效的安全通訊端層（SSL）憑證鏈結供您的網域。於 <0>{{link}}</0> 上您可取得免費的憑證或您可從受信任的憑證授權單位之一購買它。\",\n  \"encryption_certificates_input\": \"於此複製/貼上您的隱私增強郵件編碼之（PEM-encoded）憑證。\",\n  \"encryption_certificates_source_content\": \"貼上該憑證內容\",\n  \"encryption_certificates_source_path\": \"設定一個憑證檔案路徑\",\n  \"encryption_chain_invalid\": \"憑證鏈結為無效的\",\n  \"encryption_chain_valid\": \"憑證鏈結為有效的\",\n  \"encryption_config_saved\": \"加密配置被儲存\",\n  \"encryption_desc\": \"供 DNS 和管理員網路介面兩者之加密（HTTPS/TLS）支援\",\n  \"encryption_doq\": \"DNS-over-QUIC 連接埠\",\n  \"encryption_doq_desc\": \"如果此連接埠被配置，AdGuard Home 將於此連接埠上運行 DNS-over-QUIC 伺服器。\",\n  \"encryption_dot\": \"DNS-over-TLS 連接埠\",\n  \"encryption_dot_desc\": \"如果該連接埠被配置，AdGuard Home 將於此連接埠上運行 DNS-over-TLS 伺服器。\",\n  \"encryption_enable\": \"啟用加密（HTTPS、DNS-over-HTTPS 和 DNS-over-TLS）\",\n  \"encryption_enable_desc\": \"如果加密被啟用，AdGuard Home 管理員介面透過 HTTPS 將運作，且該 DNS 伺服器將留心監聽透過 DNS-over-HTTPS 和 DNS-over-TLS 之請求。\",\n  \"encryption_expire\": \"到期\",\n  \"encryption_hostnames\": \"主機名稱\",\n  \"encryption_https\": \"HTTPS 連接埠\",\n  \"encryption_https_desc\": \"如果 HTTPS 連接埠被配置，AdGuard Home 管理員介面透過 HTTPS 將為可存取的，且它也將於 '/dns-query' 位置上提供 DNS-over-HTTPS。\",\n  \"encryption_issuer\": \"簽發者\",\n  \"encryption_key\": \"私密金鑰\",\n  \"encryption_key_input\": \"於此複製/貼上您的隱私增強郵件編碼之（PEM-encoded）私密金鑰供您的憑證。\",\n  \"encryption_key_invalid\": \"此為無效的 {{type}} 私密金鑰\",\n  \"encryption_key_source_content\": \"貼上該私密金鑰內容\",\n  \"encryption_key_source_path\": \"設定私密金鑰檔案安裝路徑\",\n  \"encryption_key_valid\": \"此為有效的 {{type}} 私密金鑰\",\n  \"encryption_plain_dns_desc\": \"預設啟用一般 DNS。您可以停用它以強制所有裝置使用加密 DNS。若要這樣做，您必須啟用至少一個加密 DNS 通訊協定\",\n  \"encryption_plain_dns_enable\": \"啟用一般的 DNS\",\n  \"encryption_plain_dns_error\": \"若要停用一般 DNS，請啟用至少一個加密 DNS 通訊協定\",\n  \"encryption_private_key_path\": \"私密金鑰路徑\",\n  \"encryption_redirect\": \"自動地重新導向到 HTTPS\",\n  \"encryption_redirect_desc\": \"如果被勾選，AdGuard Home 將自動地重新導向您從 HTTP 到 HTTPS 位址。\",\n  \"encryption_reset\": \"您確定您想要重置加密設定嗎？\",\n  \"encryption_server\": \"伺服器名稱\",\n  \"encryption_server_desc\": \"如果設定，AdGuard Home 會偵測 ClientID、回應 DDR 查詢，並執行其他連線驗證。如果未設定，則會停用這些功能。必須符合憑證中的一個 DNS 名稱。\",\n  \"encryption_server_enter\": \"輸入您的域名\",\n  \"encryption_settings\": \"加密設定\",\n  \"encryption_status\": \"狀態\",\n  \"encryption_subject\": \"物件\",\n  \"encryption_title\": \"加密\",\n  \"encryption_warning\": \"警告\",\n  \"enforce_safe_search\": \"使用安全搜尋\",\n  \"enforce_save_search_hint\": \"AdGuard Home 將在下列的搜尋引擎：Google、YouTube、Bing、DuckDuckGo、Ecosia、Yandex 和 Pixabay 中強制執行安全搜尋。\",\n  \"enforced_save_search\": \"已強制執行的安全搜尋\",\n  \"enter_cache_size\": \"輸入快取大小（位元組）\",\n  \"enter_cache_ttl_max_override\": \"輸入最大的存活時間（秒）\",\n  \"enter_cache_ttl_min_override\": \"輸入最小的存活時間（秒）\",\n  \"enter_name_hint\": \"輸入名稱\",\n  \"enter_url_or_path_hint\": \"輸入一個該清單之網址或絕對的路徑\",\n  \"enter_valid_allowlist\": \"輸入一個到該允許清單之有效的網址。\",\n  \"enter_valid_blocklist\": \"輸入一個到該封鎖清單之有效的網址。\",\n  \"error_details\": \"錯誤細節\",\n  \"example_comment\": \"! 看，一個註解。\",\n  \"example_comment_hash\": \"# 也是一個註解。\",\n  \"example_comment_meaning\": \"只是一個註解；\",\n  \"example_meaning_filter_block\": \"封鎖至 example.org 網域及其所有的子網域之存取；\",\n  \"example_meaning_filter_whitelist\": \"解除封鎖至 example.org 網域及其所有的子網域之存取；\",\n  \"example_meaning_host_block\": \"對 example.org（但非對其子網域）以 127.0.0.1 回覆；\",\n  \"example_multiple_upstreams_reserved\": \"<0>特定網域</0>的多個上游伺服器；\",\n  \"example_regex_meaning\": \"封鎖至與該已明確指定的規則運算式（Regular Expression）相符的網域之存取。\",\n  \"example_rewrite_domain\": \"僅對此域名改寫回應。\",\n  \"example_rewrite_wildcard\": \"對於所有的 <0>example.org</0> 子網域改寫回應。\",\n  \"example_upstream_comment\": \"註解。\",\n  \"example_upstream_doh\": \"加密的 <0>DNS-over-HTTPS</0>；\",\n  \"example_upstream_doh3\": \"有強制的 <0>HTTP/3</0> 且無退回到 HTTP/2 或更低版本之加密的 DNS-over-HTTPS；\",\n  \"example_upstream_doq\": \"加密的 <0>DNS-over-QUIC</0>；\",\n  \"example_upstream_dot\": \"加密的 <0>DNS-over-TLS</0>；\",\n  \"example_upstream_regular\": \"常規 DNS（透過 UDP）；\",\n  \"example_upstream_regular_port\": \"常規 DNS（透過 UDP，含連接埠）；\",\n  \"example_upstream_reserved\": \"<0>特定網域</0>的上游；\",\n  \"example_upstream_sdns\": \"關於 <1>DNSCrypt</1> 或 <2>DNS-over-HTTPS</2> 解析器之 <0>DNS 戳記</0>；\",\n  \"example_upstream_tcp\": \"常規 DNS（透過 TCP）；\",\n  \"example_upstream_tcp_hostname\": \"常規 DNS（透過 TCP，主機名稱）；\",\n  \"example_upstream_tcp_port\": \"常規 DNS（透過 TCP，含連接埠）；\",\n  \"example_upstream_udp\": \"常規 DNS（透過 UDP，主機名稱）；\",\n  \"examples_title\": \"範例\",\n  \"fallback_dns_desc\": \"當上游 DNS 伺服器未回覆時被使用的應變 DNS 伺服器之清單。此語法與在上面主要上游欄位中的相同。\",\n  \"fallback_dns_placeholder\": \"每行輸入一個應變 DNS 伺服器\",\n  \"fallback_dns_title\": \"應變 DNS 伺服器\",\n  \"faq\": \"常見問答集\",\n  \"fastest_addr\": \"最快的 IP 位址\",\n  \"fastest_addr_desc\": \"等待<b>所有</b> DNS 伺服器的回應，測量每個伺服器的 TCP 連線速度，並返回連線速度最快的伺服器的 IP 位址。<br/>如果一個或多個上游伺服器沒有回應，此模式會顯著減慢 DNS 查詢速度。確保您的上游伺服器穩定且上游超時時間短。\",\n  \"filter\": \"過濾器\",\n  \"filter_added_successfully\": \"該清單已被成功地加入\",\n  \"filter_allowlist\": \"警告：此動作也將把 \\\"{{disallowed_rule}}\\\" 規則排除在被允許的用戶端的清單之外。\",\n  \"filter_category_general\": \"一般的\",\n  \"filter_category_general_desc\": \"封鎖大多數朝向裝置的追蹤和廣告之清單\",\n  \"filter_category_other\": \"其它的\",\n  \"filter_category_other_desc\": \"其它的封鎖清單\",\n  \"filter_category_regional\": \"區域性的\",\n  \"filter_category_regional_desc\": \"專注於區域性的廣告和追蹤伺服器之清單\",\n  \"filter_category_security\": \"安全性\",\n  \"filter_category_security_desc\": \"專門地旨在封鎖惡意、網路釣魚和詐騙的網域之清單\",\n  \"filter_removed_successfully\": \"該清單已被成功地移除\",\n  \"filter_updated\": \"該清單已被成功地更新\",\n  \"filtered\": \"受過濾的\",\n  \"filtered_custom_rules\": \"被自訂的過濾規則過濾\",\n  \"filtering_rules_learn_more\": \"<0>了解更多</0>有關創建您自己的主機（hosts）清單。\",\n  \"filters\": \"過濾器\",\n  \"filters_and_hosts_hint\": \"AdGuard Home 懂得基本的廣告封鎖規則和主機檔案語法。\",\n  \"filters_block_toggle_hint\": \"您可在<a>過濾器</a>設定中設置封鎖規則。\",\n  \"filters_configuration\": \"過濾器配置\",\n  \"filters_enable\": \"啟用過濾器\",\n  \"filters_interval\": \"過濾器更新間隔\",\n  \"fix\": \"修復\",\n  \"for_last_days\": \"在最近的 {{count}} 日內\",\n  \"for_last_days_plural\": \"在最近的 {{count}} 日內\",\n  \"for_last_hours\": \"在過去的 {{count}} 小時內\",\n  \"for_last_hours_plural\": \"在過去的 {{count}} 小時內\",\n  \"forgot_password\": \"忘了密碼嗎？\",\n  \"forgot_password_desc\": \"請遵循<0>這些步驟</0>以創建一組新的密碼供您的使用者帳戶。\",\n  \"form_add_id\": \"新增識別碼\",\n  \"form_answer\": \"輸入 IP 位址或域名\",\n  \"form_client_name\": \"輸入用戶端名稱\",\n  \"form_domain\": \"輸入域名或萬用字元（wildcard）\",\n  \"form_enter_blocked_response_ttl\": \"請輸入已封鎖回應的存活時間（秒）\",\n  \"form_enter_host\": \"輸入主機名稱\",\n  \"form_enter_hostname\": \"輸入主機名稱\",\n  \"form_enter_id\": \"輸入識別碼\",\n  \"form_enter_ip\": \"輸入 IP\",\n  \"form_enter_mac\": \"輸入媒體存取控制（MAC）\",\n  \"form_enter_rate_limit\": \"輸入速率限制\",\n  \"form_enter_rate_limit_subnet_len\": \"輸入用於速率限制的子網路前綴長度\",\n  \"form_enter_subnet_ip\": \"在子網路 \\\"{{cidr}}\\\" 中輸入一組 IP 位址\",\n  \"form_enter_upstream_timeout\": \"輸入上游伺服器超時時間（以秒為單位）\",\n  \"form_error_answer_format\": \"無效的回應格式\",\n  \"form_error_client_id_format\": \"用戶端 ID 必須只包含數字、小寫字母和連字號\",\n  \"form_error_domain_format\": \"無效的網域格式\",\n  \"form_error_equal\": \"必須為不相等的\",\n  \"form_error_gateway_ip\": \"租約不能有閘道的 IP 位址\",\n  \"form_error_ip4_format\": \"無效的 IPv4 位址\",\n  \"form_error_ip4_gateway_format\": \"無效閘道的 IPv4 位址\",\n  \"form_error_ip6_format\": \"無效的 IPv6 位址\",\n  \"form_error_ip_format\": \"無效的 IP 位址\",\n  \"form_error_mac_format\": \"無效的媒體存取控制（MAC）位址\",\n  \"form_error_password\": \"不相符的密碼\",\n  \"form_error_password_length\": \"密碼長度必須為 {{min}} 到 {{max}} 個字符\",\n  \"form_error_port\": \"輸入有效的連接埠號碼\",\n  \"form_error_port_range\": \"輸入在 80-65535 之範圍內的連接埠號碼\",\n  \"form_error_port_unsafe\": \"不安全的連接埠\",\n  \"form_error_positive\": \"必須大於 0\",\n  \"form_error_required\": \"必填的欄位\",\n  \"form_error_server_name\": \"無效的伺服器名稱\",\n  \"form_error_subnet\": \"子網路 \\\"{{cidr}}\\\" 不包含該 IP 位址 \\\"{{ip}}\\\"\",\n  \"form_error_url_format\": \"無效的網址格式\",\n  \"form_error_url_or_path_format\": \"該清單之無效的網址或絕對的路徑\",\n  \"form_select_tags\": \"選擇用戶端標記\",\n  \"found_in_known_domain_db\": \"在已知的域名資料庫中被發現。\",\n  \"friday\": \"星期五\",\n  \"friday_short\": \"週五\",\n  \"gateway_or_subnet_invalid\": \"無效的子網路遮罩\",\n  \"general_settings\": \"一般設定\",\n  \"general_statistics\": \"一般的統計資料\",\n  \"get_started\": \"開始吧\",\n  \"greater_range_start_error\": \"必須大於起始範圍\",\n  \"homepage\": \"首頁\",\n  \"host_whitelisted\": \"該主機被允許\",\n  \"ignore_domains\": \"被忽略的網域（被換行分隔）\",\n  \"ignore_domains_desc_query\": \"符合這些規則的查詢不會被寫入查詢記錄中\",\n  \"ignore_domains_desc_stats\": \"符合這些規則的查詢不會被記錄在統計資料中\",\n  \"ignore_domains_title\": \"被忽略的網域\",\n  \"ignore_query_log\": \"在查詢記錄中忽略此用戶端\",\n  \"ignore_statistics\": \"在統計資料中忽略此用戶端\",\n  \"install_auth_confirm\": \"確認密碼\",\n  \"install_auth_desc\": \"往您的 AdGuard Home 管理員網路介面之密碼驗證必須被配置。即使 AdGuard Home 僅在您的區域網路中為可存取的，保護它免於不受限制的存取為仍然重要的。\",\n  \"install_auth_password\": \"密碼\",\n  \"install_auth_password_enter\": \"輸入密碼\",\n  \"install_auth_title\": \"驗證\",\n  \"install_auth_username\": \"使用者名稱\",\n  \"install_auth_username_enter\": \"輸入使用者名稱\",\n  \"install_devices_address\": \"AdGuard Home DNS 伺服器正在監聽下列的位址\",\n  \"install_devices_android_list_1\": \"從 Android 選單主畫面中，輕觸設定。\",\n  \"install_devices_android_list_2\": \"於該選單上輕觸 Wi-Fi。正在列出所有可用的網路之畫面將被顯示（不可能為行動連線設定自訂的 DNS）。\",\n  \"install_devices_android_list_3\": \"長按您所連線至的網路，然後輕觸修改網路。\",\n  \"install_devices_android_list_4\": \"於某些裝置上，您可能需要檢查關於進階的方框以查看進一步的設定。為了調整您的 Android DNS 設定，您將需要把 IP 設定從 DHCP 轉換成靜態。\",\n  \"install_devices_android_list_5\": \"更改 DNS 1 和 DNS 2 值為您的 AdGuard Home 伺服器位址。\",\n  \"install_devices_desc\": \"為了開始使用 AdGuard Home，您需要配置您的裝置以使用它。\",\n  \"install_devices_ios_list_1\": \"從主畫面中，輕觸設定。\",\n  \"install_devices_ios_list_2\": \"在左側的選單中選擇 Wi-Fi（不可能為行動網路配置 DNS）。\",\n  \"install_devices_ios_list_3\": \"向目前現行的網路之名稱輕觸。\",\n  \"install_devices_ios_list_4\": \"在該 DNS 欄位中，輸入您的 AdGuard Home 伺服器位址。\",\n  \"install_devices_macos_list_1\": \"點擊 Apple 圖像，然後去系統偏好設定。\",\n  \"install_devices_macos_list_2\": \"點擊網路。\",\n  \"install_devices_macos_list_3\": \"選擇在您的清單中之首要的連線，然後點擊進階的。\",\n  \"install_devices_macos_list_4\": \"選擇該 DNS 分頁，然後輸入您的 AdGuard Home 伺服器位址。\",\n  \"install_devices_router\": \"路由器\",\n  \"install_devices_router_desc\": \"此設置將自動地涵蓋所有被連線到您的家庭路由器之裝置，而您將無需手動地配置它們。\",\n  \"install_devices_router_list_1\": \"開啟用於您的路由器之偏好設定。通常，您可透過網址，諸如 http://192.168.0.1/ 或 http://192.168.1.1/，從您的瀏覽器中存取它。您可能被提醒去輸入密碼。如果您不記得它，您經常可透過按壓於該路由器本身上的按鈕來重置密碼，但請明白如果此步驟被選擇，您將可能失去整個路由器配置。如果您的路由器需要應用程式去設置它，請於您的手機或個人電腦上安裝該應用程式，並使用它來存取該路由器的設定。\",\n  \"install_devices_router_list_2\": \"找到 DHCP/DNS 設定。尋找緊鄰著允許兩組或三組數字集的欄位之 DNS 字母，每組被拆成四個含有一至三個數字的群集。\",\n  \"install_devices_router_list_3\": \"在那裡輸入您的 AdGuard Home 伺服器位址。\",\n  \"install_devices_router_list_4\": \"於某些路由器機型上，自訂的 DNS 伺服器無法被設置。在這種情況下，設置 AdGuard Home 作為 <0>DHCP</0> 伺服器可能有所幫助。否則，您應查明有關如何對您的特定路由器型號自訂 DNS 伺服器之路由器用法說明。\",\n  \"install_devices_title\": \"配置您的裝置\",\n  \"install_devices_windows_list_1\": \"通過開始功能表或 Windows 搜尋，開啟控制台。\",\n  \"install_devices_windows_list_2\": \"去網路和網際網路類別，然後去網路和共用中心。\",\n  \"install_devices_windows_list_3\": \"在左側面板中，點擊\\\"變更介面卡設定\\\"。\",\n  \"install_devices_windows_list_4\": \"向您現行的連線點擊滑鼠右鍵，然後選擇內容。\",\n  \"install_devices_windows_list_5\": \"在清單中找到\\\"網際網路通訊協定第 4 版（TCP/IPv4）\\\"[或用於 IPv6，\\\"網際網路通訊協定第 6 版（TCP/IPv6）\\\"]，選擇它，然後再次向內容點擊。\",\n  \"install_devices_windows_list_6\": \"選擇\\\"使用下列的 DNS 伺服器位址\\\"，然後輸入您的 AdGuard Home 伺服器位址。\",\n  \"install_saved\": \"被成功地儲存\",\n  \"install_settings_all_interfaces\": \"所有的介面\",\n  \"install_settings_dns\": \"DNS 伺服器\",\n  \"install_settings_dns_desc\": \"您將需要配置您的裝置或路由器以使用於下列的位址上之 DNS 伺服器：\",\n  \"install_settings_interface_link\": \"您的 AdGuard Home 管理員網路介面將於下列的位址上為可用的：\",\n  \"install_settings_listen\": \"監聽介面\",\n  \"install_settings_port\": \"連接埠\",\n  \"install_settings_title\": \"管理員網路介面\",\n  \"install_static_configure\": \"AdGuard Home 已偵測到動態 IP 位址 <0>{{ip}}</0> 被使用。您想要它被設定為您的靜態位址嗎？\",\n  \"install_static_error\": \"AdGuard Home 無法自動地配置它供此網路介面。請尋找有關如何手動地完成這個的用法說明。\",\n  \"install_static_ok\": \"好消息！該靜態 IP 位址已被配置\",\n  \"install_step\": \"步驟\",\n  \"install_submit_desc\": \"該設置程序被完成，且您準備好開始使用 AdGuard Home。\",\n  \"install_submit_title\": \"恭喜！\",\n  \"install_welcome_desc\": \"AdGuard Home 是全網路範圍廣告和追蹤器封鎖的 DNS 伺服器。它的目的為讓您控制您的整個網路和所有您的裝置，且不需要使用用戶端程式。\",\n  \"install_welcome_title\": \"歡迎至 AdGuard Home！\",\n  \"interval_24_hour\": \"24 小時\",\n  \"interval_6_hour\": \"6 小時\",\n  \"interval_days\": \"{{count}} 日\",\n  \"interval_days_plural\": \"{{count}} 日\",\n  \"interval_hours\": \"{{count}} 小時\",\n  \"interval_hours_plural\": \"{{count}} 小時\",\n  \"ip\": \"IP\",\n  \"ip_address\": \"IP 位址\",\n  \"known_tracker\": \"已知的追蹤器\",\n  \"last_rule_in_allowlist\": \"無法禁止此用戶端，因為排除規則 \\\"{{disallowed_rule}}\\\" 會停用「允許的用戶端」清單。\",\n  \"last_time_updated_table_header\": \"最近的更新時間\",\n  \"list_confirm_delete\": \"您確定您想要刪除該清單嗎？\",\n  \"list_label\": \"清單\",\n  \"list_updated\": \"{{count}} 清單被更新\",\n  \"list_updated_plural\": \"{{count}} 清單被更新\",\n  \"list_url_table_header\": \"清單網址\",\n  \"load_balancing\": \"負載平衡\",\n  \"load_balancing_desc\": \"一次查詢一台上游伺服器。<br/>AdGuard Home 使用加權隨機演算法來選擇具有最少失敗查詢和最低平均查詢時間的伺服器。\",\n  \"loading_table_status\": \"正在載入…\",\n  \"local_ptr_default_resolver\": \"預設下，AdGuard Home 使用以下反向的 DNS 解析器：{{ip}}。\",\n  \"local_ptr_desc\": \"AdGuard Home 使用的 DNS 伺服器用於私人 PTR、SOA 和 NS 請求。如果請求要求包含私人 IP 範圍內的子網域的 ARPA 網域（例如 \\\"192.168.12.34\\\"），並來自具有私人 IP 位址的用戶端，該請求被視為私人。如果未設定，將使用您的作業系統的預設 DNS 解析器，但不包括 AdGuard Home 的 IP 位址。\",\n  \"local_ptr_no_default_resolver\": \"AdGuard Home 無法為此系統決定合適的私人反向的 DNS 解析器。\",\n  \"local_ptr_placeholder\": \"每行輸入一個 IP 位址\",\n  \"local_ptr_title\": \"私人反向的 DNS 伺服器\",\n  \"location\": \"位置\",\n  \"log_and_stats_section_label\": \"查詢記錄和統計資料\",\n  \"lower_range_start_error\": \"必須低於起始範圍\",\n  \"main_settings\": \"主設定\",\n  \"make_static\": \"靜態化\",\n  \"manual_update\": \"請<a>遵循這些步驟</a>以手動地更新。\",\n  \"milliseconds_abbreviation\": \"ms\",\n  \"monday\": \"星期一\",\n  \"monday_short\": \"週一\",\n  \"name\": \"名稱\",\n  \"name_table_header\": \"名稱\",\n  \"netname\": \"網路名稱\",\n  \"network\": \"網路\",\n  \"new_allowlist\": \"新的允許清單\",\n  \"new_blocklist\": \"新的封鎖清單\",\n  \"next\": \"下一頁\",\n  \"next_btn\": \"下一頁\",\n  \"no_blocklist_added\": \"無已加入的封鎖清單\",\n  \"no_clients_found\": \"無已發現之用戶端\",\n  \"no_domains_found\": \"無已發現之網域\",\n  \"no_logs_found\": \"無已發現之記錄\",\n  \"no_servers_specified\": \"無已明確指定的伺服器\",\n  \"no_upstreams_data_found\": \"找不到上游伺服器資料\",\n  \"no_whitelist_added\": \"無已加入的允許清單\",\n  \"nothing_found\": \"沒找到什麼\",\n  \"null_ip\": \"無效的 IP\",\n  \"number_of_dns_query_blocked_24_hours\": \"被廣告封鎖過濾器和主機封鎖清單阻擋的 DNS 請求之數量\",\n  \"number_of_dns_query_blocked_24_hours_adult\": \"已封鎖的成人網站之數量\",\n  \"number_of_dns_query_blocked_24_hours_by_sec\": \"被 AdGuard 瀏覽安全模組封鎖的 DNS 請求之數量\",\n  \"number_of_dns_query_days\": \"在最近的 {{count}} 日內已處理的 DNS 查詢之數量\",\n  \"number_of_dns_query_days_plural\": \"在最近的 {{count}} 日內已處理的 DNS 查詢之數量\",\n  \"number_of_dns_query_hours\": \"過去 {{count}} 小時內處理的 DNS 查詢次數\",\n  \"number_of_dns_query_hours_plural\": \"過去 {{count}} 小時內處理的 DNS 查詢次數\",\n  \"number_of_dns_query_to_safe_search\": \"安全搜尋已被強制執行之屬於搜尋引擎的 DNS 請求之數量\",\n  \"nxdomain\": \"不存在的網域（NXDOMAIN）\",\n  \"off\": \"關著\",\n  \"on\": \"開著\",\n  \"open_dashboard\": \"開啟儀表板\",\n  \"orgname\": \"組織名稱\",\n  \"original_response\": \"原始的回應\",\n  \"out_of_range_error\": \"必須在\\\"{{start}}\\\"-\\\"{{end}}\\\"範圍之外\",\n  \"page_table_footer_text\": \"頁面\",\n  \"parallel_requests\": \"並行的請求\",\n  \"parental_control\": \"家長控制\",\n  \"password_label\": \"密碼\",\n  \"password_placeholder\": \"輸入密碼\",\n  \"plain_dns\": \"一般的 DNS\",\n  \"port_53_faq_link\": \"連接埠 53 常被 \\\"DNSStubListener\\\" 或 \\\"systemd-resolved\\\" 服務佔用。請閱讀有關如何解決這個的<0>用法說明</0>。\",\n  \"previous_btn\": \"上一頁\",\n  \"privacy_policy\": \"隱私政策\",\n  \"processing_update\": \"請稍候，AdGuard Home 正被更新\",\n  \"protection_section_label\": \"防護\",\n  \"protocol\": \"協定\",\n  \"punycode\": \"國際化域名代碼（Punycode）\",\n  \"query_log\": \"查詢記錄\",\n  \"query_log_clear\": \"清除查詢記錄\",\n  \"query_log_cleared\": \"該查詢記錄已被成功地清除\",\n  \"query_log_configuration\": \"記錄配置\",\n  \"query_log_confirm_clear\": \"您確定您想要清除整個查詢記錄嗎？\",\n  \"query_log_disabled\": \"查詢記錄功能已停用，請至「<0>設定</0>」調整\",\n  \"query_log_enable\": \"啟用記錄\",\n  \"query_log_filtered\": \"被 {{filter}} 過濾\",\n  \"query_log_response_status\": \"狀態：{{value}}\",\n  \"query_log_retention\": \"查詢記錄保留時間\",\n  \"query_log_retention_confirm\": \"您確定要更改記錄檔保存期限嗎？如果您縮短期限部分資料可能將會遺失\",\n  \"query_log_strict_search\": \"使用雙引號於嚴謹的搜尋\",\n  \"query_log_updated\": \"該查詢記錄已被成功地更新\",\n  \"rate_limit\": \"速率限制\",\n  \"rate_limit_desc\": \"每個用戶端被允許的每秒請求之數量。設定它為 0 表示無限制。\",\n  \"rate_limit_subnet_len_ipv4\": \"用於 IPv4 位址的子網路前綴長度\",\n  \"rate_limit_subnet_len_ipv4_desc\": \"用於速率限制的 IPv4 位址的子網路前綴長度。預設值為 24\",\n  \"rate_limit_subnet_len_ipv4_error\": \"IPv4 子網路前綴長度應在 0 至 32 之間\",\n  \"rate_limit_subnet_len_ipv6\": \"用於 IPv6 位址的子網路前綴長度\",\n  \"rate_limit_subnet_len_ipv6_desc\": \"用於速率限制的 IPv6 位址的子網路前綴長度。預設值為 56\",\n  \"rate_limit_subnet_len_ipv6_error\": \"IPv6 子網路前綴長度應在 0 至 128 之間\",\n  \"rate_limit_whitelist\": \"速率限制允許清單\",\n  \"rate_limit_whitelist_desc\": \"從速率限制被排除的 IP 位址\",\n  \"rate_limit_whitelist_placeholder\": \"每行輸入一個 IP 位址\",\n  \"refresh_btn\": \"重新整理\",\n  \"refresh_statics\": \"重新整理統計資料\",\n  \"refused\": \"已拒絕（REFUSED）\",\n  \"report_an_issue\": \"報告問題\",\n  \"request_details\": \"請求細節\",\n  \"request_table_header\": \"請求\",\n  \"requests_count\": \"請求總數\",\n  \"reset_settings\": \"重置設定\",\n  \"resolve_clients_desc\": \"透過傳送指標（PTR）查詢到對應的解析器（私人 DNS 伺服器供區域的用戶端，上游的伺服器供有公共 IP 位址的用戶端），反向地解析用戶端的 IP 位址變為它們的主機名稱。\",\n  \"resolve_clients_title\": \"啟用用戶端的 IP 位址之反向的解析\",\n  \"response_code\": \"回應碼\",\n  \"response_details\": \"回應細節\",\n  \"response_table_header\": \"回應\",\n  \"response_time\": \"回應時間\",\n  \"rewrite_A\": \"<0>A</0>：特殊的數值，阻止 <0>A</0> 記錄免於該上游\",\n  \"rewrite_AAAA\": \"<0>AAAA</0>：特殊的數值，阻止 <0>AAAA</0> 記錄免於該上游\",\n  \"rewrite_add\": \"新增 DNS 改寫\",\n  \"rewrite_added\": \"對於 \\\"{{key}}\\\" 之 DNS 改寫被成功地加入\",\n  \"rewrite_applied\": \"改寫規則被套用\",\n  \"rewrite_confirm_delete\": \"您確定您想要刪除對於 \\\"{{key}}\\\" 之 DNS 改寫嗎？\",\n  \"rewrite_deleted\": \"對於 \\\"{{key}}\\\" 之 DNS 改寫被成功地刪除\",\n  \"rewrite_desc\": \"允許輕易地配置自訂的 DNS 回應供特定的域名。\",\n  \"rewrite_domain_name\": \"域名：新增一筆正規名稱（CNAME）記錄\",\n  \"rewrite_edit\": \"編輯 DNS 重寫\",\n  \"rewrite_hosts_applied\": \"被該主機檔案規則改寫\",\n  \"rewrite_ip_address\": \"IP 位址：在一個 A 或 AAAA 回應中使用此 IP\",\n  \"rewrite_not_found\": \"無已發現之 DNS 改寫\",\n  \"rewrite_settings_updated\": \"DNS 重寫設定更新成功\",\n  \"rewrite_updated\": \"DNS 重寫已成功更新\",\n  \"rewrites_disabled_table_header\": \"重寫已停用\",\n  \"rewrites_enabled_table_header\": \"重寫已啟用\",\n  \"rewritten\": \"已改寫的\",\n  \"rows_table_footer_text\": \"列\",\n  \"rule_added_to_custom_filtering_toast\": \"被加至自訂的過濾規則中的規則：{{rule}}\",\n  \"rule_label\": \"規則\",\n  \"rule_removed_from_custom_filtering_toast\": \"從自訂的過濾規則中被移除的規則：{{rule}}\",\n  \"rules_count_table_header\": \"規則總數\",\n  \"safe_browsing\": \"安全瀏覽\",\n  \"safe_search\": \"安全搜尋\",\n  \"saturday\": \"星期六\",\n  \"saturday_short\": \"週六\",\n  \"save_btn\": \"儲存\",\n  \"save_config\": \"儲存配置\",\n  \"schedule_add\": \"新增排程\",\n  \"schedule_current_timezone\": \"當前時區：{{value}}\",\n  \"schedule_desc\": \"設定封鎖服務的無活動期間\",\n  \"schedule_edit\": \"編輯排程\",\n  \"schedule_from\": \"從\",\n  \"schedule_invalid_select\": \"開始時間必須早於結束時間\",\n  \"schedule_modal_description\": \"此排程將取代相同星期中的任何現有排程。每個星期的每一天只能有一個無活動時段。\",\n  \"schedule_modal_time_off\": \"無封鎖服務：\",\n  \"schedule_new\": \"新排程\",\n  \"schedule_remove\": \"刪除排程\",\n  \"schedule_save\": \"儲存排程\",\n  \"schedule_select_days\": \"選擇日期\",\n  \"schedule_services\": \"暫停封鎖服務\",\n  \"schedule_services_desc\": \"設定封鎖服務過濾器的暫停排程\",\n  \"schedule_services_desc_client\": \"為此用戶端設定封鎖服務過濾器的暫停排程\",\n  \"schedule_time_all_day\": \"全天\",\n  \"schedule_timezone\": \"選擇時區\",\n  \"schedule_to\": \"至\",\n  \"served_from_cache_label\": \"從快取中\",\n  \"service_name\": \"服務名稱\",\n  \"set_static_ip\": \"設定一組靜態 IP 位址\",\n  \"settings\": \"設定\",\n  \"settings_custom\": \"自訂的\",\n  \"settings_global\": \"全域的\",\n  \"setup_config_to_enable_dhcp_server\": \"設置配置以啟用 DHCP 伺服器\",\n  \"setup_dns_notice\": \"為了使用 <1>DNS-over-HTTPS</1> 或 <1>DNS-over-TLS</1>，您需要在 AdGuard Home 設定裡<0>配置加密</0>。\",\n  \"setup_dns_privacy_1\": \"<0>DNS-over-TLS：</0>使用 <1>{{address}}</1> 字串。\",\n  \"setup_dns_privacy_2\": \"<0>DNS-over-HTTPS：</0>使用 <1>{{address}}</1> 字串。\",\n  \"setup_dns_privacy_3\": \"<0>這裡是您可使用的軟體之清單。</0>\",\n  \"setup_dns_privacy_4\": \"於 iOS 14 或 macOS Big Sur 裝置上，您可下載新增 <highlight>DNS-over-HTTPS</highlight> 或 <highlight>DNS-over-TLS</highlight> 伺服器到 DNS 設定之特殊的 '.mobileconfig' 檔案。\",\n  \"setup_dns_privacy_android_1\": \"Android 9 原生地支援 DNS-over-TLS。為了配置它，去設定 → 網路 & 網際網路 → 進階 → 私人 DNS 並在那輸入您的域名。\",\n  \"setup_dns_privacy_android_2\": \"<0>AdGuard for Android</0> 支援 <1>DNS-over-HTTPS</1> 和 <1>DNS-over-TLS</1>。\",\n  \"setup_dns_privacy_android_3\": \"<0>Intra</0> 對 Android 新增 <1>DNS-over-HTTPS</1> 支援。\",\n  \"setup_dns_privacy_ioc_mac\": \"iOS 和 macOS 配置\",\n  \"setup_dns_privacy_ios_1\": \"<0>DNSCloak</0> 支援 <1>DNS-over-HTTPS</1>，但為了配置它以使用您自己的伺服器，您將需要為它產生一個 <2>DNS 戳記</2>。\",\n  \"setup_dns_privacy_ios_2\": \"<0>AdGuard for iOS</0> 支援 <1>DNS-over-HTTPS</1> 和 <1>DNS-over-TLS</1> 設置。\",\n  \"setup_dns_privacy_other_1\": \"於任何的平台上，AdGuard Home 它本身可以是安全的 DNS 用戶端。\",\n  \"setup_dns_privacy_other_2\": \"<0>dnsproxy</0> 支援所有已知安全的 DNS 協定。\",\n  \"setup_dns_privacy_other_3\": \"<0>dnscrypt-proxy</0> 支援 <1>DNS-over-HTTPS</1>。\",\n  \"setup_dns_privacy_other_4\": \"<0>Mozilla Firefox</0> 支援 <1>DNS-over-HTTPS</1>。\",\n  \"setup_dns_privacy_other_5\": \"在<0>這裡</0>和<1>這裡</1>，您將發現更多的執行。\",\n  \"setup_dns_privacy_other_title\": \"其它的執行\",\n  \"setup_guide\": \"設置指南\",\n  \"show_all_filter_type\": \"顯示全部\",\n  \"show_blocked_responses\": \"已封鎖的\",\n  \"show_filtered_type\": \"顯示受過濾的\",\n  \"show_processed_responses\": \"已處理的\",\n  \"show_whitelisted_responses\": \"被允許的\",\n  \"sign_in\": \"登入\",\n  \"sign_out\": \"登出\",\n  \"source_label\": \"來源\",\n  \"static_ip\": \"靜態 IP 位址\",\n  \"static_ip_desc\": \"AdGuard Home 是一台伺服器，因此它需要一組靜態 IP 位址以正確地運作。否則，在某些時候，您的路由器可能分配一組不同的 IP 位址給此裝置。\",\n  \"statistics_clear\": \" 清除統計資料\",\n  \"statistics_clear_confirm\": \"您確定您想要清除統計資料嗎？\",\n  \"statistics_cleared\": \"統計資料被成功地清除\",\n  \"statistics_configuration\": \"統計資料配置\",\n  \"statistics_enable\": \"啟用統計資料\",\n  \"statistics_retention\": \"統計資料保留\",\n  \"statistics_retention_confirm\": \"您確定您想要更改統計資料保留嗎？如果您減少該間隔值，某些資料將被丟失\",\n  \"statistics_retention_desc\": \"如果您減少該間隔值，某些資料將被丟失\",\n  \"stats_adult\": \"已封鎖的成人網站\",\n  \"stats_disabled\": \"統計功能目前停用中，請至<0>設定頁面</0>重新開啟。\",\n  \"stats_disabled_short\": \"該統計資料已停用\",\n  \"stats_malware_phishing\": \"已封鎖的惡意軟體/網路釣魚\",\n  \"stats_params\": \"統計資料配置\",\n  \"stats_query_domain\": \"熱門已查詢的網域\",\n  \"subnet_error\": \"位址必須在子網路中\",\n  \"sunday\": \"星期日\",\n  \"sunday_short\": \"週日\",\n  \"system_host_files\": \"系統主機檔案\",\n  \"table_client\": \"用戶端\",\n  \"table_name\": \"名稱\",\n  \"tags_desc\": \"您可選擇對應該用戶端的標記。包括在過濾規則中的標記以更準確地套用它們。<0>了解更多</0>。\",\n  \"tags_title\": \"標記\",\n  \"test_upstream_btn\": \"測試上行資料流\",\n  \"theme_auto\": \"自動\",\n  \"theme_auto_desc\": \"自動（基於裝置的配色方案）\",\n  \"theme_dark\": \"深色\",\n  \"theme_dark_desc\": \"深色主題\",\n  \"theme_light\": \"淺色\",\n  \"theme_light_desc\": \"淺色主題\",\n  \"thursday\": \"星期四\",\n  \"thursday_short\": \"週四\",\n  \"time_table_header\": \"時間\",\n  \"top_blocked_domains\": \"熱門已封鎖的網域\",\n  \"top_clients\": \"熱門用戶端\",\n  \"top_upstreams\": \"熱門上游\",\n  \"topline_expired_certificate\": \"您的安全通訊端層（SSL）憑證為已到期的。更新<0>加密設定</0>。\",\n  \"topline_expiring_certificate\": \"您的安全通訊端層（SSL）憑證即將到期。更新<0>加密設定</0>。\",\n  \"tracker_source\": \"追蹤器來源\",\n  \"try_again\": \"再次嘗試\",\n  \"ttl_cache_validation\": \"最小的快取存活時間（TTL）覆寫必須小於或等於最大的\",\n  \"tuesday\": \"星期二\",\n  \"tuesday_short\": \"週二\",\n  \"type_table_header\": \"類型\",\n  \"unavailable_dhcp\": \"DHCP 為不可用的\",\n  \"unavailable_dhcp_desc\": \"AdGuard Home 無法於您的作業系統上執行 DHCP 伺服器\",\n  \"unblock\": \"解除封鎖\",\n  \"unblock_all\": \"解除封鎖全部\",\n  \"unblock_for_this_client_only\": \"僅對此用戶端解除封鎖\",\n  \"unknown_filter\": \"未知的過濾器 {{filterId}}\",\n  \"update_announcement\": \"AdGuard Home {{version}} 現為可用的！關於更多的資訊，<0>點擊這裡</0>。\",\n  \"update_failed\": \"自動更新已失敗。請<a>遵循這些步驟</a>以手動地更新。\",\n  \"update_now\": \"立即更新\",\n  \"updated_custom_filtering_toast\": \"自訂的規則被成功地儲存\",\n  \"updated_save_search_toast\": \"安全搜尋設定更新成功\",\n  \"updated_upstream_dns_toast\": \"上游的伺服器被成功地儲存\",\n  \"updates_checked\": \"AdGuard Home 的新版本為可用的\",\n  \"updates_version_equal\": \"AdGuard Home 為最新的\",\n  \"upstream\": \"上游伺服器\",\n  \"upstream_dns\": \"上游的 DNS 伺服器\",\n  \"upstream_dns_cache_configuration\": \"上游 DNS 快取設定\",\n  \"upstream_dns_client_desc\": \"如果您將此欄位留空，AdGuard Home 將使用在 <0>DNS 設定</0>中被配置的伺服器。\",\n  \"upstream_dns_configured_in_file\": \"被配置在 {{path}}\",\n  \"upstream_dns_help\": \"每行輸入一個伺服器位址。<a>了解更多</a>有關配置上游的 DNS 伺服器。\",\n  \"upstream_parallel\": \"透過同時地查詢所有上游的伺服器，使用並行的查詢以加速解析。\",\n  \"upstream_timeout\": \"上游超時\",\n  \"upstream_timeout_desc\": \"指定等待來自此上游伺服器回應的秒數\",\n  \"upstreams\": \"上游\",\n  \"use_adguard_browsing_sec\": \"使用 AdGuard 瀏覽安全網路服務\",\n  \"use_adguard_browsing_sec_hint\": \"AdGuard Home 將檢查該網域是否被瀏覽安全網路服務封鎖。它將使用對隱私友好的查找應用程式介面（API）以執行檢查：僅域名 SHA256 雜湊的短前綴被傳送到該伺服器。\",\n  \"use_adguard_parental\": \"使用 AdGuard 家長控制之網路服務\",\n  \"use_adguard_parental_hint\": \"AdGuard Home 將檢查網域是否包含成人資料。它使用如同瀏覽安全網路服務一樣之對隱私友好的應用程式介面（API）。\",\n  \"use_private_ptr_resolvers_desc\": \"透過私有上游伺服器、DHCP 或 /etc/hosts 等管道，解析含有私有 IP 位址的 ARPA 網域的 PTR、SOA 與 NS 請求。若停用此功能，AdGuard Home 將以 NXDOMAIN 回應所有相關請求。\",\n  \"use_private_ptr_resolvers_title\": \"使用私人反向的 DNS 解析器\",\n  \"use_saved_key\": \"使用該先前已儲存的金鑰\",\n  \"username_label\": \"使用者名稱\",\n  \"username_placeholder\": \"輸入使用者名稱\",\n  \"validated_with_dnssec\": \"已用網域名稱系統安全性擴充功能（DNSSEC）驗證\",\n  \"version\": \"版本\",\n  \"version_request_error\": \"更新檢查已失敗。請檢查您的網際網路連線。\",\n  \"wednesday\": \"星期三\",\n  \"wednesday_short\": \"週三\",\n  \"whois\": \"WHOIS\"\n}\n"
  },
  {
    "path": "client/src/__locales-services/ar.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"الذكاء الاصطناعي\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"شبكات توصيل المحتوى (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"خدمات المواعدة\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"القمار والمراهنة\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"الألعاب الرقمية\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"استضافة الويب\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"خدمات المراسلة\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"أدوات الخصوصية\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"التسوق\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"شبكات التواصل الإجتماعية\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"تطوير البرمجيات\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"خدمات البث المباشر\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/be.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Штучны інтэлект\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Сеткі дастаўкі кантэнту (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Сэрвісы знаёмстваў\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Азартныя гульні і стаўкі\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Гэймінг\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Вэб-хостынг\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Сэрвісы паведамленняў\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Інструменты прыватнасці\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Крамы\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Сацыяльныя сеткі\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Распрацоўка праграмнага забеспячэння\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Стрымінгавыя сэрвісы\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/bg.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Изкуствен интелект\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Мрежи за доставяне на съдържание (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Служби за запознанства\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Хазарт и залагания\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Гейминг\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Уеб хостинг\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Услуги за съобщения\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Инструменти за поверителност\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Пазаруване\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Социални мрежи\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Разработка на софтуер\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Стрийминг услуги\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/cs.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Umělá inteligence\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Sítě pro doručování obsahu (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Seznamovací služby\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Hazardní hry a sázení\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Hraní\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Webhosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Služby pro zasílání zpráv\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Nástroje pro ochranu soukromí\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Nakupování\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Sociální sítě\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Vývoj softwaru\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streamovací služby\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/da.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Kunstig intelligens\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Indholdsleveringsnetværk (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Dating-tjenester\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Spil og væddemål\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Gaming\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Webhosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Beskedtjenester\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Fortrolighedsværktøjer\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Shopping\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Sociale netværk\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Softwareudvikling\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streamingtjenester\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/de.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Künstliche Intelligenz\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Content-Delivery-Netzwerke (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Dating-Dienste\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Glücksspiel und Wetten\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Spiele\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Webhosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Messaging-Dienste\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Datenschutz-Tools\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Einkaufen\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Soziale Netzwerke\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Softwareentwicklung\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streaming-Dienste\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/en.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Artificial intelligence\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Content delivery networks (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Dating services\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Gambling and betting\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Gaming\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Web hosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Messaging services\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Privacy tools\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Shopping\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Social networks\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Software development\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streaming services\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/es.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Inteligencia artificial\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Redes de entrega de contenido (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Servicios de citas\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Juegos de azar y apuestas\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Juegos\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Web hosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Servicios de mensajería\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Herramientas de privacidad\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Compras\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Redes sociales\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Desarrollo de software\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Servicios de streaming\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/fa.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"هوش مصنوعی\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"شبکه‌های تحویل محتوا (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"خدمات دوستیابی\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"قمار و شرط‌بندی\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"بازی\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"میزبانی وب\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"خدمات پیام رسان\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"ابزارهای حریم خصوصی\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"خرید\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"شبکه اجتماعی\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"توسعه نرمافزار\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"خدمات استریم\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/fi.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Tekoäly\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Sisällönjakeluverkot (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Deittipalvelut\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Uhkapelit ja vedonlyönti\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Pelaaminen\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Web-hosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Viestipalvelut\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Yksityisyyden työkalut\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Verkkokaupat\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Sosiaaliset verkostot\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Ohjelmistokehitys\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Suoratoistopalvelut\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/fr.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Intelligence artificielle\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Réseaux de distribution de contenu (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Services de rencontres\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Jeux de hasard et paris sportifs\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Jeux vidéo\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Hébergement Web\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Services de messagerie\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Outils de confidentialité\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Shopping\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Réseaux sociaux\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Développement de logiciels\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Services de streaming\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/hr.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Umjetna inteligencija\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Mreže za isporuku sadržaja (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Usluge za upoznavanje\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Kockanje i klađenje\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Igre\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Web hosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Usluge razmjene poruka\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Alati za privatnost\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Kupovina\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Društvene mreže\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Razvoj softwarea\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Usluge streaminga\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/hu.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Mesterséges intelligencia\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Tartalomszolgáltató hálózatok (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Társkereső szolgáltatások\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Szerencsejáték és fogadás\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Játék\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Webtárhely\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Üzenetküldő szolgáltatások\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Adatvédelmi eszközök\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Vásárlás\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Közösségi háló\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Szoftverfejlesztés\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streaming szolgáltatások\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/id.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Kecerdasan buatan\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Jaringan pengiriman konten (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Layanan kencan\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Perjudian dan taruhan\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Permainan\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Hosting web\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Layanan pesan\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Alat privasi\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Belanja\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Jaringan sosial\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Pengembangan software\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Layanan siar\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/it.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Intelligenza artificiale\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Reti dedicate alla distribuzione dei contenuti (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Servizi di incontri\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Giochi d'azzardo e scommesse\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Giochi\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Hosting web\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Servizi di messaggistica\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Strumenti per riservatezza\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Compere\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Reti sociali\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Sviluppo di programmi\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Servizi di streaming\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/ja.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"人工知能（AI）\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"コンテンツ配信ネットワーク（CDN）\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"出会い系サービス\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"ギャンブルおよび賭博\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"ゲーム\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"ウェブホスティング\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"メッセージサービス\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"プライバシーツール\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"ショッピング\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"SNS\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"ソフトウェア開発\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"ストリーミングサービス\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/ko.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"인공지능\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"콘텐츠 전송 네트워크(CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"데이트 서비스\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"도박 및 베팅\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"게임\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"웹 호스팅\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"메시징 서비스\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"개인정보 보호 도구\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"쇼핑\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"소셜 네트워크\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"소프트웨어 개발\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"스트리밍 서비스\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/nl.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Kunstmatige intelligentie\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Content Delivery Networks (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Datingdiensten\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Gokken en wedden\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Gamen\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Webhosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Berichtendiensten\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Privacyhulpmiddelen\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Winkelen\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Sociale netwerken\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Softwareontwikkeling\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streamingdiensten\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/no.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Kunstig intelligens\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Nettverk for innholdslevering (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Datingtjenester\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Gambling og tipping\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Spilling\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Webhotell\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Meldingstjenester\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Personvernverktøy\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Handling\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Sosiale nettverk\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Programvareutvikling\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Strømmetjenester\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/pl.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Sztuczna inteligencja\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Sieci dostarczania treści (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Serwisy randkowe\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Hazard i zakłady\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Gier\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Hosting web\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Usługi przesyłania wiadomości\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Narzędzia prywatności\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Zakupy\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Portale społecznościowe\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Rozwój oprogramowania\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Serwisy streamingowe\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/pt-br.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Inteligência artificial\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Redes de entrega de conteúdo (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Serviços de namoro\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Jogos de azar e apostas\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Jogos digitais\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Hospedagem web\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Serviços de mensagens\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Ferramentas de privacidade\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Compras\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Redes sociais\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Desenvolvimento de software\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Serviços de streaming\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/pt-pt.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Inteligência artificial\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Redes de distribuição de conteúdos (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Serviços de encontros\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Jogos de azar e apostas\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Jogos\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Alojamento web\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Serviços de mensagens\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Ferramentas de privacidade\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Compras\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Redes sociais\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Desenvolvimento de software\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Serviços de streaming\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/ro.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Inteligenţă artificială\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Rețele de livrare a conținutului (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Servicii de întâlniri\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Jocuri de noroc și pariuri\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Jocuri\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Găzduire web\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Servicii de mesagerie\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Instrumente de confidențialitate\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Cumpărături\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Rețele sociale\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Dezvoltare de program\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Servicii de streaming\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/ru.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Искусственный интеллект\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Сети доставки контента (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Сервисы знакомств\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Азартные игры и ставки\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Игры\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Веб-хостинг\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Сервисы обмена сообщениями\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Инструменты конфиденциальности\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Шопинг\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Социальные сети\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Разработка программного обеспечения\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Стриминговые сервисы\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/si-lk.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"කෘතිම බුද්ධිය\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"අන්තර්ගත බෙදාහැරීමේ ජාල (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"ආලය සේවා\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"සූදුව සහ ඔට්ටු ඇල්ලීම\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"ක්‍රීඩා කිරීම\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"වෙබ් සත්කාරකත්වය\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"පණිවිඩ සේවා\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"රහස්‍යතා මෙවලම්\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"සාප්පුයාම\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"සමාජ ජාල\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"මෘදුකාංග සංවර්ධනය\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"ප්‍රවාහ සේවා\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/sk.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Umelá inteligencia\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Siete na doručovanie obsahu (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Zoznamovacie služby\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Hazardné hry a stávkovanie\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Hranie hier\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Webhosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Služby zasielania správ\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Nástroje na ochranu súkromia\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Nakupovanie\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Sociálne siete\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Vývoj softvéru\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streamovacie služby\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/sl.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Umetna inteligenca\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Omrežja za dostavo vsebin (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Storitve za zmenke\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Igre na srečo in stave\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Igre\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Spletno gostovanje\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Storitve sporočanja\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Orodja za zasebnost\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Nakupi\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Družbeno omrežje\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Razvoj programske opreme\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Storitve pretakanja\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/sr-cs.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Veštačka inteligencija\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Mreže za isporuku sadržaja (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Servisi za upoznavanje\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Kockanje i klađenje\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Igre\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Veb hosting\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Servisi za razmenu poruka\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Alati za privatnost\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Kupovina\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Društvene mreže\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Razvoj softvera\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streaming servisi\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/sv.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Artificiell intelligens\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Innehållsleveransnätverk (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Dejtingtjänster\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Spel och vadslagning\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Gaming\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Webbhotell\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Meddelandetjänster\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Integritetsverktyg\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Shopping\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Sociala nätverk\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Programvaruutveckling\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Streamingtjänster\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/th.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"ปัญญาประดิษฐ์\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"เครือข่ายการจัดส่งเนื้อหา (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"บริการหาคู่\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"การพนันและการเดิมพัน\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"การเล่นเกม\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"เว็บโฮสติ้ง\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"บริการส่งข้อความ\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"เครื่องมือความเป็นส่วนตัว\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"ช้อปปิ้ง\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"สังคมออนไลน์\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"การพัฒนาซอฟต์แวร์\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"บริการสตรีมมิ่ง\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/tr.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Yapay zekâ\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"İçerik dağıtım ağları (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Flört hizmetleri\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Kumar ve bahis\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Oyun\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Web barındırma\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Mesajlaşma hizmetleri\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Gizlilik araçları\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Alışveriş\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Sosyal ağlar\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Yazılım geliştirme\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Canlı yayın akışı hizmetleri\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/uk.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Штучний інтелект\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Мережі доставки контенту (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Сервіси знайомств\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Азартні ігри та ставки\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Геймінг\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Вебхостинг\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Сервіси обміну повідомленнями\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Інструменти конфіденційності\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Магазини\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Соціальні мережі\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Розробка програмного забезпечення\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Стримінги\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/vi.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"Trí tuệ nhân tạo\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"Mạng phân phối nội dung (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"Dịch vụ hẹn hò\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"Cờ bạc và cá cược\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"Chơi game\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Lưu trữ web\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"Dịch vụ nhắn tin\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"Công cụ bảo mật\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"Mua sắm\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"Mạng xã hội\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"Phát triển phần mềm\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"Dịch vụ phát trực tuyến\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/zh-cn.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"人工智能\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"内容分发网络（CDN）\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"交友服务\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"赌博和博彩\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"游戏\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"网站托管\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"消息服务\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"隐私工具\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"购物\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"社交网络\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"软件开发\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"串流服务\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/zh-hk.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"人工智能\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"內容傳遞網路 (CDN)\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"約會服務\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"賭博和投注\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"遊戲\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"網頁寄存\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"訊息傳送服務\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"隱私權工具\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"購物\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"社交媒體\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"軟體開發\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"串流服務\"\n  }\n}\n"
  },
  {
    "path": "client/src/__locales-services/zh-tw.json",
    "content": "{\n  \"servicesgroup.ai.name\": {\n    \"message\": \"人工智慧\"\n  },\n  \"servicesgroup.cdn.name\": {\n    \"message\": \"內容傳遞網路（CDN）\"\n  },\n  \"servicesgroup.dating.name\": {\n    \"message\": \"約會服務\"\n  },\n  \"servicesgroup.gambling.name\": {\n    \"message\": \"博弈與賭博\"\n  },\n  \"servicesgroup.gaming.name\": {\n    \"message\": \"遊戲\"\n  },\n  \"servicesgroup.hosting.name\": {\n    \"message\": \"Web 主機服務\"\n  },\n  \"servicesgroup.messenger.name\": {\n    \"message\": \"訊息服務\"\n  },\n  \"servicesgroup.privacy.name\": {\n    \"message\": \"隱私工具\"\n  },\n  \"servicesgroup.shopping.name\": {\n    \"message\": \"購物\"\n  },\n  \"servicesgroup.social_network.name\": {\n    \"message\": \"社群網路\"\n  },\n  \"servicesgroup.software.name\": {\n    \"message\": \"軟體開發\"\n  },\n  \"servicesgroup.streaming.name\": {\n    \"message\": \"串流服務\"\n  }\n}\n"
  },
  {
    "path": "client/src/__tests__/helpers.test.ts",
    "content": "import { describe, expect, test, afterEach, vi, beforeEach, it } from 'vitest';\n\nimport { sortIp, countClientsStatistics, findAddressType, subnetMaskToBitMask } from '../helpers/helpers';\nimport { ADDRESS_TYPES } from '../helpers/constants';\n\ndescribe('sortIp', () => {\n    describe('ipv4', () => {\n        test('one octet differ', () => {\n            const arr = ['127.0.2.0', '127.0.3.0', '127.0.1.0'];\n            const sortedArr = ['127.0.1.0', '127.0.2.0', '127.0.3.0'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('few octets differ', () => {\n            const arr = [\n                '192.168.11.10',\n                '192.168.10.0',\n                '192.168.11.11',\n                '192.168.10.10',\n                '192.168.1.10',\n                '192.168.0.1',\n                '192.168.1.0',\n                '192.168.1.1',\n                '192.168.11.0',\n                '192.168.0.10',\n                '192.168.10.11',\n                '192.168.0.11',\n                '192.168.1.11',\n                '192.168.0.0',\n                '192.168.10.1',\n                '192.168.11.1',\n            ];\n            const sortedArr = [\n                '192.168.0.0',\n                '192.168.0.1',\n                '192.168.0.10',\n                '192.168.0.11',\n                '192.168.1.0',\n                '192.168.1.1',\n                '192.168.1.10',\n                '192.168.1.11',\n                '192.168.10.0',\n                '192.168.10.1',\n                '192.168.10.10',\n                '192.168.10.11',\n                '192.168.11.0',\n                '192.168.11.1',\n                '192.168.11.10',\n                '192.168.11.11',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n\n            // Example from issue https://github.com/AdguardTeam/AdGuardHome/issues/1778#issuecomment-640937599\n            const arr2 = [\n                '192.168.2.11',\n                '192.168.3.1',\n                '192.168.2.100',\n                '192.168.2.2',\n                '192.168.2.1',\n                '192.168.2.10',\n                '192.168.2.99',\n                '192.168.2.200',\n                '192.168.2.199',\n            ];\n            const sortedArr2 = [\n                '192.168.2.1',\n                '192.168.2.2',\n                '192.168.2.10',\n                '192.168.2.11',\n                '192.168.2.99',\n                '192.168.2.100',\n                '192.168.2.199',\n                '192.168.2.200',\n                '192.168.3.1',\n            ];\n\n            expect(arr2.sort(sortIp)).toStrictEqual(sortedArr2);\n        });\n    });\n\n    describe('ipv6', () => {\n        test('only long form', () => {\n            const arr = ['2001:db8:11a3:9d7:0:0:0:2', '2001:db8:11a3:9d7:0:0:0:3', '2001:db8:11a3:9d7:0:0:0:1'];\n            const sortedArr = ['2001:db8:11a3:9d7:0:0:0:1', '2001:db8:11a3:9d7:0:0:0:2', '2001:db8:11a3:9d7:0:0:0:3'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('only short form', () => {\n            const arr = ['2001:db8::', '2001:db7::', '2001:db9::'];\n            const sortedArr = ['2001:db7::', '2001:db8::', '2001:db9::'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('long and short forms', () => {\n            const arr = [\n                '2001:db8::',\n                '2001:db7:11a3:9d7:0:0:0:2',\n                '2001:db6:11a3:9d7:0:0:0:1',\n                '2001:db6::',\n                '2001:db7:11a3:9d7:0:0:0:1',\n                '2001:db7::',\n            ];\n            const sortedArr = [\n                '2001:db6::',\n                '2001:db6:11a3:9d7:0:0:0:1',\n                '2001:db7::',\n                '2001:db7:11a3:9d7:0:0:0:1',\n                '2001:db7:11a3:9d7:0:0:0:2',\n                '2001:db8::',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n    });\n\n    describe('ipv4 and ipv6', () => {\n        test('ipv6 long form', () => {\n            const arr = [\n                '127.0.0.3',\n                '2001:db8:11a3:9d7:0:0:0:1',\n                '2001:db8:11a3:9d7:0:0:0:3',\n                '127.0.0.1',\n                '2001:db8:11a3:9d7:0:0:0:2',\n                '127.0.0.2',\n            ];\n            const sortedArr = [\n                '127.0.0.1',\n                '127.0.0.2',\n                '127.0.0.3',\n                '2001:db8:11a3:9d7:0:0:0:1',\n                '2001:db8:11a3:9d7:0:0:0:2',\n                '2001:db8:11a3:9d7:0:0:0:3',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('ipv6 short form', () => {\n            const arr = [\n                '2001:db8:11a3:9d7::1',\n                '127.0.0.3',\n                '2001:db8:11a3:9d7::3',\n                '127.0.0.1',\n                '2001:db8:11a3:9d7::2',\n                '127.0.0.2',\n            ];\n            const sortedArr = [\n                '127.0.0.1',\n                '127.0.0.2',\n                '127.0.0.3',\n                '2001:db8:11a3:9d7::1',\n                '2001:db8:11a3:9d7::2',\n                '2001:db8:11a3:9d7::3',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('ipv6 long and short forms', () => {\n            const arr = [\n                '2001:db8:11a3:9d7::1',\n                '127.0.0.3',\n                '2001:db8:11a3:9d7:0:0:0:2',\n                '127.0.0.1',\n                '2001:db8:11a3:9d7::3',\n                '127.0.0.2',\n            ];\n            const sortedArr = [\n                '127.0.0.1',\n                '127.0.0.2',\n                '127.0.0.3',\n                '2001:db8:11a3:9d7::1',\n                '2001:db8:11a3:9d7:0:0:0:2',\n                '2001:db8:11a3:9d7::3',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('always put ipv4 before ipv6', () => {\n            const arr = [\n                '::1',\n                '0.0.0.2',\n                '127.0.0.1',\n                '::2',\n                '2001:db8:11a3:9d7:0:0:0:2',\n                '0.0.0.1',\n                '2001:db8:11a3:9d7::1',\n            ];\n            const sortedArr = [\n                '0.0.0.1',\n                '0.0.0.2',\n                '127.0.0.1',\n                '::1',\n                '::2',\n                '2001:db8:11a3:9d7::1',\n                '2001:db8:11a3:9d7:0:0:0:2',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n    });\n\n    describe('cidr', () => {\n        test('only ipv4 cidr', () => {\n            const arr = ['192.168.0.1/9', '192.168.0.1/7', '192.168.0.1/8'];\n            const sortedArr = ['192.168.0.1/7', '192.168.0.1/8', '192.168.0.1/9'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('ipv4 and cidr ipv4', () => {\n            const arr = ['192.168.0.1/9', '192.168.0.1', '192.168.0.1/32', '192.168.0.1/7', '192.168.0.1/8'];\n            const sortedArr = ['192.168.0.1/7', '192.168.0.1/8', '192.168.0.1/9', '192.168.0.1/32', '192.168.0.1'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('only ipv6 cidr', () => {\n            const arr = [\n                '2001:db8:11a3:9d7::1/32',\n                '2001:db8:11a3:9d7::1/64',\n                '2001:db8:11a3:9d7::1/128',\n                '2001:db8:11a3:9d7::1/24',\n            ];\n            const sortedArr = [\n                '2001:db8:11a3:9d7::1/24',\n                '2001:db8:11a3:9d7::1/32',\n                '2001:db8:11a3:9d7::1/64',\n                '2001:db8:11a3:9d7::1/128',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n\n        test('ipv6 and cidr ipv6', () => {\n            const arr = [\n                '2001:db8:11a3:9d7::1/32',\n                '2001:db8:11a3:9d7::1',\n                '2001:db8:11a3:9d7::1/64',\n                '2001:db8:11a3:9d7::1/128',\n                '2001:db8:11a3:9d7::1/24',\n            ];\n            const sortedArr = [\n                '2001:db8:11a3:9d7::1/24',\n                '2001:db8:11a3:9d7::1/32',\n                '2001:db8:11a3:9d7::1/64',\n                '2001:db8:11a3:9d7::1/128',\n                '2001:db8:11a3:9d7::1',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n    });\n\n    describe('invalid input', () => {\n        const originalWarn = console.warn;\n\n        beforeEach(() => {\n            console.warn = vi.fn();\n        });\n\n        afterEach(() => {\n            expect(console.warn).toHaveBeenCalled();\n            console.warn = originalWarn;\n        });\n\n        test('invalid strings', () => {\n            const arr = ['invalid ip', 'invalid cidr'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(arr);\n        });\n\n        test('invalid ip', () => {\n            const arr = ['127.0.0.2.', '.127.0.0.1.', '.2001:db8:11a3:9d7:0:0:0:0'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(arr);\n        });\n\n        test('invalid cidr', () => {\n            const arr = ['127.0.0.2/33', '2001:db8:11a3:9d7:0:0:0:0/129'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(arr);\n        });\n\n        test('valid and invalid ip', () => {\n            const arr = ['127.0.0.4.', '127.0.0.1', '.127.0.0.3', '127.0.0.2'];\n\n            expect(arr.sort(sortIp)).toStrictEqual(arr);\n        });\n    });\n\n    describe('mixed', () => {\n        test('ipv4, ipv6 in short and long forms and cidr', () => {\n            const arr = [\n                '2001:db8:11a3:9d7:0:0:0:1/32',\n                '192.168.1.2',\n                '127.0.0.2',\n                '2001:db8:11a3:9d7::1/128',\n                '2001:db8:11a3:9d7:0:0:0:1',\n                '127.0.0.1/12',\n                '192.168.1.1',\n                '2001:db8::/32',\n                '2001:db8:11a3:9d7::1/24',\n                '192.168.1.2/12',\n                '2001:db7::/32',\n                '127.0.0.1',\n                '2001:db8:11a3:9d7:0:0:0:2',\n                '192.168.1.1/24',\n                '2001:db7::/64',\n                '2001:db7::',\n                '2001:db8::',\n                '2001:db8:11a3:9d7:0:0:0:1/128',\n                '192.168.1.1/12',\n                '127.0.0.1/32',\n                '::1',\n            ];\n            const sortedArr = [\n                '127.0.0.1/12',\n                '127.0.0.1/32',\n                '127.0.0.1',\n                '127.0.0.2',\n                '192.168.1.1/12',\n                '192.168.1.1/24',\n                '192.168.1.1',\n                '192.168.1.2/12',\n                '192.168.1.2',\n                '::1',\n                '2001:db7::/32',\n                '2001:db7::/64',\n                '2001:db7::',\n                '2001:db8::/32',\n                '2001:db8::',\n                '2001:db8:11a3:9d7::1/24',\n                '2001:db8:11a3:9d7:0:0:0:1/32',\n                '2001:db8:11a3:9d7::1/128',\n                '2001:db8:11a3:9d7:0:0:0:1/128',\n                '2001:db8:11a3:9d7:0:0:0:1',\n                '2001:db8:11a3:9d7:0:0:0:2',\n            ];\n\n            expect(arr.sort(sortIp)).toStrictEqual(sortedArr);\n        });\n    });\n});\n\ndescribe('findAddressType', () => {\n    it('should return IP type for IP addresses', () => {\n        expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP);\n    });\n\n    it('should return CIDR type for CIDR addresses', () => {\n        expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR);\n    });\n\n    it('should return UNKNOWN type for MAC addresses', () => {\n        expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN);\n    });\n});\n\ndescribe('countClientsStatistics', () => {\n    test('single ip', () => {\n        expect(\n            countClientsStatistics(['127.0.0.1'], {\n                '127.0.0.1': 1,\n            }),\n        ).toStrictEqual(1);\n    });\n\n    test('multiple ip', () => {\n        expect(\n            countClientsStatistics(['127.0.0.1', '127.0.0.2'], {\n                '127.0.0.1': 1,\n                '127.0.0.2': 2,\n            }),\n        ).toStrictEqual(1 + 2);\n    });\n\n    test('cidr', () => {\n        expect(\n            countClientsStatistics(['127.0.0.0/8'], {\n                '127.0.0.1': 1,\n                '127.0.0.2': 2,\n            }),\n        ).toStrictEqual(1 + 2);\n    });\n\n    test('cidr and multiple ip', () => {\n        expect(\n            countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], {\n                '1.1.1.1': 1,\n                '2.2.2.2': 2,\n                '3.3.3.3': 3,\n            }),\n        ).toStrictEqual(1 + 2 + 3);\n    });\n\n    test('mac', () => {\n        expect(\n            countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], {\n                '1.1.1.1': 1,\n                '2.2.2.2': 2,\n                '3.3.3.3': 3,\n            }),\n        ).toStrictEqual(2 + 3);\n    });\n\n    test('not found', () => {\n        expect(\n            countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], {\n                '1.1.1.1': 1,\n                '2.2.2.2': 2,\n                '3.3.3.3': 3,\n            }),\n        ).toStrictEqual(0);\n    });\n});\n\ndescribe('subnetMaskToBitMask', () => {\n    const subnetMasks = [\n        '0.0.0.0',\n        '128.0.0.0',\n        '192.0.0.0',\n        '224.0.0.0',\n        '240.0.0.0',\n        '248.0.0.0',\n        '252.0.0.0',\n        '254.0.0.0',\n        '255.0.0.0',\n        '255.128.0.0',\n        '255.192.0.0',\n        '255.224.0.0',\n        '255.240.0.0',\n        '255.248.0.0',\n        '255.252.0.0',\n        '255.254.0.0',\n        '255.255.0.0',\n        '255.255.128.0',\n        '255.255.192.0',\n        '255.255.224.0',\n        '255.255.240.0',\n        '255.255.248.0',\n        '255.255.252.0',\n        '255.255.254.0',\n        '255.255.255.0',\n        '255.255.255.128',\n        '255.255.255.192',\n        '255.255.255.224',\n        '255.255.255.240',\n        '255.255.255.248',\n        '255.255.255.252',\n        '255.255.255.254',\n        '255.255.255.255',\n    ];\n\n    test('correct for all subnetMasks', () => {\n        expect(\n            subnetMasks\n                .map((subnetMask) => {\n                    const bitmask = subnetMaskToBitMask(subnetMask);\n                    return subnetMasks[bitmask] === subnetMask;\n                })\n                .every((res) => res === true),\n        ).toEqual(true);\n    });\n});\n"
  },
  {
    "path": "client/src/actions/access.ts",
    "content": "import { createAction } from 'redux-actions';\nimport i18next from 'i18next';\n\nimport apiClient from '../api/Api';\nimport { addErrorToast, addSuccessToast } from './toasts';\n\nimport { splitByNewLine } from '../helpers/helpers';\n\nexport const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST');\nexport const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE');\nexport const getAccessListSuccess = createAction('GET_ACCESS_LIST_SUCCESS');\n\nexport const getAccessList = () => async (dispatch: any) => {\n    dispatch(getAccessListRequest());\n    try {\n        const data = await apiClient.getAccessList();\n        dispatch(getAccessListSuccess(data));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getAccessListFailure());\n    }\n};\n\nexport const setAccessListRequest = createAction('SET_ACCESS_LIST_REQUEST');\nexport const setAccessListFailure = createAction('SET_ACCESS_LIST_FAILURE');\nexport const setAccessListSuccess = createAction('SET_ACCESS_LIST_SUCCESS');\n\nexport const setAccessList = (config: any) => async (dispatch: any) => {\n    dispatch(setAccessListRequest());\n    try {\n        const { allowed_clients, disallowed_clients, blocked_hosts } = config;\n\n        const values = {\n            allowed_clients: splitByNewLine(allowed_clients),\n            disallowed_clients: splitByNewLine(disallowed_clients),\n            blocked_hosts: splitByNewLine(blocked_hosts),\n        };\n\n        await apiClient.setAccessList(values);\n        dispatch(setAccessListSuccess());\n        dispatch(addSuccessToast('access_settings_saved'));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setAccessListFailure());\n    }\n};\n\nexport const toggleClientBlockRequest = createAction('TOGGLE_CLIENT_BLOCK_REQUEST');\nexport const toggleClientBlockFailure = createAction('TOGGLE_CLIENT_BLOCK_FAILURE');\nexport const toggleClientBlockSuccess = createAction('TOGGLE_CLIENT_BLOCK_SUCCESS');\n\ntype AccessList = {\n    allowed_clients?: string[];\n    disallowed_clients?: string[];\n    blocked_hosts?: string[];\n};\n\ntype AccessListValues = {\n    allowed_clients: string[];\n    disallowed_clients: string[];\n    blocked_hosts: string[];\n};\n\ntype GetNextClientAccessListArgs = {\n    accessList: AccessList;\n    ip: string;\n    disallowed: boolean;\n    disallowedRule: string;\n};\n\nconst addUnique = (items: string[], value: string) => (items.includes(value) ? items : items.concat(value));\n\nconst removeValue = (items: string[], value: string) => items.filter((item) => item !== value);\n\nconst getNextClientAccessList = ({\n    accessList,\n    ip,\n    disallowed,\n    disallowedRule,\n}: GetNextClientAccessListArgs): AccessListValues => {\n    const values = {\n        blocked_hosts: accessList.blocked_hosts ?? [],\n        allowed_clients: accessList.allowed_clients ?? [],\n        disallowed_clients: accessList.disallowed_clients ?? [],\n    };\n    const isAllowlistMode = values.allowed_clients.length > 0;\n\n    if (disallowed && isAllowlistMode) {\n        return {\n            ...values,\n            allowed_clients: addUnique(values.allowed_clients, ip),\n        };\n    }\n\n    if (disallowed) {\n        return {\n            ...values,\n            disallowed_clients: removeValue(values.disallowed_clients, disallowedRule || ip),\n        };\n    }\n\n    if (isAllowlistMode) {\n        return {\n            ...values,\n            allowed_clients: removeValue(values.allowed_clients, ip),\n        };\n    }\n\n    return {\n        ...values,\n        disallowed_clients: addUnique(values.disallowed_clients, ip),\n    };\n};\n\nexport const toggleClientBlock =\n    (ip: string, disallowed: boolean, disallowed_rule: string) => async (dispatch: any) => {\n        dispatch(toggleClientBlockRequest());\n        try {\n            const accessList: AccessList = await apiClient.getAccessList();\n            const values = getNextClientAccessList({\n                accessList,\n                ip,\n                disallowed,\n                disallowedRule: disallowed_rule,\n            });\n\n            await apiClient.setAccessList(values);\n            dispatch(toggleClientBlockSuccess(values));\n\n            if (disallowed) {\n                dispatch(addSuccessToast(i18next.t('client_unblocked', { ip: disallowed_rule || ip })));\n            } else {\n                dispatch(addSuccessToast(i18next.t('client_blocked', { ip })));\n            }\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(toggleClientBlockFailure());\n        }\n    };\n"
  },
  {
    "path": "client/src/actions/clients.ts",
    "content": "import { createAction } from 'redux-actions';\nimport i18next from 'i18next';\nimport apiClient from '../api/Api';\n\nimport { getClients } from './index';\nimport { addErrorToast, addSuccessToast } from './toasts';\n\nexport const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL');\n\nexport const addClientRequest = createAction('ADD_CLIENT_REQUEST');\nexport const addClientFailure = createAction('ADD_CLIENT_FAILURE');\nexport const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');\n\nexport const addClient = (config: any) => async (dispatch: any) => {\n    dispatch(addClientRequest());\n    try {\n        await apiClient.addClient(config);\n        dispatch(addClientSuccess());\n        dispatch(toggleClientModal());\n        dispatch(addSuccessToast(i18next.t('client_added', { key: config.name })));\n        dispatch(getClients());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(addClientFailure());\n    }\n};\n\nexport const deleteClientRequest = createAction('DELETE_CLIENT_REQUEST');\nexport const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE');\nexport const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS');\n\nexport const deleteClient = (config: any) => async (dispatch: any) => {\n    dispatch(deleteClientRequest());\n    try {\n        await apiClient.deleteClient(config);\n        dispatch(deleteClientSuccess());\n        dispatch(addSuccessToast(i18next.t('client_deleted', { key: config.name })));\n        dispatch(getClients());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(deleteClientFailure());\n    }\n};\n\nexport const updateClientRequest = createAction('UPDATE_CLIENT_REQUEST');\nexport const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE');\nexport const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');\n\nexport const updateClient = (config: any, name: any) => async (dispatch: any) => {\n    dispatch(updateClientRequest());\n    try {\n        const data = { name, data: { ...config } };\n\n        await apiClient.updateClient(data);\n        dispatch(updateClientSuccess());\n        dispatch(toggleClientModal());\n        dispatch(addSuccessToast(i18next.t('client_updated', { key: name })));\n        dispatch(getClients());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(updateClientFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/dnsConfig.ts",
    "content": "import { createAction } from 'redux-actions';\nimport i18next from 'i18next';\n\nimport apiClient from '../api/Api';\n\nimport { splitByNewLine } from '../helpers/helpers';\nimport { addErrorToast, addSuccessToast } from './toasts';\n\nexport const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST');\nexport const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE');\nexport const getDnsConfigSuccess = createAction('GET_DNS_CONFIG_SUCCESS');\n\nexport const getDnsConfig = () => async (dispatch: any) => {\n    dispatch(getDnsConfigRequest());\n    try {\n        const data = await apiClient.getDnsConfig();\n        dispatch(getDnsConfigSuccess(data));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getDnsConfigFailure());\n    }\n};\n\nexport const clearDnsCacheRequest = createAction('CLEAR_DNS_CACHE_REQUEST');\nexport const clearDnsCacheFailure = createAction('CLEAR_DNS_CACHE_FAILURE');\nexport const clearDnsCacheSuccess = createAction('CLEAR_DNS_CACHE_SUCCESS');\n\nexport const clearDnsCache = () => async (dispatch: any) => {\n    dispatch(clearDnsCacheRequest());\n    try {\n        const data = await apiClient.clearCache();\n        dispatch(clearDnsCacheSuccess(data));\n        dispatch(addSuccessToast(i18next.t('cache_cleared')));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(clearDnsCacheFailure());\n    }\n};\n\nexport const setDnsConfigRequest = createAction('SET_DNS_CONFIG_REQUEST');\nexport const setDnsConfigFailure = createAction('SET_DNS_CONFIG_FAILURE');\nexport const setDnsConfigSuccess = createAction('SET_DNS_CONFIG_SUCCESS');\n\nexport const setDnsConfig = (config: any) => async (dispatch: any) => {\n    dispatch(setDnsConfigRequest());\n    try {\n        const data = { ...config };\n\n        let hasDnsSettings = false;\n        if (Object.prototype.hasOwnProperty.call(data, 'bootstrap_dns')) {\n            data.bootstrap_dns = splitByNewLine(config.bootstrap_dns);\n            hasDnsSettings = true;\n        }\n        if (Object.prototype.hasOwnProperty.call(data, 'fallback_dns')) {\n            data.fallback_dns = splitByNewLine(config.fallback_dns);\n            hasDnsSettings = true;\n        }\n        if (Object.prototype.hasOwnProperty.call(data, 'local_ptr_upstreams')) {\n            data.local_ptr_upstreams = splitByNewLine(config.local_ptr_upstreams);\n            hasDnsSettings = true;\n        }\n        if (Object.prototype.hasOwnProperty.call(data, 'upstream_dns')) {\n            data.upstream_dns = splitByNewLine(config.upstream_dns);\n            hasDnsSettings = true;\n        }\n        if (Object.prototype.hasOwnProperty.call(data, 'ratelimit_whitelist')) {\n            data.ratelimit_whitelist = splitByNewLine(config.ratelimit_whitelist);\n            hasDnsSettings = true;\n        }\n\n        await apiClient.setDnsConfig(data);\n\n        if (hasDnsSettings) {\n            dispatch(addSuccessToast('updated_upstream_dns_toast'));\n        } else {\n            dispatch(addSuccessToast('config_successfully_saved'));\n        }\n\n        dispatch(setDnsConfigSuccess(config));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setDnsConfigFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/encryption.ts",
    "content": "import { createAction } from 'redux-actions';\nimport apiClient from '../api/Api';\n\nimport { redirectToCurrentProtocol } from '../helpers/helpers';\nimport { addErrorToast, addSuccessToast } from './toasts';\n\nexport const getTlsStatusRequest = createAction('GET_TLS_STATUS_REQUEST');\nexport const getTlsStatusFailure = createAction('GET_TLS_STATUS_FAILURE');\nexport const getTlsStatusSuccess = createAction('GET_TLS_STATUS_SUCCESS');\n\nexport const getTlsStatus = () => async (dispatch: any) => {\n    dispatch(getTlsStatusRequest());\n    try {\n        const status = await apiClient.getTlsStatus();\n        status.certificate_chain = atob(status.certificate_chain);\n        status.private_key = atob(status.private_key);\n\n        dispatch(getTlsStatusSuccess(status));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getTlsStatusFailure());\n    }\n};\n\nexport const setTlsConfigRequest = createAction('SET_TLS_CONFIG_REQUEST');\nexport const setTlsConfigFailure = createAction('SET_TLS_CONFIG_FAILURE');\nexport const setTlsConfigSuccess = createAction('SET_TLS_CONFIG_SUCCESS');\nexport const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');\n\nexport const setTlsConfig = (config: any) => async (dispatch: any, getState: any) => {\n    dispatch(setTlsConfigRequest());\n    try {\n        const { httpPort } = getState().dashboard;\n        const values = { ...config };\n        values.certificate_chain = btoa(values.certificate_chain);\n        values.private_key = btoa(values.private_key);\n        values.port_https = values.port_https || 0;\n        values.port_dns_over_tls = values.port_dns_over_tls || 0;\n        values.port_dns_over_quic = values.port_dns_over_quic || 0;\n\n        const response = await apiClient.setTlsConfig(values);\n        response.certificate_chain = atob(response.certificate_chain);\n        response.private_key = atob(response.private_key);\n\n        if (values.enabled && values.force_https && window.location.protocol === 'http:') {\n            window.location.reload();\n            return;\n        }\n        redirectToCurrentProtocol(response, httpPort);\n\n        const dnsStatus = await apiClient.getGlobalStatus();\n        if (dnsStatus) {\n            if (dnsStatus.protection_disabled_duration === 0) {\n                dnsStatus.protection_disabled_duration = null;\n            }\n            dispatch(dnsStatusSuccess(dnsStatus));\n        }\n\n        dispatch(setTlsConfigSuccess(response));\n        dispatch(addSuccessToast('encryption_config_saved'));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setTlsConfigFailure());\n    }\n};\n\nexport const validateTlsConfigRequest = createAction('VALIDATE_TLS_CONFIG_REQUEST');\nexport const validateTlsConfigFailure = createAction('VALIDATE_TLS_CONFIG_FAILURE');\nexport const validateTlsConfigSuccess = createAction('VALIDATE_TLS_CONFIG_SUCCESS');\n\nexport const validateTlsConfig = (config: any) => async (dispatch: any) => {\n    dispatch(validateTlsConfigRequest());\n    try {\n        const values = { ...config };\n        values.certificate_chain = btoa(values.certificate_chain);\n        values.private_key = btoa(values.private_key);\n        values.port_https = values.port_https || 0;\n        values.port_dns_over_tls = values.port_dns_over_tls || 0;\n        values.port_dns_over_quic = values.port_dns_over_quic || 0;\n\n        const response = await apiClient.validateTlsConfig(values);\n        response.certificate_chain = atob(response.certificate_chain);\n        response.private_key = atob(response.private_key);\n        dispatch(validateTlsConfigSuccess(response));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(validateTlsConfigFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/filtering.ts",
    "content": "import { createAction } from 'redux-actions';\nimport { showLoading, hideLoading } from 'react-redux-loading-bar';\nimport i18next from 'i18next';\n\nimport { normalizeFilteringStatus, normalizeRulesTextarea } from '../helpers/helpers';\nimport apiClient from '../api/Api';\nimport { addErrorToast, addSuccessToast } from './toasts';\n\nexport const toggleFilteringModal = createAction('FILTERING_MODAL_TOGGLE');\nexport const handleRulesChange = createAction('HANDLE_RULES_CHANGE');\n\nexport const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQUEST');\nexport const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE');\nexport const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS');\n\nexport const getFilteringStatus = () => async (dispatch: any) => {\n    dispatch(getFilteringStatusRequest());\n    try {\n        const status = await apiClient.getFilteringStatus();\n        dispatch(getFilteringStatusSuccess({ ...normalizeFilteringStatus(status) }));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getFilteringStatusFailure());\n    }\n};\n\nexport const setRulesRequest = createAction('SET_RULES_REQUEST');\nexport const setRulesFailure = createAction('SET_RULES_FAILURE');\nexport const setRulesSuccess = createAction('SET_RULES_SUCCESS');\n\nexport const setRules = (rules: any) => async (dispatch: any) => {\n    dispatch(setRulesRequest());\n    try {\n        const normalizedRules = {\n            rules: normalizeRulesTextarea(rules)?.split('\\n'),\n        };\n        await apiClient.setRules(normalizedRules);\n        dispatch(addSuccessToast('updated_custom_filtering_toast'));\n        dispatch(setRulesSuccess());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setRulesFailure());\n    }\n};\n\nexport const addFilterRequest = createAction('ADD_FILTER_REQUEST');\nexport const addFilterFailure = createAction('ADD_FILTER_FAILURE');\nexport const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');\n\nexport const addFilter =\n    (url: any, name: any, whitelist = false) =>\n    async (dispatch: any, getState: any) => {\n        dispatch(addFilterRequest());\n        try {\n            await apiClient.addFilter({ url, name, whitelist });\n            dispatch(addFilterSuccess(url));\n            if (getState().filtering.isModalOpen) {\n                dispatch(toggleFilteringModal());\n            }\n            dispatch(addSuccessToast('filter_added_successfully'));\n            dispatch(getFilteringStatus());\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(addFilterFailure());\n        }\n    };\n\nexport const removeFilterRequest = createAction('REMOVE_FILTER_REQUEST');\nexport const removeFilterFailure = createAction('REMOVE_FILTER_FAILURE');\nexport const removeFilterSuccess = createAction('REMOVE_FILTER_SUCCESS');\n\nexport const removeFilter =\n    (url: any, whitelist = false) =>\n    async (dispatch: any, getState: any) => {\n        dispatch(removeFilterRequest());\n        try {\n            await apiClient.removeFilter({ url, whitelist });\n            dispatch(removeFilterSuccess(url));\n            if (getState().filtering.isModalOpen) {\n                dispatch(toggleFilteringModal());\n            }\n            dispatch(addSuccessToast('filter_removed_successfully'));\n            dispatch(getFilteringStatus());\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(removeFilterFailure());\n        }\n    };\n\nexport const toggleFilterRequest = createAction('FILTER_TOGGLE_REQUEST');\nexport const toggleFilterFailure = createAction('FILTER_TOGGLE_FAILURE');\nexport const toggleFilterSuccess = createAction('FILTER_TOGGLE_SUCCESS');\n\nexport const toggleFilterStatus =\n    (url: any, data: any, whitelist = false) =>\n    async (dispatch: any) => {\n        dispatch(toggleFilterRequest());\n        try {\n            await apiClient.setFilterUrl({ url, data, whitelist });\n            dispatch(toggleFilterSuccess(url));\n            dispatch(getFilteringStatus());\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(toggleFilterFailure());\n        }\n    };\n\nexport const editFilterRequest = createAction('EDIT_FILTER_REQUEST');\nexport const editFilterFailure = createAction('EDIT_FILTER_FAILURE');\nexport const editFilterSuccess = createAction('EDIT_FILTER_SUCCESS');\n\nexport const editFilter =\n    (url: any, data: any, whitelist = false) =>\n    async (dispatch: any, getState: any) => {\n        dispatch(editFilterRequest());\n        try {\n            await apiClient.setFilterUrl({ url, data, whitelist });\n            dispatch(editFilterSuccess(url));\n            if (getState().filtering.isModalOpen) {\n                dispatch(toggleFilteringModal());\n            }\n            dispatch(addSuccessToast('filter_updated'));\n            dispatch(getFilteringStatus());\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(editFilterFailure());\n        }\n    };\n\nexport const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST');\nexport const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE');\nexport const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');\n\nexport const refreshFilters = (config: any) => async (dispatch: any) => {\n    dispatch(refreshFiltersRequest());\n    dispatch(showLoading());\n    try {\n        const data = await apiClient.refreshFilters(config);\n        const { updated } = data;\n        dispatch(refreshFiltersSuccess());\n\n        if (updated > 0) {\n            dispatch(addSuccessToast(i18next.t('list_updated', { count: updated })));\n        } else {\n            dispatch(addSuccessToast('all_lists_up_to_date_toast'));\n        }\n\n        dispatch(getFilteringStatus());\n        dispatch(hideLoading());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(refreshFiltersFailure());\n        dispatch(hideLoading());\n    }\n};\n\nexport const setFiltersConfigRequest = createAction('SET_FILTERS_CONFIG_REQUEST');\nexport const setFiltersConfigFailure = createAction('SET_FILTERS_CONFIG_FAILURE');\nexport const setFiltersConfigSuccess = createAction('SET_FILTERS_CONFIG_SUCCESS');\n\nexport const setFiltersConfig = (config: any) => async (dispatch: any, getState: any) => {\n    dispatch(setFiltersConfigRequest());\n    try {\n        const { enabled } = config;\n        const prevEnabled = getState().filtering.enabled;\n        let successToastMessage = 'config_successfully_saved';\n\n        if (prevEnabled !== enabled) {\n            successToastMessage = enabled ? 'enabled_filtering_toast' : 'disabled_filtering_toast';\n        }\n\n        await apiClient.setFiltersConfig(config);\n        dispatch(addSuccessToast(successToastMessage));\n        dispatch(setFiltersConfigSuccess(config));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setFiltersConfigFailure());\n    }\n};\n\nexport const checkHostRequest = createAction('CHECK_HOST_REQUEST');\nexport const checkHostFailure = createAction('CHECK_HOST_FAILURE');\nexport const checkHostSuccess = createAction('CHECK_HOST_SUCCESS');\n\n/**\n *\n * @param {object} host\n * @param {string} host.name\n * @returns {undefined}\n */\nexport const checkHost = (host: any) => async (dispatch: any) => {\n    dispatch(checkHostRequest());\n    try {\n        const data = await apiClient.checkHost(host);\n        const { name: hostname } = host;\n\n        dispatch(\n            checkHostSuccess({\n                hostname,\n                ...data,\n            }),\n        );\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(checkHostFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/index.tsx",
    "content": "import { createAction } from 'redux-actions';\nimport i18next from 'i18next';\nimport axios from 'axios';\n\nimport endsWith from 'lodash/endsWith';\nimport escapeRegExp from 'lodash/escapeRegExp';\nimport React from 'react';\nimport { compose } from 'redux';\nimport {\n    splitByNewLine,\n    sortClients,\n    filterOutComments,\n    msToSeconds,\n    msToMinutes,\n    msToHours,\n} from '../helpers/helpers';\nimport {\n    BLOCK_ACTIONS,\n    CHECK_TIMEOUT,\n    STATUS_RESPONSE,\n    SETTINGS_NAMES,\n    MANUAL_UPDATE_LINK,\n    DISABLE_PROTECTION_TIMINGS,\n} from '../helpers/constants';\nimport { areEqualVersions } from '../helpers/version';\nimport { getTlsStatus } from './encryption';\nimport apiClient from '../api/Api';\nimport { addErrorToast, addNoticeToast, addSuccessToast } from './toasts';\nimport { getFilteringStatus, setRules } from './filtering';\n\nexport const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');\nexport const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');\n\n/**\n *\n * @param {*} settingKey = SETTINGS_NAMES\n * @param {*} status: boolean | SafeSearchConfig\n * @returns\n */\nexport const toggleSetting = (settingKey: any, status: any) => async (dispatch: any) => {\n    let successMessage = '';\n    try {\n        switch (settingKey) {\n            case SETTINGS_NAMES.safebrowsing:\n                if (status) {\n                    successMessage = 'disabled_safe_browsing_toast';\n                    await apiClient.disableSafebrowsing();\n                } else {\n                    successMessage = 'enabled_safe_browsing_toast';\n                    await apiClient.enableSafebrowsing();\n                }\n                dispatch(toggleSettingStatus({ settingKey }));\n                break;\n            case SETTINGS_NAMES.parental:\n                if (status) {\n                    successMessage = 'disabled_parental_toast';\n                    await apiClient.disableParentalControl();\n                } else {\n                    successMessage = 'enabled_parental_toast';\n                    await apiClient.enableParentalControl();\n                }\n                dispatch(toggleSettingStatus({ settingKey }));\n                break;\n            case SETTINGS_NAMES.safesearch:\n                successMessage = 'updated_save_search_toast';\n                await apiClient.updateSafesearch(status);\n                dispatch(toggleSettingStatus({ settingKey, value: status }));\n                break;\n            default:\n                break;\n        }\n        dispatch(addSuccessToast(successMessage));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n    }\n};\n\nexport const initSettingsRequest = createAction('SETTINGS_INIT_REQUEST');\nexport const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE');\nexport const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');\n\nexport const initSettings =\n    (\n        settingsList = {\n            safebrowsing: {},\n            parental: {},\n        },\n    ) =>\n    async (dispatch: any) => {\n        dispatch(initSettingsRequest());\n        try {\n            const safebrowsingStatus = await apiClient.getSafebrowsingStatus();\n            const parentalStatus = await apiClient.getParentalStatus();\n            const safesearchStatus = await apiClient.getSafesearchStatus();\n            const { safebrowsing, parental } = settingsList;\n            const newSettingsList = {\n                safebrowsing: {\n                    ...safebrowsing,\n                    enabled: safebrowsingStatus.enabled,\n                },\n                parental: {\n                    ...parental,\n                    enabled: parentalStatus.enabled,\n                },\n                safesearch: {\n                    ...safesearchStatus,\n                },\n            };\n            dispatch(initSettingsSuccess({ settingsList: newSettingsList }));\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(initSettingsFailure());\n        }\n    };\n\nexport const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');\nexport const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');\nexport const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');\n\nconst getDisabledMessage = (time: any) => {\n    switch (time) {\n        case DISABLE_PROTECTION_TIMINGS.HALF_MINUTE:\n            return i18next.t('disable_notify_for_seconds', {\n                count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE),\n            });\n        case DISABLE_PROTECTION_TIMINGS.MINUTE:\n            return i18next.t('disable_notify_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) });\n        case DISABLE_PROTECTION_TIMINGS.TEN_MINUTES:\n            return i18next.t('disable_notify_for_minutes', {\n                count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES),\n            });\n        case DISABLE_PROTECTION_TIMINGS.HOUR:\n            return i18next.t('disable_notify_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) });\n        case DISABLE_PROTECTION_TIMINGS.TOMORROW:\n            return i18next.t('disable_notify_until_tomorrow');\n        default:\n            return 'disabled_protection';\n    }\n};\n\nexport const toggleProtection =\n    (status: any, time = null) =>\n    async (dispatch: any) => {\n        dispatch(toggleProtectionRequest());\n        try {\n            const successMessage = status ? getDisabledMessage(time) : 'enabled_protection';\n            await apiClient.setProtection({ enabled: !status, duration: time });\n            dispatch(addSuccessToast(successMessage));\n            dispatch(toggleProtectionSuccess({ disabledDuration: time }));\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(toggleProtectionFailure());\n        }\n    };\n\nexport const setDisableDurationTime = createAction('SET_DISABLED_DURATION_TIME');\n\nexport const setProtectionTimerTime = (updatedTime: any) => async (dispatch: any) => {\n    dispatch(setDisableDurationTime({ timeToEnableProtection: updatedTime }));\n};\n\nexport const getVersionRequest = createAction('GET_VERSION_REQUEST');\nexport const getVersionFailure = createAction('GET_VERSION_FAILURE');\nexport const getVersionSuccess = createAction('GET_VERSION_SUCCESS');\n\nexport const getVersion =\n    (recheck = false) =>\n    async (dispatch: any, getState: any) => {\n        dispatch(getVersionRequest());\n        try {\n            const data = await apiClient.getGlobalVersion({ recheck_now: recheck });\n            dispatch(getVersionSuccess(data));\n\n            if (recheck) {\n                const { dnsVersion } = getState().dashboard;\n                const currentVersion = dnsVersion === 'undefined' ? 0 : dnsVersion;\n\n                if (data && !areEqualVersions(currentVersion, data.new_version)) {\n                    dispatch(addSuccessToast('updates_checked'));\n                } else {\n                    dispatch(addSuccessToast('updates_version_equal'));\n                }\n            }\n        } catch (error) {\n            dispatch(addErrorToast({ error: 'version_request_error' }));\n            dispatch(getVersionFailure());\n        }\n    };\n\nexport const getUpdateRequest = createAction('GET_UPDATE_REQUEST');\nexport const getUpdateFailure = createAction('GET_UPDATE_FAILURE');\nexport const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS');\n\nconst checkStatus = async (handleRequestSuccess: any, handleRequestError: any, attempts = 60) => {\n    let timeout;\n\n    if (attempts === 0) {\n        handleRequestError();\n    }\n\n    const rmTimeout = (t: any) => t && clearTimeout(t);\n\n    try {\n        const response = await axios.get(`${apiClient.baseUrl}/status`);\n        rmTimeout(timeout);\n        if (response?.status === 200) {\n            handleRequestSuccess(response);\n            if (response.data.running === false) {\n                timeout = setTimeout(\n                    checkStatus,\n                    CHECK_TIMEOUT,\n                    handleRequestSuccess,\n                    handleRequestError,\n                    attempts - 1,\n                );\n            }\n        }\n    } catch (error) {\n        rmTimeout(timeout);\n        timeout = setTimeout(checkStatus, CHECK_TIMEOUT, handleRequestSuccess, handleRequestError, attempts - 1);\n    }\n};\n\nexport const getUpdate = () => async (dispatch: any, getState: any) => {\n    const { dnsVersion, dnsStartTime } = getState().dashboard;\n\n    dispatch(getUpdateRequest());\n    const handleRequestError = () => {\n        const options = {\n            components: {\n                a: <a href={MANUAL_UPDATE_LINK} target=\"_blank\" rel=\"noopener noreferrer\" />,\n            },\n        };\n\n        dispatch(addNoticeToast({ error: 'update_failed', options }));\n        dispatch(getUpdateFailure());\n    };\n\n    const handleRequestSuccess = (response: any) => {\n        const responseVersion = response.data?.version;\n        const responseStartTime = response.data?.start_time;\n\n        if (dnsVersion !== responseVersion || dnsStartTime !== responseStartTime) {\n            dispatch(getUpdateSuccess());\n\n            window.location.reload();\n        }\n    };\n\n    try {\n        await apiClient.getUpdate();\n        checkStatus(handleRequestSuccess, handleRequestError);\n    } catch (error) {\n        handleRequestError();\n    }\n};\n\nexport const getClientsRequest = createAction('GET_CLIENTS_REQUEST');\nexport const getClientsFailure = createAction('GET_CLIENTS_FAILURE');\nexport const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');\n\nexport const getClients = () => async (dispatch: any) => {\n    dispatch(getClientsRequest());\n    try {\n        const data = await apiClient.getClients();\n        const sortedClients = data.clients && sortClients(data.clients);\n        const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients);\n\n        dispatch(\n            getClientsSuccess({\n                clients: sortedClients || [],\n                autoClients: sortedAutoClients || [],\n                supportedTags: data.supported_tags || [],\n            }),\n        );\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getClientsFailure());\n    }\n};\n\nexport const getProfileRequest = createAction('GET_PROFILE_REQUEST');\nexport const getProfileFailure = createAction('GET_PROFILE_FAILURE');\nexport const getProfileSuccess = createAction('GET_PROFILE_SUCCESS');\n\nexport const getProfile = () => async (dispatch: any) => {\n    dispatch(getProfileRequest());\n    try {\n        const profile = await apiClient.getProfile();\n        dispatch(getProfileSuccess(profile));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getProfileFailure());\n    }\n};\n\nexport const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');\nexport const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');\nexport const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');\nexport const setDnsRunningStatus = createAction('SET_DNS_RUNNING_STATUS');\n\nexport const getDnsStatus = () => async (dispatch: any) => {\n    dispatch(dnsStatusRequest());\n\n    const handleRequestError = () => {\n        dispatch(addErrorToast({ error: 'dns_status_error' }));\n        dispatch(dnsStatusFailure());\n\n        window.location.reload();\n    };\n\n    const handleRequestSuccess = (response: any) => {\n        const dnsStatus = response.data;\n        if (dnsStatus.protection_disabled_duration === 0) {\n            dnsStatus.protection_disabled_duration = null;\n        }\n        const { running } = dnsStatus;\n        const runningStatus = dnsStatus && running;\n        if (runningStatus === true) {\n            dispatch(dnsStatusSuccess(dnsStatus));\n            dispatch(getVersion());\n            dispatch(getTlsStatus());\n            dispatch(getProfile());\n        } else {\n            dispatch(setDnsRunningStatus(running));\n        }\n    };\n\n    try {\n        checkStatus(handleRequestSuccess, handleRequestError);\n    } catch (error) {\n        handleRequestError();\n    }\n};\n\nexport const timerStatusRequest = createAction('TIMER_STATUS_REQUEST');\nexport const timerStatusFailure = createAction('TIMER_STATUS_FAILURE');\nexport const timerStatusSuccess = createAction('TIMER_STATUS_SUCCESS');\n\nexport const getTimerStatus = () => async (dispatch: any) => {\n    dispatch(timerStatusRequest());\n\n    const handleRequestError = () => {\n        dispatch(addErrorToast({ error: 'dns_status_error' }));\n        dispatch(dnsStatusFailure());\n\n        window.location.reload();\n    };\n\n    const handleRequestSuccess = (response: any) => {\n        const dnsStatus = response.data;\n        if (dnsStatus.protection_disabled_duration === 0) {\n            dnsStatus.protection_disabled_duration = null;\n        }\n        const { running } = dnsStatus;\n        const runningStatus = dnsStatus && running;\n        if (runningStatus === true) {\n            dispatch(timerStatusSuccess(dnsStatus));\n        } else {\n            dispatch(setDnsRunningStatus(running));\n        }\n    };\n\n    try {\n        checkStatus(handleRequestSuccess, handleRequestError);\n    } catch (error) {\n        handleRequestError();\n    }\n};\n\nexport const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');\nexport const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');\nexport const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');\n\nexport const testUpstream =\n    ({ bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns }: any, upstream_dns_file: any) =>\n    async (dispatch: any) => {\n        dispatch(testUpstreamRequest());\n        try {\n            const removeComments = compose(filterOutComments, splitByNewLine);\n\n            const config = {\n                bootstrap_dns: splitByNewLine(bootstrap_dns),\n                private_upstream: splitByNewLine(local_ptr_upstreams),\n                fallback_dns: splitByNewLine(fallback_dns),\n                ...(upstream_dns_file\n                    ? null\n                    : {\n                          upstream_dns: removeComments(upstream_dns),\n                      }),\n            };\n\n            const upstreamResponse = await apiClient.testUpstream(config);\n            const testMessages = Object.keys(upstreamResponse).map((key) => {\n                const message = upstreamResponse[key];\n                if (message.startsWith('WARNING:')) {\n                    dispatch(addErrorToast({ error: i18next.t('dns_test_warning_toast', { key }) }));\n                } else if (message.endsWith(': parsing error')) {\n                    const info = message.substring(0, message.indexOf(':'));\n                    const [sectionKey, line] = info.split(' ');\n                    const section = i18next.t(sectionKey);\n                    dispatch(\n                        addErrorToast({\n                            error: i18next.t('dns_test_parsing_error_toast', {\n                                section,\n                                line,\n                            }),\n                        }),\n                    );\n                } else if (message !== 'OK') {\n                    dispatch(addErrorToast({ error: i18next.t('dns_test_not_ok_toast', { key }) }));\n                }\n                return message;\n            });\n\n            if (testMessages.every((message) => message === 'OK' || message.startsWith('WARNING:'))) {\n                dispatch(addSuccessToast('dns_test_ok_toast'));\n            }\n\n            dispatch(testUpstreamSuccess());\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(testUpstreamFailure());\n        }\n    };\n\nexport const testUpstreamWithFormValues = (formValues: any) => async (dispatch: any, getState: any) => {\n    const { upstream_dns_file } = getState().dnsConfig;\n    const { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } = formValues;\n\n    return dispatch(\n        testUpstream(\n            {\n                bootstrap_dns,\n                upstream_dns,\n                local_ptr_upstreams,\n                fallback_dns,\n            },\n            upstream_dns_file,\n        ),\n    );\n};\n\nexport const changeLanguageRequest = createAction('CHANGE_LANGUAGE_REQUEST');\nexport const changeLanguageFailure = createAction('CHANGE_LANGUAGE_FAILURE');\nexport const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS');\n\nexport const changeLanguage = (lang: any) => async (dispatch: any) => {\n    dispatch(changeLanguageRequest());\n    try {\n        await apiClient.changeLanguage({ language: lang });\n        dispatch(changeLanguageSuccess());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(changeLanguageFailure());\n    }\n};\n\nexport const changeThemeRequest = createAction('CHANGE_THEME_REQUEST');\nexport const changeThemeFailure = createAction('CHANGE_THEME_FAILURE');\nexport const changeThemeSuccess = createAction('CHANGE_THEME_SUCCESS');\n\nexport const changeTheme = (theme: any) => async (dispatch: any) => {\n    dispatch(changeThemeRequest());\n    try {\n        await apiClient.changeTheme({ theme });\n        dispatch(changeThemeSuccess({ theme }));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(changeThemeFailure());\n    }\n};\n\nexport const getDhcpStatusRequest = createAction('GET_DHCP_STATUS_REQUEST');\nexport const getDhcpStatusSuccess = createAction('GET_DHCP_STATUS_SUCCESS');\nexport const getDhcpStatusFailure = createAction('GET_DHCP_STATUS_FAILURE');\n\nexport const getDhcpStatus = () => async (dispatch: any) => {\n    dispatch(getDhcpStatusRequest());\n    try {\n        const globalStatus = await apiClient.getGlobalStatus();\n        if (globalStatus.dhcp_available) {\n            const status = await apiClient.getDhcpStatus();\n            status.dhcp_available = globalStatus.dhcp_available;\n            dispatch(getDhcpStatusSuccess(status));\n        } else {\n            dispatch(getDhcpStatusFailure());\n        }\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getDhcpStatusFailure());\n    }\n};\n\nexport const getDhcpInterfacesRequest = createAction('GET_DHCP_INTERFACES_REQUEST');\nexport const getDhcpInterfacesSuccess = createAction('GET_DHCP_INTERFACES_SUCCESS');\nexport const getDhcpInterfacesFailure = createAction('GET_DHCP_INTERFACES_FAILURE');\n\nexport const getDhcpInterfaces = () => async (dispatch: any) => {\n    dispatch(getDhcpInterfacesRequest());\n    try {\n        const interfaces = await apiClient.getDhcpInterfaces();\n        dispatch(getDhcpInterfacesSuccess(interfaces));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getDhcpInterfacesFailure());\n    }\n};\n\nexport const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST');\nexport const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS');\nexport const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');\n\nexport const findActiveDhcp = (selectedInterface: any) => async (dispatch: any, getState: any) => {\n    dispatch(findActiveDhcpRequest());\n    try {\n        const req = {\n            interface: selectedInterface,\n        };\n        const activeDhcp = await apiClient.findActiveDhcp(req);\n        dispatch(findActiveDhcpSuccess(activeDhcp));\n        const { check, interface_name, interfaces } = getState().dhcp;\n        const v4 = check?.v4 ?? { static_ip: {}, other_server: {} };\n        const v6 = check?.v6 ?? { other_server: {} };\n\n        let isError = false;\n        let isStaticIPError = false;\n\n        const hasV4Interface = !!interfaces[selectedInterface]?.ipv4_addresses;\n        const hasV6Interface = !!interfaces[selectedInterface]?.ipv6_addresses;\n\n        if (hasV4Interface && v4.other_server.found === STATUS_RESPONSE.ERROR) {\n            isError = true;\n            if (v4.other_server.error) {\n                dispatch(addErrorToast({ error: v4.other_server.error }));\n            }\n        }\n\n        if (hasV6Interface && v6.other_server.found === STATUS_RESPONSE.ERROR) {\n            isError = true;\n            if (v6.other_server.error) {\n                dispatch(addErrorToast({ error: v6.other_server.error }));\n            }\n        }\n\n        if (hasV4Interface && v4.static_ip.static === STATUS_RESPONSE.ERROR) {\n            isStaticIPError = true;\n            dispatch(addErrorToast({ error: 'dhcp_static_ip_error' }));\n        }\n\n        if (isError) {\n            dispatch(addErrorToast({ error: 'dhcp_error' }));\n        }\n\n        if (isStaticIPError || isError) {\n            // No need to proceed if there was an error discovering DHCP server\n            return;\n        }\n\n        if (\n            (hasV4Interface && v4.other_server.found === STATUS_RESPONSE.YES) ||\n            (hasV6Interface && v6.other_server.found === STATUS_RESPONSE.YES)\n        ) {\n            dispatch(addErrorToast({ error: 'dhcp_found' }));\n        } else if (hasV4Interface && v4.static_ip.static === STATUS_RESPONSE.NO && v4.static_ip.ip && interface_name) {\n            const warning = i18next.t('dhcp_dynamic_ip_found', {\n                interfaceName: interface_name,\n                ipAddress: v4.static_ip.ip,\n                interpolation: {\n                    prefix: '<0>{{',\n                    suffix: '}}</0>',\n                },\n            });\n            dispatch(addErrorToast({ error: warning }));\n        } else {\n            dispatch(addSuccessToast('dhcp_not_found'));\n        }\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(findActiveDhcpFailure());\n    }\n};\n\nexport const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST');\nexport const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');\nexport const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');\n\nexport const setDhcpConfig = (values: any) => async (dispatch: any) => {\n    dispatch(setDhcpConfigRequest());\n    try {\n        await apiClient.setDhcpConfig(values);\n        dispatch(setDhcpConfigSuccess(values));\n        dispatch(addSuccessToast('dhcp_config_saved'));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setDhcpConfigFailure());\n    }\n};\n\nexport const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');\nexport const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE');\nexport const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS');\n\nexport const toggleDhcp = (values: any) => async (dispatch: any) => {\n    dispatch(toggleDhcpRequest());\n    let config = {\n        ...values,\n        enabled: false,\n    };\n    let successMessage = 'disabled_dhcp';\n\n    if (!values.enabled) {\n        config = {\n            ...values,\n            enabled: true,\n        };\n        successMessage = 'enabled_dhcp';\n    }\n\n    try {\n        await apiClient.setDhcpConfig(config);\n        dispatch(toggleDhcpSuccess());\n        dispatch(addSuccessToast(successMessage));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(toggleDhcpFailure());\n    }\n};\n\nexport const resetDhcpRequest = createAction('RESET_DHCP_REQUEST');\nexport const resetDhcpSuccess = createAction('RESET_DHCP_SUCCESS');\nexport const resetDhcpFailure = createAction('RESET_DHCP_FAILURE');\n\nexport const resetDhcp = () => async (dispatch: any) => {\n    dispatch(resetDhcpRequest());\n    try {\n        const status = await apiClient.resetDhcp();\n        dispatch(resetDhcpSuccess(status));\n        dispatch(addSuccessToast('dhcp_config_saved'));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(resetDhcpFailure());\n    }\n};\n\nexport const resetDhcpLeasesRequest = createAction('RESET_DHCP_LEASES_REQUEST');\nexport const resetDhcpLeasesSuccess = createAction('RESET_DHCP_LEASES_SUCCESS');\nexport const resetDhcpLeasesFailure = createAction('RESET_DHCP_LEASES_FAILURE');\n\nexport const resetDhcpLeases = () => async (dispatch: any) => {\n    dispatch(resetDhcpLeasesRequest());\n    try {\n        const status = await apiClient.resetDhcpLeases();\n        dispatch(resetDhcpLeasesSuccess(status));\n        dispatch(addSuccessToast('dhcp_reset_leases_success'));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(resetDhcpLeasesFailure());\n    }\n};\n\nexport const toggleLeaseModal = createAction('TOGGLE_LEASE_MODAL');\n\nexport const addStaticLeaseRequest = createAction('ADD_STATIC_LEASE_REQUEST');\nexport const addStaticLeaseFailure = createAction('ADD_STATIC_LEASE_FAILURE');\nexport const addStaticLeaseSuccess = createAction('ADD_STATIC_LEASE_SUCCESS');\n\nexport const addStaticLease = (config: any) => async (dispatch: any) => {\n    dispatch(addStaticLeaseRequest());\n    try {\n        const name = config.hostname || config.ip;\n        await apiClient.addStaticLease(config);\n        dispatch(addStaticLeaseSuccess(config));\n        dispatch(addSuccessToast(i18next.t('dhcp_lease_added', { key: name })));\n        dispatch(toggleLeaseModal());\n        dispatch(getDhcpStatus());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(addStaticLeaseFailure());\n    }\n};\n\nexport const removeStaticLeaseRequest = createAction('REMOVE_STATIC_LEASE_REQUEST');\nexport const removeStaticLeaseFailure = createAction('REMOVE_STATIC_LEASE_FAILURE');\nexport const removeStaticLeaseSuccess = createAction('REMOVE_STATIC_LEASE_SUCCESS');\n\nexport const removeStaticLease = (config: any) => async (dispatch: any) => {\n    dispatch(removeStaticLeaseRequest());\n    try {\n        const name = config.hostname || config.ip;\n        await apiClient.removeStaticLease(config);\n        dispatch(removeStaticLeaseSuccess(config));\n        dispatch(addSuccessToast(i18next.t('dhcp_lease_deleted', { key: name })));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(removeStaticLeaseFailure());\n    }\n};\n\nexport const updateStaticLeaseRequest = createAction('UPDATE_STATIC_LEASE_REQUEST');\nexport const updateStaticLeaseFailure = createAction('UPDATE_STATIC_LEASE_FAILURE');\nexport const updateStaticLeaseSuccess = createAction('UPDATE_STATIC_LEASE_SUCCESS');\n\nexport const updateStaticLease = (config: any) => async (dispatch: any) => {\n    dispatch(updateStaticLeaseRequest());\n    try {\n        await apiClient.updateStaticLease(config);\n        dispatch(updateStaticLeaseSuccess(config));\n        dispatch(addSuccessToast(i18next.t('dhcp_lease_updated', { key: config.hostname || config.ip })));\n        dispatch(toggleLeaseModal());\n        dispatch(getDhcpStatus());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(updateStaticLeaseFailure());\n    }\n};\n\nexport const removeToast = createAction('REMOVE_TOAST');\n\nexport const toggleBlocking =\n    (type: any, domain: any, baseRule?: string, baseUnblocking?: string) => async (dispatch: any, getState: any) => {\n        const baseBlockingRule = baseRule || `||${domain}^$important`;\n        const baseUnblockingRule = baseUnblocking || `@@${baseBlockingRule}`;\n        const { userRules } = getState().filtering;\n\n        const lineEnding = !endsWith(userRules, '\\n') ? '\\n' : '';\n\n        const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblockingRule : baseBlockingRule;\n        const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule;\n        const preparedBlockingRule = new RegExp(`(^|\\n)${escapeRegExp(blockingRule)}($|\\n)`);\n        const preparedUnblockingRule = new RegExp(`(^|\\n)${escapeRegExp(unblockingRule)}($|\\n)`);\n\n        const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);\n        const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);\n\n        if (matchPreparedBlockingRule) {\n            await dispatch(setRules(userRules.replace(`${blockingRule}`, '')));\n            dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));\n        } else if (!matchPreparedUnblockingRule) {\n            await dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\\n`));\n            dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));\n        } else if (matchPreparedUnblockingRule) {\n            dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));\n            return;\n        } else if (!matchPreparedBlockingRule) {\n            dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));\n            return;\n        }\n\n        dispatch(getFilteringStatus());\n    };\n\nexport const toggleBlockingForClient = (type: any, domain: any, client: any) => {\n    const escapedClientName = client\n        .replace(/'/g, \"\\\\'\")\n        .replace(/\"/g, '\\\\\"')\n        .replace(/,/g, '\\\\,')\n        .replace(/\\|/g, '\\\\|');\n    const baseRule = `||${domain}^$client='${escapedClientName}'`;\n    const baseUnblocking = `@@${baseRule}`;\n\n    return toggleBlocking(type, domain, baseRule, baseUnblocking);\n};\n"
  },
  {
    "path": "client/src/actions/install.ts",
    "content": "import { createAction } from 'redux-actions';\nimport apiClient from '../api/Api';\nimport { addErrorToast, addSuccessToast } from './toasts';\n\nexport const nextStep = createAction('NEXT_STEP');\nexport const prevStep = createAction('PREV_STEP');\n\nexport const getDefaultAddressesRequest = createAction('GET_DEFAULT_ADDRESSES_REQUEST');\nexport const getDefaultAddressesFailure = createAction('GET_DEFAULT_ADDRESSES_FAILURE');\nexport const getDefaultAddressesSuccess = createAction('GET_DEFAULT_ADDRESSES_SUCCESS');\n\nexport const getDefaultAddresses = () => async (dispatch: any) => {\n    dispatch(getDefaultAddressesRequest());\n    try {\n        const addresses = await apiClient.getDefaultAddresses();\n        dispatch(getDefaultAddressesSuccess(addresses));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getDefaultAddressesFailure());\n    }\n};\n\nexport const setAllSettingsRequest = createAction('SET_ALL_SETTINGS_REQUEST');\nexport const setAllSettingsFailure = createAction('SET_ALL_SETTINGS_FAILURE');\nexport const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS');\n\nexport const setAllSettings = (values: any) => async (dispatch: any) => {\n    dispatch(setAllSettingsRequest());\n    try {\n        const config = { ...values };\n        delete config.confirm_password;\n\n        await apiClient.setAllSettings(config);\n        dispatch(setAllSettingsSuccess());\n        dispatch(addSuccessToast('install_saved'));\n        dispatch(nextStep());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setAllSettingsFailure());\n        dispatch(prevStep());\n    }\n};\n\nexport const checkConfigRequest = createAction('CHECK_CONFIG_REQUEST');\nexport const checkConfigFailure = createAction('CHECK_CONFIG_FAILURE');\nexport const checkConfigSuccess = createAction('CHECK_CONFIG_SUCCESS');\n\nexport const checkConfig = (values: any) => async (dispatch: any) => {\n    dispatch(checkConfigRequest());\n    try {\n        const check = await apiClient.checkConfig(values);\n        dispatch(checkConfigSuccess({\n            web: { ...values.web, ...check.web },\n            dns: { ...values.dns, ...check.dns },\n            static_ip: check.static_ip,\n        }));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(checkConfigFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/login.ts",
    "content": "import { createAction } from 'redux-actions';\n\nimport apiClient from '../api/Api';\nimport { addErrorToast } from './toasts';\nimport { HTML_PAGES } from '../helpers/constants';\n\nexport const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST');\nexport const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE');\nexport const processLoginSuccess = createAction('PROCESS_LOGIN_SUCCESS');\n\nexport const processLogin = (values: any) => async (dispatch: any) => {\n    dispatch(processLoginRequest());\n    try {\n        await apiClient.login(values);\n        const dashboardUrl =\n            window.location.origin + window.location.pathname.replace(HTML_PAGES.LOGIN, HTML_PAGES.MAIN);\n        window.location.replace(dashboardUrl);\n        dispatch(processLoginSuccess());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(processLoginFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/queryLogs.ts",
    "content": "import { createAction } from 'redux-actions';\n\nimport apiClient from '../api/Api';\n\nimport { normalizeLogs } from '../helpers/helpers';\nimport { DEFAULT_LOGS_FILTER, QUERY_LOGS_PAGE_LIMIT } from '../helpers/constants';\nimport { addErrorToast, addSuccessToast } from './toasts';\nimport { SearchFormValues } from '../components/Logs';\n\nconst getLogsWithParams = async (config: any) => {\n    const { older_than, filter, ...values } = config;\n    const rawLogs = await apiClient.getQueryLog({\n        ...filter,\n        older_than,\n    });\n    const { data, oldest } = rawLogs;\n\n    return {\n        logs: normalizeLogs(data),\n        oldest,\n        older_than,\n        filter,\n        ...values,\n    };\n};\n\nexport const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUEST');\nexport const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');\nexport const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');\n\nconst shortPollQueryLogs = async (data: any, filter: any, dispatch: any, currentQuery?: string, total?: any) => {\n    const { logs, oldest } = data;\n    const totalData = total || { logs };\n\n    const previousQuery = filter?.search;\n    const isQueryTheSame =\n        typeof previousQuery === 'string' && typeof currentQuery === 'string' && previousQuery === currentQuery;\n\n    const isShortPollingNeeded =\n        (logs.length < QUERY_LOGS_PAGE_LIMIT || totalData.logs.length < QUERY_LOGS_PAGE_LIMIT) &&\n        oldest !== '' &&\n        isQueryTheSame;\n\n    if (isShortPollingNeeded) {\n        dispatch(getAdditionalLogsRequest());\n\n        try {\n            const additionalLogs = await getLogsWithParams({\n                older_than: oldest,\n                filter,\n            });\n            if (additionalLogs.oldest.length > 0) {\n                return await shortPollQueryLogs(additionalLogs, filter, dispatch, currentQuery, {\n                    logs: [...totalData.logs, ...additionalLogs.logs],\n                    oldest: additionalLogs.oldest,\n                });\n            }\n            dispatch(getAdditionalLogsSuccess());\n            return totalData;\n        } catch (error) {\n            dispatch(addErrorToast({ error }));\n            dispatch(getAdditionalLogsFailure(error));\n        }\n    }\n\n    dispatch(getAdditionalLogsSuccess());\n    return totalData;\n};\n\nexport const toggleDetailedLogs = createAction('TOGGLE_DETAILED_LOGS');\n\nexport const getLogsRequest = createAction('GET_LOGS_REQUEST');\nexport const getLogsFailure = createAction('GET_LOGS_FAILURE');\nexport const getLogsSuccess = createAction('GET_LOGS_SUCCESS');\n\nexport const updateLogs = () => async (dispatch: any, getState: any) => {\n    try {\n        const { logs, oldest, older_than } = getState().queryLogs;\n\n        dispatch(\n            getLogsSuccess({\n                logs,\n                oldest,\n                older_than,\n            }),\n        );\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getLogsFailure(error));\n    }\n};\n\nexport const getLogs = (currentQuery?: string) => async (dispatch: any, getState: any) => {\n    dispatch(getLogsRequest());\n    try {\n        const { isFiltered, filter, oldest } = getState().queryLogs;\n\n        const data = await getLogsWithParams({\n            older_than: oldest,\n            filter,\n        });\n\n        if (isFiltered) {\n            const additionalData = await shortPollQueryLogs(data, filter, dispatch, currentQuery);\n            const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;\n            dispatch(getLogsSuccess(updatedData));\n        } else {\n            dispatch(getLogsSuccess(data));\n        }\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getLogsFailure(error));\n    }\n};\n\nexport const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');\n\n/**\n *\n * @param filter\n * @param {string} filter.search\n * @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object\n * @returns function\n */\nexport const setLogsFilter = (filter: SearchFormValues) => setLogsFilterRequest(filter);\n\nexport const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST');\nexport const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');\nexport const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');\n\nexport const setFilteredLogs = (filter?: SearchFormValues) => async (dispatch: any) => {\n    dispatch(setFilteredLogsRequest());\n    try {\n        const data = await getLogsWithParams({\n            older_than: '',\n            filter,\n        });\n\n        const currentQuery = filter?.search;\n\n        const additionalData = await shortPollQueryLogs(data, filter, dispatch, currentQuery);\n        const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;\n\n        dispatch(\n            setFilteredLogsSuccess({\n                ...updatedData,\n                filter,\n            }),\n        );\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setFilteredLogsFailure(error));\n    }\n};\n\nexport const resetFilteredLogs = () => setFilteredLogs(DEFAULT_LOGS_FILTER);\n\nexport const refreshFilteredLogs = () => async (dispatch: any, getState: any) => {\n    const { filter } = getState().queryLogs;\n    await dispatch(setFilteredLogs(filter));\n};\n\nexport const clearLogsRequest = createAction('CLEAR_LOGS_REQUEST');\nexport const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE');\nexport const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS');\n\nexport const clearLogs = () => async (dispatch: any) => {\n    dispatch(clearLogsRequest());\n    try {\n        await apiClient.clearQueryLog();\n        dispatch(clearLogsSuccess());\n        dispatch(addSuccessToast('query_log_cleared'));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(clearLogsFailure(error));\n    }\n};\n\nexport const getLogsConfigRequest = createAction('GET_LOGS_CONFIG_REQUEST');\nexport const getLogsConfigFailure = createAction('GET_LOGS_CONFIG_FAILURE');\nexport const getLogsConfigSuccess = createAction('GET_LOGS_CONFIG_SUCCESS');\n\nexport const getLogsConfig = () => async (dispatch: any) => {\n    dispatch(getLogsConfigRequest());\n    try {\n        const data = await apiClient.getQueryLogConfig();\n        dispatch(getLogsConfigSuccess(data));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getLogsConfigFailure());\n    }\n};\n\nexport const setLogsConfigRequest = createAction('SET_LOGS_CONFIG_REQUEST');\nexport const setLogsConfigFailure = createAction('SET_LOGS_CONFIG_FAILURE');\nexport const setLogsConfigSuccess = createAction('SET_LOGS_CONFIG_SUCCESS');\n\nexport const setLogsConfig = (config: any) => async (dispatch: any) => {\n    dispatch(setLogsConfigRequest());\n    try {\n        await apiClient.setQueryLogConfig(config);\n        dispatch(addSuccessToast('config_successfully_saved'));\n        dispatch(setLogsConfigSuccess(config));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setLogsConfigFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/rewrites.ts",
    "content": "import { createAction } from 'redux-actions';\nimport i18next from 'i18next';\nimport apiClient from '../api/Api';\nimport { addErrorToast, addSuccessToast } from './toasts';\nimport type { RootState } from '../initialState';\n\nexport const toggleRewritesModal = createAction('TOGGLE_REWRITES_MODAL');\n\nexport const getRewritesListRequest = createAction('GET_REWRITES_LIST_REQUEST');\nexport const getRewritesListFailure = createAction('GET_REWRITES_LIST_FAILURE');\nexport const getRewritesListSuccess = createAction('GET_REWRITES_LIST_SUCCESS');\n\nexport const getRewritesList = () => async (dispatch: any) => {\n    dispatch(getRewritesListRequest());\n    try {\n        const data = await apiClient.getRewritesList();\n        dispatch(getRewritesListSuccess(data));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getRewritesListFailure());\n    }\n};\n\nexport const addRewriteRequest = createAction('ADD_REWRITE_REQUEST');\nexport const addRewriteFailure = createAction('ADD_REWRITE_FAILURE');\nexport const addRewriteSuccess = createAction('ADD_REWRITE_SUCCESS');\n\nexport const addRewrite = (config: any) => async (dispatch: any) => {\n    dispatch(addRewriteRequest());\n    try {\n        await apiClient.addRewrite(config);\n        dispatch(addRewriteSuccess(config));\n        dispatch(toggleRewritesModal());\n        dispatch(getRewritesList());\n        dispatch(addSuccessToast(i18next.t('rewrite_added', { key: config.domain })));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(addRewriteFailure());\n    }\n};\n\nexport const updateRewriteRequest = createAction('UPDATE_REWRITE_REQUEST');\nexport const updateRewriteFailure = createAction('UPDATE_REWRITE_FAILURE');\nexport const updateRewriteSuccess = createAction('UPDATE_REWRITE_SUCCESS');\n\n/**\n * @param {Object} config\n * @param {string} config.target - current DNS rewrite value\n * @param {string} config.update - updated DNS rewrite value\n */\nexport const updateRewrite = (config: any) => async (dispatch: any, getState: () => RootState) => {\n    dispatch(updateRewriteRequest());\n    try {\n        await apiClient.updateRewrite(config);\n        dispatch(updateRewriteSuccess());\n        const state = getState();\n        if (state?.rewrites?.isModalOpen) {\n            dispatch(toggleRewritesModal());\n        }\n        dispatch(getRewritesList());\n        dispatch(addSuccessToast(i18next.t('rewrite_updated', { key: config.domain })));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(updateRewriteFailure());\n    }\n};\n\nexport const deleteRewriteRequest = createAction('DELETE_REWRITE_REQUEST');\nexport const deleteRewriteFailure = createAction('DELETE_REWRITE_FAILURE');\nexport const deleteRewriteSuccess = createAction('DELETE_REWRITE_SUCCESS');\n\nexport const deleteRewrite = (config: any) => async (dispatch: any) => {\n    dispatch(deleteRewriteRequest());\n    try {\n        await apiClient.deleteRewrite(config);\n        dispatch(deleteRewriteSuccess());\n        dispatch(getRewritesList());\n        dispatch(addSuccessToast(i18next.t('rewrite_deleted', { key: config.domain })));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(deleteRewriteFailure());\n    }\n};\n\nexport const getRewriteSettingsRequest = createAction('GET_REWRITE_SETTINGS_REQUEST');\nexport const getRewriteSettingsFailure = createAction('GET_REWRITE_SETTINGS_FAILURE');\nexport const getRewriteSettingsSuccess = createAction('GET_REWRITE_SETTINGS_SUCCESS');\n\nexport const getRewriteSettings = () => async (dispatch: any) => {\n    dispatch(getRewriteSettingsRequest());\n    try {\n        const data = await apiClient.getRewriteSettings();\n        dispatch(getRewriteSettingsSuccess(data));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getRewriteSettingsFailure());\n    }\n};\n\nexport const updateRewriteSettingsRequest = createAction('UPDATE_REWRITE_SETTINGS_REQUEST');\nexport const updateRewriteSettingsFailure = createAction('UPDATE_REWRITE_SETTINGS_FAILURE');\nexport const updateRewriteSettingsSuccess = createAction('UPDATE_REWRITE_SETTINGS_SUCCESS');\n\nexport const updateRewriteSettings = (config: any) => async (dispatch: any) => {\n    dispatch(updateRewriteSettingsRequest());\n    try {\n        await apiClient.updateRewriteSettings(config);\n        dispatch(updateRewriteSettingsSuccess(config));\n        dispatch(getRewriteSettings());\n        dispatch(addSuccessToast(i18next.t('rewrite_settings_updated')));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(updateRewriteSettingsFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/services.ts",
    "content": "import { createAction } from 'redux-actions';\nimport apiClient from '../api/Api';\nimport { addErrorToast, addSuccessToast } from './toasts';\n\nexport const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST');\nexport const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');\nexport const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');\n\nexport const getBlockedServices = () => async (dispatch: any) => {\n    dispatch(getBlockedServicesRequest());\n    try {\n        const data = await apiClient.getBlockedServices();\n        dispatch(getBlockedServicesSuccess(data));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getBlockedServicesFailure());\n    }\n};\n\nexport const getAllBlockedServicesRequest = createAction('GET_ALL_BLOCKED_SERVICES_REQUEST');\nexport const getAllBlockedServicesFailure = createAction('GET_ALL_BLOCKED_SERVICES_FAILURE');\nexport const getAllBlockedServicesSuccess = createAction('GET_ALL_BLOCKED_SERVICES_SUCCESS');\n\nexport const getAllBlockedServices = () => async (dispatch: any) => {\n    dispatch(getAllBlockedServicesRequest());\n    try {\n        const data = await apiClient.getAllBlockedServices();\n        dispatch(getAllBlockedServicesSuccess(data));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getAllBlockedServicesFailure());\n    }\n};\n\nexport const updateBlockedServicesRequest = createAction('UPDATE_BLOCKED_SERVICES_REQUEST');\nexport const updateBlockedServicesFailure = createAction('UPDATE_BLOCKED_SERVICES_FAILURE');\nexport const updateBlockedServicesSuccess = createAction('UPDATE_BLOCKED_SERVICES_SUCCESS');\n\nexport const updateBlockedServices = (values: any) => async (dispatch: any) => {\n    dispatch(updateBlockedServicesRequest());\n    try {\n        await apiClient.updateBlockedServices(values);\n        dispatch(updateBlockedServicesSuccess());\n        dispatch(getBlockedServices());\n        dispatch(addSuccessToast('blocked_services_saved'));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(updateBlockedServicesFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/stats.ts",
    "content": "import { createAction } from 'redux-actions';\n\nimport apiClient from '../api/Api';\nimport { normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';\nimport { addErrorToast, addSuccessToast } from './toasts';\n\nexport const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST');\nexport const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE');\nexport const getStatsConfigSuccess = createAction('GET_STATS_CONFIG_SUCCESS');\n\nexport const getStatsConfig = () => async (dispatch: any) => {\n    dispatch(getStatsConfigRequest());\n    try {\n        const data = await apiClient.getStatsConfig();\n        dispatch(getStatsConfigSuccess(data));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getStatsConfigFailure());\n    }\n};\n\nexport const setStatsConfigRequest = createAction('SET_STATS_CONFIG_REQUEST');\nexport const setStatsConfigFailure = createAction('SET_STATS_CONFIG_FAILURE');\nexport const setStatsConfigSuccess = createAction('SET_STATS_CONFIG_SUCCESS');\n\nexport const setStatsConfig = (config: any) => async (dispatch: any) => {\n    dispatch(setStatsConfigRequest());\n    try {\n        await apiClient.setStatsConfig(config);\n        dispatch(addSuccessToast('config_successfully_saved'));\n        dispatch(setStatsConfigSuccess(config));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(setStatsConfigFailure());\n    }\n};\n\nexport const getStatsRequest = createAction('GET_STATS_REQUEST');\nexport const getStatsFailure = createAction('GET_STATS_FAILURE');\nexport const getStatsSuccess = createAction('GET_STATS_SUCCESS');\n\nexport const getStats = () => async (dispatch: any) => {\n    dispatch(getStatsRequest());\n    try {\n        const stats = await apiClient.getStats();\n        const normalizedTopClients = normalizeTopStats(stats.top_clients);\n\n        const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');\n        const clients = await apiClient.searchClients(clientsParams);\n        const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');\n\n        const normalizedStats = {\n            ...stats,\n            top_blocked_domains: normalizeTopStats(stats.top_blocked_domains),\n            top_clients: topClientsWithInfo,\n            top_queried_domains: normalizeTopStats(stats.top_queried_domains),\n            avg_processing_time: secondsToMilliseconds(stats.avg_processing_time),\n            top_upstreams_responses: normalizeTopStats(stats.top_upstreams_responses),\n            top_upstrems_avg_time: normalizeTopStats(stats.top_upstreams_avg_time),\n        };\n\n        dispatch(getStatsSuccess(normalizedStats));\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(getStatsFailure());\n    }\n};\n\nexport const resetStatsRequest = createAction('RESET_STATS_REQUEST');\nexport const resetStatsFailure = createAction('RESET_STATS_FAILURE');\nexport const resetStatsSuccess = createAction('RESET_STATS_SUCCESS');\n\nexport const resetStats = () => async (dispatch: any) => {\n    dispatch(getStatsRequest());\n    try {\n        await apiClient.resetStats();\n        dispatch(addSuccessToast('statistics_cleared'));\n        dispatch(resetStatsSuccess());\n    } catch (error) {\n        dispatch(addErrorToast({ error }));\n        dispatch(resetStatsFailure());\n    }\n};\n"
  },
  {
    "path": "client/src/actions/toasts.ts",
    "content": "import { createAction } from 'redux-actions';\n\nexport const addErrorToast = createAction('ADD_ERROR_TOAST');\nexport const addSuccessToast = createAction('ADD_SUCCESS_TOAST');\nexport const addNoticeToast = createAction('ADD_NOTICE_TOAST');\n"
  },
  {
    "path": "client/src/api/Api.ts",
    "content": "import axios from 'axios';\n\nimport { BASE_URL } from '../../constants';\n\nimport { getPathWithQueryString } from '../helpers/helpers';\nimport { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES } from '../helpers/constants';\nimport i18n from '../i18n';\nimport { LANGUAGES } from '../helpers/twosky';\n\nclass Api {\n    baseUrl = BASE_URL;\n\n    async makeRequest(path: any, method = 'POST', config: any = {}) {\n        const url = `${this.baseUrl}/${path}`;\n\n        const axiosConfig = config || {};\n        if (method !== 'GET' && axiosConfig.data) {\n            axiosConfig.headers = axiosConfig.headers || {};\n            axiosConfig.headers['Content-Type'] = axiosConfig.headers['Content-Type'] || 'application/json';\n        }\n\n        try {\n            const response = await axios({\n                url,\n                method,\n                ...axiosConfig,\n            });\n            return response.data;\n        } catch (error) {\n            const errorPath = url;\n\n            if (error.response) {\n                const { pathname } = document.location;\n                const shouldRedirect = pathname !== HTML_PAGES.LOGIN && pathname !== HTML_PAGES.INSTALL;\n\n                if (error.response.status === 403 && shouldRedirect) {\n                    const loginPageUrl = window.location.href.replace(R_PATH_LAST_PART, HTML_PAGES.LOGIN);\n                    window.location.replace(loginPageUrl);\n                    return false;\n                }\n\n                throw new Error(`${errorPath} | ${error.response.data} | ${error.response.status}`);\n            }\n\n            throw new Error(`${errorPath} | ${error.message || error}`);\n        }\n    }\n\n    // Global methods\n    GLOBAL_STATUS = { path: 'status', method: 'GET' };\n\n    GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };\n\n    GLOBAL_VERSION = { path: 'version.json', method: 'POST' };\n\n    GLOBAL_UPDATE = { path: 'update', method: 'POST' };\n\n    getGlobalStatus() {\n        const { path, method } = this.GLOBAL_STATUS;\n\n        return this.makeRequest(path, method);\n    }\n\n    testUpstream(servers: any) {\n        const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;\n        const config = {\n            data: servers,\n        };\n        return this.makeRequest(path, method, config);\n    }\n\n    getGlobalVersion(data: any) {\n        const { path, method } = this.GLOBAL_VERSION;\n        const config = {\n            data,\n        };\n        return this.makeRequest(path, method, config);\n    }\n\n    getUpdate() {\n        const { path, method } = this.GLOBAL_UPDATE;\n\n        return this.makeRequest(path, method);\n    }\n\n    // Filtering\n    FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };\n\n    FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' };\n\n    FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' };\n\n    FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' };\n\n    FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' };\n\n    FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' };\n\n    FILTERING_CONFIG = { path: 'filtering/config', method: 'POST' };\n\n    FILTERING_CHECK_HOST = { path: 'filtering/check_host', method: 'GET' };\n\n    getFilteringStatus() {\n        const { path, method } = this.FILTERING_STATUS;\n\n        return this.makeRequest(path, method);\n    }\n\n    refreshFilters(config: any) {\n        const { path, method } = this.FILTERING_REFRESH;\n        const parameters = {\n            data: config,\n        };\n\n        return this.makeRequest(path, method, parameters);\n    }\n\n    addFilter(config: any) {\n        const { path, method } = this.FILTERING_ADD_FILTER;\n        const parameters = {\n            data: config,\n        };\n\n        return this.makeRequest(path, method, parameters);\n    }\n\n    removeFilter(config: any) {\n        const { path, method } = this.FILTERING_REMOVE_FILTER;\n        const parameters = {\n            data: config,\n        };\n\n        return this.makeRequest(path, method, parameters);\n    }\n\n    setRules(rules: any) {\n        const { path, method } = this.FILTERING_SET_RULES;\n        const parameters = {\n            data: rules,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    setFiltersConfig(config: any) {\n        const { path, method } = this.FILTERING_CONFIG;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    setFilterUrl(config: any) {\n        const { path, method } = this.FILTERING_SET_URL;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    checkHost(params: any) {\n        const { path, method } = this.FILTERING_CHECK_HOST;\n        const url = getPathWithQueryString(path, params);\n\n        return this.makeRequest(url, method);\n    }\n\n    // Parental\n    PARENTAL_STATUS = { path: 'parental/status', method: 'GET' };\n\n    PARENTAL_ENABLE = { path: 'parental/enable', method: 'POST' };\n\n    PARENTAL_DISABLE = { path: 'parental/disable', method: 'POST' };\n\n    getParentalStatus() {\n        const { path, method } = this.PARENTAL_STATUS;\n\n        return this.makeRequest(path, method);\n    }\n\n    enableParentalControl() {\n        const { path, method } = this.PARENTAL_ENABLE;\n\n        return this.makeRequest(path, method);\n    }\n\n    disableParentalControl() {\n        const { path, method } = this.PARENTAL_DISABLE;\n\n        return this.makeRequest(path, method);\n    }\n\n    // Safebrowsing\n    SAFEBROWSING_STATUS = { path: 'safebrowsing/status', method: 'GET' };\n\n    SAFEBROWSING_ENABLE = { path: 'safebrowsing/enable', method: 'POST' };\n\n    SAFEBROWSING_DISABLE = { path: 'safebrowsing/disable', method: 'POST' };\n\n    getSafebrowsingStatus() {\n        const { path, method } = this.SAFEBROWSING_STATUS;\n\n        return this.makeRequest(path, method);\n    }\n\n    enableSafebrowsing() {\n        const { path, method } = this.SAFEBROWSING_ENABLE;\n\n        return this.makeRequest(path, method);\n    }\n\n    disableSafebrowsing() {\n        const { path, method } = this.SAFEBROWSING_DISABLE;\n\n        return this.makeRequest(path, method);\n    }\n\n    // Safesearch\n    SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' };\n\n    SAFESEARCH_UPDATE = { path: 'safesearch/settings', method: 'PUT' };\n\n    getSafesearchStatus() {\n        const { path, method } = this.SAFESEARCH_STATUS;\n\n        return this.makeRequest(path, method);\n    }\n\n    /**\n     * interface SafeSearchConfig {\n        \"enabled\": boolean,\n        \"bing\": boolean,\n        \"duckduckgo\": boolean,\n        \"google\": boolean,\n        \"pixabay\": boolean,\n        \"yandex\": boolean,\n        \"youtube\": boolean\n     * }\n     * @param {*} data - SafeSearchConfig\n     * @returns 200 ok\n     */\n    updateSafesearch(data: any) {\n        const { path, method } = this.SAFESEARCH_UPDATE;\n        return this.makeRequest(path, method, { data });\n    }\n\n    // enableSafesearch() {\n    //     const { path, method } = this.SAFESEARCH_ENABLE;\n    //     return this.makeRequest(path, method);\n    // }\n\n    // disableSafesearch() {\n    //     const { path, method } = this.SAFESEARCH_DISABLE;\n    //     return this.makeRequest(path, method);\n    // }\n\n    // Language\n\n    async changeLanguage(config: any) {\n        const profile = await this.getProfile();\n        profile.language = config.language;\n\n        return this.setProfile(profile);\n    }\n\n    // Theme\n\n    async changeTheme(config: any) {\n        const profile = await this.getProfile();\n        profile.theme = config.theme;\n\n        return this.setProfile(profile);\n    }\n\n    // DHCP\n    DHCP_STATUS = { path: 'dhcp/status', method: 'GET' };\n\n    DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' };\n\n    DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' };\n\n    DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };\n\n    DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' };\n\n    DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' };\n\n    DHCP_UPDATE_STATIC_LEASE = { path: 'dhcp/update_static_lease', method: 'POST' };\n\n    DHCP_RESET = { path: 'dhcp/reset', method: 'POST' };\n\n    DHCP_LEASES_RESET = { path: 'dhcp/reset_leases', method: 'POST' };\n\n    getDhcpStatus() {\n        const { path, method } = this.DHCP_STATUS;\n\n        return this.makeRequest(path, method);\n    }\n\n    getDhcpInterfaces() {\n        const { path, method } = this.DHCP_INTERFACES;\n\n        return this.makeRequest(path, method);\n    }\n\n    setDhcpConfig(config: any) {\n        const { path, method } = this.DHCP_SET_CONFIG;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    findActiveDhcp(req: any) {\n        const { path, method } = this.DHCP_FIND_ACTIVE;\n        const parameters = {\n            data: req,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    addStaticLease(config: any) {\n        const { path, method } = this.DHCP_ADD_STATIC_LEASE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    removeStaticLease(config: any) {\n        const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    updateStaticLease(config: any) {\n        const { path, method } = this.DHCP_UPDATE_STATIC_LEASE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    resetDhcp() {\n        const { path, method } = this.DHCP_RESET;\n\n        return this.makeRequest(path, method);\n    }\n\n    resetDhcpLeases() {\n        const { path, method } = this.DHCP_LEASES_RESET;\n\n        return this.makeRequest(path, method);\n    }\n\n    // Installation\n    INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };\n\n    INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };\n\n    INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' };\n\n    getDefaultAddresses() {\n        const { path, method } = this.INSTALL_GET_ADDRESSES;\n\n        return this.makeRequest(path, method);\n    }\n\n    setAllSettings(config: any) {\n        const { path, method } = this.INSTALL_CONFIGURE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    checkConfig(config: any) {\n        const { path, method } = this.INSTALL_CHECK_CONFIG;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    // DNS-over-HTTPS and DNS-over-TLS\n    TLS_STATUS = { path: 'tls/status', method: 'GET' };\n\n    TLS_CONFIG = { path: 'tls/configure', method: 'POST' };\n\n    TLS_VALIDATE = { path: 'tls/validate', method: 'POST' };\n\n    getTlsStatus() {\n        const { path, method } = this.TLS_STATUS;\n\n        return this.makeRequest(path, method);\n    }\n\n    setTlsConfig(config: any) {\n        const { path, method } = this.TLS_CONFIG;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    validateTlsConfig(config: any) {\n        const { path, method } = this.TLS_VALIDATE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    // Per-client settings\n    GET_CLIENTS = { path: 'clients', method: 'GET' };\n\n    SEARCH_CLIENTS = { path: 'clients/search', method: 'POST' };\n\n    ADD_CLIENT = { path: 'clients/add', method: 'POST' };\n\n    DELETE_CLIENT = { path: 'clients/delete', method: 'POST' };\n\n    UPDATE_CLIENT = { path: 'clients/update', method: 'POST' };\n\n    getClients() {\n        const { path, method } = this.GET_CLIENTS;\n\n        return this.makeRequest(path, method);\n    }\n\n    addClient(config: any) {\n        const { path, method } = this.ADD_CLIENT;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    deleteClient(config: any) {\n        const { path, method } = this.DELETE_CLIENT;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    updateClient(config: any) {\n        const { path, method } = this.UPDATE_CLIENT;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    searchClients(config: any) {\n        const { path, method } = this.SEARCH_CLIENTS;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    // DNS access settings\n    ACCESS_LIST = { path: 'access/list', method: 'GET' };\n\n    ACCESS_SET = { path: 'access/set', method: 'POST' };\n\n    getAccessList() {\n        const { path, method } = this.ACCESS_LIST;\n\n        return this.makeRequest(path, method);\n    }\n\n    setAccessList(config: any) {\n        const { path, method } = this.ACCESS_SET;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    // DNS rewrites\n    REWRITES_LIST = { path: 'rewrite/list', method: 'GET' };\n\n    REWRITE_ADD = { path: 'rewrite/add', method: 'POST' };\n\n    REWRITE_UPDATE = { path: 'rewrite/update', method: 'PUT' };\n\n    REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' };\n\n    REWRITE_SETTINGS = { path: 'rewrite/settings', method: 'GET' };\n\n    REWRITE_SETTINGS_UPDATE = { path: 'rewrite/settings/update', method: 'PUT' };\n\n    getRewritesList() {\n        const { path, method } = this.REWRITES_LIST;\n\n        return this.makeRequest(path, method);\n    }\n\n    addRewrite(config: any) {\n        const { path, method } = this.REWRITE_ADD;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    updateRewrite(config: any) {\n        const { path, method } = this.REWRITE_UPDATE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    updateRewriteSettings(config: any) {\n        const { path, method } = this.REWRITE_SETTINGS_UPDATE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    deleteRewrite(config: any) {\n        const { path, method } = this.REWRITE_DELETE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    getRewriteSettings() {\n        const { path, method } = this.REWRITE_SETTINGS;\n\n        return this.makeRequest(path, method);\n    }\n\n    // Blocked services\n    BLOCKED_SERVICES_GET = { path: 'blocked_services/get', method: 'GET' };\n\n    BLOCKED_SERVICES_UPDATE = { path: 'blocked_services/update', method: 'PUT' };\n\n    BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };\n\n    getAllBlockedServices() {\n        const { path, method } = this.BLOCKED_SERVICES_ALL;\n\n        return this.makeRequest(path, method);\n    }\n\n    getBlockedServices() {\n        const { path, method } = this.BLOCKED_SERVICES_GET;\n\n        return this.makeRequest(path, method);\n    }\n\n    updateBlockedServices(config: any) {\n        const { path, method } = this.BLOCKED_SERVICES_UPDATE;\n        const parameters = {\n            data: config,\n        };\n        return this.makeRequest(path, method, parameters);\n    }\n\n    // Settings for statistics\n    GET_STATS = { path: 'stats', method: 'GET' };\n\n    GET_STATS_CONFIG = { path: 'stats/config', method: 'GET' };\n\n    UPDATE_STATS_CONFIG = { path: 'stats/config/update', method: 'PUT' };\n\n    STATS_RESET = { path: 'stats_reset', method: 'POST' };\n\n    getStats() {\n        const { path, method } = this.GET_STATS;\n\n        return this.makeRequest(path, method);\n    }\n\n    getStatsConfig() {\n        const { path, method } = this.GET_STATS_CONFIG;\n\n        return this.makeRequest(path, method);\n    }\n\n    setStatsConfig(data: any) {\n        const { path, method } = this.UPDATE_STATS_CONFIG;\n        const config = {\n            data,\n        };\n        return this.makeRequest(path, method, config);\n    }\n\n    resetStats() {\n        const { path, method } = this.STATS_RESET;\n\n        return this.makeRequest(path, method);\n    }\n\n    // Query log\n    GET_QUERY_LOG = { path: 'querylog', method: 'GET' };\n\n    UPDATE_QUERY_LOG_CONFIG = { path: 'querylog/config/update', method: 'PUT' };\n\n    GET_QUERY_LOG_CONFIG = { path: 'querylog/config', method: 'GET' };\n\n    QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' };\n\n    getQueryLog(params: any) {\n        const { path, method } = this.GET_QUERY_LOG;\n        // eslint-disable-next-line no-param-reassign\n        params.limit = QUERY_LOGS_PAGE_LIMIT;\n        const url = getPathWithQueryString(path, params);\n\n        return this.makeRequest(url, method);\n    }\n\n    getQueryLogConfig() {\n        const { path, method } = this.GET_QUERY_LOG_CONFIG;\n\n        return this.makeRequest(path, method);\n    }\n\n    setQueryLogConfig(data: any) {\n        const { path, method } = this.UPDATE_QUERY_LOG_CONFIG;\n        const config = {\n            data,\n        };\n        return this.makeRequest(path, method, config);\n    }\n\n    clearQueryLog() {\n        const { path, method } = this.QUERY_LOG_CLEAR;\n\n        return this.makeRequest(path, method);\n    }\n\n    // Login\n    LOGIN = { path: 'login', method: 'POST' };\n\n    login(data: any) {\n        const { path, method } = this.LOGIN;\n        const config = {\n            data,\n        };\n        return this.makeRequest(path, method, config);\n    }\n\n    // Profile\n    GET_PROFILE = { path: 'profile', method: 'GET' };\n\n    UPDATE_PROFILE = { path: 'profile/update', method: 'PUT' };\n\n    getProfile() {\n        const { path, method } = this.GET_PROFILE;\n\n        return this.makeRequest(path, method);\n    }\n\n    setProfile(data: any) {\n        const theme = data.theme ? data.theme : THEMES.auto;\n        const defaultLanguage = i18n.language ? i18n.language : LANGUAGES.en;\n        const language = data.language ? data.language : defaultLanguage;\n\n        const { path, method } = this.UPDATE_PROFILE;\n        const config = { data: { theme, language } };\n\n        return this.makeRequest(path, method, config);\n    }\n\n    // DNS config\n    GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' };\n\n    SET_DNS_CONFIG = { path: 'dns_config', method: 'POST' };\n\n    getDnsConfig() {\n        const { path, method } = this.GET_DNS_CONFIG;\n\n        return this.makeRequest(path, method);\n    }\n\n    setDnsConfig(data: any) {\n        const { path, method } = this.SET_DNS_CONFIG;\n        const config = {\n            data,\n        };\n        return this.makeRequest(path, method, config);\n    }\n\n    SET_PROTECTION = { path: 'protection', method: 'POST' };\n\n    setProtection(data: any) {\n        const { enabled, duration } = data;\n        const { path, method } = this.SET_PROTECTION;\n\n        return this.makeRequest(path, method, { data: { enabled, duration } });\n    }\n\n    // Cache\n    CLEAR_CACHE = { path: 'cache_clear', method: 'POST' };\n\n    clearCache() {\n        const { path, method } = this.CLEAR_CACHE;\n\n        return this.makeRequest(path, method);\n    }\n}\n\nconst apiClient = new Api();\nexport default apiClient;\n"
  },
  {
    "path": "client/src/components/App/index.css",
    "content": ":root {\n    --black: #131313;\n    --bgcolor: #f5f7fb;\n    --mcolor: #495057;\n    --scolor: rgba(74, 74, 74, 0.7);\n    --border-color: rgba(0, 40, 100, 0.12);\n    --header-bgcolor: #fff;\n    --card-bgcolor: #fff;\n    --card-border-color: rgba(0, 40, 100, 0.12);\n    --ctrl-bgcolor: #fff;\n    --ctrl-select-bgcolor: rgba(69, 79, 94, 0.12);\n    --ctrl-dropdown-color: #212529;\n    --ctrl-dropdown-bgcolor-focus: #f8f9fa;\n    --ctrl-dropdown-color-focus: #16181b;\n    --btn-success-bgcolor: #5eba00;\n    --form-disabled-bgcolor: #f8f9fa;\n    --form-disabled-color: #495057;\n    --rt-nodata-bgcolor: rgba(255, 255, 255, 0.8);\n    --rt-nodata-color: rgba(0, 0, 0, 0.5);\n    --modal-overlay-bgcolor: rgba(255, 255, 255, 0.75);\n    --logs__table-bgcolor: #fff;\n    --logs__row--blue-bgcolor: #e5effd;\n    --logs__row--white-bgcolor: #fff;\n    --detailed-info-color: #888888;\n    --yellow-pale: rgba(247, 181, 0, 0.1);\n    --green79: #67b279;\n    --gray-a5: #a5a5a5;\n    --gray-d8: #d8d8d8;\n    --gray-f3: #f3f3f3;\n    --loading-bg: rgba(255, 255, 255, 0.48);\n    --font-family-monospace: Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace;\n    --font-size-disable-autozoom: 1rem;\n    --alert-message-color: #24426c;\n    --alert-message-border: #cbdbf2;\n    --alert-message-bg: #dae5f5;\n    --checkbox-bg: #e2e2e2;\n    --radio-bg: #ffffff;\n}\n\n[data-theme='dark'] {\n    --black: #ffffff;\n    --bgcolor: #131313;\n    --mcolor: #e6e6e6;\n    --scolor: #a5a5a5;\n    --header-bgcolor: #131313;\n    --border-color: #222;\n    --card-bgcolor: #1c1c1c;\n    --card-border-color: #3d3d3d;\n    --ctrl-bgcolor: #1c1c1c;\n    --ctrl-select-bgcolor: #3d3d3d;\n    --ctrl-dropdown-color: #fff;\n    --ctrl-dropdown-bgcolor-focus: #000;\n    --ctrl-dropdown-color-focus: #fff;\n    --btn-success-bgcolor: #67b279;\n    --form-disabled-bgcolor: #2d2d2d;\n    --form-disabled-color: #a5a5a5;\n    --logs__text-color: #f3f3f3;\n    --rt-nodata-bgcolor: #1c1c1c;\n    --rt-nodata-color: #fff;\n    --modal-overlay-bgcolor: rgba(19, 19, 19, 0.75);\n    --logs__table-bgcolor: #3d3d3d;\n    --logs__row--blue-bgcolor: #467fcf;\n    --logs__row--white-bgcolor: #1c1c1c;\n    --detailed-info-color: #fff;\n    --gray300: #f3f3f3;\n    --loading-bg: #131313;\n    --alert-message-color: #e6e6e6;\n    --alert-message-border: #363648;\n    --alert-message-bg: #363648;\n    --checkbox-bg: #a4a4a4;\n    --radio-bg: #a4a4a4;\n}\n\nbody {\n    margin: 0;\n    padding: 0;\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n}\n\n/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */\n@media screen and (max-width: 767px) {\n    input,\n    select,\n    textarea {\n        font-size: var(--font-size-disable-autozoom);\n    }\n}\n\n.status {\n    margin-top: 30px;\n}\n\n.container--wrap {\n    min-height: calc(100vh - 372px);\n}\n\n@media screen and (min-width: 768px) {\n    .container--wrap {\n        min-height: calc(100vh - 168px);\n    }\n}\n\n@media screen and (min-width: 992px) {\n    .container--wrap {\n        min-height: calc(100vh - 187px);\n    }\n}\n\n.loading-bar {\n    position: fixed;\n    top: 0;\n    left: 0;\n    z-index: 103;\n    height: 3px;\n    background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);\n}\n\n.modal-body--medium {\n    max-height: 20rem;\n    overflow-y: auto;\n}\n\n.modal-body--filters {\n    max-height: 600px;\n    overflow-y: auto;\n}\n\n.modal-body__item:not(:first-child) {\n    padding-top: 1.5rem;\n}\n\n.font-monospace {\n    font-family: var(--font-family-monospace);\n}\n\n.mw-75 {\n    max-width: 75% !important;\n}\n\n.cursor--not-allowed {\n    cursor: not-allowed;\n}\n\n.ReactModal__Body--open {\n    overflow: hidden;\n}\n\na.btn-success.disabled {\n    color: #fff;\n}\n"
  },
  {
    "path": "client/src/components/App/index.tsx",
    "content": "import React, { useEffect } from 'react';\n\nimport { HashRouter, Route } from 'react-router-dom';\nimport LoadingBar from 'react-redux-loading-bar';\nimport { hot } from 'react-hot-loader/root';\n\nimport 'react-table/react-table.css';\nimport '../ui/Tabler.css';\nimport '../ui/ReactTable.css';\nimport './index.css';\n\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\nimport Toasts from '../Toasts';\nimport Footer from '../ui/Footer';\nimport Status from '../ui/Status';\nimport UpdateTopline from '../ui/UpdateTopline';\nimport UpdateOverlay from '../ui/UpdateOverlay';\nimport EncryptionTopline from '../ui/EncryptionTopline';\nimport Icons from '../ui/Icons';\nimport i18n from '../../i18n';\n\nimport Loading from '../ui/Loading';\nimport { FILTERS_URLS, MENU_URLS, SETTINGS_URLS, THEMES } from '../../helpers/constants';\n\nimport { getLogsUrlParams, setHtmlLangAttr, setUITheme } from '../../helpers/helpers';\n\nimport Header from '../Header';\n\nimport { getDnsStatus, getTimerStatus } from '../../actions';\n\nimport Dashboard from '../../containers/Dashboard';\nimport SetupGuide from '../../containers/SetupGuide';\nimport Settings from '../../containers/Settings';\nimport Dns from '../../containers/Dns';\nimport Encryption from '../../containers/Encryption';\n\nimport Dhcp from '../Settings/Dhcp';\nimport Clients from '../../containers/Clients';\nimport DnsBlocklist from '../../containers/DnsBlocklist';\nimport DnsAllowlist from '../../containers/DnsAllowlist';\nimport DnsRewrites from '../../containers/DnsRewrites';\nimport CustomRules from '../../containers/CustomRules';\n\nimport Services from '../Filters/Services';\n\nimport Logs from '../Logs';\nimport ProtectionTimer from '../ProtectionTimer';\nimport { RootState } from '../../initialState';\n\nconst ROUTES = [\n    {\n        path: MENU_URLS.root,\n        component: Dashboard,\n        exact: true,\n    },\n    {\n        path: [`${MENU_URLS.logs}${getLogsUrlParams(':search?', ':response_status?')}`, MENU_URLS.logs],\n        component: Logs,\n    },\n    {\n        path: MENU_URLS.guide,\n        component: SetupGuide,\n    },\n    {\n        path: SETTINGS_URLS.settings,\n        component: Settings,\n    },\n    {\n        path: SETTINGS_URLS.dns,\n        component: Dns,\n    },\n    {\n        path: SETTINGS_URLS.encryption,\n        component: Encryption,\n    },\n    {\n        path: SETTINGS_URLS.dhcp,\n        component: Dhcp,\n    },\n    {\n        path: SETTINGS_URLS.clients,\n        component: Clients,\n    },\n    {\n        path: FILTERS_URLS.dns_blocklists,\n        component: DnsBlocklist,\n    },\n    {\n        path: FILTERS_URLS.dns_allowlists,\n        component: DnsAllowlist,\n    },\n    {\n        path: FILTERS_URLS.dns_rewrites,\n        component: DnsRewrites,\n    },\n    {\n        path: FILTERS_URLS.custom_rules,\n        component: CustomRules,\n    },\n    {\n        path: FILTERS_URLS.blocked_services,\n        component: Services,\n    },\n];\n\nconst App = () => {\n    const dispatch = useDispatch();\n    const { language, isCoreRunning, isUpdateAvailable, processing, theme } = useSelector<\n        RootState,\n        RootState['dashboard']\n    >((state) => state.dashboard, shallowEqual);\n\n    const { processing: processingEncryption } = useSelector<RootState, RootState['encryption']>(\n        (state) => state.encryption,\n        shallowEqual,\n    );\n\n    const updateAvailable = isCoreRunning && isUpdateAvailable;\n\n    useEffect(() => {\n        dispatch(getDnsStatus());\n\n        const handleVisibilityChange = () => {\n            if (document.visibilityState === 'visible') {\n                dispatch(getTimerStatus());\n            }\n        };\n\n        document.addEventListener('visibilitychange', handleVisibilityChange);\n\n        return () => {\n            document.removeEventListener('visibilitychange', handleVisibilityChange);\n        };\n    }, []);\n\n    const setLanguage = () => {\n        if (processing || !language) {\n            return;\n        }\n\n        i18n.changeLanguage(language);\n        setHtmlLangAttr(language);\n    };\n\n    useEffect(() => {\n        setLanguage();\n    }, [language]);\n\n    const handleAutoTheme = (e: any, accountTheme: any) => {\n        if (accountTheme !== THEMES.auto) {\n            return;\n        }\n\n        if (e.matches) {\n            setUITheme(THEMES.dark);\n        } else {\n            setUITheme(THEMES.light);\n        }\n    };\n\n    useEffect(() => {\n        if (theme !== THEMES.auto) {\n            setUITheme(theme);\n\n            return;\n        }\n\n        const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');\n        setUITheme(theme);\n\n        if (colorSchemeMedia.addEventListener !== undefined) {\n            colorSchemeMedia.addEventListener('change', (e) => {\n                handleAutoTheme(e, theme);\n            });\n        } else {\n            // Deprecated addListener for older versions of Safari.\n            colorSchemeMedia.addListener((e) => {\n                handleAutoTheme(e, theme);\n            });\n        }\n    }, [theme]);\n\n    const reloadPage = () => {\n        window.location.reload();\n    };\n\n    return (\n        <HashRouter hashType=\"noslash\">\n            {updateAvailable && (\n                <>\n                    <UpdateTopline />\n\n                    <UpdateOverlay />\n                </>\n            )}\n\n            {!processingEncryption && <EncryptionTopline />}\n\n            <LoadingBar className=\"loading-bar\" updateTime={1000} />\n\n            <Header />\n\n            <ProtectionTimer />\n\n            <div className=\"container container--wrap pb-5 pt-5\">\n                {processing && <Loading />}\n\n                {!isCoreRunning && (\n                    <div className=\"row row-cards\">\n                        <div className=\"col-lg-12\">\n                            <Status reloadPage={reloadPage} message=\"dns_start\" />\n\n                            <Loading />\n                        </div>\n                    </div>\n                )}\n                {!processing &&\n                    isCoreRunning &&\n                    ROUTES.map((route, index) => (\n                        <Route key={index} exact={route.exact} path={route.path} component={route.component} />\n                    ))}\n            </div>\n\n            <Footer />\n\n            <Toasts />\n\n            <Icons />\n        </HashRouter>\n    );\n};\n\nexport default hot(App);\n"
  },
  {
    "path": "client/src/components/Dashboard/BlockedDomains.tsx",
    "content": "import React from 'react';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport { withTranslation, Trans } from 'react-i18next';\n\nimport { TFunction } from 'i18next';\nimport Card from '../ui/Card';\n\nimport Cell from '../ui/Cell';\n\nimport DomainCell from './DomainCell';\n\nimport { getPercent } from '../../helpers/helpers';\nimport { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';\n\nconst CountCell = (totalBlocked: any) =>\n    function cell(row: any) {\n        const { value } = row;\n        const percent = getPercent(totalBlocked, value);\n\n        return <Cell value={value} percent={percent} color={STATUS_COLORS.red} search={row.original.domain} />;\n    };\n\ninterface BlockedDomainsProps {\n    topBlockedDomains: unknown[];\n    blockedFiltering: number;\n    replacedSafebrowsing: number;\n    replacedSafesearch: number;\n    replacedParental: number;\n    refreshButton: React.ReactNode;\n    subtitle: string;\n    t: TFunction;\n}\n\nconst BlockedDomains = ({\n    t,\n    refreshButton,\n    topBlockedDomains,\n    subtitle,\n    blockedFiltering,\n    replacedSafebrowsing,\n    replacedParental,\n    replacedSafesearch,\n}: BlockedDomainsProps) => {\n    const totalBlocked = blockedFiltering + replacedSafebrowsing + replacedParental + replacedSafesearch;\n\n    return (\n        <Card title={t('top_blocked_domains')} subtitle={subtitle} bodyType=\"card-table\" refresh={refreshButton}>\n            <ReactTable\n                data={topBlockedDomains.map(({ name: domain, count }: any) => ({\n                    domain,\n                    count,\n                }))}\n                columns={[\n                    {\n                        Header: <Trans>domain</Trans>,\n                        accessor: 'domain',\n                        Cell: DomainCell,\n                    },\n                    {\n                        Header: <Trans>requests_count</Trans>,\n                        accessor: 'count',\n                        maxWidth: 190,\n                        Cell: CountCell(totalBlocked),\n                    },\n                ]}\n                showPagination={false}\n                noDataText={t('no_domains_found')}\n                minRows={TABLES_MIN_ROWS}\n                defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}\n                className=\"-highlight card-table-overflow--limited stats__table\"\n            />\n        </Card>\n    );\n};\n\nexport default withTranslation()(BlockedDomains);\n"
  },
  {
    "path": "client/src/components/Dashboard/Clients.tsx",
    "content": "import React, { useState } from 'react';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\nimport classNames from 'classnames';\n\nimport Card from '../ui/Card';\nimport Cell from '../ui/Cell';\n\nimport { getPercent, sortIp, splitByNewLine } from '../../helpers/helpers';\nimport {\n    BLOCK_ACTIONS,\n    DASHBOARD_TABLES_DEFAULT_PAGE_SIZE,\n    STATUS_COLORS,\n    TABLES_MIN_ROWS,\n} from '../../helpers/constants';\nimport { toggleClientBlock } from '../../actions/access';\n\nimport { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';\nimport { getStats } from '../../actions/stats';\n\nimport IconTooltip from '../Logs/Cells/IconTooltip';\nimport { RootState } from '../../initialState';\n\nconst getClientsPercentColor = (percent: any) => {\n    if (percent > 50) {\n        return STATUS_COLORS.green;\n    }\n    if (percent > 10) {\n        return STATUS_COLORS.yellow;\n    }\n    return STATUS_COLORS.red;\n};\n\nconst CountCell = (row: any) => {\n    const {\n        value,\n        original: { ip },\n    } = row;\n\n    const numDnsQueries = useSelector<RootState>((state) => state.stats.numDnsQueries, shallowEqual);\n\n    const percent = getPercent(numDnsQueries, value);\n    const percentColor = getClientsPercentColor(percent);\n\n    return <Cell value={value} percent={percent} color={percentColor} search={ip} />;\n};\n\nconst renderBlockingButton = (ip: any, disallowed: any, disallowed_rule: any) => {\n    const dispatch = useDispatch();\n    const { t } = useTranslation();\n\n    const processingSet = useSelector<RootState, RootState['access']['processingSet']>(\n        (state) => state.access.processingSet,\n    );\n\n    const allowedClients = useSelector<RootState, RootState['access']['allowed_clients']>(\n        (state) => state.access.allowed_clients,\n        shallowEqual,\n    );\n\n    const [isOptionsOpened, setOptionsOpened] = useState(false);\n\n    const toggleClientStatus = async (ip: any, disallowed: any, disallowed_rule: any) => {\n        let confirmMessage;\n\n        if (disallowed) {\n            confirmMessage = t('client_confirm_unblock', { ip: disallowed_rule || ip });\n        } else {\n            confirmMessage = `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`;\n            if (allowedClients.length > 0) {\n                confirmMessage = confirmMessage.concat(`\\n\\n${t('filter_allowlist', { disallowed_rule: ip })}`);\n            }\n        }\n\n        if (window.confirm(confirmMessage)) {\n            await dispatch(toggleClientBlock(ip, disallowed, disallowed_rule));\n            await dispatch(getStats());\n        }\n    };\n\n    const onClick = () => {\n        toggleClientStatus(ip, disallowed, disallowed_rule);\n        setOptionsOpened(false);\n    };\n\n    const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;\n\n    const allowedClientsList = splitByNewLine(allowedClients || '');\n    const lastRuleInAllowlist = !disallowed && allowedClientsList.length === 1 && allowedClientsList[0] === ip;\n    const disabled = processingSet || lastRuleInAllowlist;\n    return (\n        <div className=\"table__action\">\n            <button type=\"button\" className=\"btn btn-icon btn-sm px-0\" onClick={() => setOptionsOpened(true)}>\n                <svg className=\"icon24 icon--lightgray button-action__icon\">\n                    <use xlinkHref=\"#bullets\" />\n                </svg>\n            </button>\n            {isOptionsOpened && (\n                <IconTooltip\n                    className=\"icon24\"\n                    tooltipClass=\"button-action--arrow-option-container\"\n                    xlinkHref=\"bullets\"\n                    triggerClass=\"btn btn-icon btn-sm px-0 button-action__hidden-trigger\"\n                    content={\n                        <button\n                            className={classNames(\n                                'button-action--arrow-option px-4 py-1',\n                                disallowed ? 'bg--green' : 'bg--danger',\n                            )}\n                            onClick={onClick}\n                            disabled={disabled}\n                            title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule: ip }) : ''}>\n                            <Trans>{text}</Trans>\n                        </button>\n                    }\n                    placement=\"bottom-end\"\n                    trigger=\"click\"\n                    onVisibilityChange={setOptionsOpened}\n                    defaultTooltipShown={true}\n                    delayHide={0}\n                />\n            )}\n        </div>\n    );\n};\n\nconst ClientCell = (row: any) => {\n    const {\n        value,\n        original: {\n            info,\n            info: { disallowed, disallowed_rule },\n        },\n    } = row;\n\n    return (\n        <>\n            <div className=\"logs__row logs__row--overflow logs__row--column d-flex align-items-center\">\n                {renderFormattedClientCell(value, info, true)}\n                {renderBlockingButton(value, disallowed, disallowed_rule)}\n            </div>\n        </>\n    );\n};\n\ninterface ClientsProps {\n    refreshButton: React.ReactNode;\n    subtitle: string;\n}\n\nconst Clients = ({ refreshButton, subtitle }: ClientsProps) => {\n    const { t } = useTranslation();\n\n    const topClients = useSelector<RootState, RootState['stats']['topClients']>(\n        (state) => state.stats.topClients,\n        shallowEqual,\n    );\n\n    return (\n        <Card title={t('top_clients')} subtitle={subtitle} bodyType=\"card-table\" refresh={refreshButton}>\n            <ReactTable\n                data={topClients.map(({ name: ip, count, info, blocked }: any) => ({\n                    ip,\n                    count,\n                    info,\n                    blocked,\n                }))}\n                columns={[\n                    {\n                        Header: <Trans>client_table_header</Trans>,\n                        accessor: 'ip',\n                        sortMethod: sortIp,\n                        Cell: ClientCell,\n                    },\n                    {\n                        Header: <Trans>requests_count</Trans>,\n                        accessor: 'count',\n                        minWidth: 180,\n                        maxWidth: 200,\n                        Cell: CountCell,\n                    },\n                ]}\n                showPagination={false}\n                noDataText={t('no_clients_found')}\n                minRows={TABLES_MIN_ROWS}\n                defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}\n                className=\"-highlight card-table-overflow--limited clients__table\"\n                getTrProps={(_state: any, rowInfo: any) => {\n                    if (!rowInfo) {\n                        return {};\n                    }\n\n                    const {\n                        info: { disallowed },\n                    } = rowInfo.original;\n\n                    return disallowed ? { className: 'logs__row--red' } : {};\n                }}\n            />\n        </Card>\n    );\n};\n\nexport default Clients;\n"
  },
  {
    "path": "client/src/components/Dashboard/Counters.tsx",
    "content": "import React from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport round from 'lodash/round';\nimport { shallowEqual, useSelector } from 'react-redux';\n\nimport Card from '../ui/Card';\n\nimport { formatNumber, msToDays, msToHours } from '../../helpers/helpers';\n\nimport LogsSearchLink from '../ui/LogsSearchLink';\nimport { RESPONSE_FILTER, TIME_UNITS } from '../../helpers/constants';\n\nimport Tooltip from '../ui/Tooltip';\nimport { RootState } from '../../initialState';\n\ninterface RowProps {\n    label: string;\n    count: string;\n    response_status?: string;\n    tooltipTitle: string;\n    translationComponents?: React.ReactElement[];\n}\n\nconst Row = ({ label, count, response_status, tooltipTitle, translationComponents }: RowProps) => {\n    const content = response_status ? (\n        <LogsSearchLink response_status={response_status}>{count}</LogsSearchLink>\n    ) : (\n        count\n    );\n\n    return (\n        <div className=\"counters__row\" key={label}>\n            <div className=\"counters__column\">\n                <span className=\"counters__title\">\n                    <Trans components={translationComponents}>{label}</Trans>\n                </span>\n\n                <span className=\"counters__tooltip\">\n                    <Tooltip\n                        content={tooltipTitle}\n                        placement=\"top\"\n                        className=\"tooltip-container tooltip-custom--narrow text-center\">\n                        <svg className=\"icons icon--20 icon--lightgray ml-2\">\n                            <use xlinkHref=\"#question\" />\n                        </svg>\n                    </Tooltip>\n                </span>\n            </div>\n\n            <div className=\"counters__column counters__column--value\">\n                <strong>{content}</strong>\n            </div>\n        </div>\n    );\n};\n\ninterface CountersProps {\n    refreshButton: React.ReactNode;\n    subtitle: string;\n}\n\nconst Counters = ({ refreshButton, subtitle }: CountersProps) => {\n    const {\n        interval,\n        numDnsQueries,\n        numBlockedFiltering,\n        numReplacedSafebrowsing,\n        numReplacedParental,\n        numReplacedSafesearch,\n        avgProcessingTime,\n        timeUnits,\n    } = useSelector<RootState, RootState['stats']>((state) => state.stats, shallowEqual);\n    const { t } = useTranslation();\n\n    const dnsQueryTooltip =\n        timeUnits === TIME_UNITS.HOURS\n            ? t('number_of_dns_query_hours', { count: msToHours(interval) })\n            : t('number_of_dns_query_days', { count: msToDays(interval) });\n\n    const rows: RowProps[] = [\n        {\n            label: 'dns_query',\n            count: formatNumber(numDnsQueries),\n            tooltipTitle: dnsQueryTooltip,\n            response_status: RESPONSE_FILTER.ALL.QUERY,\n        },\n        {\n            label: 'blocked_by',\n            count: formatNumber(numBlockedFiltering),\n            tooltipTitle: 'number_of_dns_query_blocked_24_hours',\n            response_status: RESPONSE_FILTER.BLOCKED.QUERY,\n\n            translationComponents: [\n                <a href=\"#filters\" key=\"0\">\n                    link\n                </a>,\n            ],\n        },\n        {\n            label: 'stats_malware_phishing',\n            count: formatNumber(numReplacedSafebrowsing),\n            tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec',\n            response_status: RESPONSE_FILTER.BLOCKED_THREATS.QUERY,\n        },\n        {\n            label: 'stats_adult',\n            count: formatNumber(numReplacedParental),\n            tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult',\n            response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY,\n        },\n        {\n            label: 'enforced_save_search',\n            count: formatNumber(numReplacedSafesearch),\n            tooltipTitle: 'number_of_dns_query_to_safe_search',\n            response_status: RESPONSE_FILTER.SAFE_SEARCH.QUERY,\n        },\n        {\n            label: 'average_processing_time',\n            count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : '0',\n            tooltipTitle: 'average_processing_time_hint',\n        },\n    ];\n\n    return (\n        <Card title={t('general_statistics')} subtitle={subtitle} bodyType=\"card-table\" refresh={refreshButton}>\n            <div className=\"counters\">\n                {rows.map((row, index) => {\n                    return <Row {...row} key={index} />;\n                })}\n            </div>\n        </Card>\n    );\n};\n\nexport default Counters;\n"
  },
  {
    "path": "client/src/components/Dashboard/Dashboard.css",
    "content": ".dashboard-protection-button.btn-gray {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n    border-right-color: #a4a4a4;\n}\n\n.stats__table .popover__body {\n    left: -10px;\n    min-width: 270px;\n    transform: none;\n}\n\n.stats__table .popover__body:after {\n    left: 23px;\n}\n\n.stats__table .rt-tr-group:first-child .popover__body,\n.stats__table .rt-tr-group:nth-child(2) .popover__body {\n    top: calc(100% + 5px);\n    bottom: initial;\n    z-index: 1;\n}\n\n.stats__table .rt-tr-group:first-child .popover__body:after,\n.stats__table .rt-tr-group:nth-child(2) .popover__body:after {\n    top: -11px;\n    border-top: 6px solid transparent;\n    border-bottom: 6px solid #585965;\n}\n\n.table__action {\n    position: relative;\n    margin-left: auto;\n}\n\n.table__action .btn-icon {\n    outline: 0;\n    box-shadow: none;\n}\n\n.page-title--dashboard {\n    display: flex;\n    align-items: center;\n}\n\n@media (max-width: 767.98px) {\n    .page-title--dashboard {\n        flex-direction: column;\n        align-items: flex-start;\n    }\n}\n\n.counters__row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    border-top: 1px solid var(--card-border-color);\n    padding: 0.75rem 1.5rem;\n}\n\n.counters__column--value {\n    flex-shrink: 0;\n    margin-left: 1.5rem;\n    text-align: right;\n    white-space: nowrap;\n}\n\n@media screen and (min-width: 768px) {\n    .counters__column {\n        display: flex;\n        align-items: center;\n        overflow: hidden;\n    }\n\n    .counters__title {\n        display: block;\n        max-width: 100%;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n        overflow: hidden;\n    }\n\n    .counters__tooltip {\n        display: block;\n        flex-shrink: 0;\n    }\n}\n"
  },
  {
    "path": "client/src/components/Dashboard/DomainCell.tsx",
    "content": "import React from 'react';\n\nimport { Trans } from 'react-i18next';\nimport { getSourceData, getTrackerData } from '../../helpers/trackers/trackers';\n\nimport Tooltip from '../ui/Tooltip';\n\nimport { captitalizeWords } from '../../helpers/helpers';\n\nconst renderLabel = (value: any) => (\n    <strong>\n        <Trans>{value}</Trans>\n    </strong>\n);\n\ninterface renderLinkProps {\n    url: string;\n    name: string;\n}\n\nconst renderLink = ({ url, name }: renderLinkProps) => (\n    <a className=\"tooltip-custom__content-link\" target=\"_blank\" rel=\"noopener noreferrer\" href={url}>\n        <strong>{name}</strong>\n    </a>\n);\n\nconst getTrackerInfo = (trackerData: any) => [\n    {\n        key: 'name_table_header',\n        value: trackerData,\n        render: renderLink,\n    },\n    {\n        key: 'category_label',\n        value: captitalizeWords(trackerData.category),\n        render: renderLabel,\n    },\n    {\n        key: 'source_label',\n        value: getSourceData(trackerData),\n        render: renderLink,\n    },\n];\n\ninterface DomainCellProps {\n    value: string;\n}\n\nconst DomainCell = ({ value }: DomainCellProps) => {\n    const trackerData = getTrackerData(value);\n\n    const content = trackerData && (\n        <div className=\"popover__list\">\n            <div className=\"tooltip-custom__content-title mb-1\">\n                <Trans>found_in_known_domain_db</Trans>\n            </div>\n            {getTrackerInfo(trackerData).map(({ key, value, render }) => (\n                <div key={key} className=\"tooltip-custom__content-item\">\n                    <Trans>{key}</Trans>: {render(value)}\n                </div>\n            ))}\n        </div>\n    );\n\n    return (\n        <div className=\"logs__row\">\n            <div className=\"logs__text\" title={value}>\n                {value}\n            </div>\n            {trackerData && (\n                <Tooltip content={content} placement=\"top\" className=\"tooltip-container tooltip-custom--wide\">\n                    <svg className=\"icons icon--24 icon--green ml-1\">\n                        <use xlinkHref=\"#privacy\" />\n                    </svg>\n                </Tooltip>\n            )}\n        </div>\n    );\n};\n\nexport default DomainCell;\n"
  },
  {
    "path": "client/src/components/Dashboard/QueriedDomains.tsx",
    "content": "import React from 'react';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport { withTranslation, Trans } from 'react-i18next';\n\nimport Card from '../ui/Card';\nimport Cell from '../ui/Cell';\nimport DomainCell from './DomainCell';\n\nimport { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';\n\nimport { getPercent } from '../../helpers/helpers';\n\nconst getQueriedPercentColor = (percent: any) => {\n    if (percent > 10) {\n        return STATUS_COLORS.red;\n    }\n    if (percent > 5) {\n        return STATUS_COLORS.yellow;\n    }\n    return STATUS_COLORS.green;\n};\n\nconst countCell = (dnsQueries: any) =>\n    function cell(row: any) {\n        const { value } = row;\n        const percent = getPercent(dnsQueries, value);\n        const percentColor = getQueriedPercentColor(percent);\n\n        return <Cell value={value} percent={percent} color={percentColor} search={row.original.domain} />;\n    };\n\ninterface QueriedDomainsProps {\n    topQueriedDomains: unknown[];\n    dnsQueries: number;\n    refreshButton: React.ReactNode;\n    subtitle: string;\n    t: (...args: unknown[]) => string;\n}\n\nconst QueriedDomains = ({ t, refreshButton, topQueriedDomains, subtitle, dnsQueries }: QueriedDomainsProps) => (\n    <Card title={t('stats_query_domain')} subtitle={subtitle} bodyType=\"card-table\" refresh={refreshButton}>\n        <ReactTable\n            data={topQueriedDomains.map(({ name: domain, count }: any) => ({\n                domain,\n                count,\n            }))}\n            columns={[\n                {\n                    Header: <Trans>domain</Trans>,\n                    accessor: 'domain',\n                    Cell: DomainCell,\n                },\n                {\n                    Header: <Trans>requests_count</Trans>,\n                    accessor: 'count',\n                    maxWidth: 190,\n                    Cell: countCell(dnsQueries),\n                },\n            ]}\n            showPagination={false}\n            noDataText={t('no_domains_found')}\n            minRows={TABLES_MIN_ROWS}\n            defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}\n            className=\"-highlight card-table-overflow--limited stats__table\"\n        />\n    </Card>\n);\n\nexport default withTranslation()(QueriedDomains);\n"
  },
  {
    "path": "client/src/components/Dashboard/Statistics.tsx",
    "content": "import React from 'react';\n\nimport { Link } from 'react-router-dom';\nimport { withTranslation, Trans } from 'react-i18next';\n\nimport StatsCard from './StatsCard';\n\nimport { getPercent, normalizeHistory } from '../../helpers/helpers';\nimport { RESPONSE_FILTER } from '../../helpers/constants';\n\nconst getNormalizedHistory = (data: any, interval: any, id: any) => [{ data: normalizeHistory(data), id }];\n\ninterface StatisticsProps {\n    interval: number;\n    dnsQueries: number[];\n    blockedFiltering: unknown[];\n    replacedSafebrowsing: unknown[];\n    replacedParental: unknown[];\n    numDnsQueries: number;\n    numBlockedFiltering: number;\n    numReplacedSafebrowsing: number;\n    numReplacedParental: number;\n    refreshButton: React.ReactNode;\n}\n\nconst Statistics = ({\n    interval,\n    dnsQueries,\n    blockedFiltering,\n    replacedSafebrowsing,\n    replacedParental,\n    numDnsQueries,\n    numBlockedFiltering,\n    numReplacedSafebrowsing,\n    numReplacedParental,\n}: StatisticsProps) => (\n    <div className=\"row\">\n        <div className=\"col-sm-6 col-lg-3\">\n            <StatsCard\n                total={numDnsQueries}\n                lineData={getNormalizedHistory(dnsQueries, interval, 'dnsQuery')}\n                title={\n                    <Link to=\"logs\">\n                        <Trans>dns_query</Trans>\n                    </Link>\n                }\n                color=\"blue\"\n            />\n        </div>\n\n        <div className=\"col-sm-6 col-lg-3\">\n            <StatsCard\n                total={numBlockedFiltering}\n                lineData={getNormalizedHistory(blockedFiltering, interval, 'blockedFiltering')}\n                percent={getPercent(numDnsQueries, numBlockedFiltering)}\n                title={\n                    <Trans\n                        components={[\n                            <Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED.QUERY}`} key=\"0\">\n                                link\n                            </Link>,\n                        ]}>\n                        blocked_by\n                    </Trans>\n                }\n                color=\"red\"\n            />\n        </div>\n\n        <div className=\"col-sm-6 col-lg-3\">\n            <StatsCard\n                total={numReplacedSafebrowsing}\n                lineData={getNormalizedHistory(replacedSafebrowsing, interval, 'replacedSafebrowsing')}\n                percent={getPercent(numDnsQueries, numReplacedSafebrowsing)}\n                title={\n                    <Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_THREATS.QUERY}`}>\n                        <Trans>stats_malware_phishing</Trans>\n                    </Link>\n                }\n                color=\"green\"\n            />\n        </div>\n\n        <div className=\"col-sm-6 col-lg-3\">\n            <StatsCard\n                total={numReplacedParental}\n                lineData={getNormalizedHistory(replacedParental, interval, 'replacedParental')}\n                percent={getPercent(numDnsQueries, numReplacedParental)}\n                title={\n                    <Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY}`}>\n                        <Trans>stats_adult</Trans>\n                    </Link>\n                }\n                color=\"yellow\"\n            />\n        </div>\n    </div>\n);\n\nexport default withTranslation()(Statistics);\n"
  },
  {
    "path": "client/src/components/Dashboard/StatsCard.tsx",
    "content": "import React from 'react';\n\nimport { STATUS_COLORS } from '../../helpers/constants';\n\nimport { formatNumber } from '../../helpers/helpers';\n\nimport Card from '../ui/Card';\n\nimport Line from '../ui/Line';\n\ninterface StatsCardProps {\n    total: number;\n    lineData: unknown[];\n    title: object;\n    color: string;\n    percent?: number;\n}\n\nconst StatsCard = ({ total, lineData, percent, title, color }: StatsCardProps) => (\n    <Card type=\"card--full\" bodyType=\"card-wrap\">\n        <div className=\"card-body-stats\">\n            <div className={`card-value card-value-stats text-${color}`}>{formatNumber(total)}</div>\n\n            <div className=\"card-title-stats\">{title}</div>\n        </div>\n        {percent >= 0 && <div className={`card-value card-value-percent text-${color}`}>{percent}</div>}\n\n        <div className=\"card-chart-bg\">\n            <Line data={lineData} color={STATUS_COLORS[color]} />\n        </div>\n    </Card>\n);\n\nexport default StatsCard;\n"
  },
  {
    "path": "client/src/components/Dashboard/UpstreamAvgTime.tsx",
    "content": "import React from 'react';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport round from 'lodash/round';\nimport { withTranslation, Trans } from 'react-i18next';\n\nimport { TFunction } from 'i18next';\nimport Card from '../ui/Card';\n\nimport DomainCell from './DomainCell';\nimport { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, TABLES_MIN_ROWS } from '../../helpers/constants';\nimport { formatNumber } from '../../helpers/helpers';\n\ninterface TimeCellProps {\n    value?: string | number;\n}\n\nconst TimeCell = ({ value }: TimeCellProps) => {\n    if (!value) {\n        return '–';\n    }\n\n    const valueInMilliseconds = formatNumber(round(Number(value) * 1000));\n\n    return (\n        <div className=\"logs__row o-hidden\">\n            <span className=\"logs__text logs__text--full\" title={valueInMilliseconds.toString()}>\n                {valueInMilliseconds}&nbsp;ms\n            </span>\n        </div>\n    );\n};\n\ninterface UpstreamAvgTimeProps {\n    topUpstreamsAvgTime: { name: string; count: number }[];\n    refreshButton: React.ReactNode;\n    subtitle: string;\n    t: TFunction;\n}\n\nconst UpstreamAvgTime = ({ t, refreshButton, topUpstreamsAvgTime, subtitle }: UpstreamAvgTimeProps) => (\n    <Card title={t('average_upstream_response_time')} subtitle={subtitle} bodyType=\"card-table\" refresh={refreshButton}>\n        <ReactTable\n            data={topUpstreamsAvgTime.map(({ name: domain, count }: { name: string; count: number }) => ({\n                domain,\n                count,\n            }))}\n            columns={[\n                {\n                    Header: <Trans>upstream</Trans>,\n                    accessor: 'domain',\n                    Cell: DomainCell,\n                },\n                {\n                    Header: <Trans>response_time</Trans>,\n                    accessor: 'count',\n                    maxWidth: 190,\n                    Cell: TimeCell,\n                },\n            ]}\n            showPagination={false}\n            noDataText={t('no_upstreams_data_found')}\n            minRows={TABLES_MIN_ROWS}\n            defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}\n            className=\"-highlight card-table-overflow--limited stats__table\"\n        />\n    </Card>\n);\n\nexport default withTranslation()(UpstreamAvgTime);\n"
  },
  {
    "path": "client/src/components/Dashboard/UpstreamResponses.tsx",
    "content": "import React from 'react';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport { withTranslation, Trans } from 'react-i18next';\n\nimport { TFunction } from 'i18next';\nimport Card from '../ui/Card';\n\nimport Cell from '../ui/Cell';\n\nimport DomainCell from './DomainCell';\n\nimport { getPercent } from '../../helpers/helpers';\nimport { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';\n\nconst CountCell = (totalBlocked: any) =>\n    function cell(row: any) {\n        const { value } = row;\n        const percent = getPercent(totalBlocked, value);\n\n        return <Cell value={value} percent={percent} color={STATUS_COLORS.green} />;\n    };\n\nconst getTotalUpstreamRequests = (stats: any) => {\n    let total = 0;\n    stats.forEach(({ count }: any) => {\n        total += count;\n    });\n\n    return total;\n};\n\ninterface UpstreamResponsesProps {\n    topUpstreamsResponses: { name: string; count: number }[];\n    refreshButton: React.ReactNode;\n    subtitle: string;\n    t: TFunction;\n}\n\nconst UpstreamResponses = ({ t, refreshButton, topUpstreamsResponses, subtitle }: UpstreamResponsesProps) => (\n    <Card title={t('top_upstreams')} subtitle={subtitle} bodyType=\"card-table\" refresh={refreshButton}>\n        <ReactTable\n            data={topUpstreamsResponses.map(({ name: domain, count }: { name: string; count: number }) => ({\n                domain,\n                count,\n            }))}\n            columns={[\n                {\n                    Header: <Trans>upstream</Trans>,\n                    accessor: 'domain',\n                    Cell: DomainCell,\n                },\n                {\n                    Header: <Trans>requests_count</Trans>,\n                    accessor: 'count',\n                    maxWidth: 190,\n                    Cell: CountCell(getTotalUpstreamRequests(topUpstreamsResponses)),\n                },\n            ]}\n            showPagination={false}\n            noDataText={t('no_upstreams_data_found')}\n            minRows={TABLES_MIN_ROWS}\n            defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}\n            className=\"-highlight card-table-overflow--limited stats__table\"\n        />\n    </Card>\n);\n\nexport default withTranslation()(UpstreamResponses);\n"
  },
  {
    "path": "client/src/components/Dashboard/index.tsx",
    "content": "import React, { useEffect } from 'react';\n\nimport { HashLink as Link } from 'react-router-hash-link';\nimport { Trans, useTranslation } from 'react-i18next';\nimport classNames from 'classnames';\n\nimport Statistics from './Statistics';\nimport Counters from './Counters';\nimport Clients from './Clients';\nimport QueriedDomains from './QueriedDomains';\nimport BlockedDomains from './BlockedDomains';\nimport { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS, SETTINGS_URLS, TIME_UNITS } from '../../helpers/constants';\nimport { msToSeconds, msToMinutes, msToHours, msToDays } from '../../helpers/helpers';\n\nimport PageTitle from '../ui/PageTitle';\n\nimport Loading from '../ui/Loading';\nimport './Dashboard.css';\n\nimport Dropdown from '../ui/Dropdown';\nimport UpstreamResponses from './UpstreamResponses';\n\nimport UpstreamAvgTime from './UpstreamAvgTime';\nimport { AccessData, DashboardData, StatsData } from '../../initialState';\n\ninterface DashboardProps {\n    dashboard: DashboardData;\n    stats: StatsData;\n    access: AccessData;\n    getStats: (...args: unknown[]) => unknown;\n    getStatsConfig: (...args: unknown[]) => unknown;\n    toggleProtection: (...args: unknown[]) => unknown;\n    getClients: (...args: unknown[]) => unknown;\n    getAccessList: () => (dispatch: any) => void;\n}\n\nconst Dashboard = ({\n    getAccessList,\n    getStats,\n    getStatsConfig,\n    dashboard: { protectionEnabled, processingProtection, protectionDisabledDuration },\n    toggleProtection,\n    stats,\n    access,\n}: DashboardProps) => {\n    const { t } = useTranslation();\n\n    const getAllStats = () => {\n        getAccessList();\n        getStats();\n        getStatsConfig();\n    };\n\n    useEffect(() => {\n        getAllStats();\n    }, []);\n    const getSubtitle = () => {\n        if (!stats.enabled) {\n            return t('stats_disabled_short');\n        }\n\n        const msIn7Days = 604800000;\n\n        if (stats.timeUnits === TIME_UNITS.HOURS && stats.interval === msIn7Days) {\n            return t('for_last_days', { count: msToDays(stats.interval) });\n        }\n\n        return stats.timeUnits === TIME_UNITS.HOURS\n            ? t('for_last_hours', { count: msToHours(stats.interval) })\n            : t('for_last_days', { count: msToDays(stats.interval) });\n    };\n\n    const buttonClass = classNames('btn btn-sm dashboard-protection-button', {\n        'btn-gray': protectionEnabled,\n        'btn-success': !protectionEnabled,\n    });\n\n    const refreshButton = (\n        <button\n            type=\"button\"\n            className=\"btn btn-icon btn-outline-primary btn-sm\"\n            title={t('refresh_btn')}\n            onClick={() => getAllStats()}>\n            <svg className=\"icons icon12\">\n                <use xlinkHref=\"#refresh\" />\n            </svg>\n        </button>\n    );\n\n    const statsProcessing = stats.processingStats || stats.processingGetConfig || access.processing;\n\n    const subtitle = getSubtitle();\n\n    const DISABLE_PROTECTION_ITEMS = [\n        {\n            text: t('disable_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }),\n            disableTime: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE,\n        },\n        {\n            text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }),\n            disableTime: DISABLE_PROTECTION_TIMINGS.MINUTE,\n        },\n        {\n            text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }),\n            disableTime: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES,\n        },\n        {\n            text: t('disable_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }),\n            disableTime: DISABLE_PROTECTION_TIMINGS.HOUR,\n        },\n        {\n            text: t('disable_until_tomorrow'),\n            disableTime: DISABLE_PROTECTION_TIMINGS.TOMORROW,\n        },\n    ];\n\n    const getDisableProtectionItems = () =>\n        Object.values(DISABLE_PROTECTION_ITEMS).map((item: any, index: any) => (\n            <div\n                key={`disable_timings_${index}`}\n                className=\"dropdown-item\"\n                onClick={() => {\n                    toggleProtection(protectionEnabled, item.disableTime - ONE_SECOND_IN_MS);\n                }}>\n                {item.text}\n            </div>\n        ));\n\n    const getRemaningTimeText = (milliseconds: any) => {\n        if (!milliseconds) {\n            return '';\n        }\n\n        const date = new Date(milliseconds);\n        const hh = date.getUTCHours();\n        const mm = `0${date.getUTCMinutes()}`.slice(-2);\n        const ss = `0${date.getUTCSeconds()}`.slice(-2);\n        const formattedHH = `0${hh}`.slice(-2);\n\n        return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`;\n    };\n\n    const getProtectionBtnText = (status: any) => (status ? t('disable_protection') : t('enable_protection'));\n\n    return (\n        <>\n            <PageTitle title={t('dashboard')} containerClass=\"page-title--dashboard\">\n                <div className=\"page-title__protection\">\n                    <button\n                        type=\"button\"\n                        className={buttonClass}\n                        onClick={() => {\n                            toggleProtection(protectionEnabled);\n                        }}\n                        disabled={processingProtection}>\n                        {protectionDisabledDuration\n                            ? `${t('enable_protection_timer', { time: getRemaningTimeText(protectionDisabledDuration) })}`\n                            : getProtectionBtnText(protectionEnabled)}\n                    </button>\n\n                    {protectionEnabled && (\n                        <Dropdown\n                            label=\"\"\n                            baseClassName=\"dropdown-protection\"\n                            icon=\"arrow-down\"\n                            controlClassName=\"dropdown-protection__toggle\"\n                            menuClassName=\"dropdown-menu dropdown-menu-arrow dropdown-menu--protection\">\n                            {getDisableProtectionItems()}\n                        </Dropdown>\n                    )}\n                </div>\n\n                <button type=\"button\" className=\"btn btn-outline-primary btn-sm\" onClick={getAllStats}>\n                    <Trans>refresh_statics</Trans>\n                </button>\n            </PageTitle>\n\n            {statsProcessing && <Loading />}\n\n            {!statsProcessing && (\n                <div className=\"row row-cards dashboard\">\n                    <div className=\"col-lg-12\">\n                        {stats.interval === 0 && (\n                            <div className=\"alert alert-warning\" role=\"alert\">\n                                <Trans\n                                    components={[\n                                        <Link to={`${SETTINGS_URLS.settings}#stats-config`} key=\"0\">\n                                            link\n                                        </Link>,\n                                    ]}>\n                                    stats_disabled\n                                </Trans>\n                            </div>\n                        )}\n\n                        <Statistics\n                            interval={msToDays(stats.interval)}\n                            dnsQueries={stats.dnsQueries}\n                            blockedFiltering={stats.blockedFiltering}\n                            replacedSafebrowsing={stats.replacedSafebrowsing}\n                            replacedParental={stats.replacedParental}\n                            numDnsQueries={stats.numDnsQueries}\n                            numBlockedFiltering={stats.numBlockedFiltering}\n                            numReplacedSafebrowsing={stats.numReplacedSafebrowsing}\n                            numReplacedParental={stats.numReplacedParental}\n                            refreshButton={refreshButton}\n                        />\n                    </div>\n\n                    <div className=\"col-lg-6\">\n                        <Counters subtitle={subtitle} refreshButton={refreshButton} />\n                    </div>\n\n                    <div className=\"col-lg-6\">\n                        <Clients subtitle={subtitle} refreshButton={refreshButton} />\n                    </div>\n\n                    <div className=\"col-lg-6\">\n                        <QueriedDomains\n                            subtitle={subtitle}\n                            dnsQueries={stats.numDnsQueries}\n                            topQueriedDomains={stats.topQueriedDomains}\n                            refreshButton={refreshButton}\n                        />\n                    </div>\n\n                    <div className=\"col-lg-6\">\n                        <BlockedDomains\n                            subtitle={subtitle}\n                            topBlockedDomains={stats.topBlockedDomains}\n                            blockedFiltering={stats.numBlockedFiltering}\n                            replacedSafebrowsing={stats.numReplacedSafebrowsing}\n                            replacedSafesearch={stats.numReplacedSafesearch}\n                            replacedParental={stats.numReplacedParental}\n                            refreshButton={refreshButton}\n                        />\n                    </div>\n\n                    <div className=\"col-lg-6\">\n                        <UpstreamResponses\n                            subtitle={subtitle}\n                            topUpstreamsResponses={stats.topUpstreamsResponses}\n                            refreshButton={refreshButton}\n                        />\n                    </div>\n\n                    <div className=\"col-lg-6\">\n                        <UpstreamAvgTime\n                            subtitle={subtitle}\n                            topUpstreamsAvgTime={stats.topUpstreamsAvgTime}\n                            refreshButton={refreshButton}\n                        />\n                    </div>\n                </div>\n            )}\n        </>\n    );\n};\n\nexport default Dashboard;\n"
  },
  {
    "path": "client/src/components/Filters/Actions.tsx",
    "content": "import React from 'react';\nimport { withTranslation, Trans } from 'react-i18next';\n\ninterface ActionsProps {\n    handleAdd: (...args: unknown[]) => unknown;\n    handleRefresh: (...args: unknown[]) => unknown;\n    processingRefreshFilters: boolean;\n    whitelist?: boolean;\n}\n\nconst Actions = ({ handleAdd, handleRefresh, processingRefreshFilters, whitelist }: ActionsProps) => (\n    <div className=\"card-actions\">\n        <button className=\"btn btn-success btn-standard mr-2 btn-large mb-2\" type=\"submit\" onClick={handleAdd}>\n            {whitelist ? <Trans>add_allowlist</Trans> : <Trans>add_blocklist</Trans>}\n        </button>\n\n        <button\n            className=\"btn btn-primary btn-standard mb-2\"\n            type=\"submit\"\n            onClick={handleRefresh}\n            disabled={processingRefreshFilters}>\n            <Trans>check_updates_btn</Trans>\n        </button>\n    </div>\n);\n\nexport default withTranslation()(Actions);\n"
  },
  {
    "path": "client/src/components/Filters/Check/Info.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport classNames from 'classnames';\n\nimport i18next from 'i18next';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\nimport {\n    checkFiltered,\n    checkRewrite,\n    checkRewriteHosts,\n    checkWhiteList,\n    checkSafeSearch,\n    checkSafeBrowsing,\n    checkParental,\n    getRulesToFilterList,\n} from '../../../helpers/helpers';\nimport { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants';\n\nimport { toggleBlocking } from '../../../actions';\nimport { RootState } from '../../../initialState';\n\nconst renderBlockingButton = (isFiltered: any, domain: any) => {\n    const processingRules = useSelector((state: RootState) => state.filtering.processingRules);\n    const dispatch = useDispatch();\n    const { t } = useTranslation();\n\n    const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;\n\n    const onClick = async () => {\n        await dispatch(toggleBlocking(buttonType, domain));\n    };\n\n    const buttonClass = classNames(\n        'mt-3 button-action button-action--main button-action--active button-action--small',\n        {\n            'button-action--unblock': isFiltered,\n        },\n    );\n\n    return (\n        <button type=\"button\" className={buttonClass} onClick={onClick} disabled={processingRules}>\n            {t(buttonType)}\n        </button>\n    );\n};\n\nconst getTitle = () => {\n    const { t } = useTranslation();\n\n    const filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual);\n\n    const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual);\n\n    const rules = useSelector((state: RootState) => state.filtering.check.rules, shallowEqual);\n\n    const reason = useSelector((state: RootState) => state.filtering.check.reason);\n\n    const getReasonFiltered = (reason: any) => {\n        const filterKey = reason.replace(FILTERED, '');\n        return i18next.t('query_log_filtered', { filter: filterKey });\n    };\n\n    const ruleAndFilterNames = getRulesToFilterList(rules, filters, whitelistFilters);\n\n    const REASON_TO_TITLE_MAP = {\n        [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: t('check_not_found'),\n        [FILTERED_STATUS.REWRITE]: t('rewrite_applied'),\n        [FILTERED_STATUS.REWRITE_HOSTS]: t('rewrite_hosts_applied'),\n        [FILTERED_STATUS.FILTERED_BLACK_LIST]: ruleAndFilterNames,\n        [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: ruleAndFilterNames,\n        [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason),\n        [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason),\n        [FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason),\n    };\n\n    if (Object.prototype.hasOwnProperty.call(REASON_TO_TITLE_MAP, reason)) {\n        return REASON_TO_TITLE_MAP[reason];\n    }\n\n    return (\n        <>\n            <div>{t('check_reason', { reason })}</div>\n\n            <div>\n                {t('rule_label')}: &nbsp;\n                {ruleAndFilterNames}\n            </div>\n        </>\n    );\n};\n\nconst Info = () => {\n    const { hostname, reason, service_name, cname, ip_addrs } = useSelector(\n        (state: RootState) => state.filtering.check,\n        shallowEqual,\n    );\n    const { t } = useTranslation();\n\n    const title = getTitle();\n\n    const className = classNames('card mb-0 p-3', {\n        'logs__row--red': checkFiltered(reason),\n        'logs__row--blue': checkRewrite(reason) || checkRewriteHosts(reason),\n        'logs__row--green': checkWhiteList(reason),\n    });\n\n    const onlyFiltered = checkSafeSearch(reason) || checkSafeBrowsing(reason) || checkParental(reason);\n\n    const isFiltered = checkFiltered(reason);\n\n    return (\n        <div className={className}>\n            <div>\n                <strong>{hostname}</strong>\n            </div>\n\n            <div>{title}</div>\n            {!onlyFiltered && (\n                <>\n                    {service_name && <div>{t('check_service', { service: service_name })}</div>}\n\n                    {cname && <div>{t('check_cname', { cname })}</div>}\n\n                    {ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>}\n                    {renderBlockingButton(isFiltered, hostname)}\n                </>\n            )}\n        </div>\n    );\n};\n\nexport default Info;\n"
  },
  {
    "path": "client/src/components/Filters/Check/index.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useSelector } from 'react-redux';\nimport { Controller, useForm } from 'react-hook-form';\n\nimport Card from '../../ui/Card';\nimport Info from './Info';\n\nimport { RootState } from '../../../initialState';\nimport { validateRequiredValue } from '../../../helpers/validators';\nimport { Input } from '../../ui/Controls/Input';\nimport { DNS_RECORD_TYPES } from '../../../helpers/constants';\nimport { Select } from '../../ui/Controls/Select';\n\nexport type FilteringCheckFormValues = {\n    name: string;\n    client?: string;\n    qtype?: string;\n}\n\ntype Props = {\n    onSubmit?: (data: FilteringCheckFormValues) => void;\n};\n\nconst Check = ({ onSubmit }: Props) => {\n    const { t } = useTranslation();\n\n    const processingCheck = useSelector((state: RootState) => state.filtering.processingCheck);\n    const hostname = useSelector((state: RootState) => state.filtering.check.hostname);\n\n    const {\n        control,\n        handleSubmit,\n        formState: { isValid },\n    } = useForm<FilteringCheckFormValues>({\n        mode: 'onBlur',\n        defaultValues: {\n            name: '',\n            client: '',\n            qtype: DNS_RECORD_TYPES[0],\n        },\n    });\n\n    return (\n        <Card title={t('check_title')} subtitle={t('check_desc')}>\n            <form onSubmit={handleSubmit(onSubmit)}>\n                <div className=\"row\">\n                    <div className=\"col-12 col-md-6\">\n                        <Controller\n                            name=\"name\"\n                            control={control}\n                            rules={{ validate: validateRequiredValue }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"text\"\n                                    label={t('check_hostname')}\n                                    data-testid=\"check_domain_name\"\n                                    placeholder=\"example.com\"\n                                    error={fieldState.error?.message}\n                                />\n                            )}\n                        />\n\n                        <Controller\n                            name=\"client\"\n                            control={control}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"text\"\n                                    data-testid=\"check_client_id\"\n                                    label={t('check_client_id')}\n                                    placeholder={t('check_enter_client_id')}\n                                    error={fieldState.error?.message}\n                                />\n                            )}\n                        />\n\n                        <Controller\n                            name=\"qtype\"\n                            control={control}\n                            render={({ field }) => (\n                                <Select\n                                    {...field}\n                                    label={t('check_dns_record')}\n                                    data-testid=\"check_dns_record_type\"\n                                >\n                                    {DNS_RECORD_TYPES.map((type) => (\n                                        <option key={type} value={type}>\n                                            {type}\n                                        </option>\n                                    ))}\n                                </Select>\n                            )}\n                        />\n\n                        <button\n                            className=\"btn btn-success btn-standard btn-large\"\n                            type=\"submit\"\n                            data-testid=\"check_domain_submit\"\n                            disabled={!isValid || processingCheck}\n                        >\n                            {t('check')}\n                        </button>\n\n                        {hostname && (\n                            <>\n                                <hr />\n                                <Info />\n                            </>\n                        )}\n                    </div>\n                </div>\n            </form>\n        </Card>\n    );\n};\n\nexport default Check;\n"
  },
  {
    "path": "client/src/components/Filters/CustomRules.tsx",
    "content": "import React, { Component } from 'react';\nimport { Trans, withTranslation } from 'react-i18next';\n\nimport Card from '../ui/Card';\n\nimport PageTitle from '../ui/PageTitle';\n\nimport Examples from './Examples';\n\nimport Check, { FilteringCheckFormValues } from './Check';\n\nimport { getTextareaCommentsHighlight, syncScroll } from '../../helpers/highlightTextareaComments';\nimport { COMMENT_LINE_DEFAULT_TOKEN } from '../../helpers/constants';\nimport '../ui/texareaCommentsHighlight.css';\nimport { FilteringData } from '../../initialState';\n\ninterface CustomRulesProps {\n    filtering: FilteringData;\n    setRules: (...args: unknown[]) => unknown;\n    checkHost: (...args: unknown[]) => string;\n    getFilteringStatus: (...args: unknown[]) => unknown;\n    handleRulesChange: (...args: unknown[]) => unknown;\n    t: (...args: unknown[]) => string;\n}\n\nclass CustomRules extends Component<CustomRulesProps> {\n    ref = React.createRef();\n\n    componentDidMount() {\n        this.props.getFilteringStatus();\n    }\n\n    handleChange = (e: any) => {\n        const { value } = e.currentTarget;\n        this.handleRulesChange(value);\n    };\n\n    handleSubmit = (e: any) => {\n        e.preventDefault();\n        this.handleRulesSubmit();\n    };\n\n    handleRulesChange = (value: any) => {\n        this.props.handleRulesChange({ userRules: value });\n    };\n\n    handleRulesSubmit = () => {\n        this.props.setRules(this.props.filtering.userRules);\n    };\n\n    handleCheck = (values: FilteringCheckFormValues) => {\n        const params: FilteringCheckFormValues = { name: values.name };\n\n        if (values.client) {\n            params.client = values.client;\n        }\n\n        if (values.qtype) {\n            params.qtype = values.qtype;\n        }\n\n        this.props.checkHost(params);\n    };\n\n    onScroll = (e: any) => syncScroll(e, this.ref);\n\n    render() {\n        const {\n            t,\n            filtering: { userRules },\n        } = this.props;\n\n        return (\n            <>\n                <PageTitle title={t('custom_filtering_rules')} />\n\n                <Card subtitle={t('custom_filter_rules_hint')}>\n                    <form onSubmit={this.handleSubmit}>\n                        <div className=\"text-edit-container mb-4\">\n                            <textarea\n                                data-testid=\"custom_rule_textarea\"\n                                className=\"form-control font-monospace text-input\"\n                                value={userRules}\n                                onChange={this.handleChange}\n                                onScroll={this.onScroll}\n                            />\n                            {getTextareaCommentsHighlight(this.ref, userRules, [\n                                COMMENT_LINE_DEFAULT_TOKEN,\n                                '!',\n                            ])}\n                        </div>\n\n                        <div className=\"card-actions\">\n                            <button\n                                data-testid=\"apply_custom_rule\"\n                                className=\"btn btn-success btn-standard btn-large\"\n                                type=\"submit\"\n                                onClick={this.handleSubmit}>\n                                <Trans>apply_btn</Trans>\n                            </button>\n                        </div>\n                    </form>\n\n                    <hr />\n\n                    <Examples />\n                </Card>\n\n                <Check onSubmit={this.handleCheck} />\n            </>\n        );\n    }\n}\n\nexport default withTranslation()(CustomRules);\n"
  },
  {
    "path": "client/src/components/Filters/DnsAllowlist.tsx",
    "content": "import React, { Component } from 'react';\nimport { withTranslation } from 'react-i18next';\n\nimport PageTitle from '../ui/PageTitle';\nimport Card from '../ui/Card';\nimport Modal from './Modal';\nimport Actions from './Actions';\nimport Table from './Table';\n\nimport { MODAL_TYPE } from '../../helpers/constants';\n\nimport { getCurrentFilter } from '../../helpers/helpers';\n\ninterface DnsAllowlistProps {\n    getFilteringStatus: (...args: unknown[]) => unknown;\n    filtering: {\n        modalType: string;\n        modalFilterUrl: string;\n        isModalOpen: boolean;\n        isFilterAdded: boolean;\n        processingRefreshFilters: boolean;\n        processingRemoveFilter: boolean;\n        processingAddFilter: boolean;\n        processingConfigFilter: boolean;\n        processingFilters: boolean;\n        whitelistFilters: any[];\n    };\n    removeFilter: (...args: unknown[]) => unknown;\n    toggleFilterStatus: (...args: unknown[]) => unknown;\n    addFilter: (...args: unknown[]) => unknown;\n    toggleFilteringModal: (...args: unknown[]) => unknown;\n    handleRulesChange: (...args: unknown[]) => unknown;\n    refreshFilters: (...args: unknown[]) => unknown;\n    editFilter: (...args: unknown[]) => unknown;\n    t: (...args: unknown[]) => string;\n}\n\nclass DnsAllowlist extends Component<DnsAllowlistProps> {\n    componentDidMount() {\n        this.props.getFilteringStatus();\n    }\n\n    handleSubmit = (values: any) => {\n        const { name, url } = values;\n\n        const { filtering } = this.props;\n        const whitelist = true;\n\n        if (filtering.modalType === MODAL_TYPE.EDIT_FILTERS) {\n            this.props.editFilter(filtering.modalFilterUrl, values, whitelist);\n        } else {\n            this.props.addFilter(url, name, whitelist);\n        }\n    };\n\n    handleDelete = (url: any) => {\n        if (window.confirm(this.props.t('list_confirm_delete'))) {\n            const whitelist = true;\n\n            this.props.removeFilter(url, whitelist);\n        }\n    };\n\n    toggleFilter = (url: any, data: any) => {\n        const whitelist = true;\n\n        this.props.toggleFilterStatus(url, data, whitelist);\n    };\n\n    handleRefresh = () => {\n        this.props.refreshFilters({ whitelist: true });\n    };\n\n    openAddFiltersModal = () => {\n        this.props.toggleFilteringModal({ type: MODAL_TYPE.ADD_FILTERS });\n    };\n\n    render() {\n        const {\n            t,\n            toggleFilteringModal,\n            addFilter,\n            filtering: {\n                whitelistFilters,\n                isModalOpen,\n                isFilterAdded,\n                processingRefreshFilters,\n                processingRemoveFilter,\n                processingAddFilter,\n                processingConfigFilter,\n                processingFilters,\n                modalType,\n                modalFilterUrl,\n            },\n        } = this.props;\n        const currentFilterData = getCurrentFilter(modalFilterUrl, whitelistFilters);\n        const loading =\n            processingConfigFilter ||\n            processingFilters ||\n            processingAddFilter ||\n            processingRemoveFilter ||\n            processingRefreshFilters;\n\n        const whitelist = true;\n\n        return (\n            <>\n                <PageTitle title={t('dns_allowlists')} subtitle={t('dns_allowlists_desc')} />\n                <div className=\"content\">\n                    <div className=\"row\">\n                        <div className=\"col-md-12\">\n                            <Card subtitle={t('filters_and_hosts_hint')}>\n                                <Table\n                                    filters={whitelistFilters}\n                                    loading={loading}\n                                    processingConfigFilter={processingConfigFilter}\n                                    toggleFilteringModal={toggleFilteringModal}\n                                    handleDelete={this.handleDelete}\n                                    toggleFilter={this.toggleFilter}\n                                    whitelist={whitelist}\n                                />\n\n                                <Actions\n                                    handleAdd={this.openAddFiltersModal}\n                                    handleRefresh={this.handleRefresh}\n                                    processingRefreshFilters={processingRefreshFilters}\n                                    whitelist={whitelist}\n                                />\n                            </Card>\n                        </div>\n                    </div>\n                </div>\n\n                <Modal\n                    filters={whitelistFilters}\n                    isOpen={isModalOpen}\n                    toggleFilteringModal={toggleFilteringModal}\n                    addFilter={addFilter}\n                    isFilterAdded={isFilterAdded}\n                    processingAddFilter={processingAddFilter}\n                    processingConfigFilter={processingConfigFilter}\n                    handleSubmit={this.handleSubmit}\n                    modalType={modalType}\n                    currentFilterData={currentFilterData}\n                    whitelist={whitelist}\n                />\n            </>\n        );\n    }\n}\n\nexport default withTranslation()(DnsAllowlist);\n"
  },
  {
    "path": "client/src/components/Filters/DnsBlocklist.tsx",
    "content": "import React, { Component } from 'react';\nimport { withTranslation } from 'react-i18next';\n\nimport PageTitle from '../ui/PageTitle';\n\nimport Card from '../ui/Card';\nimport Modal from './Modal';\nimport Actions from './Actions';\nimport Table from './Table';\nimport { MODAL_TYPE } from '../../helpers/constants';\n\nimport { getCurrentFilter } from '../../helpers/helpers';\n\nimport filtersCatalog from '../../helpers/filters/filters';\nimport { FilteringData } from '../../initialState';\n\ninterface DnsBlocklistProps {\n    getFilteringStatus: (...args: unknown[]) => unknown;\n    filtering: FilteringData;\n    removeFilter: (...args: unknown[]) => unknown;\n    toggleFilterStatus: (...args: unknown[]) => unknown;\n    addFilter: (...args: unknown[]) => unknown;\n    toggleFilteringModal: (...args: unknown[]) => unknown;\n    handleRulesChange: (...args: unknown[]) => unknown;\n    refreshFilters: (...args: unknown[]) => unknown;\n    editFilter: (...args: unknown[]) => unknown;\n    t: (...args: unknown[]) => string;\n}\n\nclass DnsBlocklist extends Component<DnsBlocklistProps> {\n    componentDidMount() {\n        this.props.getFilteringStatus();\n    }\n\n    handleSubmit = (values: any) => {\n        const { modalFilterUrl, modalType } = this.props.filtering;\n\n        switch (modalType) {\n            case MODAL_TYPE.EDIT_FILTERS:\n                this.props.editFilter(modalFilterUrl, values);\n                break;\n            case MODAL_TYPE.ADD_FILTERS: {\n                const { name, url } = values;\n\n                this.props.addFilter(url, name);\n                break;\n            }\n            case MODAL_TYPE.CHOOSE_FILTERING_LIST: {\n                const changedValues = Object.entries(values)?.reduce((acc: any, [key, value]) => {\n                    if (value && key in filtersCatalog.filters) {\n                        acc[key] = value;\n                    }\n                    return acc;\n                }, {});\n\n                Object.keys(changedValues).forEach((fieldName) => {\n                    // filterId is actually in the field name\n\n                    const { source, name } = filtersCatalog.filters[fieldName];\n\n                    this.props.addFilter(source, name);\n                });\n                break;\n            }\n            default:\n                break;\n        }\n    };\n\n    handleDelete = (url: any) => {\n        if (window.confirm(this.props.t('list_confirm_delete'))) {\n            this.props.removeFilter(url);\n        }\n    };\n\n    toggleFilter = (url: any, data: any) => {\n        this.props.toggleFilterStatus(url, data);\n    };\n\n    handleRefresh = () => {\n        this.props.refreshFilters({ whitelist: false });\n    };\n\n    openSelectTypeModal = () => {\n        this.props.toggleFilteringModal({ type: MODAL_TYPE.SELECT_MODAL_TYPE });\n    };\n\n    render() {\n        const {\n            t,\n\n            toggleFilteringModal,\n\n            addFilter,\n\n            filtering: {\n                filters,\n                isModalOpen,\n                isFilterAdded,\n                processingRefreshFilters,\n                processingRemoveFilter,\n                processingAddFilter,\n                processingConfigFilter,\n                processingFilters,\n                modalType,\n                modalFilterUrl,\n            },\n        } = this.props;\n        const currentFilterData = getCurrentFilter(modalFilterUrl, filters);\n        const loading =\n            processingConfigFilter ||\n            processingFilters ||\n            processingAddFilter ||\n            processingRemoveFilter ||\n            processingRefreshFilters;\n\n        return (\n            <>\n                <PageTitle title={t('dns_blocklists')} subtitle={t('dns_blocklists_desc')} />\n\n                <div className=\"content\">\n                    <div className=\"row\">\n                        <div className=\"col-md-12\">\n                            <Card subtitle={t('filters_and_hosts_hint')}>\n                                <Table\n                                    filters={filters}\n                                    loading={loading}\n                                    processingConfigFilter={processingConfigFilter}\n                                    toggleFilteringModal={toggleFilteringModal}\n                                    handleDelete={this.handleDelete}\n                                    toggleFilter={this.toggleFilter}\n                                />\n\n                                <Actions\n                                    handleAdd={this.openSelectTypeModal}\n                                    handleRefresh={this.handleRefresh}\n                                    processingRefreshFilters={processingRefreshFilters}\n                                />\n                            </Card>\n                        </div>\n                    </div>\n                </div>\n\n                <Modal\n                    filtersCatalog={filtersCatalog}\n                    filters={filters}\n                    isOpen={isModalOpen}\n                    toggleFilteringModal={toggleFilteringModal}\n                    addFilter={addFilter}\n                    isFilterAdded={isFilterAdded}\n                    processingAddFilter={processingAddFilter}\n                    processingConfigFilter={processingConfigFilter}\n                    handleSubmit={this.handleSubmit}\n                    modalType={modalType}\n                    currentFilterData={currentFilterData}\n                />\n            </>\n        );\n    }\n}\n\nexport default withTranslation()(DnsBlocklist);\n"
  },
  {
    "path": "client/src/components/Filters/Examples.tsx",
    "content": "import React, { Fragment } from 'react';\nimport { withTranslation, Trans } from 'react-i18next';\n\nconst Examples = () => (\n    <Fragment>\n        <div className=\"list leading-loose\">\n            <Trans>examples_title</Trans>:\n            <ol className=\"leading-loose\">\n                <li>\n                    <code>||example.org^</code>:<Trans>example_meaning_filter_block</Trans>\n                </li>\n\n                <li>\n                    <code> @@||example.org^</code>:<Trans>example_meaning_filter_whitelist</Trans>\n                </li>\n\n                <li>\n                    <code>127.0.0.1 example.org</code>:<Trans>example_meaning_host_block</Trans>\n                </li>\n\n                <li>\n                    <code>\n                        <Trans>example_comment</Trans>\n                    </code>\n                    :<Trans>example_comment_meaning</Trans>\n                </li>\n\n                <li>\n                    <code>\n                        <Trans>example_comment_hash</Trans>\n                    </code>\n                    :<Trans>example_comment_meaning</Trans>\n                </li>\n\n                <li>\n                    <code>/REGEX/</code>:<Trans>example_regex_meaning</Trans>\n                </li>\n            </ol>\n        </div>\n\n        <p className=\"mt-1\">\n            <Trans\n                components={[\n                    <a\n                        href=\"https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax&from=ui&app=home\"\n                        target=\"_blank\"\n                        rel=\"noopener noreferrer\"\n                        key=\"0\">\n                        link\n                    </a>,\n                ]}>\n                filtering_rules_learn_more\n            </Trans>\n        </p>\n    </Fragment>\n);\n\nexport default withTranslation()(Examples);\n"
  },
  {
    "path": "client/src/components/Filters/FiltersList.tsx",
    "content": "import React from 'react';\nimport classNames from 'classnames';\nimport { Controller, useFormContext } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { Checkbox } from '../ui/Controls/Checkbox';\n\nconst getIconsData = (homepage: string, source: string) => [\n    {\n        iconName: 'dashboard',\n        href: homepage,\n        className: 'ml-1',\n    },\n    {\n        iconName: 'info',\n        href: source,\n    },\n];\n\nconst renderIcons = (iconsData: { iconName: string; href: string; className?: string }[]) =>\n    iconsData.map(({ iconName, href, className = '' }) => (\n        <a\n            key={iconName}\n            href={href}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className={classNames('d-flex align-items-center', className)}>\n            <svg className=\"icon icon--15 mr-1 icon--gray\">\n                <use xlinkHref={`#${iconName}`} />\n            </svg>\n        </a>\n    ));\n\ntype Filter = {\n    categoryId: string;\n    homepage: string;\n    source: string;\n    name: string;\n};\n\ntype Category = {\n    name: string;\n    description: string;\n};\n\ntype Props = {\n    categories: Record<string, Category>;\n    filters: Record<string, Filter>;\n    selectedSources: Record<string, boolean>;\n};\n\nexport const FiltersList = ({ categories, filters, selectedSources }: Props) => {\n    const { t } = useTranslation();\n    const { control } = useFormContext();\n\n    return (\n        <>\n            {Object.entries(categories).map(([categoryId, category]) => {\n                const categoryFilters = Object.entries(filters)\n                    .filter(([, filter]) => filter.categoryId === categoryId)\n                    .map(([key, filter]) => ({ ...filter, id: key }));\n\n                return (\n                    <div key={category.name} className=\"modal-body__item\">\n                        <h6 className=\"font-weight-bold mb-1\">{t(category.name)}</h6>\n                        <p className=\"mb-3\">{t(category.description)}</p>\n                        {categoryFilters.map((filter) => {\n                            const { homepage, source, name, id } = filter;\n                            const isSelected = selectedSources[source];\n                            const iconsData = getIconsData(homepage, source);\n\n                            return (\n                                <div key={name} className=\"d-flex align-items-center pb-1\">\n                                    <Controller\n                                        name={id}\n                                        control={control}\n                                        render={({ field }) => (\n                                            <Checkbox\n                                                {...field}\n                                                data-testid={`filters_${id}`}\n                                                title={name}\n                                                disabled={isSelected}\n                                            />\n                                        )}\n                                    />\n                                    {renderIcons(iconsData)}\n                                </div>\n                            );\n                        })}\n                    </div>\n                );\n            })}\n        </>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Filters/Form.tsx",
    "content": "import React from 'react';\nimport { useForm, Controller, FormProvider } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { validatePath, validateRequiredValue } from '../../helpers/validators';\n\nimport { MODAL_OPEN_TIMEOUT, MODAL_TYPE } from '../../helpers/constants';\nimport filtersCatalog from '../../helpers/filters/filters';\nimport { FiltersList } from './FiltersList';\nimport { Input } from '../ui/Controls/Input';\n\ntype FormValues = {\n    enabled: boolean;\n    name: string;\n    url: string;\n};\n\nconst defaultValues: FormValues = {\n    enabled: true,\n    name: '',\n    url: '',\n};\n\ntype Props = {\n    closeModal: () => void;\n    onSubmit: (values: FormValues) => void;\n    processingAddFilter: boolean;\n    processingConfigFilter: boolean;\n    whitelist?: boolean;\n    modalType: string;\n    toggleFilteringModal: ({ type }: { type?: keyof typeof MODAL_TYPE }) => void;\n    selectedSources?: Record<string, boolean>;\n    initialValues?: FormValues;\n};\n\nexport const Form = ({\n    closeModal,\n    processingAddFilter,\n    processingConfigFilter,\n    whitelist,\n    modalType,\n    toggleFilteringModal,\n    selectedSources,\n    onSubmit,\n    initialValues,\n}: Props) => {\n    const { t } = useTranslation();\n\n    const methods = useForm({\n        defaultValues: {\n            ...defaultValues,\n            ...initialValues,\n        },\n        mode: 'onBlur',\n    });\n    const { handleSubmit, control } = methods;\n\n    const openModal = (modalType: keyof typeof MODAL_TYPE, timeout = MODAL_OPEN_TIMEOUT) => {\n        toggleFilteringModal(undefined);\n        setTimeout(() => toggleFilteringModal({ type: modalType }), timeout);\n    };\n\n    const openFilteringListModal = () => openModal('CHOOSE_FILTERING_LIST');\n\n    const openAddFiltersModal = () => openModal('ADD_FILTERS');\n\n    return (\n        <FormProvider {...methods}>\n            <form onSubmit={handleSubmit(onSubmit)}>\n                <div className=\"modal-body modal-body--filters\">\n                    {modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (\n                        <div className=\"d-flex justify-content-around\">\n                            <button\n                                onClick={openFilteringListModal}\n                                className=\"btn btn-success btn-standard mr-2 btn-large\">\n                                {t('choose_from_list')}\n                            </button>\n\n                            <button onClick={openAddFiltersModal} className=\"btn btn-primary btn-standard\">\n                                {t('add_custom_list')}\n                            </button>\n                        </div>\n                    )}\n                    {modalType === MODAL_TYPE.CHOOSE_FILTERING_LIST && (\n                        <FiltersList\n                            categories={filtersCatalog.categories}\n                            filters={filtersCatalog.filters}\n                            selectedSources={selectedSources}\n                        />\n                    )}\n                    {modalType !== MODAL_TYPE.CHOOSE_FILTERING_LIST && modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (\n                        <>\n                            <div className=\"form__group\">\n                                <Controller\n                                    name=\"name\"\n                                    control={control}\n                                    render={({ field, fieldState }) => (\n                                        <Input\n                                            {...field}\n                                            type=\"text\"\n                                            data-testid=\"filters_name\"\n                                            placeholder={t('enter_name_hint')}\n                                            error={fieldState.error?.message}\n                                            trimOnBlur\n                                        />\n                                    )}\n                                />\n                            </div>\n\n                            <div className=\"form__group\">\n                                <Controller\n                                    name=\"url\"\n                                    control={control}\n                                    rules={{ validate: { validateRequiredValue, validatePath } }}\n                                    render={({ field, fieldState }) => (\n                                        <Input\n                                            {...field}\n                                            type=\"text\"\n                                            data-testid=\"filters_url\"\n                                            placeholder={t('enter_url_or_path_hint')}\n                                            error={fieldState.error?.message}\n                                            trimOnBlur\n                                        />\n                                    )}\n                                />\n                            </div>\n\n                            <div className=\"form__description\">\n                                {whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}\n                            </div>\n                        </>\n                    )}\n                </div>\n\n                <div className=\"modal-footer\">\n                    <button type=\"button\" className=\"btn btn-secondary\" onClick={closeModal}>\n                        {t('cancel_btn')}\n                    </button>\n\n                    {modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (\n                        <button\n                            type=\"submit\"\n                            data-testid=\"filters_save\"\n                            className=\"btn btn-success\"\n                            disabled={processingAddFilter || processingConfigFilter}>\n                            {t('save_btn')}\n                        </button>\n                    )}\n                </div>\n            </form>\n        </FormProvider>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Filters/Modal.tsx",
    "content": "import React, { Component } from 'react';\n\nimport ReactModal from 'react-modal';\nimport { withTranslation } from 'react-i18next';\n\nimport { MODAL_TYPE } from '../../helpers/constants';\n\nimport { Form } from './Form';\nimport '../ui/Modal.css';\n\nimport { getMap } from '../../helpers/helpers';\n\nReactModal.setAppElement('#root');\n\nconst MODAL_TYPE_TO_TITLE_TYPE_MAP = {\n    [MODAL_TYPE.EDIT_FILTERS]: 'edit',\n    [MODAL_TYPE.ADD_FILTERS]: 'new',\n    [MODAL_TYPE.EDIT_CLIENT]: 'edit',\n    [MODAL_TYPE.ADD_CLIENT]: 'new',\n    [MODAL_TYPE.SELECT_MODAL_TYPE]: 'new',\n    [MODAL_TYPE.CHOOSE_FILTERING_LIST]: 'choose',\n};\n\n/**\n * @param modalType {'EDIT_FILTERS' | 'ADD_FILTERS' | 'CHOOSE_FILTERING_LIST'}\n * @param whitelist {boolean}\n * @returns {'new_allowlist' | 'edit_allowlist' | 'choose_allowlist' |\n *           'new_blocklist' | 'edit_blocklist' | 'choose_blocklist' | null}\n */\nconst getTitle = (modalType: any, whitelist: any) => {\n    const titleType = MODAL_TYPE_TO_TITLE_TYPE_MAP[modalType];\n    if (!titleType) {\n        return null;\n    }\n    return `${titleType}_${whitelist ? 'allowlist' : 'blocklist'}`;\n};\n\nconst getSelectedValues = (filters: any, catalogSourcesToIdMap: any) =>\n    filters.reduce(\n        (acc: any, { url }: any) => {\n            if (Object.prototype.hasOwnProperty.call(catalogSourcesToIdMap, url)) {\n                const fieldId = `filter${catalogSourcesToIdMap[url]}`;\n                acc.selectedFilterIds[fieldId] = true;\n                acc.selectedSources[url] = true;\n            }\n            return acc;\n        },\n        {\n            selectedFilterIds: {},\n            selectedSources: {},\n        },\n    );\n\ninterface ModalProps {\n    toggleFilteringModal: (...args: unknown[]) => unknown;\n    isOpen: boolean;\n    addFilter: (...args: unknown[]) => unknown;\n    isFilterAdded: boolean;\n    processingAddFilter: boolean;\n    processingConfigFilter: boolean;\n    handleSubmit: (values: any) => void;\n    modalType: string;\n    currentFilterData: object;\n    t: (...args: unknown[]) => string;\n    whitelist?: boolean;\n    filters: unknown[];\n    filtersCatalog?: any;\n}\n\nclass Modal extends Component<ModalProps> {\n    closeModal = () => {\n        this.props.toggleFilteringModal();\n    };\n\n    render() {\n        const {\n            isOpen,\n            processingAddFilter,\n            processingConfigFilter,\n            handleSubmit,\n            modalType,\n            currentFilterData,\n            whitelist,\n            toggleFilteringModal,\n            filters,\n            t,\n            filtersCatalog,\n        } = this.props;\n\n        let initialValues;\n        let selectedSources;\n        switch (modalType) {\n            case MODAL_TYPE.EDIT_FILTERS:\n                initialValues = currentFilterData;\n                break;\n            case MODAL_TYPE.CHOOSE_FILTERING_LIST: {\n                const catalogSourcesToIdMap = getMap(Object.values(filtersCatalog.filters), 'source', 'id');\n\n                const selectedValues = getSelectedValues(filters, catalogSourcesToIdMap);\n                initialValues = selectedValues.selectedFilterIds;\n                selectedSources = selectedValues.selectedSources;\n                break;\n            }\n            default:\n                break;\n        }\n\n        const title = t(getTitle(modalType, whitelist));\n\n        return (\n            <ReactModal\n                className=\"Modal__Bootstrap modal-dialog modal-dialog-centered\"\n                closeTimeoutMS={0}\n                isOpen={isOpen}\n                onRequestClose={this.closeModal}>\n                <div className=\"modal-content\">\n                    <div className=\"modal-header\">\n                        {title && <h4 className=\"modal-title\">{title}</h4>}\n\n                        <button type=\"button\" className=\"close\" onClick={this.closeModal}>\n                            <span className=\"sr-only\">Close</span>\n                        </button>\n                    </div>\n\n                    <Form\n                        selectedSources={selectedSources}\n                        initialValues={initialValues}\n                        modalType={modalType}\n                        onSubmit={handleSubmit}\n                        processingAddFilter={processingAddFilter}\n                        processingConfigFilter={processingConfigFilter}\n                        closeModal={this.closeModal}\n                        whitelist={whitelist}\n                        toggleFilteringModal={toggleFilteringModal}\n                    />\n                </div>\n            </ReactModal>\n        );\n    }\n}\n\nexport default withTranslation()(Modal);\n"
  },
  {
    "path": "client/src/components/Filters/Rewrites/Form.tsx",
    "content": "import React from 'react';\nimport { Controller, useForm } from 'react-hook-form';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators';\nimport { Input } from '../../ui/Controls/Input';\n\ninterface RewriteFormValues {\n    domain: string;\n    answer: string;\n}\n\ntype Props = {\n    processingAdd: boolean;\n    currentRewrite?: RewriteFormValues;\n    toggleRewritesModal: () => void;\n    onSubmit?: (data: RewriteFormValues) => Promise<void> | void;\n};\n\nconst Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }: Props) => {\n    const { t } = useTranslation();\n\n    const {\n        handleSubmit,\n        reset,\n        control,\n        formState: { isDirty, isSubmitting },\n    } = useForm<RewriteFormValues>({\n        mode: 'onBlur',\n        defaultValues: {\n            domain: currentRewrite?.domain || '',\n            answer: currentRewrite?.answer || '',\n        },\n    });\n\n    const handleFormSubmit = async (data: RewriteFormValues) => {\n        if (onSubmit) {\n            await onSubmit(data);\n        }\n    };\n\n    return (\n        <form onSubmit={handleSubmit(handleFormSubmit)}>\n            <div className=\"modal-body\">\n                <div className=\"form__desc form__desc--top\">\n                    <Trans>domain_desc</Trans>\n                </div>\n                <div className=\"form__group\">\n                    <Controller\n                        name=\"domain\"\n                        control={control}\n                        rules={{\n                            validate: {\n                                validate: validateDomain,\n                                required: validateRequiredValue,\n                            },\n                        }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"text\"\n                                data-testid=\"rewrites_domain\"\n                                placeholder={t('form_domain')}\n                                error={fieldState.error?.message}\n                            />\n                        )}\n                    />\n                </div>\n                <Trans>examples_title</Trans>:\n                <ol className=\"leading-loose\">\n                    <li>\n                        <code>example.org</code> – <Trans>example_rewrite_domain</Trans>\n                    </li>\n                    <li>\n                        <code>*.example.org</code> –&nbsp;\n                        <span>\n                            <Trans components={[<code key=\"0\">text</code>]}>example_rewrite_wildcard</Trans>\n                        </span>\n                    </li>\n                </ol>\n                <div className=\"form__group\">\n                    <Controller\n                        name=\"answer\"\n                        control={control}\n                        rules={{\n                            validate: {\n                                validate: validateAnswer,\n                                required: validateRequiredValue,\n                            },\n                        }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"text\"\n                                data-testid=\"rewrites_answer\"\n                                placeholder={t('form_answer')}\n                                error={fieldState.error?.message}\n                            />\n                        )}\n                    />\n                </div>\n            </div>\n\n            <ul>\n                {['rewrite_ip_address', 'rewrite_domain_name', 'rewrite_A', 'rewrite_AAAA'].map((str) => (\n                    <li key={str}>\n                        <Trans components={[<code key=\"0\">text</code>]}>{str}</Trans>\n                    </li>\n                ))}\n            </ul>\n\n            <div className=\"modal-footer\">\n                <div className=\"btn-list\">\n                    <button\n                        type=\"button\"\n                        data-testid=\"rewrites_cancel\"\n                        className=\"btn btn-secondary btn-standard\"\n                        disabled={isSubmitting || processingAdd}\n                        onClick={() => {\n                            reset();\n                            toggleRewritesModal();\n                        }}>\n                        <Trans>cancel_btn</Trans>\n                    </button>\n\n                    <button\n                        type=\"submit\"\n                        data-testid=\"rewrites_save\"\n                        className=\"btn btn-success btn-standard\"\n                        disabled={isSubmitting || !isDirty || processingAdd}>\n                        <Trans>save_btn</Trans>\n                    </button>\n                </div>\n            </div>\n        </form>\n    );\n};\n\nexport default Form;\n"
  },
  {
    "path": "client/src/components/Filters/Rewrites/Modal.tsx",
    "content": "import React from 'react';\nimport { Trans, withTranslation } from 'react-i18next';\n\nimport ReactModal from 'react-modal';\n\nimport { MODAL_TYPE } from '../../../helpers/constants';\n\nimport Form from './Form';\n\ninterface ModalProps {\n    isModalOpen: boolean;\n    handleSubmit: (values: any) => void;\n    toggleRewritesModal: (...args: unknown[]) => unknown;\n    processingAdd: boolean;\n    processingDelete: boolean;\n    modalType: string;\n    currentRewrite?: { answer: string, domain: string; };\n}\n\nconst Modal = (props: ModalProps) => {\n    const {\n        isModalOpen,\n        handleSubmit,\n        toggleRewritesModal,\n        processingAdd,\n        modalType,\n        currentRewrite,\n    } = props;\n\n    return (\n        <ReactModal\n            className=\"Modal__Bootstrap modal-dialog modal-dialog-centered\"\n            closeTimeoutMS={0}\n            isOpen={isModalOpen}\n            onRequestClose={() => toggleRewritesModal()}>\n            <div className=\"modal-content\">\n                <div className=\"modal-header\">\n                    <h4 className=\"modal-title\">\n                        {modalType === MODAL_TYPE.EDIT_REWRITE ? (\n                            <Trans>rewrite_edit</Trans>\n                        ) : (\n                            <Trans>rewrite_add</Trans>\n                        )}\n                    </h4>\n\n                    <button type=\"button\" className=\"close\" onClick={() => toggleRewritesModal()}>\n                        <span className=\"sr-only\">Close</span>\n                    </button>\n                </div>\n\n                <Form\n                    onSubmit={handleSubmit}\n                    toggleRewritesModal={toggleRewritesModal}\n                    processingAdd={processingAdd}\n                    currentRewrite={currentRewrite}\n                />\n            </div>\n        </ReactModal>\n    );\n};\n\nexport default withTranslation()(Modal);\n"
  },
  {
    "path": "client/src/components/Filters/Rewrites/Table.tsx",
    "content": "import React, { Component } from 'react';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport { withTranslation } from 'react-i18next';\n\nimport { sortIp } from '../../../helpers/helpers';\nimport { MODAL_TYPE, TABLES_MIN_ROWS } from '../../../helpers/constants';\nimport { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';\n\ninterface TableProps {\n    t: (...args: unknown[]) => string;\n    list: unknown[];\n    processing: boolean;\n    processingAdd: boolean;\n    processingDelete: boolean;\n    processingUpdate: boolean;\n    settings: Record<string, boolean>;\n    handleDelete: (...args: unknown[]) => unknown;\n    toggleRewritesModal: (...args: unknown[]) => unknown;\n    toggleRewrite: (...args: unknown[]) => unknown;\n}\n\nclass Table extends Component<TableProps> {\n    cellWrap = ({ value }: any) => (\n        <div className=\"logs__row o-hidden\">\n            <span className=\"logs__text\" title={value}>\n                {value}\n            </span>\n        </div>\n    );\n\n    renderCheckbox = ({ original }: any) => {\n        const { processing, settings, toggleRewrite } = this.props;\n        const isEnabledSettings = Boolean(settings && settings.enabled);\n\n        return (\n            <label className=\"checkbox\">\n                <input\n                    data-testid=\"rewrite-enabled\"\n                    type=\"checkbox\"\n                    className=\"checkbox__input\"\n                    onChange={() => toggleRewrite(original)}\n                    checked={original.enabled}\n                    disabled={processing || !isEnabledSettings}\n                />\n\n                <span className=\"checkbox__label\" />\n            </label>\n        );\n    };\n\n    columns = [\n        {\n            Header: this.props.t('enabled_table_header'),\n            accessor: 'enabled',\n            Cell: this.renderCheckbox,\n            width: 90,\n            className: 'text-center',\n            resizable: false,\n        },\n        {\n            Header: this.props.t('domain'),\n            accessor: 'domain',\n            Cell: this.cellWrap,\n        },\n        {\n            Header: this.props.t('answer'),\n            accessor: 'answer',\n            sortMethod: sortIp,\n            Cell: this.cellWrap,\n        },\n        {\n            Header: this.props.t('actions_table_header'),\n            accessor: 'actions',\n            maxWidth: 100,\n            sortable: false,\n            resizable: false,\n            Cell: (row: any) => {\n                const { original } = row;\n\n                return (\n                    <div className=\"logs__row logs__row--center\">\n                        <button\n                            data-testid=\"edit-rewrite\"\n                            type=\"button\"\n                            className=\"btn btn-icon btn-outline-primary btn-sm mr-2\"\n                            onClick={() => {\n                                this.props.toggleRewritesModal({\n                                    type: MODAL_TYPE.EDIT_REWRITE,\n                                    currentRewrite: original,\n                                });\n                            }}\n                            disabled={this.props.processingUpdate}\n                            title={this.props.t('edit_table_action')}>\n                            <svg className=\"icons icon12\">\n                                <use xlinkHref=\"#edit\" />\n                            </svg>\n                        </button>\n\n                        <button\n                            data-testid=\"delete-rewrite\"\n                            type=\"button\"\n                            className=\"btn btn-icon btn-outline-secondary btn-sm\"\n                            onClick={() => this.props.handleDelete(original)}\n                            title={this.props.t('delete_table_action')}>\n                            <svg className=\"icons\">\n                                <use xlinkHref=\"#delete\" />\n                            </svg>\n                        </button>\n                    </div>\n                );\n            },\n        },\n    ];\n\n    render() {\n        const { t, list, processing, processingAdd, processingDelete } = this.props;\n\n        return (\n            <ReactTable\n                data={list || []}\n                columns={this.columns}\n                loading={processing || processingAdd || processingDelete}\n                className=\"-striped -highlight card-table-overflow\"\n                showPagination\n                defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE) || 10}\n                onPageSizeChange={(size: any) =>\n                    LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE, size)\n                }\n                minRows={TABLES_MIN_ROWS}\n                ofText=\"/\"\n                previousText={t('previous_btn')}\n                nextText={t('next_btn')}\n                pageText={t('page_table_footer_text')}\n                rowsText={t('rows_table_footer_text')}\n                loadingText={t('loading_table_status')}\n                noDataText={t('rewrite_not_found')}\n            />\n        );\n    }\n}\n\nexport default withTranslation()(Table);\n"
  },
  {
    "path": "client/src/components/Filters/Rewrites/index.tsx",
    "content": "import React, { Component, Fragment } from 'react';\nimport { Trans, withTranslation } from 'react-i18next';\nimport cn from 'classnames';\n\nimport Table from './Table';\n\nimport Modal from './Modal';\n\nimport Card from '../../ui/Card';\n\nimport PageTitle from '../../ui/PageTitle';\nimport { MODAL_TYPE } from '../../../helpers/constants';\nimport { RewritesData } from '../../../initialState';\n\ninterface RewritesProps {\n    t: (...args: unknown[]) => string;\n    getRewritesList: () => (dispatch: any) => void;\n    toggleRewritesModal: (...args: unknown[]) => unknown;\n    addRewrite: (...args: unknown[]) => unknown;\n    deleteRewrite: (...args: unknown[]) => unknown;\n    updateRewrite: (...args: unknown[]) => unknown;\n    updateRewriteSettings: (...args: unknown[]) => unknown;\n    getRewriteSettings: () => (dispatch: any) => void;\n    rewrites: RewritesData;\n}\n\nclass Rewrites extends Component<RewritesProps> {\n    componentDidMount() {\n        this.props.getRewritesList();\n        this.props.getRewriteSettings();\n    }\n\n    handleDelete = (values: any) => {\n        // eslint-disable-next-line no-alert\n        if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) {\n            this.props.deleteRewrite(values);\n        }\n    };\n\n    handleSubmit = (values: any) => {\n        const { modalType, currentRewrite } = this.props.rewrites;\n\n        if (modalType === MODAL_TYPE.EDIT_REWRITE && currentRewrite) {\n            this.props.updateRewrite({\n                target: currentRewrite,\n                update: values,\n            });\n        } else {\n            this.props.addRewrite(values);\n        }\n    };\n\n    toggleRewrite = (currentRewrite: any) => {\n        const updatedRewrite = { ...currentRewrite, enabled: !currentRewrite.enabled };\n\n        this.props.updateRewrite({\n            target: currentRewrite,\n            update: updatedRewrite,\n        });\n    };\n\n    toggleRewriteSettings = () => {\n        const { enabled } = this.props.rewrites.settings;\n\n        this.props.updateRewriteSettings({ enabled: !enabled });\n    };\n\n    render() {\n        const {\n            t,\n            rewrites,\n            toggleRewritesModal,\n        } = this.props;\n\n        const {\n            list,\n            isModalOpen,\n            processing,\n            processingAdd,\n            processingDelete,\n            processingUpdate,\n            modalType,\n            currentRewrite,\n            settings\n        } = rewrites;\n\n        const isEnabledSettings = settings.enabled;\n\n        return (\n            <Fragment>\n                <PageTitle title={t('dns_rewrites')} subtitle={t('rewrite_desc')} />\n\n                <div className={cn(isEnabledSettings ? 'text-success' : 'text-warning', 'mb-2')}>\n                    {isEnabledSettings ? t('rewrites_enabled_table_header') : t('rewrites_disabled_table_header')}\n                </div>\n\n                <Card id=\"rewrites\" bodyType=\"card-body box-body--settings\">\n                    <Fragment>\n                        <Table\n                            list={list}\n                            processing={processing}\n                            processingAdd={processingAdd}\n                            processingDelete={processingDelete}\n                            processingUpdate={processingUpdate}\n                            handleDelete={this.handleDelete}\n                            toggleRewritesModal={toggleRewritesModal}\n                            toggleRewrite={this.toggleRewrite}\n                            settings={settings}\n                        />\n\n                        <div className=\"card-actions\">\n                            <button\n                                data-testid=\"add-rewrite\"\n                                type=\"button\"\n                                className=\"btn btn-success btn-standard  mr-2\"\n                                onClick={() => toggleRewritesModal({ type: MODAL_TYPE.ADD_REWRITE })}\n                                disabled={processingAdd}>\n                                <Trans>rewrite_add</Trans>\n                            </button>\n\n                            <button\n                                data-testid=\"toggle-rewrite-settings\"\n                                type=\"button\"\n                                className=\"btn btn-primary btn-standard\"\n                                onClick={() => this.toggleRewriteSettings()}\n                                disabled={processingUpdate}>\n                                <Trans>{isEnabledSettings ? 'disable_rewrites' : 'enable_rewrites'}</Trans>\n                            </button>\n                        </div>\n\n                        <Modal\n                            isModalOpen={isModalOpen}\n                            modalType={modalType}\n                            toggleRewritesModal={toggleRewritesModal}\n                            handleSubmit={this.handleSubmit}\n                            processingAdd={processingAdd}\n                            processingDelete={processingDelete}\n                            currentRewrite={currentRewrite}\n                        />\n                    </Fragment>\n                </Card>\n            </Fragment>\n        );\n    }\n}\n\nexport default withTranslation()(Rewrites);\n"
  },
  {
    "path": "client/src/components/Filters/Services/Form.tsx",
    "content": "import React, { useMemo } from 'react';\n\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { Controller, useForm } from 'react-hook-form';\n\nimport { ServiceField } from './ServiceField';\n\nexport type BlockedService = {\n    id: string;\n    name: string;\n    icon_svg: string;\n    group_id: string;\n};\n\nexport type ServiceGroups = {\n    id: string;\n}\n\ntype FormValues = {\n    blocked_services: Record<string, boolean>;\n};\n\ninterface FormProps {\n    initialValues: Record<string, boolean>;\n    blockedServices: BlockedService[];\n    serviceGroups: ServiceGroups[];\n    onSubmit: (values: FormValues) => void;\n    processing: boolean;\n    processingSet: boolean;\n}\n\nexport const Form = ({\n    initialValues,\n    blockedServices,\n    serviceGroups,\n    processing,\n    processingSet,\n    onSubmit,\n}: FormProps) => {\n    const { t } = useTranslation();\n\n    const {\n        handleSubmit,\n        control,\n        setValue,\n        formState: { isSubmitting },\n    } = useForm<FormValues>({\n        mode: 'onBlur',\n        defaultValues: { blocked_services: initialValues }\n    });\n\n    const isServicesControlsDisabled = processing || processingSet;\n    const isSubmitDisabled = processing || processingSet || isSubmitting;\n\n    const servicesByGroup = useMemo(() => {\n        return blockedServices.reduce((acc, service) => {\n            if (!acc[service.group_id]) {\n                acc[service.group_id] = [];\n            }\n            acc[service.group_id].push(service);\n            return acc;\n        }, {} as Record<string, BlockedService[]>);\n    }, [blockedServices]);\n\n    const handleToggleAllServices = (isSelected: boolean) => {\n        blockedServices.forEach((service) => {\n            if (!isServicesControlsDisabled) {\n                setValue(`blocked_services.${service.id}`, isSelected);\n            }\n        });\n    };\n\n    const handleToggleGroupServices = (groupId: string, isSelected: boolean) => {\n        if (isServicesControlsDisabled) {\n            return;\n        }\n        servicesByGroup[groupId].forEach((service) => {\n            setValue(`blocked_services.${service.id}`, isSelected);\n        });\n    };\n\n    const handleSubmitWithGroups = (values: FormValues) => {\n        if (!values || !values.blocked_services) {\n            return onSubmit(values);\n        }\n\n        const enabledIdsMap = Object.fromEntries(\n            blockedServices\n                .filter(service => values.blocked_services?.[service.id])\n                .map(service => [service.id, true] as const)\n        );\n\n        return onSubmit({ blocked_services: enabledIdsMap });\n    };\n\n    return (\n        <form onSubmit={handleSubmit(handleSubmitWithGroups)}>\n            <div className=\"form__group\">\n                <div className=\"blocked_services row mb-5\">\n                    <div className=\"col-12 col-md-6 mb-4 mb-md-0\">\n                        <button\n                            type=\"button\"\n                            data-testid=\"blocked_services_block_all\"\n                            className=\"btn btn-secondary btn-block font-weight-normal\"\n                            disabled={isServicesControlsDisabled}\n                            onClick={() => handleToggleAllServices(true)}>\n                            <Trans>block_all</Trans>\n                        </button>\n                    </div>\n                    <div className=\"col-12 col-md-6\">\n                        <button\n                            type=\"button\"\n                            data-testid=\"blocked_services_unblock_all\"\n                            className=\"btn btn-secondary btn-block font-weight-normal\"\n                            disabled={isServicesControlsDisabled}\n                            onClick={() => handleToggleAllServices(false)}>\n                            <Trans>unblock_all</Trans>\n                        </button>\n                    </div>\n                </div>\n\n                {serviceGroups.map((group) => {\n                    const groupServices = servicesByGroup[group.id];\n\n                    return (\n                        <div key={group.id} className=\"services-group mb-2\">\n                            <h3 className=\"h5 mb-3\">\n                                {t(`servicesgroup.${group.id}.name`, { ns: 'services' })}\n                            </h3>\n\n                            {groupServices.length > 1 && (\n                                <div className=\"actions mb-3 d-flex gap-4\">\n                                    <button\n                                        type=\"button\"\n                                        className=\"btn btn-link p-0 text-danger font-weight-normal mr-5\"\n                                        disabled={isServicesControlsDisabled}\n                                        onClick={() => handleToggleGroupServices(group.id, true)}\n                                    >\n                                        <Trans>block_all</Trans>\n                                    </button>\n\n                                    <button\n                                        type=\"button\"\n                                        className=\"btn btn-link p-0 text-success font-weight-normal\"\n                                        disabled={isServicesControlsDisabled}\n                                        onClick={() => handleToggleGroupServices(group.id, false)}\n                                    >\n                                        <Trans>unblock_all</Trans>\n                                    </button>\n                                </div>\n                            )}\n\n                            <div className=\"services__wrapper\">\n                                <div className=\"services\">\n                                    {groupServices.map((service) => (\n                                        <Controller\n                                            key={service.id}\n                                            name={`blocked_services.${service.id}`}\n                                            control={control}\n                                            render={({ field }) => (\n                                                <ServiceField\n                                                    {...field}\n                                                    data-testid={`blocked_services_${service.id}`}\n                                                    data-groupid={`blocked_services_${service.group_id}`}\n                                                    placeholder={service.name}\n                                                    disabled={isServicesControlsDisabled}\n                                                    icon={service.icon_svg}\n                                                />\n                                            )}\n                                        />\n                                    ))}\n                                </div>\n                            </div>\n                        </div>\n                    );\n                })}\n            </div>\n\n            <div className=\"btn-list\">\n                <button\n                    type=\"submit\"\n                    data-testid=\"blocked_services_save\"\n                    className=\"btn btn-success btn-standard btn-large\"\n                    disabled={isSubmitDisabled}>\n                    <Trans>save_btn</Trans>\n                </button>\n            </div>\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Filters/Services/ScheduleForm/Modal.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ReactModal from 'react-modal';\n\nimport { Timezone } from './Timezone';\n\nimport { TimeSelect } from './TimeSelect';\n\nimport { TimePeriod } from './TimePeriod';\nimport { getFullDayName, getShortDayName } from './helpers';\nimport { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';\n\nexport const DAYS_OF_WEEK = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];\n\nconst INITIAL_START_TIME_MS = 0;\nconst INITIAL_END_TIME_MS = 86340000;\n\ninterface ModalProps {\n    schedule: {\n        time_zone: string;\n    };\n    currentDay?: string;\n    isOpen: boolean;\n    onClose: (...args: unknown[]) => unknown;\n    onSubmit: (values: any) => void;\n}\n\nexport const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: ModalProps) => {\n    const [t] = useTranslation();\n\n    const intialTimezone =\n        schedule.time_zone === LOCAL_TIMEZONE_VALUE\n            ? Intl.DateTimeFormat().resolvedOptions().timeZone\n            : schedule.time_zone;\n\n    const [timezone, setTimezone] = useState(intialTimezone);\n    const [days, setDays] = useState<Set<string>>(new Set());\n\n    const [startTime, setStartTime] = useState(INITIAL_START_TIME_MS);\n    const [endTime, setEndTime] = useState(INITIAL_END_TIME_MS);\n\n    const [wrongPeriod, setWrongPeriod] = useState(true);\n\n    useEffect(() => {\n        if (currentDay) {\n            const newDays = new Set([currentDay]);\n            setDays(newDays);\n\n            setStartTime(schedule[currentDay].start);\n            setEndTime(schedule[currentDay].end);\n        }\n    }, [currentDay]);\n\n    useEffect(() => {\n        if (startTime >= endTime) {\n            setWrongPeriod(true);\n        } else {\n            setWrongPeriod(false);\n        }\n    }, [startTime, endTime]);\n\n    const addDays = (day: any) => {\n        const newDays = new Set(days);\n\n        if (newDays.has(day)) {\n            newDays.delete(day);\n        } else {\n            newDays.add(day);\n        }\n\n        setDays(newDays);\n    };\n\n    const activeDay = (day: any) => {\n        return days.has(day);\n    };\n\n    const onFormSubmit = (e: any) => {\n        e.preventDefault();\n\n        const newSchedule = schedule;\n\n        Array.from(days).forEach((day) => {\n            newSchedule[day] = {\n                start: startTime,\n                end: endTime,\n            };\n        });\n\n        if (timezone !== intialTimezone) {\n            newSchedule.time_zone = timezone;\n        }\n\n        onSubmit(newSchedule);\n    };\n\n    return (\n        <ReactModal\n            className=\"Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--schedule\"\n            closeTimeoutMS={0}\n            isOpen={isOpen}\n            onRequestClose={onClose}>\n            <div className=\"modal-content\">\n                <div className=\"modal-header\">\n                    <h4 className=\"modal-title\">{currentDay ? t('schedule_edit') : t('schedule_new')}</h4>\n\n                    <button type=\"button\" className=\"close\" onClick={onClose}>\n                        <span className=\"sr-only\">Close</span>\n                    </button>\n                </div>\n\n                <form onSubmit={onFormSubmit}>\n                    <div className=\"modal-body\">\n                        <Timezone timezone={timezone} setTimezone={setTimezone} />\n\n                        <div className=\"schedule__days\">\n                            {DAYS_OF_WEEK.map((day) => (\n                                <button\n                                    type=\"button\"\n                                    key={day}\n                                    className=\"btn schedule__button-day\"\n                                    data-active={activeDay(day)}\n                                    onClick={() => addDays(day)}>\n                                    {getShortDayName(t, day)}\n                                </button>\n                            ))}\n                        </div>\n\n                        <div className=\"schedule__time-wrap\">\n                            <div className=\"schedule__time-row\">\n                                <TimeSelect value={startTime} onChange={(v) => setStartTime(v)} />\n\n                                <TimeSelect value={endTime} onChange={(v) => setEndTime(v)} />\n                            </div>\n\n                            {wrongPeriod && <div className=\"schedule__error\">{t('schedule_invalid_select')}</div>}\n                        </div>\n\n                        <div className=\"schedule__info\">\n                            <div className=\"schedule__info-title\">{t('schedule_modal_time_off')}</div>\n\n                            <div className=\"schedule__info-row\">\n                                <svg className=\"icons schedule__info-icon\">\n                                    <use xlinkHref=\"#calendar\" />\n                                </svg>\n                                {days.size ? (\n                                    Array.from(days)\n                                        .map((day) => getFullDayName(t, day))\n                                        .join(', ')\n                                ) : (\n                                    <span>—</span>\n                                )}\n                            </div>\n\n                            <div className=\"schedule__info-row\">\n                                <svg className=\"icons schedule__info-icon\">\n                                    <use xlinkHref=\"#watch\" />\n                                </svg>\n                                {wrongPeriod ? (\n                                    <span>—</span>\n                                ) : (\n                                    <TimePeriod startTimeMs={startTime} endTimeMs={endTime} />\n                                )}\n                            </div>\n                        </div>\n\n                        <div className=\"schedule__notice\">{t('schedule_modal_description')}</div>\n                    </div>\n\n                    <div className=\"modal-footer\">\n                        <div className=\"btn-list\">\n                            <button\n                                type=\"button\"\n                                className=\"btn btn-success btn-standard\"\n                                disabled={days.size === 0 || wrongPeriod}\n                                onClick={onFormSubmit}>\n                                {currentDay ? t('schedule_save') : t('schedule_add')}\n                            </button>\n                        </div>\n                    </div>\n                </form>\n            </div>\n        </ReactModal>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Filters/Services/ScheduleForm/TimePeriod.tsx",
    "content": "import React from 'react';\n\nimport { getTimeFromMs } from './helpers';\n\ninterface TimePeriodProps {\n    startTimeMs: number;\n    endTimeMs: number;\n}\n\nexport const TimePeriod = ({ startTimeMs, endTimeMs }: TimePeriodProps) => {\n    const startTime = getTimeFromMs(startTimeMs);\n    const endTime = getTimeFromMs(endTimeMs);\n\n    return (\n        <div className=\"schedule__time\">\n            <time>\n                {startTime.hours}:{startTime.minutes}\n            </time>\n            &nbsp;–&nbsp;\n            <time>\n                {endTime.hours}:{endTime.minutes}\n            </time>\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Filters/Services/ScheduleForm/TimeSelect.tsx",
    "content": "import React, { useState } from 'react';\n\nimport { getTimeFromMs, convertTimeToMs } from './helpers';\n\ninterface TimeSelectProps {\n    value: number;\n    onChange: (time: number) => void;\n}\n\nexport const TimeSelect = ({ value, onChange }: TimeSelectProps) => {\n    const { hours: initialHours, minutes: initialMinutes } = getTimeFromMs(value);\n\n    const [hours, setHours] = useState(initialHours);\n    const [minutes, setMinutes] = useState(initialMinutes);\n\n    const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));\n\n    const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));\n\n    const onHourChange = (event: any) => {\n        setHours(event.target.value);\n        onChange(convertTimeToMs(event.target.value, minutes));\n    };\n\n    const onMinuteChange = (event: any) => {\n        setMinutes(event.target.value);\n        onChange(convertTimeToMs(hours, event.target.value));\n    };\n\n    return (\n        <div className=\"schedule__time-select\">\n            <select value={hours} onChange={onHourChange} className=\"form-control custom-select\">\n                {hourOptions.map((hour) => (\n                    <option key={hour} value={hour}>\n                        {hour}\n                    </option>\n                ))}\n            </select>\n            &nbsp;:&nbsp;\n            <select value={minutes} onChange={onMinuteChange} className=\"form-control custom-select\">\n                {minuteOptions.map((minute) => (\n                    <option key={minute} value={minute}>\n                        {minute}\n                    </option>\n                ))}\n            </select>\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Filters/Services/ScheduleForm/Timezone.tsx",
    "content": "import React from 'react';\nimport ct from 'countries-and-timezones';\nimport { useTranslation } from 'react-i18next';\n\nimport { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';\n\ninterface TimezoneProps {\n    timezone: string;\n    setTimezone: (...args: unknown[]) => unknown;\n}\n\nexport const Timezone = ({ timezone, setTimezone }: TimezoneProps) => {\n    const [t] = useTranslation();\n\n    const onTimeZoneChange = (event: any) => {\n        setTimezone(event.target.value);\n    };\n\n    const timezones = ct.getAllTimezones();\n\n    return (\n        <div className=\"schedule__timezone\">\n            <label className=\"form__label form__label--with-desc mb-2\">{t('schedule_timezone')}</label>\n\n            <select className=\"form-control custom-select\" value={timezone} onChange={onTimeZoneChange}>\n                <option value={LOCAL_TIMEZONE_VALUE}>{t('schedule_timezone')}</option>\n                {/* TODO: get timezones from backend method when the method is ready */}\n                {Object.keys(timezones).map((zone) => (\n                    <option key={zone} value={zone}>\n                        {zone} (GMT{timezones[zone].utcOffsetStr})\n                    </option>\n                ))}\n            </select>\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Filters/Services/ScheduleForm/helpers.ts",
    "content": "export const getFullDayName = (t: any, abbreviation: any) => {\n    const dayMap = {\n        sun: t('sunday'),\n        mon: t('monday'),\n        tue: t('tuesday'),\n        wed: t('wednesday'),\n        thu: t('thursday'),\n        fri: t('friday'),\n        sat: t('saturday'),\n    };\n\n    return dayMap[abbreviation] || '';\n};\n\nexport const getShortDayName = (t: any, abbreviation: any) => {\n    const dayMap = {\n        sun: t('sunday_short'),\n        mon: t('monday_short'),\n        tue: t('tuesday_short'),\n        wed: t('wednesday_short'),\n        thu: t('thursday_short'),\n        fri: t('friday_short'),\n        sat: t('saturday_short'),\n    };\n\n    return dayMap[abbreviation] || '';\n};\n\nexport const getTimeFromMs = (value: any) => {\n    const selectedTime = new Date(value);\n    const hours = selectedTime.getUTCHours();\n    const minutes = selectedTime.getUTCMinutes();\n\n    return {\n        hours: hours.toString().padStart(2, '0'),\n\n        minutes: minutes.toString().padStart(2, '0'),\n    };\n};\n\nexport const convertTimeToMs = (hours: any, minutes: any) => {\n    const selectedTime = new Date(0);\n    selectedTime.setUTCHours(parseInt(hours, 10));\n    selectedTime.setUTCMinutes(parseInt(minutes, 10));\n\n    return selectedTime.getTime();\n};\n"
  },
  {
    "path": "client/src/components/Filters/Services/ScheduleForm/index.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport cn from 'classnames';\n\nimport { Modal } from './Modal';\nimport { getFullDayName, getShortDayName } from './helpers';\nimport { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';\n\nimport { TimePeriod } from './TimePeriod';\nimport './styles.css';\n\ninterface ScheduleFormProps {\n    schedule?: {\n        time_zone: string;\n    };\n    onScheduleSubmit: (values: any) => void;\n    clientForm?: boolean;\n}\n\nexport const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: ScheduleFormProps) => {\n    const [t] = useTranslation();\n    const [modalOpen, setModalOpen] = useState(false);\n    const [currentDay, setCurrentDay] = useState();\n\n    const onModalOpen = () => setModalOpen(true);\n    const onModalClose = () => setModalOpen(false);\n\n    const filteredScheduleKeys = schedule ? Object.keys(schedule).filter((v) => v !== 'time_zone') : [];\n    const scheduleMap = new Map();\n    filteredScheduleKeys.forEach((day) => scheduleMap.set(day, schedule[day]));\n\n    const onSubmit = (values: any) => {\n        onScheduleSubmit(values);\n        onModalClose();\n    };\n\n    const onDelete = (day: any) => {\n        scheduleMap.delete(day);\n\n        const scheduleWeek = Object.fromEntries(Array.from(scheduleMap.entries()));\n\n        onScheduleSubmit({\n            time_zone: schedule.time_zone,\n            ...scheduleWeek,\n        });\n    };\n\n    const onEdit = (day: any) => {\n        setCurrentDay(day);\n        onModalOpen();\n    };\n\n    const onAdd = () => {\n        setCurrentDay(undefined);\n        onModalOpen();\n    };\n\n    return (\n        <div>\n            <div className=\"schedule__current-timezone\">\n                {t('schedule_current_timezone', { value: schedule?.time_zone || LOCAL_TIMEZONE_VALUE })}\n            </div>\n\n            <div className=\"schedule__rows\">\n                {filteredScheduleKeys.map((day) => {\n                    const data = schedule[day];\n\n                    if (!data) {\n                        return undefined;\n                    }\n\n                    return (\n                        <div key={day} className=\"schedule__row\">\n                            <div className=\"schedule__day\">{getFullDayName(t, day)}</div>\n\n                            <div className=\"schedule__day schedule__day--mobile\">{getShortDayName(t, day)}</div>\n\n                            <TimePeriod startTimeMs={data.start} endTimeMs={data.end} />\n\n                            <div className=\"schedule__actions\">\n                                <button\n                                    type=\"button\"\n                                    className=\"btn btn-icon btn-outline-primary btn-sm schedule__button\"\n                                    title={t('edit_table_action')}\n                                    onClick={() => onEdit(day)}>\n                                    <svg className=\"icons icon12\">\n                                        <use xlinkHref=\"#edit\" />\n                                    </svg>\n                                </button>\n\n                                <button\n                                    type=\"button\"\n                                    className=\"btn btn-icon btn-outline-secondary btn-sm schedule__button\"\n                                    title={t('delete_table_action')}\n                                    onClick={() => onDelete(day)}>\n                                    <svg className=\"icons\">\n                                        <use xlinkHref=\"#delete\" />\n                                    </svg>\n                                </button>\n                            </div>\n                        </div>\n                    );\n                })}\n            </div>\n\n            <button\n                type=\"button\"\n                className={cn(\n                    'btn',\n                    { 'btn-outline-success btn-sm': clientForm },\n                    { 'btn-success btn-standard': !clientForm },\n                )}\n                onClick={onAdd}>\n                {t('schedule_new')}\n            </button>\n\n            {modalOpen && (\n                <Modal\n                    isOpen={modalOpen}\n                    onClose={onModalClose}\n                    onSubmit={onSubmit}\n                    schedule={schedule}\n                    currentDay={currentDay}\n                />\n            )}\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Filters/Services/ScheduleForm/styles.css",
    "content": ".schedule__row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    margin-bottom: 8px;\n}\n\n.schedule__row:last-child {\n    margin-bottom: 0;\n}\n\n.schedule__rows {\n    margin-bottom: 24px;\n}\n\n.schedule__day {\n    display: none;\n    min-width: 110px;\n}\n\n.schedule__day--mobile {\n    display: block;\n    min-width: 50px;\n}\n\n@media screen and (min-width: 767px) {\n    .schedule__row {\n        justify-content: flex-start;\n    }\n\n    .schedule__day {\n        display: block;\n    }\n\n    .schedule__day--mobile {\n        display: none;\n    }\n\n    .schedule__actions {\n        margin-left: 32px;\n        white-space: nowrap;\n    }\n}\n\n.schedule__time {\n    min-width: 110px;\n}\n\n.schedule__button {\n    border: 0;\n}\n\n.schedule__days {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 8px;\n    margin-bottom: 24px;\n}\n\n.schedule__button-day {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    min-width: 60px;\n    height: 32px;\n    font-size: 14px;\n    line-height: 14px;\n    color: #495057;\n    background-color: transparent;\n    border: 1px solid var(--card-border-color);\n    border-radius: 4px;\n    cursor: pointer;\n    outline: 0;\n}\n\n.schedule__button-day[data-active='true'] {\n    color: var(--btn-success-bgcolor);\n    border-color: var(--btn-success-bgcolor);\n}\n\n.schedule__time-wrap {\n    margin-bottom: 24px;\n}\n\n.schedule__time-row {\n    display: flex;\n    align-items: center;\n    gap: 16px;\n}\n\n.schedule__time-select {\n    display: flex;\n    align-items: center;\n}\n\n.schedule__error {\n    margin-top: 4px;\n    font-size: 13px;\n    color: #cd201f;\n}\n\n.schedule__timezone {\n    margin-bottom: 24px;\n}\n\n.schedule__current-timezone {\n    margin-bottom: 16px;\n}\n\n.schedule__info {\n    margin-bottom: 24px;\n}\n\n.schedule__notice {\n    font-size: 13px;\n}\n\n.schedule__info-title {\n    margin-bottom: 8px;\n}\n\n.schedule__info-row {\n    display: flex;\n    align-items: center;\n    margin-bottom: 4px;\n}\n\n.schedule__info-icon {\n    width: 24px;\n    height: 24px;\n    margin-right: 8px;\n    color: #495057;\n    flex-shrink: 0;\n}\n"
  },
  {
    "path": "client/src/components/Filters/Services/ServiceField.tsx",
    "content": "import React from 'react';\nimport cn from 'classnames';\nimport { FieldValues, ControllerRenderProps } from 'react-hook-form';\n\ntype Props = ControllerRenderProps<FieldValues> & {\n    placeholder: string;\n    disabled?: boolean;\n    className?: string;\n    icon?: string;\n    error?: string;\n};\n\nexport const ServiceField = React.forwardRef<HTMLInputElement, Props>(\n    ({ name, value, onChange, onBlur, placeholder, disabled, className, icon, error, ...rest }, ref) => (\n        <>\n            <label className={cn('service custom-switch', className)}>\n                <input\n                    name={name}\n                    type=\"checkbox\"\n                    className=\"custom-switch-input\"\n                    checked={!!value}\n                    onChange={onChange}\n                    onBlur={onBlur}\n                    ref={ref}\n                    disabled={disabled}\n                    {...rest}\n                />\n\n                <span className=\"service__switch custom-switch-indicator\"></span>\n\n                <span className=\"service__text\" title={placeholder}>\n                    {placeholder}\n                </span>\n                {icon && <div dangerouslySetInnerHTML={{ __html: window.atob(icon) }} className=\"service__icon\" />}\n            </label>\n\n            {!disabled && error && <span className=\"form__message form__message--error\">{error}</span>}\n        </>\n    ),\n);\n\nServiceField.displayName = 'ServiceField';\n"
  },
  {
    "path": "client/src/components/Filters/Services/index.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport { Form } from './Form';\n\nimport Card from '../../ui/Card';\nimport { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';\n\nimport PageTitle from '../../ui/PageTitle';\n\nimport { ScheduleForm } from './ScheduleForm';\nimport { RootState } from '../../../initialState';\n\nconst getInitialDataForServices = (initial: any) =>\n    initial\n        ? initial.reduce(\n              (acc: Record<string, boolean>, service: any) => {\n                  acc[service] = true;\n                  return acc;\n              },\n              {} as Record<string, boolean>,\n          )\n        : initial;\n\nconst Services = () => {\n    const [t] = useTranslation();\n    const dispatch = useDispatch();\n\n    const services = useSelector((state: RootState) => state.services);\n\n    useEffect(() => {\n        dispatch(getBlockedServices());\n        dispatch(getAllBlockedServices());\n    }, []);\n\n    const handleSubmit = (values: any) => {\n        if (!values || !values.blocked_services) {\n            return;\n        }\n\n        const blocked_services = Object.keys(values.blocked_services).filter(\n            (service) => values.blocked_services[service],\n        );\n\n        dispatch(\n            updateBlockedServices({\n                ids: blocked_services,\n                schedule: services.list.schedule,\n            }),\n        );\n    };\n\n    const handleScheduleSubmit = (values: any) => {\n        dispatch(\n            updateBlockedServices({\n                ids: services.list.ids,\n                schedule: values,\n            }),\n        );\n    };\n\n    const initialValues = getInitialDataForServices(services.list.ids);\n\n    if (!initialValues) {\n        return null;\n    }\n\n    return (\n        <>\n            <PageTitle title={t('blocked_services')} subtitle={t('blocked_services_desc')} />\n\n            <Card bodyType=\"card-body box-body--settings\">\n                <div className=\"form\">\n                    <Form\n                        initialValues={initialValues}\n                        blockedServices={services.allServices}\n                        serviceGroups={services.allGroups}\n                        processing={services.processing}\n                        processingSet={services.processingSet}\n                        onSubmit={handleSubmit}\n                    />\n                </div>\n            </Card>\n\n            <Card\n                title={t('schedule_services')}\n                subtitle={t('schedule_services_desc')}\n                bodyType=\"card-body box-body--settings\"\n            >\n                <ScheduleForm schedule={services.list.schedule} onScheduleSubmit={handleScheduleSubmit} />\n            </Card>\n        </>\n    );\n};\n\nexport default Services;\n"
  },
  {
    "path": "client/src/components/Filters/Table.tsx",
    "content": "import React, { Component } from 'react';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport { withTranslation, Trans } from 'react-i18next';\n\nimport CellWrap from '../ui/CellWrap';\nimport { MODAL_TYPE } from '../../helpers/constants';\n\nimport { formatDetailedDateTime } from '../../helpers/helpers';\n\nimport { isValidAbsolutePath } from '../../helpers/form';\nimport { LOCAL_STORAGE_KEYS, LocalStorageHelper } from '../../helpers/localStorageHelper';\n\ninterface TableProps {\n    filters: unknown[];\n    loading: boolean;\n    processingConfigFilter: boolean;\n    toggleFilteringModal: (...args: unknown[]) => unknown;\n    handleDelete: (...args: unknown[]) => unknown;\n    toggleFilter: (...args: unknown[]) => unknown;\n    t: (...args: unknown[]) => string;\n    whitelist?: boolean;\n}\n\nclass Table extends Component<TableProps> {\n    getDateCell = (row: any) => CellWrap(row, formatDetailedDateTime);\n\n    renderCheckbox = ({ original }: any) => {\n        const { processingConfigFilter, toggleFilter } = this.props;\n        const { url, name, enabled } = original;\n        const data = { name, url, enabled: !enabled };\n\n        return (\n            <label className=\"checkbox\">\n                <input\n                    type=\"checkbox\"\n                    className=\"checkbox__input\"\n                    onChange={() => toggleFilter(url, data)}\n                    checked={enabled}\n                    disabled={processingConfigFilter}\n                />\n\n                <span className=\"checkbox__label\" />\n            </label>\n        );\n    };\n\n    columns = [\n        {\n            Header: <Trans>enabled_table_header</Trans>,\n            accessor: 'enabled',\n            Cell: this.renderCheckbox,\n            width: 90,\n            className: 'text-center',\n            resizable: false,\n        },\n        {\n            Header: <Trans>name_table_header</Trans>,\n            accessor: 'name',\n            minWidth: 180,\n            Cell: CellWrap,\n        },\n        {\n            Header: <Trans>list_url_table_header</Trans>,\n            accessor: 'url',\n            minWidth: 180,\n            // eslint-disable-next-line react/prop-types\n            Cell: ({ value }: any) => (\n                <div className=\"logs__row\">\n                    {isValidAbsolutePath(value) ? (\n                        value\n                    ) : (\n                        <a href={value} target=\"_blank\" rel=\"noopener noreferrer\" className=\"link logs__text\">\n                            {value}\n                        </a>\n                    )}\n                </div>\n            ),\n        },\n        {\n            Header: <Trans>rules_count_table_header</Trans>,\n            accessor: 'rulesCount',\n            className: 'text-center',\n            minWidth: 100,\n            Cell: (props: any) => props.value.toLocaleString(),\n        },\n        {\n            Header: <Trans>last_time_updated_table_header</Trans>,\n            accessor: 'lastUpdated',\n            className: 'text-center',\n            minWidth: 180,\n            Cell: this.getDateCell,\n        },\n        {\n            Header: <Trans>actions_table_header</Trans>,\n            accessor: 'actions',\n            className: 'text-center',\n            width: 100,\n            sortable: false,\n            resizable: false,\n            Cell: (row: any) => {\n                const { original } = row;\n                const { url } = original;\n\n                const { t, toggleFilteringModal, handleDelete } = this.props;\n\n                return (\n                    <div className=\"logs__row logs__row--center\">\n                        <button\n                            type=\"button\"\n                            className=\"btn btn-icon btn-outline-primary btn-sm mr-2\"\n                            title={t('edit_table_action')}\n                            onClick={() =>\n                                toggleFilteringModal({\n                                    type: MODAL_TYPE.EDIT_FILTERS,\n                                    url,\n                                })\n                            }>\n                            <svg className=\"icons icon12\">\n                                <use xlinkHref=\"#edit\" />\n                            </svg>\n                        </button>\n\n                        <button\n                            type=\"button\"\n                            className=\"btn btn-icon btn-outline-secondary btn-sm\"\n                            onClick={() => handleDelete(url)}\n                            title={t('delete_table_action')}>\n                            <svg className=\"icons icon12\">\n                                <use xlinkHref=\"#delete\" />\n                            </svg>\n                        </button>\n                    </div>\n                );\n            },\n        },\n    ];\n\n    render() {\n        const { loading, filters, t, whitelist } = this.props;\n\n        const localStorageKey = whitelist\n            ? LOCAL_STORAGE_KEYS.ALLOWLIST_PAGE_SIZE\n            : LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE;\n\n        return (\n            <ReactTable\n                data={filters}\n                columns={this.columns}\n                showPagination\n                defaultPageSize={LocalStorageHelper.getItem(localStorageKey) || 10}\n                onPageSizeChange={(size: any) => LocalStorageHelper.setItem(localStorageKey, size)}\n                loading={loading}\n                minRows={6}\n                ofText=\"/\"\n                previousText={t('previous_btn')}\n                nextText={t('next_btn')}\n                pageText={t('page_table_footer_text')}\n                rowsText={t('rows_table_footer_text')}\n                loadingText={t('loading_table_status')}\n                noDataText={whitelist ? t('no_whitelist_added') : t('no_blocklist_added')}\n            />\n        );\n    }\n}\n\nexport default withTranslation()(Table);\n"
  },
  {
    "path": "client/src/components/Header/Header.css",
    "content": ".nav-tabs .nav-link.active {\n    border-color: var(--btn-success-bgcolor);\n    color: var(--btn-success-bgcolor);\n    background: transparent;\n}\n\n.nav-tabs .nav-link.active:hover {\n    border-color: #4b9400;\n    color: #4b9400;\n}\n\n.nav-icon {\n    width: 15px;\n    height: 15px;\n    margin-right: 6px;\n    stroke: #9aa0ac;\n}\n\n.nav-tabs .nav-link.active .nav-icon,\n.nav-tabs .nav-item.show .nav-icon {\n    stroke: var(--green-74);\n}\n\n.nav-tabs .nav-link.active:hover .nav-icon,\n.nav-tabs .nav-item.show:hover .nav-icon {\n    stroke: #58a273;\n}\n\n.nav-tabs .nav-link {\n    width: 100%;\n    border: 0;\n    padding: 20px 0;\n}\n\n.header {\n    position: relative;\n    padding: 5px 0;\n    z-index: 102;\n}\n\n.mobile-menu {\n    position: fixed;\n    z-index: 103;\n    top: 0;\n    right: calc(100% + 5px);\n    display: block;\n    width: 250px;\n    height: 100vh;\n    transition: transform 0.3s ease;\n    background-color: var(--header-bgcolor);\n    overflow-y: auto;\n}\n\n.mobile-menu--active {\n    transform: translateX(255px);\n    box-shadow: 15px 0 50px rgba(0, 0, 0, 0.1);\n}\n\n.nav-tabs .nav-link--back {\n    height: 63px;\n    padding: 20px 0 21px;\n    font-weight: 600;\n}\n\n.nav-tabs .nav-link--account {\n    max-width: 160px;\n    font-size: 0.9rem;\n    text-overflow: ellipsis;\n    overflow: hidden;\n}\n\n.header-brand-img {\n    height: 24px;\n}\n\n.nav-tabs .nav-item.show .nav-link {\n    color: var(--green-74);\n    background-color: #fff;\n    border-bottom-color: var(--green-74);\n}\n\n.header__right {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    min-width: 100px;\n}\n\n.header__logout {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: 25px;\n    height: 25px;\n    min-width: 25px;\n    padding: 2px;\n    margin-left: 10px;\n    color: #9aa0ac;\n}\n\n.header__logout:hover,\n.header__logout:focus {\n    color: #6e7687;\n}\n\n.header__logout-icon {\n    height: 100%;\n}\n\n.header__row {\n    display: flex;\n    align-items: center;\n}\n\n.header__container {\n    width: 100%;\n    max-width: 1200px;\n    padding-right: 0.75rem;\n    padding-left: 0.75rem;\n    margin-right: auto;\n    margin-left: auto;\n}\n\n.header__column:last-child {\n    margin-left: auto;\n}\n\n.nav-tabs {\n    margin: 0;\n}\n\n@media screen and (min-width: 992px) {\n    .header {\n        padding: 0;\n    }\n\n    .nav-tabs .nav-link {\n        width: auto;\n        border-bottom: 1px solid transparent;\n        font-size: 13px;\n        white-space: nowrap;\n    }\n\n    .mobile-menu {\n        position: static;\n        display: flex;\n        padding: 0;\n        width: auto;\n        height: auto;\n        box-shadow: none;\n        overflow: initial;\n    }\n\n    .mobile-menu--active {\n        transform: none;\n        box-shadow: none;\n    }\n\n    .nav-icon {\n        display: none;\n    }\n\n    .nav-icon--gray {\n        color: #9aa0ac;\n    }\n\n    .nav-icon--white {\n        color: #fff;\n    }\n\n    .header-brand-img {\n        height: 32px;\n    }\n\n    .header__logout {\n        width: 35px;\n        height: 35px;\n        padding: 5px;\n    }\n\n    .header__row {\n        justify-content: space-between;\n    }\n\n    .header__column:last-child {\n        margin-left: 0;\n    }\n\n    .nav-tabs {\n        margin: 0 -0.75rem;\n    }\n}\n\n@media screen and (min-width: 1280px) {\n    .nav-tabs .nav-link {\n        font-size: 14px;\n    }\n\n    .nav-icon {\n        display: block;\n    }\n}\n\n.dns-status {\n    padding: 0.35em 0.5em;\n    line-height: 10px;\n}\n"
  },
  {
    "path": "client/src/components/Header/Menu.tsx",
    "content": "import React, { Component } from 'react';\n\nimport { NavLink } from 'react-router-dom';\n\nimport enhanceWithClickOutside from 'react-click-outside';\nimport classnames from 'classnames';\nimport { Trans, withTranslation } from 'react-i18next';\nimport { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants';\n\nimport Dropdown from '../ui/Dropdown';\n\nconst MENU_ITEMS = [\n    {\n        route: MENU_URLS.root,\n        exact: true,\n        icon: 'dashboard',\n        text: 'dashboard',\n        order: 0,\n    },\n\n    // Settings dropdown should have visual order 1\n\n    // Filters dropdown should have visual order 2\n\n    {\n        route: MENU_URLS.logs,\n        icon: 'log',\n        text: 'query_log',\n        order: 3,\n    },\n    {\n        route: MENU_URLS.guide,\n        icon: 'setup',\n        text: 'setup_guide',\n        order: 4,\n    },\n];\n\nconst SETTINGS_ITEMS = [\n    {\n        route: SETTINGS_URLS.settings,\n        text: 'general_settings',\n    },\n    {\n        route: SETTINGS_URLS.dns,\n        text: 'dns_settings',\n    },\n    {\n        route: SETTINGS_URLS.encryption,\n        text: 'encryption_settings',\n    },\n    {\n        route: SETTINGS_URLS.clients,\n        text: 'client_settings',\n    },\n    {\n        route: SETTINGS_URLS.dhcp,\n        text: 'dhcp_settings',\n    },\n];\n\nconst FILTERS_ITEMS = [\n    {\n        route: FILTERS_URLS.dns_blocklists,\n        text: 'dns_blocklists',\n    },\n    {\n        route: FILTERS_URLS.dns_allowlists,\n        text: 'dns_allowlists',\n    },\n    {\n        route: FILTERS_URLS.dns_rewrites,\n        text: 'dns_rewrites',\n    },\n    {\n        route: FILTERS_URLS.blocked_services,\n        text: 'blocked_services',\n    },\n    {\n        route: FILTERS_URLS.custom_rules,\n        text: 'custom_filtering_rules',\n    },\n];\n\ninterface MenuProps {\n    isMenuOpen: boolean;\n    closeMenu: (...args: unknown[]) => unknown;\n    pathname: string;\n    t?: (...args: unknown[]) => string;\n}\n\nclass Menu extends Component<MenuProps> {\n    handleClickOutside = () => {\n        this.props.closeMenu();\n    };\n\n    closeMenu = () => {\n        this.props.closeMenu();\n    };\n\n    getActiveClassForDropdown = (URLS: any) => {\n        const isActivePage = Object.values(URLS)\n\n            .some((item: any) => item === this.props.pathname);\n\n        return isActivePage ? 'active' : '';\n    };\n\n    getNavLink = ({ route, exact, text, order, className, icon }: any) => (\n        <NavLink\n            to={route}\n            key={route}\n            exact={exact || false}\n            className={`order-${order} ${className}`}\n            onClick={this.closeMenu}>\n            {icon && (\n                <svg className=\"nav-icon\">\n                    <use xlinkHref={`#${icon}`} />\n                </svg>\n            )}\n\n            <Trans>{text}</Trans>\n        </NavLink>\n    );\n\n    getDropdown = ({ label, order, URLS, icon, ITEMS }: any) => (\n        <Dropdown\n            label={this.props.t(label)}\n            baseClassName=\"dropdown\"\n            controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}\n            icon={icon}>\n            {ITEMS.map((item: any) =>\n                this.getNavLink({\n                    ...item,\n                    order,\n                    className: 'dropdown-item',\n                }),\n            )}\n        </Dropdown>\n    );\n\n    render() {\n        const menuClass = classnames({\n            'header__column mobile-menu': true,\n\n            'mobile-menu--active': this.props.isMenuOpen,\n        });\n        return (\n            <>\n                <div className={menuClass}>\n                    <ul className=\"nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap\">\n                        {MENU_ITEMS.map((item) => (\n                            <li className={`nav-item order-${item.order}`} key={item.text} onClick={this.closeMenu}>\n                                {this.getNavLink({\n                                    ...item,\n                                    className: 'nav-link',\n                                })}\n                            </li>\n                        ))}\n\n                        <li className=\"nav-item order-1\">\n                            {this.getDropdown({\n                                order: 1,\n                                label: 'settings',\n                                icon: 'settings',\n                                URLS: SETTINGS_URLS,\n                                ITEMS: SETTINGS_ITEMS,\n                            })}\n                        </li>\n\n                        <li className=\"nav-item order-2\">\n                            {this.getDropdown({\n                                order: 2,\n                                label: 'filters',\n                                icon: 'filters',\n                                URLS: FILTERS_URLS,\n                                ITEMS: FILTERS_ITEMS,\n                            })}\n                        </li>\n                    </ul>\n                </div>\n            </>\n        );\n    }\n}\n\nexport default withTranslation()(enhanceWithClickOutside(Menu));\n"
  },
  {
    "path": "client/src/components/Header/index.tsx",
    "content": "import React, { useState } from 'react';\n\nimport { Link, useLocation } from 'react-router-dom';\nimport { shallowEqual, useSelector } from 'react-redux';\nimport { useTranslation } from 'react-i18next';\nimport classnames from 'classnames';\n\nimport Menu from './Menu';\n\nimport { Logo } from '../ui/svg/logo';\nimport './Header.css';\nimport { RootState } from '../../initialState';\n\nconst Header = () => {\n    const [isMenuOpen, setIsMenuOpen] = useState(false);\n    const { t } = useTranslation();\n\n    const { protectionEnabled, processing, isCoreRunning, processingProfile, name } = useSelector(\n        (state: RootState) => state.dashboard,\n        shallowEqual,\n    );\n\n    const { pathname } = useLocation();\n\n    const toggleMenuOpen = () => {\n        setIsMenuOpen((isMenuOpen) => !isMenuOpen);\n    };\n\n    const closeMenu = () => {\n        setIsMenuOpen(false);\n    };\n\n    const badgeClass = classnames('badge dns-status', {\n        'badge-success': protectionEnabled,\n        'badge-danger': !protectionEnabled,\n    });\n\n    return (\n        <div className=\"header\">\n            <div className=\"header__container\">\n                <div className=\"header__row\">\n                    <div className=\"header-toggler d-lg-none ml-lg-0 collapsed\" onClick={toggleMenuOpen}>\n                        <span className=\"header-toggler-icon\" />\n                    </div>\n\n                    <div className=\"header__column\">\n                        <div className=\"d-flex align-items-center\">\n                            <Link to=\"/\" className=\"nav-link pl-0 pr-1\">\n                                <Logo className=\"header-brand-img\" />\n                            </Link>\n                            {!processing && isCoreRunning && (\n                                <span className={badgeClass}>{t(protectionEnabled ? 'on' : 'off')}</span>\n                            )}\n                        </div>\n                    </div>\n\n                    <Menu pathname={pathname} isMenuOpen={isMenuOpen} closeMenu={closeMenu} />\n\n                    <div className=\"header__column\">\n                        <div className=\"header__right\">\n                            {!processingProfile && name && (\n                                <a href=\"control/logout\" className=\"btn btn-sm btn-outline-secondary\" data-testid=\"sign_out\">\n                                    {t('sign_out')}\n                                </a>\n                            )}\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    );\n};\n\nexport default Header;\n"
  },
  {
    "path": "client/src/components/Logs/AnonymizerNotification.tsx",
    "content": "import React from 'react';\nimport { Trans } from 'react-i18next';\n\nimport { HashLink as Link } from 'react-router-hash-link';\n\nconst AnonymizerNotification = () => (\n    <div className=\"alert alert-primary mt-6\">\n        <Trans\n            components={[\n                <strong key=\"0\">text</strong>,\n\n                <Link to=\"/settings#logs-config\" key=\"1\">\n                    link\n                </Link>,\n            ]}>\n            anonymizer_notification\n        </Trans>\n    </div>\n);\n\nexport default AnonymizerNotification;\n"
  },
  {
    "path": "client/src/components/Logs/Cells/ClientCell.tsx",
    "content": "import React, { useState } from 'react';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\nimport { nanoid } from 'nanoid';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport { Link, useHistory } from 'react-router-dom';\n\nimport { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';\nimport { BLOCK_ACTIONS } from '../../../helpers/constants';\n\nimport { toggleBlocking, toggleBlockingForClient } from '../../../actions';\n\nimport IconTooltip from './IconTooltip';\n\nimport { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';\nimport { toggleClientBlock } from '../../../actions/access';\nimport { getBlockClientInfo } from './helpers';\nimport { getStats } from '../../../actions/stats';\nimport { updateLogs } from '../../../actions/queryLogs';\nimport { RootState } from '../../../initialState';\n\ninterface ClientCellProps {\n    client: string;\n    client_id?: string;\n    client_info?: {\n        name: string;\n        whois: {\n            country?: string;\n            city?: string;\n            orgname?: string;\n        };\n        disallowed: boolean;\n        disallowed_rule: string;\n    };\n    domain: string;\n    reason: string;\n}\n\nconst ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCellProps) => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const history = useHistory();\n\n    const autoClients = useSelector((state: RootState) => state.dashboard.autoClients, shallowEqual);\n\n    const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);\n\n    const allowedClients = useSelector((state: RootState) => state.access.allowed_clients, shallowEqual);\n    const [isOptionsOpened, setOptionsOpened] = useState(false);\n\n    const autoClient = autoClients.find((autoClient: any) => autoClient.name === client);\n\n    const clients = useSelector((state: RootState) => state.dashboard.clients);\n    const source = autoClient?.source;\n    const whoisAvailable = client_info && Object.keys(client_info.whois).length > 0;\n    const clientName = client_info?.name || client_id;\n    const clientInfo = client_info && {\n        ...client_info,\n        whois_info: client_info?.whois,\n        name: clientName,\n    };\n\n    const id = nanoid();\n\n    const data = {\n        address: client,\n        name: clientName,\n        country: client_info?.whois?.country,\n        city: client_info?.whois?.city,\n        network: client_info?.whois?.orgname,\n        source_label: source,\n    };\n\n    const processedData = Object.entries(data);\n\n    const isFiltered = checkFiltered(reason);\n\n    const clientIds = clients.map((c: any) => c.ids).flat();\n\n    const nameClass = classNames('w-90 o-hidden d-flex flex-column', {\n        'mt-2': isDetailed && !client_info?.name && !whoisAvailable,\n        'white-space--nowrap': isDetailed,\n    });\n\n    const hintClass = classNames('icons mr-4 icon--24 logs__question icon--lightgray', {\n        'my-3': isDetailed,\n    });\n\n    const renderBlockingButton = (isFiltered: any, domain: any) => {\n        const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;\n\n        const {\n            confirmMessage,\n            buttonKey: blockingClientKey,\n            lastRuleInAllowlist,\n        } = getBlockClientInfo(\n            client,\n            client_info?.disallowed || false,\n            client_info?.disallowed_rule || '',\n            allowedClients,\n        );\n\n        const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';\n        const clientNameBlockingFor = getBlockingClientName(clients, client);\n\n        const onClick = async () => {\n            await dispatch(toggleBlocking(buttonType, domain));\n            await dispatch(getStats());\n            setOptionsOpened(false);\n        };\n\n        const BUTTON_OPTIONS = [\n            {\n                name: buttonType,\n                onClick,\n                className: isFiltered ? 'bg--green' : 'bg--danger',\n            },\n            {\n                name: blockingForClientKey,\n                onClick: () => {\n                    dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));\n                    setOptionsOpened(false);\n                },\n            },\n            {\n                name: blockingClientKey,\n                onClick: async () => {\n                    if (window.confirm(confirmMessage)) {\n                        await dispatch(\n                            toggleClientBlock(\n                                client,\n                                client_info?.disallowed || false,\n                                client_info?.disallowed_rule || '',\n                            ),\n                        );\n                        await dispatch(updateLogs());\n                        setOptionsOpened(false);\n                    }\n                },\n                disabled: lastRuleInAllowlist,\n            },\n        ];\n\n        if (!clientIds.includes(client)) {\n            BUTTON_OPTIONS.push({\n                name: 'add_persistent_client',\n                onClick: () => {\n                    history.push(`/#clients?clientId=${client}`);\n                },\n            });\n        }\n\n        const getOptions = (options: any) => {\n            if (options.length === 0) {\n                return null;\n            }\n\n            return (\n                <>\n                    {options.map(({ name, onClick, disabled, className }: any) => (\n                        <button\n                            key={name}\n                            className={classNames('button-action--arrow-option px-4 py-1', className)}\n                            onClick={onClick}\n                            disabled={disabled}>\n                            {t(name)}\n                        </button>\n                    ))}\n                </>\n            );\n        };\n\n        const content = getOptions(BUTTON_OPTIONS);\n\n        const containerClass = classNames('button-action__container', {\n            'button-action__container--detailed': isDetailed,\n        });\n\n        return (\n            <div className={containerClass}>\n                <button type=\"button\" className=\"btn btn-icon btn-sm px-0\" onClick={() => setOptionsOpened(true)}>\n                    <svg className=\"icon24 icon--lightgray button-action__icon\">\n                        <use xlinkHref=\"#bullets\" />\n                    </svg>\n                </button>\n                {isOptionsOpened && (\n                    <IconTooltip\n                        className=\"icon24\"\n                        tooltipClass=\"button-action--arrow-option-container\"\n                        xlinkHref=\"bullets\"\n                        triggerClass=\"btn btn-icon btn-sm px-0 button-action__hidden-trigger\"\n                        content={content}\n                        placement=\"bottom-end\"\n                        trigger=\"click\"\n                        onVisibilityChange={setOptionsOpened}\n                        defaultTooltipShown={true}\n                        delayHide={0}\n                    />\n                )}\n            </div>\n        );\n    };\n\n    return (\n        <div className=\"o-hidden h-100 logs__cell logs__cell--client\" role=\"gridcell\">\n            <IconTooltip\n                className={hintClass}\n                columnClass=\"grid grid--limited\"\n                tooltipClass=\"px-5 pb-5 pt-4\"\n                xlinkHref=\"question\"\n                contentItemClass=\"text-truncate key-colon o-hidden\"\n                title=\"client_details\"\n                content={processedData}\n                placement=\"bottom\"\n            />\n\n            <div className={nameClass}>\n                <div data-tip={true} data-for={id}>\n                    {renderFormattedClientCell(client, clientInfo, isDetailed, true)}\n                </div>\n                {isDetailed && clientName && !whoisAvailable && (\n                    <Link\n                        className=\"detailed-info d-none d-sm-block logs__text logs__text--link logs__text--client\"\n                        to={`logs?search=\"${encodeURIComponent(clientName)}\"`}\n                        title={clientName}>\n                        {clientName}\n                    </Link>\n                )}\n            </div>\n            {renderBlockingButton(isFiltered, domain)}\n        </div>\n    );\n};\n\nexport default ClientCell;\n"
  },
  {
    "path": "client/src/components/Logs/Cells/DateCell.tsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\n\nimport { formatDateTime, formatTime } from '../../../helpers/helpers';\nimport { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT } from '../../../helpers/constants';\nimport { RootState } from '../../../initialState';\n\ninterface DateCellProps {\n    time: string;\n}\n\nconst DateCell = ({ time }: DateCellProps) => {\n    const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);\n\n    if (!time) {\n        return <>–</>;\n    }\n\n    const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);\n\n    const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);\n\n    return (\n        <div className=\"logs__cell logs__cell logs__cell--date text-truncate\" role=\"gridcell\">\n            <div className=\"logs__time\" title={formattedTime}>\n                {formattedTime}\n            </div>\n            {isDetailed && (\n                <div className=\"detailed-info d-none d-sm-block text-truncate\" title={formattedDate}>\n                    {formattedDate}\n                </div>\n            )}\n        </div>\n    );\n};\n\nexport default DateCell;\n"
  },
  {
    "path": "client/src/components/Logs/Cells/DomainCell.tsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport {\n    DEFAULT_SHORT_DATE_FORMAT_OPTIONS,\n    LONG_TIME_FORMAT,\n    SCHEME_TO_PROTOCOL_MAP,\n} from '../../../helpers/constants';\n\nimport { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';\nimport { getSourceData } from '../../../helpers/trackers/trackers';\n\nimport IconTooltip from './IconTooltip';\nimport { RootState } from '../../../initialState';\n\ninterface DomainCellProps {\n    answer_dnssec: boolean;\n    client_proto: string;\n    domain: string;\n    unicodeName?: string;\n    time: string;\n    type: string;\n    tracker?: {\n        name: string;\n        category: string;\n    };\n    ecs?: string;\n}\n\nconst DomainCell = ({\n    answer_dnssec,\n    client_proto,\n    domain,\n    unicodeName,\n    time,\n    tracker,\n    type,\n    ecs,\n}: DomainCellProps) => {\n    const { t } = useTranslation();\n\n    const dnssec_enabled = useSelector((state: RootState) => state.dnsConfig.dnssec_enabled);\n\n    const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);\n\n    const hasTracker = !!tracker;\n\n    const lockIconClass = classNames('icons icon--24 d-none d-sm-block', {\n        'icon--green': answer_dnssec,\n        'icon--disabled': !answer_dnssec,\n        'my-3': isDetailed,\n    });\n\n    const privacyIconClass = classNames('icons mx-2 icon--24 d-none d-sm-block logs__question', {\n        'icon--green': hasTracker,\n        'icon--disabled': !hasTracker,\n        'my-3': isDetailed,\n    });\n\n    const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';\n    const ip = type ? `${t('type_table_header')}: ${type}` : '';\n\n    let requestDetailsObj: {\n        time_table_header: string;\n        date: string;\n        domain: string;\n        punycode?: string;\n        ecs?: string;\n        type_table_header?: string;\n        protocol?: string;\n    } = {\n        time_table_header: formatTime(time, LONG_TIME_FORMAT),\n        date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),\n        domain,\n    };\n\n    if (domain && unicodeName) {\n        requestDetailsObj = {\n            ...requestDetailsObj,\n            domain: unicodeName,\n            punycode: domain,\n        };\n    }\n\n    if (ecs) {\n        requestDetailsObj = {\n            ...requestDetailsObj,\n            ecs,\n        };\n    }\n\n    requestDetailsObj = {\n        ...requestDetailsObj,\n        type_table_header: type,\n        protocol,\n    };\n\n    const sourceData = getSourceData(tracker);\n\n    const knownTrackerDataObj = {\n        name_table_header: tracker?.name,\n        category_label: hasTracker && captitalizeWords(tracker.category),\n        source_label: sourceData && (\n            <a href={sourceData.url} target=\"_blank\" rel=\"noopener noreferrer\" className=\"link--green\">\n                {sourceData.name}\n            </a>\n        ),\n    };\n\n    const renderGrid = (content: any, idx: any) => {\n        const preparedContent = typeof content === 'string' ? t(content) : content;\n\n        const className = classNames('text-truncate o-hidden', { 'overflow-break': preparedContent?.length > 100 });\n\n        return (\n            <div key={idx} className={className}>\n                {preparedContent}\n            </div>\n        );\n    };\n\n    const getGrid = (contentObj: any, title: string, className?: string) => [\n        <div key={title} className={classNames('pb-2 grid--title', className)}>\n            {t(title)}\n        </div>,\n\n        <div key={`${title}-1`} className=\"grid grid--limited\">\n            {React.Children.map(Object.entries(contentObj), renderGrid)}\n        </div>,\n    ];\n\n    const requestDetails = getGrid(requestDetailsObj, 'request_details');\n\n    const renderContent = hasTracker\n        ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4'))\n        : requestDetails;\n\n    const valueClass = classNames('w-100 text-truncate', {\n        'px-2 d-flex justify-content-center flex-column': isDetailed,\n    });\n\n    const details = [ip, protocol].filter(Boolean).join(', ');\n\n    return (\n        <div className=\"d-flex o-hidden logs__cell logs__cell logs__cell--domain\" role=\"gridcell\">\n            {dnssec_enabled && (\n                <IconTooltip\n                    className={lockIconClass}\n                    tooltipClass=\"py-4 px-5 pb-45\"\n                    canShowTooltip={!!answer_dnssec}\n                    xlinkHref=\"lock\"\n                    columnClass=\"w-100\"\n                    content=\"validated_with_dnssec\"\n                    placement=\"bottom\"\n                />\n            )}\n\n            <IconTooltip\n                className={privacyIconClass}\n                tooltipClass=\"pt-4 pb-5 px-5 mw-75\"\n                xlinkHref=\"privacy\"\n                contentItemClass=\"key-colon\"\n                renderContent={renderContent}\n                placement=\"bottom\"\n            />\n\n            <div className={valueClass}>\n                {unicodeName ? (\n                    <div className=\"text-truncate overflow-break-mobile\" title={unicodeName}>\n                        {unicodeName}\n                    </div>\n                ) : (\n                    <div className=\"text-truncate overflow-break-mobile\" title={domain}>\n                        {domain}\n                    </div>\n                )}\n                {details && isDetailed && (\n                    <div className=\"detailed-info d-none d-sm-block text-truncate\" title={details}>\n                        {details}\n                    </div>\n                )}\n            </div>\n        </div>\n    );\n};\n\nexport default DomainCell;\n"
  },
  {
    "path": "client/src/components/Logs/Cells/Header.tsx",
    "content": "import { useTranslation } from 'react-i18next';\nimport { useDispatch, useSelector } from 'react-redux';\nimport classNames from 'classnames';\nimport React from 'react';\nimport { toggleDetailedLogs } from '../../../actions/queryLogs';\n\nimport HeaderCell from './HeaderCell';\nimport { RootState } from '../../../initialState';\n\nconst Header = () => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n\n    const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);\n    const disableDetailedMode = () => dispatch(toggleDetailedLogs(false));\n    const enableDetailedMode = () => dispatch(toggleDetailedLogs(true));\n\n    const HEADERS = [\n        {\n            className: 'logs__cell--date',\n            content: 'time_table_header',\n        },\n        {\n            className: 'logs__cell--domain',\n            content: 'request_table_header',\n        },\n        {\n            className: 'logs__cell--response',\n            content: 'response_table_header',\n        },\n        {\n            className: 'logs__cell--client',\n\n            content: (\n                <>\n                    {t('client_table_header')}\n\n                    {\n                        <span>\n                            <svg\n                                className={classNames('icons icon--24 icon--green cursor--pointer mr-2', {\n                                    'icon--selected': !isDetailed,\n                                })}\n                                onClick={disableDetailedMode}>\n                                <title>{t('compact')}</title>\n\n                                <use xlinkHref=\"#list\" />\n                            </svg>\n\n                            <svg\n                                className={classNames('icons icon--24 icon--green cursor--pointer', {\n                                    'icon--selected': isDetailed,\n                                })}\n                                onClick={enableDetailedMode}>\n                                <title>{t('default')}</title>\n\n                                <use xlinkHref=\"#detailed_list\" />\n                            </svg>\n                        </span>\n                    }\n                </>\n            ),\n        },\n    ];\n\n    return (\n        <div className=\"logs__cell--header__container px-5\" role=\"row\">\n            {HEADERS.map(HeaderCell)}\n        </div>\n    );\n};\n\nexport default Header;\n"
  },
  {
    "path": "client/src/components/Logs/Cells/HeaderCell.tsx",
    "content": "import classNames from 'classnames';\nimport React from 'react';\nimport { useTranslation } from 'react-i18next';\n\ninterface HeaderCellProps {\n    content: string | React.ReactElement;\n    className?: string;\n}\n\nconst HeaderCell = ({ content, className }: HeaderCellProps, idx: any) => {\n    const { t } = useTranslation();\n\n    return (\n        <div\n            key={idx}\n            className={classNames('logs__cell--header__item logs__cell logs__text--bold', className)}\n            role=\"columnheader\">\n            {typeof content === 'string' ? t(content) : content}\n        </div>\n    );\n};\n\nexport default HeaderCell;\n"
  },
  {
    "path": "client/src/components/Logs/Cells/IconTooltip.css",
    "content": ".tooltip-custom__container {\n    min-width: 150px;\n    padding: 1rem 1.5rem 1.25rem 1.5rem;\n    font-size: 16px !important;\n    box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);\n    border-radius: 4px !important;\n    pointer-events: auto !important;\n    background-color: var(--ctrl-bgcolor);\n    color: var(--mcolor);\n    z-index: 102;\n    overflow-y: auto;\n    max-height: 100%;\n}\n\n.white-space--nowrap {\n    white-space: nowrap !important;\n}\n\n.overflow-break {\n    white-space: normal !important;\n    overflow-wrap: break-word;\n}\n\n@media (max-width: 991.98px) {\n    .overflow-break-mobile {\n        white-space: normal !important;\n        overflow-wrap: break-word;\n    }\n}\n\n.grid {\n    display: grid;\n    grid-template-columns: repeat(2, min-content);\n    grid-row-gap: 0.5rem;\n    grid-column-gap: 1rem;\n}\n\n.grid--limited {\n    grid-template-columns: repeat(2, minmax(0, min-content));\n}\n\n.grid--gap-bg {\n    grid-column-gap: 1.5rem;\n}\n\n.grid--title {\n    font-weight: 600;\n}\n\n.grid--title:not(:first-child) {\n    padding-top: 1rem;\n}\n\n@media (max-width: 1024px) {\n    .grid .title--border {\n        margin-bottom: 4px;\n        font-weight: 600;\n    }\n\n    .grid .key-colon {\n        margin-right: 4px;\n        color: var(--gray-8);\n    }\n\n    .grid__row {\n        display: flex;\n        align-items: flex-start;\n        flex-wrap: wrap;\n        margin-bottom: 2px;\n        font-size: 14px;\n        word-break: break-all;\n        overflow: hidden;\n    }\n\n    .grid__row .filteringRules__filter,\n    .grid__row .filteringRules {\n        margin-bottom: 0;\n    }\n}\n\n@media (max-width: 767.98px) {\n    .grid {\n        grid-template-columns: 35% 55%;\n    }\n\n    .grid * {\n        grid-column: 1 / -1;\n    }\n\n    .grid > :nth-child(even) {\n        margin: -0.5rem 0 0;\n    }\n\n    .grid > .key__time_table_header,\n    .grid > .key__data,\n    .grid > .key__encryption_status,\n    .grid > .key__elapsed {\n        grid-column: 1 / span 1;\n    }\n\n    .grid > .value__time_table_header,\n    .grid > .value__data,\n    .grid > .value__encryption_status,\n    .grid > .value__elapsed {\n        grid-column: 2 / span 1;\n        margin: 0 !important;\n    }\n}\n\n.grid .key-colon:nth-child(odd)::after {\n    content: ':';\n}\n\n.grid__one-row {\n    grid-template-columns: 15rem;\n}\n\n.grid__flow-column {\n    grid-auto-flow: column;\n}\n\n.grid-content > * {\n    justify-content: space-between !important;\n    width: 100% !important;\n    overflow: hidden;\n    -o-text-overflow: ellipsis;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.title--border {\n    padding-top: 1rem;\n}\n\n.title--border:before {\n    content: '';\n    position: absolute;\n    left: 0;\n    border-top: 0.5px solid var(--gray-d8) !important;\n    width: 100%;\n    margin-top: -0.5rem;\n}\n\n.icon-cross {\n    position: absolute;\n    right: 0.5rem;\n    top: 0.5rem;\n}\n"
  },
  {
    "path": "client/src/components/Logs/Cells/IconTooltip.tsx",
    "content": "import React from 'react';\nimport { Trans } from 'react-i18next';\nimport classNames from 'classnames';\nimport PopperJS from 'popper.js';\nimport { TriggerTypes } from 'react-popper-tooltip';\n\nimport { processContent } from '../../../helpers/helpers';\n\nimport Tooltip from '../../ui/Tooltip';\nimport 'react-popper-tooltip/dist/styles.css';\nimport './IconTooltip.css';\nimport { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants';\n\ninterface IconTooltipProps {\n    className?: string;\n    trigger?: TriggerTypes;\n    triggerClass?: string;\n    contentItemClass?: string;\n    columnClass?: string;\n    tooltipClass?: string;\n    title?: string;\n    placement?: PopperJS.Placement;\n    canShowTooltip?: boolean;\n    xlinkHref?: string;\n    content?: React.ReactNode;\n    renderContent?: React.ReactElement[];\n    onVisibilityChange?: (...args: unknown[]) => unknown;\n    defaultTooltipShown?: boolean;\n    delayHide?: number;\n}\n\nconst IconTooltip = ({\n    className,\n    contentItemClass,\n    columnClass,\n    triggerClass,\n    canShowTooltip = true,\n    xlinkHref,\n    title,\n    placement,\n    tooltipClass,\n    content,\n    trigger,\n    onVisibilityChange,\n    defaultTooltipShown,\n    delayHide,\n\n    renderContent = content\n        ? React.Children.map(\n              processContent(content),\n\n              (item, idx) => (\n                  <div key={idx} className={contentItemClass}>\n                      <Trans>{item || '—'}</Trans>\n                  </div>\n              ),\n          )\n        : null,\n}: IconTooltipProps) => {\n    const tooltipContent = (\n        <>\n            {title && (\n                <div className=\"pb-4 h-25 grid-content font-weight-bold\">\n                    <Trans>{title}</Trans>\n                </div>\n            )}\n\n            <div className={classNames(columnClass)}>{renderContent}</div>\n        </>\n    );\n\n    const tooltipClassName = classNames('tooltip-custom__container', tooltipClass, { 'd-none': !canShowTooltip });\n\n    return (\n        <Tooltip\n            className={tooltipClassName}\n            content={tooltipContent}\n            placement={placement}\n            triggerClass={triggerClass}\n            trigger={trigger}\n            onVisibilityChange={onVisibilityChange}\n            delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}\n            delayHide={delayHide}\n            defaultTooltipShown={defaultTooltipShown}>\n            {xlinkHref && (\n                <svg className={className}>\n                    <use xlinkHref={`#${xlinkHref}`} />\n                </svg>\n            )}\n        </Tooltip>\n    );\n};\n\nexport default IconTooltip;\n"
  },
  {
    "path": "client/src/components/Logs/Cells/ResponseCell.tsx",
    "content": "import { useTranslation } from 'react-i18next';\nimport { shallowEqual, useSelector } from 'react-redux';\nimport classNames from 'classnames';\nimport React from 'react';\nimport { getRulesToFilterList, formatElapsedMs, getFilterNames, getServiceName } from '../../../helpers/helpers';\nimport { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';\n\nimport IconTooltip from './IconTooltip';\nimport { RootState } from '../../../initialState';\n\ninterface ResponseCellProps {\n    elapsedMs: string;\n    originalResponse?: unknown[];\n    reason: string;\n    response: unknown[];\n    status: string;\n    upstream: string;\n    cached: boolean;\n    rules?: {\n        text: string;\n        filter_list_id: number;\n    }[];\n    service_name?: string;\n}\n\nconst ResponseCell = ({\n    elapsedMs,\n    originalResponse,\n    reason,\n    response,\n    status,\n    upstream,\n    rules,\n    service_name,\n    cached,\n}: ResponseCellProps) => {\n    const { t } = useTranslation();\n\n    const filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual);\n\n    const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual);\n\n    const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);\n\n    const services = useSelector((store: RootState) => store?.services);\n\n    const formattedElapsedMs = formatElapsedMs(elapsedMs, t);\n\n    const isBlocked =\n        reason === FILTERED_STATUS.FILTERED_BLACK_LIST || reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;\n\n    const isBlockedByResponse = originalResponse.length > 0 && isBlocked;\n\n    const statusLabel = t(\n        isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason,\n    );\n\n    const boldStatusLabel = <span className=\"font-weight-bold\">{statusLabel}</span>;\n\n    const renderResponses = (responseArr: any) => {\n        if (!responseArr || responseArr.length === 0) {\n            return '';\n        }\n\n        return (\n            <div>\n                {responseArr.map((response: any) => {\n                    const className = classNames('white-space--nowrap', {\n                        'overflow-break': response.length > 100,\n                    });\n\n                    return <div key={response} className={className}>{`${response}\\n`}</div>;\n                })}\n            </div>\n        );\n    };\n\n    const COMMON_CONTENT = {\n        encryption_status: boldStatusLabel,\n        install_settings_dns: upstream,\n        ...(cached && {\n            served_from_cache_label: (\n                <svg className=\"icons icon--20 icon--green mb-1\">\n                    <use xlinkHref=\"#check\" />\n                </svg>\n            ),\n        }),\n        elapsed: formattedElapsedMs,\n        response_code: status,\n        ...(service_name &&\n            services.allServices && { service_name: getServiceName(services.allServices, service_name) }),\n        ...(rules.length > 0 && { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }),\n        response_table_header: renderResponses(response),\n        original_response: renderResponses(originalResponse),\n    };\n\n    const content =\n        rules.length > 0\n            ? Object.entries(COMMON_CONTENT)\n            : Object.entries({\n                  ...COMMON_CONTENT,\n                  filter: '',\n              });\n\n    const getDetailedInfo = (reason: any) => {\n        switch (reason) {\n            case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE:\n                if (!service_name || !services.allServices) {\n                    return formattedElapsedMs;\n                }\n                return getServiceName(services.allServices, service_name);\n            case FILTERED_STATUS.FILTERED_BLACK_LIST:\n            case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:\n                return getFilterNames(rules, filters, whitelistFilters).join(', ');\n            default:\n                return formattedElapsedMs;\n        }\n    };\n\n    const detailedInfo = getDetailedInfo(reason);\n\n    return (\n        <div className=\"logs__cell logs__cell--response\" role=\"gridcell\">\n            <IconTooltip\n                className={classNames('icons mr-4 icon--24 icon--lightgray logs__question', { 'my-3': isDetailed })}\n                columnClass=\"grid grid--limited\"\n                tooltipClass=\"px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details\"\n                contentItemClass=\"text-truncate key-colon o-hidden\"\n                xlinkHref=\"question\"\n                title=\"response_details\"\n                content={content}\n                placement=\"bottom\"\n            />\n\n            <div className=\"text-truncate\">\n                <div className=\"text-truncate\" title={statusLabel}>\n                    {statusLabel}\n                </div>\n\n                {isDetailed && (\n                    <div className=\"detailed-info d-none d-sm-block pt-1 text-truncate\" title={detailedInfo}>\n                        {detailedInfo}\n                    </div>\n                )}\n            </div>\n        </div>\n    );\n};\n\nexport default ResponseCell;\n"
  },
  {
    "path": "client/src/components/Logs/Cells/helpers/index.ts",
    "content": "import i18next from 'i18next';\nimport { splitByNewLine } from '../../../../helpers/helpers';\n\nexport const BUTTON_PREFIX = 'btn_';\n\nexport const getBlockClientInfo = (ip: any, disallowed: any, disallowed_rule: any, allowedClients: any) => {\n    let confirmMessage;\n\n    if (disallowed) {\n        confirmMessage = i18next.t('client_confirm_unblock', { ip: disallowed_rule || ip });\n    } else {\n        confirmMessage = `${i18next.t('adg_will_drop_dns_queries')} ${i18next.t('client_confirm_block', { ip })}`;\n        if (allowedClients.length > 0) {\n            confirmMessage = confirmMessage.concat(`\\n\\n${i18next.t('filter_allowlist', { disallowed_rule: ip })}`);\n        }\n    }\n\n    const buttonKey = i18next.t(disallowed ? 'allow_this_client' : 'disallow_this_client');\n    const allowedClientsList = splitByNewLine(allowedClients || '');\n    const lastRuleInAllowlist = !disallowed && allowedClientsList.length === 1 && allowedClientsList[0] === ip;\n\n    return {\n        confirmMessage,\n        buttonKey,\n        lastRuleInAllowlist,\n    };\n};\n"
  },
  {
    "path": "client/src/components/Logs/Cells/index.tsx",
    "content": "import React, { Dispatch, memo, SetStateAction } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport {\n    captitalizeWords,\n    checkFiltered,\n    getRulesToFilterList,\n    formatDateTime,\n    formatElapsedMs,\n    formatTime,\n    getBlockingClientName,\n    getServiceName,\n    processContent,\n} from '../../../helpers/helpers';\nimport {\n    BLOCK_ACTIONS,\n    DEFAULT_SHORT_DATE_FORMAT_OPTIONS,\n    FILTERED_STATUS,\n    FILTERED_STATUS_TO_META_MAP,\n    LONG_TIME_FORMAT,\n    QUERY_STATUS_COLORS,\n    SCHEME_TO_PROTOCOL_MAP,\n} from '../../../helpers/constants';\nimport { getSourceData } from '../../../helpers/trackers/trackers';\n\nimport { toggleBlocking, toggleBlockingForClient } from '../../../actions';\n\nimport DateCell from './DateCell';\n\nimport DomainCell from './DomainCell';\n\nimport ResponseCell from './ResponseCell';\n\nimport ClientCell from './ClientCell';\nimport { toggleClientBlock } from '../../../actions/access';\nimport { getBlockClientInfo, BUTTON_PREFIX } from './helpers';\nimport { updateLogs } from '../../../actions/queryLogs';\n\nimport '../Logs.css';\nimport { RootState } from '../../../initialState';\n\ninterface RowProps {\n    style?: object;\n    rowProps: {\n        reason: string;\n        answer_dnssec: boolean;\n        client: string;\n        domain: string;\n        elapsedMs: string;\n        response: unknown[];\n        time: string;\n        tracker?: {\n            name: string;\n            category: string;\n        };\n        upstream: string;\n        cached: boolean;\n        type: string;\n        client_proto: string;\n        client_id?: string;\n        ecs?: string;\n        client_info?: {\n            name: string;\n            whois: {\n                country?: string;\n                city?: string;\n                orgname?: string;\n            };\n            disallowed: boolean;\n            disallowed_rule: string;\n        };\n        rules?: {\n            text: string;\n            filter_list_id: number;\n        }[];\n        originalResponse?: unknown[];\n        status: string;\n        service_name?: string;\n    };\n    isSmallScreen: boolean;\n    setDetailedDataCurrent: Dispatch<SetStateAction<any>>;\n    setButtonType: (...args: unknown[]) => unknown;\n    setModalOpened: (...args: unknown[]) => unknown;\n}\n\nconst Row = memo(\n    ({\n        style,\n        rowProps,\n        rowProps: { reason },\n        isSmallScreen,\n        setDetailedDataCurrent,\n        setButtonType,\n        setModalOpened,\n    }: RowProps) => {\n        const dispatch = useDispatch();\n        const { t } = useTranslation();\n\n        const dnssec_enabled = useSelector((state: RootState) => state.dnsConfig.dnssec_enabled);\n\n        const filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual);\n\n        const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual);\n\n        const autoClients = useSelector((state: RootState) => state.dashboard.autoClients, shallowEqual);\n\n        const processingSet = useSelector((state: RootState) => state.access.processingSet);\n\n        const allowedClients = useSelector((state: RootState) => state.access.allowed_clients, shallowEqual);\n\n        const services = useSelector((state: RootState) => state?.services);\n\n        const clients = useSelector((state: RootState) => state.dashboard.clients);\n\n        const onClick = () => {\n            if (!isSmallScreen) {\n                return;\n            }\n            const {\n                answer_dnssec,\n                client,\n                domain,\n                elapsedMs,\n                client_info,\n                response,\n                time,\n                tracker,\n                upstream,\n                type,\n                client_proto,\n                client_id,\n                rules,\n                originalResponse,\n                status,\n                service_name,\n                cached,\n            } = rowProps;\n\n            const hasTracker = !!tracker;\n\n            const autoClient = autoClients.find((autoClient: any) => autoClient.name === client);\n\n            const source = autoClient?.source;\n\n            const formattedElapsedMs = formatElapsedMs(elapsedMs, t);\n            const isFiltered = checkFiltered(reason);\n\n            const isBlocked =\n                reason === FILTERED_STATUS.FILTERED_BLACK_LIST || reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;\n\n            const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;\n            const onToggleBlock = () => {\n                dispatch(toggleBlocking(buttonType, domain));\n            };\n\n            const isBlockedByResponse = originalResponse.length > 0 && isBlocked;\n            const requestStatus = t(\n                isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason,\n            );\n\n            const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';\n\n            const sourceData = getSourceData(tracker);\n\n            const {\n                confirmMessage,\n                buttonKey: blockingClientKey,\n                lastRuleInAllowlist,\n            } = getBlockClientInfo(\n                client,\n                client_info?.disallowed || false,\n                client_info?.disallowed_rule || '',\n                allowedClients,\n            );\n\n            const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';\n            const clientNameBlockingFor = getBlockingClientName(clients, client);\n\n            const onBlockingForClientClick = () => {\n                dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));\n            };\n\n            const onBlockingClientClick = async () => {\n                if (window.confirm(confirmMessage)) {\n                    await dispatch(\n                        toggleClientBlock(client, client_info?.disallowed || false, client_info?.disallowed_rule || ''),\n                    );\n                    await dispatch(updateLogs());\n                    setModalOpened(false);\n                }\n            };\n\n            const blockButton = (\n                <>\n                    <div className=\"title--border\" />\n\n                    <button\n                        type=\"button\"\n                        className={classNames(\n                            'button-action--arrow-option mb-1',\n                            { 'bg--danger': !isBlocked },\n                            { 'bg--green': isFiltered },\n                        )}\n                        onClick={onToggleBlock}>\n                        {t(buttonType)}\n                    </button>\n                </>\n            );\n\n            const blockForClientButton = (\n                <button\n                    className=\"text-center font-weight-bold py-1 button-action--arrow-option\"\n                    onClick={onBlockingForClientClick}>\n                    {t(blockingForClientKey)}\n                </button>\n            );\n\n            const blockClientButton = (\n                <button\n                    className=\"text-center font-weight-bold py-1 button-action--arrow-option\"\n                    onClick={onBlockingClientClick}\n                    disabled={processingSet || lastRuleInAllowlist}>\n                    {t(blockingClientKey)}\n                </button>\n            );\n\n            const detailedData = {\n                time_table_header: formatTime(time, LONG_TIME_FORMAT),\n\n                date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),\n                encryption_status: isBlocked ? <div className=\"bg--danger\">{requestStatus}</div> : requestStatus,\n                ...(FILTERED_STATUS.FILTERED_BLOCKED_SERVICE &&\n                    service_name &&\n                    services.allServices && { service_name: getServiceName(services.allServices, service_name) }),\n                domain,\n                type_table_header: type,\n                protocol,\n                known_tracker: hasTracker && 'title',\n                table_name: tracker?.name,\n                category_label: hasTracker && captitalizeWords(tracker.category),\n                tracker_source: hasTracker && sourceData && (\n                    <a href={sourceData.url} target=\"_blank\" rel=\"noopener noreferrer\" className=\"link--green\">\n                        {sourceData.name}\n                    </a>\n                ),\n                response_details: 'title',\n                install_settings_dns: upstream,\n                ...(cached && {\n                    served_from_cache_label: (\n                        <svg className=\"icons icon--20 icon--green\">\n                            <use xlinkHref=\"#check\" />\n                        </svg>\n                    ),\n                }),\n                elapsed: formattedElapsedMs,\n                ...(rules.length > 0 && { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }),\n                response_table_header: response?.join('\\n'),\n                response_code: status,\n                client_details: 'title',\n                ip_address: client,\n                name: client_info?.name || client_id,\n                country: client_info?.whois?.country,\n                city: client_info?.whois?.city,\n                network: client_info?.whois?.orgname,\n                source_label: source,\n                validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,\n                original_response: originalResponse?.join('\\n'),\n                [BUTTON_PREFIX + buttonType]: blockButton,\n                [BUTTON_PREFIX + blockingForClientKey]: blockForClientButton,\n                [BUTTON_PREFIX + blockingClientKey]: blockClientButton,\n            };\n\n            setDetailedDataCurrent(processContent(detailedData));\n            setButtonType(buttonType);\n            setModalOpened(true);\n        };\n\n        const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);\n\n        const className = classNames(\n            'd-flex px-5 logs__row',\n            `logs__row--${FILTERED_STATUS_TO_META_MAP?.[reason]?.COLOR ?? QUERY_STATUS_COLORS.WHITE}`,\n            {\n                'logs__cell--detailed': isDetailed,\n            },\n        );\n\n        return (\n            <div style={style} className={className} onClick={onClick} role=\"row\" data-testid=\"querylog_cell\">\n                <DateCell {...rowProps} />\n\n                <DomainCell {...rowProps} />\n\n                <ResponseCell {...rowProps} />\n\n                <ClientCell {...rowProps} />\n            </div>\n        );\n    },\n);\n\nRow.displayName = 'Row';\n\nexport default Row;\n"
  },
  {
    "path": "client/src/components/Logs/Disabled.tsx",
    "content": "import React, { Fragment } from 'react';\nimport { Trans } from 'react-i18next';\n\nimport { HashLink as Link } from 'react-router-hash-link';\n\nimport Card from '../ui/Card';\n\nconst Disabled = () => (\n    <Fragment>\n        <div className=\"page-header\">\n            <h1 className=\"page-title page-title--large\">\n                <Trans>query_log</Trans>\n            </h1>\n        </div>\n\n        <Card>\n            <div className=\"lead text-center py-6\">\n                <Trans\n                    components={[\n                        <Link to=\"/settings#logs-config\" key=\"0\">\n                            link\n                        </Link>,\n                    ]}>\n                    query_log_disabled\n                </Trans>\n            </div>\n        </Card>\n    </Fragment>\n);\n\nexport default Disabled;\n"
  },
  {
    "path": "client/src/components/Logs/Filters/Form.tsx",
    "content": "import React, { useEffect } from 'react';\n\nimport { useTranslation } from 'react-i18next';\nimport { useDispatch } from 'react-redux';\n\nimport { useHistory } from 'react-router-dom';\nimport classNames from 'classnames';\nimport { useFormContext } from 'react-hook-form';\nimport queryString from 'query-string';\n\nimport {\n    DEBOUNCE_FILTER_TIMEOUT,\n    DEFAULT_LOGS_FILTER,\n    RESPONSE_FILTER,\n    RESPONSE_FILTER_QUERIES,\n} from '../../../helpers/constants';\nimport { setLogsFilter } from '../../../actions/queryLogs';\nimport useDebounce from '../../../helpers/useDebounce';\n\nimport { getLogsUrlParams } from '../../../helpers/helpers';\n\nimport { SearchField } from './SearchField';\nimport { SearchFormValues } from '..';\n\ntype Props = {\n    className?: string;\n    setIsLoading: (value: boolean) => void;\n};\n\nexport const Form = ({ className, setIsLoading }: Props) => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const history = useHistory();\n\n    const { register, watch, setValue } = useFormContext<SearchFormValues>();\n\n    const searchValue = watch('search');\n    const responseStatusValue = watch('response_status');\n\n    const [debouncedSearch, setDebouncedSearch] = useDebounce(searchValue.trim(), DEBOUNCE_FILTER_TIMEOUT);\n\n    useEffect(() => {\n        dispatch(\n            setLogsFilter({\n                response_status: responseStatusValue,\n                search: debouncedSearch,\n            }),\n        );\n\n        history.replace(`${getLogsUrlParams(debouncedSearch, responseStatusValue)}`);\n    }, [responseStatusValue, debouncedSearch]);\n\n    useEffect(() => {\n        if (responseStatusValue && !(responseStatusValue in RESPONSE_FILTER_QUERIES)) {\n            setValue('response_status', DEFAULT_LOGS_FILTER.response_status);\n        }\n    }, [responseStatusValue, setValue]);\n\n    useEffect(() => {\n        const { search: searchUrlParam } = queryString.parse(history.location.search);\n\n        if (searchUrlParam !== searchValue) {\n            setValue('search', searchUrlParam ? searchUrlParam.toString() : '');\n        }\n    }, [history.location.search]);\n\n    const onInputClear = async () => {\n        setIsLoading(true);\n        history.push(getLogsUrlParams(DEFAULT_LOGS_FILTER.search, responseStatusValue));\n        setIsLoading(false);\n    };\n\n    const onEnterPress = (e: React.KeyboardEvent<HTMLInputElement>) => {\n        if (e.key === 'Enter') {\n            setDebouncedSearch(searchValue);\n        }\n    };\n\n    return (\n        <form\n            className=\"d-flex flex-wrap form-control--container\"\n            onSubmit={(e) => {\n                e.preventDefault();\n            }}>\n            <div className=\"field__search\">\n                <SearchField\n                    data-testid=\"querylog_search\"\n                    value={searchValue}\n                    handleChange={(val) => setValue('search', val)}\n                    onKeyDown={onEnterPress}\n                    onClear={onInputClear}\n                    placeholder={t('domain_or_client')}\n                    tooltip={t('query_log_strict_search')}\n                    className={classNames('form-control form-control--search form-control--transparent', className)}\n                />\n            </div>\n\n            <div className=\"field__select\">\n                <select\n                    {...register('response_status')}\n                    className=\"form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent d-sm-block\">\n                    {Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (\n                        <option key={LABEL} value={QUERY} disabled={disabled}>\n                            {t(LABEL)}\n                        </option>\n                    ))}\n                </select>\n            </div>\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Logs/Filters/SearchField.tsx",
    "content": "import React, { ComponentProps } from 'react';\nimport Tooltip from '../../ui/Tooltip';\n\ninterface Props extends ComponentProps<'input'> {\n    handleChange: (newValue: string) => void;\n    onClear: () => void;\n    tooltip?: string;\n}\n\nexport const SearchField = ({\n    handleChange,\n    onClear,\n    value,\n    tooltip,\n    className,\n    ...rest\n}: Props) => {\n    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n        handleChange(e.target.value);\n    };\n\n    const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {\n        e.target.value = e.target.value.trim();\n        handleChange(e.target.value)\n    }\n\n    return (\n        <>\n            <div className=\"input-group-search input-group-search__icon--magnifier\">\n                <svg className=\"icons icon--24 icon--gray\">\n                    <use xlinkHref=\"#magnifier\" />\n                </svg>\n            </div>\n            <input\n                className={className}\n                value={value}\n                onChange={handleInputChange}\n                onBlur={handleBlur}\n                {...rest}\n            />\n            {typeof value === 'string' && value.length > 0 && (\n                <div\n                    className=\"input-group-search input-group-search__icon--cross\"\n                    onClick={onClear}\n                >\n                    <svg className=\"icons icon--20 icon--gray\">\n                    <use xlinkHref=\"#cross\" />\n                    </svg>\n                </div>\n            )}\n            {tooltip && (\n                <span className=\"input-group-search input-group-search__icon--tooltip\">\n                    <Tooltip content={tooltip} className=\"tooltip-container\">\n                        <svg className=\"icons icon--20 icon--gray\">\n                            <use xlinkHref=\"#question\" />\n                        </svg>\n                    </Tooltip>\n                </span>\n            )}\n        </>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Logs/Filters/index.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useDispatch } from 'react-redux';\n\nimport { Form } from './Form';\nimport { refreshFilteredLogs } from '../../../actions/queryLogs';\nimport { addSuccessToast } from '../../../actions/toasts';\n\ninterface FiltersProps {\n    processingGetLogs: boolean;\n    setIsLoading: (...args: unknown[]) => unknown;\n}\n\nconst Filters = ({ setIsLoading }: FiltersProps) => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n\n    const refreshLogs = async () => {\n        setIsLoading(true);\n        await dispatch(refreshFilteredLogs());\n        dispatch(addSuccessToast('query_log_updated'));\n        setIsLoading(false);\n    };\n\n    return (\n        <div className=\"page-header page-header--logs\">\n            <h1 className=\"page-title page-title--large\">\n                {t('query_log')}\n\n                <button\n                    type=\"button\"\n                    className=\"btn btn-icon--green logs__refresh\"\n                    title={t('refresh_btn')}\n                    onClick={refreshLogs}>\n                    <svg className=\"icons icon--24\">\n                        <use xlinkHref=\"#update\" />\n                    </svg>\n                </button>\n            </h1>\n            <Form\n                setIsLoading={setIsLoading}\n            />\n        </div>\n    );\n};\n\nexport default Filters;\n"
  },
  {
    "path": "client/src/components/Logs/InfiniteTable.tsx",
    "content": "import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useTranslation } from 'react-i18next';\nimport throttle from 'lodash/throttle';\n\nimport Loading from '../ui/Loading';\n\nimport Header from './Cells/Header';\nimport { getLogs } from '../../actions/queryLogs';\n\nimport Row from './Cells';\n\nimport { isScrolledIntoView } from '../../helpers/helpers';\nimport { QUERY_LOGS_PAGE_LIMIT } from '../../helpers/constants';\nimport { RootState } from '../../initialState';\n\ninterface InfiniteTableProps {\n    isLoading: boolean;\n    items: unknown[];\n    isSmallScreen: boolean;\n    currentQuery: string;\n    setDetailedDataCurrent: Dispatch<SetStateAction<any>>;\n    setButtonType: (...args: unknown[]) => unknown;\n    setModalOpened: (...args: unknown[]) => unknown;\n}\n\nconst InfiniteTable = ({\n    isLoading,\n    items,\n    isSmallScreen,\n    currentQuery,\n    setDetailedDataCurrent,\n    setButtonType,\n    setModalOpened,\n}: InfiniteTableProps) => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const loader = useRef(null);\n    const loadingRef = useRef(null);\n\n    const isEntireLog = useSelector((state: RootState) => state.queryLogs.isEntireLog);\n\n    const processingGetLogs = useSelector((state: RootState) => state.queryLogs.processingGetLogs);\n    const loading = isLoading || processingGetLogs;\n\n    const listener = useCallback(() => {\n        if (!loadingRef.current && loader.current && isScrolledIntoView(loader.current)) {\n            dispatch(getLogs(currentQuery));\n        }\n    }, []);\n\n    useEffect(() => {\n        loadingRef.current = processingGetLogs;\n    }, [processingGetLogs]);\n\n    useEffect(() => {\n        listener();\n    }, [items.length < QUERY_LOGS_PAGE_LIMIT, isEntireLog]);\n\n    useEffect(() => {\n        const THROTTLE_TIME = 100;\n        const throttledListener = throttle(listener, THROTTLE_TIME);\n\n        window.addEventListener('scroll', throttledListener);\n        return () => {\n            window.removeEventListener('scroll', throttledListener);\n        };\n    }, []);\n\n    const renderRow = (row: any, idx: any) => (\n        <Row\n            key={idx}\n            rowProps={row}\n            isSmallScreen={isSmallScreen}\n            setDetailedDataCurrent={setDetailedDataCurrent}\n            setButtonType={setButtonType}\n            setModalOpened={setModalOpened}\n        />\n    );\n\n    const isNothingFound = items.length === 0 && !processingGetLogs;\n\n    return (\n        <div className=\"logs__table\" role=\"grid\">\n            {loading && <Loading />}\n\n            <Header />\n            {isNothingFound ? (\n                <label className=\"logs__no-data\">{t('nothing_found')}</label>\n            ) : (\n                <>\n                    {items.map(renderRow)}\n                    {!isEntireLog && (\n                        <div ref={loader} className=\"logs__loading text-center\">\n                            {t('loading_table_status')}\n                        </div>\n                    )}\n                </>\n            )}\n        </div>\n    );\n};\n\nexport default InfiniteTable;\n"
  },
  {
    "path": "client/src/components/Logs/Logs.css",
    "content": ":root {\n    --blue: #e5effd;\n    --green-pale: rgba(103, 178, 121, 0.1);\n    --red: rgba(223, 56, 18, 0.05);\n    --white: #fff;\n    --yellow: rgba(247, 181, 0, 0.1);\n    --size-date: 70;\n    --size-domain: 180;\n    --size-response: 150;\n    --size-client: 123;\n    --gray-216: rgba(216, 216, 216, 0.23);\n    --gray-4d: #4d4d4d;\n    --gray-f3: #f3f3f3;\n    --gray-8: #888;\n    --gray-3: #333;\n    --danger: #df3812;\n    --white80: rgba(255, 255, 255, 0.8);\n    --btn-block: #c23814;\n    --btn-block-disabled: #e3b3a6;\n    --btn-block-active: #a62200;\n    --btn-unblock: #888888;\n    --btn-unblock-disabled: #d8d8d8;\n    --btn-unblock-active: #4d4d4d;\n    --option-border-radius: 4px;\n}\n\n[data-theme='dark'] {\n    --red: rgba(223, 56, 18, 0.25);\n    --green-pale: rgba(103, 178, 121, 0.25);\n    --yellow: rgba(247, 181, 0, 0.2);\n}\n\n.logs__text {\n    padding: 0 1px;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n    font-size: 1rem;\n    font-family: var(--font-family-sans-serif);\n    color: var(--logs__text-color);\n    letter-spacing: 0;\n    line-height: 1.5rem;\n}\n\n[data-theme='dark'] .logs__text a {\n    color: var(--gray-f3);\n}\n\n[data-theme='dark'] .logs__text a:hover {\n    color: var(--gray-f3);\n}\n\n.logs__text--bold {\n    font-weight: 600;\n}\n\n.logs__time {\n    font-size: 1rem;\n    line-height: 1.5;\n}\n\n.detailed-info {\n    font-size: 0.8rem;\n    line-height: 1.4;\n    color: var(--detailed-info-color);\n}\n\n.logs__text--link {\n    color: #467fcf;\n}\n\n.logs__text--link:hover,\n.logs__text--link:focus {\n    color: #295a9f;\n}\n\n[data-theme='dark'] .logs__text--link,\n[data-theme='dark'] .logs__text--link:hover,\n[data-theme='dark'] .logs__text--link:focus {\n    color: var(--gray-f3);\n}\n\n.logs__table .logs__text--client {\n    padding-right: 32px;\n}\n\n.icon--selected {\n    background-color: var(--gray-f3);\n    border: solid 1px var(--gray-d8);\n    border-radius: 4px;\n}\n\n[data-theme='dark'] .icon--selected {\n    opacity: 0.75;\n}\n\n.text-pre {\n    white-space: pre-wrap !important;\n    overflow-wrap: break-word;\n    overflow: visible;\n}\n\n.link--green {\n    color: var(--green79);\n}\n\n.w-90 {\n    max-width: 90% !important;\n}\n\n.pb-45 {\n    padding-bottom: 1.25rem !important;\n}\n\n.mh-100 {\n    max-height: 100% !important;\n}\n\n.icon24 {\n    width: 24px;\n    height: 24px;\n}\n\n.icon12 {\n    width: 12px;\n    height: 12px;\n}\n\n.cursor--pointer {\n    cursor: pointer;\n}\n\n.custom-select__arrow--left {\n    background: var(--white) url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj4KICAgIDxwb2x5bGluZSBwb2ludHM9IjYgOSAxMiAxNSAxOCA5Ij48L3BvbHlsaW5lPgo8L3N2Zz4K\") no-repeat;\n    background-position: 5px 9px;\n    background-size: 22px;\n}\n\n.custom-select--logs {\n    padding: 0.5rem 0.75rem 0.5rem 2rem !important;\n}\n\n.form-control--search {\n    padding: 0 2.5rem;\n    height: 2.25rem;\n    flex-grow: 1;\n}\n\n.form-control--transparent {\n    background-color: transparent !important;\n}\n\n[data-theme='dark'] .form-control--transparent option {\n    background-color: var(--card-bgcolor);\n}\n\n.input-group-search {\n    background-color: transparent;\n    position: relative;\n    width: 1.5rem;\n    height: 1.5rem;\n    top: 0.4rem;\n}\n\n.input-group-search__icon--magnifier {\n    left: 2rem;\n}\n\n.input-group-search__icon--cross {\n    left: -4.5rem;\n    cursor: pointer;\n}\n\n.input-group-search__icon--tooltip {\n    left: -4rem;\n}\n\n.form-control--container {\n    flex: auto;\n}\n\n.field__search {\n    display: flex;\n    flex-grow: 1;\n}\n\n@media (max-width: 767.98px) {\n    .form-control--container {\n        width: 100%;\n        flex-direction: column;\n    }\n\n    .form-control--search {\n        width: 100%;\n    }\n\n    .field__select {\n        margin-top: 1.5rem;\n        padding-left: 24px;\n        padding-right: 24px;\n    }\n}\n\n@media screen and (max-width: 767.98px) {\n    .logs__table .logs__cell--response,\n    .logs__table .logs__cell--client {\n        display: none !important;\n    }\n}\n\n.logs__refresh {\n    position: relative;\n    top: 3px;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: 2.5rem;\n    height: 2.5rem;\n    padding: 0;\n    margin-left: 0.9375rem;\n    background-color: transparent;\n}\n\n.logs__cell {\n    padding: 1rem 1rem 0.5rem 0;\n}\n\n.logs__cell--date {\n    width: 4.375rem;\n    flex: var(--size-date) 0 auto;\n}\n\n.logs__cell--domain {\n    width: 11.25rem;\n    flex: var(--size-domain) 0 auto;\n}\n\n.logs__cell--response {\n    width: 9.375rem;\n    flex: var(--size-response) 0 auto;\n}\n\n.logs__cell--client {\n    width: 7.6875rem;\n    flex: var(--size-client) 0 auto;\n    padding-right: 0;\n    position: relative;\n}\n\n@media screen and (min-width: 1025px) {\n    .logs__cell--client {\n        width: 13rem;\n    }\n}\n\n.logs__cell--header__container > .logs__cell--header__item {\n    border-right: 0;\n    font-size: 1rem;\n}\n\n.logs__cell--header__container > .logs__cell--header__item:last-child {\n    padding-right: 0;\n}\n\n.button-action__container {\n    display: flex;\n    position: absolute;\n    right: 2px;\n    bottom: 0.5rem;\n}\n\n@media screen and (max-width: 1024px) {\n    .button-action__container {\n        display: none;\n    }\n}\n\n.button-action__container--detailed {\n    bottom: 1.3rem;\n}\n\n.button-action {\n    outline: 0 !important;\n    background: var(--btn-block);\n    border-radius: var(--option-border-radius);\n    font-size: 0.8rem;\n    color: var(--white);\n    letter-spacing: 0;\n    text-align: center;\n    line-height: 28px;\n    border: 0;\n}\n\n.button-action--small {\n    width: fit-content;\n}\n\n.button-action--unblock {\n    background: var(--btn-unblock);\n}\n\n.button-action--main {\n    padding: 0 1rem;\n    display: flex;\n    align-items: center;\n}\n\n.button-action--with-options {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n}\n\n.button-action:hover {\n    cursor: pointer;\n}\n\n.button-action--arrow-option {\n    background: transparent;\n    border: 0;\n    display: block;\n    width: 100%;\n    padding-top: 0.2rem;\n    padding-bottom: 0.2rem;\n    text-align: center;\n    font-weight: 700;\n    color: inherit;\n    cursor: pointer;\n}\n\n.button-action--arrow-option:hover,\n.button-action--arrow-option:focus {\n    outline: none;\n}\n\n.button-action--arrow-option:focus-visible {\n    outline: 2px solid #295a9f;\n}\n\n.button-action--arrow-option:disabled {\n    opacity: 0.5;\n    cursor: default;\n}\n\n.tooltip-custom__container .button-action--arrow-option {\n    padding-bottom: 0;\n    text-align: left;\n    font-weight: 400;\n}\n\n.tooltip-custom__container .button-action--arrow-option:not(:disabled):hover {\n    background: var(--gray-f3);\n    overflow: hidden;\n}\n\n[data-theme='dark'] .tooltip-custom__container .button-action--arrow-option:not(:disabled):hover {\n    background: var(--ctrl-dropdown-bgcolor-focus);\n}\n\n.button-action--arrow-option-container {\n    overflow: visible;\n    transform-origin: left;\n    padding: 1rem 0;\n}\n\n.logs__row {\n    position: relative;\n    display: flex;\n    min-height: 26px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.logs__row--center {\n    align-items: center;\n}\n\n.logs__row--icons {\n    flex-wrap: wrap;\n}\n\n.logs__table .logs__row {\n    border-bottom: 2px solid var(--gray-216);\n}\n\n.logs__tag {\n    text-overflow: ellipsis;\n    overflow: hidden;\n}\n\n/* QUERY_STATUS_COLORS */\n.logs__row--blue {\n    background-color: var(--logs__row--blue-bgcolor);\n}\n\n[data-theme='dark'] .logs__row--blue .logs__text--link {\n    color: var(--white);\n}\n\n.logs__row--green {\n    background-color: var(--green-pale);\n}\n\n.logs__row--red {\n    background-color: var(--red) !important;\n}\n\n.logs__row--white {\n    background-color: var(--logs__row--white-bgcolor);\n}\n\n.logs__row--yellow {\n    background-color: var(--yellow);\n}\n\n.logs__no-data {\n    color: var(--mcolor);\n    background-color: var(--logs__table-bgcolor);\n    pointer-events: none;\n    font-weight: 600;\n    text-align: center;\n    padding-top: 21rem;\n    display: block;\n}\n\n.logs__loading {\n    padding: 1rem 0;\n}\n\n.logs__table {\n    background-color: var(--logs__table-bgcolor);\n    border: 0;\n    border-radius: 8px;\n    min-height: 43rem;\n    max-width: 100%;\n    align-items: stretch;\n    width: 100%;\n    border-collapse: collapse;\n    contain: layout;\n    overflow-x: hidden;\n    overflow-y: auto;\n    will-change: scroll-position;\n}\n\n.logs__table .logs__cell--response,\n.logs__table .logs__cell--client {\n    display: flex;\n}\n\n.logs__cell--header__container {\n    display: flex;\n}\n\n.logs__table > .logs__cell--header__container > .logs__cell--client {\n    display: flex;\n    justify-content: space-between;\n}\n\n.logs__table .loading:after {\n    top: 10%;\n}\n\n.logs__table .loading:before {\n    min-height: 100%;\n}\n\n.logs__whois {\n    display: inline;\n    font-size: 12px;\n    white-space: nowrap;\n}\n\n.logs__whois::after {\n    content: '|';\n    padding: 0 5px;\n    opacity: 0.3;\n}\n\n.logs__whois:last-child::after {\n    content: '';\n}\n\n.logs__whois-icon.icons {\n    position: relative;\n    top: -2px;\n    width: 12px;\n    height: 12px;\n    margin-right: 1px;\n    opacity: 0.5;\n}\n\n.filteringRules__rule {\n    margin-bottom: 0;\n}\n\n.filteringRules__filter {\n    font-style: italic;\n    font-weight: 400;\n    margin-bottom: 1rem;\n}\n\n.bg--danger {\n    color: var(--danger);\n}\n\n.bg--green {\n    color: var(--green79);\n}\n\n[data-theme='dark'] .logs__question.icon--lightgray {\n    color: var(--gray-f3);\n}\n\n@media (max-width: 1024px) {\n    .logs__question {\n        display: none;\n    }\n}\n\n.logs__modal {\n    max-width: 720px;\n}\n\n.logs__modal-wrap {\n    padding: 1rem 1.5rem;\n    background-color: var(--card-bgcolor);\n}\n\n.button-action__hidden-trigger {\n    position: absolute;\n    top: 0;\n    right: 0;\n    width: 1px;\n    height: 33px;\n    margin: -1px;\n    padding: 0;\n    overflow: hidden;\n    border: 0;\n    clip: rect(0 0 0 0);\n}\n\n[data-theme='dark'] .button-action__icon {\n    color: var(--gray-f3);\n}\n"
  },
  {
    "path": "client/src/components/Logs/index.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Trans } from 'react-i18next';\n\nimport Modal from 'react-modal';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport { useHistory } from 'react-router-dom';\nimport queryString from 'query-string';\nimport classNames from 'classnames';\nimport { FormProvider, useForm } from 'react-hook-form';\nimport { BLOCK_ACTIONS, DEFAULT_LOGS_FILTER, MEDIUM_SCREEN_SIZE } from '../../helpers/constants';\n\nimport Loading from '../ui/Loading';\n\nimport Filters from './Filters';\n\nimport Disabled from './Disabled';\nimport { getFilteringStatus } from '../../actions/filtering';\n\nimport { getClients } from '../../actions';\nimport { getDnsConfig } from '../../actions/dnsConfig';\nimport { getAccessList } from '../../actions/access';\nimport { getAllBlockedServices } from '../../actions/services';\nimport { getLogsConfig, resetFilteredLogs, setFilteredLogs, toggleDetailedLogs } from '../../actions/queryLogs';\n\nimport InfiniteTable from './InfiniteTable';\nimport './Logs.css';\nimport { BUTTON_PREFIX } from './Cells/helpers';\n\nimport AnonymizerNotification from './AnonymizerNotification';\nimport { RootState } from '../../initialState';\n\nexport type SearchFormValues = {\n    search: string;\n    response_status: string;\n};\n\nconst processContent = (data: any, _buttonType: string) =>\n    Object.entries(data).map(([key, value]) => {\n        if (!value) {\n            return null;\n        }\n\n        const isTitle = value === 'title';\n        const isButton = key.startsWith(BUTTON_PREFIX);\n        const isBoolean = typeof value === 'boolean';\n        const isHidden = isBoolean && value === false;\n\n        let keyClass = 'key-colon';\n\n        if (isTitle) {\n            keyClass = 'title--border';\n        }\n        if (isButton || isBoolean) {\n            keyClass = '';\n        }\n\n        return isHidden ? null : (\n            <div className=\"grid__row\" key={key}>\n                <div\n                    className={classNames(`key__${key}`, keyClass, {\n                        'font-weight-bold': isBoolean && value === true,\n                    })}>\n                    <Trans>{isButton ? value : key}</Trans>\n                </div>\n\n                <div className={`value__${key} text-pre text-truncate`}>\n                    <Trans>{isTitle || isButton || isBoolean ? '' : value || '—'}</Trans>\n                </div>\n            </div>\n        );\n    });\n\nconst Logs = () => {\n    const dispatch = useDispatch();\n    const history = useHistory();\n\n    const { response_status: response_status_url_param, search: search_url_param } = queryString.parse(\n        history.location.search,\n    );\n\n    const {\n        enabled,\n        processingGetConfig,\n        processingGetLogs,\n        anonymize_client_ip: anonymizeClientIp,\n    } = useSelector((state: RootState) => state.queryLogs, shallowEqual);\n\n    const filter = useSelector((state: RootState) => state.queryLogs.filter, shallowEqual);\n\n    const logs = useSelector((state: RootState) => state.queryLogs.logs, shallowEqual);\n\n    const search = search_url_param || filter?.search || '';\n    const response_status = response_status_url_param || filter?.response_status || '';\n\n    const formMethods = useForm<SearchFormValues>({\n        mode: 'onBlur',\n        defaultValues: {\n            search: search || DEFAULT_LOGS_FILTER.search,\n            response_status: response_status || DEFAULT_LOGS_FILTER.response_status,\n        },\n    });\n\n    const { watch } = formMethods;\n    const currentQuery = watch('search');\n\n    const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth <= MEDIUM_SCREEN_SIZE);\n    const [detailedDataCurrent, setDetailedDataCurrent] = useState({});\n    const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);\n    const [isModalOpened, setModalOpened] = useState(false);\n    const [isLoading, setIsLoading] = useState(false);\n\n    const closeModal = () => setModalOpened(false);\n\n    useEffect(() => {\n        (async () => {\n            setIsLoading(true);\n            await dispatch(\n                setFilteredLogs({\n                    search,\n                    response_status,\n                }),\n            );\n            setIsLoading(false);\n        })();\n    }, [response_status, search]);\n\n    const mediaQuery = window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`);\n    const mediaQueryHandler = (e: any) => {\n        setIsSmallScreen(e.matches);\n        if (e.matches) {\n            dispatch(toggleDetailedLogs(false));\n        } else {\n            dispatch(toggleDetailedLogs(true));\n        }\n    };\n\n    useEffect(() => {\n        try {\n            mediaQuery.addEventListener('change', mediaQueryHandler);\n        } catch (e1) {\n            try {\n                // Safari 13.1 do not support mediaQuery.addEventListener('change', handler)\n                mediaQuery.addListener(mediaQueryHandler);\n            } catch (e2) {\n                console.error(e2);\n            }\n        }\n\n        (async () => {\n            setIsLoading(true);\n            dispatch(getFilteringStatus());\n            dispatch(getClients());\n            dispatch(getAllBlockedServices());\n            try {\n                await Promise.all([dispatch(getLogsConfig()), dispatch(getDnsConfig()), dispatch(getAccessList())]);\n            } catch (err) {\n                console.error(err);\n            } finally {\n                setIsLoading(false);\n            }\n        })();\n\n        return () => {\n            try {\n                mediaQuery.removeEventListener('change', mediaQueryHandler);\n            } catch (e1) {\n                try {\n                    // Safari 13.1 do not support mediaQuery.addEventListener('change', handler)\n                    mediaQuery.removeListener(mediaQueryHandler);\n                } catch (e2) {\n                    console.error(e2);\n                }\n            }\n\n            dispatch(resetFilteredLogs());\n        };\n    }, []);\n\n    useEffect(() => {\n        if (!history.location.search) {\n            (async () => {\n                setIsLoading(true);\n\n                await dispatch(setFilteredLogs());\n                setIsLoading(false);\n            })();\n        }\n    }, [history.location.search]);\n\n    const renderPage = () => (\n        <>\n            <FormProvider {...formMethods}>\n                <Filters\n                    setIsLoading={setIsLoading}\n                    processingGetLogs={processingGetLogs}\n                />\n            </FormProvider>\n\n            <InfiniteTable\n                isLoading={isLoading}\n                items={logs}\n                isSmallScreen={isSmallScreen}\n                setDetailedDataCurrent={setDetailedDataCurrent}\n                setButtonType={setButtonType}\n                setModalOpened={setModalOpened}\n                currentQuery={currentQuery}\n            />\n\n            <Modal\n                portalClassName=\"grid\"\n                isOpen={isSmallScreen && isModalOpened}\n                onRequestClose={closeModal}\n                style={{\n                    content: {\n                        width: 'calc(100% - 32px)',\n                        height: 'fit-content',\n                        left: '50%',\n                        top: 47,\n                        padding: '0',\n                        maxWidth: '720px',\n                        transform: 'translateX(-50%)',\n                    },\n                    overlay: {\n                        backgroundColor: 'rgba(0,0,0,0.5)',\n                    },\n                }}>\n                <div className=\"logs__modal-wrap\">\n                    <svg className=\"icon icon--24 icon-cross d-block cursor--pointer\" onClick={closeModal}>\n                        <use xlinkHref=\"#cross\" />\n                    </svg>\n\n                    {processContent(detailedDataCurrent, buttonType)}\n                </div>\n            </Modal>\n        </>\n    );\n\n    return (\n        <>\n            {enabled && (\n                <>\n                    {processingGetConfig && <Loading />}\n\n                    {anonymizeClientIp && <AnonymizerNotification />}\n                    {!processingGetConfig && renderPage()}\n                </>\n            )}\n\n            {!enabled && !processingGetConfig && <Disabled />}\n        </>\n    );\n};\n\nexport default Logs;\n"
  },
  {
    "path": "client/src/components/ProtectionTimer/index.ts",
    "content": "import { useEffect } from 'react';\nimport { connect } from 'react-redux';\n\nimport { ONE_SECOND_IN_MS } from '../../helpers/constants';\n\nimport { setProtectionTimerTime, toggleProtectionSuccess } from '../../actions';\n\nlet interval: any = null;\n\ninterface ProtectionTimerProps {\n    protectionDisabledDuration?: number;\n    toggleProtectionSuccess: (...args: unknown[]) => unknown;\n    setProtectionTimerTime: (...args: unknown[]) => unknown;\n}\n\nconst ProtectionTimer = ({\n    protectionDisabledDuration,\n    toggleProtectionSuccess,\n    setProtectionTimerTime,\n}: ProtectionTimerProps) => {\n    useEffect(() => {\n        if (protectionDisabledDuration !== null && protectionDisabledDuration < ONE_SECOND_IN_MS) {\n            toggleProtectionSuccess({ disabledDuration: null });\n        }\n\n        if (protectionDisabledDuration) {\n            interval = setInterval(() => {\n                setProtectionTimerTime(protectionDisabledDuration - ONE_SECOND_IN_MS);\n            }, ONE_SECOND_IN_MS);\n        }\n\n        return () => {\n            clearInterval(interval);\n        };\n    }, [protectionDisabledDuration]);\n\n    return null;\n};\n\nconst mapStateToProps = (state: any) => {\n    const { dashboard } = state;\n    const { protectionEnabled, protectionDisabledDuration } = dashboard;\n    return { protectionEnabled, protectionDisabledDuration };\n};\n\nconst mapDispatchToProps = {\n    toggleProtectionSuccess,\n    setProtectionTimerTime,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(ProtectionTimer);\n"
  },
  {
    "path": "client/src/components/Settings/Clients/AutoClients.tsx",
    "content": "import React, { Component } from 'react';\nimport { withTranslation } from 'react-i18next';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\n\nimport Card from '../../ui/Card';\n\nimport CellWrap from '../../ui/CellWrap';\n\nimport whoisCell from './whoisCell';\n\nimport LogsSearchLink from '../../ui/LogsSearchLink';\n\nimport { sortIp, formatNumber } from '../../../helpers/helpers';\nimport { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';\nimport { TABLES_MIN_ROWS } from '../../../helpers/constants';\n\nconst COLUMN_MIN_WIDTH = 200;\n\ninterface AutoClientsProps {\n    t: (...args: unknown[]) => string;\n    autoClients: any[];\n    normalizedTopClients: any;\n}\n\nclass AutoClients extends Component<AutoClientsProps> {\n    columns = [\n        {\n            Header: this.props.t('table_client'),\n            accessor: 'ip',\n            minWidth: COLUMN_MIN_WIDTH,\n            Cell: CellWrap,\n            sortMethod: sortIp,\n        },\n        {\n            Header: this.props.t('table_name'),\n            accessor: 'name',\n            minWidth: COLUMN_MIN_WIDTH,\n            Cell: CellWrap,\n        },\n        {\n            Header: this.props.t('source_label'),\n            accessor: 'source',\n            minWidth: COLUMN_MIN_WIDTH,\n            Cell: CellWrap,\n        },\n        {\n            Header: this.props.t('whois'),\n            accessor: 'whois_info',\n            minWidth: COLUMN_MIN_WIDTH,\n\n            Cell: whoisCell(this.props.t),\n        },\n        {\n            Header: this.props.t('requests_count'),\n\n            accessor: (row: any) => this.props.normalizedTopClients.auto[row.ip] || 0,\n            sortMethod: (a: any, b: any) => b - a,\n            id: 'statistics',\n            minWidth: COLUMN_MIN_WIDTH,\n            Cell: (row: any) => {\n                const { value: clientStats } = row;\n\n                if (clientStats) {\n                    return (\n                        <div className=\"logs__row\">\n                            <div className=\"logs__text\" title={clientStats}>\n                                <LogsSearchLink search={row.original.ip}>{formatNumber(clientStats)}</LogsSearchLink>\n                            </div>\n                        </div>\n                    );\n                }\n\n                return '–';\n            },\n        },\n    ];\n\n    render() {\n        const { t, autoClients } = this.props;\n\n        return (\n            <Card\n                title={t('auto_clients_title')}\n                subtitle={t('auto_clients_desc')}\n                bodyType=\"card-body box-body--settings\">\n                <ReactTable\n                    data={autoClients || []}\n                    columns={this.columns}\n                    defaultSorted={[\n                        {\n                            id: 'statistics',\n                            asc: true,\n                        },\n                    ]}\n                    className=\"-striped -highlight card-table-overflow\"\n                    showPagination\n                    defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE) || 10}\n                    onPageSizeChange={(size: any) =>\n                        LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE, size)\n                    }\n                    minRows={TABLES_MIN_ROWS}\n                    ofText=\"/\"\n                    previousText={t('previous_btn')}\n                    nextText={t('next_btn')}\n                    pageText={t('page_table_footer_text')}\n                    rowsText={t('rows_table_footer_text')}\n                    loadingText={t('loading_table_status')}\n                    noDataText={t('clients_not_found')}\n                />\n            </Card>\n        );\n    }\n}\n\nexport default withTranslation()(AutoClients);\n"
  },
  {
    "path": "client/src/components/Settings/Clients/ClientsTable/ClientsTable.tsx",
    "content": "/* eslint-disable react/display-name */\n/* eslint-disable react/prop-types */\nimport React, { useEffect } from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport { useHistory, useLocation } from 'react-router-dom';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\n\nimport { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';\n\nimport { initSettings } from '../../../../actions';\nimport { splitByNewLine, countClientsStatistics, sortIp, getService, formatNumber } from '../../../../helpers/helpers';\nimport { MODAL_TYPE, LOCAL_TIMEZONE_VALUE, TABLES_MIN_ROWS } from '../../../../helpers/constants';\n\nimport Card from '../../../ui/Card';\n\nimport CellWrap from '../../../ui/CellWrap';\n\nimport LogsSearchLink from '../../../ui/LogsSearchLink';\n\nimport Modal from '../Modal';\nimport { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../../helpers/localStorageHelper';\nimport { Client, NormalizedTopClients, RootState } from '../../../../initialState';\n\ninterface ClientsTableProps {\n    clients: Client[];\n    normalizedTopClients: NormalizedTopClients;\n    toggleClientModal: (...args: unknown[]) => unknown;\n    deleteClient: (...args: unknown[]) => string;\n    addClient: (...args: unknown[]) => string;\n    updateClient: (...args: unknown[]) => string;\n    isModalOpen: boolean;\n    modalType: string;\n    modalClientName: string;\n    processingAdding: boolean;\n    processingDeleting: boolean;\n    processingUpdating: boolean;\n    getStats: (...args: unknown[]) => unknown;\n    supportedTags: string[];\n}\n\nconst ClientsTable = ({\n    clients,\n    normalizedTopClients,\n    isModalOpen,\n    modalClientName,\n    modalType,\n    addClient,\n    updateClient,\n    deleteClient,\n    toggleClientModal,\n    processingAdding,\n    processingDeleting,\n    processingUpdating,\n    getStats,\n    supportedTags,\n}: ClientsTableProps) => {\n    const [t] = useTranslation();\n    const dispatch = useDispatch();\n    const location = useLocation();\n    const history = useHistory();\n\n    const services = useSelector((state: RootState) => state?.services);\n\n    const globalSettings = useSelector((state: RootState) => state?.settings.settingsList);\n    const params = new URLSearchParams(location.search);\n    const clientId = params.get('clientId');\n\n    useEffect(() => {\n        dispatch(getAllBlockedServices());\n        dispatch(getBlockedServices());\n        dispatch(initSettings());\n\n        if (clientId) {\n            toggleClientModal({\n                type: MODAL_TYPE.ADD_CLIENT,\n            });\n        }\n    }, []);\n\n    const handleFormAdd = (values: any) => {\n        addClient(values);\n    };\n\n    const handleFormUpdate = (values: any, name: any) => {\n        updateClient(values, name);\n    };\n\n    const handleSubmit = (values: any) => {\n        const config = { ...values };\n\n        if (values) {\n            if (values.blocked_services) {\n                config.blocked_services = Object.keys(values.blocked_services).filter(\n                    (service) => values.blocked_services[service],\n                );\n            }\n\n            if (values.upstreams && typeof values.upstreams === 'string') {\n                config.upstreams = splitByNewLine(values.upstreams);\n            } else {\n                config.upstreams = [];\n            }\n\n            if (values.tags) {\n                config.tags = values.tags.map((tag: any) => tag.value);\n            } else {\n                config.tags = [];\n            }\n\n            if (values.ids) {\n                config.ids = values.ids.map((id) => id.name);\n            } else {\n                config.ids = [];\n            }\n\n            if (typeof values.upstreams_cache_size === 'string') {\n                config.upstreams_cache_size = 0;\n            }\n        }\n\n        if (modalType === MODAL_TYPE.EDIT_CLIENT) {\n            handleFormUpdate(config, modalClientName);\n        } else {\n            handleFormAdd(config);\n        }\n\n        if (clientId) {\n            history.push('/#clients');\n        }\n    };\n\n    const getOptionsWithLabels = (options: any) =>\n        options.map((option: any) => ({\n            value: option,\n            label: option,\n        }));\n\n    const getClient = (name: any, clients: any) => {\n        const client = clients.find((item: any) => name === item.name);\n\n        if (client) {\n            const { upstreams, tags, ...values } = client;\n            return {\n                upstreams: (upstreams && upstreams.join('\\n')) || '',\n                tags: (tags && getOptionsWithLabels(tags)) || [],\n                ...values,\n            };\n        }\n\n        return {\n            ids: [''],\n            tags: [],\n            use_global_settings: true,\n            use_global_blocked_services: true,\n            blocked_services_schedule: {\n                time_zone: LOCAL_TIMEZONE_VALUE,\n            },\n            safe_search: { ...(globalSettings?.safesearch || {}) },\n        };\n    };\n\n    const handleDelete = (data: any) => {\n        // eslint-disable-next-line no-alert\n        if (window.confirm(t('client_confirm_delete', { key: data.name }))) {\n            deleteClient(data);\n            getStats();\n        }\n    };\n\n    const handleClose = () => {\n        toggleClientModal();\n\n        if (clientId) {\n            history.push('/#clients');\n        }\n    };\n\n    const columns = [\n        {\n            Header: t('table_client'),\n            accessor: 'ids',\n            minWidth: 150,\n            Cell: (row: any) => {\n                const { value } = row;\n\n                return (\n                    <div className=\"logs__row o-hidden\">\n                        <span className=\"logs__text\">\n                            {value.map((address: any) => (\n                                <div key={address} title={address}>\n                                    {address}\n                                </div>\n                            ))}\n                        </span>\n                    </div>\n                );\n            },\n            sortMethod: sortIp,\n        },\n        {\n            Header: t('table_name'),\n            accessor: 'name',\n            minWidth: 120,\n            Cell: CellWrap,\n        },\n        {\n            Header: t('settings'),\n            accessor: 'use_global_settings',\n            minWidth: 120,\n            Cell: ({ value }: any) => {\n                const title = value ? <Trans>settings_global</Trans> : <Trans>settings_custom</Trans>;\n\n                return (\n                    <div className=\"logs__row o-hidden\">\n                        <div className=\"logs__text\">{title}</div>\n                    </div>\n                );\n            },\n        },\n        {\n            Header: t('blocked_services'),\n            accessor: 'blocked_services',\n            minWidth: 180,\n            Cell: (row: any) => {\n                const { value, original } = row;\n\n                if (original.use_global_blocked_services) {\n                    return <Trans>settings_global</Trans>;\n                }\n\n                if (value && services.allServices) {\n                    return (\n                        <div className=\"logs__row logs__row--icons\">\n                            {value.map((service: any) => {\n                                const serviceInfo = getService(services.allServices, service);\n\n                                if (serviceInfo?.icon_svg) {\n                                    return (\n                                        <div\n                                            key={serviceInfo.name}\n                                            dangerouslySetInnerHTML={{\n                                                __html: window.atob(serviceInfo.icon_svg),\n                                            }}\n                                            className=\"service__icon service__icon--table\"\n                                            title={serviceInfo.name}\n                                        />\n                                    );\n                                }\n\n                                return null;\n                            })}\n                        </div>\n                    );\n                }\n\n                return <div className=\"logs__row logs__row--icons\">–</div>;\n            },\n        },\n        {\n            Header: t('upstreams'),\n            accessor: 'upstreams',\n            minWidth: 120,\n            Cell: ({ value }: any) => {\n                const title =\n                    value && value.length > 0 ? <Trans>settings_custom</Trans> : <Trans>settings_global</Trans>;\n\n                return (\n                    <div className=\"logs__row o-hidden\">\n                        <div className=\"logs__text\">{title}</div>\n                    </div>\n                );\n            },\n        },\n        {\n            Header: t('tags_title'),\n            accessor: 'tags',\n            minWidth: 140,\n            Cell: (row: any) => {\n                const { value } = row;\n\n                if (!value || value.length < 1) {\n                    return '–';\n                }\n\n                return (\n                    <div className=\"logs__row o-hidden\">\n                        <span className=\"logs__text\">\n                            {value.map((tag: any) => (\n                                <div key={tag} title={tag} className=\"logs__tag small\">\n                                    {tag}\n                                </div>\n                            ))}\n                        </span>\n                    </div>\n                );\n            },\n        },\n        {\n            Header: t('requests_count'),\n            id: 'statistics',\n            accessor: (row: any) => countClientsStatistics(row.ids, normalizedTopClients.auto),\n            sortMethod: (a: any, b: any) => b - a,\n            minWidth: 120,\n            Cell: (row: any) => {\n                let content = row.value;\n                if (typeof content === \"number\") {\n                    content = formatNumber(content);\n                } else {\n                    content = CellWrap(row);\n                }\n                if (!content) {\n                    return content;\n                }\n                return <LogsSearchLink search={row.original.name}>{content}</LogsSearchLink>;\n            },\n        },\n        {\n            Header: t('actions_table_header'),\n            accessor: 'actions',\n            maxWidth: 100,\n            sortable: false,\n            resizable: false,\n            Cell: (row: any) => {\n                const clientName = row.original.name;\n\n                return (\n                    <div className=\"logs__row logs__row--center\">\n                        <button\n                            type=\"button\"\n                            className=\"btn btn-icon btn-outline-primary btn-sm mr-2\"\n                            onClick={() =>\n                                toggleClientModal({\n                                    type: MODAL_TYPE.EDIT_CLIENT,\n                                    name: clientName,\n                                })\n                            }\n                            disabled={processingUpdating}\n                            title={t('edit_table_action')}>\n                            <svg className=\"icons icon12\">\n                                <use xlinkHref=\"#edit\" />\n                            </svg>\n                        </button>\n\n                        <button\n                            type=\"button\"\n                            className=\"btn btn-icon btn-outline-secondary btn-sm\"\n                            onClick={() => handleDelete({ name: clientName })}\n                            disabled={processingDeleting}\n                            title={t('delete_table_action')}>\n                            <svg className=\"icons icon12\">\n                                <use xlinkHref=\"#delete\" />\n                            </svg>\n                        </button>\n                    </div>\n                );\n            },\n        },\n    ];\n\n    const currentClientData = getClient(modalClientName, clients);\n    const tagsOptions = getOptionsWithLabels(supportedTags);\n\n    return (\n        <Card title={t('clients_title')} subtitle={t('clients_desc')} bodyType=\"card-body box-body--settings\">\n            <>\n                <ReactTable\n                    data={clients || []}\n                    columns={columns}\n                    defaultSorted={[\n                        {\n                            id: 'statistics',\n                            asc: true,\n                        },\n                    ]}\n                    className=\"-striped -highlight card-table-overflow\"\n                    showPagination\n                    defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE) || 10}\n                    onPageSizeChange={(size: any) =>\n                        LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE, size)\n                    }\n                    minRows={TABLES_MIN_ROWS}\n                    ofText=\"/\"\n                    previousText={t('previous_btn')}\n                    nextText={t('next_btn')}\n                    pageText={t('page_table_footer_text')}\n                    rowsText={t('rows_table_footer_text')}\n                    loadingText={t('loading_table_status')}\n                    noDataText={t('clients_not_found')}\n                />\n\n                <button\n                    type=\"button\"\n                    className=\"btn btn-success btn-standard mt-3\"\n                    onClick={() => toggleClientModal(MODAL_TYPE.ADD_FILTERS)}\n                    disabled={processingAdding}>\n                    <Trans>client_add</Trans>\n                </button>\n\n                <Modal\n                    isModalOpen={isModalOpen}\n                    modalType={modalType}\n                    handleClose={handleClose}\n                    currentClientData={currentClientData}\n                    handleSubmit={handleSubmit}\n                    processingAdding={processingAdding}\n                    processingUpdating={processingUpdating}\n                    tagsOptions={tagsOptions}\n                    clientId={clientId}\n                />\n            </>\n        </Card>\n    );\n};\n\nexport default ClientsTable;\n"
  },
  {
    "path": "client/src/components/Settings/Clients/ClientsTable/index.ts",
    "content": "export { default as ClientsTable } from './ClientsTable';\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Form/components/BlockedServices.tsx",
    "content": "import React from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { Controller, useFormContext } from 'react-hook-form';\nimport { ClientForm } from '../types';\nimport { BlockedService } from '../../../../Filters/Services/Form';\nimport { ServiceField } from '../../../../Filters/Services/ServiceField';\n\ntype Props = {\n    services: BlockedService[];\n};\n\nexport const BlockedServices = ({ services }: Props) => {\n    const { t } = useTranslation();\n    const { watch, setValue, control } = useFormContext<ClientForm>();\n\n    const useGlobalServices = watch('use_global_blocked_services');\n\n    const handleToggleAllServices = (isSelected: boolean) => {\n        services.forEach((service: BlockedService) => setValue(`blocked_services.${service.id}`, isSelected));\n    };\n\n    return (\n        <div title={t('block_services')}>\n            <div className=\"form__group\">\n                <Controller\n                    name=\"use_global_blocked_services\"\n                    control={control}\n                    render={({ field }) => (\n                        <ServiceField\n                            {...field}\n                            data-testid=\"clients_use_global_blocked_services\"\n                            placeholder={t('blocked_services_global')}\n                            className=\"service--global\"\n                        />\n                    )}\n                />\n\n                <div className=\"row mb-4\">\n                    <div className=\"col-6\">\n                        <button\n                            type=\"button\"\n                            data-testid=\"clients_block_all\"\n                            className=\"btn btn-secondary btn-block\"\n                            disabled={useGlobalServices}\n                            onClick={() => handleToggleAllServices(true)}>\n                            <Trans>block_all</Trans>\n                        </button>\n                    </div>\n\n                    <div className=\"col-6\">\n                        <button\n                            type=\"button\"\n                            data-testid=\"clients_unblock_all\"\n                            className=\"btn btn-secondary btn-block\"\n                            disabled={useGlobalServices}\n                            onClick={() => handleToggleAllServices(false)}>\n                            <Trans>unblock_all</Trans>\n                        </button>\n                    </div>\n                </div>\n                {services.length > 0 && (\n                    <div className=\"services\">\n                        {services.map((service: BlockedService) => (\n                            <Controller\n                                key={service.id}\n                                name={`blocked_services.${service.id}`}\n                                control={control}\n                                render={({ field }) => (\n                                    <ServiceField\n                                        {...field}\n                                        data-testid={`clients_service_${service.id}`}\n                                        placeholder={service.name}\n                                        disabled={useGlobalServices}\n                                        icon={service.icon_svg}\n                                    />\n                                )}\n                            />\n                        ))}\n                    </div>\n                )}\n            </div>\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Form/components/ClientIds.tsx",
    "content": "import React from 'react';\nimport { Controller, useFieldArray, useFormContext } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { ClientForm } from '../types';\nimport { Input } from '../../../../ui/Controls/Input';\nimport { validateClientId, validateRequiredValue } from '../../../../../helpers/validators';\n\nexport const ClientIds = () => {\n    const { t } = useTranslation();\n    const { control } = useFormContext<ClientForm>();\n\n    const { fields, append, remove } = useFieldArray<ClientForm>({\n        control,\n        name: 'ids',\n    });\n\n    return (\n        <div className=\"form__group\">\n            {fields.map((field, index) => (\n                <div key={field.id} className=\"mb-1\">\n                    <Controller\n                        name={`ids.${index}.name`}\n                        control={control}\n                        rules={{\n                            validate: {\n                                required: (value) => validateRequiredValue(value),\n                                validId: (value) => validateClientId(value),\n                            },\n                        }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"text\"\n                                data-testid={`clients_id_${index}`}\n                                placeholder={t('form_enter_id')}\n                                error={fieldState.error?.message}\n                                onBlur={(event) => {\n                                    const trimmedValue = event.target.value.trim();\n                                    field.onBlur();\n                                    field.onChange(trimmedValue);\n                                }}\n                                rightAddon={\n                                    index !== 0 && (\n                                        <span className=\"input-group-append\">\n                                            <button\n                                                type=\"button\"\n                                                data-testid={`clients_id_remove_${index}`}\n                                                className=\"btn btn-secondary btn-icon btn-icon--green\"\n                                                onClick={() => remove(index)}>\n                                                <svg className=\"icon icon--24\">\n                                                    <use xlinkHref=\"#cross\" />\n                                                </svg>\n                                            </button>\n                                        </span>\n                                    )\n                                }\n                            />\n                        )}\n                    />\n                </div>\n            ))}\n            <button\n                type=\"button\"\n                data-testid=\"clients_id_add\"\n                className=\"btn btn-link btn-block btn-sm\"\n                onClick={() => append({ name: '' })}\n                title={t('form_add_id')}>\n                <svg className=\"icon icon--24\">\n                    <use xlinkHref=\"#plus\" />\n                </svg>\n            </button>\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Form/components/MainSettings.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Controller, useFormContext } from 'react-hook-form';\nimport i18next from 'i18next';\nimport { captitalizeWords } from '../../../../../helpers/helpers';\nimport { ClientForm } from '../types';\nimport { Checkbox } from '../../../../ui/Controls/Checkbox';\n\ntype ProtectionSettings = 'use_global_settings' | 'filtering_enabled' | 'safebrowsing_enabled' | 'parental_enabled';\n\nconst settingsCheckboxes: {\n    name: ProtectionSettings;\n    placeholder: string;\n}[] = [\n    {\n        name: 'use_global_settings',\n        placeholder: i18next.t('client_global_settings'),\n    },\n    {\n        name: 'filtering_enabled',\n        placeholder: i18next.t('block_domain_use_filters_and_hosts'),\n    },\n    {\n        name: 'safebrowsing_enabled',\n        placeholder: i18next.t('use_adguard_browsing_sec'),\n    },\n    {\n        name: 'parental_enabled',\n        placeholder: i18next.t('use_adguard_parental'),\n    },\n];\n\ntype LogsStatsSettings = 'ignore_querylog' | 'ignore_statistics';\n\nconst logAndStatsCheckboxes: { name: LogsStatsSettings; placeholder: string }[] = [\n    {\n        name: 'ignore_querylog',\n        placeholder: i18next.t('ignore_query_log'),\n    },\n    {\n        name: 'ignore_statistics',\n        placeholder: i18next.t('ignore_statistics'),\n    },\n];\n\ntype Props = {\n    safeSearchServices: Record<string, boolean>;\n};\n\nexport const MainSettings = ({ safeSearchServices }: Props) => {\n    const { t } = useTranslation();\n    const { watch, control } = useFormContext<ClientForm>();\n\n    const useGlobalSettings = watch('use_global_settings');\n\n    return (\n        <div title={t('main_settings')}>\n            <div className=\"form__label--bot form__label--bold\">{t('protection_section_label')}</div>\n            {settingsCheckboxes.map((setting) => (\n                <div className=\"form__group\" key={setting.name}>\n                    <Controller\n                        name={setting.name}\n                        control={control}\n                        render={({ field }) => (\n                            <Checkbox\n                                {...field}\n                                data-testid={`clients_${setting.name}`}\n                                title={setting.placeholder}\n                                disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}\n                            />\n                        )}\n                    />\n                </div>\n            ))}\n\n            <div className=\"form__group\">\n                <Controller\n                    name=\"safe_search.enabled\"\n                    control={control}\n                    render={({ field }) => (\n                        <Checkbox\n                            data-testid=\"clients_safe_search\"\n                            {...field}\n                            title={t('enforce_safe_search')}\n                            disabled={useGlobalSettings}\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"form__group--inner\">\n                {Object.keys(safeSearchServices).map((searchKey) => (\n                    <div key={searchKey}>\n                        <Controller\n                            name={`safe_search.${searchKey}`}\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox\n                                    {...field}\n                                    data-testid={`clients_safe_search_${searchKey}`}\n                                    title={captitalizeWords(searchKey)}\n                                    disabled={useGlobalSettings}\n                                />\n                            )}\n                        />\n                    </div>\n                ))}\n            </div>\n\n            <div className=\"form__label--bold form__label--top form__label--bot\">\n                {t('log_and_stats_section_label')}\n            </div>\n            {logAndStatsCheckboxes.map((setting) => (\n                <div className=\"form__group\" key={setting.name}>\n                    <Controller\n                        name={setting.name}\n                        control={control}\n                        render={({ field }) => (\n                            <Checkbox {...field} data-testid={`clients_${setting.name}`} title={setting.placeholder} />\n                        )}\n                    />\n                </div>\n            ))}\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Form/components/ScheduleServices.tsx",
    "content": "import React from 'react';\nimport { Trans } from 'react-i18next';\nimport { useFormContext } from 'react-hook-form';\nimport { ScheduleForm } from '../../../../Filters/Services/ScheduleForm';\nimport { ClientForm } from '../types';\n\nexport const ScheduleServices = () => {\n    const { watch, setValue } = useFormContext<ClientForm>();\n\n    const blockedServicesSchedule = watch('blocked_services_schedule');\n\n    const handleScheduleSubmit = (values: any) => {\n        setValue('blocked_services_schedule', values);\n    };\n\n    return (\n        <>\n            <div className=\"form__desc mb-4\">\n                <Trans>schedule_services_desc_client</Trans>\n            </div>\n\n            <ScheduleForm schedule={blockedServicesSchedule} onScheduleSubmit={handleScheduleSubmit} clientForm />\n        </>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Form/components/UpstreamDns.tsx",
    "content": "import React from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { Controller, useFormContext } from 'react-hook-form';\nimport Examples from '../../../Dns/Upstream/Examples';\nimport { UINT32_RANGE } from '../../../../../helpers/constants';\nimport { Textarea } from '../../../../ui/Controls/Textarea';\nimport { ClientForm } from '../types';\nimport { Checkbox } from '../../../../ui/Controls/Checkbox';\nimport { Input } from '../../../../ui/Controls/Input';\nimport { toNumber } from '../../../../../helpers/form';\n\nexport const UpstreamDns = () => {\n    const { t } = useTranslation();\n\n    const { control } = useFormContext<ClientForm>();\n\n    return (\n        <div title={t('upstream_dns')}>\n            <div className=\"form__desc mb-3\">\n                <Trans components={[<a href=\"#dns\" key=\"0\" />]}>upstream_dns_client_desc</Trans>\n            </div>\n\n            <Controller\n                name=\"upstreams\"\n                control={control}\n                render={({ field }) => (\n                    <Textarea\n                        {...field}\n                        data-testid=\"clients_upstreams\"\n                        className=\"form-control form-control--textarea mb-5\"\n                        placeholder={t('upstream_dns')}\n                        trimOnBlur\n                    />\n                )}\n            />\n\n            <Examples />\n\n            <div className=\"form__label--bold mt-5 mb-3\">{t('upstream_dns_cache_configuration')}</div>\n\n            <div className=\"form__group mb-2\">\n                <Controller\n                    name=\"upstreams_cache_enabled\"\n                    control={control}\n                    render={({ field }) => (\n                        <Checkbox\n                            {...field}\n                            data-testid=\"clients_upstreams_cache_enabled\"\n                            title={t('enable_upstream_dns_cache')}\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"form__group form__group--settings\">\n                <label htmlFor=\"upstreams_cache_size\" className=\"form__label\">\n                    {t('dns_cache_size')}\n                </label>\n\n                <Controller\n                    name=\"upstreams_cache_size\"\n                    control={control}\n                    render={({ field, fieldState }) => (\n                        <Input\n                            {...field}\n                            type=\"number\"\n                            data-testid=\"clients_upstreams_cache_size\"\n                            placeholder={t('enter_cache_size')}\n                            error={fieldState.error?.message}\n                            min={0}\n                            max={UINT32_RANGE.MAX}\n                            onChange={(e) => {\n                                const { value } = e.target;\n                                field.onChange(toNumber(value));\n                            }}\n                        />\n                    )}\n                />\n            </div>\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Form/components/index.ts",
    "content": "export { BlockedServices } from './BlockedServices';\nexport { ClientIds } from './ClientIds';\nexport { ScheduleServices } from './ScheduleServices';\nexport { MainSettings } from './MainSettings';\nexport { UpstreamDns } from './UpstreamDns';\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Form/index.tsx",
    "content": "import React, { useState } from 'react';\nimport { useSelector } from 'react-redux';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { Controller, FormProvider, useForm } from 'react-hook-form';\nimport Select from 'react-select';\n\nimport Tabs from '../../../ui/Tabs';\nimport { CLIENT_ID_LINK, LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';\nimport { RootState } from '../../../../initialState';\nimport { Input } from '../../../ui/Controls/Input';\nimport { validateRequiredValue } from '../../../../helpers/validators';\nimport { ClientForm } from './types';\nimport { BlockedServices, ClientIds, MainSettings, ScheduleServices, UpstreamDns } from './components';\n\nimport '../Service.css';\n\nconst defaultFormValues: ClientForm = {\n    ids: [{ name: '' }],\n    name: '',\n    tags: [],\n    use_global_settings: false,\n    filtering_enabled: false,\n    safebrowsing_enabled: false,\n    parental_enabled: false,\n    ignore_querylog: false,\n    ignore_statistics: false,\n    blocked_services: {},\n    safe_search: { enabled: false },\n    upstreams: '',\n    upstreams_cache_enabled: false,\n    upstreams_cache_size: 0,\n    use_global_blocked_services: false,\n    blocked_services_schedule: {\n        time_zone: LOCAL_TIMEZONE_VALUE,\n    },\n};\n\ntype Props = {\n    onSubmit: (values: ClientForm) => void;\n    onClose: () => void;\n    useGlobalSettings?: boolean;\n    useGlobalServices?: boolean;\n    blockedServicesSchedule?: {\n        time_zone: string;\n    };\n    processingAdding: boolean;\n    processingUpdating: boolean;\n    tagsOptions: { label: string; value: string }[];\n    initialValues?: ClientForm;\n};\n\nexport const Form = ({\n    onSubmit,\n    onClose,\n    processingAdding,\n    processingUpdating,\n    tagsOptions,\n    initialValues,\n}: Props) => {\n    const { t } = useTranslation();\n    const methods = useForm<ClientForm>({\n        defaultValues: {\n            ...defaultFormValues,\n            ...initialValues,\n        },\n        mode: 'onBlur',\n    });\n\n    const {\n        handleSubmit,\n        reset,\n        control,\n        formState: { isSubmitting, isValid },\n    } = methods;\n\n    const services = useSelector((store: RootState) => store?.services);\n    const { safe_search } = initialValues;\n    const safeSearchServices = { ...safe_search };\n    delete safeSearchServices.enabled;\n\n    const [activeTabLabel, setActiveTabLabel] = useState('settings');\n\n    const tabs = {\n        settings: {\n            title: 'settings',\n            component: <MainSettings safeSearchServices={safeSearchServices} />,\n        },\n        block_services: {\n            title: 'block_services',\n            component: <BlockedServices services={services?.allServices} />,\n        },\n        schedule_services: {\n            title: 'schedule_services',\n            component: <ScheduleServices />,\n        },\n        upstream_dns: {\n            title: 'upstream_dns',\n            component: <UpstreamDns />,\n        },\n    };\n\n    const activeTab = tabs[activeTabLabel].component;\n\n    return (\n        <FormProvider {...methods}>\n            <form onSubmit={handleSubmit(onSubmit)}>\n                <div className=\"modal-body\">\n                    <div className=\"form__group mb-0\">\n                        <div className=\"form__group\">\n                            <Controller\n                                name=\"name\"\n                                control={control}\n                                rules={{ validate: validateRequiredValue }}\n                                render={({ field, fieldState }) => (\n                                    <Input\n                                        {...field}\n                                        type=\"text\"\n                                        data-testid=\"clients_name\"\n                                        placeholder={t('form_client_name')}\n                                        error={fieldState.error?.message}\n                                        onBlur={(event) => {\n                                            const trimmedValue = event.target.value.trim();\n                                            field.onBlur();\n                                            field.onChange(trimmedValue);\n                                        }}\n                                    />\n                                )}\n                            />\n                        </div>\n\n                        <div className=\"form__group mb-4\">\n                            <div className=\"form__label\">\n                                <strong className=\"mr-3\">\n                                    <Trans>tags_title</Trans>\n                                </strong>\n                            </div>\n\n                            <div className=\"form__desc mt-0 mb-2\">\n                                <Trans\n                                    components={[\n                                        <a\n                                            target=\"_blank\"\n                                            rel=\"noopener noreferrer\"\n                                            href=\"https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax_ctag&from=ui&app=home\"\n                                            key=\"0\"\n                                        />,\n                                    ]}>\n                                    tags_desc\n                                </Trans>\n                            </div>\n\n                            <Controller\n                                name=\"tags\"\n                                control={control}\n                                render={({ field }) => (\n                                    <Select\n                                        {...field}\n                                        data-testid=\"clients_tags\"\n                                        options={tagsOptions}\n                                        className=\"basic-multi-select\"\n                                        classNamePrefix=\"select\"\n                                        isMulti\n                                    />\n                                )}\n                            />\n                        </div>\n\n                        <div className=\"form__group\">\n                            <div className=\"form__label\">\n                                <strong className=\"mr-3\">\n                                    <Trans>client_identifier</Trans>\n                                </strong>\n                            </div>\n\n                            <div className=\"form__desc mt-0\">\n                                <Trans\n                                    components={[\n                                        <a href={CLIENT_ID_LINK} target=\"_blank\" rel=\"noopener noreferrer\" key=\"0\" />,\n                                    ]}>\n                                    client_identifier_desc\n                                </Trans>\n                            </div>\n                        </div>\n\n                        <div className=\"form__group\">\n                            <ClientIds />\n                        </div>\n                    </div>\n\n                    <Tabs\n                        controlClass=\"form\"\n                        tabs={tabs}\n                        activeTabLabel={activeTabLabel}\n                        setActiveTabLabel={setActiveTabLabel}>\n                        {activeTab}\n                    </Tabs>\n                </div>\n\n                <div className=\"modal-footer\">\n                    <div className=\"btn-list\">\n                        <button\n                            type=\"button\"\n                            className=\"btn btn-secondary btn-standard\"\n                            disabled={isSubmitting}\n                            onClick={() => {\n                                reset();\n                                onClose();\n                            }}>\n                            <Trans>cancel_btn</Trans>\n                        </button>\n\n                        <button\n                            type=\"submit\"\n                            className=\"btn btn-success btn-standard\"\n                            disabled={isSubmitting || !isValid || processingAdding || processingUpdating}>\n                            <Trans>save_btn</Trans>\n                        </button>\n                    </div>\n                </div>\n            </form>\n        </FormProvider>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Form/types.ts",
    "content": "export type ClientForm = {\n    name: string;\n    tags: { value: string; label: string }[];\n    ids: { name: string }[];\n    use_global_settings: boolean;\n    use_global_blocked_services: boolean;\n    blocked_services_schedule: {\n        time_zone: string;\n    };\n    safe_search: {\n        enabled: boolean;\n        [key: string]: boolean;\n    };\n    upstreams: string;\n    upstreams_cache_enabled: boolean;\n    upstreams_cache_size: number;\n    blocked_services: Record<string, boolean>;\n    filtering_enabled: boolean;\n    safebrowsing_enabled: boolean;\n    parental_enabled: boolean;\n    ignore_querylog: boolean;\n    ignore_statistics: boolean;\n};\n\nexport type SubmitClientForm = Omit<ClientForm, 'ids' | 'tags'> & {\n    ids: string[];\n    tags: string[];\n};\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Modal.tsx",
    "content": "import React from 'react';\nimport { Trans, withTranslation } from 'react-i18next';\n\nimport ReactModal from 'react-modal';\n\nimport { MODAL_TYPE } from '../../../helpers/constants';\nimport { Form } from './Form';\n\nconst normalizeIds = (initialIds?: string[]): { name: string }[] => {\n    if (!initialIds || initialIds.length === 0) {\n        return [{ name: '' }];\n    }\n\n    return initialIds.map((id: string) => ({ name: id }));\n};\n\nconst getInitialData = ({ initial, modalType, clientId, clientName }: any) => {\n    if (initial && initial.blocked_services) {\n        const { blocked_services } = initial;\n        const blocked = {};\n\n        blocked_services.forEach((service: any) => {\n            blocked[service] = true;\n        });\n\n        return {\n            ...initial,\n            blocked_services: blocked,\n            ids: normalizeIds(initial.ids),\n        };\n    }\n\n    if (modalType !== MODAL_TYPE.EDIT_CLIENT && clientId) {\n        return {\n            ...initial,\n            name: clientName,\n            ids: [{ name: clientId }],\n        };\n    }\n\n    return {\n        ...initial,\n        ids: normalizeIds(initial.ids),\n    };\n};\n\ninterface ModalProps {\n    isModalOpen: boolean;\n    modalType: string;\n    currentClientData: object;\n    handleSubmit: (values: any) => void;\n    handleClose: (...args: unknown[]) => unknown;\n    processingAdding: boolean;\n    processingUpdating: boolean;\n    tagsOptions: { label: string; value: string }[];\n    t: (...args: unknown[]) => string;\n    clientId?: string;\n}\n\nconst Modal = ({\n    isModalOpen,\n    modalType,\n    currentClientData,\n    handleSubmit,\n    handleClose,\n    processingAdding,\n    processingUpdating,\n    tagsOptions,\n    clientId,\n    t,\n}: ModalProps) => {\n    const initialData = getInitialData({\n        initial: currentClientData,\n        modalType,\n        clientId,\n        clientName: t('client_name', { id: clientId }),\n    });\n\n    return (\n        <ReactModal\n            className=\"Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients\"\n            closeTimeoutMS={0}\n            isOpen={isModalOpen}\n            onRequestClose={handleClose}>\n            <div className=\"modal-content\">\n                <div className=\"modal-header\">\n                    <h4 className=\"modal-title\">\n                        {modalType === MODAL_TYPE.EDIT_CLIENT ? <Trans>client_edit</Trans> : <Trans>client_new</Trans>}\n                    </h4>\n\n                    <button type=\"button\" className=\"close\" onClick={handleClose}>\n                        <span className=\"sr-only\">Close</span>\n                    </button>\n                </div>\n\n                <Form\n                    initialValues={{ ...initialData }}\n                    onSubmit={handleSubmit}\n                    onClose={handleClose}\n                    processingAdding={processingAdding}\n                    processingUpdating={processingUpdating}\n                    tagsOptions={tagsOptions}\n                />\n            </div>\n        </ReactModal>\n    );\n};\n\nexport default withTranslation()(Modal);\n"
  },
  {
    "path": "client/src/components/Settings/Clients/Service.css",
    "content": ".service {\n    display: flex;\n    flex-direction: row-reverse;\n    align-items: center;\n    margin-bottom: 15px;\n    padding: 10px 15px;\n    border: 1px solid var(--card-border-color);\n    border-radius: 4px;\n    cursor: pointer;\n}\n\n.service__text {\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n}\n\n@media screen and (min-width: 768px) {\n    .services {\n        display: flex;\n        flex-flow: row wrap;\n    }\n\n    .service {\n        flex-grow: 0;\n        flex-shrink: 0;\n        flex-basis: calc(99.9% * 4 / 12 - (30px - 30px * 4 / 12));\n        max-width: calc(99.9% * 4 / 12 - (30px - 30px * 4 / 12));\n        width: calc(99.9% * 4 / 12 - (30px - 30px * 4 / 12));\n    }\n\n    .service--global {\n        flex-basis: 1;\n        max-width: 100%;\n        width: 100%;\n    }\n\n    .service:nth-child(1n) {\n        margin-right: 30px;\n        margin-left: 0;\n    }\n\n    .service:nth-child(3n) {\n        margin-right: 0;\n        margin-left: auto;\n    }\n}\n\n.service__icon {\n    width: 20px;\n    height: 20px;\n    flex-shrink: 0;\n    margin-right: 10px;\n    color: #495057;\n}\n\n.service__icon svg {\n    width: 20px;\n    height: 20px;\n    fill: #495057;\n}\n\n.service--global .service__icon {\n    display: none;\n}\n\n.service__icon--table {\n    margin-bottom: 3px;\n    color: #9aa0ac;\n}\n\n.service__switch {\n    margin-left: auto;\n    border: 1px solid rgba(150, 150, 150, 0.12);\n}\n\n.custom-switch-input:checked ~ .service__switch {\n    background-color: #cd201f;\n}\n\n.custom-switch-input:focus ~ .service__switch {\n    box-shadow: 0 0 0 2px #cd201f3b;\n    border-color: #ec4241;\n}\n\n.custom-switch-input:disabled ~ .service__switch,\n.custom-switch-input:disabled ~ .service__text,\n.custom-switch-input:disabled ~ .service__icon {\n    opacity: 0.5;\n    cursor: pointer;\n}\n"
  },
  {
    "path": "client/src/components/Settings/Clients/index.tsx",
    "content": "import React, { Component, Fragment } from 'react';\nimport { withTranslation } from 'react-i18next';\n\nimport { ClientsTable } from './ClientsTable';\n\nimport AutoClients from './AutoClients';\n\nimport PageTitle from '../../ui/PageTitle';\n\nimport Loading from '../../ui/Loading';\nimport { ClientsData, DashboardData, StatsData } from '../../../initialState';\n\ninterface ClientsProps {\n    t: (...args: unknown[]) => string;\n    dashboard: DashboardData;\n    stats: StatsData;\n    clients: ClientsData;\n    toggleClientModal: (...args: unknown[]) => unknown;\n    deleteClient: (...args: unknown[]) => string;\n    addClient: (...args: unknown[]) => string;\n    updateClient: (...args: unknown[]) => string;\n    getClients: (...args: unknown[]) => unknown;\n    getStats: (...args: unknown[]) => unknown;\n}\n\nclass Clients extends Component<ClientsProps> {\n    componentDidMount() {\n        this.props.getClients();\n\n        this.props.getStats();\n    }\n\n    render() {\n        const {\n            t,\n\n            dashboard,\n\n            stats,\n\n            clients,\n\n            addClient,\n\n            updateClient,\n\n            deleteClient,\n\n            toggleClientModal,\n\n            getStats,\n        } = this.props;\n\n        return (\n            <Fragment>\n                <PageTitle title={t('client_settings')} />\n\n                {(stats.processingStats || dashboard.processingClients) && <Loading />}\n                {!stats.processingStats && !dashboard.processingClients && (\n                    <Fragment>\n                        <ClientsTable\n                            clients={dashboard.clients}\n                            normalizedTopClients={stats.normalizedTopClients}\n                            isModalOpen={clients.isModalOpen}\n                            modalClientName={clients.modalClientName}\n                            modalType={clients.modalType}\n                            addClient={addClient}\n                            updateClient={updateClient}\n                            deleteClient={deleteClient}\n                            toggleClientModal={toggleClientModal}\n                            processingAdding={clients.processingAdding}\n                            processingDeleting={clients.processingDeleting}\n                            processingUpdating={clients.processingUpdating}\n                            getStats={getStats}\n                            supportedTags={dashboard.supportedTags}\n                        />\n\n                        <AutoClients\n                            autoClients={dashboard.autoClients}\n                            normalizedTopClients={stats.normalizedTopClients}\n                        />\n                    </Fragment>\n                )}\n            </Fragment>\n        );\n    }\n}\n\nexport default withTranslation()(Clients);\n"
  },
  {
    "path": "client/src/components/Settings/Clients/whoisCell.tsx",
    "content": "import React, { Fragment } from 'react';\n\nimport { normalizeWhois } from '../../../helpers/helpers';\nimport { WHOIS_ICONS } from '../../../helpers/constants';\n\nconst getFormattedWhois = (value: any, t: any) => {\n    const whoisInfo = normalizeWhois(value);\n    const whoisKeys = Object.keys(whoisInfo);\n\n    if (whoisKeys.length > 0) {\n        return whoisKeys.map((key) => {\n            const icon = WHOIS_ICONS[key];\n            return (\n                <div key={key} title={t(key)}>\n                    {icon && (\n                        <Fragment>\n                            <svg className=\"logs__whois-icon text-muted-dark icons icon--24\">\n                                <use xlinkHref={`#${icon}`} />\n                            </svg>\n                            &nbsp;\n                        </Fragment>\n                    )}\n                    {whoisInfo[key]}\n                </div>\n            );\n        });\n    }\n\n    return '–';\n};\n\nconst whoisCell = (t: any) =>\n    function cell(row: any) {\n        const { value } = row;\n\n        return (\n            <div className=\"logs__row o-hidden\">\n                <div className=\"logs__text logs__text--wrap\">{getFormattedWhois(value, t)}</div>\n            </div>\n        );\n    };\n\nexport default whoisCell;\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/FormDHCPv4.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { Controller, useFormContext } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\n\nimport { UINT32_RANGE } from '../../../helpers/constants';\nimport {\n    validateGatewaySubnetMask,\n    validateIpForGatewaySubnetMask,\n    validateIpv4,\n    validateIpv4RangeEnd,\n    validateNotInRange,\n    validateRequiredValue,\n} from '../../../helpers/validators';\nimport { DhcpFormValues } from '.';\nimport { Input } from '../../ui/Controls/Input';\nimport { toNumber } from '../../../helpers/form';\n\ntype FormDHCPv4Props = {\n    processingConfig?: boolean;\n    ipv4placeholders?: {\n        gateway_ip: string;\n        subnet_mask: string;\n        range_start: string;\n        range_end: string;\n        lease_duration: string;\n    };\n    interfaces: any;\n    onSubmit?: (data: DhcpFormValues) => void;\n};\n\nconst FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }: FormDHCPv4Props) => {\n    const { t } = useTranslation();\n\n    const {\n        handleSubmit,\n        formState: { errors, isSubmitting },\n        control,\n        watch,\n    } = useFormContext<DhcpFormValues>();\n\n    const interfaceName = watch('interface_name');\n    const isInterfaceIncludesIpv4 = interfaces?.[interfaceName]?.ipv4_addresses;\n\n    const formValues = watch('v4');\n    const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);\n    const hasV4Errors = errors.v4 && Object.keys(errors.v4).length > 0;\n\n    const isDisabled = useMemo(() => {\n        return isSubmitting || hasV4Errors || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig;\n    }, [isSubmitting, hasV4Errors, processingConfig, isInterfaceIncludesIpv4, isEmptyConfig]);\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)}>\n            <div className=\"row\">\n                <div className=\"col-lg-6\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"v4.gateway_ip\"\n                            control={control}\n                            rules={{\n                                validate: {\n                                    ipv4: validateIpv4,\n                                    required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),\n                                    notInRange: validateNotInRange,\n                                },\n                            }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"text\"\n                                    data-testid=\"v4_gateway_ip\"\n                                    label={t('dhcp_form_gateway_input')}\n                                    placeholder={t(ipv4placeholders.gateway_ip)}\n                                    error={fieldState.error?.message}\n                                    disabled={!isInterfaceIncludesIpv4}\n                                />\n                            )}\n                        />\n                    </div>\n\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"v4.subnet_mask\"\n                            control={control}\n                            rules={{\n                                validate: {\n                                    required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),\n                                    subnet: validateGatewaySubnetMask,\n                                },\n                            }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"text\"\n                                    data-testid=\"v4_subnet_mask\"\n                                    label={t('dhcp_form_subnet_input')}\n                                    placeholder={t(ipv4placeholders.subnet_mask)}\n                                    error={fieldState.error?.message}\n                                    disabled={!isInterfaceIncludesIpv4}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n\n                <div className=\"col-lg-6\">\n                    <div className=\"form__group mb-0\">\n                        <div className=\"row\">\n                            <div className=\"col-12\">\n                                <label>{t('dhcp_form_range_title')}</label>\n                            </div>\n\n                            <div className=\"col\">\n                                <Controller\n                                    name=\"v4.range_start\"\n                                    control={control}\n                                    rules={{\n                                        validate: {\n                                            ipv4: validateIpv4,\n                                            gateway: validateIpForGatewaySubnetMask,\n                                        },\n                                    }}\n                                    render={({ field, fieldState }) => (\n                                        <Input\n                                            {...field}\n                                            type=\"text\"\n                                            data-testid=\"v4_range_start\"\n                                            placeholder={t(ipv4placeholders.range_start)}\n                                            error={fieldState.error?.message}\n                                            disabled={!isInterfaceIncludesIpv4}\n                                        />\n                                    )}\n                                />\n                            </div>\n\n                            <div className=\"col\">\n                                <Controller\n                                    name=\"v4.range_end\"\n                                    control={control}\n                                    rules={{\n                                        validate: {\n                                            ipv4: validateIpv4,\n                                            rangeEnd: validateIpv4RangeEnd,\n                                            gateway: validateIpForGatewaySubnetMask,\n                                        },\n                                    }}\n                                    render={({ field, fieldState }) => (\n                                        <Input\n                                            {...field}\n                                            type=\"text\"\n                                            data-testid=\"v4_range_end\"\n                                            placeholder={t(ipv4placeholders.range_end)}\n                                            error={fieldState.error?.message}\n                                            disabled={!isInterfaceIncludesIpv4}\n                                        />\n                                    )}\n                                />\n                            </div>\n                        </div>\n                    </div>\n\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"v4.lease_duration\"\n                            control={control}\n                            rules={{\n                                validate: {\n                                    required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),\n                                },\n                            }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"number\"\n                                    data-testid=\"v4_lease_duration\"\n                                    label={t('dhcp_form_lease_title')}\n                                    placeholder={t(ipv4placeholders.lease_duration)}\n                                    error={fieldState.error?.message}\n                                    disabled={!isInterfaceIncludesIpv4}\n                                    min={1}\n                                    max={UINT32_RANGE.MAX}\n                                    value={field.value ?? ''}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"btn-list\">\n                <button\n                    data-testid=\"v4_save\"\n                    type=\"submit\"\n                    className=\"btn btn-success btn-standard\"\n                    disabled={isDisabled}>\n                    {t('save_config')}\n                </button>\n            </div>\n        </form>\n    );\n};\n\nexport default FormDHCPv4;\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/FormDHCPv6.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { Controller, useFormContext } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\n\nimport { UINT32_RANGE } from '../../../helpers/constants';\nimport { validateIpv6, validateRequiredValue } from '../../../helpers/validators';\nimport { DhcpFormValues } from '.';\nimport { Input } from '../../ui/Controls/Input';\nimport { toNumber } from '../../../helpers/form';\n\ntype FormDHCPv6Props = {\n    processingConfig?: boolean;\n    ipv6placeholders?: {\n        range_start: string;\n        range_end: string;\n        lease_duration: string;\n    };\n    interfaces: any;\n    onSubmit?: (data: DhcpFormValues) => Promise<void> | void;\n};\n\nconst FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }: FormDHCPv6Props) => {\n    const { t } = useTranslation();\n    const {\n        handleSubmit,\n        formState: { isSubmitting, isValid },\n        control,\n        watch,\n    } = useFormContext<DhcpFormValues>();\n\n    const interfaceName = watch('interface_name');\n    const isInterfaceIncludesIpv6 = interfaces?.[interfaceName]?.ipv6_addresses;\n\n    const formValues = watch('v6');\n    const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);\n\n    const isDisabled = useMemo(() => {\n        return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv6 || isEmptyConfig;\n    }, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv6, isEmptyConfig]);\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)}>\n            <div className=\"row\">\n                <div className=\"col-lg-6\">\n                    <div className=\"form__group mb-0\">\n                        <div className=\"row\">\n                            <div className=\"col-12\">\n                                <label>{t('dhcp_form_range_title')}</label>\n                            </div>\n\n                            <div className=\"col\">\n                                <Controller\n                                    name=\"v6.range_start\"\n                                    control={control}\n                                    rules={{\n                                        validate: isInterfaceIncludesIpv6\n                                            ? {\n                                                  ipv6: validateIpv6,\n                                                  required: validateRequiredValue,\n                                              }\n                                            : undefined,\n                                    }}\n                                    render={({ field, fieldState }) => (\n                                        <Input\n                                            {...field}\n                                            type=\"text\"\n                                            data-testid=\"v6_range_start\"\n                                            placeholder={t(ipv6placeholders.range_start)}\n                                            error={fieldState.error?.message}\n                                            disabled={!isInterfaceIncludesIpv6}\n                                        />\n                                    )}\n                                />\n                            </div>\n\n                            <div className=\"col\">\n                                <Controller\n                                    name=\"v6.range_end\"\n                                    control={control}\n                                    render={({ field, fieldState }) => (\n                                        <Input\n                                            {...field}\n                                            type=\"text\"\n                                            data-testid=\"v6_range_end\"\n                                            placeholder={t(ipv6placeholders.range_end)}\n                                            error={fieldState.error?.message}\n                                            disabled\n                                        />\n                                    )}\n                                />\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"row\">\n                <div className=\"col-lg-6 form__group form__group--settings\">\n                    <Controller\n                        name=\"v6.lease_duration\"\n                        control={control}\n                        rules={{\n                            validate: isInterfaceIncludesIpv6\n                                ? {\n                                      required: validateRequiredValue,\n                                  }\n                                : undefined,\n                        }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"number\"\n                                data-testid=\"v6_lease_duration\"\n                                label={t('dhcp_form_lease_title')}\n                                placeholder={t(ipv6placeholders.lease_duration)}\n                                error={fieldState.error?.message}\n                                disabled={!isInterfaceIncludesIpv6}\n                                min={1}\n                                max={UINT32_RANGE.MAX}\n                                onChange={(e) => {\n                                    const { value } = e.target;\n                                    field.onChange(toNumber(value));\n                                }}\n                            />\n                        )}\n                    />\n                </div>\n            </div>\n\n            <div className=\"btn-list\">\n                <button\n                    data-testid=\"v6_save\"\n                    type=\"submit\"\n                    className=\"btn btn-success btn-standard\"\n                    disabled={isDisabled}>\n                    {t('save_config')}\n                </button>\n            </div>\n        </form>\n    );\n};\n\nexport default FormDHCPv6;\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/Interfaces.tsx",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { useFormContext } from 'react-hook-form';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { validateRequiredValue } from '../../../helpers/validators';\nimport { RootState } from '../../../initialState';\nimport { DhcpFormValues } from '.';\n\nconst renderInterfaces = (interfaces: any) =>\n    Object.keys(interfaces).map((item) => {\n        const option = interfaces[item];\n        const { name } = option;\n\n        const [interfaceIPv4] = option?.ipv4_addresses ?? [];\n        const [interfaceIPv6] = option?.ipv6_addresses ?? [];\n\n        const optionContent = [name, interfaceIPv4, interfaceIPv6].filter(Boolean).join(' - ');\n\n        return (\n            <option value={name} key={name}>\n                {optionContent}\n            </option>\n        );\n    });\n\nconst getInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: any) => [\n    {\n        name: 'dhcp_form_gateway_input',\n        value: gateway_ip,\n    },\n    {\n        name: 'dhcp_hardware_address',\n        value: hardware_address,\n    },\n    {\n        name: 'dhcp_ip_addresses',\n        value: ip_addresses,\n        render: (ip_addresses: any) =>\n            ip_addresses.map((ip: any) => (\n                <span key={ip} className=\"interface__ip\">\n                    {ip}\n                </span>\n            )),\n    },\n];\n\ninterface RenderInterfaceValuesProps {\n    gateway_ip: string;\n    hardware_address: string;\n    ip_addresses: string[];\n}\n\nconst renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: RenderInterfaceValuesProps) => (\n    <div className=\"d-flex align-items-end dhcp__interfaces-info\">\n        <ul className=\"list-unstyled m-0\">\n            {getInterfaceValues({\n                gateway_ip,\n                hardware_address,\n                ip_addresses,\n            }).map(\n                ({ name, value, render }) =>\n                    value && (\n                        <li key={name}>\n                            <span className=\"interface__title\">\n                                <Trans>{name}</Trans>:{' '}\n                            </span>\n                            {render?.(value) || value}\n                        </li>\n                    ),\n            )}\n        </ul>\n    </div>\n);\n\nconst Interfaces = () => {\n    const { t } = useTranslation();\n    const {\n        register,\n        watch,\n        formState: { errors },\n    } = useFormContext<DhcpFormValues>();\n\n    const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp);\n\n    const interface_name = watch('interface_name');\n\n    if (processingInterfaces || !interfaces) {\n        return null;\n    }\n\n    const interfaceValue = interface_name && interfaces[interface_name];\n\n    return (\n        <div className=\"row dhcp__interfaces\">\n            <div className=\"col col__dhcp\">\n                <label htmlFor=\"interface_name\" className=\"form__label\">\n                    {t('dhcp_interface_select')}\n                </label>\n                <select\n                    id=\"interface_name\"\n                    data-testid=\"interface_name\"\n                    className=\"form-control custom-select pl-4 col-md\"\n                    disabled={enabled}\n                    {...register('interface_name', {\n                        validate: validateRequiredValue,\n                    })}>\n                    <option value=\"\" disabled={enabled}>\n                        {t('dhcp_interface_select')}\n                    </option>\n                    {renderInterfaces(interfaces)}\n                </select>\n                {errors.interface_name && (\n                    <div className=\"form__message form__message--error\">{t(errors.interface_name.message)}</div>\n                )}\n            </div>\n            {interfaceValue &&\n                renderInterfaceValues({\n                    gateway_ip: interfaceValue.gateway_ip,\n                    hardware_address: interfaceValue.hardware_address,\n                    ip_addresses: interfaceValue.ip_addresses,\n                })}\n        </div>\n    );\n};\n\nexport default Interfaces;\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/Leases.tsx",
    "content": "import React, { Component } from 'react';\nimport { connect } from 'react-redux';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport { Trans, withTranslation } from 'react-i18next';\nimport { LEASES_TABLE_DEFAULT_PAGE_SIZE, MODAL_TYPE } from '../../../helpers/constants';\n\nimport { sortIp } from '../../../helpers/helpers';\n\nimport { toggleLeaseModal } from '../../../actions';\n\ninterface LeasesProps {\n    leases?: unknown[];\n    t?: (...args: unknown[]) => string;\n    dispatch?: (...args: unknown[]) => unknown;\n    disabledLeasesButton?: boolean;\n}\n\nclass Leases extends Component<LeasesProps> {\n    cellWrap = ({ value }: any) => (\n        <div className=\"logs__row o-hidden\">\n            <span className=\"logs__text\" title={value}>\n                {value}\n            </span>\n        </div>\n    );\n\n    convertToStatic = (data: any) => () => {\n        const { dispatch } = this.props;\n        dispatch(\n            toggleLeaseModal({\n                type: MODAL_TYPE.ADD_LEASE,\n                config: data,\n            }),\n        );\n    };\n\n    makeStatic = ({ row }: any) => {\n        const { t, disabledLeasesButton } = this.props;\n        return (\n            <div className=\"logs__row logs__row--center\">\n                <button\n                    type=\"button\"\n                    className=\"btn btn-icon btn-icon--green btn-outline-success btn-sm\"\n                    title={t('make_static')}\n                    onClick={this.convertToStatic(row)}\n                    disabled={disabledLeasesButton}>\n                    <svg className=\"icons icon12\">\n                        <use xlinkHref=\"#plus\" />\n                    </svg>\n                </button>\n            </div>\n        );\n    };\n\n    render() {\n        const { leases, t } = this.props;\n        return (\n            <ReactTable\n                data={leases || []}\n                columns={[\n                    {\n                        Header: 'MAC',\n                        accessor: 'mac',\n                        minWidth: 180,\n                        Cell: this.cellWrap,\n                    },\n                    {\n                        Header: 'IP',\n                        accessor: 'ip',\n                        minWidth: 230,\n                        Cell: this.cellWrap,\n                        sortMethod: sortIp,\n                    },\n                    {\n                        Header: <Trans>dhcp_table_hostname</Trans>,\n                        accessor: 'hostname',\n                        minWidth: 230,\n                        Cell: this.cellWrap,\n                    },\n                    {\n                        Header: <Trans>dhcp_table_expires</Trans>,\n                        accessor: 'expires',\n                        minWidth: 220,\n                        Cell: this.cellWrap,\n                    },\n                    {\n                        Header: <Trans>actions_table_header</Trans>,\n                        Cell: this.makeStatic,\n                    },\n                ]}\n                pageSize={LEASES_TABLE_DEFAULT_PAGE_SIZE}\n                showPageSizeOptions={false}\n                showPagination={leases.length > LEASES_TABLE_DEFAULT_PAGE_SIZE}\n                noDataText={t('dhcp_leases_not_found')}\n                minRows={6}\n                className=\"-striped -highlight card-table-overflow\"\n            />\n        );\n    }\n}\n\nexport default withTranslation()(\n    connect(\n        () => ({}),\n        (dispatch) => ({ dispatch }),\n    )(Leases),\n);\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/StaticLeases/Form.tsx",
    "content": "import React from 'react';\nimport { useForm, Controller } from 'react-hook-form';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { normalizeMac } from '../../../../helpers/form';\nimport {\n    validateIpv4,\n    validateMac,\n    validateRequiredValue,\n    validateIpv4InCidr,\n    validateIpGateway,\n} from '../../../../helpers/validators';\n\nimport { toggleLeaseModal } from '../../../../actions';\nimport { RootState } from '../../../../initialState';\nimport { Input } from '../../../ui/Controls/Input';\n\ntype Props = {\n    initialValues?: {\n        mac?: string;\n        ip?: string;\n        hostname?: string;\n        cidr?: string;\n        gatewayIp?: string;\n    };\n    processingAdding?: boolean;\n    cidr?: string;\n    isEdit?: boolean;\n    onSubmit: (data: any) => void;\n};\n\nexport const Form = ({ initialValues, processingAdding, cidr, isEdit, onSubmit }: Props) => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const dynamicLease = useSelector((store: RootState) => store.dhcp.leaseModalConfig, shallowEqual);\n\n    const {\n        handleSubmit,\n        control,\n        reset,\n        formState: { isSubmitting, isDirty },\n    } = useForm({\n        defaultValues: initialValues,\n        mode: 'onBlur',\n    });\n\n    const onClick = () => {\n        reset();\n        dispatch(toggleLeaseModal());\n    };\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)}>\n            <div className=\"modal-body\">\n                <div className=\"form__group\">\n                    <Controller\n                        name=\"mac\"\n                        control={control}\n                        rules={{ validate: { required: validateRequiredValue, mac: validateMac } }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"text\"\n                                data-testid=\"static_lease_mac\"\n                                placeholder={t('form_enter_mac')}\n                                disabled={isEdit}\n                                error={fieldState.error?.message}\n                                onChange={(e) => field.onChange(normalizeMac(e.target.value))}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"form__group\">\n                    <Controller\n                        name=\"ip\"\n                        control={control}\n                        rules={{\n                            validate: {\n                                required: validateRequiredValue,\n                                ipv4: validateIpv4,\n                                inCidr: validateIpv4InCidr,\n                                gateway: validateIpGateway,\n                            },\n                        }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"text\"\n                                data-testid=\"static_lease_ip\"\n                                error={fieldState.error?.message}\n                                placeholder={t('form_enter_subnet_ip', { cidr })}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"form__group\">\n                    <Controller\n                        name=\"hostname\"\n                        control={control}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"text\"\n                                data-testid=\"static_lease_hostname\"\n                                error={fieldState.error?.message}\n                                placeholder={t('form_enter_hostname')}\n                            />\n                        )}\n                    />\n                </div>\n            </div>\n\n            <div className=\"modal-footer\">\n                <div className=\"btn-list\">\n                    <button\n                        type=\"button\"\n                        data-testid=\"static_lease_cancel\"\n                        className=\"btn btn-secondary btn-standard\"\n                        disabled={isSubmitting}\n                        onClick={onClick}>\n                        <Trans>cancel_btn</Trans>\n                    </button>\n\n                    <button\n                        type=\"submit\"\n                        data-testid=\"static_lease_save\"\n                        className=\"btn btn-success btn-standard\"\n                        disabled={isSubmitting || processingAdding || (!isDirty && !dynamicLease)}>\n                        <Trans>save_btn</Trans>\n                    </button>\n                </div>\n            </div>\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/StaticLeases/Modal.tsx",
    "content": "import React from 'react';\nimport { Trans, withTranslation } from 'react-i18next';\n\nimport ReactModal from 'react-modal';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport { Form } from './Form';\n\nimport { toggleLeaseModal } from '../../../../actions';\nimport { MODAL_TYPE } from '../../../../helpers/constants';\nimport { RootState } from '../../../../initialState';\n\ninterface ModalProps {\n    isModalOpen: boolean;\n    modalType: string;\n    handleSubmit: (values: any) => void;\n    processingAdding: boolean;\n    cidr: string;\n    gatewayIp?: string;\n}\n\nconst Modal = ({\n    isModalOpen,\n    modalType,\n    handleSubmit,\n    processingAdding,\n    cidr,\n    gatewayIp,\n}: ModalProps) => {\n    const dispatch = useDispatch();\n\n    const toggleModal = () => dispatch(toggleLeaseModal());\n\n    const leaseInitialData = useSelector((state: RootState) => state.dhcp.leaseModalConfig, shallowEqual);\n\n    return (\n        <ReactModal\n            className=\"Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients\"\n            closeTimeoutMS={0}\n            isOpen={isModalOpen}\n            onRequestClose={toggleModal}>\n            <div className=\"modal-content\">\n                <div className=\"modal-header\">\n                    <h4 className=\"modal-title\">\n                        {modalType === MODAL_TYPE.EDIT_LEASE ? (\n                            <Trans>dhcp_edit_static_lease</Trans>\n                        ) : (\n                            <Trans>dhcp_new_static_lease</Trans>\n                        )}\n                    </h4>\n\n                    <button type=\"button\" className=\"close\" onClick={toggleModal}>\n                        <span className=\"sr-only\">Close</span>\n                    </button>\n                </div>\n\n                <Form\n                    initialValues={{\n                        mac: leaseInitialData?.mac ?? '',\n                        ip: leaseInitialData?.ip ?? '',\n                        hostname: leaseInitialData?.hostname ?? '',\n                        cidr,\n                        gatewayIp,\n                    }}\n                    onSubmit={handleSubmit}\n                    processingAdding={processingAdding}\n                    cidr={cidr}\n                    isEdit={modalType === MODAL_TYPE.EDIT_LEASE}\n                />\n            </div>\n        </ReactModal>\n    );\n};\n\nexport default withTranslation()(Modal);\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/StaticLeases/index.tsx",
    "content": "import React from 'react';\n\n// @ts-expect-error FIXME: update react-table\nimport ReactTable from 'react-table';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { useDispatch } from 'react-redux';\nimport { LEASES_TABLE_DEFAULT_PAGE_SIZE, MODAL_TYPE } from '../../../../helpers/constants';\n\nimport { sortIp } from '../../../../helpers/helpers';\n\nimport Modal from './Modal';\nimport { addStaticLease, removeStaticLease, toggleLeaseModal, updateStaticLease } from '../../../../actions';\n\ninterface cellWrapProps {\n    value: string;\n}\n\nconst cellWrap = ({ value }: cellWrapProps) => (\n    <div className=\"logs__row o-hidden\">\n        <span className=\"logs__text\" title={value}>\n            {value}\n        </span>\n    </div>\n);\n\ninterface StaticLeasesProps {\n    staticLeases: unknown[];\n    isModalOpen: boolean;\n    modalType: string;\n    processingAdding: boolean;\n    processingDeleting: boolean;\n    processingUpdating: boolean;\n    cidr: string;\n    gatewayIp?: string;\n}\n\nconst StaticLeases = ({\n    isModalOpen,\n    modalType,\n    processingAdding,\n    processingDeleting,\n    processingUpdating,\n    staticLeases,\n    cidr,\n    gatewayIp,\n}: StaticLeasesProps) => {\n    const [t] = useTranslation();\n    const dispatch = useDispatch();\n\n    const handleSubmit = (data: any) => {\n        const { mac, ip, hostname } = data;\n\n        if (modalType === MODAL_TYPE.EDIT_LEASE) {\n            dispatch(updateStaticLease({ mac, ip, hostname }));\n        } else {\n            dispatch(addStaticLease({ mac, ip, hostname }));\n        }\n    };\n\n    const handleDelete = (ip: any, mac: any, hostname = '') => {\n        const name = hostname || ip;\n        // eslint-disable-next-line no-alert\n        if (window.confirm(t('delete_confirm', { key: name }))) {\n            dispatch(\n                removeStaticLease({\n                    ip,\n                    mac,\n                    hostname,\n                }),\n            );\n        }\n    };\n\n    return (\n        <>\n            <ReactTable\n                data={staticLeases || []}\n                columns={[\n                    {\n                        Header: 'MAC',\n                        accessor: 'mac',\n                        minWidth: 180,\n                        Cell: cellWrap,\n                    },\n                    {\n                        Header: 'IP',\n                        accessor: 'ip',\n                        minWidth: 230,\n                        sortMethod: sortIp,\n                        Cell: cellWrap,\n                    },\n                    {\n                        Header: <Trans>dhcp_table_hostname</Trans>,\n                        accessor: 'hostname',\n                        minWidth: 230,\n                        Cell: cellWrap,\n                    },\n                    {\n                        Header: <Trans>actions_table_header</Trans>,\n                        accessor: 'actions',\n                        maxWidth: 150,\n                        sortable: false,\n                        resizable: false,\n                        // eslint-disable-next-line react/display-name\n                        Cell: (row: any) => {\n                            const { ip, mac, hostname } = row.original;\n\n                            return (\n                                <div className=\"logs__row logs__row--center\">\n                                    <button\n                                        type=\"button\"\n                                        className=\"btn btn-icon btn-outline-primary btn-sm mr-2\"\n                                        onClick={() =>\n                                            dispatch(\n                                                toggleLeaseModal({\n                                                    type: MODAL_TYPE.EDIT_LEASE,\n                                                    config: { ip, mac, hostname },\n                                                }),\n                                            )\n                                        }\n                                        disabled={processingUpdating}\n                                        title={t('edit_table_action')}>\n                                        <svg className=\"icons icon12\">\n                                            <use xlinkHref=\"#edit\" />\n                                        </svg>\n                                    </button>\n\n                                    <button\n                                        type=\"button\"\n                                        className=\"btn btn-icon btn-outline-secondary btn-sm\"\n                                        onClick={() => handleDelete(ip, mac, hostname)}\n                                        disabled={processingDeleting}\n                                        title={t('delete_table_action')}>\n                                        <svg className=\"icons icon12\">\n                                            <use xlinkHref=\"#delete\" />\n                                        </svg>\n                                    </button>\n                                </div>\n                            );\n                        },\n                    },\n                ]}\n                pageSize={LEASES_TABLE_DEFAULT_PAGE_SIZE}\n                showPageSizeOptions={false}\n                showPagination={staticLeases.length > LEASES_TABLE_DEFAULT_PAGE_SIZE}\n                noDataText={t('dhcp_static_leases_not_found')}\n                className=\"-striped -highlight card-table-overflow\"\n                minRows={6}\n            />\n\n            <Modal\n                isModalOpen={isModalOpen}\n                modalType={modalType}\n                handleSubmit={handleSubmit}\n                processingAdding={processingAdding}\n                cidr={cidr}\n                gatewayIp={gatewayIp}\n            />\n        </>\n    );\n};\n\nexport default StaticLeases;\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/index.css",
    "content": ".dhcp-form__button {\n    margin: 0 1rem;\n}\n\n.page-title--dhcp {\n    display: flex;\n    align-items: center;\n}\n\n.col__dhcp {\n    flex: 0 0 50%;\n    max-width: 50%;\n    padding-right: 0;\n}\n\n.dhcp__interfaces {\n    padding-bottom: 1rem;\n}\n\n.dhcp__interfaces-info {\n    padding: 0.5rem 0.75rem 0;\n    line-break: anywhere;\n}\n\n@media (max-width: 991.98px) {\n    .dhcp-form__button {\n        margin: 0.5rem 0;\n        display: block;\n    }\n\n    .page-title--dhcp {\n        flex-direction: column;\n        align-items: flex-start;\n        padding-bottom: 0.5rem;\n    }\n\n    .col__dhcp {\n        flex: 0 0 100%;\n        max-width: 100%;\n        padding-right: 0.75rem;\n    }\n\n    .dhcp__interfaces {\n        flex-direction: column;\n        padding-bottom: 0.5rem;\n    }\n}\n"
  },
  {
    "path": "client/src/components/Settings/Dhcp/index.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\nimport classNames from 'classnames';\n\nimport { FormProvider, useForm } from 'react-hook-form';\nimport { DHCP_DESCRIPTION_PLACEHOLDERS, STATUS_RESPONSE } from '../../../helpers/constants';\n\nimport Leases from './Leases';\n\nimport StaticLeases from './StaticLeases/index';\n\nimport Card from '../../ui/Card';\n\nimport PageTitle from '../../ui/PageTitle';\n\nimport Loading from '../../ui/Loading';\nimport {\n    findActiveDhcp,\n    getDhcpInterfaces,\n    getDhcpStatus,\n    resetDhcp,\n    setDhcpConfig,\n    resetDhcpLeases,\n    toggleDhcp,\n    toggleLeaseModal,\n} from '../../../actions';\n\nimport FormDHCPv4 from './FormDHCPv4';\n\nimport FormDHCPv6 from './FormDHCPv6';\n\nimport Interfaces from './Interfaces';\nimport {\n    calculateDhcpPlaceholdersIpv4,\n    calculateDhcpPlaceholdersIpv6,\n    subnetMaskToBitMask,\n} from '../../../helpers/helpers';\nimport './index.css';\nimport { RootState } from '../../../initialState';\n\ntype IPv4FormValues = {\n    gateway_ip?: string;\n    subnet_mask?: string;\n    range_start?: string;\n    range_end?: string;\n    lease_duration?: number;\n}\n\ntype IPv6FormValues = {\n    range_start?: string;\n    range_end?: string;\n    lease_duration?: number;\n}\n\nconst getDefaultV4Values = (v4: IPv4FormValues) => {\n    const emptyForm = Object.entries(v4).every(\n        ([key, value]) => key === 'lease_duration' || value === ''\n    );\n\n    if (emptyForm) {\n        return {\n            ...v4,\n            lease_duration: undefined,\n        }\n    }\n\n    return v4;\n}\n\nexport type DhcpFormValues = {\n    v4?: IPv4FormValues;\n    v6?: IPv6FormValues;\n    interface_name?: string;\n};\n\nconst DEFAULT_V4_VALUES = {\n    gateway_ip: '',\n    subnet_mask: '',\n    range_start: '',\n    range_end: '',\n    lease_duration: undefined,\n};\n\nconst DEFAULT_V6_VALUES = {\n    range_start: '',\n    range_end: '',\n    lease_duration: undefined,\n};\n\nconst Dhcp = () => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const {\n        processingStatus,\n        processingConfig,\n        processing,\n        processingInterfaces,\n        check,\n        leases,\n        staticLeases,\n        isModalOpen,\n        processingAdding,\n        processingDeleting,\n        processingUpdating,\n        processingDhcp,\n        v4,\n        v6,\n        interface_name: interfaceName,\n        enabled,\n        dhcp_available,\n        interfaces,\n        modalType,\n    } = useSelector((state: RootState) => state.dhcp, shallowEqual);\n\n    const methods = useForm<DhcpFormValues>({\n        mode: 'onBlur',\n        defaultValues: {\n            v4: getDefaultV4Values(v4),\n            v6,\n            interface_name: interfaceName || '',\n        },\n    });\n    const { watch, reset } = methods;\n\n    const interface_name = watch('interface_name');\n    const isInterfaceIncludesIpv4 = useSelector(\n        (state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,\n    );\n    const ipv4Config = watch('v4');\n\n    const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);\n    const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);\n\n    useEffect(() => {\n        dispatch(getDhcpStatus());\n    }, []);\n\n    useEffect(() => {\n        if (dhcp_available) {\n            dispatch(getDhcpInterfaces());\n        }\n    }, [dhcp_available]);\n\n    useEffect(() => {\n        if (v4 || v6 || interfaceName) {\n            reset({\n                v4: {\n                    ...DEFAULT_V4_VALUES,\n                    ...getDefaultV4Values(v4),\n                },\n                v6: {\n                    ...DEFAULT_V6_VALUES,\n                    ...v6,\n                },\n                interface_name: interfaceName || '',\n            });\n        }\n    }, [v4, v6, interfaceName, reset]);\n\n    useEffect(() => {\n        const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];\n        const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];\n        const gateway_ip = interfaces?.[interface_name]?.gateway_ip;\n\n        const v4placeholders = ipv4\n            ? calculateDhcpPlaceholdersIpv4(ipv4, gateway_ip)\n            : DHCP_DESCRIPTION_PLACEHOLDERS.ipv4;\n\n        const v6placeholders = ipv6 ? calculateDhcpPlaceholdersIpv6() : DHCP_DESCRIPTION_PLACEHOLDERS.ipv6;\n\n        setIpv4Placeholders(v4placeholders);\n        setIpv6Placeholders(v6placeholders);\n    }, [interface_name]);\n\n    const clear = () => {\n        // eslint-disable-next-line no-alert\n        if (window.confirm(t('dhcp_reset'))) {\n            reset({\n                v4: DEFAULT_V4_VALUES,\n                v6: DEFAULT_V6_VALUES,\n                interface_name: '',\n            });\n            dispatch(resetDhcp());\n            dispatch(getDhcpStatus());\n        }\n    };\n\n    const handleSubmit = (values: DhcpFormValues) => {\n        dispatch(\n            setDhcpConfig({\n                interface_name,\n                ...values,\n            }),\n        );\n    };\n\n    const handleReset = () => {\n        if (window.confirm(t('dhcp_reset_leases_confirm'))) {\n            dispatch(resetDhcpLeases());\n        }\n    };\n\n    const enteredSomeV4Value = Object.values(v4).some(Boolean);\n\n    const enteredSomeV6Value = Object.values(v6).some(Boolean);\n    const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;\n\n    const getToggleDhcpButton = () => {\n        const filledConfig = interface_name && (Object.values(v4).every(Boolean) || Object.values(v6).every(Boolean));\n\n        const className = classNames('btn btn-sm', {\n            'btn-gray': enabled,\n            'btn-outline-success': !enabled,\n        });\n\n        const onClickDisable = () => dispatch(toggleDhcp({ enabled }));\n        const onClickEnable = () => {\n            const values = {\n                enabled,\n                interface_name,\n                v4: enteredSomeV4Value ? v4 : {},\n                v6: enteredSomeV6Value ? v6 : {},\n            };\n            dispatch(toggleDhcp(values));\n        };\n\n        return (\n            <button\n                type=\"button\"\n                className={className}\n                onClick={enabled ? onClickDisable : onClickEnable}\n                disabled={processingDhcp || processingConfig || (!enabled && (!filledConfig || !check))}>\n                <Trans>{enabled ? 'dhcp_disable' : 'dhcp_enable'}</Trans>\n            </button>\n        );\n    };\n\n    const statusButtonClass = classNames('btn btn-sm dhcp-form__button', {\n        'btn-loading btn-primary': processingStatus,\n        'btn-outline-primary': !processingStatus,\n    });\n\n    const onClick = () => dispatch(findActiveDhcp(interface_name));\n\n    const toggleModal = () => dispatch(toggleLeaseModal());\n\n    if (processing || processingInterfaces) {\n        return <Loading />;\n    }\n\n    if (!processing && !dhcp_available) {\n        return (\n            <div className=\"text-center pt-5\">\n                <h2>\n                    <Trans>unavailable_dhcp</Trans>\n                </h2>\n\n                <h4>\n                    <Trans>unavailable_dhcp_desc</Trans>\n                </h4>\n            </div>\n        );\n    }\n\n    const toggleDhcpButton = getToggleDhcpButton();\n\n    const inputtedIPv4values = ipv4Config.gateway_ip && ipv4Config.subnet_mask;\n\n    const isEmptyConfig = !Object.values(ipv4Config).some(Boolean);\n    const disabledLeasesButton = Boolean(\n        !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values,\n    );\n    const cidr = inputtedIPv4values ? `${ipv4Config.gateway_ip}/${subnetMaskToBitMask(ipv4Config.subnet_mask)}` : '';\n\n    return (\n        <>\n            <PageTitle title={t('dhcp_settings')} subtitle={t('dhcp_description')} containerClass=\"page-title--dhcp\">\n                {toggleDhcpButton}\n\n                <button\n                    type=\"button\"\n                    className={statusButtonClass}\n                    onClick={onClick}\n                    disabled={enabled || !interface_name || processingConfig}>\n                    <Trans>check_dhcp_servers</Trans>\n                </button>\n\n                <button\n                    type=\"button\"\n                    className=\"btn btn-sm btn-outline-secondary\"\n                    disabled={!enteredSomeValue || processingConfig}\n                    onClick={clear}>\n                    <Trans>reset_settings</Trans>\n                </button>\n            </PageTitle>\n            {!processing && !processingInterfaces && (\n                <>\n                    {!enabled &&\n                        check &&\n                        (check.v4.other_server.found !== STATUS_RESPONSE.NO ||\n                            check.v6.other_server.found !== STATUS_RESPONSE.NO) && (\n                            <div className=\"mb-5\">\n                                <hr />\n\n                                <div className=\"text-danger\">\n                                    <Trans>dhcp_warning</Trans>\n                                </div>\n                            </div>\n                        )}\n\n                    <FormProvider {...methods}>\n                        <Interfaces />\n                        <Card title={t('dhcp_ipv4_settings')} bodyType=\"card-body box-body--settings\">\n                            <div>\n                                <FormDHCPv4\n                                    onSubmit={handleSubmit}\n                                    processingConfig={processingConfig}\n                                    ipv4placeholders={ipv4placeholders}\n                                    interfaces={interfaces}\n                                />\n                            </div>\n                        </Card>\n                        <Card title={t('dhcp_ipv6_settings')} bodyType=\"card-body box-body--settings\">\n                            <div>\n                                <FormDHCPv6\n                                    onSubmit={handleSubmit}\n                                    processingConfig={processingConfig}\n                                    ipv6placeholders={ipv6placeholders}\n                                    interfaces={interfaces}\n                                />\n                            </div>\n                        </Card>\n                    </FormProvider>\n\n                    {enabled && (\n                        <Card title={t('dhcp_leases')} bodyType=\"card-body box-body--settings\">\n                            <div className=\"row\">\n                                <div className=\"col\">\n                                    <Leases leases={leases} disabledLeasesButton={disabledLeasesButton} />\n                                </div>\n                            </div>\n                        </Card>\n                    )}\n\n                    <Card title={t('dhcp_static_leases')} bodyType=\"card-body box-body--settings\">\n                        <div className=\"row\">\n                            <div className=\"col-12\">\n                                <StaticLeases\n                                    staticLeases={staticLeases}\n                                    isModalOpen={isModalOpen}\n                                    modalType={modalType}\n                                    processingAdding={processingAdding}\n                                    processingDeleting={processingDeleting}\n                                    processingUpdating={processingUpdating}\n                                    cidr={cidr}\n                                    gatewayIp={ipv4Config.gateway_ip}\n                                />\n\n                                <div className=\"btn-list mt-2\">\n                                    <button\n                                        type=\"button\"\n                                        className=\"btn btn-success btn-standard mt-3\"\n                                        onClick={toggleModal}\n                                        disabled={disabledLeasesButton}>\n                                        <Trans>dhcp_add_static_lease</Trans>\n                                    </button>\n\n                                    <button\n                                        type=\"button\"\n                                        className=\"btn btn-secondary btn-standard mt-3\"\n                                        onClick={handleReset}>\n                                        <Trans>dhcp_reset_leases</Trans>\n                                    </button>\n                                </div>\n                            </div>\n                        </div>\n                    </Card>\n                </>\n            )}\n        </>\n    );\n};\n\nexport default Dhcp;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Access/Form.tsx",
    "content": "import React, { ReactNode } from 'react';\nimport { Controller, useForm } from 'react-hook-form';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport i18next from 'i18next';\nimport { CLIENT_ID_LINK } from '../../../../helpers/constants';\nimport { removeEmptyLines, trimMultilineString } from '../../../../helpers/helpers';\nimport { Textarea } from '../../../ui/Controls/Textarea';\n\ntype FormData = {\n    allowed_clients: string;\n    disallowed_clients: string;\n    blocked_hosts: string;\n};\n\nconst fields: {\n    id: keyof FormData;\n    title: string;\n    subtitle: ReactNode;\n    normalizeOnBlur: (value: string) => string;\n}[] = [\n    {\n        id: 'allowed_clients',\n        title: i18next.t('access_allowed_title'),\n        subtitle: (\n            <Trans\n                components={{\n                    a: <a href={CLIENT_ID_LINK} target=\"_blank\" rel=\"noopener noreferrer\" />,\n                }}>\n                access_allowed_desc\n            </Trans>\n        ),\n        normalizeOnBlur: removeEmptyLines,\n    },\n    {\n        id: 'disallowed_clients',\n        title: i18next.t('access_disallowed_title'),\n        subtitle: (\n            <Trans\n                components={{\n                    a: <a href={CLIENT_ID_LINK} target=\"_blank\" rel=\"noopener noreferrer\" />,\n                }}>\n                access_disallowed_desc\n            </Trans>\n        ),\n        normalizeOnBlur: trimMultilineString,\n    },\n    {\n        id: 'blocked_hosts',\n        title: i18next.t('access_blocked_title'),\n        subtitle: i18next.t('access_blocked_desc'),\n        normalizeOnBlur: removeEmptyLines,\n    },\n];\n\ntype FormProps = {\n    initialValues?: {\n        allowed_clients?: string;\n        disallowed_clients?: string;\n        blocked_hosts?: string;\n    };\n    onSubmit: (data: FormData) => void;\n    processingSet: boolean;\n};\n\nconst Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {\n    const { t } = useTranslation();\n\n    const {\n        control,\n        handleSubmit,\n        watch,\n        formState: { isSubmitting },\n    } = useForm<FormData>({\n        mode: 'onBlur',\n        defaultValues: {\n            allowed_clients: initialValues?.allowed_clients || '',\n            disallowed_clients: initialValues?.disallowed_clients || '',\n            blocked_hosts: initialValues?.blocked_hosts || '',\n        },\n    });\n\n    const allowedClients = watch('allowed_clients');\n\n    const renderField = ({\n        id,\n        title,\n        subtitle,\n        normalizeOnBlur,\n    }: {\n        id: keyof FormData;\n        title: string;\n        subtitle: ReactNode;\n        normalizeOnBlur: (value: string) => string;\n    }) => {\n        const disabled = allowedClients && id === 'disallowed_clients';\n\n        return (\n            <div key={id} className=\"form__group mb-5\">\n                <label className=\"form__label form__label--with-desc\" htmlFor={id}>\n                    {title}\n                    {disabled && <>&nbsp;({t('disabled')})</>}\n                </label>\n\n                <div className=\"form__desc form__desc--top\">{subtitle}</div>\n\n                <Controller\n                    name={id}\n                    control={control}\n                    render={({ field }) => (\n                        <Textarea\n                            {...field}\n                            id={id}\n                            data-testid={id}\n                            disabled={disabled || processingSet}\n                            onBlur={(e) => {\n                                field.onChange(normalizeOnBlur(e.target.value));\n                            }}\n                        />\n                    )}\n                />\n            </div>\n        );\n    };\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)}>\n            {fields.map((f) => renderField(f))}\n\n            <div className=\"card-actions\">\n                <div className=\"btn-list\">\n                    <button\n                        type=\"submit\"\n                        data-testid=\"access_save\"\n                        className=\"btn btn-success btn-standard\"\n                        disabled={isSubmitting || processingSet}>\n                        {t('save_config')}\n                    </button>\n                </div>\n            </div>\n        </form>\n    );\n};\n\nexport default Form;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Access/index.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport Form from './Form';\n\nimport Card from '../../../ui/Card';\nimport { setAccessList } from '../../../../actions/access';\nimport { RootState } from '../../../../initialState';\n\nconst Access = () => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const { processingSet, ...values } = useSelector((state: RootState) => state.access, shallowEqual);\n\n    const handleFormSubmit = (values: any) => {\n        dispatch(setAccessList(values));\n    };\n\n    return (\n        <Card title={t('access_title')} subtitle={t('access_desc')} bodyType=\"card-body box-body--settings\">\n            <Form initialValues={values} onSubmit={handleFormSubmit} processingSet={processingSet} />\n        </Card>\n    );\n};\n\nexport default Access;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Cache/Form.tsx",
    "content": "import React from 'react';\nimport { Controller, useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport i18next from 'i18next';\nimport { clearDnsCache } from '../../../../actions/dnsConfig';\nimport { CACHE_CONFIG_FIELDS, UINT32_RANGE } from '../../../../helpers/constants';\nimport { replaceZeroWithEmptyString } from '../../../../helpers/helpers';\nimport { RootState } from '../../../../initialState';\nimport { Checkbox } from '../../../ui/Controls/Checkbox';\n\nconst INPUTS_FIELDS = [\n    {\n        name: CACHE_CONFIG_FIELDS.cache_size,\n        title: i18next.t('cache_size'),\n        description: i18next.t('cache_size_desc'),\n        placeholder: i18next.t('enter_cache_size'),\n    },\n    {\n        name: CACHE_CONFIG_FIELDS.cache_ttl_min,\n        title: i18next.t('cache_ttl_min_override'),\n        description: i18next.t('cache_ttl_min_override_desc'),\n        placeholder: i18next.t('enter_cache_ttl_min_override'),\n    },\n    {\n        name: CACHE_CONFIG_FIELDS.cache_ttl_max,\n        title: i18next.t('cache_ttl_max_override'),\n        description: i18next.t('cache_ttl_max_override_desc'),\n        placeholder: i18next.t('enter_cache_ttl_max_override'),\n    },\n];\n\ntype FormData = {\n    cache_enabled: boolean;\n    cache_size: number;\n    cache_ttl_min: number;\n    cache_ttl_max: number;\n    cache_optimistic: boolean;\n};\n\ntype CacheFormProps = {\n    initialValues?: Partial<FormData>;\n    onSubmit: (data: FormData) => void;\n};\n\nconst Form = ({ initialValues, onSubmit }: CacheFormProps) => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n\n    const { processingSetConfig } = useSelector((state: RootState) => state.dnsConfig);\n\n    const {\n        register,\n        handleSubmit,\n        watch,\n        control,\n        formState: { isSubmitting },\n    } = useForm<FormData>({\n        mode: 'onBlur',\n        defaultValues: {\n            cache_enabled: initialValues?.cache_enabled || false,\n            cache_size: initialValues?.cache_size || 0,\n            cache_ttl_min: initialValues?.cache_ttl_min || 0,\n            cache_ttl_max: initialValues?.cache_ttl_max || 0,\n            cache_optimistic: initialValues?.cache_optimistic || false,\n        },\n    });\n\n    const cache_enabled = watch('cache_enabled');\n    const cache_size = watch('cache_size');\n    const cache_ttl_min = watch('cache_ttl_min');\n    const cache_ttl_max = watch('cache_ttl_max');\n\n    const minExceedsMax = cache_ttl_min > 0 && cache_ttl_max > 0 && cache_ttl_min > cache_ttl_max;\n    const cacheSizeZeroWhenEnabled = cache_enabled && cache_size === 0;\n\n    const handleClearCache = () => {\n        if (window.confirm(t('confirm_dns_cache_clear'))) {\n            dispatch(clearDnsCache());\n        }\n    };\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)}>\n            <div className=\"row\">\n                <div className=\"col-12 col-md-7\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"cache_enabled\"\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox\n                                    {...field}\n                                    data-testid=\"dns_cache_enabled\"\n                                    title={t('cache_enabled')}\n                                    subtitle={t('cache_enabled_desc')}\n                                    disabled={processingSetConfig}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n\n                {INPUTS_FIELDS.map(({ name, title, description, placeholder }) => (\n                    <div className=\"col-12\" key={name}>\n                        <div className=\"col-12 col-md-7 p-0\">\n                            <div className=\"form__group form__group--settings\">\n                                <label htmlFor={name} className=\"form__label form__label--with-desc\">\n                                    {title}\n                                </label>\n\n                                <div className=\"form__desc form__desc--top\">{description}</div>\n\n                                <input\n                                    type=\"number\"\n                                    data-testid={`dns_${name}`}\n                                    className=\"form-control\"\n                                    placeholder={placeholder}\n                                    disabled={processingSetConfig}\n                                    min={0}\n                                    max={UINT32_RANGE.MAX}\n                                    {...register(name as keyof FormData, {\n                                        valueAsNumber: true,\n                                        setValueAs: (value) => replaceZeroWithEmptyString(value),\n                                    })}\n                                />\n\n                                {name === CACHE_CONFIG_FIELDS.cache_size && cacheSizeZeroWhenEnabled && (\n                                    <span className=\"form__message form__message--error\">\n                                        {t('cache_size_validation')}\n                                    </span>\n                                )}\n                            </div>\n                        </div>\n                    </div>\n                ))}\n                {minExceedsMax && <span className=\"text-danger pl-3 pb-3\">{t('ttl_cache_validation')}</span>}\n            </div>\n\n            <div className=\"row\">\n                <div className=\"col-12 col-md-7\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"cache_optimistic\"\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox\n                                    {...field}\n                                    data-testid=\"dns_cache_optimistic\"\n                                    title={t('cache_optimistic')}\n                                    subtitle={t('cache_optimistic_desc')}\n                                    disabled={processingSetConfig}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n            </div>\n\n            <button\n                type=\"submit\"\n                data-testid=\"dns_save\"\n                className=\"btn btn-success btn-standard btn-large\"\n                disabled={isSubmitting || processingSetConfig || minExceedsMax || cacheSizeZeroWhenEnabled}>\n                {t('save_btn')}\n            </button>\n\n            <button\n                type=\"button\"\n                data-testid=\"dns_clear\"\n                className=\"btn btn-outline-secondary btn-standard form__button\"\n                onClick={handleClearCache}>\n                {t('clear_cache')}\n            </button>\n        </form>\n    );\n};\n\nexport default Form;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Cache/index.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport Card from '../../../ui/Card';\n\nimport Form from './Form';\nimport { setDnsConfig } from '../../../../actions/dnsConfig';\n\nimport { replaceEmptyStringsWithZeroes, replaceZeroWithEmptyString } from '../../../../helpers/helpers';\nimport { RootState } from '../../../../initialState';\n\nconst CacheConfig = () => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const { cache_enabled, cache_size, cache_ttl_max, cache_ttl_min, cache_optimistic } = useSelector(\n        (state: RootState) => state.dnsConfig,\n        shallowEqual,\n    );\n\n    const handleFormSubmit = (values: any) => {\n        const completedFields = replaceEmptyStringsWithZeroes(values);\n        dispatch(setDnsConfig(completedFields));\n    };\n\n    return (\n        <Card\n            title={t('dns_cache_config')}\n            subtitle={t('dns_cache_config_desc')}\n            bodyType=\"card-body box-body--settings\"\n            id=\"dns-config\">\n            <div className=\"form\">\n                <Form\n                    initialValues={{\n                        cache_enabled,\n                        cache_size: replaceZeroWithEmptyString(cache_size),\n                        cache_ttl_max: replaceZeroWithEmptyString(cache_ttl_max),\n                        cache_ttl_min: replaceZeroWithEmptyString(cache_ttl_min),\n                        cache_optimistic,\n                    }}\n                    onSubmit={handleFormSubmit}\n                />\n            </div>\n        </Card>\n    );\n};\n\nexport default CacheConfig;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Config/Form.tsx",
    "content": "import React from 'react';\nimport { Controller, useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\n\nimport i18next from 'i18next';\nimport { validateIp, validateIpv4, validateIpv6, validateRequiredValue } from '../../../../helpers/validators';\n\nimport { BLOCKING_MODES, UINT32_RANGE } from '../../../../helpers/constants';\nimport { Checkbox } from '../../../ui/Controls/Checkbox';\nimport { Input } from '../../../ui/Controls/Input';\nimport { toNumber } from '../../../../helpers/form';\nimport { Textarea } from '../../../ui/Controls/Textarea';\nimport { Radio } from '../../../ui/Controls/Radio';\n\nconst checkboxes: {\n    name: 'dnssec_enabled' | 'disable_ipv6';\n    placeholder: string;\n    subtitle: string;\n}[] = [\n    {\n        name: 'dnssec_enabled',\n        placeholder: i18next.t('dnssec_enable'),\n        subtitle: i18next.t('dnssec_enable_desc'),\n    },\n    {\n        name: 'disable_ipv6',\n        placeholder: i18next.t('disable_ipv6'),\n        subtitle: i18next.t('disable_ipv6_desc'),\n    },\n];\n\nconst customIps: {\n    name: 'blocking_ipv4' | 'blocking_ipv6';\n    label: string;\n    description: string;\n    validateIp: (value: string) => string;\n}[] = [\n    {\n        name: 'blocking_ipv4',\n        label: i18next.t('blocking_ipv4'),\n        description: i18next.t('blocking_ipv4_desc'),\n        validateIp: validateIpv4,\n    },\n    {\n        name: 'blocking_ipv6',\n        label: i18next.t('blocking_ipv6'),\n        description: i18next.t('blocking_ipv6_desc'),\n        validateIp: validateIpv6,\n    },\n];\n\nconst blockingModeOptions = [\n    {\n        value: BLOCKING_MODES.default,\n        label: i18next.t('default'),\n    },\n    {\n        value: BLOCKING_MODES.refused,\n        label: i18next.t('refused'),\n    },\n    {\n        value: BLOCKING_MODES.nxdomain,\n        label: i18next.t('nxdomain'),\n    },\n    {\n        value: BLOCKING_MODES.null_ip,\n        label: i18next.t('null_ip'),\n    },\n    {\n        value: BLOCKING_MODES.custom_ip,\n        label: i18next.t('custom_ip'),\n    },\n];\n\nconst blockingModeDescriptions = [\n    i18next.t(`blocking_mode_default`),\n    i18next.t(`blocking_mode_refused`),\n    i18next.t(`blocking_mode_nxdomain`),\n    i18next.t(`blocking_mode_null_ip`),\n    i18next.t(`blocking_mode_custom_ip`),\n];\n\ntype FormData = {\n    ratelimit: number;\n    ratelimit_subnet_len_ipv4: number;\n    ratelimit_subnet_len_ipv6: number;\n    ratelimit_whitelist: string;\n    edns_cs_enabled: boolean;\n    edns_cs_use_custom: boolean;\n    edns_cs_custom_ip?: string;\n    dnssec_enabled: boolean;\n    disable_ipv6: boolean;\n    blocking_mode: string;\n    blocking_ipv4?: string;\n    blocking_ipv6?: string;\n    blocked_response_ttl: number;\n};\n\ntype Props = {\n    processing?: boolean;\n    initialValues?: Partial<FormData>;\n    onSubmit: (data: FormData) => void;\n};\n\nconst Form = ({ processing, initialValues, onSubmit }: Props) => {\n    const { t } = useTranslation();\n\n    const {\n        handleSubmit,\n        watch,\n        control,\n        formState: { isSubmitting },\n    } = useForm<FormData>({\n        mode: 'onBlur',\n        defaultValues: initialValues,\n    });\n\n    const blocking_mode = watch('blocking_mode');\n    const edns_cs_enabled = watch('edns_cs_enabled');\n    const edns_cs_use_custom = watch('edns_cs_use_custom');\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)}>\n            <div className=\"row\">\n                <div className=\"col-12 col-md-7\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"ratelimit\"\n                            control={control}\n                            rules={{ validate: validateRequiredValue }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    data-testid=\"dns_config_ratelimit\"\n                                    type=\"number\"\n                                    label={t('rate_limit')}\n                                    desc={t('rate_limit_desc')}\n                                    error={fieldState.error?.message}\n                                    min={UINT32_RANGE.MIN}\n                                    max={UINT32_RANGE.MAX}\n                                    disabled={processing}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n\n                <div className=\"col-12 col-md-7\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"ratelimit_subnet_len_ipv4\"\n                            control={control}\n                            rules={{ validate: validateRequiredValue }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    data-testid=\"dns_config_subnet_ipv4\"\n                                    type=\"number\"\n                                    label={t('rate_limit_subnet_len_ipv4')}\n                                    desc={t('rate_limit_subnet_len_ipv4_desc')}\n                                    error={fieldState.error?.message}\n                                    min={0}\n                                    max={32}\n                                    disabled={processing}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n\n                <div className=\"col-12 col-md-7\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"ratelimit_subnet_len_ipv6\"\n                            control={control}\n                            rules={{ validate: validateRequiredValue }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    data-testid=\"dns_config_subnet_ipv6\"\n                                    type=\"number\"\n                                    label={t('rate_limit_subnet_len_ipv6')}\n                                    desc={t('rate_limit_subnet_len_ipv6_desc')}\n                                    error={fieldState.error?.message}\n                                    min={0}\n                                    max={128}\n                                    disabled={processing}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n\n                <div className=\"col-12 col-md-7\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"ratelimit_whitelist\"\n                            control={control}\n                            render={({ field, fieldState }) => (\n                                <Textarea\n                                    {...field}\n                                    data-testid=\"dns_config_subnet_ipv6\"\n                                    label={t('rate_limit_whitelist')}\n                                    desc={t('rate_limit_whitelist_desc')}\n                                    error={fieldState.error?.message}\n                                    disabled={processing}\n                                    trimOnBlur\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n\n                <div className=\"col-12\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"edns_cs_enabled\"\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox\n                                    {...field}\n                                    data-testid=\"dns_config_edns_cs_enabled\"\n                                    title={t('edns_enable')}\n                                    disabled={processing}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n\n                <div className=\"col-12 form__group form__group--inner\">\n                    <div className=\"form__group\">\n                        <Controller\n                            name=\"edns_cs_use_custom\"\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox\n                                    {...field}\n                                    data-testid=\"dns_config_edns_use_custom_ip\"\n                                    title={t('edns_use_custom_ip')}\n                                    disabled={processing || !edns_cs_enabled}\n                                />\n                            )}\n                        />\n                    </div>\n\n                    {edns_cs_use_custom && (\n                        <Controller\n                            name=\"edns_cs_custom_ip\"\n                            control={control}\n                            rules={{\n                                validate: {\n                                    required: validateRequiredValue,\n                                    id: validateIp,\n                                },\n                            }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    data-testid=\"dns_config_edns_cs_custom_ip\"\n                                    error={fieldState.error?.message}\n                                    disabled={processing || !edns_cs_enabled}\n                                />\n                            )}\n                        />\n                    )}\n                </div>\n\n                {checkboxes.map(({ name, placeholder, subtitle }) => (\n                    <div className=\"col-12\" key={name}>\n                        <div className=\"form__group form__group--settings\">\n                            <Controller\n                                name={name}\n                                control={control}\n                                render={({ field }) => (\n                                    <Checkbox\n                                        {...field}\n                                        data-testid={`dns_config_${name}`}\n                                        title={placeholder}\n                                        subtitle={subtitle}\n                                        disabled={processing}\n                                    />\n                                )}\n                            />\n                        </div>\n                    </div>\n                ))}\n\n                <div className=\"col-12\">\n                    <div className=\"form__group form__group--settings mb-4\">\n                        <label className=\"form__label form__label--with-desc\">{t('blocking_mode')}</label>\n\n                        <div className=\"form__desc form__desc--top\">\n                            {blockingModeDescriptions.map((desc: string) => (\n                                <li key={desc}>{desc}</li>\n                            ))}\n                        </div>\n\n                        <div className=\"custom-controls-stacked\">\n                            <Controller\n                                name=\"blocking_mode\"\n                                control={control}\n                                render={({ field }) => (\n                                    <Radio {...field} options={blockingModeOptions} disabled={processing} />\n                                )}\n                            />\n                        </div>\n                    </div>\n                </div>\n                {blocking_mode === BLOCKING_MODES.custom_ip && (\n                    <>\n                        {customIps.map(({ label, description, name, validateIp }) => (\n                            <div className=\"col-12 col-sm-6\" key={name}>\n                                <div className=\"form__group form__group--settings\">\n                                    <Controller\n                                        name={name}\n                                        control={control}\n                                        rules={{\n                                            validate: {\n                                                required: validateRequiredValue,\n                                                ip: validateIp,\n                                            },\n                                        }}\n                                        render={({ field, fieldState }) => (\n                                            <Input\n                                                {...field}\n                                                data-testid=\"dns_config_blocked_response_ttl\"\n                                                type=\"text\"\n                                                label={label}\n                                                desc={description}\n                                                error={fieldState.error?.message}\n                                                disabled={processing}\n                                            />\n                                        )}\n                                    />\n                                </div>\n                            </div>\n                        ))}\n                    </>\n                )}\n\n                <div className=\"col-12 col-md-7\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"blocked_response_ttl\"\n                            control={control}\n                            rules={{ validate: validateRequiredValue }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    data-testid=\"dns_config_blocked_response_ttl\"\n                                    type=\"number\"\n                                    label={t('blocked_response_ttl')}\n                                    desc={t('blocked_response_ttl_desc')}\n                                    error={fieldState.error?.message}\n                                    min={UINT32_RANGE.MIN}\n                                    max={UINT32_RANGE.MAX}\n                                    disabled={processing}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n            </div>\n\n            <button\n                type=\"submit\"\n                data-testid=\"dns_config_save\"\n                className=\"btn btn-success btn-standard btn-large\"\n                disabled={isSubmitting || processing}>\n                {t('save_btn')}\n            </button>\n        </form>\n    );\n};\n\nexport default Form;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Config/index.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport Card from '../../../ui/Card';\n\nimport Form from './Form';\nimport { setDnsConfig } from '../../../../actions/dnsConfig';\nimport { RootState } from '../../../../initialState';\n\nconst Config = () => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const {\n        blocking_mode,\n        ratelimit,\n        ratelimit_subnet_len_ipv4,\n        ratelimit_subnet_len_ipv6,\n        ratelimit_whitelist,\n        blocking_ipv4,\n        blocking_ipv6,\n        blocked_response_ttl,\n        edns_cs_enabled,\n        edns_cs_use_custom,\n        edns_cs_custom_ip,\n        dnssec_enabled,\n        disable_ipv6,\n        processingSetConfig,\n    } = useSelector((state: RootState) => state.dnsConfig, shallowEqual);\n\n    const handleFormSubmit = (values: any) => {\n        dispatch(setDnsConfig(values));\n    };\n\n    return (\n        <Card title={t('dns_config')} bodyType=\"card-body box-body--settings\" id=\"dns-config\">\n            <div className=\"form\">\n                <Form\n                    initialValues={{\n                        ratelimit,\n                        ratelimit_subnet_len_ipv4,\n                        ratelimit_subnet_len_ipv6,\n                        ratelimit_whitelist,\n                        blocking_mode,\n                        blocking_ipv4,\n                        blocking_ipv6,\n                        blocked_response_ttl,\n                        edns_cs_enabled,\n                        disable_ipv6,\n                        dnssec_enabled,\n                        edns_cs_use_custom,\n                        edns_cs_custom_ip,\n                    }}\n                    onSubmit={handleFormSubmit}\n                    processing={processingSetConfig}\n                />\n            </div>\n        </Card>\n    );\n};\n\nexport default Config;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Upstream/Examples.tsx",
    "content": "import React from 'react';\nimport { Trans, withTranslation } from 'react-i18next';\nimport { COMMENT_LINE_DEFAULT_TOKEN } from '../../../../helpers/constants';\n\ninterface ExamplesProps {\n    t: (...args: unknown[]) => string;\n}\n\nconst Examples = (props: ExamplesProps) => (\n    <div className=\"list leading-loose\">\n        <Trans>examples_title</Trans>:\n        <ol className=\"leading-loose\">\n            <li>\n                <code>94.140.14.140</code>, <code>2a10:50c0::1:ff</code>: {props.t('example_upstream_regular')}\n            </li>\n\n            <li>\n                <code>94.140.14.140:53</code>, <code>[2a10:50c0::1:ff]:53</code>:{' '}\n                {props.t('example_upstream_regular_port')}\n            </li>\n\n            <li>\n                <code>udp://unfiltered.adguard-dns.com</code>: <Trans>example_upstream_udp</Trans>\n            </li>\n\n            <li>\n                <code>tcp://94.140.14.140</code>, <code>tcp://[2a10:50c0::1:ff]</code>:{' '}\n                <Trans>example_upstream_tcp</Trans>\n            </li>\n\n            <li>\n                <code>tcp://94.140.14.140:53</code>, <code>tcp://[2a10:50c0::1:ff]:53</code>:{' '}\n                <Trans>example_upstream_tcp_port</Trans>\n            </li>\n\n            <li>\n                <code>tcp://unfiltered.adguard-dns.com</code>: <Trans>example_upstream_tcp_hostname</Trans>\n            </li>\n\n            <li>\n                <code>tls://unfiltered.adguard-dns.com</code>:{' '}\n                <Trans\n                    components={[\n                        <a\n                            href=\"https://en.wikipedia.org/wiki/DNS_over_TLS\"\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                            key=\"0\">\n                            DNS-over-TLS\n                        </a>,\n                    ]}>\n                    example_upstream_dot\n                </Trans>\n            </li>\n\n            <li>\n                <code>https://unfiltered.adguard-dns.com/dns-query</code>:{' '}\n                <Trans\n                    components={[\n                        <a\n                            href=\"https://en.wikipedia.org/wiki/DNS_over_HTTPS\"\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                            key=\"0\">\n                            DNS-over-HTTPS\n                        </a>,\n                    ]}>\n                    example_upstream_doh\n                </Trans>\n            </li>\n\n            <li>\n                <code>h3://unfiltered.adguard-dns.com/dns-query</code>:{' '}\n                <Trans\n                    components={[\n                        <a\n                            href=\"https://en.wikipedia.org/wiki/HTTP/3\"\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                            key=\"0\">\n                            HTTP/3\n                        </a>,\n                    ]}>\n                    example_upstream_doh3\n                </Trans>\n            </li>\n\n            <li>\n                <code>quic://unfiltered.adguard-dns.com</code>:{' '}\n                <Trans\n                    components={[\n                        <a\n                            href=\"https://datatracker.ietf.org/doc/html/rfc9250\"\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                            key=\"0\">\n                            DNS-over-QUIC\n                        </a>,\n                    ]}>\n                    example_upstream_doq\n                </Trans>\n            </li>\n\n            <li>\n                <code>sdns://...</code>:{' '}\n                <Trans\n                    components={[\n                        <a href=\"https://dnscrypt.info/stamps/\" target=\"_blank\" rel=\"noopener noreferrer\" key=\"0\">\n                            DNS Stamps\n                        </a>,\n\n                        <a href=\"https://dnscrypt.info/\" target=\"_blank\" rel=\"noopener noreferrer\" key=\"1\">\n                            DNSCrypt\n                        </a>,\n\n                        <a\n                            href=\"https://en.wikipedia.org/wiki/DNS_over_HTTPS\"\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                            key=\"2\">\n                            DNS-over-HTTPS\n                        </a>,\n                    ]}>\n                    example_upstream_sdns\n                </Trans>\n            </li>\n\n            <li>\n                <code>[/example.local/]94.140.14.140</code>:{' '}\n                <Trans\n                    components={[\n                        <a\n                            href=\"https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains\"\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                            key=\"0\">\n                            Link\n                        </a>,\n                    ]}>\n                    example_upstream_reserved\n                </Trans>\n            </li>\n\n            <li>\n                <code>[/example.local/]94.140.14.140 2a10:50c0::1:ff</code>:{' '}\n                <Trans\n                    components={[\n                        <a\n                            href=\"https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains\"\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                            key=\"0\">\n                            Link\n                        </a>,\n                    ]}>\n                    example_multiple_upstreams_reserved\n                </Trans>\n            </li>\n\n            <li>\n                <code>{COMMENT_LINE_DEFAULT_TOKEN} comment</code>: <Trans>example_upstream_comment</Trans>\n            </li>\n        </ol>\n    </div>\n);\n\nexport default withTranslation()(Examples);\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Upstream/Form.tsx",
    "content": "import React, { useRef } from 'react';\nimport { Controller, useForm } from 'react-hook-form';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport i18next from 'i18next';\nimport clsx from 'clsx';\nimport { testUpstreamWithFormValues } from '../../../../actions';\nimport { DNS_REQUEST_OPTIONS, UINT32_RANGE, UPSTREAM_CONFIGURATION_WIKI_LINK } from '../../../../helpers/constants';\nimport { removeEmptyLines } from '../../../../helpers/helpers';\nimport { getTextareaCommentsHighlight, syncScroll } from '../../../../helpers/highlightTextareaComments';\nimport { RootState } from '../../../../initialState';\nimport '../../../ui/texareaCommentsHighlight.css';\nimport Examples from './Examples';\nimport { Checkbox } from '../../../ui/Controls/Checkbox';\nimport { Textarea } from '../../../ui/Controls/Textarea';\nimport { Radio } from '../../../ui/Controls/Radio';\nimport { Input } from '../../../ui/Controls/Input';\nimport { validateRequiredValue } from '../../../../helpers/validators';\nimport { toNumber } from '../../../../helpers/form';\n\nconst UPSTREAM_DNS_NAME = 'upstream_dns';\n\ntype FormData = {\n    upstream_dns: string;\n    upstream_mode: string;\n    fallback_dns: string;\n    bootstrap_dns: string;\n    local_ptr_upstreams: string;\n    use_private_ptr_resolvers: boolean;\n    resolve_clients: boolean;\n    upstream_timeout: number;\n};\n\ntype FormProps = {\n    initialValues?: Partial<FormData>;\n    onSubmit: (data: FormData) => void;\n};\n\nconst upstreamModeOptions = [\n    {\n        label: i18next.t('load_balancing'),\n        desc: <Trans components={{ br: <br />, b: <b /> }}>load_balancing_desc</Trans>,\n        value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,\n    },\n    {\n        label: i18next.t('parallel_requests'),\n        desc: <Trans components={{ br: <br />, b: <b /> }}>upstream_parallel</Trans>,\n        value: DNS_REQUEST_OPTIONS.PARALLEL,\n    },\n    {\n        label: i18next.t('fastest_addr'),\n        desc: <Trans components={{ br: <br />, b: <b /> }}>fastest_addr_desc</Trans>,\n        value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,\n    },\n];\n\nconst Form = ({ initialValues, onSubmit }: FormProps) => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n    const {\n        control,\n        handleSubmit,\n        watch,\n        formState: { isSubmitting },\n    } = useForm<FormData>({\n        mode: 'onBlur',\n        defaultValues: {\n            upstream_dns: initialValues?.upstream_dns || '',\n            upstream_mode: initialValues?.upstream_mode || DNS_REQUEST_OPTIONS.LOAD_BALANCING,\n            fallback_dns: initialValues?.fallback_dns || '',\n            bootstrap_dns: initialValues?.bootstrap_dns || '',\n            local_ptr_upstreams: initialValues?.local_ptr_upstreams || '',\n            use_private_ptr_resolvers: initialValues?.use_private_ptr_resolvers || false,\n            resolve_clients: initialValues?.resolve_clients || false,\n            upstream_timeout: initialValues?.upstream_timeout || 0,\n        },\n    });\n\n    const upstream_dns = watch('upstream_dns');\n    const processingTestUpstream = useSelector((state: RootState) => state.settings.processingTestUpstream);\n    const processingSetConfig = useSelector((state: RootState) => state.dnsConfig.processingSetConfig);\n    const defaultLocalPtrUpstreams = useSelector((state: RootState) => state.dnsConfig.default_local_ptr_upstreams);\n    const upstream_dns_file = useSelector((state: RootState) => state.dnsConfig.upstream_dns_file);\n\n    const handleUpstreamTest = () => {\n        const formValues = {\n            bootstrap_dns: watch('bootstrap_dns'),\n            upstream_dns: watch('upstream_dns'),\n            local_ptr_upstreams: watch('local_ptr_upstreams'),\n            fallback_dns: watch('fallback_dns'),\n        };\n        dispatch(testUpstreamWithFormValues(formValues));\n    };\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)} className=\"form--upstream\">\n            <div className=\"row\">\n                <label className=\"col form__label\" htmlFor=\"upstream_dns\">\n                    <Trans\n                        components={{\n                            a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target=\"_blank\" rel=\"noopener noreferrer\" />,\n                        }}>\n                        upstream_dns_help\n                    </Trans>{' '}\n                    <Trans\n                        components={[\n                            <a\n                                href=\"https://link.adtidy.org/forward.html?action=dns_kb_providers&from=ui&app=home\"\n                                target=\"_blank\"\n                                rel=\"noopener noreferrer\"\n                                key=\"0\">\n                                DNS providers\n                            </a>,\n                        ]}>\n                        dns_providers\n                    </Trans>\n                </label>\n\n                <div className=\"col-12 mb-4\">\n                    <div className=\"text-edit-container\">\n                        <Controller\n                            name=\"upstream_dns\"\n                            control={control}\n                            render={({ field }) => (\n                                <>\n                                    <Textarea\n                                        {...field}\n                                        id={UPSTREAM_DNS_NAME}\n                                        data-testid=\"upstream_dns\"\n                                        className=\"form-control--textarea-large text-input\"\n                                        wrapperClassName=\"mb-0\"\n                                        placeholder={t('upstream_dns')}\n                                        disabled={!!upstream_dns_file || processingSetConfig || processingTestUpstream}\n                                        onScroll={(e) => syncScroll(e, textareaRef)}\n                                        trimOnBlur\n                                    />\n                                    {getTextareaCommentsHighlight(textareaRef, upstream_dns)}\n                                </>\n                            )}\n                        />\n                    </div>\n                </div>\n\n                <div className=\"col-12\">\n                    <Examples />\n                    <hr />\n                </div>\n\n                <div className=\"col-12 mb-4\">\n                    <Controller\n                        name=\"upstream_mode\"\n                        control={control}\n                        render={({ field }) => (\n                            <Radio\n                                {...field}\n                                options={upstreamModeOptions}\n                                disabled={processingSetConfig || processingTestUpstream}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"col-12\">\n                    <label className=\"form__label form__label--with-desc\" htmlFor=\"fallback_dns\">\n                        {t('fallback_dns_title')}\n                    </label>\n\n                    <div className=\"form__desc form__desc--top\">{t('fallback_dns_desc')}</div>\n\n                    <Controller\n                        name=\"fallback_dns\"\n                        control={control}\n                        render={({ field }) => (\n                            <Textarea\n                                {...field}\n                                id=\"fallback_dns\"\n                                data-testid=\"fallback_dns\"\n                                wrapperClassName=\"mb-0\"\n                                placeholder={t('fallback_dns_placeholder')}\n                                disabled={processingSetConfig}\n                                trimOnBlur\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"col-12\">\n                    <hr />\n                </div>\n\n                <div className=\"col-12\">\n                    <label className=\"form__label form__label--with-desc\" htmlFor=\"bootstrap_dns\">\n                        {t('bootstrap_dns')}\n                    </label>\n\n                    <div className=\"form__desc form__desc--top\">{t('bootstrap_dns_desc')}</div>\n\n                    <Controller\n                        name=\"bootstrap_dns\"\n                        control={control}\n                        render={({ field }) => (\n                            <Textarea\n                                {...field}\n                                id=\"bootstrap_dns\"\n                                data-testid=\"bootstrap_dns\"\n                                placeholder={t('bootstrap_dns')}\n                                wrapperClassName=\"mb-0\"\n                                disabled={processingSetConfig}\n                                onBlur={(e) => {\n                                    const value = removeEmptyLines(e.target.value);\n                                    field.onChange(value);\n                                }}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"col-12\">\n                    <hr />\n                </div>\n\n                <div className=\"col-12\">\n                    <label className=\"form__label form__label--with-desc\" htmlFor=\"local_ptr\">\n                        {t('local_ptr_title')}\n                    </label>\n\n                    <div className=\"form__desc form__desc--top\">{t('local_ptr_desc')}</div>\n\n                    <div className=\"form__desc form__desc--top\">\n                        {defaultLocalPtrUpstreams?.length > 0\n                            ? t('local_ptr_default_resolver', {\n                                  ip: defaultLocalPtrUpstreams.map((s: any) => `\"${s}\"`).join(', '),\n                              })\n                            : t('local_ptr_no_default_resolver')}\n                    </div>\n\n                    <Controller\n                        name=\"local_ptr_upstreams\"\n                        control={control}\n                        render={({ field }) => (\n                            <Textarea\n                                {...field}\n                                id=\"local_ptr_upstreams\"\n                                data-testid=\"local_ptr_upstreams\"\n                                placeholder={t('local_ptr_placeholder')}\n                                disabled={processingSetConfig}\n                                trimOnBlur\n                            />\n                        )}\n                    />\n\n                    <div className=\"mt-4\">\n                        <Controller\n                            name=\"use_private_ptr_resolvers\"\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox\n                                    {...field}\n                                    data-testid=\"dns_use_private_ptr_resolvers\"\n                                    title={t('use_private_ptr_resolvers_title')}\n                                    subtitle={t('use_private_ptr_resolvers_desc')}\n                                    disabled={processingSetConfig}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n\n                <div className=\"col-12\">\n                    <hr />\n                </div>\n\n                <div className=\"col-12 mb-4\">\n                    <Controller\n                        name=\"resolve_clients\"\n                        control={control}\n                        render={({ field }) => (\n                            <Checkbox\n                                {...field}\n                                data-testid=\"dns_resolve_clients\"\n                                title={t('resolve_clients_title')}\n                                subtitle={t('resolve_clients_desc')}\n                                disabled={processingSetConfig}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"col-12\">\n                    <hr />\n                </div>\n\n                <div className=\"col-12 col-md-7\">\n                    <div className=\"form__group\">\n                        <label htmlFor=\"upstream_timeout\" className=\"form__label form__label--with-desc\">\n                            <Trans>upstream_timeout</Trans>\n                        </label>\n\n                        <div className=\"form__desc form__desc--top\">\n                            <Trans>upstream_timeout_desc</Trans>\n                        </div>\n\n                        <Controller\n                            name=\"upstream_timeout\"\n                            control={control}\n                            rules={{ validate: validateRequiredValue }}\n                            render={({ field }) => (\n                                <Input\n                                    {...field}\n                                    type=\"number\"\n                                    id=\"upstream_timeout\"\n                                    data-testid=\"upstream_timeout\"\n                                    placeholder={t('form_enter_upstream_timeout')}\n                                    disabled={processingSetConfig}\n                                    min={1}\n                                    max={UINT32_RANGE.MAX}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                />\n                            )}\n                        />\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"card-actions\">\n                <div className=\"btn-list\">\n                    <button\n                        type=\"button\"\n                        data-testid=\"dns_upstream_test\"\n                        className={clsx('btn btn-primary btn-standard mr-2', {\n                            'btn-loading': processingTestUpstream,\n                        })}\n                        onClick={handleUpstreamTest}\n                        disabled={!upstream_dns || processingTestUpstream}>\n                        {t('test_upstream_btn')}\n                    </button>\n\n                    <button\n                        type=\"submit\"\n                        data-testid=\"dns_upstream_save\"\n                        className=\"btn btn-success btn-standard\"\n                        disabled={isSubmitting || processingSetConfig || processingTestUpstream}>\n                        {t('apply_btn')}\n                    </button>\n                </div>\n            </div>\n        </form>\n    );\n};\n\nexport default Form;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/Upstream/index.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport Form from './Form';\n\nimport Card from '../../../ui/Card';\nimport { setDnsConfig } from '../../../../actions/dnsConfig';\nimport { RootState } from '../../../../initialState';\n\nconst Upstream = () => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n    const {\n        upstream_dns,\n        fallback_dns,\n        bootstrap_dns,\n        upstream_mode,\n        resolve_clients,\n        local_ptr_upstreams,\n        use_private_ptr_resolvers,\n        upstream_timeout,\n    } = useSelector((state: RootState) => state.dnsConfig, shallowEqual);\n\n    const upstream_dns_file = useSelector((state: RootState) => state.dnsConfig.upstream_dns_file);\n\n    const handleSubmit = (values: any) => {\n        const {\n            fallback_dns,\n            bootstrap_dns,\n            upstream_dns,\n            upstream_mode,\n            resolve_clients,\n            local_ptr_upstreams,\n            use_private_ptr_resolvers,\n            upstream_timeout,\n        } = values;\n\n        const dnsConfig = {\n            fallback_dns,\n            bootstrap_dns,\n            upstream_mode,\n            resolve_clients,\n            local_ptr_upstreams,\n            use_private_ptr_resolvers,\n            upstream_timeout,\n            ...(upstream_dns_file ? null : { upstream_dns }),\n        };\n\n        dispatch(setDnsConfig(dnsConfig));\n    };\n\n    const upstreamDns = upstream_dns_file\n        ? t('upstream_dns_configured_in_file', { path: upstream_dns_file })\n        : upstream_dns;\n\n    return (\n        <Card title={t('upstream_dns')} bodyType=\"card-body box-body--settings\">\n            <div className=\"row\">\n                <div className=\"col\">\n                    <Form\n                        initialValues={{\n                            upstream_dns: upstreamDns,\n                            fallback_dns,\n                            bootstrap_dns,\n                            upstream_mode,\n                            resolve_clients,\n                            local_ptr_upstreams,\n                            use_private_ptr_resolvers,\n                            upstream_timeout,\n                        }}\n                        onSubmit={handleSubmit}\n                    />\n                </div>\n            </div>\n        </Card>\n    );\n};\n\nexport default Upstream;\n"
  },
  {
    "path": "client/src/components/Settings/Dns/index.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport Upstream from './Upstream';\n\nimport Access from './Access';\n\nimport Config from './Config';\n\nimport PageTitle from '../../ui/PageTitle';\n\nimport Loading from '../../ui/Loading';\n\nimport CacheConfig from './Cache';\nimport { getDnsConfig } from '../../../actions/dnsConfig';\nimport { getAccessList } from '../../../actions/access';\nimport { RootState } from '../../../initialState';\n\nconst Dns = () => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n\n    const processing = useSelector((state: RootState) => state.access.processing);\n\n    const processingGetConfig = useSelector((state: RootState) => state.dnsConfig.processingGetConfig);\n\n    const isDataLoading = processing || processingGetConfig;\n\n    useEffect(() => {\n        dispatch(getAccessList());\n        dispatch(getDnsConfig());\n    }, []);\n\n    return (\n        <>\n            <PageTitle title={t('dns_settings')} />\n            {isDataLoading ? (\n                <Loading />\n            ) : (\n                <>\n                    <Upstream />\n\n                    <Config />\n\n                    <CacheConfig />\n\n                    <Access />\n                </>\n            )}\n        </>\n    );\n};\n\nexport default Dns;\n"
  },
  {
    "path": "client/src/components/Settings/Encryption/CertificateStatus.tsx",
    "content": "import React, { Fragment } from 'react';\nimport { withTranslation, Trans } from 'react-i18next';\nimport format from 'date-fns/format';\n\nimport { EMPTY_DATE } from '../../../helpers/constants';\n\ninterface CertificateStatusProps {\n    validChain: boolean;\n    validCert: boolean;\n    subject?: string;\n    issuer?: string;\n    notAfter?: string;\n    dnsNames?: string[];\n}\n\nconst CertificateStatus = ({ validChain, validCert, subject, issuer, notAfter, dnsNames }: CertificateStatusProps) => (\n    <Fragment>\n        <div className=\"form__label form__label--bold\">\n            <Trans>encryption_status</Trans>:\n        </div>\n\n        <ul className=\"encryption__list\">\n            <li className={validChain ? 'text-success' : 'text-danger'}>\n                {validChain ? <Trans>encryption_chain_valid</Trans> : <Trans>encryption_chain_invalid</Trans>}\n            </li>\n            {validCert && (\n                <Fragment>\n                    {subject && (\n                        <li>\n                            <Trans>encryption_subject</Trans>:&nbsp;\n                            {subject}\n                        </li>\n                    )}\n                    {issuer && (\n                        <li>\n                            <Trans>encryption_issuer</Trans>:&nbsp;\n                            {issuer}\n                        </li>\n                    )}\n                    {notAfter && notAfter !== EMPTY_DATE && (\n                        <li>\n                            <Trans>encryption_expire</Trans>:&nbsp;\n                            {format(notAfter, 'YYYY-MM-DD HH:mm:ss')}\n                        </li>\n                    )}\n                    {dnsNames && (\n                        <li>\n                            <Trans>encryption_hostnames</Trans>:&nbsp;\n                            {dnsNames.join(', ')}\n                        </li>\n                    )}\n                </Fragment>\n            )}\n        </ul>\n    </Fragment>\n);\n\nexport default withTranslation()(CertificateStatus);\n"
  },
  {
    "path": "client/src/components/Settings/Encryption/Form.tsx",
    "content": "import React from 'react';\n\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport { Controller, useForm } from 'react-hook-form';\nimport i18next from 'i18next';\nimport {\n    validateServerName,\n    validateIsSafePort,\n    validatePort,\n    validatePortQuic,\n    validatePortTLS,\n    validatePlainDns,\n} from '../../../helpers/validators';\n\nimport KeyStatus from './KeyStatus';\n\nimport CertificateStatus from './CertificateStatus';\nimport {\n    DNS_OVER_QUIC_PORT,\n    DNS_OVER_TLS_PORT,\n    STANDARD_HTTPS_PORT,\n    ENCRYPTION_SOURCE,\n} from '../../../helpers/constants';\nimport { Checkbox } from '../../ui/Controls/Checkbox';\nimport { Radio } from '../../ui/Controls/Radio';\nimport { Input } from '../../ui/Controls/Input';\nimport { Textarea } from '../../ui/Controls/Textarea';\nimport { EncryptionData } from '../../../initialState';\nimport { toNumber } from '../../../helpers/form';\n\nconst certificateSourceOptions = [\n    {\n        label: i18next.t('encryption_certificates_source_path'),\n        value: ENCRYPTION_SOURCE.PATH,\n    },\n    {\n        label: i18next.t('encryption_certificates_source_content'),\n        value: ENCRYPTION_SOURCE.CONTENT,\n    },\n];\n\nconst keySourceOptions = [\n    {\n        label: i18next.t('encryption_key_source_path'),\n        value: ENCRYPTION_SOURCE.PATH,\n    },\n    {\n        label: i18next.t('encryption_key_source_content'),\n        value: ENCRYPTION_SOURCE.CONTENT,\n    },\n];\n\nconst validationMessage = (warningValidation: string, isWarning: boolean) => {\n    if (!warningValidation) {\n        return null;\n    }\n\n    if (isWarning) {\n        return (\n            <div className=\"col-12\">\n                <p>\n                    <Trans>encryption_warning</Trans>: {warningValidation}\n                </p>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"col-12\">\n            <p className=\"text-danger\">{warningValidation}</p>\n        </div>\n    );\n};\n\nexport type EncryptionFormValues = {\n    enabled?: boolean;\n    serve_plain_dns?: boolean;\n    server_name?: string;\n    force_https?: boolean;\n    port_https?: number;\n    port_dns_over_tls?: number;\n    port_dns_over_quic?: number;\n    certificate_chain?: string;\n    private_key?: string;\n    certificate_path?: string;\n    private_key_path?: string;\n    certificate_source?: string;\n    key_source?: string;\n    private_key_saved?: boolean;\n};\n\ntype Props = {\n    initialValues: EncryptionFormValues;\n    encryption: EncryptionData;\n    onSubmit: (values: EncryptionFormValues) => void;\n    debouncedConfigValidation: (values: EncryptionFormValues) => void;\n    setTlsConfig: (values: Partial<EncryptionData>) => void;\n    validateTlsConfig: (values: Partial<EncryptionData>) => void;\n};\n\nconst defaultValues = {\n    enabled: false,\n    serve_plain_dns: true,\n    server_name: '',\n    force_https: false,\n    port_https: STANDARD_HTTPS_PORT,\n    port_dns_over_tls: DNS_OVER_TLS_PORT,\n    port_dns_over_quic: DNS_OVER_QUIC_PORT,\n    certificate_chain: '',\n    private_key: '',\n    certificate_path: '',\n    private_key_path: '',\n    certificate_source: ENCRYPTION_SOURCE.PATH,\n    key_source: ENCRYPTION_SOURCE.PATH,\n    private_key_saved: false,\n};\n\nexport const Form = ({\n    initialValues,\n    encryption,\n    onSubmit,\n    setTlsConfig,\n    debouncedConfigValidation,\n    validateTlsConfig,\n}: Props) => {\n    const { t } = useTranslation();\n\n    const {\n        not_after,\n        valid_chain,\n        valid_key,\n        valid_cert,\n        valid_pair,\n        dns_names,\n        key_type,\n        issuer,\n        subject,\n        warning_validation,\n        processingConfig,\n        processingValidate,\n    } = encryption;\n\n    const {\n        control,\n        handleSubmit,\n        watch,\n        reset,\n        setValue,\n        setError,\n        getValues,\n        formState: { isSubmitting, isValid },\n    } = useForm<EncryptionFormValues>({\n        defaultValues: {\n            ...defaultValues,\n            ...initialValues,\n        },\n        mode: 'onBlur',\n    });\n\n    const {\n        enabled: isEnabled,\n        serve_plain_dns: servePlainDns,\n        certificate_chain: certificateChain,\n        private_key: privateKey,\n        private_key_path: privateKeyPath,\n        key_source: privateKeySource,\n        private_key_saved: privateKeySaved,\n        certificate_path: certificatePath,\n        certificate_source: certificateSource,\n    } = watch();\n\n    const handleBlur = () => {\n        debouncedConfigValidation(getValues());\n    };\n\n    const isSavingDisabled = () => {\n        const processing = isSubmitting || processingConfig || processingValidate;\n\n        if (servePlainDns && !isEnabled) {\n            return !isValid || processing;\n        }\n\n        return !isValid || processing || !valid_key || !valid_cert || !valid_pair;\n    };\n\n    const clearFields = () => {\n        if (window.confirm(t('encryption_reset'))) {\n            reset();\n            setTlsConfig(defaultValues);\n            validateTlsConfig(defaultValues);\n        }\n    };\n\n    const validatePorts = (values: EncryptionFormValues) => {\n        const errors: { port_dns_over_tls?: string; port_https?: string } = {};\n\n        if (values.port_dns_over_tls && values.port_https) {\n            if (values.port_dns_over_tls === values.port_https) {\n                errors.port_dns_over_tls = i18next.t('form_error_equal');\n                errors.port_https = i18next.t('form_error_equal');\n            }\n        }\n\n        return errors;\n    };\n\n    const onFormSubmit = (data: EncryptionFormValues) => {\n        const validationErrors = validatePorts(data);\n\n        if (Object.keys(validationErrors).length > 0) {\n            Object.entries(validationErrors).forEach(([field, message]) => {\n                setError(field as keyof EncryptionFormValues, { type: 'manual', message });\n            });\n        } else {\n            onSubmit(data);\n        }\n    };\n\n    const isDisabled = isSavingDisabled();\n    const isWarning = valid_key && valid_cert && valid_pair;\n\n    return (\n        <form onSubmit={handleSubmit(onFormSubmit)}>\n            <div className=\"row\">\n                <div className=\"col-12\">\n                    <div className=\"form__group form__group--settings mb-3\">\n                        <Controller\n                            name=\"enabled\"\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox {...field} title={t('encryption_enable')} onBlur={handleBlur} />\n                            )}\n                        />\n                    </div>\n\n                    <div className=\"form__desc\">\n                        <Trans>encryption_enable_desc</Trans>\n                    </div>\n\n                    <div className=\"form__group mb-3 mt-5\">\n                        <Controller\n                            name=\"serve_plain_dns\"\n                            control={control}\n                            rules={{\n                                validate: (value) => validatePlainDns(value, getValues()),\n                            }}\n                            render={({ field }) => <Checkbox {...field} title={t('encryption_plain_dns_enable')} />}\n                        />\n                    </div>\n\n                    <div className=\"form__desc\">\n                        <Trans>encryption_plain_dns_desc</Trans>\n                    </div>\n\n                    <hr />\n                </div>\n\n                <div className=\"col-12\">\n                    <label className=\"form__label\" htmlFor=\"server_name\">\n                        <Trans>encryption_server</Trans>\n                    </label>\n                </div>\n\n                <div className=\"col-lg-6\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"server_name\"\n                            control={control}\n                            rules={{ validate: validateServerName }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"text\"\n                                    placeholder={t('encryption_server_enter')}\n                                    error={fieldState.error?.message}\n                                    disabled={!isEnabled}\n                                    onBlur={handleBlur}\n                                />\n                            )}\n                        />\n\n                        <div className=\"form__desc\">\n                            <Trans>encryption_server_desc</Trans>\n                        </div>\n                    </div>\n                </div>\n\n                <div className=\"col-lg-6\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"force_https\"\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox {...field} title={t('encryption_redirect')} disabled={!isEnabled} />\n                            )}\n                        />\n\n                        <div className=\"form__desc\">\n                            <Trans>encryption_redirect_desc</Trans>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"row\">\n                <div className=\"col-lg-6\">\n                    <div className=\"form__group form__group--settings\">\n                        <label className=\"form__label\" htmlFor=\"port_https\">\n                            <Trans>encryption_https</Trans>\n                        </label>\n\n                        <Controller\n                            name=\"port_https\"\n                            control={control}\n                            rules={{ validate: { validatePort, validateIsSafePort } }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"number\"\n                                    placeholder={t('encryption_https')}\n                                    error={fieldState.error?.message}\n                                    disabled={!isEnabled}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                    onBlur={handleBlur}\n                                />\n                            )}\n                        />\n\n                        <div className=\"form__desc\">\n                            <Trans>encryption_https_desc</Trans>\n                        </div>\n                    </div>\n                </div>\n\n                <div className=\"col-lg-6\">\n                    <div className=\"form__group form__group--settings\">\n                        <label className=\"form__label\" htmlFor=\"port_dns_over_tls\">\n                            <Trans>encryption_dot</Trans>\n                        </label>\n\n                        <Controller\n                            name=\"port_dns_over_tls\"\n                            control={control}\n                            rules={{ validate: validatePortTLS }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"number\"\n                                    placeholder={t('encryption_dot')}\n                                    error={fieldState.error?.message}\n                                    disabled={!isEnabled}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                    onBlur={handleBlur}\n                                />\n                            )}\n                        />\n\n                        <div className=\"form__desc\">\n                            <Trans>encryption_dot_desc</Trans>\n                        </div>\n                    </div>\n                </div>\n\n                <div className=\"col-lg-6\">\n                    <div className=\"form__group form__group--settings\">\n                        <label className=\"form__label\" htmlFor=\"port_dns_over_quic\">\n                            <Trans>encryption_doq</Trans>\n                        </label>\n\n                        <Controller\n                            name=\"port_dns_over_quic\"\n                            control={control}\n                            rules={{ validate: validatePortQuic }}\n                            render={({ field, fieldState }) => (\n                                <Input\n                                    {...field}\n                                    type=\"number\"\n                                    placeholder={t('encryption_doq')}\n                                    error={fieldState.error?.message}\n                                    disabled={!isEnabled}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}\n                                    onBlur={handleBlur}\n                                />\n                            )}\n                        />\n\n                        <div className=\"form__desc\">\n                            <Trans>encryption_doq_desc</Trans>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"row\">\n                <div className=\"col-12\">\n                    <div className=\"form__group form__group--settings\">\n                        <label\n                            className=\"form__label form__label--with-desc form__label--bold\"\n                            htmlFor=\"certificate_chain\">\n                            <Trans>encryption_certificates</Trans>\n                        </label>\n\n                        <div className=\"form__desc form__desc--top\">\n                            <Trans\n                                values={{ link: 'letsencrypt.org' }}\n                                components={[\n                                    <a\n                                        target=\"_blank\"\n                                        rel=\"noopener noreferrer\"\n                                        href=\"https://letsencrypt.org/\"\n                                        key=\"0\">\n                                        link\n                                    </a>,\n                                ]}>\n                                encryption_certificates_desc\n                            </Trans>\n                        </div>\n\n                        <div className=\"form__inline mb-2\">\n                            <div className=\"custom-controls-stacked\">\n                                <Controller\n                                    name=\"certificate_source\"\n                                    control={control}\n                                    render={({ field }) => (\n                                        <Radio {...field} options={certificateSourceOptions} disabled={!isEnabled} />\n                                    )}\n                                />\n                            </div>\n                        </div>\n\n                        {certificateSource === ENCRYPTION_SOURCE.CONTENT ? (\n                            <Controller\n                                name=\"certificate_chain\"\n                                control={control}\n                                render={({ field, fieldState }) => (\n                                    <Textarea\n                                        {...field}\n                                        placeholder={t('encryption_certificates_input')}\n                                        disabled={!isEnabled}\n                                        error={fieldState.error?.message}\n                                        onBlur={handleBlur}\n                                    />\n                                )}\n                            />\n                        ) : (\n                            <Controller\n                                name=\"certificate_path\"\n                                control={control}\n                                render={({ field, fieldState }) => (\n                                    <Input\n                                        {...field}\n                                        type=\"text\"\n                                        placeholder={t('encryption_certificate_path')}\n                                        error={fieldState.error?.message}\n                                        disabled={!isEnabled}\n                                        onBlur={handleBlur}\n                                    />\n                                )}\n                            />\n                        )}\n                    </div>\n\n                    <div className=\"form__status\">\n                        {(certificateChain || certificatePath) && (\n                            <CertificateStatus\n                                validChain={valid_chain}\n                                validCert={valid_cert}\n                                subject={subject}\n                                issuer={issuer}\n                                notAfter={not_after}\n                                dnsNames={dns_names}\n                            />\n                        )}\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"row\">\n                <div className=\"col-12\">\n                    <div className=\"form__group form__group--settings mt-3\">\n                        <label className=\"form__label form__label--bold\" htmlFor=\"private_key\">\n                            <Trans>encryption_key</Trans>\n                        </label>\n\n                        <div className=\"form__inline mb-2\">\n                            <div className=\"custom-controls-stacked\">\n                                <Controller\n                                    name=\"key_source\"\n                                    control={control}\n                                    render={({ field }) => (\n                                        <Radio {...field} options={keySourceOptions} disabled={!isEnabled} />\n                                    )}\n                                />\n                            </div>\n                        </div>\n\n                        {privateKeySource === ENCRYPTION_SOURCE.CONTENT ? (\n                            <>\n                                <Controller\n                                    name=\"private_key_saved\"\n                                    control={control}\n                                    render={({ field }) => (\n                                        <Checkbox\n                                            {...field}\n                                            title={t('use_saved_key')}\n                                            disabled={!isEnabled}\n                                            onChange={(checked: boolean) => {\n                                                if (checked) {\n                                                    setValue('private_key', '');\n                                                }\n                                                field.onChange(checked);\n                                            }}\n                                            onBlur={handleBlur}\n                                        />\n                                    )}\n                                />\n\n                                <Controller\n                                    name=\"private_key\"\n                                    control={control}\n                                    render={({ field, fieldState }) => (\n                                        <Textarea\n                                            {...field}\n                                            placeholder={t('encryption_key_input')}\n                                            disabled={!isEnabled || privateKeySaved}\n                                            error={fieldState.error?.message}\n                                            onBlur={handleBlur}\n                                        />\n                                    )}\n                                />\n                            </>\n                        ) : (\n                            <Controller\n                                name=\"private_key_path\"\n                                control={control}\n                                render={({ field, fieldState }) => (\n                                    <Input\n                                        {...field}\n                                        type=\"text\"\n                                        placeholder={t('encryption_private_key_path')}\n                                        error={fieldState.error?.message}\n                                        disabled={!isEnabled}\n                                        onBlur={handleBlur}\n                                    />\n                                )}\n                            />\n                        )}\n                    </div>\n\n                    <div className=\"form__status\">\n                        {(privateKey || privateKeyPath) && <KeyStatus validKey={valid_key} keyType={key_type} />}\n                    </div>\n                </div>\n                {validationMessage(warning_validation, isWarning)}\n            </div>\n\n            <div className=\"btn-list mt-2\">\n                <button type=\"submit\" disabled={isDisabled} className=\"btn btn-success btn-standart\">\n                    <Trans>save_config</Trans>\n                </button>\n\n                <button\n                    type=\"button\"\n                    className=\"btn btn-secondary btn-standart\"\n                    disabled={isSubmitting || processingConfig}\n                    onClick={clearFields}>\n                    <Trans>reset_settings</Trans>\n                </button>\n            </div>\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/Encryption/KeyStatus.tsx",
    "content": "import React, { Fragment } from 'react';\nimport { withTranslation, Trans } from 'react-i18next';\n\ninterface KeyStatusProps {\n    validKey: boolean;\n    keyType: string;\n}\n\nconst KeyStatus = ({ validKey, keyType }: KeyStatusProps) => (\n    <Fragment>\n        <div className=\"form__label form__label--bold\">\n            <Trans>encryption_status</Trans>:\n        </div>\n\n        <ul className=\"encryption__list\">\n            <li className={validKey ? 'text-success' : 'text-danger'}>\n                {validKey ? (\n                    <Trans values={{ type: keyType }}>encryption_key_valid</Trans>\n                ) : (\n                    <Trans values={{ type: keyType }}>encryption_key_invalid</Trans>\n                )}\n            </li>\n        </ul>\n    </Fragment>\n);\n\nexport default withTranslation()(KeyStatus);\n"
  },
  {
    "path": "client/src/components/Settings/Encryption/index.tsx",
    "content": "import React, { useCallback, useMemo } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { debounce } from 'lodash';\nimport { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';\n\nimport { EncryptionFormValues, Form } from './Form';\nimport Card from '../../ui/Card';\nimport PageTitle from '../../ui/PageTitle';\nimport Loading from '../../ui/Loading';\nimport { EncryptionData } from '../../../initialState';\n\ntype Props = {\n    encryption: EncryptionData;\n    setTlsConfig: (values: Partial<EncryptionData>) => void;\n    validateTlsConfig: (values: Partial<EncryptionData>) => void;\n};\n\nexport const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Props) => {\n    const { t } = useTranslation();\n\n    const initialValues = useMemo((): EncryptionFormValues => {\n        const {\n            enabled,\n            serve_plain_dns,\n            server_name,\n            force_https,\n            port_https,\n            port_dns_over_tls,\n            port_dns_over_quic,\n            certificate_chain,\n            private_key,\n            certificate_path,\n            private_key_path,\n            private_key_saved,\n        } = encryption;\n        const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;\n        const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;\n\n        return {\n            enabled,\n            serve_plain_dns,\n            server_name,\n            force_https,\n            port_https,\n            port_dns_over_tls,\n            port_dns_over_quic,\n            certificate_chain,\n            private_key,\n            certificate_path,\n            private_key_path,\n            private_key_saved,\n            certificate_source,\n            key_source,\n        };\n    }, [encryption]);\n\n    const getSubmitValues = useCallback((values: any) => {\n        const { certificate_source, key_source, private_key_saved, ...config } = values;\n\n        if (certificate_source === ENCRYPTION_SOURCE.PATH) {\n            config.certificate_chain = '';\n        } else {\n            config.certificate_path = '';\n        }\n\n        if (key_source === ENCRYPTION_SOURCE.PATH) {\n            config.private_key = '';\n        } else {\n            config.private_key_path = '';\n\n            if (private_key_saved) {\n                config.private_key = '';\n                config.private_key_saved = private_key_saved;\n            }\n        }\n\n        return config;\n    }, []);\n\n    const handleFormSubmit = useCallback(\n        (values: any) => {\n            const submitValues = getSubmitValues(values);\n            setTlsConfig(submitValues);\n        },\n        [getSubmitValues, setTlsConfig],\n    );\n\n    const validateConfig = useCallback((values) => {\n        const submitValues = getSubmitValues(values);\n\n        if (submitValues.enabled) {\n            validateTlsConfig(submitValues);\n        }\n    }, []);\n\n    const debouncedConfigValidation = useMemo(() => debounce(validateConfig, DEBOUNCE_TIMEOUT), [validateConfig]);\n\n    return (\n        <div className=\"encryption\">\n            <PageTitle title={t('encryption_settings')} />\n\n            {encryption.processing ? (\n                <Loading />\n            ) : (\n                <Card\n                    title={t('encryption_title')}\n                    subtitle={t('encryption_desc')}\n                    bodyType=\"card-body box-body--settings\">\n                    <Form\n                        initialValues={initialValues}\n                        onSubmit={handleFormSubmit}\n                        debouncedConfigValidation={debouncedConfigValidation}\n                        setTlsConfig={setTlsConfig}\n                        validateTlsConfig={validateTlsConfig}\n                        encryption={encryption}\n                    />\n                </Card>\n            )}\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/FiltersConfig/index.tsx",
    "content": "import React, { useEffect, useRef } from 'react';\nimport { Controller, useForm } from 'react-hook-form';\nimport { Trans, useTranslation } from 'react-i18next';\n\nimport i18next from 'i18next';\nimport { toNumber } from '../../../helpers/form';\nimport { DAY, FILTERS_INTERVALS_HOURS, FILTERS_RELATIVE_LINK } from '../../../helpers/constants';\nimport { Checkbox } from '../../ui/Controls/Checkbox';\nimport { Select } from '../../ui/Controls/Select';\n\nconst THREE_DAYS_INTERVAL = DAY * 3;\nconst SEVEN_DAYS_INTERVAL = DAY * 7;\n\nconst getTitleForInterval = (interval: number) => {\n    if (interval === 0) {\n        return i18next.t('disabled');\n    }\n\n    if (interval === THREE_DAYS_INTERVAL || interval === SEVEN_DAYS_INTERVAL) {\n        return i18next.t('interval_days', { count: interval / DAY });\n    }\n\n    return i18next.t('interval_hours', { count: interval });\n};\n\nexport type FormValues = {\n    enabled: boolean;\n    interval: number;\n};\n\ntype Props = {\n    initialValues: FormValues;\n    setFiltersConfig: (values: FormValues) => void;\n    processing: boolean;\n};\n\nexport const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: Props) => {\n    const { t } = useTranslation();\n    const prevFormValuesRef = useRef<FormValues>(initialValues);\n\n    const { watch, control } = useForm({\n        mode: 'onBlur',\n        defaultValues: initialValues,\n    });\n\n    const formValues = watch();\n\n    useEffect(() => {\n        const prevFormValues = prevFormValuesRef.current;\n\n        if (JSON.stringify(prevFormValues) !== JSON.stringify(formValues)) {\n            setFiltersConfig(formValues);\n            prevFormValuesRef.current = formValues;\n        }\n    }, [formValues]);\n\n    const components = {\n        a: <a href={FILTERS_RELATIVE_LINK} rel=\"noopener noreferrer\" />,\n    };\n\n    return (\n        <>\n            <div className=\"row\">\n                <div className=\"col-12\">\n                    <div className=\"form__group form__group--settings\">\n                        <Controller\n                            name=\"enabled\"\n                            control={control}\n                            render={({ field }) => (\n                                <Checkbox\n                                    {...field}\n                                    data-testid=\"filters_enabled\"\n                                    title={t('block_domain_use_filters_and_hosts')}\n                                    disabled={processing}\n                                />\n                            )}\n                        />\n\n                        <p>\n                            <Trans components={components}>filters_block_toggle_hint</Trans>\n                        </p>\n                    </div>\n                </div>\n\n                <div className=\"col-12 col-md-5\">\n                    <div className=\"form__group form__group--inner mb-5\">\n                        <label className=\"form__label\">\n                            <Trans>filters_interval</Trans>\n                        </label>\n                        <Controller\n                            name=\"interval\"\n                            control={control}\n                            render={({ field }) => (\n                                <Select\n                                    {...field}\n                                    data-testid=\"filters_interval\"\n                                    disabled={processing}\n                                    onChange={(e) => {\n                                        const { value } = e.target;\n                                        field.onChange(toNumber(value));\n                                    }}>\n                                    {FILTERS_INTERVALS_HOURS.map((interval) => (\n                                        <option value={interval} key={interval}>\n                                            {getTitleForInterval(interval)}\n                                        </option>\n                                    ))}\n                                </Select>\n                            )}\n                        />\n                    </div>\n                </div>\n            </div>\n        </>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/FormButton.css",
    "content": ".form__button {\n    margin-left: 1.5rem;\n}\n\n@media (max-width: 500px) {\n    .form__button {\n        margin-left: 0;\n        margin-top: 1rem;\n        display: block;\n    }\n}\n"
  },
  {
    "path": "client/src/components/Settings/LogsConfig/Form.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport i18next from 'i18next';\nimport { Controller, useForm } from 'react-hook-form';\n\nimport { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';\nimport { QUERY_LOG_INTERVALS_DAYS, HOUR, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';\nimport '../FormButton.css';\nimport { Checkbox } from '../../ui/Controls/Checkbox';\nimport { Input } from '../../ui/Controls/Input';\nimport { toNumber } from '../../../helpers/form';\nimport { Textarea } from '../../ui/Controls/Textarea';\n\nconst getIntervalTitle = (interval: number) => {\n    switch (interval) {\n        case RETENTION_CUSTOM:\n            return i18next.t('settings_custom');\n        case 6 * HOUR:\n            return i18next.t('interval_6_hour');\n        case DAY:\n            return i18next.t('interval_24_hour');\n        default:\n            return i18next.t('interval_days', { count: interval / DAY });\n    }\n};\n\nexport type FormValues = {\n    enabled: boolean;\n    anonymize_client_ip: boolean;\n    interval: number;\n    customInterval?: number | null;\n    ignored: string;\n    ignored_enabled: boolean;\n};\n\ntype Props = {\n    initialValues: Partial<FormValues>;\n    processing: boolean;\n    processingReset: boolean;\n    onSubmit: (values: FormValues) => void;\n    onReset: () => void;\n};\n\nexport const Form = ({ initialValues, processing, processingReset, onSubmit, onReset }: Props) => {\n    const { t } = useTranslation();\n\n    const {\n        handleSubmit,\n        watch,\n        setValue,\n        control,\n        formState: { isSubmitting },\n    } = useForm<FormValues>({\n        mode: 'onBlur',\n        defaultValues: {\n            enabled: initialValues.enabled || false,\n            anonymize_client_ip: initialValues.anonymize_client_ip || false,\n            interval: initialValues.interval || DAY,\n            customInterval: initialValues.customInterval || null,\n            ignored: initialValues.ignored || '',\n            ignored_enabled: initialValues.ignored_enabled ?? true,\n        },\n    });\n\n    const intervalValue = watch('interval');\n    const customIntervalValue = watch('customInterval');\n\n    useEffect(() => {\n        if (QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)) {\n            setValue('customInterval', null);\n        }\n    }, [intervalValue]);\n\n    const onSubmitForm = (data: FormValues) => {\n        onSubmit(data);\n    };\n\n    const handleIgnoredBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {\n        const trimmed = trimLinesAndRemoveEmpty(e.target.value);\n        setValue('ignored', trimmed);\n    };\n\n    const disableSubmit = isSubmitting || processing || (intervalValue === RETENTION_CUSTOM && !customIntervalValue);\n\n    return (\n        <form onSubmit={handleSubmit(onSubmitForm)}>\n            <div className=\"form__group form__group--settings\">\n                <Controller\n                    name=\"enabled\"\n                    control={control}\n                    render={({ field }) => (\n                        <Checkbox\n                            {...field}\n                            data-testid=\"logs_enabled\"\n                            title={t('query_log_enable')}\n                            disabled={processing}\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"form__group form__group--settings\">\n                <Controller\n                    name=\"anonymize_client_ip\"\n                    control={control}\n                    render={({ field }) => (\n                        <Checkbox\n                            {...field}\n                            data-testid=\"logs_anonymize_client_ip\"\n                            title={t('anonymize_client_ip')}\n                            subtitle={t('anonymize_client_ip_desc')}\n                            disabled={processing}\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"form__label\">\n                <Trans>query_log_retention</Trans>\n            </div>\n\n            <div className=\"form__group form__group--settings\">\n                <div className=\"custom-controls-stacked\">\n                    <label className=\"custom-control custom-radio\">\n                        <input\n                            type=\"radio\"\n                            data-testid=\"logs_config_interval\"\n                            className=\"custom-control-input\"\n                            disabled={processing}\n                            checked={!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)}\n                            value={RETENTION_CUSTOM}\n                            onChange={(e) => {\n                                setValue('interval', parseInt(e.target.value, 10));\n                            }}\n                        />\n\n                        <span className=\"custom-control-label\">{getIntervalTitle(RETENTION_CUSTOM)}</span>\n                    </label>\n\n                    {!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue) && (\n                        <div className=\"form__group--input\">\n                            <div className=\"form__desc form__desc--top\">{t('custom_rotation_input')}</div>\n\n                            <Controller\n                                name=\"customInterval\"\n                                control={control}\n                                render={({ field, fieldState }) => (\n                                    <Input\n                                        {...field}\n                                        data-testid=\"logs_config_custom_interval\"\n                                        disabled={processing}\n                                        error={fieldState.error?.message}\n                                        min={RETENTION_RANGE.MIN}\n                                        max={RETENTION_RANGE.MAX}\n                                        onChange={(e) => {\n                                            const { value } = e.target;\n                                            field.onChange(toNumber(value));\n                                        }}\n                                    />\n                                )}\n                            />\n                        </div>\n                    )}\n\n                    {QUERY_LOG_INTERVALS_DAYS.map((interval) => (\n                        <label key={interval} className=\"custom-control custom-radio\">\n                            <input\n                                type=\"radio\"\n                                className=\"custom-control-input\"\n                                data-testid={`logs_config_${interval}`}\n                                disabled={processing}\n                                value={interval}\n                                checked={intervalValue === interval}\n                                onChange={(e) => {\n                                    setValue('interval', parseInt(e.target.value, 10));\n                                }}\n                            />\n\n                            <span className=\"custom-control-label\">{getIntervalTitle(interval)}</span>\n                        </label>\n                    ))}\n                </div>\n            </div>\n\n            <div className=\"form__group form__group--settings\">\n                <Controller\n                    name=\"ignored_enabled\"\n                    control={control}\n                    render={({ field }) => (\n                        <Checkbox\n                            {...field}\n                            data-testid=\"logs_config_ignored_enabled\"\n                            title={t('ignore_domains_title')}\n                            subtitle={t('ignore_domains_desc_query')}\n                            disabled={processing}\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"form__group form__group--settings\">\n                <Controller\n                    name=\"ignored\"\n                    control={control}\n                    render={({ field, fieldState }) => (\n                        <Textarea\n                            {...field}\n                            data-testid=\"logs_config_ingored\"\n                            placeholder={t('ignore_domains')}\n                            className=\"text-input\"\n                            disabled={processing}\n                            error={fieldState.error?.message}\n                            onBlur={handleIgnoredBlur}\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"mt-5\">\n                <button\n                    type=\"submit\"\n                    data-testid=\"logs_config_save\"\n                    className=\"btn btn-success btn-standard btn-large\"\n                    disabled={disableSubmit}>\n                    <Trans>save_btn</Trans>\n                </button>\n\n                <button\n                    type=\"button\"\n                    data-testid=\"logs_config_clear\"\n                    className=\"btn btn-outline-secondary btn-standard form__button\"\n                    onClick={onReset}\n                    disabled={processingReset}>\n                    <Trans>query_log_clear</Trans>\n                </button>\n            </div>\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/LogsConfig/index.tsx",
    "content": "import React, { Component } from 'react';\nimport { withTranslation } from 'react-i18next';\n\nimport Card from '../../ui/Card';\n\nimport { Form, FormValues } from './Form';\nimport { HOUR } from '../../../helpers/constants';\n\ninterface LogsConfigProps {\n    interval: number;\n    customInterval?: number;\n    enabled: boolean;\n    anonymize_client_ip: boolean;\n    processing: boolean;\n    ignored: unknown[];\n    ignoredEnabled: boolean;\n    processingClear: boolean;\n    setLogsConfig: (...args: unknown[]) => unknown;\n    clearLogs: (...args: unknown[]) => unknown;\n    t: (...args: unknown[]) => string;\n}\n\nclass LogsConfig extends Component<LogsConfigProps> {\n    handleFormSubmit = (values: FormValues) => {\n        const { t, interval: prevInterval } = this.props;\n        const { interval, customInterval, ...rest } = values;\n\n        const newInterval = customInterval ? customInterval * HOUR : interval;\n\n        const data = {\n            ...rest,\n            ignored: values.ignored ? values.ignored.split('\\n') : [],\n            interval: newInterval,\n        };\n\n        if (newInterval < prevInterval) {\n            // eslint-disable-next-line no-alert\n            if (window.confirm(t('query_log_retention_confirm'))) {\n                this.props.setLogsConfig(data);\n            }\n        } else {\n            this.props.setLogsConfig(data);\n        }\n    };\n\n    handleClear = () => {\n        const { t, clearLogs } = this.props;\n        // eslint-disable-next-line no-alert\n        if (window.confirm(t('query_log_confirm_clear'))) {\n            clearLogs();\n        }\n    };\n\n    render() {\n        const {\n            t,\n            enabled,\n            interval,\n            processing,\n            processingClear,\n            anonymize_client_ip,\n            ignored,\n            ignoredEnabled,\n            customInterval,\n        } = this.props;\n\n        return (\n            <Card title={t('query_log_configuration')} bodyType=\"card-body box-body--settings\" id=\"logs-config\">\n                <div className=\"form\">\n                    <Form\n                        initialValues={{\n                            enabled,\n                            interval,\n                            customInterval,\n                            anonymize_client_ip,\n                            ignored: ignored?.join('\\n'),\n                            ignored_enabled: ignoredEnabled,\n                        }}\n                        processing={processing}\n                        processingReset={processingClear}\n                        onSubmit={this.handleFormSubmit}\n                        onReset={this.handleClear}\n                    />\n                </div>\n            </Card>\n        );\n    }\n}\n\nexport default withTranslation()(LogsConfig);\n"
  },
  {
    "path": "client/src/components/Settings/Settings.css",
    "content": ".form__group {\n    position: relative;\n    margin-bottom: 15px;\n}\n\n.form__group:last-child {\n    margin-bottom: 0;\n}\n\n.form__group--settings:last-child {\n    margin-bottom: 20px;\n}\n\n.form__group--inner {\n    max-width: 300px;\n    margin-top: -10px;\n    margin-left: 40px;\n    font-size: 14px;\n}\n\n.form__group--input {\n    max-width: 300px;\n    margin: 0 1.5rem 10px;\n}\n\n.form__group--checkbox {\n    margin-bottom: 25px;\n}\n\n.form__group--inner .form__group--checkbox {\n    margin-bottom: 12px;\n}\n\n.form__group--inner .form__group--checkbox:last-child {\n    margin-bottom: 0;\n}\n\n.form__inline {\n    display: flex;\n    justify-content: flex-start;\n}\n\n.btn-standard {\n    padding-left: 20px;\n    padding-right: 20px;\n}\n\n.btn-large {\n    min-width: 150px;\n}\n\n.form-control--textarea {\n    min-height: 110px;\n    transition: 0.3s ease-in-out background-color;\n}\n\n.form-control--textarea-small {\n    min-height: 90px;\n}\n\n.form-control--textarea-large {\n    min-height: 240px;\n}\n\n.form__message {\n    margin-top: 4px;\n    font-size: 11px;\n}\n\n.form__message--error {\n    color: #cd201f;\n}\n\n.form__message--left-pad {\n    padding-left: 0.85rem;\n}\n\n.interface__title {\n    font-size: 13px;\n    font-weight: 700;\n}\n\n.interface__ip:after {\n    content: ', ';\n}\n\n.interface__ip:last-child:after {\n    content: '';\n}\n\n.form__desc {\n    margin-top: 10px;\n    font-size: 13px;\n    color: var(--scolor);\n}\n\n.form__desc--top {\n    margin: 0 0 8px;\n}\n\n.form__label {\n    margin-bottom: 8px;\n}\n\n.form__label--bold {\n    font-weight: 700;\n}\n\n.form__label--with-desc {\n    margin-bottom: 0;\n}\n\n.form__label--bot {\n    margin-bottom: 10px;\n}\n\n.form__label--top {\n    margin-top: 10px;\n}\n\n.form__status {\n    margin-top: 10px;\n    font-size: 14px;\n    line-height: 1.7;\n}\n\n.encryption__list {\n    padding-left: 0;\n}\n\n.encryption__list li {\n    list-style: inside;\n}\n\n.btn-icon {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: 30px;\n    height: 30px;\n    background-color: transparent;\n    overflow: hidden;\n}\n\n.btn-icon--green {\n    color: var(--green);\n}\n\n.btn-icon-sm {\n    width: 23px;\n    height: 23px;\n    min-width: 23px;\n    padding: 5px;\n}\n\n.custom-control-label,\n.custom-control-label:before {\n    transition:\n        0.3s ease-in-out background-color,\n        0.3s ease-in-out color;\n}\n\n.custom-select:disabled {\n    background-color: #f9f9f9;\n}\n\n.custom-select {\n    transition:\n        0.3s ease-in-out background-color,\n        0.3s ease-in-out color;\n}\n"
  },
  {
    "path": "client/src/components/Settings/StatsConfig/Form.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport i18next from 'i18next';\n\nimport { Controller, useForm } from 'react-hook-form';\nimport { STATS_INTERVALS_DAYS, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';\n\nimport '../FormButton.css';\nimport { Checkbox } from '../../ui/Controls/Checkbox';\nimport { Input } from '../../ui/Controls/Input';\nimport { toNumber } from '../../../helpers/form';\nimport { Textarea } from '../../ui/Controls/Textarea';\n\nconst getIntervalTitle = (interval: any) => {\n    switch (interval) {\n        case RETENTION_CUSTOM:\n            return i18next.t('settings_custom');\n        case DAY:\n            return i18next.t('interval_24_hour');\n        default:\n            return i18next.t('interval_days', { count: interval / DAY });\n    }\n};\n\nexport type FormValues = {\n    enabled: boolean;\n    interval: number;\n    customInterval?: number | null;\n    ignored: string;\n    ignored_enabled: boolean;\n};\n\nconst defaultFormValues = {\n    enabled: false,\n    interval: DAY,\n    customInterval: null,\n    ignored: '',\n    ignored_enabled: true,\n};\n\ntype Props = {\n    initialValues: FormValues;\n    processing: boolean;\n    processingReset: boolean;\n    onSubmit: (values: FormValues) => void;\n    onReset: () => void;\n};\n\nexport const Form = ({ initialValues, processing, processingReset, onSubmit, onReset }: Props) => {\n    const { t } = useTranslation();\n\n    const {\n        handleSubmit,\n        watch,\n        setValue,\n        control,\n        formState: { isSubmitting },\n    } = useForm<FormValues>({\n        mode: 'onBlur',\n        defaultValues: {\n            ...defaultFormValues,\n            ...initialValues,\n        },\n    });\n\n    const intervalValue = watch('interval');\n    const customIntervalValue = watch('customInterval');\n\n    useEffect(() => {\n        if (STATS_INTERVALS_DAYS.includes(intervalValue)) {\n            setValue('customInterval', null);\n        }\n    }, [intervalValue]);\n\n    const onSubmitForm = (data: FormValues) => {\n        onSubmit(data);\n    };\n\n    const disableSubmit = isSubmitting || processing || (intervalValue === RETENTION_CUSTOM && !customIntervalValue);\n\n    return (\n        <form onSubmit={handleSubmit(onSubmitForm)}>\n            <div className=\"form__group form__group--settings\">\n                <Controller\n                    name=\"enabled\"\n                    control={control}\n                    render={({ field }) => (\n                        <Checkbox\n                            {...field}\n                            data-testid=\"stats_config_enabled\"\n                            title={t('statistics_enable')}\n                            disabled={processing}\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"form__label form__label--with-desc\">\n                <Trans>statistics_retention</Trans>\n            </div>\n\n            <div className=\"form__desc form__desc--top\">\n                <Trans>statistics_retention_desc</Trans>\n            </div>\n\n            <div className=\"form__group form__group--settings mt-2\">\n                <div className=\"custom-controls-stacked\">\n                    <label className=\"custom-control custom-radio\">\n                        <input\n                            type=\"radio\"\n                            data-testid=\"stats_config_interval\"\n                            className=\"custom-control-input\"\n                            disabled={processing}\n                            checked={!STATS_INTERVALS_DAYS.includes(intervalValue)}\n                            value={RETENTION_CUSTOM}\n                            onChange={(e) => {\n                                setValue('interval', parseInt(e.target.value, 10));\n                            }}\n                        />\n\n                        <span className=\"custom-control-label\">{getIntervalTitle(RETENTION_CUSTOM)}</span>\n                    </label>\n\n                    {!STATS_INTERVALS_DAYS.includes(intervalValue) && (\n                        <div className=\"form__group--input\">\n                            <div className=\"form__desc form__desc--top\">{i18next.t('custom_retention_input')}</div>\n\n                            <Controller\n                                name=\"customInterval\"\n                                control={control}\n                                render={({ field, fieldState }) => (\n                                    <Input\n                                        {...field}\n                                        data-testid=\"stats_config_custom_interval\"\n                                        disabled={processing}\n                                        error={fieldState.error?.message}\n                                        min={RETENTION_RANGE.MIN}\n                                        max={RETENTION_RANGE.MAX}\n                                        onChange={(e) => {\n                                            const { value } = e.target;\n                                            field.onChange(toNumber(value));\n                                        }}\n                                    />\n                                )}\n                            />\n                        </div>\n                    )}\n                    {STATS_INTERVALS_DAYS.map((interval) => (\n                        <label key={interval} className=\"custom-control custom-radio\">\n                            <input\n                                type=\"radio\"\n                                className=\"custom-control-input\"\n                                disabled={processing}\n                                value={interval}\n                                checked={intervalValue === interval}\n                                onChange={(e) => {\n                                    setValue('interval', parseInt(e.target.value, 10));\n                                }}\n                            />\n\n                            <span className=\"custom-control-label\">{getIntervalTitle(interval)}</span>\n                        </label>\n                    ))}\n                </div>\n            </div>\n\n            <div className=\"form__group form__group--settings\">\n                <Controller\n                    name=\"ignored_enabled\"\n                    control={control}\n                    render={({ field }) => (\n                        <Checkbox\n                            {...field}\n                            data-testid=\"stats_config_ignored_enabled\"\n                            title={t('ignore_domains_title')}\n                            subtitle={t('ignore_domains_desc_stats')}\n                            disabled={processing}\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"form__group form__group--settings\">\n                <Controller\n                    name=\"ignored\"\n                    control={control}\n                    render={({ field, fieldState }) => (\n                        <Textarea\n                            {...field}\n                            data-testid=\"stats_config_ignored\"\n                            placeholder={t('ignore_domains')}\n                            className=\"text-input\"\n                            disabled={processing}\n                            error={fieldState.error?.message}\n                            trimOnBlur\n                        />\n                    )}\n                />\n            </div>\n\n            <div className=\"mt-5\">\n                <button\n                    type=\"submit\"\n                    data-testid=\"stats_config_save\"\n                    className=\"btn btn-success btn-standard btn-large\"\n                    disabled={disableSubmit}>\n                    <Trans>save_btn</Trans>\n                </button>\n\n                <button\n                    type=\"button\"\n                    data-testid=\"stats_config_clear\"\n                    className=\"btn btn-outline-secondary btn-standard form__button\"\n                    onClick={onReset}\n                    disabled={processingReset}>\n                    <Trans>statistics_clear</Trans>\n                </button>\n            </div>\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/components/Settings/StatsConfig/index.tsx",
    "content": "import React, { Component } from 'react';\nimport { withTranslation } from 'react-i18next';\n\nimport Card from '../../ui/Card';\n\nimport { Form, FormValues } from './Form';\nimport { HOUR } from '../../../helpers/constants';\n\ninterface StatsConfigProps {\n    interval: number;\n    customInterval?: number;\n    ignored: unknown[];\n    ignoredEnabled: boolean;\n    enabled: boolean;\n    processing: boolean;\n    processingReset: boolean;\n    setStatsConfig: (...args: unknown[]) => unknown;\n    resetStats: (...args: unknown[]) => unknown;\n    t: (...args: unknown[]) => string;\n}\n\nclass StatsConfig extends Component<StatsConfigProps> {\n    handleFormSubmit = ({ enabled, interval, ignored, ignored_enabled, customInterval }: FormValues) => {\n        const { t, interval: prevInterval } = this.props;\n        const newInterval = customInterval ? customInterval * HOUR : interval;\n\n        const config = {\n            enabled,\n            interval: newInterval,\n            ignored: ignored ? ignored.split('\\n') : [],\n            ignored_enabled,\n        };\n\n        if (config.interval < prevInterval) {\n            if (window.confirm(t('statistics_retention_confirm'))) {\n                this.props.setStatsConfig(config);\n            }\n        } else {\n            this.props.setStatsConfig(config);\n        }\n    };\n\n    handleReset = () => {\n        const { t, resetStats } = this.props;\n        // eslint-disable-next-line no-alert\n        if (window.confirm(t('statistics_clear_confirm'))) {\n            resetStats();\n        }\n    };\n\n    render() {\n        const {\n            t,\n            interval,\n            customInterval,\n            processing,\n            processingReset,\n            ignored,\n            ignoredEnabled,\n            enabled,\n        } = this.props;\n\n        return (\n            <Card title={t('statistics_configuration')} bodyType=\"card-body box-body--settings\" id=\"stats-config\">\n                <div className=\"form\">\n                    <Form\n                        initialValues={{\n                            interval,\n                            customInterval,\n                            enabled,\n                            ignored: ignored.join('\\n'),\n                            ignored_enabled: ignoredEnabled,\n                        }}\n                        processing={processing}\n                        processingReset={processingReset}\n                        onSubmit={this.handleFormSubmit}\n                        onReset={this.handleReset}\n                    />\n                </div>\n            </Card>\n        );\n    }\n}\n\nexport default withTranslation()(StatsConfig);\n"
  },
  {
    "path": "client/src/components/Settings/index.tsx",
    "content": "import React, { Component, Fragment } from 'react';\nimport { withTranslation } from 'react-i18next';\n\nimport i18next from 'i18next';\nimport StatsConfig from './StatsConfig';\n\nimport LogsConfig from './LogsConfig';\n\nimport { FiltersConfig } from './FiltersConfig';\n\nimport { Checkbox } from '../ui/Controls/Checkbox';\n\nimport Loading from '../ui/Loading';\n\nimport PageTitle from '../ui/PageTitle';\n\nimport Card from '../ui/Card';\n\nimport { getObjectKeysSorted, captitalizeWords } from '../../helpers/helpers';\nimport './Settings.css';\nimport { SettingsData } from '../../initialState';\n\nconst ORDER_KEY = 'order';\n\nconst SETTINGS = {\n    safebrowsing: {\n        enabled: false,\n        title: i18next.t('use_adguard_browsing_sec'),\n        subtitle: i18next.t('use_adguard_browsing_sec_hint'),\n        testId: 'safebrowsing',\n        [ORDER_KEY]: 0,\n    },\n    parental: {\n        enabled: false,\n        title: i18next.t('use_adguard_parental'),\n        subtitle: i18next.t('use_adguard_parental_hint'),\n        testId: 'parental',\n        [ORDER_KEY]: 1,\n    },\n};\n\ninterface SettingsProps {\n    initSettings: (...args: unknown[]) => unknown;\n    settings: SettingsData;\n    toggleSetting: (...args: unknown[]) => unknown;\n    getStatsConfig: (...args: unknown[]) => unknown;\n    setStatsConfig: (...args: unknown[]) => unknown;\n    resetStats: (...args: unknown[]) => unknown;\n    setFiltersConfig: (...args: unknown[]) => unknown;\n    getFilteringStatus: (...args: unknown[]) => unknown;\n    t: (...args: unknown[]) => string;\n    getLogsConfig?: (...args: unknown[]) => unknown;\n    setLogsConfig?: (...args: unknown[]) => unknown;\n    clearLogs?: (...args: unknown[]) => unknown;\n    stats?: {\n        processingGetConfig?: boolean;\n        interval?: number;\n        customInterval?: number;\n        enabled?: boolean;\n        ignored?: unknown[];\n        ignored_enabled?: boolean;\n        processingSetConfig?: boolean;\n        processingReset?: boolean;\n    };\n    queryLogs?: {\n        enabled?: boolean;\n        interval?: number;\n        customInterval?: number;\n        anonymize_client_ip?: boolean;\n        processingSetConfig?: boolean;\n        processingClear?: boolean;\n        processingGetConfig?: boolean;\n        ignored?: unknown[];\n        ignored_enabled?: boolean;\n    };\n    filtering?: {\n        interval?: number;\n        enabled?: boolean;\n        processingSetConfig?: boolean;\n    };\n}\n\nclass Settings extends Component<SettingsProps> {\n    componentDidMount() {\n        this.props.initSettings(SETTINGS);\n\n        this.props.getStatsConfig();\n\n        this.props.getLogsConfig();\n\n        this.props.getFilteringStatus();\n    }\n\n    renderSettings = (settings: any) =>\n        getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {\n            const setting = settings[key];\n            const { enabled, title, subtitle, testId } = setting;\n\n            return (\n                <div key={key} className=\"form__group form__group--checkbox\">\n                    <Checkbox\n                        data-testid={testId}\n                        value={enabled}\n                        title={title}\n                        subtitle={subtitle}\n                        onChange={(checked) => this.props.toggleSetting(key, !checked)}\n                    />\n                </div>\n            );\n        });\n\n    renderSafeSearch = () => {\n        const {\n            settings: {\n                settingsList: { safesearch },\n            },\n        } = this.props;\n        const { enabled } = safesearch || {};\n        const searches = { ...(safesearch || {}) };\n        delete searches.enabled;\n\n        return (\n            <>\n                <div className=\"form__group form__group--checkbox\">\n                    <Checkbox\n                        data-testid=\"safesearch\"\n                        value={enabled}\n                        title={i18next.t('enforce_safe_search')}\n                        subtitle={i18next.t('enforce_save_search_hint')}\n                        onChange={(checked) =>\n                            this.props.toggleSetting('safesearch', { ...safesearch, enabled: checked })\n                        }\n                    />\n                </div>\n\n                <div className=\"form__group--inner\">\n                    {Object.keys(searches).map((searchKey) => (\n                        <div key={searchKey} className=\"form__group form__group--checkbox\">\n                            <Checkbox\n                                value={searches[searchKey]}\n                                title={captitalizeWords(searchKey)}\n                                disabled={!safesearch.enabled}\n                                onChange={(checked) =>\n                                    this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })\n                                }\n                            />\n                        </div>\n                    ))}\n                </div>\n            </>\n        );\n    };\n\n    render() {\n        const {\n            settings,\n            setStatsConfig,\n            resetStats,\n            stats,\n            queryLogs,\n            setLogsConfig,\n            clearLogs,\n            filtering,\n            setFiltersConfig,\n            t,\n        } = this.props;\n\n        const isDataReady = !settings.processing && !stats.processingGetConfig && !queryLogs.processingGetConfig;\n\n        return (\n            <Fragment>\n                <PageTitle title={t('general_settings')} />\n\n                {!isDataReady && <Loading />}\n\n                {isDataReady && (\n                    <div className=\"content\">\n                        <div className=\"row\">\n                            <div className=\"col-md-12\">\n                                <Card bodyType=\"card-body box-body--settings\">\n                                    <div className=\"form\">\n                                        <FiltersConfig\n                                            initialValues={{\n                                                interval: filtering.interval,\n                                                enabled: filtering.enabled,\n                                            }}\n                                            processing={filtering.processingSetConfig}\n                                            setFiltersConfig={setFiltersConfig}\n                                        />\n                                        {this.renderSettings(settings.settingsList)}\n                                        {this.renderSafeSearch()}\n                                    </div>\n                                </Card>\n                            </div>\n\n                            <div className=\"col-md-12\">\n                                <LogsConfig\n                                    enabled={queryLogs.enabled}\n                                    ignored={queryLogs.ignored}\n                                    ignoredEnabled={queryLogs.ignored_enabled}\n                                    interval={queryLogs.interval}\n                                    customInterval={queryLogs.customInterval}\n                                    anonymize_client_ip={queryLogs.anonymize_client_ip}\n                                    processing={queryLogs.processingSetConfig}\n                                    processingClear={queryLogs.processingClear}\n                                    setLogsConfig={setLogsConfig}\n                                    clearLogs={clearLogs}\n                                />\n                            </div>\n\n                            <div className=\"col-md-12\">\n                                <StatsConfig\n                                    interval={stats.interval}\n                                    customInterval={stats.customInterval}\n                                    ignored={stats.ignored}\n                                    ignoredEnabled={stats.ignored_enabled}\n                                    enabled={stats.enabled}\n                                    processing={stats.processingSetConfig}\n                                    processingReset={stats.processingReset}\n                                    setStatsConfig={setStatsConfig}\n                                    resetStats={resetStats}\n                                />\n                            </div>\n                        </div>\n                    </div>\n                )}\n            </Fragment>\n        );\n    }\n}\n\nexport default withTranslation()(Settings);\n"
  },
  {
    "path": "client/src/components/SetupGuide/Guide.css",
    "content": ".guide {\n    max-width: 768px;\n    margin: 0 auto;\n}\n\n.guide__title {\n    margin-bottom: 10px;\n    font-size: 17px;\n    font-weight: 700;\n}\n\n.guide__desc {\n    margin-bottom: 20px;\n    font-size: 15px;\n}\n\n.guide__list {\n    margin-top: 16px;\n    padding-left: 0;\n}\n\n@media screen and (min-width: 768px) {\n    .guide__list {\n        padding-left: 24px;\n    }\n}\n\n.guide__address {\n    display: block;\n    margin-bottom: 7px;\n    font-size: 13px;\n    font-weight: 700;\n}\n\n@media screen and (min-width: 768px) {\n    .guide__address {\n        display: list-item;\n        font-size: 15px;\n    }\n}\n"
  },
  {
    "path": "client/src/components/SetupGuide/index.tsx",
    "content": "import React from 'react';\nimport { Trans, withTranslation } from 'react-i18next';\n\nimport { Guide } from '../ui/Guide';\n\nimport Card from '../ui/Card';\n\nimport PageTitle from '../ui/PageTitle';\nimport './Guide.css';\nimport { DashboardData } from '../../initialState';\n\ninterface SetupGuideProps {\n    dashboard: DashboardData;\n    t: (id: string) => string;\n}\n\nconst SetupGuide = ({ t, dashboard: { dnsAddresses } }: SetupGuideProps) => (\n    <div className=\"guide\">\n        <PageTitle title={t('setup_guide')} />\n\n        <Card>\n            <div className=\"guide__title\">\n                <Trans>install_devices_title</Trans>\n            </div>\n\n            <div className=\"guide__desc\">\n                <Trans>install_devices_desc</Trans>\n\n                <div className=\"mt-1\">\n                    <Trans>install_devices_address</Trans>:\n                </div>\n\n                <ul className=\"guide__list\">\n                    {dnsAddresses.map((ip: any) => (\n                        <li key={ip} className=\"guide__address\">\n                            {ip}\n                        </li>\n                    ))}\n                </ul>\n            </div>\n\n            <Guide dnsAddresses={dnsAddresses} />\n        </Card>\n    </div>\n);\n\nexport default withTranslation()(SetupGuide);\n"
  },
  {
    "path": "client/src/components/Toasts/Toast.css",
    "content": ".toasts {\n    position: fixed;\n    right: 24px;\n    bottom: 24px;\n    z-index: 105;\n    width: 345px;\n}\n\n@media (max-width: 425px) {\n    .toasts {\n        right: 0;\n        width: 100%;\n    }\n}\n\n.toast {\n    display: flex;\n    align-items: flex-start;\n    margin-bottom: 12px;\n    padding: 16px;\n    font-weight: 600;\n    color: #ffffff;\n    border-radius: 4px;\n    background-color: rgba(236, 53, 53, 0.75);\n}\n\n.toast--success {\n    background-color: rgba(90, 173, 99, 0.75);\n}\n\n.toast:last-child {\n    margin-bottom: 0;\n}\n\n.toast__content {\n    flex: 1 1 auto;\n    margin: 0 12px 0 0;\n    text-overflow: ellipsis;\n    overflow: hidden;\n}\n\n.toast__content a {\n    font-weight: 600;\n    color: #fff;\n    text-decoration: underline;\n}\n\n.toast__dismiss {\n    display: block;\n    flex: 0 0 auto;\n    padding: 0;\n    background: transparent;\n    border: 0;\n    cursor: pointer;\n}\n\n.toast__dismiss:hover,\n.toast__dismiss:focus {\n    outline: none;\n}\n\n.toast-enter {\n    opacity: 0.01;\n}\n\n.toast-enter-active {\n    opacity: 1;\n    transition: all 0.3s ease-out;\n}\n\n.toast-exit {\n    opacity: 1;\n}\n\n.toast-exit-active {\n    opacity: 0.01;\n    transition: all 0.3s ease-out;\n}\n"
  },
  {
    "path": "client/src/components/Toasts/Toast.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Trans } from 'react-i18next';\nimport { useDispatch } from 'react-redux';\nimport { TOAST_TIMEOUTS } from '../../helpers/constants';\n\nimport { removeToast } from '../../actions';\n\ninterface ToastProps {\n    id: string;\n    message: string;\n    type: string;\n    options?: object;\n}\n\nconst Toast = ({ id, message, type, options }: ToastProps) => {\n    const dispatch = useDispatch();\n    const [timerId, setTimerId] = useState(null);\n\n    const clearRemoveToastTimeout = () => clearTimeout(timerId);\n    const removeCurrentToast = () => dispatch(removeToast(id));\n    const setRemoveToastTimeout = () => {\n        const timeout = TOAST_TIMEOUTS[type];\n        const timerId = setTimeout(removeCurrentToast, timeout);\n\n        setTimerId(timerId);\n    };\n\n    useEffect(() => {\n        setRemoveToastTimeout();\n    }, []);\n\n    return (\n        <div\n            className={`toast toast--${type}`}\n            onMouseOver={clearRemoveToastTimeout}\n            onMouseOut={setRemoveToastTimeout}>\n            <p className=\"toast__content\">\n                <Trans i18nKey={message} {...options} />\n            </p>\n\n            <button className=\"toast__dismiss\" onClick={removeCurrentToast}>\n                <svg\n                    stroke=\"#fff\"\n                    fill=\"none\"\n                    width=\"20\"\n                    height=\"20\"\n                    strokeWidth=\"2\"\n                    viewBox=\"0 0 24 24\"\n                    xmlns=\"http://www.w3.org/2000/svg\">\n                    <path d=\"m18 6-12 12\" />\n\n                    <path d=\"m6 6 12 12\" />\n                </svg>\n            </button>\n        </div>\n    );\n};\n\nexport default Toast;\n"
  },
  {
    "path": "client/src/components/Toasts/index.tsx",
    "content": "import React from 'react';\nimport { useSelector, shallowEqual } from 'react-redux';\n\nimport { CSSTransition, TransitionGroup } from 'react-transition-group';\nimport { TOAST_TRANSITION_TIMEOUT } from '../../helpers/constants';\n\nimport Toast from './Toast';\nimport './Toast.css';\nimport { RootState } from '../../initialState';\n\nconst Toasts = () => {\n    const toasts = useSelector((state: RootState) => state.toasts, shallowEqual);\n\n    return (\n        <TransitionGroup className=\"toasts\">\n            {toasts.notices?.map((toast: any) => {\n                const { id } = toast;\n\n                return (\n                    <CSSTransition key={id} timeout={TOAST_TRANSITION_TIMEOUT} classNames=\"toast\">\n                        <Toast {...toast} />\n                    </CSSTransition>\n                );\n            })}\n        </TransitionGroup>\n    );\n};\n\nexport default Toasts;\n"
  },
  {
    "path": "client/src/components/ui/Card.css",
    "content": ".card-header {\n    align-items: center;\n    justify-content: space-between;\n    padding: 0.6rem 1.5rem;\n    flex-shrink: 0;\n}\n\n.card-subtitle {\n    margin: 4px 0;\n    line-height: 1.4;\n}\n\n.card-table-overflow {\n    overflow-y: auto;\n    max-height: 100%;\n}\n\n.card-table-overflow--limited {\n    overflow-y: auto;\n    max-height: 17.5rem;\n}\n\n.dashboard .card-table {\n    overflow: hidden;\n}\n\n.dashboard .card-table-overflow--limited {\n    max-height: 292px;\n}\n\n.dashboard .ReactTable .rt-tr-group {\n    min-height: 52px;\n}\n\n.card-actions {\n    margin-top: 20px;\n}\n\n.card-actions-top {\n    margin-bottom: 20px;\n}\n\n.card-graph {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.card-body--status {\n    padding: 2.5rem 1.5rem;\n    text-align: center;\n}\n\n.card-body--loading {\n    position: relative;\n}\n\n.card-body--loading:before {\n    content: '';\n    position: absolute;\n    top: 0;\n    left: 0;\n    z-index: 100;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(255, 255, 255, 0.6);\n}\n\n.card-body--loading:after {\n    content: '';\n    position: absolute;\n    z-index: 101;\n    left: 50%;\n    top: 50%;\n    width: 40px;\n    height: 40px;\n    margin-top: -20px;\n    margin-left: -20px;\n    background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E');\n    will-change: transform;\n    animation: clockwise 2s linear infinite;\n}\n\n.card-title-stats {\n    font-size: 13px;\n    color: #9aa0ac;\n}\n\n.card-body-stats {\n    position: relative;\n    flex: 1 1 auto;\n    margin: 0;\n    padding: 1rem 1.5rem;\n}\n\n.card-value-stats {\n    display: block;\n    font-size: 2.1rem;\n    line-height: 2.7rem;\n    height: 2.7rem;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n}\n\n.card-value-percent {\n    position: absolute;\n    top: 15px;\n    right: 15px;\n    font-size: 0.9rem;\n    line-height: 1;\n    height: auto;\n}\n\n.card-value-percent:after {\n    content: '%';\n}\n\n.card--full {\n    height: calc(100% - 1.5rem);\n}\n\n.card-wrap {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n}\n\n@media screen and (min-width: 1280px) {\n    .card-title-stats {\n        font-size: 14px;\n    }\n}\n\n@media (min-width: 992px) {\n    .dashboard .card:not(.card--full) {\n        height: 360px;\n    }\n}\n\n.card .logs__cell--red {\n    background-color: #fff4f2;\n}\n\n.card .logs__cell--green {\n    background-color: #f1faf3;\n}\n\n.card .logs__row--blue {\n    background-color: #ecf7ff;\n}\n\n[data-theme='dark'] .card .logs__row--blue {\n    background-color: var(--logs__row--blue-bgcolor);\n}\n"
  },
  {
    "path": "client/src/components/ui/Card.tsx",
    "content": "import React from 'react';\n\nimport './Card.css';\n\ninterface CardProps {\n    id?: string;\n    title?: string;\n    subtitle?: string;\n    bodyType?: string;\n    type?: string;\n    refresh?: React.ReactNode;\n    children: React.ReactNode;\n}\n\nconst Card = ({ type, id, title, subtitle, refresh, bodyType, children }: CardProps) => (\n    <div className={type ? `card ${type}` : 'card'} id={id || ''}>\n        {(title || subtitle) && (\n            <div className=\"card-header with-border\">\n                <div className=\"card-inner\">\n                    {title && <div className=\"card-title\">{title}</div>}\n\n                    {subtitle && <div className=\"card-subtitle\" dangerouslySetInnerHTML={{ __html: subtitle }} />}\n                </div>\n\n                {refresh && <div className=\"card-options\">{refresh}</div>}\n            </div>\n        )}\n\n        <div className={bodyType || 'card-body'}>{children}</div>\n    </div>\n);\n\nexport default Card;\n"
  },
  {
    "path": "client/src/components/ui/Cell.tsx",
    "content": "import React from 'react';\n\nimport LogsSearchLink from './LogsSearchLink';\n\nimport { formatNumber } from '../../helpers/helpers';\n\ninterface CellProps {\n    value: number;\n    percent: number;\n    color: string;\n    search?: string;\n    onSearchRedirect?: (...args: unknown[]) => string;\n}\n\nconst Cell = ({ value, percent, color, search }: CellProps) => (\n    <div className=\"stats__row\">\n        <div className=\"stats__row-value mb-1\">\n            <strong>\n                {search ? <LogsSearchLink search={search}>{formatNumber(value)}</LogsSearchLink> : formatNumber(value)}\n            </strong>\n\n            <small className=\"ml-3 text-muted\">{percent}%</small>\n        </div>\n\n        <div className=\"progress progress-xs\">\n            <div\n                className=\"progress-bar\"\n                style={{\n                    width: `${percent}%`,\n                    backgroundColor: color,\n                }}\n            />\n        </div>\n    </div>\n);\n\nexport default Cell;\n"
  },
  {
    "path": "client/src/components/ui/CellWrap.tsx",
    "content": "import React from 'react';\n\ninterface CellWrapProps {\n    value?: string | number;\n    formatValue?: (...args: unknown[]) => unknown;\n    formatTitle?: (...args: unknown[]) => unknown;\n}\n\nconst CellWrap = ({ value }: CellWrapProps, formatValue?: any, formatTitle = formatValue) => {\n    if (!value) {\n        return '–';\n    }\n    const cellValue = typeof formatValue === 'function' ? formatValue(value) : value;\n    const cellTitle = typeof formatTitle === 'function' ? formatTitle(value) : value;\n\n    return (\n        <div className=\"logs__row o-hidden\">\n            <span className=\"logs__text logs__text--full\" title={cellTitle}>\n                {cellValue}\n            </span>\n        </div>\n    );\n};\n\nexport default CellWrap;\n"
  },
  {
    "path": "client/src/components/ui/Controls/Checkbox/checkbox.css",
    "content": ".checkbox {\n    display: inline-block;\n    margin: 0;\n}\n\n.checkbox--single {\n    display: block;\n    margin: 2px auto 6px;\n}\n\n.checkbox--single .checkbox__label:before {\n    margin-right: 0;\n}\n\n.checkbox--settings .checkbox__label:before {\n    top: 2px;\n    margin-right: 20px;\n}\n\n.checkbox--settings .checkbox__label-title {\n    margin-bottom: 2px;\n    font-weight: 600;\n}\n\n.checkbox--form .checkbox__label:before {\n    top: 1px;\n    margin-right: 10px;\n}\n\n.checkbox__label {\n    position: relative;\n    display: flex;\n    align-items: flex-start;\n    justify-content: center;\n    font-size: 14px;\n    font-weight: 400;\n    user-select: none;\n    cursor: pointer;\n}\n\n.checkbox__label:before {\n    content: '';\n    position: relative;\n    top: 1px;\n    display: inline-block;\n    vertical-align: middle;\n    width: 20px;\n    height: 20px;\n    min-width: 20px;\n    margin-right: 10px;\n    background-color: var(--checkbox-bg);\n    background-repeat: no-repeat;\n    background-position: center center;\n    background-size: 12px 10px;\n    border-radius: 3px;\n    transition:\n        0.3s ease-in-out box-shadow,\n        0.3s ease-in-out opacity;\n}\n\n.checkbox__label--l {\n    &:before {\n        width: 30px;\n        height: 30px;\n    }\n}\n\n.checkbox__label .checkbox__label-text {\n    line-height: 1.3;\n}\n\n.checkbox__label .checkbox__label-text .md__paragraph {\n    display: inline-block;\n    vertical-align: baseline;\n    margin: 0;\n    text-align: left;\n    line-height: 1.3;\n}\n\n.checkbox__input {\n    position: absolute;\n    opacity: 0;\n}\n\n.checkbox__input:checked + .checkbox__label:before {\n    background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMi4zIDkuMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxwYXRoIGQ9Ik0xMS44IDAuNUw1LjMgOC41IDAuNSA0LjIiLz48L3N2Zz4=);\n}\n\n.checkbox__input:focus + .checkbox__label:before {\n    box-shadow: 0 0 1px 1px rgba(74, 74, 74, 0.32);\n}\n\n.checkbox__input:disabled + .checkbox__label {\n    cursor: default;\n    color: var(--gray);\n}\n\n.checkbox__input:disabled + .checkbox__label:before {\n    opacity: 0.6;\n}\n\n.checkbox__label-text {\n    max-width: 515px;\n    line-height: 1.5;\n}\n\n.checkbox__label-text--long {\n    max-width: initial;\n}\n\n.checkbox__label-title {\n    display: block;\n    line-height: 1.5;\n}\n\n.checkbox__label-subtitle {\n    display: block;\n    line-height: 1.5;\n    color: var(--scolor);\n}\n"
  },
  {
    "path": "client/src/components/ui/Controls/Checkbox/index.tsx",
    "content": "import React, { forwardRef, ReactNode } from 'react';\nimport clsx from 'clsx';\n\nimport './checkbox.css';\n\ntype Props = {\n    title: string;\n    subtitle?: ReactNode;\n    value: boolean;\n    name?: string;\n    disabled?: boolean;\n    className?: string;\n    error?: string;\n    onChange: (value: boolean) => void;\n    onBlur?: () => void;\n};\n\nexport const Checkbox = forwardRef<HTMLInputElement, Props>(\n    (\n        { title, subtitle, value, name, disabled, error, className = 'checkbox--form', onChange, onBlur, ...rest },\n        ref,\n    ) => (\n        <>\n            <label className={clsx('checkbox', className)}>\n                <span className=\"checkbox__marker\" />\n                <input\n                    name={name}\n                    type=\"checkbox\"\n                    className=\"checkbox__input\"\n                    disabled={disabled}\n                    checked={value}\n                    onChange={(e) => onChange(e.target.checked)}\n                    onBlur={onBlur}\n                    ref={ref}\n                    {...rest}\n                />\n                <span className=\"checkbox__label\">\n                    <span className=\"checkbox__label-text checkbox__label-text--long\">\n                        <span className=\"checkbox__label-title\">{title}</span>\n\n                        {subtitle && <span className=\"checkbox__label-subtitle\">{subtitle}</span>}\n                    </span>\n                </span>\n            </label>\n            {error && <div className=\"form__message form__message--error\">{error}</div>}\n        </>\n    ),\n);\n\nCheckbox.displayName = 'Checkbox';\n"
  },
  {
    "path": "client/src/components/ui/Controls/Input.tsx",
    "content": "import React, { ComponentProps, forwardRef, ReactNode } from 'react';\nimport clsx from 'clsx';\n\ntype Props = ComponentProps<'input'> & {\n    label?: string;\n    desc?: string;\n    leftAddon?: ReactNode;\n    rightAddon?: ReactNode;\n    error?: string;\n    trimOnBlur?: boolean;\n};\n\nexport const Input = forwardRef<HTMLInputElement, Props>(\n    ({ name, label, desc, className, leftAddon, rightAddon, error, trimOnBlur, onBlur, ...rest }, ref) => (\n        <div className={clsx('form-group', { 'has-error': !!error })}>\n            {label && (\n                <label className={clsx('form__label', { 'form__label--with-desc': !!desc })} htmlFor={name}>\n                    {label}\n                </label>\n            )}\n            {desc && <div className=\"form__desc form__desc--top\">{desc}</div>}\n            <div className=\"input-group\">\n                {leftAddon && <div>{leftAddon}</div>}\n                <input\n                    className={clsx('form-control', { 'is-invalid': !!error }, className)}\n                    ref={ref}\n                    onBlur={(e) => {\n                        if (trimOnBlur) {\n                            e.target.value = e.target.value.trim();\n                            rest.onChange(e);\n                        }\n                        if (onBlur) {\n                            onBlur(e);\n                        }\n                    }}\n                    {...rest}\n                />\n                {rightAddon && <div>{rightAddon}</div>}\n            </div>\n            {error && <div className=\"form__message form__message--error mt-1\">{error}</div>}\n        </div>\n    ),\n);\n\nInput.displayName = 'Input';\n"
  },
  {
    "path": "client/src/components/ui/Controls/Radio.tsx",
    "content": "import React, { forwardRef, ReactNode } from 'react';\n\ntype Props<T> = {\n    name: string;\n    value: T;\n    onChange: (e: T) => void;\n    options: { label: string; desc?: ReactNode; value: T }[];\n    disabled?: boolean;\n    error?: string;\n};\n\nexport const Radio = forwardRef<HTMLInputElement, Props<string | boolean | number | undefined>>(\n    ({ disabled, onChange, value, options, name, error, ...rest }, ref) => {\n        const getId = (label: string) => (name ? `${label}_${name}` : label);\n\n        return (\n            <div>\n                {options.map((o) => {\n                    const checked = value === o.value;\n\n                    return (\n                        <label\n                            key={`${getId(o.label)}`}\n                            htmlFor={getId(o.label)}\n                            className=\"custom-control custom-radio\">\n                            <input\n                                id={getId(o.label)}\n                                data-testid={o.value}\n                                type=\"radio\"\n                                className=\"custom-control-input\"\n                                onChange={() => onChange(o.value)}\n                                checked={checked}\n                                disabled={disabled}\n                                ref={ref}\n                                {...rest}\n                            />\n\n                            <span className=\"custom-control-label\">{o.label}</span>\n\n                            {o.desc && <span className=\"checkbox__label-subtitle\">{o.desc}</span>}\n                        </label>\n                    );\n                })}\n                {!disabled && error && <span className=\"form__message form__message--error\">{error}</span>}\n            </div>\n        );\n    },\n);\n\nRadio.displayName = 'Radio';\n"
  },
  {
    "path": "client/src/components/ui/Controls/Select.tsx",
    "content": "import React, { ComponentProps, forwardRef } from 'react';\nimport clsx from 'clsx';\n\ntype SelectProps = ComponentProps<'select'> & {\n    label?: string;\n    error?: string;\n};\n\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(\n    ({ name, label, className, error, children, ...rest }, ref) => (\n        <div className={clsx('form-group', { 'has-error': !!error })}>\n            {label && (\n                <label className=\"form__label\" htmlFor={name}>\n                    {label}\n                </label>\n            )}\n            <div className=\"input-group\">\n                <select className={clsx('form-control custom-select', className)} ref={ref} {...rest}>\n                    {children}\n                </select>\n            </div>\n            {error && <div className=\"form__message form__message--error mt-1\">{error}</div>}\n        </div>\n    ),\n);\n\nSelect.displayName = 'Select';\n"
  },
  {
    "path": "client/src/components/ui/Controls/Textarea.tsx",
    "content": "import React, { ComponentProps, forwardRef } from 'react';\nimport clsx from 'clsx';\nimport { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';\n\ntype Props = ComponentProps<'textarea'> & {\n    className?: string;\n    wrapperClassName?: string;\n    label?: string;\n    desc?: string;\n    error?: string;\n    trimOnBlur?: boolean;\n};\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, Props>(\n    ({ name, label, desc, className, wrapperClassName, error, trimOnBlur, onBlur, ...rest }, ref) => (\n        <div className={clsx('form-group', wrapperClassName, { 'has-error': !!error })}>\n            {label && (\n                <label className={clsx('form__label', { 'form__label--with-desc': !!desc })} htmlFor={name}>\n                    {label}\n                </label>\n            )}\n            {desc && <div className=\"form__desc form__desc--top\">{desc}</div>}\n            <textarea\n                className={clsx(\n                    'form-control form-control--textarea form-control--textarea-small font-monospace',\n                    className,\n                )}\n                ref={ref}\n                onBlur={(e) => {\n                    if (trimOnBlur) {\n                        const normalizedValue = trimLinesAndRemoveEmpty(e.target.value);\n                        rest.onChange(normalizedValue);\n                    }\n                    if (onBlur) {\n                        onBlur(e);\n                    }\n                }}\n                {...rest}\n            />\n            {error && <div className=\"form__message form__message--error\">{error}</div>}\n        </div>\n    ),\n);\n\nTextarea.displayName = 'Textarea';\n"
  },
  {
    "path": "client/src/components/ui/Dropdown.css",
    "content": ".dropdown-item {\n    cursor: pointer;\n}\n\n.dropdown-item.active,\n.dropdown-item:active {\n    background-color: var(--btn-success-bgcolor);\n}\n\n.dropdown-menu {\n    cursor: default;\n}\n\n.dropdown-menu.dropdown-menu--protection {\n    top: calc(100% + 8px);\n    left: 50%;\n    transform: translateX(-50%);\n}\n\n.dropdown-menu.dropdown-menu-arrow.dropdown-menu--protection::before,\n.dropdown-menu.dropdown-menu-arrow.dropdown-menu--protection::after {\n    left: 50%;\n    transform: translateX(-50%);\n}\n\n.dropdown-protection {\n    align-self: stretch;\n    width: 26px;\n    display: flex;\n    position: relative;\n    border: 1px solid #868e96;\n    border-top-right-radius: 3px;\n    border-bottom-right-radius: 3px;\n    border-left: none;\n    cursor: pointer;\n}\n\n.dropdown-protection__toggle {\n    width: 100%;\n    display: block;\n    position: relative;\n    background-color: #868e96;\n    transition: background-color 0.15s ease-in-out;\n}\n\n.dropdown-protection__toggle:hover {\n    background-color: #727b84;\n}\n\n.dropdown-protection__toggle .nav-icon {\n    width: 100%;\n    height: 100%;\n    display: block;\n    position: absolute;\n    top: 0;\n    left: 0;\n    color: var(--white);\n    transition: 0.15s ease-in-out transform;\n    transform-origin: center;\n}\n\n.dropdown-protection.show .nav-icon {\n    transform: rotate(180deg);\n}\n"
  },
  {
    "path": "client/src/components/ui/Dropdown.tsx",
    "content": "import React, { Component } from 'react';\nimport classnames from 'classnames';\nimport { withTranslation } from 'react-i18next';\n\nimport enhanceWithClickOutside from 'react-click-outside';\n\nimport './Dropdown.css';\n\ntype DropdownProps = {\n    label: string;\n    children: React.ReactNode;\n    controlClassName: string;\n    menuClassName: string;\n    baseClassName: string;\n    icon?: string;\n}\n\ntype DropdownState = {\n    isOpen: boolean;\n}\n\nclass Dropdown extends Component<DropdownProps, DropdownState> {\n    state = {\n        isOpen: false,\n    };\n\n    toggleDropdown = () => {\n        this.setState((prevState) => ({ isOpen: !prevState.isOpen }));\n    };\n\n    hideDropdown = () => {\n        this.setState({ isOpen: false });\n    };\n\n    handleClickOutside = () => {\n        if (this.state.isOpen) {\n            this.hideDropdown();\n        }\n    };\n\n    render() {\n        const {\n            label,\n            controlClassName,\n            menuClassName = 'dropdown-menu dropdown-menu-arrow',\n            baseClassName = 'dropdown',\n            icon,\n            children,\n        } = this.props;\n\n        const { isOpen } = this.state;\n\n        const dropdownClass = classnames({\n            [baseClassName]: true,\n            show: isOpen,\n        });\n\n        const dropdownMenuClass = classnames({\n            [menuClassName]: true,\n            show: isOpen,\n        });\n\n        const ariaSettings = isOpen ? 'true' : 'false';\n\n        return (\n            <div className={dropdownClass}>\n                <a className={controlClassName} aria-expanded={ariaSettings} onClick={this.toggleDropdown}>\n                    {icon && (\n                        <svg className=\"nav-icon\">\n                            <use xlinkHref={`#${icon}`} />\n                        </svg>\n                    )}\n                    {label}\n                </a>\n\n                <div className={dropdownMenuClass} onClick={this.hideDropdown}>\n                    {children}\n                </div>\n            </div>\n        );\n    }\n}\n\nexport default withTranslation()(enhanceWithClickOutside(Dropdown));\n"
  },
  {
    "path": "client/src/components/ui/EncryptionTopline.tsx",
    "content": "import React from 'react';\nimport { Trans } from 'react-i18next';\nimport isAfter from 'date-fns/is_after';\nimport addDays from 'date-fns/add_days';\nimport { useSelector } from 'react-redux';\n\nimport Topline from './Topline';\nimport { EMPTY_DATE } from '../../helpers/constants';\nimport { RootState } from '../../initialState';\n\nconst EXPIRATION_ENUM = {\n    VALID: 'VALID',\n    EXPIRED: 'EXPIRED',\n    EXPIRING: 'EXPIRING',\n};\n\nconst EXPIRATION_STATE = {\n    [EXPIRATION_ENUM.EXPIRED]: {\n        toplineType: 'danger',\n        i18nKey: 'topline_expired_certificate',\n    },\n    [EXPIRATION_ENUM.EXPIRING]: {\n        toplineType: 'warning',\n        i18nKey: 'topline_expiring_certificate',\n    },\n};\n\nconst getExpirationFlags = (not_after: any) => {\n    const DAYS_BEFORE_EXPIRATION = 5;\n\n    const now = Date.now();\n    const isExpiring = isAfter(addDays(now, DAYS_BEFORE_EXPIRATION), not_after);\n    const isExpired = isAfter(now, not_after);\n\n    return {\n        isExpiring,\n        isExpired,\n    };\n};\n\nconst getExpirationEnumKey = (not_after: any) => {\n    const { isExpiring, isExpired } = getExpirationFlags(not_after);\n\n    if (isExpired) {\n        return EXPIRATION_ENUM.EXPIRED;\n    }\n\n    if (isExpiring) {\n        return EXPIRATION_ENUM.EXPIRING;\n    }\n\n    return EXPIRATION_ENUM.VALID;\n};\n\nconst EncryptionTopline = () => {\n    const not_after = useSelector((state: RootState) => state.encryption.not_after);\n\n    if (not_after === EMPTY_DATE) {\n        return null;\n    }\n\n    const expirationStateKey = getExpirationEnumKey(not_after);\n\n    if (expirationStateKey === EXPIRATION_ENUM.VALID) {\n        return null;\n    }\n\n    const { toplineType, i18nKey } = EXPIRATION_STATE[expirationStateKey];\n\n    return (\n        <Topline type={toplineType}>\n            <Trans\n                components={[\n                    <a href=\"#encryption\" key=\"0\">\n                        link\n                    </a>,\n                ]}>\n                {i18nKey}\n            </Trans>\n        </Topline>\n    );\n};\n\nexport default EncryptionTopline;\n"
  },
  {
    "path": "client/src/components/ui/Footer.css",
    "content": ".footer {\n    padding: 1rem 0;\n}\n\n.footer__row {\n    display: flex;\n    align-items: center;\n    flex-direction: column;\n}\n\n.footer__column {\n    margin-bottom: 15px;\n}\n\n.footer__column--links {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n}\n\n.footer__column--theme {\n    min-width: 220px;\n    margin-bottom: 0;\n}\n\n.footer__column--language {\n    min-width: 220px;\n    margin-bottom: 0;\n}\n\n.footer__link {\n    display: inline-block;\n    vertical-align: middle;\n    margin-bottom: 8px;\n}\n\n.footer__link--report {\n    position: relative;\n    top: 1px;\n    margin-right: 0;\n}\n\n@media screen and (min-width: 768px) {\n    .footer__copyright {\n        margin-right: 25px;\n    }\n\n    .footer__row {\n        flex-direction: row;\n    }\n\n    .footer__column {\n        margin-bottom: 0;\n    }\n\n    .footer__column--language {\n        min-width: initial;\n        margin-left: 20px;\n    }\n\n    .footer__column--theme {\n        min-width: initial;\n        margin-left: auto;\n    }\n\n    .footer__column--links {\n        display: block;\n    }\n\n    .footer__link {\n        margin: 0 20px 0 0;\n    }\n}\n\n.btn-secondary.footer__theme-button,\n[data-theme='dark'] .btn-secondary.footer__theme-button {\n    height: 38px;\n    border-color: var(--ctrl-select-bgcolor);\n}\n\n.footer__theme-icon {\n    display: inline-block;\n    vertical-align: middle;\n    width: 24px;\n    height: 24px;\n    color: var(--gray-ac);\n}\n\n[data-theme='dark'] .footer__theme-icon {\n    color: var(--mcolor);\n}\n\n.footer__theme-icon--active,\n[data-theme='dark'] .footer__theme-icon--active {\n    color: var(--btn-success-bgcolor);\n}\n\n.footer__themes {\n    margin: 0 auto 24px;\n    text-align: center;\n}\n\n@media screen and (min-width: 768px) {\n    .footer__themes {\n        margin: 0;\n        text-align: left;\n    }\n}\n"
  },
  {
    "path": "client/src/components/ui/Footer.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useDispatch, useSelector } from 'react-redux';\nimport cn from 'classnames';\n\nimport { REPOSITORY, PRIVACY_POLICY_LINK, THEMES } from '../../helpers/constants';\nimport { LANGUAGES } from '../../helpers/twosky';\nimport i18n from '../../i18n';\n\nimport Version from './Version';\nimport './Footer.css';\nimport './Select.css';\n\nimport { setHtmlLangAttr, setUITheme } from '../../helpers/helpers';\n\nimport { changeLanguage, changeTheme } from '../../actions';\nimport { RootState } from '../../initialState';\n\nconst linksData = [\n    {\n        href: REPOSITORY.URL,\n        name: 'homepage',\n    },\n    {\n        href: PRIVACY_POLICY_LINK,\n        name: 'privacy_policy',\n    },\n    {\n        href: REPOSITORY.ISSUES,\n        className: 'btn btn-outline-primary btn-sm footer__link--report',\n        name: 'report_an_issue',\n    },\n];\n\nconst Footer = () => {\n    const { t } = useTranslation();\n    const dispatch = useDispatch();\n\n    const currentTheme = useSelector((state: RootState) => (state.dashboard ? state.dashboard.theme : THEMES.auto));\n    const profileName = useSelector((state: RootState) => (state.dashboard ? state.dashboard.name : ''));\n    const isLoggedIn = profileName !== '';\n    const [currentThemeLocal, setCurrentThemeLocal] = useState(THEMES.auto);\n\n    const getYear = () => {\n        const today = new Date();\n        return today.getFullYear();\n    };\n\n    const onLanguageChange = (language: string) => {\n        i18n.changeLanguage(language);\n        setHtmlLangAttr(language);\n\n        if (isLoggedIn) {\n            dispatch(changeLanguage(language));\n        }\n    };\n\n    const onThemeChange = (value: any) => {\n        if (isLoggedIn) {\n            dispatch(changeTheme(value));\n        } else {\n            setUITheme(value);\n            setCurrentThemeLocal(value);\n        }\n    };\n\n    const renderCopyright = () => (\n        <div className=\"footer__column\">\n            <div className=\"footer__copyright\">\n                {t('copyright')} &copy; {getYear()}{' '}\n                <a\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    href=\"https://link.adtidy.org/forward.html?action=home&from=ui&app=home\">\n                    AdGuard\n                </a>\n            </div>\n        </div>\n    );\n\n    const renderLinks = (linksData: any) =>\n        linksData.map(({ name, href, className = '' }: any) => (\n            <a\n                key={name}\n                href={href}\n                className={cn('footer__link', className)}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\">\n                {t(name)}\n            </a>\n        ));\n\n    const renderThemeButtons = () => {\n        const currentValue = isLoggedIn ? currentTheme : currentThemeLocal;\n\n        const content = {\n            auto: {\n                desc: t('theme_auto_desc'),\n                icon: '#auto',\n                testId: 'theme_auto',\n            },\n            dark: {\n                desc: t('theme_dark_desc'),\n                icon: '#dark',\n                testId: 'theme_dark',\n            },\n            light: {\n                desc: t('theme_light_desc'),\n                icon: '#light',\n                testId: 'theme_light',\n            },\n        };\n\n        return Object.values(THEMES)\n\n            .map((theme: any) => (\n                <button\n                    key={theme}\n                    type=\"button\"\n                    className=\"btn btn-sm btn-secondary footer__theme-button\"\n                    onClick={() => onThemeChange(theme)}\n                    title={content[theme].desc}\n                    data-testid={content[theme].testId}\n                >\n                    <svg className={cn('footer__theme-icon', { 'footer__theme-icon--active': currentValue === theme })}>\n                        <use xlinkHref={content[theme].icon} />\n                    </svg>\n                </button>\n            ));\n    };\n\n    return (\n        <>\n            <footer className=\"footer\">\n                <div className=\"container\">\n                    <div className=\"footer__row\">\n                        <div className=\"footer__column footer__column--links\">{renderLinks(linksData)}</div>\n\n                        <div className=\"footer__column footer__column--theme\">\n                            <div className=\"footer__themes\">\n                                <div className=\"btn-group\">{renderThemeButtons()}</div>\n                            </div>\n                        </div>\n\n                        <div className=\"footer__column footer__column--language\">\n                            <select\n                                className=\"form-control select select--language\"\n                                value={i18n.language}\n                                onChange={(e) => onLanguageChange(e.target.value)}>\n                                {Object.keys(LANGUAGES).map((lang) => (\n                                    <option key={lang} value={lang}>\n                                        {LANGUAGES[lang]}\n                                    </option>\n                                ))}\n                            </select>\n                        </div>\n                    </div>\n                </div>\n            </footer>\n\n            <div className=\"footer\">\n                <div className=\"container\">\n                    <div className=\"footer__row\">\n                        {renderCopyright()}\n\n                        <div className=\"footer__column footer__column--language\">\n                            <Version />\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </>\n    );\n};\n\nexport default Footer;\n"
  },
  {
    "path": "client/src/components/ui/Guide/Guide.tsx",
    "content": "import React, { useState } from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport i18next from 'i18next';\nimport { useSelector } from 'react-redux';\n\nimport { MOBILE_CONFIG_LINKS } from '../../../helpers/constants';\n\nimport Tabs from '../Tabs';\n\nimport { MobileConfigForm } from './MobileConfigForm';\nimport { RootState } from '../../../initialState';\n\ninterface renderLiProps {\n    label?: string;\n    components?: JSX.Element[];\n}\n\nconst renderLi = ({ label, components }: renderLiProps) => (\n    <li key={label}>\n        <Trans\n            components={components?.map((props: any) => {\n                if (React.isValidElement(props)) {\n                    return props;\n                }\n                const {\n                    // eslint-disable-next-line react/prop-types\n                    href,\n                    target = '_blank',\n                    rel = 'noopener noreferrer',\n                    key = '0',\n                } = props;\n\n                return (\n                    <a href={href} target={target} rel={rel} key={key}>\n                        link\n                    </a>\n                );\n            })}>\n            {label}\n        </Trans>\n    </li>\n);\n\nconst getDnsPrivacyList = () => [\n    {\n        title: 'Android',\n        list: [\n            {\n                label: 'setup_dns_privacy_android_1',\n            },\n            {\n                label: 'setup_dns_privacy_android_2',\n                components: [\n                    {\n                        key: 0,\n                        href: 'https://link.adtidy.org/forward.html?action=android&from=ui&app=home',\n                    },\n\n                    <code key=\"1\">text</code>,\n                ],\n            },\n            {\n                label: 'setup_dns_privacy_android_3',\n                components: [\n                    {\n                        key: 0,\n                        href: 'https://getintra.org/',\n                    },\n\n                    <code key=\"1\">text</code>,\n                ],\n            },\n        ],\n    },\n    {\n        title: 'iOS',\n        list: [\n            {\n                label: 'setup_dns_privacy_ios_2',\n                components: [\n                    {\n                        key: 0,\n                        href: 'https://link.adtidy.org/forward.html?action=ios&from=ui&app=home',\n                    },\n\n                    <code key=\"1\">text</code>,\n                ],\n            },\n            {\n                label: 'setup_dns_privacy_ios_1',\n                components: [\n                    {\n                        key: 0,\n                        href: 'https://itunes.apple.com/app/id1452162351',\n                    },\n\n                    <code key=\"1\">text</code>,\n                    {\n                        key: 2,\n                        href: 'https://dnscrypt.info/stamps',\n                    },\n                ],\n            },\n        ],\n    },\n    {\n        title: 'setup_dns_privacy_other_title',\n        list: [\n            {\n                label: 'setup_dns_privacy_other_1',\n            },\n            {\n                label: 'setup_dns_privacy_other_2',\n                components: [\n                    {\n                        key: 0,\n                        href: 'https://github.com/AdguardTeam/dnsproxy',\n                    },\n                ],\n            },\n            {\n                href: 'https://github.com/jedisct1/dnscrypt-proxy',\n                label: 'setup_dns_privacy_other_3',\n                components: [\n                    {\n                        key: 0,\n                        href: 'https://github.com/jedisct1/dnscrypt-proxy',\n                    },\n\n                    <code key=\"1\">text</code>,\n                ],\n            },\n            {\n                label: 'setup_dns_privacy_other_4',\n                components: [\n                    {\n                        key: 0,\n                        href: 'https://support.mozilla.org/kb/firefox-dns-over-https',\n                    },\n\n                    <code key=\"1\">text</code>,\n                ],\n            },\n            {\n                label: 'setup_dns_privacy_other_5',\n                components: [\n                    {\n                        key: 0,\n                        href: 'https://dnscrypt.info/implementations',\n                    },\n                    {\n                        key: 1,\n                        href: 'https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Clients',\n                    },\n                ],\n            },\n        ],\n    },\n];\n\ninterface renderDnsPrivacyListProps {\n    title: string;\n    list: unknown[];\n    renderList?: (...args: unknown[]) => string;\n}\n\nconst renderDnsPrivacyList = ({ title, list }: renderDnsPrivacyListProps) => (\n    <div className=\"tab__paragraph\" key={title}>\n        <strong>\n            <Trans>{title}</Trans>\n        </strong>\n\n        <ul>\n            {list.map(({ label, components, renderComponent = renderLi }: any) =>\n                renderComponent({ label, components }),\n            )}\n        </ul>\n    </div>\n);\n\nconst getTabs = ({ tlsAddress, httpsAddress, showDnsPrivacyNotice, serverName, portHttps, t }: any) => ({\n    Router: {\n        // eslint-disable-next-line react/display-name\n        getTitle: () => (\n            <p>\n                <Trans>install_devices_router_desc</Trans>\n            </p>\n        ),\n        title: 'Router',\n        list: [\n            'install_devices_router_list_1',\n            'install_devices_router_list_2',\n            'install_devices_router_list_3',\n\n            // eslint-disable-next-line react/jsx-key\n            <Trans\n                components={[\n                    <a href=\"#dhcp\" key=\"0\">\n                        link\n                    </a>,\n                ]}>\n                install_devices_router_list_4\n            </Trans>,\n        ],\n    },\n    Windows: {\n        title: 'Windows',\n        list: [\n            'install_devices_windows_list_1',\n            'install_devices_windows_list_2',\n            'install_devices_windows_list_3',\n            'install_devices_windows_list_4',\n            'install_devices_windows_list_5',\n            'install_devices_windows_list_6',\n        ],\n    },\n    macOS: {\n        title: 'macOS',\n        list: [\n            'install_devices_macos_list_1',\n            'install_devices_macos_list_2',\n            'install_devices_macos_list_3',\n            'install_devices_macos_list_4',\n        ],\n    },\n    Android: {\n        title: 'Android',\n        list: [\n            'install_devices_android_list_1',\n            'install_devices_android_list_2',\n            'install_devices_android_list_3',\n            'install_devices_android_list_4',\n            'install_devices_android_list_5',\n        ],\n    },\n    iOS: {\n        title: 'iOS',\n        list: [\n            'install_devices_ios_list_1',\n            'install_devices_ios_list_2',\n            'install_devices_ios_list_3',\n            'install_devices_ios_list_4',\n        ],\n    },\n    dns_privacy: {\n        title: 'dns_privacy',\n        getTitle: function Title() {\n            return (\n                <div title={t('dns_privacy')}>\n                    <div className=\"tab__text\">\n                        {tlsAddress?.length > 0 && (\n                            <div className=\"tab__paragraph\">\n                                <Trans\n                                    values={{ address: tlsAddress[0] }}\n                                    components={[<strong key=\"0\">text</strong>, <code key=\"1\">text</code>]}>\n                                    setup_dns_privacy_1\n                                </Trans>\n                            </div>\n                        )}\n                        {httpsAddress?.length > 0 && (\n                            <div className=\"tab__paragraph\">\n                                <Trans\n                                    values={{ address: httpsAddress[0] }}\n                                    components={[<strong key=\"0\">text</strong>, <code key=\"1\">text</code>]}>\n                                    setup_dns_privacy_2\n                                </Trans>\n                            </div>\n                        )}\n                        {showDnsPrivacyNotice ? (\n                            <div className=\"tab__paragraph\">\n                                <Trans\n                                    components={[\n                                        <a\n                                            href=\"https://github.com/AdguardTeam/AdguardHome/wiki/Encryption\"\n                                            target=\"_blank\"\n                                            rel=\"noopener noreferrer\"\n                                            key=\"0\">\n                                            link\n                                        </a>,\n\n                                        <code key=\"1\">text</code>,\n                                    ]}>\n                                    setup_dns_notice\n                                </Trans>\n                            </div>\n                        ) : (\n                            <>\n                                <div className=\"tab__paragraph\">\n                                    <Trans components={[<p key=\"0\">text</p>]}>setup_dns_privacy_3</Trans>\n                                </div>\n                                {getDnsPrivacyList().map(renderDnsPrivacyList)}\n\n                                <div>\n                                    <strong>\n                                        <Trans>setup_dns_privacy_ioc_mac</Trans>\n                                    </strong>\n                                </div>\n\n                                <div className=\"mb-3\">\n                                    <Trans components={{ highlight: <code /> }}>setup_dns_privacy_4</Trans>\n                                </div>\n\n                                <MobileConfigForm\n                                    initialValues={{\n                                        host: serverName,\n                                        clientId: '',\n                                        protocol: MOBILE_CONFIG_LINKS.DOH,\n                                        port: portHttps,\n                                    }}\n                                />\n                            </>\n                        )}\n                    </div>\n                </div>\n            );\n        },\n    },\n});\n\ninterface renderContentProps {\n    title: string;\n    list: unknown[];\n    getTitle?: (...args: unknown[]) => unknown;\n}\n\nconst renderContent = ({ title, list, getTitle }: renderContentProps) => (\n    <div title={i18next.t(title)}>\n        <div className=\"tab__title\">{i18next.t(title)}</div>\n\n        <div className=\"tab__text\">\n            {getTitle?.()}\n            {list && (\n                <ol>\n                    {list.map((item: any) => (\n                        <li key={item}>\n                            <Trans>{item}</Trans>\n                        </li>\n                    ))}\n                </ol>\n            )}\n        </div>\n    </div>\n);\n\ninterface GuideProps {\n    dnsAddresses?: unknown[];\n}\n\nexport const Guide = ({ dnsAddresses }: GuideProps) => {\n    const { t } = useTranslation();\n\n    const serverName = useSelector((state: RootState) => state.encryption?.server_name);\n\n    const portHttps = useSelector((state: RootState) => state.encryption?.port_https);\n    const tlsAddress = dnsAddresses?.filter((item: any) => item.includes('tls://')) ?? '';\n    const httpsAddress = dnsAddresses?.filter((item: any) => item.includes('https://')) ?? '';\n    const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1;\n\n    const [activeTabLabel, setActiveTabLabel] = useState('Router');\n\n    const tabs = getTabs({\n        tlsAddress,\n        httpsAddress,\n        showDnsPrivacyNotice,\n        serverName,\n        portHttps,\n        t,\n    });\n\n    const activeTab = renderContent(tabs[activeTabLabel]);\n\n    return (\n        <div>\n            <Tabs tabs={tabs} activeTabLabel={activeTabLabel} setActiveTabLabel={setActiveTabLabel}>\n                {activeTab}\n            </Tabs>\n        </div>\n    );\n};\n\nGuide.defaultProps = {\n    dnsAddresses: [],\n};\n"
  },
  {
    "path": "client/src/components/ui/Guide/MobileConfigForm.tsx",
    "content": "import React from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { Controller, useForm } from 'react-hook-form';\nimport i18next from 'i18next';\nimport cn from 'classnames';\n\nimport { getPathWithQueryString } from '../../../helpers/helpers';\nimport { CLIENT_ID_LINK, MOBILE_CONFIG_LINKS, STANDARD_HTTPS_PORT } from '../../../helpers/constants';\nimport { toNumber } from '../../../helpers/form';\nimport {\n    validateConfigClientId,\n    validateServerName,\n    validatePort,\n    validateIsSafePort,\n} from '../../../helpers/validators';\nimport { Input } from '../Controls/Input';\nimport { Select } from '../Controls/Select';\n\nconst getDownloadLink = (host: string, clientId: string, protocol: string, invalid: boolean) => {\n    if (!host || invalid) {\n        return (\n            <button type=\"button\" className=\"btn btn-success btn-standard btn-large disabled\">\n                {i18next.t('download_mobileconfig')}\n            </button>\n        );\n    }\n\n    const linkParams: { host: string; client_id?: string } = { host };\n\n    if (clientId) {\n        linkParams.client_id = clientId;\n    }\n\n    return (\n        <a\n            href={getPathWithQueryString(protocol, linkParams)}\n            className={cn('btn btn-success btn-standard btn-large')}\n            download>\n            {i18next.t('download_mobileconfig')}\n        </a>\n    );\n};\n\ntype FormValues = {\n    host: string;\n    clientId: string;\n    protocol: string;\n    port?: number;\n};\n\ntype Props = {\n    initialValues?: FormValues;\n};\n\nconst defaultFormValues = {\n    host: '',\n    clientId: '',\n    protocol: MOBILE_CONFIG_LINKS.DOT,\n    port: undefined,\n};\n\nexport const MobileConfigForm = ({ initialValues }: Props) => {\n    const { t } = useTranslation();\n\n    const {\n        watch,\n        control,\n        formState: { isValid },\n    } = useForm<FormValues>({\n        mode: 'onBlur',\n        defaultValues: {\n            ...defaultFormValues,\n            ...initialValues,\n        },\n    });\n\n    const protocol = watch('protocol');\n    const host = watch('host');\n    const clientId = watch('clientId');\n    const port = watch('port');\n\n    const getHostName = () => {\n        if (port && port !== STANDARD_HTTPS_PORT && protocol === MOBILE_CONFIG_LINKS.DOH) {\n            return `${host}:${port}`;\n        }\n\n        return host;\n    };\n\n    return (\n        <form onSubmit={(e) => e.preventDefault()}>\n            <div>\n                <div className=\"form__group form__group--settings\">\n                    <div className=\"row\">\n                        <div className=\"col\">\n                            <Controller\n                                name=\"host\"\n                                control={control}\n                                rules={{ validate: validateServerName }}\n                                render={({ field, fieldState }) => (\n                                    <Input\n                                        {...field}\n                                        type=\"text\"\n                                        data-testid=\"mobile_config_host\"\n                                        label={t('dhcp_table_hostname')}\n                                        placeholder={t('form_enter_hostname')}\n                                        error={fieldState.error?.message}\n                                    />\n                                )}\n                            />\n                        </div>\n                        {protocol === MOBILE_CONFIG_LINKS.DOH && (\n                            <div className=\"col\">\n                                <Controller\n                                    name=\"port\"\n                                    control={control}\n                                    rules={{\n                                        validate: {\n                                            range: (value) => validatePort(value) || true,\n                                            safety: (value) => validateIsSafePort(value) || true,\n                                        },\n                                    }}\n                                    render={({ field, fieldState }) => (\n                                        <Input\n                                            {...field}\n                                            type=\"number\"\n                                            data-testid=\"mobile_config_port\"\n                                            label={t('encryption_https')}\n                                            placeholder={t('encryption_https')}\n                                            error={fieldState.error?.message}\n                                            onChange={(e) => {\n                                                const { value } = e.target;\n                                                field.onChange(toNumber(value));\n                                            }}\n                                        />\n                                    )}\n                                />\n                            </div>\n                        )}\n                    </div>\n                </div>\n\n                <div className=\"form__group form__group--settings\">\n                    <label htmlFor=\"clientId\" className=\"form__label form__label--with-desc\">\n                        {t('client_id')}\n                    </label>\n\n                    <div className=\"form__desc form__desc--top\">\n                        <Trans\n                            components={{ a: <a href={CLIENT_ID_LINK} target=\"_blank\" rel=\"noopener noreferrer\" /> }}>\n                            client_id_desc\n                        </Trans>\n                    </div>\n\n                    <Controller\n                        name=\"clientId\"\n                        control={control}\n                        rules={{\n                            validate: validateConfigClientId,\n                        }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"text\"\n                                data-testid=\"mobile_config_client_id\"\n                                placeholder={t('client_id_placeholder')}\n                                error={fieldState.error?.message}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"form__group form__group--settings\">\n                    <Controller\n                        name=\"protocol\"\n                        control={control}\n                        render={({ field }) => (\n                            <Select {...field} label={t('protocol')} data-testid=\"mobile_config_protocol\">\n                                <option value={MOBILE_CONFIG_LINKS.DOT}>{t('dns_over_tls')}</option>\n                                <option value={MOBILE_CONFIG_LINKS.DOH}>{t('dns_over_https')}</option>\n                            </Select>\n                        )}\n                    />\n                </div>\n            </div>\n\n            {getDownloadLink(getHostName(), clientId, protocol, !isValid)}\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/components/ui/Guide/index.ts",
    "content": "export * from './Guide';\n"
  },
  {
    "path": "client/src/components/ui/Icons.css",
    "content": ".icons {\n    display: inline-block;\n    vertical-align: middle;\n}\n\n.icon--24 {\n    --size: 1.5rem;\n\n    width: var(--size);\n    height: var(--size);\n}\n\n.icon--20 {\n    --size: 1.25rem;\n\n    width: var(--size);\n    height: var(--size);\n}\n\n.icon--18 {\n    --size: 1.125rem;\n\n    width: var(--size);\n    height: var(--size);\n}\n\n.icon--15 {\n    --size: 0.95rem;\n\n    width: var(--size);\n    height: var(--size);\n}\n\n.icon--gray {\n    color: var(--gray-a5);\n}\n\n.icon--green {\n    color: var(--green-74);\n}\n\n.icon--disabled {\n    color: var(--gray-d8);\n}\n\n.icon--lightgray {\n    color: var(--gray-8);\n}\n"
  },
  {
    "path": "client/src/components/ui/Icons.tsx",
    "content": "import React from 'react';\n\nimport './Icons.css';\n\nconst Icons = () => (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" className=\"d-none\">\n        <symbol id=\"android\" viewBox=\"0 0 14 16\" fill=\"currentColor\">\n            <path d=\"M11.2 5.2H2.8c-.2 0-.3.1-.3.3v6.7c0 .2.2.3.3.3h1.5v2.3c0 .5.4.9 1 .9s1-.4 1-.9v-2.3h1.4v2.3c0 .5.4.9 1 .9s1-.4 1-.9v-2.3h1.5c.2 0 .3-.1.3-.3V5.5c.1-.2-.1-.3-.3-.3zM1 5.2c-.6 0-1 .4-1 .9V10c0 .5.4.9 1 .9s1-.4 1-.9V6.1c0-.5-.4-.9-1-.9zm12 0c-.6 0-1 .4-1 .9V10c0 .5.4.9 1 .9s1-.4 1-.9V6.1c0-.5-.5-.9-1-.9zM2.9 4.7h8.3c.2 0 .4-.2.3-.4-.3-1.2-1.1-2.3-2.2-2.9L10 .3c0-.1 0-.2-.1-.2-.1-.1-.2-.1-.3 0l-.7 1.2C8.3 1 7.7.9 7 .9s-1.3.1-1.9.4L4.4.1C4.3 0 4.2 0 4.1 0c-.1.1-.1.2 0 .3l.7 1.2c-1.1.6-2 1.6-2.2 2.9-.1.1.1.3.3.3zm6.2-2.1c.2 0 .4.2.4.4s-.2.3-.4.3-.4-.2-.4-.4.2-.3.4-.3zm-4.2 0c.2 0 .4.2.4.4s-.2.4-.4.4-.4-.2-.4-.4.2-.4.4-.4z\" />\n        </symbol>\n\n        <symbol id=\"macos\" viewBox=\"0 0 42 42\" fill=\"currentColor\">\n            <path d=\"M23.091,14.018 L23.091,13.676 L22.028,13.749 C21.727,13.768 21.501,13.832 21.349,13.94 C21.197,14.049 21.121,14.2 21.121,14.393 C21.121,14.581 21.196,14.731 21.347,14.842 C21.497,14.954 21.699,15.009 21.951,15.009 C22.112,15.009 22.263,14.984 22.402,14.935 C22.541,14.886 22.663,14.817 22.765,14.729 C22.867,14.642 22.947,14.538 23.004,14.417 C23.062,14.296 23.091,14.163 23.091,14.018 Z M21,0.25 C9.421,0.25 0.25,9.421 0.25,21 C0.25,32.58 9.421,41.75 21,41.75 C32.579,41.75 41.75,32.58 41.75,21 C41.75,9.421 32.58,0.25 21,0.25 Z M25.028,12.549 C25.126,12.274 25.264,12.038 25.443,11.842 C25.622,11.646 25.837,11.495 26.089,11.389 C26.341,11.283 26.622,11.23 26.931,11.23 C27.21,11.23 27.462,11.272 27.686,11.355 C27.911,11.438 28.103,11.55 28.264,11.691 C28.425,11.832 28.553,11.996 28.647,12.184 C28.741,12.372 28.797,12.571 28.816,12.78 L27.983,12.78 C27.962,12.665 27.924,12.557 27.87,12.458 C27.816,12.359 27.745,12.273 27.657,12.2 C27.568,12.127 27.464,12.07 27.345,12.029 C27.225,11.987 27.091,11.967 26.94,11.967 C26.763,11.967 26.602,12.003 26.459,12.074 C26.315,12.145 26.192,12.246 26.09,12.376 C25.988,12.506 25.909,12.665 25.853,12.851 C25.796,13.038 25.768,13.245 25.768,13.473 C25.768,13.709 25.796,13.921 25.853,14.107 C25.909,14.294 25.989,14.451 26.093,14.58 C26.196,14.709 26.321,14.808 26.466,14.876 C26.611,14.944 26.771,14.979 26.945,14.979 C27.23,14.979 27.462,14.912 27.642,14.778 C27.822,14.644 27.938,14.448 27.992,14.19 L28.826,14.19 C28.802,14.418 28.739,14.626 28.637,14.814 C28.535,15.002 28.403,15.162 28.241,15.295 C28.078,15.428 27.887,15.531 27.667,15.603 C27.447,15.675 27.205,15.712 26.942,15.712 C26.63,15.712 26.349,15.66 26.096,15.557 C25.844,15.454 25.627,15.305 25.447,15.112 C25.267,14.919 25.128,14.684 25.03,14.407 C24.932,14.13 24.883,13.819 24.883,13.472 C24.881,13.133 24.93,12.825 25.028,12.549 Z M13.175,11.287 L14.009,11.287 L14.009,12.028 L14.025,12.028 C14.076,11.905 14.143,11.794 14.225,11.698 C14.307,11.601 14.401,11.519 14.509,11.45 C14.616,11.381 14.735,11.329 14.863,11.293 C14.992,11.257 15.128,11.239 15.27,11.239 C15.576,11.239 15.835,11.312 16.045,11.458 C16.256,11.604 16.406,11.814 16.494,12.088 L16.515,12.088 C16.571,11.956 16.645,11.838 16.736,11.734 C16.827,11.63 16.932,11.54 17.05,11.466 C17.168,11.392 17.298,11.336 17.439,11.297 C17.58,11.258 17.728,11.239 17.884,11.239 C18.099,11.239 18.294,11.273 18.47,11.342 C18.646,11.411 18.796,11.507 18.921,11.632 C19.046,11.757 19.142,11.909 19.209,12.087 C19.276,12.265 19.31,12.463 19.31,12.681 L19.31,15.662 L18.44,15.662 L18.44,12.89 C18.44,12.603 18.366,12.38 18.218,12.223 C18.071,12.066 17.86,11.987 17.586,11.987 C17.452,11.987 17.329,12.011 17.217,12.058 C17.106,12.105 17.009,12.171 16.929,12.256 C16.848,12.34 16.785,12.442 16.74,12.56 C16.694,12.678 16.671,12.807 16.671,12.947 L16.671,15.662 L15.813,15.662 L15.813,12.818 C15.813,12.692 15.793,12.578 15.754,12.476 C15.715,12.374 15.66,12.287 15.587,12.214 C15.515,12.141 15.426,12.086 15.323,12.047 C15.219,12.008 15.103,11.988 14.974,11.988 C14.84,11.988 14.716,12.013 14.601,12.063 C14.487,12.113 14.389,12.182 14.307,12.27 C14.225,12.359 14.161,12.463 14.116,12.584 C14.072,12.704 14,12.836 14,12.978 L14,15.661 L13.175,15.661 L13.175,11.287 Z M15.068,32.226 C11.243,32.226 8.844,29.568 8.844,25.326 C8.844,21.084 11.243,18.417 15.068,18.417 C18.893,18.417 21.283,21.084 21.283,25.326 C21.283,29.567 18.893,32.226 15.068,32.226 Z M22.15,15.651 C22.009,15.687 21.865,15.705 21.717,15.705 C21.499,15.705 21.3,15.674 21.119,15.612 C20.937,15.55 20.782,15.463 20.652,15.35 C20.522,15.237 20.42,15.101 20.348,14.941 C20.275,14.781 20.239,14.603 20.239,14.407 C20.239,14.023 20.382,13.723 20.668,13.507 C20.954,13.291 21.368,13.165 21.911,13.13 L23.091,13.062 L23.091,12.724 C23.091,12.472 23.011,12.279 22.851,12.148 C22.691,12.017 22.465,11.951 22.172,11.951 C22.054,11.951 21.943,11.966 21.841,11.995 C21.739,12.025 21.649,12.067 21.571,12.122 C21.493,12.177 21.428,12.243 21.378,12.32 C21.327,12.396 21.292,12.482 21.273,12.576 L20.455,12.576 C20.46,12.383 20.508,12.204 20.598,12.04 C20.688,11.876 20.81,11.734 20.965,11.613 C21.12,11.492 21.301,11.398 21.511,11.331 C21.721,11.264 21.949,11.23 22.196,11.23 C22.462,11.23 22.703,11.263 22.919,11.331 C23.135,11.399 23.32,11.494 23.473,11.619 C23.626,11.744 23.744,11.894 23.827,12.07 C23.91,12.246 23.952,12.443 23.952,12.66 L23.952,15.661 L23.119,15.661 L23.119,14.932 L23.098,14.932 C23.036,15.05 22.958,15.157 22.863,15.252 C22.767,15.347 22.66,15.429 22.541,15.496 C22.421,15.563 22.291,15.615 22.15,15.651 Z M27.653,32.226 C24.736,32.226 22.753,30.698 22.615,28.299 L24.514,28.299 C24.662,29.67 25.987,30.578 27.802,30.578 C29.543,30.578 30.794,29.67 30.794,28.429 C30.794,27.355 30.034,26.706 28.275,26.262 L26.561,25.836 C24.097,25.225 22.977,24.104 22.977,22.261 C22.977,19.992 24.959,18.417 27.784,18.417 C30.544,18.417 32.47,20.001 32.544,22.279 L30.664,22.279 C30.534,20.908 29.414,20.065 27.746,20.065 C26.088,20.065 24.94,20.917 24.94,22.149 C24.94,23.121 25.662,23.696 27.422,24.14 L28.867,24.501 C31.618,25.168 32.748,26.252 32.748,28.197 C32.747,30.679 30.784,32.226 27.653,32.226 Z M15.068,20.12 C12.447,20.12 10.808,22.13 10.808,25.325 C10.808,28.511 12.447,30.521 15.068,30.521 C17.68,30.521 19.328,28.511 19.328,25.325 C19.329,22.13 17.68,20.12 15.068,20.12 Z\" />\n        </symbol>\n\n        <symbol id=\"windows\" viewBox=\"0 0 14 16\" fill=\"currentColor\">\n            <path d=\"M0 13.7L6.5 14.6 6.5 8.4 0 8.4z\" />\n\n            <path d=\"M0 7.6L6.5 7.6 6.5 1.3 0 2.2z\" />\n\n            <path d=\"M7.2 14.7L15.9 15.9 15.9 8.4 15.9 8.4 7.2 8.4z\" />\n\n            <path d=\"M7.2 1.2L7.2 7.6 15.9 7.6 15.9 0z\" />\n        </symbol>\n\n        <symbol id=\"ios\" viewBox=\"0 0 512 512\" fill=\"currentColor\">\n            <path d=\"M395.748 272.046c-.646-64.841 52.88-95.938 55.271-97.483-30.075-44.01-76.925-50.039-93.62-50.736-39.871-4.037-77.798 23.474-98.033 23.474-20.184 0-51.409-22.877-84.476-22.276-43.458.646-83.529 25.269-105.906 64.19-45.152 78.35-11.563 194.42 32.445 257.963 21.504 31.104 47.146 66.038 80.813 64.79 32.421-1.294 44.681-20.979 83.878-20.979 39.196 0 50.215 20.979 84.524 20.335 34.888-.648 56.991-31.699 78.347-62.898 24.694-36.084 34.862-71.019 35.462-72.812-.775-.354-68.031-26.119-68.705-103.568zM331.28 81.761C349.149 60.082 361.21 30.005 357.92 0c-25.739 1.048-56.938 17.145-75.405 38.775-16.57 19.188-31.075 49.813-27.188 79.218 28.734 2.242 58.065-14.602 75.953-36.232z\" />\n        </symbol>\n\n        <symbol id=\"router\" viewBox=\"0 0 30 30\" fill=\"currentColor\">\n            <path d=\"M17.646 2.332a1 1 0 0 0-.697 1.719 6.984 6.984 0 0 1 0 9.898 1 1 0 1 0 1.414 1.414c3.507-3.506 3.507-9.22 0-12.726a1 1 0 0 0-.717-.305zm-12.662.654A1 1 0 0 0 4 4v14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h22a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H12V9a1 1 0 0 0-1.016-1.014A1 1 0 0 0 10 9v9H6V4a1 1 0 0 0-1.016-1.014zm9.834 2.176a1 1 0 0 0-.697 1.717 2.985 2.985 0 0 1 0 4.242 1 1 0 1 0 1.414 1.414 5.014 5.014 0 0 0 0-7.07 1 1 0 0 0-.717-.303zM5 21a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm4 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z\" />\n        </symbol>\n\n        <symbol\n            id=\"edit\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\" />\n\n            <path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\" />\n        </symbol>\n\n        <symbol\n            id=\"delete\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <path d=\"m3 6h2 16\" />\n\n            <path d=\"m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n\n            <path d=\"m10 11v6\" />\n\n            <path d=\"m14 11v6\" />\n        </symbol>\n\n        <symbol\n            id=\"back\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <path d=\"m19 12h-14\" />\n\n            <path d=\"m12 19-7-7 7-7\" />\n        </symbol>\n\n        <symbol\n            id=\"dashboard\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <path d=\"m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z\" />\n\n            <path d=\"m9 22v-10h6v10\" />\n        </symbol>\n\n        <symbol\n            id=\"filters\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <path d=\"m22 3h-20l8 9.46v6.54l4 2v-8.54z\" />\n        </symbol>\n\n        <symbol\n            id=\"log\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <path d=\"m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z\" />\n\n            <path d=\"m14 2v6h6\" />\n\n            <path d=\"m16 13h-8\" />\n\n            <path d=\"m16 17h-8\" />\n\n            <path d=\"m10 9h-1-1\" />\n        </symbol>\n\n        <symbol\n            id=\"setup\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\n\n            <path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"></path>\n\n            <line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"17\"></line>\n        </symbol>\n\n        <symbol\n            id=\"settings\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <circle cx=\"12\" cy=\"12\" r=\"3\" />\n\n            <path d=\"m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z\" />\n        </symbol>\n\n        <symbol\n            id=\"refresh\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <polyline points=\"23 4 23 10 17 10\"></polyline>\n\n            <polyline points=\"1 20 1 14 7 14\"></polyline>\n\n            <path d=\"M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15\"></path>\n        </symbol>\n\n        <symbol\n            id=\"dns_privacy\"\n            viewBox=\"0 0 30 30\"\n            stroke=\"none\"\n            fill=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\">\n            <path d=\"M15 3C10.57 3 6.701 5.419 4.623 9h2.39a10.063 10.063 0 0 1 4.05-3.19c-.524.89-.961 1.973-1.3 3.19h2.108c.79-2.459 1.998-4 3.129-4s2.339 1.541 3.129 4h2.107c-.338-1.217-.774-2.3-1.299-3.19A10.062 10.062 0 0 1 22.989 9h2.389C23.298 5.419 19.43 3 15 3zm7.035 9.129c-1.372 0-2.264.73-2.264 1.842 0 .896.538 1.463 1.579 1.66l.75.15c.65.13.898.3.898.615 0 .375-.37.635-.91.635-.6 0-1.014-.265-1.049-.68h-1.38c.023 1.097.93 1.776 2.37 1.776 1.491 0 2.399-.717 2.399-1.904 0-.903-.504-1.412-1.63-1.63l-.734-.142c-.6-.118-.851-.3-.851-.611 0-.378.336-.62.844-.62.509 0 .891.28.923.682h1.336c-.024-1.053-.948-1.773-2.28-1.773zm-16.185.148v5.696h2.39c1.712 0 2.662-1.033 2.662-2.903 0-1.779-.966-2.793-2.662-2.793H5.85zm6.933.004v5.692h1.373v-3.235h.076l2.377 3.235h1.149V12.28h-1.373v3.203h-.076l-2.372-3.203h-1.154zm-5.486 1.16h.682c.912 0 1.449.596 1.449 1.657 0 1.128-.51 1.713-1.45 1.713h-.681v-3.37zM4.623 21C6.701 24.581 10.57 27 15 27c4.43 0 8.299-2.419 10.377-6h-2.389a10.063 10.063 0 0 1-4.049 3.19c.524-.89.96-1.973 1.297-3.19H18.13c-.79 2.459-1.996 4-3.127 4-1.131 0-2.339-1.541-3.129-4h-2.11c.339 1.217.776 2.3 1.3 3.19A10.056 10.056 0 0 1 7.013 21h-2.39z\"></path>\n        </symbol>\n\n        <symbol id=\"question\" width=\"20px\" height=\"20px\">\n            <g\n                transform=\"translate(-982.000000, -454.000000) translate(416.000000, 440.000000) translate(564.000000, 12.000000)\"\n                fill=\"none\"\n                fillRule=\"evenodd\">\n                <circle stroke=\"currentColor\" strokeWidth=\"1.5\" cx=\"12\" cy=\"12\" r=\"9.25\" />\n\n                <path\n                    d=\"M11.011 13.915c0-.627.076-1.126.227-1.498.15-.372.427-.738.829-1.1.401-.36.669-.653.802-.88.133-.226.2-.464.2-.715 0-.757-.346-1.136-1.039-1.136-.329 0-.592.102-.79.306-.197.204-.3.485-.309.843H9c.009-.856.283-1.525.822-2.01.54-.483 1.276-.725 2.208-.725.941 0 1.671.23 2.19.689.52.46.78 1.108.78 1.945 0 .381-.084.74-.253 1.079-.169.338-.464.714-.886 1.126l-.54.517a1.85 1.85 0 00-.578 1.15l-.027.41H11.01zm-.193 2.063c0-.3.101-.547.303-.742.202-.195.46-.292.776-.292.315 0 .574.097.776.292a.988.988 0 01.303.742.98.98 0 01-.297.733c-.197.193-.458.289-.782.289s-.585-.096-.783-.289a.98.98 0 01-.296-.733z\"\n                    fill=\"currentColor\"\n                />\n            </g>\n        </symbol>\n\n        <symbol id=\"network\" viewBox=\"0 0 50 50\" fill=\"currentColor\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n            <path d=\"M 25 7 C 15.941406 7 7.339844 10.472656 0.78125 16.773438 L 0.0625 17.464844 L 5.59375 23.230469 L 6.320313 22.539063 C 11.378906 17.679688 18.015625 15 25 15 C 31.984375 15 38.621094 17.679688 43.683594 22.539063 L 44.40625 23.230469 L 49.941406 17.464844 L 49.21875 16.769531 C 42.660156 10.46875 34.058594 7 25 7 Z M 25 19 C 19.046875 19 13.394531 21.28125 9.085938 25.421875 L 8.363281 26.113281 L 13.921875 31.90625 L 14.644531 31.210938 C 17.464844 28.496094 21.144531 27 25 27 C 28.855469 27 32.535156 28.496094 35.355469 31.210938 L 36.078125 31.90625 L 41.636719 26.113281 L 40.917969 25.421875 C 36.605469 21.28125 30.953125 19 25 19 Z M 25 31 C 22.15625 31 19.453125 32.089844 17.390625 34.074219 L 16.671875 34.765625 L 25 43.441406 L 33.328125 34.765625 L 32.609375 34.074219 C 30.546875 32.089844 27.84375 31 25 31 Z\" />\n        </symbol>\n\n        <symbol id=\"location\" viewBox=\"0 0 24 24\" fill=\"currentColor\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n            <path d=\"M12,2C8.134,2,5,5.134,5,9c0,5,7,13,7,13s7-8,7-13C19,5.134,15.866,2,12,2z M12,11.5c-1.381,0-2.5-1.119-2.5-2.5 c0-1.381,1.119-2.5,2.5-2.5s2.5,1.119,2.5,2.5C14.5,10.381,13.381,11.5,12,11.5z\" />\n        </symbol>\n\n        <symbol\n            id=\"cross\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n\n            <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n        </symbol>\n\n        <symbol\n            id=\"plus\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"></line>\n\n            <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>\n        </symbol>\n\n        <symbol\n            id=\"update\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.5\">\n            <path d=\"M19 12c0-3.866-3.134-7-7-7-2.1476 0-4.0692.967-5.3533 2.4895M5.0007 12c0 3.866 3.134 7 7 7 2.1476 0 4.0692-.967 5.3533-2.4895\" />\n\n            <path d=\"M3 12.849L5.017 11 7 13M21 11.151L18.983 13 17 11\" />\n        </symbol>\n\n        <symbol\n            id=\"privacy\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.5\">\n            <path d=\"M2.5866 12.3095C6.794 7.3586 11.1651 5.7805 15.7 7.5755m2.4625 1.4893c1.0875.8216 2.1525 1.9173 3.2428 3.1815M2.6235 12.2657c2.0598 2.3114 3.8824 3.8055 5.4679 4.4823M11.0093 17.5762c3.5788.4657 6.8214-1.2685 10.3071-5.3102M4.33 21.33L20.319 2.697\" />\n\n            <path d=\"M10.2684 14.379c-2.5431-2.023 0-6.5648 3.6615-4.183\" />\n        </symbol>\n\n        <symbol\n            id=\"lock\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.5\">\n            <path d=\"M17.3386 18.4244c-1.0523 1.6302-2.7179 2.8256-5.3388 2.8256-2.6208 0-4.2863-1.1953-5.3386-2.8255-1.754-2.7172-1.8987-6.5446-1.9098-8.3217C7.0486 9.172 9.743 8.75 11.9998 8.75c2.2571 0 4.9519.4222 7.2492 1.3528-.0116 1.777-.1563 5.6044-1.9104 8.3216z\" />\n\n            <path d=\"M12 14v2\" />\n\n            <path d=\"M8 9c0-2.688.0284-6 3.8654-6S16 6.2023 16 9\" />\n        </symbol>\n\n        <symbol\n            id=\"list\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.5\">\n            <path d=\"M4 6h16M4 10h13M4 14h16M4 18h8.591\" />\n        </symbol>\n\n        <symbol\n            id=\"detailed_list\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.5\">\n            <path d=\"M4 6h16M4 10h16V6H4zM4 14h16v4H4zM4 18h8.591\" />\n        </symbol>\n\n        <symbol\n            id=\"magnifier\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.5\">\n            <circle id=\"Oval\" cx=\"9.5\" cy=\"9.5\" r=\"5.5\"></circle>\n\n            <path d=\"M14,14 L19,19\" id=\"Line\"></path>\n        </symbol>\n\n        <symbol\n            id=\"arrow-left\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.5\">\n            <svg width=\"24px\" height=\"24px\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n                <svg width=\"24\" height=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n                    <path\n                        d=\"M14 6l-6 6 6 6\"\n                        stroke=\"#888\"\n                        strokeWidth=\"1.5\"\n                        fill=\"none\"\n                        fillRule=\"evenodd\"\n                        strokeLinecap=\"round\"\n                    />\n                </svg>\n            </svg>\n        </symbol>\n\n        <symbol id=\"arrow-down\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n            <svg xmlns=\"http://www.w3.org/2000/svg\">\n                <path\n                    fillRule=\"evenodd\"\n                    d=\"M6.2 8.2a.64.64 0 0 1 .94 0L12 13.32l4.86-5.1a.64.64 0 0 1 .94 0c.27.27.27.71 0 .98l-5.33 5.6a.64.64 0 0 1-.94 0L6.2 9.2a.72.72 0 0 1 0-.98Z\"\n                    clipRule=\"evenodd\"\n                />\n            </svg>\n        </symbol>\n\n        <symbol\n            id=\"arrow-right\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.5\">\n            <svg width=\"24\" height=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n                <path\n                    d=\"M10 6l6 6-6 6\"\n                    stroke=\"#888\"\n                    strokeWidth=\"1.5\"\n                    fill=\"none\"\n                    fillRule=\"evenodd\"\n                    strokeLinecap=\"round\"\n                />\n            </svg>\n        </symbol>\n\n        <symbol\n            id=\"info\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"2\">\n            <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 128 128\">\n                <path d=\"M64 1C29.3 1 1 29.3 1 64s28.3 63 63 63 63-28.3 63-63S98.7 1 64 1zm0 118C33.7 119 9 94.3 9 64S33.7 9 64 9s55 24.7 55 55-24.7 55-55 55z\" />\n\n                <path d=\"M60 54.5h8v40h-8zM60 35.5h8v8h-8z\" />\n            </svg>\n        </symbol>\n\n        <symbol id=\"auto\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\">\n            <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z\"\n                stroke=\"currentColor\"\n                strokeWidth=\"1.5\"\n            />\n\n            <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M12 3V21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3Z\"\n                fill=\"currentColor\"\n                stroke=\"currentColor\"\n                strokeWidth=\"1.5\"\n            />\n        </symbol>\n\n        <symbol id=\"dark\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\">\n            <path\n                d=\"M3.80737 15.731L3.9895 15.0034C3.71002 14.9335 3.41517 15.0298 3.23088 15.2512C3.0466 15.4727 3.00545 15.7801 3.12501 16.0422L3.80737 15.731ZM14.1926 3.26892L14.3747 2.54137C14.0953 2.47141 13.8004 2.56772 13.6161 2.78917C13.4318 3.01062 13.3907 3.31806 13.5102 3.58018L14.1926 3.26892ZM12 20.2499C8.66479 20.2499 5.79026 18.2708 4.48974 15.4197L3.12501 16.0422C4.66034 19.4081 8.05588 21.7499 12 21.7499V20.2499ZM20.25 11.9999C20.25 16.5563 16.5563 20.2499 12 20.2499V21.7499C17.3848 21.7499 21.75 17.3847 21.75 11.9999H20.25ZM14.0105 3.99647C17.5955 4.89391 20.25 8.13787 20.25 11.9999H21.75C21.75 7.43347 18.6114 3.60193 14.3747 2.54137L14.0105 3.99647ZM13.5102 3.58018C13.9851 4.6211 14.25 5.77857 14.25 6.99995H15.75C15.75 5.5595 15.4371 4.1901 14.875 2.95766L13.5102 3.58018ZM14.25 6.99995C14.25 11.5563 10.5563 15.2499 5.99999 15.2499V16.7499C11.3848 16.7499 15.75 12.3847 15.75 6.99995H14.25ZM5.99999 15.2499C5.30559 15.2499 4.63225 15.1643 3.9895 15.0034L3.62525 16.4585C4.38616 16.649 5.18181 16.7499 5.99999 16.7499V15.2499Z\"\n                fill=\"currentColor\"\n            />\n        </symbol>\n\n        <symbol id=\"light\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\">\n            <path\n                d=\"M12 3.75C16.5563 3.75 20.25 7.44365 20.25 12H21.75C21.75 6.61522 17.3848 2.25 12 2.25V3.75ZM20.25 12C20.25 16.5563 16.5563 20.25 12 20.25V21.75C17.3848 21.75 21.75 17.3848 21.75 12H20.25ZM12 20.25C7.44365 20.25 3.75 16.5563 3.75 12H2.25C2.25 17.3848 6.61522 21.75 12 21.75V20.25ZM3.75 12C3.75 7.44365 7.44365 3.75 12 3.75V2.25C6.61522 2.25 2.25 6.61522 2.25 12H3.75Z\"\n                fill=\"currentColor\"\n            />\n\n            <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14C13.1046 14 14 13.1046 14 12C13.9987 10.896 13.104 10.0013 12 10Z\"\n                fill=\"currentColor\"\n            />\n        </symbol>\n\n        <symbol id=\"chevron-down\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\n            <g fill=\"none\" fillRule=\"evenodd\">\n                <path d=\"M0 0h24v24H0z\" fill=\"#878787\" fillOpacity=\".01\" />\n\n                <path\n                    stroke=\"currentColor\"\n                    strokeWidth=\"1.5\"\n                    strokeLinecap=\"round\"\n                    d=\"M8.036 10.93l3.93 4.07 4.068-3.93\"\n                />\n            </g>\n        </symbol>\n\n        <symbol\n            id=\"calendar\"\n            fill=\"none\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            width=\"24\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\">\n            <rect x=\"4\" y=\"5.5\" width=\"16\" height=\"14\" rx=\"3\" />\n\n            <path d=\"M12 4V7\" />\n\n            <path d=\"M8 4L8 7\" />\n\n            <path d=\"M16 4V7\" />\n\n            <path d=\"M9.7397 15.5V11L8 13\" />\n\n            <path d=\"M14.7397 15.5V11L13 13\" />\n        </symbol>\n\n        <symbol\n            id=\"watch\"\n            fill=\"none\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            width=\"24\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\">\n            <circle cx=\"12\" cy=\"12\" r=\"9\" />\n\n            <path d=\"M16.1215 12.1213H11.8789V7.87866\" />\n        </symbol>\n\n        <symbol id=\"bullets\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\n            <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M12 7C11.1716 7 10.5 6.32843 10.5 5.5C10.5 4.67157 11.1716 4 12 4C12.8284 4 13.5 4.67157 13.5 5.5C13.5 6.32843 12.8284 7 12 7Z\"\n                fill=\"currentColor\"\n            />\n\n            <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M12 13.5C11.1716 13.5 10.5 12.8284 10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5Z\"\n                fill=\"currentColor\"\n            />\n\n            <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M12 20C11.1716 20 10.5 19.3284 10.5 18.5C10.5 17.6716 11.1716 17 12 17C12.8284 17 13.5 17.6716 13.5 18.5C13.5 19.3284 12.8284 20 12 20Z\"\n                fill=\"currentColor\"\n            />\n        </symbol>\n\n        <symbol\n            id=\"check\"\n            fill=\"none\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\">\n            <path d=\"M5 11.7665L10.5878 17L19 8\" />\n        </symbol>\n    </svg>\n);\n\nexport default Icons;\n"
  },
  {
    "path": "client/src/components/ui/Line.css",
    "content": ".line__tooltip {\n    padding: 2px 10px 7px;\n    line-height: 1.1;\n    color: var(--white);\n    background-color: var(--gray-3);\n    border-radius: 4px;\n    opacity: 90%;\n}\n\n.line__tooltip-text {\n    font-size: 0.7rem;\n}\n\n.card-chart-bg {\n    color: var(--black);\n}\n\n.card-chart-bg path[d^='M0,32'] {\n    transform: translateY(32px);\n}\n"
  },
  {
    "path": "client/src/components/ui/Line.tsx",
    "content": "import React from 'react';\nimport { ResponsiveLine } from '@nivo/line';\nimport addDays from 'date-fns/add_days';\nimport subDays from 'date-fns/sub_days';\nimport subHours from 'date-fns/sub_hours';\nimport dateFormat from 'date-fns/format';\nimport round from 'lodash/round';\nimport { useSelector } from 'react-redux';\nimport './Line.css';\n\nimport { msToDays, msToHours } from '../../helpers/helpers';\nimport { TIME_UNITS } from '../../helpers/constants';\nimport { RootState } from '../../initialState';\n\ninterface LineProps {\n    data: any[];\n    color?: string;\n    width?: number;\n    height?: number;\n}\n\nconst Line = ({ data, color = 'black' }: LineProps) => {\n    const interval = useSelector((state: RootState) => state.stats.interval);\n\n    const timeUnits = useSelector((state: RootState) => state.stats.timeUnits);\n\n    return (\n        <ResponsiveLine\n            enableArea\n            animate\n            enableSlices=\"x\"\n            curve=\"linear\"\n            colors={[color]}\n            data={data}\n            theme={{\n                crosshair: {\n                    line: {\n                        stroke: 'currentColor',\n                        strokeWidth: 1,\n                        strokeOpacity: 0.5,\n                    },\n                },\n            }}\n            xScale={{\n                type: 'linear',\n                min: 0,\n                max: 'auto',\n            }}\n            crosshairType=\"x\"\n            axisLeft={null}\n            axisBottom={null}\n            enableGridX={null}\n            enableGridY={null}\n            enablePoints={null}\n            xFormat={(x: number) => {\n                if (timeUnits === TIME_UNITS.HOURS) {\n                    const hoursAgo = msToHours(interval) - x - 1;\n                    return dateFormat(subHours(Date.now(), hoursAgo), 'D MMM HH:00');\n                }\n\n                const daysAgo = subDays(Date.now(), msToDays(interval) - 1);\n\n                return dateFormat(addDays(daysAgo, x), 'D MMM YYYY');\n            }}\n            yFormat={(y: number) => round(y, 2)}\n            sliceTooltip={(slice) => {\n                const { xFormatted, yFormatted } = slice.slice.points[0].data;\n\n                return (\n                    <div className=\"line__tooltip\">\n                        <span className=\"line__tooltip-text\">\n                            <strong>{yFormatted}</strong>\n\n                            <br />\n\n                            <small>{xFormatted}</small>\n                        </span>\n                    </div>\n                );\n            }}\n        />\n    );\n};\n\nexport default Line;\n"
  },
  {
    "path": "client/src/components/ui/Loading.css",
    "content": ".loading {\n    position: relative;\n    z-index: 101;\n    opacity: 0;\n    animation: opacity 0.2s linear 0.2s forwards;\n}\n\n.loading:before {\n    content: '';\n    position: fixed;\n    top: 0;\n    left: 0;\n    z-index: 100;\n    width: 100%;\n    min-height: 100vh;\n    background-color: var(--loading-bg);\n}\n\n.loading:after {\n    content: '';\n    position: fixed;\n    z-index: 101;\n    left: 50%;\n    top: 50%;\n    width: 40px;\n    height: 40px;\n    margin-top: -20px;\n    margin-left: -20px;\n    background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E');\n    will-change: transform;\n    animation: clockwise 2s linear infinite;\n}\n\n@keyframes opacity {\n    0% {\n        opacity: 0;\n    }\n\n    100% {\n        opacity: 0.8;\n    }\n}\n\n@keyframes clockwise {\n    0% {\n        transform: rotate(0deg);\n    }\n\n    100% {\n        transform: rotate(360deg);\n    }\n}\n"
  },
  {
    "path": "client/src/components/ui/Loading.tsx",
    "content": "import React from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport './Loading.css';\n\ninterface LoadingProps {\n    className?: string;\n    text?: string;\n}\n\nconst Loading = ({ className, text }: LoadingProps) => {\n    const { t } = useTranslation();\n\n    return <div className={classNames('loading', className)}>{t(text)}</div>;\n};\n\nexport default Loading;\n"
  },
  {
    "path": "client/src/components/ui/LogsSearchLink.tsx",
    "content": "import React from 'react';\n\nimport { Link } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport { getLogsUrlParams } from '../../helpers/helpers';\nimport { MENU_URLS } from '../../helpers/constants';\n\ninterface LogsSearchLinkProps {\n    children: string | number | React.ReactElement;\n    search?: string;\n    response_status?: string;\n    link?: string;\n}\n\nconst LogsSearchLink = ({\n    search = '',\n    response_status = '',\n    children,\n    link = MENU_URLS.logs,\n}: LogsSearchLinkProps) => {\n    const { t } = useTranslation();\n\n    const to =\n        link === MENU_URLS.logs\n            ? `${MENU_URLS.logs}${getLogsUrlParams(search && `\"${search}\"`, response_status)}`\n            : link;\n\n    return (\n        <Link to={to} tabIndex={0} title={t('click_to_view_queries')} aria-label={t('click_to_view_queries')}>\n            {children}\n        </Link>\n    );\n};\n\nexport default LogsSearchLink;\n"
  },
  {
    "path": "client/src/components/ui/Modal.css",
    "content": ".ReactModal__Overlay {\n    -webkit-perspective: 600;\n    perspective: 600;\n    opacity: 0;\n    overflow-x: hidden;\n    overflow-y: auto;\n    background-color: rgba(0, 0, 0, 0.5);\n    z-index: 104;\n}\n\n.ReactModal__Overlay--after-open {\n    opacity: 1;\n    transition: opacity 150ms ease-out;\n    background-color: var(--modal-overlay-bgcolor) !important;\n}\n\n.ReactModal__Content {\n    -webkit-transform: scale(0.5) rotateX(-30deg);\n    transform: scale(0.5) rotateX(-30deg);\n}\n\n.ReactModal__Content--after-open {\n    -webkit-transform: scale(1) rotateX(0deg);\n    transform: scale(1) rotateX(0deg);\n    transition: all 150ms ease-in;\n}\n\n.ReactModal__Overlay--before-close {\n    opacity: 0;\n}\n\n.ReactModal__Content--before-close {\n    -webkit-transform: scale(0.5) rotateX(30deg);\n    transform: scale(0.5) rotateX(30deg);\n    transition: all 150ms ease-in;\n}\n\n.ReactModal__Content.modal-dialog {\n    border: none;\n    background-color: transparent;\n}\n\n@media (min-width: 576px) {\n    .modal-dialog--clients,\n    .modal-dialog--schedule {\n        max-width: 650px;\n    }\n}\n"
  },
  {
    "path": "client/src/components/ui/Overlay.css",
    "content": ".overlay {\n    display: none;\n    position: fixed;\n    top: 0;\n    left: 0;\n    z-index: 110;\n    width: 100%;\n    height: 100%;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding: 20px;\n    font-size: 28px;\n    font-weight: 600;\n    text-align: center;\n    background-color: var(--rt-nodata-bgcolor);\n}\n\n.overlay--visible {\n    display: flex;\n}\n\n.overlay__loading {\n    width: 40px;\n    height: 40px;\n    margin-bottom: 20px;\n    background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E');\n    will-change: transform;\n    animation: clockwise 2s linear infinite;\n}\n\n@keyframes clockwise {\n    0% {\n        transform: rotate(0deg);\n    }\n\n    100% {\n        transform: rotate(360deg);\n    }\n}\n"
  },
  {
    "path": "client/src/components/ui/PageTitle.css",
    "content": ".page-header {\n    flex-direction: column;\n    align-items: flex-start;\n}\n\n.page-header--logs {\n    flex-direction: row;\n    align-items: flex-end;\n    margin: 0.5rem 0 2.8rem;\n}\n\n.page-header--logs .page-title {\n    display: inline-flex;\n    align-items: center;\n}\n\n@media (max-width: 991px) {\n    .page-header--logs {\n        flex-direction: column;\n        align-items: center;\n        margin-bottom: 0 0 1.1rem;\n    }\n\n    .page-header--logs .page-title {\n        margin-bottom: 1.1rem;\n        font-size: 1.8rem;\n    }\n}\n\n.page-subtitle {\n    margin-left: 0;\n    font-size: 0.9rem;\n}\n\n.page-title--large {\n    font-size: 36px;\n    line-height: 46px;\n}\n"
  },
  {
    "path": "client/src/components/ui/PageTitle.tsx",
    "content": "import React from 'react';\n\nimport './PageTitle.css';\n\ninterface PageTitleProps {\n    title: string;\n    subtitle?: string;\n    children?: React.ReactNode;\n    containerClass?: string;\n}\n\nconst PageTitle = ({ title, subtitle, children, containerClass }: PageTitleProps) => (\n    <div className=\"page-header\">\n        <div className={containerClass}>\n            <h1 className=\"page-title pr-2\">{title}</h1>\n            {children}\n        </div>\n\n        {subtitle && <div className=\"page-subtitle\">{subtitle}</div>}\n    </div>\n);\n\nexport default PageTitle;\n"
  },
  {
    "path": "client/src/components/ui/ReactTable.css",
    "content": ".ReactTable .rt-th,\n.ReactTable .rt-td {\n    padding: 10px 15px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.card-table .ReactTable .rt-td {\n    overflow: visible;\n}\n\n.ReactTable .rt-noData {\n    color: var(--rt-nodata-color);\n    background-color: var(--rt-nodata-bgcolor);\n}\n\n.ReactTable .-loading {\n    color: var(--rt-nodata-color);\n    background-color: var(--rt-nodata-bgcolor);\n}\n\n.ReactTable .-loading .-loading-inner {\n    color: var(--gray300);\n}\n\n.ReactTable .-pagination input,\n.ReactTable .-pagination select {\n    color: var(--rt-nodata-color);\n    background-color: var(--rt-nodata-bgcolor);\n}\n\n[data-theme='dark'] .ReactTable .rt-table::-webkit-scrollbar-track {\n    background-color: var(--card-bgcolor);\n}\n\n[data-theme='dark'] .ReactTable .rt-table::-webkit-scrollbar-thumb {\n    background-color: #888888;\n}\n\n[data-theme='dark'] .ReactTable .-pagination .-btn {\n    filter: invert(1);\n}\n\n[data-theme='dark'] .ReactTable .-pagination .-btn:disabled {\n    opacity: 1;\n}\n\n.rt-tr-group.logs__row--red {\n    background-color: rgba(223, 56, 18, 0.05);\n}\n\n.rt-tr-group.logs__row--green {\n    background-color: rgba(103, 178, 121, 0.1);\n}\n\n.rt-tr-group.logs__row--blue {\n    background-color: #e5effd;\n}\n\n.rt-tr-group.logs__row--yellow {\n    background-color: var(--yellow-pale);\n}\n"
  },
  {
    "path": "client/src/components/ui/Select.css",
    "content": ".select.select--theme {\n    height: 45px;\n    padding: 0 32px 2px 11px;\n    outline: 0;\n    border-color: var(--ctrl-select-bgcolor);\n    background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj4KICAgIDxwb2x5bGluZSBwb2ludHM9IjYgOSAxMiAxNSAxOCA5Ij48L3BvbHlsaW5lPgo8L3N2Zz4K\");\n    background-repeat: no-repeat;\n    background-position: right 9px center;\n    background-size: 17px 20px;\n    appearance: none;\n    cursor: pointer;\n}\n\n.select--theme::-ms-expand {\n    opacity: 0;\n}\n\n.select.select--language {\n    height: 45px;\n    padding: 0 32px 2px 33px;\n    outline: 0;\n    border-color: var(--ctrl-select-bgcolor);\n    background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItZ2xvYmUiPgogICAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTAiLz4KICAgIDxwYXRoIGQ9Ik0yIDEyaDIwTTEyIDJhMTUuMyAxNS4zIDAgMCAxIDQgMTAgMTUuMyAxNS4zIDAgMCAxLTQgMTAgMTUuMyAxNS4zIDAgMCAxLTQtMTAgMTUuMyAxNS4zIDAgMCAxIDQtMTB6Ii8+Cjwvc3ZnPgo=\"), url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj4KICAgIDxwb2x5bGluZSBwb2ludHM9IjYgOSAxMiAxNSAxOCA5Ij48L3BvbHlsaW5lPgo8L3N2Zz4K\");\n    background-repeat: no-repeat, no-repeat;\n    background-position:\n        left 11px center,\n        right 9px center;\n    background-size:\n        14px,\n        17px 20px;\n    appearance: none;\n    cursor: pointer;\n}\n\n.select--language::-ms-expand {\n    opacity: 0;\n}\n\n.basic-multi-select .select__control {\n    border: 1px solid var(--card-border-color);\n    border-radius: 3px;\n    background-color: var(--ctrl-bgcolor);\n}\n\n.basic-multi-select .select__control:hover {\n    border: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.basic-multi-select .select__control--is-focused,\n.basic-multi-select .select__control--is-focused:hover {\n    border-color: #1991eb;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.basic-multi-select .select__placeholder {\n    color: #adb5bd;\n}\n\n.basic-multi-select .select__menu {\n    z-index: 3;\n    background-color: var(--ctrl-bgcolor);\n}\n\n[data-theme='dark'] .basic-multi-select .select__option:hover,\n[data-theme='dark'] .basic-multi-select .select__option--is-focused,\n[data-theme='dark'] .basic-multi-select .select__option--is-focused:hover {\n    background-color: var(--ctrl-select-bgcolor);\n    color: var(--ctrl-dropdown-color);\n}\n\n[data-theme='dark'] .select__multi-value__remove svg {\n    filter: invert(1);\n}\n"
  },
  {
    "path": "client/src/components/ui/Status.tsx",
    "content": "import React from 'react';\nimport { withTranslation, Trans } from 'react-i18next';\n\nimport Card from './Card';\n\ninterface StatusProps {\n    message: string;\n    buttonMessage?: string;\n    reloadPage?: (...args: unknown[]) => unknown;\n}\n\nconst Status = ({ message, buttonMessage, reloadPage }: StatusProps) => (\n    <div className=\"status\">\n        <Card bodyType=\"card-body card-body--status\">\n            <div className=\"h4 font-weight-light mb-4\">\n                <Trans>{message}</Trans>\n            </div>\n            {buttonMessage && (\n                <button className=\"btn btn-success\" onClick={reloadPage}>\n                    <Trans>{buttonMessage}</Trans>\n                </button>\n            )}\n        </Card>\n    </div>\n);\n\nexport default withTranslation()(Status);\n"
  },
  {
    "path": "client/src/components/ui/Tab.tsx",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\ninterface TabProps {\n    activeTabLabel: string;\n    label: string;\n    onClick: (...args: unknown[]) => unknown;\n    title?: string;\n}\n\nconst Tab = ({ activeTabLabel, label, title, onClick }: TabProps) => {\n    const [t] = useTranslation();\n    const handleClick = () => onClick(label);\n\n    const tabClass = classnames({\n        tab__control: true,\n        'tab__control--active': activeTabLabel === label,\n    });\n\n    return (\n        <div className={tabClass} onClick={handleClick}>\n            <svg className=\"tab__icon\">\n                <use xlinkHref={`#${label.toLowerCase()}`} />\n            </svg>\n            {t(title || label)}\n        </div>\n    );\n};\n\nexport default Tab;\n"
  },
  {
    "path": "client/src/components/ui/Tabler.css",
    "content": "@charset \"UTF-8\";\n/**\nDashboard UI\n */\n\n/*!\n * Bootstrap v4.0.0 (https://getbootstrap.com)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/* stylelint-disable */\n:root {\n    --blue: #467fcf;\n    --indigo: #6574cd;\n    --purple: #a55eea;\n    --pink: #f66d9b;\n    --red: #cd201f;\n    --orange: #fd9644;\n    --yellow: #f1c40f;\n    --green: #5eba00;\n    --green-74: #66b574;\n    --green-86: #66b586;\n    --teal: #2bcbba;\n    --cyan: #17a2b8;\n    --white: #fff;\n    --gray: #868e96;\n    --gray-ac: #9aa0ac;\n    --gray-dark: #343a40;\n    --azure: #45aaf2;\n    --lime: #7bd235;\n    --primary: #467fcf;\n    --secondary: #868e96;\n    --success: #5eba00;\n    --info: #45aaf2;\n    --warning: #f1c40f;\n    --danger: #cd201f;\n    --light: #f8f9fa;\n    --dark: #343a40;\n    --breakpoint-xs: 0;\n    --breakpoint-sm: 576px;\n    --breakpoint-md: 768px;\n    --breakpoint-lg: 992px;\n    --breakpoint-xl: 1280px;\n    --font-family-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,\n        'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';\n    --font-family-monospace: Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n}\n\n*,\n*::before,\n*::after {\n    box-sizing: border-box;\n}\n\nhtml {\n    font-family: sans-serif;\n    line-height: 1.15;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    -ms-overflow-style: scrollbar;\n    -webkit-tap-highlight-color: transparent;\n}\n\n@-ms-viewport {\n    width: device-width;\n}\n\narticle,\naside,\ndialog,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nnav,\nsection {\n    display: block;\n}\n\nbody {\n    margin: 0;\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n    font-size: 0.9375rem;\n    font-weight: 400;\n    line-height: 1.5;\n    color: var(--mcolor);\n    text-align: left;\n    background-color: var(--bgcolor);\n}\n\n[tabindex='-1']:focus {\n    outline: 0 !important;\n}\n\nhr {\n    box-sizing: content-box;\n    height: 0;\n    overflow: visible;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n    margin-top: 0;\n    margin-bottom: 0.66em;\n}\n\np {\n    margin-top: 0;\n    margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n    text-decoration: underline;\n    -webkit-text-decoration: underline dotted;\n    text-decoration: underline dotted;\n    cursor: help;\n    border-bottom: 0;\n}\n\naddress {\n    margin-bottom: 1rem;\n    font-style: normal;\n    line-height: inherit;\n}\n\nol,\nul,\ndl {\n    margin-top: 0;\n    margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n    margin-bottom: 0;\n}\n\ndt {\n    font-weight: 700;\n}\n\ndd {\n    margin-bottom: 0.5rem;\n    margin-left: 0;\n}\n\nblockquote {\n    margin: 0 0 1rem;\n}\n\ndfn {\n    font-style: italic;\n}\n\nb,\nstrong {\n    font-weight: bolder;\n}\n\nsmall {\n    font-size: 80%;\n}\n\nsub,\nsup {\n    position: relative;\n    font-size: 75%;\n    line-height: 0;\n    vertical-align: baseline;\n}\n\nsub {\n    bottom: -0.25em;\n}\n\nsup {\n    top: -0.5em;\n}\n\na {\n    color: #467fcf;\n    text-decoration: none;\n    background-color: transparent;\n    -webkittext-decoration-skip-ink: objects;\n}\n\na:hover {\n    color: #295a9f;\n    text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n    color: inherit;\n    text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover,\na:not([href]):not([tabindex]):focus {\n    color: inherit;\n    text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n    outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n    font-family: monospace, monospace;\n    font-size: 1em;\n}\n\npre {\n    margin-top: 0;\n    margin-bottom: 1rem;\n    overflow: auto;\n    -ms-overflow-style: scrollbar;\n}\n\nfigure {\n    margin: 0 0 1rem;\n}\n\nimg {\n    vertical-align: middle;\n    border-style: none;\n}\n\nsvg:not(:root) {\n    overflow: hidden;\n}\n\ntable {\n    border-collapse: collapse;\n}\n\ncaption {\n    padding-top: 0.75rem;\n    padding-bottom: 0.75rem;\n    color: #9aa0ac;\n    text-align: left;\n    caption-side: bottom;\n}\n\nth {\n    text-align: inherit;\n}\n\nlabel {\n    display: inline-block;\n    margin-bottom: 0.5rem;\n}\n\nbutton {\n    border-radius: 0;\n}\n\nbutton:focus {\n    outline: 1px dotted;\n    outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n    margin: 0;\n    font-family: inherit;\n    font-size: inherit;\n    line-height: inherit;\n}\n\nbutton,\ninput {\n    overflow: visible;\n}\n\nbutton,\nselect {\n    text-transform: none;\n}\n\nbutton,\nhtml [type='button'],\n[type='reset'],\n[type='submit'] {\n    -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type='button']::-moz-focus-inner,\n[type='reset']::-moz-focus-inner,\n[type='submit']::-moz-focus-inner {\n    padding: 0;\n    border-style: none;\n}\n\ninput[type='radio'],\ninput[type='checkbox'] {\n    box-sizing: border-box;\n    padding: 0;\n}\n\ninput[type='date'],\ninput[type='time'],\ninput[type='datetime-local'],\ninput[type='month'] {\n    -webkit-appearance: listbox;\n}\n\ntextarea {\n    overflow: auto;\n    resize: vertical;\n}\n\nfieldset {\n    min-width: 0;\n    padding: 0;\n    margin: 0;\n    border: 0;\n}\n\nlegend {\n    display: block;\n    width: 100%;\n    max-width: 100%;\n    padding: 0;\n    margin-bottom: 0.5rem;\n    font-size: 1.5rem;\n    line-height: inherit;\n    color: inherit;\n    white-space: normal;\n}\n\nprogress {\n    vertical-align: baseline;\n}\n\n[type='number']::-webkit-inner-spin-button,\n[type='number']::-webkit-outer-spin-button {\n    height: auto;\n}\n\n[type='search'] {\n    outline-offset: -2px;\n    -webkit-appearance: none;\n}\n\n[type='search']::-webkit-search-cancel-button,\n[type='search']::-webkit-search-decoration {\n    -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n    font: inherit;\n    -webkit-appearance: button;\n}\n\noutput {\n    display: inline-block;\n}\n\nsummary {\n    display: list-item;\n    cursor: pointer;\n}\n\ntemplate {\n    display: none;\n}\n\n[hidden] {\n    display: none !important;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n    margin-bottom: 0.66em;\n    font-family: inherit;\n    font-weight: 600;\n    line-height: 1.1;\n    color: inherit;\n}\n\nh1,\n.h1 {\n    font-size: 2rem;\n}\n\nh2,\n.h2 {\n    font-size: 1.75rem;\n}\n\nh3,\n.h3 {\n    font-size: 1.5rem;\n}\n\nh4,\n.h4 {\n    font-size: 1.125rem;\n}\n\nh5,\n.h5 {\n    font-size: 1rem;\n}\n\nh6,\n.h6 {\n    font-size: 0.875rem;\n}\n\n.lead {\n    font-size: 1.171875rem;\n    font-weight: 300;\n}\n\n.display-1 {\n    font-size: 4.5rem;\n    font-weight: 300;\n    line-height: 1.1;\n}\n\n.display-2 {\n    font-size: 4rem;\n    font-weight: 300;\n    line-height: 1.1;\n}\n\n.display-3 {\n    font-size: 3.5rem;\n    font-weight: 300;\n    line-height: 1.1;\n}\n\n.display-4 {\n    font-size: 3rem;\n    font-weight: 300;\n    line-height: 1.1;\n}\n\nhr {\n    margin-top: 1rem;\n    margin-bottom: 1rem;\n    border: 0;\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n[data-theme='dark'] hr {\n    border-color: var(--card-border-color);\n}\n\nsmall,\n.small {\n    font-size: 87.5%;\n    font-weight: 400;\n}\n\nmark,\n.mark {\n    padding: 0.2em;\n    background-color: #fcf8e3;\n}\n\n.list-unstyled {\n    padding-left: 0;\n    list-style: none;\n}\n\n.list-inline {\n    padding-left: 0;\n    list-style: none;\n}\n\n.list-inline-item {\n    display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n    margin-right: 0.5rem;\n}\n\n.initialism {\n    font-size: 90%;\n    text-transform: uppercase;\n}\n\n.blockquote {\n    margin-bottom: 1rem;\n    font-size: 1.171875rem;\n}\n\n.blockquote-footer {\n    display: block;\n    font-size: 80%;\n    color: #868e96;\n}\n\n.blockquote-footer::before {\n    content: '\\2014 \\00A0';\n}\n\n.img-fluid {\n    max-width: 100%;\n    height: auto;\n}\n\n.img-thumbnail {\n    padding: 0.25rem;\n    background-color: #fff;\n    border: 1px solid #dee2e6;\n    border-radius: 3px;\n    max-width: 100%;\n    height: auto;\n}\n\n.figure {\n    display: inline-block;\n}\n\n.figure-img {\n    margin-bottom: 0.5rem;\n    line-height: 1;\n}\n\n.figure-caption {\n    font-size: 90%;\n    color: #868e96;\n}\n\ncode,\nkbd,\npre,\nsamp {\n    font-family: Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n}\n\ncode {\n    font-size: 85%;\n    color: inherit;\n    word-break: break-word;\n}\n\na > code {\n    color: inherit;\n}\n\nkbd {\n    padding: 0.2rem 0.4rem;\n    font-size: 85%;\n    color: #fff;\n    background-color: #343a40;\n    border-radius: 3px;\n}\n\nkbd kbd {\n    padding: 0;\n    font-size: 100%;\n    font-weight: 700;\n}\n\npre {\n    display: block;\n    font-size: 85%;\n    color: #212529;\n}\n\npre code {\n    font-size: inherit;\n    color: inherit;\n    word-break: normal;\n}\n\n.pre-scrollable {\n    max-height: 340px;\n    overflow-y: scroll;\n}\n\n.container {\n    width: 100%;\n    padding-right: 0.75rem;\n    padding-left: 0.75rem;\n    margin-right: auto;\n    margin-left: auto;\n}\n\n@media (min-width: 576px) {\n    .container {\n        max-width: 540px;\n    }\n}\n\n@media (min-width: 768px) {\n    .container {\n        max-width: 720px;\n    }\n}\n\n@media (min-width: 992px) {\n    .container {\n        max-width: 960px;\n    }\n}\n\n@media (min-width: 1280px) {\n    .container {\n        max-width: 1200px;\n    }\n}\n\n.container-fluid {\n    width: 100%;\n    padding-right: 0.75rem;\n    padding-left: 0.75rem;\n    margin-right: auto;\n    margin-left: auto;\n}\n\n.row {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    margin-right: -0.75rem;\n    margin-left: -0.75rem;\n}\n\n.no-gutters {\n    margin-right: 0;\n    margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*='col-'] {\n    padding-right: 0;\n    padding-left: 0;\n}\n\n.col-1,\n.col-2,\n.col-3,\n.col-4,\n.col-5,\n.col-6,\n.col-7,\n.col-8,\n.col-9,\n.col-10,\n.col-11,\n.col-12,\n.col,\n.col-auto,\n.col-sm-1,\n.col-sm-2,\n.col-sm-3,\n.col-sm-4,\n.col-sm-5,\n.col-sm-6,\n.col-sm-7,\n.col-sm-8,\n.col-sm-9,\n.col-sm-10,\n.col-sm-11,\n.col-sm-12,\n.col-sm,\n.col-sm-auto,\n.col-md-1,\n.col-md-2,\n.col-md-3,\n.col-md-4,\n.col-md-5,\n.col-md-6,\n.col-md-7,\n.col-md-8,\n.col-md-9,\n.col-md-10,\n.col-md-11,\n.col-md-12,\n.col-md,\n.col-md-auto,\n.col-lg-1,\n.col-lg-2,\n.col-lg-3,\n.col-lg-4,\n.col-lg-5,\n.col-lg-6,\n.col-lg-7,\n.col-lg-8,\n.col-lg-9,\n.col-lg-10,\n.col-lg-11,\n.col-lg-12,\n.col-lg,\n.col-lg-auto,\n.col-xl-1,\n.col-xl-2,\n.col-xl-3,\n.col-xl-4,\n.col-xl-5,\n.col-xl-6,\n.col-xl-7,\n.col-xl-8,\n.col-xl-9,\n.col-xl-10,\n.col-xl-11,\n.col-xl-12,\n.col-xl,\n.col-xl-auto {\n    position: relative;\n    width: 100%;\n    min-height: 1px;\n    padding-right: 0.75rem;\n    padding-left: 0.75rem;\n}\n\n.col {\n    -ms-flex-preferred-size: 0;\n    flex-basis: 0;\n    -ms-flex-positive: 1;\n    flex-grow: 1;\n    max-width: 100%;\n}\n\n.col-auto {\n    -ms-flex: 0 0 auto;\n    flex: 0 0 auto;\n    width: auto;\n    max-width: none;\n}\n\n.col-1 {\n    -ms-flex: 0 0 8.33333333%;\n    flex: 0 0 8.33333333%;\n    max-width: 8.33333333%;\n}\n\n.col-2 {\n    -ms-flex: 0 0 16.66666667%;\n    flex: 0 0 16.66666667%;\n    max-width: 16.66666667%;\n}\n\n.col-3 {\n    -ms-flex: 0 0 25%;\n    flex: 0 0 25%;\n    max-width: 25%;\n}\n\n.col-4 {\n    -ms-flex: 0 0 33.33333333%;\n    flex: 0 0 33.33333333%;\n    max-width: 33.33333333%;\n}\n\n.col-5 {\n    -ms-flex: 0 0 41.66666667%;\n    flex: 0 0 41.66666667%;\n    max-width: 41.66666667%;\n}\n\n.col-6 {\n    -ms-flex: 0 0 50%;\n    flex: 0 0 50%;\n    max-width: 50%;\n}\n\n.col-7 {\n    -ms-flex: 0 0 58.33333333%;\n    flex: 0 0 58.33333333%;\n    max-width: 58.33333333%;\n}\n\n.col-8 {\n    -ms-flex: 0 0 66.66666667%;\n    flex: 0 0 66.66666667%;\n    max-width: 66.66666667%;\n}\n\n.col-9 {\n    -ms-flex: 0 0 75%;\n    flex: 0 0 75%;\n    max-width: 75%;\n}\n\n.col-10 {\n    -ms-flex: 0 0 83.33333333%;\n    flex: 0 0 83.33333333%;\n    max-width: 83.33333333%;\n}\n\n.col-11 {\n    -ms-flex: 0 0 91.66666667%;\n    flex: 0 0 91.66666667%;\n    max-width: 91.66666667%;\n}\n\n.col-12 {\n    -ms-flex: 0 0 100%;\n    flex: 0 0 100%;\n    max-width: 100%;\n}\n\n.order-first {\n    -ms-flex-order: -1;\n    order: -1;\n}\n\n.order-last {\n    -ms-flex-order: 13;\n    order: 13;\n}\n\n.order-0 {\n    -ms-flex-order: 0;\n    order: 0;\n}\n\n.order-1 {\n    -ms-flex-order: 1;\n    order: 1;\n}\n\n.order-2 {\n    -ms-flex-order: 2;\n    order: 2;\n}\n\n.order-3 {\n    -ms-flex-order: 3;\n    order: 3;\n}\n\n.order-4 {\n    -ms-flex-order: 4;\n    order: 4;\n}\n\n.order-5 {\n    -ms-flex-order: 5;\n    order: 5;\n}\n\n.order-6 {\n    -ms-flex-order: 6;\n    order: 6;\n}\n\n.order-7 {\n    -ms-flex-order: 7;\n    order: 7;\n}\n\n.order-8 {\n    -ms-flex-order: 8;\n    order: 8;\n}\n\n.order-9 {\n    -ms-flex-order: 9;\n    order: 9;\n}\n\n.order-10 {\n    -ms-flex-order: 10;\n    order: 10;\n}\n\n.order-11 {\n    -ms-flex-order: 11;\n    order: 11;\n}\n\n.order-12 {\n    -ms-flex-order: 12;\n    order: 12;\n}\n\n.offset-1 {\n    margin-left: 8.33333333%;\n}\n\n.offset-2 {\n    margin-left: 16.66666667%;\n}\n\n.offset-3 {\n    margin-left: 25%;\n}\n\n.offset-4 {\n    margin-left: 33.33333333%;\n}\n\n.offset-5 {\n    margin-left: 41.66666667%;\n}\n\n.offset-6 {\n    margin-left: 50%;\n}\n\n.offset-7 {\n    margin-left: 58.33333333%;\n}\n\n.offset-8 {\n    margin-left: 66.66666667%;\n}\n\n.offset-9 {\n    margin-left: 75%;\n}\n\n.offset-10 {\n    margin-left: 83.33333333%;\n}\n\n.offset-11 {\n    margin-left: 91.66666667%;\n}\n\n@media (min-width: 576px) {\n    .col-sm {\n        -ms-flex-preferred-size: 0;\n        flex-basis: 0;\n        -ms-flex-positive: 1;\n        flex-grow: 1;\n        max-width: 100%;\n    }\n    .col-sm-auto {\n        -ms-flex: 0 0 auto;\n        flex: 0 0 auto;\n        width: auto;\n        max-width: none;\n    }\n    .col-sm-1 {\n        -ms-flex: 0 0 8.33333333%;\n        flex: 0 0 8.33333333%;\n        max-width: 8.33333333%;\n    }\n    .col-sm-2 {\n        -ms-flex: 0 0 16.66666667%;\n        flex: 0 0 16.66666667%;\n        max-width: 16.66666667%;\n    }\n    .col-sm-3 {\n        -ms-flex: 0 0 25%;\n        flex: 0 0 25%;\n        max-width: 25%;\n    }\n    .col-sm-4 {\n        -ms-flex: 0 0 33.33333333%;\n        flex: 0 0 33.33333333%;\n        max-width: 33.33333333%;\n    }\n    .col-sm-5 {\n        -ms-flex: 0 0 41.66666667%;\n        flex: 0 0 41.66666667%;\n        max-width: 41.66666667%;\n    }\n    .col-sm-6 {\n        -ms-flex: 0 0 50%;\n        flex: 0 0 50%;\n        max-width: 50%;\n    }\n    .col-sm-7 {\n        -ms-flex: 0 0 58.33333333%;\n        flex: 0 0 58.33333333%;\n        max-width: 58.33333333%;\n    }\n    .col-sm-8 {\n        -ms-flex: 0 0 66.66666667%;\n        flex: 0 0 66.66666667%;\n        max-width: 66.66666667%;\n    }\n    .col-sm-9 {\n        -ms-flex: 0 0 75%;\n        flex: 0 0 75%;\n        max-width: 75%;\n    }\n    .col-sm-10 {\n        -ms-flex: 0 0 83.33333333%;\n        flex: 0 0 83.33333333%;\n        max-width: 83.33333333%;\n    }\n    .col-sm-11 {\n        -ms-flex: 0 0 91.66666667%;\n        flex: 0 0 91.66666667%;\n        max-width: 91.66666667%;\n    }\n    .col-sm-12 {\n        -ms-flex: 0 0 100%;\n        flex: 0 0 100%;\n        max-width: 100%;\n    }\n    .order-sm-first {\n        -ms-flex-order: -1;\n        order: -1;\n    }\n    .order-sm-last {\n        -ms-flex-order: 13;\n        order: 13;\n    }\n    .order-sm-0 {\n        -ms-flex-order: 0;\n        order: 0;\n    }\n    .order-sm-1 {\n        -ms-flex-order: 1;\n        order: 1;\n    }\n    .order-sm-2 {\n        -ms-flex-order: 2;\n        order: 2;\n    }\n    .order-sm-3 {\n        -ms-flex-order: 3;\n        order: 3;\n    }\n    .order-sm-4 {\n        -ms-flex-order: 4;\n        order: 4;\n    }\n    .order-sm-5 {\n        -ms-flex-order: 5;\n        order: 5;\n    }\n    .order-sm-6 {\n        -ms-flex-order: 6;\n        order: 6;\n    }\n    .order-sm-7 {\n        -ms-flex-order: 7;\n        order: 7;\n    }\n    .order-sm-8 {\n        -ms-flex-order: 8;\n        order: 8;\n    }\n    .order-sm-9 {\n        -ms-flex-order: 9;\n        order: 9;\n    }\n    .order-sm-10 {\n        -ms-flex-order: 10;\n        order: 10;\n    }\n    .order-sm-11 {\n        -ms-flex-order: 11;\n        order: 11;\n    }\n    .order-sm-12 {\n        -ms-flex-order: 12;\n        order: 12;\n    }\n    .offset-sm-0 {\n        margin-left: 0;\n    }\n    .offset-sm-1 {\n        margin-left: 8.33333333%;\n    }\n    .offset-sm-2 {\n        margin-left: 16.66666667%;\n    }\n    .offset-sm-3 {\n        margin-left: 25%;\n    }\n    .offset-sm-4 {\n        margin-left: 33.33333333%;\n    }\n    .offset-sm-5 {\n        margin-left: 41.66666667%;\n    }\n    .offset-sm-6 {\n        margin-left: 50%;\n    }\n    .offset-sm-7 {\n        margin-left: 58.33333333%;\n    }\n    .offset-sm-8 {\n        margin-left: 66.66666667%;\n    }\n    .offset-sm-9 {\n        margin-left: 75%;\n    }\n    .offset-sm-10 {\n        margin-left: 83.33333333%;\n    }\n    .offset-sm-11 {\n        margin-left: 91.66666667%;\n    }\n}\n\n@media (min-width: 768px) {\n    .col-md {\n        -ms-flex-preferred-size: 0;\n        flex-basis: 0;\n        -ms-flex-positive: 1;\n        flex-grow: 1;\n        max-width: 100%;\n    }\n    .col-md-auto {\n        -ms-flex: 0 0 auto;\n        flex: 0 0 auto;\n        width: auto;\n        max-width: none;\n    }\n    .col-md-1 {\n        -ms-flex: 0 0 8.33333333%;\n        flex: 0 0 8.33333333%;\n        max-width: 8.33333333%;\n    }\n    .col-md-2 {\n        -ms-flex: 0 0 16.66666667%;\n        flex: 0 0 16.66666667%;\n        max-width: 16.66666667%;\n    }\n    .col-md-3 {\n        -ms-flex: 0 0 25%;\n        flex: 0 0 25%;\n        max-width: 25%;\n    }\n    .col-md-4 {\n        -ms-flex: 0 0 33.33333333%;\n        flex: 0 0 33.33333333%;\n        max-width: 33.33333333%;\n    }\n    .col-md-5 {\n        -ms-flex: 0 0 41.66666667%;\n        flex: 0 0 41.66666667%;\n        max-width: 41.66666667%;\n    }\n    .col-md-6 {\n        -ms-flex: 0 0 50%;\n        flex: 0 0 50%;\n        max-width: 50%;\n    }\n    .col-md-7 {\n        -ms-flex: 0 0 58.33333333%;\n        flex: 0 0 58.33333333%;\n        max-width: 58.33333333%;\n    }\n    .col-md-8 {\n        -ms-flex: 0 0 66.66666667%;\n        flex: 0 0 66.66666667%;\n        max-width: 66.66666667%;\n    }\n    .col-md-9 {\n        -ms-flex: 0 0 75%;\n        flex: 0 0 75%;\n        max-width: 75%;\n    }\n    .col-md-10 {\n        -ms-flex: 0 0 83.33333333%;\n        flex: 0 0 83.33333333%;\n        max-width: 83.33333333%;\n    }\n    .col-md-11 {\n        -ms-flex: 0 0 91.66666667%;\n        flex: 0 0 91.66666667%;\n        max-width: 91.66666667%;\n    }\n    .col-md-12 {\n        -ms-flex: 0 0 100%;\n        flex: 0 0 100%;\n        max-width: 100%;\n    }\n    .order-md-first {\n        -ms-flex-order: -1;\n        order: -1;\n    }\n    .order-md-last {\n        -ms-flex-order: 13;\n        order: 13;\n    }\n    .order-md-0 {\n        -ms-flex-order: 0;\n        order: 0;\n    }\n    .order-md-1 {\n        -ms-flex-order: 1;\n        order: 1;\n    }\n    .order-md-2 {\n        -ms-flex-order: 2;\n        order: 2;\n    }\n    .order-md-3 {\n        -ms-flex-order: 3;\n        order: 3;\n    }\n    .order-md-4 {\n        -ms-flex-order: 4;\n        order: 4;\n    }\n    .order-md-5 {\n        -ms-flex-order: 5;\n        order: 5;\n    }\n    .order-md-6 {\n        -ms-flex-order: 6;\n        order: 6;\n    }\n    .order-md-7 {\n        -ms-flex-order: 7;\n        order: 7;\n    }\n    .order-md-8 {\n        -ms-flex-order: 8;\n        order: 8;\n    }\n    .order-md-9 {\n        -ms-flex-order: 9;\n        order: 9;\n    }\n    .order-md-10 {\n        -ms-flex-order: 10;\n        order: 10;\n    }\n    .order-md-11 {\n        -ms-flex-order: 11;\n        order: 11;\n    }\n    .order-md-12 {\n        -ms-flex-order: 12;\n        order: 12;\n    }\n    .offset-md-0 {\n        margin-left: 0;\n    }\n    .offset-md-1 {\n        margin-left: 8.33333333%;\n    }\n    .offset-md-2 {\n        margin-left: 16.66666667%;\n    }\n    .offset-md-3 {\n        margin-left: 25%;\n    }\n    .offset-md-4 {\n        margin-left: 33.33333333%;\n    }\n    .offset-md-5 {\n        margin-left: 41.66666667%;\n    }\n    .offset-md-6 {\n        margin-left: 50%;\n    }\n    .offset-md-7 {\n        margin-left: 58.33333333%;\n    }\n    .offset-md-8 {\n        margin-left: 66.66666667%;\n    }\n    .offset-md-9 {\n        margin-left: 75%;\n    }\n    .offset-md-10 {\n        margin-left: 83.33333333%;\n    }\n    .offset-md-11 {\n        margin-left: 91.66666667%;\n    }\n}\n\n@media (min-width: 992px) {\n    .col-lg {\n        -ms-flex-preferred-size: 0;\n        flex-basis: 0;\n        -ms-flex-positive: 1;\n        flex-grow: 1;\n        max-width: 100%;\n    }\n    .col-lg-auto {\n        -ms-flex: 0 0 auto;\n        flex: 0 0 auto;\n        width: auto;\n        max-width: none;\n    }\n    .col-lg-1 {\n        -ms-flex: 0 0 8.33333333%;\n        flex: 0 0 8.33333333%;\n        max-width: 8.33333333%;\n    }\n    .col-lg-2 {\n        -ms-flex: 0 0 16.66666667%;\n        flex: 0 0 16.66666667%;\n        max-width: 16.66666667%;\n    }\n    .col-lg-3 {\n        -ms-flex: 0 0 25%;\n        flex: 0 0 25%;\n        max-width: 25%;\n    }\n    .col-lg-4 {\n        -ms-flex: 0 0 33.33333333%;\n        flex: 0 0 33.33333333%;\n        max-width: 33.33333333%;\n    }\n    .col-lg-5 {\n        -ms-flex: 0 0 41.66666667%;\n        flex: 0 0 41.66666667%;\n        max-width: 41.66666667%;\n    }\n    .col-lg-6 {\n        -ms-flex: 0 0 50%;\n        flex: 0 0 50%;\n        max-width: 50%;\n    }\n    .col-lg-7 {\n        -ms-flex: 0 0 58.33333333%;\n        flex: 0 0 58.33333333%;\n        max-width: 58.33333333%;\n    }\n    .col-lg-8 {\n        -ms-flex: 0 0 66.66666667%;\n        flex: 0 0 66.66666667%;\n        max-width: 66.66666667%;\n    }\n    .col-lg-9 {\n        -ms-flex: 0 0 75%;\n        flex: 0 0 75%;\n        max-width: 75%;\n    }\n    .col-lg-10 {\n        -ms-flex: 0 0 83.33333333%;\n        flex: 0 0 83.33333333%;\n        max-width: 83.33333333%;\n    }\n    .col-lg-11 {\n        -ms-flex: 0 0 91.66666667%;\n        flex: 0 0 91.66666667%;\n        max-width: 91.66666667%;\n    }\n    .col-lg-12 {\n        -ms-flex: 0 0 100%;\n        flex: 0 0 100%;\n        max-width: 100%;\n    }\n    .order-lg-first {\n        -ms-flex-order: -1;\n        order: -1;\n    }\n    .order-lg-last {\n        -ms-flex-order: 13;\n        order: 13;\n    }\n    .order-lg-0 {\n        -ms-flex-order: 0;\n        order: 0;\n    }\n    .order-lg-1 {\n        -ms-flex-order: 1;\n        order: 1;\n    }\n    .order-lg-2 {\n        -ms-flex-order: 2;\n        order: 2;\n    }\n    .order-lg-3 {\n        -ms-flex-order: 3;\n        order: 3;\n    }\n    .order-lg-4 {\n        -ms-flex-order: 4;\n        order: 4;\n    }\n    .order-lg-5 {\n        -ms-flex-order: 5;\n        order: 5;\n    }\n    .order-lg-6 {\n        -ms-flex-order: 6;\n        order: 6;\n    }\n    .order-lg-7 {\n        -ms-flex-order: 7;\n        order: 7;\n    }\n    .order-lg-8 {\n        -ms-flex-order: 8;\n        order: 8;\n    }\n    .order-lg-9 {\n        -ms-flex-order: 9;\n        order: 9;\n    }\n    .order-lg-10 {\n        -ms-flex-order: 10;\n        order: 10;\n    }\n    .order-lg-11 {\n        -ms-flex-order: 11;\n        order: 11;\n    }\n    .order-lg-12 {\n        -ms-flex-order: 12;\n        order: 12;\n    }\n    .offset-lg-0 {\n        margin-left: 0;\n    }\n    .offset-lg-1 {\n        margin-left: 8.33333333%;\n    }\n    .offset-lg-2 {\n        margin-left: 16.66666667%;\n    }\n    .offset-lg-3 {\n        margin-left: 25%;\n    }\n    .offset-lg-4 {\n        margin-left: 33.33333333%;\n    }\n    .offset-lg-5 {\n        margin-left: 41.66666667%;\n    }\n    .offset-lg-6 {\n        margin-left: 50%;\n    }\n    .offset-lg-7 {\n        margin-left: 58.33333333%;\n    }\n    .offset-lg-8 {\n        margin-left: 66.66666667%;\n    }\n    .offset-lg-9 {\n        margin-left: 75%;\n    }\n    .offset-lg-10 {\n        margin-left: 83.33333333%;\n    }\n    .offset-lg-11 {\n        margin-left: 91.66666667%;\n    }\n}\n\n@media (min-width: 1280px) {\n    .col-xl {\n        -ms-flex-preferred-size: 0;\n        flex-basis: 0;\n        -ms-flex-positive: 1;\n        flex-grow: 1;\n        max-width: 100%;\n    }\n    .col-xl-auto {\n        -ms-flex: 0 0 auto;\n        flex: 0 0 auto;\n        width: auto;\n        max-width: none;\n    }\n    .col-xl-1 {\n        -ms-flex: 0 0 8.33333333%;\n        flex: 0 0 8.33333333%;\n        max-width: 8.33333333%;\n    }\n    .col-xl-2 {\n        -ms-flex: 0 0 16.66666667%;\n        flex: 0 0 16.66666667%;\n        max-width: 16.66666667%;\n    }\n    .col-xl-3 {\n        -ms-flex: 0 0 25%;\n        flex: 0 0 25%;\n        max-width: 25%;\n    }\n    .col-xl-4 {\n        -ms-flex: 0 0 33.33333333%;\n        flex: 0 0 33.33333333%;\n        max-width: 33.33333333%;\n    }\n    .col-xl-5 {\n        -ms-flex: 0 0 41.66666667%;\n        flex: 0 0 41.66666667%;\n        max-width: 41.66666667%;\n    }\n    .col-xl-6 {\n        -ms-flex: 0 0 50%;\n        flex: 0 0 50%;\n        max-width: 50%;\n    }\n    .col-xl-7 {\n        -ms-flex: 0 0 58.33333333%;\n        flex: 0 0 58.33333333%;\n        max-width: 58.33333333%;\n    }\n    .col-xl-8 {\n        -ms-flex: 0 0 66.66666667%;\n        flex: 0 0 66.66666667%;\n        max-width: 66.66666667%;\n    }\n    .col-xl-9 {\n        -ms-flex: 0 0 75%;\n        flex: 0 0 75%;\n        max-width: 75%;\n    }\n    .col-xl-10 {\n        -ms-flex: 0 0 83.33333333%;\n        flex: 0 0 83.33333333%;\n        max-width: 83.33333333%;\n    }\n    .col-xl-11 {\n        -ms-flex: 0 0 91.66666667%;\n        flex: 0 0 91.66666667%;\n        max-width: 91.66666667%;\n    }\n    .col-xl-12 {\n        -ms-flex: 0 0 100%;\n        flex: 0 0 100%;\n        max-width: 100%;\n    }\n    .order-xl-first {\n        -ms-flex-order: -1;\n        order: -1;\n    }\n    .order-xl-last {\n        -ms-flex-order: 13;\n        order: 13;\n    }\n    .order-xl-0 {\n        -ms-flex-order: 0;\n        order: 0;\n    }\n    .order-xl-1 {\n        -ms-flex-order: 1;\n        order: 1;\n    }\n    .order-xl-2 {\n        -ms-flex-order: 2;\n        order: 2;\n    }\n    .order-xl-3 {\n        -ms-flex-order: 3;\n        order: 3;\n    }\n    .order-xl-4 {\n        -ms-flex-order: 4;\n        order: 4;\n    }\n    .order-xl-5 {\n        -ms-flex-order: 5;\n        order: 5;\n    }\n    .order-xl-6 {\n        -ms-flex-order: 6;\n        order: 6;\n    }\n    .order-xl-7 {\n        -ms-flex-order: 7;\n        order: 7;\n    }\n    .order-xl-8 {\n        -ms-flex-order: 8;\n        order: 8;\n    }\n    .order-xl-9 {\n        -ms-flex-order: 9;\n        order: 9;\n    }\n    .order-xl-10 {\n        -ms-flex-order: 10;\n        order: 10;\n    }\n    .order-xl-11 {\n        -ms-flex-order: 11;\n        order: 11;\n    }\n    .order-xl-12 {\n        -ms-flex-order: 12;\n        order: 12;\n    }\n    .offset-xl-0 {\n        margin-left: 0;\n    }\n    .offset-xl-1 {\n        margin-left: 8.33333333%;\n    }\n    .offset-xl-2 {\n        margin-left: 16.66666667%;\n    }\n    .offset-xl-3 {\n        margin-left: 25%;\n    }\n    .offset-xl-4 {\n        margin-left: 33.33333333%;\n    }\n    .offset-xl-5 {\n        margin-left: 41.66666667%;\n    }\n    .offset-xl-6 {\n        margin-left: 50%;\n    }\n    .offset-xl-7 {\n        margin-left: 58.33333333%;\n    }\n    .offset-xl-8 {\n        margin-left: 66.66666667%;\n    }\n    .offset-xl-9 {\n        margin-left: 75%;\n    }\n    .offset-xl-10 {\n        margin-left: 83.33333333%;\n    }\n    .offset-xl-11 {\n        margin-left: 91.66666667%;\n    }\n}\n\n.table,\n.text-wrap table {\n    width: 100%;\n    max-width: 100%;\n    margin-bottom: 1rem;\n    background-color: transparent;\n}\n\n.table th,\n.text-wrap table th,\n.table td,\n.text-wrap table td {\n    padding: 0.75rem;\n    vertical-align: top;\n    border-top: 1px solid #dee2e6;\n}\n\n.table thead th,\n.text-wrap table thead th {\n    vertical-align: bottom;\n    border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody,\n.text-wrap table tbody + tbody {\n    border-top: 2px solid #dee2e6;\n}\n\n.table .table,\n.text-wrap table .table,\n.table .text-wrap table,\n.text-wrap .table table,\n.text-wrap table table {\n    background-color: #f5f7fb;\n}\n\n.table-sm th,\n.table-sm td {\n    padding: 0.3rem;\n}\n\n.table-bordered,\n.text-wrap table {\n    border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.text-wrap table th,\n.table-bordered td,\n.text-wrap table td {\n    border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.text-wrap table thead th,\n.table-bordered thead td,\n.text-wrap table thead td {\n    border-bottom-width: 2px;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n    background-color: rgba(0, 0, 0, 0.02);\n}\n\n.table-hover tbody tr:hover {\n    background-color: rgba(0, 0, 0, 0.04);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n    background-color: #cbdbf2;\n}\n\n.table-hover .table-primary:hover {\n    background-color: #b7cded;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n    background-color: #b7cded;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n    background-color: #dddfe2;\n}\n\n.table-hover .table-secondary:hover {\n    background-color: #cfd2d6;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n    background-color: #cfd2d6;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n    background-color: #d2ecb8;\n}\n\n.table-hover .table-success:hover {\n    background-color: #c5e7a4;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n    background-color: #c5e7a4;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n    background-color: #cbe7fb;\n}\n\n.table-hover .table-info:hover {\n    background-color: #b3dcf9;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n    background-color: #b3dcf9;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n    background-color: #fbeebc;\n}\n\n.table-hover .table-warning:hover {\n    background-color: #fae8a4;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n    background-color: #fae8a4;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n    background-color: #f1c1c0;\n}\n\n.table-hover .table-danger:hover {\n    background-color: #ecacab;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n    background-color: #ecacab;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n    background-color: #fdfdfe;\n}\n\n.table-hover .table-light:hover {\n    background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n    background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n    background-color: #c6c8ca;\n}\n\n.table-hover .table-dark:hover {\n    background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n    background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n    background-color: rgba(0, 0, 0, 0.04);\n}\n\n.table-hover .table-active:hover {\n    background-color: rgba(0, 0, 0, 0.04);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n    background-color: rgba(0, 0, 0, 0.04);\n}\n\n.table .thead-dark th,\n.text-wrap table .thead-dark th {\n    color: #f5f7fb;\n    background-color: #212529;\n    border-color: #32383e;\n}\n\n.table .thead-light th,\n.text-wrap table .thead-light th {\n    color: #495057;\n    background-color: #e9ecef;\n    border-color: #dee2e6;\n}\n\n.table-dark {\n    color: #f5f7fb;\n    background-color: #212529;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n    border-color: #32383e;\n}\n\n.table-dark.table-bordered,\n.text-wrap table.table-dark {\n    border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n    background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n    background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n    .table-responsive-sm {\n        display: block;\n        width: 100%;\n        overflow-x: auto;\n        -webkit-overflow-scrolling: touch;\n        -ms-overflow-style: -ms-autohiding-scrollbar;\n    }\n    .table-responsive-sm > .table-bordered,\n    .text-wrap .table-responsive-sm > table {\n        border: 0;\n    }\n}\n\n@media (max-width: 767.98px) {\n    .table-responsive-md {\n        display: block;\n        width: 100%;\n        overflow-x: auto;\n        -webkit-overflow-scrolling: touch;\n        -ms-overflow-style: -ms-autohiding-scrollbar;\n    }\n    .table-responsive-md > .table-bordered,\n    .text-wrap .table-responsive-md > table {\n        border: 0;\n    }\n}\n\n@media (max-width: 991.98px) {\n    .table-responsive-lg {\n        display: block;\n        width: 100%;\n        overflow-x: auto;\n        -webkit-overflow-scrolling: touch;\n        -ms-overflow-style: -ms-autohiding-scrollbar;\n    }\n    .table-responsive-lg > .table-bordered,\n    .text-wrap .table-responsive-lg > table {\n        border: 0;\n    }\n}\n\n@media (max-width: 1279.98px) {\n    .table-responsive-xl {\n        display: block;\n        width: 100%;\n        overflow-x: auto;\n        -webkit-overflow-scrolling: touch;\n        -ms-overflow-style: -ms-autohiding-scrollbar;\n    }\n    .table-responsive-xl > .table-bordered,\n    .text-wrap .table-responsive-xl > table {\n        border: 0;\n    }\n}\n\n.table-responsive {\n    display: block;\n    width: 100%;\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n}\n\n.table-responsive > .table-bordered,\n.text-wrap .table-responsive > table {\n    border: 0;\n}\n\n.form-control {\n    display: block;\n    width: 100%;\n    padding: 0.375rem 0.75rem;\n    font-size: 0.9375rem;\n    line-height: 1.6;\n    color: var(--mcolor);\n    background-color: var(--card-bgcolor);\n    background-clip: padding-box;\n    border: 1px solid var(--card-border-color);\n    border-radius: 3px;\n    transition:\n        border-color 0.15s ease-in-out,\n        box-shadow 0.15s ease-in-out;\n}\n\n.form-control::-ms-expand {\n    background-color: transparent;\n    border: 0;\n}\n\n.form-control:focus {\n    color: var(--mcolor);\n    background-color: var(--ctrl-bgcolor);\n    border-color: #1991eb;\n    outline: 0;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.form-control::-webkit-input-placeholder {\n    color: #adb5bd;\n    opacity: 1;\n}\n\n.form-control::-moz-placeholder {\n    color: #adb5bd;\n    opacity: 1;\n}\n\n.form-control:-ms-input-placeholder {\n    color: #adb5bd;\n    opacity: 1;\n}\n\n.form-control::-ms-input-placeholder {\n    color: #adb5bd;\n    opacity: 1;\n}\n\n.form-control::placeholder {\n    color: #adb5bd;\n    opacity: 1;\n}\n\n.form-control:disabled,\n.form-control[readonly] {\n    background-color: var(--form-disabled-bgcolor);\n    color: var(--form-disabled-color);\n    opacity: 1;\n}\n\nselect.form-control:not([size]):not([multiple]) {\n    height: 2.375rem;\n}\n\nselect.form-control:focus::-ms-value {\n    color: #495057;\n    background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n    display: block;\n    width: 100%;\n}\n\n.col-form-label {\n    padding-top: calc(0.375rem + 1px);\n    padding-bottom: calc(0.375rem + 1px);\n    margin-bottom: 0;\n    font-size: inherit;\n    line-height: 1.6;\n}\n\n.col-form-label-lg {\n    padding-top: calc(0.5rem + 1px);\n    padding-bottom: calc(0.5rem + 1px);\n    font-size: 1.125rem;\n    line-height: 1.44444444;\n}\n\n.col-form-label-sm {\n    padding-top: calc(0.25rem + 1px);\n    padding-bottom: calc(0.25rem + 1px);\n    font-size: 0.875rem;\n    line-height: 1.14285714;\n}\n\n.form-control-plaintext {\n    display: block;\n    width: 100%;\n    padding-top: 0.375rem;\n    padding-bottom: 0.375rem;\n    margin-bottom: 0;\n    line-height: 1.6;\n    background-color: transparent;\n    border: solid transparent;\n    border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm,\n.input-group-sm > .form-control-plaintext.form-control,\n.input-group-sm > .input-group-prepend > .form-control-plaintext.input-group-text,\n.input-group-sm > .input-group-append > .form-control-plaintext.input-group-text,\n.input-group-sm > .input-group-prepend > .form-control-plaintext.btn,\n.input-group-sm > .input-group-append > .form-control-plaintext.btn,\n.form-control-plaintext.form-control-lg,\n.input-group-lg > .form-control-plaintext.form-control,\n.input-group-lg > .input-group-prepend > .form-control-plaintext.input-group-text,\n.input-group-lg > .input-group-append > .form-control-plaintext.input-group-text,\n.input-group-lg > .input-group-prepend > .form-control-plaintext.btn,\n.input-group-lg > .input-group-append > .form-control-plaintext.btn {\n    padding-right: 0;\n    padding-left: 0;\n}\n\n.form-control-sm,\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n    padding: 0.25rem 0.5rem;\n    font-size: 0.875rem;\n    line-height: 1.14285714;\n    border-radius: 3px;\n}\n\nselect.form-control-sm:not([size]):not([multiple]),\n.input-group-sm > select.form-control:not([size]):not([multiple]),\n.input-group-sm > .input-group-prepend > select.input-group-text:not([size]):not([multiple]),\n.input-group-sm > .input-group-append > select.input-group-text:not([size]):not([multiple]),\n.input-group-sm > .input-group-prepend > select.btn:not([size]):not([multiple]),\n.input-group-sm > .input-group-append > select.btn:not([size]):not([multiple]) {\n    height: calc(1.8125rem + 2px);\n}\n\n.form-control-lg,\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n    padding: 0.5rem 1rem;\n    font-size: 1.125rem;\n    line-height: 1.44444444;\n    border-radius: 3px;\n}\n\nselect.form-control-lg:not([size]):not([multiple]),\n.input-group-lg > select.form-control:not([size]):not([multiple]),\n.input-group-lg > .input-group-prepend > select.input-group-text:not([size]):not([multiple]),\n.input-group-lg > .input-group-append > select.input-group-text:not([size]):not([multiple]),\n.input-group-lg > .input-group-prepend > select.btn:not([size]):not([multiple]),\n.input-group-lg > .input-group-append > select.btn:not([size]):not([multiple]) {\n    height: calc(2.6875rem + 2px);\n}\n\n.form-group {\n    margin-bottom: 1rem;\n}\n\n.form-text {\n    display: block;\n    margin-top: 0.25rem;\n}\n\n.form-row {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    margin-right: -5px;\n    margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*='col-'] {\n    padding-right: 5px;\n    padding-left: 5px;\n}\n\n.form-check {\n    position: relative;\n    display: block;\n    padding-left: 1.25rem;\n}\n\n.form-check-input {\n    position: absolute;\n    margin-top: 0.3rem;\n    margin-left: -1.25rem;\n}\n\n.form-check-input:disabled ~ .form-check-label {\n    color: #9aa0ac;\n}\n\n.form-check-label {\n    margin-bottom: 0;\n}\n\n.form-check-inline {\n    display: -ms-inline-flexbox;\n    display: inline-flex;\n    -ms-flex-align: center;\n    align-items: center;\n    padding-left: 0;\n    margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n    position: static;\n    margin-top: 0;\n    margin-right: 0.3125rem;\n    margin-left: 0;\n}\n\n.valid-feedback {\n    display: none;\n    width: 100%;\n    margin-top: 0.25rem;\n    font-size: 87.5%;\n    color: #5eba00;\n}\n\n.valid-tooltip {\n    position: absolute;\n    top: 100%;\n    z-index: 5;\n    display: none;\n    max-width: 100%;\n    padding: 0.5rem;\n    margin-top: 0.1rem;\n    font-size: 0.875rem;\n    line-height: 1;\n    color: #fff;\n    background-color: rgba(94, 186, 0, 0.8);\n    border-radius: 0.2rem;\n}\n\n.was-validated .form-control:valid,\n.form-control.is-valid,\n.was-validated .custom-select:valid,\n.custom-select.is-valid {\n    border-color: #5eba00;\n}\n\n.was-validated .form-control:valid:focus,\n.form-control.is-valid:focus,\n.was-validated .custom-select:valid:focus,\n.custom-select.is-valid:focus {\n    border-color: #5eba00;\n    box-shadow: 0 0 0 2px rgba(94, 186, 0, 0.25);\n}\n\n.was-validated .form-control:valid ~ .valid-feedback,\n.was-validated .form-control:valid ~ .valid-tooltip,\n.form-control.is-valid ~ .valid-feedback,\n.form-control.is-valid ~ .valid-tooltip,\n.was-validated .custom-select:valid ~ .valid-feedback,\n.was-validated .custom-select:valid ~ .valid-tooltip,\n.custom-select.is-valid ~ .valid-feedback,\n.custom-select.is-valid ~ .valid-tooltip {\n    display: block;\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label,\n.form-check-input.is-valid ~ .form-check-label {\n    color: #5eba00;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip,\n.form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n    display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label,\n.custom-control-input.is-valid ~ .custom-control-label {\n    color: #5eba00;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before,\n.custom-control-input.is-valid ~ .custom-control-label::before {\n    background-color: #9eff3b;\n}\n\n.was-validated .custom-control-input:valid ~ .valid-feedback,\n.was-validated .custom-control-input:valid ~ .valid-tooltip,\n.custom-control-input.is-valid ~ .valid-feedback,\n.custom-control-input.is-valid ~ .valid-tooltip {\n    display: block;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,\n.custom-control-input.is-valid:checked ~ .custom-control-label::before {\n    background-color: #78ed00;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,\n.custom-control-input.is-valid:focus ~ .custom-control-label::before {\n    box-shadow:\n        0 0 0 1px #f5f7fb,\n        0 0 0 2px rgba(94, 186, 0, 0.25);\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label,\n.custom-file-input.is-valid ~ .custom-file-label {\n    border-color: #5eba00;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label::before,\n.custom-file-input.is-valid ~ .custom-file-label::before {\n    border-color: inherit;\n}\n\n.was-validated .custom-file-input:valid ~ .valid-feedback,\n.was-validated .custom-file-input:valid ~ .valid-tooltip,\n.custom-file-input.is-valid ~ .valid-feedback,\n.custom-file-input.is-valid ~ .valid-tooltip {\n    display: block;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label,\n.custom-file-input.is-valid:focus ~ .custom-file-label {\n    box-shadow: 0 0 0 2px rgba(94, 186, 0, 0.25);\n}\n\n.invalid-feedback {\n    display: none;\n    width: 100%;\n    margin-top: 0.25rem;\n    font-size: 87.5%;\n    color: #cd201f;\n}\n\n.invalid-tooltip {\n    position: absolute;\n    top: 100%;\n    z-index: 5;\n    display: none;\n    max-width: 100%;\n    padding: 0.5rem;\n    margin-top: 0.1rem;\n    font-size: 0.875rem;\n    line-height: 1;\n    color: #fff;\n    background-color: rgba(205, 32, 31, 0.8);\n    border-radius: 0.2rem;\n}\n\n.was-validated .form-control:invalid,\n.form-control.is-invalid,\n.was-validated .custom-select:invalid,\n.custom-select.is-invalid {\n    border-color: #cd201f;\n}\n\n.was-validated .form-control:invalid:focus,\n.form-control.is-invalid:focus,\n.was-validated .custom-select:invalid:focus,\n.custom-select.is-invalid:focus {\n    border-color: #cd201f;\n    box-shadow: 0 0 0 2px rgba(205, 32, 31, 0.25);\n}\n\n.was-validated .form-control:invalid ~ .invalid-feedback,\n.was-validated .form-control:invalid ~ .invalid-tooltip,\n.form-control.is-invalid ~ .invalid-feedback,\n.form-control.is-invalid ~ .invalid-tooltip,\n.was-validated .custom-select:invalid ~ .invalid-feedback,\n.was-validated .custom-select:invalid ~ .invalid-tooltip,\n.custom-select.is-invalid ~ .invalid-feedback,\n.custom-select.is-invalid ~ .invalid-tooltip {\n    display: block;\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label,\n.form-check-input.is-invalid ~ .form-check-label {\n    color: #cd201f;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip,\n.form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n    display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label,\n.custom-control-input.is-invalid ~ .custom-control-label {\n    color: #cd201f;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before,\n.custom-control-input.is-invalid ~ .custom-control-label::before {\n    background-color: #ec8080;\n}\n\n.was-validated .custom-control-input:invalid ~ .invalid-feedback,\n.was-validated .custom-control-input:invalid ~ .invalid-tooltip,\n.custom-control-input.is-invalid ~ .invalid-feedback,\n.custom-control-input.is-invalid ~ .invalid-tooltip {\n    display: block;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,\n.custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n    background-color: #e23e3d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,\n.custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n    box-shadow:\n        0 0 0 1px #f5f7fb,\n        0 0 0 2px rgba(205, 32, 31, 0.25);\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label,\n.custom-file-input.is-invalid ~ .custom-file-label {\n    border-color: #cd201f;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label::before,\n.custom-file-input.is-invalid ~ .custom-file-label::before {\n    border-color: inherit;\n}\n\n.was-validated .custom-file-input:invalid ~ .invalid-feedback,\n.was-validated .custom-file-input:invalid ~ .invalid-tooltip,\n.custom-file-input.is-invalid ~ .invalid-feedback,\n.custom-file-input.is-invalid ~ .invalid-tooltip {\n    display: block;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,\n.custom-file-input.is-invalid:focus ~ .custom-file-label {\n    box-shadow: 0 0 0 2px rgba(205, 32, 31, 0.25);\n}\n\n.form-inline {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-flow: row wrap;\n    flex-flow: row wrap;\n    -ms-flex-align: center;\n    align-items: center;\n}\n\n.form-inline .form-check {\n    width: 100%;\n}\n\n@media (min-width: 576px) {\n    .form-inline label {\n        display: -ms-flexbox;\n        display: flex;\n        -ms-flex-align: center;\n        align-items: center;\n        -ms-flex-pack: center;\n        justify-content: center;\n        margin-bottom: 0;\n    }\n    .form-inline .form-group {\n        display: -ms-flexbox;\n        display: flex;\n        -ms-flex: 0 0 auto;\n        flex: 0 0 auto;\n        -ms-flex-flow: row wrap;\n        flex-flow: row wrap;\n        -ms-flex-align: center;\n        align-items: center;\n        margin-bottom: 0;\n    }\n    .form-inline .form-control {\n        display: inline-block;\n        width: auto;\n        vertical-align: middle;\n    }\n    .form-inline .form-control-plaintext {\n        display: inline-block;\n    }\n    .form-inline .input-group {\n        width: auto;\n    }\n    .form-inline .form-check {\n        display: -ms-flexbox;\n        display: flex;\n        -ms-flex-align: center;\n        align-items: center;\n        -ms-flex-pack: center;\n        justify-content: center;\n        width: auto;\n        padding-left: 0;\n    }\n    .form-inline .form-check-input {\n        position: relative;\n        margin-top: 0;\n        margin-right: 0.25rem;\n        margin-left: 0;\n    }\n    .form-inline .custom-control {\n        -ms-flex-align: center;\n        align-items: center;\n        -ms-flex-pack: center;\n        justify-content: center;\n    }\n    .form-inline .custom-control-label {\n        margin-bottom: 0;\n    }\n}\n\n.btn {\n    display: inline-block;\n    font-weight: 400;\n    text-align: center;\n    white-space: nowrap;\n    vertical-align: middle;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    border: 1px solid transparent;\n    padding: 0.375rem 0.75rem;\n    font-size: 0.9375rem;\n    line-height: 1.84615385;\n    border-radius: 3px;\n    transition:\n        color 0.15s ease-in-out,\n        background-color 0.15s ease-in-out,\n        border-color 0.15s ease-in-out,\n        box-shadow 0.15s ease-in-out;\n}\n\n.btn:hover,\n.btn:focus {\n    text-decoration: none;\n}\n\n.btn:focus,\n.btn.focus {\n    outline: 0;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.btn.disabled,\n.btn:disabled {\n    opacity: 0.65;\n}\n\n.btn:not(:disabled):not(.disabled) {\n    cursor: pointer;\n}\n\n.btn:not(:disabled):not(.disabled):active,\n.btn:not(:disabled):not(.disabled).active {\n    background-image: none;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n    pointer-events: none;\n}\n\n.btn-primary {\n    color: #fff;\n    background-color: #467fcf;\n    border-color: #467fcf;\n}\n\n.btn-primary:hover {\n    color: #fff;\n    background-color: #316cbe;\n    border-color: #2f66b3;\n}\n\n.btn-primary:focus,\n.btn-primary.focus {\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.5);\n}\n\n.btn-primary.disabled,\n.btn-primary:disabled {\n    color: #fff;\n    background-color: #467fcf;\n    border-color: #467fcf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active,\n.btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n    color: #fff;\n    background-color: #2f66b3;\n    border-color: #2c60a9;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus,\n.btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.5);\n}\n\n.btn-secondary {\n    color: #fff;\n    background-color: #868e96;\n    border-color: #868e96;\n}\n\n.btn-secondary:hover {\n    color: #fff;\n    background-color: #727b84;\n    border-color: #6c757d;\n}\n\n.btn-secondary:focus,\n.btn-secondary.focus {\n    box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);\n}\n\n.btn-secondary.disabled,\n.btn-secondary:disabled {\n    color: #fff;\n    background-color: #868e96;\n    border-color: #868e96;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active,\n.btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n    color: #fff;\n    background-color: #6c757d;\n    border-color: #666e76;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus,\n.btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);\n}\n\n[data-theme='dark'] .btn-secondary {\n    color: #868e96;\n    background-color: transparent;\n    background-image: none;\n    border-color: #868e96;\n}\n\n[data-theme='dark'] .btn-secondary:hover {\n    color: #fff;\n    background-color: #868e96;\n    border-color: #868e96;\n}\n\n[data-theme='dark'] .btn-secondary:focus,\n[data-theme='dark'] .btn-secondary.focus {\n    box-shadow: none;\n}\n\n[data-theme='dark'] .btn-secondary:focus-visible,\n[data-theme='dark'] .btn-secondary.focus {\n    box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);\n}\n\n[data-theme='dark'] .btn-secondary.disabled,\n[data-theme='dark'] .btn-secondary:disabled {\n    color: #868e96;\n    background-color: transparent;\n    border: none;\n}\n\n[data-theme='dark'] .btn-secondary:not(:disabled):not(.disabled):active,\n[data-theme='dark'] .btn-secondary:not(:disabled):not(.disabled).active {\n    color: #fff;\n    background-color: #868e96;\n    border-color: #868e96;\n}\n\n[data-theme='dark'] .btn-secondary:not(:disabled):not(.disabled):active:focus,\n[data-theme='dark'] .btn-secondary:not(:disabled):not(.disabled).active:focus {\n    box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);\n}\n\n.btn-success {\n    color: #fff;\n    background-color: var(--btn-success-bgcolor);\n    border-color: var(--btn-success-bgcolor);\n}\n\n.btn-success:hover {\n    color: #fff;\n    background-color: #4b9400;\n    border-color: #4b9400;\n}\n\n.btn-success:focus,\n.btn-success.focus {\n    box-shadow: 0 0 0 2px rgba(94, 186, 0, 0.5);\n}\n\n.btn-success.disabled,\n.btn-success:disabled {\n    color: #fff;\n    background-color: #5eba00;\n    border-color: #5eba00;\n}\n\n.btn-success:not(:disabled):not(.disabled):active,\n.btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n    color: #fff;\n    background-color: #448700;\n    border-color: #448700;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus,\n.btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(94, 186, 0, 0.5);\n}\n\n.btn-info {\n    color: #fff;\n    background-color: #45aaf2;\n    border-color: #45aaf2;\n}\n\n.btn-info:hover {\n    color: #fff;\n    background-color: #219af0;\n    border-color: #1594ef;\n}\n\n.btn-info:focus,\n.btn-info.focus {\n    box-shadow: 0 0 0 2px rgba(69, 170, 242, 0.5);\n}\n\n.btn-info.disabled,\n.btn-info:disabled {\n    color: #fff;\n    background-color: #45aaf2;\n    border-color: #45aaf2;\n}\n\n.btn-info:not(:disabled):not(.disabled):active,\n.btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n    color: #fff;\n    background-color: #1594ef;\n    border-color: #108ee7;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus,\n.btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(69, 170, 242, 0.5);\n}\n\n.btn-warning {\n    color: #fff;\n    background-color: #f1c40f;\n    border-color: #f1c40f;\n}\n\n.btn-warning:hover {\n    color: #fff;\n    background-color: #cea70c;\n    border-color: #c29d0b;\n}\n\n.btn-warning:focus,\n.btn-warning.focus {\n    box-shadow: 0 0 0 2px rgba(241, 196, 15, 0.5);\n}\n\n.btn-warning.disabled,\n.btn-warning:disabled {\n    color: #fff;\n    background-color: #f1c40f;\n    border-color: #f1c40f;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active,\n.btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n    color: #fff;\n    background-color: #c29d0b;\n    border-color: #b6940b;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus,\n.btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(241, 196, 15, 0.5);\n}\n\n.btn-danger {\n    color: #fff;\n    background-color: #cd201f;\n    border-color: #cd201f;\n}\n\n.btn-danger:hover {\n    color: #fff;\n    background-color: #ac1b1a;\n    border-color: #a11918;\n}\n\n.btn-danger:focus,\n.btn-danger.focus {\n    box-shadow: 0 0 0 2px rgba(205, 32, 31, 0.5);\n}\n\n.btn-danger.disabled,\n.btn-danger:disabled {\n    color: #fff;\n    background-color: #cd201f;\n    border-color: #cd201f;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active,\n.btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n    color: #fff;\n    background-color: #a11918;\n    border-color: #961717;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus,\n.btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(205, 32, 31, 0.5);\n}\n\n.btn-light {\n    color: #495057;\n    background-color: #f8f9fa;\n    border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n    color: #495057;\n    background-color: #e2e6ea;\n    border-color: #dae0e5;\n}\n\n.btn-light:focus,\n.btn-light.focus {\n    box-shadow: 0 0 0 2px rgba(248, 249, 250, 0.5);\n}\n\n.btn-light.disabled,\n.btn-light:disabled {\n    color: #495057;\n    background-color: #f8f9fa;\n    border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active,\n.btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n    color: #495057;\n    background-color: #dae0e5;\n    border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus,\n.btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(248, 249, 250, 0.5);\n}\n\n.btn-dark {\n    color: #fff;\n    background-color: #343a40;\n    border-color: #343a40;\n}\n\n.btn-dark:hover {\n    color: #fff;\n    background-color: #23272b;\n    border-color: #1d2124;\n}\n\n.btn-dark:focus,\n.btn-dark.focus {\n    box-shadow: 0 0 0 2px rgba(52, 58, 64, 0.5);\n}\n\n.btn-dark.disabled,\n.btn-dark:disabled {\n    color: #fff;\n    background-color: #343a40;\n    border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active,\n.btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n    color: #fff;\n    background-color: #1d2124;\n    border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus,\n.btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-primary {\n    color: #467fcf;\n    background-color: transparent;\n    background-image: none;\n    border-color: #467fcf;\n}\n\n.btn-outline-primary:hover {\n    color: #fff;\n    background-color: #467fcf;\n    border-color: #467fcf;\n}\n\n.btn-outline-primary:focus,\n.btn-outline-primary.focus {\n    box-shadow: none;\n}\n\n.btn-outline-primary:focus-visible,\n.btn-outline-primary.focus {\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.5);\n}\n\n.btn-outline-primary.disabled,\n.btn-outline-primary:disabled {\n    color: #467fcf;\n    background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active,\n.btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n    color: #fff;\n    background-color: #467fcf;\n    border-color: #467fcf;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus,\n.btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.5);\n}\n\n.btn-outline-secondary {\n    color: #868e96;\n    background-color: transparent;\n    background-image: none;\n    border-color: #868e96;\n}\n\n.btn-outline-secondary:hover {\n    color: #fff;\n    background-color: #868e96;\n    border-color: #868e96;\n}\n\n.btn-outline-secondary:focus,\n.btn-outline-secondary.focus {\n    box-shadow: none;\n}\n\n.btn-outline-secondary:focus-visible,\n.btn-outline-secondary.focus {\n    box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);\n}\n\n.btn-outline-secondary.disabled,\n.btn-outline-secondary:disabled {\n    color: #868e96;\n    background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active,\n.btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n    color: #fff;\n    background-color: #868e96;\n    border-color: #868e96;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,\n.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);\n}\n\n.btn-outline-success {\n    color: #5eba00;\n    background-color: transparent;\n    background-image: none;\n    border-color: #5eba00;\n}\n\n.btn-outline-success:hover {\n    color: #fff;\n    background-color: #5eba00;\n    border-color: #5eba00;\n}\n\n.btn-outline-success:focus,\n.btn-outline-success.focus {\n    box-shadow: 0 0 0 2px rgba(94, 186, 0, 0.5);\n}\n\n.btn-outline-success.disabled,\n.btn-outline-success:disabled {\n    color: #5eba00;\n    background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active,\n.btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n    color: #fff;\n    background-color: #5eba00;\n    border-color: #5eba00;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus,\n.btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(94, 186, 0, 0.5);\n}\n\n.btn-outline-info {\n    color: #45aaf2;\n    background-color: transparent;\n    background-image: none;\n    border-color: #45aaf2;\n}\n\n.btn-outline-info:hover {\n    color: #fff;\n    background-color: #45aaf2;\n    border-color: #45aaf2;\n}\n\n.btn-outline-info:focus,\n.btn-outline-info.focus {\n    box-shadow: 0 0 0 2px rgba(69, 170, 242, 0.5);\n}\n\n.btn-outline-info.disabled,\n.btn-outline-info:disabled {\n    color: #45aaf2;\n    background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active,\n.btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n    color: #fff;\n    background-color: #45aaf2;\n    border-color: #45aaf2;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus,\n.btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(69, 170, 242, 0.5);\n}\n\n.btn-outline-warning {\n    color: #f1c40f;\n    background-color: transparent;\n    background-image: none;\n    border-color: #f1c40f;\n}\n\n.btn-outline-warning:hover {\n    color: #fff;\n    background-color: #f1c40f;\n    border-color: #f1c40f;\n}\n\n.btn-outline-warning:focus,\n.btn-outline-warning.focus {\n    box-shadow: 0 0 0 2px rgba(241, 196, 15, 0.5);\n}\n\n.btn-outline-warning.disabled,\n.btn-outline-warning:disabled {\n    color: #f1c40f;\n    background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active,\n.btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n    color: #fff;\n    background-color: #f1c40f;\n    border-color: #f1c40f;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus,\n.btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(241, 196, 15, 0.5);\n}\n\n.btn-outline-danger {\n    color: #cd201f;\n    background-color: transparent;\n    background-image: none;\n    border-color: #cd201f;\n}\n\n.btn-outline-danger:hover {\n    color: #fff;\n    background-color: #cd201f;\n    border-color: #cd201f;\n}\n\n.btn-outline-danger:focus,\n.btn-outline-danger.focus {\n    box-shadow: 0 0 0 2px rgba(205, 32, 31, 0.5);\n}\n\n.btn-outline-danger.disabled,\n.btn-outline-danger:disabled {\n    color: #cd201f;\n    background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active,\n.btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n    color: #fff;\n    background-color: #cd201f;\n    border-color: #cd201f;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus,\n.btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(205, 32, 31, 0.5);\n}\n\n.btn-outline-light {\n    color: #f8f9fa;\n    background-color: transparent;\n    background-image: none;\n    border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n    color: #495057;\n    background-color: #f8f9fa;\n    border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus,\n.btn-outline-light.focus {\n    box-shadow: 0 0 0 2px rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled,\n.btn-outline-light:disabled {\n    color: #f8f9fa;\n    background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active,\n.btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n    color: #495057;\n    background-color: #f8f9fa;\n    border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus,\n.btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n    color: #343a40;\n    background-color: transparent;\n    background-image: none;\n    border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n    color: #fff;\n    background-color: #343a40;\n    border-color: #343a40;\n}\n\n.btn-outline-dark:focus,\n.btn-outline-dark.focus {\n    box-shadow: 0 0 0 2px rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled,\n.btn-outline-dark:disabled {\n    color: #343a40;\n    background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active,\n.btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n    color: #fff;\n    background-color: #343a40;\n    border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus,\n.btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n    font-weight: 400;\n    color: #467fcf;\n    background-color: transparent;\n}\n\n.btn-link:hover {\n    color: #295a9f;\n    text-decoration: underline;\n    background-color: transparent;\n    border-color: transparent;\n}\n\n.btn-link:focus,\n.btn-link.focus {\n    text-decoration: underline;\n    border-color: transparent;\n    box-shadow: none;\n}\n\n.btn-link:disabled,\n.btn-link.disabled {\n    color: #868e96;\n}\n\n.btn-lg,\n.btn-group-lg > .btn {\n    padding: 0.5rem 1rem;\n    font-size: 1.125rem;\n    line-height: 1.625;\n    border-radius: 3px;\n}\n\n.btn-sm,\n.btn-group-sm > .btn {\n    padding: 0.25rem 0.5rem;\n    font-size: 0.875rem;\n    line-height: 1.33333333;\n    border-radius: 3px;\n}\n\n.btn-block {\n    display: block;\n    width: 100%;\n}\n\n.btn-block + .btn-block {\n    margin-top: 0.5rem;\n}\n\ninput[type='submit'].btn-block,\ninput[type='reset'].btn-block,\ninput[type='button'].btn-block {\n    width: 100%;\n}\n\n.fade {\n    opacity: 0;\n    transition: opacity 0.15s linear;\n}\n\n.fade.show {\n    opacity: 1;\n}\n\n.collapse {\n    display: none;\n}\n\n.collapse.show {\n    display: block;\n}\n\ntr.collapse.show {\n    display: table-row;\n}\n\ntbody.collapse.show {\n    display: table-row-group;\n}\n\n.collapsing {\n    position: relative;\n    height: 0;\n    overflow: hidden;\n    transition: height 0.35s ease;\n}\n\n.dropup,\n.dropdown {\n    position: relative;\n}\n\n.dropdown-toggle::after {\n    display: inline-block;\n    width: 0;\n    height: 0;\n    margin-left: 0.255em;\n    vertical-align: 0.255em;\n    content: '';\n    border-top: 0.3em solid;\n    border-right: 0.3em solid transparent;\n    border-bottom: 0;\n    border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n    margin-left: 0;\n}\n\n.dropdown-menu {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    z-index: 1000;\n    display: none;\n    float: left;\n    min-width: 10rem;\n    padding: 0.5rem 0;\n    margin: 0.125rem 0 0;\n    font-size: 0.9375rem;\n    color: #495057;\n    text-align: left;\n    list-style: none;\n    background-color: var(--ctrl-bgcolor);\n    background-clip: padding-box;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    border-radius: 3px;\n}\n\n[data-theme='dark'] .dropdown-menu {\n    border: 1px solid var(--card-border-color);\n}\n\n.dropup .dropdown-menu {\n    margin-top: 0;\n    margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n    display: inline-block;\n    width: 0;\n    height: 0;\n    margin-left: 0.255em;\n    vertical-align: 0.255em;\n    content: '';\n    border-top: 0;\n    border-right: 0.3em solid transparent;\n    border-bottom: 0.3em solid;\n    border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n    margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n    margin-top: 0;\n    margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n    display: inline-block;\n    width: 0;\n    height: 0;\n    margin-left: 0.255em;\n    vertical-align: 0.255em;\n    content: '';\n    border-top: 0.3em solid transparent;\n    border-bottom: 0.3em solid transparent;\n    border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n    margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n    vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n    margin-top: 0;\n    margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n    display: inline-block;\n    width: 0;\n    height: 0;\n    margin-left: 0.255em;\n    vertical-align: 0.255em;\n    content: '';\n}\n\n.dropleft .dropdown-toggle::after {\n    display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n    display: inline-block;\n    width: 0;\n    height: 0;\n    margin-right: 0.255em;\n    vertical-align: 0.255em;\n    content: '';\n    border-top: 0.3em solid transparent;\n    border-right: 0.3em solid;\n    border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n    margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n    vertical-align: 0;\n}\n\n.dropdown-divider {\n    height: 0;\n    margin: 0.5rem 0;\n    overflow: hidden;\n    border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n    display: block;\n    width: 100%;\n    padding: 0.25rem 1.5rem;\n    clear: both;\n    font-weight: 400;\n    color: var(--ctrl-dropdown-color);\n    text-align: inherit;\n    white-space: nowrap;\n    background-color: transparent;\n    border: 0;\n}\n\n.dropdown-item:hover,\n.dropdown-item:focus {\n    color: var(--ctrl-dropdown-color-focus);\n    text-decoration: none;\n    background-color: var(--ctrl-dropdown-bgcolor-focus);\n}\n\n.dropdown-item.active,\n.dropdown-item:active {\n    color: #fff;\n    text-decoration: none;\n    background-color: #467fcf;\n}\n\n.dropdown-item.disabled,\n.dropdown-item:disabled {\n    color: #868e96;\n    background-color: transparent;\n}\n\n.dropdown-menu.show {\n    display: block;\n}\n\n.dropdown-header {\n    display: block;\n    padding: 0.5rem 1.5rem;\n    margin-bottom: 0;\n    font-size: 0.875rem;\n    color: #868e96;\n    white-space: nowrap;\n}\n\n.btn-group,\n.btn-group-vertical {\n    position: relative;\n    display: -ms-inline-flexbox;\n    display: inline-flex;\n    vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n    position: relative;\n    -ms-flex: 0 1 auto;\n    flex: 0 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n    z-index: 1;\n}\n\n.btn-group > .btn:focus,\n.btn-group > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n    z-index: 1;\n}\n\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group,\n.btn-group-vertical .btn + .btn,\n.btn-group-vertical .btn + .btn-group,\n.btn-group-vertical .btn-group + .btn,\n.btn-group-vertical .btn-group + .btn-group {\n    margin-left: -1px;\n}\n\n.btn-toolbar {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    -ms-flex-pack: start;\n    justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n    width: auto;\n}\n\n.btn-group > .btn:first-child {\n    margin-left: 0;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n    padding-right: 0.5625rem;\n    padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after {\n    margin-left: 0;\n}\n\n.btn-sm + .dropdown-toggle-split,\n.btn-group-sm > .btn + .dropdown-toggle-split {\n    padding-right: 0.375rem;\n    padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split,\n.btn-group-lg > .btn + .dropdown-toggle-split {\n    padding-right: 0.75rem;\n    padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n    -ms-flex-direction: column;\n    flex-direction: column;\n    -ms-flex-align: start;\n    align-items: flex-start;\n    -ms-flex-pack: center;\n    justify-content: center;\n}\n\n.btn-group-vertical .btn,\n.btn-group-vertical .btn-group {\n    width: 100%;\n}\n\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n    margin-top: -1px;\n    margin-left: 0;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n    border-bottom-right-radius: 0;\n    border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n    border-top-left-radius: 0;\n    border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n    margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type='radio'],\n.btn-group-toggle > .btn input[type='checkbox'],\n.btn-group-toggle > .btn-group > .btn input[type='radio'],\n.btn-group-toggle > .btn-group > .btn input[type='checkbox'] {\n    position: absolute;\n    clip: rect(0, 0, 0, 0);\n    pointer-events: none;\n}\n\n.input-group {\n    position: relative;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    -ms-flex-align: stretch;\n    align-items: stretch;\n    width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .custom-select,\n.input-group > .custom-file {\n    position: relative;\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n    width: 1%;\n    margin-bottom: 0;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file:focus {\n    z-index: 3;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n    margin-left: -1px;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::before {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label,\n.input-group > .custom-file:not(:first-child) .custom-file-label::before {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n    display: -ms-flexbox;\n    display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n    position: relative;\n    z-index: 2;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n    margin-left: -1px;\n}\n\n.input-group-prepend {\n    margin-right: -1px;\n}\n\n.input-group-append {\n    margin-left: -1px;\n}\n\n.input-group-text {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    padding: 0.375rem 0.75rem;\n    margin-bottom: 0;\n    font-size: 0.9375rem;\n    font-weight: 400;\n    line-height: 1.6;\n    color: #495057;\n    text-align: center;\n    white-space: nowrap;\n    background-color: #fbfbfc;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    border-radius: 3px;\n}\n\n.input-group-text input[type='radio'],\n.input-group-text input[type='checkbox'] {\n    margin-top: 0;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n}\n\n.custom-control {\n    position: relative;\n    display: block;\n    min-height: 1.5rem;\n    padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n    display: -ms-inline-flexbox;\n    display: inline-flex;\n    margin-right: 1rem;\n}\n\n.custom-control-input {\n    position: absolute;\n    z-index: -1;\n    opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n    color: #fff;\n    background-color: #467fcf;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n    box-shadow:\n        0 0 0 1px #f5f7fb,\n        0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.custom-control-input:active ~ .custom-control-label::before {\n    color: #fff;\n    background-color: #d4e1f4;\n}\n\n.custom-control-input:disabled ~ .custom-control-label {\n    color: #868e96;\n}\n\n.custom-control-input:disabled ~ .custom-control-label::before {\n    background-color: #e9ecef;\n}\n\n.custom-control-label {\n    margin-bottom: 0;\n}\n\n.custom-control-label::before {\n    position: absolute;\n    top: 0.25rem;\n    left: 0;\n    display: block;\n    width: 1rem;\n    height: 1rem;\n    pointer-events: none;\n    content: '';\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    background-color: #dee2e6;\n}\n\n.custom-control-label::after {\n    position: absolute;\n    top: 0.25rem;\n    left: 0;\n    display: block;\n    width: 1rem;\n    height: 1rem;\n    content: '';\n    background-repeat: no-repeat;\n    background-position: center center;\n    background-size: 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n    border-radius: 3px;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before {\n    background-color: #467fcf;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n    background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n    background-color: #467fcf;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n    background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n    background-color: rgba(70, 127, 207, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n    background-color: rgba(70, 127, 207, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n    border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::before {\n    background-color: #467fcf;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n    background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n    background-color: rgba(70, 127, 207, 0.5);\n}\n\n.custom-select {\n    display: inline-block;\n    width: 100%;\n    height: 2.375rem;\n    padding: 0.5rem 1.75rem 0.5rem 0.75rem;\n    line-height: 1.5;\n    color: var(--mcolor);\n    vertical-align: middle;\n    background: var(--card-bgcolor)\n        url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==')\n        no-repeat right 0.75rem center;\n    background-size: 8px 10px;\n    border: 1px solid var(--card-border-color);\n    border-radius: 3px;\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    appearance: none;\n}\n\n.custom-select:focus {\n    border-color: #1991eb;\n    outline: 0;\n    box-shadow:\n        inset 0 1px 2px rgba(0, 0, 0, 0.075),\n        0 0 5px rgba(25, 145, 235, 0.5);\n}\n\n.custom-select:focus::-ms-value {\n    color: #495057;\n    background-color: #fff;\n}\n\n.custom-select[multiple],\n.custom-select[size]:not([size='1']) {\n    height: auto;\n    padding-right: 0.75rem;\n    background-image: none;\n}\n\n.custom-select:disabled {\n    color: #868e96;\n    background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n    opacity: 0;\n}\n\n.custom-select-sm {\n    height: calc(1.8125rem + 2px);\n    padding-top: 0.5rem;\n    padding-bottom: 0.5rem;\n    font-size: 75%;\n}\n\n.custom-select-lg {\n    height: calc(2.6875rem + 2px);\n    padding-top: 0.5rem;\n    padding-bottom: 0.5rem;\n    font-size: 125%;\n}\n\n.custom-file {\n    position: relative;\n    display: inline-block;\n    width: 100%;\n    height: 2.375rem;\n    margin-bottom: 0;\n}\n\n.custom-file-input {\n    position: relative;\n    z-index: 2;\n    width: 100%;\n    height: 2.375rem;\n    margin: 0;\n    opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-control {\n    border-color: #1991eb;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.custom-file-input:focus ~ .custom-file-control::before {\n    border-color: #1991eb;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n    content: 'Browse';\n}\n\n.custom-file-label {\n    position: absolute;\n    top: 0;\n    right: 0;\n    left: 0;\n    z-index: 1;\n    height: 2.375rem;\n    padding: 0.375rem 0.75rem;\n    line-height: 1.5;\n    color: #495057;\n    background-color: #fff;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    border-radius: 3px;\n}\n\n.custom-file-label::after {\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    z-index: 3;\n    display: block;\n    height: calc(2.375rem - 1px * 2);\n    padding: 0.375rem 0.75rem;\n    line-height: 1.5;\n    color: #495057;\n    content: 'Browse';\n    background-color: #fbfbfc;\n    border-left: 1px solid rgba(0, 40, 100, 0.12);\n    border-radius: 0 3px 3px 0;\n}\n\n.nav {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    padding-left: 0;\n    margin-bottom: 0;\n    list-style: none;\n}\n\n.nav-link {\n    display: block;\n    padding: 0.5rem 1rem;\n}\n\n.nav-link:hover,\n.nav-link:focus {\n    text-decoration: none;\n}\n\n.nav-link.disabled {\n    color: #868e96;\n}\n\n.nav-tabs {\n    border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n    margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n    border: 1px solid transparent;\n    border-top-left-radius: 3px;\n    border-top-right-radius: 3px;\n}\n\n.nav-tabs .nav-link:hover,\n.nav-tabs .nav-link:focus {\n    border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n    color: #868e96;\n    background-color: transparent;\n    border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n    color: #495057;\n    background-color: #f5f7fb;\n    border-color: #dee2e6 #dee2e6 #f5f7fb;\n}\n\n.nav-tabs .dropdown-menu {\n    margin-top: -1px;\n    border-top-left-radius: 0;\n    border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n    border-radius: 3px;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n    color: #fff;\n    background-color: #467fcf;\n}\n\n.nav-fill .nav-item {\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n    text-align: center;\n}\n\n.nav-justified .nav-item {\n    -ms-flex-preferred-size: 0;\n    flex-basis: 0;\n    -ms-flex-positive: 1;\n    flex-grow: 1;\n    text-align: center;\n}\n\n.tab-content > .tab-pane {\n    display: none;\n}\n\n.tab-content > .active {\n    display: block;\n}\n\n.navbar {\n    position: relative;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: justify;\n    justify-content: space-between;\n    padding: 0.5rem 1rem;\n}\n\n.navbar > .container,\n.navbar > .container-fluid {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: justify;\n    justify-content: space-between;\n}\n\n.navbar-brand {\n    display: inline-block;\n    padding-top: 0.359375rem;\n    padding-bottom: 0.359375rem;\n    margin-right: 1rem;\n    font-size: 1.125rem;\n    line-height: inherit;\n    white-space: nowrap;\n}\n\n.navbar-brand:hover,\n.navbar-brand:focus {\n    text-decoration: none;\n}\n\n.navbar-nav {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    padding-left: 0;\n    margin-bottom: 0;\n    list-style: none;\n}\n\n.navbar-nav .nav-link {\n    padding-right: 0;\n    padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n    position: static;\n    float: none;\n}\n\n.navbar-text {\n    display: inline-block;\n    padding-top: 0.5rem;\n    padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n    -ms-flex-preferred-size: 100%;\n    flex-basis: 100%;\n    -ms-flex-positive: 1;\n    flex-grow: 1;\n    -ms-flex-align: center;\n    align-items: center;\n}\n\n.navbar-toggler {\n    padding: 0.25rem 0.75rem;\n    font-size: 1.125rem;\n    line-height: 1;\n    background-color: transparent;\n    border: 1px solid transparent;\n    border-radius: 3px;\n}\n\n.navbar-toggler:hover,\n.navbar-toggler:focus {\n    text-decoration: none;\n}\n\n.navbar-toggler:not(:disabled):not(.disabled) {\n    cursor: pointer;\n}\n\n.navbar-toggler-icon {\n    display: inline-block;\n    width: 1.5em;\n    height: 1.5em;\n    vertical-align: middle;\n    content: '';\n    background: no-repeat center center;\n    background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n    .navbar-expand-sm > .container,\n    .navbar-expand-sm > .container-fluid {\n        padding-right: 0;\n        padding-left: 0;\n    }\n}\n\n@media (min-width: 576px) {\n    .navbar-expand-sm {\n        -ms-flex-flow: row nowrap;\n        flex-flow: row nowrap;\n        -ms-flex-pack: start;\n        justify-content: flex-start;\n    }\n    .navbar-expand-sm .navbar-nav {\n        -ms-flex-direction: row;\n        flex-direction: row;\n    }\n    .navbar-expand-sm .navbar-nav .dropdown-menu {\n        position: absolute;\n    }\n    .navbar-expand-sm .navbar-nav .dropdown-menu-right {\n        right: 0;\n        left: auto;\n    }\n    .navbar-expand-sm .navbar-nav .nav-link {\n        padding-right: 0.5rem;\n        padding-left: 0.5rem;\n    }\n    .navbar-expand-sm > .container,\n    .navbar-expand-sm > .container-fluid {\n        -ms-flex-wrap: nowrap;\n        flex-wrap: nowrap;\n    }\n    .navbar-expand-sm .navbar-collapse {\n        display: -ms-flexbox !important;\n        display: flex !important;\n        -ms-flex-preferred-size: auto;\n        flex-basis: auto;\n    }\n    .navbar-expand-sm .navbar-toggler {\n        display: none;\n    }\n    .navbar-expand-sm .dropup .dropdown-menu {\n        top: auto;\n        bottom: 100%;\n    }\n}\n\n@media (max-width: 767.98px) {\n    .navbar-expand-md > .container,\n    .navbar-expand-md > .container-fluid {\n        padding-right: 0;\n        padding-left: 0;\n    }\n}\n\n@media (min-width: 768px) {\n    .navbar-expand-md {\n        -ms-flex-flow: row nowrap;\n        flex-flow: row nowrap;\n        -ms-flex-pack: start;\n        justify-content: flex-start;\n    }\n    .navbar-expand-md .navbar-nav {\n        -ms-flex-direction: row;\n        flex-direction: row;\n    }\n    .navbar-expand-md .navbar-nav .dropdown-menu {\n        position: absolute;\n    }\n    .navbar-expand-md .navbar-nav .dropdown-menu-right {\n        right: 0;\n        left: auto;\n    }\n    .navbar-expand-md .navbar-nav .nav-link {\n        padding-right: 0.5rem;\n        padding-left: 0.5rem;\n    }\n    .navbar-expand-md > .container,\n    .navbar-expand-md > .container-fluid {\n        -ms-flex-wrap: nowrap;\n        flex-wrap: nowrap;\n    }\n    .navbar-expand-md .navbar-collapse {\n        display: -ms-flexbox !important;\n        display: flex !important;\n        -ms-flex-preferred-size: auto;\n        flex-basis: auto;\n    }\n    .navbar-expand-md .navbar-toggler {\n        display: none;\n    }\n    .navbar-expand-md .dropup .dropdown-menu {\n        top: auto;\n        bottom: 100%;\n    }\n}\n\n@media (max-width: 991.98px) {\n    .navbar-expand-lg > .container,\n    .navbar-expand-lg > .container-fluid {\n        padding-right: 0;\n        padding-left: 0;\n    }\n}\n\n@media (min-width: 992px) {\n    .navbar-expand-lg {\n        -ms-flex-flow: row nowrap;\n        flex-flow: row nowrap;\n        -ms-flex-pack: start;\n        justify-content: flex-start;\n    }\n    .navbar-expand-lg .navbar-nav {\n        -ms-flex-direction: row;\n        flex-direction: row;\n    }\n    .navbar-expand-lg .navbar-nav .dropdown-menu {\n        position: absolute;\n    }\n    .navbar-expand-lg .navbar-nav .dropdown-menu-right {\n        right: 0;\n        left: auto;\n    }\n    .navbar-expand-lg .navbar-nav .nav-link {\n        padding-right: 0.5rem;\n        padding-left: 0.5rem;\n    }\n    .navbar-expand-lg > .container,\n    .navbar-expand-lg > .container-fluid {\n        -ms-flex-wrap: nowrap;\n        flex-wrap: nowrap;\n    }\n    .navbar-expand-lg .navbar-collapse {\n        display: -ms-flexbox !important;\n        display: flex !important;\n        -ms-flex-preferred-size: auto;\n        flex-basis: auto;\n    }\n    .navbar-expand-lg .navbar-toggler {\n        display: none;\n    }\n    .navbar-expand-lg .dropup .dropdown-menu {\n        top: auto;\n        bottom: 100%;\n    }\n}\n\n@media (max-width: 1279.98px) {\n    .navbar-expand-xl > .container,\n    .navbar-expand-xl > .container-fluid {\n        padding-right: 0;\n        padding-left: 0;\n    }\n}\n\n@media (min-width: 1280px) {\n    .navbar-expand-xl {\n        -ms-flex-flow: row nowrap;\n        flex-flow: row nowrap;\n        -ms-flex-pack: start;\n        justify-content: flex-start;\n    }\n    .navbar-expand-xl .navbar-nav {\n        -ms-flex-direction: row;\n        flex-direction: row;\n    }\n    .navbar-expand-xl .navbar-nav .dropdown-menu {\n        position: absolute;\n    }\n    .navbar-expand-xl .navbar-nav .dropdown-menu-right {\n        right: 0;\n        left: auto;\n    }\n    .navbar-expand-xl .navbar-nav .nav-link {\n        padding-right: 0.5rem;\n        padding-left: 0.5rem;\n    }\n    .navbar-expand-xl > .container,\n    .navbar-expand-xl > .container-fluid {\n        -ms-flex-wrap: nowrap;\n        flex-wrap: nowrap;\n    }\n    .navbar-expand-xl .navbar-collapse {\n        display: -ms-flexbox !important;\n        display: flex !important;\n        -ms-flex-preferred-size: auto;\n        flex-basis: auto;\n    }\n    .navbar-expand-xl .navbar-toggler {\n        display: none;\n    }\n    .navbar-expand-xl .dropup .dropdown-menu {\n        top: auto;\n        bottom: 100%;\n    }\n}\n\n.navbar-expand {\n    -ms-flex-flow: row nowrap;\n    flex-flow: row nowrap;\n    -ms-flex-pack: start;\n    justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n    padding-right: 0;\n    padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n    -ms-flex-direction: row;\n    flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n    position: absolute;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu-right {\n    right: 0;\n    left: auto;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n    padding-right: 0.5rem;\n    padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n    -ms-flex-wrap: nowrap;\n    flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n    display: -ms-flexbox !important;\n    display: flex !important;\n    -ms-flex-preferred-size: auto;\n    flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n    display: none;\n}\n\n.navbar-expand .dropup .dropdown-menu {\n    top: auto;\n    bottom: 100%;\n}\n\n.navbar-light .navbar-brand {\n    color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover,\n.navbar-light .navbar-brand:focus {\n    color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n    color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover,\n.navbar-light .navbar-nav .nav-link:focus {\n    color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n    color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n    color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n    color: rgba(0, 0, 0, 0.5);\n    border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n    background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\");\n}\n\n.navbar-light .navbar-text {\n    color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n    color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover,\n.navbar-light .navbar-text a:focus {\n    color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n    color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover,\n.navbar-dark .navbar-brand:focus {\n    color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n    color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover,\n.navbar-dark .navbar-nav .nav-link:focus {\n    color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n    color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n    color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n    color: rgba(255, 255, 255, 0.5);\n    border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n    background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\");\n}\n\n.navbar-dark .navbar-text {\n    color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n    color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover,\n.navbar-dark .navbar-text a:focus {\n    color: #fff;\n}\n\n.card {\n    position: relative;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    min-width: 0;\n    word-wrap: break-word;\n    background-color: var(--card-bgcolor);\n    background-clip: border-box;\n    border: 1px solid var(--card-border-color);\n    border-radius: 3px;\n}\n\n.card > hr {\n    margin-right: 0;\n    margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n    border-top-left-radius: 3px;\n    border-top-right-radius: 3px;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n    border-bottom-right-radius: 3px;\n    border-bottom-left-radius: 3px;\n}\n\n.card-body {\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n    padding: 1.5rem;\n}\n\n.card-title {\n    margin-bottom: 1.5rem;\n}\n\n.card-subtitle {\n    margin-top: -0.75rem;\n    margin-bottom: 0;\n}\n\n.card-text:last-child {\n    margin-bottom: 0;\n}\n\n.card-link:hover {\n    text-decoration: none;\n}\n\n.card-link + .card-link {\n    margin-left: 1.5rem;\n}\n\n.card-header {\n    padding: 1.5rem 1.5rem;\n    margin-bottom: 0;\n    background-color: rgba(0, 0, 0, 0.03);\n    border-bottom: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.card-header:first-child {\n    border-radius: calc(3px - 1px) calc(3px - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n    border-top: 0;\n}\n\n.card-footer {\n    padding: 1.5rem 1.5rem;\n    background-color: rgba(0, 0, 0, 0.03);\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.card-footer:last-child {\n    border-radius: 0 0 calc(3px - 1px) calc(3px - 1px);\n}\n\n.card-header-tabs {\n    margin-right: -0.75rem;\n    margin-bottom: -1.5rem;\n    margin-left: -0.75rem;\n    border-bottom: 0;\n}\n\n.card-header-pills {\n    margin-right: -0.75rem;\n    margin-left: -0.75rem;\n}\n\n.card-img-overlay {\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    padding: 1.25rem;\n}\n\n.card-img {\n    width: 100%;\n    border-radius: calc(3px - 1px);\n}\n\n.card-img-top {\n    width: 100%;\n    border-top-left-radius: calc(3px - 1px);\n    border-top-right-radius: calc(3px - 1px);\n}\n\n.card-img-bottom {\n    width: 100%;\n    border-bottom-right-radius: calc(3px - 1px);\n    border-bottom-left-radius: calc(3px - 1px);\n}\n\n.card-deck {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n}\n\n.card-deck .card {\n    margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n    .card-deck {\n        -ms-flex-flow: row wrap;\n        flex-flow: row wrap;\n        margin-right: -0.75rem;\n        margin-left: -0.75rem;\n    }\n    .card-deck .card {\n        display: -ms-flexbox;\n        display: flex;\n        -ms-flex: 1 0 0%;\n        flex: 1 0 0%;\n        -ms-flex-direction: column;\n        flex-direction: column;\n        margin-right: 0.75rem;\n        margin-bottom: 0;\n        margin-left: 0.75rem;\n    }\n}\n\n.card-group {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n}\n\n.card-group > .card {\n    margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n    .card-group {\n        -ms-flex-flow: row wrap;\n        flex-flow: row wrap;\n    }\n    .card-group > .card {\n        -ms-flex: 1 0 0%;\n        flex: 1 0 0%;\n        margin-bottom: 0;\n    }\n    .card-group > .card + .card {\n        margin-left: 0;\n        border-left: 0;\n    }\n    .card-group > .card:first-child {\n        border-top-right-radius: 0;\n        border-bottom-right-radius: 0;\n    }\n    .card-group > .card:first-child .card-img-top,\n    .card-group > .card:first-child .card-header {\n        border-top-right-radius: 0;\n    }\n    .card-group > .card:first-child .card-img-bottom,\n    .card-group > .card:first-child .card-footer {\n        border-bottom-right-radius: 0;\n    }\n    .card-group > .card:last-child {\n        border-top-left-radius: 0;\n        border-bottom-left-radius: 0;\n    }\n    .card-group > .card:last-child .card-img-top,\n    .card-group > .card:last-child .card-header {\n        border-top-left-radius: 0;\n    }\n    .card-group > .card:last-child .card-img-bottom,\n    .card-group > .card:last-child .card-footer {\n        border-bottom-left-radius: 0;\n    }\n    .card-group > .card:only-child {\n        border-radius: 3px;\n    }\n    .card-group > .card:only-child .card-img-top,\n    .card-group > .card:only-child .card-header {\n        border-top-left-radius: 3px;\n        border-top-right-radius: 3px;\n    }\n    .card-group > .card:only-child .card-img-bottom,\n    .card-group > .card:only-child .card-footer {\n        border-bottom-right-radius: 3px;\n        border-bottom-left-radius: 3px;\n    }\n    .card-group > .card:not(:first-child):not(:last-child):not(:only-child) {\n        border-radius: 0;\n    }\n    .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-top,\n    .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,\n    .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-header,\n    .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-footer {\n        border-radius: 0;\n    }\n}\n\n.card-columns .card {\n    margin-bottom: 1.5rem;\n}\n\n@media (min-width: 576px) {\n    .card-columns {\n        -webkit-column-count: 3;\n        -moz-column-count: 3;\n        column-count: 3;\n        -webkit-column-gap: 1.25rem;\n        -moz-column-gap: 1.25rem;\n        column-gap: 1.25rem;\n    }\n    .card-columns .card {\n        display: inline-block;\n        width: 100%;\n    }\n}\n\n.breadcrumb {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    padding: 0.75rem 1rem;\n    margin-bottom: 1rem;\n    list-style: none;\n    background-color: #e9ecef;\n    border-radius: 3px;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n    display: inline-block;\n    padding-right: 0.5rem;\n    padding-left: 0.5rem;\n    color: #868e96;\n    content: '/';\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n    text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n    text-decoration: none;\n}\n\n.breadcrumb-item.active {\n    color: #868e96;\n}\n\n.pagination {\n    display: -ms-flexbox;\n    display: flex;\n    padding-left: 0;\n    list-style: none;\n    border-radius: 3px;\n}\n\n.page-link {\n    position: relative;\n    display: block;\n    padding: 0.5rem 0.75rem;\n    margin-left: -1px;\n    line-height: 1.25;\n    color: #495057;\n    background-color: #fff;\n    border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n    color: #295a9f;\n    text-decoration: none;\n    background-color: #e9ecef;\n    border-color: #dee2e6;\n}\n\n.page-link:focus {\n    z-index: 2;\n    outline: 0;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.page-link:not(:disabled):not(.disabled) {\n    cursor: pointer;\n}\n\n.page-item:first-child .page-link {\n    margin-left: 0;\n    border-top-left-radius: 3px;\n    border-bottom-left-radius: 3px;\n}\n\n.page-item:last-child .page-link {\n    border-top-right-radius: 3px;\n    border-bottom-right-radius: 3px;\n}\n\n.page-item.active .page-link {\n    z-index: 1;\n    color: #fff;\n    background-color: #467fcf;\n    border-color: #467fcf;\n}\n\n.page-item.disabled .page-link {\n    color: #ced4da;\n    pointer-events: none;\n    cursor: auto;\n    background-color: #fff;\n    border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n    padding: 0.75rem 1.5rem;\n    font-size: 1.125rem;\n    line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n    border-top-left-radius: 3px;\n    border-bottom-left-radius: 3px;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n    border-top-right-radius: 3px;\n    border-bottom-right-radius: 3px;\n}\n\n.pagination-sm .page-link {\n    padding: 0.25rem 0.5rem;\n    font-size: 0.875rem;\n    line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n    border-top-left-radius: 3px;\n    border-bottom-left-radius: 3px;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n    border-top-right-radius: 3px;\n    border-bottom-right-radius: 3px;\n}\n\n.badge {\n    display: inline-block;\n    padding: 0.25em 0.4em;\n    font-size: 75%;\n    font-weight: 600;\n    line-height: 1;\n    text-align: center;\n    white-space: nowrap;\n    vertical-align: baseline;\n    border-radius: 3px;\n}\n\n.badge:empty {\n    display: none;\n}\n\n.btn .badge {\n    position: relative;\n    top: -1px;\n}\n\n.badge-pill {\n    padding-right: 0.6em;\n    padding-left: 0.6em;\n    border-radius: 10rem;\n}\n\n.badge-primary {\n    color: #fff;\n    background-color: #467fcf;\n}\n\n.badge-primary[href]:hover,\n.badge-primary[href]:focus {\n    color: #fff;\n    text-decoration: none;\n    background-color: #2f66b3;\n}\n\n.badge-secondary {\n    color: #fff;\n    background-color: #868e96;\n}\n\n.badge-secondary[href]:hover,\n.badge-secondary[href]:focus {\n    color: #fff;\n    text-decoration: none;\n    background-color: #6c757d;\n}\n\n.badge-success {\n    color: #fff;\n    background-color: #5eba00;\n}\n\n.badge-success[href]:hover,\n.badge-success[href]:focus {\n    color: #fff;\n    text-decoration: none;\n    background-color: #448700;\n}\n\n.badge-info {\n    color: #fff;\n    background-color: #45aaf2;\n}\n\n.badge-info[href]:hover,\n.badge-info[href]:focus {\n    color: #fff;\n    text-decoration: none;\n    background-color: #1594ef;\n}\n\n.badge-warning {\n    color: #fff;\n    background-color: #f1c40f;\n}\n\n.badge-warning[href]:hover,\n.badge-warning[href]:focus {\n    color: #fff;\n    text-decoration: none;\n    background-color: #c29d0b;\n}\n\n.badge-danger {\n    color: #fff;\n    background-color: #cd201f;\n}\n\n.badge-danger[href]:hover,\n.badge-danger[href]:focus {\n    color: #fff;\n    text-decoration: none;\n    background-color: #a11918;\n}\n\n.badge-light {\n    color: #495057;\n    background-color: #f8f9fa;\n}\n\n.badge-light[href]:hover,\n.badge-light[href]:focus {\n    color: #495057;\n    text-decoration: none;\n    background-color: #dae0e5;\n}\n\n.badge-dark {\n    color: #fff;\n    background-color: #343a40;\n}\n\n.badge-dark[href]:hover,\n.badge-dark[href]:focus {\n    color: #fff;\n    text-decoration: none;\n    background-color: #1d2124;\n}\n\n.jumbotron {\n    padding: 2rem 1rem;\n    margin-bottom: 2rem;\n    background-color: #e9ecef;\n    border-radius: 3px;\n}\n\n@media (min-width: 576px) {\n    .jumbotron {\n        padding: 4rem 2rem;\n    }\n}\n\n.jumbotron-fluid {\n    padding-right: 0;\n    padding-left: 0;\n    border-radius: 0;\n}\n\n.alert {\n    position: relative;\n    padding: 0.75rem 1.25rem;\n    margin-bottom: 1rem;\n    border: 1px solid transparent;\n    border-radius: 3px;\n}\n\n.alert-heading {\n    color: inherit;\n}\n\n.alert-link {\n    font-weight: 600;\n}\n\n.alert-dismissible {\n    padding-right: 3.90625rem;\n}\n\n.alert-dismissible .close {\n    position: absolute;\n    top: 0;\n    right: 0;\n    padding: 0.75rem 1.25rem;\n    color: inherit;\n}\n\n.alert-primary {\n    color: var(--alert-message-color);\n    background-color: var(--alert-message-bg);\n    border-color: var(--alert-message-border);\n}\n\n.alert-primary hr {\n    border-top-color: #b7cded;\n}\n\n.alert-primary .alert-link {\n    color: #172b46;\n}\n\n.alert-secondary {\n    color: #464a4e;\n    background-color: #e7e8ea;\n    border-color: #dddfe2;\n}\n\n.alert-secondary hr {\n    border-top-color: #cfd2d6;\n}\n\n.alert-secondary .alert-link {\n    color: #2e3133;\n}\n\n.alert-success {\n    color: #316100;\n    background-color: #dff1cc;\n    border-color: #d2ecb8;\n}\n\n.alert-success hr {\n    border-top-color: #c5e7a4;\n}\n\n.alert-success .alert-link {\n    color: #172e00;\n}\n\n.alert-info {\n    color: var(--alert-message-color);\n    background-color: var(--alert-message-bg);\n    border-color: var(--alert-message-border);\n}\n\n.alert-info hr {\n    border-top-color: #b3dcf9;\n}\n\n.alert-info .alert-link {\n    color: #193c56;\n}\n\n.alert-warning {\n    color: #7d6608;\n    background-color: #fcf3cf;\n    border-color: #fbeebc;\n}\n\n.alert-warning hr {\n    border-top-color: #fae8a4;\n}\n\n.alert-warning .alert-link {\n    color: #4d3f05;\n}\n\n.alert-danger {\n    color: #6b1110;\n    background-color: #f5d2d2;\n    border-color: #f1c1c0;\n}\n\n.alert-danger hr {\n    border-top-color: #ecacab;\n}\n\n.alert-danger .alert-link {\n    color: #3f0a09;\n}\n\n.alert-light {\n    color: #818182;\n    background-color: #fefefe;\n    border-color: #fdfdfe;\n}\n\n.alert-light hr {\n    border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n    color: #686868;\n}\n\n.alert-dark {\n    color: #1b1e21;\n    background-color: #d6d8d9;\n    border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n    border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n    color: #040505;\n}\n\n@-webkit-keyframes progress-bar-stripes {\n    from {\n        background-position: 1rem 0;\n    }\n    to {\n        background-position: 0 0;\n    }\n}\n\n@keyframes progress-bar-stripes {\n    from {\n        background-position: 1rem 0;\n    }\n    to {\n        background-position: 0 0;\n    }\n}\n\n.progress {\n    display: -ms-flexbox;\n    display: flex;\n    height: 1rem;\n    overflow: hidden;\n    font-size: 0.703125rem;\n    background-color: #e9ecef;\n    border-radius: 3px;\n}\n\n.progress-bar {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    -ms-flex-pack: center;\n    justify-content: center;\n    color: #fff;\n    text-align: center;\n    background-color: #467fcf;\n    transition: width 0.6s ease;\n}\n\n.progress-bar-striped {\n    background-image: linear-gradient(\n        45deg,\n        rgba(255, 255, 255, 0.15) 25%,\n        transparent 25%,\n        transparent 50%,\n        rgba(255, 255, 255, 0.15) 50%,\n        rgba(255, 255, 255, 0.15) 75%,\n        transparent 75%,\n        transparent\n    );\n    background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n    -webkit-animation: progress-bar-stripes 1s linear infinite;\n    animation: progress-bar-stripes 1s linear infinite;\n}\n\n.media {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: start;\n    align-items: flex-start;\n}\n\n.media-body {\n    -ms-flex: 1;\n    flex: 1;\n}\n\n.list-group {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    padding-left: 0;\n    margin-bottom: 0;\n}\n\n.list-group-item-action {\n    width: 100%;\n    color: #495057;\n    text-align: inherit;\n}\n\n.list-group-item-action:hover,\n.list-group-item-action:focus {\n    color: #495057;\n    text-decoration: none;\n    background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n    color: #495057;\n    background-color: #e9ecef;\n}\n\n.list-group-item {\n    position: relative;\n    display: block;\n    padding: 0.75rem 1.25rem;\n    margin-bottom: -1px;\n    background-color: #fff;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.list-group-item:first-child {\n    border-top-left-radius: 3px;\n    border-top-right-radius: 3px;\n}\n\n.list-group-item:last-child {\n    margin-bottom: 0;\n    border-bottom-right-radius: 3px;\n    border-bottom-left-radius: 3px;\n}\n\n.list-group-item:hover,\n.list-group-item:focus {\n    z-index: 1;\n    text-decoration: none;\n}\n\n.list-group-item.disabled,\n.list-group-item:disabled {\n    color: #868e96;\n    background-color: #fff;\n}\n\n.list-group-item.active {\n    z-index: 2;\n    color: #467fcf;\n    background-color: #f8fafd;\n    border-color: rgba(0, 40, 100, 0.12);\n}\n\n.list-group-flush .list-group-item {\n    border-right: 0;\n    border-left: 0;\n    border-radius: 0;\n}\n\n.list-group-flush:first-child .list-group-item:first-child {\n    border-top: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n    border-bottom: 0;\n}\n\n.list-group-item-primary {\n    color: #24426c;\n    background-color: #cbdbf2;\n}\n\n.list-group-item-primary.list-group-item-action:hover,\n.list-group-item-primary.list-group-item-action:focus {\n    color: #24426c;\n    background-color: #b7cded;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n    color: #fff;\n    background-color: #24426c;\n    border-color: #24426c;\n}\n\n.list-group-item-secondary {\n    color: #464a4e;\n    background-color: #dddfe2;\n}\n\n.list-group-item-secondary.list-group-item-action:hover,\n.list-group-item-secondary.list-group-item-action:focus {\n    color: #464a4e;\n    background-color: #cfd2d6;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n    color: #fff;\n    background-color: #464a4e;\n    border-color: #464a4e;\n}\n\n.list-group-item-success {\n    color: #316100;\n    background-color: #d2ecb8;\n}\n\n.list-group-item-success.list-group-item-action:hover,\n.list-group-item-success.list-group-item-action:focus {\n    color: #316100;\n    background-color: #c5e7a4;\n}\n\n.list-group-item-success.list-group-item-action.active {\n    color: #fff;\n    background-color: #316100;\n    border-color: #316100;\n}\n\n.list-group-item-info {\n    color: #24587e;\n    background-color: #cbe7fb;\n}\n\n.list-group-item-info.list-group-item-action:hover,\n.list-group-item-info.list-group-item-action:focus {\n    color: #24587e;\n    background-color: #b3dcf9;\n}\n\n.list-group-item-info.list-group-item-action.active {\n    color: #fff;\n    background-color: #24587e;\n    border-color: #24587e;\n}\n\n.list-group-item-warning {\n    color: #7d6608;\n    background-color: #fbeebc;\n}\n\n.list-group-item-warning.list-group-item-action:hover,\n.list-group-item-warning.list-group-item-action:focus {\n    color: #7d6608;\n    background-color: #fae8a4;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n    color: #fff;\n    background-color: #7d6608;\n    border-color: #7d6608;\n}\n\n.list-group-item-danger {\n    color: #6b1110;\n    background-color: #f1c1c0;\n}\n\n.list-group-item-danger.list-group-item-action:hover,\n.list-group-item-danger.list-group-item-action:focus {\n    color: #6b1110;\n    background-color: #ecacab;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n    color: #fff;\n    background-color: #6b1110;\n    border-color: #6b1110;\n}\n\n.list-group-item-light {\n    color: #818182;\n    background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover,\n.list-group-item-light.list-group-item-action:focus {\n    color: #818182;\n    background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n    color: #fff;\n    background-color: #818182;\n    border-color: #818182;\n}\n\n.list-group-item-dark {\n    color: #1b1e21;\n    background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover,\n.list-group-item-dark.list-group-item-action:focus {\n    color: #1b1e21;\n    background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n    color: #fff;\n    background-color: #1b1e21;\n    border-color: #1b1e21;\n}\n\n.close {\n    float: right;\n    font-size: 1.40625rem;\n    font-weight: 700;\n    line-height: 1;\n    color: #000;\n    text-shadow: 0 1px 0 #fff;\n    opacity: 0.5;\n}\n\n.close:hover,\n.close:focus {\n    color: #000;\n    text-decoration: none;\n    opacity: 0.75;\n}\n\n.close:not(:disabled):not(.disabled) {\n    cursor: pointer;\n}\n\nbutton.close {\n    padding: 0;\n    background-color: transparent;\n    border: 0;\n    -webkit-appearance: none;\n}\n\n.modal-open {\n    overflow: hidden;\n}\n\n.modal {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 1050;\n    display: none;\n    overflow: hidden;\n    outline: 0;\n}\n\n.modal-open .modal {\n    overflow-x: hidden;\n    overflow-y: auto;\n}\n\n.modal-dialog {\n    position: relative;\n    width: auto;\n    margin: 0.5rem;\n    pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n    transition: -webkit-transform 0.3s ease-out;\n    transition: transform 0.3s ease-out;\n    transition:\n        transform 0.3s ease-out,\n        -webkit-transform 0.3s ease-out;\n    -webkit-transform: translate(0, -25%);\n    transform: translate(0, -25%);\n}\n\n.modal.show .modal-dialog {\n    -webkit-transform: translate(0, 0);\n    transform: translate(0, 0);\n}\n\n.modal-dialog-centered {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    min-height: calc(100% - (0.5rem * 2));\n}\n\n.modal-content {\n    position: relative;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    width: 100%;\n    pointer-events: auto;\n    background-color: var(--card-bgcolor);\n    background-clip: padding-box;\n    border: 1px solid var(--card-border-color);\n    border-radius: 3px;\n    outline: 0;\n}\n\n.modal-backdrop {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 1040;\n    background-color: #000;\n}\n\n.modal-backdrop.fade {\n    opacity: 0;\n}\n\n.modal-backdrop.show {\n    opacity: 0.5;\n}\n\n.modal-header {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: start;\n    align-items: flex-start;\n    -ms-flex-pack: justify;\n    justify-content: space-between;\n    padding: 1rem;\n    border-bottom: 1px solid var(--card-border-color);\n    border-top-left-radius: 3px;\n    border-top-right-radius: 3px;\n}\n\n.modal-header .close {\n    padding: 1rem;\n    margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n    margin-bottom: 0;\n    line-height: 1.5;\n}\n\n.modal-body {\n    position: relative;\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n    padding: 1rem;\n}\n\n.modal-footer {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: end;\n    justify-content: flex-end;\n    padding: 1rem;\n    border-top: 1px solid var(--card-border-color);\n}\n\n.modal-footer > :not(:first-child) {\n    margin-left: 0.25rem;\n}\n\n.modal-footer > :not(:last-child) {\n    margin-right: 0.25rem;\n}\n\n.modal-scrollbar-measure {\n    position: absolute;\n    top: -9999px;\n    width: 50px;\n    height: 50px;\n    overflow: scroll;\n}\n\n@media (min-width: 576px) {\n    .modal-dialog {\n        max-width: 500px;\n        margin: 1.75rem auto;\n    }\n    .modal-dialog-centered {\n        min-height: calc(100% - (1.75rem * 2));\n    }\n    .modal-sm {\n        max-width: 300px;\n    }\n}\n\n@media (min-width: 992px) {\n    .modal-lg {\n        max-width: 800px;\n    }\n}\n\n.tooltip {\n    position: absolute;\n    z-index: 1070;\n    display: block;\n    margin: 0;\n    font-family:\n        'Source Sans Pro',\n        -apple-system,\n        BlinkMacSystemFont,\n        'Segoe UI',\n        'Helvetica Neue',\n        Arial,\n        sans-serif;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 1.5;\n    text-align: left;\n    text-align: start;\n    text-decoration: none;\n    text-shadow: none;\n    text-transform: none;\n    letter-spacing: normal;\n    word-break: normal;\n    word-spacing: normal;\n    white-space: normal;\n    line-break: auto;\n    font-size: 0.875rem;\n    word-wrap: break-word;\n    opacity: 0;\n}\n\n.tooltip.show {\n    opacity: 0.9;\n}\n\n.tooltip .arrow {\n    position: absolute;\n    display: block;\n    width: 0.8rem;\n    height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n    position: absolute;\n    content: '';\n    border-color: transparent;\n    border-style: solid;\n}\n\n.bs-tooltip-top,\n.bs-tooltip-auto[x-placement^='top'] {\n    padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow,\n.bs-tooltip-auto[x-placement^='top'] .arrow {\n    bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before,\n.bs-tooltip-auto[x-placement^='top'] .arrow::before {\n    top: 0;\n    border-width: 0.4rem 0.4rem 0;\n    border-top-color: #000;\n}\n\n.bs-tooltip-right,\n.bs-tooltip-auto[x-placement^='right'] {\n    padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow,\n.bs-tooltip-auto[x-placement^='right'] .arrow {\n    left: 0;\n    width: 0.4rem;\n    height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before,\n.bs-tooltip-auto[x-placement^='right'] .arrow::before {\n    right: 0;\n    border-width: 0.4rem 0.4rem 0.4rem 0;\n    border-right-color: #000;\n}\n\n.bs-tooltip-bottom,\n.bs-tooltip-auto[x-placement^='bottom'] {\n    padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow,\n.bs-tooltip-auto[x-placement^='bottom'] .arrow {\n    top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before,\n.bs-tooltip-auto[x-placement^='bottom'] .arrow::before {\n    bottom: 0;\n    border-width: 0 0.4rem 0.4rem;\n    border-bottom-color: #000;\n}\n\n.bs-tooltip-left,\n.bs-tooltip-auto[x-placement^='left'] {\n    padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow,\n.bs-tooltip-auto[x-placement^='left'] .arrow {\n    right: 0;\n    width: 0.4rem;\n    height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before,\n.bs-tooltip-auto[x-placement^='left'] .arrow::before {\n    left: 0;\n    border-width: 0.4rem 0 0.4rem 0.4rem;\n    border-left-color: #000;\n}\n\n.tooltip-inner {\n    max-width: 200px;\n    padding: 0.25rem 0.5rem;\n    color: #fff;\n    text-align: center;\n    background-color: #000;\n    border-radius: 3px;\n}\n\n.popover {\n    position: absolute;\n    top: 0;\n    left: 0;\n    z-index: 1060;\n    display: block;\n    max-width: 276px;\n    font-family:\n        'Source Sans Pro',\n        -apple-system,\n        BlinkMacSystemFont,\n        'Segoe UI',\n        'Helvetica Neue',\n        Arial,\n        sans-serif;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 1.5;\n    text-align: left;\n    text-align: start;\n    text-decoration: none;\n    text-shadow: none;\n    text-transform: none;\n    letter-spacing: normal;\n    word-break: normal;\n    word-spacing: normal;\n    white-space: normal;\n    line-break: auto;\n    font-size: 0.875rem;\n    word-wrap: break-word;\n    background-color: #fff;\n    background-clip: padding-box;\n    border: 1px solid #dee3eb;\n    border-radius: 3px;\n}\n\n.popover .arrow {\n    position: absolute;\n    display: block;\n    width: 0.5rem;\n    height: 0.5rem;\n    margin: 0 3px;\n}\n\n.popover .arrow::before,\n.popover .arrow::after {\n    position: absolute;\n    display: block;\n    content: '';\n    border-color: transparent;\n    border-style: solid;\n}\n\n.bs-popover-top,\n.bs-popover-auto[x-placement^='top'] {\n    margin-bottom: 0.5rem;\n}\n\n.bs-popover-top .arrow,\n.bs-popover-auto[x-placement^='top'] .arrow {\n    bottom: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-top .arrow::before,\n.bs-popover-auto[x-placement^='top'] .arrow::before,\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^='top'] .arrow::after {\n    border-width: 0.5rem 0.25rem 0;\n}\n\n.bs-popover-top .arrow::before,\n.bs-popover-auto[x-placement^='top'] .arrow::before {\n    bottom: 0;\n    border-top-color: #dee3eb;\n}\n\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^='top'] .arrow::after {\n    bottom: 1px;\n    border-top-color: #fff;\n}\n\n.bs-popover-right,\n.bs-popover-auto[x-placement^='right'] {\n    margin-left: 0.5rem;\n}\n\n.bs-popover-right .arrow,\n.bs-popover-auto[x-placement^='right'] .arrow {\n    left: calc((0.5rem + 1px) * -1);\n    width: 0.5rem;\n    height: 0.5rem;\n    margin: 3px 0;\n}\n\n.bs-popover-right .arrow::before,\n.bs-popover-auto[x-placement^='right'] .arrow::before,\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^='right'] .arrow::after {\n    border-width: 0.25rem 0.5rem 0.25rem 0;\n}\n\n.bs-popover-right .arrow::before,\n.bs-popover-auto[x-placement^='right'] .arrow::before {\n    left: 0;\n    border-right-color: #dee3eb;\n}\n\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^='right'] .arrow::after {\n    left: 1px;\n    border-right-color: #fff;\n}\n\n.bs-popover-bottom,\n.bs-popover-auto[x-placement^='bottom'] {\n    margin-top: 0.5rem;\n}\n\n.bs-popover-bottom .arrow,\n.bs-popover-auto[x-placement^='bottom'] .arrow {\n    top: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-bottom .arrow::before,\n.bs-popover-auto[x-placement^='bottom'] .arrow::before,\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^='bottom'] .arrow::after {\n    border-width: 0 0.25rem 0.5rem 0.25rem;\n}\n\n.bs-popover-bottom .arrow::before,\n.bs-popover-auto[x-placement^='bottom'] .arrow::before {\n    top: 0;\n    border-bottom-color: #dee3eb;\n}\n\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^='bottom'] .arrow::after {\n    top: 1px;\n    border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before,\n.bs-popover-auto[x-placement^='bottom'] .popover-header::before {\n    position: absolute;\n    top: 0;\n    left: 50%;\n    display: block;\n    width: 0.5rem;\n    margin-left: -0.25rem;\n    content: '';\n    border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left,\n.bs-popover-auto[x-placement^='left'] {\n    margin-right: 0.5rem;\n}\n\n.bs-popover-left .arrow,\n.bs-popover-auto[x-placement^='left'] .arrow {\n    right: calc((0.5rem + 1px) * -1);\n    width: 0.5rem;\n    height: 0.5rem;\n    margin: 3px 0;\n}\n\n.bs-popover-left .arrow::before,\n.bs-popover-auto[x-placement^='left'] .arrow::before,\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^='left'] .arrow::after {\n    border-width: 0.25rem 0 0.25rem 0.5rem;\n}\n\n.bs-popover-left .arrow::before,\n.bs-popover-auto[x-placement^='left'] .arrow::before {\n    right: 0;\n    border-left-color: #dee3eb;\n}\n\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^='left'] .arrow::after {\n    right: 1px;\n    border-left-color: #fff;\n}\n\n.popover-header {\n    padding: 0.5rem 0.75rem;\n    margin-bottom: 0;\n    font-size: 0.9375rem;\n    color: inherit;\n    background-color: #f7f7f7;\n    border-bottom: 1px solid #ebebeb;\n    border-top-left-radius: calc(3px - 1px);\n    border-top-right-radius: calc(3px - 1px);\n}\n\n.popover-header:empty {\n    display: none;\n}\n\n.popover-body {\n    padding: 0.75rem 1rem;\n    color: #6e7687;\n}\n\n.carousel {\n    position: relative;\n}\n\n.carousel-inner {\n    position: relative;\n    width: 100%;\n    overflow: hidden;\n}\n\n.carousel-item {\n    position: relative;\n    display: none;\n    -ms-flex-align: center;\n    align-items: center;\n    width: 100%;\n    transition: -webkit-transform 0.6s ease;\n    transition: transform 0.6s ease;\n    transition:\n        transform 0.6s ease,\n        -webkit-transform 0.6s ease;\n    -webkit-backface-visibility: hidden;\n    backface-visibility: hidden;\n    -webkit-perspective: 1000px;\n    perspective: 1000px;\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n    display: block;\n}\n\n.carousel-item-next,\n.carousel-item-prev {\n    position: absolute;\n    top: 0;\n}\n\n.carousel-item-next.carousel-item-left,\n.carousel-item-prev.carousel-item-right {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n}\n\n@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) {\n    .carousel-item-next.carousel-item-left,\n    .carousel-item-prev.carousel-item-right {\n        -webkit-transform: translate3d(0, 0, 0);\n        transform: translate3d(0, 0, 0);\n    }\n}\n\n.carousel-item-next,\n.active.carousel-item-right {\n    -webkit-transform: translateX(100%);\n    transform: translateX(100%);\n}\n\n@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) {\n    .carousel-item-next,\n    .active.carousel-item-right {\n        -webkit-transform: translate3d(100%, 0, 0);\n        transform: translate3d(100%, 0, 0);\n    }\n}\n\n.carousel-item-prev,\n.active.carousel-item-left {\n    -webkit-transform: translateX(-100%);\n    transform: translateX(-100%);\n}\n\n@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) {\n    .carousel-item-prev,\n    .active.carousel-item-left {\n        -webkit-transform: translate3d(-100%, 0, 0);\n        transform: translate3d(-100%, 0, 0);\n    }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: center;\n    justify-content: center;\n    width: 15%;\n    color: #fff;\n    text-align: center;\n    opacity: 0.5;\n}\n\n.carousel-control-prev:hover,\n.carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n    color: #fff;\n    text-decoration: none;\n    outline: 0;\n    opacity: 0.9;\n}\n\n.carousel-control-prev {\n    left: 0;\n}\n\n.carousel-control-next {\n    right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n    display: inline-block;\n    width: 20px;\n    height: 20px;\n    background: transparent no-repeat center center;\n    background-size: 100% 100%;\n}\n\n.carousel-control-prev-icon {\n    background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E\");\n}\n\n.carousel-control-next-icon {\n    background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E\");\n}\n\n.carousel-indicators {\n    position: absolute;\n    right: 0;\n    bottom: 10px;\n    left: 0;\n    z-index: 15;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-pack: center;\n    justify-content: center;\n    padding-left: 0;\n    margin-right: 15%;\n    margin-left: 15%;\n    list-style: none;\n}\n\n.carousel-indicators li {\n    position: relative;\n    -ms-flex: 0 1 auto;\n    flex: 0 1 auto;\n    width: 30px;\n    height: 3px;\n    margin-right: 3px;\n    margin-left: 3px;\n    text-indent: -999px;\n    background-color: rgba(255, 255, 255, 0.5);\n}\n\n.carousel-indicators li::before {\n    position: absolute;\n    top: -10px;\n    left: 0;\n    display: inline-block;\n    width: 100%;\n    height: 10px;\n    content: '';\n}\n\n.carousel-indicators li::after {\n    position: absolute;\n    bottom: -10px;\n    left: 0;\n    display: inline-block;\n    width: 100%;\n    height: 10px;\n    content: '';\n}\n\n.carousel-indicators .active {\n    background-color: #fff;\n}\n\n.carousel-caption {\n    position: absolute;\n    right: 15%;\n    bottom: 20px;\n    left: 15%;\n    z-index: 10;\n    padding-top: 20px;\n    padding-bottom: 20px;\n    color: #fff;\n    text-align: center;\n}\n\n.align-baseline {\n    vertical-align: baseline !important;\n}\n\n.align-top {\n    vertical-align: top !important;\n}\n\n.align-middle {\n    vertical-align: middle !important;\n}\n\n.align-bottom {\n    vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n    vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n    vertical-align: text-top !important;\n}\n\n.bg-primary {\n    background-color: #467fcf !important;\n}\n\na.bg-primary:hover,\na.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n    background-color: #2f66b3 !important;\n}\n\n.bg-secondary {\n    background-color: #868e96 !important;\n}\n\na.bg-secondary:hover,\na.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n    background-color: #6c757d !important;\n}\n\n.bg-success {\n    background-color: #5eba00 !important;\n}\n\na.bg-success:hover,\na.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n    background-color: #448700 !important;\n}\n\n.bg-info {\n    background-color: #45aaf2 !important;\n}\n\na.bg-info:hover,\na.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n    background-color: #1594ef !important;\n}\n\n.bg-warning {\n    background-color: #f1c40f !important;\n}\n\na.bg-warning:hover,\na.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n    background-color: #c29d0b !important;\n}\n\n.bg-danger {\n    background-color: #cd201f !important;\n}\n\na.bg-danger:hover,\na.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n    background-color: #a11918 !important;\n}\n\n.bg-light {\n    background-color: #f8f9fa !important;\n}\n\na.bg-light:hover,\na.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n    background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n    background-color: #343a40 !important;\n}\n\na.bg-dark:hover,\na.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n    background-color: #1d2124 !important;\n}\n\n.bg-white {\n    background-color: #fff !important;\n}\n\n.bg-transparent {\n    background-color: transparent !important;\n}\n\n.border {\n    border: 1px solid rgba(0, 40, 100, 0.12) !important;\n}\n\n.border-top {\n    border-top: 1px solid rgba(0, 40, 100, 0.12) !important;\n}\n\n.border-right {\n    border-right: 1px solid rgba(0, 40, 100, 0.12) !important;\n}\n\n.border-bottom {\n    border-bottom: 1px solid rgba(0, 40, 100, 0.12) !important;\n}\n\n.border-left {\n    border-left: 1px solid rgba(0, 40, 100, 0.12) !important;\n}\n\n.border-0 {\n    border: 0 !important;\n}\n\n.border-top-0 {\n    border-top: 0 !important;\n}\n\n.border-right-0 {\n    border-right: 0 !important;\n}\n\n.border-bottom-0 {\n    border-bottom: 0 !important;\n}\n\n.border-left-0 {\n    border-left: 0 !important;\n}\n\n.border-primary {\n    border-color: #467fcf !important;\n}\n\n.border-secondary {\n    border-color: #868e96 !important;\n}\n\n.border-success {\n    border-color: #5eba00 !important;\n}\n\n.border-info {\n    border-color: #45aaf2 !important;\n}\n\n.border-warning {\n    border-color: #f1c40f !important;\n}\n\n.border-danger {\n    border-color: #cd201f !important;\n}\n\n.border-light {\n    border-color: #f8f9fa !important;\n}\n\n.border-dark {\n    border-color: #343a40 !important;\n}\n\n.border-white {\n    border-color: #fff !important;\n}\n\n.rounded {\n    border-radius: 3px !important;\n}\n\n.rounded-top {\n    border-top-left-radius: 3px !important;\n    border-top-right-radius: 3px !important;\n}\n\n.rounded-right {\n    border-top-right-radius: 3px !important;\n    border-bottom-right-radius: 3px !important;\n}\n\n.rounded-bottom {\n    border-bottom-right-radius: 3px !important;\n    border-bottom-left-radius: 3px !important;\n}\n\n.rounded-left {\n    border-top-left-radius: 3px !important;\n    border-bottom-left-radius: 3px !important;\n}\n\n.rounded-circle {\n    border-radius: 50% !important;\n}\n\n.rounded-0 {\n    border-radius: 0 !important;\n}\n\n.clearfix::after {\n    display: block;\n    clear: both;\n    content: '';\n}\n\n.d-none {\n    display: none !important;\n}\n\n.d-inline {\n    display: inline !important;\n}\n\n.d-inline-block {\n    display: inline-block !important;\n}\n\n.d-block {\n    display: block !important;\n}\n\n.d-table {\n    display: table !important;\n}\n\n.d-table-row {\n    display: table-row !important;\n}\n\n.d-table-cell {\n    display: table-cell !important;\n}\n\n.d-flex {\n    display: -ms-flexbox !important;\n    display: flex !important;\n}\n\n.d-inline-flex {\n    display: -ms-inline-flexbox !important;\n    display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n    .d-sm-none {\n        display: none !important;\n    }\n    .d-sm-inline {\n        display: inline !important;\n    }\n    .d-sm-inline-block {\n        display: inline-block !important;\n    }\n    .d-sm-block {\n        display: block !important;\n    }\n    .d-sm-table {\n        display: table !important;\n    }\n    .d-sm-table-row {\n        display: table-row !important;\n    }\n    .d-sm-table-cell {\n        display: table-cell !important;\n    }\n    .d-sm-flex {\n        display: -ms-flexbox !important;\n        display: flex !important;\n    }\n    .d-sm-inline-flex {\n        display: -ms-inline-flexbox !important;\n        display: inline-flex !important;\n    }\n}\n\n@media (min-width: 768px) {\n    .d-md-none {\n        display: none !important;\n    }\n    .d-md-inline {\n        display: inline !important;\n    }\n    .d-md-inline-block {\n        display: inline-block !important;\n    }\n    .d-md-block {\n        display: block !important;\n    }\n    .d-md-table {\n        display: table !important;\n    }\n    .d-md-table-row {\n        display: table-row !important;\n    }\n    .d-md-table-cell {\n        display: table-cell !important;\n    }\n    .d-md-flex {\n        display: -ms-flexbox !important;\n        display: flex !important;\n    }\n    .d-md-inline-flex {\n        display: -ms-inline-flexbox !important;\n        display: inline-flex !important;\n    }\n}\n\n@media (min-width: 992px) {\n    .d-lg-none {\n        display: none !important;\n    }\n    .d-lg-inline {\n        display: inline !important;\n    }\n    .d-lg-inline-block {\n        display: inline-block !important;\n    }\n    .d-lg-block {\n        display: block !important;\n    }\n    .d-lg-table {\n        display: table !important;\n    }\n    .d-lg-table-row {\n        display: table-row !important;\n    }\n    .d-lg-table-cell {\n        display: table-cell !important;\n    }\n    .d-lg-flex {\n        display: -ms-flexbox !important;\n        display: flex !important;\n    }\n    .d-lg-inline-flex {\n        display: -ms-inline-flexbox !important;\n        display: inline-flex !important;\n    }\n}\n\n@media (min-width: 1280px) {\n    .d-xl-none {\n        display: none !important;\n    }\n    .d-xl-inline {\n        display: inline !important;\n    }\n    .d-xl-inline-block {\n        display: inline-block !important;\n    }\n    .d-xl-block {\n        display: block !important;\n    }\n    .d-xl-table {\n        display: table !important;\n    }\n    .d-xl-table-row {\n        display: table-row !important;\n    }\n    .d-xl-table-cell {\n        display: table-cell !important;\n    }\n    .d-xl-flex {\n        display: -ms-flexbox !important;\n        display: flex !important;\n    }\n    .d-xl-inline-flex {\n        display: -ms-inline-flexbox !important;\n        display: inline-flex !important;\n    }\n}\n\n@media print {\n    .d-print-none {\n        display: none !important;\n    }\n    .d-print-inline {\n        display: inline !important;\n    }\n    .d-print-inline-block {\n        display: inline-block !important;\n    }\n    .d-print-block {\n        display: block !important;\n    }\n    .d-print-table {\n        display: table !important;\n    }\n    .d-print-table-row {\n        display: table-row !important;\n    }\n    .d-print-table-cell {\n        display: table-cell !important;\n    }\n    .d-print-flex {\n        display: -ms-flexbox !important;\n        display: flex !important;\n    }\n    .d-print-inline-flex {\n        display: -ms-inline-flexbox !important;\n        display: inline-flex !important;\n    }\n}\n\n.embed-responsive {\n    position: relative;\n    display: block;\n    width: 100%;\n    padding: 0;\n    overflow: hidden;\n}\n\n.embed-responsive::before {\n    display: block;\n    content: '';\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    border: 0;\n}\n\n.embed-responsive-21by9::before {\n    padding-top: 42.85714286%;\n}\n\n.embed-responsive-16by9::before {\n    padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n    padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n    padding-top: 100%;\n}\n\n.flex-row {\n    -ms-flex-direction: row !important;\n    flex-direction: row !important;\n}\n\n.flex-column {\n    -ms-flex-direction: column !important;\n    flex-direction: column !important;\n}\n\n.flex-row-reverse {\n    -ms-flex-direction: row-reverse !important;\n    flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n    -ms-flex-direction: column-reverse !important;\n    flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n    -ms-flex-wrap: wrap !important;\n    flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n    -ms-flex-wrap: nowrap !important;\n    flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n    -ms-flex-wrap: wrap-reverse !important;\n    flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n    -ms-flex-pack: start !important;\n    justify-content: flex-start !important;\n}\n\n.justify-content-end {\n    -ms-flex-pack: end !important;\n    justify-content: flex-end !important;\n}\n\n.justify-content-center {\n    -ms-flex-pack: center !important;\n    justify-content: center !important;\n}\n\n.justify-content-between {\n    -ms-flex-pack: justify !important;\n    justify-content: space-between !important;\n}\n\n.justify-content-around {\n    -ms-flex-pack: distribute !important;\n    justify-content: space-around !important;\n}\n\n.align-items-start {\n    -ms-flex-align: start !important;\n    align-items: flex-start !important;\n}\n\n.align-items-end {\n    -ms-flex-align: end !important;\n    align-items: flex-end !important;\n}\n\n.align-items-center {\n    -ms-flex-align: center !important;\n    align-items: center !important;\n}\n\n.align-items-baseline {\n    -ms-flex-align: baseline !important;\n    align-items: baseline !important;\n}\n\n.align-items-stretch {\n    -ms-flex-align: stretch !important;\n    align-items: stretch !important;\n}\n\n.align-content-start {\n    -ms-flex-line-pack: start !important;\n    align-content: flex-start !important;\n}\n\n.align-content-end {\n    -ms-flex-line-pack: end !important;\n    align-content: flex-end !important;\n}\n\n.align-content-center {\n    -ms-flex-line-pack: center !important;\n    align-content: center !important;\n}\n\n.align-content-between {\n    -ms-flex-line-pack: justify !important;\n    align-content: space-between !important;\n}\n\n.align-content-around {\n    -ms-flex-line-pack: distribute !important;\n    align-content: space-around !important;\n}\n\n.align-content-stretch {\n    -ms-flex-line-pack: stretch !important;\n    align-content: stretch !important;\n}\n\n.align-self-auto {\n    -ms-flex-item-align: auto !important;\n    align-self: auto !important;\n}\n\n.align-self-start {\n    -ms-flex-item-align: start !important;\n    align-self: flex-start !important;\n}\n\n.align-self-end {\n    -ms-flex-item-align: end !important;\n    align-self: flex-end !important;\n}\n\n.align-self-center {\n    -ms-flex-item-align: center !important;\n    align-self: center !important;\n}\n\n.align-self-baseline {\n    -ms-flex-item-align: baseline !important;\n    align-self: baseline !important;\n}\n\n.align-self-stretch {\n    -ms-flex-item-align: stretch !important;\n    align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n    .flex-sm-row {\n        -ms-flex-direction: row !important;\n        flex-direction: row !important;\n    }\n    .flex-sm-column {\n        -ms-flex-direction: column !important;\n        flex-direction: column !important;\n    }\n    .flex-sm-row-reverse {\n        -ms-flex-direction: row-reverse !important;\n        flex-direction: row-reverse !important;\n    }\n    .flex-sm-column-reverse {\n        -ms-flex-direction: column-reverse !important;\n        flex-direction: column-reverse !important;\n    }\n    .flex-sm-wrap {\n        -ms-flex-wrap: wrap !important;\n        flex-wrap: wrap !important;\n    }\n    .flex-sm-nowrap {\n        -ms-flex-wrap: nowrap !important;\n        flex-wrap: nowrap !important;\n    }\n    .flex-sm-wrap-reverse {\n        -ms-flex-wrap: wrap-reverse !important;\n        flex-wrap: wrap-reverse !important;\n    }\n    .justify-content-sm-start {\n        -ms-flex-pack: start !important;\n        justify-content: flex-start !important;\n    }\n    .justify-content-sm-end {\n        -ms-flex-pack: end !important;\n        justify-content: flex-end !important;\n    }\n    .justify-content-sm-center {\n        -ms-flex-pack: center !important;\n        justify-content: center !important;\n    }\n    .justify-content-sm-between {\n        -ms-flex-pack: justify !important;\n        justify-content: space-between !important;\n    }\n    .justify-content-sm-around {\n        -ms-flex-pack: distribute !important;\n        justify-content: space-around !important;\n    }\n    .align-items-sm-start {\n        -ms-flex-align: start !important;\n        align-items: flex-start !important;\n    }\n    .align-items-sm-end {\n        -ms-flex-align: end !important;\n        align-items: flex-end !important;\n    }\n    .align-items-sm-center {\n        -ms-flex-align: center !important;\n        align-items: center !important;\n    }\n    .align-items-sm-baseline {\n        -ms-flex-align: baseline !important;\n        align-items: baseline !important;\n    }\n    .align-items-sm-stretch {\n        -ms-flex-align: stretch !important;\n        align-items: stretch !important;\n    }\n    .align-content-sm-start {\n        -ms-flex-line-pack: start !important;\n        align-content: flex-start !important;\n    }\n    .align-content-sm-end {\n        -ms-flex-line-pack: end !important;\n        align-content: flex-end !important;\n    }\n    .align-content-sm-center {\n        -ms-flex-line-pack: center !important;\n        align-content: center !important;\n    }\n    .align-content-sm-between {\n        -ms-flex-line-pack: justify !important;\n        align-content: space-between !important;\n    }\n    .align-content-sm-around {\n        -ms-flex-line-pack: distribute !important;\n        align-content: space-around !important;\n    }\n    .align-content-sm-stretch {\n        -ms-flex-line-pack: stretch !important;\n        align-content: stretch !important;\n    }\n    .align-self-sm-auto {\n        -ms-flex-item-align: auto !important;\n        align-self: auto !important;\n    }\n    .align-self-sm-start {\n        -ms-flex-item-align: start !important;\n        align-self: flex-start !important;\n    }\n    .align-self-sm-end {\n        -ms-flex-item-align: end !important;\n        align-self: flex-end !important;\n    }\n    .align-self-sm-center {\n        -ms-flex-item-align: center !important;\n        align-self: center !important;\n    }\n    .align-self-sm-baseline {\n        -ms-flex-item-align: baseline !important;\n        align-self: baseline !important;\n    }\n    .align-self-sm-stretch {\n        -ms-flex-item-align: stretch !important;\n        align-self: stretch !important;\n    }\n}\n\n@media (min-width: 768px) {\n    .flex-md-row {\n        -ms-flex-direction: row !important;\n        flex-direction: row !important;\n    }\n    .flex-md-column {\n        -ms-flex-direction: column !important;\n        flex-direction: column !important;\n    }\n    .flex-md-row-reverse {\n        -ms-flex-direction: row-reverse !important;\n        flex-direction: row-reverse !important;\n    }\n    .flex-md-column-reverse {\n        -ms-flex-direction: column-reverse !important;\n        flex-direction: column-reverse !important;\n    }\n    .flex-md-wrap {\n        -ms-flex-wrap: wrap !important;\n        flex-wrap: wrap !important;\n    }\n    .flex-md-nowrap {\n        -ms-flex-wrap: nowrap !important;\n        flex-wrap: nowrap !important;\n    }\n    .flex-md-wrap-reverse {\n        -ms-flex-wrap: wrap-reverse !important;\n        flex-wrap: wrap-reverse !important;\n    }\n    .justify-content-md-start {\n        -ms-flex-pack: start !important;\n        justify-content: flex-start !important;\n    }\n    .justify-content-md-end {\n        -ms-flex-pack: end !important;\n        justify-content: flex-end !important;\n    }\n    .justify-content-md-center {\n        -ms-flex-pack: center !important;\n        justify-content: center !important;\n    }\n    .justify-content-md-between {\n        -ms-flex-pack: justify !important;\n        justify-content: space-between !important;\n    }\n    .justify-content-md-around {\n        -ms-flex-pack: distribute !important;\n        justify-content: space-around !important;\n    }\n    .align-items-md-start {\n        -ms-flex-align: start !important;\n        align-items: flex-start !important;\n    }\n    .align-items-md-end {\n        -ms-flex-align: end !important;\n        align-items: flex-end !important;\n    }\n    .align-items-md-center {\n        -ms-flex-align: center !important;\n        align-items: center !important;\n    }\n    .align-items-md-baseline {\n        -ms-flex-align: baseline !important;\n        align-items: baseline !important;\n    }\n    .align-items-md-stretch {\n        -ms-flex-align: stretch !important;\n        align-items: stretch !important;\n    }\n    .align-content-md-start {\n        -ms-flex-line-pack: start !important;\n        align-content: flex-start !important;\n    }\n    .align-content-md-end {\n        -ms-flex-line-pack: end !important;\n        align-content: flex-end !important;\n    }\n    .align-content-md-center {\n        -ms-flex-line-pack: center !important;\n        align-content: center !important;\n    }\n    .align-content-md-between {\n        -ms-flex-line-pack: justify !important;\n        align-content: space-between !important;\n    }\n    .align-content-md-around {\n        -ms-flex-line-pack: distribute !important;\n        align-content: space-around !important;\n    }\n    .align-content-md-stretch {\n        -ms-flex-line-pack: stretch !important;\n        align-content: stretch !important;\n    }\n    .align-self-md-auto {\n        -ms-flex-item-align: auto !important;\n        align-self: auto !important;\n    }\n    .align-self-md-start {\n        -ms-flex-item-align: start !important;\n        align-self: flex-start !important;\n    }\n    .align-self-md-end {\n        -ms-flex-item-align: end !important;\n        align-self: flex-end !important;\n    }\n    .align-self-md-center {\n        -ms-flex-item-align: center !important;\n        align-self: center !important;\n    }\n    .align-self-md-baseline {\n        -ms-flex-item-align: baseline !important;\n        align-self: baseline !important;\n    }\n    .align-self-md-stretch {\n        -ms-flex-item-align: stretch !important;\n        align-self: stretch !important;\n    }\n}\n\n@media (min-width: 992px) {\n    .flex-lg-row {\n        -ms-flex-direction: row !important;\n        flex-direction: row !important;\n    }\n    .flex-lg-column {\n        -ms-flex-direction: column !important;\n        flex-direction: column !important;\n    }\n    .flex-lg-row-reverse {\n        -ms-flex-direction: row-reverse !important;\n        flex-direction: row-reverse !important;\n    }\n    .flex-lg-column-reverse {\n        -ms-flex-direction: column-reverse !important;\n        flex-direction: column-reverse !important;\n    }\n    .flex-lg-wrap {\n        -ms-flex-wrap: wrap !important;\n        flex-wrap: wrap !important;\n    }\n    .flex-lg-nowrap {\n        -ms-flex-wrap: nowrap !important;\n        flex-wrap: nowrap !important;\n    }\n    .flex-lg-wrap-reverse {\n        -ms-flex-wrap: wrap-reverse !important;\n        flex-wrap: wrap-reverse !important;\n    }\n    .justify-content-lg-start {\n        -ms-flex-pack: start !important;\n        justify-content: flex-start !important;\n    }\n    .justify-content-lg-end {\n        -ms-flex-pack: end !important;\n        justify-content: flex-end !important;\n    }\n    .justify-content-lg-center {\n        -ms-flex-pack: center !important;\n        justify-content: center !important;\n    }\n    .justify-content-lg-between {\n        -ms-flex-pack: justify !important;\n        justify-content: space-between !important;\n    }\n    .justify-content-lg-around {\n        -ms-flex-pack: distribute !important;\n        justify-content: space-around !important;\n    }\n    .align-items-lg-start {\n        -ms-flex-align: start !important;\n        align-items: flex-start !important;\n    }\n    .align-items-lg-end {\n        -ms-flex-align: end !important;\n        align-items: flex-end !important;\n    }\n    .align-items-lg-center {\n        -ms-flex-align: center !important;\n        align-items: center !important;\n    }\n    .align-items-lg-baseline {\n        -ms-flex-align: baseline !important;\n        align-items: baseline !important;\n    }\n    .align-items-lg-stretch {\n        -ms-flex-align: stretch !important;\n        align-items: stretch !important;\n    }\n    .align-content-lg-start {\n        -ms-flex-line-pack: start !important;\n        align-content: flex-start !important;\n    }\n    .align-content-lg-end {\n        -ms-flex-line-pack: end !important;\n        align-content: flex-end !important;\n    }\n    .align-content-lg-center {\n        -ms-flex-line-pack: center !important;\n        align-content: center !important;\n    }\n    .align-content-lg-between {\n        -ms-flex-line-pack: justify !important;\n        align-content: space-between !important;\n    }\n    .align-content-lg-around {\n        -ms-flex-line-pack: distribute !important;\n        align-content: space-around !important;\n    }\n    .align-content-lg-stretch {\n        -ms-flex-line-pack: stretch !important;\n        align-content: stretch !important;\n    }\n    .align-self-lg-auto {\n        -ms-flex-item-align: auto !important;\n        align-self: auto !important;\n    }\n    .align-self-lg-start {\n        -ms-flex-item-align: start !important;\n        align-self: flex-start !important;\n    }\n    .align-self-lg-end {\n        -ms-flex-item-align: end !important;\n        align-self: flex-end !important;\n    }\n    .align-self-lg-center {\n        -ms-flex-item-align: center !important;\n        align-self: center !important;\n    }\n    .align-self-lg-baseline {\n        -ms-flex-item-align: baseline !important;\n        align-self: baseline !important;\n    }\n    .align-self-lg-stretch {\n        -ms-flex-item-align: stretch !important;\n        align-self: stretch !important;\n    }\n}\n\n@media (min-width: 1280px) {\n    .flex-xl-row {\n        -ms-flex-direction: row !important;\n        flex-direction: row !important;\n    }\n    .flex-xl-column {\n        -ms-flex-direction: column !important;\n        flex-direction: column !important;\n    }\n    .flex-xl-row-reverse {\n        -ms-flex-direction: row-reverse !important;\n        flex-direction: row-reverse !important;\n    }\n    .flex-xl-column-reverse {\n        -ms-flex-direction: column-reverse !important;\n        flex-direction: column-reverse !important;\n    }\n    .flex-xl-wrap {\n        -ms-flex-wrap: wrap !important;\n        flex-wrap: wrap !important;\n    }\n    .flex-xl-nowrap {\n        -ms-flex-wrap: nowrap !important;\n        flex-wrap: nowrap !important;\n    }\n    .flex-xl-wrap-reverse {\n        -ms-flex-wrap: wrap-reverse !important;\n        flex-wrap: wrap-reverse !important;\n    }\n    .justify-content-xl-start {\n        -ms-flex-pack: start !important;\n        justify-content: flex-start !important;\n    }\n    .justify-content-xl-end {\n        -ms-flex-pack: end !important;\n        justify-content: flex-end !important;\n    }\n    .justify-content-xl-center {\n        -ms-flex-pack: center !important;\n        justify-content: center !important;\n    }\n    .justify-content-xl-between {\n        -ms-flex-pack: justify !important;\n        justify-content: space-between !important;\n    }\n    .justify-content-xl-around {\n        -ms-flex-pack: distribute !important;\n        justify-content: space-around !important;\n    }\n    .align-items-xl-start {\n        -ms-flex-align: start !important;\n        align-items: flex-start !important;\n    }\n    .align-items-xl-end {\n        -ms-flex-align: end !important;\n        align-items: flex-end !important;\n    }\n    .align-items-xl-center {\n        -ms-flex-align: center !important;\n        align-items: center !important;\n    }\n    .align-items-xl-baseline {\n        -ms-flex-align: baseline !important;\n        align-items: baseline !important;\n    }\n    .align-items-xl-stretch {\n        -ms-flex-align: stretch !important;\n        align-items: stretch !important;\n    }\n    .align-content-xl-start {\n        -ms-flex-line-pack: start !important;\n        align-content: flex-start !important;\n    }\n    .align-content-xl-end {\n        -ms-flex-line-pack: end !important;\n        align-content: flex-end !important;\n    }\n    .align-content-xl-center {\n        -ms-flex-line-pack: center !important;\n        align-content: center !important;\n    }\n    .align-content-xl-between {\n        -ms-flex-line-pack: justify !important;\n        align-content: space-between !important;\n    }\n    .align-content-xl-around {\n        -ms-flex-line-pack: distribute !important;\n        align-content: space-around !important;\n    }\n    .align-content-xl-stretch {\n        -ms-flex-line-pack: stretch !important;\n        align-content: stretch !important;\n    }\n    .align-self-xl-auto {\n        -ms-flex-item-align: auto !important;\n        align-self: auto !important;\n    }\n    .align-self-xl-start {\n        -ms-flex-item-align: start !important;\n        align-self: flex-start !important;\n    }\n    .align-self-xl-end {\n        -ms-flex-item-align: end !important;\n        align-self: flex-end !important;\n    }\n    .align-self-xl-center {\n        -ms-flex-item-align: center !important;\n        align-self: center !important;\n    }\n    .align-self-xl-baseline {\n        -ms-flex-item-align: baseline !important;\n        align-self: baseline !important;\n    }\n    .align-self-xl-stretch {\n        -ms-flex-item-align: stretch !important;\n        align-self: stretch !important;\n    }\n}\n\n.float-left {\n    float: left !important;\n}\n\n.float-right {\n    float: right !important;\n}\n\n.float-none {\n    float: none !important;\n}\n\n@media (min-width: 576px) {\n    .float-sm-left {\n        float: left !important;\n    }\n    .float-sm-right {\n        float: right !important;\n    }\n    .float-sm-none {\n        float: none !important;\n    }\n}\n\n@media (min-width: 768px) {\n    .float-md-left {\n        float: left !important;\n    }\n    .float-md-right {\n        float: right !important;\n    }\n    .float-md-none {\n        float: none !important;\n    }\n}\n\n@media (min-width: 992px) {\n    .float-lg-left {\n        float: left !important;\n    }\n    .float-lg-right {\n        float: right !important;\n    }\n    .float-lg-none {\n        float: none !important;\n    }\n}\n\n@media (min-width: 1280px) {\n    .float-xl-left {\n        float: left !important;\n    }\n    .float-xl-right {\n        float: right !important;\n    }\n    .float-xl-none {\n        float: none !important;\n    }\n}\n\n.position-static {\n    position: static !important;\n}\n\n.position-relative {\n    position: relative !important;\n}\n\n.position-absolute {\n    position: absolute !important;\n}\n\n.position-fixed {\n    position: fixed !important;\n}\n\n.position-sticky {\n    position: -webkit-sticky !important;\n    position: sticky !important;\n}\n\n.fixed-top {\n    position: fixed;\n    top: 0;\n    right: 0;\n    left: 0;\n    z-index: 1030;\n}\n\n.fixed-bottom {\n    position: fixed;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 1030;\n}\n\n@supports ((position: -webkit-sticky) or (position: sticky)) {\n    .sticky-top {\n        position: -webkit-sticky;\n        position: sticky;\n        top: 0;\n        z-index: 1020;\n    }\n}\n\n.sr-only {\n    position: absolute;\n    width: 1px;\n    height: 1px;\n    padding: 0;\n    overflow: hidden;\n    clip: rect(0, 0, 0, 0);\n    white-space: nowrap;\n    -webkit-clip-path: inset(50%);\n    clip-path: inset(50%);\n    border: 0;\n}\n\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n    position: static;\n    width: auto;\n    height: auto;\n    overflow: visible;\n    clip: auto;\n    white-space: normal;\n    -webkit-clip-path: none;\n    clip-path: none;\n}\n\n.w-25 {\n    width: 25% !important;\n}\n\n.w-50 {\n    width: 50% !important;\n}\n\n.w-75 {\n    width: 75% !important;\n}\n\n.w-100 {\n    width: 100% !important;\n}\n\n.w-0 {\n    width: 0 !important;\n}\n\n.w-1 {\n    width: 0.25rem !important;\n}\n\n.w-2 {\n    width: 0.5rem !important;\n}\n\n.w-3 {\n    width: 0.75rem !important;\n}\n\n.w-4 {\n    width: 1rem !important;\n}\n\n.w-5 {\n    width: 1.5rem !important;\n}\n\n.w-6 {\n    width: 2rem !important;\n}\n\n.w-7 {\n    width: 3rem !important;\n}\n\n.w-8 {\n    width: 4rem !important;\n}\n\n.w-9 {\n    width: 6rem !important;\n}\n\n.w-auto {\n    width: auto !important;\n}\n\n.h-25 {\n    height: 25% !important;\n}\n\n.h-50 {\n    height: 50% !important;\n}\n\n.h-75 {\n    height: 75% !important;\n}\n\n.h-100 {\n    height: 100% !important;\n}\n\n.h-0 {\n    height: 0 !important;\n}\n\n.h-1 {\n    height: 0.25rem !important;\n}\n\n.h-2 {\n    height: 0.5rem !important;\n}\n\n.h-3 {\n    height: 0.75rem !important;\n}\n\n.h-4 {\n    height: 1rem !important;\n}\n\n.h-5 {\n    height: 1.5rem !important;\n}\n\n.h-6 {\n    height: 2rem !important;\n}\n\n.h-7 {\n    height: 3rem !important;\n}\n\n.h-8 {\n    height: 4rem !important;\n}\n\n.h-9 {\n    height: 6rem !important;\n}\n\n.h-auto {\n    height: auto !important;\n}\n\n.mw-100 {\n    max-width: 100% !important;\n}\n\n.mh-100 {\n    max-height: 100% !important;\n}\n\n.m-0 {\n    margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n    margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n    margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n    margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n    margin-left: 0 !important;\n}\n\n.m-1 {\n    margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n    margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n    margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n    margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n    margin-left: 0.25rem !important;\n}\n\n.m-2 {\n    margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n    margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n    margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n    margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n    margin-left: 0.5rem !important;\n}\n\n.m-3 {\n    margin: 0.75rem !important;\n}\n\n.mt-3,\n.my-3 {\n    margin-top: 0.75rem !important;\n}\n\n.mr-3,\n.mx-3 {\n    margin-right: 0.75rem !important;\n}\n\n.mb-3,\n.my-3 {\n    margin-bottom: 0.75rem !important;\n}\n\n.ml-3,\n.mx-3 {\n    margin-left: 0.75rem !important;\n}\n\n.m-4 {\n    margin: 1rem !important;\n}\n\n.mt-4,\n.my-4 {\n    margin-top: 1rem !important;\n}\n\n.mr-4,\n.mx-4 {\n    margin-right: 1rem !important;\n}\n\n.mb-4,\n.my-4 {\n    margin-bottom: 1rem !important;\n}\n\n.ml-4,\n.mx-4 {\n    margin-left: 1rem !important;\n}\n\n.m-5 {\n    margin: 1.5rem !important;\n}\n\n.mt-5,\n.my-5 {\n    margin-top: 1.5rem !important;\n}\n\n.mr-5,\n.mx-5 {\n    margin-right: 1.5rem !important;\n}\n\n.mb-5,\n.my-5 {\n    margin-bottom: 1.5rem !important;\n}\n\n.ml-5,\n.mx-5 {\n    margin-left: 1.5rem !important;\n}\n\n.m-6 {\n    margin: 2rem !important;\n}\n\n.mt-6,\n.my-6 {\n    margin-top: 2rem !important;\n}\n\n.mr-6,\n.mx-6 {\n    margin-right: 2rem !important;\n}\n\n.mb-6,\n.my-6 {\n    margin-bottom: 2rem !important;\n}\n\n.ml-6,\n.mx-6 {\n    margin-left: 2rem !important;\n}\n\n.m-7 {\n    margin: 3rem !important;\n}\n\n.mt-7,\n.my-7 {\n    margin-top: 3rem !important;\n}\n\n.mr-7,\n.mx-7 {\n    margin-right: 3rem !important;\n}\n\n.mb-7,\n.my-7 {\n    margin-bottom: 3rem !important;\n}\n\n.ml-7,\n.mx-7 {\n    margin-left: 3rem !important;\n}\n\n.m-8 {\n    margin: 4rem !important;\n}\n\n.mt-8,\n.my-8 {\n    margin-top: 4rem !important;\n}\n\n.mr-8,\n.mx-8 {\n    margin-right: 4rem !important;\n}\n\n.mb-8,\n.my-8 {\n    margin-bottom: 4rem !important;\n}\n\n.ml-8,\n.mx-8 {\n    margin-left: 4rem !important;\n}\n\n.m-9 {\n    margin: 6rem !important;\n}\n\n.mt-9,\n.my-9 {\n    margin-top: 6rem !important;\n}\n\n.mr-9,\n.mx-9 {\n    margin-right: 6rem !important;\n}\n\n.mb-9,\n.my-9 {\n    margin-bottom: 6rem !important;\n}\n\n.ml-9,\n.mx-9 {\n    margin-left: 6rem !important;\n}\n\n.p-0 {\n    padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n    padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n    padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n    padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n    padding-left: 0 !important;\n}\n\n.p-1 {\n    padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n    padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n    padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n    padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n    padding-left: 0.25rem !important;\n}\n\n.p-2 {\n    padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n    padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n    padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n    padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n    padding-left: 0.5rem !important;\n}\n\n.p-3 {\n    padding: 0.75rem !important;\n}\n\n.pt-3,\n.py-3 {\n    padding-top: 0.75rem !important;\n}\n\n.pr-3,\n.px-3 {\n    padding-right: 0.75rem !important;\n}\n\n.pb-3,\n.py-3 {\n    padding-bottom: 0.75rem !important;\n}\n\n.pl-3,\n.px-3 {\n    padding-left: 0.75rem !important;\n}\n\n.p-4 {\n    padding: 1rem !important;\n}\n\n.pt-4,\n.py-4 {\n    padding-top: 1rem !important;\n}\n\n.pr-4,\n.px-4 {\n    padding-right: 1rem !important;\n}\n\n.pb-4,\n.py-4 {\n    padding-bottom: 1rem !important;\n}\n\n.pl-4,\n.px-4 {\n    padding-left: 1rem !important;\n}\n\n.p-5 {\n    padding: 1.5rem !important;\n}\n\n.pt-5,\n.py-5 {\n    padding-top: 1.5rem !important;\n}\n\n.pr-5,\n.px-5 {\n    padding-right: 1.5rem !important;\n}\n\n.pb-5,\n.py-5 {\n    padding-bottom: 1.5rem !important;\n}\n\n.pl-5,\n.px-5 {\n    padding-left: 1.5rem !important;\n}\n\n.p-6 {\n    padding: 2rem !important;\n}\n\n.pt-6,\n.py-6 {\n    padding-top: 2rem !important;\n}\n\n.pr-6,\n.px-6 {\n    padding-right: 2rem !important;\n}\n\n.pb-6,\n.py-6 {\n    padding-bottom: 2rem !important;\n}\n\n.pl-6,\n.px-6 {\n    padding-left: 2rem !important;\n}\n\n.p-7 {\n    padding: 3rem !important;\n}\n\n.pt-7,\n.py-7 {\n    padding-top: 3rem !important;\n}\n\n.pr-7,\n.px-7 {\n    padding-right: 3rem !important;\n}\n\n.pb-7,\n.py-7 {\n    padding-bottom: 3rem !important;\n}\n\n.pl-7,\n.px-7 {\n    padding-left: 3rem !important;\n}\n\n.p-8 {\n    padding: 4rem !important;\n}\n\n.pt-8,\n.py-8 {\n    padding-top: 4rem !important;\n}\n\n.pr-8,\n.px-8 {\n    padding-right: 4rem !important;\n}\n\n.pb-8,\n.py-8 {\n    padding-bottom: 4rem !important;\n}\n\n.pl-8,\n.px-8 {\n    padding-left: 4rem !important;\n}\n\n.p-9 {\n    padding: 6rem !important;\n}\n\n.pt-9,\n.py-9 {\n    padding-top: 6rem !important;\n}\n\n.pr-9,\n.px-9 {\n    padding-right: 6rem !important;\n}\n\n.pb-9,\n.py-9 {\n    padding-bottom: 6rem !important;\n}\n\n.pl-9,\n.px-9 {\n    padding-left: 6rem !important;\n}\n\n.m-auto {\n    margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n    margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n    margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n    margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n    margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n    .m-sm-0 {\n        margin: 0 !important;\n    }\n    .mt-sm-0,\n    .my-sm-0 {\n        margin-top: 0 !important;\n    }\n    .mr-sm-0,\n    .mx-sm-0 {\n        margin-right: 0 !important;\n    }\n    .mb-sm-0,\n    .my-sm-0 {\n        margin-bottom: 0 !important;\n    }\n    .ml-sm-0,\n    .mx-sm-0 {\n        margin-left: 0 !important;\n    }\n    .m-sm-1 {\n        margin: 0.25rem !important;\n    }\n    .mt-sm-1,\n    .my-sm-1 {\n        margin-top: 0.25rem !important;\n    }\n    .mr-sm-1,\n    .mx-sm-1 {\n        margin-right: 0.25rem !important;\n    }\n    .mb-sm-1,\n    .my-sm-1 {\n        margin-bottom: 0.25rem !important;\n    }\n    .ml-sm-1,\n    .mx-sm-1 {\n        margin-left: 0.25rem !important;\n    }\n    .m-sm-2 {\n        margin: 0.5rem !important;\n    }\n    .mt-sm-2,\n    .my-sm-2 {\n        margin-top: 0.5rem !important;\n    }\n    .mr-sm-2,\n    .mx-sm-2 {\n        margin-right: 0.5rem !important;\n    }\n    .mb-sm-2,\n    .my-sm-2 {\n        margin-bottom: 0.5rem !important;\n    }\n    .ml-sm-2,\n    .mx-sm-2 {\n        margin-left: 0.5rem !important;\n    }\n    .m-sm-3 {\n        margin: 0.75rem !important;\n    }\n    .mt-sm-3,\n    .my-sm-3 {\n        margin-top: 0.75rem !important;\n    }\n    .mr-sm-3,\n    .mx-sm-3 {\n        margin-right: 0.75rem !important;\n    }\n    .mb-sm-3,\n    .my-sm-3 {\n        margin-bottom: 0.75rem !important;\n    }\n    .ml-sm-3,\n    .mx-sm-3 {\n        margin-left: 0.75rem !important;\n    }\n    .m-sm-4 {\n        margin: 1rem !important;\n    }\n    .mt-sm-4,\n    .my-sm-4 {\n        margin-top: 1rem !important;\n    }\n    .mr-sm-4,\n    .mx-sm-4 {\n        margin-right: 1rem !important;\n    }\n    .mb-sm-4,\n    .my-sm-4 {\n        margin-bottom: 1rem !important;\n    }\n    .ml-sm-4,\n    .mx-sm-4 {\n        margin-left: 1rem !important;\n    }\n    .m-sm-5 {\n        margin: 1.5rem !important;\n    }\n    .mt-sm-5,\n    .my-sm-5 {\n        margin-top: 1.5rem !important;\n    }\n    .mr-sm-5,\n    .mx-sm-5 {\n        margin-right: 1.5rem !important;\n    }\n    .mb-sm-5,\n    .my-sm-5 {\n        margin-bottom: 1.5rem !important;\n    }\n    .ml-sm-5,\n    .mx-sm-5 {\n        margin-left: 1.5rem !important;\n    }\n    .m-sm-6 {\n        margin: 2rem !important;\n    }\n    .mt-sm-6,\n    .my-sm-6 {\n        margin-top: 2rem !important;\n    }\n    .mr-sm-6,\n    .mx-sm-6 {\n        margin-right: 2rem !important;\n    }\n    .mb-sm-6,\n    .my-sm-6 {\n        margin-bottom: 2rem !important;\n    }\n    .ml-sm-6,\n    .mx-sm-6 {\n        margin-left: 2rem !important;\n    }\n    .m-sm-7 {\n        margin: 3rem !important;\n    }\n    .mt-sm-7,\n    .my-sm-7 {\n        margin-top: 3rem !important;\n    }\n    .mr-sm-7,\n    .mx-sm-7 {\n        margin-right: 3rem !important;\n    }\n    .mb-sm-7,\n    .my-sm-7 {\n        margin-bottom: 3rem !important;\n    }\n    .ml-sm-7,\n    .mx-sm-7 {\n        margin-left: 3rem !important;\n    }\n    .m-sm-8 {\n        margin: 4rem !important;\n    }\n    .mt-sm-8,\n    .my-sm-8 {\n        margin-top: 4rem !important;\n    }\n    .mr-sm-8,\n    .mx-sm-8 {\n        margin-right: 4rem !important;\n    }\n    .mb-sm-8,\n    .my-sm-8 {\n        margin-bottom: 4rem !important;\n    }\n    .ml-sm-8,\n    .mx-sm-8 {\n        margin-left: 4rem !important;\n    }\n    .m-sm-9 {\n        margin: 6rem !important;\n    }\n    .mt-sm-9,\n    .my-sm-9 {\n        margin-top: 6rem !important;\n    }\n    .mr-sm-9,\n    .mx-sm-9 {\n        margin-right: 6rem !important;\n    }\n    .mb-sm-9,\n    .my-sm-9 {\n        margin-bottom: 6rem !important;\n    }\n    .ml-sm-9,\n    .mx-sm-9 {\n        margin-left: 6rem !important;\n    }\n    .p-sm-0 {\n        padding: 0 !important;\n    }\n    .pt-sm-0,\n    .py-sm-0 {\n        padding-top: 0 !important;\n    }\n    .pr-sm-0,\n    .px-sm-0 {\n        padding-right: 0 !important;\n    }\n    .pb-sm-0,\n    .py-sm-0 {\n        padding-bottom: 0 !important;\n    }\n    .pl-sm-0,\n    .px-sm-0 {\n        padding-left: 0 !important;\n    }\n    .p-sm-1 {\n        padding: 0.25rem !important;\n    }\n    .pt-sm-1,\n    .py-sm-1 {\n        padding-top: 0.25rem !important;\n    }\n    .pr-sm-1,\n    .px-sm-1 {\n        padding-right: 0.25rem !important;\n    }\n    .pb-sm-1,\n    .py-sm-1 {\n        padding-bottom: 0.25rem !important;\n    }\n    .pl-sm-1,\n    .px-sm-1 {\n        padding-left: 0.25rem !important;\n    }\n    .p-sm-2 {\n        padding: 0.5rem !important;\n    }\n    .pt-sm-2,\n    .py-sm-2 {\n        padding-top: 0.5rem !important;\n    }\n    .pr-sm-2,\n    .px-sm-2 {\n        padding-right: 0.5rem !important;\n    }\n    .pb-sm-2,\n    .py-sm-2 {\n        padding-bottom: 0.5rem !important;\n    }\n    .pl-sm-2,\n    .px-sm-2 {\n        padding-left: 0.5rem !important;\n    }\n    .p-sm-3 {\n        padding: 0.75rem !important;\n    }\n    .pt-sm-3,\n    .py-sm-3 {\n        padding-top: 0.75rem !important;\n    }\n    .pr-sm-3,\n    .px-sm-3 {\n        padding-right: 0.75rem !important;\n    }\n    .pb-sm-3,\n    .py-sm-3 {\n        padding-bottom: 0.75rem !important;\n    }\n    .pl-sm-3,\n    .px-sm-3 {\n        padding-left: 0.75rem !important;\n    }\n    .p-sm-4 {\n        padding: 1rem !important;\n    }\n    .pt-sm-4,\n    .py-sm-4 {\n        padding-top: 1rem !important;\n    }\n    .pr-sm-4,\n    .px-sm-4 {\n        padding-right: 1rem !important;\n    }\n    .pb-sm-4,\n    .py-sm-4 {\n        padding-bottom: 1rem !important;\n    }\n    .pl-sm-4,\n    .px-sm-4 {\n        padding-left: 1rem !important;\n    }\n    .p-sm-5 {\n        padding: 1.5rem !important;\n    }\n    .pt-sm-5,\n    .py-sm-5 {\n        padding-top: 1.5rem !important;\n    }\n    .pr-sm-5,\n    .px-sm-5 {\n        padding-right: 1.5rem !important;\n    }\n    .pb-sm-5,\n    .py-sm-5 {\n        padding-bottom: 1.5rem !important;\n    }\n    .pl-sm-5,\n    .px-sm-5 {\n        padding-left: 1.5rem !important;\n    }\n    .p-sm-6 {\n        padding: 2rem !important;\n    }\n    .pt-sm-6,\n    .py-sm-6 {\n        padding-top: 2rem !important;\n    }\n    .pr-sm-6,\n    .px-sm-6 {\n        padding-right: 2rem !important;\n    }\n    .pb-sm-6,\n    .py-sm-6 {\n        padding-bottom: 2rem !important;\n    }\n    .pl-sm-6,\n    .px-sm-6 {\n        padding-left: 2rem !important;\n    }\n    .p-sm-7 {\n        padding: 3rem !important;\n    }\n    .pt-sm-7,\n    .py-sm-7 {\n        padding-top: 3rem !important;\n    }\n    .pr-sm-7,\n    .px-sm-7 {\n        padding-right: 3rem !important;\n    }\n    .pb-sm-7,\n    .py-sm-7 {\n        padding-bottom: 3rem !important;\n    }\n    .pl-sm-7,\n    .px-sm-7 {\n        padding-left: 3rem !important;\n    }\n    .p-sm-8 {\n        padding: 4rem !important;\n    }\n    .pt-sm-8,\n    .py-sm-8 {\n        padding-top: 4rem !important;\n    }\n    .pr-sm-8,\n    .px-sm-8 {\n        padding-right: 4rem !important;\n    }\n    .pb-sm-8,\n    .py-sm-8 {\n        padding-bottom: 4rem !important;\n    }\n    .pl-sm-8,\n    .px-sm-8 {\n        padding-left: 4rem !important;\n    }\n    .p-sm-9 {\n        padding: 6rem !important;\n    }\n    .pt-sm-9,\n    .py-sm-9 {\n        padding-top: 6rem !important;\n    }\n    .pr-sm-9,\n    .px-sm-9 {\n        padding-right: 6rem !important;\n    }\n    .pb-sm-9,\n    .py-sm-9 {\n        padding-bottom: 6rem !important;\n    }\n    .pl-sm-9,\n    .px-sm-9 {\n        padding-left: 6rem !important;\n    }\n    .m-sm-auto {\n        margin: auto !important;\n    }\n    .mt-sm-auto,\n    .my-sm-auto {\n        margin-top: auto !important;\n    }\n    .mr-sm-auto,\n    .mx-sm-auto {\n        margin-right: auto !important;\n    }\n    .mb-sm-auto,\n    .my-sm-auto {\n        margin-bottom: auto !important;\n    }\n    .ml-sm-auto,\n    .mx-sm-auto {\n        margin-left: auto !important;\n    }\n}\n\n@media (min-width: 768px) {\n    .m-md-0 {\n        margin: 0 !important;\n    }\n    .mt-md-0,\n    .my-md-0 {\n        margin-top: 0 !important;\n    }\n    .mr-md-0,\n    .mx-md-0 {\n        margin-right: 0 !important;\n    }\n    .mb-md-0,\n    .my-md-0 {\n        margin-bottom: 0 !important;\n    }\n    .ml-md-0,\n    .mx-md-0 {\n        margin-left: 0 !important;\n    }\n    .m-md-1 {\n        margin: 0.25rem !important;\n    }\n    .mt-md-1,\n    .my-md-1 {\n        margin-top: 0.25rem !important;\n    }\n    .mr-md-1,\n    .mx-md-1 {\n        margin-right: 0.25rem !important;\n    }\n    .mb-md-1,\n    .my-md-1 {\n        margin-bottom: 0.25rem !important;\n    }\n    .ml-md-1,\n    .mx-md-1 {\n        margin-left: 0.25rem !important;\n    }\n    .m-md-2 {\n        margin: 0.5rem !important;\n    }\n    .mt-md-2,\n    .my-md-2 {\n        margin-top: 0.5rem !important;\n    }\n    .mr-md-2,\n    .mx-md-2 {\n        margin-right: 0.5rem !important;\n    }\n    .mb-md-2,\n    .my-md-2 {\n        margin-bottom: 0.5rem !important;\n    }\n    .ml-md-2,\n    .mx-md-2 {\n        margin-left: 0.5rem !important;\n    }\n    .m-md-3 {\n        margin: 0.75rem !important;\n    }\n    .mt-md-3,\n    .my-md-3 {\n        margin-top: 0.75rem !important;\n    }\n    .mr-md-3,\n    .mx-md-3 {\n        margin-right: 0.75rem !important;\n    }\n    .mb-md-3,\n    .my-md-3 {\n        margin-bottom: 0.75rem !important;\n    }\n    .ml-md-3,\n    .mx-md-3 {\n        margin-left: 0.75rem !important;\n    }\n    .m-md-4 {\n        margin: 1rem !important;\n    }\n    .mt-md-4,\n    .my-md-4 {\n        margin-top: 1rem !important;\n    }\n    .mr-md-4,\n    .mx-md-4 {\n        margin-right: 1rem !important;\n    }\n    .mb-md-4,\n    .my-md-4 {\n        margin-bottom: 1rem !important;\n    }\n    .ml-md-4,\n    .mx-md-4 {\n        margin-left: 1rem !important;\n    }\n    .m-md-5 {\n        margin: 1.5rem !important;\n    }\n    .mt-md-5,\n    .my-md-5 {\n        margin-top: 1.5rem !important;\n    }\n    .mr-md-5,\n    .mx-md-5 {\n        margin-right: 1.5rem !important;\n    }\n    .mb-md-5,\n    .my-md-5 {\n        margin-bottom: 1.5rem !important;\n    }\n    .ml-md-5,\n    .mx-md-5 {\n        margin-left: 1.5rem !important;\n    }\n    .m-md-6 {\n        margin: 2rem !important;\n    }\n    .mt-md-6,\n    .my-md-6 {\n        margin-top: 2rem !important;\n    }\n    .mr-md-6,\n    .mx-md-6 {\n        margin-right: 2rem !important;\n    }\n    .mb-md-6,\n    .my-md-6 {\n        margin-bottom: 2rem !important;\n    }\n    .ml-md-6,\n    .mx-md-6 {\n        margin-left: 2rem !important;\n    }\n    .m-md-7 {\n        margin: 3rem !important;\n    }\n    .mt-md-7,\n    .my-md-7 {\n        margin-top: 3rem !important;\n    }\n    .mr-md-7,\n    .mx-md-7 {\n        margin-right: 3rem !important;\n    }\n    .mb-md-7,\n    .my-md-7 {\n        margin-bottom: 3rem !important;\n    }\n    .ml-md-7,\n    .mx-md-7 {\n        margin-left: 3rem !important;\n    }\n    .m-md-8 {\n        margin: 4rem !important;\n    }\n    .mt-md-8,\n    .my-md-8 {\n        margin-top: 4rem !important;\n    }\n    .mr-md-8,\n    .mx-md-8 {\n        margin-right: 4rem !important;\n    }\n    .mb-md-8,\n    .my-md-8 {\n        margin-bottom: 4rem !important;\n    }\n    .ml-md-8,\n    .mx-md-8 {\n        margin-left: 4rem !important;\n    }\n    .m-md-9 {\n        margin: 6rem !important;\n    }\n    .mt-md-9,\n    .my-md-9 {\n        margin-top: 6rem !important;\n    }\n    .mr-md-9,\n    .mx-md-9 {\n        margin-right: 6rem !important;\n    }\n    .mb-md-9,\n    .my-md-9 {\n        margin-bottom: 6rem !important;\n    }\n    .ml-md-9,\n    .mx-md-9 {\n        margin-left: 6rem !important;\n    }\n    .p-md-0 {\n        padding: 0 !important;\n    }\n    .pt-md-0,\n    .py-md-0 {\n        padding-top: 0 !important;\n    }\n    .pr-md-0,\n    .px-md-0 {\n        padding-right: 0 !important;\n    }\n    .pb-md-0,\n    .py-md-0 {\n        padding-bottom: 0 !important;\n    }\n    .pl-md-0,\n    .px-md-0 {\n        padding-left: 0 !important;\n    }\n    .p-md-1 {\n        padding: 0.25rem !important;\n    }\n    .pt-md-1,\n    .py-md-1 {\n        padding-top: 0.25rem !important;\n    }\n    .pr-md-1,\n    .px-md-1 {\n        padding-right: 0.25rem !important;\n    }\n    .pb-md-1,\n    .py-md-1 {\n        padding-bottom: 0.25rem !important;\n    }\n    .pl-md-1,\n    .px-md-1 {\n        padding-left: 0.25rem !important;\n    }\n    .p-md-2 {\n        padding: 0.5rem !important;\n    }\n    .pt-md-2,\n    .py-md-2 {\n        padding-top: 0.5rem !important;\n    }\n    .pr-md-2,\n    .px-md-2 {\n        padding-right: 0.5rem !important;\n    }\n    .pb-md-2,\n    .py-md-2 {\n        padding-bottom: 0.5rem !important;\n    }\n    .pl-md-2,\n    .px-md-2 {\n        padding-left: 0.5rem !important;\n    }\n    .p-md-3 {\n        padding: 0.75rem !important;\n    }\n    .pt-md-3,\n    .py-md-3 {\n        padding-top: 0.75rem !important;\n    }\n    .pr-md-3,\n    .px-md-3 {\n        padding-right: 0.75rem !important;\n    }\n    .pb-md-3,\n    .py-md-3 {\n        padding-bottom: 0.75rem !important;\n    }\n    .pl-md-3,\n    .px-md-3 {\n        padding-left: 0.75rem !important;\n    }\n    .p-md-4 {\n        padding: 1rem !important;\n    }\n    .pt-md-4,\n    .py-md-4 {\n        padding-top: 1rem !important;\n    }\n    .pr-md-4,\n    .px-md-4 {\n        padding-right: 1rem !important;\n    }\n    .pb-md-4,\n    .py-md-4 {\n        padding-bottom: 1rem !important;\n    }\n    .pl-md-4,\n    .px-md-4 {\n        padding-left: 1rem !important;\n    }\n    .p-md-5 {\n        padding: 1.5rem !important;\n    }\n    .pt-md-5,\n    .py-md-5 {\n        padding-top: 1.5rem !important;\n    }\n    .pr-md-5,\n    .px-md-5 {\n        padding-right: 1.5rem !important;\n    }\n    .pb-md-5,\n    .py-md-5 {\n        padding-bottom: 1.5rem !important;\n    }\n    .pl-md-5,\n    .px-md-5 {\n        padding-left: 1.5rem !important;\n    }\n    .p-md-6 {\n        padding: 2rem !important;\n    }\n    .pt-md-6,\n    .py-md-6 {\n        padding-top: 2rem !important;\n    }\n    .pr-md-6,\n    .px-md-6 {\n        padding-right: 2rem !important;\n    }\n    .pb-md-6,\n    .py-md-6 {\n        padding-bottom: 2rem !important;\n    }\n    .pl-md-6,\n    .px-md-6 {\n        padding-left: 2rem !important;\n    }\n    .p-md-7 {\n        padding: 3rem !important;\n    }\n    .pt-md-7,\n    .py-md-7 {\n        padding-top: 3rem !important;\n    }\n    .pr-md-7,\n    .px-md-7 {\n        padding-right: 3rem !important;\n    }\n    .pb-md-7,\n    .py-md-7 {\n        padding-bottom: 3rem !important;\n    }\n    .pl-md-7,\n    .px-md-7 {\n        padding-left: 3rem !important;\n    }\n    .p-md-8 {\n        padding: 4rem !important;\n    }\n    .pt-md-8,\n    .py-md-8 {\n        padding-top: 4rem !important;\n    }\n    .pr-md-8,\n    .px-md-8 {\n        padding-right: 4rem !important;\n    }\n    .pb-md-8,\n    .py-md-8 {\n        padding-bottom: 4rem !important;\n    }\n    .pl-md-8,\n    .px-md-8 {\n        padding-left: 4rem !important;\n    }\n    .p-md-9 {\n        padding: 6rem !important;\n    }\n    .pt-md-9,\n    .py-md-9 {\n        padding-top: 6rem !important;\n    }\n    .pr-md-9,\n    .px-md-9 {\n        padding-right: 6rem !important;\n    }\n    .pb-md-9,\n    .py-md-9 {\n        padding-bottom: 6rem !important;\n    }\n    .pl-md-9,\n    .px-md-9 {\n        padding-left: 6rem !important;\n    }\n    .m-md-auto {\n        margin: auto !important;\n    }\n    .mt-md-auto,\n    .my-md-auto {\n        margin-top: auto !important;\n    }\n    .mr-md-auto,\n    .mx-md-auto {\n        margin-right: auto !important;\n    }\n    .mb-md-auto,\n    .my-md-auto {\n        margin-bottom: auto !important;\n    }\n    .ml-md-auto,\n    .mx-md-auto {\n        margin-left: auto !important;\n    }\n}\n\n@media (min-width: 992px) {\n    .m-lg-0 {\n        margin: 0 !important;\n    }\n    .mt-lg-0,\n    .my-lg-0 {\n        margin-top: 0 !important;\n    }\n    .mr-lg-0,\n    .mx-lg-0 {\n        margin-right: 0 !important;\n    }\n    .mb-lg-0,\n    .my-lg-0 {\n        margin-bottom: 0 !important;\n    }\n    .ml-lg-0,\n    .mx-lg-0 {\n        margin-left: 0 !important;\n    }\n    .m-lg-1 {\n        margin: 0.25rem !important;\n    }\n    .mt-lg-1,\n    .my-lg-1 {\n        margin-top: 0.25rem !important;\n    }\n    .mr-lg-1,\n    .mx-lg-1 {\n        margin-right: 0.25rem !important;\n    }\n    .mb-lg-1,\n    .my-lg-1 {\n        margin-bottom: 0.25rem !important;\n    }\n    .ml-lg-1,\n    .mx-lg-1 {\n        margin-left: 0.25rem !important;\n    }\n    .m-lg-2 {\n        margin: 0.5rem !important;\n    }\n    .mt-lg-2,\n    .my-lg-2 {\n        margin-top: 0.5rem !important;\n    }\n    .mr-lg-2,\n    .mx-lg-2 {\n        margin-right: 0.5rem !important;\n    }\n    .mb-lg-2,\n    .my-lg-2 {\n        margin-bottom: 0.5rem !important;\n    }\n    .ml-lg-2,\n    .mx-lg-2 {\n        margin-left: 0.5rem !important;\n    }\n    .m-lg-3 {\n        margin: 0.75rem !important;\n    }\n    .mt-lg-3,\n    .my-lg-3 {\n        margin-top: 0.75rem !important;\n    }\n    .mr-lg-3,\n    .mx-lg-3 {\n        margin-right: 0.75rem !important;\n    }\n    .mb-lg-3,\n    .my-lg-3 {\n        margin-bottom: 0.75rem !important;\n    }\n    .ml-lg-3,\n    .mx-lg-3 {\n        margin-left: 0.75rem !important;\n    }\n    .m-lg-4 {\n        margin: 1rem !important;\n    }\n    .mt-lg-4,\n    .my-lg-4 {\n        margin-top: 1rem !important;\n    }\n    .mr-lg-4,\n    .mx-lg-4 {\n        margin-right: 1rem !important;\n    }\n    .mb-lg-4,\n    .my-lg-4 {\n        margin-bottom: 1rem !important;\n    }\n    .ml-lg-4,\n    .mx-lg-4 {\n        margin-left: 1rem !important;\n    }\n    .m-lg-5 {\n        margin: 1.5rem !important;\n    }\n    .mt-lg-5,\n    .my-lg-5 {\n        margin-top: 1.5rem !important;\n    }\n    .mr-lg-5,\n    .mx-lg-5 {\n        margin-right: 1.5rem !important;\n    }\n    .mb-lg-5,\n    .my-lg-5 {\n        margin-bottom: 1.5rem !important;\n    }\n    .ml-lg-5,\n    .mx-lg-5 {\n        margin-left: 1.5rem !important;\n    }\n    .m-lg-6 {\n        margin: 2rem !important;\n    }\n    .mt-lg-6,\n    .my-lg-6 {\n        margin-top: 2rem !important;\n    }\n    .mr-lg-6,\n    .mx-lg-6 {\n        margin-right: 2rem !important;\n    }\n    .mb-lg-6,\n    .my-lg-6 {\n        margin-bottom: 2rem !important;\n    }\n    .ml-lg-6,\n    .mx-lg-6 {\n        margin-left: 2rem !important;\n    }\n    .m-lg-7 {\n        margin: 3rem !important;\n    }\n    .mt-lg-7,\n    .my-lg-7 {\n        margin-top: 3rem !important;\n    }\n    .mr-lg-7,\n    .mx-lg-7 {\n        margin-right: 3rem !important;\n    }\n    .mb-lg-7,\n    .my-lg-7 {\n        margin-bottom: 3rem !important;\n    }\n    .ml-lg-7,\n    .mx-lg-7 {\n        margin-left: 3rem !important;\n    }\n    .m-lg-8 {\n        margin: 4rem !important;\n    }\n    .mt-lg-8,\n    .my-lg-8 {\n        margin-top: 4rem !important;\n    }\n    .mr-lg-8,\n    .mx-lg-8 {\n        margin-right: 4rem !important;\n    }\n    .mb-lg-8,\n    .my-lg-8 {\n        margin-bottom: 4rem !important;\n    }\n    .ml-lg-8,\n    .mx-lg-8 {\n        margin-left: 4rem !important;\n    }\n    .m-lg-9 {\n        margin: 6rem !important;\n    }\n    .mt-lg-9,\n    .my-lg-9 {\n        margin-top: 6rem !important;\n    }\n    .mr-lg-9,\n    .mx-lg-9 {\n        margin-right: 6rem !important;\n    }\n    .mb-lg-9,\n    .my-lg-9 {\n        margin-bottom: 6rem !important;\n    }\n    .ml-lg-9,\n    .mx-lg-9 {\n        margin-left: 6rem !important;\n    }\n    .p-lg-0 {\n        padding: 0 !important;\n    }\n    .pt-lg-0,\n    .py-lg-0 {\n        padding-top: 0 !important;\n    }\n    .pr-lg-0,\n    .px-lg-0 {\n        padding-right: 0 !important;\n    }\n    .pb-lg-0,\n    .py-lg-0 {\n        padding-bottom: 0 !important;\n    }\n    .pl-lg-0,\n    .px-lg-0 {\n        padding-left: 0 !important;\n    }\n    .p-lg-1 {\n        padding: 0.25rem !important;\n    }\n    .pt-lg-1,\n    .py-lg-1 {\n        padding-top: 0.25rem !important;\n    }\n    .pr-lg-1,\n    .px-lg-1 {\n        padding-right: 0.25rem !important;\n    }\n    .pb-lg-1,\n    .py-lg-1 {\n        padding-bottom: 0.25rem !important;\n    }\n    .pl-lg-1,\n    .px-lg-1 {\n        padding-left: 0.25rem !important;\n    }\n    .p-lg-2 {\n        padding: 0.5rem !important;\n    }\n    .pt-lg-2,\n    .py-lg-2 {\n        padding-top: 0.5rem !important;\n    }\n    .pr-lg-2,\n    .px-lg-2 {\n        padding-right: 0.5rem !important;\n    }\n    .pb-lg-2,\n    .py-lg-2 {\n        padding-bottom: 0.5rem !important;\n    }\n    .pl-lg-2,\n    .px-lg-2 {\n        padding-left: 0.5rem !important;\n    }\n    .p-lg-3 {\n        padding: 0.75rem !important;\n    }\n    .pt-lg-3,\n    .py-lg-3 {\n        padding-top: 0.75rem !important;\n    }\n    .pr-lg-3,\n    .px-lg-3 {\n        padding-right: 0.75rem !important;\n    }\n    .pb-lg-3,\n    .py-lg-3 {\n        padding-bottom: 0.75rem !important;\n    }\n    .pl-lg-3,\n    .px-lg-3 {\n        padding-left: 0.75rem !important;\n    }\n    .p-lg-4 {\n        padding: 1rem !important;\n    }\n    .pt-lg-4,\n    .py-lg-4 {\n        padding-top: 1rem !important;\n    }\n    .pr-lg-4,\n    .px-lg-4 {\n        padding-right: 1rem !important;\n    }\n    .pb-lg-4,\n    .py-lg-4 {\n        padding-bottom: 1rem !important;\n    }\n    .pl-lg-4,\n    .px-lg-4 {\n        padding-left: 1rem !important;\n    }\n    .p-lg-5 {\n        padding: 1.5rem !important;\n    }\n    .pt-lg-5,\n    .py-lg-5 {\n        padding-top: 1.5rem !important;\n    }\n    .pr-lg-5,\n    .px-lg-5 {\n        padding-right: 1.5rem !important;\n    }\n    .pb-lg-5,\n    .py-lg-5 {\n        padding-bottom: 1.5rem !important;\n    }\n    .pl-lg-5,\n    .px-lg-5 {\n        padding-left: 1.5rem !important;\n    }\n    .p-lg-6 {\n        padding: 2rem !important;\n    }\n    .pt-lg-6,\n    .py-lg-6 {\n        padding-top: 2rem !important;\n    }\n    .pr-lg-6,\n    .px-lg-6 {\n        padding-right: 2rem !important;\n    }\n    .pb-lg-6,\n    .py-lg-6 {\n        padding-bottom: 2rem !important;\n    }\n    .pl-lg-6,\n    .px-lg-6 {\n        padding-left: 2rem !important;\n    }\n    .p-lg-7 {\n        padding: 3rem !important;\n    }\n    .pt-lg-7,\n    .py-lg-7 {\n        padding-top: 3rem !important;\n    }\n    .pr-lg-7,\n    .px-lg-7 {\n        padding-right: 3rem !important;\n    }\n    .pb-lg-7,\n    .py-lg-7 {\n        padding-bottom: 3rem !important;\n    }\n    .pl-lg-7,\n    .px-lg-7 {\n        padding-left: 3rem !important;\n    }\n    .p-lg-8 {\n        padding: 4rem !important;\n    }\n    .pt-lg-8,\n    .py-lg-8 {\n        padding-top: 4rem !important;\n    }\n    .pr-lg-8,\n    .px-lg-8 {\n        padding-right: 4rem !important;\n    }\n    .pb-lg-8,\n    .py-lg-8 {\n        padding-bottom: 4rem !important;\n    }\n    .pl-lg-8,\n    .px-lg-8 {\n        padding-left: 4rem !important;\n    }\n    .p-lg-9 {\n        padding: 6rem !important;\n    }\n    .pt-lg-9,\n    .py-lg-9 {\n        padding-top: 6rem !important;\n    }\n    .pr-lg-9,\n    .px-lg-9 {\n        padding-right: 6rem !important;\n    }\n    .pb-lg-9,\n    .py-lg-9 {\n        padding-bottom: 6rem !important;\n    }\n    .pl-lg-9,\n    .px-lg-9 {\n        padding-left: 6rem !important;\n    }\n    .m-lg-auto {\n        margin: auto !important;\n    }\n    .mt-lg-auto,\n    .my-lg-auto {\n        margin-top: auto !important;\n    }\n    .mr-lg-auto,\n    .mx-lg-auto {\n        margin-right: auto !important;\n    }\n    .mb-lg-auto,\n    .my-lg-auto {\n        margin-bottom: auto !important;\n    }\n    .ml-lg-auto,\n    .mx-lg-auto {\n        margin-left: auto !important;\n    }\n}\n\n@media (min-width: 1280px) {\n    .m-xl-0 {\n        margin: 0 !important;\n    }\n    .mt-xl-0,\n    .my-xl-0 {\n        margin-top: 0 !important;\n    }\n    .mr-xl-0,\n    .mx-xl-0 {\n        margin-right: 0 !important;\n    }\n    .mb-xl-0,\n    .my-xl-0 {\n        margin-bottom: 0 !important;\n    }\n    .ml-xl-0,\n    .mx-xl-0 {\n        margin-left: 0 !important;\n    }\n    .m-xl-1 {\n        margin: 0.25rem !important;\n    }\n    .mt-xl-1,\n    .my-xl-1 {\n        margin-top: 0.25rem !important;\n    }\n    .mr-xl-1,\n    .mx-xl-1 {\n        margin-right: 0.25rem !important;\n    }\n    .mb-xl-1,\n    .my-xl-1 {\n        margin-bottom: 0.25rem !important;\n    }\n    .ml-xl-1,\n    .mx-xl-1 {\n        margin-left: 0.25rem !important;\n    }\n    .m-xl-2 {\n        margin: 0.5rem !important;\n    }\n    .mt-xl-2,\n    .my-xl-2 {\n        margin-top: 0.5rem !important;\n    }\n    .mr-xl-2,\n    .mx-xl-2 {\n        margin-right: 0.5rem !important;\n    }\n    .mb-xl-2,\n    .my-xl-2 {\n        margin-bottom: 0.5rem !important;\n    }\n    .ml-xl-2,\n    .mx-xl-2 {\n        margin-left: 0.5rem !important;\n    }\n    .m-xl-3 {\n        margin: 0.75rem !important;\n    }\n    .mt-xl-3,\n    .my-xl-3 {\n        margin-top: 0.75rem !important;\n    }\n    .mr-xl-3,\n    .mx-xl-3 {\n        margin-right: 0.75rem !important;\n    }\n    .mb-xl-3,\n    .my-xl-3 {\n        margin-bottom: 0.75rem !important;\n    }\n    .ml-xl-3,\n    .mx-xl-3 {\n        margin-left: 0.75rem !important;\n    }\n    .m-xl-4 {\n        margin: 1rem !important;\n    }\n    .mt-xl-4,\n    .my-xl-4 {\n        margin-top: 1rem !important;\n    }\n    .mr-xl-4,\n    .mx-xl-4 {\n        margin-right: 1rem !important;\n    }\n    .mb-xl-4,\n    .my-xl-4 {\n        margin-bottom: 1rem !important;\n    }\n    .ml-xl-4,\n    .mx-xl-4 {\n        margin-left: 1rem !important;\n    }\n    .m-xl-5 {\n        margin: 1.5rem !important;\n    }\n    .mt-xl-5,\n    .my-xl-5 {\n        margin-top: 1.5rem !important;\n    }\n    .mr-xl-5,\n    .mx-xl-5 {\n        margin-right: 1.5rem !important;\n    }\n    .mb-xl-5,\n    .my-xl-5 {\n        margin-bottom: 1.5rem !important;\n    }\n    .ml-xl-5,\n    .mx-xl-5 {\n        margin-left: 1.5rem !important;\n    }\n    .m-xl-6 {\n        margin: 2rem !important;\n    }\n    .mt-xl-6,\n    .my-xl-6 {\n        margin-top: 2rem !important;\n    }\n    .mr-xl-6,\n    .mx-xl-6 {\n        margin-right: 2rem !important;\n    }\n    .mb-xl-6,\n    .my-xl-6 {\n        margin-bottom: 2rem !important;\n    }\n    .ml-xl-6,\n    .mx-xl-6 {\n        margin-left: 2rem !important;\n    }\n    .m-xl-7 {\n        margin: 3rem !important;\n    }\n    .mt-xl-7,\n    .my-xl-7 {\n        margin-top: 3rem !important;\n    }\n    .mr-xl-7,\n    .mx-xl-7 {\n        margin-right: 3rem !important;\n    }\n    .mb-xl-7,\n    .my-xl-7 {\n        margin-bottom: 3rem !important;\n    }\n    .ml-xl-7,\n    .mx-xl-7 {\n        margin-left: 3rem !important;\n    }\n    .m-xl-8 {\n        margin: 4rem !important;\n    }\n    .mt-xl-8,\n    .my-xl-8 {\n        margin-top: 4rem !important;\n    }\n    .mr-xl-8,\n    .mx-xl-8 {\n        margin-right: 4rem !important;\n    }\n    .mb-xl-8,\n    .my-xl-8 {\n        margin-bottom: 4rem !important;\n    }\n    .ml-xl-8,\n    .mx-xl-8 {\n        margin-left: 4rem !important;\n    }\n    .m-xl-9 {\n        margin: 6rem !important;\n    }\n    .mt-xl-9,\n    .my-xl-9 {\n        margin-top: 6rem !important;\n    }\n    .mr-xl-9,\n    .mx-xl-9 {\n        margin-right: 6rem !important;\n    }\n    .mb-xl-9,\n    .my-xl-9 {\n        margin-bottom: 6rem !important;\n    }\n    .ml-xl-9,\n    .mx-xl-9 {\n        margin-left: 6rem !important;\n    }\n    .p-xl-0 {\n        padding: 0 !important;\n    }\n    .pt-xl-0,\n    .py-xl-0 {\n        padding-top: 0 !important;\n    }\n    .pr-xl-0,\n    .px-xl-0 {\n        padding-right: 0 !important;\n    }\n    .pb-xl-0,\n    .py-xl-0 {\n        padding-bottom: 0 !important;\n    }\n    .pl-xl-0,\n    .px-xl-0 {\n        padding-left: 0 !important;\n    }\n    .p-xl-1 {\n        padding: 0.25rem !important;\n    }\n    .pt-xl-1,\n    .py-xl-1 {\n        padding-top: 0.25rem !important;\n    }\n    .pr-xl-1,\n    .px-xl-1 {\n        padding-right: 0.25rem !important;\n    }\n    .pb-xl-1,\n    .py-xl-1 {\n        padding-bottom: 0.25rem !important;\n    }\n    .pl-xl-1,\n    .px-xl-1 {\n        padding-left: 0.25rem !important;\n    }\n    .p-xl-2 {\n        padding: 0.5rem !important;\n    }\n    .pt-xl-2,\n    .py-xl-2 {\n        padding-top: 0.5rem !important;\n    }\n    .pr-xl-2,\n    .px-xl-2 {\n        padding-right: 0.5rem !important;\n    }\n    .pb-xl-2,\n    .py-xl-2 {\n        padding-bottom: 0.5rem !important;\n    }\n    .pl-xl-2,\n    .px-xl-2 {\n        padding-left: 0.5rem !important;\n    }\n    .p-xl-3 {\n        padding: 0.75rem !important;\n    }\n    .pt-xl-3,\n    .py-xl-3 {\n        padding-top: 0.75rem !important;\n    }\n    .pr-xl-3,\n    .px-xl-3 {\n        padding-right: 0.75rem !important;\n    }\n    .pb-xl-3,\n    .py-xl-3 {\n        padding-bottom: 0.75rem !important;\n    }\n    .pl-xl-3,\n    .px-xl-3 {\n        padding-left: 0.75rem !important;\n    }\n    .p-xl-4 {\n        padding: 1rem !important;\n    }\n    .pt-xl-4,\n    .py-xl-4 {\n        padding-top: 1rem !important;\n    }\n    .pr-xl-4,\n    .px-xl-4 {\n        padding-right: 1rem !important;\n    }\n    .pb-xl-4,\n    .py-xl-4 {\n        padding-bottom: 1rem !important;\n    }\n    .pl-xl-4,\n    .px-xl-4 {\n        padding-left: 1rem !important;\n    }\n    .p-xl-5 {\n        padding: 1.5rem !important;\n    }\n    .pt-xl-5,\n    .py-xl-5 {\n        padding-top: 1.5rem !important;\n    }\n    .pr-xl-5,\n    .px-xl-5 {\n        padding-right: 1.5rem !important;\n    }\n    .pb-xl-5,\n    .py-xl-5 {\n        padding-bottom: 1.5rem !important;\n    }\n    .pl-xl-5,\n    .px-xl-5 {\n        padding-left: 1.5rem !important;\n    }\n    .p-xl-6 {\n        padding: 2rem !important;\n    }\n    .pt-xl-6,\n    .py-xl-6 {\n        padding-top: 2rem !important;\n    }\n    .pr-xl-6,\n    .px-xl-6 {\n        padding-right: 2rem !important;\n    }\n    .pb-xl-6,\n    .py-xl-6 {\n        padding-bottom: 2rem !important;\n    }\n    .pl-xl-6,\n    .px-xl-6 {\n        padding-left: 2rem !important;\n    }\n    .p-xl-7 {\n        padding: 3rem !important;\n    }\n    .pt-xl-7,\n    .py-xl-7 {\n        padding-top: 3rem !important;\n    }\n    .pr-xl-7,\n    .px-xl-7 {\n        padding-right: 3rem !important;\n    }\n    .pb-xl-7,\n    .py-xl-7 {\n        padding-bottom: 3rem !important;\n    }\n    .pl-xl-7,\n    .px-xl-7 {\n        padding-left: 3rem !important;\n    }\n    .p-xl-8 {\n        padding: 4rem !important;\n    }\n    .pt-xl-8,\n    .py-xl-8 {\n        padding-top: 4rem !important;\n    }\n    .pr-xl-8,\n    .px-xl-8 {\n        padding-right: 4rem !important;\n    }\n    .pb-xl-8,\n    .py-xl-8 {\n        padding-bottom: 4rem !important;\n    }\n    .pl-xl-8,\n    .px-xl-8 {\n        padding-left: 4rem !important;\n    }\n    .p-xl-9 {\n        padding: 6rem !important;\n    }\n    .pt-xl-9,\n    .py-xl-9 {\n        padding-top: 6rem !important;\n    }\n    .pr-xl-9,\n    .px-xl-9 {\n        padding-right: 6rem !important;\n    }\n    .pb-xl-9,\n    .py-xl-9 {\n        padding-bottom: 6rem !important;\n    }\n    .pl-xl-9,\n    .px-xl-9 {\n        padding-left: 6rem !important;\n    }\n    .m-xl-auto {\n        margin: auto !important;\n    }\n    .mt-xl-auto,\n    .my-xl-auto {\n        margin-top: auto !important;\n    }\n    .mr-xl-auto,\n    .mx-xl-auto {\n        margin-right: auto !important;\n    }\n    .mb-xl-auto,\n    .my-xl-auto {\n        margin-bottom: auto !important;\n    }\n    .ml-xl-auto,\n    .mx-xl-auto {\n        margin-left: auto !important;\n    }\n}\n\n.text-justify {\n    text-align: justify !important;\n}\n\n.text-nowrap {\n    white-space: nowrap !important;\n}\n\n.text-truncate {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.text-left {\n    text-align: left !important;\n}\n\n.text-right {\n    text-align: right !important;\n}\n\n.text-center {\n    text-align: center !important;\n}\n\n@media (min-width: 576px) {\n    .text-sm-left {\n        text-align: left !important;\n    }\n    .text-sm-right {\n        text-align: right !important;\n    }\n    .text-sm-center {\n        text-align: center !important;\n    }\n}\n\n@media (min-width: 768px) {\n    .text-md-left {\n        text-align: left !important;\n    }\n    .text-md-right {\n        text-align: right !important;\n    }\n    .text-md-center {\n        text-align: center !important;\n    }\n}\n\n@media (min-width: 992px) {\n    .text-lg-left {\n        text-align: left !important;\n    }\n    .text-lg-right {\n        text-align: right !important;\n    }\n    .text-lg-center {\n        text-align: center !important;\n    }\n}\n\n@media (min-width: 1280px) {\n    .text-xl-left {\n        text-align: left !important;\n    }\n    .text-xl-right {\n        text-align: right !important;\n    }\n    .text-xl-center {\n        text-align: center !important;\n    }\n}\n\n.text-lowercase {\n    text-transform: lowercase !important;\n}\n\n.text-uppercase {\n    text-transform: uppercase !important;\n}\n\n.text-capitalize {\n    text-transform: capitalize !important;\n}\n\n.font-weight-light {\n    font-weight: 300 !important;\n}\n\n.font-weight-normal {\n    font-weight: 400 !important;\n}\n\n.font-weight-bold {\n    font-weight: 700 !important;\n}\n\n.font-italic {\n    font-style: italic !important;\n}\n\n.text-white {\n    color: #fff !important;\n}\n\n.text-primary {\n    color: #467fcf !important;\n}\n\na.text-primary:hover,\na.text-primary:focus {\n    color: #2f66b3 !important;\n}\n\n.text-secondary {\n    color: #868e96 !important;\n}\n\na.text-secondary:hover,\na.text-secondary:focus {\n    color: #6c757d !important;\n}\n\n.text-success {\n    color: #5eba00 !important;\n}\n\na.text-success:hover,\na.text-success:focus {\n    color: #448700 !important;\n}\n\n.text-info {\n    color: #45aaf2 !important;\n}\n\na.text-info:hover,\na.text-info:focus {\n    color: #1594ef !important;\n}\n\n.text-warning {\n    color: #f1c40f !important;\n}\n\na.text-warning:hover,\na.text-warning:focus {\n    color: #c29d0b !important;\n}\n\n.text-danger {\n    color: #cd201f !important;\n}\n\na.text-danger:hover,\na.text-danger:focus {\n    color: #a11918 !important;\n}\n\n.text-light {\n    color: #f8f9fa !important;\n}\n\na.text-light:hover,\na.text-light:focus {\n    color: #dae0e5 !important;\n}\n\n.text-dark {\n    color: #343a40 !important;\n}\n\na.text-dark:hover,\na.text-dark:focus {\n    color: #1d2124 !important;\n}\n\n.text-muted {\n    color: #9aa0ac !important;\n}\n\n.text-hide {\n    font: 0/0 a;\n    color: transparent;\n    text-shadow: none;\n    background-color: transparent;\n    border: 0;\n}\n\n.visible {\n    visibility: visible !important;\n}\n\n.invisible {\n    visibility: hidden !important;\n}\n\n@media print {\n    *,\n    *::before,\n    *::after {\n        text-shadow: none !important;\n        box-shadow: none !important;\n    }\n    a:not(.btn) {\n        text-decoration: underline;\n    }\n    abbr[title]::after {\n        content: ' (' attr(title) ')';\n    }\n    pre {\n        white-space: pre-wrap !important;\n    }\n    pre,\n    blockquote {\n        border: 1px solid #999;\n        page-break-inside: avoid;\n    }\n    thead {\n        display: table-header-group;\n    }\n    tr,\n    img {\n        page-break-inside: avoid;\n    }\n    p,\n    h2,\n    h3 {\n        orphans: 3;\n        widows: 3;\n    }\n    h2,\n    h3 {\n        page-break-after: avoid;\n    }\n    @page {\n        size: a3;\n    }\n    body {\n        min-width: 992px !important;\n    }\n    .container {\n        min-width: 992px !important;\n    }\n    .navbar {\n        display: none;\n    }\n    .badge {\n        border: 1px solid #000;\n    }\n    .table,\n    .text-wrap table {\n        border-collapse: collapse !important;\n    }\n    .table td,\n    .text-wrap table td,\n    .table th,\n    .text-wrap table th {\n        background-color: #fff !important;\n    }\n    .table-bordered th,\n    .text-wrap table th,\n    .table-bordered td,\n    .text-wrap table td {\n        border: 1px solid #ddd !important;\n    }\n}\n\nhtml {\n    font-size: 16px;\n    height: 100%;\n    direction: ltr;\n}\n\nbody {\n    direction: ltr;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    -webkit-tap-highlight-color: transparent;\n    -webkit-text-size-adjust: none;\n    -ms-touch-action: manipulation;\n    touch-action: manipulation;\n    -webkit-font-feature-settings: 'liga' 0;\n    font-feature-settings: 'liga' 0;\n    height: 100%;\n    overflow-y: scroll;\n    position: relative;\n}\n\n@media print {\n    body {\n        background: none;\n    }\n}\n\nbody *::-webkit-scrollbar {\n    width: 6px;\n    height: 6px;\n    transition: 0.3s background;\n}\n\nbody *::-webkit-scrollbar-thumb {\n    background: #ced4da;\n}\n\nbody *:hover::-webkit-scrollbar-thumb {\n    background: #adb5bd;\n}\n\n.lead {\n    line-height: 1.4;\n}\n\na {\n    -webkit-text-decoration-skip: auto;\n    text-decoration-skip-ink: auto;\n}\n\nh1 a,\nh2 a,\nh3 a,\nh4 a,\nh5 a,\nh6 a,\n.h1 a,\n.h2 a,\n.h3 a,\n.h4 a,\n.h5 a,\n.h6 a {\n    color: inherit;\n}\n\nstrong,\nb {\n    font-weight: 600;\n}\n\np,\nul,\nol,\nblockquote {\n    margin-bottom: 1em;\n}\n\nblockquote {\n    font-style: italic;\n    color: #6e7687;\n    padding-left: 2rem;\n    border-left: 2px solid rgba(0, 40, 100, 0.12);\n}\n\nblockquote p {\n    margin-bottom: 1rem;\n}\n\nblockquote cite {\n    display: block;\n    text-align: right;\n}\n\nblockquote cite:before {\n    content: '— ';\n}\n\ncode {\n    background: rgba(0, 0, 0, 0.025);\n    border: 1px solid rgba(0, 0, 0, 0.05);\n    border-radius: 3px;\n    padding: 3px;\n}\n\npre code {\n    padding: 0;\n    border-radius: 0;\n    border: none;\n    background: none;\n}\n\nhr {\n    margin-top: 2rem;\n    margin-bottom: 2rem;\n}\n\npre {\n    color: #343a40;\n    padding: 1rem;\n    overflow: auto;\n    font-size: 85%;\n    line-height: 1.45;\n    background-color: #f8fafc;\n    border-radius: 3px;\n    -moz-tab-size: 4;\n    -o-tab-size: 4;\n    tab-size: 4;\n    text-shadow: 0 1px white;\n    -webkit-hyphens: none;\n    -moz-hyphens: none;\n    -ms-hyphens: none;\n    hyphens: none;\n}\n\nimg {\n    max-width: 100%;\n}\n\n.text-wrap {\n    font-size: 1rem;\n    line-height: 1.66;\n}\n\n.text-wrap > :first-child {\n    margin-top: 0;\n}\n\n.text-wrap > :last-child {\n    margin-bottom: 0;\n}\n\n.text-wrap > h1,\n.text-wrap > h2,\n.text-wrap > h3,\n.text-wrap > h4,\n.text-wrap > h5,\n.text-wrap > h6 {\n    margin-top: 1em;\n}\n\n.section-nav {\n    background-color: #f8f9fa;\n    margin: 1rem 0;\n    padding: 0.5rem 1rem;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    border-radius: 3px;\n    list-style: none;\n}\n\n.section-nav:before {\n    content: 'Table of contents:';\n    display: block;\n    font-weight: 600;\n}\n\n@media print {\n    .container {\n        max-width: none;\n    }\n}\n\n.row-cards > .col,\n.row-cards > [class*='col-'] {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n}\n\n.row-deck > .col,\n.row-deck > [class*='col-'] {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: stretch;\n    align-items: stretch;\n}\n\n.row-deck > .col .card,\n.row-deck > [class*='col-'] .card {\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n}\n\n.col-text {\n    max-width: 48rem;\n}\n\n.col-login {\n    max-width: 24rem;\n}\n\n.gutters-0 {\n    margin-right: 0;\n    margin-left: 0;\n}\n\n.gutters-0 > .col,\n.gutters-0 > [class*='col-'] {\n    padding-right: 0;\n    padding-left: 0;\n}\n\n.gutters-0 .card {\n    margin-bottom: 0;\n}\n\n.gutters-xs {\n    margin-right: -0.25rem;\n    margin-left: -0.25rem;\n}\n\n.gutters-xs > .col,\n.gutters-xs > [class*='col-'] {\n    padding-right: 0.25rem;\n    padding-left: 0.25rem;\n}\n\n.gutters-xs .card {\n    margin-bottom: 0.5rem;\n}\n\n.gutters-sm {\n    margin-right: -0.5rem;\n    margin-left: -0.5rem;\n}\n\n.gutters-sm > .col,\n.gutters-sm > [class*='col-'] {\n    padding-right: 0.5rem;\n    padding-left: 0.5rem;\n}\n\n.gutters-sm .card {\n    margin-bottom: 1rem;\n}\n\n.gutters-lg {\n    margin-right: -1rem;\n    margin-left: -1rem;\n}\n\n.gutters-lg > .col,\n.gutters-lg > [class*='col-'] {\n    padding-right: 1rem;\n    padding-left: 1rem;\n}\n\n.gutters-lg .card {\n    margin-bottom: 2rem;\n}\n\n.gutters-xl {\n    margin-right: -1.5rem;\n    margin-left: -1.5rem;\n}\n\n.gutters-xl > .col,\n.gutters-xl > [class*='col-'] {\n    padding-right: 1.5rem;\n    padding-left: 1.5rem;\n}\n\n.gutters-xl .card {\n    margin-bottom: 3rem;\n}\n\n.page {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    -ms-flex-pack: center;\n    justify-content: center;\n    min-height: 100%;\n}\n\nbody.fixed-header .page {\n    padding-top: 4.5rem;\n}\n\n@media (min-width: 1600px) {\n    body.aside-opened .page {\n        margin-right: 22rem;\n    }\n}\n\n.page-main {\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n}\n\n.page-content {\n    margin: 0.75rem 0;\n}\n\n@media (min-width: 768px) {\n    .page-content {\n        margin: 1.5rem 0;\n    }\n}\n\n.page-header {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    flex-direction: column;\n    margin: 0 0 1.5rem;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n}\n\n.page-title {\n    margin: 0;\n    font-size: 1.5rem;\n    font-weight: 400;\n    line-height: 2.5rem;\n}\n\n.page-title__protection {\n    display: flex;\n    align-items: stretch;\n    margin: 0 0.5rem;\n}\n\n@media (max-width: 767.98px) {\n    .page-title__protection {\n        margin: 0.5rem 0;\n    }\n}\n\n.page-title-icon {\n    color: #9aa0ac;\n    font-size: 1.25rem;\n}\n\n.page-subtitle {\n    font-size: 0.8125rem;\n    color: #6e7687;\n    margin-left: 2rem;\n}\n\n.page-subtitle a {\n    color: inherit;\n}\n\n.page-options {\n    margin-left: auto;\n}\n\n.page-breadcrumb {\n    -ms-flex-preferred-size: 100%;\n    flex-basis: 100%;\n}\n\n.page-description {\n    margin: 0.25rem 0 0;\n    color: #6e7687;\n}\n\n.page-description a {\n    color: inherit;\n}\n\n.page-single {\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: center;\n    justify-content: center;\n    padding: 1rem 0;\n}\n\n.content-heading {\n    font-weight: 400;\n    margin: 2rem 0 1.5rem;\n    font-size: 1.25rem;\n    line-height: 1.25;\n}\n\n.content-heading:first-child {\n    margin-top: 0;\n}\n\n.aside {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    width: 22rem;\n    background: #ffffff;\n    border-left: 1px solid rgba(0, 40, 100, 0.12);\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    z-index: 100;\n    visibility: hidden;\n    box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.05);\n}\n\n@media (min-width: 1600px) {\n    body.aside-opened .aside {\n        visibility: visible;\n    }\n}\n\n.aside-body {\n    padding: 1.5rem;\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n    overflow: auto;\n}\n\n.aside-footer {\n    padding: 1rem 1.5rem;\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.aside-header {\n    padding: 1rem 1.5rem;\n    border-bottom: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.header {\n    padding-top: 0.75rem;\n    padding-bottom: 0.75rem;\n    background: var(--header-bgcolor);\n    border-bottom: 1px solid var(--border-color);\n}\n\nbody.fixed-header .header {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    z-index: 1030;\n}\n\n@media print {\n    .header {\n        display: none;\n    }\n}\n\n.header .dropdown-menu {\n    margin-top: 0.75rem;\n}\n\n.nav-unread {\n    position: absolute;\n    top: 0.25rem;\n    right: 0.25rem;\n    background: #cd201f;\n    width: 0.5rem;\n    height: 0.5rem;\n    border-radius: 50%;\n}\n\n.header-brand {\n    color: inherit;\n    margin-right: 1rem;\n    font-size: 1.25rem;\n    white-space: nowrap;\n    font-weight: 600;\n    padding: 0;\n    transition: 0.3s opacity;\n    line-height: 2rem;\n}\n\n.header-brand:hover {\n    opacity: 0.8;\n    color: inherit;\n    text-decoration: none;\n}\n\n.header-brand-img {\n    height: 2rem;\n    line-height: 2rem;\n    vertical-align: bottom;\n    margin-right: 0.5rem;\n    width: auto;\n}\n\n[data-theme='dark'] .header-brand-img {\n    filter: invert(1);\n}\n\n.header-avatar {\n    width: 2rem;\n    height: 2rem;\n    display: inline-block;\n    vertical-align: bottom;\n    border-radius: 50%;\n}\n\n.header-btn {\n    display: inline-block;\n    width: 2rem;\n    height: 2rem;\n    line-height: 2rem;\n    text-align: center;\n    font-size: 1rem;\n}\n\n.header-btn.has-new {\n    position: relative;\n}\n\n.header-btn.has-new:before {\n    content: '';\n    width: 6px;\n    height: 6px;\n    background: #cd201f;\n    position: absolute;\n    top: 4px;\n    right: 4px;\n    border-radius: 50%;\n}\n\n.header-toggler {\n    width: 2rem;\n    height: 2rem;\n    position: relative;\n    color: #9aa0ac;\n}\n\n.header-toggler:hover {\n    color: #6e7687;\n}\n\n.header-toggler-icon {\n    position: absolute;\n    width: 1rem;\n    height: 2px;\n    color: inherit;\n    background: currentColor;\n    border-radius: 3px;\n    top: 50%;\n    left: 50%;\n    margin: -2px 0 0 -0.5rem;\n    box-shadow:\n        0 5px currentColor,\n        0 -5px currentColor;\n}\n\n.footer {\n    background: var(--card-bgcolor);\n    border-top: 1px solid var(--card-border-color);\n    font-size: 0.875rem;\n    padding: 1.25rem 0;\n    color: #9aa0ac;\n    position: relative;\n    z-index: 102;\n}\n\n.footer a:not(.btn) {\n    color: #6e7687;\n}\n\n@media print {\n    .footer {\n        display: none;\n    }\n}\n\n.bg-blue-lightest {\n    background-color: #edf2fa !important;\n}\n\na.bg-blue-lightest:hover,\na.bg-blue-lightest:focus,\nbutton.bg-blue-lightest:hover,\nbutton.bg-blue-lightest:focus {\n    background-color: #c5d5ef !important;\n}\n\n.bg-blue-lighter {\n    background-color: #c8d9f1 !important;\n}\n\na.bg-blue-lighter:hover,\na.bg-blue-lighter:focus,\nbutton.bg-blue-lighter:hover,\nbutton.bg-blue-lighter:focus {\n    background-color: #9fbde7 !important;\n}\n\n.bg-blue-light {\n    background-color: #7ea5dd !important;\n}\n\na.bg-blue-light:hover,\na.bg-blue-light:focus,\nbutton.bg-blue-light:hover,\nbutton.bg-blue-light:focus {\n    background-color: #5689d2 !important;\n}\n\n.bg-blue-dark {\n    background-color: #3866a6 !important;\n}\n\na.bg-blue-dark:hover,\na.bg-blue-dark:focus,\nbutton.bg-blue-dark:hover,\nbutton.bg-blue-dark:focus {\n    background-color: #2b4f80 !important;\n}\n\n.bg-blue-darker {\n    background-color: #1c3353 !important;\n}\n\na.bg-blue-darker:hover,\na.bg-blue-darker:focus,\nbutton.bg-blue-darker:hover,\nbutton.bg-blue-darker:focus {\n    background-color: #0f1c2d !important;\n}\n\n.bg-blue-darkest {\n    background-color: #0e1929 !important;\n}\n\na.bg-blue-darkest:hover,\na.bg-blue-darkest:focus,\nbutton.bg-blue-darkest:hover,\nbutton.bg-blue-darkest:focus {\n    background-color: #010203 !important;\n}\n\n.bg-indigo-lightest {\n    background-color: #f0f1fa !important;\n}\n\na.bg-indigo-lightest:hover,\na.bg-indigo-lightest:focus,\nbutton.bg-indigo-lightest:hover,\nbutton.bg-indigo-lightest:focus {\n    background-color: #cacded !important;\n}\n\n.bg-indigo-lighter {\n    background-color: #d1d5f0 !important;\n}\n\na.bg-indigo-lighter:hover,\na.bg-indigo-lighter:focus,\nbutton.bg-indigo-lighter:hover,\nbutton.bg-indigo-lighter:focus {\n    background-color: #abb2e3 !important;\n}\n\n.bg-indigo-light {\n    background-color: #939edc !important;\n}\n\na.bg-indigo-light:hover,\na.bg-indigo-light:focus,\nbutton.bg-indigo-light:hover,\nbutton.bg-indigo-light:focus {\n    background-color: #6c7bd0 !important;\n}\n\n.bg-indigo-dark {\n    background-color: #515da4 !important;\n}\n\na.bg-indigo-dark:hover,\na.bg-indigo-dark:focus,\nbutton.bg-indigo-dark:hover,\nbutton.bg-indigo-dark:focus {\n    background-color: #404a82 !important;\n}\n\n.bg-indigo-darker {\n    background-color: #282e52 !important;\n}\n\na.bg-indigo-darker:hover,\na.bg-indigo-darker:focus,\nbutton.bg-indigo-darker:hover,\nbutton.bg-indigo-darker:focus {\n    background-color: #171b30 !important;\n}\n\n.bg-indigo-darkest {\n    background-color: #141729 !important;\n}\n\na.bg-indigo-darkest:hover,\na.bg-indigo-darkest:focus,\nbutton.bg-indigo-darkest:hover,\nbutton.bg-indigo-darkest:focus {\n    background-color: #030407 !important;\n}\n\n.bg-purple-lightest {\n    background-color: #f6effd !important;\n}\n\na.bg-purple-lightest:hover,\na.bg-purple-lightest:focus,\nbutton.bg-purple-lightest:hover,\nbutton.bg-purple-lightest:focus {\n    background-color: #ddc2f7 !important;\n}\n\n.bg-purple-lighter {\n    background-color: #e4cff9 !important;\n}\n\na.bg-purple-lighter:hover,\na.bg-purple-lighter:focus,\nbutton.bg-purple-lighter:hover,\nbutton.bg-purple-lighter:focus {\n    background-color: #cba2f3 !important;\n}\n\n.bg-purple-light {\n    background-color: #c08ef0 !important;\n}\n\na.bg-purple-light:hover,\na.bg-purple-light:focus,\nbutton.bg-purple-light:hover,\nbutton.bg-purple-light:focus {\n    background-color: #a761ea !important;\n}\n\n.bg-purple-dark {\n    background-color: #844bbb !important;\n}\n\na.bg-purple-dark:hover,\na.bg-purple-dark:focus,\nbutton.bg-purple-dark:hover,\nbutton.bg-purple-dark:focus {\n    background-color: #6a3a99 !important;\n}\n\n.bg-purple-darker {\n    background-color: #42265e !important;\n}\n\na.bg-purple-darker:hover,\na.bg-purple-darker:focus,\nbutton.bg-purple-darker:hover,\nbutton.bg-purple-darker:focus {\n    background-color: #29173a !important;\n}\n\n.bg-purple-darkest {\n    background-color: #21132f !important;\n}\n\na.bg-purple-darkest:hover,\na.bg-purple-darkest:focus,\nbutton.bg-purple-darkest:hover,\nbutton.bg-purple-darkest:focus {\n    background-color: #08040b !important;\n}\n\n.bg-pink-lightest {\n    background-color: #fef0f5 !important;\n}\n\na.bg-pink-lightest:hover,\na.bg-pink-lightest:focus,\nbutton.bg-pink-lightest:hover,\nbutton.bg-pink-lightest:focus {\n    background-color: #fbc0d5 !important;\n}\n\n.bg-pink-lighter {\n    background-color: #fcd3e1 !important;\n}\n\na.bg-pink-lighter:hover,\na.bg-pink-lighter:focus,\nbutton.bg-pink-lighter:hover,\nbutton.bg-pink-lighter:focus {\n    background-color: #f9a3c0 !important;\n}\n\n.bg-pink-light {\n    background-color: #f999b9 !important;\n}\n\na.bg-pink-light:hover,\na.bg-pink-light:focus,\nbutton.bg-pink-light:hover,\nbutton.bg-pink-light:focus {\n    background-color: #f66998 !important;\n}\n\n.bg-pink-dark {\n    background-color: #c5577c !important;\n}\n\na.bg-pink-dark:hover,\na.bg-pink-dark:focus,\nbutton.bg-pink-dark:hover,\nbutton.bg-pink-dark:focus {\n    background-color: #ad3c62 !important;\n}\n\n.bg-pink-darker {\n    background-color: #622c3e !important;\n}\n\na.bg-pink-darker:hover,\na.bg-pink-darker:focus,\nbutton.bg-pink-darker:hover,\nbutton.bg-pink-darker:focus {\n    background-color: #3f1c28 !important;\n}\n\n.bg-pink-darkest {\n    background-color: #31161f !important;\n}\n\na.bg-pink-darkest:hover,\na.bg-pink-darkest:focus,\nbutton.bg-pink-darkest:hover,\nbutton.bg-pink-darkest:focus {\n    background-color: #0e0609 !important;\n}\n\n.bg-red-lightest {\n    background-color: #fae9e9 !important;\n}\n\na.bg-red-lightest:hover,\na.bg-red-lightest:focus,\nbutton.bg-red-lightest:hover,\nbutton.bg-red-lightest:focus {\n    background-color: #f1bfbf !important;\n}\n\n.bg-red-lighter {\n    background-color: #f0bcbc !important;\n}\n\na.bg-red-lighter:hover,\na.bg-red-lighter:focus,\nbutton.bg-red-lighter:hover,\nbutton.bg-red-lighter:focus {\n    background-color: #e79292 !important;\n}\n\n.bg-red-light {\n    background-color: #dc6362 !important;\n}\n\na.bg-red-light:hover,\na.bg-red-light:focus,\nbutton.bg-red-light:hover,\nbutton.bg-red-light:focus {\n    background-color: #d33a38 !important;\n}\n\n.bg-red-dark {\n    background-color: #a41a19 !important;\n}\n\na.bg-red-dark:hover,\na.bg-red-dark:focus,\nbutton.bg-red-dark:hover,\nbutton.bg-red-dark:focus {\n    background-color: #781312 !important;\n}\n\n.bg-red-darker {\n    background-color: #520d0c !important;\n}\n\na.bg-red-darker:hover,\na.bg-red-darker:focus,\nbutton.bg-red-darker:hover,\nbutton.bg-red-darker:focus {\n    background-color: #260605 !important;\n}\n\n.bg-red-darkest {\n    background-color: #290606 !important;\n}\n\na.bg-red-darkest:hover,\na.bg-red-darkest:focus,\nbutton.bg-red-darkest:hover,\nbutton.bg-red-darkest:focus {\n    background-color: black !important;\n}\n\n.bg-orange-lightest {\n    background-color: #fff5ec !important;\n}\n\na.bg-orange-lightest:hover,\na.bg-orange-lightest:focus,\nbutton.bg-orange-lightest:hover,\nbutton.bg-orange-lightest:focus {\n    background-color: peachpuff !important;\n}\n\n.bg-orange-lighter {\n    background-color: #fee0c7 !important;\n}\n\na.bg-orange-lighter:hover,\na.bg-orange-lighter:focus,\nbutton.bg-orange-lighter:hover,\nbutton.bg-orange-lighter:focus {\n    background-color: #fdc495 !important;\n}\n\n.bg-orange-light {\n    background-color: #feb67c !important;\n}\n\na.bg-orange-light:hover,\na.bg-orange-light:focus,\nbutton.bg-orange-light:hover,\nbutton.bg-orange-light:focus {\n    background-color: #fe9a49 !important;\n}\n\n.bg-orange-dark {\n    background-color: #ca7836 !important;\n}\n\na.bg-orange-dark:hover,\na.bg-orange-dark:focus,\nbutton.bg-orange-dark:hover,\nbutton.bg-orange-dark:focus {\n    background-color: #a2602b !important;\n}\n\n.bg-orange-darker {\n    background-color: #653c1b !important;\n}\n\na.bg-orange-darker:hover,\na.bg-orange-darker:focus,\nbutton.bg-orange-darker:hover,\nbutton.bg-orange-darker:focus {\n    background-color: #3d2410 !important;\n}\n\n.bg-orange-darkest {\n    background-color: #331e0e !important;\n}\n\na.bg-orange-darkest:hover,\na.bg-orange-darkest:focus,\nbutton.bg-orange-darkest:hover,\nbutton.bg-orange-darkest:focus {\n    background-color: #0b0603 !important;\n}\n\n.bg-yellow-lightest {\n    background-color: #fef9e7 !important;\n}\n\na.bg-yellow-lightest:hover,\na.bg-yellow-lightest:focus,\nbutton.bg-yellow-lightest:hover,\nbutton.bg-yellow-lightest:focus {\n    background-color: #fcedb6 !important;\n}\n\n.bg-yellow-lighter {\n    background-color: #fbedb7 !important;\n}\n\na.bg-yellow-lighter:hover,\na.bg-yellow-lighter:focus,\nbutton.bg-yellow-lighter:hover,\nbutton.bg-yellow-lighter:focus {\n    background-color: #f8e187 !important;\n}\n\n.bg-yellow-light {\n    background-color: #f5d657 !important;\n}\n\na.bg-yellow-light:hover,\na.bg-yellow-light:focus,\nbutton.bg-yellow-light:hover,\nbutton.bg-yellow-light:focus {\n    background-color: #f2ca27 !important;\n}\n\n.bg-yellow-dark {\n    background-color: #c19d0c !important;\n}\n\na.bg-yellow-dark:hover,\na.bg-yellow-dark:focus,\nbutton.bg-yellow-dark:hover,\nbutton.bg-yellow-dark:focus {\n    background-color: #917609 !important;\n}\n\n.bg-yellow-darker {\n    background-color: #604e06 !important;\n}\n\na.bg-yellow-darker:hover,\na.bg-yellow-darker:focus,\nbutton.bg-yellow-darker:hover,\nbutton.bg-yellow-darker:focus {\n    background-color: #302703 !important;\n}\n\n.bg-yellow-darkest {\n    background-color: #302703 !important;\n}\n\na.bg-yellow-darkest:hover,\na.bg-yellow-darkest:focus,\nbutton.bg-yellow-darkest:hover,\nbutton.bg-yellow-darkest:focus {\n    background-color: black !important;\n}\n\n.bg-green-lightest {\n    background-color: #eff8e6 !important;\n}\n\na.bg-green-lightest:hover,\na.bg-green-lightest:focus,\nbutton.bg-green-lightest:hover,\nbutton.bg-green-lightest:focus {\n    background-color: #d6edbe !important;\n}\n\n.bg-green-lighter {\n    background-color: #cfeab3 !important;\n}\n\na.bg-green-lighter:hover,\na.bg-green-lighter:focus,\nbutton.bg-green-lighter:hover,\nbutton.bg-green-lighter:focus {\n    background-color: #b6df8b !important;\n}\n\n.bg-green-light {\n    background-color: #8ecf4d !important;\n}\n\na.bg-green-light:hover,\na.bg-green-light:focus,\nbutton.bg-green-light:hover,\nbutton.bg-green-light:focus {\n    background-color: #75b831 !important;\n}\n\n.bg-green-dark {\n    background-color: #4b9500 !important;\n}\n\na.bg-green-dark:hover,\na.bg-green-dark:focus,\nbutton.bg-green-dark:hover,\nbutton.bg-green-dark:focus {\n    background-color: #316200 !important;\n}\n\n.bg-green-darker {\n    background-color: #264a00 !important;\n}\n\na.bg-green-darker:hover,\na.bg-green-darker:focus,\nbutton.bg-green-darker:hover,\nbutton.bg-green-darker:focus {\n    background-color: #0c1700 !important;\n}\n\n.bg-green-darkest {\n    background-color: #132500 !important;\n}\n\na.bg-green-darkest:hover,\na.bg-green-darkest:focus,\nbutton.bg-green-darkest:hover,\nbutton.bg-green-darkest:focus {\n    background-color: black !important;\n}\n\n.bg-teal-lightest {\n    background-color: #eafaf8 !important;\n}\n\na.bg-teal-lightest:hover,\na.bg-teal-lightest:focus,\nbutton.bg-teal-lightest:hover,\nbutton.bg-teal-lightest:focus {\n    background-color: #c1f0ea !important;\n}\n\n.bg-teal-lighter {\n    background-color: #bfefea !important;\n}\n\na.bg-teal-lighter:hover,\na.bg-teal-lighter:focus,\nbutton.bg-teal-lighter:hover,\nbutton.bg-teal-lighter:focus {\n    background-color: #96e5dd !important;\n}\n\n.bg-teal-light {\n    background-color: #6bdbcf !important;\n}\n\na.bg-teal-light:hover,\na.bg-teal-light:focus,\nbutton.bg-teal-light:hover,\nbutton.bg-teal-light:focus {\n    background-color: #42d1c2 !important;\n}\n\n.bg-teal-dark {\n    background-color: #22a295 !important;\n}\n\na.bg-teal-dark:hover,\na.bg-teal-dark:focus,\nbutton.bg-teal-dark:hover,\nbutton.bg-teal-dark:focus {\n    background-color: #19786e !important;\n}\n\n.bg-teal-darker {\n    background-color: #11514a !important;\n}\n\na.bg-teal-darker:hover,\na.bg-teal-darker:focus,\nbutton.bg-teal-darker:hover,\nbutton.bg-teal-darker:focus {\n    background-color: #082723 !important;\n}\n\n.bg-teal-darkest {\n    background-color: #092925 !important;\n}\n\na.bg-teal-darkest:hover,\na.bg-teal-darkest:focus,\nbutton.bg-teal-darkest:hover,\nbutton.bg-teal-darkest:focus {\n    background-color: black !important;\n}\n\n.bg-cyan-lightest {\n    background-color: #e8f6f8 !important;\n}\n\na.bg-cyan-lightest:hover,\na.bg-cyan-lightest:focus,\nbutton.bg-cyan-lightest:hover,\nbutton.bg-cyan-lightest:focus {\n    background-color: #c1e7ec !important;\n}\n\n.bg-cyan-lighter {\n    background-color: #b9e3ea !important;\n}\n\na.bg-cyan-lighter:hover,\na.bg-cyan-lighter:focus,\nbutton.bg-cyan-lighter:hover,\nbutton.bg-cyan-lighter:focus {\n    background-color: #92d3de !important;\n}\n\n.bg-cyan-light {\n    background-color: #5dbecd !important;\n}\n\na.bg-cyan-light:hover,\na.bg-cyan-light:focus,\nbutton.bg-cyan-light:hover,\nbutton.bg-cyan-light:focus {\n    background-color: #3aabbd !important;\n}\n\n.bg-cyan-dark {\n    background-color: #128293 !important;\n}\n\na.bg-cyan-dark:hover,\na.bg-cyan-dark:focus,\nbutton.bg-cyan-dark:hover,\nbutton.bg-cyan-dark:focus {\n    background-color: #0c5a66 !important;\n}\n\n.bg-cyan-darker {\n    background-color: #09414a !important;\n}\n\na.bg-cyan-darker:hover,\na.bg-cyan-darker:focus,\nbutton.bg-cyan-darker:hover,\nbutton.bg-cyan-darker:focus {\n    background-color: #03191d !important;\n}\n\n.bg-cyan-darkest {\n    background-color: #052025 !important;\n}\n\na.bg-cyan-darkest:hover,\na.bg-cyan-darkest:focus,\nbutton.bg-cyan-darkest:hover,\nbutton.bg-cyan-darkest:focus {\n    background-color: black !important;\n}\n\n.bg-white-lightest {\n    background-color: white !important;\n}\n\na.bg-white-lightest:hover,\na.bg-white-lightest:focus,\nbutton.bg-white-lightest:hover,\nbutton.bg-white-lightest:focus {\n    background-color: #e6e5e5 !important;\n}\n\n.bg-white-lighter {\n    background-color: white !important;\n}\n\na.bg-white-lighter:hover,\na.bg-white-lighter:focus,\nbutton.bg-white-lighter:hover,\nbutton.bg-white-lighter:focus {\n    background-color: #e6e5e5 !important;\n}\n\n.bg-white-light {\n    background-color: white !important;\n}\n\na.bg-white-light:hover,\na.bg-white-light:focus,\nbutton.bg-white-light:hover,\nbutton.bg-white-light:focus {\n    background-color: #e6e5e5 !important;\n}\n\n.bg-white-dark {\n    background-color: #cccccc !important;\n}\n\na.bg-white-dark:hover,\na.bg-white-dark:focus,\nbutton.bg-white-dark:hover,\nbutton.bg-white-dark:focus {\n    background-color: #b3b2b2 !important;\n}\n\n.bg-white-darker {\n    background-color: #666666 !important;\n}\n\na.bg-white-darker:hover,\na.bg-white-darker:focus,\nbutton.bg-white-darker:hover,\nbutton.bg-white-darker:focus {\n    background-color: #4d4c4c !important;\n}\n\n.bg-white-darkest {\n    background-color: #333333 !important;\n}\n\na.bg-white-darkest:hover,\na.bg-white-darkest:focus,\nbutton.bg-white-darkest:hover,\nbutton.bg-white-darkest:focus {\n    background-color: #1a1919 !important;\n}\n\n.bg-gray-lightest {\n    background-color: #f3f4f5 !important;\n}\n\na.bg-gray-lightest:hover,\na.bg-gray-lightest:focus,\nbutton.bg-gray-lightest:hover,\nbutton.bg-gray-lightest:focus {\n    background-color: #d7dbde !important;\n}\n\n.bg-gray-lighter {\n    background-color: #dbdde0 !important;\n}\n\na.bg-gray-lighter:hover,\na.bg-gray-lighter:focus,\nbutton.bg-gray-lighter:hover,\nbutton.bg-gray-lighter:focus {\n    background-color: #c0c3c8 !important;\n}\n\n.bg-gray-light {\n    background-color: #aab0b6 !important;\n}\n\na.bg-gray-light:hover,\na.bg-gray-light:focus,\nbutton.bg-gray-light:hover,\nbutton.bg-gray-light:focus {\n    background-color: #8f979e !important;\n}\n\n.bg-gray-dark {\n    background-color: #6b7278 !important;\n}\n\na.bg-gray-dark:hover,\na.bg-gray-dark:focus,\nbutton.bg-gray-dark:hover,\nbutton.bg-gray-dark:focus {\n    background-color: #53585d !important;\n}\n\n.bg-gray-darker {\n    background-color: #36393c !important;\n}\n\na.bg-gray-darker:hover,\na.bg-gray-darker:focus,\nbutton.bg-gray-darker:hover,\nbutton.bg-gray-darker:focus {\n    background-color: #1e2021 !important;\n}\n\n.bg-gray-darkest {\n    background-color: #1b1c1e !important;\n}\n\na.bg-gray-darkest:hover,\na.bg-gray-darkest:focus,\nbutton.bg-gray-darkest:hover,\nbutton.bg-gray-darkest:focus {\n    background-color: #030303 !important;\n}\n\n.bg-gray-dark-lightest {\n    background-color: #ebebec !important;\n}\n\na.bg-gray-dark-lightest:hover,\na.bg-gray-dark-lightest:focus,\nbutton.bg-gray-dark-lightest:hover,\nbutton.bg-gray-dark-lightest:focus {\n    background-color: #d1d1d3 !important;\n}\n\n.bg-gray-dark-lighter {\n    background-color: #c2c4c6 !important;\n}\n\na.bg-gray-dark-lighter:hover,\na.bg-gray-dark-lighter:focus,\nbutton.bg-gray-dark-lighter:hover,\nbutton.bg-gray-dark-lighter:focus {\n    background-color: #a8abad !important;\n}\n\n.bg-gray-dark-light {\n    background-color: #717579 !important;\n}\n\na.bg-gray-dark-light:hover,\na.bg-gray-dark-light:focus,\nbutton.bg-gray-dark-light:hover,\nbutton.bg-gray-dark-light:focus {\n    background-color: #585c5f !important;\n}\n\n.bg-gray-dark-dark {\n    background-color: #2a2e33 !important;\n}\n\na.bg-gray-dark-dark:hover,\na.bg-gray-dark-dark:focus,\nbutton.bg-gray-dark-dark:hover,\nbutton.bg-gray-dark-dark:focus {\n    background-color: #131517 !important;\n}\n\n.bg-gray-dark-darker {\n    background-color: #15171a !important;\n}\n\na.bg-gray-dark-darker:hover,\na.bg-gray-dark-darker:focus,\nbutton.bg-gray-dark-darker:hover,\nbutton.bg-gray-dark-darker:focus {\n    background-color: black !important;\n}\n\n.bg-gray-dark-darkest {\n    background-color: #0a0c0d !important;\n}\n\na.bg-gray-dark-darkest:hover,\na.bg-gray-dark-darkest:focus,\nbutton.bg-gray-dark-darkest:hover,\nbutton.bg-gray-dark-darkest:focus {\n    background-color: black !important;\n}\n\n.bg-azure-lightest {\n    background-color: #ecf7fe !important;\n}\n\na.bg-azure-lightest:hover,\na.bg-azure-lightest:focus,\nbutton.bg-azure-lightest:hover,\nbutton.bg-azure-lightest:focus {\n    background-color: #bce3fb !important;\n}\n\n.bg-azure-lighter {\n    background-color: #c7e6fb !important;\n}\n\na.bg-azure-lighter:hover,\na.bg-azure-lighter:focus,\nbutton.bg-azure-lighter:hover,\nbutton.bg-azure-lighter:focus {\n    background-color: #97d1f8 !important;\n}\n\n.bg-azure-light {\n    background-color: #7dc4f6 !important;\n}\n\na.bg-azure-light:hover,\na.bg-azure-light:focus,\nbutton.bg-azure-light:hover,\nbutton.bg-azure-light:focus {\n    background-color: #4daef3 !important;\n}\n\n.bg-azure-dark {\n    background-color: #3788c2 !important;\n}\n\na.bg-azure-dark:hover,\na.bg-azure-dark:focus,\nbutton.bg-azure-dark:hover,\nbutton.bg-azure-dark:focus {\n    background-color: #2c6c9a !important;\n}\n\n.bg-azure-darker {\n    background-color: #1c4461 !important;\n}\n\na.bg-azure-darker:hover,\na.bg-azure-darker:focus,\nbutton.bg-azure-darker:hover,\nbutton.bg-azure-darker:focus {\n    background-color: #112839 !important;\n}\n\n.bg-azure-darkest {\n    background-color: #0e2230 !important;\n}\n\na.bg-azure-darkest:hover,\na.bg-azure-darkest:focus,\nbutton.bg-azure-darkest:hover,\nbutton.bg-azure-darkest:focus {\n    background-color: #020609 !important;\n}\n\n.bg-lime-lightest {\n    background-color: #f2fbeb !important;\n}\n\na.bg-lime-lightest:hover,\na.bg-lime-lightest:focus,\nbutton.bg-lime-lightest:hover,\nbutton.bg-lime-lightest:focus {\n    background-color: #d6f3c1 !important;\n}\n\n.bg-lime-lighter {\n    background-color: #d7f2c2 !important;\n}\n\na.bg-lime-lighter:hover,\na.bg-lime-lighter:focus,\nbutton.bg-lime-lighter:hover,\nbutton.bg-lime-lighter:focus {\n    background-color: #bbe998 !important;\n}\n\n.bg-lime-light {\n    background-color: #a3e072 !important;\n}\n\na.bg-lime-light:hover,\na.bg-lime-light:focus,\nbutton.bg-lime-light:hover,\nbutton.bg-lime-light:focus {\n    background-color: #88d748 !important;\n}\n\n.bg-lime-dark {\n    background-color: #62a82a !important;\n}\n\na.bg-lime-dark:hover,\na.bg-lime-dark:focus,\nbutton.bg-lime-dark:hover,\nbutton.bg-lime-dark:focus {\n    background-color: #4a7f20 !important;\n}\n\n.bg-lime-darker {\n    background-color: #315415 !important;\n}\n\na.bg-lime-darker:hover,\na.bg-lime-darker:focus,\nbutton.bg-lime-darker:hover,\nbutton.bg-lime-darker:focus {\n    background-color: #192b0b !important;\n}\n\n.bg-lime-darkest {\n    background-color: #192a0b !important;\n}\n\na.bg-lime-darkest:hover,\na.bg-lime-darkest:focus,\nbutton.bg-lime-darkest:hover,\nbutton.bg-lime-darkest:focus {\n    background-color: #010200 !important;\n}\n\n.display-1 i,\n.display-2 i,\n.display-3 i,\n.display-4 i {\n    vertical-align: baseline;\n    font-size: 0.815em;\n}\n\n.text-inherit {\n    color: inherit !important;\n}\n\n.text-default {\n    color: #495057 !important;\n}\n\n.text-muted-dark {\n    color: #6e7687 !important;\n}\n\n.tracking-tight {\n    letter-spacing: -0.05em !important;\n}\n\n.tracking-normal {\n    letter-spacing: 0 !important;\n}\n\n.tracking-wide {\n    letter-spacing: 0.05em !important;\n}\n\n.leading-none {\n    line-height: 1 !important;\n}\n\n.leading-tight {\n    line-height: 1.25 !important;\n}\n\n.leading-normal {\n    line-height: 1.5 !important;\n}\n\n.leading-loose {\n    line-height: 2 !important;\n}\n\n.bg-blue {\n    background-color: #467fcf !important;\n}\n\na.bg-blue:hover,\na.bg-blue:focus,\nbutton.bg-blue:hover,\nbutton.bg-blue:focus {\n    background-color: #2f66b3 !important;\n}\n\n.text-blue {\n    color: #467fcf !important;\n}\n\n.bg-indigo {\n    background-color: #6574cd !important;\n}\n\na.bg-indigo:hover,\na.bg-indigo:focus,\nbutton.bg-indigo:hover,\nbutton.bg-indigo:focus {\n    background-color: #3f51c1 !important;\n}\n\n.text-indigo {\n    color: #6574cd !important;\n}\n\n.bg-purple {\n    background-color: #a55eea !important;\n}\n\na.bg-purple:hover,\na.bg-purple:focus,\nbutton.bg-purple:hover,\nbutton.bg-purple:focus {\n    background-color: #8c31e4 !important;\n}\n\n.text-purple {\n    color: #a55eea !important;\n}\n\n.bg-pink {\n    background-color: #f66d9b !important;\n}\n\na.bg-pink:hover,\na.bg-pink:focus,\nbutton.bg-pink:hover,\nbutton.bg-pink:focus {\n    background-color: #f33d7a !important;\n}\n\n.text-pink {\n    color: #f66d9b !important;\n}\n\n.bg-red {\n    background-color: #cd201f !important;\n}\n\na.bg-red:hover,\na.bg-red:focus,\nbutton.bg-red:hover,\nbutton.bg-red:focus {\n    background-color: #a11918 !important;\n}\n\n.text-red {\n    color: #cd201f !important;\n}\n\n.bg-orange {\n    background-color: #fd9644 !important;\n}\n\na.bg-orange:hover,\na.bg-orange:focus,\nbutton.bg-orange:hover,\nbutton.bg-orange:focus {\n    background-color: #fc7a12 !important;\n}\n\n.text-orange {\n    color: #fd9644 !important;\n}\n\n.bg-yellow {\n    background-color: #f1c40f !important;\n}\n\na.bg-yellow:hover,\na.bg-yellow:focus,\nbutton.bg-yellow:hover,\nbutton.bg-yellow:focus {\n    background-color: #c29d0b !important;\n}\n\n.text-yellow {\n    color: #f1c40f !important;\n}\n\n.bg-green {\n    background-color: #5eba00 !important;\n}\n\na.bg-green:hover,\na.bg-green:focus,\nbutton.bg-green:hover,\nbutton.bg-green:focus {\n    background-color: #448700 !important;\n}\n\n.text-green {\n    color: #5eba00 !important;\n}\n\n.bg-teal {\n    background-color: #2bcbba !important;\n}\n\na.bg-teal:hover,\na.bg-teal:focus,\nbutton.bg-teal:hover,\nbutton.bg-teal:focus {\n    background-color: #22a193 !important;\n}\n\n.text-teal {\n    color: #2bcbba !important;\n}\n\n.bg-cyan {\n    background-color: #17a2b8 !important;\n}\n\na.bg-cyan:hover,\na.bg-cyan:focus,\nbutton.bg-cyan:hover,\nbutton.bg-cyan:focus {\n    background-color: #117a8b !important;\n}\n\n.text-cyan {\n    color: #17a2b8 !important;\n}\n\n.bg-white {\n    background-color: #fff !important;\n}\n\na.bg-white:hover,\na.bg-white:focus,\nbutton.bg-white:hover,\nbutton.bg-white:focus {\n    background-color: #e6e5e5 !important;\n}\n\n.text-white {\n    color: #fff !important;\n}\n\n.bg-gray {\n    background-color: #868e96 !important;\n}\n\na.bg-gray:hover,\na.bg-gray:focus,\nbutton.bg-gray:hover,\nbutton.bg-gray:focus {\n    background-color: #6c757d !important;\n}\n\n.text-gray {\n    color: #868e96 !important;\n}\n\n.bg-gray-dark {\n    background-color: #343a40 !important;\n}\n\na.bg-gray-dark:hover,\na.bg-gray-dark:focus,\nbutton.bg-gray-dark:hover,\nbutton.bg-gray-dark:focus {\n    background-color: #1d2124 !important;\n}\n\n.text-gray-dark {\n    color: #343a40 !important;\n}\n\n.bg-azure {\n    background-color: #45aaf2 !important;\n}\n\na.bg-azure:hover,\na.bg-azure:focus,\nbutton.bg-azure:hover,\nbutton.bg-azure:focus {\n    background-color: #1594ef !important;\n}\n\n.text-azure {\n    color: #45aaf2 !important;\n}\n\n.bg-lime {\n    background-color: #7bd235 !important;\n}\n\na.bg-lime:hover,\na.bg-lime:focus,\nbutton.bg-lime:hover,\nbutton.bg-lime:focus {\n    background-color: #63ad27 !important;\n}\n\n.text-lime {\n    color: #7bd235 !important;\n}\n\n.icon {\n    color: #9aa0ac !important;\n}\n\n.icon i {\n    vertical-align: -1px;\n}\n\na.icon {\n    text-decoration: none;\n    cursor: pointer;\n}\n\na.icon:hover {\n    color: #495057 !important;\n}\n\n.o-auto {\n    overflow: auto !important;\n}\n\n.o-hidden {\n    overflow: hidden !important;\n}\n\n.shadow {\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;\n}\n\n.shadow-none {\n    box-shadow: none !important;\n}\n\n.nav-link,\n.nav-item {\n    padding: 0 0.75rem;\n    min-width: 2rem;\n    transition: 0.3s color;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    cursor: pointer;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n}\n\n.nav-link .badge,\n.nav-item .badge {\n    position: absolute;\n    top: 0;\n    right: 0;\n    padding: 0.2rem 0.25rem;\n    min-width: 1rem;\n}\n\n.nav-tabs {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    color: #9aa0ac;\n    margin: 0 -0.75rem;\n}\n\n.nav-tabs .nav-link {\n    border: 0;\n    color: inherit;\n    border-bottom: 1px solid transparent;\n    margin-bottom: -1px;\n    transition: 0.3s border-color;\n    font-weight: 400;\n    padding: 1rem 0;\n}\n\n.nav-tabs .nav-link:hover:not(.disabled) {\n    border-color: #6e7687;\n    color: #6e7687;\n}\n\n.nav-tabs .nav-link.active {\n    border-color: #467fcf;\n    color: #467fcf;\n    background: transparent;\n}\n\n.nav-tabs .nav-link.disabled {\n    opacity: 0.4;\n    cursor: default;\n    pointer-events: none;\n}\n\n.nav-tabs .nav-item {\n    margin-bottom: 0;\n    position: relative;\n}\n\n.nav-tabs .nav-item i {\n    margin-right: 0.25rem;\n    line-height: 1;\n    font-size: 0.875rem;\n    width: 0.875rem;\n    vertical-align: baseline;\n    display: inline-block;\n}\n\n.nav-tabs .nav-item:hover .nav-submenu {\n    display: block;\n}\n\n.nav-tabs .nav-submenu {\n    display: none;\n    position: absolute;\n    background: #fff;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    border-top: none;\n    z-index: 10;\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n    min-width: 10rem;\n    border-radius: 0 0 3px 3px;\n}\n\n.nav-tabs .nav-submenu .nav-item {\n    display: block;\n    padding: 0.5rem 1rem;\n    color: #9aa0ac;\n    margin: 0 !important;\n    cursor: pointer;\n    transition: 0.3s background;\n}\n\n.nav-tabs .nav-submenu .nav-item.active {\n    color: #467fcf;\n}\n\n.nav-tabs .nav-submenu .nav-item:hover {\n    color: #6e7687;\n    text-decoration: none;\n    background: rgba(0, 0, 0, 0.024);\n}\n\n.btn {\n    cursor: pointer;\n    font-weight: 600;\n    letter-spacing: 0.03em;\n    font-size: 0.8125rem;\n    min-width: 2.375rem;\n}\n\n.btn i {\n    font-size: 1rem;\n    vertical-align: -2px;\n}\n\n.btn-icon {\n    padding-left: 0.5rem;\n    padding-right: 0.5rem;\n    text-align: center;\n}\n\n.btn-secondary {\n    color: #495057;\n    background-color: #fff;\n    border-color: rgba(0, 40, 100, 0.12);\n    box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.05);\n}\n\n.btn-secondary:hover {\n    color: #495057;\n    background-color: #f6f6f6;\n    border-color: rgba(0, 20, 49, 0.12);\n}\n\n.btn-secondary:focus,\n.btn-secondary.focus {\n    box-shadow: 0 0 0 2px rgba(0, 40, 100, 0.5);\n}\n\n.btn-secondary.disabled,\n.btn-secondary:disabled {\n    color: #495057;\n    background-color: #fff;\n    border-color: rgba(0, 40, 100, 0.12);\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active,\n.btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n    color: #495057;\n    background-color: #e6e5e5;\n    border-color: rgba(0, 15, 36, 0.12);\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus,\n.btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(0, 40, 100, 0.5);\n}\n\n.btn-pill {\n    border-radius: 10rem;\n    padding-left: 1.5em;\n    padding-right: 1.5em;\n}\n\n.btn-square {\n    border-radius: 0;\n}\n\n.btn-facebook {\n    color: #fff;\n    background-color: #3b5998;\n    border-color: #3b5998;\n}\n\n.btn-facebook:hover {\n    color: #fff;\n    background-color: #30497c;\n    border-color: #2d4373;\n}\n\n.btn-facebook:focus,\n.btn-facebook.focus {\n    box-shadow: 0 0 0 2px rgba(59, 89, 152, 0.5);\n}\n\n.btn-facebook.disabled,\n.btn-facebook:disabled {\n    color: #fff;\n    background-color: #3b5998;\n    border-color: #3b5998;\n}\n\n.btn-facebook:not(:disabled):not(.disabled):active,\n.btn-facebook:not(:disabled):not(.disabled).active,\n.show > .btn-facebook.dropdown-toggle {\n    color: #fff;\n    background-color: #2d4373;\n    border-color: #293e6a;\n}\n\n.btn-facebook:not(:disabled):not(.disabled):active:focus,\n.btn-facebook:not(:disabled):not(.disabled).active:focus,\n.show > .btn-facebook.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(59, 89, 152, 0.5);\n}\n\n.btn-twitter {\n    color: #fff;\n    background-color: #1da1f2;\n    border-color: #1da1f2;\n}\n\n.btn-twitter:hover {\n    color: #fff;\n    background-color: #0d8ddc;\n    border-color: #0c85d0;\n}\n\n.btn-twitter:focus,\n.btn-twitter.focus {\n    box-shadow: 0 0 0 2px rgba(29, 161, 242, 0.5);\n}\n\n.btn-twitter.disabled,\n.btn-twitter:disabled {\n    color: #fff;\n    background-color: #1da1f2;\n    border-color: #1da1f2;\n}\n\n.btn-twitter:not(:disabled):not(.disabled):active,\n.btn-twitter:not(:disabled):not(.disabled).active,\n.show > .btn-twitter.dropdown-toggle {\n    color: #fff;\n    background-color: #0c85d0;\n    border-color: #0b7ec4;\n}\n\n.btn-twitter:not(:disabled):not(.disabled):active:focus,\n.btn-twitter:not(:disabled):not(.disabled).active:focus,\n.show > .btn-twitter.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(29, 161, 242, 0.5);\n}\n\n.btn-google {\n    color: #fff;\n    background-color: #dc4e41;\n    border-color: #dc4e41;\n}\n\n.btn-google:hover {\n    color: #fff;\n    background-color: #d03526;\n    border-color: #c63224;\n}\n\n.btn-google:focus,\n.btn-google.focus {\n    box-shadow: 0 0 0 2px rgba(220, 78, 65, 0.5);\n}\n\n.btn-google.disabled,\n.btn-google:disabled {\n    color: #fff;\n    background-color: #dc4e41;\n    border-color: #dc4e41;\n}\n\n.btn-google:not(:disabled):not(.disabled):active,\n.btn-google:not(:disabled):not(.disabled).active,\n.show > .btn-google.dropdown-toggle {\n    color: #fff;\n    background-color: #c63224;\n    border-color: #bb2f22;\n}\n\n.btn-google:not(:disabled):not(.disabled):active:focus,\n.btn-google:not(:disabled):not(.disabled).active:focus,\n.show > .btn-google.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(220, 78, 65, 0.5);\n}\n\n.btn-youtube {\n    color: #fff;\n    background-color: #f00;\n    border-color: #f00;\n}\n\n.btn-youtube:hover {\n    color: #fff;\n    background-color: #d90000;\n    border-color: #cc0000;\n}\n\n.btn-youtube:focus,\n.btn-youtube.focus {\n    box-shadow: 0 0 0 2px rgba(255, 0, 0, 0.5);\n}\n\n.btn-youtube.disabled,\n.btn-youtube:disabled {\n    color: #fff;\n    background-color: #f00;\n    border-color: #f00;\n}\n\n.btn-youtube:not(:disabled):not(.disabled):active,\n.btn-youtube:not(:disabled):not(.disabled).active,\n.show > .btn-youtube.dropdown-toggle {\n    color: #fff;\n    background-color: #cc0000;\n    border-color: #bf0000;\n}\n\n.btn-youtube:not(:disabled):not(.disabled):active:focus,\n.btn-youtube:not(:disabled):not(.disabled).active:focus,\n.show > .btn-youtube.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(255, 0, 0, 0.5);\n}\n\n.btn-vimeo {\n    color: #fff;\n    background-color: #1ab7ea;\n    border-color: #1ab7ea;\n}\n\n.btn-vimeo:hover {\n    color: #fff;\n    background-color: #139ecb;\n    border-color: #1295bf;\n}\n\n.btn-vimeo:focus,\n.btn-vimeo.focus {\n    box-shadow: 0 0 0 2px rgba(26, 183, 234, 0.5);\n}\n\n.btn-vimeo.disabled,\n.btn-vimeo:disabled {\n    color: #fff;\n    background-color: #1ab7ea;\n    border-color: #1ab7ea;\n}\n\n.btn-vimeo:not(:disabled):not(.disabled):active,\n.btn-vimeo:not(:disabled):not(.disabled).active,\n.show > .btn-vimeo.dropdown-toggle {\n    color: #fff;\n    background-color: #1295bf;\n    border-color: #108cb4;\n}\n\n.btn-vimeo:not(:disabled):not(.disabled):active:focus,\n.btn-vimeo:not(:disabled):not(.disabled).active:focus,\n.show > .btn-vimeo.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(26, 183, 234, 0.5);\n}\n\n.btn-dribbble {\n    color: #fff;\n    background-color: #ea4c89;\n    border-color: #ea4c89;\n}\n\n.btn-dribbble:hover {\n    color: #fff;\n    background-color: #e62a72;\n    border-color: #e51e6b;\n}\n\n.btn-dribbble:focus,\n.btn-dribbble.focus {\n    box-shadow: 0 0 0 2px rgba(234, 76, 137, 0.5);\n}\n\n.btn-dribbble.disabled,\n.btn-dribbble:disabled {\n    color: #fff;\n    background-color: #ea4c89;\n    border-color: #ea4c89;\n}\n\n.btn-dribbble:not(:disabled):not(.disabled):active,\n.btn-dribbble:not(:disabled):not(.disabled).active,\n.show > .btn-dribbble.dropdown-toggle {\n    color: #fff;\n    background-color: #e51e6b;\n    border-color: #dc1a65;\n}\n\n.btn-dribbble:not(:disabled):not(.disabled):active:focus,\n.btn-dribbble:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dribbble.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(234, 76, 137, 0.5);\n}\n\n.btn-github {\n    color: #fff;\n    background-color: #181717;\n    border-color: #181717;\n}\n\n.btn-github:hover {\n    color: #fff;\n    background-color: #040404;\n    border-color: black;\n}\n\n.btn-github:focus,\n.btn-github.focus {\n    box-shadow: 0 0 0 2px rgba(24, 23, 23, 0.5);\n}\n\n.btn-github.disabled,\n.btn-github:disabled {\n    color: #fff;\n    background-color: #181717;\n    border-color: #181717;\n}\n\n.btn-github:not(:disabled):not(.disabled):active,\n.btn-github:not(:disabled):not(.disabled).active,\n.show > .btn-github.dropdown-toggle {\n    color: #fff;\n    background-color: black;\n    border-color: black;\n}\n\n.btn-github:not(:disabled):not(.disabled):active:focus,\n.btn-github:not(:disabled):not(.disabled).active:focus,\n.show > .btn-github.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(24, 23, 23, 0.5);\n}\n\n.btn-instagram {\n    color: #fff;\n    background-color: #e4405f;\n    border-color: #e4405f;\n}\n\n.btn-instagram:hover {\n    color: #fff;\n    background-color: #de1f44;\n    border-color: #d31e40;\n}\n\n.btn-instagram:focus,\n.btn-instagram.focus {\n    box-shadow: 0 0 0 2px rgba(228, 64, 95, 0.5);\n}\n\n.btn-instagram.disabled,\n.btn-instagram:disabled {\n    color: #fff;\n    background-color: #e4405f;\n    border-color: #e4405f;\n}\n\n.btn-instagram:not(:disabled):not(.disabled):active,\n.btn-instagram:not(:disabled):not(.disabled).active,\n.show > .btn-instagram.dropdown-toggle {\n    color: #fff;\n    background-color: #d31e40;\n    border-color: #c81c3d;\n}\n\n.btn-instagram:not(:disabled):not(.disabled):active:focus,\n.btn-instagram:not(:disabled):not(.disabled).active:focus,\n.show > .btn-instagram.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(228, 64, 95, 0.5);\n}\n\n.btn-pinterest {\n    color: #fff;\n    background-color: #bd081c;\n    border-color: #bd081c;\n}\n\n.btn-pinterest:hover {\n    color: #fff;\n    background-color: #980617;\n    border-color: #8c0615;\n}\n\n.btn-pinterest:focus,\n.btn-pinterest.focus {\n    box-shadow: 0 0 0 2px rgba(189, 8, 28, 0.5);\n}\n\n.btn-pinterest.disabled,\n.btn-pinterest:disabled {\n    color: #fff;\n    background-color: #bd081c;\n    border-color: #bd081c;\n}\n\n.btn-pinterest:not(:disabled):not(.disabled):active,\n.btn-pinterest:not(:disabled):not(.disabled).active,\n.show > .btn-pinterest.dropdown-toggle {\n    color: #fff;\n    background-color: #8c0615;\n    border-color: #800513;\n}\n\n.btn-pinterest:not(:disabled):not(.disabled):active:focus,\n.btn-pinterest:not(:disabled):not(.disabled).active:focus,\n.show > .btn-pinterest.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(189, 8, 28, 0.5);\n}\n\n.btn-vk {\n    color: #fff;\n    background-color: #6383a8;\n    border-color: #6383a8;\n}\n\n.btn-vk:hover {\n    color: #fff;\n    background-color: #527093;\n    border-color: #4d6a8b;\n}\n\n.btn-vk:focus,\n.btn-vk.focus {\n    box-shadow: 0 0 0 2px rgba(99, 131, 168, 0.5);\n}\n\n.btn-vk.disabled,\n.btn-vk:disabled {\n    color: #fff;\n    background-color: #6383a8;\n    border-color: #6383a8;\n}\n\n.btn-vk:not(:disabled):not(.disabled):active,\n.btn-vk:not(:disabled):not(.disabled).active,\n.show > .btn-vk.dropdown-toggle {\n    color: #fff;\n    background-color: #4d6a8b;\n    border-color: #496482;\n}\n\n.btn-vk:not(:disabled):not(.disabled):active:focus,\n.btn-vk:not(:disabled):not(.disabled).active:focus,\n.show > .btn-vk.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(99, 131, 168, 0.5);\n}\n\n.btn-rss {\n    color: #fff;\n    background-color: #ffa500;\n    border-color: #ffa500;\n}\n\n.btn-rss:hover {\n    color: #fff;\n    background-color: #d98c00;\n    border-color: #cc8400;\n}\n\n.btn-rss:focus,\n.btn-rss.focus {\n    box-shadow: 0 0 0 2px rgba(255, 165, 0, 0.5);\n}\n\n.btn-rss.disabled,\n.btn-rss:disabled {\n    color: #fff;\n    background-color: #ffa500;\n    border-color: #ffa500;\n}\n\n.btn-rss:not(:disabled):not(.disabled):active,\n.btn-rss:not(:disabled):not(.disabled).active,\n.show > .btn-rss.dropdown-toggle {\n    color: #fff;\n    background-color: #cc8400;\n    border-color: #bf7c00;\n}\n\n.btn-rss:not(:disabled):not(.disabled):active:focus,\n.btn-rss:not(:disabled):not(.disabled).active:focus,\n.show > .btn-rss.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(255, 165, 0, 0.5);\n}\n\n.btn-flickr {\n    color: #fff;\n    background-color: #0063dc;\n    border-color: #0063dc;\n}\n\n.btn-flickr:hover {\n    color: #fff;\n    background-color: #0052b6;\n    border-color: #004ca9;\n}\n\n.btn-flickr:focus,\n.btn-flickr.focus {\n    box-shadow: 0 0 0 2px rgba(0, 99, 220, 0.5);\n}\n\n.btn-flickr.disabled,\n.btn-flickr:disabled {\n    color: #fff;\n    background-color: #0063dc;\n    border-color: #0063dc;\n}\n\n.btn-flickr:not(:disabled):not(.disabled):active,\n.btn-flickr:not(:disabled):not(.disabled).active,\n.show > .btn-flickr.dropdown-toggle {\n    color: #fff;\n    background-color: #004ca9;\n    border-color: #00469c;\n}\n\n.btn-flickr:not(:disabled):not(.disabled):active:focus,\n.btn-flickr:not(:disabled):not(.disabled).active:focus,\n.show > .btn-flickr.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(0, 99, 220, 0.5);\n}\n\n.btn-bitbucket {\n    color: #fff;\n    background-color: #0052cc;\n    border-color: #0052cc;\n}\n\n.btn-bitbucket:hover {\n    color: #fff;\n    background-color: #0043a6;\n    border-color: #003e99;\n}\n\n.btn-bitbucket:focus,\n.btn-bitbucket.focus {\n    box-shadow: 0 0 0 2px rgba(0, 82, 204, 0.5);\n}\n\n.btn-bitbucket.disabled,\n.btn-bitbucket:disabled {\n    color: #fff;\n    background-color: #0052cc;\n    border-color: #0052cc;\n}\n\n.btn-bitbucket:not(:disabled):not(.disabled):active,\n.btn-bitbucket:not(:disabled):not(.disabled).active,\n.show > .btn-bitbucket.dropdown-toggle {\n    color: #fff;\n    background-color: #003e99;\n    border-color: #00388c;\n}\n\n.btn-bitbucket:not(:disabled):not(.disabled):active:focus,\n.btn-bitbucket:not(:disabled):not(.disabled).active:focus,\n.show > .btn-bitbucket.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(0, 82, 204, 0.5);\n}\n\n.btn-blue {\n    color: #fff;\n    background-color: #467fcf;\n    border-color: #467fcf;\n}\n\n.btn-blue:hover {\n    color: #fff;\n    background-color: #316cbe;\n    border-color: #2f66b3;\n}\n\n.btn-blue:focus,\n.btn-blue.focus {\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.5);\n}\n\n.btn-blue.disabled,\n.btn-blue:disabled {\n    color: #fff;\n    background-color: #467fcf;\n    border-color: #467fcf;\n}\n\n.btn-blue:not(:disabled):not(.disabled):active,\n.btn-blue:not(:disabled):not(.disabled).active,\n.show > .btn-blue.dropdown-toggle {\n    color: #fff;\n    background-color: #2f66b3;\n    border-color: #2c60a9;\n}\n\n.btn-blue:not(:disabled):not(.disabled):active:focus,\n.btn-blue:not(:disabled):not(.disabled).active:focus,\n.show > .btn-blue.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.5);\n}\n\n.btn-indigo {\n    color: #fff;\n    background-color: #6574cd;\n    border-color: #6574cd;\n}\n\n.btn-indigo:hover {\n    color: #fff;\n    background-color: #485ac4;\n    border-color: #3f51c1;\n}\n\n.btn-indigo:focus,\n.btn-indigo.focus {\n    box-shadow: 0 0 0 2px rgba(101, 116, 205, 0.5);\n}\n\n.btn-indigo.disabled,\n.btn-indigo:disabled {\n    color: #fff;\n    background-color: #6574cd;\n    border-color: #6574cd;\n}\n\n.btn-indigo:not(:disabled):not(.disabled):active,\n.btn-indigo:not(:disabled):not(.disabled).active,\n.show > .btn-indigo.dropdown-toggle {\n    color: #fff;\n    background-color: #3f51c1;\n    border-color: #3b4db7;\n}\n\n.btn-indigo:not(:disabled):not(.disabled):active:focus,\n.btn-indigo:not(:disabled):not(.disabled).active:focus,\n.show > .btn-indigo.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(101, 116, 205, 0.5);\n}\n\n.btn-purple {\n    color: #fff;\n    background-color: #a55eea;\n    border-color: #a55eea;\n}\n\n.btn-purple:hover {\n    color: #fff;\n    background-color: #923ce6;\n    border-color: #8c31e4;\n}\n\n.btn-purple:focus,\n.btn-purple.focus {\n    box-shadow: 0 0 0 2px rgba(165, 94, 234, 0.5);\n}\n\n.btn-purple.disabled,\n.btn-purple:disabled {\n    color: #fff;\n    background-color: #a55eea;\n    border-color: #a55eea;\n}\n\n.btn-purple:not(:disabled):not(.disabled):active,\n.btn-purple:not(:disabled):not(.disabled).active,\n.show > .btn-purple.dropdown-toggle {\n    color: #fff;\n    background-color: #8c31e4;\n    border-color: #8526e3;\n}\n\n.btn-purple:not(:disabled):not(.disabled):active:focus,\n.btn-purple:not(:disabled):not(.disabled).active:focus,\n.show > .btn-purple.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(165, 94, 234, 0.5);\n}\n\n.btn-pink {\n    color: #fff;\n    background-color: #f66d9b;\n    border-color: #f66d9b;\n}\n\n.btn-pink:hover {\n    color: #fff;\n    background-color: #f44982;\n    border-color: #f33d7a;\n}\n\n.btn-pink:focus,\n.btn-pink.focus {\n    box-shadow: 0 0 0 2px rgba(246, 109, 155, 0.5);\n}\n\n.btn-pink.disabled,\n.btn-pink:disabled {\n    color: #fff;\n    background-color: #f66d9b;\n    border-color: #f66d9b;\n}\n\n.btn-pink:not(:disabled):not(.disabled):active,\n.btn-pink:not(:disabled):not(.disabled).active,\n.show > .btn-pink.dropdown-toggle {\n    color: #fff;\n    background-color: #f33d7a;\n    border-color: #f23172;\n}\n\n.btn-pink:not(:disabled):not(.disabled):active:focus,\n.btn-pink:not(:disabled):not(.disabled).active:focus,\n.show > .btn-pink.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(246, 109, 155, 0.5);\n}\n\n.btn-red {\n    color: #fff;\n    background-color: #cd201f;\n    border-color: #cd201f;\n}\n\n.btn-red:hover {\n    color: #fff;\n    background-color: #ac1b1a;\n    border-color: #a11918;\n}\n\n.btn-red:focus,\n.btn-red.focus {\n    box-shadow: 0 0 0 2px rgba(205, 32, 31, 0.5);\n}\n\n.btn-red.disabled,\n.btn-red:disabled {\n    color: #fff;\n    background-color: #cd201f;\n    border-color: #cd201f;\n}\n\n.btn-red:not(:disabled):not(.disabled):active,\n.btn-red:not(:disabled):not(.disabled).active,\n.show > .btn-red.dropdown-toggle {\n    color: #fff;\n    background-color: #a11918;\n    border-color: #961717;\n}\n\n.btn-red:not(:disabled):not(.disabled):active:focus,\n.btn-red:not(:disabled):not(.disabled).active:focus,\n.show > .btn-red.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(205, 32, 31, 0.5);\n}\n\n.btn-orange {\n    color: #fff;\n    background-color: #fd9644;\n    border-color: #fd9644;\n}\n\n.btn-orange:hover {\n    color: #fff;\n    background-color: #fd811e;\n    border-color: #fc7a12;\n}\n\n.btn-orange:focus,\n.btn-orange.focus {\n    box-shadow: 0 0 0 2px rgba(253, 150, 68, 0.5);\n}\n\n.btn-orange.disabled,\n.btn-orange:disabled {\n    color: #fff;\n    background-color: #fd9644;\n    border-color: #fd9644;\n}\n\n.btn-orange:not(:disabled):not(.disabled):active,\n.btn-orange:not(:disabled):not(.disabled).active,\n.show > .btn-orange.dropdown-toggle {\n    color: #fff;\n    background-color: #fc7a12;\n    border-color: #fc7305;\n}\n\n.btn-orange:not(:disabled):not(.disabled):active:focus,\n.btn-orange:not(:disabled):not(.disabled).active:focus,\n.show > .btn-orange.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(253, 150, 68, 0.5);\n}\n\n.btn-yellow {\n    color: #fff;\n    background-color: #f1c40f;\n    border-color: #f1c40f;\n}\n\n.btn-yellow:hover {\n    color: #fff;\n    background-color: #cea70c;\n    border-color: #c29d0b;\n}\n\n.btn-yellow:focus,\n.btn-yellow.focus {\n    box-shadow: 0 0 0 2px rgba(241, 196, 15, 0.5);\n}\n\n.btn-yellow.disabled,\n.btn-yellow:disabled {\n    color: #fff;\n    background-color: #f1c40f;\n    border-color: #f1c40f;\n}\n\n.btn-yellow:not(:disabled):not(.disabled):active,\n.btn-yellow:not(:disabled):not(.disabled).active,\n.show > .btn-yellow.dropdown-toggle {\n    color: #fff;\n    background-color: #c29d0b;\n    border-color: #b6940b;\n}\n\n.btn-yellow:not(:disabled):not(.disabled):active:focus,\n.btn-yellow:not(:disabled):not(.disabled).active:focus,\n.show > .btn-yellow.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(241, 196, 15, 0.5);\n}\n\n.btn-green {\n    color: #fff;\n    background-color: #5eba00;\n    border-color: #5eba00;\n}\n\n.btn-green:hover {\n    color: #fff;\n    background-color: #4b9400;\n    border-color: #448700;\n}\n\n.btn-green:focus,\n.btn-green.focus {\n    box-shadow: 0 0 0 2px rgba(94, 186, 0, 0.5);\n}\n\n.btn-green.disabled,\n.btn-green:disabled {\n    color: #fff;\n    background-color: #5eba00;\n    border-color: #5eba00;\n}\n\n.btn-green:not(:disabled):not(.disabled):active,\n.btn-green:not(:disabled):not(.disabled).active,\n.show > .btn-green.dropdown-toggle {\n    color: #fff;\n    background-color: #448700;\n    border-color: #3e7a00;\n}\n\n.btn-green:not(:disabled):not(.disabled):active:focus,\n.btn-green:not(:disabled):not(.disabled).active:focus,\n.show > .btn-green.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(94, 186, 0, 0.5);\n}\n\n.btn-teal {\n    color: #fff;\n    background-color: #2bcbba;\n    border-color: #2bcbba;\n}\n\n.btn-teal:hover {\n    color: #fff;\n    background-color: #24ab9d;\n    border-color: #22a193;\n}\n\n.btn-teal:focus,\n.btn-teal.focus {\n    box-shadow: 0 0 0 2px rgba(43, 203, 186, 0.5);\n}\n\n.btn-teal.disabled,\n.btn-teal:disabled {\n    color: #fff;\n    background-color: #2bcbba;\n    border-color: #2bcbba;\n}\n\n.btn-teal:not(:disabled):not(.disabled):active,\n.btn-teal:not(:disabled):not(.disabled).active,\n.show > .btn-teal.dropdown-toggle {\n    color: #fff;\n    background-color: #22a193;\n    border-color: #20968a;\n}\n\n.btn-teal:not(:disabled):not(.disabled):active:focus,\n.btn-teal:not(:disabled):not(.disabled).active:focus,\n.show > .btn-teal.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(43, 203, 186, 0.5);\n}\n\n.btn-cyan {\n    color: #fff;\n    background-color: #17a2b8;\n    border-color: #17a2b8;\n}\n\n.btn-cyan:hover {\n    color: #fff;\n    background-color: #138496;\n    border-color: #117a8b;\n}\n\n.btn-cyan:focus,\n.btn-cyan.focus {\n    box-shadow: 0 0 0 2px rgba(23, 162, 184, 0.5);\n}\n\n.btn-cyan.disabled,\n.btn-cyan:disabled {\n    color: #fff;\n    background-color: #17a2b8;\n    border-color: #17a2b8;\n}\n\n.btn-cyan:not(:disabled):not(.disabled):active,\n.btn-cyan:not(:disabled):not(.disabled).active,\n.show > .btn-cyan.dropdown-toggle {\n    color: #fff;\n    background-color: #117a8b;\n    border-color: #10707f;\n}\n\n.btn-cyan:not(:disabled):not(.disabled):active:focus,\n.btn-cyan:not(:disabled):not(.disabled).active:focus,\n.show > .btn-cyan.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(23, 162, 184, 0.5);\n}\n\n.btn-white {\n    color: #495057;\n    background-color: #fff;\n    border-color: #fff;\n}\n\n.btn-white:hover {\n    color: #495057;\n    background-color: #ececec;\n    border-color: #e6e5e5;\n}\n\n.btn-white:focus,\n.btn-white.focus {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);\n}\n\n.btn-white.disabled,\n.btn-white:disabled {\n    color: #495057;\n    background-color: #fff;\n    border-color: #fff;\n}\n\n.btn-white:not(:disabled):not(.disabled):active,\n.btn-white:not(:disabled):not(.disabled).active,\n.show > .btn-white.dropdown-toggle {\n    color: #495057;\n    background-color: #e6e5e5;\n    border-color: #dfdfdf;\n}\n\n.btn-white:not(:disabled):not(.disabled):active:focus,\n.btn-white:not(:disabled):not(.disabled).active:focus,\n.show > .btn-white.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);\n}\n\n.btn-gray {\n    color: #fff;\n    background-color: #868e96;\n    border-color: #868e96;\n}\n\n.btn-gray:hover {\n    color: #fff;\n    background-color: #727b84;\n    border-color: #6c757d;\n}\n\n.btn-gray:focus,\n.btn-gray.focus {\n    box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);\n}\n\n.btn-gray.disabled,\n.btn-gray:disabled {\n    color: #fff;\n    background-color: #868e96;\n    border-color: #868e96;\n}\n\n.btn-gray:not(:disabled):not(.disabled):active,\n.btn-gray:not(:disabled):not(.disabled).active,\n.show > .btn-gray.dropdown-toggle {\n    color: #fff;\n    background-color: #6c757d;\n    border-color: #666e76;\n}\n\n.btn-gray:not(:disabled):not(.disabled):active:focus,\n.btn-gray:not(:disabled):not(.disabled).active:focus,\n.show > .btn-gray.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);\n}\n\n.btn-gray-dark {\n    color: #fff;\n    background-color: #343a40;\n    border-color: #343a40;\n}\n\n.btn-gray-dark:hover {\n    color: #fff;\n    background-color: #23272b;\n    border-color: #1d2124;\n}\n\n.btn-gray-dark:focus,\n.btn-gray-dark.focus {\n    box-shadow: 0 0 0 2px rgba(52, 58, 64, 0.5);\n}\n\n.btn-gray-dark.disabled,\n.btn-gray-dark:disabled {\n    color: #fff;\n    background-color: #343a40;\n    border-color: #343a40;\n}\n\n.btn-gray-dark:not(:disabled):not(.disabled):active,\n.btn-gray-dark:not(:disabled):not(.disabled).active,\n.show > .btn-gray-dark.dropdown-toggle {\n    color: #fff;\n    background-color: #1d2124;\n    border-color: #171a1d;\n}\n\n.btn-gray-dark:not(:disabled):not(.disabled):active:focus,\n.btn-gray-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-gray-dark.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(52, 58, 64, 0.5);\n}\n\n.btn-azure {\n    color: #fff;\n    background-color: #45aaf2;\n    border-color: #45aaf2;\n}\n\n.btn-azure:hover {\n    color: #fff;\n    background-color: #219af0;\n    border-color: #1594ef;\n}\n\n.btn-azure:focus,\n.btn-azure.focus {\n    box-shadow: 0 0 0 2px rgba(69, 170, 242, 0.5);\n}\n\n.btn-azure.disabled,\n.btn-azure:disabled {\n    color: #fff;\n    background-color: #45aaf2;\n    border-color: #45aaf2;\n}\n\n.btn-azure:not(:disabled):not(.disabled):active,\n.btn-azure:not(:disabled):not(.disabled).active,\n.show > .btn-azure.dropdown-toggle {\n    color: #fff;\n    background-color: #1594ef;\n    border-color: #108ee7;\n}\n\n.btn-azure:not(:disabled):not(.disabled):active:focus,\n.btn-azure:not(:disabled):not(.disabled).active:focus,\n.show > .btn-azure.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(69, 170, 242, 0.5);\n}\n\n.btn-lime {\n    color: #fff;\n    background-color: #7bd235;\n    border-color: #7bd235;\n}\n\n.btn-lime:hover {\n    color: #fff;\n    background-color: #69b829;\n    border-color: #63ad27;\n}\n\n.btn-lime:focus,\n.btn-lime.focus {\n    box-shadow: 0 0 0 2px rgba(123, 210, 53, 0.5);\n}\n\n.btn-lime.disabled,\n.btn-lime:disabled {\n    color: #fff;\n    background-color: #7bd235;\n    border-color: #7bd235;\n}\n\n.btn-lime:not(:disabled):not(.disabled):active,\n.btn-lime:not(:disabled):not(.disabled).active,\n.show > .btn-lime.dropdown-toggle {\n    color: #fff;\n    background-color: #63ad27;\n    border-color: #5da324;\n}\n\n.btn-lime:not(:disabled):not(.disabled):active:focus,\n.btn-lime:not(:disabled):not(.disabled).active:focus,\n.show > .btn-lime.dropdown-toggle:focus {\n    box-shadow: 0 0 0 2px rgba(123, 210, 53, 0.5);\n}\n\n.btn-option {\n    background: transparent;\n    color: #9aa0ac;\n}\n\n.btn-option:hover {\n    color: #6e7687;\n}\n\n.btn-option:focus {\n    box-shadow: none;\n    color: #6e7687;\n}\n\n.btn-sm,\n.btn-group-sm > .btn {\n    font-size: 0.75rem;\n    min-width: 1.625rem;\n}\n\n.btn-lg,\n.btn-group-lg > .btn {\n    font-size: 1rem;\n    min-width: 2.75rem;\n    font-weight: 400;\n}\n\n.btn-list {\n    margin-bottom: -0.5rem;\n    font-size: 0;\n}\n\n.btn-list > .btn,\n.btn-list > .dropdown {\n    margin-bottom: 0.5rem;\n}\n\n.btn-list > .btn:not(:last-child),\n.btn-list > .dropdown:not(:last-child) {\n    margin-right: 0.5rem;\n}\n\n.btn-loading {\n    color: transparent !important;\n    pointer-events: none;\n    position: relative;\n}\n\n.btn-loading:after {\n    content: '';\n    -webkit-animation: loader 500ms infinite linear;\n    animation: loader 500ms infinite linear;\n    border: 2px solid #fff;\n    border-radius: 50%;\n    border-right-color: transparent !important;\n    border-top-color: transparent !important;\n    display: block;\n    height: 1.4em;\n    width: 1.4em;\n    position: absolute;\n    left: calc(50% - (1.4em / 2));\n    top: calc(50% - (1.4em / 2));\n    -webkit-transform-origin: center;\n    transform-origin: center;\n    position: absolute !important;\n}\n\n.btn-loading.btn-sm:after,\n.btn-group-sm > .btn-loading.btn:after {\n    height: 1em;\n    width: 1em;\n    left: calc(50% - (1em / 2));\n    top: calc(50% - (1em / 2));\n}\n\n.btn-loading.btn-secondary:after {\n    border-color: #495057;\n}\n\n.alert {\n    font-size: 0.9375rem;\n}\n\n.alert-icon {\n    padding-left: 3rem;\n}\n\n.alert-icon > i {\n    color: inherit !important;\n    font-size: 1rem;\n    position: absolute;\n    top: 1rem;\n    left: 1rem;\n}\n\n.alert-avatar {\n    padding-left: 3.75rem;\n}\n\n.alert-avatar .avatar {\n    position: absolute;\n    top: 0.5rem;\n    left: 0.75rem;\n}\n\n.close {\n    font-size: 1rem;\n    line-height: 1.5;\n    transition: 0.3s color;\n}\n\n.close:before {\n    content: '';\n    position: relative;\n    top: 4px;\n    display: inline-block;\n    width: 20px;\n    height: 20px;\n    background-size: 100%;\n    background-repeat: no-repeat;\n    background-image: url(\"data:image/svg+xml;base64,PHN2ZyBzdHJva2U9IiM4NDg2OGMiIGZpbGw9Im5vbmUiIGhlaWdodD0iMjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIgogICAgIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPHBhdGggZD0ibTE4IDYtMTIgMTIiLz4KICAgIDxwYXRoIGQ9Im02IDYgMTIgMTIiLz4KPC9zdmc+Cg==\");\n}\n\n.badge {\n    color: #fff;\n}\n\n.badge-default {\n    background: #e9ecef;\n    color: #868e96;\n}\n\n.table thead th,\n.text-wrap table thead th {\n    border-top: 0;\n    border-bottom-width: 1px;\n    padding-top: 0.5rem;\n    padding-bottom: 0.5rem;\n}\n\n.table th,\n.text-wrap table th {\n    color: #9aa0ac;\n    text-transform: uppercase;\n    font-size: 0.875rem;\n    font-weight: 400;\n}\n\n.table-md th,\n.table-md td {\n    padding: 0.5rem;\n}\n\n.table-vcenter td,\n.table-vcenter th {\n    vertical-align: middle;\n}\n\n.table-center td,\n.table-center th {\n    text-align: center;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n    background: transparent;\n}\n\n.table-striped tbody tr:nth-of-type(even) {\n    background-color: rgba(0, 0, 0, 0.02);\n}\n\n.table-calendar {\n    margin: 0 0 0.75rem;\n}\n\n.table-calendar td,\n.table-calendar th {\n    border: 0;\n    text-align: center;\n    padding: 0 !important;\n    width: 14.28571429%;\n    line-height: 2.5rem;\n}\n\n.table-calendar td {\n    border-top: 0;\n}\n\n.table-calendar-link {\n    line-height: 2rem;\n    min-width: calc(2rem + 2px);\n    display: inline-block;\n    border-radius: 3px;\n    background: #f8f9fa;\n    color: #495057;\n    font-weight: 600;\n    transition:\n        0.3s background,\n        0.3s color;\n    position: relative;\n}\n\n.table-calendar-link:before {\n    content: '';\n    width: 4px;\n    height: 4px;\n    position: absolute;\n    left: 0.25rem;\n    top: 0.25rem;\n    border-radius: 50px;\n    background: #467fcf;\n}\n\n.table-calendar-link:hover {\n    color: #fff;\n    text-decoration: none;\n    background: #467fcf;\n    transition: 0.3s background;\n}\n\n.table-calendar-link:hover:before {\n    background: #fff;\n}\n\n.table-header {\n    cursor: pointer;\n    transition: 0.3s color;\n}\n\n.table-header:hover {\n    color: #495057 !important;\n}\n\n.table-header:after {\n    content: '\\f0dc';\n    font-family: FontAwesome;\n    display: inline-block;\n    margin-left: 0.5rem;\n    font-size: 0.75rem;\n}\n\n.table-header-asc {\n    color: #495057 !important;\n}\n\n.table-header-asc:after {\n    content: '\\f0de';\n}\n\n.table-header-desc {\n    color: #495057 !important;\n}\n\n.table-header-desc:after {\n    content: '\\f0dd';\n}\n\n.page-breadcrumb {\n    background: none;\n    padding: 0;\n    margin: 1rem 0 0;\n    font-size: 0.875rem;\n}\n\n@media (min-width: 768px) {\n    .page-breadcrumb {\n        margin: -0.5rem 0 0;\n    }\n}\n\n.page-breadcrumb .breadcrumb-item {\n    color: #9aa0ac;\n}\n\n.page-breadcrumb .breadcrumb-item.active {\n    color: #6e7687;\n}\n\n.pagination-simple .page-item .page-link {\n    background: none;\n    border: none;\n}\n\n.pagination-simple .page-item.active .page-link {\n    color: #495057;\n    font-weight: 700;\n}\n\n.pagination-pager .page-prev {\n    margin-right: auto;\n}\n\n.pagination-pager .page-next {\n    margin-left: auto;\n}\n\n.page-total-text {\n    margin-right: 1rem;\n    -ms-flex-item-align: center;\n    align-self: center;\n    color: #6e7687;\n}\n\n.card {\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n    position: relative;\n    margin-bottom: 1.5rem;\n    width: 100%;\n}\n\n.card .card {\n    box-shadow: none;\n}\n\n@media print {\n    .card {\n        box-shadow: none;\n        border: none;\n    }\n}\n\n.card-body {\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n    margin: 0;\n    padding: 1.5rem 1.5rem;\n    position: relative;\n}\n\n.card-body + .card-body {\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.card-body > :last-child {\n    margin-bottom: 0;\n}\n\n@media print {\n    .card-body {\n        padding: 0;\n    }\n}\n\n.card-body-scrollable {\n    overflow: auto;\n}\n\n.card-footer,\n.card-bottom {\n    padding: 1rem 1.5rem;\n    background: none;\n}\n\n.card-footer {\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n    color: #6e7687;\n}\n\n.card-header {\n    background: none;\n    padding: 0.5rem 1.5rem;\n    display: -ms-flexbox;\n    display: flex;\n    min-height: 3.5rem;\n    -ms-flex-align: center;\n    align-items: center;\n}\n\n.card-header .card-title {\n    margin-bottom: 0;\n}\n\n.card-header.border-0 + .card-body {\n    padding-top: 0;\n}\n\n@media print {\n    .card-header {\n        display: none;\n    }\n}\n\n.card-img-top {\n    border-top-left-radius: 3px;\n    border-top-right-radius: 3px;\n}\n\n.card-img-overlay {\n    background-color: rgba(0, 0, 0, 0.4);\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n}\n\n.card-title {\n    font-size: 1.125rem;\n    line-height: 1.2;\n    font-weight: 400;\n    margin-bottom: 1.5rem;\n}\n\n.card-title a {\n    color: inherit;\n}\n\n.card-title:only-child {\n    margin-bottom: 0;\n}\n\n.card-title small,\n.card-subtitle {\n    color: #9aa0ac;\n    font-size: 0.875rem;\n    display: block;\n    margin: -0.75rem 0 1rem;\n    line-height: 1.1;\n    font-weight: 400;\n}\n\n.card-table {\n    margin-bottom: 0;\n}\n\n.card-table tr:first-child td,\n.card-table tr:first-child th {\n    border-top: 0;\n}\n\n.card-table tr td:first-child,\n.card-table tr th:first-child {\n    padding-left: 1.5rem;\n}\n\n.card-table tr td:last-child,\n.card-table tr th:last-child {\n    padding-right: 1.5rem;\n}\n\n.card-body + .card-table {\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.card-profile .card-header {\n    height: 9rem;\n    background-size: cover;\n}\n\n.card-profile-img {\n    max-width: 6rem;\n    margin-top: -5rem;\n    margin-bottom: 1rem;\n    border: 3px solid #fff;\n    border-radius: 100%;\n    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n}\n\n.card-link + .card-link {\n    margin-left: 1rem;\n}\n\n.card-body + .card-list-group {\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.card-list-group .list-group-item {\n    border-right: 0;\n    border-left: 0;\n    border-radius: 0;\n    padding-left: 1.5rem;\n    padding-right: 1.5rem;\n}\n\n.card-list-group .list-group-item:last-child {\n    border-bottom: 0;\n}\n\n.card-list-group .list-group-item:first-child {\n    border-top: 0;\n}\n\n.card-header-tabs {\n    margin: -1.25rem 0;\n    border-bottom: 0;\n    line-height: 2rem;\n}\n\n.card-header-tabs .nav-item {\n    margin-bottom: 1px;\n}\n\n.card-header-pills {\n    margin: -0.75rem 0;\n}\n\n.card-aside {\n    -ms-flex-direction: row;\n    flex-direction: row;\n}\n\n.card-aside-column {\n    min-width: 5rem;\n    width: 30%;\n    border-top-left-radius: 3px;\n    border-bottom-left-radius: 3px;\n    background: no-repeat center/cover;\n}\n\n.card-value {\n    font-size: 2.5rem;\n    line-height: 3.4rem;\n    height: 3.4rem;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    font-weight: 400;\n}\n\n.card-value i {\n    vertical-align: middle;\n}\n\n.card-chart-bg {\n    height: 4rem;\n    position: relative;\n    z-index: 1;\n}\n\n.card-options {\n    margin-left: auto;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-order: 100;\n    order: 100;\n    margin-right: -0.5rem;\n    color: #9aa0ac;\n    -ms-flex-item-align: center;\n    align-self: center;\n}\n\n.card-options a {\n    margin-left: 0.5rem;\n    color: #9aa0ac;\n    display: inline-block;\n    min-width: 1rem;\n}\n\n.card-options a:hover {\n    text-decoration: none;\n    color: #6e7687;\n}\n\n.card-options a i {\n    font-size: 1rem;\n    vertical-align: middle;\n}\n\n.card-options .dropdown-toggle:after {\n    display: none;\n}\n\n/*\nCard options\n */\n\n.card-collapsed > :not(.card-header):not(.card-status) {\n    display: none;\n}\n\n.card-collapsed .card-options-collapse i:before {\n    content: '\\e92d';\n}\n\n.card-fullscreen .card-options-fullscreen i:before {\n    content: '\\e992';\n}\n\n.card-fullscreen .card-options-remove {\n    display: none;\n}\n\n/*\nCard maps\n */\n\n.card-map {\n    height: 15rem;\n    background: #e9ecef;\n}\n\n.card-map-placeholder {\n    background: no-repeat center;\n}\n\n/**\nCard tabs\n */\n\n.card-tabs {\n    display: -ms-flexbox;\n    display: flex;\n}\n\n.card-tabs-bottom .card-tabs-item {\n    border: 0;\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.card-tabs-bottom .card-tabs-item.active {\n    border-top-color: #fff;\n}\n\n.card-tabs-item {\n    -ms-flex: 1 1 auto;\n    flex: 1 1 auto;\n    display: block;\n    padding: 1rem 1.5rem;\n    border-bottom: 1px solid rgba(0, 40, 100, 0.12);\n    color: inherit;\n    overflow: hidden;\n}\n\na.card-tabs-item {\n    background: #fafbfc;\n}\n\na.card-tabs-item:hover {\n    text-decoration: none;\n    color: inherit;\n}\n\na.card-tabs-item:focus {\n    z-index: 1;\n}\n\na.card-tabs-item.active {\n    background: #fff;\n    border-bottom-color: #fff;\n}\n\n.card-tabs-item + .card-tabs-item {\n    border-left: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n/**\nCard status\n */\n\n.card-status {\n    position: absolute;\n    top: -1px;\n    left: -1px;\n    right: -1px;\n    height: 3px;\n    border-radius: 3px 3px 0 0;\n    background: rgba(0, 40, 100, 0.12);\n}\n\n.card-status-left {\n    right: auto;\n    bottom: 0;\n    height: auto;\n    width: 3px;\n    border-radius: 3px 0 0 3px;\n}\n\n/**\nCard icon\n */\n\n.card-icon {\n    width: 3rem;\n    font-size: 2.5rem;\n    line-height: 3rem;\n    text-align: center;\n}\n\n/**\nCard fullscreen\n */\n\n.card-fullscreen {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    z-index: 1;\n    margin: 0;\n}\n\n/**\nCard alert\n */\n\n.card-alert {\n    border-radius: 0;\n    margin: -1px -1px 0;\n}\n\n.card-category {\n    font-size: 0.875rem;\n    text-transform: uppercase;\n    text-align: center;\n    font-weight: 600;\n    letter-spacing: 0.05em;\n    margin: 0 0 0.5rem;\n}\n\n.popover {\n    -webkit-filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.1));\n}\n\n.popover.bs-popover-top,\n.popover.bs-popover-auto[x-placement^='top'] {\n    margin-bottom: 0.625rem;\n}\n\n.popover .arrow {\n    margin-left: calc(0.25rem + 2px);\n}\n\n.dropdown {\n    display: inline-block;\n}\n\n.dropdown-menu {\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n    min-width: 12rem;\n}\n\n.dropdown-item {\n    color: #6e7687;\n}\n\n[data-theme='dark'] .dropdown-item {\n    color: #ffffff;\n}\n\n.dropdown-menu-arrow:before {\n    position: absolute;\n    top: -6px;\n    left: 12px;\n    display: inline-block;\n    border-right: 5px solid transparent;\n    border-bottom: 5px solid rgba(0, 40, 100, 0.12);\n    border-left: 5px solid transparent;\n    border-bottom-color: rgba(0, 0, 0, 0.2);\n    content: '';\n}\n\n[data-theme='dark'] .dropdown-menu-arrow:before {\n    border-bottom-color: var(--ctrl-bgcolor);\n}\n\n.dropdown-menu-arrow:after {\n    position: absolute;\n    top: -5px;\n    left: 12px;\n    display: inline-block;\n    border-right: 5px solid transparent;\n    border-bottom: 5px solid var(--ctrl-bgcolor);\n    border-left: 5px solid transparent;\n    content: '';\n}\n\n[data-theme='dark'] .dropdown-menu-arrow:after {\n    border-bottom: 5px solid var(--card-border-color);\n}\n\n.dropdown-menu-arrow.dropdown-menu-right:before,\n.dropdown-menu-arrow.dropdown-menu-right:after {\n    left: auto;\n    right: 12px;\n}\n\n.dropdown-toggle {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    cursor: pointer;\n}\n\n.dropdown-toggle:after {\n    vertical-align: 0.155em;\n}\n\n.dropdown-toggle:empty:after {\n    margin-left: 0;\n}\n\n.dropdown-icon {\n    color: #9aa0ac;\n    margin-right: 0.5rem;\n    margin-left: -0.5rem;\n    width: 1em;\n    display: inline-block;\n    text-align: center;\n    vertical-align: -1px;\n}\n\n.list-inline-dots .list-inline-item + .list-inline-item:before {\n    content: '· ';\n    margin-left: -2px;\n    margin-right: 3px;\n}\n\n.list-separated-item {\n    padding: 1rem 0;\n}\n\n.list-separated-item:first-child {\n    padding-top: 0;\n}\n\n.list-separated-item:last-child {\n    padding-bottom: 0;\n}\n\n.list-separated-item + .list-separated-item {\n    border-top: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.list-group-item.active .icon {\n    color: inherit !important;\n}\n\n.list-group-transparent .list-group-item {\n    background: none;\n    border: 0;\n    padding: 0.5rem 1rem;\n    border-radius: 3px;\n}\n\n.list-group-transparent .list-group-item.active {\n    background: rgba(70, 127, 207, 0.06);\n    font-weight: 600;\n}\n\n.avatar {\n    width: 2rem;\n    height: 2rem;\n    line-height: 2rem;\n    border-radius: 50%;\n    display: inline-block;\n    background: #ced4da no-repeat center/cover;\n    position: relative;\n    text-align: center;\n    color: #868e96;\n    font-weight: 600;\n    vertical-align: bottom;\n    font-size: 0.875rem;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n.avatar i {\n    font-size: 125%;\n    vertical-align: sub;\n}\n\n.avatar-status {\n    position: absolute;\n    right: -2px;\n    bottom: -2px;\n    width: 0.75rem;\n    height: 0.75rem;\n    border: 2px solid #fff;\n    background: #868e96;\n    border-radius: 50%;\n}\n\n.avatar-sm {\n    width: 1.5rem;\n    height: 1.5rem;\n    line-height: 1.5rem;\n    font-size: 0.75rem;\n}\n\n.avatar-md {\n    width: 2.5rem;\n    height: 2.5rem;\n    line-height: 2.5rem;\n    font-size: 1rem;\n}\n\n.avatar-lg {\n    width: 3rem;\n    height: 3rem;\n    line-height: 3rem;\n    font-size: 1.25rem;\n}\n\n.avatar-xl {\n    width: 4rem;\n    height: 4rem;\n    line-height: 4rem;\n    font-size: 1.75rem;\n}\n\n.avatar-xxl {\n    width: 5rem;\n    height: 5rem;\n    line-height: 5rem;\n    font-size: 2rem;\n}\n\n.avatar-placeholder {\n    background: #ced4da\n        url('data:image/svg+xml;charset=utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"134\" height=\"134\" viewBox=\"0 0 134 134\"><path fill=\"#868e96\" d=\"M65.92 66.34h2.16c14.802.42 30.928 6.062 29.283 20.35l-1.618 13.32c-.844 6.815-5.208 7.828-13.972 7.866H52.23c-8.764-.038-13.13-1.05-13.973-7.865l-1.62-13.32C34.994 72.4 51.12 66.76 65.92 66.34zM49.432 43.934c0-9.82 7.99-17.81 17.807-17.81 9.82 0 17.81 7.99 17.81 17.81 0 9.82-7.99 17.807-17.81 17.807-9.82 0-17.808-7.987-17.808-17.806z\"/></svg>')\n        no-repeat center/80%;\n}\n\n.avatar-list {\n    margin: 0 0 -0.5rem;\n    padding: 0;\n    font-size: 0;\n}\n\n.avatar-list .avatar {\n    margin-bottom: 0.5rem;\n}\n\n.avatar-list .avatar:not(:last-child) {\n    margin-right: 0.5rem;\n}\n\n.avatar-list-stacked .avatar {\n    margin-right: -0.8em !important;\n}\n\n.avatar-list-stacked .avatar {\n    box-shadow: 0 0 0 2px #fff;\n}\n\n.avatar-blue {\n    background-color: #c8d9f1;\n    color: #467fcf;\n}\n\n.avatar-indigo {\n    background-color: #d1d5f0;\n    color: #6574cd;\n}\n\n.avatar-purple {\n    background-color: #e4cff9;\n    color: #a55eea;\n}\n\n.avatar-pink {\n    background-color: #fcd3e1;\n    color: #f66d9b;\n}\n\n.avatar-red {\n    background-color: #f0bcbc;\n    color: #cd201f;\n}\n\n.avatar-orange {\n    background-color: #fee0c7;\n    color: #fd9644;\n}\n\n.avatar-yellow {\n    background-color: #fbedb7;\n    color: #f1c40f;\n}\n\n.avatar-green {\n    background-color: #cfeab3;\n    color: #5eba00;\n}\n\n.avatar-teal {\n    background-color: #bfefea;\n    color: #2bcbba;\n}\n\n.avatar-cyan {\n    background-color: #b9e3ea;\n    color: #17a2b8;\n}\n\n.avatar-white {\n    background-color: white;\n    color: #fff;\n}\n\n.avatar-gray {\n    background-color: #dbdde0;\n    color: #868e96;\n}\n\n.avatar-gray-dark {\n    background-color: #c2c4c6;\n    color: #343a40;\n}\n\n.avatar-azure {\n    background-color: #c7e6fb;\n    color: #45aaf2;\n}\n\n.avatar-lime {\n    background-color: #d7f2c2;\n    color: #7bd235;\n}\n\n.product-price {\n    font-size: 1rem;\n}\n\n.product-price strong {\n    font-size: 1.5rem;\n}\n\n@-webkit-keyframes indeterminate {\n    0% {\n        left: -35%;\n        right: 100%;\n    }\n    100%,\n    60% {\n        left: 100%;\n        right: -90%;\n    }\n}\n\n@keyframes indeterminate {\n    0% {\n        left: -35%;\n        right: 100%;\n    }\n    100%,\n    60% {\n        left: 100%;\n        right: -90%;\n    }\n}\n\n@-webkit-keyframes indeterminate-short {\n    0% {\n        left: -200%;\n        right: 100%;\n    }\n    100%,\n    60% {\n        left: 107%;\n        right: -8%;\n    }\n}\n\n@keyframes indeterminate-short {\n    0% {\n        left: -200%;\n        right: 100%;\n    }\n    100%,\n    60% {\n        left: 107%;\n        right: -8%;\n    }\n}\n\n.progress {\n    position: relative;\n}\n\n.progress-xs,\n.progress-xs .progress-bar {\n    height: 0.25rem;\n}\n\n.progress-sm,\n.progress-sm .progress-bar {\n    height: 0.5rem;\n}\n\n.progress-bar-indeterminate:after,\n.progress-bar-indeterminate:before {\n    content: '';\n    position: absolute;\n    background-color: inherit;\n    left: 0;\n    will-change: left, right;\n    top: 0;\n    bottom: 0;\n}\n\n.progress-bar-indeterminate:before {\n    -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;\n    animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;\n}\n\n.progress-bar-indeterminate:after {\n    -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;\n    animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;\n    -webkit-animation-delay: 1.15s;\n    animation-delay: 1.15s;\n}\n\n@-webkit-keyframes loader {\n    from {\n        -webkit-transform: rotate(0deg);\n        transform: rotate(0deg);\n    }\n    to {\n        -webkit-transform: rotate(360deg);\n        transform: rotate(360deg);\n    }\n}\n\n@keyframes loader {\n    from {\n        -webkit-transform: rotate(0deg);\n        transform: rotate(0deg);\n    }\n    to {\n        -webkit-transform: rotate(360deg);\n        transform: rotate(360deg);\n    }\n}\n\n/**\nDimmer\n*/\n\n.dimmer {\n    position: relative;\n}\n\n.dimmer .loader {\n    display: none;\n    margin: 0 auto;\n    position: absolute;\n    top: 50%;\n    left: 0;\n    right: 0;\n    -webkit-transform: translateY(-50%);\n    transform: translateY(-50%);\n}\n\n.dimmer.active .loader {\n    display: block;\n}\n\n.dimmer.active .dimmer-content {\n    opacity: 0.04;\n    pointer-events: none;\n}\n\n/**\nLoader\n*/\n\n.loader {\n    display: block;\n    position: relative;\n    height: 2.5rem;\n    width: 2.5rem;\n    color: #467fcf;\n}\n\n.loader:before,\n.loader:after {\n    width: 2.5rem;\n    height: 2.5rem;\n    margin: -1.25rem 0 0 -1.25rem;\n    position: absolute;\n    content: '';\n    top: 50%;\n    left: 50%;\n}\n\n.loader:before {\n    border-radius: 50%;\n    border: 3px solid currentColor;\n    opacity: 0.15;\n}\n\n.loader:after {\n    -webkit-animation: loader 0.6s linear;\n    animation: loader 0.6s linear;\n    -webkit-animation-iteration-count: infinite;\n    animation-iteration-count: infinite;\n    border-radius: 50%;\n    border: 3px solid;\n    border-color: transparent;\n    border-top-color: currentColor;\n    box-shadow: 0 0 0 1px transparent;\n}\n\n.icons-list {\n    list-style: none;\n    margin: 0 -1px -1px 0;\n    padding: 0;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n}\n\n.icons-list > li {\n    -ms-flex: 1 0 4rem;\n    flex: 1 0 4rem;\n}\n\n.icons-list-wrap {\n    overflow: hidden;\n}\n\n.icons-list-item {\n    text-align: center;\n    height: 4rem;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: center;\n    justify-content: center;\n    border-right: 1px solid rgba(0, 40, 100, 0.12);\n    border-bottom: 1px solid rgba(0, 40, 100, 0.12);\n}\n\n.icons-list-item i {\n    font-size: 1.25rem;\n}\n\n.img-gallery {\n    margin-right: -0.25rem;\n    margin-left: -0.25rem;\n    margin-bottom: -0.5rem;\n}\n\n.img-gallery > .col,\n.img-gallery > [class*='col-'] {\n    padding-left: 0.25rem;\n    padding-right: 0.25rem;\n    padding-bottom: 0.5rem;\n}\n\n.link-overlay {\n    position: relative;\n}\n\n.link-overlay:hover .link-overlay-bg {\n    opacity: 1;\n}\n\n.link-overlay-bg {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background: rgba(70, 127, 207, 0.8);\n    display: -ms-flexbox;\n    display: flex;\n    color: #fff;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: center;\n    justify-content: center;\n    font-size: 1.25rem;\n    opacity: 0;\n    transition: 0.3s opacity;\n}\n\n.media-icon {\n    width: 2rem;\n    height: 2rem;\n    line-height: 2rem;\n    text-align: center;\n    border-radius: 100%;\n}\n\n.media-list {\n    margin: 0;\n    padding: 0;\n    list-style: none;\n}\n\ntextarea[cols] {\n    height: auto;\n}\n\n.form-group {\n    display: block;\n}\n\n.form-label {\n    display: block;\n    margin-bottom: 0.375rem;\n    font-weight: 600;\n    font-size: 0.875rem;\n}\n\n.form-label-small {\n    float: right;\n    font-weight: 400;\n    font-size: 87.5%;\n}\n\n.form-footer {\n    margin-top: 2rem;\n}\n\n.custom-control {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n.custom-controls-stacked .custom-control {\n    margin-bottom: 0.25rem;\n}\n\n.custom-control-label {\n    vertical-align: middle;\n}\n\n.custom-control-label:before {\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    background-color: var(--radio-bg);\n    background-size: 0.5rem;\n}\n\n.custom-control-description {\n    line-height: 1.5rem;\n}\n\n.input-group-prepend,\n.input-group-append,\n.input-group-btn {\n    font-size: 0.9375rem;\n}\n\n.input-group-prepend > .btn,\n.input-group-append > .btn,\n.input-group-btn > .btn {\n    height: 100%;\n    border-color: rgba(0, 40, 100, 0.12);\n}\n\n.input-group-prepend > .input-group-text {\n    border-right: 0;\n}\n\n.input-group-append > .input-group-text {\n    border-left: 0;\n}\n\n/**\nIcon input\n */\n\n.input-icon {\n    position: relative;\n}\n\n.input-icon .form-control:not(:last-child) {\n    padding-right: 2.5rem;\n}\n\n.input-icon .form-control:not(:first-child) {\n    padding-left: 2.5rem;\n}\n\n.input-icon-addon {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    color: #9aa0ac;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: center;\n    justify-content: center;\n    min-width: 2.5rem;\n    pointer-events: none;\n}\n\n.input-icon-addon:last-child {\n    left: auto;\n    right: 0;\n}\n\n.form-fieldset {\n    background: #f8f9fa;\n    border: 1px solid #e9ecef;\n    padding: 1rem;\n    border-radius: 3px;\n    margin-bottom: 1rem;\n}\n\n.form-required {\n    color: #cd201f;\n}\n\n.form-required:before {\n    content: ' ';\n}\n\n.state-valid {\n    padding-right: 2rem;\n    background: url(\"data:image/svg+xml;charset=utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%235eba00' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-check'><polyline points='20 6 9 17 4 12'></polyline></svg>\")\n        no-repeat center right 0.5rem/1rem;\n}\n\n.state-invalid {\n    padding-right: 2rem;\n    background: url(\"data:image/svg+xml;charset=utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23cd201f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-x'><line x1='18' y1='6' x2='6' y2='18'></line><line x1='6' y1='6' x2='18' y2='18'></line></svg>\")\n        no-repeat center right 0.5rem/1rem;\n}\n\n.form-help {\n    display: inline-block;\n    width: 1rem;\n    height: 1rem;\n    text-align: center;\n    line-height: 1rem;\n    color: #9aa0ac;\n    background: #f8f9fa;\n    border-radius: 50%;\n    font-size: 0.75rem;\n    transition:\n        0.3s background-color,\n        0.3s color;\n    text-decoration: none;\n    cursor: pointer;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n.form-help:hover,\n.form-help[aria-describedby] {\n    background: #467fcf;\n    color: #fff;\n}\n\n.sparkline {\n    display: inline-block;\n    height: 2rem;\n}\n\n.jqstooltip {\n    box-sizing: content-box;\n    font-family: inherit !important;\n    background: #333 !important;\n    border: none !important;\n    border-radius: 3px;\n    font-size: 11px !important;\n    font-weight: 700 !important;\n    line-height: 1 !important;\n    padding: 6px !important;\n}\n\n.jqstooltip .jqsfield {\n    font: inherit !important;\n}\n\n.social-links li a {\n    background: #f8f8f8;\n    border-radius: 50%;\n    color: #9aa0ac;\n    display: inline-block;\n    height: 1.75rem;\n    width: 1.75rem;\n    line-height: 1.75rem;\n    text-align: center;\n}\n\n.map,\n.chart {\n    position: relative;\n    padding-top: 56.25%;\n}\n\n.map-square,\n.chart-square {\n    padding-top: 100%;\n}\n\n.map-content,\n.chart-content {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n}\n\n.map-header {\n    margin-top: -1.5rem;\n    margin-bottom: 1.5rem;\n    height: 15rem;\n    position: relative;\n    margin-bottom: -1.5rem;\n}\n\n.map-header:before {\n    content: '';\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    height: 10rem;\n    background: linear-gradient(to bottom, rgba(245, 247, 251, 0) 5%, #f5f7fb 95%);\n    pointer-events: none;\n}\n\n.map-header-layer {\n    height: 100%;\n}\n\n.map-static {\n    height: 120px;\n    width: 100%;\n    max-width: 640px;\n    background-position: center center;\n    background-size: 640px 120px;\n}\n\n@-webkit-keyframes status-pulse {\n    0%,\n    100% {\n        opacity: 1;\n    }\n    50% {\n        opacity: 0.32;\n    }\n}\n\n@keyframes status-pulse {\n    0%,\n    100% {\n        opacity: 1;\n    }\n    50% {\n        opacity: 0.32;\n    }\n}\n\n.status-icon {\n    content: '';\n    width: 0.5rem;\n    height: 0.5rem;\n    display: inline-block;\n    background: currentColor;\n    border-radius: 50%;\n    -webkit-transform: translateY(-1px);\n    transform: translateY(-1px);\n    margin-right: 0.375rem;\n    vertical-align: middle;\n}\n\n.status-animated {\n    -webkit-animation: 1s status-pulse infinite ease;\n    animation: 1s status-pulse infinite ease;\n}\n\n.chart-circle {\n    display: block;\n    height: 8rem;\n    width: 8rem;\n    position: relative;\n}\n\n.chart-circle canvas {\n    margin: 0 auto;\n    display: block;\n    max-width: 100%;\n    max-height: 100%;\n}\n\n.chart-circle-xs {\n    height: 2.5rem;\n    width: 2.5rem;\n    font-size: 0.8rem;\n}\n\n.chart-circle-sm {\n    height: 4rem;\n    width: 4rem;\n    font-size: 0.8rem;\n}\n\n.chart-circle-lg {\n    height: 10rem;\n    width: 10rem;\n    font-size: 0.8rem;\n}\n\n.chart-circle-value {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    margin-left: auto;\n    margin-right: auto;\n    bottom: 0;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-pack: center;\n    justify-content: center;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    line-height: 1;\n}\n\n.chart-circle-value small {\n    display: block;\n    color: #9aa0ac;\n    font-size: 0.9375rem;\n}\n\n.chips {\n    margin: 0 0 -0.5rem;\n}\n\n.chips .chip {\n    margin: 0 0.5rem 0.5rem 0;\n}\n\n.chip {\n    display: inline-block;\n    height: 2rem;\n    line-height: 2rem;\n    font-size: 0.875rem;\n    font-weight: 500;\n    color: #6e7687;\n    padding: 0 0.75rem;\n    border-radius: 1rem;\n    background-color: #f8f9fa;\n    transition: 0.3s background;\n}\n\n.chip .avatar {\n    float: left;\n    margin: 0 0.5rem 0 -0.75rem;\n    height: 2rem;\n    width: 2rem;\n    border-radius: 50%;\n}\n\na.chip:hover {\n    color: inherit;\n    text-decoration: none;\n    background-color: #e9ecef;\n}\n\n.stamp {\n    color: #fff;\n    background: #868e96;\n    display: inline-block;\n    min-width: 2rem;\n    height: 2rem;\n    padding: 0 0.25rem;\n    line-height: 2rem;\n    text-align: center;\n    border-radius: 3px;\n    font-weight: 600;\n}\n\n.stamp-md {\n    min-width: 2.5rem;\n    height: 2.5rem;\n    line-height: 2.5rem;\n}\n\n.chat {\n    outline: 0;\n    margin: 0;\n    padding: 0;\n    list-style-type: none;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n    -ms-flex-pack: end;\n    justify-content: flex-end;\n    min-height: 100%;\n}\n\n.chat-line {\n    padding: 0;\n    text-align: right;\n    position: relative;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: row-reverse;\n    flex-direction: row-reverse;\n}\n\n.chat-line + .chat-line {\n    padding-top: 1rem;\n}\n\n.chat-message {\n    position: relative;\n    display: inline-block;\n    background-color: #467fcf;\n    color: #fff;\n    font-size: 0.875rem;\n    padding: 0.375rem 0.5rem;\n    border-radius: 3px;\n    white-space: normal;\n    text-align: left;\n    margin: 0 0.5rem 0 2.5rem;\n    line-height: 1.4;\n}\n\n.chat-message > :last-child {\n    margin-bottom: 0 !important;\n}\n\n.chat-message:after {\n    content: '';\n    position: absolute;\n    right: -5px;\n    top: 7px;\n    border-bottom: 6px solid transparent;\n    border-left: 6px solid #467fcf;\n    border-top: 6px solid transparent;\n}\n\n.chat-message img {\n    max-width: 100%;\n}\n\n.chat-message p {\n    margin-bottom: 1em;\n}\n\n.chat-line-friend {\n    -ms-flex-direction: row;\n    flex-direction: row;\n}\n\n.chat-line-friend + .chat-line-friend {\n    margin-top: -0.5rem;\n}\n\n.chat-line-friend + .chat-line-friend .chat-author {\n    visibility: hidden;\n}\n\n.chat-line-friend + .chat-line-friend .chat-message:after {\n    display: none;\n}\n\n.chat-line-friend .chat-message {\n    background-color: #f3f3f3;\n    color: #495057;\n    margin-left: 0.5rem;\n    margin-right: 2.5rem;\n}\n\n.chat-line-friend .chat-message:after {\n    right: auto;\n    left: -5px;\n    border-left-width: 0;\n    border-right: 5px solid #f3f3f3;\n}\n\n.example {\n    padding: 1.5rem;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    border-radius: 3px 3px 0 0;\n    font-size: 0.9375rem;\n}\n\n.example-bg {\n    background: #f5f7fb;\n}\n\n.example + .highlight {\n    border-top: none;\n    margin-top: 0;\n    border-radius: 0 0 3px 3px;\n}\n\n.highlight {\n    margin: 1rem 0 2rem;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    border-radius: 3px;\n    font-size: 0.9375rem;\n    max-height: 40rem;\n    overflow: auto;\n    background: #fcfcfc;\n}\n\n.highlight pre {\n    margin-bottom: 0;\n    background-color: transparent;\n}\n\n.example-column {\n    margin: 0 auto;\n}\n\n.example-column > .card:last-of-type {\n    margin-bottom: 0;\n}\n\n.example-column-1 {\n    max-width: 20rem;\n}\n\n.example-column-2 {\n    max-width: 40rem;\n}\n\n.tag {\n    font-size: 0.75rem;\n    color: #6e7687;\n    background-color: #e9ecef;\n    border-radius: 3px;\n    padding: 0 0.5rem;\n    line-height: 2em;\n    display: -ms-inline-flexbox;\n    display: inline-flex;\n    cursor: default;\n    font-weight: 400;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\na.tag {\n    text-decoration: none;\n    cursor: pointer;\n    transition:\n        0.3s color,\n        0.3s background;\n}\n\na.tag:hover {\n    background-color: rgba(110, 118, 135, 0.2);\n    color: inherit;\n}\n\n.tag-addon {\n    display: inline-block;\n    padding: 0 0.5rem;\n    color: inherit;\n    text-decoration: none;\n    background: rgba(0, 0, 0, 0.06);\n    margin: 0 -0.5rem 0 0.5rem;\n    text-align: center;\n    min-width: 1.5rem;\n}\n\n.tag-addon:last-child {\n    border-top-right-radius: 3px;\n    border-bottom-right-radius: 3px;\n}\n\n.tag-addon i {\n    vertical-align: middle;\n    margin: 0 -0.25rem;\n}\n\na.tag-addon {\n    text-decoration: none;\n    cursor: pointer;\n    transition:\n        0.3s color,\n        0.3s background;\n}\n\na.tag-addon:hover {\n    background: rgba(0, 0, 0, 0.16);\n    color: inherit;\n}\n\n.tag-avatar {\n    width: 1.5rem;\n    height: 1.5rem;\n    border-radius: 3px 0 0 3px;\n    margin: 0 0.5rem 0 -0.5rem;\n}\n\n.tag-blue {\n    background-color: #467fcf;\n    color: #fff;\n}\n\n.tag-indigo {\n    background-color: #6574cd;\n    color: #fff;\n}\n\n.tag-purple {\n    background-color: #a55eea;\n    color: #fff;\n}\n\n.tag-pink {\n    background-color: #f66d9b;\n    color: #fff;\n}\n\n.tag-red {\n    background-color: #cd201f;\n    color: #fff;\n}\n\n.tag-orange {\n    background-color: #fd9644;\n    color: #fff;\n}\n\n.tag-yellow {\n    background-color: #f1c40f;\n    color: #fff;\n}\n\n.tag-green {\n    background-color: #5eba00;\n    color: #fff;\n}\n\n.tag-teal {\n    background-color: #2bcbba;\n    color: #fff;\n}\n\n.tag-cyan {\n    background-color: #17a2b8;\n    color: #fff;\n}\n\n.tag-white {\n    background-color: #fff;\n    color: #fff;\n}\n\n.tag-gray {\n    background-color: #868e96;\n    color: #fff;\n}\n\n.tag-gray-dark {\n    background-color: #343a40;\n    color: #fff;\n}\n\n.tag-azure {\n    background-color: #45aaf2;\n    color: #fff;\n}\n\n.tag-lime {\n    background-color: #7bd235;\n    color: #fff;\n}\n\n.tag-primary {\n    background-color: #467fcf;\n    color: #fff;\n}\n\n.tag-secondary {\n    background-color: #868e96;\n    color: #fff;\n}\n\n.tag-success {\n    background-color: #5eba00;\n    color: #fff;\n}\n\n.tag-info {\n    background-color: #45aaf2;\n    color: #fff;\n}\n\n.tag-warning {\n    background-color: #f1c40f;\n    color: #fff;\n}\n\n.tag-danger {\n    background-color: #cd201f;\n    color: #fff;\n}\n\n.tag-light {\n    background-color: #f8f9fa;\n    color: #fff;\n}\n\n.tag-dark {\n    background-color: #343a40;\n    color: #fff;\n}\n\n.tag-rounded {\n    border-radius: 50px;\n}\n\n.tag-rounded .tag-avatar {\n    border-radius: 50px;\n}\n\n.tags {\n    margin-bottom: -0.5rem;\n    font-size: 0;\n}\n\n.tags > .tag {\n    margin-bottom: 0.5rem;\n}\n\n.tags > .tag:not(:last-child) {\n    margin-right: 0.5rem;\n}\n\n.highlight .hll {\n    background-color: #ffc;\n}\n\n.highlight .c {\n    color: #999;\n}\n\n.highlight .k {\n    color: #069;\n}\n\n.highlight .o {\n    color: #555;\n}\n\n.highlight .cm {\n    color: #999;\n}\n\n.highlight .cp {\n    color: #099;\n}\n\n.highlight .c1 {\n    color: #999;\n}\n\n.highlight .cs {\n    color: #999;\n}\n\n.highlight .gd {\n    background-color: #fcc;\n    border: 1px solid #c00;\n}\n\n.highlight .ge {\n    font-style: italic;\n}\n\n.highlight .gr {\n    color: #f00;\n}\n\n.highlight .gh {\n    color: #030;\n}\n\n.highlight .gi {\n    background-color: #cfc;\n    border: 1px solid #0c0;\n}\n\n.highlight .go {\n    color: #aaa;\n}\n\n.highlight .gp {\n    color: #009;\n}\n\n.highlight .gu {\n    color: #030;\n}\n\n.highlight .gt {\n    color: #9c6;\n}\n\n.highlight .kc {\n    color: #069;\n}\n\n.highlight .kd {\n    color: #069;\n}\n\n.highlight .kn {\n    color: #069;\n}\n\n.highlight .kp {\n    color: #069;\n}\n\n.highlight .kr {\n    color: #069;\n}\n\n.highlight .kt {\n    color: #078;\n}\n\n.highlight .m {\n    color: #f60;\n}\n\n.highlight .s {\n    color: #d44950;\n}\n\n.highlight .na {\n    color: #4f9fcf;\n}\n\n.highlight .nb {\n    color: #366;\n}\n\n.highlight .nc {\n    color: #0a8;\n}\n\n.highlight .no {\n    color: #360;\n}\n\n.highlight .nd {\n    color: #99f;\n}\n\n.highlight .ni {\n    color: #999;\n}\n\n.highlight .ne {\n    color: #c00;\n}\n\n.highlight .nf {\n    color: #c0f;\n}\n\n.highlight .nl {\n    color: #99f;\n}\n\n.highlight .nn {\n    color: #0cf;\n}\n\n.highlight .nt {\n    color: #2f6f9f;\n}\n\n.highlight .nv {\n    color: #033;\n}\n\n.highlight .ow {\n    color: #000;\n}\n\n.highlight .w {\n    color: #bbb;\n}\n\n.highlight .mf {\n    color: #f60;\n}\n\n.highlight .mh {\n    color: #f60;\n}\n\n.highlight .mi {\n    color: #f60;\n}\n\n.highlight .mo {\n    color: #f60;\n}\n\n.highlight .sb {\n    color: #c30;\n}\n\n.highlight .sc {\n    color: #c30;\n}\n\n.highlight .sd {\n    font-style: italic;\n    color: #c30;\n}\n\n.highlight .s2 {\n    color: #c30;\n}\n\n.highlight .se {\n    color: #c30;\n}\n\n.highlight .sh {\n    color: #c30;\n}\n\n.highlight .si {\n    color: #a00;\n}\n\n.highlight .sx {\n    color: #c30;\n}\n\n.highlight .sr {\n    color: #3aa;\n}\n\n.highlight .s1 {\n    color: #c30;\n}\n\n.highlight .ss {\n    color: #fc3;\n}\n\n.highlight .bp {\n    color: #366;\n}\n\n.highlight .vc {\n    color: #033;\n}\n\n.highlight .vg {\n    color: #033;\n}\n\n.highlight .vi {\n    color: #033;\n}\n\n.highlight .il {\n    color: #f60;\n}\n\n.highlight .css .o,\n.highlight .css .o + .nt,\n.highlight .css .nt + .nt {\n    color: #999;\n}\n\n.highlight .language-bash::before,\n.highlight .language-sh::before {\n    color: #009;\n    content: '$ ';\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n.highlight .language-powershell::before {\n    color: #009;\n    content: 'PM> ';\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n.custom-range {\n    -ms-flex-align: center;\n    align-items: center;\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    appearance: none;\n    background: none;\n    cursor: pointer;\n    display: -ms-flexbox;\n    display: flex;\n    height: 100%;\n    min-height: 2.375rem;\n    overflow: hidden;\n    padding: 0;\n    border: 0;\n}\n\n.custom-range:focus {\n    box-shadow: none;\n    outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n    border-color: #467fcf;\n    background-color: #467fcf;\n}\n\n.custom-range:focus::-moz-range-thumb {\n    border-color: #467fcf;\n    background-color: #467fcf;\n}\n\n.custom-range:focus::-ms-thumb {\n    border-color: #467fcf;\n    background-color: #467fcf;\n}\n\n.custom-range::-moz-focus-outer {\n    border: 0;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n    background: #467fcf;\n    content: '';\n    height: 2px;\n    pointer-events: none;\n}\n\n.custom-range::-webkit-slider-thumb {\n    width: 14px;\n    height: 14px;\n    -webkit-appearance: none;\n    appearance: none;\n    background: #fff;\n    border-radius: 50px;\n    box-shadow:\n        1px 0 0 -6px rgba(0, 50, 126, 0.12),\n        6px 0 0 -6px rgba(0, 50, 126, 0.12),\n        7px 0 0 -6px rgba(0, 50, 126, 0.12),\n        8px 0 0 -6px rgba(0, 50, 126, 0.12),\n        9px 0 0 -6px rgba(0, 50, 126, 0.12),\n        10px 0 0 -6px rgba(0, 50, 126, 0.12),\n        11px 0 0 -6px rgba(0, 50, 126, 0.12),\n        12px 0 0 -6px rgba(0, 50, 126, 0.12),\n        13px 0 0 -6px rgba(0, 50, 126, 0.12),\n        14px 0 0 -6px rgba(0, 50, 126, 0.12),\n        15px 0 0 -6px rgba(0, 50, 126, 0.12),\n        16px 0 0 -6px rgba(0, 50, 126, 0.12),\n        17px 0 0 -6px rgba(0, 50, 126, 0.12),\n        18px 0 0 -6px rgba(0, 50, 126, 0.12),\n        19px 0 0 -6px rgba(0, 50, 126, 0.12),\n        20px 0 0 -6px rgba(0, 50, 126, 0.12),\n        21px 0 0 -6px rgba(0, 50, 126, 0.12),\n        22px 0 0 -6px rgba(0, 50, 126, 0.12),\n        23px 0 0 -6px rgba(0, 50, 126, 0.12),\n        24px 0 0 -6px rgba(0, 50, 126, 0.12),\n        25px 0 0 -6px rgba(0, 50, 126, 0.12),\n        26px 0 0 -6px rgba(0, 50, 126, 0.12),\n        27px 0 0 -6px rgba(0, 50, 126, 0.12),\n        28px 0 0 -6px rgba(0, 50, 126, 0.12),\n        29px 0 0 -6px rgba(0, 50, 126, 0.12),\n        30px 0 0 -6px rgba(0, 50, 126, 0.12),\n        31px 0 0 -6px rgba(0, 50, 126, 0.12),\n        32px 0 0 -6px rgba(0, 50, 126, 0.12),\n        33px 0 0 -6px rgba(0, 50, 126, 0.12),\n        34px 0 0 -6px rgba(0, 50, 126, 0.12),\n        35px 0 0 -6px rgba(0, 50, 126, 0.12),\n        36px 0 0 -6px rgba(0, 50, 126, 0.12),\n        37px 0 0 -6px rgba(0, 50, 126, 0.12),\n        38px 0 0 -6px rgba(0, 50, 126, 0.12),\n        39px 0 0 -6px rgba(0, 50, 126, 0.12),\n        40px 0 0 -6px rgba(0, 50, 126, 0.12),\n        41px 0 0 -6px rgba(0, 50, 126, 0.12),\n        42px 0 0 -6px rgba(0, 50, 126, 0.12),\n        43px 0 0 -6px rgba(0, 50, 126, 0.12),\n        44px 0 0 -6px rgba(0, 50, 126, 0.12),\n        45px 0 0 -6px rgba(0, 50, 126, 0.12),\n        46px 0 0 -6px rgba(0, 50, 126, 0.12),\n        47px 0 0 -6px rgba(0, 50, 126, 0.12),\n        48px 0 0 -6px rgba(0, 50, 126, 0.12),\n        49px 0 0 -6px rgba(0, 50, 126, 0.12),\n        50px 0 0 -6px rgba(0, 50, 126, 0.12),\n        51px 0 0 -6px rgba(0, 50, 126, 0.12),\n        52px 0 0 -6px rgba(0, 50, 126, 0.12),\n        53px 0 0 -6px rgba(0, 50, 126, 0.12),\n        54px 0 0 -6px rgba(0, 50, 126, 0.12),\n        55px 0 0 -6px rgba(0, 50, 126, 0.12),\n        56px 0 0 -6px rgba(0, 50, 126, 0.12),\n        57px 0 0 -6px rgba(0, 50, 126, 0.12),\n        58px 0 0 -6px rgba(0, 50, 126, 0.12),\n        59px 0 0 -6px rgba(0, 50, 126, 0.12),\n        60px 0 0 -6px rgba(0, 50, 126, 0.12),\n        61px 0 0 -6px rgba(0, 50, 126, 0.12),\n        62px 0 0 -6px rgba(0, 50, 126, 0.12),\n        63px 0 0 -6px rgba(0, 50, 126, 0.12),\n        64px 0 0 -6px rgba(0, 50, 126, 0.12),\n        65px 0 0 -6px rgba(0, 50, 126, 0.12),\n        66px 0 0 -6px rgba(0, 50, 126, 0.12),\n        67px 0 0 -6px rgba(0, 50, 126, 0.12),\n        68px 0 0 -6px rgba(0, 50, 126, 0.12),\n        69px 0 0 -6px rgba(0, 50, 126, 0.12),\n        70px 0 0 -6px rgba(0, 50, 126, 0.12),\n        71px 0 0 -6px rgba(0, 50, 126, 0.12),\n        72px 0 0 -6px rgba(0, 50, 126, 0.12),\n        73px 0 0 -6px rgba(0, 50, 126, 0.12),\n        74px 0 0 -6px rgba(0, 50, 126, 0.12),\n        75px 0 0 -6px rgba(0, 50, 126, 0.12),\n        76px 0 0 -6px rgba(0, 50, 126, 0.12),\n        77px 0 0 -6px rgba(0, 50, 126, 0.12),\n        78px 0 0 -6px rgba(0, 50, 126, 0.12),\n        79px 0 0 -6px rgba(0, 50, 126, 0.12),\n        80px 0 0 -6px rgba(0, 50, 126, 0.12),\n        81px 0 0 -6px rgba(0, 50, 126, 0.12),\n        82px 0 0 -6px rgba(0, 50, 126, 0.12),\n        83px 0 0 -6px rgba(0, 50, 126, 0.12),\n        84px 0 0 -6px rgba(0, 50, 126, 0.12),\n        85px 0 0 -6px rgba(0, 50, 126, 0.12),\n        86px 0 0 -6px rgba(0, 50, 126, 0.12),\n        87px 0 0 -6px rgba(0, 50, 126, 0.12),\n        88px 0 0 -6px rgba(0, 50, 126, 0.12),\n        89px 0 0 -6px rgba(0, 50, 126, 0.12),\n        90px 0 0 -6px rgba(0, 50, 126, 0.12),\n        91px 0 0 -6px rgba(0, 50, 126, 0.12),\n        92px 0 0 -6px rgba(0, 50, 126, 0.12),\n        93px 0 0 -6px rgba(0, 50, 126, 0.12),\n        94px 0 0 -6px rgba(0, 50, 126, 0.12),\n        95px 0 0 -6px rgba(0, 50, 126, 0.12),\n        96px 0 0 -6px rgba(0, 50, 126, 0.12),\n        97px 0 0 -6px rgba(0, 50, 126, 0.12),\n        98px 0 0 -6px rgba(0, 50, 126, 0.12),\n        99px 0 0 -6px rgba(0, 50, 126, 0.12),\n        100px 0 0 -6px rgba(0, 50, 126, 0.12),\n        101px 0 0 -6px rgba(0, 50, 126, 0.12),\n        102px 0 0 -6px rgba(0, 50, 126, 0.12),\n        103px 0 0 -6px rgba(0, 50, 126, 0.12),\n        104px 0 0 -6px rgba(0, 50, 126, 0.12),\n        105px 0 0 -6px rgba(0, 50, 126, 0.12),\n        106px 0 0 -6px rgba(0, 50, 126, 0.12),\n        107px 0 0 -6px rgba(0, 50, 126, 0.12),\n        108px 0 0 -6px rgba(0, 50, 126, 0.12),\n        109px 0 0 -6px rgba(0, 50, 126, 0.12),\n        110px 0 0 -6px rgba(0, 50, 126, 0.12),\n        111px 0 0 -6px rgba(0, 50, 126, 0.12),\n        112px 0 0 -6px rgba(0, 50, 126, 0.12),\n        113px 0 0 -6px rgba(0, 50, 126, 0.12),\n        114px 0 0 -6px rgba(0, 50, 126, 0.12),\n        115px 0 0 -6px rgba(0, 50, 126, 0.12),\n        116px 0 0 -6px rgba(0, 50, 126, 0.12),\n        117px 0 0 -6px rgba(0, 50, 126, 0.12),\n        118px 0 0 -6px rgba(0, 50, 126, 0.12),\n        119px 0 0 -6px rgba(0, 50, 126, 0.12),\n        120px 0 0 -6px rgba(0, 50, 126, 0.12),\n        121px 0 0 -6px rgba(0, 50, 126, 0.12),\n        122px 0 0 -6px rgba(0, 50, 126, 0.12),\n        123px 0 0 -6px rgba(0, 50, 126, 0.12),\n        124px 0 0 -6px rgba(0, 50, 126, 0.12),\n        125px 0 0 -6px rgba(0, 50, 126, 0.12),\n        126px 0 0 -6px rgba(0, 50, 126, 0.12),\n        127px 0 0 -6px rgba(0, 50, 126, 0.12),\n        128px 0 0 -6px rgba(0, 50, 126, 0.12),\n        129px 0 0 -6px rgba(0, 50, 126, 0.12),\n        130px 0 0 -6px rgba(0, 50, 126, 0.12),\n        131px 0 0 -6px rgba(0, 50, 126, 0.12),\n        132px 0 0 -6px rgba(0, 50, 126, 0.12),\n        133px 0 0 -6px rgba(0, 50, 126, 0.12),\n        134px 0 0 -6px rgba(0, 50, 126, 0.12),\n        135px 0 0 -6px rgba(0, 50, 126, 0.12),\n        136px 0 0 -6px rgba(0, 50, 126, 0.12),\n        137px 0 0 -6px rgba(0, 50, 126, 0.12),\n        138px 0 0 -6px rgba(0, 50, 126, 0.12),\n        139px 0 0 -6px rgba(0, 50, 126, 0.12),\n        140px 0 0 -6px rgba(0, 50, 126, 0.12),\n        141px 0 0 -6px rgba(0, 50, 126, 0.12),\n        142px 0 0 -6px rgba(0, 50, 126, 0.12),\n        143px 0 0 -6px rgba(0, 50, 126, 0.12),\n        144px 0 0 -6px rgba(0, 50, 126, 0.12),\n        145px 0 0 -6px rgba(0, 50, 126, 0.12),\n        146px 0 0 -6px rgba(0, 50, 126, 0.12),\n        147px 0 0 -6px rgba(0, 50, 126, 0.12),\n        148px 0 0 -6px rgba(0, 50, 126, 0.12),\n        149px 0 0 -6px rgba(0, 50, 126, 0.12),\n        150px 0 0 -6px rgba(0, 50, 126, 0.12),\n        151px 0 0 -6px rgba(0, 50, 126, 0.12),\n        152px 0 0 -6px rgba(0, 50, 126, 0.12),\n        153px 0 0 -6px rgba(0, 50, 126, 0.12),\n        154px 0 0 -6px rgba(0, 50, 126, 0.12),\n        155px 0 0 -6px rgba(0, 50, 126, 0.12),\n        156px 0 0 -6px rgba(0, 50, 126, 0.12),\n        157px 0 0 -6px rgba(0, 50, 126, 0.12),\n        158px 0 0 -6px rgba(0, 50, 126, 0.12),\n        159px 0 0 -6px rgba(0, 50, 126, 0.12),\n        160px 0 0 -6px rgba(0, 50, 126, 0.12),\n        161px 0 0 -6px rgba(0, 50, 126, 0.12),\n        162px 0 0 -6px rgba(0, 50, 126, 0.12),\n        163px 0 0 -6px rgba(0, 50, 126, 0.12),\n        164px 0 0 -6px rgba(0, 50, 126, 0.12),\n        165px 0 0 -6px rgba(0, 50, 126, 0.12),\n        166px 0 0 -6px rgba(0, 50, 126, 0.12),\n        167px 0 0 -6px rgba(0, 50, 126, 0.12),\n        168px 0 0 -6px rgba(0, 50, 126, 0.12),\n        169px 0 0 -6px rgba(0, 50, 126, 0.12),\n        170px 0 0 -6px rgba(0, 50, 126, 0.12),\n        171px 0 0 -6px rgba(0, 50, 126, 0.12),\n        172px 0 0 -6px rgba(0, 50, 126, 0.12),\n        173px 0 0 -6px rgba(0, 50, 126, 0.12),\n        174px 0 0 -6px rgba(0, 50, 126, 0.12),\n        175px 0 0 -6px rgba(0, 50, 126, 0.12),\n        176px 0 0 -6px rgba(0, 50, 126, 0.12),\n        177px 0 0 -6px rgba(0, 50, 126, 0.12),\n        178px 0 0 -6px rgba(0, 50, 126, 0.12),\n        179px 0 0 -6px rgba(0, 50, 126, 0.12),\n        180px 0 0 -6px rgba(0, 50, 126, 0.12),\n        181px 0 0 -6px rgba(0, 50, 126, 0.12),\n        182px 0 0 -6px rgba(0, 50, 126, 0.12),\n        183px 0 0 -6px rgba(0, 50, 126, 0.12),\n        184px 0 0 -6px rgba(0, 50, 126, 0.12),\n        185px 0 0 -6px rgba(0, 50, 126, 0.12),\n        186px 0 0 -6px rgba(0, 50, 126, 0.12),\n        187px 0 0 -6px rgba(0, 50, 126, 0.12),\n        188px 0 0 -6px rgba(0, 50, 126, 0.12),\n        189px 0 0 -6px rgba(0, 50, 126, 0.12),\n        190px 0 0 -6px rgba(0, 50, 126, 0.12),\n        191px 0 0 -6px rgba(0, 50, 126, 0.12),\n        192px 0 0 -6px rgba(0, 50, 126, 0.12),\n        193px 0 0 -6px rgba(0, 50, 126, 0.12),\n        194px 0 0 -6px rgba(0, 50, 126, 0.12),\n        195px 0 0 -6px rgba(0, 50, 126, 0.12),\n        196px 0 0 -6px rgba(0, 50, 126, 0.12),\n        197px 0 0 -6px rgba(0, 50, 126, 0.12),\n        198px 0 0 -6px rgba(0, 50, 126, 0.12),\n        199px 0 0 -6px rgba(0, 50, 126, 0.12),\n        200px 0 0 -6px rgba(0, 50, 126, 0.12),\n        201px 0 0 -6px rgba(0, 50, 126, 0.12),\n        202px 0 0 -6px rgba(0, 50, 126, 0.12),\n        203px 0 0 -6px rgba(0, 50, 126, 0.12),\n        204px 0 0 -6px rgba(0, 50, 126, 0.12),\n        205px 0 0 -6px rgba(0, 50, 126, 0.12),\n        206px 0 0 -6px rgba(0, 50, 126, 0.12),\n        207px 0 0 -6px rgba(0, 50, 126, 0.12),\n        208px 0 0 -6px rgba(0, 50, 126, 0.12),\n        209px 0 0 -6px rgba(0, 50, 126, 0.12),\n        210px 0 0 -6px rgba(0, 50, 126, 0.12),\n        211px 0 0 -6px rgba(0, 50, 126, 0.12),\n        212px 0 0 -6px rgba(0, 50, 126, 0.12),\n        213px 0 0 -6px rgba(0, 50, 126, 0.12),\n        214px 0 0 -6px rgba(0, 50, 126, 0.12),\n        215px 0 0 -6px rgba(0, 50, 126, 0.12),\n        216px 0 0 -6px rgba(0, 50, 126, 0.12),\n        217px 0 0 -6px rgba(0, 50, 126, 0.12),\n        218px 0 0 -6px rgba(0, 50, 126, 0.12),\n        219px 0 0 -6px rgba(0, 50, 126, 0.12),\n        220px 0 0 -6px rgba(0, 50, 126, 0.12),\n        221px 0 0 -6px rgba(0, 50, 126, 0.12),\n        222px 0 0 -6px rgba(0, 50, 126, 0.12),\n        223px 0 0 -6px rgba(0, 50, 126, 0.12),\n        224px 0 0 -6px rgba(0, 50, 126, 0.12),\n        225px 0 0 -6px rgba(0, 50, 126, 0.12),\n        226px 0 0 -6px rgba(0, 50, 126, 0.12),\n        227px 0 0 -6px rgba(0, 50, 126, 0.12),\n        228px 0 0 -6px rgba(0, 50, 126, 0.12),\n        229px 0 0 -6px rgba(0, 50, 126, 0.12),\n        230px 0 0 -6px rgba(0, 50, 126, 0.12),\n        231px 0 0 -6px rgba(0, 50, 126, 0.12),\n        232px 0 0 -6px rgba(0, 50, 126, 0.12),\n        233px 0 0 -6px rgba(0, 50, 126, 0.12),\n        234px 0 0 -6px rgba(0, 50, 126, 0.12),\n        235px 0 0 -6px rgba(0, 50, 126, 0.12),\n        236px 0 0 -6px rgba(0, 50, 126, 0.12),\n        237px 0 0 -6px rgba(0, 50, 126, 0.12),\n        238px 0 0 -6px rgba(0, 50, 126, 0.12),\n        239px 0 0 -6px rgba(0, 50, 126, 0.12),\n        240px 0 0 -6px rgba(0, 50, 126, 0.12);\n    margin-top: -6px;\n    border: 1px solid rgba(0, 30, 75, 0.12);\n    transition:\n        0.3s border-color,\n        0.3s background-color;\n}\n\n.custom-range::-moz-range-track {\n    width: 240px;\n    height: 2px;\n    background: rgba(0, 50, 126, 0.12);\n}\n\n.custom-range::-moz-range-thumb {\n    width: 14px;\n    height: 14px;\n    background: #fff;\n    border-radius: 50px;\n    border: 1px solid rgba(0, 30, 75, 0.12);\n    position: relative;\n    transition:\n        0.3s border-color,\n        0.3s background-color;\n}\n\n.custom-range::-moz-range-progress {\n    height: 2px;\n    background: #467fcf;\n    border: 0;\n    margin-top: 0;\n}\n\n.custom-range::-ms-track {\n    background: transparent;\n    border: 0;\n    border-color: transparent;\n    border-radius: 0;\n    border-width: 0;\n    color: transparent;\n    height: 2px;\n    margin-top: 10px;\n    width: 240px;\n}\n\n.custom-range::-ms-thumb {\n    width: 240px;\n    height: 2px;\n    background: #fff;\n    border-radius: 50px;\n    border: 1px solid rgba(0, 30, 75, 0.12);\n    transition:\n        0.3s border-color,\n        0.3s background-color;\n}\n\n.custom-range::-ms-fill-lower {\n    background: #467fcf;\n    border-radius: 0;\n}\n\n.custom-range::-ms-fill-upper {\n    background: rgba(0, 50, 126, 0.12);\n    border-radius: 0;\n}\n\n.custom-range::-ms-tooltip {\n    display: none;\n}\n\n.selectgroup {\n    display: -ms-inline-flexbox;\n    display: inline-flex;\n}\n\n.selectgroup-item {\n    -ms-flex-positive: 1;\n    flex-grow: 1;\n    position: relative;\n}\n\n.selectgroup-item + .selectgroup-item {\n    margin-left: -1px;\n}\n\n.selectgroup-item:not(:first-child) .selectgroup-button {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n}\n\n.selectgroup-item:not(:last-child) .selectgroup-button {\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n}\n\n.selectgroup-input {\n    opacity: 0;\n    position: absolute;\n    z-index: -1;\n    top: 0;\n    left: 0;\n}\n\n.selectgroup-button {\n    display: block;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    text-align: center;\n    padding: 0.375rem 1rem;\n    position: relative;\n    cursor: pointer;\n    border-radius: 3px;\n    color: #9aa0ac;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    font-size: 0.9375rem;\n    line-height: 1.5;\n    min-width: 2.375rem;\n}\n\n.selectgroup-button-icon {\n    padding-left: 0.5rem;\n    padding-right: 0.5rem;\n    font-size: 1.125rem;\n    line-height: 1.125rem;\n}\n\n.selectgroup-input:checked + .selectgroup-button {\n    border-color: #467fcf;\n    z-index: 1;\n    color: #467fcf;\n    background: #edf2fa;\n}\n\n.selectgroup-input:focus + .selectgroup-button {\n    border-color: #467fcf;\n    z-index: 2;\n    color: #467fcf;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.selectgroup-pills {\n    -ms-flex-wrap: wrap;\n    flex-wrap: wrap;\n    -ms-flex-align: start;\n    align-items: flex-start;\n}\n\n.selectgroup-pills .selectgroup-item {\n    margin-right: 0.5rem;\n    -ms-flex-positive: 0;\n    flex-grow: 0;\n}\n\n.selectgroup-pills .selectgroup-button {\n    border-radius: 50px !important;\n}\n\n.custom-switch {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    cursor: default;\n    display: -ms-inline-flexbox;\n    display: inline-flex;\n    -ms-flex-align: center;\n    align-items: center;\n    margin: 0;\n}\n\n.custom-switch-input {\n    position: absolute;\n    z-index: -1;\n    opacity: 0;\n}\n\n.custom-switches-stacked {\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-direction: column;\n    flex-direction: column;\n}\n\n.custom-switches-stacked .custom-switch {\n    margin-bottom: 0.5rem;\n}\n\n.custom-switch-indicator {\n    flex-shrink: 0;\n    display: inline-block;\n    height: 1.25rem;\n    width: 2.25rem;\n    background: #e9ecef;\n    border-radius: 50px;\n    position: relative;\n    vertical-align: bottom;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    transition:\n        0.3s border-color,\n        0.3s background-color;\n}\n\n[data-theme='dark'] .custom-switch-indicator {\n    opacity: 0.75;\n}\n\n.custom-switch-indicator:before {\n    content: '';\n    position: absolute;\n    height: calc(1.25rem - 4px);\n    width: calc(1.25rem - 4px);\n    top: 1px;\n    left: 1px;\n    background: #fff;\n    border-radius: 50%;\n    transition: 0.3s left;\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.4);\n}\n\n.custom-switch-input:checked ~ .custom-switch-indicator {\n    background: #467fcf;\n}\n\n.custom-switch-input:checked ~ .custom-switch-indicator:before {\n    left: calc(1rem + 1px);\n}\n\n.custom-switch-input:focus ~ .custom-switch-indicator {\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n    border-color: #467fcf;\n}\n\n.custom-switch-description {\n    margin-left: 0.5rem;\n    color: #6e7687;\n    transition: 0.3s color;\n}\n\n.custom-switch-input:checked ~ .custom-switch-description {\n    color: #495057;\n}\n\n.imagecheck {\n    margin: 0;\n    position: relative;\n    cursor: pointer;\n}\n\n.imagecheck-input {\n    position: absolute;\n    z-index: -1;\n    opacity: 0;\n}\n\n.imagecheck-figure {\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    border-radius: 3px;\n    margin: 0;\n    position: relative;\n}\n\n.imagecheck-input:focus ~ .imagecheck-figure {\n    border-color: #467fcf;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.imagecheck-input:checked ~ .imagecheck-figure {\n    border-color: rgba(0, 40, 100, 0.24);\n}\n\n.imagecheck-figure:before {\n    content: '';\n    position: absolute;\n    top: 0.25rem;\n    left: 0.25rem;\n    display: block;\n    width: 1rem;\n    height: 1rem;\n    pointer-events: none;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    background: #467fcf\n        url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E\")\n        no-repeat center center/50% 50%;\n    color: #fff;\n    z-index: 1;\n    border-radius: 3px;\n    opacity: 0;\n    transition: 0.3s opacity;\n}\n\n.imagecheck-input:checked ~ .imagecheck-figure:before {\n    opacity: 1;\n}\n\n.imagecheck-image {\n    max-width: 100%;\n    opacity: 0.64;\n    transition: 0.3s opacity;\n}\n\n.imagecheck-image:first-child {\n    border-top-left-radius: 2px;\n    border-top-right-radius: 2px;\n}\n\n.imagecheck-image:last-child {\n    border-bottom-left-radius: 2px;\n    border-bottom-right-radius: 2px;\n}\n\n.imagecheck:hover .imagecheck-image,\n.imagecheck-input:focus ~ .imagecheck-figure .imagecheck-image,\n.imagecheck-input:checked ~ .imagecheck-figure .imagecheck-image {\n    opacity: 1;\n}\n\n.imagecheck-caption {\n    text-align: center;\n    padding: 0.25rem 0.25rem;\n    color: #9aa0ac;\n    font-size: 0.875rem;\n    transition: 0.3s color;\n}\n\n.imagecheck:hover .imagecheck-caption,\n.imagecheck-input:focus ~ .imagecheck-figure .imagecheck-caption,\n.imagecheck-input:checked ~ .imagecheck-figure .imagecheck-caption {\n    color: #495057;\n}\n\n.colorinput {\n    margin: 0;\n    position: relative;\n    cursor: pointer;\n}\n\n.colorinput-input {\n    position: absolute;\n    z-index: -1;\n    opacity: 0;\n}\n\n.colorinput-color {\n    display: inline-block;\n    width: 1.75rem;\n    height: 1.75rem;\n    border-radius: 3px;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    color: #fff;\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n}\n\n.colorinput-color:before {\n    content: '';\n    opacity: 0;\n    position: absolute;\n    top: 0.25rem;\n    left: 0.25rem;\n    height: 1.25rem;\n    width: 1.25rem;\n    transition: 0.3s opacity;\n    background: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E\")\n        no-repeat center center/50% 50%;\n}\n\n.colorinput-input:checked ~ .colorinput-color:before {\n    opacity: 1;\n}\n\n.colorinput-input:focus ~ .colorinput-color {\n    border-color: #467fcf;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.timeline {\n    position: relative;\n    margin: 0 0 2rem;\n    padding: 0;\n    list-style: none;\n}\n\n.timeline:before {\n    background-color: #e9ecef;\n    position: absolute;\n    display: block;\n    content: '';\n    width: 1px;\n    height: 100%;\n    top: 0;\n    bottom: 0;\n    left: 4px;\n}\n\n.timeline-item {\n    position: relative;\n    display: -ms-flexbox;\n    display: flex;\n    padding-left: 2rem;\n    margin: 0.5rem 0;\n}\n\n.timeline-item:first-child:before,\n.timeline-item:last-child:before {\n    content: '';\n    position: absolute;\n    background: #fff;\n    width: 1px;\n    left: 0.25rem;\n}\n\n.timeline-item:first-child {\n    margin-top: 0;\n}\n\n.timeline-item:first-child:before {\n    top: 0;\n    height: 0.5rem;\n}\n\n.timeline-item:last-child {\n    margin-bottom: 0;\n}\n\n.timeline-item:last-child:before {\n    top: 0.5rem;\n    bottom: 0;\n}\n\n.timeline-badge {\n    position: absolute;\n    display: block;\n    width: 0.4375rem;\n    height: 0.4375rem;\n    left: 1px;\n    top: 0.5rem;\n    border-radius: 100%;\n    border: 1px solid #fff;\n    background: #adb5bd;\n}\n\n.timeline-time {\n    white-space: nowrap;\n    margin-left: auto;\n    color: #9aa0ac;\n    font-size: 87.5%;\n}\n\n.payment {\n    width: 2.5rem;\n    height: 1.5rem;\n    display: inline-block;\n    background: no-repeat center/100% 100%;\n    vertical-align: bottom;\n    font-style: normal;\n    box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.1);\n    border-radius: 2px;\n}\n\nsvg {\n    -ms-touch-action: none;\n    touch-action: none;\n}\n\n.jvectormap-container {\n    width: 100%;\n    height: 100%;\n    position: relative;\n    overflow: hidden;\n    -ms-touch-action: none;\n    touch-action: none;\n}\n\n.jvectormap-tip {\n    position: absolute;\n    display: none;\n    border-radius: 3px;\n    background: #212529;\n    color: white;\n    padding: 6px;\n    font-size: 11px;\n    line-height: 1;\n    font-weight: 700;\n}\n\n.jvectormap-tip small {\n    font-size: inherit;\n    font-weight: 400;\n}\n\n.jvectormap-zoomin,\n.jvectormap-zoomout,\n.jvectormap-goback {\n    position: absolute;\n    left: 10px;\n    border-radius: 3px;\n    background: #292929;\n    padding: 3px;\n    color: white;\n    cursor: pointer;\n    line-height: 10px;\n    text-align: center;\n    box-sizing: content-box;\n}\n\n.jvectormap-zoomin,\n.jvectormap-zoomout {\n    width: 10px;\n    height: 10px;\n}\n\n.jvectormap-zoomin {\n    top: 10px;\n}\n\n.jvectormap-zoomout {\n    top: 30px;\n}\n\n.jvectormap-goback {\n    bottom: 10px;\n    z-index: 1000;\n    padding: 6px;\n}\n\n.jvectormap-spinner {\n    position: absolute;\n    left: 0;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    background: url(data:image/gif;base64,R0lGODlhIAAgAPMAAP///wAAAMbGxoSEhLa2tpqamjY2NlZWVtjY2OTk5Ly8vB4eHgQEBAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ/V/nmOM82XiHRLYKhKP1oZmADdEAAAh+QQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY/CZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB+A4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6+Ho7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq+B6QDtuetcaBPnW6+O7wDHpIiK9SaVK5GgV543tzjgGcghAgAh+QQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK++G+w48edZPK+M6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE+G+cD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm+FNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk+aV+oJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0/VNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc+XiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30/iI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE/jiuL04RGEBgwWhShRgQExHBAAh+QQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR+ipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY+Yip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd+MFCN6HAAIKgNggY0KtEBAAh+QQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1+vsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d+jYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg+ygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0+bm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h+Kr0SJ8MFihpNbx+4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX+BP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA==);\n}\n\n.jvectormap-legend-title {\n    font-weight: bold;\n    font-size: 14px;\n    text-align: center;\n}\n\n.jvectormap-legend-cnt {\n    position: absolute;\n}\n\n.jvectormap-legend-cnt-h {\n    bottom: 0;\n    right: 0;\n}\n\n.jvectormap-legend-cnt-v {\n    top: 0;\n    right: 0;\n}\n\n.jvectormap-legend {\n    background: black;\n    color: white;\n    border-radius: 3px;\n}\n\n.jvectormap-legend-cnt-h .jvectormap-legend {\n    float: left;\n    margin: 0 10px 10px 0;\n    padding: 3px 3px 1px 3px;\n}\n\n.jvectormap-legend-cnt-h .jvectormap-legend .jvectormap-legend-tick {\n    float: left;\n}\n\n.jvectormap-legend-cnt-v .jvectormap-legend {\n    margin: 10px 10px 0 0;\n    padding: 3px;\n}\n\n.jvectormap-legend-cnt-h .jvectormap-legend-tick {\n    width: 40px;\n}\n\n.jvectormap-legend-cnt-h .jvectormap-legend-tick-sample {\n    height: 15px;\n}\n\n.jvectormap-legend-cnt-v .jvectormap-legend-tick-sample {\n    height: 20px;\n    width: 20px;\n    display: inline-block;\n    vertical-align: middle;\n}\n\n.jvectormap-legend-tick-text {\n    font-size: 12px;\n}\n\n.jvectormap-legend-cnt-h .jvectormap-legend-tick-text {\n    text-align: center;\n}\n\n.jvectormap-legend-cnt-v .jvectormap-legend-tick-text {\n    display: inline-block;\n    vertical-align: middle;\n    line-height: 20px;\n    padding-left: 3px;\n}\n\n/**\n * selectize.css (v0.12.4)\n * Copyright (c) 2013–2015 Brian Reavis & contributors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this\n * file except in compliance with the License. You may obtain a copy of the License at:\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF\n * ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n *\n * @author Brian Reavis <brian@thirdroute.com>\n */\n\n.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {\n    visibility: visible !important;\n    background: #f2f2f2 !important;\n    background: rgba(0, 0, 0, 0.06) !important;\n    border: 0 none !important;\n    box-shadow: inset 0 0 12px 4px #fff;\n}\n\n.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {\n    content: '!';\n    visibility: hidden;\n}\n\n.selectize-control.plugin-drag_drop .ui-sortable-helper {\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\n}\n\n.selectize-dropdown-header {\n    position: relative;\n    padding: 5px 8px;\n    border-bottom: 1px solid #d0d0d0;\n    background: #f8f8f8;\n    border-radius: 3px 3px 0 0;\n}\n\n.selectize-dropdown-header-close {\n    position: absolute;\n    right: 8px;\n    top: 50%;\n    color: #495057;\n    opacity: 0.4;\n    margin-top: -12px;\n    line-height: 20px;\n    font-size: 20px !important;\n}\n\n.selectize-dropdown-header-close:hover {\n    color: #000;\n}\n\n.selectize-dropdown.plugin-optgroup_columns .optgroup {\n    border-right: 1px solid #f2f2f2;\n    border-top: 0 none;\n    float: left;\n    box-sizing: border-box;\n}\n\n.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {\n    border-right: 0 none;\n}\n\n.selectize-dropdown.plugin-optgroup_columns .optgroup:before {\n    display: none;\n}\n\n.selectize-dropdown.plugin-optgroup_columns .optgroup-header {\n    border-top: 0 none;\n}\n\n.selectize-control.plugin-remove_button [data-value] {\n    position: relative;\n    padding-right: 24px !important;\n}\n\n.selectize-control.plugin-remove_button [data-value] .remove {\n    z-index: 1;\n    /* fixes ie bug (see #392) */\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    width: 17px;\n    text-align: center;\n    font-weight: bold;\n    font-size: 12px;\n    color: inherit;\n    text-decoration: none;\n    vertical-align: middle;\n    display: inline-block;\n    padding: 2px 0 0 0;\n    border-left: 1px solid #d0d0d0;\n    border-radius: 0 2px 2px 0;\n    box-sizing: border-box;\n}\n\n.selectize-control.plugin-remove_button [data-value] .remove:hover {\n    background: rgba(0, 0, 0, 0.05);\n}\n\n.selectize-control.plugin-remove_button [data-value].active .remove {\n    border-left-color: #cacaca;\n}\n\n.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover {\n    background: none;\n}\n\n.selectize-control.plugin-remove_button .disabled [data-value] .remove {\n    border-left-color: #fff;\n}\n\n.selectize-control.plugin-remove_button .remove-single {\n    position: absolute;\n    right: 28px;\n    top: 6px;\n    font-size: 23px;\n}\n\n.selectize-control {\n    position: relative;\n    padding: 0;\n    border: 0;\n}\n\n.selectize-dropdown,\n.selectize-input,\n.selectize-input input {\n    color: #495057;\n    font-family: inherit;\n    font-size: 15px;\n    line-height: 18px;\n    -webkit-font-smoothing: inherit;\n}\n\n.selectize-input,\n.selectize-control.single .selectize-input.input-active {\n    background: #fff;\n    cursor: text;\n    display: inline-block;\n}\n\n.selectize-input {\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    padding: 0.5625rem 0.75rem;\n    display: inline-block;\n    display: block;\n    width: 100%;\n    overflow: hidden;\n    position: relative;\n    z-index: 1;\n    box-sizing: border-box;\n    border-radius: 3px;\n    transition:\n        0.3s border-color,\n        0.3s box-shadow;\n}\n\n.selectize-control.multi .selectize-input.has-items {\n    padding: 7px 0.75rem 4px 7px;\n}\n\n.selectize-input.full {\n    background-color: #fff;\n}\n\n.selectize-input.disabled,\n.selectize-input.disabled * {\n    cursor: default !important;\n}\n\n.selectize-input.focus {\n    border-color: #467fcf;\n    box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);\n}\n\n.selectize-input.dropdown-active {\n    border-radius: 3px 3px 0 0;\n}\n\n.selectize-input > * {\n    vertical-align: baseline;\n    display: -moz-inline-stack;\n    display: inline-block;\n    zoom: 1;\n    *display: inline;\n}\n\n.selectize-control.multi .selectize-input > div {\n    cursor: pointer;\n    margin: 0 3px 3px 0;\n    padding: 2px 6px;\n    background: #e9ecef;\n    color: #495057;\n    font-size: 13px;\n    border: 0 solid rgba(0, 40, 100, 0.12);\n    border-radius: 3px;\n    font-weight: 400;\n}\n\n.selectize-control.multi .selectize-input > div.active {\n    background: #e8e8e8;\n    color: #303030;\n    border: 0 solid #cacaca;\n}\n\n.selectize-control.multi .selectize-input.disabled > div,\n.selectize-control.multi .selectize-input.disabled > div.active {\n    color: #7d7d7d;\n    background: #fff;\n    border: 0 solid #fff;\n}\n\n.selectize-input > input {\n    display: inline-block !important;\n    padding: 0 !important;\n    min-height: 0 !important;\n    max-height: none !important;\n    max-width: 100% !important;\n    margin: 0 2px 0 0 !important;\n    text-indent: 0 !important;\n    border: 0 none !important;\n    background: none !important;\n    line-height: inherit !important;\n    box-shadow: none !important;\n}\n\n.selectize-input > input::-ms-clear {\n    display: none;\n}\n\n.selectize-input > input:focus {\n    outline: none !important;\n}\n\n.selectize-input::after {\n    content: ' ';\n    display: block;\n    clear: left;\n}\n\n.selectize-input.dropdown-active::before {\n    content: ' ';\n    display: block;\n    position: absolute;\n    background: #f0f0f0;\n    height: 1px;\n    bottom: 0;\n    left: 0;\n    right: 0;\n}\n\n.selectize-dropdown {\n    position: absolute;\n    z-index: 10;\n    border: 1px solid rgba(0, 40, 100, 0.12);\n    background: #fff;\n    margin: -1px 0 0 0;\n    border-top: 0 none;\n    box-sizing: border-box;\n    border-radius: 0 0 3px 3px;\n    height: auto;\n    padding: 0;\n}\n\n.selectize-dropdown [data-selectable] {\n    cursor: pointer;\n    overflow: hidden;\n}\n\n.selectize-dropdown [data-selectable] .highlight {\n    background: rgba(125, 168, 208, 0.2);\n    border-radius: 1px;\n}\n\n.selectize-dropdown [data-selectable],\n.selectize-dropdown .optgroup-header {\n    padding: 6px 0.75rem;\n}\n\n.selectize-dropdown .optgroup:first-child .optgroup-header {\n    border-top: 0 none;\n}\n\n.selectize-dropdown .optgroup-header {\n    color: #495057;\n    background: #fff;\n    cursor: default;\n}\n\n.selectize-dropdown .active {\n    background-color: #f1f4f8;\n    color: #467fcf;\n}\n\n.selectize-dropdown .active.create {\n    color: #495057;\n}\n\n.selectize-dropdown .create {\n    color: rgba(48, 48, 48, 0.5);\n}\n\n.selectize-dropdown-content {\n    overflow-y: auto;\n    overflow-x: hidden;\n    max-height: 200px;\n    -webkit-overflow-scrolling: touch;\n}\n\n.selectize-control.single .selectize-input,\n.selectize-control.single .selectize-input input {\n    cursor: pointer;\n}\n\n.selectize-control.single .selectize-input.input-active,\n.selectize-control.single .selectize-input.input-active input {\n    cursor: text;\n}\n\n.selectize-control.single .selectize-input:after {\n    content: '';\n    display: block;\n    position: absolute;\n    top: 13px;\n    right: 12px;\n    width: 8px;\n    height: 10px;\n    background: #fff\n        url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'%3E%3Cpath fill='#999' d='M0 0L10 0L5 5L0 0'/%3E%3C/svg%3E\")\n        no-repeat center;\n    background-size: 8px 10px;\n    transition: 0.3s transform;\n}\n\n.selectize-control.single .selectize-input.dropdown-active:after {\n    -webkit-transform: rotate(180deg);\n    transform: rotate(180deg);\n}\n\n.selectize-control .selectize-input.disabled {\n    opacity: 0.5;\n    background-color: #fafafa;\n}\n\n.selectize-dropdown .image,\n.selectize-input .image {\n    width: 1.25rem;\n    height: 1.25rem;\n    background-size: contain;\n    margin: -1px 0.5rem -1px -4px;\n    line-height: 1.25rem;\n    float: left;\n    display: -ms-flexbox;\n    display: flex;\n    -ms-flex-align: center;\n    align-items: center;\n    -ms-flex-pack: center;\n    justify-content: center;\n}\n\n.selectize-dropdown .image img,\n.selectize-input .image img {\n    max-width: 100%;\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.4);\n    border-radius: 2px;\n}\n\n.selectize-input .image {\n    width: 1.5rem;\n    height: 1.5rem;\n    margin: -3px 0.75rem -3px -5px;\n}\n\n.fe {\n    speak: none;\n    text-transform: none;\n    line-height: 1;\n}\n\n.fe:before {\n    content: '';\n    display: inline-block;\n    width: 15px;\n    height: 15px;\n    background-size: 100%;\n    background-repeat: no-repeat;\n}\n/* stylelint-enable */\n"
  },
  {
    "path": "client/src/components/ui/Tabs.css",
    "content": ".tabs__controls {\n    display: flex;\n    justify-content: space-between;\n    margin-bottom: 15px;\n    padding: 10px 0;\n    border-bottom: 1px solid var(--card-border-color);\n    overflow: auto;\n}\n\n@media screen and (min-width: 768px) {\n    .tabs__controls {\n        padding: 15px 0;\n        overflow: initial;\n    }\n}\n\n.tabs__controls--form {\n    justify-content: flex-start;\n}\n\n.tabs__controls--form .tab__control {\n    min-width: initial;\n    margin-right: 25px;\n    font-size: 14px;\n}\n\n.tabs__controls--form .tab__icon {\n    display: none;\n}\n\n.tab__control {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    min-width: 70px;\n    font-size: 13px;\n    white-space: nowrap;\n    color: #555555;\n    cursor: pointer;\n    opacity: 0.6;\n}\n\n[data-theme='dark'] .tab__control {\n    filter: invert(1);\n}\n\n@media screen and (min-width: 768px) {\n    .tab__control {\n        white-space: normal;\n    }\n}\n\n.tab__control:hover,\n.tab__control:focus {\n    opacity: 1;\n}\n\n.tab__control--active {\n    color: #4a4a4a;\n    opacity: 1;\n}\n\n.tab__title {\n    margin-bottom: 10px;\n    font-size: 16px;\n    font-weight: 700;\n}\n\n.tab__icon {\n    width: 24px;\n    height: 24px;\n    margin-bottom: 6px;\n    fill: #4a4a4a;\n    touch-action: initial;\n}\n\n.tab__text {\n    line-height: 1.7;\n}\n\n.tab__text li,\n.tab__text p {\n    margin-bottom: 5px;\n}\n\n.tab__text ul,\n.tab__text ol {\n    padding-left: 25px;\n}\n\n.tab__paragraph {\n    margin-bottom: 10px;\n}\n"
  },
  {
    "path": "client/src/components/ui/Tabs.tsx",
    "content": "import React from 'react';\nimport classnames from 'classnames';\n\nimport Tab from './Tab';\nimport './Tabs.css';\n\ninterface TabsProps {\n    controlClass?: string;\n    tabs: object;\n    activeTabLabel: string;\n    setActiveTabLabel: (...args: unknown[]) => unknown;\n    children: React.ReactElement;\n}\n\nconst Tabs = (props: TabsProps) => {\n    const { tabs, controlClass, activeTabLabel, setActiveTabLabel, children: activeTab } = props;\n\n    const onClickTabControl = (tabLabel: any) => setActiveTabLabel(tabLabel);\n\n    const getControlClass = classnames({\n        tabs__controls: true,\n        [`tabs__controls--${controlClass}`]: controlClass,\n    });\n\n    return (\n        <div className=\"tabs\">\n            <div className={getControlClass}>\n                {Object.values(tabs).map((props: any) => {\n                    // eslint-disable-next-line react/prop-types\n                    const { title, label = title } = props;\n                    return (\n                        <Tab\n                            key={label}\n                            label={label}\n                            title={title}\n                            activeTabLabel={activeTabLabel}\n                            onClick={onClickTabControl}\n                        />\n                    );\n                })}\n            </div>\n\n            <div className=\"tabs__content\">{activeTab}</div>\n        </div>\n    );\n};\n\nexport default Tabs;\n"
  },
  {
    "path": "client/src/components/ui/Tooltip.css",
    "content": ".tooltip-container {\n    border: 0;\n    padding: 0.7rem;\n    box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);\n}\n\n[data-theme='dark'] .tooltip-container {\n    background-color: var(--ctrl-select-bgcolor);\n    color: var(--mcolor);\n}\n\n.tooltip-custom--narrow {\n    max-width: 14rem;\n}\n\n.tooltip-custom--wide {\n    max-width: 18rem;\n}\n\n.tooltip-custom__trigger {\n    cursor: pointer;\n}\n\n.tooltip-custom__content-title {\n    margin-bottom: 0.1875rem;\n}\n\n.tooltip-custom__content-item {\n    margin-bottom: 0.125rem;\n}\n\n.tooltip-custom__content-item:last-child {\n    margin-bottom: 0;\n}\n\n.tooltip-custom__content-link {\n    color: var(--green-86);\n}\n"
  },
  {
    "path": "client/src/components/ui/Tooltip.tsx",
    "content": "import React from 'react';\nimport PopperJS from 'popper.js';\nimport TooltipTrigger, { TriggerTypes } from 'react-popper-tooltip';\nimport { useTranslation } from 'react-i18next';\n\nimport { HIDE_TOOLTIP_DELAY, MEDIUM_SCREEN_SIZE, SHOW_TOOLTIP_DELAY } from '../../helpers/constants';\nimport 'react-popper-tooltip/dist/styles.css';\nimport './Tooltip.css';\n\ninterface TooltipProps {\n    children: React.ReactElement;\n    content: string | React.ReactElement | React.ReactElement[];\n    placement?: PopperJS.Placement;\n    trigger?: TriggerTypes;\n    delayHide?: number;\n    delayShow?: number;\n    className?: string;\n    triggerClass?: string;\n    onVisibilityChange?: (...args: unknown[]) => unknown;\n    defaultTooltipShown?: boolean;\n}\n\ninterface renderTooltipProps {\n    tooltipRef?: object;\n    getTooltipProps?: (...args: unknown[]) => Record<any, any>;\n}\n\ninterface renderTriggerProps {\n    triggerRef?: object;\n    getTriggerProps?: (...args: unknown[]) => Record<any, any>;\n}\n\nconst Tooltip = ({\n    children,\n    content,\n    triggerClass = 'tooltip-custom__trigger',\n    className = 'tooltip-container',\n    placement = 'bottom',\n    trigger = 'hover',\n    delayShow = SHOW_TOOLTIP_DELAY,\n    delayHide = HIDE_TOOLTIP_DELAY,\n    onVisibilityChange,\n    defaultTooltipShown,\n}: TooltipProps) => {\n    const { t } = useTranslation();\n    const touchEventsAvailable = 'ontouchstart' in window;\n\n    let triggerValue = trigger;\n    let delayHideValue = delayHide;\n    let delayShowValue = delayShow;\n\n    if (window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`).matches || touchEventsAvailable) {\n        triggerValue = 'click';\n        delayHideValue = 0;\n        delayShowValue = 0;\n    }\n\n    const renderTooltip = ({ tooltipRef, getTooltipProps }: renderTooltipProps) => (\n        <div\n            {...getTooltipProps({\n                ref: tooltipRef,\n                className,\n            })}>\n            {typeof content === 'string' ? t(content) : content}\n        </div>\n    );\n\n    const renderTrigger = ({ getTriggerProps, triggerRef }: renderTriggerProps) => (\n        <span\n            {...getTriggerProps({\n                ref: triggerRef,\n                className: triggerClass,\n            })}>\n            {children}\n        </span>\n    );\n\n    return (\n        <TooltipTrigger\n            placement={placement}\n            trigger={triggerValue}\n            delayHide={delayHideValue}\n            delayShow={delayShowValue}\n            tooltip={renderTooltip}\n            onVisibilityChange={onVisibilityChange}\n            defaultTooltipShown={defaultTooltipShown}>\n            {renderTrigger}\n        </TooltipTrigger>\n    );\n};\n\nexport default Tooltip;\n"
  },
  {
    "path": "client/src/components/ui/Topline.css",
    "content": ".topline {\n    position: relative;\n    z-index: 102;\n    margin-bottom: 0;\n    padding: 0.75rem 0;\n}\n"
  },
  {
    "path": "client/src/components/ui/Topline.tsx",
    "content": "import React from 'react';\n\nimport './Topline.css';\n\ninterface ToplineProps {\n    children: React.ReactNode;\n    type: string;\n}\n\nconst Topline = (props: ToplineProps) => (\n    <div className={`alert alert-${props.type} topline`}>\n        <div className=\"container\">{props.children}</div>\n    </div>\n);\n\nexport default Topline;\n"
  },
  {
    "path": "client/src/components/ui/UpdateOverlay.tsx",
    "content": "import React from 'react';\nimport { Trans } from 'react-i18next';\nimport classnames from 'classnames';\nimport { useSelector } from 'react-redux';\nimport './Overlay.css';\nimport { RootState } from '../../initialState';\n\nconst UpdateOverlay = () => {\n    const processingUpdate = useSelector((state: RootState) => state.dashboard.processingUpdate);\n    const overlayClass = classnames('overlay', {\n        'overlay--visible': processingUpdate,\n    });\n\n    return (\n        <div className={overlayClass}>\n            <div className=\"overlay__loading\"></div>\n\n            <Trans>processing_update</Trans>\n        </div>\n    );\n};\n\nexport default UpdateOverlay;\n"
  },
  {
    "path": "client/src/components/ui/UpdateTopline.tsx",
    "content": "import React from 'react';\nimport { Trans } from 'react-i18next';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport Topline from './Topline';\n\nimport { getUpdate } from '../../actions';\nimport { MANUAL_UPDATE_LINK } from '../../helpers/constants';\nimport { RootState } from '../../initialState';\n\nconst UpdateTopline = () => {\n    const { announcementUrl, newVersion, canAutoUpdate, processingUpdate } = useSelector(\n        (state: RootState) => state.dashboard,\n        shallowEqual,\n    );\n    const dispatch = useDispatch();\n\n    const handleUpdate = () => {\n        dispatch(getUpdate());\n    };\n\n    return (\n        <Topline type=\"info\">\n            <>\n                <Trans\n                    values={{ version: newVersion }}\n                    components={[\n                        <a href={announcementUrl} target=\"_blank\" rel=\"noopener noreferrer\" key=\"0\">\n                            Click here\n                        </a>,\n                    ]}>\n                    update_announcement\n                </Trans>\n                &nbsp;\n                {canAutoUpdate ? (\n                    <button\n                        type=\"button\"\n                        className=\"btn btn-sm btn-primary ml-3\"\n                        onClick={handleUpdate}\n                        disabled={processingUpdate}>\n                        <Trans>update_now</Trans>\n                    </button>\n                ) : (\n                    <Trans\n                        components={{\n                            a: (\n                                <a href={MANUAL_UPDATE_LINK} target=\"_blank\" rel=\"noopener noreferrer\" key=\"0\">\n                                    Link\n                                </a>\n                            ),\n                        }}>\n                        manual_update\n                    </Trans>\n                )}\n            </>\n        </Topline>\n    );\n};\n\nexport default UpdateTopline;\n"
  },
  {
    "path": "client/src/components/ui/Version.css",
    "content": ".version {\n    font-size: 0.8rem;\n}\n\n@media screen and (min-width: 1280px) {\n    .version {\n        font-size: 0.85rem;\n    }\n}\n\n.version__value {\n    font-weight: 600;\n}\n\n@media screen and (min-width: 992px) {\n    .version__value {\n        max-width: 100%;\n        overflow: visible;\n    }\n}\n\n.version__link {\n    position: relative;\n    display: inline-block;\n    border-bottom: 1px dashed #495057;\n    cursor: pointer;\n}\n\n.version__text {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n@media screen and (min-width: 992px) {\n    .version__text {\n        justify-content: flex-end;\n    }\n}\n"
  },
  {
    "path": "client/src/components/ui/Version.tsx",
    "content": "import React from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { shallowEqual, useDispatch, useSelector } from 'react-redux';\n\nimport { getVersion } from '../../actions';\nimport './Version.css';\nimport { RootState } from '../../initialState';\n\nconst Version = () => {\n    const dispatch = useDispatch();\n    const { t } = useTranslation();\n    const dashboard = useSelector((state: RootState) => state.dashboard, shallowEqual);\n    const install = useSelector((state: RootState) => state.install, shallowEqual);\n\n    if (!dashboard && !install) {\n        return null;\n    }\n\n    const version = dashboard?.dnsVersion || install?.dnsVersion;\n\n    const onClick = () => {\n        dispatch(getVersion(true));\n    };\n\n    return (\n        <div className=\"version\">\n            <div className=\"version__text\">\n                {version && (\n                    <>\n                        <Trans>version</Trans>:&nbsp;\n                        <span className=\"version__value\" title={version}>\n                            {version}\n                        </span>\n                    </>\n                )}\n\n                {dashboard?.checkUpdateFlag && (\n                    <button\n                        type=\"button\"\n                        className=\"btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2\"\n                        onClick={onClick}\n                        disabled={dashboard?.processingVersion}\n                        title={t('check_updates_now')}>\n                        <svg className=\"icons icon12\">\n                            <use xlinkHref=\"#refresh\" />\n                        </svg>\n                    </button>\n                )}\n            </div>\n        </div>\n    );\n};\n\nexport default Version;\n"
  },
  {
    "path": "client/src/components/ui/svg/logo.tsx",
    "content": "import React, { memo } from 'react';\n\ntype Props = {\n    className?: string;\n};\n\nexport const Logo = memo(({ className }: Props) => {\n    return (\n        <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"164\" height=\"41\" viewBox=\"0 0 164 41\" className={className}>\n            <g fillRule=\"evenodd\">\n                <path d=\"M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z\" />\n                <path\n                    fillRule=\"nonzero\"\n                    d=\"M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z\"\n                />\n                <path d=\"M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27\" />\n            </g>\n        </svg>\n    );\n});\n\nLogo.displayName = 'Logo';\n"
  },
  {
    "path": "client/src/components/ui/texareaCommentsHighlight.css",
    "content": ".text-edit-container {\n    position: relative;\n    min-height: 240px;\n    overflow: hidden;\n}\n\n.text-input,\n.text-output {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    padding: 16px;\n    background: transparent;\n    white-space: pre-wrap;\n    line-height: 24px;\n    word-wrap: break-word;\n    font-size: var(--font-size-disable-autozoom);\n    margin: 0;\n    overscroll-behavior: none;\n}\n\n.text-input {\n    position: relative;\n    opacity: 1;\n    min-height: 240px;\n}\n\n.text-output {\n    pointer-events: none;\n    z-index: 3;\n    overflow-y: auto;\n    background: transparent;\n    border: 1px solid transparent;\n}\n\n.text-transparent {\n    color: transparent;\n}\n"
  },
  {
    "path": "client/src/configureStore.ts",
    "content": "import { createStore, applyMiddleware, compose, Reducer } from 'redux';\nimport thunk from 'redux-thunk';\n\nconst middlewares = [thunk];\n\nexport default function configureStore<T>(\n    reducer: Reducer<T>,\n    initialState: any\n) {\n    const store = createStore(\n        reducer,\n        initialState,\n        compose(applyMiddleware(...middlewares))\n    );\n    return store;\n}\n"
  },
  {
    "path": "client/src/containers/Clients.ts",
    "content": "import { connect } from 'react-redux';\n\nimport { getClients } from '../actions';\nimport { getStats } from '../actions/stats';\nimport { addClient, updateClient, deleteClient, toggleClientModal } from '../actions/clients';\n\nimport Clients from '../components/Settings/Clients';\n\nconst mapStateToProps = (state: any) => {\n    const { dashboard, clients, stats } = state;\n    const props = {\n        dashboard,\n        clients,\n        stats,\n    };\n    return props;\n};\n\ntype DispatchProps = {\n    getClients: (dispatch: any) => void;\n    getStats: (...args: unknown[]) => unknown;\n    addClient: (dispatch: any) => void;\n    updateClient: (config: any, name: any) => (dispatch: any) => void;\n    deleteClient: (config: any, name: any) => (dispatch: any) => void;\n    toggleClientModal: (...args: unknown[]) => unknown;\n}\n\nconst mapDispatchToProps: DispatchProps = {\n    getClients,\n    getStats,\n    addClient,\n    updateClient,\n    deleteClient,\n    toggleClientModal,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Clients);\n"
  },
  {
    "path": "client/src/containers/CustomRules.ts",
    "content": "import { connect } from 'react-redux';\nimport { setRules, getFilteringStatus, handleRulesChange, checkHost } from '../actions/filtering';\n\nimport CustomRules from '../components/Filters/CustomRules';\nimport { RootState } from '../initialState';\n\nconst mapStateToProps = (state: RootState) => {\n    const { filtering } = state;\n    const props = { filtering };\n    return props;\n};\n\ntype DispatchProps = {\n    setRules: (...args: unknown[]) => unknown;\n    getFilteringStatus: (...args: unknown[]) => unknown;\n    handleRulesChange: (...args: unknown[]) => unknown;\n    checkHost: (dispatch: any) => void;\n}\n\nconst mapDispatchToProps: DispatchProps = {\n    setRules,\n    getFilteringStatus,\n    handleRulesChange,\n    checkHost,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(CustomRules);\n"
  },
  {
    "path": "client/src/containers/Dashboard.ts",
    "content": "import { connect } from 'react-redux';\n\nimport { toggleProtection, getClients } from '../actions';\nimport { getStats, getStatsConfig } from '../actions/stats';\nimport { getAccessList } from '../actions/access';\n\nimport Dashboard from '../components/Dashboard';\nimport { RootState } from '../initialState';\n\nconst mapStateToProps = (state: RootState) => {\n    const { dashboard, stats, access } = state;\n    const props = { dashboard, stats, access };\n    return props;\n};\n\ntype DispatchProps = {\n    toggleProtection: (...args: unknown[]) => unknown;\n    getClients: (...args: unknown[]) => unknown;\n    getStats: (...args: unknown[]) => unknown;\n    getStatsConfig: (...args: unknown[]) => unknown;\n    getAccessList: () => (dispatch: any) => void;\n};\n\nconst mapDispatchToProps: DispatchProps = {\n    toggleProtection,\n    getClients,\n    getStats,\n    getStatsConfig,\n    getAccessList,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Dashboard);\n"
  },
  {
    "path": "client/src/containers/Dhcp.ts",
    "content": "import { connect } from 'react-redux';\nimport {\n    toggleDhcp,\n    getDhcpStatus,\n    getDhcpInterfaces,\n    setDhcpConfig,\n    findActiveDhcp,\n    toggleLeaseModal,\n    addStaticLease,\n    removeStaticLease,\n    resetDhcp,\n} from '../actions';\n\nimport Dhcp from '../components/Settings/Dhcp';\n\nconst mapStateToProps = (state: any) => {\n    const { dhcp } = state;\n    const props = {\n        dhcp,\n    };\n    return props;\n};\n\nconst mapDispatchToProps = {\n    toggleDhcp,\n    getDhcpStatus,\n    getDhcpInterfaces,\n    setDhcpConfig,\n    findActiveDhcp,\n    toggleLeaseModal,\n    addStaticLease,\n    removeStaticLease,\n    resetDhcp,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Dhcp);\n"
  },
  {
    "path": "client/src/containers/Dns.ts",
    "content": "import { connect } from 'react-redux';\nimport { getAccessList, setAccessList } from '../actions/access';\nimport { getRewritesList, addRewrite, deleteRewrite, toggleRewritesModal } from '../actions/rewrites';\nimport { getDnsConfig, setDnsConfig } from '../actions/dnsConfig';\n\nimport Dns from '../components/Settings/Dns';\n\nconst mapStateToProps = (state: any) => {\n    const { dashboard, settings, access, rewrites, dnsConfig } = state;\n    const props = {\n        dashboard,\n        settings,\n        access,\n        rewrites,\n        dnsConfig,\n    };\n    return props;\n};\n\nconst mapDispatchToProps = {\n    getAccessList,\n    setAccessList,\n    getRewritesList,\n    addRewrite,\n    deleteRewrite,\n    toggleRewritesModal,\n    getDnsConfig,\n    setDnsConfig,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Dns);\n"
  },
  {
    "path": "client/src/containers/DnsAllowlist.ts",
    "content": "import { connect } from 'react-redux';\nimport {\n    setRules,\n    getFilteringStatus,\n    addFilter,\n    removeFilter,\n    toggleFilterStatus,\n    toggleFilteringModal,\n    refreshFilters,\n    handleRulesChange,\n    editFilter,\n} from '../actions/filtering';\n\nimport DnsAllowlist from '../components/Filters/DnsAllowlist';\n\nconst mapStateToProps = (state: any) => {\n    const { filtering } = state;\n    const props = { filtering };\n    return props;\n};\n\nconst mapDispatchToProps = {\n    setRules,\n    getFilteringStatus,\n    addFilter,\n    removeFilter,\n    toggleFilterStatus,\n    toggleFilteringModal,\n    refreshFilters,\n    handleRulesChange,\n    editFilter,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(DnsAllowlist);\n"
  },
  {
    "path": "client/src/containers/DnsBlocklist.ts",
    "content": "import { connect } from 'react-redux';\nimport {\n    setRules,\n    getFilteringStatus,\n    addFilter,\n    removeFilter,\n    toggleFilterStatus,\n    toggleFilteringModal,\n    refreshFilters,\n    handleRulesChange,\n    editFilter,\n} from '../actions/filtering';\n\nimport DnsBlocklist from '../components/Filters/DnsBlocklist';\n\nconst mapStateToProps = (state: any) => {\n    const { filtering } = state;\n    const props = { filtering };\n    return props;\n};\n\nconst mapDispatchToProps = {\n    setRules,\n    getFilteringStatus,\n    addFilter,\n    removeFilter,\n    toggleFilterStatus,\n    toggleFilteringModal,\n    refreshFilters,\n    handleRulesChange,\n    editFilter,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(DnsBlocklist);\n"
  },
  {
    "path": "client/src/containers/DnsRewrites.ts",
    "content": "import { connect } from 'react-redux';\nimport { getRewritesList, addRewrite, deleteRewrite, updateRewrite, toggleRewritesModal, updateRewriteSettings, getRewriteSettings } from '../actions/rewrites';\n\nimport Rewrites from '../components/Filters/Rewrites';\nimport { RootState } from '../initialState';\n\nconst mapStateToProps = (state: RootState) => {\n    const { rewrites } = state;\n    const props = { rewrites };\n    return props;\n};\n\ntype DispatchProps = {\n    getRewritesList: () => (dispatch: any) => void;\n    toggleRewritesModal: (...args: unknown[]) => unknown;\n    addRewrite: (...args: unknown[]) => unknown;\n    deleteRewrite: (...args: unknown[]) => unknown;\n    updateRewrite: (...args: unknown[]) => unknown;\n    updateRewriteSettings: (...args: unknown[]) => unknown;\n    getRewriteSettings: () => (dispatch: any) => void;\n}\n\nconst mapDispatchToProps: DispatchProps = {\n    getRewritesList,\n    addRewrite,\n    deleteRewrite,\n    updateRewrite,\n    toggleRewritesModal,\n    updateRewriteSettings,\n    getRewriteSettings,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Rewrites);\n"
  },
  {
    "path": "client/src/containers/Encryption.ts",
    "content": "import { connect } from 'react-redux';\nimport { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';\n\nimport { Encryption } from '../components/Settings/Encryption';\n\nconst mapStateToProps = (state: any) => {\n    const { encryption } = state;\n    const props = {\n        encryption,\n    };\n    return props;\n};\n\nconst mapDispatchToProps = {\n    getTlsStatus,\n    setTlsConfig,\n    validateTlsConfig,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Encryption);\n"
  },
  {
    "path": "client/src/containers/Settings.ts",
    "content": "import { connect } from 'react-redux';\n\nimport { initSettings, toggleSetting } from '../actions';\nimport { getBlockedServices, updateBlockedServices } from '../actions/services';\nimport { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';\nimport { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';\nimport { getFilteringStatus, setFiltersConfig } from '../actions/filtering';\n\nimport Settings from '../components/Settings';\n\nconst mapStateToProps = (state: any) => {\n    const { settings, services, stats, queryLogs, filtering } = state;\n    const props = {\n        settings,\n        services,\n        stats,\n        queryLogs,\n        filtering,\n    };\n    return props;\n};\n\nconst mapDispatchToProps = {\n    initSettings,\n    toggleSetting,\n    getBlockedServices,\n    updateBlockedServices,\n    getStatsConfig,\n    setStatsConfig,\n    resetStats,\n    clearLogs,\n    getLogsConfig,\n    setLogsConfig,\n    getFilteringStatus,\n    setFiltersConfig,\n};\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Settings);\n"
  },
  {
    "path": "client/src/containers/SetupGuide.ts",
    "content": "import { connect } from 'react-redux';\n\nimport * as actionCreators from '../actions';\n\nimport SetupGuide from '../components/SetupGuide';\n\nconst mapStateToProps = (state: any) => {\n    const { dashboard } = state;\n    const props = { dashboard };\n    return props;\n};\n\nexport default connect(mapStateToProps, actionCreators)(SetupGuide);\n"
  },
  {
    "path": "client/src/helpers/constants.ts",
    "content": "export const R_URL_REQUIRES_PROTOCOL = /^https?:\\/\\/[^/\\s]+(\\/.*)?$/;\n\n// matches hostname or *.wildcard\nexport const R_HOST = /^(\\*\\.)?[\\w.-]+$/;\n\nexport const R_IPV4 = /^(?:(?:^|\\.)(?:2(?:5[0-5]|[0-4]\\d)|1?\\d?\\d)){4}$/;\n\nexport const R_IPV6 =\n    /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;\n\nexport const R_CIDR =\n    /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/([0-9]|[1-2][0-9]|3[0-2]))$/;\n\nexport const R_MAC =\n    /^((([a-fA-F0-9][a-fA-F0-9]+[-:]){5})([a-fA-F0-9]{2})$)|^((([a-fA-F0-9][a-fA-F0-9]+[-:]){7})([a-fA-F0-9]{2})$)|^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9]{4})$|^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){3}([a-fA-F0-9]{4})$/;\nexport const R_MAC_WITHOUT_COLON = /^([a-fA-F0-9]{2}){5}([a-fA-F0-9]{2})$|^([a-fA-F0-9]{2}){7}([a-fA-F0-9]{2})$/;\n\nexport const R_CIDR_IPV6 =\n    /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/(12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))$/;\n\nexport const R_DOMAIN = /^([a-zA-Z0-9][a-zA-Z0-9-_]*\\.)*[a-zA-Z0-9]*[a-zA-Z0-9-_]*[[a-zA-Z0-9]+$/;\n\nexport const R_PATH_LAST_PART = /\\/[^/]*$/;\n\n// eslint-disable-next-line no-control-regex\nexport const R_UNIX_ABSOLUTE_PATH = /^(\\/[^/\\x00]+)+$/;\n\n// eslint-disable-next-line no-control-regex\nexport const R_WIN_ABSOLUTE_PATH = /^([a-zA-Z]:)?(\\\\|\\/)(?:[^\\\\/:*?\"<>|\\x00]+\\\\)*[^\\\\/:*?\"<>|\\x00]*$/;\n\nexport const R_CLIENT_ID = /^[a-z0-9-]{1,63}$/;\n\nexport const R_IPV4_SUBNET = /^([0-9]|[1-2][0-9]|3[0-2])?$/;\n\nexport const R_IPV6_SUBNET = /^([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])?$/;\n\nexport const MIN_PASSWORD_LENGTH = 8;\nexport const MAX_PASSWORD_LENGTH = 72;\n\nexport const HTML_PAGES = {\n    INSTALL: '/install.html',\n    LOGIN: '/login.html',\n    MAIN: '/',\n};\n\nexport const STATS_NAMES = {\n    avg_processing_time: 'average_processing_time',\n    blocked_filtering: 'Blocked by filters',\n    dns_queries: 'DNS queries',\n    replaced_parental: 'stats_adult',\n    replaced_safebrowsing: 'stats_malware_phishing',\n    replaced_safesearch: 'enforced_save_search',\n};\n\nexport const STATUS_COLORS = {\n    blue: '#467fcf',\n    red: '#cd201f',\n    green: '#5eba00',\n    yellow: '#f1c40f',\n};\n\nexport const REPOSITORY = {\n    URL: 'https://github.com/AdguardTeam/AdGuardHome',\n    TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/trackers.json',\n    ISSUES: 'https://github.com/AdguardTeam/AdGuardHome/issues/new/choose',\n};\n\nexport const CLIENT_ID_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#clientid';\nexport const MANUAL_UPDATE_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#manual-update';\nexport const PORT_53_FAQ_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#bindinuse';\nexport const PRIVACY_POLICY_LINK = 'https://link.adtidy.org/forward.html?action=privacy&from=ui&app=home';\nexport const UPSTREAM_CONFIGURATION_WIKI_LINK =\n    'https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams';\n\nexport const FILTERS_RELATIVE_LINK = '#filters';\n\nexport const ADDRESS_IN_USE_TEXT = 'address already in use';\n\nexport const INSTALL_FIRST_STEP = 1;\nexport const INSTALL_TOTAL_STEPS = 5;\n\nexport const SETTINGS_NAMES = {\n    filtering: 'filtering',\n    safebrowsing: 'safebrowsing',\n    parental: 'parental',\n    safesearch: 'safesearch',\n};\n\nexport const STANDARD_DNS_PORT = 53;\nexport const STANDARD_WEB_PORT = 80;\nexport const STANDARD_HTTPS_PORT = 443;\nexport const DNS_OVER_TLS_PORT = 853;\nexport const DNS_OVER_QUIC_PORT = 853;\nexport const MIN_PORT = 1;\nexport const MAX_PORT = 65535;\n\nexport const EMPTY_DATE = '0001-01-01T00:00:00Z';\n\nexport const DEBOUNCE_TIMEOUT = 300;\nexport const DEBOUNCE_FILTER_TIMEOUT = 500;\nexport const CHECK_TIMEOUT = 1000;\nexport const HIDE_TOOLTIP_DELAY = 300;\nexport const SHOW_TOOLTIP_DELAY = 200;\nexport const MODAL_OPEN_TIMEOUT = 150;\n\nexport const UNSAFE_PORTS = [\n    1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111,\n    113, 115, 117, 119, 123, 135, 139, 143, 179, 389, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 556, 563, 587,\n    601, 636, 993, 995, 2049, 3659, 4045, 6000, 6665, 6666, 6667, 6668, 6669,\n];\n\nexport const ALL_INTERFACES_IP = '0.0.0.0';\n\nexport const STATUS_RESPONSE = {\n    YES: 'yes',\n    NO: 'no',\n    ERROR: 'error',\n} as const;\n\nexport const MODAL_TYPE = {\n    SELECT_MODAL_TYPE: 'SELECT_MODAL_TYPE',\n    ADD_FILTERS: 'ADD_FILTERS',\n    EDIT_FILTERS: 'EDIT_FILTERS',\n    CHOOSE_FILTERING_LIST: 'CHOOSE_FILTERING_LIST',\n    ADD_REWRITE: 'ADD_REWRITE',\n    EDIT_REWRITE: 'EDIT_REWRITE',\n    EDIT_LEASE: 'EDIT_LEASE',\n    ADD_LEASE: 'ADD_LEASE',\n    ADD_CLIENT: 'ADD_CLIENT',\n    EDIT_CLIENT: 'EDIT_CLIENT',\n};\n\nexport const CLIENT_ID = {\n    MAC: 'mac',\n    IP: 'ip',\n};\n\nexport const MENU_URLS = {\n    root: '/',\n    logs: '/logs',\n    guide: '/guide',\n};\n\nexport const SETTINGS_URLS = {\n    encryption: '/encryption',\n    dhcp: '/dhcp',\n    dns: '/dns',\n    settings: '/settings',\n    clients: '/clients',\n};\n\nexport const FILTERS_URLS = {\n    dns_blocklists: '/filters',\n    dns_allowlists: '/dns_allowlists',\n    dns_rewrites: '/dns_rewrites',\n    custom_rules: '/custom_rules',\n    blocked_services: '/blocked_services',\n};\n\nexport const ENCRYPTION_SOURCE = {\n    PATH: 'path',\n    CONTENT: 'content',\n};\n\nexport const FILTERED = 'Filtered';\nexport const NOT_FILTERED = 'NotFiltered';\n\nexport const DISABLED_STATS_INTERVAL = 0;\n\nexport const HOUR = 60 * 60 * 1000;\n\nexport const DAY = HOUR * 24;\n\nexport const STATS_INTERVALS_DAYS = [DAY, DAY * 7, DAY * 30, DAY * 90];\n\nexport const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * 90];\n\nexport const RETENTION_CUSTOM = 1;\n\nexport const RETENTION_CUSTOM_INPUT = 'custom_retention_input';\n\nexport const CUSTOM_INTERVAL = 'customInterval';\n\nexport const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];\n\n// Note that translation strings contain these modes (blocking_mode_CONSTANT)\n// i.e. blocking_mode_default, blocking_mode_null_ip\nexport const BLOCKING_MODES = {\n    default: 'default',\n    refused: 'refused',\n    nxdomain: 'nxdomain',\n    null_ip: 'null_ip',\n    custom_ip: 'custom_ip',\n};\n\n// Note that translation strings contain these modes (theme_CONSTANT)\n// i.e. theme_auto, theme_light.\nexport const THEMES = {\n    auto: 'auto',\n    dark: 'dark',\n    light: 'light',\n};\n\nexport const WHOIS_ICONS = {\n    location: 'location',\n    orgname: 'network',\n    netname: 'network',\n    descr: '',\n};\n\nexport const DEFAULT_LOGS_FILTER = {\n    search: '',\n    response_status: 'all',\n};\n\nexport const DEFAULT_LANGUAGE = 'en';\n\nexport const QUERY_LOGS_PAGE_LIMIT = 20;\n\nexport const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20;\n\nexport const FILTERED_STATUS = {\n    FILTERED_BLACK_LIST: 'FilteredBlackList',\n    NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList',\n    NOT_FILTERED_NOT_FOUND: 'NotFilteredNotFound',\n    FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService',\n    REWRITE: 'Rewrite',\n    REWRITE_HOSTS: 'RewriteEtcHosts',\n    REWRITE_RULE: 'RewriteRule',\n    FILTERED_SAFE_SEARCH: 'FilteredSafeSearch',\n    FILTERED_SAFE_BROWSING: 'FilteredSafeBrowsing',\n    FILTERED_PARENTAL: 'FilteredParental',\n};\n\nexport const RESPONSE_FILTER = {\n    ALL: {\n        QUERY: 'all',\n        LABEL: 'all_queries',\n    },\n    FILTERED: {\n        QUERY: 'filtered',\n        LABEL: 'filtered',\n    },\n    PROCESSED: {\n        QUERY: 'processed',\n        LABEL: 'show_processed_responses',\n    },\n    BLOCKED: {\n        QUERY: 'blocked',\n        LABEL: 'show_blocked_responses',\n    },\n    BLOCKED_SERVICES: {\n        QUERY: 'blocked_services',\n        LABEL: 'blocked_services',\n    },\n    BLOCKED_THREATS: {\n        QUERY: 'blocked_safebrowsing',\n        LABEL: 'blocked_threats',\n    },\n    BLOCKED_ADULT_WEBSITES: {\n        QUERY: 'blocked_parental',\n        LABEL: 'blocked_adult_websites',\n    },\n    ALLOWED: {\n        QUERY: 'whitelisted',\n        LABEL: 'allowed',\n    },\n    REWRITTEN: {\n        QUERY: 'rewritten',\n        LABEL: 'rewritten',\n    },\n    SAFE_SEARCH: {\n        QUERY: 'safe_search',\n        LABEL: 'safe_search',\n    },\n};\n\nexport const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER).reduce(\n    (\n        acc: Record<string, string>,\n        {\n            QUERY,\n        }: {\n            QUERY: string;\n        },\n    ) => {\n        acc[QUERY] = QUERY;\n        return acc;\n    },\n    {},\n);\n\nexport const QUERY_STATUS_COLORS = {\n    BLUE: 'blue',\n    GREEN: 'green',\n    RED: 'red',\n    WHITE: 'white',\n    YELLOW: 'yellow',\n};\n\nexport const FILTERED_STATUS_TO_META_MAP = {\n    [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {\n        LABEL: RESPONSE_FILTER.ALLOWED.LABEL,\n        COLOR: QUERY_STATUS_COLORS.GREEN,\n    },\n    [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {\n        LABEL: RESPONSE_FILTER.PROCESSED.LABEL,\n        COLOR: QUERY_STATUS_COLORS.WHITE,\n    },\n    [FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {\n        LABEL: 'blocked_service',\n        COLOR: QUERY_STATUS_COLORS.RED,\n    },\n    [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {\n        LABEL: RESPONSE_FILTER.SAFE_SEARCH.LABEL,\n        COLOR: QUERY_STATUS_COLORS.YELLOW,\n    },\n    [FILTERED_STATUS.FILTERED_BLACK_LIST]: {\n        LABEL: RESPONSE_FILTER.BLOCKED.LABEL,\n        COLOR: QUERY_STATUS_COLORS.RED,\n    },\n    [FILTERED_STATUS.REWRITE]: {\n        LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,\n        COLOR: QUERY_STATUS_COLORS.BLUE,\n    },\n    [FILTERED_STATUS.REWRITE_HOSTS]: {\n        LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,\n        COLOR: QUERY_STATUS_COLORS.BLUE,\n    },\n    [FILTERED_STATUS.REWRITE_RULE]: {\n        LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,\n        COLOR: QUERY_STATUS_COLORS.BLUE,\n    },\n    [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: {\n        LABEL: RESPONSE_FILTER.BLOCKED_THREATS.LABEL,\n        COLOR: QUERY_STATUS_COLORS.YELLOW,\n    },\n    [FILTERED_STATUS.FILTERED_PARENTAL]: {\n        LABEL: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.LABEL,\n        COLOR: QUERY_STATUS_COLORS.YELLOW,\n    },\n};\n\nexport const DEFAULT_TIME_FORMAT = 'HH:mm:ss';\n\nexport const LONG_TIME_FORMAT = 'HH:mm:ss.SSS';\n\nexport const DEFAULT_SHORT_DATE_FORMAT_OPTIONS = {\n    year: 'numeric',\n    month: 'numeric',\n    day: 'numeric',\n    hour12: false,\n} as const;\n\nexport const DEFAULT_DATE_FORMAT_OPTIONS = {\n    year: 'numeric',\n    month: 'numeric',\n    day: 'numeric',\n    hour: 'numeric',\n    hourCycle: 'h23',\n    minute: 'numeric',\n} as const;\n\nexport const DETAILED_DATE_FORMAT_OPTIONS = {\n    ...DEFAULT_DATE_FORMAT_OPTIONS,\n    month: 'long',\n} as const;\n\nexport const SPECIAL_FILTER_ID = {\n    CUSTOM_FILTERING_RULES: 0,\n    SYSTEM_HOSTS: -1,\n    BLOCKED_SERVICES: -2,\n    PARENTAL: -3,\n    SAFE_BROWSING: -4,\n    SAFE_SEARCH: -5,\n};\n\nexport const BLOCK_ACTIONS = {\n    BLOCK: 'block',\n    UNBLOCK: 'unblock',\n};\n\nexport const SCHEME_TO_PROTOCOL_MAP = {\n    dnscrypt: 'dnscrypt',\n    doh: 'dns_over_https',\n    dot: 'dns_over_tls',\n    doq: 'dns_over_quic',\n    '': 'plain_dns',\n};\n\nexport const DNS_REQUEST_OPTIONS = {\n    PARALLEL: 'parallel',\n    FASTEST_ADDR: 'fastest_addr',\n    LOAD_BALANCING: 'load_balance',\n};\n\nexport const DHCP_FORM_NAMES = {\n    DHCPv4: 'dhcpv4',\n    DHCPv6: 'dhcpv6',\n    DHCP_INTERFACES: 'dhcpInterfaces',\n};\n\nexport const FORM_NAME = {\n    UPSTREAM: 'upstream',\n    DOMAIN_CHECK: 'domainCheck',\n    FILTER: 'filter',\n    REWRITES: 'rewrites',\n    LOGS_FILTER: 'logsFilter',\n    CLIENT: 'client',\n    LEASE: 'lease',\n    ACCESS: 'access',\n    BLOCKING_MODE: 'blockingMode',\n    ENCRYPTION: 'encryption',\n    FILTER_CONFIG: 'filterConfig',\n    LOG_CONFIG: 'logConfig',\n    SERVICES: 'services',\n    STATS_CONFIG: 'statsConfig',\n    INSTALL: 'install',\n    LOGIN: 'login',\n    CACHE: 'cache',\n    MOBILE_CONFIG: 'mobileConfig',\n    ...DHCP_FORM_NAMES,\n};\n\nexport const SMALL_SCREEN_SIZE = 767;\nexport const MEDIUM_SCREEN_SIZE = 1024;\n\nexport const SECONDS_IN_DAY = 60 * 60 * 24;\n\nexport const UINT32_RANGE = {\n    MIN: 0,\n    MAX: 4294967295,\n};\n\nexport const RETENTION_RANGE = {\n    MIN: 1,\n    MAX: 365 * 24,\n};\n\nexport const DHCP_VALUES_PLACEHOLDERS = {\n    ipv4: {\n        subnet_mask: '255.255.255.0',\n        lease_duration: SECONDS_IN_DAY.toString(),\n    },\n    ipv6: {\n        range_start: '2001::1',\n        range_end: 'ff',\n        lease_duration: SECONDS_IN_DAY.toString(),\n    },\n};\n\nexport const DHCP_DESCRIPTION_PLACEHOLDERS = {\n    ipv4: {\n        gateway_ip: 'dhcp_form_gateway_input',\n        subnet_mask: 'dhcp_form_subnet_input',\n        range_start: 'dhcp_form_range_start',\n        range_end: 'dhcp_form_range_end',\n        lease_duration: 'dhcp_form_lease_input',\n    },\n    ipv6: {\n        range_start: 'dhcp_form_range_start',\n        range_end: 'dhcp_form_range_end',\n        lease_duration: 'dhcp_form_lease_input',\n    },\n};\n\nexport const TOAST_TRANSITION_TIMEOUT = 500;\n\nexport const TOAST_TYPES = {\n    SUCCESS: 'success',\n    ERROR: 'error',\n    NOTICE: 'notice',\n};\n\nexport const SUCCESS_TOAST_TIMEOUT = 5000;\n\nexport const ONE_SECOND_IN_MS = 1000;\nexport const FAILURE_TOAST_TIMEOUT = 30000;\n\nexport const TOAST_TIMEOUTS = {\n    [TOAST_TYPES.SUCCESS]: SUCCESS_TOAST_TIMEOUT,\n    [TOAST_TYPES.ERROR]: FAILURE_TOAST_TIMEOUT,\n    [TOAST_TYPES.NOTICE]: FAILURE_TOAST_TIMEOUT,\n};\n\nexport const ADDRESS_TYPES = {\n    IP: 'IP',\n    CIDR: 'CIDR',\n    CLIENT_ID: 'CLIENT_ID',\n    UNKNOWN: 'UNKNOWN',\n};\n\nexport const CACHE_CONFIG_FIELDS = {\n    cache_size: 'cache_size',\n    cache_ttl_min: 'cache_ttl_min',\n    cache_ttl_max: 'cache_ttl_max',\n};\n\nexport const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;\nexport const COMMENT_LINE_DEFAULT_TOKEN = '#';\n\nexport const MOBILE_CONFIG_LINKS = {\n    DOT: 'apple/dot.mobileconfig',\n    DOH: 'apple/doh.mobileconfig',\n};\n\n// Timings for disable protection in milliseconds\nexport const DISABLE_PROTECTION_TIMINGS = {\n    HALF_MINUTE: 30 * 1000,\n    MINUTE: 60 * 1000,\n    TEN_MINUTES: 10 * 60 * 1000,\n    HOUR: 60 * 60 * 1000,\n    TOMORROW: 24 * 60 * 60 * 1000,\n};\n\nexport const LOCAL_TIMEZONE_VALUE = 'Local';\n\nexport const TABLES_MIN_ROWS = 5;\n\nexport const DASHBOARD_TABLES_DEFAULT_PAGE_SIZE = 100;\n\nexport const TIME_UNITS = {\n    HOURS: 'hours',\n    DAYS: 'days',\n};\n\nexport const DNS_RECORD_TYPES = [\n    \"A\", \"AAAA\", \"AFSDB\", \"APL\", \"CAA\", \"CDNSKEY\", \"CDS\", \"CERT\", \"CNAME\",\n    \"CSYNC\", \"DHCID\", \"DLV\", \"DNAME\", \"DNSKEY\", \"DS\", \"EUI48\", \"EUI64\",\n    \"HINFO\", \"HIP\", \"HTTPS\", \"IPSECKEY\", \"KEY\", \"KX\", \"LOC\", \"MX\", \"NAPTR\",\n    \"NS\", \"NSEC\", \"NSEC3\", \"NSEC3PARAM\", \"OPENPGPKEY\", \"PTR\", \"RP\", \"RRSIG\",\n    \"SIG\", \"SMIMEA\", \"SOA\", \"SRV\", \"SSHFP\", \"SVCB\", \"TA\", \"TKEY\",\n    \"TLSA\", \"TSIG\", \"TXT\", \"URI\", \"ZONEMD\"\n];\n"
  },
  {
    "path": "client/src/helpers/filters/filters.ts",
    "content": "// Code generated by go run ./scripts/vetted-filters/main.go; DO NOT EDIT.\n\n/* eslint quote-props: 'off', quotes: 'off', comma-dangle: 'off', semi: 'off' */\n\nexport default {\n    \"categories\": {\n        \"general\": {\n            \"name\": \"filter_category_general\",\n            \"description\": \"filter_category_general_desc\"\n        },\n        \"other\": {\n            \"name\": \"filter_category_other\",\n            \"description\": \"filter_category_other_desc\"\n        },\n        \"regional\": {\n            \"name\": \"filter_category_regional\",\n            \"description\": \"filter_category_regional_desc\"\n        },\n        \"security\": {\n            \"name\": \"filter_category_security\",\n            \"description\": \"filter_category_security_desc\"\n        }\n    },\n    \"filters\": {\n        \"1hosts_lite\": {\n            \"name\": \"1Hosts (Lite)\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://badmojr.github.io/1Hosts/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt\"\n        },\n        \"1hosts_xtra\": {\n            \"name\": \"1Hosts (Xtra)\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://badmojr.github.io/1Hosts/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_70.txt\"\n        },\n        \"CHN_adrules\": {\n            \"name\": \"CHN: AdRules DNS List\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/Cats-Team/AdRules\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_29.txt\"\n        },\n        \"CHN_anti_ad\": {\n            \"name\": \"CHN: anti-AD\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://anti-ad.net/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_21.txt\"\n        },\n        \"HUN_hufilter\": {\n            \"name\": \"HUN: Hufilter\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/hufilter/hufilter\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_35.txt\"\n        },\n        \"IDN_abpindo\": {\n            \"name\": \"IDN: ABPindo\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/ABPindo/indonesianadblockrules\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_22.txt\"\n        },\n        \"IRN_unwanted_iranian_domains\": {\n            \"name\": \"IRN: PersianBlocker list\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/MasterKia/PersianBlocker\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_19.txt\"\n        },\n        \"ISR_easyList_hebrew\": {\n            \"name\": \"ISR: EasyList Hebrew\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/easylist/EasyListHebrew\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_43.txt\"\n        },\n        \"KOR_list_kr\": {\n            \"name\": \"KOR: List-KR DNS\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/List-KR/List-KR\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_25.txt\"\n        },\n        \"KOR_youslist\": {\n            \"name\": \"KOR: YousList\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/yous/YousList\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_15.txt\"\n        },\n        \"LIT_easylist_lithuania\": {\n            \"name\": \"LIT: EasyList Lithuania\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/EasyList-Lithuania/easylist_lithuania\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_36.txt\"\n        },\n        \"MKD_macedonian_pi_hole_blocklist\": {\n            \"name\": \"MKD: Macedonian Pi-hole Blocklist\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/cchevy/macedonian-pi-hole-blocklist\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_20.txt\"\n        },\n        \"NOR_dandelion_sprouts_anti_malware_list\": {\n            \"name\": \"NOR: Dandelion Sprouts nordiske filtre\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/DandelionSprout/adfilt\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_13.txt\"\n        },\n        \"POL_cert_polska_list_of_malicious_domains\": {\n            \"name\": \"POL: CERT Polska List of malicious domains\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://cert.pl/posts/2020/03/ostrzezenia_phishing/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_41.txt\"\n        },\n        \"POL_polish_filters_for_pi_hole\": {\n            \"name\": \"POL: Polish filters for Pi-hole\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://www.certyficate.it/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_14.txt\"\n        },\n        \"SWE_frellwit_swedish_hosts_file\": {\n            \"name\": \"SWE: Frellwit's Swedish Hosts File\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/lassekongo83/Frellwits-filter-lists/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_17.txt\"\n        },\n        \"TUR_turk_adlist\": {\n            \"name\": \"TUR: turk-adlist\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/bkrucarci/turk-adlist\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_26.txt\"\n        },\n        \"TUR_turkish_ad_hosts\": {\n            \"name\": \"TUR: Turkish Ad Hosts\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"https://github.com/symbuzzer/Turkish-Ad-Hosts\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_40.txt\"\n        },\n        \"VNM_abpvn\": {\n            \"name\": \"VNM: ABPVN List\",\n            \"categoryId\": \"regional\",\n            \"homepage\": \"http://abpvn.com/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_16.txt\"\n        },\n        \"adguard_dns_filter\": {\n            \"name\": \"AdGuard DNS filter\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://github.com/AdguardTeam/AdGuardSDNSFilter\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt\"\n        },\n        \"adguard_popup_filter\": {\n            \"name\": \"AdGuard DNS Popup Hosts filter\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://github.com/AdguardTeam/AdGuardSDNSFilter\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_59.txt\"\n        },\n        \"awavenue_ads_rule\": {\n            \"name\": \"AWAvenue Ads Rule\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://awavenue.top/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_53.txt\"\n        },\n        \"curben_phishing_filter\": {\n            \"name\": \"Phishing URL Blocklist (PhishTank and OpenPhish)\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://gitlab.com/malware-filter/phishing-filter\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_30.txt\"\n        },\n        \"dan_pollocks_list\": {\n            \"name\": \"Dan Pollock's List\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://someonewhocares.org/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_4.txt\"\n        },\n        \"dandelion_sprouts_anti_malware_list\": {\n            \"name\": \"Dandelion Sprout's Anti-Malware List\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/DandelionSprout/adfilt\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_12.txt\"\n        },\n        \"dandelion_sprouts_anti_push_notifications\": {\n            \"name\": \"Dandelion Sprout's Anti Push Notifications\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/DandelionSprout/adfilt\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_39.txt\"\n        },\n        \"dandelion_sprouts_game_console_adblock_list\": {\n            \"name\": \"Dandelion Sprout's Game Console Adblock List\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/DandelionSprout/adfilt\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_6.txt\"\n        },\n        \"hagezi_allowlist_referral\": {\n            \"name\": \"HaGeZi's Allowlist Referral\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists#referral\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_45.txt\"\n        },\n        \"hagezi_antipiracy_blocklist\": {\n            \"name\": \"HaGeZi's Anti-Piracy Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists#piracy\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_46.txt\"\n        },\n        \"hagezi_apple_tracker_blocklist\": {\n            \"name\": \"HaGeZi's Apple Tracker Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_67.txt\"\n        },\n        \"hagezi_badware_hoster_blocklist\": {\n            \"name\": \"HaGeZi's Badware Hoster Blocklist\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_55.txt\"\n        },\n        \"hagezi_dns_rebind_protection\": {\n            \"name\": \"HaGeZi's DNS Rebind Protection\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_71.txt\"\n        },\n        \"hagezi_dyndns_blocklist\": {\n            \"name\": \"HaGeZi's DynDNS Blocklist\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_54.txt\"\n        },\n        \"hagezi_encrypted_dns_vpn_tor_proxy_bypass\": {\n            \"name\": \"HaGeZi's Encrypted DNS/VPN/TOR/Proxy Bypass\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists#bypass\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_52.txt\"\n        },\n        \"hagezi_gambling_blocklist\": {\n            \"name\": \"HaGeZi's Gambling Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists#gambling\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_47.txt\"\n        },\n        \"hagezi_multinormal\": {\n            \"name\": \"HaGeZi's Normal Blocklist\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_34.txt\"\n        },\n        \"hagezi_oppo_realme_tracker_blocklist\": {\n            \"name\": \"HaGeZi's OPPO \\u0026 Realme Tracker Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_66.txt\"\n        },\n        \"hagezi_pro\": {\n            \"name\": \"HaGeZi's Pro Blocklist\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_48.txt\"\n        },\n        \"hagezi_pro++\": {\n            \"name\": \"HaGeZi's Pro++ Blocklist\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_51.txt\"\n        },\n        \"hagezi_samsung_tracker_blocklist\": {\n            \"name\": \"HaGeZi's Samsung Tracker Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_61.txt\"\n        },\n        \"hagezi_the_worlds_most_abused_tlds\": {\n            \"name\": \"HaGeZi's The World's Most Abused TLDs\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_56.txt\"\n        },\n        \"hagezi_threat_intelligence_feeds\": {\n            \"name\": \"HaGeZi's Threat Intelligence Feeds\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_44.txt\"\n        },\n        \"hagezi_ultimate\": {\n            \"name\": \"HaGeZi's Ultimate Blocklist\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_49.txt\"\n        },\n        \"hagezi_url_shortener_blocklist\": {\n            \"name\": \"HaGeZi's URL Shortener Blocklist\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_68.txt\"\n        },\n        \"hagezi_vivo_tracker_blocklist\": {\n            \"name\": \"HaGeZi's Vivo Tracker Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_65.txt\"\n        },\n        \"hagezi_windows_office_tracker_blocklist\": {\n            \"name\": \"HaGeZi's Windows/Office Tracker Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_63.txt\"\n        },\n        \"hagezi_xiaomi_tracking_blocklist\": {\n            \"name\": \"HaGeZi's Xiaomi Tracker Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/hagezi/dns-blocklists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_60.txt\"\n        },\n        \"no_google\": {\n            \"name\": \"No Google\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/nickspaargaren/no-google\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_37.txt\"\n        },\n        \"nocoin_filter_list\": {\n            \"name\": \"NoCoin Filter List\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/hoshsadiq/adblock-nocoin-list/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_8.txt\"\n        },\n        \"oisd_basic\": {\n            \"name\": \"OISD Blocklist Small\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://oisd.nl/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_5.txt\"\n        },\n        \"oisd_full\": {\n            \"name\": \"OISD Blocklist Big\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://oisd.nl/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_27.txt\"\n        },\n        \"perflyst_dandelion_sprout_smart_tv_blocklist_for_adguard_home\": {\n            \"name\": \"Perflyst and Dandelion Sprout's Smart-TV Blocklist\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/Perflyst/PiHoleBlocklist\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_7.txt\"\n        },\n        \"peter_lowe_list\": {\n            \"name\": \"Peter Lowe's Blocklist\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://pgl.yoyo.org/adservers/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_3.txt\"\n        },\n        \"phishing_army\": {\n            \"name\": \"Phishing Army\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://phishing.army/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_18.txt\"\n        },\n        \"scam_blocklist_by_durablenapkin\": {\n            \"name\": \"Scam Blocklist by DurableNapkin\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/durablenapkin/scamblocklist\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_10.txt\"\n        },\n        \"shadowwhisperer_tracking_list\": {\n            \"name\": \"ShadowWhisperer Tracking List\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://github.com/ShadowWhisperer/BlockLists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_69.txt\"\n        },\n        \"shadowwhisperers_dating_list\": {\n            \"name\": \"ShadowWhisperer's Dating List\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/ShadowWhisperer/BlockLists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_57.txt\"\n        },\n        \"shadowwhisperers_malware_list\": {\n            \"name\": \"ShadowWhisperer's Malware List\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/ShadowWhisperer/BlockLists\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_42.txt\"\n        },\n        \"staklerware_indicators_list\": {\n            \"name\": \"Stalkerware Indicators List\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/AssoEchap/stalkerware-indicators\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_31.txt\"\n        },\n        \"steven_blacks_list\": {\n            \"name\": \"Steven Black's List\",\n            \"categoryId\": \"general\",\n            \"homepage\": \"https://github.com/StevenBlack/hosts\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_33.txt\"\n        },\n        \"the_big_list_of_hacked_malware_web_sites\": {\n            \"name\": \"The Big List of Hacked Malware Web Sites\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/mitchellkrogza/The-Big-List-of-Hacked-Malware-Web-Sites\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_9.txt\"\n        },\n        \"ublock_badware_risks\": {\n            \"name\": \"uBlock₀ filters – Badware risks\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://github.com/uBlockOrigin/uAssets\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_50.txt\"\n        },\n        \"ukrainian_security_filter\": {\n            \"name\": \"Ukrainian Security Filter\",\n            \"categoryId\": \"other\",\n            \"homepage\": \"https://github.com/braveinnovators/ukrainian-security-filter\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_62.txt\"\n        },\n        \"urlhaus_filter_online\": {\n            \"name\": \"Malicious URL Blocklist (URLHaus)\",\n            \"categoryId\": \"security\",\n            \"homepage\": \"https://urlhaus.abuse.ch/\",\n            \"source\": \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_11.txt\"\n        }\n    }\n}\n"
  },
  {
    "path": "client/src/helpers/form.tsx",
    "content": "import { R_MAC_WITHOUT_COLON, R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from './constants';\n\n/**\n *\n * @param {string} ip\n * @returns {*}\n */\nexport const ip4ToInt = (ip: any) => {\n    const intIp = ip.split('.').reduce((int: any, oct: any) => int * 256 + parseInt(oct, 10), 0);\n    return Number.isNaN(intIp) ? 0 : intIp;\n};\n\n/**\n * @param value {string}\n * @returns {*|number}\n */\nexport const toNumber = (value: any) => value && parseInt(value, 10);\n\n/**\n * @param value {string}\n * @returns {*|number}\n */\n\nexport const toFloatNumber = (value: any) => value && parseFloat(value);\n\n/**\n * @param value {string}\n * @returns {boolean}\n */\nexport const isValidAbsolutePath = (value: any) => R_WIN_ABSOLUTE_PATH.test(value) || R_UNIX_ABSOLUTE_PATH.test(value);\n\n/**\n * @param value {string}\n * @returns {*|string}\n */\nexport const normalizeMac = (value: any) => {\n    if (value && R_MAC_WITHOUT_COLON.test(value)) {\n        return value.match(/.{2}/g).join(':');\n    }\n\n    return value;\n};\n"
  },
  {
    "path": "client/src/helpers/helpers.tsx",
    "content": "import 'url-polyfill';\nimport dateParse from 'date-fns/parse';\nimport dateFormat from 'date-fns/format';\nimport round from 'lodash/round';\nimport axios from 'axios';\nimport i18n from 'i18next';\nimport ipaddr, { IPv4, IPv6 } from 'ipaddr.js';\nimport queryString from 'query-string';\nimport React from 'react';\nimport { getTrackerData } from './trackers/trackers';\n\nimport {\n    ADDRESS_TYPES,\n    CHECK_TIMEOUT,\n    COMMENT_LINE_DEFAULT_TOKEN,\n    DEFAULT_DATE_FORMAT_OPTIONS,\n    DEFAULT_LANGUAGE,\n    DEFAULT_TIME_FORMAT,\n    DETAILED_DATE_FORMAT_OPTIONS,\n    DHCP_VALUES_PLACEHOLDERS,\n    FILTERED,\n    FILTERED_STATUS,\n    R_CLIENT_ID,\n    STANDARD_DNS_PORT,\n    STANDARD_HTTPS_PORT,\n    STANDARD_WEB_PORT,\n    SPECIAL_FILTER_ID,\n    THEMES,\n} from './constants';\nimport { LOCAL_STORAGE_KEYS, LocalStorageHelper } from './localStorageHelper';\nimport { DhcpInterface, InstallInterface } from '../initialState';\n\n/**\n * @param time {string} The time to format\n * @param options {string}\n * @returns {string} Returns the time in the format HH:mm:ss\n */\nexport const formatTime = (time: any, options = DEFAULT_TIME_FORMAT) => {\n    const parsedTime = dateParse(time);\n    return dateFormat(parsedTime, options);\n};\n\n/**\n * @param dateTime {string} The date to format\n * @param [options] {object} Date.prototype.toLocaleString([locales[, options]]) options argument\n * @returns {string} Returns the date and time in the specified format\n */\nexport const formatDateTime = (dateTime: string, options: Intl.DateTimeFormatOptions = DEFAULT_DATE_FORMAT_OPTIONS) => {\n    const parsedTime = new Date(dateTime);\n\n    return parsedTime.toLocaleString(navigator.language, options);\n};\n\n/**\n * @param dateTime {string} The date to format\n * @returns {string} Returns the date and time in the format with the full month name\n */\nexport const formatDetailedDateTime = (dateTime: string) => formatDateTime(dateTime, DETAILED_DATE_FORMAT_OPTIONS);\n\nexport const normalizeLogs = (logs: any) =>\n    logs.map((log: any) => {\n        const {\n            answer,\n            answer_dnssec,\n            client,\n            client_proto,\n            client_id,\n            client_info,\n            elapsedMs,\n            question,\n            reason,\n            status,\n            time,\n            filterId,\n            rule,\n            rules,\n            service_name,\n            original_answer,\n            upstream,\n            cached,\n            ecs,\n        } = log;\n\n        const { name: domain, unicode_name: unicodeName, type } = question;\n\n        const processResponse = (data: any) =>\n            data\n                ? data.map((response: any) => {\n                      const { value, type, ttl } = response;\n                      return `${type}: ${value} (ttl=${ttl})`;\n                  })\n                : [];\n\n        let newRules = rules;\n        /* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */\n        if (rule !== undefined && filterId !== undefined && rules !== undefined && rules.length === 0) {\n            newRules = {\n                filter_list_id: filterId,\n                text: rule,\n            };\n        }\n\n        return {\n            time,\n            domain,\n            unicodeName,\n            type,\n            response: processResponse(answer),\n            reason,\n            client,\n            client_proto,\n            client_id,\n            client_info,\n            /* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */\n            filterId,\n            rule,\n            rules: newRules,\n            status,\n            service_name,\n            originalAnswer: original_answer,\n            originalResponse: processResponse(original_answer),\n            tracker: getTrackerData(domain),\n            answer_dnssec,\n            elapsedMs,\n            upstream,\n            cached,\n            ecs,\n        };\n    });\n\nexport const normalizeHistory = (history: any) =>\n    history.map((item, idx) => ({\n        x: idx,\n        y: item,\n    }));\n\nexport const normalizeTopStats = (stats: any) =>\n    stats.map((item: any) => ({\n        name: Object.keys(item)[0],\n\n        count: Object.values(item)[0],\n    }));\n\nexport const addClientInfo = (data: any, clients: any, ...params: any[]) =>\n    data.map((row: any) => {\n        let info = '';\n        params.find((param) => {\n            const id = row[param];\n            if (id) {\n                const client = clients.find((item: any) => item[id]) || '';\n                info = client?.[id] ?? '';\n            }\n\n            return info;\n        });\n\n        return {\n            ...row,\n            info,\n        };\n    });\n\nexport const normalizeFilters = (filters: any) =>\n    filters\n        ? filters.map((filter: any) => {\n              const { id, url, enabled, last_updated, name = 'Default name', rules_count = 0 } = filter;\n\n              return {\n                  id,\n                  url,\n                  enabled,\n                  lastUpdated: last_updated,\n                  name,\n                  rulesCount: rules_count,\n              };\n          })\n        : [];\n\nexport const normalizeFilteringStatus = (filteringStatus: any) => {\n    const { enabled, filters, user_rules: userRules, interval, whitelist_filters } = filteringStatus;\n    const newUserRules = Array.isArray(userRules) ? userRules.join('\\n') : '';\n\n    return {\n        enabled,\n        userRules: newUserRules,\n        filters: normalizeFilters(filters),\n        whitelistFilters: normalizeFilters(whitelist_filters),\n        interval,\n    };\n};\n\nexport const getPercent = (amount: any, number: any) => {\n    if (amount > 0 && number > 0) {\n        return round(100 / (amount / number), 2);\n    }\n    return 0;\n};\n\nexport const captitalizeWords = (text: any) =>\n    text\n        .split(/[ -_]/g)\n        .map((str: any) => str.charAt(0).toUpperCase() + str.substr(1))\n        .join(' ');\n\nexport const getInterfaceIp = (option: any) => {\n    const onlyIPv6 = option.ip_addresses.every((ip: any) => ip.includes(':'));\n    let [interfaceIP] = option.ip_addresses;\n\n    if (!onlyIPv6) {\n        option.ip_addresses.forEach((ip: any) => {\n            if (!ip.includes(':')) {\n                interfaceIP = ip;\n            }\n        });\n    }\n\n    return interfaceIP;\n};\n\nexport const getIpList = (interfaces: InstallInterface[]) =>\n    Object.values(interfaces)\n        .reduce((acc: string[], curr: InstallInterface) => acc.concat(curr.ip_addresses), [] as string[])\n        .sort();\n\n/**\n * @param {string} ip\n * @param {number} [port]\n * @returns {string}\n */\nexport const getDnsAddress = (ip: any, port = 0) => {\n    const isStandardDnsPort = port === STANDARD_DNS_PORT;\n    let address = ip;\n\n    if (port) {\n        if (ip.includes(':') && !isStandardDnsPort) {\n            address = `[${ip}]:${port}`;\n        } else if (!isStandardDnsPort) {\n            address = `${ip}:${port}`;\n        }\n    }\n\n    return address;\n};\n\n/**\n * @param {string} ip\n * @param {number} [port]\n * @returns {string}\n */\nexport const getWebAddress = (ip: any, port = 0) => {\n    const isStandardWebPort = port === STANDARD_WEB_PORT;\n    let address = `http://${ip}`;\n\n    if (port && !isStandardWebPort) {\n        if (ip.includes(':') && !ip.includes('[')) {\n            address = `http://[${ip}]:${port}`;\n        } else {\n            address = `http://${ip}:${port}`;\n        }\n    }\n\n    return address;\n};\n\nexport const checkRedirect = (url: any, attempts: number = 1) => {\n    let count = attempts || 1;\n\n    if (count > 10) {\n        window.location.replace(url);\n        return false;\n    }\n\n    const rmTimeout = (t: any) => t && clearTimeout(t);\n    const setRecursiveTimeout = (time: any, ...args: any[]) => setTimeout(checkRedirect, time, ...args);\n\n    let timeout: any;\n\n    axios\n        .get(url)\n        .then((response) => {\n            rmTimeout(timeout);\n            if (response) {\n                window.location.replace(url);\n                return;\n            }\n            timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, (count += 1));\n        })\n        .catch((error) => {\n            rmTimeout(timeout);\n            if (error.response) {\n                window.location.replace(url);\n                return;\n            }\n            timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, (count += 1));\n        });\n\n    return false;\n};\n\nexport const redirectToCurrentProtocol = (values: any, httpPort = 80) => {\n    const { protocol, hostname, hash, port } = window.location;\n    const { enabled, force_https, port_https } = values;\n    const httpsPort = port_https !== STANDARD_HTTPS_PORT ? `:${port_https}` : '';\n\n    if (protocol !== 'https:' && enabled && force_https && port_https) {\n        checkRedirect(`https://${hostname}${httpsPort}/${hash}`);\n    } else if (protocol === 'https:' && enabled && port_https && port_https !== parseInt(port, 10)) {\n        checkRedirect(`https://${hostname}${httpsPort}/${hash}`);\n    } else if (protocol === 'https:' && (!enabled || !port_https)) {\n        window.location.replace(`http://${hostname}:${httpPort}/${hash}`);\n    }\n};\n\n/**\n * @param {string} text\n * @returns []string\n */\nexport const splitByNewLine = (text: any) => text.split('\\n').filter((n: any) => n.trim());\n\n/**\n * @param {string} text\n * @returns {string}\n */\nexport const trimMultilineString = (text: any) =>\n    splitByNewLine(text)\n        .map((line: any) => line.trim())\n        .join('\\n');\n\n/**\n * @param {string} text\n * @returns {string}\n */\nexport const removeEmptyLines = (text: any) => splitByNewLine(text).join('\\n');\n\n/**\n * @param {string} input\n * @returns {string}\n */\nexport const trimLinesAndRemoveEmpty = (input: any) =>\n    input\n        .split('\\n')\n        .map((line: any) => line.trim())\n        .filter(Boolean)\n        .join('\\n');\n\n/**\n * Normalizes the topClients array\n *\n * @param {Object[]} topClients\n * @param {string} topClients.name\n * @param {number} topClients.count\n * @param {Object} topClients.info\n * @param {string} topClients.info.name\n * @returns {Object} normalizedTopClients\n * @returns {Object.<string, number>} normalizedTopClients.auto - auto clients\n * @returns {Object.<string, number>} normalizedTopClients.configured - configured clients\n */\nexport const normalizeTopClients = (topClients: any) =>\n    topClients.reduce(\n        (acc: any, clientObj: any) => {\n            const {\n                name,\n                count,\n                info: { name: infoName },\n            } = clientObj;\n            acc.auto[name] = count;\n            acc.configured[infoName] = count;\n            return acc;\n        },\n        {\n            auto: {},\n            configured: {},\n        },\n    );\n\nexport const sortClients = (clients: any) => {\n    const compare = (a: any, b: any) => {\n        const nameA = a.name.toUpperCase();\n        const nameB = b.name.toUpperCase();\n\n        if (nameA > nameB) {\n            return 1;\n        }\n        if (nameA < nameB) {\n            return -1;\n        }\n\n        return 0;\n    };\n\n    return clients.sort(compare);\n};\n\nexport const toggleAllServices = (services: any, change: any, isSelected: any) => {\n    services.forEach((service: any) => change(`blocked_services.${service.id}`, isSelected));\n};\n\nexport const msToSeconds = (milliseconds: any) => Math.floor(milliseconds / 1000);\n\nexport const msToMinutes = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60);\n\nexport const msToHours = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60 / 60);\n\nexport const secondsToMilliseconds = (seconds: any) => {\n    if (seconds) {\n        return seconds * 1000;\n    }\n\n    return seconds;\n};\n\nexport const msToDays = (milliseconds: any) => Math.floor(milliseconds / 1000 / 60 / 60 / 24);\n\nexport const normalizeRulesTextarea = (text: any) => text?.replace(/^\\n/g, '').replace(/\\n\\s*\\n/g, '\\n');\n\nexport const normalizeWhois = (whois: any) => {\n    if (Object.keys(whois).length > 0) {\n        const { city, country, ...values } = whois;\n        let location = country || '';\n\n        if (city && location) {\n            location = `${location}, ${city}`;\n        } else if (city) {\n            location = city;\n        }\n\n        if (location) {\n            return {\n                location,\n                ...values,\n            };\n        }\n\n        return { ...values };\n    }\n\n    return whois;\n};\n\nexport const getPathWithQueryString = (path: any, params: any) => {\n    const searchParams = new URLSearchParams(params);\n\n    return `${path}?${searchParams.toString()}`;\n};\n\nexport const getParamsForClientsSearch = (data: any, param: any, additionalParam?: any) => {\n    const clients = new Set();\n    data.forEach((e: any) => {\n        clients.add(e[param]);\n        if (e[additionalParam]) {\n            clients.add(e[additionalParam]);\n        }\n    });\n\n    return {\n        clients: Array.from(clients).map((id) => ({ id })),\n    };\n};\n\n/**\n * Creates onBlur handler that can normalize input if normalization function is specified\n *\n * @param {Object} event\n * @param {Object} event.target\n * @param {string} event.target.value\n * @param {Object} input\n * @param {function} input.onBlur\n * @param {function} [normalizeOnBlur]\n * @returns {function}\n */\n\nexport const checkFiltered = (reason: any) => reason.indexOf(FILTERED) === 0;\nexport const checkRewrite = (reason: any) => reason === FILTERED_STATUS.REWRITE;\nexport const checkRewriteHosts = (reason: any) => reason === FILTERED_STATUS.REWRITE_HOSTS;\nexport const checkBlackList = (reason: any) => reason === FILTERED_STATUS.FILTERED_BLACK_LIST;\nexport const checkWhiteList = (reason: any) => reason === FILTERED_STATUS.NOT_FILTERED_WHITE_LIST;\n// eslint-disable-next-line max-len\nexport const checkNotFilteredNotFound = (reason: any) => reason === FILTERED_STATUS.NOT_FILTERED_NOT_FOUND;\nexport const checkSafeSearch = (reason: any) => reason === FILTERED_STATUS.FILTERED_SAFE_SEARCH;\nexport const checkSafeBrowsing = (reason: any) => reason === FILTERED_STATUS.FILTERED_SAFE_BROWSING;\nexport const checkParental = (reason: any) => reason === FILTERED_STATUS.FILTERED_PARENTAL;\nexport const checkBlockedService = (reason: any) => reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;\n\nexport const getCurrentFilter = (url: any, filters: any) => {\n    const filter = filters?.find((item: any) => url === item.url);\n\n    if (filter) {\n        const { enabled, name, url } = filter;\n        return {\n            enabled,\n            name,\n            url,\n        };\n    }\n\n    return {\n        name: '',\n        url: '',\n    };\n};\n\n/**\n * @param {object} initialValues\n * @param {object} values\n * @returns {object} Returns different values of objects\n */\n\nexport const getObjDiff = (initialValues: any, values: any) =>\n    Object.entries(values)\n\n        .reduce((acc: any, [key, value]) => {\n            if (value !== initialValues[key]) {\n                acc[key] = value;\n            }\n            return acc;\n        }, {});\n\n/**\n * @param num {number} to format\n * @returns {string} Returns a string with a language-sensitive representation of this number\n */\nexport const formatNumber = (num: number): string => {\n    const currentLanguage = i18n.languages[0] || DEFAULT_LANGUAGE;\n    return num.toLocaleString(currentLanguage);\n};\n\n/**\n * @param arr {array}\n * @param key {string}\n * @param value {string}\n * @returns {object}\n */\nexport const getMap = (arr: any, key: any, value: any) =>\n    arr.reduce((acc: any, curr: any) => {\n        acc[curr[key]] = curr[value];\n        return acc;\n    }, {});\n\n/**\n * @param parsedIp {object} ipaddr.js IPv4 or IPv6 object\n * @param parsedCidr {array} ipaddr.js CIDR array\n * @returns {boolean}\n */\nconst isIpMatchCidr = (parsedIp: any, parsedCidr: any) => {\n    try {\n        const cidrIpVersion = parsedCidr[0].kind();\n        const ipVersion = parsedIp.kind();\n\n        return ipVersion === cidrIpVersion && parsedIp.match(parsedCidr);\n    } catch (e) {\n        return false;\n    }\n};\n\nexport const isIpInCidr = (ip: any, cidr: any) => {\n    try {\n        const parsedIp = ipaddr.parse(ip);\n        const parsedCidr = ipaddr.parseCIDR(cidr);\n\n        return isIpMatchCidr(parsedIp, parsedCidr);\n    } catch (e) {\n        console.error(e);\n        return false;\n    }\n};\n\n/**\n *\n * @param {string} subnetMask\n * @returns {IPv4 | null}\n */\nexport const parseSubnetMask = (subnetMask: any) => {\n    try {\n        return ipaddr.parse(subnetMask).prefixLengthFromSubnetMask();\n    } catch (e) {\n        console.error(e);\n        return null;\n    }\n};\n\n/**\n *\n * @param {string} subnetMask\n * @returns {*}\n */\nexport const subnetMaskToBitMask = (subnetMask: any) =>\n    subnetMask.split('.').reduce((acc: any, cur: any) => acc - Math.log2(256 - Number(cur)), 32);\n\n/**\n *\n * @param ipOrCidr\n * @returns {'IP' | 'CIDR' | 'CLIENT_ID' | 'UNKNOWN'}\n *\n */\nexport const findAddressType = (address: any) => {\n    try {\n        const cidrMaybe = address.includes('/');\n\n        if (!cidrMaybe && ipaddr.isValid(address)) {\n            return ADDRESS_TYPES.IP;\n        }\n        if (cidrMaybe && ipaddr.parseCIDR(address)) {\n            return ADDRESS_TYPES.CIDR;\n        }\n        if (R_CLIENT_ID.test(address)) {\n            return ADDRESS_TYPES.CLIENT_ID;\n        }\n\n        return ADDRESS_TYPES.UNKNOWN;\n    } catch (e) {\n        return ADDRESS_TYPES.UNKNOWN;\n    }\n};\n\n/**\n * @param ids {string[]}\n * @returns {Object}\n */\nexport const separateIpsAndCidrs = (ids: any) =>\n    ids.reduce(\n        (acc: any, curr: any) => {\n            const addressType = findAddressType(curr);\n\n            if (addressType === ADDRESS_TYPES.IP) {\n                acc.ips.push(curr);\n            }\n            if (addressType === ADDRESS_TYPES.CIDR) {\n                acc.cidrs.push(curr);\n            }\n            if (addressType === ADDRESS_TYPES.CLIENT_ID) {\n                acc.clientIds.push(curr);\n            }\n            return acc;\n        },\n        { ips: [], cidrs: [], clientIds: [] },\n    );\n\nexport const countClientsStatistics = (ids: any, autoClients: any) => {\n    const { ips, cidrs, clientIds } = separateIpsAndCidrs(ids);\n\n    const ipsCount = ips.reduce((acc: any, curr: any) => {\n        const count = autoClients[curr] || 0;\n        return acc + count;\n    }, 0);\n\n    const clientIdsCount = clientIds.reduce((acc: any, curr: any) => {\n        const count = autoClients[curr] || 0;\n        return acc + count;\n    }, 0);\n\n    const cidrsCount = Object.entries(autoClients).reduce((acc: any, curr: any) => {\n        const [id, count] = curr;\n        if (!ipaddr.isValid(id)) {\n            return acc;\n        }\n        if (cidrs.some((cidr: any) => isIpInCidr(id, cidr))) {\n            // eslint-disable-next-line no-param-reassign\n            acc += count;\n        }\n        return acc;\n    }, 0);\n\n    return ipsCount + cidrsCount + clientIdsCount;\n};\n\n/**\n * @param {string} elapsedMs\n * @param {function} t translate\n * @returns {string}\n */\nexport const formatElapsedMs = (elapsedMs: string, t: (key: string) => string) => {\n    const parsedElapsedMs = parseFloat(elapsedMs);\n\n    if (Number.isNaN(parsedElapsedMs)) {\n        return elapsedMs;\n    }\n\n    const formattedValue = parsedElapsedMs < 1\n        ? parsedElapsedMs.toFixed(2)\n        : Math.floor(parsedElapsedMs).toString();\n\n    return `${formattedValue} ${t('milliseconds_abbreviation')}`;\n};\n\n/**\n * @param language {string}\n */\nexport const setHtmlLangAttr = (language: any) => {\n    window.document.documentElement.lang = language;\n};\n\n/**\n * Set local storage theme field\n *\n * @param {string} theme\n */\nexport const setTheme = (theme: any) => {\n    LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.THEME, theme);\n};\n\n/**\n * Get local storage theme field\n *\n * @returns {string}\n */\n\nexport const getTheme = () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.THEME) || THEMES.light;\n\n/**\n * Sets UI theme.\n *\n * @param theme\n */\nexport const setUITheme = (theme: any) => {\n    let currentTheme = theme || getTheme();\n\n    if (currentTheme === THEMES.auto) {\n        const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\n        currentTheme = prefersDark ? THEMES.dark : THEMES.light;\n    }\n    setTheme(currentTheme);\n    document.body.dataset.theme = currentTheme;\n};\n\n/**\n * @param values {object}\n * @returns {object}\n */\n\nexport const replaceEmptyStringsWithZeroes = (values: any) =>\n    Object.entries(values)\n\n        .reduce((acc: any, [key, value]) => {\n            acc[key] = value === '' ? 0 : value;\n            return acc;\n        }, {});\n\n/**\n * @param value {number || string}\n * @returns {string}\n */\nexport const replaceZeroWithEmptyString = (value: any) => (parseInt(value, 10) === 0 ? '' : value);\n\n/**\n * @param {string} search\n * @param {string} [response_status]\n * @returns {string}\n */\nexport const getLogsUrlParams = (search: any, response_status: any) =>\n    `?${queryString.stringify({\n        search: search || undefined,\n        response_status: response_status || undefined,\n    })}`;\n\nexport const processContent = (content: any) =>\n    Array.isArray(content) ? content.filter(([, value]) => value).reduce((acc, val) => acc.concat(val), []) : content;\n\n// TODO check getObjectKeysSorted\ntype NestedObject = {\n    [key: string]: any;\n    order: number;\n};\n\nexport const getObjectKeysSorted = <T extends Record<string, NestedObject>, K extends keyof NestedObject>(\n    object: T,\n    sortKey: K,\n): string[] => {\n    return Object.entries(object)\n        .sort(([, a], [, b]) => (a[sortKey] as number) - (b[sortKey] as number))\n        .map(([key]) => key);\n};\n\n/**\n * @param ip\n * @returns {[IPv4|IPv6, 33|129]}\n */\nconst getParsedIpWithPrefixLength = (ip: any) => {\n    const MAX_PREFIX_LENGTH_V4 = 32;\n    const MAX_PREFIX_LENGTH_V6 = 128;\n\n    const parsedIp = ipaddr.parse(ip);\n    const prefixLength = parsedIp.kind() === 'ipv4' ? MAX_PREFIX_LENGTH_V4 : MAX_PREFIX_LENGTH_V6;\n\n    // Increment prefix length to always put IP after CIDR, e.g. 127.0.0.1/32, 127.0.0.1\n    return [parsedIp, prefixLength + 1];\n};\n\n/**\n * Helper function for IP and CIDR comparison (supports both v4 and v6)\n * @param item - ip or cidr\n * @returns {number[]}\n */\nconst getAddressesComparisonBytes = (item: any) => {\n    // Sort ipv4 before ipv6\n    const IP_V4_COMPARISON_CODE = 0;\n    const IP_V6_COMPARISON_CODE = 1;\n\n    const [parsedIp, cidr] = ipaddr.isValid(item) ? getParsedIpWithPrefixLength(item) : ipaddr.parseCIDR(item);\n\n    const [normalizedBytes, ipVersionComparisonCode] =\n        (parsedIp as IPv4 | IPv6).kind() === 'ipv4'\n            ? [(parsedIp as IPv4).toIPv4MappedAddress().parts, IP_V4_COMPARISON_CODE]\n            : [(parsedIp as IPv6).parts, IP_V6_COMPARISON_CODE];\n\n    return [ipVersionComparisonCode, ...normalizedBytes, cidr];\n};\n\n/**\n * Compare function for IP and CIDR sort in ascending order (supports both v4 and v6)\n * @param a\n * @param b\n * @returns {number} -1 | 0 | 1\n */\nexport const sortIp = (a: any, b: any) => {\n    try {\n        const comparisonBytesA = Array.isArray(a) ? getAddressesComparisonBytes(a[0]) : getAddressesComparisonBytes(a);\n        const comparisonBytesB = Array.isArray(b) ? getAddressesComparisonBytes(b[0]) : getAddressesComparisonBytes(b);\n\n        for (let i = 0; i < comparisonBytesA.length; i += 1) {\n            const byteA = comparisonBytesA[i];\n            const byteB = comparisonBytesB[i];\n\n            if (byteA === byteB) {\n                // eslint-disable-next-line no-continue\n                continue;\n            }\n            return byteA > byteB ? 1 : -1;\n        }\n\n        return 0;\n    } catch (e) {\n        console.warn(e);\n        return 0;\n    }\n};\n\n/**\n * @param {number} filterId\n * @returns {string}\n */\nexport const getSpecialFilterName = (filterId: any) => {\n    switch (filterId) {\n        case SPECIAL_FILTER_ID.CUSTOM_FILTERING_RULES:\n            return i18n.t('custom_filter_rules');\n        case SPECIAL_FILTER_ID.SYSTEM_HOSTS:\n            return i18n.t('system_host_files');\n        case SPECIAL_FILTER_ID.BLOCKED_SERVICES:\n            return i18n.t('blocked_services');\n        case SPECIAL_FILTER_ID.PARENTAL:\n            return i18n.t('parental_control');\n        case SPECIAL_FILTER_ID.SAFE_BROWSING:\n            return i18n.t('safe_browsing');\n        case SPECIAL_FILTER_ID.SAFE_SEARCH:\n            return i18n.t('safe_search');\n        default:\n            return i18n.t('unknown_filter', { filterId });\n    }\n};\n\nexport type Filter = {\n    enabled: boolean;\n    id: number;\n    lastUpdated: string;\n    name: string;\n    rulesCount: number;\n    url: string;\n};\n\nexport type Rule = {\n    filter_list_id: number;\n    text: string;\n};\n\nexport const getFilterName = (\n    filters: Filter[],\n    whitelistFilters: Filter[],\n    filterId: number,\n    resolveFilterName = (filter: Filter) => (filter ? filter.name : i18n.t('unknown_filter', { filterId })),\n) => {\n    const specialFilterIds = Object.values(SPECIAL_FILTER_ID);\n    if (specialFilterIds.includes(filterId)) {\n        return getSpecialFilterName(filterId);\n    }\n\n    const matchIdPredicate = (filter: Filter) => filter.id === filterId;\n    const filter = filters.find(matchIdPredicate) || whitelistFilters.find(matchIdPredicate);\n    return resolveFilterName(filter);\n};\n\nexport const getFilterNames = (rules: Rule[], filters: Filter[], whitelistFilters: Filter[]) =>\n    rules.map(({ filter_list_id }: any) => getFilterName(filters, whitelistFilters, filter_list_id));\n\nexport const getRuleNames = (rules: Rule[]) => rules.map(({ text }: Rule) => text);\n\nexport const getFilterNameToRulesMap = (rules: Rule[], filters: Filter[], whitelistFilters: Filter[]) =>\n    rules.reduce((acc: any, { text, filter_list_id }: Rule) => {\n        const filterName = getFilterName(filters, whitelistFilters, filter_list_id);\n\n        acc[filterName] = (acc[filterName] || []).concat(text);\n        return acc;\n    }, {});\n\nexport const getRulesToFilterList = (\n    rules: Rule[],\n    filters: Filter[],\n    whitelistFilters: Filter[],\n    classes = {\n        list: 'filteringRules',\n        rule: 'filteringRules__rule font-monospace',\n        filter: 'filteringRules__filter',\n    },\n) => {\n    const filterNameToRulesMap: { string: string[] } = getFilterNameToRulesMap(rules, filters, whitelistFilters);\n\n    return (\n        <dl className={classes.list}>\n            {Object.entries(filterNameToRulesMap).reduce(\n                (acc: any, [filterName, rulesArr]) =>\n                    acc\n                        .concat(\n                            rulesArr.map((rule: any, i: any) => (\n                                <dd key={i} className={classes.rule}>\n                                    {rule}\n                                </dd>\n                            )),\n                        )\n                        .concat(\n                            <dt className={classes.filter} key={classes.filter}>\n                                {filterName}\n                            </dt>,\n                        ),\n                [],\n            )}\n        </dl>\n    );\n};\n\n/**\n * @param ip {string}\n * @param gateway_ip {string}\n * @returns {{range_end: string, subnet_mask: string, range_start: string,\n * lease_duration: string, gateway_ip: string}}\n */\nexport const calculateDhcpPlaceholdersIpv4 = (ip: string, gateway_ip: string) => {\n    const LAST_OCTET_IDX = 3;\n    const LAST_OCTET_RANGE_START = 100;\n    const LAST_OCTET_RANGE_END = 200;\n\n    const addr = ipaddr.parse(ip) as IPv4;\n\n    addr.octets[LAST_OCTET_IDX] = LAST_OCTET_RANGE_START;\n    const range_start = addr.toString();\n\n    addr.octets[LAST_OCTET_IDX] = LAST_OCTET_RANGE_END;\n    const range_end = addr.toString();\n\n    const { subnet_mask, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv4;\n\n    return {\n        gateway_ip: gateway_ip || ip,\n        subnet_mask,\n        range_start,\n        range_end,\n        lease_duration,\n    };\n};\n\nexport const calculateDhcpPlaceholdersIpv6 = () => {\n    const { range_start, range_end, lease_duration } = DHCP_VALUES_PLACEHOLDERS.ipv6;\n\n    return {\n        range_start,\n        range_end,\n        lease_duration,\n    };\n};\n\n/**\n * Add ip_addresses property - concatenated ipv4_addresses and ipv6_addresses for every interface\n * @param interfaces\n * @param interfaces.ipv4_addresses {string[]}\n * @param interfaces.ipv6_addresses {string[]}\n * @returns interfaces Interfaces enriched with ip_addresses property\n */\n\nexport const enrichWithConcatenatedIpAddresses = (interfaces: DhcpInterface[]) =>\n    Object.entries(interfaces)\n\n        .reduce((acc: any, [k, v]) => {\n            const ipv4_addresses = v.ipv4_addresses ?? [];\n            const ipv6_addresses = v.ipv6_addresses ?? [];\n\n            acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses);\n            return acc;\n        }, interfaces);\n\nexport const isScrolledIntoView = (el: any) => {\n    const rect = el.getBoundingClientRect();\n    const elemTop = rect.top;\n    const elemBottom = rect.bottom;\n\n    return elemTop < window.innerHeight && elemBottom >= 0;\n};\n\n/**\n * If this is a manually created client, return its name.\n * If this is a \"runtime\" client, return it's IP address.\n * @param clients {Array.<object>}\n * @param ip {string}\n * @returns {string}\n */\nexport const getBlockingClientName = (clients: any, ip: any) => {\n    for (let i = 0; i < clients.length; i += 1) {\n        const client = clients[i];\n\n        if (client.ids.includes(ip)) {\n            return client.name;\n        }\n    }\n    return ip;\n};\n\n/**\n * @param {string[]} lines\n * @returns {string[]}\n */\nexport const filterOutComments = (lines: any) =>\n    lines.filter((line: any) => !line.startsWith(COMMENT_LINE_DEFAULT_TOKEN));\n\n/**\n * @param {array} services\n * @param {string} id\n * @returns {string}\n */\nexport const getService = (services: any, id: any) => services.find((s: any) => s.id === id);\n\n/**\n * @param {array} services\n * @param {string} id\n * @returns {string}\n */\nexport const getServiceName = (services: any, id: any) => getService(services, id)?.name;\n\n/**\n * @param {array} services\n * @param {string} id\n * @returns {string}\n */\nexport const getServiceIcon = (services: any, id: any) => getService(services, id)?.icon_svg;\n"
  },
  {
    "path": "client/src/helpers/highlightTextareaComments.tsx",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport { COMMENT_LINE_DEFAULT_TOKEN } from './constants';\n\nconst renderHighlightedLine = (line: any, idx: any, commentLineTokens = [COMMENT_LINE_DEFAULT_TOKEN]) => {\n    const isComment = commentLineTokens.some((token) => line.trim().startsWith(token));\n\n    const lineClassName = classnames({\n        'text-gray': isComment,\n        'text-transparent': !isComment,\n    });\n\n    return (\n        <div className={lineClassName} key={idx}>\n            {line || '\\n'}\n        </div>\n    );\n};\nexport const getTextareaCommentsHighlight = (ref: any, lines: any, commentLineTokens?: any, className = '') => {\n    const renderLine = (line: any, idx: any) => renderHighlightedLine(line, idx, commentLineTokens);\n\n    return (\n        <code className={classnames('text-output font-monospace', className)} ref={ref}>\n            {lines?.split('\\n').map(renderLine)}\n        </code>\n    );\n};\n\nexport const syncScroll = (e: any, ref: any) => {\n    // eslint-disable-next-line no-param-reassign\n    ref.current.scrollTop = e.target.scrollTop;\n};\n"
  },
  {
    "path": "client/src/helpers/localStorageHelper.ts",
    "content": "export const LOCAL_STORAGE_KEYS = {\n    THEME: 'account_theme',\n    BLOCKLIST_PAGE_SIZE: 'blocklist_page_size',\n    ALLOWLIST_PAGE_SIZE: 'allowlist_page_size',\n    CLIENTS_PAGE_SIZE: 'clients_page_size',\n    REWRITES_PAGE_SIZE: 'rewrites_page_size',\n    AUTO_CLIENTS_PAGE_SIZE: 'auto_clients_page_size',\n};\n\nexport const LocalStorageHelper = {\n    setItem(key: any, value: any) {\n        try {\n            localStorage.setItem(key, JSON.stringify(value));\n        } catch (error) {\n            console.error(`Error setting ${key} in local storage: ${error.message}`);\n        }\n    },\n\n    getItem(key: any) {\n        try {\n            const item = localStorage.getItem(key);\n            return item ? JSON.parse(item) : null;\n        } catch (error) {\n            console.error(`Error getting ${key} from local storage: ${error.message}`);\n            return null;\n        }\n    },\n\n    removeItem(key: any) {\n        try {\n            localStorage.removeItem(key);\n        } catch (error) {\n            console.error(`Error removing ${key} from local storage: ${error.message}`);\n        }\n    },\n\n    clear() {\n        try {\n            localStorage.clear();\n        } catch (error) {\n            console.error(`Error clearing local storage: ${error.message}`);\n        }\n    },\n};\n"
  },
  {
    "path": "client/src/helpers/renderFormattedClientCell.tsx",
    "content": "import React from 'react';\n\nimport { Link } from 'react-router-dom';\n\nimport { normalizeWhois } from './helpers';\nimport { WHOIS_ICONS } from './constants';\n\nconst getFormattedWhois = (whois: any) => {\n    const whoisInfo = normalizeWhois(whois);\n    return Object.keys(whoisInfo).map((key) => {\n        const icon = WHOIS_ICONS[key];\n        return (\n            <span className=\"logs__whois text-muted\" key={key} title={whoisInfo[key]}>\n                {icon && (\n                    <>\n                        <svg className=\"logs__whois-icon icons icon--18\">\n                            <use xlinkHref={`#${icon}`} />\n                        </svg>\n                        &nbsp;\n                    </>\n                )}\n                {whoisInfo[key]}\n            </span>\n        );\n    });\n};\n\n/**\n * @param {string} value\n * @param {object} info\n * @param {string} info.name\n * @param {object} info.whois_info\n * @param {boolean} [isDetailed]\n * @param {boolean} [isLogs]\n * @returns {JSXElement}\n */\nexport const renderFormattedClientCell = (value: any, info: any, isDetailed = false, isLogs = false) => {\n    let whoisContainer = null;\n    let nameContainer = value;\n\n    if (info) {\n        const { name, whois_info } = info;\n        const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;\n\n        if (name) {\n            const nameValue = (\n                <div\n                    className=\"logs__text logs__text--link logs__text--nowrap logs__text--client\"\n                    title={`${name} (${value})`}>\n                    {name}&nbsp;<small>{`(${value})`}</small>\n                </div>\n            );\n\n            if (!isLogs) {\n                nameContainer = nameValue;\n            } else {\n                nameContainer = !whoisAvailable && isDetailed ? <small title={value}>{value}</small> : nameValue;\n            }\n        }\n\n        if (whoisAvailable && isDetailed) {\n            whoisContainer = (\n                <div className=\"logs__text logs__text--wrap logs__text--whois\">{getFormattedWhois(whois_info)}</div>\n            );\n        }\n    }\n\n    return (\n        <div className=\"logs__text logs__text--client mw-100\" title={value}>\n            <Link to={`logs?search=\"${encodeURIComponent(value)}\"`}>{nameContainer}</Link>\n            {whoisContainer}\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/helpers/trackers/trackers.json",
    "content": "{\n  \"timeUpdated\": \"2026-02-09T10:25:05.786Z\",\n  \"categories\": {\n    \"0\": \"audio_video_player\",\n    \"1\": \"comments\",\n    \"2\": \"customer_interaction\",\n    \"3\": \"pornvertising\",\n    \"4\": \"advertising\",\n    \"5\": \"essential\",\n    \"6\": \"site_analytics\",\n    \"7\": \"social_media\",\n    \"8\": \"misc\",\n    \"9\": \"cdn\",\n    \"10\": \"hosting\",\n    \"11\": \"unknown\",\n    \"12\": \"extensions\",\n    \"13\": \"email\",\n    \"14\": \"consent\",\n    \"15\": \"telemetry\",\n    \"101\": \"mobile_analytics\"\n  },\n  \"trackers\": {\n    \"163\": {\n      \"name\": \"163\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.163.com/\",\n      \"companyId\": \"163\"\n    },\n    \"1000mercis\": {\n      \"name\": \"1000mercis\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.1000mercis.com/\",\n      \"companyId\": \"1000mercis\"\n    },\n    \"161media\": {\n      \"name\": \"Platform161\",\n      \"categoryId\": 4,\n      \"url\": \"https://platform161.com/\",\n      \"companyId\": \"platform161\"\n    },\n    \"1822direkt.de\": {\n      \"name\": \"1822direkt.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.1822direkt.de/\",\n      \"companyId\": \"1822direkt\",\n      \"source\": \"AdGuard\"\n    },\n    \"1dmp.io\": {\n      \"name\": \"1DMP\",\n      \"categoryId\": 4,\n      \"url\": \"https://1dmp.io/\",\n      \"companyId\": \"1dmp\"\n    },\n    \"1plusx\": {\n      \"name\": \"1plusX\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.1plusx.com/\",\n      \"companyId\": \"1plusx\"\n    },\n    \"1sponsor\": {\n      \"name\": \"1sponsor\",\n      \"categoryId\": 4,\n      \"url\": \"http://fr.1sponsor.com/\",\n      \"companyId\": \"1sponsor\"\n    },\n    \"1tag\": {\n      \"name\": \"1tag\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.dentsuaegisnetwork.com/\",\n      \"companyId\": \"dentsu_aegis_network\"\n    },\n    \"1und1\": {\n      \"name\": \"1&1 IONOS\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.ionos.com/\",\n      \"companyId\": \"1und1\",\n      \"source\": \"AdGuard\"\n    },\n    \"24-ads.com\": {\n      \"name\": \"24-ADS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.24-ads.com/\",\n      \"companyId\": \"24-ads.com\",\n      \"source\": \"AdGuard\"\n    },\n    \"24_7\": {\n      \"name\": \"[24]7\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.247-inc.com/\",\n      \"companyId\": \"24_7\"\n    },\n    \"24log\": {\n      \"name\": \"24log\",\n      \"categoryId\": 6,\n      \"url\": \"http://24log.ru/\",\n      \"companyId\": \"24log\"\n    },\n    \"24smi\": {\n      \"name\": \"24SMI\",\n      \"categoryId\": 8,\n      \"url\": \"https://24smi.org/\",\n      \"companyId\": \"24smi\",\n      \"source\": \"AdGuard\"\n    },\n    \"2leep\": {\n      \"name\": \"2leep\",\n      \"categoryId\": 4,\n      \"url\": \"http://2leep.com/\",\n      \"companyId\": \"2leep\"\n    },\n    \"33across\": {\n      \"name\": \"33Across\",\n      \"categoryId\": 4,\n      \"url\": \"http://33across.com/\",\n      \"companyId\": \"33across\"\n    },\n    \"3dstats\": {\n      \"name\": \"3DStats\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.3dstats.com/\",\n      \"companyId\": \"3dstats\"\n    },\n    \"3gpp\": {\n      \"name\": \"3GPP Network\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.3gpp.org/\",\n      \"companyId\": \"3gpp\",\n      \"source\": \"AdGuard\"\n    },\n    \"4chan\": {\n      \"name\": \"4Chan\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.4chan.org/\",\n      \"companyId\": \"4chan\",\n      \"source\": \"AdGuard\"\n    },\n    \"4finance_com\": {\n      \"name\": \"4finance\",\n      \"categoryId\": 2,\n      \"url\": \"https://4finance.com/\",\n      \"companyId\": \"4finance\",\n      \"source\": \"AdGuard\"\n    },\n    \"4w_marketplace\": {\n      \"name\": \"4w Marketplace\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.4wmarketplace.com/\",\n      \"companyId\": \"4w_marketplace\"\n    },\n    \"500friends\": {\n      \"name\": \"500friends\",\n      \"categoryId\": 2,\n      \"url\": \"http://500friends.com/\",\n      \"companyId\": \"500friends\"\n    },\n    \"51.la\": {\n      \"name\": \"51.La\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.51.la/\",\n      \"companyId\": \"51.la\"\n    },\n    \"5min_media\": {\n      \"name\": \"5min Media\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.5min.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"6sense\": {\n      \"name\": \"6Sense\",\n      \"categoryId\": 6,\n      \"url\": \"http://home.grepdata.com\",\n      \"companyId\": \"6sense\"\n    },\n    \"77tracking\": {\n      \"name\": \"77Tracking\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.77agency.com/\",\n      \"companyId\": \"77agency\"\n    },\n    \"7plus\": {\n      \"name\": \"7plus\",\n      \"categoryId\": 0,\n      \"url\": \"https://7plus.com.au/\",\n      \"companyId\": \"seven_group_holdings\",\n      \"source\": \"AdGuard\"\n    },\n    \"7tv.de\": {\n      \"name\": \"7tv.app\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.7tv.app/\",\n      \"companyId\": \"7tv\",\n      \"source\": \"AdGuard\"\n    },\n    \"888media\": {\n      \"name\": \"888media\",\n      \"categoryId\": 4,\n      \"url\": \"http://888media.net/\",\n      \"companyId\": \"888_media\"\n    },\n    \"8digits\": {\n      \"name\": \"8digits\",\n      \"categoryId\": 6,\n      \"url\": \"http://8digits.com/\",\n      \"companyId\": \"8digits\"\n    },\n    \"94j7afz2nr.xyz\": {\n      \"name\": \"94j7afz2nr.xyz\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"99stats\": {\n      \"name\": \"99stats\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.99stats.com/\",\n      \"companyId\": \"99stats\"\n    },\n    \"a3cloud_net\": {\n      \"name\": \"a3cloud.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"a8\": {\n      \"name\": \"A8\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.a8.net/\",\n      \"companyId\": \"a8\"\n    },\n    \"aaxads.com\": {\n      \"name\": \"Acceptable Ads Exchange\",\n      \"categoryId\": 4,\n      \"url\": \"https://aax.media/\",\n      \"companyId\": null\n    },\n    \"ab_tasty\": {\n      \"name\": \"AB Tasty\",\n      \"categoryId\": 6,\n      \"url\": \"https://en.abtasty.com\",\n      \"companyId\": \"ab_tasty\"\n    },\n    \"abc\": {\n      \"name\": \"Australian Broadcasting Corporation\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.abc.net.au/\",\n      \"companyId\": \"australian_government\",\n      \"source\": \"AdGuard\"\n    },\n    \"ablida\": {\n      \"name\": \"ablida\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ablida.de/\",\n      \"companyId\": null\n    },\n    \"accelia\": {\n      \"name\": \"Accelia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.durasite.net/\",\n      \"companyId\": \"accelia\"\n    },\n    \"accengage\": {\n      \"name\": \"Accengage\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.accengage.com/\",\n      \"companyId\": \"accengage\"\n    },\n    \"accessanalyzer\": {\n      \"name\": \"AccessAnalyzer\",\n      \"categoryId\": 6,\n      \"url\": \"http://ax.xrea.com/\",\n      \"companyId\": \"accessanalyzer\"\n    },\n    \"accesstrade\": {\n      \"name\": \"AccessTrade\",\n      \"categoryId\": 4,\n      \"url\": \"http://accesstrade.net/\",\n      \"companyId\": \"accesstrade\"\n    },\n    \"accord_group\": {\n      \"name\": \"Accord Group\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.accordgroup.co.uk/\",\n      \"companyId\": \"accord_group\"\n    },\n    \"accordant_media\": {\n      \"name\": \"Accordant Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.accordantmedia.com/\",\n      \"companyId\": \"accordant_media\"\n    },\n    \"accuen_media\": {\n      \"name\": \"Accuen Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.accuenmedia.com/\",\n      \"companyId\": \"accuen_media\"\n    },\n    \"acestream.net\": {\n      \"name\": \"ActStream\",\n      \"categoryId\": 12,\n      \"url\": \"http://www.acestream.org/\",\n      \"companyId\": null\n    },\n    \"acint.net\": {\n      \"name\": \"Artificial Computation Intelligence\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.acint.net/\",\n      \"companyId\": \"acint\"\n    },\n    \"acloudimages\": {\n      \"name\": \"Acloudimages\",\n      \"categoryId\": 4,\n      \"url\": \"http://adsterra.com\",\n      \"companyId\": \"adsterra\"\n    },\n    \"acpm.fr\": {\n      \"name\": \"ACPM\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.acpm.fr/\",\n      \"companyId\": null\n    },\n    \"acquia.com\": {\n      \"name\": \"Acquia\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.acquia.com/\",\n      \"companyId\": null\n    },\n    \"acrweb\": {\n      \"name\": \"ACRWEB\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.ziyu.net/\",\n      \"companyId\": \"acrweb\"\n    },\n    \"actionpay\": {\n      \"name\": \"actionpay\",\n      \"categoryId\": 4,\n      \"url\": \"http://actionpay.ru/\",\n      \"companyId\": \"actionpay\"\n    },\n    \"active_agent\": {\n      \"name\": \"Active Agent\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.active-agent.com/\",\n      \"companyId\": \"active_agent\"\n    },\n    \"active_campaign\": {\n      \"name\": \"Active Campaign\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.activecampaign.com\",\n      \"companyId\": \"active_campaign\"\n    },\n    \"active_performance\": {\n      \"name\": \"Active Performance\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.active-performance.de/\",\n      \"companyId\": \"active_performance\"\n    },\n    \"activeconversion\": {\n      \"name\": \"ActiveConversion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.activeconversion.com/\",\n      \"companyId\": \"activeconversion\"\n    },\n    \"activecore\": {\n      \"name\": \"activecore\",\n      \"categoryId\": 6,\n      \"url\": \"http://activecore.jp/\",\n      \"companyId\": \"activecore\"\n    },\n    \"activemeter\": {\n      \"name\": \"ActiveMeter\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.activemeter.com/\",\n      \"companyId\": \"activeconversion\"\n    },\n    \"activengage\": {\n      \"name\": \"ActivEngage\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.activengage.com\",\n      \"companyId\": \"activengage\"\n    },\n    \"acton\": {\n      \"name\": \"Act-On Beacon\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.actonsoftware.com/\",\n      \"companyId\": \"act-on\"\n    },\n    \"acuity_ads\": {\n      \"name\": \"Acuity Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.acuityads.com/\",\n      \"companyId\": \"acuity_ads\"\n    },\n    \"acxiom\": {\n      \"name\": \"Acxiom\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.acxiom.com\",\n      \"companyId\": \"acxiom\"\n    },\n    \"ad-blocker.org\": {\n      \"name\": \"ad-blocker.org\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"ad-center\": {\n      \"name\": \"Ad-Center\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ad-center.com\",\n      \"companyId\": \"ad-center\"\n    },\n    \"ad-delivery.net\": {\n      \"name\": \"ad-delivery.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"ad-sys\": {\n      \"name\": \"Ad-Sys\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ad-sys.com/\",\n      \"companyId\": \"ad-sys\"\n    },\n    \"ad.agio\": {\n      \"name\": \"Ad.agio\",\n      \"categoryId\": 4,\n      \"url\": \"http://neodatagroup.com/\",\n      \"companyId\": \"neodata\"\n    },\n    \"ad2click\": {\n      \"name\": \"Ad2Click\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ad2click.com/\",\n      \"companyId\": \"ad2click_media\"\n    },\n    \"ad2games\": {\n      \"name\": \"ad2games\",\n      \"categoryId\": 4,\n      \"url\": \"http://web.ad2games.com/\",\n      \"companyId\": \"ad2games\"\n    },\n    \"ad360\": {\n      \"name\": \"Ad360\",\n      \"categoryId\": 4,\n      \"url\": \"http://ad360.vn\",\n      \"companyId\": \"ad360\"\n    },\n    \"ad4game\": {\n      \"name\": \"ad4game\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ad4game.com/\",\n      \"companyId\": \"ad4game\"\n    },\n    \"ad4mat\": {\n      \"name\": \"ad4mat\",\n      \"categoryId\": 4,\n      \"url\": \"http://ad4mat.info\",\n      \"companyId\": \"ad4mat\"\n    },\n    \"ad6media\": {\n      \"name\": \"ad6media\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ad6media.fr/\",\n      \"companyId\": \"ad6media\"\n    },\n    \"ad_decisive\": {\n      \"name\": \"Ad Decisive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.lagardere-global-advertising.com/\",\n      \"companyId\": \"lagardere_advertising\"\n    },\n    \"ad_dynamo\": {\n      \"name\": \"Ad Dynamo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.addynamo.com/\",\n      \"companyId\": \"ad_dynamo\"\n    },\n    \"ad_ebis\": {\n      \"name\": \"AD EBiS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ebis.ne.jp/en/\",\n      \"companyId\": \"ad_ebis\"\n    },\n    \"ad_lightning\": {\n      \"name\": \"Ad Lightning\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adlightning.com/\",\n      \"companyId\": \"ad_lightning\"\n    },\n    \"ad_magnet\": {\n      \"name\": \"Ad Magnet\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admagnet.com/\",\n      \"companyId\": \"ad_magnet\"\n    },\n    \"ad_spirit\": {\n      \"name\": \"Ad Spirit\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adspirit.de\",\n      \"companyId\": \"adspirit\"\n    },\n    \"adac_de\": {\n      \"name\": \"adac.de\",\n      \"categoryId\": 8,\n      \"url\": \"http://adac.de/\",\n      \"companyId\": null\n    },\n    \"adacado\": {\n      \"name\": \"Adacado\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adacado.com/\",\n      \"companyId\": \"adacado\"\n    },\n    \"adadyn\": {\n      \"name\": \"Adadyn\",\n      \"categoryId\": 4,\n      \"url\": \"http://ozonemedia.com/index.html\",\n      \"companyId\": \"adadyn\"\n    },\n    \"adality_gmbh\": {\n      \"name\": \"adality GmbH\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.arvato.com/\",\n      \"companyId\": \"arvato\"\n    },\n    \"adalliance.io\": {\n      \"name\": \"Ad Alliance\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ad-alliance.de/\",\n      \"companyId\": null\n    },\n    \"adalyser.com\": {\n      \"name\": \"Adalyser\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.adalyser.com/\",\n      \"companyId\": \"onesoon\"\n    },\n    \"adaos\": {\n      \"name\": \"ADAOS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.24-interactive.com\",\n      \"companyId\": \"24_interactive\"\n    },\n    \"adap.tv\": {\n      \"name\": \"Adap.tv\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adap.tv/\",\n      \"companyId\": \"verizon\"\n    },\n    \"adaptiveblue_smartlinks\": {\n      \"name\": \"AdaptiveBlue SmartLinks\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.adaptiveblue.com/smartlinks.html\",\n      \"companyId\": \"telfie\"\n    },\n    \"adara_analytics\": {\n      \"name\": \"ADARA Analytics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adaramedia.com/\",\n      \"companyId\": \"adara_analytics\"\n    },\n    \"adasia_holdings\": {\n      \"name\": \"AdAsia Holdings\",\n      \"categoryId\": 4,\n      \"url\": \"https://adasiaholdings.com/\",\n      \"companyId\": \"adasia_holdings\"\n    },\n    \"adbetclickin.pink\": {\n      \"name\": \"adbetnet\",\n      \"categoryId\": 4,\n      \"url\": \"http://adbetnet.com/\",\n      \"companyId\": null\n    },\n    \"adbetnet.com\": {\n      \"name\": \"adbetnet\",\n      \"categoryId\": 4,\n      \"url\": \"https://adbetnet.com/\",\n      \"companyId\": null\n    },\n    \"adblade.com\": {\n      \"name\": \"Adblade\",\n      \"categoryId\": 4,\n      \"url\": \"https://adblade.com/\",\n      \"companyId\": \"adblade\"\n    },\n    \"adbooth\": {\n      \"name\": \"Adbooth\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adbooth.com/\",\n      \"companyId\": \"adbooth_media_group\"\n    },\n    \"adbox\": {\n      \"name\": \"AdBox\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adbox.lv/\",\n      \"companyId\": \"adbox\"\n    },\n    \"adbrain\": {\n      \"name\": \"Adbrain\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.adbrain.com/\",\n      \"companyId\": \"adbrain\"\n    },\n    \"adbrite\": {\n      \"name\": \"AdBrite\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adbrite.com/\",\n      \"companyId\": \"centro\"\n    },\n    \"adbull\": {\n      \"name\": \"AdBull\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adbull.com/\",\n      \"companyId\": \"adbull\"\n    },\n    \"adbutler\": {\n      \"name\": \"AdButler\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adbutler.com/d\",\n      \"companyId\": \"sparklit_networks\"\n    },\n    \"adc_media\": {\n      \"name\": \"ad:C media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adcmedia.de/en/\",\n      \"companyId\": \"ad:c_media\"\n    },\n    \"adcash\": {\n      \"name\": \"Adcash\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adcash.com\",\n      \"companyId\": \"adcash\"\n    },\n    \"adchakra\": {\n      \"name\": \"AdChakra\",\n      \"categoryId\": 6,\n      \"url\": \"http://adchakra.com/\",\n      \"companyId\": \"adchakra\"\n    },\n    \"adchina\": {\n      \"name\": \"AdChina\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adchina.com/\",\n      \"companyId\": null,\n      \"source\": \"AdGuard\"\n    },\n    \"adcito\": {\n      \"name\": \"Adcito\",\n      \"categoryId\": 4,\n      \"url\": \"http://adcito.com/\",\n      \"companyId\": \"adcito\"\n    },\n    \"adclear\": {\n      \"name\": \"AdClear\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adclear.de/en/home.html\",\n      \"companyId\": \"adclear\"\n    },\n    \"adclerks\": {\n      \"name\": \"Adclerks\",\n      \"categoryId\": 4,\n      \"url\": \"https://adclerks.com/\",\n      \"companyId\": \"adclerks\"\n    },\n    \"adclickmedia\": {\n      \"name\": \"AdClickMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adclickmedia.com/\",\n      \"companyId\": \"adclickmedia\"\n    },\n    \"adclickzone\": {\n      \"name\": \"AdClickZone\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adclickzone.com/\",\n      \"companyId\": \"adclickzone\"\n    },\n    \"adcloud\": {\n      \"name\": \"adcloud\",\n      \"categoryId\": 4,\n      \"url\": \"https://ad-cloud.jp\",\n      \"companyId\": \"adcloud\"\n    },\n    \"adcolony\": {\n      \"name\": \"AdColony\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adcolony.com/history-of-adcolony/\",\n      \"companyId\": \"digital_turbine\",\n      \"source\": \"AdGuard\"\n    },\n    \"adconion\": {\n      \"name\": \"Adconion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adconion.com/\",\n      \"companyId\": \"singtel\"\n    },\n    \"adcrowd\": {\n      \"name\": \"Adcrowd\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adcrowd.com\",\n      \"companyId\": \"adcrowd\"\n    },\n    \"adcurve\": {\n      \"name\": \"AdCurve\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.shop2market.com/\",\n      \"companyId\": \"adcurve\"\n    },\n    \"add_to_calendar\": {\n      \"name\": \"Add To Calendar\",\n      \"categoryId\": 2,\n      \"url\": \"http://addtocalendar.com/\",\n      \"companyId\": \"addtocalendar\"\n    },\n    \"addaptive\": {\n      \"name\": \"Addaptive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.datapointmedia.com/\",\n      \"companyId\": \"addaptive\"\n    },\n    \"addefend\": {\n      \"name\": \"AdDefend\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.addefend.com/\",\n      \"companyId\": null\n    },\n    \"addfreestats\": {\n      \"name\": \"AddFreeStats\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.addfreestats.com/\",\n      \"companyId\": \"3dstats\"\n    },\n    \"addinto\": {\n      \"name\": \"AddInto\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.addinto.com/\",\n      \"companyId\": \"addinto\"\n    },\n    \"addshoppers\": {\n      \"name\": \"AddShoppers\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.addshoppers.com/\",\n      \"companyId\": \"addshoppers\"\n    },\n    \"addthis\": {\n      \"name\": \"AddThis\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.addthis.com/\",\n      \"companyId\": \"oracle\"\n    },\n    \"addvalue\": {\n      \"name\": \"Addvalue\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.addvalue.de/en/\",\n      \"companyId\": \"addvalue.de\"\n    },\n    \"addyon\": {\n      \"name\": \"AddyON\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.addyon.com/homepage.php\",\n      \"companyId\": \"addyon\"\n    },\n    \"adeasy\": {\n      \"name\": \"AdEasy\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adeasy.ru/\",\n      \"companyId\": \"adeasy\"\n    },\n    \"adelphic\": {\n      \"name\": \"Adelphic\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.adelphic.com/\",\n      \"companyId\": \"adelphic\"\n    },\n    \"adengage\": {\n      \"name\": \"AdEngage\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adengage.com\",\n      \"companyId\": \"synacor\"\n    },\n    \"adespresso\": {\n      \"name\": \"AdEspresso\",\n      \"categoryId\": 4,\n      \"url\": \"http://adespresso.com\",\n      \"companyId\": \"adespresso\"\n    },\n    \"adexcite\": {\n      \"name\": \"AdExcite\",\n      \"categoryId\": 4,\n      \"url\": \"http://adexcite.com\",\n      \"companyId\": \"adexcite\"\n    },\n    \"adextent\": {\n      \"name\": \"AdExtent\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adextent.com/\",\n      \"companyId\": \"adextent\"\n    },\n    \"adf.ly\": {\n      \"name\": \"AdF.ly\",\n      \"categoryId\": 4,\n      \"url\": \"http://adf.ly/\",\n      \"companyId\": \"adf.ly\"\n    },\n    \"adfalcon\": {\n      \"name\": \"AdFalcon\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adfalcon.com/\",\n      \"companyId\": \"adfalcon\"\n    },\n    \"adfocus\": {\n      \"name\": \"AdFocus\",\n      \"categoryId\": 4,\n      \"url\": \"http://adfoc.us/\",\n      \"companyId\": \"adfoc.us\"\n    },\n    \"adforgames\": {\n      \"name\": \"AdForGames\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adforgames.com/\",\n      \"companyId\": \"adforgames\"\n    },\n    \"adform\": {\n      \"name\": \"Adform\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adform.com\",\n      \"companyId\": \"adform\"\n    },\n    \"adfox\": {\n      \"name\": \"AdFox\",\n      \"categoryId\": 4,\n      \"url\": \"http://adfox.ru\",\n      \"companyId\": \"yandex\"\n    },\n    \"adfreestyle\": {\n      \"name\": \"adFreestyle\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adfreestyle.pl/\",\n      \"companyId\": \"adfreestyle\"\n    },\n    \"adfront\": {\n      \"name\": \"AdFront\",\n      \"categoryId\": 4,\n      \"url\": \"http://buysellads.com/\",\n      \"companyId\": \"buysellads.com\"\n    },\n    \"adfrontiers\": {\n      \"name\": \"AdFrontiers\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adfrontiers.com/\",\n      \"companyId\": \"adfrontiers\"\n    },\n    \"adgear\": {\n      \"name\": \"AdGear\",\n      \"categoryId\": 4,\n      \"url\": \"http://adgear.com/\",\n      \"companyId\": \"samsung\"\n    },\n    \"adgebra\": {\n      \"name\": \"Adgebra\",\n      \"categoryId\": 4,\n      \"url\": \"https://adgebra.in/\",\n      \"companyId\": \"adgebra\"\n    },\n    \"adgenie\": {\n      \"name\": \"adGENIE\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adgenie.co.uk/\",\n      \"companyId\": \"ve\"\n    },\n    \"adgile\": {\n      \"name\": \"Adgile\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adgile.com/\",\n      \"companyId\": \"adgile_media\"\n    },\n    \"adglare.net\": {\n      \"name\": \"Adglare\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adglare.com/\",\n      \"companyId\": null\n    },\n    \"adglue\": {\n      \"name\": \"Adglue\",\n      \"categoryId\": 4,\n      \"url\": \"http://admans.de/de.html\",\n      \"companyId\": \"admans\"\n    },\n    \"adgoal\": {\n      \"name\": \"adgoal\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adgoal.de/\",\n      \"companyId\": \"adgoal\"\n    },\n    \"adgorithms\": {\n      \"name\": \"Adgorithms\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adgorithms.com/\",\n      \"companyId\": \"albert\"\n    },\n    \"adgoto\": {\n      \"name\": \"ADGoto\",\n      \"categoryId\": 4,\n      \"url\": \"http://adgoto.com/\",\n      \"companyId\": \"adgoto\"\n    },\n    \"adguard\": {\n      \"name\": \"AdGuard\",\n      \"categoryId\": 8,\n      \"url\": \"https://adguard.com/\",\n      \"companyId\": \"adguard\",\n      \"source\": \"AdGuard\"\n    },\n    \"adguard_dns\": {\n      \"name\": \"AdGuard DNS\",\n      \"categoryId\": 8,\n      \"url\": \"https://adguard-dns.io/\",\n      \"companyId\": \"adguard\",\n      \"source\": \"AdGuard\"\n    },\n    \"adguard_vpn\": {\n      \"name\": \"AdGuard VPN\",\n      \"categoryId\": 8,\n      \"url\": \"https://adguard-vpn.com/\",\n      \"companyId\": \"adguard\",\n      \"source\": \"AdGuard\"\n    },\n    \"adhands\": {\n      \"name\": \"AdHands\",\n      \"categoryId\": 4,\n      \"url\": \"http://promo.adhands.ru/\",\n      \"companyId\": \"adhands\"\n    },\n    \"adhese\": {\n      \"name\": \"Adhese\",\n      \"categoryId\": 4,\n      \"url\": \"http://adhese.com\",\n      \"companyId\": \"adhese\"\n    },\n    \"adhitz\": {\n      \"name\": \"AdHitz\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adhitz.com/\",\n      \"companyId\": \"adhitz\"\n    },\n    \"adhood\": {\n      \"name\": \"adhood\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adhood.com/\",\n      \"companyId\": \"adhood\"\n    },\n    \"adify\": {\n      \"name\": \"Adify\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adify.com/\",\n      \"companyId\": \"cox_enterpries\"\n    },\n    \"adikteev\": {\n      \"name\": \"Adikteev\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adikteev.com/\",\n      \"companyId\": \"adikteev\"\n    },\n    \"adimpact\": {\n      \"name\": \"Adimpact\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adimpact.com/\",\n      \"companyId\": \"adimpact\"\n    },\n    \"adinch\": {\n      \"name\": \"Adinch\",\n      \"categoryId\": 4,\n      \"url\": \"http://adinch.com/\",\n      \"companyId\": \"adinch\"\n    },\n    \"adition\": {\n      \"name\": \"Adition\",\n      \"categoryId\": 4,\n      \"url\": \"http://en.adition.com/\",\n      \"companyId\": \"prosieben_sat1\"\n    },\n    \"adjal\": {\n      \"name\": \"Adjal\",\n      \"categoryId\": 4,\n      \"url\": \"http://adjal.com/\",\n      \"companyId\": \"marketing_adjal\"\n    },\n    \"adjs\": {\n      \"name\": \"ADJS\",\n      \"categoryId\": 4,\n      \"url\": \"https://github.com/widgital/adjs\",\n      \"companyId\": \"adjs\"\n    },\n    \"adjug\": {\n      \"name\": \"AdJug\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adjug.com/\",\n      \"companyId\": \"adjug\"\n    },\n    \"adjust\": {\n      \"name\": \"Adjust GmbH\",\n      \"categoryId\": 101,\n      \"url\": \"https://www.adjust.com/\",\n      \"companyId\": \"applovin\",\n      \"source\": \"AdGuard\"\n    },\n    \"adk2\": {\n      \"name\": \"adk2\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adk2.com/\",\n      \"companyId\": \"adk2_plymedia\"\n    },\n    \"adklip\": {\n      \"name\": \"adklip\",\n      \"categoryId\": 4,\n      \"url\": \"http://adklip.com\",\n      \"companyId\": \"adklip\"\n    },\n    \"adknowledge\": {\n      \"name\": \"Adknowledge\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adknowledge.com/\",\n      \"companyId\": \"adknowledge\"\n    },\n    \"adkontekst\": {\n      \"name\": \"Adkontekst\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.en.adkontekst.pl/\",\n      \"companyId\": \"adkontekst\"\n    },\n    \"adkontekst.pl\": {\n      \"name\": \"Adkontekst\",\n      \"categoryId\": 4,\n      \"url\": \"http://netsprint.eu/\",\n      \"companyId\": \"netsprint\"\n    },\n    \"adlabs\": {\n      \"name\": \"AdLabs\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adlabs.ru/\",\n      \"companyId\": \"adlabs\"\n    },\n    \"adlantic\": {\n      \"name\": \"AdLantic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adlantic.nl/\",\n      \"companyId\": \"adlantic_online_advertising\"\n    },\n    \"adlantis\": {\n      \"name\": \"AdLantis\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adlantis.jp/\",\n      \"companyId\": \"adlantis\"\n    },\n    \"adless\": {\n      \"name\": \"Adless\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adless.io/\",\n      \"companyId\": \"adless\"\n    },\n    \"adlive_header_bidding\": {\n      \"name\": \"Adlive Header Bidding\",\n      \"categoryId\": 4,\n      \"url\": \"http://adlive.io/\",\n      \"companyId\": \"adlive\"\n    },\n    \"adloox\": {\n      \"name\": \"Adloox\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adloox.com\",\n      \"companyId\": \"adloox\"\n    },\n    \"admachine\": {\n      \"name\": \"AdMachine\",\n      \"categoryId\": 4,\n      \"url\": \"https://admachine.co/\",\n      \"companyId\": null\n    },\n    \"adman\": {\n      \"name\": \"ADMAN\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adman.gr/\",\n      \"companyId\": \"adman\"\n    },\n    \"adman_media\": {\n      \"name\": \"ADman Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admanmedia.com/\",\n      \"companyId\": \"ad_man_media\"\n    },\n    \"admantx.com\": {\n      \"name\": \"ADmantX\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admantx.com/\",\n      \"companyId\": \"expert_system_spa\"\n    },\n    \"admaster\": {\n      \"name\": \"AdMaster\",\n      \"categoryId\": 4,\n      \"url\": \"http://admaster.net\",\n      \"companyId\": \"admaster\"\n    },\n    \"admaster.cn\": {\n      \"name\": \"AdMaster.cn\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admaster.com.cn/\",\n      \"companyId\": \"admaster\"\n    },\n    \"admatic\": {\n      \"name\": \"Admatic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admatic.com.tr/#1page\",\n      \"companyId\": \"admatic\"\n    },\n    \"admatrix\": {\n      \"name\": \"Admatrix\",\n      \"categoryId\": 4,\n      \"url\": \"https://admatrix.jp/login#block01\",\n      \"companyId\": \"admatrix\"\n    },\n    \"admax\": {\n      \"name\": \"Admax\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admaxnetwork.com/index.php\",\n      \"companyId\": \"komli\"\n    },\n    \"admaxim\": {\n      \"name\": \"AdMaxim\",\n      \"categoryId\": 4,\n      \"url\": \"http://admaxim.com/\",\n      \"companyId\": \"admaxim\"\n    },\n    \"admaya\": {\n      \"name\": \"Admaya\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admaya.in/\",\n      \"companyId\": \"admaya\"\n    },\n    \"admedia\": {\n      \"name\": \"AdMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://admedia.com/\",\n      \"companyId\": \"admedia\"\n    },\n    \"admedo_com\": {\n      \"name\": \"Admedo\",\n      \"categoryId\": 4,\n      \"url\": \"http://admedo.com/\",\n      \"companyId\": \"admedo\"\n    },\n    \"admeira.ch\": {\n      \"name\": \"AdMeira\",\n      \"categoryId\": 4,\n      \"url\": \"http://admeira.ch/\",\n      \"companyId\": \"admeira\"\n    },\n    \"admeld\": {\n      \"name\": \"AdMeld\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admeld.com\",\n      \"companyId\": \"google\"\n    },\n    \"admeo\": {\n      \"name\": \"Admeo\",\n      \"categoryId\": 4,\n      \"url\": \"http://admeo.ru/\",\n      \"companyId\": \"admeo.ru\"\n    },\n    \"admeta\": {\n      \"name\": \"Admeta\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admeta.com/\",\n      \"companyId\": \"admeta\"\n    },\n    \"admicro\": {\n      \"name\": \"AdMicro\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admicro.vn/\",\n      \"companyId\": \"admicro\"\n    },\n    \"admitad.com\": {\n      \"name\": \"Admitad\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.admitad.com/en/#\",\n      \"companyId\": \"admitad\"\n    },\n    \"admixer\": {\n      \"name\": \"Admixer\",\n      \"categoryId\": 4,\n      \"url\": \"https://admixer.com/\",\n      \"companyId\": \"admixer\",\n      \"source\": \"AdGuard\"\n    },\n    \"admixer.net\": {\n      \"name\": \"Admixer\",\n      \"categoryId\": 4,\n      \"url\": \"https://admixer.net/\",\n      \"companyId\": \"admixer\"\n    },\n    \"admized\": {\n      \"name\": \"ADMIZED\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"admo.tv\": {\n      \"name\": \"Admo.tv\",\n      \"categoryId\": 4,\n      \"url\": \"https://admo.tv/\",\n      \"companyId\": \"admo.tv\"\n    },\n    \"admob\": {\n      \"name\": \"AdMob\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admob.com/\",\n      \"companyId\": \"google\"\n    },\n    \"admost\": {\n      \"name\": \"adMOST\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admost.com/\",\n      \"companyId\": \"admost\"\n    },\n    \"admotion\": {\n      \"name\": \"Admotion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.admotionus.com/\",\n      \"companyId\": \"admotion\"\n    },\n    \"admulti\": {\n      \"name\": \"ADmulti\",\n      \"categoryId\": 4,\n      \"url\": \"http://admulti.com\",\n      \"companyId\": \"admulti\"\n    },\n    \"adnegah\": {\n      \"name\": \"Adnegah\",\n      \"categoryId\": 4,\n      \"url\": \"https://adnegah.net/\",\n      \"companyId\": \"adnegah\"\n    },\n    \"adnet\": {\n      \"name\": \"Adnet\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adnet.vn/\",\n      \"companyId\": \"adnet\"\n    },\n    \"adnet.de\": {\n      \"name\": \"adNET.de\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adnet.de\",\n      \"companyId\": \"adnet.de\"\n    },\n    \"adnet_media\": {\n      \"name\": \"Adnet Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adnetmedia.lt/\",\n      \"companyId\": \"adnet_media\"\n    },\n    \"adnetwork.net\": {\n      \"name\": \"AdNetwork.net\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adnetwork.net/\",\n      \"companyId\": \"adnetwork.net\"\n    },\n    \"adnetworkperformance.com\": {\n      \"name\": \"adnetworkperformance.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"adnexio\": {\n      \"name\": \"AdNexio\",\n      \"categoryId\": 4,\n      \"url\": \"http://adnexio.com/\",\n      \"companyId\": \"adnexio\"\n    },\n    \"adnium.com\": {\n      \"name\": \"Adnium\",\n      \"categoryId\": 4,\n      \"url\": \"https://adnium.com/\",\n      \"companyId\": null\n    },\n    \"adnologies\": {\n      \"name\": \"Adnologies\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adnologies.com/\",\n      \"companyId\": \"adnologies_gmbh\"\n    },\n    \"adnow\": {\n      \"name\": \"Adnow\",\n      \"categoryId\": 4,\n      \"url\": \"http://adnow.com/\",\n      \"companyId\": \"adnow\"\n    },\n    \"adnymics\": {\n      \"name\": \"Adnymics\",\n      \"categoryId\": 4,\n      \"url\": \"http://adnymics.com/en/\",\n      \"companyId\": \"adnymics\"\n    },\n    \"adobe_audience_manager\": {\n      \"name\": \"Adobe Audience Manager\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.demdex.com/\",\n      \"companyId\": \"adobe\"\n    },\n    \"adobe_developer\": {\n      \"name\": \"Adobe Developer\",\n      \"categoryId\": 8,\n      \"url\": \"https://developer.adobe.com/\",\n      \"companyId\": \"adobe\",\n      \"source\": \"AdGuard\"\n    },\n    \"adobe_dynamic_media\": {\n      \"name\": \"Adobe Dynamic Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adobe.com/\",\n      \"companyId\": \"adobe\"\n    },\n    \"adobe_dynamic_tag_management\": {\n      \"name\": \"Adobe Dynamic Tag Management\",\n      \"categoryId\": 5,\n      \"url\": \"https://dtm.adobe.com/sign_in\",\n      \"companyId\": \"adobe\"\n    },\n    \"adobe_experience_cloud\": {\n      \"name\": \"Adobe Experience Cloud\",\n      \"categoryId\": 6,\n      \"url\": \"https://business.adobe.com/\",\n      \"companyId\": \"adobe\",\n      \"source\": \"AdGuard\"\n    },\n    \"adobe_experience_league\": {\n      \"name\": \"Adobe Experience League\",\n      \"categoryId\": 6,\n      \"url\": \"https://experienceleague.adobe.com/\",\n      \"companyId\": \"adobe\",\n      \"source\": \"AdGuard\"\n    },\n    \"adobe_login\": {\n      \"name\": \"Adobe Login\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.adobe.com/\",\n      \"companyId\": \"adobe\"\n    },\n    \"adobe_tagmanager\": {\n      \"name\": \"Adobe TagManager\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adobe.com/\",\n      \"companyId\": \"adobe\"\n    },\n    \"adobe_test_and_target\": {\n      \"name\": \"Adobe Target\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adobe.com/marketing/target.html\",\n      \"companyId\": \"adobe\"\n    },\n    \"adobe_typekit\": {\n      \"name\": \"Adobe Typekit\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.adobe.com/\",\n      \"companyId\": \"adobe\"\n    },\n    \"adocean\": {\n      \"name\": \"AdOcean\",\n      \"categoryId\": 4,\n      \"url\": \"http://adocean.cz/en\",\n      \"companyId\": \"adocean\"\n    },\n    \"adometry\": {\n      \"name\": \"Adometry\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adometry.com/\",\n      \"companyId\": \"google\"\n    },\n    \"adomik\": {\n      \"name\": \"Adomik\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"adon_network\": {\n      \"name\": \"AdOn Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adonnetwork.com/\",\n      \"companyId\": \"adon_network\"\n    },\n    \"adonion\": {\n      \"name\": \"AdOnion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adonion.com/\",\n      \"companyId\": \"adonion\"\n    },\n    \"adonly\": {\n      \"name\": \"AdOnly\",\n      \"categoryId\": 4,\n      \"url\": \"https://gloadmarket.com/\",\n      \"companyId\": \"adonly\"\n    },\n    \"adoperator\": {\n      \"name\": \"AdOperator\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adoperator.com/start/\",\n      \"companyId\": \"adoperator\"\n    },\n    \"adoric\": {\n      \"name\": \"Adoric\",\n      \"categoryId\": 6,\n      \"url\": \"https://adoric.com/\",\n      \"companyId\": \"adoric\"\n    },\n    \"adorika\": {\n      \"name\": \"Adorika\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adorika.com/\",\n      \"companyId\": \"adorika\"\n    },\n    \"adosia\": {\n      \"name\": \"Adosia\",\n      \"categoryId\": 4,\n      \"url\": \"https://adosia.com\",\n      \"companyId\": \"adosia\"\n    },\n    \"adotmob.com\": {\n      \"name\": \"Adotmob\",\n      \"categoryId\": 4,\n      \"url\": \"https://adotmob.com/\",\n      \"companyId\": \"adotmob\"\n    },\n    \"adotube\": {\n      \"name\": \"AdoTube\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adotube.com\",\n      \"companyId\": \"exponential_interactive\"\n    },\n    \"adparlor\": {\n      \"name\": \"AdParlor\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adparlor.com/\",\n      \"companyId\": \"fluent\"\n    },\n    \"adpartner\": {\n      \"name\": \"adpartner\",\n      \"categoryId\": 4,\n      \"url\": \"http://adpartner.pro/\",\n      \"companyId\": \"adpartner\"\n    },\n    \"adpeeps\": {\n      \"name\": \"Ad Peeps\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adpeeps.com/\",\n      \"companyId\": \"ad_peeps\"\n    },\n    \"adperfect\": {\n      \"name\": \"AdPerfect\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adperfect.com/\",\n      \"companyId\": \"adperfect\"\n    },\n    \"adperium\": {\n      \"name\": \"AdPerium\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adperium.com/\",\n      \"companyId\": \"adperium\"\n    },\n    \"adpilot\": {\n      \"name\": \"AdPilot\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adpilotgroup.com/\",\n      \"companyId\": \"adpilot\"\n    },\n    \"adplan\": {\n      \"name\": \"AdPlan\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adplan.ne.jp/\",\n      \"companyId\": \"adplan\"\n    },\n    \"adplus\": {\n      \"name\": \"ADPLUS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adplus.co.id/\",\n      \"companyId\": \"adplus\"\n    },\n    \"adprofex\": {\n      \"name\": \"AdProfex\",\n      \"categoryId\": 4,\n      \"url\": \"https://adprofex.com/\",\n      \"companyId\": \"adprofex\",\n      \"source\": \"AdGuard\"\n    },\n    \"adprofy\": {\n      \"name\": \"AdProfy\",\n      \"categoryId\": 4,\n      \"url\": \"http://adprofy.com/\",\n      \"companyId\": \"adprofy\"\n    },\n    \"adpulse\": {\n      \"name\": \"AdPulse\",\n      \"categoryId\": 4,\n      \"url\": \"http://adpulse.ir/\",\n      \"companyId\": \"adpulse.ir\"\n    },\n    \"adpv\": {\n      \"name\": \"Adpv\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adpv.com/\",\n      \"companyId\": \"adpv\"\n    },\n    \"adreactor\": {\n      \"name\": \"AdReactor\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adreactor.com/\",\n      \"companyId\": \"adreactor\"\n    },\n    \"adrecord\": {\n      \"name\": \"Adrecord\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adrecord.com/\",\n      \"companyId\": \"adrecord\"\n    },\n    \"adrecover\": {\n      \"name\": \"AdRecover\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adrecover.com/\",\n      \"companyId\": \"adpushup\"\n    },\n    \"adresult\": {\n      \"name\": \"ADResult\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adresult.jp/\",\n      \"companyId\": \"adresult\"\n    },\n    \"adriver\": {\n      \"name\": \"AdRiver\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adriver.ru/\",\n      \"companyId\": \"ad_river\"\n    },\n    \"adroll\": {\n      \"name\": \"AdRoll\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adroll.com/\",\n      \"companyId\": \"adroll\"\n    },\n    \"adroll_pixel\": {\n      \"name\": \"AdRoll Pixel\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adroll.com/\",\n      \"companyId\": \"adroll\"\n    },\n    \"adroll_roundtrip\": {\n      \"name\": \"AdRoll Roundtrip\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adroll.com/\",\n      \"companyId\": \"adroll\"\n    },\n    \"adrom\": {\n      \"name\": \"adRom\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adrom.net/\",\n      \"companyId\": null\n    },\n    \"adru.net\": {\n      \"name\": \"adru.net\",\n      \"categoryId\": 4,\n      \"url\": \"http://adru.net/\",\n      \"companyId\": \"adru.net\"\n    },\n    \"adrunnr\": {\n      \"name\": \"AdRunnr\",\n      \"categoryId\": 4,\n      \"url\": \"https://adrunnr.com/\",\n      \"companyId\": \"adrunnr\"\n    },\n    \"adsame\": {\n      \"name\": \"Adsame\",\n      \"categoryId\": 4,\n      \"url\": \"http://adsame.com/\",\n      \"companyId\": \"adsame\"\n    },\n    \"adsbookie\": {\n      \"name\": \"AdsBookie\",\n      \"categoryId\": 4,\n      \"url\": \"http://adsbookie.com/\",\n      \"companyId\": null\n    },\n    \"adscale\": {\n      \"name\": \"AdScale\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adscale.de/\",\n      \"companyId\": \"stroer\"\n    },\n    \"adscience\": {\n      \"name\": \"Adscience\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adscience.nl/\",\n      \"companyId\": \"adscience\"\n    },\n    \"adsco.re\": {\n      \"name\": \"Adscore\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adscore.com/\",\n      \"companyId\": null\n    },\n    \"adsensecamp\": {\n      \"name\": \"AdsenseCamp\",\n      \"categoryId\": 4,\n      \"url\": \"http://adsensecamp.com\",\n      \"companyId\": \"adsensecamp\"\n    },\n    \"adserverpub\": {\n      \"name\": \"AdServerPub\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adserverpub.com/\",\n      \"companyId\": \"adserverpub\"\n    },\n    \"adservice_media\": {\n      \"name\": \"Adservice Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adservicemedia.com/\",\n      \"companyId\": \"adservice_media\"\n    },\n    \"adsfactor\": {\n      \"name\": \"Adsfactor\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adsfactor.com/\",\n      \"companyId\": \"pixels_asia\"\n    },\n    \"adside\": {\n      \"name\": \"AdSide\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adside.com/\",\n      \"companyId\": \"adside\"\n    },\n    \"adskeeper\": {\n      \"name\": \"AdsKeeper\",\n      \"categoryId\": 4,\n      \"url\": \"http://adskeeper.co.uk/\",\n      \"companyId\": \"adskeeper\"\n    },\n    \"adskom\": {\n      \"name\": \"ADSKOM\",\n      \"categoryId\": 4,\n      \"url\": \"http://adskom.com/\",\n      \"companyId\": \"adskom\"\n    },\n    \"adslot\": {\n      \"name\": \"Adslot\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adslot.com/\",\n      \"companyId\": \"adslot\"\n    },\n    \"adsnative\": {\n      \"name\": \"adsnative\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adsnative.com/\",\n      \"companyId\": \"adsnative\"\n    },\n    \"adsniper.ru\": {\n      \"name\": \"AdSniper\",\n      \"categoryId\": 4,\n      \"url\": \"http://ad-sniper.com/\",\n      \"companyId\": \"adsniper\"\n    },\n    \"adspeed\": {\n      \"name\": \"AdSpeed\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adspeed.com/\",\n      \"companyId\": \"adspeed\"\n    },\n    \"adspyglass\": {\n      \"name\": \"AdSpyglass\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adspyglass.com/\",\n      \"companyId\": \"adspyglass\"\n    },\n    \"adstage\": {\n      \"name\": \"AdStage\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adstage.io/\",\n      \"companyId\": \"adstage\"\n    },\n    \"adstanding\": {\n      \"name\": \"AdStanding\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adstanding.com/en/\",\n      \"companyId\": \"adstanding\"\n    },\n    \"adstars\": {\n      \"name\": \"Adstars\",\n      \"categoryId\": 4,\n      \"url\": \"http://adstars.co.id\",\n      \"companyId\": \"adstars\"\n    },\n    \"adstir\": {\n      \"name\": \"adstir\",\n      \"categoryId\": 4,\n      \"url\": \"https://en.ad-stir.com/\",\n      \"companyId\": \"united_inc\"\n    },\n    \"adsupply\": {\n      \"name\": \"AdSupply\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adsupply.com/\",\n      \"companyId\": \"adsupply\"\n    },\n    \"adswizz\": {\n      \"name\": \"AdsWizz\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adswizz.com/\",\n      \"companyId\": \"adswizz\"\n    },\n    \"adtaily\": {\n      \"name\": \"AdTaily\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adtaily.pl/\",\n      \"companyId\": \"adtaily\"\n    },\n    \"adtarget.me\": {\n      \"name\": \"Adtarget.me\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adtarget.me/\",\n      \"companyId\": \"adtarget.me\"\n    },\n    \"adtech\": {\n      \"name\": \"ADTECH\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.adtechus.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"adtegrity\": {\n      \"name\": \"Adtegrity\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adtegrity.com/\",\n      \"companyId\": \"adtegrity\"\n    },\n    \"adtelligence.de\": {\n      \"name\": \"Adtelligence\",\n      \"categoryId\": 4,\n      \"url\": \"https://adtelligence.com/\",\n      \"companyId\": null\n    },\n    \"adtheorent\": {\n      \"name\": \"Adtheorent\",\n      \"categoryId\": 4,\n      \"url\": \"http://adtheorent.com/\",\n      \"companyId\": \"adtheorant\"\n    },\n    \"adthink\": {\n      \"name\": \"Adthink\",\n      \"categoryId\": 4,\n      \"url\": \"https://adthink.com/\",\n      \"companyId\": \"adthink\"\n    },\n    \"adtiger\": {\n      \"name\": \"AdTiger\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adtiger.de/\",\n      \"companyId\": \"adtiger\"\n    },\n    \"adtima\": {\n      \"name\": \"Adtima\",\n      \"categoryId\": 4,\n      \"url\": \"http://adtima.vn/\",\n      \"companyId\": \"adtima\"\n    },\n    \"adtng.com\": {\n      \"name\": \"adtng.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"adtoma\": {\n      \"name\": \"Adtoma\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adtoma.com/\",\n      \"companyId\": \"adtoma\"\n    },\n    \"adtr02.com\": {\n      \"name\": \"adtr02.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"adtraction\": {\n      \"name\": \"Adtraction\",\n      \"categoryId\": 4,\n      \"url\": \"http://adtraction.com/\",\n      \"companyId\": \"adtraction\"\n    },\n    \"adtraxx\": {\n      \"name\": \"AdTraxx\",\n      \"categoryId\": 4,\n      \"url\": \"https://www1.adtraxx.de/\",\n      \"companyId\": \"adtrax\"\n    },\n    \"adtriba.com\": {\n      \"name\": \"AdTriba\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.adtriba.com/\",\n      \"companyId\": null\n    },\n    \"adtrue\": {\n      \"name\": \"Adtrue\",\n      \"categoryId\": 4,\n      \"url\": \"http://adtrue.com/\",\n      \"companyId\": \"adtrue\"\n    },\n    \"adtrustmedia\": {\n      \"name\": \"AdTrustMedia\",\n      \"categoryId\": 4,\n      \"url\": \"https://adtrustmedia.com/\",\n      \"companyId\": \"adtrustmedia\"\n    },\n    \"adtube\": {\n      \"name\": \"AdTube\",\n      \"categoryId\": 4,\n      \"url\": \"http://adtube.ir/\",\n      \"companyId\": \"adtube\"\n    },\n    \"adult_webmaster_empire\": {\n      \"name\": \"Adult Webmaster Empire\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.awempire.com/\",\n      \"companyId\": \"adult_webmaster_empire\"\n    },\n    \"adultadworld\": {\n      \"name\": \"AdultAdWorld\",\n      \"categoryId\": 3,\n      \"url\": \"http://adultadworld.com/\",\n      \"companyId\": \"adult_adworld\"\n    },\n    \"adup-tech.com\": {\n      \"name\": \"AdUp Technology\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adup-tech.com/\",\n      \"companyId\": \"adup_technology\"\n    },\n    \"advaction\": {\n      \"name\": \"Advaction\",\n      \"categoryId\": 4,\n      \"url\": \"http://advaction.ru/\",\n      \"companyId\": \"advaction\"\n    },\n    \"advalo\": {\n      \"name\": \"Advalo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.advalo.com\",\n      \"companyId\": \"advalo\"\n    },\n    \"advanced_hosters\": {\n      \"name\": \"Advanced Hosters\",\n      \"categoryId\": 9,\n      \"url\": \"https://advancedhosters.com/\",\n      \"companyId\": null\n    },\n    \"advark\": {\n      \"name\": \"Advark\",\n      \"categoryId\": 4,\n      \"url\": \"https://advarkads.com/\",\n      \"companyId\": \"advark\"\n    },\n    \"adventori\": {\n      \"name\": \"ADventori\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.adventori.com/\",\n      \"companyId\": \"adventori\"\n    },\n    \"adverline\": {\n      \"name\": \"Adverline\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adverline.com/\",\n      \"companyId\": \"adverline\"\n    },\n    \"adversal\": {\n      \"name\": \"Adversal\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.adversal.com/\",\n      \"companyId\": \"adversal\"\n    },\n    \"adverserve\": {\n      \"name\": \"adverServe\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adverserve.com/\",\n      \"companyId\": \"adverserve\"\n    },\n    \"adverteerdirect\": {\n      \"name\": \"Adverteerdirect\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adverteerdirect.nl/\",\n      \"companyId\": \"adverteerdirect\"\n    },\n    \"adverticum\": {\n      \"name\": \"Adverticum\",\n      \"categoryId\": 4,\n      \"url\": \"https://adverticum.net/english/\",\n      \"companyId\": \"adverticum\"\n    },\n    \"advertise.com\": {\n      \"name\": \"Advertise.com\",\n      \"categoryId\": 4,\n      \"url\": \"http://advertise.com/\",\n      \"companyId\": \"advertise.com\"\n    },\n    \"advertisespace\": {\n      \"name\": \"AdvertiseSpace\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.advertisespace.com/\",\n      \"companyId\": \"advertisespace\"\n    },\n    \"advertising.com\": {\n      \"name\": \"Verizon Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.verizonmedia.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"advertlets\": {\n      \"name\": \"Advertlets\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.advertlets.com/\",\n      \"companyId\": \"advertlets\"\n    },\n    \"advertserve\": {\n      \"name\": \"AdvertServe\",\n      \"categoryId\": 4,\n      \"url\": \"https://secure.advertserve.com/\",\n      \"companyId\": \"advertserve\"\n    },\n    \"advidi\": {\n      \"name\": \"Advidi\",\n      \"categoryId\": 4,\n      \"url\": \"http://advidi.com/\",\n      \"companyId\": \"advidi\"\n    },\n    \"advmaker.ru\": {\n      \"name\": \"advmaker.ru\",\n      \"categoryId\": 4,\n      \"url\": \"http://advmaker.ru/\",\n      \"companyId\": \"advmaker.ru\"\n    },\n    \"advolution\": {\n      \"name\": \"Advolution\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.advolution.de\",\n      \"companyId\": \"advolution\"\n    },\n    \"adwebster\": {\n      \"name\": \"adwebster\",\n      \"categoryId\": 4,\n      \"url\": \"http://adwebster.com\",\n      \"companyId\": \"adwebster\"\n    },\n    \"adwit\": {\n      \"name\": \"Adwit\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adwitserver.com\",\n      \"companyId\": \"adwit\"\n    },\n    \"adworx.at\": {\n      \"name\": \"ADworx\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adworx.at/\",\n      \"companyId\": \"ors\"\n    },\n    \"adworxs.net\": {\n      \"name\": \"adworxs.net\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adworxs.net/?lang=en\",\n      \"companyId\": null\n    },\n    \"adxion\": {\n      \"name\": \"adXion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adxion.com\",\n      \"companyId\": \"adxion\"\n    },\n    \"adxpansion\": {\n      \"name\": \"AdXpansion\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.adxpansion.com/\",\n      \"companyId\": \"adxpansion\"\n    },\n    \"adxpose\": {\n      \"name\": \"AdXpose\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adxpose.com/home.page\",\n      \"companyId\": \"comscore\"\n    },\n    \"adxprtz.com\": {\n      \"name\": \"adxprtz.com\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"adyoulike\": {\n      \"name\": \"Adyoulike\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adyoulike.com/\",\n      \"companyId\": \"adyoulike\"\n    },\n    \"adzerk\": {\n      \"name\": \"Adzerk\",\n      \"categoryId\": 4,\n      \"url\": \"http://adzerk.com/\",\n      \"companyId\": \"adzerk\"\n    },\n    \"adzly\": {\n      \"name\": \"adzly\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adzly.com/\",\n      \"companyId\": \"adzly\"\n    },\n    \"aemediatraffic\": {\n      \"name\": \"Aemediatraffic\",\n      \"categoryId\": 6,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"aerify_media\": {\n      \"name\": \"Aerify Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://aerifymedia.com/\",\n      \"companyId\": \"aerify_media\"\n    },\n    \"aeris_weather\": {\n      \"name\": \"Aeris Weather\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.aerisweather.com/\",\n      \"companyId\": \"aerisweather\"\n    },\n    \"affectv\": {\n      \"name\": \"Hybrid Theory\",\n      \"categoryId\": 4,\n      \"url\": \"https://hybridtheory.com/\",\n      \"companyId\": \"affectv\"\n    },\n    \"affilbox\": {\n      \"name\": \"Affilbox\",\n      \"categoryId\": 4,\n      \"url\": \"https://affilbox.com/\",\n      \"companyId\": \"affilbox\",\n      \"source\": \"AdGuard\"\n    },\n    \"affiliate-b\": {\n      \"name\": \"Affiliate-B\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.affiliate-b.com/\",\n      \"companyId\": \"affiliate_b\"\n    },\n    \"affiliate4you\": {\n      \"name\": \"Affiliate4You\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.affiliate4you.nl/\",\n      \"companyId\": \"family_blend\"\n    },\n    \"affiliatebuzz\": {\n      \"name\": \"AffiliateBuzz\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.affiliatebuzz.com/\",\n      \"companyId\": \"affiliatebuzz\"\n    },\n    \"affiliatefuture\": {\n      \"name\": \"AffiliateFuture\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.affiliatefuture.com\",\n      \"companyId\": \"affiliatefuture\"\n    },\n    \"affiliatelounge\": {\n      \"name\": \"AffiliateLounge\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.affiliatelounge.com/\",\n      \"companyId\": \"betsson_group_affiliates\"\n    },\n    \"affiliation_france\": {\n      \"name\": \"Affiliation France\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.affiliation-france.com/\",\n      \"companyId\": \"affiliation-france\"\n    },\n    \"affiliator\": {\n      \"name\": \"Affiliator\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.affiliator.com/\",\n      \"companyId\": \"affiliator\"\n    },\n    \"affiliaweb\": {\n      \"name\": \"Affiliaweb\",\n      \"categoryId\": 4,\n      \"url\": \"http://affiliaweb.fr/\",\n      \"companyId\": \"affiliaweb\"\n    },\n    \"affilinet\": {\n      \"name\": \"affilinet\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.affili.net/\",\n      \"companyId\": \"axel_springer\"\n    },\n    \"affimax\": {\n      \"name\": \"AffiMax\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.affimax.de\",\n      \"companyId\": \"affimax\"\n    },\n    \"affinity\": {\n      \"name\": \"Affinity\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.affinity.com/\",\n      \"companyId\": \"affinity\"\n    },\n    \"affinity.by\": {\n      \"name\": \"Affinity.by\",\n      \"categoryId\": 4,\n      \"url\": \"http://affinity.by\",\n      \"companyId\": \"affinity_digital_agency\"\n    },\n    \"affiz_cpm\": {\n      \"name\": \"Affiz CPM\",\n      \"categoryId\": 4,\n      \"url\": \"http://cpm.affiz.com/home\",\n      \"companyId\": \"affiz_cpm\"\n    },\n    \"afftrack\": {\n      \"name\": \"Afftrack\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.afftrack.com/\",\n      \"companyId\": \"afftrack\"\n    },\n    \"afgr2.com\": {\n      \"name\": \"afgr2.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"afilio\": {\n      \"name\": \"Afilio\",\n      \"categoryId\": 6,\n      \"url\": \"http://afilio.com.br/\",\n      \"companyId\": \"afilio\"\n    },\n    \"afs_analystics\": {\n      \"name\": \"AFS Analystics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.afsanalytics.com/\",\n      \"companyId\": \"afs_analytics\"\n    },\n    \"aftonbladet_ads\": {\n      \"name\": \"Aftonbladet Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://annonswebb.aftonbladet.se/\",\n      \"companyId\": \"aftonbladet\"\n    },\n    \"aftv-serving.bid\": {\n      \"name\": \"aftv-serving.bid\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"aggregate_knowledge\": {\n      \"name\": \"Aggregate Knowledge\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.aggregateknowledge.com/\",\n      \"companyId\": \"neustar\"\n    },\n    \"agilone\": {\n      \"name\": \"AgilOne\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.agilone.com/\",\n      \"companyId\": \"agilone\"\n    },\n    \"agora\": {\n      \"name\": \"Agora\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.agora.pl/\",\n      \"companyId\": \"agora_sa\"\n    },\n    \"ahalogy\": {\n      \"name\": \"Ahalogy\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.ahalogy.com/\",\n      \"companyId\": \"ahalogy\"\n    },\n    \"ai_media_group\": {\n      \"name\": \"Ai Media Group\",\n      \"categoryId\": 4,\n      \"url\": \"http://aimediagroup.com/\",\n      \"companyId\": \"ai_media_group\"\n    },\n    \"aidata\": {\n      \"name\": \"Aidata\",\n      \"categoryId\": 4,\n      \"url\": \"http://aidata.me/\",\n      \"companyId\": \"aidata\"\n    },\n    \"aim4media\": {\n      \"name\": \"Aim4Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://aim4media.com\",\n      \"companyId\": \"aim4media\"\n    },\n    \"airbnb\": {\n      \"name\": \"Airbnb\",\n      \"categoryId\": 6,\n      \"url\": \"https://affiliate.withairbnb.com/\",\n      \"companyId\": null\n    },\n    \"airbrake\": {\n      \"name\": \"Airbrake\",\n      \"categoryId\": 4,\n      \"url\": \"https://airbrake.io/\",\n      \"companyId\": \"airbrake\"\n    },\n    \"airpr.com\": {\n      \"name\": \"AirPR\",\n      \"categoryId\": 6,\n      \"url\": \"https://airpr.com/\",\n      \"companyId\": \"airpr\"\n    },\n    \"airpush\": {\n      \"name\": \"Airpush\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.airpush.com/\",\n      \"companyId\": \"airpush\"\n    },\n    \"akamai_technologies\": {\n      \"name\": \"Akamai Technologies\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.akamai.com/\",\n      \"companyId\": \"akamai\",\n      \"source\": \"AdGuard\"\n    },\n    \"akamoihd.net\": {\n      \"name\": \"akamoihd.net\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"akane\": {\n      \"name\": \"AkaNe\",\n      \"categoryId\": 4,\n      \"url\": \"http://akane-ad.com/\",\n      \"companyId\": \"akane\"\n    },\n    \"akanoo\": {\n      \"name\": \"Akanoo\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.akanoo.com/\",\n      \"companyId\": \"akanoo\"\n    },\n    \"akavita\": {\n      \"name\": \"Akavita\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.akavita.by/en\",\n      \"companyId\": \"akavita\"\n    },\n    \"al_bawaba_advertising\": {\n      \"name\": \"Al Bawaba Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.albawaba.com/advertising\",\n      \"companyId\": \"al_bawaba\"\n    },\n    \"albacross\": {\n      \"name\": \"Albacross\",\n      \"categoryId\": 4,\n      \"url\": \"https://albacross.com\",\n      \"companyId\": \"albacross\"\n    },\n    \"aldi-international.com\": {\n      \"name\": \"aldi-international.com\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"alenty\": {\n      \"name\": \"Alenty\",\n      \"categoryId\": 4,\n      \"url\": \"https://about.ads.microsoft.com/en-us/solutions/xandr/xandr-premium-programmatic-advertising\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"alephd.com\": {\n      \"name\": \"alephd\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.alephd.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"alexa_metrics\": {\n      \"name\": \"Alexa Metrics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.alexa.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"alexa_traffic_rank\": {\n      \"name\": \"Alexa Traffic Rank\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.alexa.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"algolia.net\": {\n      \"name\": \"algolia\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.algolia.com/\",\n      \"companyId\": null\n    },\n    \"algovid.com\": {\n      \"name\": \"algovid.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"alibaba.com\": {\n      \"name\": \"Alibaba\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.alibaba.com/\",\n      \"companyId\": \"softbank\",\n      \"source\": \"AdGuard\"\n    },\n    \"alibaba_cloud\": {\n      \"name\": \"Alibaba Cloud\",\n      \"categoryId\": 10,\n      \"url\": \"https://www.alibabacloud.com/\",\n      \"companyId\": \"softbank\",\n      \"source\": \"AdGuard\"\n    },\n    \"alibaba_ucbrowser\": {\n      \"name\": \"UC Browser\",\n      \"categoryId\": 8,\n      \"url\": \"https://ucweb.com/\",\n      \"companyId\": \"softbank\",\n      \"source\": \"AdGuard\"\n    },\n    \"alipay.com\": {\n      \"name\": \"Alipay\",\n      \"categoryId\": 2,\n      \"url\": \"https://global.alipay.com/\",\n      \"companyId\": \"softbank\",\n      \"source\": \"AdGuard\"\n    },\n    \"alivechat\": {\n      \"name\": \"AliveChat\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.websitealive.com/\",\n      \"companyId\": \"websitealive\"\n    },\n    \"allegro.pl\": {\n      \"name\": \"Allegro\",\n      \"categoryId\": 8,\n      \"url\": \"https://allegro.pl\",\n      \"companyId\": \"allegro.pl\"\n    },\n    \"allin\": {\n      \"name\": \"Allin\",\n      \"categoryId\": 6,\n      \"url\": \"http://allin.com.br/\",\n      \"companyId\": \"allin\"\n    },\n    \"allo-pages.fr\": {\n      \"name\": \"Allo-Pages\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.allo-pages.fr/\",\n      \"companyId\": \"links_lab\"\n    },\n    \"allotraffic\": {\n      \"name\": \"AlloTraffic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.allotraffic.com/\",\n      \"companyId\": \"allotraffic\"\n    },\n    \"allure_media\": {\n      \"name\": \"Allure Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.alluremedia.com.au\",\n      \"companyId\": \"allure_media\"\n    },\n    \"allyes\": {\n      \"name\": \"Allyes\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.allyes.com/\",\n      \"companyId\": \"allyes\"\n    },\n    \"alooma\": {\n      \"name\": \"Alooma\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.alooma.com/\",\n      \"companyId\": \"alooma\"\n    },\n    \"altitude_digital\": {\n      \"name\": \"Altitude Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.altitudedigital.com/\",\n      \"companyId\": \"altitude_digital\"\n    },\n    \"amadesa\": {\n      \"name\": \"Amadesa\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.amadesa.com/\",\n      \"companyId\": \"amadesa\"\n    },\n    \"amap\": {\n      \"name\": \"Amap\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.amap.com/\",\n      \"companyId\": \"softbank\",\n      \"source\": \"AdGuard\"\n    },\n    \"amazon\": {\n      \"name\": \"Amazon.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.amazon.com\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"amazon_adsystem\": {\n      \"name\": \"Amazon Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"https://advertising.amazon.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"amazon_associates\": {\n      \"name\": \"Amazon Associates\",\n      \"categoryId\": 4,\n      \"url\": \"http://aws.amazon.com/associates/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"amazon_cdn\": {\n      \"name\": \"Amazon CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.amazon.com\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"amazon_cloudfront\": {\n      \"name\": \"Amazon CloudFront\",\n      \"categoryId\": 10,\n      \"url\": \"https://aws.amazon.com/cloudfront/?nc1=h_ls\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"amazon_mobile_ads\": {\n      \"name\": \"Amazon Mobile Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.amazon.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"amazon_payments\": {\n      \"name\": \"Amazon Payments\",\n      \"categoryId\": 2,\n      \"url\": \"https://pay.amazon.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"amazon_video\": {\n      \"name\": \"Amazon Instant Video\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.amazon.com\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"amazon_web_services\": {\n      \"name\": \"Amazon Web Services\",\n      \"categoryId\": 10,\n      \"url\": \"https://aws.amazon.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"ambient_digital\": {\n      \"name\": \"Ambient Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adnetwork.vn/\",\n      \"companyId\": \"ambient_digital\"\n    },\n    \"amgload.net\": {\n      \"name\": \"amgload.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"amoad\": {\n      \"name\": \"AMoAd\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.amoad.com/\",\n      \"companyId\": \"amoad\"\n    },\n    \"amobee\": {\n      \"name\": \"Amobee\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.amobee.com/\",\n      \"companyId\": \"singtel\"\n    },\n    \"amp_platform\": {\n      \"name\": \"AMP Platform\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.collective.com/\",\n      \"companyId\": \"collective\"\n    },\n    \"amplitude\": {\n      \"name\": \"Amplitude\",\n      \"categoryId\": 6,\n      \"url\": \"https://amplitude.com/\",\n      \"companyId\": \"amplitude\"\n    },\n    \"ampproject.org\": {\n      \"name\": \"AMP Project\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.ampproject.org/\",\n      \"companyId\": \"google\"\n    },\n    \"anametrix\": {\n      \"name\": \"Anametrix\",\n      \"categoryId\": 6,\n      \"url\": \"http://anametrix.com/\",\n      \"companyId\": \"anametrix\"\n    },\n    \"ancestry_cdn\": {\n      \"name\": \"Ancestry CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.ancestry.com/\",\n      \"companyId\": \"ancestry\"\n    },\n    \"ancora\": {\n      \"name\": \"Ancora\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ancoramediasolutions.com/\",\n      \"companyId\": \"ancora\"\n    },\n    \"android\": {\n      \"name\": \"Android\",\n      \"categoryId\": 101,\n      \"url\": \"https://www.android.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"anetwork\": {\n      \"name\": \"Anetwork\",\n      \"categoryId\": 4,\n      \"url\": \"http://anetwork.ir/\",\n      \"companyId\": \"anetwork\"\n    },\n    \"aniview.com\": {\n      \"name\": \"AniView\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.aniview.com/\",\n      \"companyId\": null\n    },\n    \"anonymousads\": {\n      \"name\": \"AnonymousAds\",\n      \"categoryId\": 4,\n      \"url\": \"https://a-ads.com/\",\n      \"companyId\": \"anonymousads\"\n    },\n    \"anormal_tracker\": {\n      \"name\": \"Anormal Tracker\",\n      \"categoryId\": 6,\n      \"url\": \"http://anormal-tracker.de/\",\n      \"companyId\": \"anormal-tracker\"\n    },\n    \"answers_cloud_service\": {\n      \"name\": \"Answers Cloud Service\",\n      \"categoryId\": 1,\n      \"url\": \"http://www.answers.com/\",\n      \"companyId\": \"answers.com\"\n    },\n    \"ants\": {\n      \"name\": \"Ants\",\n      \"categoryId\": 7,\n      \"url\": \"http://ants.vn/en/\",\n      \"companyId\": \"ants\"\n    },\n    \"anvato\": {\n      \"name\": \"Anvato\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.anvato.com/\",\n      \"companyId\": \"google\"\n    },\n    \"anyclip\": {\n      \"name\": \"AnyClip\",\n      \"categoryId\": 0,\n      \"url\": \"https://anyclip.com\",\n      \"companyId\": \"anyclip\"\n    },\n    \"aol_be_on\": {\n      \"name\": \"AOL Be On\",\n      \"categoryId\": 4,\n      \"url\": \"http://beon.aolnetworks.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"aol_cdn\": {\n      \"name\": \"AOL CDN\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.verizon.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"aol_images_cdn\": {\n      \"name\": \"AOL Images CDN\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.verizon.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"apa.at\": {\n      \"name\": \"Apa\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.apa.at/Site/index.de.html\",\n      \"companyId\": \"apa\"\n    },\n    \"apester\": {\n      \"name\": \"Apester\",\n      \"categoryId\": 4,\n      \"url\": \"http://apester.com/\",\n      \"companyId\": \"apester\"\n    },\n    \"apicit.net\": {\n      \"name\": \"apicit.net\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"aplus_analytics\": {\n      \"name\": \"Aplus Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://ww.deluxe.com/\",\n      \"companyId\": \"deluxe\"\n    },\n    \"appcenter\": {\n      \"name\": \"Microsoft App Center\",\n      \"categoryId\": 5,\n      \"url\": \"https://appcenter.ms/\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"appcues\": {\n      \"name\": \"Appcues\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.appcues.com/\",\n      \"companyId\": null\n    },\n    \"appdynamics\": {\n      \"name\": \"AppDynamics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.appdynamics.com\",\n      \"companyId\": \"appdynamics\"\n    },\n    \"appier\": {\n      \"name\": \"Appier\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.appier.com/en/index.html\",\n      \"companyId\": \"appier\"\n    },\n    \"apple\": {\n      \"name\": \"Apple\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.apple.com/\",\n      \"companyId\": \"apple\",\n      \"source\": \"AdGuard\"\n    },\n    \"apple_ads\": {\n      \"name\": \"Apple Search Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://searchads.apple.com/\",\n      \"companyId\": \"apple\",\n      \"source\": \"AdGuard\"\n    },\n    \"applifier\": {\n      \"name\": \"Applifier\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.applifier.com/\",\n      \"companyId\": \"applifier\"\n    },\n    \"applovin\": {\n      \"name\": \"AppLovin\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.applovin.com\",\n      \"companyId\": \"applovin\"\n    },\n    \"appmetrx\": {\n      \"name\": \"AppMetrx\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.engago.com\",\n      \"companyId\": \"engago_technologies\"\n    },\n    \"appnexus\": {\n      \"name\": \"AppNexus\",\n      \"categoryId\": 4,\n      \"url\": \"https://about.ads.microsoft.com/en-us/solutions/xandr/xandr-premium-programmatic-advertising\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"appsflyer\": {\n      \"name\": \"AppsFlyer\",\n      \"categoryId\": 101,\n      \"url\": \"https://www.appsflyer.com/\",\n      \"companyId\": \"appsflyer\",\n      \"source\": \"AdGuard\"\n    },\n    \"apptv\": {\n      \"name\": \"appTV\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.apptv.com/\",\n      \"companyId\": \"apptv\"\n    },\n    \"apture\": {\n      \"name\": \"Apture\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.apture.com/\",\n      \"companyId\": \"google\"\n    },\n    \"arcpublishing\": {\n      \"name\": \"Arc Publishing\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.arcpublishing.com/\",\n      \"companyId\": \"arc_publishing\"\n    },\n    \"ard.de\": {\n      \"name\": \"ard.de\",\n      \"categoryId\": 0,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"are_you_a_human\": {\n      \"name\": \"Are You a Human\",\n      \"categoryId\": 6,\n      \"url\": \"https://areyouahuman.com/\",\n      \"companyId\": \"distil_networks\"\n    },\n    \"arkoselabs.com\": {\n      \"name\": \"Arkose Labs\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.arkoselabs.com/\",\n      \"companyId\": null\n    },\n    \"art19\": {\n      \"name\": \"Art19\",\n      \"categoryId\": 4,\n      \"url\": \"https://art19.com/\",\n      \"companyId\": \"art19\"\n    },\n    \"artimedia\": {\n      \"name\": \"Artimedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://arti-media.net/en/\",\n      \"companyId\": \"artimedia\"\n    },\n    \"artlebedev.ru\": {\n      \"name\": \"Art.Lebedev\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.artlebedev.ru/\",\n      \"companyId\": \"art.lebedev_studio\"\n    },\n    \"aruba_media_marketing\": {\n      \"name\": \"Aruba Media Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.arubamediamarketing.it/\",\n      \"companyId\": \"aruba_media_marketing\"\n    },\n    \"arvato_canvas_fp\": {\n      \"name\": \"Arvato Canvas FP\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.arvato.com/\",\n      \"companyId\": \"arvato\"\n    },\n    \"asambeauty.com\": {\n      \"name\": \"asambeauty.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.asambeauty.com/\",\n      \"companyId\": null\n    },\n    \"ask.com\": {\n      \"name\": \"Ask.com\",\n      \"categoryId\": 7,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"aspnetcdn\": {\n      \"name\": \"Microsoft Ajax CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"assemblyexchange\": {\n      \"name\": \"Assembly Exchange\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.medialab.la/\",\n      \"companyId\": \"medialab\",\n      \"source\": \"AdGuard\"\n    },\n    \"astronomer\": {\n      \"name\": \"Astronomer\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.astronomer.io\",\n      \"companyId\": \"astronomer\"\n    },\n    \"at_internet\": {\n      \"name\": \"AT Internet\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.xiti.com/\",\n      \"companyId\": \"at_internet\"\n    },\n    \"atedra\": {\n      \"name\": \"Atedra\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.atedra.com/\",\n      \"companyId\": \"atedra\"\n    },\n    \"atg_group\": {\n      \"name\": \"ATG Ad Tech Group\",\n      \"categoryId\": 4,\n      \"url\": \"https://ad-tech-group.com/\",\n      \"companyId\": null\n    },\n    \"atg_optimization\": {\n      \"name\": \"ATG Optimization\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.atg.com/en/products-services/optimization/\",\n      \"companyId\": \"oracle\"\n    },\n    \"atg_recommendations\": {\n      \"name\": \"ATG Recommendations\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.atg.com/en/products-services/optimization/recommendations/\",\n      \"companyId\": \"oracle\"\n    },\n    \"atlas\": {\n      \"name\": \"Atlas\",\n      \"categoryId\": 4,\n      \"url\": \"https://atlassolutions.com\",\n      \"companyId\": \"facebook\"\n    },\n    \"atlas_profitbuilder\": {\n      \"name\": \"Atlas ProfitBuilder\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.atlassolutions.com/\",\n      \"companyId\": \"atlas\"\n    },\n    \"atlassian.net\": {\n      \"name\": \"Atlassian\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.atlassian.com/\",\n      \"companyId\": \"atlassian\"\n    },\n    \"atlassian_marketplace\": {\n      \"name\": \"Atlassian Marketplace\",\n      \"categoryId\": 9,\n      \"url\": \"https://marketplace.atlassian.com/\",\n      \"companyId\": \"atlassian\"\n    },\n    \"atomz_search\": {\n      \"name\": \"Atomz Search\",\n      \"categoryId\": 2,\n      \"url\": \"http://atomz.com/\",\n      \"companyId\": \"atomz\"\n    },\n    \"atsfi_de\": {\n      \"name\": \"atsfi.de\",\n      \"categoryId\": 11,\n      \"url\": \"http://www.axelspringer.de/en/index.html\",\n      \"companyId\": \"axel_springer\"\n    },\n    \"attracta\": {\n      \"name\": \"Attracta\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.attracta.com/\",\n      \"companyId\": \"attracta\"\n    },\n    \"attraqt\": {\n      \"name\": \"Attraqt\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.locayta.com/\",\n      \"companyId\": \"attraqt\"\n    },\n    \"audience2media\": {\n      \"name\": \"Audience2Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.audience2media.com/\",\n      \"companyId\": \"audience2media\"\n    },\n    \"audience_ad_network\": {\n      \"name\": \"Audience Ad Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.audienceadnetwork.com\",\n      \"companyId\": \"bridgeline_digital\"\n    },\n    \"audience_science\": {\n      \"name\": \"Audience Science\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.audiencescience.com/\",\n      \"companyId\": \"audiencescience\"\n    },\n    \"audiencerate\": {\n      \"name\": \"AudienceRate\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.audiencerate.com/\",\n      \"companyId\": \"audiencerate\"\n    },\n    \"audiencesquare.com\": {\n      \"name\": \"Audience Square\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.audiencesquare.fr/\",\n      \"companyId\": \"audience_square\"\n    },\n    \"auditude\": {\n      \"name\": \"Auditude\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.auditude.com/\",\n      \"companyId\": \"adobe\"\n    },\n    \"audtd.com\": {\n      \"name\": \"Auditorius\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.auditorius.ru/\",\n      \"companyId\": \"auditorius\"\n    },\n    \"augur\": {\n      \"name\": \"Augur\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.augur.io/\",\n      \"companyId\": \"augur\"\n    },\n    \"aumago\": {\n      \"name\": \"Aumago\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.aumago.com/\",\n      \"companyId\": \"aumago\"\n    },\n    \"aurea_clicktracks\": {\n      \"name\": \"Aurea ClickTracks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clicktracks.com/\",\n      \"companyId\": \"aurea\"\n    },\n    \"ausgezeichnet_org\": {\n      \"name\": \"ausgezeichnet.org\",\n      \"categoryId\": 2,\n      \"url\": \"http://ausgezeichnet.org/\",\n      \"companyId\": null\n    },\n    \"australia.gov\": {\n      \"name\": \"Australia.gov\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.australia.gov.au/\",\n      \"companyId\": \"australian_government\"\n    },\n    \"auth0\": {\n      \"name\": \"Auth0 Inc.\",\n      \"categoryId\": 6,\n      \"url\": \"https://auth0.com/\",\n      \"companyId\": \"auth0\"\n    },\n    \"autoid\": {\n      \"name\": \"AutoID\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.autoid.com/\",\n      \"companyId\": \"autoid\"\n    },\n    \"autonomy\": {\n      \"name\": \"Autonomy\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.optimost.com/\",\n      \"companyId\": \"hp\"\n    },\n    \"autonomy_campaign\": {\n      \"name\": \"Autonomy Campaign\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.autonomy.com/\",\n      \"companyId\": \"hp\"\n    },\n    \"autopilothq\": {\n      \"name\": \"Auto Pilot\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.autopilothq.com/\",\n      \"companyId\": \"autopilothq\"\n    },\n    \"autoscout24.com\": {\n      \"name\": \"Autoscout24\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.scout24.com/\",\n      \"companyId\": \"scout24\"\n    },\n    \"avail\": {\n      \"name\": \"Avail\",\n      \"categoryId\": 4,\n      \"url\": \"http://avail.com\",\n      \"companyId\": \"richrelevance\"\n    },\n    \"avanser\": {\n      \"name\": \"AVANSER\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.avanser.com.au/\",\n      \"companyId\": \"avanser\"\n    },\n    \"avant_metrics\": {\n      \"name\": \"Avant Metrics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.avantlink.com/\",\n      \"companyId\": \"avantlink\"\n    },\n    \"avantlink\": {\n      \"name\": \"AvantLink\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.avantlink.com/\",\n      \"companyId\": \"avantlink\"\n    },\n    \"avazu_network\": {\n      \"name\": \"Avazu Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.avazudsp.net/\",\n      \"companyId\": \"avazu_network\"\n    },\n    \"avenseo\": {\n      \"name\": \"Avenseo\",\n      \"categoryId\": 4,\n      \"url\": \"http://avenseo.com\",\n      \"companyId\": \"avenseo\"\n    },\n    \"avid_media\": {\n      \"name\": \"Avid Media\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.avidglobalmedia.com/\",\n      \"companyId\": \"avid_media\"\n    },\n    \"avocet\": {\n      \"name\": \"Avocet\",\n      \"categoryId\": 8,\n      \"url\": \"https://avocet.io/\",\n      \"companyId\": \"avocet\"\n    },\n    \"aweber\": {\n      \"name\": \"AWeber\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.aweber.com/\",\n      \"companyId\": \"aweber_communications\"\n    },\n    \"awin\": {\n      \"name\": \"AWIN\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.awin.com\",\n      \"companyId\": \"axel_springer\"\n    },\n    \"axill\": {\n      \"name\": \"Axill\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.axill.com/\",\n      \"companyId\": \"axill\"\n    },\n    \"azadify\": {\n      \"name\": \"Azadify\",\n      \"categoryId\": 4,\n      \"url\": \"http://azadify.com/engage/index.php\",\n      \"companyId\": \"azadify\"\n    },\n    \"azure\": {\n      \"name\": \"Microsoft Azure\",\n      \"categoryId\": 10,\n      \"url\": \"https://azure.microsoft.com/\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"azure_blob_storage\": {\n      \"name\": \"Azure Blob Storage\",\n      \"categoryId\": 8,\n      \"url\": \"https://azure.microsoft.com/en-us/products/storage/blobs\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"azureedge.net\": {\n      \"name\": \"Azure CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"b2bcontext\": {\n      \"name\": \"B2BContext\",\n      \"categoryId\": 4,\n      \"url\": \"http://b2bcontext.ru/\",\n      \"companyId\": \"b2bcontext\"\n    },\n    \"b2bvideo\": {\n      \"name\": \"B2Bvideo\",\n      \"categoryId\": 4,\n      \"url\": \"http://b2bvideo.ru/\",\n      \"companyId\": \"b2bvideo\"\n    },\n    \"babator.com\": {\n      \"name\": \"Babator\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.babator.com/\",\n      \"companyId\": null\n    },\n    \"back_beat_media\": {\n      \"name\": \"Back Beat Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.backbeatmedia.com\",\n      \"companyId\": \"backbeat_media\"\n    },\n    \"backtype_widgets\": {\n      \"name\": \"BackType Widgets\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.backtype.com/widgets\",\n      \"companyId\": \"backtype\"\n    },\n    \"bahn_de\": {\n      \"name\": \"Deutsche Bahn\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"baidu_ads\": {\n      \"name\": \"Baidu Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.baidu.com/\",\n      \"companyId\": \"baidu\"\n    },\n    \"baidu_static\": {\n      \"name\": \"Baidu Static\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.baidu.com/\",\n      \"companyId\": \"baidu\"\n    },\n    \"baletingo.com\": {\n      \"name\": \"baletingo.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bangdom.com\": {\n      \"name\": \"BangBros\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bankrate\": {\n      \"name\": \"Bankrate\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.bankrate.com/\",\n      \"companyId\": \"bankrate\"\n    },\n    \"banner_connect\": {\n      \"name\": \"Banner Connect\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bannerconnect.net/\",\n      \"companyId\": \"bannerconnect\"\n    },\n    \"bannerflow.com\": {\n      \"name\": \"Bannerflow\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.bannerflow.com/\",\n      \"companyId\": \"bannerflow\"\n    },\n    \"bannerplay\": {\n      \"name\": \"BannerPlay\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bannerplay.com/\",\n      \"companyId\": \"bannerplay\"\n    },\n    \"bannersnack\": {\n      \"name\": \"Bannersnack\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bannersnack.com/\",\n      \"companyId\": \"bannersnack\"\n    },\n    \"barilliance\": {\n      \"name\": \"Barilliance\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.barilliance.com/\",\n      \"companyId\": \"barilliance\"\n    },\n    \"barometer\": {\n      \"name\": \"Barometer\",\n      \"categoryId\": 2,\n      \"url\": \"http://getbarometer.com/\",\n      \"companyId\": \"barometer\"\n    },\n    \"basilic.io\": {\n      \"name\": \"basilic.io\",\n      \"categoryId\": 6,\n      \"url\": \"https://basilic.io/\",\n      \"companyId\": null\n    },\n    \"batanga_network\": {\n      \"name\": \"Batanga Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.batanganetwork.com/\",\n      \"companyId\": \"batanga_network\"\n    },\n    \"batch_media\": {\n      \"name\": \"Batch Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://batch.ba/\",\n      \"companyId\": \"prosieben_sat1\"\n    },\n    \"bauer_media\": {\n      \"name\": \"Bauer Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bauermedia.com\",\n      \"companyId\": \"bauer_media\"\n    },\n    \"baur.de\": {\n      \"name\": \"baur.de\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"baynote_observer\": {\n      \"name\": \"Baynote Observer\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.baynote.com/\",\n      \"companyId\": \"baynote\"\n    },\n    \"bazaarvoice\": {\n      \"name\": \"Bazaarvoice\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.bazaarvoice.com/\",\n      \"companyId\": \"bazaarvoice\"\n    },\n    \"bbci\": {\n      \"name\": \"BBC\",\n      \"categoryId\": 10,\n      \"url\": \"https://bbc.co.uk\",\n      \"companyId\": null\n    },\n    \"bd4travel\": {\n      \"name\": \"bd4travel\",\n      \"categoryId\": 4,\n      \"url\": \"https://bd4travel.com/\",\n      \"companyId\": \"bd4travel\"\n    },\n    \"be_opinion\": {\n      \"name\": \"Be Opinion\",\n      \"categoryId\": 2,\n      \"url\": \"http://beopinion.com/\",\n      \"companyId\": \"be_opinion\"\n    },\n    \"beachfront\": {\n      \"name\": \"Beachfront Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://beachfrontmedia.com/\",\n      \"companyId\": null\n    },\n    \"beacon_ad_network\": {\n      \"name\": \"Beacon Ad Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://beaconads.com/\",\n      \"companyId\": \"beacon_ad_network\"\n    },\n    \"beampulse.com\": {\n      \"name\": \"BeamPulse\",\n      \"categoryId\": 4,\n      \"url\": \"https://en.beampulse.com/\",\n      \"companyId\": null\n    },\n    \"beanstalk_data\": {\n      \"name\": \"Beanstalk Data\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.beanstalkdata.com/\",\n      \"companyId\": \"beanstalk_data\"\n    },\n    \"bebi\": {\n      \"name\": \"Bebi Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.bebi.com/\",\n      \"companyId\": \"bebi_media\"\n    },\n    \"beeketing.com\": {\n      \"name\": \"Beeketing\",\n      \"categoryId\": 4,\n      \"url\": \"https://beeketing.com/\",\n      \"companyId\": \"beeketing\"\n    },\n    \"beeline.ru\": {\n      \"name\": \"Beeline\",\n      \"categoryId\": 4,\n      \"url\": \"https://moskva.beeline.ru/\",\n      \"companyId\": null\n    },\n    \"beeswax\": {\n      \"name\": \"Beeswax\",\n      \"categoryId\": 4,\n      \"url\": \"http://beeswax.com/\",\n      \"companyId\": \"beeswax\"\n    },\n    \"beezup\": {\n      \"name\": \"BeezUP\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.beezup.co.uk/\",\n      \"companyId\": \"beezup\"\n    },\n    \"begun\": {\n      \"name\": \"Begun\",\n      \"categoryId\": 4,\n      \"url\": \"http://begun.ru/\",\n      \"companyId\": \"begun\"\n    },\n    \"behavioralengine\": {\n      \"name\": \"BehavioralEngine\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.behavioralengine.com/\",\n      \"companyId\": \"behavioralengine\"\n    },\n    \"belboon_gmbh\": {\n      \"name\": \"belboon GmbH\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"belco\": {\n      \"name\": \"Belco\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.belco.io/\",\n      \"companyId\": \"belco\"\n    },\n    \"belstat\": {\n      \"name\": \"BelStat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.belstat.com/\",\n      \"companyId\": \"belstat\"\n    },\n    \"bemobile.ua\": {\n      \"name\": \"Bemobile\",\n      \"categoryId\": 10,\n      \"url\": \"http://bemobile.ua/en/\",\n      \"companyId\": \"bemobile\"\n    },\n    \"bench_platform\": {\n      \"name\": \"Bench Platform\",\n      \"categoryId\": 4,\n      \"url\": \"https://benchplatform.com\",\n      \"companyId\": \"bench_platform\"\n    },\n    \"betterttv\": {\n      \"name\": \"BetterTTV\",\n      \"categoryId\": 7,\n      \"url\": \"https://nightdev.com/betterttv/\",\n      \"companyId\": \"nightdev\"\n    },\n    \"betweendigital.com\": {\n      \"name\": \"Between Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://betweendigital.ru/ssp\",\n      \"companyId\": \"between_digital\"\n    },\n    \"bid.run\": {\n      \"name\": \"Bid Run\",\n      \"categoryId\": 4,\n      \"url\": \"http://bid.run/\",\n      \"companyId\": \"bid.run\"\n    },\n    \"bidgear\": {\n      \"name\": \"BidGear\",\n      \"categoryId\": 6,\n      \"url\": \"https://bidgear.com/\",\n      \"companyId\": \"bidgear\"\n    },\n    \"bidswitch\": {\n      \"name\": \"Bidswitch\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.iponweb.com/\",\n      \"companyId\": \"iponweb\"\n    },\n    \"bidtellect\": {\n      \"name\": \"Bidtellect\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.bidtellect.com/\",\n      \"companyId\": \"bidtellect\"\n    },\n    \"bidtheatre\": {\n      \"name\": \"BidTheatre\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bidtheatre.com/\",\n      \"companyId\": \"bidtheatre\"\n    },\n    \"bidvertiser\": {\n      \"name\": \"BidVertiser\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bidvertiser.com/\",\n      \"companyId\": \"bidvertiser\"\n    },\n    \"big_mobile\": {\n      \"name\": \"Big Mobile\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bigmobile.com/\",\n      \"companyId\": \"big_mobile\"\n    },\n    \"bigcommerce.com\": {\n      \"name\": \"BigCommerce\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.bigcommerce.com/\",\n      \"companyId\": \"bigcommerce\"\n    },\n    \"bigmir.net\": {\n      \"name\": \"bigmir\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.bigmir.net/\",\n      \"companyId\": \"bigmir-internet\"\n    },\n    \"bigpoint\": {\n      \"name\": \"Bigpoint\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bild\": {\n      \"name\": \"Bild.de\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bilgin_pro\": {\n      \"name\": \"Bilgin Pro\",\n      \"categoryId\": 4,\n      \"url\": \"http://bilgin.pro/\",\n      \"companyId\": \"bilginpro\"\n    },\n    \"bilin\": {\n      \"name\": \"Bilin\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bilintechnology.com/\",\n      \"companyId\": \"bilin\"\n    },\n    \"bing_ads\": {\n      \"name\": \"Bing Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://bingads.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"bing_maps\": {\n      \"name\": \"Bing Maps\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"binge\": {\n      \"name\": \"Binge\",\n      \"categoryId\": 0,\n      \"url\": \"https://binge.com.au/\",\n      \"companyId\": \"news_corp\",\n      \"source\": \"AdGuard\"\n    },\n    \"binlayer\": {\n      \"name\": \"BinLayer\",\n      \"categoryId\": 4,\n      \"url\": \"http://binlayer.com/\",\n      \"companyId\": \"binlayer\"\n    },\n    \"binotel\": {\n      \"name\": \"Binotel\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.binotel.ua/\",\n      \"companyId\": \"binotel\"\n    },\n    \"bisnode\": {\n      \"name\": \"Bisnode\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.esendra.fi/\",\n      \"companyId\": \"bisnode\"\n    },\n    \"bitcoin_miner\": {\n      \"name\": \"Bitcoin Miner\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.bitcoinplus.com/\",\n      \"companyId\": \"bitcoin_plus\"\n    },\n    \"bitly\": {\n      \"name\": \"Bitly\",\n      \"categoryId\": 6,\n      \"url\": \"https://bitly.com/\",\n      \"companyId\": null\n    },\n    \"bitrix\": {\n      \"name\": \"Bitrix24\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.bitrix24.com/\",\n      \"companyId\": \"bitrix24\"\n    },\n    \"bitwarden\": {\n      \"name\": \"Bitwarden\",\n      \"categoryId\": 8,\n      \"url\": \"https://bitwarden.com/\",\n      \"companyId\": \"bitwarden\",\n      \"source\": \"AdGuard\"\n    },\n    \"bizcn\": {\n      \"name\": \"Bizcn\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bizcn.com/\",\n      \"companyId\": \"bizcn\"\n    },\n    \"blackdragon\": {\n      \"name\": \"BlackDragon\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.jd.com/\",\n      \"companyId\": \"jing_dong\"\n    },\n    \"blau.de\": {\n      \"name\": \"Blau\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.blau.de/\",\n      \"companyId\": null\n    },\n    \"blink_new_media\": {\n      \"name\": \"Blink New Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://engagebdr.com/\",\n      \"companyId\": \"engage_bdr\"\n    },\n    \"blis\": {\n      \"name\": \"Blis\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.blis.com/index.php\",\n      \"companyId\": \"blis\"\n    },\n    \"blogad\": {\n      \"name\": \"BlogAD\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.blogad.com.tw/\",\n      \"companyId\": \"blogad\"\n    },\n    \"blogbang\": {\n      \"name\": \"BlogBang\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.blogbang.com/\",\n      \"companyId\": \"blogbang\"\n    },\n    \"blogcatalog\": {\n      \"name\": \"BlogCatalog\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.blogcatalog.com/\",\n      \"companyId\": \"blogcatalog\"\n    },\n    \"blogcounter\": {\n      \"name\": \"BlogCounter\",\n      \"categoryId\": 6,\n      \"url\": \"http://blogcounter.com/\",\n      \"companyId\": \"adfire_gmbh\"\n    },\n    \"blogfoster.com\": {\n      \"name\": \"Blogfoster\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.blogfoster.com/\",\n      \"companyId\": \"blogfoster\"\n    },\n    \"bloggerads\": {\n      \"name\": \"BloggerAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bloggerads.net/\",\n      \"companyId\": \"bloggerads\"\n    },\n    \"blogher\": {\n      \"name\": \"BlogHer Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.blogher.com/\",\n      \"companyId\": \"penske_media_corp\"\n    },\n    \"blogimg.jp\": {\n      \"name\": \"blogimg.jp\",\n      \"categoryId\": 9,\n      \"url\": \"https://line.me/\",\n      \"companyId\": \"line\"\n    },\n    \"blogsmithmedia.com\": {\n      \"name\": \"blogsmithmedia.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.verizon.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"blogspot_com\": {\n      \"name\": \"blogspot.com\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"bloomreach\": {\n      \"name\": \"BloomReach\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.bloomreach.com/en\",\n      \"companyId\": \"bloomreach\"\n    },\n    \"blue_cherry_group\": {\n      \"name\": \"Blue Cherry Group\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bluecherrygroup.com\",\n      \"companyId\": \"blue_cherry_group\"\n    },\n    \"blue_seed\": {\n      \"name\": \"Blue Seed\",\n      \"categoryId\": 4,\n      \"url\": \"http://blueseed.tv/#/en/platform\",\n      \"companyId\": \"blue_seed\"\n    },\n    \"blueconic.net\": {\n      \"name\": \"BlueConic Plugin\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.blueconic.com/\",\n      \"companyId\": \"blueconic\"\n    },\n    \"bluecore\": {\n      \"name\": \"Bluecore\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.bluecore.com/\",\n      \"companyId\": \"triggermail\"\n    },\n    \"bluekai\": {\n      \"name\": \"BlueKai\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bluekai.com/\",\n      \"companyId\": \"oracle\"\n    },\n    \"bluelithium\": {\n      \"name\": \"Bluelithium\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bluelithium.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"bluemetrix\": {\n      \"name\": \"Bluemetrix\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bluemetrix.ie/\",\n      \"companyId\": \"bluemetrix\"\n    },\n    \"bluenewsupdate.info\": {\n      \"name\": \"bluenewsupdate.info\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bluestreak\": {\n      \"name\": \"BlueStreak\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bluestreak.com/\",\n      \"companyId\": \"dentsu_aegis_network\"\n    },\n    \"bluetriangle\": {\n      \"name\": \"Blue Triangle\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.bluetriangle.com/\",\n      \"companyId\": \"blue_triangle\"\n    },\n    \"bodelen.com\": {\n      \"name\": \"bodelen.com\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bol_affiliate_program\": {\n      \"name\": \"BOL Affiliate Program\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bol.com\",\n      \"companyId\": \"bol.com\"\n    },\n    \"bold\": {\n      \"name\": \"Bold\",\n      \"categoryId\": 4,\n      \"url\": \"https://boldcommerce.com/\",\n      \"companyId\": \"bold\"\n    },\n    \"boldchat\": {\n      \"name\": \"Boldchat\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.boldchat.com/\",\n      \"companyId\": \"boldchat\"\n    },\n    \"boltdns.net\": {\n      \"name\": \"boltdns.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bom\": {\n      \"name\": \"Bureau of Meteorology\",\n      \"categoryId\": 9,\n      \"url\": \"http://bom.gov.au/\",\n      \"companyId\": \"australian_government\",\n      \"source\": \"AdGuard\"\n    },\n    \"bombora\": {\n      \"name\": \"Bombora\",\n      \"categoryId\": 6,\n      \"url\": \"http://bombora.com/\",\n      \"companyId\": \"bombora\"\n    },\n    \"bongacams.com\": {\n      \"name\": \"bongacams.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bonial\": {\n      \"name\": \"Bonial Connect\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.bonial.com/\",\n      \"companyId\": null\n    },\n    \"boo-box\": {\n      \"name\": \"boo-box\",\n      \"categoryId\": 4,\n      \"url\": \"http://boo-box.com/\",\n      \"companyId\": \"boo-box\"\n    },\n    \"booking.com\": {\n      \"name\": \"Booking.com\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"boost_box\": {\n      \"name\": \"Boost Box\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.boostbox.com.br/\",\n      \"companyId\": \"boost_box\"\n    },\n    \"booster_video\": {\n      \"name\": \"Booster Video\",\n      \"categoryId\": 0,\n      \"url\": \"https://boostervideo.ru/\",\n      \"companyId\": \"booster_video\"\n    },\n    \"bootstrap\": {\n      \"name\": \"Bootstrap CDN\",\n      \"categoryId\": 9,\n      \"url\": \"http://getbootstrap.com/\",\n      \"companyId\": \"bootstrap_cdn\"\n    },\n    \"borrango.com\": {\n      \"name\": \"borrango.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"botscanner\": {\n      \"name\": \"BotScanner\",\n      \"categoryId\": 6,\n      \"url\": \"http://botscanner.com\",\n      \"companyId\": \"botscanner\"\n    },\n    \"boudja.com\": {\n      \"name\": \"boudja.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bounce_exchange\": {\n      \"name\": \"Bounce Exchange\",\n      \"categoryId\": 4,\n      \"url\": \"http://bounceexchange.com\",\n      \"companyId\": \"bounce_exchange\"\n    },\n    \"bouncex\": {\n      \"name\": \"BounceX\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.bouncex.com/\",\n      \"companyId\": null\n    },\n    \"box_uk\": {\n      \"name\": \"Box UK\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.clickdensity.com\",\n      \"companyId\": \"box_uk\"\n    },\n    \"boxever\": {\n      \"name\": \"Boxever\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.boxever.com/\",\n      \"companyId\": \"boxever\"\n    },\n    \"brainient\": {\n      \"name\": \"Brainient\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.brainient.com/\",\n      \"companyId\": \"brainient\"\n    },\n    \"brainsins\": {\n      \"name\": \"BrainSINS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.brainsins.com/\",\n      \"companyId\": \"brainsins\"\n    },\n    \"branch\": {\n      \"name\": \"Branch.io\",\n      \"categoryId\": 101,\n      \"url\": \"https://branch.io/\",\n      \"companyId\": \"branch_metrics_inc\",\n      \"source\": \"AdGuard\"\n    },\n    \"branch_metrics\": {\n      \"name\": \"Branch\",\n      \"categoryId\": 4,\n      \"url\": \"https://branch.io/\",\n      \"companyId\": \"branch_metrics_inc\"\n    },\n    \"brand_affinity\": {\n      \"name\": \"Brand Affinity\",\n      \"categoryId\": 4,\n      \"url\": \"http://brandaffinity.net/about\",\n      \"companyId\": \"yoonla\"\n    },\n    \"brand_networks\": {\n      \"name\": \"Brand Networks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.xa.net/\",\n      \"companyId\": \"brand_networks\"\n    },\n    \"brandmetrics.com\": {\n      \"name\": \"Brandmetrics.com\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.brandmetrics.com/\",\n      \"companyId\": null\n    },\n    \"brandreach\": {\n      \"name\": \"BrandReach\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.brandreach.com/\",\n      \"companyId\": \"brandreach\"\n    },\n    \"brandscreen\": {\n      \"name\": \"Brandscreen\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.brandscreen.com/\",\n      \"companyId\": \"zenovia\"\n    },\n    \"brandwire.tv\": {\n      \"name\": \"BrandWire\",\n      \"categoryId\": 4,\n      \"url\": \"https://brandwire.tv/\",\n      \"companyId\": null\n    },\n    \"branica\": {\n      \"name\": \"Branica\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.branica.com/\",\n      \"companyId\": \"branica\"\n    },\n    \"braze\": {\n      \"name\": \"Braze, Inc.\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.braze.com/\",\n      \"companyId\": \"braze\",\n      \"source\": \"AdGuard\"\n    },\n    \"brealtime\": {\n      \"name\": \"EMX Digital\",\n      \"categoryId\": 4,\n      \"url\": \"https://emxdigital.com/\",\n      \"companyId\": null\n    },\n    \"bridgetrack\": {\n      \"name\": \"BridgeTrack\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bridgetrack.com/\",\n      \"companyId\": \"bridgetrack\"\n    },\n    \"brightcove\": {\n      \"name\": \"Brightcove\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.brightcove.com/en/\",\n      \"companyId\": \"brightcove\"\n    },\n    \"brightcove_player\": {\n      \"name\": \"Brightcove Player\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.brightcove.com/en/\",\n      \"companyId\": \"brightcove\"\n    },\n    \"brightedge\": {\n      \"name\": \"BrightEdge\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.brightedge.com/\",\n      \"companyId\": \"brightedge\"\n    },\n    \"brightfunnel\": {\n      \"name\": \"BrightFunnel\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.brightfunnel.com/\",\n      \"companyId\": \"brightfunnel\"\n    },\n    \"brightonclick.com\": {\n      \"name\": \"brightonclick.com\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"brightroll\": {\n      \"name\": \"BrightRoll\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.brightroll.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"brilig\": {\n      \"name\": \"Brilig\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.brilig.com/\",\n      \"companyId\": \"dentsu_aegis_network\"\n    },\n    \"brillen.de\": {\n      \"name\": \"brillen.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.brillen.de/\",\n      \"companyId\": null\n    },\n    \"broadstreet\": {\n      \"name\": \"Broadstreet\",\n      \"categoryId\": 4,\n      \"url\": \"http://broadstreetads.com/\",\n      \"companyId\": \"broadstreet\"\n    },\n    \"bronto\": {\n      \"name\": \"Bronto\",\n      \"categoryId\": 4,\n      \"url\": \"http://bronto.com/\",\n      \"companyId\": \"bronto\"\n    },\n    \"brow.si\": {\n      \"name\": \"Brow.si\",\n      \"categoryId\": 4,\n      \"url\": \"https://brow.si/\",\n      \"companyId\": \"brow.si\"\n    },\n    \"browser-statistik\": {\n      \"name\": \"Browser-Statistik\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.browser-statistik.de/\",\n      \"companyId\": \"browser-statistik\"\n    },\n    \"browser_update\": {\n      \"name\": \"Browser Update\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.browser-update.org/\",\n      \"companyId\": \"browser-update\"\n    },\n    \"btncdn.com\": {\n      \"name\": \"btncdn.com\",\n      \"categoryId\": 9,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bubblestat\": {\n      \"name\": \"Bubblestat\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bubblestat.com/\",\n      \"companyId\": \"bubblestat\"\n    },\n    \"buddy_media\": {\n      \"name\": \"Buddy Media\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.salesforce.com/\",\n      \"companyId\": \"salesforce\"\n    },\n    \"buffer_button\": {\n      \"name\": \"Buffer Button\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.bufferapp.com/\",\n      \"companyId\": \"buffer\"\n    },\n    \"bugherd.com\": {\n      \"name\": \"BugHerd\",\n      \"categoryId\": 2,\n      \"url\": \"https://bugherd.com\",\n      \"companyId\": \"bugherd\"\n    },\n    \"bugsnag\": {\n      \"name\": \"Bugsnag\",\n      \"categoryId\": 6,\n      \"url\": \"https://bugsnag.com\",\n      \"companyId\": \"bugsnag\"\n    },\n    \"bulkhentai.com\": {\n      \"name\": \"bulkhentai.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bumlam.com\": {\n      \"name\": \"bumlam.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"bunchbox\": {\n      \"name\": \"Bunchbox\",\n      \"categoryId\": 6,\n      \"url\": \"https://app.bunchbox.co/login\",\n      \"companyId\": \"bunchbox\"\n    },\n    \"burda\": {\n      \"name\": \"BurdaForward\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hubert-burda-media.com/\",\n      \"companyId\": \"hubert_burda_media\"\n    },\n    \"burda_digital_systems\": {\n      \"name\": \"Burda Digital Systems\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hubert-burda-media.com/\",\n      \"companyId\": \"hubert_burda_media\"\n    },\n    \"burst_media\": {\n      \"name\": \"Burst Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.burstmedia.com/\",\n      \"companyId\": \"rhythmone\"\n    },\n    \"burt\": {\n      \"name\": \"Burt\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.burtcorp.com/\",\n      \"companyId\": \"burt\"\n    },\n    \"businessonline_analytics\": {\n      \"name\": \"BusinessOnLine Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.businessol.com/\",\n      \"companyId\": \"businessonline\"\n    },\n    \"button\": {\n      \"name\": \"Button\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.usebutton.com/\",\n      \"companyId\": \"button\",\n      \"source\": \"AdGuard\"\n    },\n    \"buysellads\": {\n      \"name\": \"BuySellAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://buysellads.com/\",\n      \"companyId\": \"buysellads.com\"\n    },\n    \"buzzadexchange.com\": {\n      \"name\": \"buzzadexchange.com\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"buzzador\": {\n      \"name\": \"Buzzador\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.buzzador.com\",\n      \"companyId\": \"buzzador\"\n    },\n    \"buzzfeed\": {\n      \"name\": \"BuzzFeed\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.buzzfeed.com\",\n      \"companyId\": \"buzzfeed\"\n    },\n    \"bwbx.io\": {\n      \"name\": \"Bloomberg CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.bloomberg.com/\",\n      \"companyId\": null\n    },\n    \"bypass\": {\n      \"name\": \"Bypass\",\n      \"categoryId\": 4,\n      \"url\": \"http://bypass.jp/\",\n      \"companyId\": \"united_inc\"\n    },\n    \"c1_exchange\": {\n      \"name\": \"C1 Exchange\",\n      \"categoryId\": 4,\n      \"url\": \"http://c1exchange.com/\",\n      \"companyId\": \"c1_exchange\"\n    },\n    \"c3_metrics\": {\n      \"name\": \"C3 Metrics\",\n      \"categoryId\": 6,\n      \"url\": \"http://c3metrics.com/\",\n      \"companyId\": \"c3_metrics\"\n    },\n    \"c8_network\": {\n      \"name\": \"C8 Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://c8.net.ua/\",\n      \"companyId\": \"c8_network\"\n    },\n    \"cackle.me\": {\n      \"name\": \"Cackle\",\n      \"categoryId\": 3,\n      \"url\": \"https://cackle.me/\",\n      \"companyId\": null\n    },\n    \"cadreon\": {\n      \"name\": \"Cadreon\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cadreon.com/\",\n      \"companyId\": \"cadreon\"\n    },\n    \"call_page\": {\n      \"name\": \"Call Page\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.callpage.io/\",\n      \"companyId\": \"call_page\"\n    },\n    \"callbackhunter\": {\n      \"name\": \"CallbackHunter\",\n      \"categoryId\": 2,\n      \"url\": \"http://callbackhunter.com/main\",\n      \"companyId\": \"callbackhunter\"\n    },\n    \"callbox\": {\n      \"name\": \"CallBox\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.centuryinteractive.com\",\n      \"companyId\": \"callbox\"\n    },\n    \"callibri\": {\n      \"name\": \"Callibri\",\n      \"categoryId\": 4,\n      \"url\": \"https://callibri.ru/\",\n      \"companyId\": \"callibri\"\n    },\n    \"callrail\": {\n      \"name\": \"CallRail\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.callrail.com/\",\n      \"companyId\": \"callrail\"\n    },\n    \"calltracking\": {\n      \"name\": \"Calltracking\",\n      \"categoryId\": 2,\n      \"url\": \"https://calltracking.ru\",\n      \"companyId\": \"calltracking\"\n    },\n    \"caltat.com\": {\n      \"name\": \"Caltat\",\n      \"categoryId\": 2,\n      \"url\": \"https://caltat.com/\",\n      \"companyId\": null\n    },\n    \"cam-content.com\": {\n      \"name\": \"Cam-Content.com\",\n      \"categoryId\": 3,\n      \"url\": \"https://www.cam-content.com/\",\n      \"companyId\": null\n    },\n    \"camakaroda.com\": {\n      \"name\": \"camakaroda.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"campus_explorer\": {\n      \"name\": \"Campus Explorer\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.campusexplorer.com/\",\n      \"companyId\": \"campus_explorer\"\n    },\n    \"canddi\": {\n      \"name\": \"CANDDI\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.canddi.com/\",\n      \"companyId\": \"canddi\"\n    },\n    \"canonical\": {\n      \"name\": \"Canonical\",\n      \"categoryId\": 8,\n      \"url\": \"https://canonical.com/\",\n      \"companyId\": \"canonical\",\n      \"source\": \"AdGuard\"\n    },\n    \"canvas\": {\n      \"name\": \"Canvas\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.canvas.net/\",\n      \"companyId\": null\n    },\n    \"capitaldata\": {\n      \"name\": \"CapitalData\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.capitaldata.fr/\",\n      \"companyId\": \"highco\"\n    },\n    \"captora\": {\n      \"name\": \"Captora\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.captora.com/\",\n      \"companyId\": \"captora\"\n    },\n    \"capture_media\": {\n      \"name\": \"Capture Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://capturemedia.ch/\",\n      \"companyId\": \"capture_media\"\n    },\n    \"capturly\": {\n      \"name\": \"Capturly\",\n      \"categoryId\": 6,\n      \"url\": \"http://capturly.com/\",\n      \"companyId\": \"capturly\"\n    },\n    \"carambola\": {\n      \"name\": \"Carambola\",\n      \"categoryId\": 4,\n      \"url\": \"http://carambo.la/\",\n      \"companyId\": \"carambola\"\n    },\n    \"carbonads\": {\n      \"name\": \"Carbon Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.carbonads.net/\",\n      \"companyId\": \"buysellads.com\"\n    },\n    \"cardinal\": {\n      \"name\": \"Cardinal\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.cardinalcommerce.com/\",\n      \"companyId\": \"visa\"\n    },\n    \"cardlytics\": {\n      \"name\": \"Cardlytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.cardlytics.com/\",\n      \"companyId\": null\n    },\n    \"carrot_quest\": {\n      \"name\": \"Carrot Quest\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.carrotquest.io/\",\n      \"companyId\": \"carrot_quest\"\n    },\n    \"cartstack\": {\n      \"name\": \"CartStack\",\n      \"categoryId\": 2,\n      \"url\": \"http://cartstack.com/\",\n      \"companyId\": \"cartstack\"\n    },\n    \"caspion\": {\n      \"name\": \"Caspion\",\n      \"categoryId\": 6,\n      \"url\": \"http://caspion.com/\",\n      \"companyId\": \"caspion\"\n    },\n    \"castle\": {\n      \"name\": \"Castle\",\n      \"categoryId\": 2,\n      \"url\": \"https://castle.io\",\n      \"companyId\": \"castle\"\n    },\n    \"catchpoint\": {\n      \"name\": \"Catchpoint\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.catchpoint.com/\",\n      \"companyId\": \"catchpoint_systems\"\n    },\n    \"cbox\": {\n      \"name\": \"Cbox\",\n      \"categoryId\": 2,\n      \"url\": \"http://cbox.ws\",\n      \"companyId\": \"cbox\"\n    },\n    \"cbs_interactive\": {\n      \"name\": \"CBS Interactive\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.cbsinteractive.com/\",\n      \"companyId\": \"cbs_interactive\"\n    },\n    \"ccm_benchmark\": {\n      \"name\": \"CCM Benchmark\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ccmbenchmark.com/\",\n      \"companyId\": null\n    },\n    \"cdk_digital_marketing\": {\n      \"name\": \"CDK Digital Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cobaltgroup.com\",\n      \"companyId\": \"cdk_digital_marketing\"\n    },\n    \"cdn-net.com\": {\n      \"name\": \"cdn-net.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"cdn13.com\": {\n      \"name\": \"cdn13.com\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"cdn77\": {\n      \"name\": \"CDN77\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.cdn77.com/\",\n      \"companyId\": null\n    },\n    \"cdnetworks.net\": {\n      \"name\": \"cdnetworks.net\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.cdnetworks.com/\",\n      \"companyId\": null\n    },\n    \"cdnnetwok_xyz\": {\n      \"name\": \"cdnnetwok.xyz\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"cdnondemand.org\": {\n      \"name\": \"cdnondemand.org\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"cdnsure.com\": {\n      \"name\": \"cdnsure.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"cdnvideo.com\": {\n      \"name\": \"CDNvideo\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.cdnvideo.com/\",\n      \"companyId\": \"cdnvideo\"\n    },\n    \"cdnwidget.com\": {\n      \"name\": \"cdnwidget.com\",\n      \"categoryId\": 9,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"cedexis_radar\": {\n      \"name\": \"Cedexis Radar\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.cedexis.com/products_radar.html\",\n      \"companyId\": \"cedexis\"\n    },\n    \"celebrus\": {\n      \"name\": \"Celebrus\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.celebrus.com/\",\n      \"companyId\": \"celebrus\"\n    },\n    \"celtra\": {\n      \"name\": \"Celtra\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.celtra.com/\",\n      \"companyId\": \"celtra\"\n    },\n    \"cendyn\": {\n      \"name\": \"Cendyn\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cendyn.com/\",\n      \"companyId\": \"cendyn\"\n    },\n    \"centraltag\": {\n      \"name\": \"CentralTag\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.centraltag.com/\",\n      \"companyId\": \"centraltag\"\n    },\n    \"centro\": {\n      \"name\": \"Centro\",\n      \"categoryId\": 4,\n      \"url\": \"http://centro.net/\",\n      \"companyId\": \"centro\"\n    },\n    \"cerberus_speed-trap\": {\n      \"name\": \"Cerberus Speed-Trap\",\n      \"categoryId\": 6,\n      \"url\": \"http://cerberusip.com/\",\n      \"companyId\": \"cerberus\"\n    },\n    \"certainsource\": {\n      \"name\": \"CertainSource\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ewaydirect.com\",\n      \"companyId\": \"certainsource\"\n    },\n    \"certifica_metric\": {\n      \"name\": \"Certifica Metric\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.comscore.com/Products_Services/Product_Index/Certifica_Metric\",\n      \"companyId\": \"comscore\"\n    },\n    \"certona\": {\n      \"name\": \"Certona\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.certona.com/products/recommendation.php\",\n      \"companyId\": \"certona\"\n    },\n    \"chameleon\": {\n      \"name\": \"Chameleon\",\n      \"categoryId\": 4,\n      \"url\": \"http://chameleon.ad/\",\n      \"companyId\": \"chamaleon\"\n    },\n    \"chango\": {\n      \"name\": \"Chango\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.chango.com/\",\n      \"companyId\": \"rubicon_project\"\n    },\n    \"channel_intelligence\": {\n      \"name\": \"Channel Intelligence\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.channelintelligence.com/\",\n      \"companyId\": \"google\"\n    },\n    \"channel_pilot_solutions\": {\n      \"name\": \"ChannelPilot Solutions\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.channelpilot.de/\",\n      \"companyId\": null\n    },\n    \"channeladvisor\": {\n      \"name\": \"ChannelAdvisor\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.channeladvisor.com/\",\n      \"companyId\": \"channeladvisor\"\n    },\n    \"channelfinder\": {\n      \"name\": \"ChannelFinder\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kpicentral.com/\",\n      \"companyId\": \"kaleidoscope_promotions\"\n    },\n    \"chaordic\": {\n      \"name\": \"Chaordic\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.chaordic.com.br/\",\n      \"companyId\": \"chaordic\"\n    },\n    \"chartbeat\": {\n      \"name\": \"ChartBeat\",\n      \"categoryId\": 6,\n      \"url\": \"http://chartbeat.com/\",\n      \"companyId\": \"chartbeat\"\n    },\n    \"chartboost\": {\n      \"name\": \"Chartboost\",\n      \"categoryId\": 4,\n      \"url\": \"http://chartboost.com/\",\n      \"companyId\": \"take-two\",\n      \"source\": \"AdGuard\"\n    },\n    \"chaser\": {\n      \"name\": \"Chaser\",\n      \"categoryId\": 2,\n      \"url\": \"http://chaser.ru/\",\n      \"companyId\": \"chaser\"\n    },\n    \"chat_beacon\": {\n      \"name\": \"Chat Beacon\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.chatbeacon.io/\",\n      \"companyId\": \"chat_beacon\"\n    },\n    \"chatango\": {\n      \"name\": \"Chatango\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.chatango.com/\",\n      \"companyId\": \"chatango\"\n    },\n    \"chatra\": {\n      \"name\": \"Chatra\",\n      \"categoryId\": 2,\n      \"url\": \"https://chatra.io\",\n      \"companyId\": \"chatra\"\n    },\n    \"chaturbate.com\": {\n      \"name\": \"chaturbate.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"chatwing\": {\n      \"name\": \"ChatWing\",\n      \"categoryId\": 2,\n      \"url\": \"http://chatwing.com/\",\n      \"companyId\": \"chatwing\"\n    },\n    \"checkmystats\": {\n      \"name\": \"CheckMyStats\",\n      \"categoryId\": 4,\n      \"url\": \"http://checkmystats.com.au\",\n      \"companyId\": \"checkmystats\"\n    },\n    \"chefkoch_de\": {\n      \"name\": \"chefkoch.de\",\n      \"categoryId\": 8,\n      \"url\": \"http://chefkoch.de/\",\n      \"companyId\": null\n    },\n    \"chin_media\": {\n      \"name\": \"Chin Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.chinmedia.vn/#\",\n      \"companyId\": \"chin_media\"\n    },\n    \"chinesean\": {\n      \"name\": \"ChineseAN\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.chinesean.com/\",\n      \"companyId\": \"chinesean\"\n    },\n    \"chitika\": {\n      \"name\": \"Chitika\",\n      \"categoryId\": 4,\n      \"url\": \"http://chitika.com/\",\n      \"companyId\": \"chitika\"\n    },\n    \"choicestream\": {\n      \"name\": \"ChoiceStream\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.choicestream.com/\",\n      \"companyId\": \"choicestream\"\n    },\n    \"chute\": {\n      \"name\": \"Chute\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.getchute.com/\",\n      \"companyId\": \"esw_capital\"\n    },\n    \"circit\": {\n      \"name\": \"circIT\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.circit.de/\",\n      \"companyId\": null\n    },\n    \"circulate\": {\n      \"name\": \"Circulate\",\n      \"categoryId\": 6,\n      \"url\": \"http://circulate.com/\",\n      \"companyId\": \"circulate\"\n    },\n    \"city_spark\": {\n      \"name\": \"City Spark\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cityspark.com/\",\n      \"companyId\": \"city_spark\"\n    },\n    \"cityads\": {\n      \"name\": \"CityAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://cityads.ru/\",\n      \"companyId\": \"cityads\"\n    },\n    \"ciuvo.com\": {\n      \"name\": \"ciuvo.com\",\n      \"categoryId\": 12,\n      \"url\": \"https://www.ciuvo.com/\",\n      \"companyId\": null\n    },\n    \"civey_widgets\": {\n      \"name\": \"Civey Widgets\",\n      \"categoryId\": 2,\n      \"url\": \"https://civey.com/\",\n      \"companyId\": \"civey\"\n    },\n    \"civicscience.com\": {\n      \"name\": \"CivicScience\",\n      \"categoryId\": 6,\n      \"url\": \"https://civicscience.com/\",\n      \"companyId\": \"civicscience\"\n    },\n    \"ciwebgroup\": {\n      \"name\": \"CIWebGroup\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ciwebgroup.com/\",\n      \"companyId\": \"ciwebgroup\"\n    },\n    \"clcknads.pro\": {\n      \"name\": \"clcknads.pro\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"clear_pier\": {\n      \"name\": \"ClearPier\",\n      \"categoryId\": 4,\n      \"url\": \"http://clearpier.com/\",\n      \"companyId\": \"clear_pier\"\n    },\n    \"clearbit.com\": {\n      \"name\": \"Clearbit\",\n      \"categoryId\": 6,\n      \"url\": \"https://clearbit.com/\",\n      \"companyId\": \"clearbit\"\n    },\n    \"clearsale\": {\n      \"name\": \"clearsale\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.clear.sale/\",\n      \"companyId\": null\n    },\n    \"clearstream.tv\": {\n      \"name\": \"Clearstream.TV\",\n      \"categoryId\": 4,\n      \"url\": \"http://clearstream.tv/\",\n      \"companyId\": \"clearstream.tv\"\n    },\n    \"clerk.io\": {\n      \"name\": \"Clerk.io\",\n      \"categoryId\": 4,\n      \"url\": \"https://clerk.io/\",\n      \"companyId\": \"clerk.io\"\n    },\n    \"clever_push\": {\n      \"name\": \"Clever Push\",\n      \"categoryId\": 6,\n      \"url\": \"https://clevertap.com/\",\n      \"companyId\": \"clever_push\"\n    },\n    \"clever_tap\": {\n      \"name\": \"CleverTap\",\n      \"categoryId\": 6,\n      \"url\": \"https://clevertap.com/\",\n      \"companyId\": \"clever_tap\"\n    },\n    \"cleversite\": {\n      \"name\": \"Cleversite\",\n      \"categoryId\": 2,\n      \"url\": \"http://cleversite.ru/\",\n      \"companyId\": \"cleversite\"\n    },\n    \"click360\": {\n      \"name\": \"Click360\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.click360.io/\",\n      \"companyId\": \"click360\"\n    },\n    \"click_and_chat\": {\n      \"name\": \"Click and Chat\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.clickandchat.com/\",\n      \"companyId\": \"clickandchat\"\n    },\n    \"click_back\": {\n      \"name\": \"Click Back\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickback.com/\",\n      \"companyId\": \"clickback\"\n    },\n    \"clickaider\": {\n      \"name\": \"ClickAider\",\n      \"categoryId\": 4,\n      \"url\": \"http://clickaider.com/\",\n      \"companyId\": \"clickaider\"\n    },\n    \"clickaine\": {\n      \"name\": \"Clickaine\",\n      \"categoryId\": 4,\n      \"url\": \"https://clickaine.com/\",\n      \"companyId\": \"clickaine\",\n      \"source\": \"AdGuard\"\n    },\n    \"clickbank\": {\n      \"name\": \"ClickBank\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickbank.com/\",\n      \"companyId\": \"clickbank\"\n    },\n    \"clickbank_proads\": {\n      \"name\": \"ClickBank ProAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cbproads.com/\",\n      \"companyId\": \"clickbank_proads\"\n    },\n    \"clickbooth\": {\n      \"name\": \"Clickbooth\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickbooth.com/\",\n      \"companyId\": \"clickbooth\"\n    },\n    \"clickcease\": {\n      \"name\": \"ClickCease\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.clickcease.com/\",\n      \"companyId\": \"click_cease\"\n    },\n    \"clickcertain\": {\n      \"name\": \"ClickCertain\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickcertain.com\",\n      \"companyId\": \"clickcertain\"\n    },\n    \"clickdesk\": {\n      \"name\": \"ClickDesk\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.clickdesk.com/\",\n      \"companyId\": \"clickdesk\"\n    },\n    \"clickdimensions\": {\n      \"name\": \"ClickDimensions\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickdimensions.com/\",\n      \"companyId\": \"clickdimensions\"\n    },\n    \"clickequations\": {\n      \"name\": \"ClickEquations\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickequations.com/\",\n      \"companyId\": \"acquisio\"\n    },\n    \"clickexperts\": {\n      \"name\": \"ClickExperts\",\n      \"categoryId\": 4,\n      \"url\": \"http://clickexperts.com/corp/index.php?lang=en\",\n      \"companyId\": \"clickexperts\"\n    },\n    \"clickforce\": {\n      \"name\": \"ClickForce\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickforce.com.tw/\",\n      \"companyId\": \"clickforce\"\n    },\n    \"clickinc\": {\n      \"name\": \"ClickInc\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickinc.com\",\n      \"companyId\": \"clickinc\"\n    },\n    \"clickintext\": {\n      \"name\": \"ClickInText\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickintext.com/\",\n      \"companyId\": \"clickintext\"\n    },\n    \"clickky\": {\n      \"name\": \"Clickky\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickky.biz/\",\n      \"companyId\": \"clickky\"\n    },\n    \"clickmeter\": {\n      \"name\": \"ClickMeter\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickmeter.com\",\n      \"companyId\": \"clickmeter\"\n    },\n    \"clickonometrics\": {\n      \"name\": \"Clickonometrics\",\n      \"categoryId\": 4,\n      \"url\": \"http://clickonometrics.pl/\",\n      \"companyId\": \"clickonometrics\"\n    },\n    \"clickpoint\": {\n      \"name\": \"Clickpoint\",\n      \"categoryId\": 4,\n      \"url\": \"http://clickpoint.com/\",\n      \"companyId\": \"clickpoint\"\n    },\n    \"clickprotector\": {\n      \"name\": \"ClickProtector\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.clickprotector.com/\",\n      \"companyId\": \"clickprotector\"\n    },\n    \"clickreport\": {\n      \"name\": \"ClickReport\",\n      \"categoryId\": 6,\n      \"url\": \"http://clickreport.com/\",\n      \"companyId\": \"clickreport\"\n    },\n    \"clicks_thru_networks\": {\n      \"name\": \"Clicks Thru Networks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clicksthrunetwork.com/\",\n      \"companyId\": \"clicksthrunetwork\"\n    },\n    \"clicksor\": {\n      \"name\": \"Clicksor\",\n      \"categoryId\": 4,\n      \"url\": \"http://clicksor.com/\",\n      \"companyId\": \"clicksor\"\n    },\n    \"clicktale\": {\n      \"name\": \"ClickTale\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.clicktale.com/\",\n      \"companyId\": \"clicktale\"\n    },\n    \"clicktripz\": {\n      \"name\": \"ClickTripz\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.clicktripz.com\",\n      \"companyId\": \"clicktripz\"\n    },\n    \"clickwinks\": {\n      \"name\": \"Clickwinks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickwinks.com/\",\n      \"companyId\": \"clickwinks\"\n    },\n    \"clicky\": {\n      \"name\": \"Clicky\",\n      \"categoryId\": 6,\n      \"url\": \"http://getclicky.com/\",\n      \"companyId\": \"clicky\"\n    },\n    \"clickyab\": {\n      \"name\": \"Clickyab\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.clickyab.com/\",\n      \"companyId\": \"clickyab\"\n    },\n    \"clicmanager\": {\n      \"name\": \"ClicManager\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clicmanager.fr/\",\n      \"companyId\": \"clicmanager\"\n    },\n    \"clip_syndicate\": {\n      \"name\": \"Clip Syndicate\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clipsyndicate.com/\",\n      \"companyId\": \"clip_syndicate\"\n    },\n    \"clixgalore\": {\n      \"name\": \"clixGalore\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clixgalore.com/\",\n      \"companyId\": \"clixgalore\"\n    },\n    \"clixmetrix\": {\n      \"name\": \"ClixMetrix\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clixmetrix.com/\",\n      \"companyId\": \"clixmedia\"\n    },\n    \"clixsense\": {\n      \"name\": \"ClixSense\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clixsense.com/\",\n      \"companyId\": \"clixsense\"\n    },\n    \"cloud-media.fr\": {\n      \"name\": \"CloudMedia\",\n      \"categoryId\": 4,\n      \"url\": \"https://cloudmedia.fr/\",\n      \"companyId\": null\n    },\n    \"cloudflare\": {\n      \"name\": \"CloudFlare\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.cloudflare.com/\",\n      \"companyId\": \"cloudflare\"\n    },\n    \"cloudimage.io\": {\n      \"name\": \"Cloudimage.io\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.cloudimage.io/en/home\",\n      \"companyId\": \"scaleflex_sas\"\n    },\n    \"cloudinary\": {\n      \"name\": \"Cloudinary\",\n      \"categoryId\": 9,\n      \"url\": \"https://cloudinary.com/\",\n      \"companyId\": null\n    },\n    \"clove_network\": {\n      \"name\": \"Clove Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clovenetwork.com/\",\n      \"companyId\": \"clove_network\"\n    },\n    \"clustrmaps\": {\n      \"name\": \"ClustrMaps\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clustrmaps.com/\",\n      \"companyId\": \"clustrmaps\"\n    },\n    \"cnbc\": {\n      \"name\": \"CNBC\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.cnbc.com/\",\n      \"companyId\": \"nbcuniversal\"\n    },\n    \"cnetcontent.com\": {\n      \"name\": \"Cnetcontent\",\n      \"categoryId\": 8,\n      \"url\": \"http://cnetcontent.com/\",\n      \"companyId\": \"cbs_interactive\"\n    },\n    \"cnstats\": {\n      \"name\": \"CNStats\",\n      \"categoryId\": 6,\n      \"url\": \"http://cnstats.ru/\",\n      \"companyId\": \"cnstats\"\n    },\n    \"cnzz.com\": {\n      \"name\": \"Umeng\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.umeng.com/\",\n      \"companyId\": \"umeng\"\n    },\n    \"coadvertise\": {\n      \"name\": \"COADVERTISE\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.coadvertise.com/\",\n      \"companyId\": \"coadvertise\"\n    },\n    \"cobrowser\": {\n      \"name\": \"CoBrowser\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.cobrowser.net/\",\n      \"companyId\": \"cobrowser.net\"\n    },\n    \"codeonclick.com\": {\n      \"name\": \"codeonclick.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"cogocast\": {\n      \"name\": \"CogoCast\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cogocast.com\",\n      \"companyId\": \"cogocast\"\n    },\n    \"coin_have\": {\n      \"name\": \"Coin Have\",\n      \"categoryId\": 4,\n      \"url\": \"https://coin-have.com/\",\n      \"companyId\": \"coin_have\"\n    },\n    \"coin_traffic\": {\n      \"name\": \"Coin Traffic\",\n      \"categoryId\": 2,\n      \"url\": \"https://cointraffic.io/\",\n      \"companyId\": \"coin_traffic\"\n    },\n    \"coinhive\": {\n      \"name\": \"Coinhive\",\n      \"categoryId\": 8,\n      \"url\": \"https://coinhive.com/\",\n      \"companyId\": \"coinhive\"\n    },\n    \"coinurl\": {\n      \"name\": \"CoinURL\",\n      \"categoryId\": 4,\n      \"url\": \"https://coinurl.com/\",\n      \"companyId\": \"coinurl\"\n    },\n    \"coll1onf.com\": {\n      \"name\": \"coll1onf.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"coll2onf.com\": {\n      \"name\": \"coll2onf.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"collarity\": {\n      \"name\": \"Collarity\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.collarity.com/\",\n      \"companyId\": \"collarity\"\n    },\n    \"columbia_online\": {\n      \"name\": \"Columbia Online\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.colombiaonline.com/\",\n      \"companyId\": \"columbia_online\"\n    },\n    \"combotag\": {\n      \"name\": \"ComboTag\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.combotag.com/\",\n      \"companyId\": null\n    },\n    \"comcast_technology_solutions\": {\n      \"name\": \"Comcast Technology Solutions\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.comcasttechnologysolutions.com/\",\n      \"companyId\": \"comcast_technology_solutions\"\n    },\n    \"comm100\": {\n      \"name\": \"Comm100\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.comm100.com/\",\n      \"companyId\": \"comm100\"\n    },\n    \"commerce_sciences\": {\n      \"name\": \"Commerce Sciences\",\n      \"categoryId\": 4,\n      \"url\": \"http://commercesciences.com/\",\n      \"companyId\": \"commerce_sciences\"\n    },\n    \"commercehub\": {\n      \"name\": \"CommerceHub\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mercent.com/\",\n      \"companyId\": \"commercehub\"\n    },\n    \"commercialvalue.org\": {\n      \"name\": \"commercialvalue.org\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"commission_junction\": {\n      \"name\": \"CJ Affiliate\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cj.com/\",\n      \"companyId\": \"conversant\"\n    },\n    \"communicator_corp\": {\n      \"name\": \"Communicator Corp\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.communicatorcorp.com/\",\n      \"companyId\": \"communicator_corp\"\n    },\n    \"communigator\": {\n      \"name\": \"CommuniGator\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.wowanalytics.co.uk/\",\n      \"companyId\": \"communigator\"\n    },\n    \"competexl\": {\n      \"name\": \"CompeteXL\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.compete.com/help/s12\",\n      \"companyId\": \"wpp\"\n    },\n    \"complex_media_network\": {\n      \"name\": \"Complex Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.complex.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"comprigo\": {\n      \"name\": \"comprigo\",\n      \"categoryId\": 12,\n      \"url\": \"https://www.comprigo.com/\",\n      \"companyId\": null\n    },\n    \"comscore\": {\n      \"name\": \"ComScore, Inc.\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.comscore.com/\",\n      \"companyId\": \"comscore\"\n    },\n    \"conative.de\": {\n      \"name\": \"CoNative\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.conative.de/\",\n      \"companyId\": null\n    },\n    \"condenastdigital.com\": {\n      \"name\": \"Condé Nast Digital\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.condenast.com/\",\n      \"companyId\": \"conde_nast\"\n    },\n    \"conduit\": {\n      \"name\": \"Conduit\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.conduit.com/\",\n      \"companyId\": \"conduit\"\n    },\n    \"confirmit\": {\n      \"name\": \"Confirmit\",\n      \"categoryId\": 4,\n      \"url\": \"http://confirmit.com/\",\n      \"companyId\": \"confirmit\"\n    },\n    \"congstar.de\": {\n      \"name\": \"congstar.de\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"connatix.com\": {\n      \"name\": \"Connatix\",\n      \"categoryId\": 4,\n      \"url\": \"https://connatix.com/\",\n      \"companyId\": \"connatix\"\n    },\n    \"connectad\": {\n      \"name\": \"ConnectAd\",\n      \"categoryId\": 4,\n      \"url\": \"https://connectad.io/\",\n      \"companyId\": \"connectad\"\n    },\n    \"connecto\": {\n      \"name\": \"Connecto\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.connecto.io/\",\n      \"companyId\": \"connecto\"\n    },\n    \"connexity\": {\n      \"name\": \"Connexity\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.connexity.com\",\n      \"companyId\": \"shopzilla\"\n    },\n    \"connextra\": {\n      \"name\": \"Connextra\",\n      \"categoryId\": 4,\n      \"url\": \"http://connextra.com/\",\n      \"companyId\": \"connextra\"\n    },\n    \"constant_contact\": {\n      \"name\": \"Constant Contact\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.constantcontact.com/index.jsp\",\n      \"companyId\": \"constant_contact\"\n    },\n    \"consumable\": {\n      \"name\": \"Consumable\",\n      \"categoryId\": 4,\n      \"url\": \"http://consumable.com/index.html\",\n      \"companyId\": \"giftconnect\"\n    },\n    \"contact_at_once\": {\n      \"name\": \"Contact At Once!\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.contactatonce.com/\",\n      \"companyId\": \"contact_at_once!\"\n    },\n    \"contact_impact\": {\n      \"name\": \"Contact Impact\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.contactimpact.de/\",\n      \"companyId\": \"axel_springer\"\n    },\n    \"contactme\": {\n      \"name\": \"ContactMe\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.contactme.com\",\n      \"companyId\": \"contactme\"\n    },\n    \"contaxe\": {\n      \"name\": \"Contaxe\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.contaxe.com/\",\n      \"companyId\": \"contaxe\"\n    },\n    \"content.ad\": {\n      \"name\": \"Content.ad\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.content.ad/\",\n      \"companyId\": \"content.ad\"\n    },\n    \"content_insights\": {\n      \"name\": \"Content Insights\",\n      \"categoryId\": 6,\n      \"url\": \"https://contentinsights.com/\",\n      \"companyId\": \"content_insights\"\n    },\n    \"contentexchange.me\": {\n      \"name\": \"Content Exchange\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.contentexchange.me/\",\n      \"companyId\": \"i.r.v.\"\n    },\n    \"contentful_gmbh\": {\n      \"name\": \"Contentful GmbH\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.contentful.com/\",\n      \"companyId\": \"contentful_gmbh\"\n    },\n    \"contentpass\": {\n      \"name\": \"ContentPass\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.contentpass.de/\",\n      \"companyId\": \"contentpass\"\n    },\n    \"contentsquare.net\": {\n      \"name\": \"ContentSquare\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.contentsquare.com/\",\n      \"companyId\": \"content_square\"\n    },\n    \"contentwrx\": {\n      \"name\": \"Contentwrx\",\n      \"categoryId\": 6,\n      \"url\": \"http://contentwrx.com/\",\n      \"companyId\": \"contentwrx\"\n    },\n    \"context\": {\n      \"name\": \"C|ON|TEXT\",\n      \"categoryId\": 4,\n      \"url\": \"http://c-on-text.com\",\n      \"companyId\": \"c_on_text\"\n    },\n    \"context.ad\": {\n      \"name\": \"Context.ad\",\n      \"categoryId\": 4,\n      \"url\": \"http://contextad.pl/\",\n      \"companyId\": \"context.ad\"\n    },\n    \"continum_net\": {\n      \"name\": \"continum.net\",\n      \"categoryId\": 10,\n      \"url\": \"http://continum.net/\",\n      \"companyId\": null\n    },\n    \"contribusource\": {\n      \"name\": \"Contribusource\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.contribusource.com/\",\n      \"companyId\": \"contribusource\"\n    },\n    \"convergetrack\": {\n      \"name\": \"ConvergeTrack\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.convergedirect.com/technology/convergetrack.shtml\",\n      \"companyId\": \"convergedirect\"\n    },\n    \"conversant\": {\n      \"name\": \"Conversant\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.conversantmedia.eu/\",\n      \"companyId\": \"conversant\"\n    },\n    \"conversio\": {\n      \"name\": \"CM Commerce\",\n      \"categoryId\": 6,\n      \"url\": \"https://cm-commerce.com/\",\n      \"companyId\": \"conversio\"\n    },\n    \"conversion_logic\": {\n      \"name\": \"Conversion Logic\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.conversionlogic.com/\",\n      \"companyId\": \"conversion_logic\"\n    },\n    \"conversionruler\": {\n      \"name\": \"ConversionRuler\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.conversionruler.com/\",\n      \"companyId\": \"market_ruler\"\n    },\n    \"conversions_box\": {\n      \"name\": \"Conversions Box\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.conversionsbox.com/\",\n      \"companyId\": \"conversions_box\"\n    },\n    \"conversions_on_demand\": {\n      \"name\": \"Conversions On Demand\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.conversionsondemand.com/\",\n      \"companyId\": \"conversions_on_demand\"\n    },\n    \"conversive\": {\n      \"name\": \"Conversive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.conversive.nl/\",\n      \"companyId\": \"conversive\"\n    },\n    \"convert\": {\n      \"name\": \"Convert\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.convert.com/\",\n      \"companyId\": \"convert\"\n    },\n    \"convertfox\": {\n      \"name\": \"ConvertFox\",\n      \"categoryId\": 2,\n      \"url\": \"https://convertfox.com/\",\n      \"companyId\": \"convertfox\"\n    },\n    \"convertro\": {\n      \"name\": \"Convertro\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.convertro.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"conviva\": {\n      \"name\": \"Conviva\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.conviva.com/\",\n      \"companyId\": \"conviva\"\n    },\n    \"cookie_consent\": {\n      \"name\": \"Cookie Consent\",\n      \"categoryId\": 5,\n      \"url\": \"https://silktide.com/\",\n      \"companyId\": \"silktide\"\n    },\n    \"cookie_script\": {\n      \"name\": \"Cookie Script\",\n      \"categoryId\": 5,\n      \"url\": \"https://cookie-script.com/\",\n      \"companyId\": \"cookie_script\"\n    },\n    \"cookiebot\": {\n      \"name\": \"Cookiebot\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.cookiebot.com/en/\",\n      \"companyId\": \"cybot\"\n    },\n    \"cookieq\": {\n      \"name\": \"CookieQ\",\n      \"categoryId\": 5,\n      \"url\": \"http://cookieq.com/CookieQ\",\n      \"companyId\": \"baycloud\"\n    },\n    \"cooliris\": {\n      \"name\": \"Cooliris\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.cooliris.com\",\n      \"companyId\": \"cooliris\"\n    },\n    \"copacet\": {\n      \"name\": \"Copacet\",\n      \"categoryId\": 4,\n      \"url\": \"http://copacet.com/\",\n      \"companyId\": \"copacet\"\n    },\n    \"coreaudience\": {\n      \"name\": \"CoreAudience\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.redaril.com/\",\n      \"companyId\": \"hearst\"\n    },\n    \"coremotives\": {\n      \"name\": \"CoreMotives\",\n      \"categoryId\": 4,\n      \"url\": \"http://coremotives.com/\",\n      \"companyId\": \"coremotives\"\n    },\n    \"coull\": {\n      \"name\": \"Coull\",\n      \"categoryId\": 4,\n      \"url\": \"http://coull.com/\",\n      \"companyId\": \"coull\"\n    },\n    \"cpm_rocket\": {\n      \"name\": \"CPM Rocket\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cpmrocket.com/\",\n      \"companyId\": \"cpm_rocket\"\n    },\n    \"cpmprofit\": {\n      \"name\": \"CPMProfit\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cpmprofit.com/\",\n      \"companyId\": \"cpmprofit\"\n    },\n    \"cpmstar\": {\n      \"name\": \"CPMStar\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cpmstar.com\",\n      \"companyId\": \"cpmstar\"\n    },\n    \"cpx.to\": {\n      \"name\": \"Captify\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.captify.co.uk/\",\n      \"companyId\": \"captify\"\n    },\n    \"cq_counter\": {\n      \"name\": \"CQ Counter\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.cqcounter.com/\",\n      \"companyId\": \"cq_counter\"\n    },\n    \"cqq5id8n.com\": {\n      \"name\": \"cqq5id8n.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"cquotient.com\": {\n      \"name\": \"CQuotient\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.demandware.com/#cquotient\",\n      \"companyId\": \"salesforce\"\n    },\n    \"craftkeys\": {\n      \"name\": \"CraftKeys\",\n      \"categoryId\": 4,\n      \"url\": \"http://craftkeys.com/\",\n      \"companyId\": \"craftkeys\"\n    },\n    \"crakmedia_network\": {\n      \"name\": \"Crakmedia Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://crakmedia.com/\",\n      \"companyId\": \"crakmedia_network\"\n    },\n    \"crankyads\": {\n      \"name\": \"CrankyAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.crankyads.com\",\n      \"companyId\": \"crankyads\"\n    },\n    \"crashlytics\": {\n      \"name\": \"Crashlytics\",\n      \"categoryId\": 101,\n      \"url\": \"https://crashlytics.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"crazy_egg\": {\n      \"name\": \"Crazy Egg\",\n      \"categoryId\": 6,\n      \"url\": \"http://crazyegg.com/\",\n      \"companyId\": \"crazy_egg\"\n    },\n    \"creafi\": {\n      \"name\": \"Creafi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.creafi.com/en/home/\",\n      \"companyId\": \"crazy4media\"\n    },\n    \"createjs\": {\n      \"name\": \"CreateJS\",\n      \"categoryId\": 9,\n      \"url\": \"https://createjs.com/\",\n      \"companyId\": null\n    },\n    \"creative_commons\": {\n      \"name\": \"Creative Commons\",\n      \"categoryId\": 8,\n      \"url\": \"https://creativecommons.org/\",\n      \"companyId\": \"creative_commons_corp\"\n    },\n    \"crimsonhexagon_com\": {\n      \"name\": \"Brandwatch\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.brandwatch.com/\",\n      \"companyId\": \"brandwatch\"\n    },\n    \"crimtan\": {\n      \"name\": \"Crimtan\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.crimtan.com/\",\n      \"companyId\": \"crimtan\"\n    },\n    \"crisp\": {\n      \"name\": \"Crisp\",\n      \"categoryId\": 2,\n      \"url\": \"https://crisp.chat/\",\n      \"companyId\": \"crisp\"\n    },\n    \"criteo\": {\n      \"name\": \"Criteo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.criteo.com/\",\n      \"companyId\": \"criteo\"\n    },\n    \"crm4d\": {\n      \"name\": \"CRM4D\",\n      \"categoryId\": 6,\n      \"url\": \"https://crm4d.com/\",\n      \"companyId\": \"crm4d\"\n    },\n    \"crossengage\": {\n      \"name\": \"CrossEngage\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.crossengage.io/\",\n      \"companyId\": \"crossengage\"\n    },\n    \"crosspixel\": {\n      \"name\": \"Cross Pixel\",\n      \"categoryId\": 4,\n      \"url\": \"http://crosspixel.net/\",\n      \"companyId\": \"cross_pixel\"\n    },\n    \"crosssell.info\": {\n      \"name\": \"econda Cross Sell\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.econda.de/en/solutions/personalization/cross-sell/\",\n      \"companyId\": \"econda\"\n    },\n    \"crossss\": {\n      \"name\": \"Crossss\",\n      \"categoryId\": 4,\n      \"url\": \"http://crossss.ru/\",\n      \"companyId\": \"crossss\"\n    },\n    \"crowd_ignite\": {\n      \"name\": \"Crowd Ignite\",\n      \"categoryId\": 4,\n      \"url\": \"http://get.crowdignite.com/\",\n      \"companyId\": \"gorilla_nation_media\"\n    },\n    \"crowd_science\": {\n      \"name\": \"Crowd Science\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.crowdscience.com/\",\n      \"companyId\": \"crowd_science\"\n    },\n    \"crowdprocess\": {\n      \"name\": \"CrowdProcess\",\n      \"categoryId\": 2,\n      \"url\": \"https://crowdprocess.com\",\n      \"companyId\": \"crowdprocess\"\n    },\n    \"crowdynews\": {\n      \"name\": \"Crowdynews\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.crowdynews.com/\",\n      \"companyId\": \"crowdynews\"\n    },\n    \"crownpeak\": {\n      \"name\": \"Crownpeak\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.crownpeak.com/\",\n      \"companyId\": \"crownpeak\"\n    },\n    \"cryptoloot_miner\": {\n      \"name\": \"CryptoLoot Miner\",\n      \"categoryId\": 4,\n      \"url\": \"https://crypto-loot.com/\",\n      \"companyId\": \"cryptoloot\"\n    },\n    \"ctnetwork\": {\n      \"name\": \"CTnetwork\",\n      \"categoryId\": 4,\n      \"url\": \"http://ctnetwork.hu/\",\n      \"companyId\": \"ctnetwork\"\n    },\n    \"ctrlshift\": {\n      \"name\": \"CtrlShift\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adzcentral.com/\",\n      \"companyId\": \"ctrlshift\"\n    },\n    \"cubed\": {\n      \"name\": \"Cubed\",\n      \"categoryId\": 6,\n      \"url\": \"http://withcubed.com/\",\n      \"companyId\": \"cubed_attribution\"\n    },\n    \"cuelinks\": {\n      \"name\": \"CueLinks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cuelinks.com/\",\n      \"companyId\": \"cuelinks\"\n    },\n    \"cup_interactive\": {\n      \"name\": \"Cup Interactive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cupinteractive.com/\",\n      \"companyId\": \"cup_interactive\"\n    },\n    \"curse.com\": {\n      \"name\": \"Curse\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.curse.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"cursecdn.com\": {\n      \"name\": \"Curse CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.curse.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"customer.io\": {\n      \"name\": \"Customer.io\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.customer.io/\",\n      \"companyId\": \"customer.io\"\n    },\n    \"customerly\": {\n      \"name\": \"Customerly\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.customerly.io/\",\n      \"companyId\": \"customerly\"\n    },\n    \"cxense\": {\n      \"name\": \"cXense\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cxense.com/\",\n      \"companyId\": \"cxense\"\n    },\n    \"cxo.name\": {\n      \"name\": \"Chip Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.chip.de/\",\n      \"companyId\": null\n    },\n    \"cyber_wing\": {\n      \"name\": \"Cyber Wing\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cyberwing.co.jp/\",\n      \"companyId\": \"cyberwing\"\n    },\n    \"cybersource\": {\n      \"name\": \"CyberSource\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.cybersource.com/en-gb.html\",\n      \"companyId\": \"visa\"\n    },\n    \"cygnus\": {\n      \"name\": \"Cygnus\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cygnus.com/\",\n      \"companyId\": \"cygnus\"\n    },\n    \"da-ads.com\": {\n      \"name\": \"da-ads.com\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"dailymail.co.uk\": {\n      \"name\": \"Daily Mail\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.dailymail.co.uk/home/index.html\",\n      \"companyId\": \"dmg_media\"\n    },\n    \"dailymotion\": {\n      \"name\": \"Dailymotion\",\n      \"categoryId\": 8,\n      \"url\": \"https://vivendi.com/\",\n      \"companyId\": \"vivendi\"\n    },\n    \"dailymotion_advertising\": {\n      \"name\": \"Dailymotion Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"http://advertising.dailymotion.com/\",\n      \"companyId\": \"vivendi\"\n    },\n    \"daisycon\": {\n      \"name\": \"Daisycon\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.daisycon.com\",\n      \"companyId\": \"daisycon\"\n    },\n    \"dantrack.net\": {\n      \"name\": \"DANtrack\",\n      \"categoryId\": 4,\n      \"url\": \"http://media.dantrack.net/privacy/\",\n      \"companyId\": \"dentsu_aegis_network\"\n    },\n    \"darwin_marketing\": {\n      \"name\": \"Darwin Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.darwinmarketing.com/\",\n      \"companyId\": \"darwin_marketing\"\n    },\n    \"dashboard_ad\": {\n      \"name\": \"Dashboard Ad\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dashboardad.com/\",\n      \"companyId\": \"premium_access\"\n    },\n    \"datacaciques.com\": {\n      \"name\": \"DataCaciques\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.datacaciques.com/\",\n      \"companyId\": null\n    },\n    \"datacoral\": {\n      \"name\": \"Datacoral\",\n      \"categoryId\": 4,\n      \"url\": \"https://datacoral.com/\",\n      \"companyId\": \"datacoral\"\n    },\n    \"datacrushers\": {\n      \"name\": \"Datacrushers\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.datacrushers.com/\",\n      \"companyId\": \"datacrushers\"\n    },\n    \"datadome\": {\n      \"name\": \"DataDome\",\n      \"categoryId\": 6,\n      \"url\": \"https://datadome.co/\",\n      \"companyId\": \"datadome\"\n    },\n    \"datalicious_datacollector\": {\n      \"name\": \"Datalicious DataCollector\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.datalicious.com/\",\n      \"companyId\": \"datalicious\"\n    },\n    \"datalicious_supertag\": {\n      \"name\": \"Datalicious SuperTag\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.datalicious.com/\",\n      \"companyId\": \"datalicious\"\n    },\n    \"datalogix\": {\n      \"name\": \"Datalogix\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.oracle.com/corporate/acquisitions/datalogix/\",\n      \"companyId\": \"oracle\"\n    },\n    \"datamind.ru\": {\n      \"name\": \"DataMind\",\n      \"categoryId\": 4,\n      \"url\": \"http://datamind.ru/\",\n      \"companyId\": \"datamind\"\n    },\n    \"datatables\": {\n      \"name\": \"DataTables\",\n      \"categoryId\": 2,\n      \"url\": \"https://datatables.net/\",\n      \"companyId\": null\n    },\n    \"datawrkz\": {\n      \"name\": \"Datawrkz\",\n      \"categoryId\": 4,\n      \"url\": \"http://datawrkz.com/\",\n      \"companyId\": \"datawrkz\"\n    },\n    \"dataxpand\": {\n      \"name\": \"Dataxpand\",\n      \"categoryId\": 4,\n      \"url\": \"http://dataxpand.com/\",\n      \"companyId\": \"dataxpand\"\n    },\n    \"dataxu\": {\n      \"name\": \"DataXu\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dataxu.com/\",\n      \"companyId\": \"dataxu\"\n    },\n    \"datds.net\": {\n      \"name\": \"datds.net\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"datonics\": {\n      \"name\": \"Datonics\",\n      \"categoryId\": 4,\n      \"url\": \"http://datonics.com/\",\n      \"companyId\": \"almondnet\"\n    },\n    \"datran\": {\n      \"name\": \"Pulsepoint\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.pulsepoint.com/\",\n      \"companyId\": \"pulsepoint_ad_exchange\"\n    },\n    \"davebestdeals.com\": {\n      \"name\": \"davebestdeals.com\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"dawandastatic.com\": {\n      \"name\": \"Dawanda CDN\",\n      \"categoryId\": 8,\n      \"url\": \"https://dawanda.com/\",\n      \"companyId\": null\n    },\n    \"dc_stormiq\": {\n      \"name\": \"DC StormIQ\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dc-storm.com/\",\n      \"companyId\": \"dc_storm\"\n    },\n    \"dcbap.com\": {\n      \"name\": \"dcbap.com\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"dcmn.com\": {\n      \"name\": \"DCMN\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.dcmn.com/\",\n      \"companyId\": null\n    },\n    \"de_persgroep\": {\n      \"name\": \"De Persgroep\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.persgroep.nl\",\n      \"companyId\": \"de_persgroep\"\n    },\n    \"deadline_funnel\": {\n      \"name\": \"Deadline Funnel\",\n      \"categoryId\": 6,\n      \"url\": \"https://deadlinefunnel.com/\",\n      \"companyId\": \"deadline_funnel\"\n    },\n    \"dealer.com\": {\n      \"name\": \"Dealer.com\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.dealer.com/\",\n      \"companyId\": \"dealer.com\"\n    },\n    \"decibel_insight\": {\n      \"name\": \"Decibel Insight\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.decibelinsight.com/\",\n      \"companyId\": \"decibel_insight\"\n    },\n    \"dedicated_media\": {\n      \"name\": \"Dedicated Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dedicatedmedia.com/\",\n      \"companyId\": \"dedicated_media\"\n    },\n    \"deep.bi\": {\n      \"name\": \"Deep.BI\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.deep.bi/#\",\n      \"companyId\": \"deep.bi\"\n    },\n    \"deepintent.com\": {\n      \"name\": \"DeepIntent\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.deepintent.com/\",\n      \"companyId\": \"deep_intent\"\n    },\n    \"defpush.com\": {\n      \"name\": \"defpush.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"deichmann.com\": {\n      \"name\": \"deichmann.com\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"delacon\": {\n      \"name\": \"Delacon\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.delacon.com.au/\",\n      \"companyId\": \"delacon\"\n    },\n    \"delivr\": {\n      \"name\": \"Delivr\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.percentmobile.com/\",\n      \"companyId\": \"delivr\"\n    },\n    \"delta_projects\": {\n      \"name\": \"Delta Projects\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adaction.se/\",\n      \"companyId\": \"delta_projects\"\n    },\n    \"deluxe\": {\n      \"name\": \"Deluxe\",\n      \"categoryId\": 6,\n      \"url\": \"https://ww.deluxe.com/\",\n      \"companyId\": \"deluxe\"\n    },\n    \"delve_networks\": {\n      \"name\": \"Delve Networks\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.delvenetworks.com/\",\n      \"companyId\": \"limelight_networks\"\n    },\n    \"demandbase\": {\n      \"name\": \"Demandbase\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.demandbase.com/\",\n      \"companyId\": \"demandbase\"\n    },\n    \"demandmedia\": {\n      \"name\": \"DemandMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.demandmedia.com\",\n      \"companyId\": \"leaf_group\"\n    },\n    \"deqwas\": {\n      \"name\": \"Deqwas\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.deqwas.com/\",\n      \"companyId\": \"deqwas\"\n    },\n    \"devatics\": {\n      \"name\": \"Devatics\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.devatics.co.uk/\",\n      \"companyId\": \"devatics\"\n    },\n    \"developer_media\": {\n      \"name\": \"Developer Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.developermedia.com/\",\n      \"companyId\": \"developer_media\"\n    },\n    \"deviantart.net\": {\n      \"name\": \"deviantart.net\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"dex_platform\": {\n      \"name\": \"DEX Platform\",\n      \"categoryId\": 4,\n      \"url\": \"http://blueadvertise.com/\",\n      \"companyId\": \"dex_platform\"\n    },\n    \"dgm\": {\n      \"name\": \"dgm\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dgm-au.com/\",\n      \"companyId\": \"apd\"\n    },\n    \"dialogtech\": {\n      \"name\": \"Dialogtech\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.dialogtech.com/\",\n      \"companyId\": \"dialogtech\"\n    },\n    \"dianomi\": {\n      \"name\": \"Dianomi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dianomi.com/cms/\",\n      \"companyId\": \"dianomi\"\n    },\n    \"didit_blizzard\": {\n      \"name\": \"Didit Blizzard\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.didit.com/blizzard\",\n      \"companyId\": \"didit\"\n    },\n    \"didit_maestro\": {\n      \"name\": \"Didit Maestro\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.didit.com/maestro\",\n      \"companyId\": \"didit\"\n    },\n    \"didomi\": {\n      \"name\": \"Didomi\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.didomi.io/en/\",\n      \"companyId\": \"didomi\"\n    },\n    \"digg_widget\": {\n      \"name\": \"Digg Widget\",\n      \"categoryId\": 2,\n      \"url\": \"http://digg.com/apple/Digg_Widget\",\n      \"companyId\": \"buysellads.com\"\n    },\n    \"digicert_trust_seal\": {\n      \"name\": \"Digicert Trust Seal\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.digicert.com/\",\n      \"companyId\": \"digicert\"\n    },\n    \"digidip\": {\n      \"name\": \"Digidip\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.digidip.net/\",\n      \"companyId\": \"digidip\"\n    },\n    \"digiglitz\": {\n      \"name\": \"Digiglitz\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.digiglitz.com/\",\n      \"companyId\": \"digiglitz\"\n    },\n    \"digilant\": {\n      \"name\": \"Digilant\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.digilant.com/\",\n      \"companyId\": \"digilant\"\n    },\n    \"digioh\": {\n      \"name\": \"Digioh\",\n      \"categoryId\": 4,\n      \"url\": \"https://digioh.com/\",\n      \"companyId\": \"digioh\",\n      \"source\": \"AdGuard\"\n    },\n    \"digital.gov\": {\n      \"name\": \"Digital.gov\",\n      \"categoryId\": 6,\n      \"url\": \"https://digital.gov/\",\n      \"companyId\": \"us_government\"\n    },\n    \"digital_control_room\": {\n      \"name\": \"Digital Control Room\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.cookiereports.com/\",\n      \"companyId\": \"digital_control_room\"\n    },\n    \"digital_nomads\": {\n      \"name\": \"Digital Nomads\",\n      \"categoryId\": 4,\n      \"url\": \"http://dnomads.net/\",\n      \"companyId\": null\n    },\n    \"digital_remedy\": {\n      \"name\": \"Digital Remedy\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.digitalremedy.com/\",\n      \"companyId\": \"digital_remedy\"\n    },\n    \"digital_river\": {\n      \"name\": \"Digital River\",\n      \"categoryId\": 4,\n      \"url\": \"http://corporate.digitalriver.com\",\n      \"companyId\": \"digital_river\"\n    },\n    \"digital_window\": {\n      \"name\": \"Digital Window\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.digitalwindow.com/\",\n      \"companyId\": \"axel_springer\"\n    },\n    \"digiteka\": {\n      \"name\": \"Digiteka\",\n      \"categoryId\": 4,\n      \"url\": \"http://digiteka.com/\",\n      \"companyId\": \"digiteka\"\n    },\n    \"digitrust\": {\n      \"name\": \"DigiTrust\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.digitru.st/\",\n      \"companyId\": \"iab\"\n    },\n    \"dihitt_badge\": {\n      \"name\": \"diHITT Badge\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.dihitt.com.br/\",\n      \"companyId\": \"dihitt\"\n    },\n    \"dimml\": {\n      \"name\": \"DimML\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"direct_keyword_link\": {\n      \"name\": \"Direct Keyword Link\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.keywordsconnect.com/\",\n      \"companyId\": \"direct_keyword_link\"\n    },\n    \"directadvert\": {\n      \"name\": \"Direct/ADVERT\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.directadvert.ru/\",\n      \"companyId\": \"directadvert\"\n    },\n    \"directrev\": {\n      \"name\": \"DirectREV\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.directrev.com/\",\n      \"companyId\": \"directrev\"\n    },\n    \"discord\": {\n      \"name\": \"Discord\",\n      \"categoryId\": 2,\n      \"url\": \"https://discordapp.com/\",\n      \"companyId\": null\n    },\n    \"disneyplus\": {\n      \"name\": \"Disney+\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.disneyplus.com/\",\n      \"companyId\": \"disney\",\n      \"source\": \"AdGuard\"\n    },\n    \"disneystreaming\": {\n      \"name\": \"Disney Streaming\",\n      \"categoryId\": 0,\n      \"url\": \"https://press.disneyplus.com\",\n      \"companyId\": \"disney\",\n      \"source\": \"AdGuard\"\n    },\n    \"display_block\": {\n      \"name\": \"display block\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.displayblock.com/\",\n      \"companyId\": \"display_block\"\n    },\n    \"disqus\": {\n      \"name\": \"Disqus\",\n      \"categoryId\": 1,\n      \"url\": \"https://disqus.com/\",\n      \"companyId\": \"zeta\"\n    },\n    \"disqus_ads\": {\n      \"name\": \"Disqus Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://disqusads.com/\",\n      \"companyId\": \"zeta\"\n    },\n    \"distil_tag\": {\n      \"name\": \"Distil Networks\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.distilnetworks.com/\",\n      \"companyId\": \"distil_networks\"\n    },\n    \"districtm.io\": {\n      \"name\": \"district m\",\n      \"categoryId\": 4,\n      \"url\": \"https://districtm.net/\",\n      \"companyId\": \"district_m\"\n    },\n    \"distroscale\": {\n      \"name\": \"Distroscale\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.distroscale.com/\",\n      \"companyId\": \"distroscale\"\n    },\n    \"div.show\": {\n      \"name\": \"div.show\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"diva\": {\n      \"name\": \"DiVa\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.vertriebsassistent.de/\",\n      \"companyId\": \"diva\"\n    },\n    \"divvit\": {\n      \"name\": \"Divvit\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.divvit.com/\",\n      \"companyId\": \"divvit\"\n    },\n    \"dm2\": {\n      \"name\": \"DM2\",\n      \"categoryId\": 4,\n      \"url\": \"http://digitalmediamanagement.com/\",\n      \"companyId\": \"digital_media_management\"\n    },\n    \"dmg_media\": {\n      \"name\": \"DMG Media\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.dmgmedia.co.uk/\",\n      \"companyId\": \"dmgt\"\n    },\n    \"dmm\": {\n      \"name\": \"DMM\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.dmm.co.jp\",\n      \"companyId\": \"dmm.r18\"\n    },\n    \"dmwd\": {\n      \"name\": \"DMWD\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"dockvine\": {\n      \"name\": \"dockvine\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.dockvine.com\",\n      \"companyId\": \"dockvine\"\n    },\n    \"docler\": {\n      \"name\": \"Docler\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.doclerholding.com/en/about/companies/33/\",\n      \"companyId\": \"docler_ip\"\n    },\n    \"dogannet\": {\n      \"name\": \"Dogannet\",\n      \"categoryId\": 4,\n      \"url\": \"http://s.dogannet.tv/\",\n      \"companyId\": \"dogannet\"\n    },\n    \"domainglass\": {\n      \"name\": \"Domain Glass\",\n      \"categoryId\": 8,\n      \"url\": \"https://domain.glass/\",\n      \"companyId\": \"domainglass\",\n      \"source\": \"AdGuard\"\n    },\n    \"domodomain\": {\n      \"name\": \"DomoDomain\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.domodomain.com/\",\n      \"companyId\": \"intelligencefocus\"\n    },\n    \"donationtools\": {\n      \"name\": \"iRobinHood\",\n      \"categoryId\": 12,\n      \"url\": \"http://www.irobinhood.org\",\n      \"companyId\": null\n    },\n    \"doofinder.com\": {\n      \"name\": \"doofinder\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.doofinder.com/\",\n      \"companyId\": null\n    },\n    \"doorbell.io\": {\n      \"name\": \"Doorbell.io\",\n      \"categoryId\": 5,\n      \"url\": \"https://doorbell.io/\",\n      \"companyId\": \"doorbell.io\"\n    },\n    \"dotandmedia\": {\n      \"name\": \"DotAndMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dotandmedia.com\",\n      \"companyId\": \"dotandmedia\"\n    },\n    \"dotmailer\": {\n      \"name\": \"dotMailer\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.dotdigitalgroup.com/\",\n      \"companyId\": \"dotdigital_group\"\n    },\n    \"dotmetrics.net\": {\n      \"name\": \"Dotmetrics\",\n      \"categoryId\": 6,\n      \"url\": \"https://dotmetrics.net/\",\n      \"companyId\": null\n    },\n    \"dotomi\": {\n      \"name\": \"Dotomi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dotomi.com/\",\n      \"companyId\": \"conversant\"\n    },\n    \"double.net\": {\n      \"name\": \"Double.net\",\n      \"categoryId\": 4,\n      \"url\": \"http://double.net/en/\",\n      \"companyId\": \"double.net\"\n    },\n    \"doubleclick\": {\n      \"name\": \"DoubleClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.doubleclick.com\",\n      \"companyId\": \"google\"\n    },\n    \"doubleclick_ad_buyer\": {\n      \"name\": \"DoubleClick Ad Exchange-Buyer\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"doubleclick_bid_manager\": {\n      \"name\": \"DoubleClick Bid Manager\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.invitemedia.com\",\n      \"companyId\": \"google\"\n    },\n    \"doubleclick_floodlight\": {\n      \"name\": \"DoubleClick Floodlight\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com/support/dfa/partner/bin/topic.py?topic=23943\",\n      \"companyId\": \"google\"\n    },\n    \"doubleclick_spotlight\": {\n      \"name\": \"DoubleClick Spotlight\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.doubleclick.com/products/richmedia\",\n      \"companyId\": \"google\"\n    },\n    \"doubleclick_video_stats\": {\n      \"name\": \"Doubleclick Video Stats\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"doublepimp\": {\n      \"name\": \"DoublePimp\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.doublepimp.com/\",\n      \"companyId\": \"doublepimp\"\n    },\n    \"doubleverify\": {\n      \"name\": \"DoubleVerify\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.doubleverify.com/\",\n      \"companyId\": \"doubleverify\"\n    },\n    \"dratio\": {\n      \"name\": \"Dratio\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.dratio.com/\",\n      \"companyId\": \"dratio\"\n    },\n    \"drawbridge\": {\n      \"name\": \"Drawbridge\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.drawbrid.ge/\",\n      \"companyId\": \"drawbridge\"\n    },\n    \"dreame_tech\": {\n      \"name\": \"Dreame Technology\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.dreame.tech/\",\n      \"companyId\": \"xiaomi\",\n      \"source\": \"AdGuard\"\n    },\n    \"dreamlab.pl\": {\n      \"name\": \"DreamLab.pl\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.dreamlab.pl/\",\n      \"companyId\": \"onet.pl\"\n    },\n    \"drift\": {\n      \"name\": \"Drift\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.drift.com/\",\n      \"companyId\": \"drift\"\n    },\n    \"drip\": {\n      \"name\": \"Drip\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.getdrip.com\",\n      \"companyId\": \"drip\"\n    },\n    \"dropbox.com\": {\n      \"name\": \"Dropbox\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.dropbox.com/\",\n      \"companyId\": null\n    },\n    \"dsnr_media_group\": {\n      \"name\": \"DSNR Media Group\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dsnrmg.com/\",\n      \"companyId\": \"dsnr_media_group\"\n    },\n    \"dsp_rambler\": {\n      \"name\": \"Rambler DSP\",\n      \"categoryId\": 4,\n      \"url\": \"http://dsp.rambler.ru/\",\n      \"companyId\": \"rambler\"\n    },\n    \"dstillery\": {\n      \"name\": \"Dstillery\",\n      \"categoryId\": 4,\n      \"url\": \"https://dstillery.com/\",\n      \"companyId\": \"dstillery\"\n    },\n    \"dtscout.com\": {\n      \"name\": \"DTScout\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dtscout.com/\",\n      \"companyId\": \"dtscout\"\n    },\n    \"dudamobile\": {\n      \"name\": \"DudaMobile\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.dudamobile.com/\",\n      \"companyId\": \"dudamobile\"\n    },\n    \"dun_and_bradstreet\": {\n      \"name\": \"Dun and Bradstreet\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.dnb.com/#\",\n      \"companyId\": \"dun_&_bradstreet\"\n    },\n    \"dwstat.cn\": {\n      \"name\": \"dwstat.cn\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.dwstat.cn/\",\n      \"companyId\": \"dwstat\"\n    },\n    \"dynad\": {\n      \"name\": \"DynAd\",\n      \"categoryId\": 4,\n      \"url\": \"http://dynad.net/\",\n      \"companyId\": \"dynad\"\n    },\n    \"dynadmic\": {\n      \"name\": \"DynAdmic\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"dynamic_1001_gmbh\": {\n      \"name\": \"Dynamic 1001 GmbH\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"dynamic_logic\": {\n      \"name\": \"Dynamic Logic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.dynamiclogic.com/\",\n      \"companyId\": \"millward_brown\"\n    },\n    \"dynamic_yield\": {\n      \"name\": \"Dynamic Yield\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.dynamicyield.com/\",\n      \"companyId\": \"dynamic_yield\"\n    },\n    \"dynamic_yield_analytics\": {\n      \"name\": \"Dynamic Yield Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.dynamicyield.com/\",\n      \"companyId\": \"dynamic_yield\"\n    },\n    \"dynata\": {\n      \"name\": \"Dynata\",\n      \"categoryId\": 4,\n      \"url\": \"http://hottraffic.nl/en\",\n      \"companyId\": \"dynata\"\n    },\n    \"dynatrace.com\": {\n      \"name\": \"Dynatrace\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.dynatrace.com/\",\n      \"companyId\": \"thoma_bravo\"\n    },\n    \"dyncdn.me\": {\n      \"name\": \"dyncdn.me\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"e-planning\": {\n      \"name\": \"e-planning\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.e-planning.net/\",\n      \"companyId\": \"e-planning\"\n    },\n    \"eadv\": {\n      \"name\": \"eADV\",\n      \"categoryId\": 4,\n      \"url\": \"http://eadv.it/\",\n      \"companyId\": \"eadv\"\n    },\n    \"eanalyzer.de\": {\n      \"name\": \"eanalyzer.de\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"early_birds\": {\n      \"name\": \"Early Birds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.early-birds.fr/\",\n      \"companyId\": \"early_birds\"\n    },\n    \"earnify\": {\n      \"name\": \"Earnify\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.earnify.com/\",\n      \"companyId\": \"earnify\"\n    },\n    \"earnify_tracker\": {\n      \"name\": \"Earnify Tracker\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.earnify.com/\",\n      \"companyId\": \"earnify\"\n    },\n    \"easyads\": {\n      \"name\": \"EasyAds\",\n      \"categoryId\": 4,\n      \"url\": \"https://easyads.bg/\",\n      \"companyId\": \"easyads\"\n    },\n    \"easylist_club\": {\n      \"name\": \"easylist.club\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"ebay\": {\n      \"name\": \"eBay Stats\",\n      \"categoryId\": 4,\n      \"url\": \"https://partnernetwork.ebay.com/\",\n      \"companyId\": \"ebay_partner_network\"\n    },\n    \"ebay_korea\": {\n      \"name\": \"eBay Korea\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ebay.com/\",\n      \"companyId\": \"ebay\"\n    },\n    \"ebay_partner_network\": {\n      \"name\": \"eBay Partner Network\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ebaypartnernetwork.com/files/hub/en-US/index.html\",\n      \"companyId\": \"ebay_partner_network\"\n    },\n    \"ebuzzing\": {\n      \"name\": \"eBuzzing\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ebuzzing.com/\",\n      \"companyId\": \"ebuzzing\"\n    },\n    \"echo\": {\n      \"name\": \"Echo\",\n      \"categoryId\": 4,\n      \"url\": \"http://js-kit.com/\",\n      \"companyId\": \"echo\"\n    },\n    \"eclick\": {\n      \"name\": \"eClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://eclick.vn\",\n      \"companyId\": \"eclick\"\n    },\n    \"econda\": {\n      \"name\": \"Econda\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.econda.de/\",\n      \"companyId\": \"econda\"\n    },\n    \"ecotag\": {\n      \"name\": \"ecotag\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.eco-tag.jp/\",\n      \"companyId\": \"ecotag\"\n    },\n    \"edgio\": {\n      \"name\": \"Edgio\",\n      \"categoryId\": 9,\n      \"url\": \"https://edg.io/\",\n      \"companyId\": \"edgio\",\n      \"source\": \"AdGuard\"\n    },\n    \"edigitalresearch\": {\n      \"name\": \"eDigitalResearch\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.edigitalresearch.com/\",\n      \"companyId\": \"edigitalresearch\"\n    },\n    \"effective_measure\": {\n      \"name\": \"Effective Measure\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.effectivemeasure.com/\",\n      \"companyId\": \"effective_measure\"\n    },\n    \"effiliation\": {\n      \"name\": \"Effiliation\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.effiliation.com/\",\n      \"companyId\": \"effiliation\"\n    },\n    \"egain\": {\n      \"name\": \"eGain\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.egain.com/\",\n      \"companyId\": \"egain\"\n    },\n    \"egain_analytics\": {\n      \"name\": \"eGain Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.egain.com/\",\n      \"companyId\": \"egain\"\n    },\n    \"ehi-siegel_de\": {\n      \"name\": \"ehi-siegel.de\",\n      \"categoryId\": 2,\n      \"url\": \"http://ehi-siegel.de/\",\n      \"companyId\": null\n    },\n    \"ekmpinpoint\": {\n      \"name\": \"ekmPinPoint\",\n      \"categoryId\": 6,\n      \"url\": \"http://ekmpinpoint.com/\",\n      \"companyId\": \"ekmpinpoint\"\n    },\n    \"ekomi\": {\n      \"name\": \"eKomi\",\n      \"categoryId\": 1,\n      \"url\": \"http://www.ekomi.co.uk\",\n      \"companyId\": \"ekomi\"\n    },\n    \"elastic_ad\": {\n      \"name\": \"Elastic Ad\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.elasticad.com\",\n      \"companyId\": \"elastic_ad\"\n    },\n    \"elastic_beanstalk\": {\n      \"name\": \"Elastic Beanstalk\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.amazon.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"electronic_arts\": {\n      \"name\": \"Electronic Arts\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.ea.com/\",\n      \"companyId\": \"electronic_arts\",\n      \"source\": \"AdGuard\"\n    },\n    \"element\": {\n      \"name\": \"Element\",\n      \"categoryId\": 7,\n      \"url\": \"https://element.io/\",\n      \"companyId\": \"element\",\n      \"source\": \"AdGuard\"\n    },\n    \"elicit\": {\n      \"name\": \"elicit\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.elicitsearch.com/\",\n      \"companyId\": \"elicit\"\n    },\n    \"eloqua\": {\n      \"name\": \"Eloqua\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.eloqua.com/\",\n      \"companyId\": \"oracle\"\n    },\n    \"eluxer_net\": {\n      \"name\": \"eluxer.net\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"email_aptitude\": {\n      \"name\": \"Email Aptitude\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.emailaptitude.com/\",\n      \"companyId\": \"email_aptitude\"\n    },\n    \"email_attitude\": {\n      \"name\": \"Email Attitude\",\n      \"categoryId\": 4,\n      \"url\": \"http://us.email-attitude.com/Default.aspx\",\n      \"companyId\": \"1000mercis\"\n    },\n    \"emarketeer\": {\n      \"name\": \"emarketeer\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.emarketeer.com/\",\n      \"companyId\": \"emarketeer\"\n    },\n    \"embed.ly\": {\n      \"name\": \"Embedly\",\n      \"categoryId\": 6,\n      \"url\": \"http://embed.ly/\",\n      \"companyId\": \"medium\"\n    },\n    \"emediate\": {\n      \"name\": \"Emediate\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.emediate.biz/\",\n      \"companyId\": \"cxense\"\n    },\n    \"emetriq\": {\n      \"name\": \"emetriq\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.emetriq.com\",\n      \"companyId\": \"emetriq\"\n    },\n    \"emma\": {\n      \"name\": \"Emma\",\n      \"categoryId\": 4,\n      \"url\": \"http://myemma.com/\",\n      \"companyId\": \"emma\"\n    },\n    \"emnet\": {\n      \"name\": \"eMnet\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.emnet.co.kr\",\n      \"companyId\": \"emnet\"\n    },\n    \"empathy\": {\n      \"name\": \"Empathy\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.colbenson.com\",\n      \"companyId\": \"empathy\"\n    },\n    \"emsmobile.de\": {\n      \"name\": \"EMS Mobile\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.emsmobile.com/\",\n      \"companyId\": null\n    },\n    \"encore_metrics\": {\n      \"name\": \"Encore Metrics\",\n      \"categoryId\": 4,\n      \"url\": \"http://sitecompass.com\",\n      \"companyId\": \"flashtalking\"\n    },\n    \"enecto_analytics\": {\n      \"name\": \"Enecto Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.enecto.com/en/\",\n      \"companyId\": \"enecto\"\n    },\n    \"engage_sciences\": {\n      \"name\": \"Engage Sciences\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.engagesciences.com/\",\n      \"companyId\": \"engagesciences\"\n    },\n    \"engageya_widget\": {\n      \"name\": \"Engageya Widget\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.engageya.com/home/\",\n      \"companyId\": \"engageya\"\n    },\n    \"engagio\": {\n      \"name\": \"Engagio\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.engagio.com/\",\n      \"companyId\": \"engagio\"\n    },\n    \"engineseeker\": {\n      \"name\": \"EngineSeeker\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.engineseeker.com/\",\n      \"companyId\": \"engineseeker\"\n    },\n    \"enquisite\": {\n      \"name\": \"Enquisite\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.enquisite.com/\",\n      \"companyId\": \"inboundwriter\"\n    },\n    \"enreach\": {\n      \"name\": \"Enreach\",\n      \"categoryId\": 4,\n      \"url\": \"https://enreach.me/\",\n      \"companyId\": \"enreach\"\n    },\n    \"ensemble\": {\n      \"name\": \"Ensemble\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tumri.com\",\n      \"companyId\": \"ensemble\"\n    },\n    \"ensighten\": {\n      \"name\": \"Ensighten\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.ensighten.com\",\n      \"companyId\": \"ensighten\"\n    },\n    \"envolve\": {\n      \"name\": \"Envolve\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.envolve.com/\",\n      \"companyId\": \"envolve\"\n    },\n    \"envybox\": {\n      \"name\": \"Envybox\",\n      \"categoryId\": 2,\n      \"url\": \"https://envybox.io/\",\n      \"companyId\": \"envybox\"\n    },\n    \"eperflex\": {\n      \"name\": \"Eperflex\",\n      \"categoryId\": 4,\n      \"url\": \"https://eperflex.com/\",\n      \"companyId\": \"ividence\"\n    },\n    \"epic_game_ads\": {\n      \"name\": \"Epic Game Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.epicgameads.com/\",\n      \"companyId\": \"epic_game_ads\"\n    },\n    \"epic_marketplace\": {\n      \"name\": \"Epic Marketplace\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.trafficmarketplace.com/\",\n      \"companyId\": \"epic_advertising\"\n    },\n    \"epom\": {\n      \"name\": \"Epom\",\n      \"categoryId\": 4,\n      \"url\": \"http://epom.com/\",\n      \"companyId\": \"epom\"\n    },\n    \"epoq\": {\n      \"name\": \"epoq\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.epoq.de/\",\n      \"companyId\": \"epoq\"\n    },\n    \"eprice\": {\n      \"name\": \"ePrice\",\n      \"categoryId\": 4,\n      \"url\": \"http://banzaiadv.it/\",\n      \"companyId\": \"eprice\"\n    },\n    \"eproof\": {\n      \"name\": \"eProof\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.eproof.com/\",\n      \"companyId\": \"eproof\"\n    },\n    \"eqs_group\": {\n      \"name\": \"EQS Group\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.eqs.com/\",\n      \"companyId\": \"eqs_group\"\n    },\n    \"eqworks\": {\n      \"name\": \"EQWorks\",\n      \"categoryId\": 4,\n      \"url\": \"http://eqads.com\",\n      \"companyId\": \"eq_works\"\n    },\n    \"eroadvertising\": {\n      \"name\": \"EroAdvertising\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.ero-advertising.com/\",\n      \"companyId\": \"ero_advertising\"\n    },\n    \"errorception\": {\n      \"name\": \"Errorception\",\n      \"categoryId\": 6,\n      \"url\": \"http://errorception.com/\",\n      \"companyId\": \"errorception\"\n    },\n    \"eshopcomp.com\": {\n      \"name\": \"eshopcomp.com\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"espn_cdn\": {\n      \"name\": \"ESPN CDN\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.espn.com/\",\n      \"companyId\": \"disney\"\n    },\n    \"esprit.de\": {\n      \"name\": \"esprit.de\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"estat\": {\n      \"name\": \"eStat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.mediametrie-estat.com/\",\n      \"companyId\": \"mediametrie\"\n    },\n    \"etag\": {\n      \"name\": \"etag\",\n      \"categoryId\": 4,\n      \"url\": \"http://etagdigital.com.br/\",\n      \"companyId\": \"etag\"\n    },\n    \"etahub.com\": {\n      \"name\": \"etahub.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"etarget\": {\n      \"name\": \"Etarget\",\n      \"categoryId\": 4,\n      \"url\": \"http://etargetnet.com/\",\n      \"companyId\": \"etarget\"\n    },\n    \"ethnio\": {\n      \"name\": \"Ethnio\",\n      \"categoryId\": 4,\n      \"url\": \"http://ethn.io/\",\n      \"companyId\": \"ethnio\"\n    },\n    \"etology\": {\n      \"name\": \"Etology\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.etology.com\",\n      \"companyId\": \"etology\"\n    },\n    \"etp\": {\n      \"name\": \"ETP\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.etpgroup.com\",\n      \"companyId\": \"etp\"\n    },\n    \"etracker\": {\n      \"name\": \"etracker\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.etracker.com/en/\",\n      \"companyId\": \"etracker_gmbh\"\n    },\n    \"etrigue\": {\n      \"name\": \"eTrigue\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.etrigue.com/\",\n      \"companyId\": \"etrigue\"\n    },\n    \"etsystatic\": {\n      \"name\": \"Etsy CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.etsy.com/\",\n      \"companyId\": \"etsy\"\n    },\n    \"eulerian\": {\n      \"name\": \"Eulerian\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.eulerian.com/\",\n      \"companyId\": \"eulerian\"\n    },\n    \"euroads\": {\n      \"name\": \"Euroads\",\n      \"categoryId\": 4,\n      \"url\": \"http://euroads.com/en/\",\n      \"companyId\": \"euroads\"\n    },\n    \"europecash\": {\n      \"name\": \"Europecash\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.europacash.com/\",\n      \"companyId\": \"europacash\"\n    },\n    \"euroweb_counter\": {\n      \"name\": \"Euroweb Counter\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.euroweb.de/\",\n      \"companyId\": \"euroweb\"\n    },\n    \"evergage.com\": {\n      \"name\": \"Evergage\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.evergage.com\",\n      \"companyId\": \"evergage\"\n    },\n    \"everstring\": {\n      \"name\": \"Everstring\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.everstring.com/\",\n      \"companyId\": \"everstring\"\n    },\n    \"everyday_health\": {\n      \"name\": \"Everyday Health\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.everydayhealth.com/\",\n      \"companyId\": \"everyday_health\"\n    },\n    \"evidon\": {\n      \"name\": \"Evidon\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.evidon.com/\",\n      \"companyId\": \"crownpeak\"\n    },\n    \"evisit_analyst\": {\n      \"name\": \"eVisit Analyst\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.evisitanalyst.com\",\n      \"companyId\": \"evisit_analyst\"\n    },\n    \"exact_drive\": {\n      \"name\": \"Exact Drive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.exactdrive.com/\",\n      \"companyId\": \"exact_drive\"\n    },\n    \"exactag\": {\n      \"name\": \"Exactag\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.exactag.com\",\n      \"companyId\": \"exactag\"\n    },\n    \"exelate\": {\n      \"name\": \"eXelate\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.exelate.com/\",\n      \"companyId\": \"nielsen\"\n    },\n    \"exitjunction\": {\n      \"name\": \"ExitJunction\",\n      \"categoryId\": 4,\n      \"url\": \"https://secure.exitjunction.com\",\n      \"companyId\": \"exitjunction\"\n    },\n    \"exoclick\": {\n      \"name\": \"ExoClick\",\n      \"categoryId\": 3,\n      \"url\": \"http://exoclick.com/\",\n      \"companyId\": \"exoclick\"\n    },\n    \"exoticads.com\": {\n      \"name\": \"exoticads\",\n      \"categoryId\": 3,\n      \"url\": \"https://exoticads.com/welcome/\",\n      \"companyId\": null\n    },\n    \"expedia\": {\n      \"name\": \"Expedia\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.trvl-px.com/\",\n      \"companyId\": \"iac_apps\"\n    },\n    \"experian\": {\n      \"name\": \"Experian\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.experian.com/\",\n      \"companyId\": \"experian_inc\"\n    },\n    \"experian_marketing_services\": {\n      \"name\": \"Experian Marketing Services\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.experian.com/\",\n      \"companyId\": \"experian_inc\"\n    },\n    \"expo-max\": {\n      \"name\": \"expo-MAX\",\n      \"categoryId\": 4,\n      \"url\": \"http://expo-max.com/\",\n      \"companyId\": \"expo-max\"\n    },\n    \"expose_box\": {\n      \"name\": \"Expose Box\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.exposebox.com/\",\n      \"companyId\": \"expose_box\"\n    },\n    \"expose_box_widgets\": {\n      \"name\": \"Expose Box Widgets\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.exposebox.com/\",\n      \"companyId\": \"expose_box\"\n    },\n    \"express.co.uk\": {\n      \"name\": \"express.co.uk\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.express.co.uk/\",\n      \"companyId\": null\n    },\n    \"expressvpn\": {\n      \"name\": \"ExpressVPN\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.expressvpn.com/\",\n      \"companyId\": \"expressvpn\"\n    },\n    \"extreme_tracker\": {\n      \"name\": \"eXTReMe Tracker\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.extremetracking.com/\",\n      \"companyId\": \"extreme_digital\"\n    },\n    \"eye_newton\": {\n      \"name\": \"Eye Newton\",\n      \"categoryId\": 2,\n      \"url\": \"http://eyenewton.ru/\",\n      \"companyId\": \"eyenewton\"\n    },\n    \"eyeota\": {\n      \"name\": \"Eyeota\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.eyeota.com/\",\n      \"companyId\": \"eyeota\"\n    },\n    \"eyereturnmarketing\": {\n      \"name\": \"Eyereturn Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"https://eyereturnmarketing.com/\",\n      \"companyId\": \"torstar_corp\"\n    },\n    \"eyeview\": {\n      \"name\": \"Eyeview\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.eyeviewdigital.com/\",\n      \"companyId\": \"eyeview\"\n    },\n    \"ezakus\": {\n      \"name\": \"Ezakus\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ezakus.com/\",\n      \"companyId\": \"np6\"\n    },\n    \"f11-ads.com\": {\n      \"name\": \"Factor Eleven\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"facebook\": {\n      \"name\": \"Facebook\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.facebook.com\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_audience\": {\n      \"name\": \"Facebook Audience Network\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.facebook.com/business/products/audience-network\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_beacon\": {\n      \"name\": \"Facebook Beacon\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.facebook.com/beacon/faq.php\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_cdn\": {\n      \"name\": \"Facebook CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.facebook.com\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_connect\": {\n      \"name\": \"Facebook Connect\",\n      \"categoryId\": 6,\n      \"url\": \"https://developers.facebook.com/connect.php\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_conversion_tracking\": {\n      \"name\": \"Facebook Conversion Tracking\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.facebook.com/\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_custom_audience\": {\n      \"name\": \"Facebook Custom Audience\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.facebook.com\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_graph\": {\n      \"name\": \"Facebook Social Graph\",\n      \"categoryId\": 7,\n      \"url\": \"https://developers.facebook.com/docs/reference/api/\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_impressions\": {\n      \"name\": \"Facebook Impressions\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.facebook.com/\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facebook_social_plugins\": {\n      \"name\": \"Facebook Social Plugins\",\n      \"categoryId\": 7,\n      \"url\": \"https://developers.facebook.com/plugins\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"facetz.dca\": {\n      \"name\": \"Facetz.DCA\",\n      \"categoryId\": 4,\n      \"url\": \"http://facetz.net\",\n      \"companyId\": \"dca\"\n    },\n    \"facilitate_digital\": {\n      \"name\": \"Facilitate Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.facilitatedigital.com/\",\n      \"companyId\": \"adslot\"\n    },\n    \"faktor.io\": {\n      \"name\": \"faktor.io\",\n      \"categoryId\": 6,\n      \"url\": \"https://faktor.io/\",\n      \"companyId\": \"faktor.io\"\n    },\n    \"fancy_widget\": {\n      \"name\": \"Fancy Widget\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.thefancy.com/\",\n      \"companyId\": \"fancy\"\n    },\n    \"fanplayr\": {\n      \"name\": \"Fanplayr\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.fanplayr.com/\",\n      \"companyId\": \"fanplayr\"\n    },\n    \"fap.to\": {\n      \"name\": \"Imagefap\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"farlight_pte_ltd\": {\n      \"name\": \"Farlight Pte Ltd.\",\n      \"categoryId\": 8,\n      \"url\": \"https://farlightgames.com/\",\n      \"companyId\": \"farlight\",\n      \"source\": \"AdGuard\"\n    },\n    \"fastly_insights\": {\n      \"name\": \"Fastly Insights\",\n      \"categoryId\": 6,\n      \"url\": \"https://insights.fastlylabs.com/\",\n      \"companyId\": \"fastly\"\n    },\n    \"fastlylb.net\": {\n      \"name\": \"Fastly\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.fastly.com/\",\n      \"companyId\": \"fastly\"\n    },\n    \"fastpic.ru\": {\n      \"name\": \"FastPic\",\n      \"categoryId\": 10,\n      \"url\": \"http://fastpic.ru/\",\n      \"companyId\": \"fastpic\"\n    },\n    \"federated_media\": {\n      \"name\": \"Federated Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.federatedmedia.net/\",\n      \"companyId\": \"hyfn\"\n    },\n    \"feedbackify\": {\n      \"name\": \"Feedbackify\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.feedbackify.com/\",\n      \"companyId\": \"feedbackify\"\n    },\n    \"feedburner.com\": {\n      \"name\": \"FeedBurner\",\n      \"categoryId\": 4,\n      \"url\": \"https://feedburner.com\",\n      \"companyId\": \"google\"\n    },\n    \"feedify\": {\n      \"name\": \"Feedify\",\n      \"categoryId\": 7,\n      \"url\": \"http://feedify.de/\",\n      \"companyId\": \"feedify\"\n    },\n    \"feedjit\": {\n      \"name\": \"Feedjit\",\n      \"categoryId\": 4,\n      \"url\": \"http://feedjit.com/\",\n      \"companyId\": \"feedjit\"\n    },\n    \"feedperfect\": {\n      \"name\": \"FeedPerfect\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.feedperfect.com/\",\n      \"companyId\": \"feedperfect\"\n    },\n    \"feedsportal\": {\n      \"name\": \"Feedsportal\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mediafed.com/\",\n      \"companyId\": \"mediafed\"\n    },\n    \"feefo\": {\n      \"name\": \"Feefo\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.feefo.com/web/en/us/\",\n      \"companyId\": \"feefo\"\n    },\n    \"fidelity_media\": {\n      \"name\": \"Fidelity Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://fidelity-media.com/\",\n      \"companyId\": \"fidelity_media\"\n    },\n    \"fiksu\": {\n      \"name\": \"Fiksu\",\n      \"categoryId\": 4,\n      \"url\": \"https://fiksu.com/\",\n      \"companyId\": \"noosphere\"\n    },\n    \"filament.io\": {\n      \"name\": \"Filament.io\",\n      \"categoryId\": 4,\n      \"url\": \"http://sharethis.com/\",\n      \"companyId\": \"sharethis\"\n    },\n    \"fileserve\": {\n      \"name\": \"FileServe\",\n      \"categoryId\": 10,\n      \"url\": \"http://fileserve.com/\",\n      \"companyId\": \"fileserve\"\n    },\n    \"financeads\": {\n      \"name\": \"FinanceADs\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.financeads.net/\",\n      \"companyId\": \"financeads_gmbh_&_co._kg\"\n    },\n    \"financial_content\": {\n      \"name\": \"Financial Content\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.financialcontent.com\",\n      \"companyId\": \"financial_content\"\n    },\n    \"findizer.fr\": {\n      \"name\": \"Findizer\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.findizer.fr/\",\n      \"companyId\": null\n    },\n    \"findologic.com\": {\n      \"name\": \"Findologic\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.findologic.com/\",\n      \"companyId\": \"findologic\"\n    },\n    \"firebase\": {\n      \"name\": \"Firebase\",\n      \"categoryId\": 101,\n      \"url\": \"https://firebase.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"firebaseio.com\": {\n      \"name\": \"Firebase\",\n      \"categoryId\": 8,\n      \"url\": \"https://firebase.google.com/\",\n      \"companyId\": \"google\"\n    },\n    \"first_impression\": {\n      \"name\": \"First Impression\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.firstimpression.io\",\n      \"companyId\": \"first_impression\"\n    },\n    \"fit_analytics\": {\n      \"name\": \"Fit Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.fitanalytics.com/\",\n      \"companyId\": \"fit_analytics\"\n    },\n    \"fivetran\": {\n      \"name\": \"Fivetran\",\n      \"categoryId\": 6,\n      \"url\": \"https://fivetran.com/\",\n      \"companyId\": \"fivetran\"\n    },\n    \"flag_ads\": {\n      \"name\": \"Flag Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.flagads.net/\",\n      \"companyId\": \"flag_ads\"\n    },\n    \"flag_counter\": {\n      \"name\": \"Flag Counter\",\n      \"categoryId\": 4,\n      \"url\": \"http://flagcounter.com/\",\n      \"companyId\": \"flag_counter\"\n    },\n    \"flash\": {\n      \"name\": \"Flash\",\n      \"categoryId\": 0,\n      \"url\": \"https://flashnews.com.au/\",\n      \"companyId\": \"news_corp\",\n      \"source\": \"AdGuard\"\n    },\n    \"flashtalking\": {\n      \"name\": \"Flashtalking\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.flashtalking.com/\",\n      \"companyId\": \"flashtalking\"\n    },\n    \"flattr_button\": {\n      \"name\": \"Flattr Button\",\n      \"categoryId\": 7,\n      \"url\": \"http://flattr.com/\",\n      \"companyId\": \"flattr\"\n    },\n    \"flexoffers\": {\n      \"name\": \"FlexOffers\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.flexoffers.com/\",\n      \"companyId\": \"flexoffers.com\"\n    },\n    \"flickr_badge\": {\n      \"name\": \"Flickr Badge\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.flickr.com/\",\n      \"companyId\": \"smugmug\"\n    },\n    \"flipboard\": {\n      \"name\": \"Flipboard\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.flipboard.com/\",\n      \"companyId\": \"flipboard\"\n    },\n    \"flite\": {\n      \"name\": \"Flite\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.flite.com/\",\n      \"companyId\": \"flite\"\n    },\n    \"flixcdn.com\": {\n      \"name\": \"flixcdn.com\",\n      \"categoryId\": 9,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"flixmedia\": {\n      \"name\": \"Flixmedia\",\n      \"categoryId\": 8,\n      \"url\": \"https://flixmedia.eu\",\n      \"companyId\": \"flixmedia\"\n    },\n    \"flocktory.com\": {\n      \"name\": \"Flocktory\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.flocktory.com/\",\n      \"companyId\": \"flocktory\"\n    },\n    \"flowplayer\": {\n      \"name\": \"Flowplayer\",\n      \"categoryId\": 4,\n      \"url\": \"https://flowplayer.org/\",\n      \"companyId\": \"flowplayer\"\n    },\n    \"fluct\": {\n      \"name\": \"Fluct\",\n      \"categoryId\": 4,\n      \"url\": \"https://corp.fluct.jp/\",\n      \"companyId\": \"fluct\"\n    },\n    \"fluent\": {\n      \"name\": \"Fluent\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.fluentco.com/\",\n      \"companyId\": \"fluent\"\n    },\n    \"fluid\": {\n      \"name\": \"Fluid\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.8thbridge.com/\",\n      \"companyId\": \"fluid\"\n    },\n    \"fluidads\": {\n      \"name\": \"FluidAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.fluidads.co/\",\n      \"companyId\": \"fluidads\"\n    },\n    \"fluidsurveys\": {\n      \"name\": \"FluidSurveys\",\n      \"categoryId\": 2,\n      \"url\": \"http://fluidsurveys.com/\",\n      \"companyId\": \"fluidware\"\n    },\n    \"flurry\": {\n      \"name\": \"Flurry\",\n      \"categoryId\": 101,\n      \"url\": \"http://www.flurry.com/\",\n      \"companyId\": \"apollo_global_management\",\n      \"source\": \"AdGuard\"\n    },\n    \"flxone\": {\n      \"name\": \"FLXONE\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.flxone.com/\",\n      \"companyId\": \"flxone\"\n    },\n    \"flyertown\": {\n      \"name\": \"Flyertown\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.flyertown.ca/\",\n      \"companyId\": \"flyertown\"\n    },\n    \"fmadserving\": {\n      \"name\": \"FMAdserving\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.fmadserving.dk/\",\n      \"companyId\": \"fm_adserving\"\n    },\n    \"fonbet\": {\n      \"name\": \"Fonbet\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.fonbet.ru\",\n      \"companyId\": \"fonbet\"\n    },\n    \"fonecta\": {\n      \"name\": \"Fonecta\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.fonecta.com/\",\n      \"companyId\": \"fonecta\"\n    },\n    \"fontawesome_com\": {\n      \"name\": \"fontawesome.com\",\n      \"categoryId\": 9,\n      \"url\": \"http://fontawesome.com/\",\n      \"companyId\": null\n    },\n    \"foodie_blogroll\": {\n      \"name\": \"Foodie Blogroll\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.foodieblogroll.com\",\n      \"companyId\": \"foodie_blogroll\"\n    },\n    \"footprint\": {\n      \"name\": \"Footprint\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.footprintlive.com/\",\n      \"companyId\": \"opentracker\"\n    },\n    \"footprintdns.com\": {\n      \"name\": \"Footprint DNS\",\n      \"categoryId\": 11,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"forcetrac\": {\n      \"name\": \"ForceTrac\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.forcetrac.com/\",\n      \"companyId\": \"force_marketing\"\n    },\n    \"forensiq\": {\n      \"name\": \"Forensiq\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cpadetective.com/\",\n      \"companyId\": \"impact\"\n    },\n    \"foresee\": {\n      \"name\": \"ForeSee\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.foresee.com/\",\n      \"companyId\": \"foresee_results\"\n    },\n    \"formisimo\": {\n      \"name\": \"Formisimo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.formisimo.com/\",\n      \"companyId\": \"formisimo\"\n    },\n    \"forter\": {\n      \"name\": \"Forter\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.forter.com/\",\n      \"companyId\": \"forter\"\n    },\n    \"fortlachanhecksof.info\": {\n      \"name\": \"fortlachanhecksof.info\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"foursquare_widget\": {\n      \"name\": \"Foursquare Widget\",\n      \"categoryId\": 4,\n      \"url\": \"https://foursquare.com/\",\n      \"companyId\": \"foursquare\"\n    },\n    \"fout.jp\": {\n      \"name\": \"FreakOut\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.fout.co.jp/\",\n      \"companyId\": \"freakout\"\n    },\n    \"fox_audience_network\": {\n      \"name\": \"Fox Audience Network\",\n      \"categoryId\": 4,\n      \"url\": \"https://publishers.foxaudiencenetwork.com/\",\n      \"companyId\": \"fox_audience_network\"\n    },\n    \"fox_sports\": {\n      \"name\": \"Fox Sports\",\n      \"categoryId\": 0,\n      \"url\": \"https://foxsports.com.au/\",\n      \"companyId\": \"news_corp\",\n      \"source\": \"AdGuard\"\n    },\n    \"foxnews_static\": {\n      \"name\": \"Fox News CDN\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.foxnews.com/\",\n      \"companyId\": \"fox_news\"\n    },\n    \"foxpush\": {\n      \"name\": \"FoxPush\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.foxpush.com/\",\n      \"companyId\": \"foxpush\"\n    },\n    \"foxtel\": {\n      \"name\": \"Foxtel\",\n      \"categoryId\": 0,\n      \"url\": \"https://foxtel.com.au/\",\n      \"companyId\": \"news_corp\",\n      \"source\": \"AdGuard\"\n    },\n    \"foxydeal_com\": {\n      \"name\": \"foxydeal.com\",\n      \"categoryId\": 12,\n      \"url\": \"https://www.foxydeal.de\",\n      \"companyId\": null\n    },\n    \"fraudlogix\": {\n      \"name\": \"FraudLogix\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.fraudlogix.com/\",\n      \"companyId\": null\n    },\n    \"free_counter\": {\n      \"name\": \"Free Counter\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.statcounterfree.com/\",\n      \"companyId\": \"free_counter\"\n    },\n    \"free_online_users\": {\n      \"name\": \"Free Online Users\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.freeonlineusers.com\",\n      \"companyId\": \"free_online_users\"\n    },\n    \"free_pagerank\": {\n      \"name\": \"Free PageRank\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.free-pagerank.com/\",\n      \"companyId\": \"free_pagerank\"\n    },\n    \"freedom_mortgage\": {\n      \"name\": \"Freedom Mortgage\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.freedommortgage.com/\",\n      \"companyId\": \"freedom_mortgage\"\n    },\n    \"freegeoip_net\": {\n      \"name\": \"freegeoip.net\",\n      \"categoryId\": 6,\n      \"url\": \"http://freegeoip.net/\",\n      \"companyId\": null\n    },\n    \"freenet_de\": {\n      \"name\": \"freenet.de\",\n      \"categoryId\": 4,\n      \"url\": \"http://freenet.de/\",\n      \"companyId\": \"debitel\"\n    },\n    \"freeview\": {\n      \"name\": \"Freeview\",\n      \"categoryId\": 0,\n      \"url\": \"https://freeview.com.au/\",\n      \"companyId\": \"freeview\",\n      \"source\": \"AdGuard\"\n    },\n    \"freewheel\": {\n      \"name\": \"FreeWheel\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.freewheel.tv/\",\n      \"companyId\": \"comcast\"\n    },\n    \"fresh8\": {\n      \"name\": \"Fresh8\",\n      \"categoryId\": 6,\n      \"url\": \"http://fresh8gaming.com/\",\n      \"companyId\": \"fresh_8_gaming\"\n    },\n    \"freshdesk\": {\n      \"name\": \"Freshdesk\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.freshdesk.com\",\n      \"companyId\": \"freshdesk\"\n    },\n    \"freshplum\": {\n      \"name\": \"Freshplum\",\n      \"categoryId\": 4,\n      \"url\": \"https://freshplum.com/\",\n      \"companyId\": \"freshplum\"\n    },\n    \"friendbuy\": {\n      \"name\": \"FriendBuy\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.friendbuy.com\",\n      \"companyId\": \"friendbuy\"\n    },\n    \"friendfeed\": {\n      \"name\": \"FriendFeed\",\n      \"categoryId\": 7,\n      \"url\": \"http://friendfeed.com/\",\n      \"companyId\": \"facebook\"\n    },\n    \"friendfinder_network\": {\n      \"name\": \"FriendFinder Network\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.ffn.com/\",\n      \"companyId\": \"friendfinder_networks\"\n    },\n    \"frosmo_optimizer\": {\n      \"name\": \"Frosmo Optimizer\",\n      \"categoryId\": 4,\n      \"url\": \"http://frosmo.com/\",\n      \"companyId\": \"frosmo\"\n    },\n    \"fruitflan\": {\n      \"name\": \"FruitFlan\",\n      \"categoryId\": 4,\n      \"url\": \"http://flan-tech.com/\",\n      \"companyId\": \"keytiles\"\n    },\n    \"fstrk.net\": {\n      \"name\": \"24metrics Fraudshield\",\n      \"categoryId\": 6,\n      \"url\": \"https://24metrics.com/\",\n      \"companyId\": \"24metrics\"\n    },\n    \"fuelx\": {\n      \"name\": \"FuelX\",\n      \"categoryId\": 4,\n      \"url\": \"http://fuelx.com/\",\n      \"companyId\": \"fuelx\"\n    },\n    \"fullstory\": {\n      \"name\": \"FullStory\",\n      \"categoryId\": 6,\n      \"url\": \"http://fullstory.com\",\n      \"companyId\": \"fullstory\"\n    },\n    \"funnelytics\": {\n      \"name\": \"Funnelytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://funnelytics.io/\",\n      \"companyId\": \"funnelytics\"\n    },\n    \"fyber\": {\n      \"name\": \"Fyber\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.fyber.com/\",\n      \"companyId\": \"fyber\"\n    },\n    \"ga_audiences\": {\n      \"name\": \"GA Audiences\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"game_advertising_online\": {\n      \"name\": \"Game Advertising Online\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.game-advertising-online.com/\",\n      \"companyId\": \"game_advertising_online\"\n    },\n    \"gameanalytics\": {\n      \"name\": \"GameAnalytics\",\n      \"categoryId\": 101,\n      \"url\": \"https://gameanalytics.com/\",\n      \"companyId\": \"mobvista\",\n      \"source\": \"AdGuard\"\n    },\n    \"gamedistribution.com\": {\n      \"name\": \"Gamedistribution.com\",\n      \"categoryId\": 8,\n      \"url\": \"http://gamedistribution.com/\",\n      \"companyId\": null\n    },\n    \"gamerdna\": {\n      \"name\": \"gamerDNA\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.gamerdnamedia.com/\",\n      \"companyId\": \"gamerdna_media\"\n    },\n    \"gannett\": {\n      \"name\": \"Gannett Media\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.gannett.com/\",\n      \"companyId\": \"gannett_digital_media_network\"\n    },\n    \"gaug.es\": {\n      \"name\": \"Gaug.es\",\n      \"categoryId\": 6,\n      \"url\": \"http://get.gaug.es/\",\n      \"companyId\": \"euroweb\"\n    },\n    \"gazprom-media_digital\": {\n      \"name\": \"Gazprom-Media Digital\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.gpm-digital.com/\",\n      \"companyId\": \"gazprom-media_digital\"\n    },\n    \"gb-world\": {\n      \"name\": \"GB-World\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.gb-world.net/\",\n      \"companyId\": \"gb-world\"\n    },\n    \"gdeslon\": {\n      \"name\": \"GdeSlon\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.gdeslon.ru/\",\n      \"companyId\": \"gdeslon\"\n    },\n    \"gdm_digital\": {\n      \"name\": \"GDM Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.gdmdigital.com/\",\n      \"companyId\": \"ve_interactive\"\n    },\n    \"geeen\": {\n      \"name\": \"Geeen\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.geeen.co.jp/\",\n      \"companyId\": \"geeen\"\n    },\n    \"gemius\": {\n      \"name\": \"Gemius\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.gemius.com\",\n      \"companyId\": \"gemius_sa\"\n    },\n    \"generaltracking_de\": {\n      \"name\": \"generaltracking.de\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"genesis\": {\n      \"name\": \"Genesis\",\n      \"categoryId\": 4,\n      \"url\": \"http://genesismedia.com/\",\n      \"companyId\": \"genesis_media\"\n    },\n    \"geniee\": {\n      \"name\": \"GENIEE\",\n      \"categoryId\": 4,\n      \"url\": \"http://geniee.co.jp/\",\n      \"companyId\": null\n    },\n    \"genius\": {\n      \"name\": \"Genius\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.genius.com/\",\n      \"companyId\": \"genius\"\n    },\n    \"genoo\": {\n      \"name\": \"Genoo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.genoo.com/\",\n      \"companyId\": \"genoo\"\n    },\n    \"geoads\": {\n      \"name\": \"GeoAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.geoads.com\",\n      \"companyId\": \"geoads\"\n    },\n    \"geolify\": {\n      \"name\": \"Geolify\",\n      \"categoryId\": 4,\n      \"url\": \"http://geolify.com/\",\n      \"companyId\": \"geolify\"\n    },\n    \"geoplugin\": {\n      \"name\": \"geoPlugin\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.geoplugin.com/\",\n      \"companyId\": \"geoplugin\"\n    },\n    \"geotrust\": {\n      \"name\": \"GeoTrust\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.geotrust.com/\",\n      \"companyId\": \"symantec\"\n    },\n    \"geovisite\": {\n      \"name\": \"Geovisite\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.geovisite.com/\",\n      \"companyId\": \"geovisite\"\n    },\n    \"gestionpub\": {\n      \"name\": \"GestionPub\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.gestionpub.com/\",\n      \"companyId\": \"gestionpub\"\n    },\n    \"get_response\": {\n      \"name\": \"Get Response\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.getresponse.com/?marketing_gv=v2\",\n      \"companyId\": \"getresponse\"\n    },\n    \"get_site_control\": {\n      \"name\": \"Get Site Control\",\n      \"categoryId\": 4,\n      \"url\": \"https://getsitecontrol.com/\",\n      \"companyId\": \"getsitecontrol\"\n    },\n    \"getconversion\": {\n      \"name\": \"GetConversion\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.getconversion.net/\",\n      \"companyId\": \"getconversion\"\n    },\n    \"getglue\": {\n      \"name\": \"GetGlue\",\n      \"categoryId\": 0,\n      \"url\": \"http://getglue.com\",\n      \"companyId\": \"telfie\"\n    },\n    \"getintent\": {\n      \"name\": \"GetIntent\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.getintent.com/\",\n      \"companyId\": \"getintent\"\n    },\n    \"getkudos\": {\n      \"name\": \"GetKudos\",\n      \"categoryId\": 1,\n      \"url\": \"https://www.getkudos.me/\",\n      \"companyId\": \"zendesk\"\n    },\n    \"getmyad\": {\n      \"name\": \"GetMyAd\",\n      \"categoryId\": 4,\n      \"url\": \"http://yottos.com\",\n      \"companyId\": \"yottos\"\n    },\n    \"getsatisfaction\": {\n      \"name\": \"GetSatisfaction\",\n      \"categoryId\": 1,\n      \"url\": \"http://getsatisfaction.com/\",\n      \"companyId\": \"get_satisfaction\"\n    },\n    \"gettyimages\": {\n      \"name\": \"Getty Images\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.gettyimages.com/\",\n      \"companyId\": null\n    },\n    \"gfk\": {\n      \"name\": \"GfK\",\n      \"categoryId\": 4,\n      \"url\": \"http://nurago.com/\",\n      \"companyId\": \"gfk_nurago\"\n    },\n    \"gfycat.com\": {\n      \"name\": \"gfycat\",\n      \"categoryId\": 7,\n      \"url\": \"https://gfycat.com/\",\n      \"companyId\": null\n    },\n    \"giant_realm\": {\n      \"name\": \"Giant Realm\",\n      \"categoryId\": 4,\n      \"url\": \"http://corp.giantrealm.com/\",\n      \"companyId\": \"giant_realm\"\n    },\n    \"giantmedia\": {\n      \"name\": \"GiantMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://giantmedia.com/\",\n      \"companyId\": \"adknowledge\"\n    },\n    \"giga\": {\n      \"name\": \"Giga\",\n      \"categoryId\": 4,\n      \"url\": \"https://gigaonclick.com\",\n      \"companyId\": \"giga\"\n    },\n    \"gigya\": {\n      \"name\": \"Gigya\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.sap.com/index.html\",\n      \"companyId\": \"sap\"\n    },\n    \"gigya_beacon\": {\n      \"name\": \"Gigya Beacon\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.gigya.com\",\n      \"companyId\": \"sap\"\n    },\n    \"gigya_socialize\": {\n      \"name\": \"Gigya Socialize\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.gigya.com\",\n      \"companyId\": \"sap\"\n    },\n    \"gigya_toolbar\": {\n      \"name\": \"Gigya Toolbar\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.gigya.com/\",\n      \"companyId\": \"sap\"\n    },\n    \"giosg\": {\n      \"name\": \"Giosg\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.giosg.com/\",\n      \"companyId\": \"giosg\"\n    },\n    \"giphy.com\": {\n      \"name\": \"Giphy\",\n      \"categoryId\": 7,\n      \"url\": \"https://giphy.com/\",\n      \"companyId\": null\n    },\n    \"giraff.io\": {\n      \"name\": \"Giraff.io\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.giraff.io/\",\n      \"companyId\": null\n    },\n    \"github\": {\n      \"name\": \"GitHub, Inc.\",\n      \"categoryId\": 2,\n      \"url\": \"https://github.com/\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"github_apps\": {\n      \"name\": \"GitHub Apps\",\n      \"categoryId\": 2,\n      \"url\": \"https://github.com/\",\n      \"companyId\": \"github\"\n    },\n    \"github_pages\": {\n      \"name\": \"Github Pages\",\n      \"categoryId\": 10,\n      \"url\": \"https://pages.github.com/\",\n      \"companyId\": \"github\"\n    },\n    \"gittigidiyor_affiliate_program\": {\n      \"name\": \"GittiGidiyor Affiliate Program\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ebay.com/\",\n      \"companyId\": \"ebay\"\n    },\n    \"gittip\": {\n      \"name\": \"Gittip\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.gittip.com/\",\n      \"companyId\": \"gittip\"\n    },\n    \"glad_cube\": {\n      \"name\": \"Glad Cube\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.glad-cube.com/\",\n      \"companyId\": \"glad_cube_inc.\"\n    },\n    \"glganltcs.space\": {\n      \"name\": \"glganltcs.space\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"global_web_index\": {\n      \"name\": \"GlobalWebIndex\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.globalwebindex.com/\",\n      \"companyId\": \"global_web_index\"\n    },\n    \"globalnotifier.com\": {\n      \"name\": \"globalnotifier.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"globalsign\": {\n      \"name\": \"GlobalSign\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"globaltakeoff\": {\n      \"name\": \"GlobalTakeoff\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.globaltakeoff.net/\",\n      \"companyId\": \"globaltakeoff\"\n    },\n    \"glomex.com\": {\n      \"name\": \"Glomex\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.glomex.com/\",\n      \"companyId\": \"glomex\"\n    },\n    \"glotgrx.com\": {\n      \"name\": \"glotgrx.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"gm_delivery\": {\n      \"name\": \"GM Delivery\",\n      \"categoryId\": 4,\n      \"url\": \"http://a.gmdelivery.com/\",\n      \"companyId\": \"gm_delivery\"\n    },\n    \"gmail\": {\n      \"name\": \"Gmail\",\n      \"categoryId\": 13,\n      \"url\": \"https://mail.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"gmo\": {\n      \"name\": \"GMO\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.gmo.media/\",\n      \"companyId\": \"gmo_media\"\n    },\n    \"gmx_net\": {\n      \"name\": \"gmx.net\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"go.com\": {\n      \"name\": \"go.com\",\n      \"categoryId\": 8,\n      \"url\": \"go.com\",\n      \"companyId\": \"disney\"\n    },\n    \"godaddy_affiliate_program\": {\n      \"name\": \"GoDaddy Affiliate Program\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.godaddy.com/\",\n      \"companyId\": \"godaddy\"\n    },\n    \"godaddy_site_analytics\": {\n      \"name\": \"GoDaddy Site Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.godaddy.com/gdshop/hosting/stats_\",\n      \"companyId\": \"godaddy\"\n    },\n    \"godaddy_site_seal\": {\n      \"name\": \"GoDaddy Site Seal\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.godaddy.com/\",\n      \"companyId\": \"godaddy\"\n    },\n    \"godatafeed\": {\n      \"name\": \"GoDataFeed\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.godatafeed.com\",\n      \"companyId\": \"godatafeed\"\n    },\n    \"goingup\": {\n      \"name\": \"GoingUp\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.goingup.com/\",\n      \"companyId\": \"goingup\"\n    },\n    \"gomez\": {\n      \"name\": \"Gomez\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.gomez.com/\",\n      \"companyId\": \"dynatrace\"\n    },\n    \"goodadvert\": {\n      \"name\": \"GoodADVERT\",\n      \"categoryId\": 4,\n      \"url\": \"http://goodadvert.ru/\",\n      \"companyId\": \"goodadvert\"\n    },\n    \"google\": {\n      \"name\": \"Google\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.google.com/\",\n      \"companyId\": \"google\"\n    },\n    \"google_ads_measurement\": {\n      \"name\": \"Google Ads Measurement\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_adsense\": {\n      \"name\": \"Google Adsense\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.google.com/adsense/\",\n      \"companyId\": \"google\"\n    },\n    \"google_adservices\": {\n      \"name\": \"Google AdServices\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_adwords_conversion\": {\n      \"name\": \"Google AdWords Conversion\",\n      \"categoryId\": 4,\n      \"url\": \"https://adwords.google.com/\",\n      \"companyId\": \"google\"\n    },\n    \"google_adwords_user_lists\": {\n      \"name\": \"Google Adwords User Lists\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_analytics\": {\n      \"name\": \"Google Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.google.com/analytics/\",\n      \"companyId\": \"google\"\n    },\n    \"google_appspot\": {\n      \"name\": \"Google Appspot\",\n      \"categoryId\": 10,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_auth\": {\n      \"name\": \"Google Auth\",\n      \"categoryId\": 2,\n      \"url\": \"https://myaccount.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_beacons\": {\n      \"name\": \"Google Beacons\",\n      \"categoryId\": 6,\n      \"url\": \"https://google.xyz\",\n      \"companyId\": \"google\"\n    },\n    \"google_chat\": {\n      \"name\": \"Google Chat\",\n      \"categoryId\": 7,\n      \"url\": \"https://mail.google.com/chat/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_cloud_platform\": {\n      \"name\": \"Google Cloud Platform\",\n      \"categoryId\": 10,\n      \"url\": \"https://cloud.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_cloud_storage\": {\n      \"name\": \"Google Cloud Storage\",\n      \"categoryId\": 10,\n      \"url\": \"https://cloud.google.com/storage/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_custom_search\": {\n      \"name\": \"Google Custom Search Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://developers.google.com/custom-search-ads/\",\n      \"companyId\": \"google\"\n    },\n    \"google_custom_search_engine\": {\n      \"name\": \"Google Programmable Search Engine\",\n      \"categoryId\": 5,\n      \"url\": \"https://programmablesearchengine.google.com/about/\",\n      \"companyId\": \"google\"\n    },\n    \"google_dns\": {\n      \"name\": \"Google DNS\",\n      \"categoryId\": 10,\n      \"url\": \"https://dns.google/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_domains\": {\n      \"name\": \"Google Domains\",\n      \"categoryId\": 10,\n      \"url\": \"https://domains.google/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_edge\": {\n      \"name\": \"Google Edge CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://peering.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_email\": {\n      \"name\": \"Google Email\",\n      \"categoryId\": 13,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_fonts\": {\n      \"name\": \"Google Fonts\",\n      \"categoryId\": 9,\n      \"url\": \"https://fonts.google.com/\",\n      \"companyId\": \"google\"\n    },\n    \"google_hosted\": {\n      \"name\": \"Google Hosted\",\n      \"categoryId\": 10,\n      \"url\": \"https://workspace.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_ima\": {\n      \"name\": \"Google IMA\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_location\": {\n      \"name\": \"Google Location\",\n      \"categoryId\": 8,\n      \"url\": \"https://patents.google.com/patent/WO2007025143A1/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_maps\": {\n      \"name\": \"Google Maps\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.google.com/maps/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_marketing\": {\n      \"name\": \"Google Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"https://marketingplatform.google.com/about/enterprise\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_meet\": {\n      \"name\": \"Google Meet\",\n      \"categoryId\": 2,\n      \"url\": \"https://meet.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_photos\": {\n      \"name\": \"Google Photos\",\n      \"categoryId\": 9,\n      \"url\": \"https://photos.google.com/\",\n      \"companyId\": \"google\"\n    },\n    \"google_pingback\": {\n      \"name\": \"Google Pingback\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_play\": {\n      \"name\": \"Google Play\",\n      \"categoryId\": 8,\n      \"url\": \"https://play.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_plus\": {\n      \"name\": \"Google+ Platform\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.google.com/+1/button/\",\n      \"companyId\": \"google\"\n    },\n    \"google_publisher_tags\": {\n      \"name\": \"Google Publisher Tags\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_remarketing\": {\n      \"name\": \"Google Dynamic Remarketing\",\n      \"categoryId\": 4,\n      \"url\": \"http://adwords.google.com/\",\n      \"companyId\": \"google\"\n    },\n    \"google_safeframe\": {\n      \"name\": \"Google Safeframe\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_servers\": {\n      \"name\": \"Google Servers\",\n      \"categoryId\": 8,\n      \"url\": \"https://support.google.com/faqs/answer/174717?hl=en\",\n      \"companyId\": \"google\"\n    },\n    \"google_shopping_reviews\": {\n      \"name\": \"Google Shopping Reviews\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_syndication\": {\n      \"name\": \"Google Syndication\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_tag_manager\": {\n      \"name\": \"Google Tag Manager\",\n      \"categoryId\": 5,\n      \"url\": \"https://marketingplatform.google.com/about/tag-manager/\",\n      \"companyId\": \"google\"\n    },\n    \"google_translate\": {\n      \"name\": \"Google Translate\",\n      \"categoryId\": 2,\n      \"url\": \"https://translate.google.com/manager\",\n      \"companyId\": \"google\"\n    },\n    \"google_travel_adds\": {\n      \"name\": \"Google Travel Adds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_trust_services\": {\n      \"name\": \"Google Trust Services\",\n      \"categoryId\": 5,\n      \"url\": \"https://pki.goog/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_trusted_stores\": {\n      \"name\": \"Google Trusted Stores\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_users\": {\n      \"name\": \"Google User Content\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_voice\": {\n      \"name\": \"Google Voice\",\n      \"categoryId\": 2,\n      \"url\": \"https://voice.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"google_website_optimizer\": {\n      \"name\": \"Google Website Optimizer\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.google.com/analytics/siteopt/prev\",\n      \"companyId\": \"google\"\n    },\n    \"google_widgets\": {\n      \"name\": \"Google Widgets\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"google_workspace\": {\n      \"name\": \"Google Workspace\",\n      \"categoryId\": 2,\n      \"url\": \"https://workspace.google.com/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"googleapis.com\": {\n      \"name\": \"Google APIs\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.googleapis.com/\",\n      \"companyId\": \"google\"\n    },\n    \"goooal\": {\n      \"name\": \"Goooal\",\n      \"categoryId\": 6,\n      \"url\": \"http://mailchimp.com/\",\n      \"companyId\": \"mailchimp\"\n    },\n    \"gorilla_nation\": {\n      \"name\": \"Gorilla Nation\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.gorillanationmedia.com\",\n      \"companyId\": \"gorilla_nation_media\"\n    },\n    \"gosquared\": {\n      \"name\": \"GoSquared\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.gosquared.com/livestats/\",\n      \"companyId\": \"gosquared\"\n    },\n    \"gostats\": {\n      \"name\": \"GoStats\",\n      \"categoryId\": 6,\n      \"url\": \"http://gostats.com/\",\n      \"companyId\": \"gostats\"\n    },\n    \"govmetric\": {\n      \"name\": \"GovMetric\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.govmetric.com/\",\n      \"companyId\": \"govmetric\"\n    },\n    \"grabo_affiliate\": {\n      \"name\": \"Grabo Affiliate\",\n      \"categoryId\": 4,\n      \"url\": \"http://grabo.bg/\",\n      \"companyId\": \"grabo_media\"\n    },\n    \"grandslammedia\": {\n      \"name\": \"GrandSlamMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.grandslammedia.com/\",\n      \"companyId\": \"grand_slam_media\"\n    },\n    \"granify\": {\n      \"name\": \"Granify\",\n      \"categoryId\": 6,\n      \"url\": \"http://granify.com/\",\n      \"companyId\": \"granify\"\n    },\n    \"grapeshot\": {\n      \"name\": \"Grapeshot\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.grapeshot.com/\",\n      \"companyId\": \"oracle\"\n    },\n    \"graph_comment\": {\n      \"name\": \"Graph Comment\",\n      \"categoryId\": 5,\n      \"url\": \"https://graphcomment.com/en/\",\n      \"companyId\": \"graph_comment\"\n    },\n    \"gravatar\": {\n      \"name\": \"Gravatar\",\n      \"categoryId\": 7,\n      \"url\": \"http://en.gravatar.com/\",\n      \"companyId\": \"automattic\"\n    },\n    \"gravitec\": {\n      \"name\": \"Gravitec\",\n      \"categoryId\": 6,\n      \"url\": \"https://gravitec.net/\",\n      \"companyId\": \"gravitec\"\n    },\n    \"gravity_insights\": {\n      \"name\": \"Gravity Insights\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.gravity.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"greatviews.de\": {\n      \"name\": \"GreatViews\",\n      \"categoryId\": 4,\n      \"url\": \"http://greatviews.de/\",\n      \"companyId\": \"parship\"\n    },\n    \"green_and_red\": {\n      \"name\": \"Green and Red\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.green-red.com/\",\n      \"companyId\": \"green_&_red_technologies\"\n    },\n    \"green_certified_site\": {\n      \"name\": \"Green Certified Site\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.advenity.com/\",\n      \"companyId\": \"advenity\"\n    },\n    \"green_story\": {\n      \"name\": \"Green Story\",\n      \"categoryId\": 6,\n      \"url\": \"https://greenstory.ca/\",\n      \"companyId\": \"green_story\"\n    },\n    \"greentube.com\": {\n      \"name\": \"Greentube Internet Entertainment Solutions\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.greentube.com/\",\n      \"companyId\": null\n    },\n    \"greystripe\": {\n      \"name\": \"Greystripe\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.greystripe.com/\",\n      \"companyId\": \"conversant\"\n    },\n    \"groove\": {\n      \"name\": \"Groove\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.groovehq.com/\",\n      \"companyId\": \"groove_networks\"\n    },\n    \"groovinads\": {\n      \"name\": \"GroovinAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.groovinads.com/en\",\n      \"companyId\": \"groovinads\"\n    },\n    \"groundtruth\": {\n      \"name\": \"GroundTruth\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.groundtruth.com/\",\n      \"companyId\": \"groundtruth\"\n    },\n    \"groupm_server\": {\n      \"name\": \"GroupM Server\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.groupm.com/\",\n      \"companyId\": \"wpp\"\n    },\n    \"gsi_media\": {\n      \"name\": \"GSI Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://gsimedia.net\",\n      \"companyId\": \"gsi_media_network\"\n    },\n    \"gstatic\": {\n      \"name\": \"Google Static\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.google.com\",\n      \"companyId\": \"google\"\n    },\n    \"gtop\": {\n      \"name\": \"GTop\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.gtopstats.com\",\n      \"companyId\": \"gtopstats\"\n    },\n    \"gugaboo\": {\n      \"name\": \"Gugaboo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.gubagoo.com/\",\n      \"companyId\": \"gubagoo\"\n    },\n    \"guj.de\": {\n      \"name\": \"Gruner + Jahr\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.guj.de/\",\n      \"companyId\": \"gruner_jahr_ag\"\n    },\n    \"gujems\": {\n      \"name\": \"G+J e|MS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.gujmedia.de/\",\n      \"companyId\": \"gruner_jahr_ag\"\n    },\n    \"gumgum\": {\n      \"name\": \"gumgum\",\n      \"categoryId\": 4,\n      \"url\": \"http://gumgum.com/\",\n      \"companyId\": \"gumgum\"\n    },\n    \"gumroad\": {\n      \"name\": \"Gumroad\",\n      \"categoryId\": 7,\n      \"url\": \"https://gumroad.com/\",\n      \"companyId\": \"gumroad\"\n    },\n    \"gunggo\": {\n      \"name\": \"Gunggo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.gunggo.com/\",\n      \"companyId\": \"gunggo\"\n    },\n    \"h12_ads\": {\n      \"name\": \"H12 Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.h12-media.com/\",\n      \"companyId\": \"h12_media_ads\"\n    },\n    \"hacker_news_button\": {\n      \"name\": \"Hacker News Button\",\n      \"categoryId\": 7,\n      \"url\": \"http://news.ycombinator.com/\",\n      \"companyId\": \"hacker_news\"\n    },\n    \"haendlerbund.de\": {\n      \"name\": \"Händlerbund\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.haendlerbund.de/en\",\n      \"companyId\": null\n    },\n    \"halogen_network\": {\n      \"name\": \"Halogen Network\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.halogennetwork.com/\",\n      \"companyId\": \"social_chorus\"\n    },\n    \"happy_fox_chat\": {\n      \"name\": \"Happy Fox Chat\",\n      \"categoryId\": 2,\n      \"url\": \"https://happyfoxchat.com/\",\n      \"companyId\": \"happy_fox_chat\"\n    },\n    \"harren_media\": {\n      \"name\": \"Harren Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.harrenmedia.com/index.html\",\n      \"companyId\": \"harren_media\"\n    },\n    \"hatchbuck\": {\n      \"name\": \"Hatchbuck\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.hatchbuck.com/\",\n      \"companyId\": \"hatchbuck\"\n    },\n    \"head_hunter\": {\n      \"name\": \"Head Hunter\",\n      \"categoryId\": 6,\n      \"url\": \"https://hh.ru/\",\n      \"companyId\": \"head_hunter\"\n    },\n    \"healte.de\": {\n      \"name\": \"healte.de\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"heap\": {\n      \"name\": \"Heap\",\n      \"categoryId\": 6,\n      \"url\": \"https://heapanalytics.com/\",\n      \"companyId\": \"heap\"\n    },\n    \"heatmap\": {\n      \"name\": \"Heatmap\",\n      \"categoryId\": 6,\n      \"url\": \"https://heatmap.me/\",\n      \"companyId\": \"heatmap\"\n    },\n    \"heimspiel\": {\n      \"name\": \"HEIM:SPIEL Medien GmbH\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.heimspiel.de\",\n      \"companyId\": null\n    },\n    \"hello_bar\": {\n      \"name\": \"Hello Bar\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.hellobar.com/\",\n      \"companyId\": \"crazy_egg\"\n    },\n    \"hellosociety\": {\n      \"name\": \"HelloSociety\",\n      \"categoryId\": 6,\n      \"url\": \"http://hellosociety.com\",\n      \"companyId\": \"hellosociety\"\n    },\n    \"here\": {\n      \"name\": \"HERE\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.here.com/\",\n      \"companyId\": null\n    },\n    \"heroku\": {\n      \"name\": \"Heroku\",\n      \"categoryId\": 10,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"heureka-widget\": {\n      \"name\": \"Heureka-Widget\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.heurekashopping.cz/\",\n      \"companyId\": \"heureka\"\n    },\n    \"heybubble\": {\n      \"name\": \"HeyBubble\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.heybubble.com/\",\n      \"companyId\": \"heybubble\"\n    },\n    \"heyos\": {\n      \"name\": \"Heyos\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.heyos.com/\",\n      \"companyId\": \"heyos\"\n    },\n    \"hi-media_performance\": {\n      \"name\": \"Hi-Media Performance\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hi-mediaperformance.co.uk/\",\n      \"companyId\": \"hi-media_performance\"\n    },\n    \"hiconversion\": {\n      \"name\": \"HiConversion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hiconversion.com\",\n      \"companyId\": \"hiconversion\"\n    },\n    \"highwebmedia.com\": {\n      \"name\": \"highwebmedia.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"highwinds\": {\n      \"name\": \"Highwinds\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.highwinds.com/\",\n      \"companyId\": \"highwinds\"\n    },\n    \"hiiir\": {\n      \"name\": \"Hiiir\",\n      \"categoryId\": 4,\n      \"url\": \"http://adpower.hiiir.com/\",\n      \"companyId\": \"hiiir\"\n    },\n    \"hiro\": {\n      \"name\": \"HIRO\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hiro-media.com/\",\n      \"companyId\": \"hiro_media\"\n    },\n    \"histats\": {\n      \"name\": \"Histats\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.histats.com/\",\n      \"companyId\": \"histats\"\n    },\n    \"hit-parade\": {\n      \"name\": \"Hit-Parade\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hit-parade.com/\",\n      \"companyId\": \"hit-parade\"\n    },\n    \"hit.ua\": {\n      \"name\": \"HIT.UA\",\n      \"categoryId\": 4,\n      \"url\": \"http://hit.ua/\",\n      \"companyId\": \"hit.ua\"\n    },\n    \"hitslink\": {\n      \"name\": \"HitsLink\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hitslink.com/\",\n      \"companyId\": \"net_applications\"\n    },\n    \"hitsniffer\": {\n      \"name\": \"HitSniffer\",\n      \"categoryId\": 4,\n      \"url\": \"http://hitsniffer.com/\",\n      \"companyId\": \"hit_sniffer\"\n    },\n    \"hittail\": {\n      \"name\": \"HitTail\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hittail.com/\",\n      \"companyId\": \"hittail\"\n    },\n    \"hivedx.com\": {\n      \"name\": \"hiveDX\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.hivedx.com/\",\n      \"companyId\": null\n    },\n    \"hiveworks\": {\n      \"name\": \"Hive Networks\",\n      \"categoryId\": 4,\n      \"url\": \"https://hiveworkscomics.com/\",\n      \"companyId\": \"hive_works\"\n    },\n    \"hockeyapp\": {\n      \"name\": \"HockeyApp\",\n      \"categoryId\": 101,\n      \"url\": \"https://hockeyapp.net/\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"hoholikik.club\": {\n      \"name\": \"hoholikik.club\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"hola_player\": {\n      \"name\": \"Hola Player\",\n      \"categoryId\": 0,\n      \"url\": \"https://holacdn.com/\",\n      \"companyId\": \"hola_cdn\"\n    },\n    \"homeaway\": {\n      \"name\": \"HomeAway\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"honeybadger\": {\n      \"name\": \"Honeybadger\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.honeybadger.io/\",\n      \"companyId\": \"honeybadger\"\n    },\n    \"hooklogic\": {\n      \"name\": \"HookLogic\",\n      \"categoryId\": 4,\n      \"url\": \"http://hooklogic.com/\",\n      \"companyId\": \"criteo\"\n    },\n    \"hop-cube\": {\n      \"name\": \"Hop-Cube\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hop-cube.com/\",\n      \"companyId\": \"hop-cube\"\n    },\n    \"hotdogsandads.com\": {\n      \"name\": \"hotdogsandads.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"hotjar\": {\n      \"name\": \"Hotjar\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.hotjar.com/\",\n      \"companyId\": \"hotjar\"\n    },\n    \"hotkeys\": {\n      \"name\": \"HotKeys\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.demandmedia.com/\",\n      \"companyId\": \"leaf_group\"\n    },\n    \"hotlog.ru\": {\n      \"name\": \"HotLog\",\n      \"categoryId\": 4,\n      \"url\": \"https://hotlog.ru/\",\n      \"companyId\": \"hotlog\"\n    },\n    \"hotwords\": {\n      \"name\": \"HOTWords\",\n      \"categoryId\": 4,\n      \"url\": \"http://hotwords.com/\",\n      \"companyId\": \"hotwords\"\n    },\n    \"howtank.com\": {\n      \"name\": \"howtank\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.howtank.com/\",\n      \"companyId\": null\n    },\n    \"hqentertainmentnetwork.com\": {\n      \"name\": \"HQ Entertainment Network\",\n      \"categoryId\": 4,\n      \"url\": \"https://hqentertainmentnetwork.com/\",\n      \"companyId\": null\n    },\n    \"hsoub\": {\n      \"name\": \"Hsoub\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hsoub.com/\",\n      \"companyId\": \"hsoub\"\n    },\n    \"hstrck.com\": {\n      \"name\": \"HEIM:SPIEL Medien GmbH\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.heimspiel.de/\",\n      \"companyId\": null\n    },\n    \"httpool\": {\n      \"name\": \"HTTPool\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.httpool.com/\",\n      \"companyId\": \"httpool\"\n    },\n    \"hubrus\": {\n      \"name\": \"HUBRUS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hubrus.com/\",\n      \"companyId\": \"hubrus\"\n    },\n    \"hubspot\": {\n      \"name\": \"HubSpot\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.hubspot.com/\",\n      \"companyId\": \"hubspot\"\n    },\n    \"hubspot_forms\": {\n      \"name\": \"HubSpot Forms\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.hubspot.com\",\n      \"companyId\": \"hubspot\"\n    },\n    \"hubvisor.io\": {\n      \"name\": \"Hubvisor\",\n      \"categoryId\": 4,\n      \"url\": \"https://hubvisor.io/\",\n      \"companyId\": null\n    },\n    \"hucksterbot\": {\n      \"name\": \"HucksterBot\",\n      \"categoryId\": 4,\n      \"url\": \"http://hucksterbot.ru/\",\n      \"companyId\": \"hucksterbot\"\n    },\n    \"hupso\": {\n      \"name\": \"Hupso\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.hupso.com/\",\n      \"companyId\": \"hupso\"\n    },\n    \"hurra_tracker\": {\n      \"name\": \"Hurra Tracker\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hurra.com/en/\",\n      \"companyId\": \"hurra_communications\"\n    },\n    \"hybrid.ai\": {\n      \"name\": \"Hybrid.ai\",\n      \"categoryId\": 4,\n      \"url\": \"https://hybrid.ai/\",\n      \"companyId\": \"hybrid_adtech\"\n    },\n    \"hype_exchange\": {\n      \"name\": \"Hype Exchange\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hypeexchange.com/\",\n      \"companyId\": \"hype_exchange\"\n    },\n    \"hypercomments\": {\n      \"name\": \"HyperComments\",\n      \"categoryId\": 1,\n      \"url\": \"http://www.hypercomments.com/\",\n      \"companyId\": \"hypercomments\"\n    },\n    \"hyves_widgets\": {\n      \"name\": \"Hyves Widgets\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.hyves.nl/\",\n      \"companyId\": \"hyves\"\n    },\n    \"hyvyd\": {\n      \"name\": \"Hyvyd GmbH\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"i-behavior\": {\n      \"name\": \"i-Behavior\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.i-behavior.com/\",\n      \"companyId\": \"kbm_group\"\n    },\n    \"i-mobile\": {\n      \"name\": \"i-mobile\",\n      \"categoryId\": 4,\n      \"url\": \"https://www2.i-mobile.co.jp/en/index.aspx\",\n      \"companyId\": \"i-mobile\"\n    },\n    \"i.ua\": {\n      \"name\": \"i.ua\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.i.ua/\",\n      \"companyId\": \"i.ua\"\n    },\n    \"i10c.net\": {\n      \"name\": \"i10c.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"i2i.jp\": {\n      \"name\": \"i2i.jp\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.i2i.jp/\",\n      \"companyId\": \"i2i.jp\"\n    },\n    \"iab_consent\": {\n      \"name\": \"IAB Consent\",\n      \"categoryId\": 5,\n      \"url\": \"https://iabtechlab.com/standards/gdpr-transparency-and-consent-framework/\",\n      \"companyId\": \"iab\"\n    },\n    \"iadvize\": {\n      \"name\": \"iAdvize\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.iadvize.com/\",\n      \"companyId\": \"iadvize\"\n    },\n    \"ibm_customer_experience\": {\n      \"name\": \"IBM Digital Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.coremetrics.com/\",\n      \"companyId\": \"ibm\"\n    },\n    \"icerocket_tracker\": {\n      \"name\": \"IceRocket Tracker\",\n      \"categoryId\": 7,\n      \"url\": \"http://tracker.icerocket.com/\",\n      \"companyId\": \"meltwater_icerocket\"\n    },\n    \"icf_technology\": {\n      \"name\": \"ICF Technology\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.icftechnology.com/\",\n      \"companyId\": null\n    },\n    \"iclick\": {\n      \"name\": \"iClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://optimix.asia/\",\n      \"companyId\": \"iclick_interactive\"\n    },\n    \"icrossing\": {\n      \"name\": \"iCrossing\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.icrossing.com/\",\n      \"companyId\": \"hearst\"\n    },\n    \"icstats\": {\n      \"name\": \"ICStats\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.icstats.nl/\",\n      \"companyId\": \"icstats\"\n    },\n    \"icuazeczpeoohx.com\": {\n      \"name\": \"icuazeczpeoohx.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"id-news.net\": {\n      \"name\": \"Ippen Digital\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ippen-digital.de/\",\n      \"companyId\": null\n    },\n    \"id5-sync\": {\n      \"name\": \"ID5 Sync\",\n      \"categoryId\": 4,\n      \"url\": \"https://id5.io/\",\n      \"companyId\": \"id5-sync\",\n      \"source\": \"AdGuard\"\n    },\n    \"id_services\": {\n      \"name\": \"ID Services\",\n      \"categoryId\": 6,\n      \"url\": \"https://id.services/\",\n      \"companyId\": \"id_services\"\n    },\n    \"ideal_media\": {\n      \"name\": \"Ideal Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://idealmedia.com/\",\n      \"companyId\": \"ideal_media\"\n    },\n    \"idealo_com\": {\n      \"name\": \"idealo.com\",\n      \"categoryId\": 4,\n      \"url\": \"http://idealo.com/\",\n      \"companyId\": null\n    },\n    \"identrust\": {\n      \"name\": \"IdenTrust, Inc.\",\n      \"categoryId\": 5,\n      \"url\": \"https://identrust.com/\",\n      \"companyId\": \"identrust\",\n      \"source\": \"AdGuard\"\n    },\n    \"ideoclick\": {\n      \"name\": \"IdeoClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://ideoclick.com\",\n      \"companyId\": \"ideoclick\"\n    },\n    \"idio\": {\n      \"name\": \"Idio\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.idio.ai/\",\n      \"companyId\": \"idio\"\n    },\n    \"ie8eamus.com\": {\n      \"name\": \"ie8eamus.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"ientry\": {\n      \"name\": \"iEntry\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ientry.com/\",\n      \"companyId\": \"ientry\"\n    },\n    \"iflychat\": {\n      \"name\": \"iFlyChat\",\n      \"categoryId\": 2,\n      \"url\": \"https://iflychat.com/\",\n      \"companyId\": \"iflychat\"\n    },\n    \"ignitionone\": {\n      \"name\": \"IgnitionOne\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.ignitionone.com/\",\n      \"companyId\": \"zeta\"\n    },\n    \"igodigital\": {\n      \"name\": \"iGoDigital\",\n      \"categoryId\": 2,\n      \"url\": \"http://igodigital.com/\",\n      \"companyId\": \"salesforce\"\n    },\n    \"ihs_markit\": {\n      \"name\": \"IHS Markit\",\n      \"categoryId\": 6,\n      \"url\": \"https://ihsmarkit.com/index.html\",\n      \"companyId\": \"ihs\"\n    },\n    \"ihs_markit_online_shopper_insigh\": {\n      \"name\": \"IHS Markit Online Shopper Insigh\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.visicogn.com/vcu.htm\",\n      \"companyId\": \"ihs\"\n    },\n    \"ihvmcqojoj.com\": {\n      \"name\": \"ihvmcqojoj.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"iias.eu\": {\n      \"name\": \"Insight Image\",\n      \"categoryId\": 3,\n      \"url\": \"http://insightimage.com/\",\n      \"companyId\": null\n    },\n    \"ijento\": {\n      \"name\": \"iJento\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ijento.com/\",\n      \"companyId\": \"ijento\"\n    },\n    \"imad\": {\n      \"name\": \"imad\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.imad.co.kr/\",\n      \"companyId\": \"i'mad_republic\"\n    },\n    \"image_advantage\": {\n      \"name\": \"Image Advantage\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.worthathousandwords.com/\",\n      \"companyId\": \"image_advantage\"\n    },\n    \"image_space_media\": {\n      \"name\": \"Image Space Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.imagespacemedia.com/\",\n      \"companyId\": \"image_space_media\"\n    },\n    \"imgix.net\": {\n      \"name\": \"ImgIX\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.imgix.com/\",\n      \"companyId\": null\n    },\n    \"imgur\": {\n      \"name\": \"Imgur\",\n      \"categoryId\": 8,\n      \"url\": \"https://imgur.com/\",\n      \"companyId\": \"medialab\",\n      \"source\": \"AdGuard\"\n    },\n    \"imho_vi\": {\n      \"name\": \"imho vi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.imho.ru\",\n      \"companyId\": \"imho\"\n    },\n    \"immanalytics\": {\n      \"name\": \"Immanalytics\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.roku.com/\",\n      \"companyId\": \"roku\"\n    },\n    \"immobilienscout24_de\": {\n      \"name\": \"immobilienscout24.de\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.scout24.com/\",\n      \"companyId\": \"scout24\"\n    },\n    \"imonomy\": {\n      \"name\": \"imonomy\",\n      \"categoryId\": 6,\n      \"url\": \"http://imonomy.com/\",\n      \"companyId\": \"imonomy\"\n    },\n    \"impact_radius\": {\n      \"name\": \"Impact Radius\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.impactradius.com/\",\n      \"companyId\": \"impact_radius\"\n    },\n    \"impresiones_web\": {\n      \"name\": \"Impresiones Web\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.iw-advertising.com/\",\n      \"companyId\": \"impresiones_web\"\n    },\n    \"improve_digital\": {\n      \"name\": \"Improve Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.improvedigital.com/\",\n      \"companyId\": \"improve_digital\"\n    },\n    \"improvely\": {\n      \"name\": \"Improvely\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.improvely.com/\",\n      \"companyId\": \"awio_web_services\"\n    },\n    \"inbenta\": {\n      \"name\": \"Inbenta\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.inbenta.com/en/\",\n      \"companyId\": \"inbenta\"\n    },\n    \"inboxsdk.com\": {\n      \"name\": \"Inbox SDK\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.inboxsdk.com/\",\n      \"companyId\": null\n    },\n    \"indeed\": {\n      \"name\": \"Indeed\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.indeed.com/\",\n      \"companyId\": \"indeed\"\n    },\n    \"index_exchange\": {\n      \"name\": \"Index Exchange\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.casalemedia.com/\",\n      \"companyId\": \"index_exchange\"\n    },\n    \"indieclick\": {\n      \"name\": \"IndieClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.indieclick.com/\",\n      \"companyId\": \"leaf_group\"\n    },\n    \"industry_brains\": {\n      \"name\": \"Industry Brains\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.industrybrains.com/\",\n      \"companyId\": \"industrybrains\"\n    },\n    \"infectious_media\": {\n      \"name\": \"Impression Desk\",\n      \"categoryId\": 4,\n      \"url\": \"https://impressiondesk.com/\",\n      \"companyId\": \"infectious_media\"\n    },\n    \"infinite_analytics\": {\n      \"name\": \"Infinite Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://infiniteanalytics.com/products/\",\n      \"companyId\": \"infinite_analytics\"\n    },\n    \"infinity_tracking\": {\n      \"name\": \"Infinity Tracking\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.infinity-tracking.com\",\n      \"companyId\": \"infinity_tracking\"\n    },\n    \"influads\": {\n      \"name\": \"InfluAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.influads.com/\",\n      \"companyId\": \"influads\"\n    },\n    \"infolinks\": {\n      \"name\": \"InfoLinks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.infolinks.com/\",\n      \"companyId\": \"infolinks\"\n    },\n    \"infonline\": {\n      \"name\": \"INFOnline\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.infonline.de/\",\n      \"companyId\": \"infonline\"\n    },\n    \"informer_technologies\": {\n      \"name\": \"Informer Technologies\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.informer.com/\",\n      \"companyId\": \"informer_technologies\"\n    },\n    \"infusionsoft\": {\n      \"name\": \"Infusionsoft by Keap\",\n      \"categoryId\": 4,\n      \"url\": \"https://keap.com/\",\n      \"companyId\": \"infusionsoft\"\n    },\n    \"innity\": {\n      \"name\": \"Innity\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.innity.com/\",\n      \"companyId\": \"innity\"\n    },\n    \"innogames.de\": {\n      \"name\": \"InnoGames\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.innogames.com/\",\n      \"companyId\": null\n    },\n    \"innovid\": {\n      \"name\": \"Innovid\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.innovid.com/\",\n      \"companyId\": \"innovid\"\n    },\n    \"inside\": {\n      \"name\": \"inside\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.inside.tm/\",\n      \"companyId\": \"powerfront\"\n    },\n    \"insider\": {\n      \"name\": \"Insider\",\n      \"categoryId\": 6,\n      \"url\": \"http://useinsider.com/\",\n      \"companyId\": \"insider\"\n    },\n    \"insightexpress\": {\n      \"name\": \"InsightExpress\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.millwardbrowndigital.com/\",\n      \"companyId\": \"millward_brown\"\n    },\n    \"inskin_media\": {\n      \"name\": \"InSkin Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.inskinmedia.com/\",\n      \"companyId\": \"inskin_media\"\n    },\n    \"inspectlet\": {\n      \"name\": \"Inspectlet\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.inspectlet.com/\",\n      \"companyId\": \"inspectlet\"\n    },\n    \"inspsearchapi.com\": {\n      \"name\": \"Infospace Search\",\n      \"categoryId\": 4,\n      \"url\": \"http://infospace.com/\",\n      \"companyId\": \"system1\"\n    },\n    \"instagram_com\": {\n      \"name\": \"Instagram\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.facebook.com/\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"instant_check_mate\": {\n      \"name\": \"Instant Check Mate\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.instantcheckmate.com/\",\n      \"companyId\": \"instant_check_mate\"\n    },\n    \"instart_logic\": {\n      \"name\": \"Instart Logic\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.instartlogic.com/\",\n      \"companyId\": \"instart_logic_inc\"\n    },\n    \"insticator\": {\n      \"name\": \"Insticator\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.insticator.com/landingpage\",\n      \"companyId\": \"insticator\"\n    },\n    \"instinctive\": {\n      \"name\": \"Instinctive\",\n      \"categoryId\": 4,\n      \"url\": \"https://instinctive.io/\",\n      \"companyId\": \"instinctive\"\n    },\n    \"intango\": {\n      \"name\": \"Intango\",\n      \"categoryId\": 4,\n      \"url\": \"https://intango.com/\",\n      \"companyId\": \"intango\"\n    },\n    \"integral_ad_science\": {\n      \"name\": \"Integral Ad Science\",\n      \"categoryId\": 4,\n      \"url\": \"https://integralads.com/\",\n      \"companyId\": \"integral_ad_science\"\n    },\n    \"integral_marketing\": {\n      \"name\": \"Integral Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"http://integral-marketing.com/\",\n      \"companyId\": \"integral_marketing\"\n    },\n    \"intelliad\": {\n      \"name\": \"intelliAd\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.intelliad.de/\",\n      \"companyId\": \"intelliad\"\n    },\n    \"intelligencefocus\": {\n      \"name\": \"IntelligenceFocus\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.intelligencefocus.com\",\n      \"companyId\": \"intelligencefocus\"\n    },\n    \"intelligent_reach\": {\n      \"name\": \"Intelligent Reach\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.intelligentreach.com/\",\n      \"companyId\": \"intelligent_reach\"\n    },\n    \"intense_debate\": {\n      \"name\": \"Intense Debate\",\n      \"categoryId\": 2,\n      \"url\": \"http://intensedebate.com/\",\n      \"companyId\": \"automattic\"\n    },\n    \"intent_iq\": {\n      \"name\": \"Intent IQ\",\n      \"categoryId\": 4,\n      \"url\": \"http://datonics.com/\",\n      \"companyId\": \"almondnet\"\n    },\n    \"intent_media\": {\n      \"name\": \"Intent\",\n      \"categoryId\": 4,\n      \"url\": \"https://intent.com/\",\n      \"companyId\": \"intent_media\"\n    },\n    \"intercom\": {\n      \"name\": \"Intercom\",\n      \"categoryId\": 2,\n      \"url\": \"http://intercom.io/\",\n      \"companyId\": \"intercom\"\n    },\n    \"interedy.info\": {\n      \"name\": \"interedy.info\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"intergi\": {\n      \"name\": \"Intergi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.intergi.com/\",\n      \"companyId\": \"intergi_entertainment\"\n    },\n    \"intermarkets.net\": {\n      \"name\": \"Intermarkets\",\n      \"categoryId\": 4,\n      \"url\": \"http://intermarkets.net/\",\n      \"companyId\": \"intermarkets\"\n    },\n    \"intermundo_media\": {\n      \"name\": \"InterMundo Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://intermundomedia.com/\",\n      \"companyId\": \"intermundo_media\"\n    },\n    \"internet_billboard\": {\n      \"name\": \"Internet BillBoard\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ibillboard.com/en/\",\n      \"companyId\": \"internet_billboard\"\n    },\n    \"internetaudioads\": {\n      \"name\": \"InternetAudioAds\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.internetaudioads.com/\",\n      \"companyId\": \"internetaudioads\"\n    },\n    \"internetbrands\": {\n      \"name\": \"InternetBrands\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.internetbrands.com/\",\n      \"companyId\": \"internet_brands\"\n    },\n    \"interpolls\": {\n      \"name\": \"Interpolls\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.interpolls.com/\",\n      \"companyId\": \"interpolls\"\n    },\n    \"interyield\": {\n      \"name\": \"Interyield\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.advertise.com/publisher-solutions/\",\n      \"companyId\": \"advertise.com\"\n    },\n    \"intilery\": {\n      \"name\": \"Intilery\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.intilery.com\",\n      \"companyId\": \"intilery\"\n    },\n    \"intimate_merger\": {\n      \"name\": \"Intimate Merger\",\n      \"categoryId\": 6,\n      \"url\": \"https://corp.intimatemerger.com/\",\n      \"companyId\": \"intimate_merger\"\n    },\n    \"investingchannel\": {\n      \"name\": \"Investing Channel\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.investingchannel.com/\",\n      \"companyId\": \"investingchannel\"\n    },\n    \"inviziads\": {\n      \"name\": \"InviziAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.inviziads.com\",\n      \"companyId\": \"inviziads\"\n    },\n    \"invoca\": {\n      \"name\": \"Invoca\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.invoca.com/\",\n      \"companyId\": \"invoca\"\n    },\n    \"invodo\": {\n      \"name\": \"Invodo\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.invodo.com/\",\n      \"companyId\": \"invodo\"\n    },\n    \"ionicframework.com\": {\n      \"name\": \"Ionic\",\n      \"categoryId\": 8,\n      \"url\": \"https://ionicframework.com/\",\n      \"companyId\": null\n    },\n    \"iotec\": {\n      \"name\": \"iotec\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.iotecglobal.com/\",\n      \"companyId\": \"iotec\"\n    },\n    \"iovation\": {\n      \"name\": \"iovation\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.iovation.com/\",\n      \"companyId\": \"iovation\"\n    },\n    \"ip-label\": {\n      \"name\": \"ip-label\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ip-label.co.uk/\",\n      \"companyId\": \"ip-label\"\n    },\n    \"ip_targeting\": {\n      \"name\": \"IP Targeting\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.iptargeting.com/\",\n      \"companyId\": \"el_toro\"\n    },\n    \"ip_tracker\": {\n      \"name\": \"IP Tracker\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ip-tracker.org/\",\n      \"companyId\": \"ip_tracker\"\n    },\n    \"iperceptions\": {\n      \"name\": \"iPerceptions\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.iperceptions.com/\",\n      \"companyId\": \"iperceptions\"\n    },\n    \"ipfingerprint\": {\n      \"name\": \"IPFingerprint\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ipfingerprint.com/\",\n      \"companyId\": \"ipfingerprint\"\n    },\n    \"ipg_mediabrands\": {\n      \"name\": \"IPG Mediabrands\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ipgmediabrands.com/\",\n      \"companyId\": \"ipg_mediabrands\"\n    },\n    \"ipify\": {\n      \"name\": \"ipify\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.ipify.org/\",\n      \"companyId\": null\n    },\n    \"ipinfo\": {\n      \"name\": \"Ipinfo\",\n      \"categoryId\": 2,\n      \"url\": \"https://ipinfo.io/\",\n      \"companyId\": \"ipinfo.io\"\n    },\n    \"iplogger\": {\n      \"name\": \"IPLogger\",\n      \"categoryId\": 6,\n      \"url\": \"http://iplogger.ru/\",\n      \"companyId\": \"iplogger\"\n    },\n    \"iprom\": {\n      \"name\": \"iprom\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.iprom.si/\",\n      \"companyId\": \"iprom\"\n    },\n    \"ipromote\": {\n      \"name\": \"iPromote\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ipromote.com/\",\n      \"companyId\": \"ipromote\"\n    },\n    \"iprospect\": {\n      \"name\": \"iProspect\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.iprospect.com/\",\n      \"companyId\": \"dentsu_aegis_network\"\n    },\n    \"iqiyi\": {\n      \"name\": \"iQiyi\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.iqiyi.com/\",\n      \"companyId\": \"iqiyi\",\n      \"source\": \"AdGuard\"\n    },\n    \"ironsource\": {\n      \"name\": \"ironSource Ltd.\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.is.com\",\n      \"companyId\": \"unity\",\n      \"source\": \"AdGuard\"\n    },\n    \"isocket\": {\n      \"name\": \"isocket\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.isocket.com/\",\n      \"companyId\": \"rubicon_project\"\n    },\n    \"isolarcloud\": {\n      \"name\": \"iSolarCloud\",\n      \"categoryId\": 6,\n      \"url\": \"https://isolarcloud.com/\",\n      \"companyId\": \"sungrow\",\n      \"source\": \"AdGuard\"\n    },\n    \"ispot.tv\": {\n      \"name\": \"iSpot.tv\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ispot.tv/\",\n      \"companyId\": null\n    },\n    \"itineraire.info\": {\n      \"name\": \"itineraire.info\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.itineraire.info/\",\n      \"companyId\": null\n    },\n    \"itunes_link_maker\": {\n      \"name\": \"iTunes Link Maker\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.apple.com/\",\n      \"companyId\": \"apple\"\n    },\n    \"ity.im\": {\n      \"name\": \"ity.im\",\n      \"categoryId\": 4,\n      \"url\": \"http://ity.im/\",\n      \"companyId\": \"ity.im\"\n    },\n    \"iubenda.com\": {\n      \"name\": \"iubenda\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.iubenda.com/\",\n      \"companyId\": \"iubenda\"\n    },\n    \"ivcbrasil.org.br\": {\n      \"name\": \"IVC Brasil\",\n      \"categoryId\": 6,\n      \"url\": \"https://ivcbrasil.org.br/#/home\",\n      \"companyId\": null\n    },\n    \"ividence\": {\n      \"name\": \"Ividence\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ividence.com/home/\",\n      \"companyId\": \"sien\"\n    },\n    \"iwiw_widgets\": {\n      \"name\": \"iWiW Widgets\",\n      \"categoryId\": 2,\n      \"url\": \"http://iwiw.hu\",\n      \"companyId\": \"iwiw\"\n    },\n    \"ixi_digital\": {\n      \"name\": \"IXI Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.equifax.com/home/en_us\",\n      \"companyId\": \"equifax\"\n    },\n    \"ixquick.com\": {\n      \"name\": \"ixquick\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.ixquick.com/\",\n      \"companyId\": \"startpage\"\n    },\n    \"izooto\": {\n      \"name\": \"iZooto\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.izooto.com/\",\n      \"companyId\": \"izooto\"\n    },\n    \"j-list_affiliate_program\": {\n      \"name\": \"J-List Affiliate Program\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.jlist.com/page/affiliates.html\",\n      \"companyId\": \"j-list\"\n    },\n    \"jaco\": {\n      \"name\": \"Jaco\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.walkme.com/\",\n      \"companyId\": \"walkme\"\n    },\n    \"janrain\": {\n      \"name\": \"Janrain\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.janrain.com/\",\n      \"companyId\": \"akamai\"\n    },\n    \"jeeng\": {\n      \"name\": \"Jeeng\",\n      \"categoryId\": 4,\n      \"url\": \"https://jeeng.com/\",\n      \"companyId\": \"jeeng\"\n    },\n    \"jeeng_widgets\": {\n      \"name\": \"Jeeng Widgets\",\n      \"categoryId\": 4,\n      \"url\": \"https://jeeng.com/\",\n      \"companyId\": \"jeeng\"\n    },\n    \"jet_interactive\": {\n      \"name\": \"Jet Interactive\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.jetinteractive.com.au/\",\n      \"companyId\": \"jet_interactive\"\n    },\n    \"jetbrains\": {\n      \"name\": \"JetBrains\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.jetbrains.com/\",\n      \"companyId\": \"jetbrains\",\n      \"source\": \"AdGuard\"\n    },\n    \"jetlore\": {\n      \"name\": \"Jetlore\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.jetlore.com/\",\n      \"companyId\": \"jetlore\"\n    },\n    \"jetpack\": {\n      \"name\": \"Jetpack\",\n      \"categoryId\": 6,\n      \"url\": \"https://jetpack.com/\",\n      \"companyId\": \"automattic\"\n    },\n    \"jetpack_digital\": {\n      \"name\": \"Jetpack Digital\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.jetpack.com/\",\n      \"companyId\": \"jetpack_digital\"\n    },\n    \"jimdo.com\": {\n      \"name\": \"jimdo.com\",\n      \"categoryId\": 10,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"jink\": {\n      \"name\": \"Jink\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.jink.de/\",\n      \"companyId\": \"jink\"\n    },\n    \"jirafe\": {\n      \"name\": \"Jirafe\",\n      \"categoryId\": 6,\n      \"url\": \"http://jirafe.com/\",\n      \"companyId\": \"jirafe\"\n    },\n    \"jivochat\": {\n      \"name\": \"JivoSite\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.jivochat.com/\",\n      \"companyId\": \"jivochat\"\n    },\n    \"jivox\": {\n      \"name\": \"Jivox\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.jivox.com/\",\n      \"companyId\": \"jivox\"\n    },\n    \"jobs_2_careers\": {\n      \"name\": \"Jobs 2 Careers\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.jobs2careers.com/\",\n      \"companyId\": \"jobs_2_careers\"\n    },\n    \"joinhoney\": {\n      \"name\": \"Honey\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.joinhoney.com/\",\n      \"companyId\": null\n    },\n    \"jornaya\": {\n      \"name\": \"Jornaya\",\n      \"categoryId\": 6,\n      \"url\": \"http://leadid.com/\",\n      \"companyId\": \"jornaya\"\n    },\n    \"jquery\": {\n      \"name\": \"jQuery\",\n      \"categoryId\": 9,\n      \"url\": \"https://jquery.org/\",\n      \"companyId\": \"js_foundation\"\n    },\n    \"js_communications\": {\n      \"name\": \"JS Communications\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.jssearch.net/\",\n      \"companyId\": \"js_communications\"\n    },\n    \"jsdelivr\": {\n      \"name\": \"jsDelivr\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.jsdelivr.com/\",\n      \"companyId\": null\n    },\n    \"jse_coin\": {\n      \"name\": \"JSE Coin\",\n      \"categoryId\": 4,\n      \"url\": \"https://jsecoin.com/\",\n      \"companyId\": \"jse_coin\"\n    },\n    \"jsuol.com.br\": {\n      \"name\": \"jsuol.com.br\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"juggcash\": {\n      \"name\": \"JuggCash\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.juggcash.com\",\n      \"companyId\": \"juggcash\"\n    },\n    \"juiceadv\": {\n      \"name\": \"JuiceADV\",\n      \"categoryId\": 4,\n      \"url\": \"http://juiceadv.com/\",\n      \"companyId\": \"juiceadv\"\n    },\n    \"juicyads\": {\n      \"name\": \"JuicyAds\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.juicyads.com/\",\n      \"companyId\": \"juicyads\"\n    },\n    \"jumplead\": {\n      \"name\": \"Jumplead\",\n      \"categoryId\": 6,\n      \"url\": \"https://jumplead.com/\",\n      \"companyId\": \"jumplead\"\n    },\n    \"jumpstart_tagging_solutions\": {\n      \"name\": \"Jumpstart Tagging Solutions\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.hearst.com/\",\n      \"companyId\": \"hearst\"\n    },\n    \"jumptap\": {\n      \"name\": \"Jumptap\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.jumptap.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"jumptime\": {\n      \"name\": \"JumpTime\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.jumptime.com/\",\n      \"companyId\": \"openx\"\n    },\n    \"just_answer\": {\n      \"name\": \"Just Answer\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.justanswer.com/\",\n      \"companyId\": \"just_answer\"\n    },\n    \"just_premium\": {\n      \"name\": \"Just Premium\",\n      \"categoryId\": 4,\n      \"url\": \"http://justpremium.com/\",\n      \"companyId\": \"just_premium\"\n    },\n    \"just_relevant\": {\n      \"name\": \"Just Relevant\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.justrelevant.com/\",\n      \"companyId\": \"just_relevant\"\n    },\n    \"jvc.gg\": {\n      \"name\": \"Jeuxvideo CDN\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.jeuxvideo.com/\",\n      \"companyId\": null\n    },\n    \"jw_player\": {\n      \"name\": \"JW Player\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.jwplayer.com/\",\n      \"companyId\": \"jw_player\"\n    },\n    \"jw_player_ad_solutions\": {\n      \"name\": \"JW Player Ad Solutions\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.longtailvideo.com/adsolution/\",\n      \"companyId\": \"jw_player\"\n    },\n    \"kaeufersiegel.de\": {\n      \"name\": \"Käufersiegel\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.kaeufersiegel.de/\",\n      \"companyId\": null\n    },\n    \"kairion.de\": {\n      \"name\": \"kairion\",\n      \"categoryId\": 4,\n      \"url\": \"https://kairion.de/\",\n      \"companyId\": \"prosieben_sat1\"\n    },\n    \"kaloo.ga\": {\n      \"name\": \"Kalooga\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.kalooga.com/\",\n      \"companyId\": \"kalooga\"\n    },\n    \"kalooga_widget\": {\n      \"name\": \"Kalooga Widget\",\n      \"categoryId\": 4,\n      \"url\": \"http://kalooga.com/\",\n      \"companyId\": \"kalooga\"\n    },\n    \"kaltura\": {\n      \"name\": \"Kaltura\",\n      \"categoryId\": 0,\n      \"url\": \"http://corp.kaltura.com/\",\n      \"companyId\": \"kaltura\"\n    },\n    \"kameleoon\": {\n      \"name\": \"Kameleoon\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.kameleoon.com/\",\n      \"companyId\": \"kameleoon\"\n    },\n    \"kampyle\": {\n      \"name\": \"Medallia\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.kampyle.com/\",\n      \"companyId\": \"medallia\"\n    },\n    \"kanoodle\": {\n      \"name\": \"Kanoodle\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kanoodle.com/\",\n      \"companyId\": \"kanoodle\"\n    },\n    \"kantar_media\": {\n      \"name\": \"Kantar Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.kantarmedia.com/\",\n      \"companyId\": \"wpp\"\n    },\n    \"karambasecurity\": {\n      \"name\": \"Karamba Security\",\n      \"categoryId\": 8,\n      \"url\": \"https://karambasecurity.com/\",\n      \"companyId\": \"karambasecurity\",\n      \"source\": \"AdGuard\"\n    },\n    \"kargo\": {\n      \"name\": \"Kargo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kargo.com/\",\n      \"companyId\": \"kargo\"\n    },\n    \"kaspersky-labs.com\": {\n      \"name\": \"Kaspersky Labs\",\n      \"categoryId\": 12,\n      \"url\": \"https://www.kaspersky.com/\",\n      \"companyId\": \"AO Kaspersky Lab\"\n    },\n    \"kataweb.it\": {\n      \"name\": \"KataWeb\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kataweb.it/\",\n      \"companyId\": null\n    },\n    \"katchup\": {\n      \"name\": \"Katchup\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.katchup.fr/\",\n      \"companyId\": \"katchup\"\n    },\n    \"kauli\": {\n      \"name\": \"Kauli\",\n      \"categoryId\": 4,\n      \"url\": \"http://kau.li/\",\n      \"companyId\": \"kauli\"\n    },\n    \"kavanga\": {\n      \"name\": \"Kavanga\",\n      \"categoryId\": 4,\n      \"url\": \"http://kavanga.ru/\",\n      \"companyId\": \"kavanga\"\n    },\n    \"kayo_sports\": {\n      \"name\": \"Kayo Sports\",\n      \"categoryId\": 0,\n      \"url\": \"https://kayosports.com.au/\",\n      \"companyId\": \"news_corp\",\n      \"source\": \"AdGuard\"\n    },\n    \"keen_io\": {\n      \"name\": \"Keen IO\",\n      \"categoryId\": 6,\n      \"url\": \"https://keen.io\",\n      \"companyId\": \"keen_io\"\n    },\n    \"kelkoo\": {\n      \"name\": \"Kelkoo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kelkoo.com/\",\n      \"companyId\": \"kelkoo\"\n    },\n    \"kenshoo\": {\n      \"name\": \"Kenshoo\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.kenshoo.com/\",\n      \"companyId\": \"kenshoo\"\n    },\n    \"keymetric\": {\n      \"name\": \"KeyMetric\",\n      \"categoryId\": 6,\n      \"url\": \"http://keymetric.net/\",\n      \"companyId\": \"keymetric\"\n    },\n    \"keytiles\": {\n      \"name\": \"Keytiles\",\n      \"categoryId\": 6,\n      \"url\": \"http://keytiles.com/\",\n      \"companyId\": \"keytiles\"\n    },\n    \"keywee\": {\n      \"name\": \"Keywee\",\n      \"categoryId\": 6,\n      \"url\": \"https://keywee.co/\",\n      \"companyId\": \"keywee\"\n    },\n    \"keywordmax\": {\n      \"name\": \"KeywordMax\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.keywordmax.com/\",\n      \"companyId\": \"digital_river\"\n    },\n    \"khoros\": {\n      \"name\": \"Khoros\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.massrelevance.com/\",\n      \"companyId\": \"khoros\"\n    },\n    \"khzbeucrltin.com\": {\n      \"name\": \"khzbeucrltin.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"kickfactory\": {\n      \"name\": \"Kickfactory\",\n      \"categoryId\": 4,\n      \"url\": \"https://kickfactory.com/\",\n      \"companyId\": \"kickfactory\"\n    },\n    \"kickfire\": {\n      \"name\": \"Kickfire\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.visistat.com/\",\n      \"companyId\": \"kickfire\"\n    },\n    \"kik\": {\n      \"name\": \"Kik\",\n      \"categoryId\": 7,\n      \"url\": \"https://kik.com/\",\n      \"companyId\": \"medialab\",\n      \"source\": \"AdGuard\"\n    },\n    \"king.com\": {\n      \"name\": \"King.com\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.king.com/\",\n      \"companyId\": \"king.com\"\n    },\n    \"king_com\": {\n      \"name\": \"King.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://king.com/\",\n      \"companyId\": \"activision_blizzard\"\n    },\n    \"kinja.com\": {\n      \"name\": \"Kinja\",\n      \"categoryId\": 6,\n      \"url\": \"https://kinja.com/\",\n      \"companyId\": \"gizmodo\"\n    },\n    \"kiosked\": {\n      \"name\": \"Kiosked\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kiosked.com/\",\n      \"companyId\": \"kiosked\"\n    },\n    \"kissmetrics.com\": {\n      \"name\": \"Kissmetrics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.kissmetrics.com/\",\n      \"companyId\": \"kissmetrics\"\n    },\n    \"kitara_media\": {\n      \"name\": \"Kitara Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kitaramedia.com/\",\n      \"companyId\": \"kitara_media\"\n    },\n    \"kixer\": {\n      \"name\": \"Kixer\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kixer.com\",\n      \"companyId\": \"kixer\"\n    },\n    \"klarna.com\": {\n      \"name\": \"Klarna\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.klarna.com/\",\n      \"companyId\": null\n    },\n    \"klaviyo\": {\n      \"name\": \"Klaviyo\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.klaviyo.com/\",\n      \"companyId\": \"klaviyo\"\n    },\n    \"klikki\": {\n      \"name\": \"Klikki\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.klikki.com/\",\n      \"companyId\": \"klikki\"\n    },\n    \"kliksaya\": {\n      \"name\": \"KlikSaya\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kliksaya.com\",\n      \"companyId\": \"kliksaya\"\n    },\n    \"kmeleo\": {\n      \"name\": \"Kméléo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.6peo.com/\",\n      \"companyId\": \"6peo\"\n    },\n    \"knoopstat\": {\n      \"name\": \"Knoopstat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.knoopstat.nl\",\n      \"companyId\": \"knoopstat\"\n    },\n    \"knotch\": {\n      \"name\": \"Knotch\",\n      \"categoryId\": 2,\n      \"url\": \"http://knotch.it\",\n      \"companyId\": \"knotch\"\n    },\n    \"komoona\": {\n      \"name\": \"Komoona\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.komoona.com/\",\n      \"companyId\": \"komoona\"\n    },\n    \"kontera_contentlink\": {\n      \"name\": \"Kontera ContentLink\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kontera.com/\",\n      \"companyId\": \"singtel\"\n    },\n    \"kontextr\": {\n      \"name\": \"Kontextr\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.kontextr.com/\",\n      \"companyId\": \"kontext\"\n    },\n    \"kontextua\": {\n      \"name\": \"Kontextua\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kontextua.com/\",\n      \"companyId\": \"kontextua\"\n    },\n    \"korrelate\": {\n      \"name\": \"Korrelate\",\n      \"categoryId\": 4,\n      \"url\": \"http://korrelate.com/\",\n      \"companyId\": \"korrelate\"\n    },\n    \"kortx\": {\n      \"name\": \"Kortx\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.kortx.io/\",\n      \"companyId\": \"kortx\"\n    },\n    \"kount\": {\n      \"name\": \"Kount\",\n      \"categoryId\": 6,\n      \"url\": \"https://kount.com/\",\n      \"companyId\": null\n    },\n    \"krux_digital\": {\n      \"name\": \"Salesforce DMP\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.salesforce.com/products/marketing-cloud/data-management/?mc=DMP\",\n      \"companyId\": \"salesforce\"\n    },\n    \"kupona\": {\n      \"name\": \"Kupona\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.kupona-media.de/en/retargeting-and-performance-media-width-kupona\",\n      \"companyId\": \"kupona\"\n    },\n    \"kxcdn.com\": {\n      \"name\": \"Keycdn\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.keycdn.com/\",\n      \"companyId\": null\n    },\n    \"kyto\": {\n      \"name\": \"Kyto\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.kyto.com/\",\n      \"companyId\": \"kyto\"\n    },\n    \"ladsp.com\": {\n      \"name\": \"Logicad\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.logicad.com/\",\n      \"companyId\": \"logicad\"\n    },\n    \"lanista_concepts\": {\n      \"name\": \"Lanista Concepts\",\n      \"categoryId\": 4,\n      \"url\": \"http://lanistaconcepts.com/\",\n      \"companyId\": \"lanista_concepts\"\n    },\n    \"latimes\": {\n      \"name\": \"Los Angeles Times\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.latimes.com/\",\n      \"companyId\": \"latimes\"\n    },\n    \"launch_darkly\": {\n      \"name\": \"Launch Darkly\",\n      \"categoryId\": 5,\n      \"url\": \"https://launchdarkly.com/index.html\",\n      \"companyId\": \"launch_darkly\"\n    },\n    \"launchbit\": {\n      \"name\": \"LaunchBit\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.launchbit.com/\",\n      \"companyId\": \"launchbit\"\n    },\n    \"launchpad\": {\n      \"name\": \"Launchpad\",\n      \"categoryId\": 8,\n      \"url\": \"https://launchpad.net/\",\n      \"companyId\": \"canonical\",\n      \"source\": \"AdGuard\"\n    },\n    \"layer-ad.org\": {\n      \"name\": \"Layer-ADS.net\",\n      \"categoryId\": 4,\n      \"url\": \"http://layer-ads.net/\",\n      \"companyId\": null\n    },\n    \"lazada\": {\n      \"name\": \"Lazada\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.lazada.com/\",\n      \"companyId\": \"lazada\"\n    },\n    \"lcx_digital\": {\n      \"name\": \"LCX Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.lcx.com/\",\n      \"companyId\": \"lcx_digital\"\n    },\n    \"le_monde.fr\": {\n      \"name\": \"Le Monde.fr\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.lemonde.fr/\",\n      \"companyId\": \"le_monde.fr\"\n    },\n    \"lead_liaison\": {\n      \"name\": \"Lead Liaison\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.leadliaison.com\",\n      \"companyId\": \"lead_liaison\"\n    },\n    \"leadback\": {\n      \"name\": \"Leadback\",\n      \"categoryId\": 6,\n      \"url\": \"http://leadback.ru/?utm_source=leadback_widget&utm_medium=eas-balt.ru&utm_campaign=self_ad\",\n      \"companyId\": \"leadback\"\n    },\n    \"leaddyno\": {\n      \"name\": \"LeadDyno\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.leaddyno.com\",\n      \"companyId\": \"leaddyno\"\n    },\n    \"leadforensics\": {\n      \"name\": \"LeadForensics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.leadforensics.com/\",\n      \"companyId\": \"lead_forensics\"\n    },\n    \"leadgenic\": {\n      \"name\": \"LeadGENIC\",\n      \"categoryId\": 4,\n      \"url\": \"https://leadgenic.com/\",\n      \"companyId\": \"leadgenic\"\n    },\n    \"leadhit\": {\n      \"name\": \"LeadHit\",\n      \"categoryId\": 2,\n      \"url\": \"http://leadhit.ru/\",\n      \"companyId\": \"leadhit\"\n    },\n    \"leadin\": {\n      \"name\": \"Leadin\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.hubspot.com/\",\n      \"companyId\": \"hubspot\"\n    },\n    \"leading_reports\": {\n      \"name\": \"Leading Reports\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.leadingreports.de/\",\n      \"companyId\": \"leading_reports\"\n    },\n    \"leadinspector\": {\n      \"name\": \"LeadInspector\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.leadinspector.de/\",\n      \"companyId\": \"leadinspector\"\n    },\n    \"leadlander\": {\n      \"name\": \"LeadLander\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.leadlander.com/\",\n      \"companyId\": \"leadlander\"\n    },\n    \"leadlife\": {\n      \"name\": \"LeadLife\",\n      \"categoryId\": 2,\n      \"url\": \"http://leadlife.com/\",\n      \"companyId\": \"leadlife\"\n    },\n    \"leadpages\": {\n      \"name\": \"Leadpages\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.leadpages.net/\",\n      \"companyId\": \"leadpages\"\n    },\n    \"leadplace\": {\n      \"name\": \"LeadPlace\",\n      \"categoryId\": 6,\n      \"url\": \"https://temelio.com\",\n      \"companyId\": \"leadplace\"\n    },\n    \"leads_by_web.com\": {\n      \"name\": \"Leads by Web.com\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.leadsbyweb.com\",\n      \"companyId\": \"web.com_group\"\n    },\n    \"leadscoreapp\": {\n      \"name\": \"LeadScoreApp\",\n      \"categoryId\": 2,\n      \"url\": \"http://leadscoreapp.com\",\n      \"companyId\": \"leadscoreapp\"\n    },\n    \"leadsius\": {\n      \"name\": \"Leadsius\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.leadsius.com/\",\n      \"companyId\": \"leadsius\"\n    },\n    \"leady\": {\n      \"name\": \"Leady\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.leady.cz/\",\n      \"companyId\": \"leady\"\n    },\n    \"leiki\": {\n      \"name\": \"Leiki\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.leiki.com\",\n      \"companyId\": \"leiki\"\n    },\n    \"lengow\": {\n      \"name\": \"Lengow\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.lengow.com/\",\n      \"companyId\": \"lengow\"\n    },\n    \"lenmit.com\": {\n      \"name\": \"lenmit.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"lentainform.com\": {\n      \"name\": \"lentainform.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.lentainform.com/\",\n      \"companyId\": null\n    },\n    \"lenua.de\": {\n      \"name\": \"Lenua System\",\n      \"categoryId\": 4,\n      \"url\": \"http://lenua.de/\",\n      \"companyId\": \"synatix\"\n    },\n    \"let_reach\": {\n      \"name\": \"Let Reach\",\n      \"categoryId\": 2,\n      \"url\": \"https://letreach.com/\",\n      \"companyId\": \"let_reach\"\n    },\n    \"lets_encrypt\": {\n      \"name\": \"Let's Encrypt\",\n      \"categoryId\": 5,\n      \"url\": \"https://letsencrypt.org/\",\n      \"companyId\": \"isrg\",\n      \"source\": \"AdGuard\"\n    },\n    \"letv\": {\n      \"name\": \"LeTV\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.le.com/\",\n      \"companyId\": \"letv\"\n    },\n    \"level3_communications\": {\n      \"name\": \"Level 3 Communications, Inc.\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.level3.com/en/\",\n      \"companyId\": \"level3_communications\"\n    },\n    \"lgads\": {\n      \"name\": \"LG Ad Solutions\",\n      \"categoryId\": 4,\n      \"url\": \"https://lgads.tv/\",\n      \"companyId\": \"lgcorp\",\n      \"source\": \"AdGuard\"\n    },\n    \"lgtv\": {\n      \"name\": \"LG TV\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.lg.com/\",\n      \"companyId\": \"lgcorp\",\n      \"source\": \"AdGuard\"\n    },\n    \"licensebuttons.net\": {\n      \"name\": \"licensebuttons.net\",\n      \"categoryId\": 9,\n      \"url\": \"https://licensebuttons.net/\",\n      \"companyId\": null\n    },\n    \"lifestreet_media\": {\n      \"name\": \"LifeStreet Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://lifestreetmedia.com/\",\n      \"companyId\": \"lifestreet_media\"\n    },\n    \"ligatus\": {\n      \"name\": \"Ligatus\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ligatus.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"limk\": {\n      \"name\": \"Limk\",\n      \"categoryId\": 4,\n      \"url\": \"https://limk.com/\",\n      \"companyId\": \"limk\"\n    },\n    \"line_apps\": {\n      \"name\": \"Line\",\n      \"categoryId\": 6,\n      \"url\": \"https://line.me/en-US/\",\n      \"companyId\": \"line\"\n    },\n    \"linezing\": {\n      \"name\": \"LineZing\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.linezing.com/\",\n      \"companyId\": \"linezing\"\n    },\n    \"linkbucks\": {\n      \"name\": \"Linkbucks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.linkbucks.com/\",\n      \"companyId\": \"linkbucks\"\n    },\n    \"linkconnector\": {\n      \"name\": \"LinkConnector\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.linkconnector.com\",\n      \"companyId\": \"linkconnector\"\n    },\n    \"linkedin\": {\n      \"name\": \"LinkedIn\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.linkedin.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"linkedin_ads\": {\n      \"name\": \"LinkedIn Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.linkedin.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"linkedin_analytics\": {\n      \"name\": \"LinkedIn Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"linkedin_marketing_solutions\": {\n      \"name\": \"LinkedIn Marketing Solutions\",\n      \"categoryId\": 4,\n      \"url\": \"https://business.linkedin.com/marketing-solutions\",\n      \"companyId\": \"microsoft\"\n    },\n    \"linkedin_widgets\": {\n      \"name\": \"LinkedIn Widgets\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.linkedin.com\",\n      \"companyId\": \"microsoft\"\n    },\n    \"linker\": {\n      \"name\": \"Linker\",\n      \"categoryId\": 4,\n      \"url\": \"https://linker.hr/\",\n      \"companyId\": \"linker\"\n    },\n    \"linkprice\": {\n      \"name\": \"LinkPrice\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.linkprice.com/\",\n      \"companyId\": \"linkprice\"\n    },\n    \"linkpulse\": {\n      \"name\": \"Linkpulse\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.linkpulse.com/\",\n      \"companyId\": \"linkpulse\"\n    },\n    \"linksalpha\": {\n      \"name\": \"LinksAlpha\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.linksalpha.com\",\n      \"companyId\": \"linksalpha\"\n    },\n    \"linksmart\": {\n      \"name\": \"LinkSmart\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.linksmart.com/\",\n      \"companyId\": \"sovrn\"\n    },\n    \"linkstorm\": {\n      \"name\": \"Linkstorm\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.linkstorms.com/\",\n      \"companyId\": \"linkstorm\"\n    },\n    \"linksynergy.com\": {\n      \"name\": \"Rakuten LinkShare\",\n      \"categoryId\": 4,\n      \"url\": \"https://rakutenmarketing.com/affiliate\",\n      \"companyId\": \"rakuten\"\n    },\n    \"linkup\": {\n      \"name\": \"LinkUp\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.linkup.com/\",\n      \"companyId\": \"linkup\"\n    },\n    \"linkwise\": {\n      \"name\": \"Linkwise\",\n      \"categoryId\": 4,\n      \"url\": \"http://linkwi.se/global-en/\",\n      \"companyId\": \"linkwise\"\n    },\n    \"linkwithin\": {\n      \"name\": \"LinkWithin\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.linkwithin.com/\",\n      \"companyId\": \"linkwithin\"\n    },\n    \"liquidm_technology_gmbh\": {\n      \"name\": \"LiquidM Technology GmbH\",\n      \"categoryId\": 4,\n      \"url\": \"https://liquidm.com/\",\n      \"companyId\": \"liquidm\"\n    },\n    \"liqwid\": {\n      \"name\": \"Liqwid\",\n      \"categoryId\": 4,\n      \"url\": \"https://liqwid.com/\",\n      \"companyId\": \"liqwid\"\n    },\n    \"list.ru\": {\n      \"name\": \"Rating@Mail.Ru\",\n      \"categoryId\": 7,\n      \"url\": \"http://list.ru/\",\n      \"companyId\": \"megafon\"\n    },\n    \"listrak\": {\n      \"name\": \"Listrak\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.listrak.com/\",\n      \"companyId\": \"listrak\"\n    },\n    \"live2support\": {\n      \"name\": \"Live2Support\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.live2support.com/\",\n      \"companyId\": \"live2support\"\n    },\n    \"live800\": {\n      \"name\": \"Live800\",\n      \"categoryId\": 2,\n      \"url\": \"http://live800.com\",\n      \"companyId\": \"live800\"\n    },\n    \"live_agent\": {\n      \"name\": \"Live Agent\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.ladesk.com/\",\n      \"companyId\": \"liveagent\"\n    },\n    \"live_help_now\": {\n      \"name\": \"Live Help Now\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.livehelpnow.net/\",\n      \"companyId\": \"live_help_now\"\n    },\n    \"live_intent\": {\n      \"name\": \"Live Intent\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.liveintent.com/\",\n      \"companyId\": \"liveintent\"\n    },\n    \"live_journal\": {\n      \"name\": \"Live Journal\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.livejournal.com/\",\n      \"companyId\": \"livejournal\"\n    },\n    \"liveadexchanger.com\": {\n      \"name\": \"liveadexchanger.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"livechat\": {\n      \"name\": \"LiveChat\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.livechatinc.com\",\n      \"companyId\": \"livechat\"\n    },\n    \"livechatnow\": {\n      \"name\": \"LiveChatNow\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.livechatnow.com/\",\n      \"companyId\": \"livechatnow!\"\n    },\n    \"liveclicker\": {\n      \"name\": \"Liveclicker\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.liveclicker.com\",\n      \"companyId\": \"liveclicker\"\n    },\n    \"livecounter\": {\n      \"name\": \"Livecounter\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.livecounter.dk/\",\n      \"companyId\": \"livecounter\"\n    },\n    \"livefyre\": {\n      \"name\": \"Livefyre\",\n      \"categoryId\": 1,\n      \"url\": \"http://www.livefyre.com/\",\n      \"companyId\": \"adobe\"\n    },\n    \"liveinternet\": {\n      \"name\": \"LiveInternet\",\n      \"categoryId\": 1,\n      \"url\": \"http://www.liveinternet.ru/\",\n      \"companyId\": \"liveinternet\"\n    },\n    \"liveperson\": {\n      \"name\": \"LivePerson\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.liveperson.com/\",\n      \"companyId\": \"liveperson\"\n    },\n    \"liveramp\": {\n      \"name\": \"LiveRamp\",\n      \"categoryId\": 4,\n      \"url\": \"https://liveramp.com/\",\n      \"companyId\": \"acxiom\"\n    },\n    \"livere\": {\n      \"name\": \"LiveRe\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.livere.com/\",\n      \"companyId\": \"livere\"\n    },\n    \"livesportmedia.eu\": {\n      \"name\": \"Livesport Media\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.livesportmedia.eu/\",\n      \"companyId\": null\n    },\n    \"livestream\": {\n      \"name\": \"Livestream\",\n      \"categoryId\": 0,\n      \"url\": \"http://vimeo.com/\",\n      \"companyId\": \"vimeo\"\n    },\n    \"livetex.ru\": {\n      \"name\": \"LiveTex\",\n      \"categoryId\": 2,\n      \"url\": \"https://livetex.ru/\",\n      \"companyId\": \"livetex\"\n    },\n    \"lkqd\": {\n      \"name\": \"LKQD\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.lkqd.com/\",\n      \"companyId\": \"nexstar\"\n    },\n    \"loadbee.com\": {\n      \"name\": \"Loadbee\",\n      \"categoryId\": 4,\n      \"url\": \"https://company.loadbee.com/de/loadbee-home\",\n      \"companyId\": null\n    },\n    \"loadercdn.com\": {\n      \"name\": \"loadercdn.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"loadsource.org\": {\n      \"name\": \"loadsource.org\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"localytics\": {\n      \"name\": \"Localytics\",\n      \"categoryId\": 101,\n      \"url\": \"https://uplandsoftware.com/localytics/\",\n      \"companyId\": \"upland\",\n      \"source\": \"AdGuard\"\n    },\n    \"lockerdome\": {\n      \"name\": \"LockerDome\",\n      \"categoryId\": 7,\n      \"url\": \"https://lockerdome.com\",\n      \"companyId\": \"lockerdome\"\n    },\n    \"lockerz_share\": {\n      \"name\": \"AddToAny\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.addtoany.com/\",\n      \"companyId\": \"addtoany\"\n    },\n    \"logan_media\": {\n      \"name\": \"Logan Media\",\n      \"categoryId\": 6,\n      \"url\": \"http://loganmedia.mobi/\",\n      \"companyId\": \"logan_media\"\n    },\n    \"logdna\": {\n      \"name\": \"LogDNA\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.answerbook.com/\",\n      \"companyId\": \"logdna\"\n    },\n    \"loggly\": {\n      \"name\": \"Loggly\",\n      \"categoryId\": 6,\n      \"url\": \"http://loggly.com/\",\n      \"companyId\": \"loggly\"\n    },\n    \"logly\": {\n      \"name\": \"logly\",\n      \"categoryId\": 6,\n      \"url\": \"http://logly.co.jp/\",\n      \"companyId\": \"logly\"\n    },\n    \"logsss.com\": {\n      \"name\": \"logsss.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"lomadee\": {\n      \"name\": \"Lomadee\",\n      \"categoryId\": 4,\n      \"url\": \"http://lomadee.com\",\n      \"companyId\": \"lomadee\"\n    },\n    \"longtail_video_analytics\": {\n      \"name\": \"JW Player Analytics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.longtailvideo.com/\",\n      \"companyId\": \"jw_player\"\n    },\n    \"loomia\": {\n      \"name\": \"Loomia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.loomia.com/\",\n      \"companyId\": \"loomia\"\n    },\n    \"loop11\": {\n      \"name\": \"Loop11\",\n      \"categoryId\": 6,\n      \"url\": \"https://360i.com/\",\n      \"companyId\": \"360i\"\n    },\n    \"loopfuse_oneview\": {\n      \"name\": \"LoopFuse OneView\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.loopfuse.com/\",\n      \"companyId\": \"loopfuse\"\n    },\n    \"lotame\": {\n      \"name\": \"Lotame\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.lotame.com/\",\n      \"companyId\": \"lotame\"\n    },\n    \"lottex_inc\": {\n      \"name\": \"vidcpm.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"lucid\": {\n      \"name\": \"Lucid\",\n      \"categoryId\": 4,\n      \"url\": \"https://luc.id/\",\n      \"companyId\": \"luc.id\"\n    },\n    \"lucid_media\": {\n      \"name\": \"Lucid Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.lucidmedia.com/\",\n      \"companyId\": \"singtel\"\n    },\n    \"lucini\": {\n      \"name\": \"Lucini\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.lucinilucini.com/\",\n      \"companyId\": \"lucini_&_lucini_communications\"\n    },\n    \"lucky_orange\": {\n      \"name\": \"Lucky Orange\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.luckyorange.com/\",\n      \"companyId\": \"lucky_orange\"\n    },\n    \"luckypushh.com\": {\n      \"name\": \"luckypushh.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"lxr100\": {\n      \"name\": \"LXR100\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netelixir.com/lxr100_PPC_management_tool.html\",\n      \"companyId\": \"netelixir\"\n    },\n    \"lynchpin_analytics\": {\n      \"name\": \"Lynchpin Analytics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.lynchpin.com/\",\n      \"companyId\": \"lynchpin_analytics\"\n    },\n    \"lytics\": {\n      \"name\": \"Lytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.lytics.com/\",\n      \"companyId\": \"lytics\"\n    },\n    \"lyuoaxruaqdo.com\": {\n      \"name\": \"lyuoaxruaqdo.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"m-pathy\": {\n      \"name\": \"m-pathy\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.m-pathy.com/\",\n      \"companyId\": \"m-pathy\"\n    },\n    \"m._p._newmedia\": {\n      \"name\": \"M. P. NEWMEDIA\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mp-newmedia.com/\",\n      \"companyId\": \"sticky\"\n    },\n    \"m4n\": {\n      \"name\": \"M4N\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.zanox.com/us/\",\n      \"companyId\": \"axel_springer\"\n    },\n    \"mad_ads_media\": {\n      \"name\": \"Mad Ads Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.madadsmedia.com/\",\n      \"companyId\": \"mad_ads_media\"\n    },\n    \"madeleine.de\": {\n      \"name\": \"madeleine.de\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"madison_logic\": {\n      \"name\": \"Madison Logic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.madisonlogic.com/\",\n      \"companyId\": \"madison_logic\"\n    },\n    \"madnet\": {\n      \"name\": \"MADNET\",\n      \"categoryId\": 4,\n      \"url\": \"http://madnet.ru/en\",\n      \"companyId\": \"madnet\"\n    },\n    \"mads\": {\n      \"name\": \"MADS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mads.com/\",\n      \"companyId\": \"mads\"\n    },\n    \"magna_advertise\": {\n      \"name\": \"Magna Advertise\",\n      \"categoryId\": 4,\n      \"url\": \"http://magna.ru/\",\n      \"companyId\": \"magna_advertise\"\n    },\n    \"magnetic\": {\n      \"name\": \"Magnetic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.magnetic.is\",\n      \"companyId\": \"magnetic\"\n    },\n    \"magnetise_group\": {\n      \"name\": \"Magnetise Group\",\n      \"categoryId\": 4,\n      \"url\": \"http://magnetisegroup.com/\",\n      \"companyId\": \"magnetise_group\"\n    },\n    \"magnify360\": {\n      \"name\": \"Magnify360\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.magnify360.com/\",\n      \"companyId\": \"magnify360\"\n    },\n    \"magnuum.com\": {\n      \"name\": \"magnuum.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"mail.ru_banner\": {\n      \"name\": \"Mail.Ru Banner Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://mail.ru/\",\n      \"companyId\": \"vk\",\n      \"source\": \"AdGuard\"\n    },\n    \"mail.ru_counter\": {\n      \"name\": \"Mail.Ru Counter\",\n      \"categoryId\": 2,\n      \"url\": \"http://mail.ru/\",\n      \"companyId\": \"vk\",\n      \"source\": \"AdGuard\"\n    },\n    \"mail.ru_group\": {\n      \"name\": \"Mail.Ru Group\",\n      \"categoryId\": 7,\n      \"url\": \"http://mail.ru/\",\n      \"companyId\": \"vk\",\n      \"source\": \"AdGuard\"\n    },\n    \"mailchimp_tracking\": {\n      \"name\": \"MailChimp Tracking\",\n      \"categoryId\": 4,\n      \"url\": \"http://mailchimp.com/\",\n      \"companyId\": \"mailchimp\"\n    },\n    \"mailerlite.com\": {\n      \"name\": \"Mailerlite\",\n      \"categoryId\": 10,\n      \"url\": \"https://www.mailerlite.com/\",\n      \"companyId\": \"mailerlite\"\n    },\n    \"mailtrack.io\": {\n      \"name\": \"MailTrack.io\",\n      \"categoryId\": 4,\n      \"url\": \"https://mailtrack.io\",\n      \"companyId\": \"mailtrack\"\n    },\n    \"mainadv\": {\n      \"name\": \"mainADV\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mainadv.com/\",\n      \"companyId\": \"mainadv\"\n    },\n    \"makazi\": {\n      \"name\": \"Makazi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.makazi.com/en/\",\n      \"companyId\": \"makazi_group\"\n    },\n    \"makeappdev.xyz\": {\n      \"name\": \"makeappdev.xyz\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"makesource.cool\": {\n      \"name\": \"makesource.cool\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"mango\": {\n      \"name\": \"Mango\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.mango-office.ru/\",\n      \"companyId\": \"mango_office\"\n    },\n    \"manycontacts\": {\n      \"name\": \"ManyContacts\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.manycontacts.com/\",\n      \"companyId\": \"manycontacts\"\n    },\n    \"mapandroute.de\": {\n      \"name\": \"Map and Route\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.mapandroute.de/\",\n      \"companyId\": null\n    },\n    \"mapbox\": {\n      \"name\": \"Mapbox\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.mapbox.com/\",\n      \"companyId\": null\n    },\n    \"maploco\": {\n      \"name\": \"MapLoco\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.maploco.com/\",\n      \"companyId\": \"maploco\"\n    },\n    \"marchex\": {\n      \"name\": \"Marchex\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.industrybrains.com/\",\n      \"companyId\": \"marchex\"\n    },\n    \"marimedia\": {\n      \"name\": \"Marimedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.marimedia.net/\",\n      \"companyId\": \"tremor_video\"\n    },\n    \"marin_search_marketer\": {\n      \"name\": \"Marin Search Marketer\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.marinsoftware.com/\",\n      \"companyId\": \"marin_software\"\n    },\n    \"mark_+_mini\": {\n      \"name\": \"Mark & Mini\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.markandmini.com/index.cfm\",\n      \"companyId\": \"edm_group\"\n    },\n    \"market_thunder\": {\n      \"name\": \"Market Thunder\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.makethunder.com/\",\n      \"companyId\": \"market_thunder\"\n    },\n    \"marketgid\": {\n      \"name\": \"MarketGid\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mgid.com/\",\n      \"companyId\": \"marketgid_usa\"\n    },\n    \"marketing_automation\": {\n      \"name\": \"Marketing Automation\",\n      \"categoryId\": 4,\n      \"url\": \"https://en.frodx.com\",\n      \"companyId\": \"frodx\"\n    },\n    \"marketo\": {\n      \"name\": \"Marketo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.marketo.com/\",\n      \"companyId\": \"marketo\"\n    },\n    \"markmonitor\": {\n      \"name\": \"MarkMonitor\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.markmonitor.com/\",\n      \"companyId\": \"markmonitor\",\n      \"source\": \"AdGuard\"\n    },\n    \"marktest\": {\n      \"name\": \"Marktest\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.marktest.com/\",\n      \"companyId\": \"marktest_group\"\n    },\n    \"marshadow.io\": {\n      \"name\": \"marshadow.io\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"martini_media\": {\n      \"name\": \"Martini Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://martinimediainc.com/\",\n      \"companyId\": \"martini_media\"\n    },\n    \"maru-edu\": {\n      \"name\": \"Maru-EDU\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.maruedr.com\",\n      \"companyId\": \"maruedr\"\n    },\n    \"marvellous_machine\": {\n      \"name\": \"Marvellous Machine\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.marvellousmachine.net/\",\n      \"companyId\": \"marvellous_machine\"\n    },\n    \"master_banner_network\": {\n      \"name\": \"Master Banner Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mbn.com.ua/\",\n      \"companyId\": \"master_banner_network\"\n    },\n    \"mastertarget\": {\n      \"name\": \"MasterTarget\",\n      \"categoryId\": 4,\n      \"url\": \"http://mastertarget.ru/\",\n      \"companyId\": \"mastertarget\"\n    },\n    \"matelso\": {\n      \"name\": \"Matelso\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.matelso.de\",\n      \"companyId\": \"matelso\"\n    },\n    \"mather_analytics\": {\n      \"name\": \"Mather Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.mathereconomics.com/\",\n      \"companyId\": \"mather_economics\"\n    },\n    \"mathjax.org\": {\n      \"name\": \"MathJax\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.mathjax.org/\",\n      \"companyId\": null\n    },\n    \"matiro\": {\n      \"name\": \"Matiro\",\n      \"categoryId\": 6,\n      \"url\": \"http://matiro.com/\",\n      \"companyId\": \"matiro\"\n    },\n    \"matomo\": {\n      \"name\": \"Matomo\",\n      \"categoryId\": 6,\n      \"url\": \"https://matomo.org/s\",\n      \"companyId\": \"matomo\"\n    },\n    \"matomy_market\": {\n      \"name\": \"Matomy Market\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.matomymarket.com/\",\n      \"companyId\": \"matomy_media\"\n    },\n    \"matrix\": {\n      \"name\": \"Matrix\",\n      \"categoryId\": 5,\n      \"url\": \"https://matrix.org/\",\n      \"companyId\": \"matrix\",\n      \"source\": \"AdGuard\"\n    },\n    \"maxbounty\": {\n      \"name\": \"MaxBounty\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.maxbounty.com/\",\n      \"companyId\": \"maxbounty\"\n    },\n    \"maxcdn\": {\n      \"name\": \"MaxCDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.maxcdn.com/\",\n      \"companyId\": null\n    },\n    \"maxlab\": {\n      \"name\": \"Maxlab\",\n      \"categoryId\": 4,\n      \"url\": \"http://maxlab.ru\",\n      \"companyId\": \"maxlab\"\n    },\n    \"maxmind\": {\n      \"name\": \"MaxMind\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.maxmind.com/\",\n      \"companyId\": \"maxmind\"\n    },\n    \"maxonclick_com\": {\n      \"name\": \"maxonclick.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"maxpoint_interactive\": {\n      \"name\": \"MaxPoint Interactive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.maxpointinteractive.com/\",\n      \"companyId\": \"maxpoint_interactive\"\n    },\n    \"maxymiser\": {\n      \"name\": \"Oracle Maxymiser\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.oracle.com/marketingcloud/products/testing-and-optimization/index.html\",\n      \"companyId\": \"oracle\"\n    },\n    \"mbr_targeting\": {\n      \"name\": \"mbr targeting\",\n      \"categoryId\": 4,\n      \"url\": \"https://mbr-targeting.com/\",\n      \"companyId\": \"stroer\"\n    },\n    \"mbuy\": {\n      \"name\": \"MBuy\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adbuyer.com/\",\n      \"companyId\": \"mbuy\"\n    },\n    \"mcabi\": {\n      \"name\": \"mCabi\",\n      \"categoryId\": 4,\n      \"url\": \"https://mcabi.mcloudglobal.com/#\",\n      \"companyId\": \"mcabi\"\n    },\n    \"mcafee_secure\": {\n      \"name\": \"McAfee Secure\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.mcafeesecure.com/us/\",\n      \"companyId\": \"mcafee\"\n    },\n    \"mconet\": {\n      \"name\": \"MCOnet\",\n      \"categoryId\": 4,\n      \"url\": \"http://mconet.biz/\",\n      \"companyId\": \"mconet\"\n    },\n    \"mdotlabs\": {\n      \"name\": \"MdotLabs\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mdotlabs.com/\",\n      \"companyId\": \"comscore\"\n    },\n    \"media-clic\": {\n      \"name\": \"Media-clic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.media-clic.com/\",\n      \"companyId\": \"media-click\"\n    },\n    \"media-imdb.com\": {\n      \"name\": \"IMDB CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.imdb.com/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"media.net\": {\n      \"name\": \"Media.net\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.media.net/\",\n      \"companyId\": \"media.net\"\n    },\n    \"media_impact\": {\n      \"name\": \"Media Impact\",\n      \"categoryId\": 4,\n      \"url\": \"https://mediaimpact.de/index.html\",\n      \"companyId\": \"media_impact\"\n    },\n    \"media_innovation_group\": {\n      \"name\": \"Xaxis\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.xaxis.com/\",\n      \"companyId\": \"media_innovation_group\"\n    },\n    \"media_today\": {\n      \"name\": \"Media Today\",\n      \"categoryId\": 4,\n      \"url\": \"http://mediatoday.ru/\",\n      \"companyId\": \"media_today\"\n    },\n    \"mediaad\": {\n      \"name\": \"MediaAd\",\n      \"categoryId\": 4,\n      \"url\": \"https://mediaad.org\",\n      \"companyId\": \"mediaad\"\n    },\n    \"mediaglu\": {\n      \"name\": \"MediaGlu\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.mediaglu.com/\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"mediahub\": {\n      \"name\": \"MediaHub\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mediahub.com/\",\n      \"companyId\": \"mediahub\"\n    },\n    \"medialab\": {\n      \"name\": \"MediaLab.AI Inc.\",\n      \"categoryId\": 8,\n      \"url\": \"https://medialab.la/\",\n      \"companyId\": \"medialab\",\n      \"source\": \"AdGuard\"\n    },\n    \"medialand\": {\n      \"name\": \"Medialand\",\n      \"categoryId\": 4,\n      \"url\": \"http://medialand.ru\",\n      \"companyId\": \"medialand\"\n    },\n    \"medialead\": {\n      \"name\": \"Medialead\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.medialead.de/\",\n      \"companyId\": \"the_reach_group\"\n    },\n    \"mediamath\": {\n      \"name\": \"MediaMath\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mediamath.com/\",\n      \"companyId\": \"mediamath\"\n    },\n    \"mediametrics\": {\n      \"name\": \"Mediametrics\",\n      \"categoryId\": 7,\n      \"url\": \"http://mediametrics.ru\",\n      \"companyId\": \"mediametrics\"\n    },\n    \"median\": {\n      \"name\": \"Median\",\n      \"categoryId\": 4,\n      \"url\": \"http://median.hu\",\n      \"companyId\": \"median\"\n    },\n    \"mediapass\": {\n      \"name\": \"MediaPass\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mediapass.com/\",\n      \"companyId\": \"mediapass\"\n    },\n    \"mediapost_communications\": {\n      \"name\": \"Mediapost Communications\",\n      \"categoryId\": 6,\n      \"url\": \"https://vrm.mediapostcommunication.net/\",\n      \"companyId\": \"mediapost_communications\"\n    },\n    \"mediarithmics.com\": {\n      \"name\": \"Mediarithmics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mediarithmics.com/en/\",\n      \"companyId\": \"mediarithmics\"\n    },\n    \"mediascope\": {\n      \"name\": \"Mediascope\",\n      \"categoryId\": 6,\n      \"url\": \"http://mediascope.net/\",\n      \"companyId\": \"mediascope\"\n    },\n    \"mediashakers\": {\n      \"name\": \"MediaShakers\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mediashakers.com/\",\n      \"companyId\": \"mediashakers\"\n    },\n    \"mediashift\": {\n      \"name\": \"MediaShift\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mediashift.com/\",\n      \"companyId\": \"mediashift\"\n    },\n    \"mediator.media\": {\n      \"name\": \"Mediator\",\n      \"categoryId\": 6,\n      \"url\": \"https://mediator.media/\",\n      \"companyId\": \"mycom_bv\"\n    },\n    \"mediav\": {\n      \"name\": \"MediaV\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.mediav.com/\",\n      \"companyId\": \"mediav\"\n    },\n    \"mediawhiz\": {\n      \"name\": \"Mediawhiz\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mediawhiz.com/\",\n      \"companyId\": \"matomy_media\"\n    },\n    \"medigo\": {\n      \"name\": \"Medigo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.mediego.com/en/\",\n      \"companyId\": \"mediego\"\n    },\n    \"medley\": {\n      \"name\": \"Medley\",\n      \"categoryId\": 4,\n      \"url\": \"http://medley.com/\",\n      \"companyId\": \"friendfinder_networks\"\n    },\n    \"medyanet\": {\n      \"name\": \"MedyaNet\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.medyanet.com.tr/\",\n      \"companyId\": \"medyanet\"\n    },\n    \"meebo_bar\": {\n      \"name\": \"Meebo Bar\",\n      \"categoryId\": 7,\n      \"url\": \"http://bar.meebo.com/\",\n      \"companyId\": \"google\"\n    },\n    \"meetrics\": {\n      \"name\": \"Meetrics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.meetrics.de/\",\n      \"companyId\": \"meetrics\"\n    },\n    \"megaindex\": {\n      \"name\": \"MegaIndex\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.megaindex.ru\",\n      \"companyId\": \"megaindex\"\n    },\n    \"meganz\": {\n      \"name\": \"Mega Ltd.\",\n      \"categoryId\": 8,\n      \"url\": \"https://mega.io/\",\n      \"companyId\": \"meganz\",\n      \"source\": \"AdGuard\"\n    },\n    \"mein-bmi.com\": {\n      \"name\": \"mein-bmi.com\",\n      \"categoryId\": 12,\n      \"url\": \"https://www.mein-bmi.com/\",\n      \"companyId\": null\n    },\n    \"melissa\": {\n      \"name\": \"Melissa\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.melissa.com/\",\n      \"companyId\": \"melissa_global_intelligence\"\n    },\n    \"melt\": {\n      \"name\": \"Melt\",\n      \"categoryId\": 4,\n      \"url\": \"http://meltdsp.com/\",\n      \"companyId\": \"melt\"\n    },\n    \"menlo\": {\n      \"name\": \"Menlo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.menlotechnologies.cn/\",\n      \"companyId\": \"menlotechnologies\"\n    },\n    \"mentad\": {\n      \"name\": \"MentAd\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mentad.com/\",\n      \"companyId\": \"mentad\"\n    },\n    \"mercado\": {\n      \"name\": \"Mercado\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.mercadolivre.com.br/\",\n      \"companyId\": \"mercado_livre\"\n    },\n    \"merchantadvantage\": {\n      \"name\": \"MerchantAdvantage\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.merchantadvantage.com/channelmanagement.cfm\",\n      \"companyId\": \"merchantadvantage\"\n    },\n    \"merchenta\": {\n      \"name\": \"Merchenta\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.merchenta.com/\",\n      \"companyId\": \"merchenta\"\n    },\n    \"mercury_media\": {\n      \"name\": \"Mercury Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://trackingsoft.com/\",\n      \"companyId\": \"mercury_media\"\n    },\n    \"merkle_research\": {\n      \"name\": \"Merkle Research\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.dentsuaegisnetwork.com/\",\n      \"companyId\": \"dentsu_aegis_network\"\n    },\n    \"merkle_rkg\": {\n      \"name\": \"Merkle RKG\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.merkleinc.com/what-we-do/digital-agency-services/rkg-now-fully-integrated-merkle\",\n      \"companyId\": \"dentsu_aegis_network\"\n    },\n    \"messenger.com\": {\n      \"name\": \"Facebook Messenger\",\n      \"categoryId\": 7,\n      \"url\": \"https://messenger.com\",\n      \"companyId\": \"facebook\"\n    },\n    \"meta_network\": {\n      \"name\": \"Meta Network\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.metanetwork.com/\",\n      \"companyId\": \"meta_network\"\n    },\n    \"metaffiliation.com\": {\n      \"name\": \"Netaffiliation\",\n      \"categoryId\": 4,\n      \"url\": \"http://netaffiliation.com/\",\n      \"companyId\": \"kwanko\"\n    },\n    \"metapeople\": {\n      \"name\": \"Metapeople\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.metapeople.com/us/\",\n      \"companyId\": \"metapeople\"\n    },\n    \"metrigo\": {\n      \"name\": \"Metrigo\",\n      \"categoryId\": 4,\n      \"url\": \"http://metrigo.com/\",\n      \"companyId\": \"metrigo\"\n    },\n    \"metriweb\": {\n      \"name\": \"MetriWeb\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.metriware.be/\",\n      \"companyId\": \"metriware\"\n    },\n    \"miaozhen\": {\n      \"name\": \"Miaozhen\",\n      \"categoryId\": 4,\n      \"url\": \"http://miaozhen.com/en/index.html\",\n      \"companyId\": \"miaozhen\"\n    },\n    \"microad\": {\n      \"name\": \"MicroAd\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.microad.co.jp/\",\n      \"companyId\": \"microad\"\n    },\n    \"microsoft\": {\n      \"name\": \"Microsoft Services\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"microsoft_adcenter_conversion\": {\n      \"name\": \"Microsoft adCenter Conversion\",\n      \"categoryId\": 4,\n      \"url\": \"https://adcenter.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"microsoft_analytics\": {\n      \"name\": \"Microsoft Analytics\",\n      \"categoryId\": 4,\n      \"url\": \"https://adcenter.microsoft.com\",\n      \"companyId\": \"microsoft\"\n    },\n    \"microsoft_clarity\": {\n      \"name\": \"Microsoft Clarity\",\n      \"categoryId\": 6,\n      \"url\": \"https://clarity.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"mindset_media\": {\n      \"name\": \"Mindset Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mindset-media.com/\",\n      \"companyId\": \"google\"\n    },\n    \"mindspark\": {\n      \"name\": \"Mindspark\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.mindspark.com/\",\n      \"companyId\": \"iac_apps\"\n    },\n    \"mindviz_tracker\": {\n      \"name\": \"MindViz Tracker\",\n      \"categoryId\": 4,\n      \"url\": \"http://mvtracker.com/\",\n      \"companyId\": \"mindviz\"\n    },\n    \"minewhat\": {\n      \"name\": \"MineWhat\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.minewhat.com\",\n      \"companyId\": \"minewhat\"\n    },\n    \"mints_app\": {\n      \"name\": \"Mints App\",\n      \"categoryId\": 2,\n      \"url\": \"https://mintsapp.io/\",\n      \"companyId\": \"mints_app\"\n    },\n    \"minute.ly\": {\n      \"name\": \"minute.ly\",\n      \"categoryId\": 0,\n      \"url\": \"http://minute.ly/\",\n      \"companyId\": \"minute.ly\"\n    },\n    \"minute.ly_video\": {\n      \"name\": \"minute.ly video\",\n      \"categoryId\": 0,\n      \"url\": \"http://minute.ly/\",\n      \"companyId\": \"minute.ly\"\n    },\n    \"mirando\": {\n      \"name\": \"Mirando\",\n      \"categoryId\": 4,\n      \"url\": \"http://mirando.de\",\n      \"companyId\": \"mirando\"\n    },\n    \"mirtesen.ru\": {\n      \"name\": \"mirtesen.ru\",\n      \"categoryId\": 7,\n      \"url\": \"https://mirtesen.ru/\",\n      \"companyId\": null\n    },\n    \"mister_bell\": {\n      \"name\": \"Mister Bell\",\n      \"categoryId\": 4,\n      \"url\": \"http://misterbell.fr/\",\n      \"companyId\": \"mister_bell\"\n    },\n    \"mixi\": {\n      \"name\": \"mixi\",\n      \"categoryId\": 7,\n      \"url\": \"http://mixi.jp/\",\n      \"companyId\": \"mixi\"\n    },\n    \"mixpanel\": {\n      \"name\": \"Mixpanel\",\n      \"categoryId\": 6,\n      \"url\": \"http://mixpanel.com/\",\n      \"companyId\": \"mixpanel\"\n    },\n    \"mixpo\": {\n      \"name\": \"Mixpo\",\n      \"categoryId\": 4,\n      \"url\": \"http://dynamicvideoad.mixpo.com/\",\n      \"companyId\": \"mixpo\"\n    },\n    \"mluvii\": {\n      \"name\": \"Mluvii\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.mluvii.com\",\n      \"companyId\": \"mluvii\"\n    },\n    \"mncdn.com\": {\n      \"name\": \"MediaNova CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.medianova.com/\",\n      \"companyId\": null\n    },\n    \"moat\": {\n      \"name\": \"Moat\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.moat.com/\",\n      \"companyId\": \"oracle\"\n    },\n    \"mobicow\": {\n      \"name\": \"Mobicow\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mobicow.com/\",\n      \"companyId\": \"mobicow\"\n    },\n    \"mobify\": {\n      \"name\": \"Mobify\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mobify.com/\",\n      \"companyId\": \"mobify\"\n    },\n    \"mobtrks.com\": {\n      \"name\": \"mobtrks.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"mocean_mobile\": {\n      \"name\": \"mOcean Mobile\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.moceanmobile.com/\",\n      \"companyId\": \"pubmatic\"\n    },\n    \"mochapp\": {\n      \"name\": \"MoChapp\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.mochapp.com/\",\n      \"companyId\": \"mochapp\"\n    },\n    \"modern_impact\": {\n      \"name\": \"Modern Impact\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.modernimpact.com/\",\n      \"companyId\": \"modern_impact\"\n    },\n    \"modernus\": {\n      \"name\": \"Modernus\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.modernus.is\",\n      \"companyId\": \"modernus\"\n    },\n    \"modulepush.com\": {\n      \"name\": \"modulepush.com\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"mogo_interactive\": {\n      \"name\": \"Mogo Interactive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mogomarketing.com/\",\n      \"companyId\": \"mogo_interactive\"\n    },\n    \"mokono_analytics\": {\n      \"name\": \"Mokono Analytics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.populis.com\",\n      \"companyId\": \"populis\"\n    },\n    \"monero_miner\": {\n      \"name\": \"Monero Miner\",\n      \"categoryId\": 8,\n      \"url\": \"http://devappgrant.space/\",\n      \"companyId\": null\n    },\n    \"monetate\": {\n      \"name\": \"Monetate\",\n      \"categoryId\": 6,\n      \"url\": \"http://monetate.com\",\n      \"companyId\": \"monetate\"\n    },\n    \"monetize_me\": {\n      \"name\": \"Monetize Me\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.monetize-me.com/\",\n      \"companyId\": \"monetize_me\"\n    },\n    \"moneytizer\": {\n      \"name\": \"Moneytizer\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.themoneytizer.com/\",\n      \"companyId\": \"the_moneytizer\"\n    },\n    \"mongoose_metrics\": {\n      \"name\": \"Mongoose Metrics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mongoosemetrics.com/\",\n      \"companyId\": \"mongoose_metrics\"\n    },\n    \"monitis\": {\n      \"name\": \"Monitis\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.monitis.com/\",\n      \"companyId\": \"monitis\"\n    },\n    \"monitus\": {\n      \"name\": \"Monitus\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.monitus.net/\",\n      \"companyId\": \"monitus\"\n    },\n    \"monotype_gmbh\": {\n      \"name\": \"Monotype GmbH\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.monotype.com/\",\n      \"companyId\": \"monotype\"\n    },\n    \"monotype_imaging\": {\n      \"name\": \"Fonts.com Store\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.fonts.com/\",\n      \"companyId\": \"monotype\"\n    },\n    \"monsido\": {\n      \"name\": \"Monsido\",\n      \"categoryId\": 6,\n      \"url\": \"https://monsido.com/\",\n      \"companyId\": \"monsido\"\n    },\n    \"monster_advertising\": {\n      \"name\": \"Monster Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.monster.com/\",\n      \"companyId\": \"monster_worldwide\"\n    },\n    \"mooxar\": {\n      \"name\": \"Mooxar\",\n      \"categoryId\": 4,\n      \"url\": \"http://mooxar.com/\",\n      \"companyId\": \"mooxar\"\n    },\n    \"mopinion.com\": {\n      \"name\": \"Mopinion\",\n      \"categoryId\": 2,\n      \"url\": \"https://mopinion.com/\",\n      \"companyId\": \"mopinion\"\n    },\n    \"mopub\": {\n      \"name\": \"MoPub\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.mopub.com/\",\n      \"companyId\": \"twitter\"\n    },\n    \"more_communication\": {\n      \"name\": \"More Communication\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.more-com.co.jp/\",\n      \"companyId\": \"more_communication\"\n    },\n    \"moreads\": {\n      \"name\": \"moreAds\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.moras.jp\",\n      \"companyId\": \"moreads\"\n    },\n    \"motigo_webstats\": {\n      \"name\": \"Motigo Webstats\",\n      \"categoryId\": 7,\n      \"url\": \"http://webstats.motigo.com/\",\n      \"companyId\": \"motigo\"\n    },\n    \"motionpoint\": {\n      \"name\": \"MotionPoint\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.motionpoint.com/\",\n      \"companyId\": \"motionpoint_corporation\"\n    },\n    \"mouseflow\": {\n      \"name\": \"Mouseflow\",\n      \"categoryId\": 6,\n      \"url\": \"http://mouseflow.com/\",\n      \"companyId\": \"mouseflow\"\n    },\n    \"mousestats\": {\n      \"name\": \"MouseStats\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mousestats.com/\",\n      \"companyId\": \"mousestats\"\n    },\n    \"mousetrace\": {\n      \"name\": \"MouseTrace\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.mousetrace.com/\",\n      \"companyId\": \"mousetrace\"\n    },\n    \"mov.ad\": {\n      \"name\": \"Mov.ad \",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"movable_ink\": {\n      \"name\": \"Movable Ink\",\n      \"categoryId\": 2,\n      \"url\": \"https://movableink.com/\",\n      \"companyId\": \"movable_ink\"\n    },\n    \"movable_media\": {\n      \"name\": \"Movable Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.movablemedia.com/\",\n      \"companyId\": \"movable_media\"\n    },\n    \"moz\": {\n      \"name\": \"Moz\",\n      \"categoryId\": 8,\n      \"url\": \"https://moz.com/\",\n      \"companyId\": null\n    },\n    \"mozilla\": {\n      \"name\": \"Mozilla Foundation\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.mozilla.org/\",\n      \"companyId\": \"mozilla\",\n      \"source\": \"AdGuard\"\n    },\n    \"mozoo\": {\n      \"name\": \"MoZoo\",\n      \"categoryId\": 4,\n      \"url\": \"http://mozoo.com/\",\n      \"companyId\": \"mozoo\"\n    },\n    \"mrp\": {\n      \"name\": \"MRP\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.mrpfd.com/\",\n      \"companyId\": \"mrp\"\n    },\n    \"mrpdata\": {\n      \"name\": \"MRP\",\n      \"categoryId\": 6,\n      \"url\": \"http://mrpdata.com/Account/Login?ReturnUrl=%2F\",\n      \"companyId\": \"fifth_story\"\n    },\n    \"mrskincash\": {\n      \"name\": \"MrSkinCash\",\n      \"categoryId\": 3,\n      \"url\": \"http://mrskincash.com/\",\n      \"companyId\": \"mrskincash.com\"\n    },\n    \"msedge\": {\n      \"name\": \"Microsoft Edge\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.microsoft.com/en-us/edge\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"msn\": {\n      \"name\": \"Microsoft Network\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"muscula\": {\n      \"name\": \"Muscula\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.universe-surf.de/\",\n      \"companyId\": \"universe_surf\"\n    },\n    \"mux_inc\": {\n      \"name\": \"Mux\",\n      \"categoryId\": 0,\n      \"url\": \"https://mux.com/\",\n      \"companyId\": \"mux_inc\"\n    },\n    \"mybloglog\": {\n      \"name\": \"MyBlogLog\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.mybloglog.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"mybuys\": {\n      \"name\": \"MyBuys\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mybuys.com/\",\n      \"companyId\": \"magnetic\"\n    },\n    \"mycdn.me\": {\n      \"name\": \"Mail.Ru CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://corp.megafon.com/\",\n      \"companyId\": \"megafon\"\n    },\n    \"mycliplister.com\": {\n      \"name\": \"Cliplister\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.cliplister.com/\",\n      \"companyId\": null\n    },\n    \"mycounter.ua\": {\n      \"name\": \"MyCounter.ua\",\n      \"categoryId\": 6,\n      \"url\": \"http://mycounter.ua\",\n      \"companyId\": \"mycounter.ua\"\n    },\n    \"myfonts\": {\n      \"name\": \"MyFonts\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.myfonts.com/\",\n      \"companyId\": \"myfonts\"\n    },\n    \"myfonts_counter\": {\n      \"name\": \"MyFonts\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.myfonts.com/\",\n      \"companyId\": \"myfonts\"\n    },\n    \"mypagerank\": {\n      \"name\": \"MyPagerank\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.mypagerank.net/\",\n      \"companyId\": \"mypagerank\"\n    },\n    \"mystat\": {\n      \"name\": \"MyStat\",\n      \"categoryId\": 7,\n      \"url\": \"http://mystat.hu/\",\n      \"companyId\": \"myst_statistics\"\n    },\n    \"mythings\": {\n      \"name\": \"myThings\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mythings.com/\",\n      \"companyId\": \"mythings\"\n    },\n    \"mytop_counter\": {\n      \"name\": \"Mytop Counter\",\n      \"categoryId\": 7,\n      \"url\": \"http://mytop-in.net/\",\n      \"companyId\": \"mytop-in\"\n    },\n    \"nab\": {\n      \"name\": \"National Australia Bank\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.nab.com.au/\",\n      \"companyId\": \"nab\",\n      \"source\": \"AdGuard\"\n    },\n    \"nakanohito.jp\": {\n      \"name\": \"Nakanohito\",\n      \"categoryId\": 4,\n      \"url\": \"http://nakanohito.jp/\",\n      \"companyId\": \"userinsight\"\n    },\n    \"namogoo\": {\n      \"name\": \"Namoogoo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.namogoo.com/\",\n      \"companyId\": null\n    },\n    \"nanigans\": {\n      \"name\": \"Nanigans\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nanigans.com/\",\n      \"companyId\": \"nanigans\"\n    },\n    \"nano_interactive\": {\n      \"name\": \"Nano Interactive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nanointeractive.com/home/de\",\n      \"companyId\": \"nano_interactive\"\n    },\n    \"nanorep\": {\n      \"name\": \"nanoRep\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.nanorep.com/\",\n      \"companyId\": \"logmein\"\n    },\n    \"narando\": {\n      \"name\": \"Narando\",\n      \"categoryId\": 0,\n      \"url\": \"https://narando.com/\",\n      \"companyId\": \"narando\"\n    },\n    \"narrativ\": {\n      \"name\": \"Narrativ\",\n      \"categoryId\": 4,\n      \"url\": \"https://narrativ.com/\",\n      \"companyId\": \"narrativ\"\n    },\n    \"narrative_io\": {\n      \"name\": \"Narrative\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.narrative.io/\",\n      \"companyId\": \"narrative.io\"\n    },\n    \"natimatica\": {\n      \"name\": \"Natimatica\",\n      \"categoryId\": 4,\n      \"url\": \"http://natimatica.com/\",\n      \"companyId\": \"natimatica\"\n    },\n    \"nativeads.com\": {\n      \"name\": \"native ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://nativeads.com/\",\n      \"companyId\": null\n    },\n    \"nativeroll\": {\n      \"name\": \"Nativeroll\",\n      \"categoryId\": 0,\n      \"url\": \"http://nativeroll.tv/\",\n      \"companyId\": \"native_roll\"\n    },\n    \"nativo\": {\n      \"name\": \"Nativo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nativo.net/\",\n      \"companyId\": \"nativo\"\n    },\n    \"navegg_dmp\": {\n      \"name\": \"Navegg\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.navegg.com/en/\",\n      \"companyId\": \"navegg\"\n    },\n    \"naver.com\": {\n      \"name\": \"Naver\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.naver.com/\",\n      \"companyId\": \"naver\"\n    },\n    \"naver_search\": {\n      \"name\": \"Naver Search\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.naver.com/\",\n      \"companyId\": \"naver\"\n    },\n    \"nbc_news\": {\n      \"name\": \"NBC News\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.nbcnews.com/\",\n      \"companyId\": null\n    },\n    \"ncol\": {\n      \"name\": \"NCOL\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ncol.com/\",\n      \"companyId\": \"ncol\"\n    },\n    \"needle\": {\n      \"name\": \"Needle\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.needle.com\",\n      \"companyId\": \"needle\"\n    },\n    \"nekudo.com\": {\n      \"name\": \"Nekudo\",\n      \"categoryId\": 2,\n      \"url\": \"https://nekudo.com/\",\n      \"companyId\": \"nekudo\"\n    },\n    \"neodata\": {\n      \"name\": \"Neodata\",\n      \"categoryId\": 4,\n      \"url\": \"http://neodatagroup.com/\",\n      \"companyId\": \"neodata\"\n    },\n    \"neory\": {\n      \"name\": \"NEORY \",\n      \"categoryId\": 4,\n      \"url\": \"https://www.neory.com/\",\n      \"companyId\": \"neory\"\n    },\n    \"nerfherdersolo_com\": {\n      \"name\": \"nerfherdersolo.com\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"net-metrix\": {\n      \"name\": \"NET-Metrix\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.net-metrix.ch/\",\n      \"companyId\": \"net-metrix\"\n    },\n    \"net-results\": {\n      \"name\": \"Net-Results\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.net-results.com/\",\n      \"companyId\": \"net-results\"\n    },\n    \"net_avenir\": {\n      \"name\": \"Net Avenir\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netavenir.com/\",\n      \"companyId\": \"net_avenir\"\n    },\n    \"net_communities\": {\n      \"name\": \"Net Communities\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netcommunities.com/\",\n      \"companyId\": \"net_communities\"\n    },\n    \"net_visibility\": {\n      \"name\": \"NET Visibility\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netvisibility.co.uk\",\n      \"companyId\": \"net_visibility\"\n    },\n    \"netbiscuits\": {\n      \"name\": \"Netbiscuits\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.netbiscuits.net/\",\n      \"companyId\": \"netbiscuits\"\n    },\n    \"netbooster_group\": {\n      \"name\": \"NetBooster Group\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netbooster.com/\",\n      \"companyId\": \"netbooster_group\"\n    },\n    \"netflix\": {\n      \"name\": \"Netflix\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.netflix.com/\",\n      \"companyId\": \"netflix\",\n      \"source\": \"AdGuard\"\n    },\n    \"netify\": {\n      \"name\": \"Netify\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.netify.ai/\",\n      \"companyId\": \"netify\",\n      \"source\": \"AdGuard\"\n    },\n    \"netletix\": {\n      \"name\": \"Netletix\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netletix.com//\",\n      \"companyId\": \"ip_de\"\n    },\n    \"netminers\": {\n      \"name\": \"Netminers\",\n      \"categoryId\": 6,\n      \"url\": \"http://netminers.dk/\",\n      \"companyId\": \"netminers\"\n    },\n    \"netmining\": {\n      \"name\": \"Netmining\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netmining.com/\",\n      \"companyId\": \"zeta\"\n    },\n    \"netmonitor\": {\n      \"name\": \"NetMonitor\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.netmanager.net/en/\",\n      \"companyId\": \"netmonitor\"\n    },\n    \"netratings_sitecensus\": {\n      \"name\": \"NetRatings SiteCensus\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nielsen-online.com/intlpage.html\",\n      \"companyId\": \"nielsen\"\n    },\n    \"netrk.net\": {\n      \"name\": \"nfxTrack\",\n      \"categoryId\": 6,\n      \"url\": \"https://netrk.net/\",\n      \"companyId\": \"netzeffekt\"\n    },\n    \"netseer\": {\n      \"name\": \"NetSeer\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netseer.com/\",\n      \"companyId\": \"netseer\"\n    },\n    \"netshelter\": {\n      \"name\": \"NetShelter\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netshelter.net/\",\n      \"companyId\": \"netshelter\"\n    },\n    \"netsprint_audience\": {\n      \"name\": \"Netsprint Audience\",\n      \"categoryId\": 6,\n      \"url\": \"http://audience.netsprint.eu/\",\n      \"companyId\": \"netsprint\"\n    },\n    \"networkedblogs\": {\n      \"name\": \"NetworkedBlogs\",\n      \"categoryId\": 7,\n      \"url\": \"http://w.networkedblogs.com/\",\n      \"companyId\": \"networkedblogs\"\n    },\n    \"neustar_adadvisor\": {\n      \"name\": \"Neustar AdAdvisor\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.targusinfo.com/\",\n      \"companyId\": \"neustar\"\n    },\n    \"new_relic\": {\n      \"name\": \"New Relic\",\n      \"categoryId\": 6,\n      \"url\": \"http://newrelic.com/\",\n      \"companyId\": \"new_relic\"\n    },\n    \"newscgp.com\": {\n      \"name\": \"News Connect\",\n      \"categoryId\": 4,\n      \"url\": \"https://newscorp.com/\",\n      \"companyId\": \"news_corp\"\n    },\n    \"newsmax\": {\n      \"name\": \"Newsmax\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.newsmax.com/\",\n      \"companyId\": \"newsmax\"\n    },\n    \"newstogram\": {\n      \"name\": \"Newstogram\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.newstogram.com/\",\n      \"companyId\": \"dailyme\"\n    },\n    \"newsupdatedir.info\": {\n      \"name\": \"newsupdatedir.info\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"newsupdatewe.info\": {\n      \"name\": \"newsupdatewe.info\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"newtention\": {\n      \"name\": \"Newtention\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.newtention.de/\",\n      \"companyId\": \"next_audience\"\n    },\n    \"nexage\": {\n      \"name\": \"Nexage\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nexage.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"nexeps.com\": {\n      \"name\": \"neXeps\",\n      \"categoryId\": 4,\n      \"url\": \"http://nexeps.com/\",\n      \"companyId\": null\n    },\n    \"next_performance\": {\n      \"name\": \"Next Performance\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nextperformance.com/\",\n      \"companyId\": \"nextperf\"\n    },\n    \"next_user\": {\n      \"name\": \"Next User\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.nextuser.com/\",\n      \"companyId\": \"next_user\"\n    },\n    \"nextag_roi_optimizer\": {\n      \"name\": \"Nextag ROI Optimizer\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nextag.com/\",\n      \"companyId\": \"nextag\"\n    },\n    \"nextclick\": {\n      \"name\": \"Nextclick\",\n      \"categoryId\": 4,\n      \"url\": \"http://nextclick.pl/\",\n      \"companyId\": \"leadbullet\"\n    },\n    \"nextstat\": {\n      \"name\": \"NextSTAT\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.nextstat.com/\",\n      \"companyId\": \"nextstat\"\n    },\n    \"neytiv\": {\n      \"name\": \"Neytiv\",\n      \"categoryId\": 6,\n      \"url\": \"http://neytiv.com/\",\n      \"companyId\": \"neytiv\"\n    },\n    \"ngage_inc.\": {\n      \"name\": \"NGage INC.\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.nginx.com/\",\n      \"companyId\": \"nginx\"\n    },\n    \"nice264.com\": {\n      \"name\": \"Nice264\",\n      \"categoryId\": 0,\n      \"url\": \"http://nice264.com/\",\n      \"companyId\": null\n    },\n    \"nimblecommerce\": {\n      \"name\": \"NimbleCommerce\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nimblecommerce.com/\",\n      \"companyId\": \"nimblecommerce\"\n    },\n    \"nine_direct_digital\": {\n      \"name\": \"Nine Digital Direct\",\n      \"categoryId\": 4,\n      \"url\": \"https://ninedigitaldirect.com.au/\",\n      \"companyId\": \"nine_entertainment\",\n      \"source\": \"AdGuard\"\n    },\n    \"ninja_access_analysis\": {\n      \"name\": \"Ninja Access Analysis\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ninja.co.jp/analysis/\",\n      \"companyId\": \"samurai_factory\"\n    },\n    \"nirror\": {\n      \"name\": \"Nirror\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.nirror.com/\",\n      \"companyId\": \"nirror\"\n    },\n    \"nitropay\": {\n      \"name\": \"NitroPay\",\n      \"categoryId\": 4,\n      \"url\": \"https://nitropay.com/\",\n      \"companyId\": \"gg_software\"\n    },\n    \"nk.pl_widgets\": {\n      \"name\": \"NK.pl Widgets\",\n      \"categoryId\": 4,\n      \"url\": \"http://nk.pl\",\n      \"companyId\": \"nk.pl\"\n    },\n    \"noaa.gov\": {\n      \"name\": \"National Oceanic and Atmospheric Administration\",\n      \"categoryId\": 8,\n      \"url\": \"https://noaa.gov/\",\n      \"companyId\": null\n    },\n    \"noddus\": {\n      \"name\": \"Noddus\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.enterprise.noddus.com/\",\n      \"companyId\": \"noddus\"\n    },\n    \"nolix\": {\n      \"name\": \"Nolix\",\n      \"categoryId\": 4,\n      \"url\": \"http://nolix.ru/\",\n      \"companyId\": \"nolix\"\n    },\n    \"nonli\": {\n      \"name\": \"Nonli\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.nonli.com/\",\n      \"companyId\": \"nonli\",\n      \"source\": \"AdGuard\"\n    },\n    \"nonstop_consulting\": {\n      \"name\": \"Resolution Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://resolutionmedia.com/\",\n      \"companyId\": \"resolution_media\"\n    },\n    \"noop.style\": {\n      \"name\": \"noop.style\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"nosto.com\": {\n      \"name\": \"nosto\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.nosto.com/\",\n      \"companyId\": null\n    },\n    \"notify\": {\n      \"name\": \"Notify\",\n      \"categoryId\": 4,\n      \"url\": \"http://notify.ag/en/\",\n      \"companyId\": null\n    },\n    \"notifyfox\": {\n      \"name\": \"Notifyfox\",\n      \"categoryId\": 6,\n      \"url\": \"https://notifyfox.com/\",\n      \"companyId\": \"notifyfox\"\n    },\n    \"notion\": {\n      \"name\": \"Notion\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.notion.so/\",\n      \"companyId\": \"notion\",\n      \"source\": \"AdGuard\"\n    },\n    \"now_interact\": {\n      \"name\": \"Now Interact\",\n      \"categoryId\": 6,\n      \"url\": \"http://nowinteract.com/\",\n      \"companyId\": \"now_interact\"\n    },\n    \"npario\": {\n      \"name\": \"nPario\",\n      \"categoryId\": 6,\n      \"url\": \"http://npario.com/\",\n      \"companyId\": \"npario\"\n    },\n    \"nplexmedia\": {\n      \"name\": \"nPlexMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nplexmedia.com/\",\n      \"companyId\": \"nplexmedia\"\n    },\n    \"nrelate\": {\n      \"name\": \"nRelate\",\n      \"categoryId\": 2,\n      \"url\": \"http://nrelate.com/\",\n      \"companyId\": \"iac_apps\"\n    },\n    \"ns8\": {\n      \"name\": \"NS8\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ns8.com/\",\n      \"companyId\": null\n    },\n    \"nt.vc\": {\n      \"name\": \"Next Tuesday GmbH\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.nexttuesday.de/\",\n      \"companyId\": null\n    },\n    \"ntent\": {\n      \"name\": \"NTENT\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.verticalsearchworks.com\",\n      \"companyId\": \"ntent\"\n    },\n    \"ntppool\": {\n      \"name\": \"Network Time Protocol\",\n      \"categoryId\": 5,\n      \"url\": \"https://ntp.org/\",\n      \"companyId\": \"network_time_foundation\",\n      \"source\": \"AdGuard\"\n    },\n    \"nttcom_online_marketing_solutions\": {\n      \"name\": \"NTTCom Online Marketing Solutions\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.digitalforest.co.jp/\",\n      \"companyId\": \"nttcom_online_marketing_solutions\"\n    },\n    \"nuffnang\": {\n      \"name\": \"Nuffnang\",\n      \"categoryId\": 4,\n      \"url\": \"http://nuffnang.com/\",\n      \"companyId\": \"nuffnang\"\n    },\n    \"nugg.ad\": {\n      \"name\": \"Nugg.Ad\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.nugg.ad/\",\n      \"companyId\": \"nugg.ad\"\n    },\n    \"nui_media\": {\n      \"name\": \"NUI Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://adjuggler.com/\",\n      \"companyId\": \"nui_media\"\n    },\n    \"numbers.md\": {\n      \"name\": \"Numbers.md\",\n      \"categoryId\": 6,\n      \"url\": \"https://numbers.md/\",\n      \"companyId\": \"numbers.md\"\n    },\n    \"numerator\": {\n      \"name\": \"Numerator\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.channeliq.com/\",\n      \"companyId\": \"numerator\"\n    },\n    \"ny_times_tagx\": {\n      \"name\": \"NY Times TagX\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.nytimes.com/\",\n      \"companyId\": \"the_new_york_times\"\n    },\n    \"nyacampwk.com\": {\n      \"name\": \"nyacampwk.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"nyetm2mkch.com\": {\n      \"name\": \"nyetm2mkch.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"nyt.com\": {\n      \"name\": \"The New York Times\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.nytimes.com/\",\n      \"companyId\": \"the_new_york_times\"\n    },\n    \"o12zs3u2n.com\": {\n      \"name\": \"o12zs3u2n.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"o2.pl\": {\n      \"name\": \"o2.pl\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.o2.pl/\",\n      \"companyId\": \"o2.pl\"\n    },\n    \"o2online.de\": {\n      \"name\": \"o2online.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.o2online.de/\",\n      \"companyId\": null\n    },\n    \"oath_inc\": {\n      \"name\": \"Oath\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.oath.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"observer\": {\n      \"name\": \"Observer\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.observerapp.com\",\n      \"companyId\": \"observer\"\n    },\n    \"ocioso\": {\n      \"name\": \"Ocioso\",\n      \"categoryId\": 7,\n      \"url\": \"http://ocioso.com.br/\",\n      \"companyId\": \"ocioso\"\n    },\n    \"oclasrv.com\": {\n      \"name\": \"oclasrv.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"octapi.net\": {\n      \"name\": \"octapi.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"octavius\": {\n      \"name\": \"Octavius\",\n      \"categoryId\": 4,\n      \"url\": \"http://octavius.rocks/\",\n      \"companyId\": \"octavius\"\n    },\n    \"office.com\": {\n      \"name\": \"office.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"office.net\": {\n      \"name\": \"office.net\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"office365.com\": {\n      \"name\": \"office365.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"oghub.io\": {\n      \"name\": \"OG Hub\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"oh_my_stats\": {\n      \"name\": \"Oh My Stats\",\n      \"categoryId\": 6,\n      \"url\": \"https://ohmystats.com/\",\n      \"companyId\": \"oh_my_stats\"\n    },\n    \"ohana_advertising_network\": {\n      \"name\": \"Ohana Advertising Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://adohana.com/\",\n      \"companyId\": \"ohana_advertising_network\"\n    },\n    \"olapic\": {\n      \"name\": \"Olapic\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.olapic.com/\",\n      \"companyId\": \"olapic\"\n    },\n    \"olark\": {\n      \"name\": \"Olark\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.olark.com/\",\n      \"companyId\": \"olark\"\n    },\n    \"olx-st.com\": {\n      \"name\": \"OLX\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.olx.com/\",\n      \"companyId\": null\n    },\n    \"omarsys.com\": {\n      \"name\": \"Omarsys\",\n      \"categoryId\": 4,\n      \"url\": \"http://omarsys.com/\",\n      \"companyId\": \"xcaliber\"\n    },\n    \"ometria\": {\n      \"name\": \"Ometria\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ometria.com/\",\n      \"companyId\": \"ometria\"\n    },\n    \"omg\": {\n      \"name\": \"OMG\",\n      \"categoryId\": 7,\n      \"url\": \"http://uk.omgpm.com/\",\n      \"companyId\": \"optimise_media\"\n    },\n    \"omniconvert.com\": {\n      \"name\": \"Omniconvert\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.omniconvert.com/\",\n      \"companyId\": \"omniconvert\"\n    },\n    \"omniscienta\": {\n      \"name\": \"Omniscienta\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.omniscienta.com/\",\n      \"companyId\": null\n    },\n    \"oms\": {\n      \"name\": \"OMS\",\n      \"categoryId\": 4,\n      \"url\": \"http://oms.eu/\",\n      \"companyId\": null\n    },\n    \"onaudience\": {\n      \"name\": \"OnAudience\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.onaudience.com/\",\n      \"companyId\": \"cloud_technologies\"\n    },\n    \"oneall\": {\n      \"name\": \"Oneall\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.oneall.com/\",\n      \"companyId\": \"oneall\"\n    },\n    \"onefeed\": {\n      \"name\": \"Onefeed\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.onefeed.co.uk\",\n      \"companyId\": \"onefeed\"\n    },\n    \"onesignal\": {\n      \"name\": \"OneSignal\",\n      \"categoryId\": 5,\n      \"url\": \"https://onesignal.com/\",\n      \"companyId\": \"onesignal\"\n    },\n    \"onestat\": {\n      \"name\": \"OneStat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.onestat.com/\",\n      \"companyId\": \"onestat_international_b.v.\"\n    },\n    \"onet.pl\": {\n      \"name\": \"onet\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.onet.pl/\",\n      \"companyId\": null\n    },\n    \"onetag\": {\n      \"name\": \"OneTag\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.onetag.com/\",\n      \"companyId\": \"onetag\"\n    },\n    \"onetrust\": {\n      \"name\": \"OneTrust\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.onetrust.com/\",\n      \"companyId\": \"onetrust\"\n    },\n    \"onfocus.io\": {\n      \"name\": \"OnFocus\",\n      \"categoryId\": 4,\n      \"url\": \"http://onfocus.io/\",\n      \"companyId\": \"onfocus\"\n    },\n    \"onlinewebstat\": {\n      \"name\": \"Onlinewebstat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.onlinewebstats.com/index.php?lang=en\",\n      \"companyId\": \"onlinewebstat\"\n    },\n    \"onswipe\": {\n      \"name\": \"Onswipe\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.onswipe.com/\",\n      \"companyId\": \"onswipe\"\n    },\n    \"onthe.io\": {\n      \"name\": \"OnThe.io\",\n      \"categoryId\": 6,\n      \"url\": \"https://t.onthe.io/media\",\n      \"companyId\": \"onthe.io\"\n    },\n    \"ontraport_autopilot\": {\n      \"name\": \"Ontraport Autopilot\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.moon-ray.com/\",\n      \"companyId\": \"ontraport\"\n    },\n    \"ooyala.com\": {\n      \"name\": \"Ooyala Player\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.ooyala.com/\",\n      \"companyId\": \"telstra\"\n    },\n    \"ooyala_analytics\": {\n      \"name\": \"Ooyala Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.telstraglobal.com/\",\n      \"companyId\": \"telstra\"\n    },\n    \"open_adexchange\": {\n      \"name\": \"Open AdExchange\",\n      \"categoryId\": 4,\n      \"url\": \"http://openadex.dk/\",\n      \"companyId\": \"open_adexchange\"\n    },\n    \"open_adstream\": {\n      \"name\": \"Open Adstream\",\n      \"categoryId\": 4,\n      \"url\": \"https://about.ads.microsoft.com/en-us/solutions/xandr/xandr-premium-programmatic-advertising\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"open_share_count\": {\n      \"name\": \"Open Share Count\",\n      \"categoryId\": 4,\n      \"url\": \"http://opensharecount.com/\",\n      \"companyId\": \"open_share_count\"\n    },\n    \"openai\": {\n      \"name\": \"OpenAI\",\n      \"categoryId\": 8,\n      \"url\": \"https://openai.com/\",\n      \"companyId\": \"openai\",\n      \"source\": \"AdGuard\"\n    },\n    \"openload\": {\n      \"name\": \"Openload\",\n      \"categoryId\": 9,\n      \"url\": \"https://openload.co/\",\n      \"companyId\": null\n    },\n    \"openstat\": {\n      \"name\": \"OpenStat\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.openstat.ru/\",\n      \"companyId\": \"openstat\"\n    },\n    \"opentracker\": {\n      \"name\": \"Opentracker\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.opentracker.net/\",\n      \"companyId\": \"opentracker\"\n    },\n    \"openwebanalytics\": {\n      \"name\": \"Open Web Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.openwebanalytics.com/\",\n      \"companyId\": \"open_web_analytics\"\n    },\n    \"openx\": {\n      \"name\": \"OpenX\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.openx.com\",\n      \"companyId\": \"openx\"\n    },\n    \"operative_media\": {\n      \"name\": \"Operative Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.operative.com/\",\n      \"companyId\": \"operative_media\"\n    },\n    \"opinary\": {\n      \"name\": \"Opinary\",\n      \"categoryId\": 2,\n      \"url\": \"http://opinary.com/\",\n      \"companyId\": \"opinary\"\n    },\n    \"opinionbar\": {\n      \"name\": \"OpinionBar\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.metrixlab.com\",\n      \"companyId\": \"metrixlab\"\n    },\n    \"oplytic\": {\n      \"name\": \"Oplytic\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.oplytic.com\",\n      \"companyId\": \"oplytic\"\n    },\n    \"oppo\": {\n      \"name\": \"OPPO\",\n      \"categoryId\": 101,\n      \"url\": \"https://www.oppo.com/\",\n      \"companyId\": \"bbk\",\n      \"source\": \"AdGuard\"\n    },\n    \"opta.net\": {\n      \"name\": \"Opta\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.optasports.de/\",\n      \"companyId\": \"opta_sports\"\n    },\n    \"optaim\": {\n      \"name\": \"OptAim\",\n      \"categoryId\": 4,\n      \"url\": \"http://optaim.com/\",\n      \"companyId\": \"optaim\"\n    },\n    \"optanaon\": {\n      \"name\": \"Optanaon by OneTrust\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.cookielaw.org/\",\n      \"companyId\": \"onetrust\"\n    },\n    \"optify\": {\n      \"name\": \"Optify\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.optify.net\",\n      \"companyId\": \"optify\"\n    },\n    \"optimatic\": {\n      \"name\": \"Optimatic\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.optimatic.com/\",\n      \"companyId\": \"optimatic\"\n    },\n    \"optimax_media_delivery\": {\n      \"name\": \"Optimax Media Delivery\",\n      \"categoryId\": 4,\n      \"url\": \"http://optmd.com/\",\n      \"companyId\": \"optimax_media_delivery\"\n    },\n    \"optimicdn.com\": {\n      \"name\": \"OptimiCDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://en.optimicdn.com/\",\n      \"companyId\": null\n    },\n    \"optimizely\": {\n      \"name\": \"Optimizely\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.optimizely.com/\",\n      \"companyId\": \"optimizely\"\n    },\n    \"optimizely_error_log\": {\n      \"name\": \"Optimizely Error Log\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.optimizely.com/\",\n      \"companyId\": \"optimizely\"\n    },\n    \"optimizely_geo_targeting\": {\n      \"name\": \"Optimizely Geographical Targeting\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.optimizely.com/\",\n      \"companyId\": \"optimizely\"\n    },\n    \"optimizely_logging\": {\n      \"name\": \"Optimizely Logging\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.optimizely.com/\",\n      \"companyId\": \"optimizely\"\n    },\n    \"optimonk\": {\n      \"name\": \"Optimonk\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.optimonk.com/\",\n      \"companyId\": \"optimonk\"\n    },\n    \"optinmonster\": {\n      \"name\": \"OptInMonster\",\n      \"categoryId\": 2,\n      \"url\": \"https://optinmonster.com/\",\n      \"companyId\": \"optinmonster\"\n    },\n    \"optinproject.com\": {\n      \"name\": \"OptinProject\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.optincollect.com/en\",\n      \"companyId\": \"optincollect\"\n    },\n    \"optomaton\": {\n      \"name\": \"Optomaton\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.optomaton.com/\",\n      \"companyId\": \"ve\"\n    },\n    \"ora.tv\": {\n      \"name\": \"Ora.TV\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ora.tv/\",\n      \"companyId\": \"ora.tv\"\n    },\n    \"oracle_infinity\": {\n      \"name\": \"Oracle Infinity Behavioral Intelligence\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.oracle.com/au/cx/marketing/digital-intelligence/\",\n      \"companyId\": \"oracle\",\n      \"source\": \"AdGuard\"\n    },\n    \"oracle_live_help\": {\n      \"name\": \"Oracle Live Help\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.oracle.com/us/products/applications/atg/live-help-on-demand/index.html\",\n      \"companyId\": \"oracle\"\n    },\n    \"oracle_rightnow\": {\n      \"name\": \"Oracle RightNow\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.oracle.com/\",\n      \"companyId\": \"oracle\"\n    },\n    \"orange\": {\n      \"name\": \"Orange\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.orange.co.uk/\",\n      \"companyId\": \"orange_mobile\"\n    },\n    \"orange142\": {\n      \"name\": \"Orange142\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.orange142.com/\",\n      \"companyId\": \"orange142\"\n    },\n    \"orange_france\": {\n      \"name\": \"Orange France\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.orange.fr/\",\n      \"companyId\": \"orange_france\"\n    },\n    \"orangesoda\": {\n      \"name\": \"OrangeSoda\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.orangesoda.com/\",\n      \"companyId\": \"orangesoda\"\n    },\n    \"orc_international\": {\n      \"name\": \"ORC International\",\n      \"categoryId\": 4,\n      \"url\": \"https://orcinternational.com/\",\n      \"companyId\": \"engine_group\"\n    },\n    \"order_groove\": {\n      \"name\": \"Order Groove\",\n      \"categoryId\": 4,\n      \"url\": \"http://ordergroove.com/\",\n      \"companyId\": \"order_groove\"\n    },\n    \"orel_site\": {\n      \"name\": \"Orel Site\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.orelsite.ru/\",\n      \"companyId\": \"orel_site\"\n    },\n    \"otclick\": {\n      \"name\": \"otClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://otclick-adv.ru/\",\n      \"companyId\": \"otclick\"\n    },\n    \"othersearch.info\": {\n      \"name\": \"FlowSurf\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"otm-r.com\": {\n      \"name\": \"OTM\",\n      \"categoryId\": 4,\n      \"url\": \"http://otm-r.com/\",\n      \"companyId\": null\n    },\n    \"otto.de\": {\n      \"name\": \"Otto Group\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"outbrain\": {\n      \"name\": \"Outbrain\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.outbrain.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"outbrain_amplify\": {\n      \"name\": \"Outbrain Amplify\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.outbrain.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"outbrain_analytics\": {\n      \"name\": \"Outbrain Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.outbrain.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"outbrain_logger\": {\n      \"name\": \"Outbrain Logger\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.outbrain.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"outbrain_pixel\": {\n      \"name\": \"Outbrain Pixel\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.outbrain.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"outbrain_utilities\": {\n      \"name\": \"Outbrain Utilities\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.outbrain.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"outbrain_widgets\": {\n      \"name\": \"Outbrain Widgets\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.outbrain.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"outlook\": {\n      \"name\": \"Microsoft Outlook\",\n      \"categoryId\": 13,\n      \"url\": \"https://outlook.live.com/\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"overheat.it\": {\n      \"name\": \"overheat\",\n      \"categoryId\": 6,\n      \"url\": \"https://overheat.io/\",\n      \"companyId\": null\n    },\n    \"owa\": {\n      \"name\": \"OWA\",\n      \"categoryId\": 6,\n      \"url\": \"http://oewa.at/\",\n      \"companyId\": \"the_austrian_web_analysis\"\n    },\n    \"owneriq\": {\n      \"name\": \"OwnerIQ\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.owneriq.com/\",\n      \"companyId\": \"owneriq\"\n    },\n    \"ownpage\": {\n      \"name\": \"Ownpage\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.ownpage.fr/index.en.html\",\n      \"companyId\": null\n    },\n    \"owox.com\": {\n      \"name\": \"OWOX\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.owox.com/\",\n      \"companyId\": \"owox_inc\"\n    },\n    \"oxamedia\": {\n      \"name\": \"OxaMedia\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.oxamedia.com/\",\n      \"companyId\": \"oxamedia\"\n    },\n    \"oxomi.com\": {\n      \"name\": \"Oxomi\",\n      \"categoryId\": 4,\n      \"url\": \"https://oxomi.com/\",\n      \"companyId\": null\n    },\n    \"oztam\": {\n      \"name\": \"OzTAM\",\n      \"categoryId\": 8,\n      \"url\": \"https://oztam.com.au/\",\n      \"companyId\": \"oztam\",\n      \"source\": \"AdGuard\"\n    },\n    \"pageanalytics.space\": {\n      \"name\": \"pageanalytics.space\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"pagefair\": {\n      \"name\": \"PageFair\",\n      \"categoryId\": 2,\n      \"url\": \"https://pagefair.com/\",\n      \"companyId\": \"blockthrough\"\n    },\n    \"pagescience\": {\n      \"name\": \"PageScience\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.precisionhealthmedia.com/index.html\",\n      \"companyId\": \"pagescience\"\n    },\n    \"paid-to-promote\": {\n      \"name\": \"Paid-To-Promote\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.paid-to-promote.net/\",\n      \"companyId\": \"paid-to-promote\"\n    },\n    \"paperg\": {\n      \"name\": \"PaperG\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.paperg.com/\",\n      \"companyId\": \"paperg\"\n    },\n    \"pardot\": {\n      \"name\": \"Pardot\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.pardot.com/\",\n      \"companyId\": \"pardot\"\n    },\n    \"parsely\": {\n      \"name\": \"Parse.ly\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.parse.ly/\",\n      \"companyId\": \"parse.ly\"\n    },\n    \"partner-ads\": {\n      \"name\": \"Partner-Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.partner-ads.com/\",\n      \"companyId\": \"partner-ads\"\n    },\n    \"passionfruit\": {\n      \"name\": \"Passionfruit\",\n      \"categoryId\": 4,\n      \"url\": \"http://passionfruitads.com/\",\n      \"companyId\": \"passionfruit\"\n    },\n    \"pathful\": {\n      \"name\": \"Pathful\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.pathful.com/\",\n      \"companyId\": \"pathful\"\n    },\n    \"pay-hit\": {\n      \"name\": \"Pay-Hit\",\n      \"categoryId\": 4,\n      \"url\": \"http://pay-hit.com/\",\n      \"companyId\": \"pay-hit\"\n    },\n    \"payclick\": {\n      \"name\": \"PayClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://payclick.it/\",\n      \"companyId\": \"payclick\"\n    },\n    \"paykickstart\": {\n      \"name\": \"PayKickstart\",\n      \"categoryId\": 6,\n      \"url\": \"https://paykickstart.com/\",\n      \"companyId\": \"paykickstart\"\n    },\n    \"paypal\": {\n      \"name\": \"PayPal\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.paypal.com\",\n      \"companyId\": \"ebay\"\n    },\n    \"pcvark.com\": {\n      \"name\": \"pcvark.com\",\n      \"categoryId\": 11,\n      \"url\": \"https://pcvark.com/\",\n      \"companyId\": null\n    },\n    \"peer39\": {\n      \"name\": \"Peer39\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.peer39.com/\",\n      \"companyId\": \"peer39\"\n    },\n    \"peer5.com\": {\n      \"name\": \"Peer5\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.peer5.com/\",\n      \"companyId\": \"peer5\"\n    },\n    \"peerius\": {\n      \"name\": \"Peerius\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.peerius.com/\",\n      \"companyId\": \"peerius\"\n    },\n    \"pendo.io\": {\n      \"name\": \"pendo\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.pendo.io/\",\n      \"companyId\": null\n    },\n    \"pepper.com\": {\n      \"name\": \"Pepper\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.pepper.com/\",\n      \"companyId\": \"6minutes\"\n    },\n    \"pepperjam\": {\n      \"name\": \"Pepperjam\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pepperjam.com\",\n      \"companyId\": \"pepperjam\"\n    },\n    \"pepsia\": {\n      \"name\": \"Pepsia\",\n      \"categoryId\": 6,\n      \"url\": \"http://pepsia.com/en/\",\n      \"companyId\": \"pepsia\"\n    },\n    \"perfdrive.com\": {\n      \"name\": \"perfdrive.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"perfect_audience\": {\n      \"name\": \"Perfect Audience\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.perfectaudience.com/\",\n      \"companyId\": \"perfect_audience\"\n    },\n    \"perfect_market\": {\n      \"name\": \"Perfect Market\",\n      \"categoryId\": 4,\n      \"url\": \"http://perfectmarket.com/\",\n      \"companyId\": \"perfect_market\"\n    },\n    \"perfops\": {\n      \"name\": \"PerfOps\",\n      \"categoryId\": 6,\n      \"url\": \"https://perfops.net/\",\n      \"companyId\": \"perfops\",\n      \"source\": \"AdGuard\"\n    },\n    \"perform_group\": {\n      \"name\": \"Perform Group\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.performgroup.co.uk/\",\n      \"companyId\": \"perform_group\"\n    },\n    \"performable\": {\n      \"name\": \"Performable\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.performable.com/\",\n      \"companyId\": \"hubspot\"\n    },\n    \"performancing_metrics\": {\n      \"name\": \"Performancing Metrics\",\n      \"categoryId\": 6,\n      \"url\": \"http://pmetrics.performancing.com\",\n      \"companyId\": \"performancing\"\n    },\n    \"performax\": {\n      \"name\": \"Performax\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.performax.cz/\",\n      \"companyId\": \"performax\"\n    },\n    \"perimeterx.net\": {\n      \"name\": \"Perimeterx\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.perimeterx.com/\",\n      \"companyId\": null\n    },\n    \"permutive\": {\n      \"name\": \"Permutive\",\n      \"categoryId\": 4,\n      \"url\": \"http://permutive.com/\",\n      \"companyId\": \"permutive\"\n    },\n    \"persgroep\": {\n      \"name\": \"De Persgroep\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.persgroep.be/\",\n      \"companyId\": \"de_persgroep\"\n    },\n    \"persianstat\": {\n      \"name\": \"PersianStat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.persianstat.com\",\n      \"companyId\": \"persianstat\"\n    },\n    \"persio\": {\n      \"name\": \"Persio\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pers.io/\",\n      \"companyId\": \"pers.io\"\n    },\n    \"personyze\": {\n      \"name\": \"Personyze\",\n      \"categoryId\": 2,\n      \"url\": \"http://personyze.com/\",\n      \"companyId\": \"personyze\"\n    },\n    \"petametrics\": {\n      \"name\": \"LiftIgniter\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.liftigniter.com/\",\n      \"companyId\": \"liftigniter\"\n    },\n    \"pheedo\": {\n      \"name\": \"Pheedo\",\n      \"categoryId\": 4,\n      \"url\": \"http://pheedo.com/\",\n      \"companyId\": \"pheedo\"\n    },\n    \"phonalytics\": {\n      \"name\": \"Phonalytics\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.phonalytics.com/\",\n      \"companyId\": \"phonalytics\"\n    },\n    \"phunware\": {\n      \"name\": \"Phunware\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.phunware.com\",\n      \"companyId\": \"phunware\"\n    },\n    \"piguiqproxy.com\": {\n      \"name\": \"piguiqproxy.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"pilot\": {\n      \"name\": \"Pilot\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.pilot.de/en/home.html\",\n      \"companyId\": \"pilot_gmbh\"\n    },\n    \"pingdom\": {\n      \"name\": \"Pingdom\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.pingdom.com/\",\n      \"companyId\": \"pingdom\"\n    },\n    \"pinterest\": {\n      \"name\": \"Pinterest\",\n      \"categoryId\": 7,\n      \"url\": \"http://pinterest.com/\",\n      \"companyId\": \"pinterest\"\n    },\n    \"pinterest_conversion_tracker\": {\n      \"name\": \"Pinterest Conversion Tracker\",\n      \"categoryId\": 6,\n      \"url\": \"http://pinterest.com/\",\n      \"companyId\": \"pinterest\"\n    },\n    \"pipz\": {\n      \"name\": \"Pipz\",\n      \"categoryId\": 4,\n      \"url\": \"https://pipz.com/br/\",\n      \"companyId\": \"pipz_automation\"\n    },\n    \"piwik\": {\n      \"name\": \"Tombstone (Matomo/Piwik before the split)\",\n      \"categoryId\": 6,\n      \"url\": \"http://piwik.org/\",\n      \"companyId\": \"matomo\"\n    },\n    \"piwik_pro_analytics_suite\": {\n      \"name\": \"Piwik PRO Analytics Suite\",\n      \"categoryId\": 6,\n      \"url\": \"https://piwik.pro/\",\n      \"companyId\": \"piwik_pro\"\n    },\n    \"pixalate\": {\n      \"name\": \"Pixalate\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pixalate.com/\",\n      \"companyId\": \"pixalate\"\n    },\n    \"pixel_union\": {\n      \"name\": \"Pixel Union\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.pixelunion.net/\",\n      \"companyId\": \"pixel_union\"\n    },\n    \"pixfuture\": {\n      \"name\": \"PixFuture\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pixfuture.com\",\n      \"companyId\": \"pixfuture\"\n    },\n    \"piximedia\": {\n      \"name\": \"Piximedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.piximedia.com/piximedia?en\",\n      \"companyId\": \"piximedia\"\n    },\n    \"pizzaandads_com\": {\n      \"name\": \"pizzaandads.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"placester\": {\n      \"name\": \"Placester\",\n      \"categoryId\": 4,\n      \"url\": \"https://placester.com/\",\n      \"companyId\": \"placester\"\n    },\n    \"pladform.ru\": {\n      \"name\": \"Pladform\",\n      \"categoryId\": 4,\n      \"url\": \"https://distribution.pladform.ru/\",\n      \"companyId\": \"pladform\"\n    },\n    \"plan.net_experience_cloud\": {\n      \"name\": \"Plan.net Experience Cloud\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.serviceplan.com/\",\n      \"companyId\": \"serviceplan\"\n    },\n    \"platform360\": {\n      \"name\": \"Platform360\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.platform360.co/#home\",\n      \"companyId\": null\n    },\n    \"platformone\": {\n      \"name\": \"Platform One\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.platform-one.co.jp/\",\n      \"companyId\": \"daconsortium\"\n    },\n    \"play_by_mamba\": {\n      \"name\": \"Play by Mamba\",\n      \"categoryId\": 4,\n      \"url\": \"http://play.mamba.ru/\",\n      \"companyId\": \"mamba\"\n    },\n    \"playbuzz.com\": {\n      \"name\": \"Playbuzz\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.playbuzz.com/\",\n      \"companyId\": \"playbuzz\"\n    },\n    \"plenty_of_fish\": {\n      \"name\": \"Plenty Of Fish\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.pof.com/\",\n      \"companyId\": \"plentyoffish\"\n    },\n    \"plex\": {\n      \"name\": \"Plex\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.plex.tv/\",\n      \"companyId\": \"plex\",\n      \"source\": \"AdGuard\"\n    },\n    \"plex_metrics\": {\n      \"name\": \"Plex Metrics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.plex.tv/\",\n      \"companyId\": \"plex\"\n    },\n    \"plista\": {\n      \"name\": \"Plista\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.plista.com\",\n      \"companyId\": \"plista\"\n    },\n    \"plugrush\": {\n      \"name\": \"PlugRush\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.plugrush.com/\",\n      \"companyId\": \"plugrush\"\n    },\n    \"pluso.ru\": {\n      \"name\": \"Pluso\",\n      \"categoryId\": 7,\n      \"url\": \"https://share.pluso.ru/\",\n      \"companyId\": \"pluso\"\n    },\n    \"plutusads\": {\n      \"name\": \"Plutusads\",\n      \"categoryId\": 4,\n      \"url\": \"http://plutusads.com\",\n      \"companyId\": \"plutusads\"\n    },\n    \"pmddby.com\": {\n      \"name\": \"pmddby.com\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"pnamic.com\": {\n      \"name\": \"pnamic.com\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"po.st\": {\n      \"name\": \"Po.st\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.po.st/\",\n      \"companyId\": \"rythmone\"\n    },\n    \"pocket\": {\n      \"name\": \"Pocket\",\n      \"categoryId\": 6,\n      \"url\": \"http://getpocket.com/\",\n      \"companyId\": \"pocket\"\n    },\n    \"pocketcents\": {\n      \"name\": \"PocketCents\",\n      \"categoryId\": 4,\n      \"url\": \"http://pocketcents.com/\",\n      \"companyId\": \"pocketcents\"\n    },\n    \"pointific\": {\n      \"name\": \"Pointific\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.pontiflex.com/\",\n      \"companyId\": \"pontiflex\"\n    },\n    \"pointroll\": {\n      \"name\": \"PointRoll\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pointroll.com/\",\n      \"companyId\": \"gannett_digital_media_network\"\n    },\n    \"poirreleast.club\": {\n      \"name\": \"poirreleast.club\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"polar.me\": {\n      \"name\": \"Polar\",\n      \"categoryId\": 4,\n      \"url\": \"https://polar.me/\",\n      \"companyId\": \"polar_inc\"\n    },\n    \"polldaddy\": {\n      \"name\": \"Polldaddy\",\n      \"categoryId\": 2,\n      \"url\": \"http://polldaddy.com/\",\n      \"companyId\": \"automattic\"\n    },\n    \"polyad\": {\n      \"name\": \"PolyAd\",\n      \"categoryId\": 4,\n      \"url\": \"http://polyad.net\",\n      \"companyId\": \"polyad\"\n    },\n    \"polyfill.io\": {\n      \"name\": \"Polyfill\",\n      \"categoryId\": 8,\n      \"url\": \"https://polyfill.io/\",\n      \"companyId\": \"polyfill.io\"\n    },\n    \"popads\": {\n      \"name\": \"PopAds\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.popads.net/\",\n      \"companyId\": \"popads\"\n    },\n    \"popcash\": {\n      \"name\": \"Popcash\",\n      \"categoryId\": 4,\n      \"url\": \"http://popcash.net/\",\n      \"companyId\": \"popcash_network\"\n    },\n    \"popcorn_metrics\": {\n      \"name\": \"Popcorn Metrics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.popcornmetrics.com/\",\n      \"companyId\": \"popcorn_metrics\"\n    },\n    \"popin.cc\": {\n      \"name\": \"popIn\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.popin.cc/\",\n      \"companyId\": \"popin\"\n    },\n    \"popmyads\": {\n      \"name\": \"PopMyAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://popmyads.com/\",\n      \"companyId\": \"popmyads\"\n    },\n    \"poponclick\": {\n      \"name\": \"PopOnClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://poponclick.com\",\n      \"companyId\": \"poponclick\"\n    },\n    \"populis\": {\n      \"name\": \"Populis\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.populis.com\",\n      \"companyId\": \"populis\"\n    },\n    \"pornhub\": {\n      \"name\": \"PornHub\",\n      \"categoryId\": 3,\n      \"url\": \"https://www.pornhub.com/\",\n      \"companyId\": \"pornhub\"\n    },\n    \"pornwave\": {\n      \"name\": \"Pornwave\",\n      \"categoryId\": 3,\n      \"url\": \"http://pornwave.com\",\n      \"companyId\": \"pornwave.com\"\n    },\n    \"porta_brazil\": {\n      \"name\": \"Porta Brazil\",\n      \"categoryId\": 4,\n      \"url\": \"http://brasil.gov.br/\",\n      \"companyId\": \"portal_brazil\"\n    },\n    \"post_affiliate_pro\": {\n      \"name\": \"Post Affiliate Pro\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.qualityunit.com/\",\n      \"companyId\": \"qualityunit\"\n    },\n    \"powerlinks\": {\n      \"name\": \"PowerLinks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.powerlinks.com/\",\n      \"companyId\": \"powerlinks\"\n    },\n    \"powerreviews\": {\n      \"name\": \"PowerReviews\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.powerreviews.com/\",\n      \"companyId\": \"powerreviews\"\n    },\n    \"powr.io\": {\n      \"name\": \"POWr\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.powr.io/\",\n      \"companyId\": \"powr\"\n    },\n    \"pozvonim\": {\n      \"name\": \"Pozvonim\",\n      \"categoryId\": 4,\n      \"url\": \"https://pozvonim.com/\",\n      \"companyId\": \"pozvonim\"\n    },\n    \"prebid\": {\n      \"name\": \"Prebid\",\n      \"categoryId\": 4,\n      \"url\": \"http://prebid.org/\",\n      \"companyId\": null\n    },\n    \"precisionclick\": {\n      \"name\": \"PrecisionClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.precisionclick.com/\",\n      \"companyId\": \"precisionclick\"\n    },\n    \"predicta\": {\n      \"name\": \"Predicta\",\n      \"categoryId\": 4,\n      \"url\": \"http://predicta.com.br/\",\n      \"companyId\": \"predicta\"\n    },\n    \"premonix\": {\n      \"name\": \"Premonix\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.premonix.com/\",\n      \"companyId\": \"premonix\"\n    },\n    \"press\": {\n      \"name\": \"Press+\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.mypressplus.com/\",\n      \"companyId\": \"press+\"\n    },\n    \"pressly\": {\n      \"name\": \"Pressly\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.pressly.com/\",\n      \"companyId\": \"pressly\"\n    },\n    \"pricegrabber\": {\n      \"name\": \"PriceGrabber\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pricegrabber.com\",\n      \"companyId\": \"pricegrabber\"\n    },\n    \"pricespider\": {\n      \"name\": \"Pricespider\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pricespider.com/\",\n      \"companyId\": \"price_spider\"\n    },\n    \"prismamediadigital.com\": {\n      \"name\": \"Prisma Media Digital\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pmdrecrute.com/\",\n      \"companyId\": \"prisma_media_digital\"\n    },\n    \"privy.com\": {\n      \"name\": \"Privy\",\n      \"categoryId\": 2,\n      \"url\": \"https://privy.com/\",\n      \"companyId\": \"privy\"\n    },\n    \"proclivity\": {\n      \"name\": \"Proclivity\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.proclivitysystems.com/\",\n      \"companyId\": \"proclivity_media\"\n    },\n    \"prodperfect\": {\n      \"name\": \"ProdPerfect\",\n      \"categoryId\": 6,\n      \"url\": \"https://prodperfect.com/\",\n      \"companyId\": \"prodperfect\"\n    },\n    \"productsup\": {\n      \"name\": \"ProductsUp\",\n      \"categoryId\": 4,\n      \"url\": \"https://productsup.io/\",\n      \"companyId\": \"productsup\"\n    },\n    \"profiliad\": {\n      \"name\": \"Profiliad\",\n      \"categoryId\": 6,\n      \"url\": \"http://profiliad.com/\",\n      \"companyId\": \"profiliad\"\n    },\n    \"profitshare\": {\n      \"name\": \"Profitshare\",\n      \"categoryId\": 6,\n      \"url\": \"https://profitshare.ro/\",\n      \"companyId\": \"profitshare\"\n    },\n    \"proformics\": {\n      \"name\": \"Proformics\",\n      \"categoryId\": 6,\n      \"url\": \"http://proformics.com/\",\n      \"companyId\": \"proformics_digital\"\n    },\n    \"programattik\": {\n      \"name\": \"Programattik\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.programattik.com/\",\n      \"companyId\": \"ttnet\"\n    },\n    \"project_wonderful\": {\n      \"name\": \"Project Wonderful\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.projectwonderful.com/\",\n      \"companyId\": \"project_wonderful\"\n    },\n    \"propel_marketing\": {\n      \"name\": \"Propel Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"http://propelmarketing.com/\",\n      \"companyId\": \"propel_marketing\"\n    },\n    \"propeller_ads\": {\n      \"name\": \"Propeller Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.propellerads.com/\",\n      \"companyId\": \"propeller_ads\"\n    },\n    \"propermedia\": {\n      \"name\": \"Proper Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://proper.io/\",\n      \"companyId\": \"propermedia\"\n    },\n    \"props\": {\n      \"name\": \"Props\",\n      \"categoryId\": 4,\n      \"url\": \"http://props.id/\",\n      \"companyId\": \"props\"\n    },\n    \"propvideo_net\": {\n      \"name\": \"propvideo.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"prospecteye\": {\n      \"name\": \"ProspectEye\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.prospecteye.com/\",\n      \"companyId\": \"prospecteye\"\n    },\n    \"prosperent\": {\n      \"name\": \"Prosperent\",\n      \"categoryId\": 4,\n      \"url\": \"http://prosperent.com\",\n      \"companyId\": \"prosperent\"\n    },\n    \"prostor\": {\n      \"name\": \"Prostor\",\n      \"categoryId\": 4,\n      \"url\": \"http://prostor-lite.ru/\",\n      \"companyId\": \"prostor\"\n    },\n    \"proton_ag\": {\n      \"name\": \"Proton AG\",\n      \"categoryId\": 2,\n      \"url\": \"https://proton.me/\",\n      \"companyId\": \"proton_foundation\",\n      \"source\": \"AdGuard\"\n    },\n    \"provide_support\": {\n      \"name\": \"Provide Support\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.providesupport.com/\",\n      \"companyId\": \"provide_support\"\n    },\n    \"proximic\": {\n      \"name\": \"Proximic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.proximic.com/\",\n      \"companyId\": \"proximic\"\n    },\n    \"proxistore.com\": {\n      \"name\": \"Proxistore\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.proxistore.com/\",\n      \"companyId\": \"proxistore\"\n    },\n    \"pscp.tv\": {\n      \"name\": \"Periscope\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.pscp.tv/\",\n      \"companyId\": \"periscope\"\n    },\n    \"pstatic.net\": {\n      \"name\": \"Naver CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.naver.com/\",\n      \"companyId\": \"naver\"\n    },\n    \"psyma\": {\n      \"name\": \"Psyma\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.psyma.com/\",\n      \"companyId\": \"psyma\"\n    },\n    \"pt_engine\": {\n      \"name\": \"Pt engine\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ptengine.jp/\",\n      \"companyId\": \"pt_engine\"\n    },\n    \"pub-fit\": {\n      \"name\": \"Pub-Fit\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pub-fit.com/\",\n      \"companyId\": \"pub-fit\"\n    },\n    \"pub.network\": {\n      \"name\": \"pub.network\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"pubble\": {\n      \"name\": \"Pubble\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.pubble.co/\",\n      \"companyId\": \"pubble\"\n    },\n    \"pubdirecte\": {\n      \"name\": \"Pubdirecte\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pubdirecte.com/\",\n      \"companyId\": \"pubdirecte\"\n    },\n    \"pubgears\": {\n      \"name\": \"PubGears\",\n      \"categoryId\": 4,\n      \"url\": \"http://pubgears.com/\",\n      \"companyId\": \"pubgears\"\n    },\n    \"public_ideas\": {\n      \"name\": \"Public Ideas\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.publicidees.co.uk/\",\n      \"companyId\": \"public-idees\"\n    },\n    \"publicidad.net\": {\n      \"name\": \"Publicidad.net\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.en.publicidad.net/\",\n      \"companyId\": \"publicidad.net\"\n    },\n    \"publir\": {\n      \"name\": \"Publir\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.publir.com\",\n      \"companyId\": \"publir\"\n    },\n    \"pubmatic\": {\n      \"name\": \"PubMatic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pubmatic.com/\",\n      \"companyId\": \"pubmatic\"\n    },\n    \"pubnub.com\": {\n      \"name\": \"PubNub\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.pubnub.com/\",\n      \"companyId\": null\n    },\n    \"puboclic\": {\n      \"name\": \"Puboclic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.puboclic.com/\",\n      \"companyId\": \"puboclic\"\n    },\n    \"pulpix.com\": {\n      \"name\": \"Pulpix\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.pulpix.com/\",\n      \"companyId\": \"adyoulike\"\n    },\n    \"pulpo_media\": {\n      \"name\": \"Pulpo Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pulpomedia.com/home.html\",\n      \"companyId\": \"pulpo_media\"\n    },\n    \"pulse360\": {\n      \"name\": \"Pulse360\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pulse360.com\",\n      \"companyId\": \"pulse360\"\n    },\n    \"pulse_insights\": {\n      \"name\": \"Pulse Insights\",\n      \"categoryId\": 6,\n      \"url\": \"http://pulseinsights.com/\",\n      \"companyId\": \"pulse_insights\"\n    },\n    \"pulsepoint\": {\n      \"name\": \"PulsePoint\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.contextweb.com/\",\n      \"companyId\": \"pulsepoint_ad_exchange\"\n    },\n    \"punchtab\": {\n      \"name\": \"PunchTab\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.punchtab.com/\",\n      \"companyId\": \"punchtab\"\n    },\n    \"purch\": {\n      \"name\": \"Purch\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.purch.com/\",\n      \"companyId\": \"purch\"\n    },\n    \"pure_chat\": {\n      \"name\": \"Pure Chat\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.purechat.com\",\n      \"companyId\": \"pure_chat\"\n    },\n    \"pureprofile\": {\n      \"name\": \"Pureprofile\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.pureprofile.com/us/\",\n      \"companyId\": \"pureprofile\"\n    },\n    \"purlive\": {\n      \"name\": \"PurLive\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.purlive.com/\",\n      \"companyId\": \"purlive\"\n    },\n    \"puserving.com\": {\n      \"name\": \"puserving.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"push.world\": {\n      \"name\": \"Push.world\",\n      \"categoryId\": 2,\n      \"url\": \"https://push.world/en\",\n      \"companyId\": \"push.world\"\n    },\n    \"push_engage\": {\n      \"name\": \"Push Engage\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.pushengage.com/\",\n      \"companyId\": \"push_engage\"\n    },\n    \"pushame.com\": {\n      \"name\": \"pushame.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"pushbullet\": {\n      \"name\": \"Pushbullet\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.pushbullet.com/\",\n      \"companyId\": \"pushbullet\"\n    },\n    \"pushcrew\": {\n      \"name\": \"VWO Engage\",\n      \"categoryId\": 2,\n      \"url\": \"https://vwo.com/engage/\",\n      \"companyId\": \"wingify\"\n    },\n    \"pusher.com\": {\n      \"name\": \"Pusher\",\n      \"categoryId\": 6,\n      \"url\": \"https://pusher.com/\",\n      \"companyId\": null\n    },\n    \"pushnative.com\": {\n      \"name\": \"pushnative.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"pushnews\": {\n      \"name\": \"Pushnews\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.pushnews.eu/\",\n      \"companyId\": \"pushnews\"\n    },\n    \"pushno.com\": {\n      \"name\": \"pushno.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"pushwhy.com\": {\n      \"name\": \"pushwhy.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"pushwoosh.com\": {\n      \"name\": \"Pushwoosh\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.pushwoosh.com/\",\n      \"companyId\": \"pushwoosh\"\n    },\n    \"pvclouds.com\": {\n      \"name\": \"pvclouds.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"q1media\": {\n      \"name\": \"Q1Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://q1media.com/\",\n      \"companyId\": \"q1media\"\n    },\n    \"q_division\": {\n      \"name\": \"Q-Division\",\n      \"categoryId\": 4,\n      \"url\": \"https://q-division.de/\",\n      \"companyId\": null\n    },\n    \"qbaka\": {\n      \"name\": \"Qbaka\",\n      \"categoryId\": 6,\n      \"url\": \"https://qbaka.com/\",\n      \"companyId\": \"qbaka\"\n    },\n    \"qcri_analytics\": {\n      \"name\": \"QCRI Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://qcri.org/\",\n      \"companyId\": \"qatar_computing_research_institute\"\n    },\n    \"qeado\": {\n      \"name\": \"Qeado\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.qeado.com/\",\n      \"companyId\": \"qeado\"\n    },\n    \"qihoo_360\": {\n      \"name\": \"Qihoo 360\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.360totalsecurity.com/en/\",\n      \"companyId\": \"qihoo_360_technology\"\n    },\n    \"qq.com\": {\n      \"name\": \"QQ International\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.qq.com/\",\n      \"companyId\": \"tencent\",\n      \"source\": \"AdGuard\"\n    },\n    \"qrius\": {\n      \"name\": \"Qrius\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.qrius.me/\",\n      \"companyId\": \"mediafed\"\n    },\n    \"qualaroo\": {\n      \"name\": \"Qualaroo\",\n      \"categoryId\": 6,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"qualcomm\": {\n      \"name\": \"Qualcomm\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.qualcomm.com/\",\n      \"companyId\": \"qualcomm\",\n      \"source\": \"AdGuard\"\n    },\n    \"qualcomm_location_service\": {\n      \"name\": \"Qualcomm Location Service\",\n      \"categoryId\": 15,\n      \"url\": \"https://www.qualcomm.com/site/privacy/services\",\n      \"companyId\": \"qualcomm\",\n      \"source\": \"AdGuard\"\n    },\n    \"qualia\": {\n      \"name\": \"Qualia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.bluecava.com/\",\n      \"companyId\": \"qualia\"\n    },\n    \"qualtrics\": {\n      \"name\": \"Qualtrics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.qualtrics.com/\",\n      \"companyId\": \"qualtrics\"\n    },\n    \"quantcast\": {\n      \"name\": \"Quantcast\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.quantcast.com/\",\n      \"companyId\": \"quantcast\"\n    },\n    \"quantcount\": {\n      \"name\": \"Quantcount\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.quantcast.com\",\n      \"companyId\": \"quantcast\"\n    },\n    \"quantum_metric\": {\n      \"name\": \"Quantum Metric\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.quantummetric.com/\",\n      \"companyId\": \"quantum_metric\"\n    },\n    \"quartic.pl\": {\n      \"name\": \"Quartic\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.quarticon.com/\",\n      \"companyId\": \"quarticon\"\n    },\n    \"qubit\": {\n      \"name\": \"Qubit Opentag\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.qubit.com/\",\n      \"companyId\": \"qubit\"\n    },\n    \"questback\": {\n      \"name\": \"Questback\",\n      \"categoryId\": 2,\n      \"url\": \"http://www1.questback.com/\",\n      \"companyId\": \"questback\"\n    },\n    \"queue-it\": {\n      \"name\": \"Queue-it\",\n      \"categoryId\": 6,\n      \"url\": \"https://queue-it.com/\",\n      \"companyId\": null\n    },\n    \"quick-counter.net\": {\n      \"name\": \"Quick-counter.net\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.quick-counter.net/\",\n      \"companyId\": \"quick-counter.net\"\n    },\n    \"quigo_adsonar\": {\n      \"name\": \"Quigo AdSonar\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.quigo.com\",\n      \"companyId\": \"verizon\"\n    },\n    \"quinstreet\": {\n      \"name\": \"QuinStreet\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.quinstreet.com/\",\n      \"companyId\": \"quinstreet\"\n    },\n    \"quintelligence\": {\n      \"name\": \"Quintelligence\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.quintelligence.com/\",\n      \"companyId\": \"quintelligence\"\n    },\n    \"quisma\": {\n      \"name\": \"Quisma\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.quisma.com/en/\",\n      \"companyId\": \"wpp\"\n    },\n    \"quora.com\": {\n      \"name\": \"Quora\",\n      \"categoryId\": 7,\n      \"url\": \"https://quora.com/\",\n      \"companyId\": null\n    },\n    \"r_advertising\": {\n      \"name\": \"R-Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.r-advertising.com/\",\n      \"companyId\": \"r-advertising\"\n    },\n    \"rackcdn.com\": {\n      \"name\": \"Rackspace\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.rackspace.com/\",\n      \"companyId\": null\n    },\n    \"radarurl\": {\n      \"name\": \"RadarURL\",\n      \"categoryId\": 6,\n      \"url\": \"http://radarurl.com/\",\n      \"companyId\": \"radarurl\"\n    },\n    \"radial\": {\n      \"name\": \"Radial\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clearsaleing.com/\",\n      \"companyId\": \"radial\"\n    },\n    \"radiumone\": {\n      \"name\": \"RadiumOne\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.radiumone.com/index.html\",\n      \"companyId\": \"rythmone\"\n    },\n    \"raisenow\": {\n      \"name\": \"RaiseNow\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.raisenow.com/de\",\n      \"companyId\": \"raisenow\"\n    },\n    \"rakuten_display\": {\n      \"name\": \"Rakuten Display\",\n      \"categoryId\": 4,\n      \"url\": \"https://rakutenmarketing.com/display\",\n      \"companyId\": \"rakuten\"\n    },\n    \"rakuten_globalmarket\": {\n      \"name\": \"Rakuten\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.rakuten.co.jp/\",\n      \"companyId\": \"rakuten\"\n    },\n    \"rakuten_widget\": {\n      \"name\": \"Rakuten Widget\",\n      \"categoryId\": 4,\n      \"url\": \"http://global.rakuten.com/corp/\",\n      \"companyId\": \"rakuten\"\n    },\n    \"rambler\": {\n      \"name\": \"Rambler\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.rambler.ru/\",\n      \"companyId\": \"rambler\"\n    },\n    \"rambler_count\": {\n      \"name\": \"Rambler Count\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.rambler.ru/\",\n      \"companyId\": \"rambler\"\n    },\n    \"rambler_widget\": {\n      \"name\": \"Rambler Widget\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.rambler.ru/\",\n      \"companyId\": \"rambler\"\n    },\n    \"rapidspike\": {\n      \"name\": \"RapidSpike\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.rapidspike.com\",\n      \"companyId\": \"rapidspike\"\n    },\n    \"ravelin\": {\n      \"name\": \"Ravelin\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.ravelin.com/\",\n      \"companyId\": null\n    },\n    \"rawgit\": {\n      \"name\": \"RawGit\",\n      \"categoryId\": 9,\n      \"url\": \"http://rawgit.com/\",\n      \"companyId\": null\n    },\n    \"raygun\": {\n      \"name\": \"Raygun\",\n      \"categoryId\": 4,\n      \"url\": \"https://raygun.com/\",\n      \"companyId\": \"raygun\"\n    },\n    \"rbc_counter\": {\n      \"name\": \"RBC Counter\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.rbc.ru/\",\n      \"companyId\": \"rbc_group\"\n    },\n    \"rcs.it\": {\n      \"name\": \"RCS\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.rcsmediagroup.it/\",\n      \"companyId\": \"rcs\"\n    },\n    \"rd_station\": {\n      \"name\": \"RD Station\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.rdstation.com/en/\",\n      \"companyId\": \"rd_station\"\n    },\n    \"rea_group\": {\n      \"name\": \"REA Group Ltd.\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.rea-group.com/\",\n      \"companyId\": \"news_corp\",\n      \"source\": \"AdGuard\"\n    },\n    \"reachforce\": {\n      \"name\": \"ReachForce\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.reachforce.com/\",\n      \"companyId\": \"reachforce\"\n    },\n    \"reachjunction\": {\n      \"name\": \"ReachJunction\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.reachjunction.com/\",\n      \"companyId\": \"reachjunction\"\n    },\n    \"reachlocal\": {\n      \"name\": \"ReachLocal\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.reachlocal.com/\",\n      \"companyId\": \"reachlocal\"\n    },\n    \"reactful\": {\n      \"name\": \"Reactful\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.reactful.com/\",\n      \"companyId\": \"reactful\"\n    },\n    \"reactivpub\": {\n      \"name\": \"Reactivpub\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.reactivpub.com/\",\n      \"companyId\": \"r-advertising\"\n    },\n    \"reactx\": {\n      \"name\": \"ReactX\",\n      \"categoryId\": 4,\n      \"url\": \"http://home.skinected.com\",\n      \"companyId\": \"reactx\"\n    },\n    \"readerboard\": {\n      \"name\": \"ReaderBoard\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.readrboard.com\",\n      \"companyId\": \"centre_phi\"\n    },\n    \"readme\": {\n      \"name\": \"ReadMe\",\n      \"categoryId\": 6,\n      \"url\": \"https://readme.com/\",\n      \"companyId\": \"readme\"\n    },\n    \"readspeaker.com\": {\n      \"name\": \"ReadSpeaker\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.readspeaker.com/\",\n      \"companyId\": null\n    },\n    \"realclick\": {\n      \"name\": \"RealClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.realclick.co.kr/\",\n      \"companyId\": \"realclick\"\n    },\n    \"realestate.com.au\": {\n      \"name\": \"realestate.com.au Pty Limited\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.realestate.com.au/\",\n      \"companyId\": \"news_corp\",\n      \"source\": \"AdGuard\"\n    },\n    \"realperson.de\": {\n      \"name\": \"Realperson Chat\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.optimise-it.de/\",\n      \"companyId\": \"optimise_it\"\n    },\n    \"realtime\": {\n      \"name\": \"Realtime\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.realtime.co/\",\n      \"companyId\": \"realtime\"\n    },\n    \"realytics\": {\n      \"name\": \"Realytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.realytics.io/\",\n      \"companyId\": \"realytics\"\n    },\n    \"rebel_mouse\": {\n      \"name\": \"Rebel Mouse\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.rebelmouse.com/\",\n      \"companyId\": \"rebelmouse\"\n    },\n    \"recaptcha\": {\n      \"name\": \"reCAPTCHA\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.google.com/recaptcha/about/\",\n      \"companyId\": \"google\",\n      \"source\": \"AdGuard\"\n    },\n    \"recettes.net\": {\n      \"name\": \"Recettes.net\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.recettes.net/\",\n      \"companyId\": \"recettes.net\"\n    },\n    \"recopick\": {\n      \"name\": \"RecoPick\",\n      \"categoryId\": 4,\n      \"url\": \"https://recopick.com/\",\n      \"companyId\": \"recopick\"\n    },\n    \"recreativ\": {\n      \"name\": \"Recreativ\",\n      \"categoryId\": 4,\n      \"url\": \"http://recreativ.ru/\",\n      \"companyId\": \"recreativ\"\n    },\n    \"recruitics\": {\n      \"name\": \"Recruitics\",\n      \"categoryId\": 6,\n      \"url\": \"http://recruitics.com/\",\n      \"companyId\": \"recruitics\"\n    },\n    \"red_ventures\": {\n      \"name\": \"Red Ventures\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.redventures.com/\",\n      \"companyId\": \"red_ventures\"\n    },\n    \"redblue_de\": {\n      \"name\": \"redblue\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.redblue.de/\",\n      \"companyId\": null\n    },\n    \"redcdn.pl\": {\n      \"name\": \"redGalaxy CDN\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.atendesoftware.pl/\",\n      \"companyId\": \"atende_software\"\n    },\n    \"reddit\": {\n      \"name\": \"Reddit\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.reddit.com\",\n      \"companyId\": \"advance\",\n      \"source\": \"AdGuard\"\n    },\n    \"redhelper\": {\n      \"name\": \"RedHelper\",\n      \"categoryId\": 2,\n      \"url\": \"http://redhelper.com/\",\n      \"companyId\": \"redhelper\"\n    },\n    \"redlotus\": {\n      \"name\": \"RedLotus\",\n      \"categoryId\": 4,\n      \"url\": \"http://triggit.com/\",\n      \"companyId\": \"redlotus\"\n    },\n    \"redtram\": {\n      \"name\": \"RedTram\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.redtram.com/\",\n      \"companyId\": \"redtram\"\n    },\n    \"redtube.com\": {\n      \"name\": \"redtube.com\",\n      \"categoryId\": 9,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"redux_media\": {\n      \"name\": \"Redux Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://reduxmedia.com/\",\n      \"companyId\": \"redux_media\"\n    },\n    \"reed_business_information\": {\n      \"name\": \"Reed Business Information\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.reedbusiness.com/\",\n      \"companyId\": \"andera_partners\"\n    },\n    \"reembed.com\": {\n      \"name\": \"reEmbed\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.reembed.com/\",\n      \"companyId\": \"reembed\"\n    },\n    \"reevoo.com\": {\n      \"name\": \"Reevoo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.reevoo.com/en/\",\n      \"companyId\": \"reevoo\"\n    },\n    \"refericon\": {\n      \"name\": \"Refericon\",\n      \"categoryId\": 4,\n      \"url\": \"https://refericon.pl/#\",\n      \"companyId\": \"refericon\"\n    },\n    \"referlocal\": {\n      \"name\": \"ReferLocal\",\n      \"categoryId\": 4,\n      \"url\": \"http://referlocal.com/\",\n      \"companyId\": \"referlocal\"\n    },\n    \"refersion\": {\n      \"name\": \"Refersion\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.refersion.com/\",\n      \"companyId\": \"refersion\"\n    },\n    \"refined_labs\": {\n      \"name\": \"Refined Labs\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.refinedlabs.com\",\n      \"companyId\": \"refined_labs\"\n    },\n    \"reflektion\": {\n      \"name\": \"Reflektion\",\n      \"categoryId\": 4,\n      \"url\": \"http://\",\n      \"companyId\": \"reflektion\"\n    },\n    \"reformal\": {\n      \"name\": \"Reformal\",\n      \"categoryId\": 2,\n      \"url\": \"http://reformal.ru/\",\n      \"companyId\": \"reformal\"\n    },\n    \"reinvigorate\": {\n      \"name\": \"Reinvigorate\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.reinvigorate.net/\",\n      \"companyId\": \"media_temple\"\n    },\n    \"rekko\": {\n      \"name\": \"Rekko\",\n      \"categoryId\": 4,\n      \"url\": \"http://convert.us/\",\n      \"companyId\": \"rekko\"\n    },\n    \"reklam_store\": {\n      \"name\": \"Reklam Store\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.reklamstore.com\",\n      \"companyId\": \"reklam_store\"\n    },\n    \"reklamport\": {\n      \"name\": \"Reklamport\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.reklamport.com/\",\n      \"companyId\": \"reklamport\"\n    },\n    \"reklamz\": {\n      \"name\": \"ReklamZ\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.reklamz.com/\",\n      \"companyId\": \"reklamz\"\n    },\n    \"rekmob\": {\n      \"name\": \"Rekmob\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.rekmob.com/\",\n      \"companyId\": \"rekmob\"\n    },\n    \"relap\": {\n      \"name\": \"Relap\",\n      \"categoryId\": 4,\n      \"url\": \"https://relap.io/\",\n      \"companyId\": \"relap\"\n    },\n    \"relay42\": {\n      \"name\": \"Relay42\",\n      \"categoryId\": 5,\n      \"url\": \"http://synovite.com\",\n      \"companyId\": \"relay42\"\n    },\n    \"relestar\": {\n      \"name\": \"Relestar\",\n      \"categoryId\": 6,\n      \"url\": \"https://relestar.com/\",\n      \"companyId\": \"relestar\"\n    },\n    \"relevant4.com\": {\n      \"name\": \"relevant4 GmbH\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.relevant4.com/\",\n      \"companyId\": null\n    },\n    \"remintrex\": {\n      \"name\": \"Remintrex\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.remintrex.com/\",\n      \"companyId\": null\n    },\n    \"remove.video\": {\n      \"name\": \"remove.video\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"repost.us\": {\n      \"name\": \"Repost.us\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.freerangecontent.com/\",\n      \"companyId\": \"repost\"\n    },\n    \"republer.com\": {\n      \"name\": \"Republer\",\n      \"categoryId\": 4,\n      \"url\": \"http://republer.com/\",\n      \"companyId\": \"republer\"\n    },\n    \"res-meter\": {\n      \"name\": \"Res-meter\",\n      \"categoryId\": 6,\n      \"url\": \"http://respublica.al/res-meter\",\n      \"companyId\": \"respublica\"\n    },\n    \"research_now\": {\n      \"name\": \"Research Now\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.researchnow.com/\",\n      \"companyId\": \"research_now\"\n    },\n    \"resonate_networks\": {\n      \"name\": \"Resonate Networks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.resonatenetworks.com/\",\n      \"companyId\": \"resonate\"\n    },\n    \"respond\": {\n      \"name\": \"Respond\",\n      \"categoryId\": 4,\n      \"url\": \"http://respondhq.com/\",\n      \"companyId\": \"respond\"\n    },\n    \"responsetap\": {\n      \"name\": \"ResponseTap\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adinsight.eu/\",\n      \"companyId\": \"responsetap\"\n    },\n    \"result_links\": {\n      \"name\": \"Result Links\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.resultlinks.com/\",\n      \"companyId\": \"result_links\"\n    },\n    \"resultspage.com\": {\n      \"name\": \"SLI Systems\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.sli-systems.com/\",\n      \"companyId\": \"sli_systems\"\n    },\n    \"retailrocket.net\": {\n      \"name\": \"Retail Rocket\",\n      \"categoryId\": 4,\n      \"url\": \"https://retailrocket.net/\",\n      \"companyId\": \"retail_rocket\"\n    },\n    \"retarget_app\": {\n      \"name\": \"Retarget App\",\n      \"categoryId\": 4,\n      \"url\": \"https://retargetapp.com/\",\n      \"companyId\": \"retargetapp\"\n    },\n    \"retargeter_beacon\": {\n      \"name\": \"ReTargeter Beacon\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.retargeter.com/\",\n      \"companyId\": \"retargeter\"\n    },\n    \"retargeting.cl\": {\n      \"name\": \"Retargeting.cl\",\n      \"categoryId\": 4,\n      \"url\": \"http://retargeting.cl/\",\n      \"companyId\": \"retargeting\"\n    },\n    \"retention_science\": {\n      \"name\": \"Retention Science\",\n      \"categoryId\": 4,\n      \"url\": \"http://retentionscience.com/\",\n      \"companyId\": \"retention_science\"\n    },\n    \"reuters_media\": {\n      \"name\": \"Reuters media\",\n      \"categoryId\": 9,\n      \"url\": \"https://reuters.com\",\n      \"companyId\": null\n    },\n    \"revcontent\": {\n      \"name\": \"RevContent\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.revcontent.com/\",\n      \"companyId\": \"revcontent\"\n    },\n    \"reve_marketing\": {\n      \"name\": \"Reve Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"http://tellafriend.socialtwist.com/\",\n      \"companyId\": \"reve_marketing\"\n    },\n    \"revenue\": {\n      \"name\": \"Revenue\",\n      \"categoryId\": 4,\n      \"url\": \"https://revenue.com/\",\n      \"companyId\": \"revenue\"\n    },\n    \"revenuehits\": {\n      \"name\": \"RevenueHits\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.revenuehits.com/\",\n      \"companyId\": \"revenuehits\"\n    },\n    \"revenuemantra\": {\n      \"name\": \"RevenueMantra\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.revenuemantra.com/\",\n      \"companyId\": \"revenuemantra\"\n    },\n    \"revive_adserver\": {\n      \"name\": \"Revive Adserver\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.revive-adserver.com/\",\n      \"companyId\": \"revive_adserver\"\n    },\n    \"revolver_maps\": {\n      \"name\": \"Revolver Maps\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.revolvermaps.com/\",\n      \"companyId\": \"revolver_maps\"\n    },\n    \"revresponse\": {\n      \"name\": \"RevResponse\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netline.com/\",\n      \"companyId\": \"netline\"\n    },\n    \"rewords\": {\n      \"name\": \"ReWords\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.rewords.pl/\",\n      \"companyId\": \"rewords\"\n    },\n    \"rhythmone\": {\n      \"name\": \"RhythmOne\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.adconductor.com/\",\n      \"companyId\": \"rhythmone\"\n    },\n    \"rhythmone_beacon\": {\n      \"name\": \"Rhythmone Beacon\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.rhythmone.com/\",\n      \"companyId\": \"rythmone\"\n    },\n    \"ria.ru\": {\n      \"name\": \"ria.ru\",\n      \"categoryId\": 8,\n      \"url\": \"https://ria.ru/\",\n      \"companyId\": null\n    },\n    \"rich_media_banner_network\": {\n      \"name\": \"Rich Media Banner Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://rmbn.ru/\",\n      \"companyId\": \"rich_media_banner_network\"\n    },\n    \"richrelevance\": {\n      \"name\": \"RichRelevance\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.richrelevance.com/\",\n      \"companyId\": \"richrelevance\"\n    },\n    \"ringier.ch\": {\n      \"name\": \"Ringier\",\n      \"categoryId\": 6,\n      \"url\": \"http://ringier.ch/en\",\n      \"companyId\": \"ringier\"\n    },\n    \"rio_seo\": {\n      \"name\": \"Rio SEO\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.meteorsolutions.com\",\n      \"companyId\": \"rio_seo\"\n    },\n    \"riskfield.com\": {\n      \"name\": \"Riskified\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.riskified.com/\",\n      \"companyId\": \"riskfield\"\n    },\n    \"rncdn3.com\": {\n      \"name\": \"Reflected Networks\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.rncdn3.com/\",\n      \"companyId\": null\n    },\n    \"ro2.biz\": {\n      \"name\": \"Ro2.biz\",\n      \"categoryId\": 4,\n      \"url\": \"http://ro2.biz/index.php?r=adikku\",\n      \"companyId\": \"ro2.biz\"\n    },\n    \"roblox\": {\n      \"name\": \"Roblox\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.roblox.com/\",\n      \"companyId\": null\n    },\n    \"rockerbox\": {\n      \"name\": \"Rockerbox\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.rockerbox.com/privacy\",\n      \"companyId\": \"rockerbox\"\n    },\n    \"rocket.ia\": {\n      \"name\": \"Rocket.ia\",\n      \"categoryId\": 4,\n      \"url\": \"https://rocket.la/\",\n      \"companyId\": \"rocket.la\"\n    },\n    \"roi_trax\": {\n      \"name\": \"ROI trax\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.oneupweb.com/\",\n      \"companyId\": \"oneupweb\"\n    },\n    \"roistat\": {\n      \"name\": \"Roistat\",\n      \"categoryId\": 6,\n      \"url\": \"https://roistat.com\",\n      \"companyId\": \"roistat\"\n    },\n    \"rollad\": {\n      \"name\": \"Rollad\",\n      \"categoryId\": 4,\n      \"url\": \"http://rollad.ru\",\n      \"companyId\": \"rollad\"\n    },\n    \"rollbar\": {\n      \"name\": \"Rollbar\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.rollbar.com/\",\n      \"companyId\": \"rollbar\"\n    },\n    \"roost\": {\n      \"name\": \"Roost\",\n      \"categoryId\": 6,\n      \"url\": \"http://roost.me/\",\n      \"companyId\": \"roost\"\n    },\n    \"rooster\": {\n      \"name\": \"Rooster\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.getrooster.com/\",\n      \"companyId\": \"rooster\"\n    },\n    \"roq.ad\": {\n      \"name\": \"Roq.ad\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.roq.ad/\",\n      \"companyId\": \"roq.ad\"\n    },\n    \"rotaban\": {\n      \"name\": \"RotaBan\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.rotaban.ru/\",\n      \"companyId\": \"rotaban\"\n    },\n    \"routenplaner-karten.com\": {\n      \"name\": \"Routenplaner Karten\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.routenplaner-karten.com/\",\n      \"companyId\": null\n    },\n    \"rovion\": {\n      \"name\": \"Rovion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.rovion.com/\",\n      \"companyId\": \"rovion\"\n    },\n    \"rsspump\": {\n      \"name\": \"RSSPump\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.rsspump.com\",\n      \"companyId\": \"rsspump\"\n    },\n    \"rtb_house\": {\n      \"name\": \"RTB House\",\n      \"categoryId\": 4,\n      \"url\": \"http://en.adpilot.com/\",\n      \"companyId\": \"rtb_house\"\n    },\n    \"rtblab\": {\n      \"name\": \"RTBmarkt\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.rtbmarkt.de/en/home/\",\n      \"companyId\": \"rtbmarkt\"\n    },\n    \"rtbsuperhub.com\": {\n      \"name\": \"rtbsuperhub.com\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"rtl_group\": {\n      \"name\": \"RTL Group\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.rtlgroup.com/www/htm/home.aspx\",\n      \"companyId\": \"rtl_group\"\n    },\n    \"rtmark.net\": {\n      \"name\": \"Advertising Technologies Ltd\",\n      \"categoryId\": 4,\n      \"url\": \"http://rtmark.net/\",\n      \"companyId\": \"big_wall_vision\"\n    },\n    \"rubicon\": {\n      \"name\": \"Rubicon\",\n      \"categoryId\": 4,\n      \"url\": \"http://rubiconproject.com/\",\n      \"companyId\": \"rubicon_project\"\n    },\n    \"ruhrgebiet\": {\n      \"name\": \"Ruhrgebiet\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ruhrgebiet-onlineservices.de/\",\n      \"companyId\": \"ruhrgebiet\"\n    },\n    \"rummycircle\": {\n      \"name\": \"RummyCircle\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.rummycircle.com/\",\n      \"companyId\": \"rummycircle\"\n    },\n    \"run\": {\n      \"name\": \"RUN\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.rundsp.com/\",\n      \"companyId\": \"run\"\n    },\n    \"runative\": {\n      \"name\": \"Runative\",\n      \"categoryId\": 4,\n      \"url\": \"https://runative.com/\",\n      \"companyId\": null\n    },\n    \"rune\": {\n      \"name\": \"Rune\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.secretrune.com/\",\n      \"companyId\": \"rune_inc.\"\n    },\n    \"runmewivel.com\": {\n      \"name\": \"runmewivel.com\",\n      \"categoryId\": 10,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"rythmxchange\": {\n      \"name\": \"Rythmxchange\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.rhythmone.com/\",\n      \"companyId\": \"rythmone\"\n    },\n    \"s24_com\": {\n      \"name\": \"Shopping24 internet group\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.s24.com/\",\n      \"companyId\": null\n    },\n    \"s3xified.com\": {\n      \"name\": \"s3xified.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"sabavision\": {\n      \"name\": \"SabaVision\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sabavision.com/en/\",\n      \"companyId\": \"sabavision\"\n    },\n    \"sagemetrics\": {\n      \"name\": \"SageMetrics\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sagemetrics.com\",\n      \"companyId\": \"ipmg\"\n    },\n    \"sailthru_horizon\": {\n      \"name\": \"Sailthru Horizon\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.sailthru.com\",\n      \"companyId\": \"sailthru\"\n    },\n    \"salecycle\": {\n      \"name\": \"SaleCycle\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.salecycle.com/\",\n      \"companyId\": \"salecycle\"\n    },\n    \"sales_feed\": {\n      \"name\": \"Sales Feed\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.salesfeed.com/\",\n      \"companyId\": \"sales_feed\"\n    },\n    \"sales_manago\": {\n      \"name\": \"SALESmanago\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.salesmanago.com/\",\n      \"companyId\": \"sales_manago\"\n    },\n    \"salesforce.com\": {\n      \"name\": \"Salesforce\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.salesforce.com/eu/\",\n      \"companyId\": \"salesforce\"\n    },\n    \"salesforce_live_agent\": {\n      \"name\": \"Salesforce Live Agent\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.salesforce.com/\",\n      \"companyId\": \"salesforce\"\n    },\n    \"salesfusion\": {\n      \"name\": \"SalesFUSION\",\n      \"categoryId\": 4,\n      \"url\": \"http://salesfusion.com/\",\n      \"companyId\": \"salesfusion\"\n    },\n    \"salespider_media\": {\n      \"name\": \"SaleSpider Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://salespidermedia.com/\",\n      \"companyId\": \"salespider_media\"\n    },\n    \"salesviewer\": {\n      \"name\": \"SalesViewer\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.salesviewer.com/\",\n      \"companyId\": \"salesviewer\"\n    },\n    \"samba.tv\": {\n      \"name\": \"Samba TV\",\n      \"categoryId\": 4,\n      \"url\": \"https://samba.tv/\",\n      \"companyId\": \"samba_tv\"\n    },\n    \"samsung\": {\n      \"name\": \"Samsung\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.samsung.com/\",\n      \"companyId\": \"samsung\",\n      \"source\": \"AdGuard\"\n    },\n    \"samsungads\": {\n      \"name\": \"Samsung Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.samsung.com/business/samsungads/\",\n      \"companyId\": \"samsung\",\n      \"source\": \"AdGuard\"\n    },\n    \"samsungapps\": {\n      \"name\": \"Samsung Apps\",\n      \"categoryId\": 101,\n      \"url\": \"https://www.samsung.com/au/apps/\",\n      \"companyId\": \"samsung\",\n      \"source\": \"AdGuard\"\n    },\n    \"samsungmobile\": {\n      \"name\": \"Samsung Mobile\",\n      \"categoryId\": 101,\n      \"url\": \"https://www.samsung.com/mobile/\",\n      \"companyId\": \"samsung\",\n      \"source\": \"AdGuard\"\n    },\n    \"samsungpush\": {\n      \"name\": \"Samsung Push\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": \"samsung\",\n      \"source\": \"AdGuard\"\n    },\n    \"samsungsds\": {\n      \"name\": \"Samsung SDS\",\n      \"categoryId\": 10,\n      \"url\": \"https://www.samsungsds.com/\",\n      \"companyId\": \"samsung\",\n      \"source\": \"AdGuard\"\n    },\n    \"samsungtv\": {\n      \"name\": \"Samsung TV\",\n      \"categoryId\": 15,\n      \"url\": \"https://www.samsung.com/au/tvs/\",\n      \"companyId\": \"samsung\",\n      \"source\": \"AdGuard\"\n    },\n    \"sanoma.fi\": {\n      \"name\": \"Sanoma\",\n      \"categoryId\": 4,\n      \"url\": \"https://sanoma.com/\",\n      \"companyId\": \"sanoma\"\n    },\n    \"sap_crm\": {\n      \"name\": \"SAP CRM\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.sap.com/products/crm.html\",\n      \"companyId\": \"sap\"\n    },\n    \"sap_sales_cloud\": {\n      \"name\": \"SAP Sales Cloud\",\n      \"categoryId\": 2,\n      \"url\": \"http://leadforce1.com/\",\n      \"companyId\": \"sap\"\n    },\n    \"sap_xm\": {\n      \"name\": \"SAP Exchange Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://sapexchange.media/\",\n      \"companyId\": null\n    },\n    \"sape.ru\": {\n      \"name\": \"Sape\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.sape.ru/en\",\n      \"companyId\": \"sape\"\n    },\n    \"sapo_ads\": {\n      \"name\": \"SAPO Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sapo.pt/\",\n      \"companyId\": \"sapo\"\n    },\n    \"sas\": {\n      \"name\": \"SAS\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.sas.com/\",\n      \"companyId\": \"sas\"\n    },\n    \"say.ac\": {\n      \"name\": \"Say.ac\",\n      \"categoryId\": 4,\n      \"url\": \"http://say.ac\",\n      \"companyId\": \"say.ac\"\n    },\n    \"say_media\": {\n      \"name\": \"Say Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.saymedia.com/\",\n      \"companyId\": \"say_media\"\n    },\n    \"sayyac\": {\n      \"name\": \"Sayyac\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.sayyac.com/\",\n      \"companyId\": \"sayyac\"\n    },\n    \"scarabresearch\": {\n      \"name\": \"Scarab Research\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.scarabresearch.com/\",\n      \"companyId\": \"emarsys\"\n    },\n    \"schibsted\": {\n      \"name\": \"Schibsted Media Group\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.schibsted.com/\",\n      \"companyId\": \"schibsted_asa\"\n    },\n    \"schneevonmorgen.com\": {\n      \"name\": \"Schnee von Morgen\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.schneevonmorgen.com/\",\n      \"companyId\": null\n    },\n    \"scoota\": {\n      \"name\": \"Scoota\",\n      \"categoryId\": 4,\n      \"url\": \"http://scoota.com/\",\n      \"companyId\": \"rockabox\"\n    },\n    \"scorecard_research_beacon\": {\n      \"name\": \"ScoreCard Research Beacon\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.scorecardresearch.com/\",\n      \"companyId\": \"comscore\"\n    },\n    \"scout_analytics\": {\n      \"name\": \"Scout Analytics\",\n      \"categoryId\": 4,\n      \"url\": \"http://scoutanalytics.com/\",\n      \"companyId\": \"scout_analytics\"\n    },\n    \"scribblelive\": {\n      \"name\": \"ScribbleLive\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"scribol\": {\n      \"name\": \"Scribol\",\n      \"categoryId\": 4,\n      \"url\": \"http://scribol.com/\",\n      \"companyId\": \"scribol\"\n    },\n    \"scripps_analytics\": {\n      \"name\": \"Scripps Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.scrippsnetworksinteractive.com/\",\n      \"companyId\": \"scripps_networks\"\n    },\n    \"scroll\": {\n      \"name\": \"Scroll\",\n      \"categoryId\": 5,\n      \"url\": \"https://scroll.com/\",\n      \"companyId\": \"scroll\"\n    },\n    \"scupio\": {\n      \"name\": \"Scupio\",\n      \"categoryId\": 4,\n      \"url\": \"http://ad.scupio.com/\",\n      \"companyId\": \"bridgewell\"\n    },\n    \"search123\": {\n      \"name\": \"Search123\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.search123.com/\",\n      \"companyId\": \"search123\"\n    },\n    \"searchforce\": {\n      \"name\": \"SearchForce\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.searchforce.com/\",\n      \"companyId\": \"searchforce\"\n    },\n    \"searchignite\": {\n      \"name\": \"SearchIgnite\",\n      \"categoryId\": 4,\n      \"url\": \"https://searchignite.com/\",\n      \"companyId\": \"zeta\"\n    },\n    \"searchrev\": {\n      \"name\": \"SearchRev\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.searchrev.com/\",\n      \"companyId\": \"searchrev\"\n    },\n    \"second_media\": {\n      \"name\": \"Second Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.secondmedia.com/\",\n      \"companyId\": \"second_media\"\n    },\n    \"sectigo\": {\n      \"name\": \"Sectigo Limited\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.sectigo.com\",\n      \"companyId\": \"sectigo\",\n      \"source\": \"AdGuard\"\n    },\n    \"securedtouch\": {\n      \"name\": \"SecuredTouch\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.securedtouch.com/\",\n      \"companyId\": null\n    },\n    \"securedvisit\": {\n      \"name\": \"SecuredVisit\",\n      \"categoryId\": 4,\n      \"url\": \"http://securedvisit.com/\",\n      \"companyId\": \"securedvisit\"\n    },\n    \"seeding_alliance\": {\n      \"name\": \"Seeding Alliance\",\n      \"categoryId\": 4,\n      \"url\": \"http://seeding-alliance.de\",\n      \"companyId\": \"stroer\"\n    },\n    \"seedtag.com\": {\n      \"name\": \"Seedtag\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.seedtag.com/en/\",\n      \"companyId\": \"seedtag\"\n    },\n    \"seevolution\": {\n      \"name\": \"SeeVolution\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.seevolution.com\",\n      \"companyId\": \"seevolution\"\n    },\n    \"segment\": {\n      \"name\": \"Segment\",\n      \"categoryId\": 6,\n      \"url\": \"https://segment.io/\",\n      \"companyId\": \"segment\"\n    },\n    \"segmento\": {\n      \"name\": \"Segmento\",\n      \"categoryId\": 4,\n      \"url\": \"https://segmento.ru/en\",\n      \"companyId\": \"segmento\"\n    },\n    \"segmint\": {\n      \"name\": \"Segmint\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.segmint.com/\",\n      \"companyId\": \"segmint\"\n    },\n    \"sekindo\": {\n      \"name\": \"Sekindo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sekindo.com/\",\n      \"companyId\": \"sekindo\"\n    },\n    \"sellpoints\": {\n      \"name\": \"Sellpoints\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.sellpoints.com/\",\n      \"companyId\": \"sellpoints\"\n    },\n    \"semantiqo.com\": {\n      \"name\": \"Semantiqo\",\n      \"categoryId\": 4,\n      \"url\": \"https://semantiqo.com/\",\n      \"companyId\": null\n    },\n    \"semasio\": {\n      \"name\": \"Semasio\",\n      \"categoryId\": 4,\n      \"url\": \"http://semasio.com/\",\n      \"companyId\": \"semasio\"\n    },\n    \"semilo\": {\n      \"name\": \"Semilo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.semilo.nl/\",\n      \"companyId\": \"semilo\"\n    },\n    \"semknox.com\": {\n      \"name\": \"SEMKNOX GmbH\",\n      \"categoryId\": 5,\n      \"url\": \"https://semknox.com/\",\n      \"companyId\": null\n    },\n    \"sendinblue\": {\n      \"name\": \"sendinblue\",\n      \"categoryId\": 4,\n      \"url\": \"https://fr.sendinblue.com/\",\n      \"companyId\": \"sendinblue\"\n    },\n    \"sendpulse.com\": {\n      \"name\": \"SendPulse\",\n      \"categoryId\": 3,\n      \"url\": \"https://sendpulse.com/\",\n      \"companyId\": null\n    },\n    \"sendsay\": {\n      \"name\": \"Sendsay\",\n      \"categoryId\": 2,\n      \"url\": \"https://sendsay.ru\",\n      \"companyId\": \"sendsay\"\n    },\n    \"sense_digital\": {\n      \"name\": \"Sense Digital\",\n      \"categoryId\": 6,\n      \"url\": \"http://sensedigital.in/\",\n      \"companyId\": \"sense_digital\"\n    },\n    \"sensors_data\": {\n      \"name\": \"Sensors Data\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.sensorsdata.cn/\",\n      \"companyId\": \"sensors_data\"\n    },\n    \"sentifi.com\": {\n      \"name\": \"Sentifi\",\n      \"categoryId\": 6,\n      \"url\": \"https://sentifi.com/\",\n      \"companyId\": \"sentifi\"\n    },\n    \"sentry\": {\n      \"name\": \"Sentry\",\n      \"categoryId\": 6,\n      \"url\": \"https://sentry.io/\",\n      \"companyId\": \"sentry\"\n    },\n    \"sepyra\": {\n      \"name\": \"Sepyra\",\n      \"categoryId\": 4,\n      \"url\": \"http://sepyra.com/\",\n      \"companyId\": \"sepyra\"\n    },\n    \"sessioncam\": {\n      \"name\": \"SessionCam\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.sessioncam.com/\",\n      \"companyId\": \"sessioncam\"\n    },\n    \"sessionly\": {\n      \"name\": \"Sessionly\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.sessionly.io/\",\n      \"companyId\": \"sessionly\"\n    },\n    \"sevenone_media\": {\n      \"name\": \"SevenOne Media\",\n      \"categoryId\": 4,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"sexadnetwork\": {\n      \"name\": \"SexAdNetwork\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.sexadnetwork.com/\",\n      \"companyId\": \"sexadnetwork\"\n    },\n    \"sexinyourcity\": {\n      \"name\": \"SexInYourCity\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.sexinyourcity.com/\",\n      \"companyId\": \"sexinyourcity\"\n    },\n    \"sextracker\": {\n      \"name\": \"SexTracker\",\n      \"categoryId\": 3,\n      \"url\": \"http://webmasters.sextracker.com/\",\n      \"companyId\": \"sextracker\"\n    },\n    \"sexypartners.net\": {\n      \"name\": \"sexypartners.net\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"seznam\": {\n      \"name\": \"Seznam\",\n      \"categoryId\": 6,\n      \"url\": \"https://onas.seznam.cz/cz/\",\n      \"companyId\": \"seznam\"\n    },\n    \"shareaholic\": {\n      \"name\": \"Shareaholic\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.shareaholic.com/\",\n      \"companyId\": \"shareaholic\"\n    },\n    \"shareasale\": {\n      \"name\": \"ShareASale\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.shareasale.com/\",\n      \"companyId\": \"shareasale\"\n    },\n    \"sharecompany\": {\n      \"name\": \"ShareCompany\",\n      \"categoryId\": 2,\n      \"url\": \"http://sharecompany.nl\",\n      \"companyId\": \"sharecompany\"\n    },\n    \"sharepoint\": {\n      \"name\": \"SharePoint\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.microsoft.com/microsoft-365/sharepoint/collaboration\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"sharethis\": {\n      \"name\": \"ShareThis\",\n      \"categoryId\": 4,\n      \"url\": \"http://sharethis.com/\",\n      \"companyId\": \"sharethis\"\n    },\n    \"sharethrough\": {\n      \"name\": \"ShareThrough\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sharethrough.com/\",\n      \"companyId\": \"sharethrough\"\n    },\n    \"sharpspring\": {\n      \"name\": \"Sharpspring\",\n      \"categoryId\": 6,\n      \"url\": \"https://sharpspring.com/\",\n      \"companyId\": \"sharpspring\"\n    },\n    \"sheego.de\": {\n      \"name\": \"sheego.de\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"sheerid\": {\n      \"name\": \"SheerID\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sheerid.com/\",\n      \"companyId\": \"sheerid\"\n    },\n    \"shinystat\": {\n      \"name\": \"ShinyStat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.shinystat.com/\",\n      \"companyId\": \"shinystat\"\n    },\n    \"shop_target\": {\n      \"name\": \"Shop Target\",\n      \"categoryId\": 4,\n      \"url\": \"http://shoptarget.com.br/\",\n      \"companyId\": \"shopback\"\n    },\n    \"shopauskunft.de\": {\n      \"name\": \"ShopAuskunft.de\",\n      \"categoryId\": 2,\n      \"url\": \"https://shopauskunft.de/\",\n      \"companyId\": null\n    },\n    \"shopgate.com\": {\n      \"name\": \"Shopgate\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.shopgate.com/\",\n      \"companyId\": null\n    },\n    \"shopify\": {\n      \"name\": \"Shopify Inc.\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.shopify.com/\",\n      \"companyId\": \"shopify\",\n      \"source\": \"AdGuard\"\n    },\n    \"shopify_stats\": {\n      \"name\": \"Shopify Stats\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.shopify.com/\",\n      \"companyId\": \"shopify\",\n      \"source\": \"AdGuard\"\n    },\n    \"shopifycdn.com\": {\n      \"name\": \"Shopify CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.shopify.com/\",\n      \"companyId\": \"shopify\"\n    },\n    \"shopifycloud.com\": {\n      \"name\": \"Shopify Cloud\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.shopify.com/\",\n      \"companyId\": \"shopify\"\n    },\n    \"shopper_approved\": {\n      \"name\": \"Shopper Approved\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.shopperapproved.com\",\n      \"companyId\": \"shopper_approved\"\n    },\n    \"shopping_com\": {\n      \"name\": \"Shopping.com\",\n      \"categoryId\": 4,\n      \"url\": \"https://partnernetwork.ebay.com/\",\n      \"companyId\": \"ebay_partner_network\"\n    },\n    \"shopping_flux\": {\n      \"name\": \"Shopping Flux\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.shopping-flux.com/\",\n      \"companyId\": \"shopping_flux\"\n    },\n    \"shoprunner\": {\n      \"name\": \"ShopRunner\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.shoprunner.com\",\n      \"companyId\": \"shoprunner\"\n    },\n    \"shopsocially\": {\n      \"name\": \"ShopSocially\",\n      \"categoryId\": 2,\n      \"url\": \"http://shopsocially.com/\",\n      \"companyId\": \"shopsocially\"\n    },\n    \"shopzilla\": {\n      \"name\": \"Shopzilla\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.shopzilla.com/\",\n      \"companyId\": \"shopzilla\"\n    },\n    \"shortnews\": {\n      \"name\": \"ShortNews.de\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.shortnews.de/#\",\n      \"companyId\": null\n    },\n    \"showrss\": {\n      \"name\": \"showRSS\",\n      \"categoryId\": 8,\n      \"url\": \"https://showrss.info/\",\n      \"companyId\": \"showrss\",\n      \"source\": \"AdGuard\"\n    },\n    \"shrink\": {\n      \"name\": \"Shrink\",\n      \"categoryId\": 2,\n      \"url\": \"http://shink.in/\",\n      \"companyId\": \"shrink.in\"\n    },\n    \"shutterstock\": {\n      \"name\": \"Shutterstock\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.shutterstock.com/\",\n      \"companyId\": \"shutterstock_inc\"\n    },\n    \"siblesectiveal.club\": {\n      \"name\": \"siblesectiveal.club\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"sidecar\": {\n      \"name\": \"Sidecar\",\n      \"categoryId\": 6,\n      \"url\": \"http://hello.getsidecar.com/\",\n      \"companyId\": \"sidecar\"\n    },\n    \"sift_science\": {\n      \"name\": \"Sift Science\",\n      \"categoryId\": 6,\n      \"url\": \"https://siftscience.com/\",\n      \"companyId\": \"sift_science\"\n    },\n    \"signal\": {\n      \"name\": \"Signal\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.signal.co/\",\n      \"companyId\": \"signal_digital\"\n    },\n    \"signifyd\": {\n      \"name\": \"Signifyd\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.signifyd.com/\",\n      \"companyId\": \"signifyd\"\n    },\n    \"silverpop\": {\n      \"name\": \"Silverpop\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.silverpop.com/\",\n      \"companyId\": \"ibm\"\n    },\n    \"similardeals.net\": {\n      \"name\": \"SimilarDeals\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.similardeals.net/\",\n      \"companyId\": null\n    },\n    \"similarweb\": {\n      \"name\": \"SimilarWeb\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.similarweb.com/\",\n      \"companyId\": \"similarweb\",\n      \"source\": \"AdGuard\"\n    },\n    \"simplereach\": {\n      \"name\": \"SimpleReach\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.nativo.com/simplereach\",\n      \"companyId\": \"nativo\"\n    },\n    \"simpli.fi\": {\n      \"name\": \"Simpli.fi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.simpli.fi\",\n      \"companyId\": \"simpli.fi\"\n    },\n    \"sina\": {\n      \"name\": \"Sina\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.sina.com/\",\n      \"companyId\": \"sina\"\n    },\n    \"sina_cdn\": {\n      \"name\": \"Sina CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.sina.com.cn/\",\n      \"companyId\": \"sina\"\n    },\n    \"singlefeed\": {\n      \"name\": \"SingleFeed\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.singlefeed.com/\",\n      \"companyId\": \"singlefeed\"\n    },\n    \"sirdata\": {\n      \"name\": \"Sirdata\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.sirdata.com/home/\",\n      \"companyId\": \"sirdata\"\n    },\n    \"site24x7\": {\n      \"name\": \"Site24x7\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.site24x7.com/\",\n      \"companyId\": \"zoho_corp\"\n    },\n    \"site_booster\": {\n      \"name\": \"Site Booster\",\n      \"categoryId\": 7,\n      \"url\": \"https://sitebooster.com/\",\n      \"companyId\": \"site_booster\"\n    },\n    \"site_stratos\": {\n      \"name\": \"Site Stratos\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.infocube.co.jp/\",\n      \"companyId\": \"infocube\"\n    },\n    \"siteapps\": {\n      \"name\": \"SiteApps\",\n      \"categoryId\": 2,\n      \"url\": \"http://siteapps.com\",\n      \"companyId\": \"siteapps\"\n    },\n    \"sitebro\": {\n      \"name\": \"SiteBro\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.sitebro.net/\",\n      \"companyId\": \"sitebro\"\n    },\n    \"siteheart\": {\n      \"name\": \"SiteHeart\",\n      \"categoryId\": 2,\n      \"url\": \"http://siteheart.com/\",\n      \"companyId\": \"siteheart\"\n    },\n    \"siteimprove\": {\n      \"name\": \"Siteimprove\",\n      \"categoryId\": 6,\n      \"url\": \"http://siteimprove.com\",\n      \"companyId\": \"siteimprove\"\n    },\n    \"siteimprove_analytics\": {\n      \"name\": \"SiteImprove Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://siteimprove.com\",\n      \"companyId\": \"siteimprove\"\n    },\n    \"sitelabweb.com\": {\n      \"name\": \"sitelabweb.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"sitemeter\": {\n      \"name\": \"SiteMeter\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.sitemeter.com/\",\n      \"companyId\": \"sitemeter,_inc.\"\n    },\n    \"sitescout\": {\n      \"name\": \"SiteScout by Centro\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sitescout.com\",\n      \"companyId\": \"centro\"\n    },\n    \"sitetag\": {\n      \"name\": \"SiteTag\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.sitetag.us/\",\n      \"companyId\": \"sitetag\"\n    },\n    \"sitewit\": {\n      \"name\": \"SiteWit\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sitewit.com/\",\n      \"companyId\": \"sitewit\"\n    },\n    \"six_apart_advertising\": {\n      \"name\": \"Six Apart Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sixapart.com/advertising/\",\n      \"companyId\": \"six_apart\"\n    },\n    \"sixt-neuwagen.de\": {\n      \"name\": \"sixt-neuwagen.de\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"skadtec.com\": {\n      \"name\": \"GP One GmbH\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.gp-one.com/\",\n      \"companyId\": null\n    },\n    \"skimlinks\": {\n      \"name\": \"SkimLinks\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.skimlinks.com/\",\n      \"companyId\": \"skimlinks\"\n    },\n    \"skroutz\": {\n      \"name\": \"Skroutz\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.skroutz.gr/\",\n      \"companyId\": \"skroutz\"\n    },\n    \"skyglue\": {\n      \"name\": \"SkyGlue\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.skyglue.com/\",\n      \"companyId\": \"skyglue_technology\"\n    },\n    \"skype\": {\n      \"name\": \"Skype\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.skype.com\",\n      \"companyId\": \"microsoft\"\n    },\n    \"skysa\": {\n      \"name\": \"Skysa\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.skysa.com/\",\n      \"companyId\": \"skysa\"\n    },\n    \"skyscnr.com\": {\n      \"name\": \"Skyscanner CDN\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.skyscanner.net/\",\n      \"companyId\": null\n    },\n    \"slack\": {\n      \"name\": \"Slack\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.slack.com/\",\n      \"companyId\": \"salesforce\",\n      \"source\": \"AdGuard\"\n    },\n    \"slashdot_widget\": {\n      \"name\": \"Slashdot Widget\",\n      \"categoryId\": 2,\n      \"url\": \"http://slashdot.org\",\n      \"companyId\": \"slashdot\"\n    },\n    \"sleeknote\": {\n      \"name\": \"Sleeknote\",\n      \"categoryId\": 2,\n      \"url\": \"https://sleeknote.com/\",\n      \"companyId\": \"sleeknote\"\n    },\n    \"sli_systems\": {\n      \"name\": \"SLI Systems\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.sli-systems.com\",\n      \"companyId\": \"sli_systems\"\n    },\n    \"slice_factory\": {\n      \"name\": \"Slice Factory\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.slicefactory.com/\",\n      \"companyId\": \"slice_factory\"\n    },\n    \"slimcutmedia\": {\n      \"name\": \"SlimCutMedia\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.slimcutmedia.com/\",\n      \"companyId\": \"slimcutmedia\"\n    },\n    \"slingpic\": {\n      \"name\": \"Slingpic\",\n      \"categoryId\": 4,\n      \"url\": \"http://slingpic.com/\",\n      \"companyId\": \"affectv\"\n    },\n    \"smaato\": {\n      \"name\": \"Smaato\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.smaato.com/\",\n      \"companyId\": \"smaato\"\n    },\n    \"smart4ads\": {\n      \"name\": \"smart4ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.smart4ads.com\",\n      \"companyId\": \"smart4ads\"\n    },\n    \"smart_adserver\": {\n      \"name\": \"SMART AdServer\",\n      \"categoryId\": 4,\n      \"url\": \"https://smartadserver.com/\",\n      \"companyId\": \"smart_adserver\"\n    },\n    \"smart_call\": {\n      \"name\": \"Smart Call\",\n      \"categoryId\": 2,\n      \"url\": \"https://smartcall.kz/\",\n      \"companyId\": \"smart_call\"\n    },\n    \"smart_content\": {\n      \"name\": \"Smart Content\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.getsmartcontent.com\",\n      \"companyId\": \"get_smart_content\"\n    },\n    \"smart_device_media\": {\n      \"name\": \"Smart Device Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.smartdevicemedia.com/\",\n      \"companyId\": \"smart_device_media\"\n    },\n    \"smart_leads\": {\n      \"name\": \"Smart Leads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.cnt.my/\",\n      \"companyId\": \"smart_leads\"\n    },\n    \"smart_selling\": {\n      \"name\": \"Smart Selling\",\n      \"categoryId\": 2,\n      \"url\": \"https://smartselling.cz/\",\n      \"companyId\": \"smart_selling\"\n    },\n    \"smartad\": {\n      \"name\": \"smartAD\",\n      \"categoryId\": 4,\n      \"url\": \"http://smartad.eu/\",\n      \"companyId\": \"smartad\"\n    },\n    \"smartbn\": {\n      \"name\": \"SmartBN\",\n      \"categoryId\": 4,\n      \"url\": \"http://smartbn.ru/\",\n      \"companyId\": \"smartbn\"\n    },\n    \"smartclick.net\": {\n      \"name\": \"SmartClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://smartclick.net/\",\n      \"companyId\": null\n    },\n    \"smartclip\": {\n      \"name\": \"SmartClip\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.smartclip.com/\",\n      \"companyId\": \"smartclip\"\n    },\n    \"smartcontext\": {\n      \"name\": \"SmartContext\",\n      \"categoryId\": 4,\n      \"url\": \"http://smartcontext.pl/\",\n      \"companyId\": \"smartcontext\"\n    },\n    \"smarter_remarketer\": {\n      \"name\": \"SmarterHQ\",\n      \"categoryId\": 4,\n      \"url\": \"https://smarterhq.com\",\n      \"companyId\": \"smarterhq\"\n    },\n    \"smarter_travel\": {\n      \"name\": \"Smarter Travel Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.smartertravel.com/\",\n      \"companyId\": \"iac_apps\"\n    },\n    \"smarterclick\": {\n      \"name\": \"Smarterclick\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.smarterclick.co.uk/\",\n      \"companyId\": \"smarter_click\"\n    },\n    \"smartertrack\": {\n      \"name\": \"SmarterTrack\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.smartertrack.com/\",\n      \"companyId\": \"smartertrack\"\n    },\n    \"smartlink.cool\": {\n      \"name\": \"smartlink.cool\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"smartlook\": {\n      \"name\": \"Smartlook\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.smartlook.com/\",\n      \"companyId\": \"smartlook\"\n    },\n    \"smartstream.tv\": {\n      \"name\": \"SmartStream.TV\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.smartstream.tv/en\",\n      \"companyId\": \"smartstream\"\n    },\n    \"smartsupp_chat\": {\n      \"name\": \"Smartsupp Chat\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.smartsupp.com/\",\n      \"companyId\": \"smartsuppp\"\n    },\n    \"smi2.ru\": {\n      \"name\": \"smi2.ru\",\n      \"categoryId\": 6,\n      \"url\": \"https://smi2.net/\",\n      \"companyId\": \"media2_stat.media\"\n    },\n    \"smooch\": {\n      \"name\": \"Smooch\",\n      \"categoryId\": 2,\n      \"url\": \"https://smooch.io/\",\n      \"companyId\": \"smooch\"\n    },\n    \"smowtion\": {\n      \"name\": \"Smowtion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.smowtion.com/\",\n      \"companyId\": \"smowtion\"\n    },\n    \"smx_ventures\": {\n      \"name\": \"SMX Ventures\",\n      \"categoryId\": 6,\n      \"url\": \"http://smxeventures.com/\",\n      \"companyId\": \"smx_ventures\"\n    },\n    \"smyte\": {\n      \"name\": \"Smyte\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.smyte.com/\",\n      \"companyId\": \"smyte\"\n    },\n    \"snacktv\": {\n      \"name\": \"SnackTV\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.verizon.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"snacktv_player\": {\n      \"name\": \"SnackTV-Player\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.verizon.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"snap\": {\n      \"name\": \"Snap\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.snap.com/\",\n      \"companyId\": \"snap_technologies\"\n    },\n    \"snap_engage\": {\n      \"name\": \"Snap Engage\",\n      \"categoryId\": 2,\n      \"url\": \"https://snapengage.com/\",\n      \"companyId\": \"snap_engage\"\n    },\n    \"snapchat\": {\n      \"name\": \"Snapchat For Business\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.snapchat.com/\",\n      \"companyId\": \"snap_technologies\"\n    },\n    \"snapcraft\": {\n      \"name\": \"Snapcraft\",\n      \"categoryId\": 8,\n      \"url\": \"https://snapcraft.io\",\n      \"companyId\": \"canonical\",\n      \"source\": \"AdGuard\"\n    },\n    \"snigelweb\": {\n      \"name\": \"SnigelWeb, Inc.\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.snigelweb.com/\",\n      \"companyId\": \"snigelweb_inc\"\n    },\n    \"snoobi\": {\n      \"name\": \"Snoobi\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.snoobi.eu/\",\n      \"companyId\": \"snoobi\"\n    },\n    \"snoobi_analytics\": {\n      \"name\": \"Snoobi Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.snoobi.com/\",\n      \"companyId\": \"snoobi_oy\"\n    },\n    \"snowplow\": {\n      \"name\": \"Snowplow\",\n      \"categoryId\": 6,\n      \"url\": \"http://snowplowanalytics.com/\",\n      \"companyId\": \"snowplow\"\n    },\n    \"soasta_mpulse\": {\n      \"name\": \"SOASTA mPulse\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.soasta.com/\",\n      \"companyId\": \"akamai\"\n    },\n    \"sociable_labs\": {\n      \"name\": \"Sociable Labs\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sociablelabs.com/\",\n      \"companyId\": \"sociable_labs\"\n    },\n    \"social_amp\": {\n      \"name\": \"Social Amp\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.merkleinc.com/\",\n      \"companyId\": \"dentsu_aegis_network\"\n    },\n    \"social_annex\": {\n      \"name\": \"Social Annex\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.socialannex.com\",\n      \"companyId\": \"social_annex\"\n    },\n    \"social_miner\": {\n      \"name\": \"Social Miner\",\n      \"categoryId\": 7,\n      \"url\": \"https://socialminer.com/\",\n      \"companyId\": \"social_miner\"\n    },\n    \"socialbeat\": {\n      \"name\": \"socialbeat\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.socialbeat.it/\",\n      \"companyId\": \"socialbeat\"\n    },\n    \"socialrms\": {\n      \"name\": \"SocialRMS\",\n      \"categoryId\": 7,\n      \"url\": \"http://socialinterface.com/socialrms/\",\n      \"companyId\": \"socialinterface\"\n    },\n    \"sociaplus.com\": {\n      \"name\": \"SociaPlus\",\n      \"categoryId\": 6,\n      \"url\": \"https://sociaplus.com/\",\n      \"companyId\": null\n    },\n    \"sociomantic\": {\n      \"name\": \"Sociomantic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sociomantic.com/\",\n      \"companyId\": \"sociomantic_labs_gmbh\"\n    },\n    \"sohu\": {\n      \"name\": \"Sohu\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.sohu.com\",\n      \"companyId\": \"sohu\"\n    },\n    \"sojern\": {\n      \"name\": \"Sojern\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sojern.com/\",\n      \"companyId\": \"sojern\"\n    },\n    \"sokrati\": {\n      \"name\": \"Sokrati\",\n      \"categoryId\": 4,\n      \"url\": \"http://sokrati.com/\",\n      \"companyId\": \"sokrati\"\n    },\n    \"solads.media\": {\n      \"name\": \"solads.media\",\n      \"categoryId\": 4,\n      \"url\": \"http://solads.media/\",\n      \"companyId\": null\n    },\n    \"solaredge\": {\n      \"name\": \"SolarEdge Technologies, Inc.\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.solaredge.com/\",\n      \"companyId\": \"solaredge\",\n      \"source\": \"AdGuard\"\n    },\n    \"solidopinion\": {\n      \"name\": \"SolidOpinion\",\n      \"categoryId\": 2,\n      \"url\": \"https://solidopinion.com/\",\n      \"companyId\": \"solidopinion\"\n    },\n    \"solve_media\": {\n      \"name\": \"Solve Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://solvemedia.com/\",\n      \"companyId\": \"solve_media\"\n    },\n    \"soma_2\": {\n      \"name\": \"SOMA 2\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.webcombi.de/\",\n      \"companyId\": \"soma_2_gmbh\"\n    },\n    \"somoaudience\": {\n      \"name\": \"SoMo Audience\",\n      \"categoryId\": 4,\n      \"url\": \"https://somoaudience.com/\",\n      \"companyId\": \"somoaudience\"\n    },\n    \"sonobi\": {\n      \"name\": \"Sonobi\",\n      \"categoryId\": 4,\n      \"url\": \"http://sonobi.com/\",\n      \"companyId\": \"sonobi\"\n    },\n    \"sonos\": {\n      \"name\": \"Sonos\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.sonos.com/\",\n      \"companyId\": \"sonos\",\n      \"source\": \"AdGuard\"\n    },\n    \"sophus3\": {\n      \"name\": \"Sophus3\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sophus3.com/\",\n      \"companyId\": \"sophus3\"\n    },\n    \"sortable\": {\n      \"name\": \"Sortable\",\n      \"categoryId\": 4,\n      \"url\": \"https://sortable.com/\",\n      \"companyId\": \"sortable\"\n    },\n    \"soundcloud\": {\n      \"name\": \"SoundCloud\",\n      \"categoryId\": 0,\n      \"url\": \"http://soundcloud.com/\",\n      \"companyId\": \"soundcloud\"\n    },\n    \"sourceknowledge_pixel\": {\n      \"name\": \"SourceKnowledge Pixel\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.provenpixel.com/\",\n      \"companyId\": \"sourceknowledge\"\n    },\n    \"sourcepoint\": {\n      \"name\": \"Sourcepoint\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.sourcepoint.com/\",\n      \"companyId\": \"sourcepoint\"\n    },\n    \"sovrn\": {\n      \"name\": \"sovrn\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.sovrn.com/\",\n      \"companyId\": \"sovrn\"\n    },\n    \"sovrn_viewability_solutions\": {\n      \"name\": \"Sovrn Signal\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.sovrn.com/publishers/signal/\",\n      \"companyId\": \"sovrn\"\n    },\n    \"spark_studios\": {\n      \"name\": \"Spark Studios\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.sparkstudios.com/\",\n      \"companyId\": \"spark_studios\"\n    },\n    \"sparkasse.de\": {\n      \"name\": \"sparkasse.de\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"speakpipe\": {\n      \"name\": \"SpeakPipe\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.speakpipe.com/\",\n      \"companyId\": \"speakpipe\"\n    },\n    \"specific_media\": {\n      \"name\": \"Specific Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.specificmedia.com\",\n      \"companyId\": \"specific_media\"\n    },\n    \"spectate\": {\n      \"name\": \"Spectate\",\n      \"categoryId\": 6,\n      \"url\": \"http://spectate.com/\",\n      \"companyId\": \"spectate\"\n    },\n    \"speed_shift_media\": {\n      \"name\": \"Speed Shift Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.speedshiftmedia.com/\",\n      \"companyId\": \"speed_shift_media\"\n    },\n    \"speedcurve\": {\n      \"name\": \"SpeedCurve\",\n      \"categoryId\": 6,\n      \"url\": \"https://speedcurve.com/\",\n      \"companyId\": null\n    },\n    \"speedyads\": {\n      \"name\": \"SpeedyAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.entireweb.com/speedyads/\",\n      \"companyId\": \"entireweb\"\n    },\n    \"speee\": {\n      \"name\": \"Speee\",\n      \"categoryId\": 4,\n      \"url\": \"https://speee.jp\",\n      \"companyId\": \"speee\"\n    },\n    \"sphere\": {\n      \"name\": \"Sphere\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sphere.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"spheremall\": {\n      \"name\": \"SphereMall\",\n      \"categoryId\": 6,\n      \"url\": \"https://spheremall.com\",\n      \"companyId\": \"spheremall\"\n    },\n    \"sphereup\": {\n      \"name\": \"SphereUp\",\n      \"categoryId\": 2,\n      \"url\": \"http://zoomd.com/\",\n      \"companyId\": \"zoomd\"\n    },\n    \"spicy\": {\n      \"name\": \"Spicy\",\n      \"categoryId\": 4,\n      \"url\": \"http://sspicy.ru/#main\",\n      \"companyId\": \"spicy_ssp\"\n    },\n    \"spider.ad\": {\n      \"name\": \"Spider.Ad\",\n      \"categoryId\": 4,\n      \"url\": \"http://spider.ad/\",\n      \"companyId\": \"spider.ad\"\n    },\n    \"spider_ads\": {\n      \"name\": \"Spider Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.spiderads.eu/\",\n      \"companyId\": \"spiderads\"\n    },\n    \"spinnakr\": {\n      \"name\": \"Spinnakr\",\n      \"categoryId\": 6,\n      \"url\": \"http://spinnakr.com/\",\n      \"companyId\": \"spinnakr\"\n    },\n    \"spokenlayer\": {\n      \"name\": \"SpokenLayer\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.spokenlayer.com\",\n      \"companyId\": \"spokenlayer\"\n    },\n    \"spongecell\": {\n      \"name\": \"Spongecell\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.spongecell.com/\",\n      \"companyId\": \"spongecell\"\n    },\n    \"sponsorads.de\": {\n      \"name\": \"SponsorAds.de\",\n      \"categoryId\": 4,\n      \"url\": \"http://sponsorads.de\",\n      \"companyId\": \"sponsorads.de\"\n    },\n    \"sportsbet_affiliates\": {\n      \"name\": \"Sportsbet Affiliates\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sportsbetaffiliates.com.au/\",\n      \"companyId\": \"sportsbet_affiliates\"\n    },\n    \"spot.im\": {\n      \"name\": \"Spot.IM\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.spot.im/\",\n      \"companyId\": \"spot.im\"\n    },\n    \"spoteffect\": {\n      \"name\": \"Spoteffect\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.spoteffects.com/home/\",\n      \"companyId\": \"spoteffect\"\n    },\n    \"spotify\": {\n      \"name\": \"Spotify\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.spotify.com/\",\n      \"companyId\": \"spotify\"\n    },\n    \"spotify_embed\": {\n      \"name\": \"Spotify Embed\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.spotify.com\",\n      \"companyId\": \"spotify\"\n    },\n    \"spotscenered.info\": {\n      \"name\": \"spotscenered.info\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"spotxchange\": {\n      \"name\": \"SpotX\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.spotx.tv/\",\n      \"companyId\": \"rtl_group\"\n    },\n    \"spoutable\": {\n      \"name\": \"Spoutable\",\n      \"categoryId\": 4,\n      \"url\": \"http://spoutable.com/\",\n      \"companyId\": \"spoutable\"\n    },\n    \"springboard\": {\n      \"name\": \"SpringBoard\",\n      \"categoryId\": 4,\n      \"url\": \"http://home.springboardplatform.com/\",\n      \"companyId\": \"springboard\"\n    },\n    \"springserve\": {\n      \"name\": \"SpringServe\",\n      \"categoryId\": 4,\n      \"url\": \"http://springserve.com/\",\n      \"companyId\": \"springserve\"\n    },\n    \"sprinklr\": {\n      \"name\": \"Sprinklr\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.sprinklr.com/\",\n      \"companyId\": \"sprinklr\"\n    },\n    \"sputnik\": {\n      \"name\": \"Sputnik\",\n      \"categoryId\": 6,\n      \"url\": \"https://cnt.sputnik.ru/\",\n      \"companyId\": \"sputnik\"\n    },\n    \"squadata\": {\n      \"name\": \"Squadata\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.email-match.net/\",\n      \"companyId\": \"squadata\"\n    },\n    \"squarespace.com\": {\n      \"name\": \"Squarespace\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.squarespace.com/\",\n      \"companyId\": null\n    },\n    \"srvtrck.com\": {\n      \"name\": \"srvtrck.com\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"srvvtrk.com\": {\n      \"name\": \"srvvtrk.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"sstatic.net\": {\n      \"name\": \"Stack Exchange\",\n      \"categoryId\": 9,\n      \"url\": \"https://sstatic.net/\",\n      \"companyId\": null\n    },\n    \"st-hatena\": {\n      \"name\": \"Hatena\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.hatena.ne.jp/\",\n      \"companyId\": \"hatena_jp\"\n    },\n    \"stackadapt\": {\n      \"name\": \"StackAdapt\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.stackadapt.com/\",\n      \"companyId\": \"stackadapt\"\n    },\n    \"stackpathdns.com\": {\n      \"name\": \"StackPath\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.stackpath.com/\",\n      \"companyId\": null\n    },\n    \"stailamedia_com\": {\n      \"name\": \"stailamedia.com\",\n      \"categoryId\": 4,\n      \"url\": \"http://stailamedia.com/\",\n      \"companyId\": null\n    },\n    \"stalluva.pro\": {\n      \"name\": \"stalluva.pro\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"startapp\": {\n      \"name\": \"StartApp\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.startapp.com/\",\n      \"companyId\": null\n    },\n    \"stat24\": {\n      \"name\": \"Stat24\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.stat24.com/en/\",\n      \"companyId\": \"stat24\"\n    },\n    \"stat4u\": {\n      \"name\": \"stat4u\",\n      \"categoryId\": 6,\n      \"url\": \"http://stat.4u.pl/\",\n      \"companyId\": \"stat4u\"\n    },\n    \"statcounter\": {\n      \"name\": \"Statcounter\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.statcounter.com/\",\n      \"companyId\": \"statcounter\"\n    },\n    \"stathat\": {\n      \"name\": \"StatHat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.stathat.com/\",\n      \"companyId\": \"stathat\"\n    },\n    \"statisfy\": {\n      \"name\": \"Statisfy\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.statisfy.com/\",\n      \"companyId\": \"statisfy\"\n    },\n    \"statsy.net\": {\n      \"name\": \"statsy.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"statuscake\": {\n      \"name\": \"StatusCake\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.statuscake.com/\",\n      \"companyId\": \"statuscake\"\n    },\n    \"statuspage.io\": {\n      \"name\": \"Statuspage\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.statuspage.io/\",\n      \"companyId\": \"atlassian\"\n    },\n    \"stayfriends.de\": {\n      \"name\": \"stayfriends.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.stayfriends.de/\",\n      \"companyId\": null\n    },\n    \"steelhouse\": {\n      \"name\": \"Steel House Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://steelhouse.com/\",\n      \"companyId\": \"steelhouse\"\n    },\n    \"steepto.com\": {\n      \"name\": \"Steepto\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.steepto.com/\",\n      \"companyId\": null\n    },\n    \"stepstone.com\": {\n      \"name\": \"StepStone\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.stepstone.com/\",\n      \"companyId\": null\n    },\n    \"stetic\": {\n      \"name\": \"Stetic\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.stetic.com/\",\n      \"companyId\": \"stetic\"\n    },\n    \"stickyads\": {\n      \"name\": \"StickyAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://corporate.comcast.com/\",\n      \"companyId\": \"comcast\"\n    },\n    \"stocktwits\": {\n      \"name\": \"StockTwits\",\n      \"categoryId\": 2,\n      \"url\": \"http://stocktwits.com\",\n      \"companyId\": \"stocktwits\"\n    },\n    \"storify\": {\n      \"name\": \"Storify\",\n      \"categoryId\": 4,\n      \"url\": \"https://storify.com/\",\n      \"companyId\": \"adobe\"\n    },\n    \"storygize\": {\n      \"name\": \"Storygize\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.storygize.com/\",\n      \"companyId\": null\n    },\n    \"strands_recommender\": {\n      \"name\": \"Strands Recommender\",\n      \"categoryId\": 4,\n      \"url\": \"http://recommender.strands.com\",\n      \"companyId\": \"strands\"\n    },\n    \"strava\": {\n      \"name\": \"Strava\",\n      \"categoryId\": 6,\n      \"url\": \"https://strava.com\",\n      \"companyId\": \"strava\"\n    },\n    \"streak\": {\n      \"name\": \"Streak\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.streak.com/\",\n      \"companyId\": \"streak\"\n    },\n    \"streamotion\": {\n      \"name\": \"Streamotion\",\n      \"categoryId\": 0,\n      \"url\": \"https://streamotion.com.au/\",\n      \"companyId\": \"news_corp\",\n      \"source\": \"AdGuard\"\n    },\n    \"streamrail.com\": {\n      \"name\": \"StreamRail\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.streamrail.com/\",\n      \"companyId\": \"ironsource\"\n    },\n    \"stride\": {\n      \"name\": \"Stride\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.getstride.com/\",\n      \"companyId\": \"stride_software\"\n    },\n    \"stripchat.com\": {\n      \"name\": \"stripchat.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"stripe.com\": {\n      \"name\": \"Stripe\",\n      \"categoryId\": 2,\n      \"url\": \"https://stripe.com/\",\n      \"companyId\": null\n    },\n    \"stripst.com\": {\n      \"name\": \"stripst.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"stroer_digital_media\": {\n      \"name\": \"Stroer Digital Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.stroeer.de/\",\n      \"companyId\": \"stroer\"\n    },\n    \"strossle\": {\n      \"name\": \"Strossle\",\n      \"categoryId\": 4,\n      \"url\": \"https://strossle.com/\",\n      \"companyId\": \"strossle\"\n    },\n    \"struq\": {\n      \"name\": \"Struq\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.struq.com/\",\n      \"companyId\": \"quantcast\"\n    },\n    \"stumbleupon_widgets\": {\n      \"name\": \"StumbleUpon Widgets\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.stumbleupon.com/\",\n      \"companyId\": \"stumbleupon\"\n    },\n    \"sub2\": {\n      \"name\": \"Sub2\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sub2tech.com/\",\n      \"companyId\": \"sub2\"\n    },\n    \"sublime_skinz\": {\n      \"name\": \"Sublime\",\n      \"categoryId\": 4,\n      \"url\": \"https://sublimeskinz.com/home\",\n      \"companyId\": \"sublime_skinz\"\n    },\n    \"suggest.io\": {\n      \"name\": \"Suggest.io\",\n      \"categoryId\": 4,\n      \"url\": \"https://suggest.io/\",\n      \"companyId\": \"suggest.io\"\n    },\n    \"sumologic.com\": {\n      \"name\": \"Sumologic\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.sumologic.com/\",\n      \"companyId\": null\n    },\n    \"sumome\": {\n      \"name\": \"Sumo\",\n      \"categoryId\": 6,\n      \"url\": \"https://sumo.com/\",\n      \"companyId\": \"sumome\"\n    },\n    \"sundaysky\": {\n      \"name\": \"SundaySky\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sundaysky.com/\",\n      \"companyId\": \"sundaysky\"\n    },\n    \"supercell\": {\n      \"name\": \"Supercell\",\n      \"categoryId\": 2,\n      \"url\": \"https://supercell.com/\",\n      \"companyId\": \"supercell\",\n      \"source\": \"AdGuard\"\n    },\n    \"supercounters\": {\n      \"name\": \"SuperCounters\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.supercounters.com/\",\n      \"companyId\": \"supercounters\"\n    },\n    \"superfastcdn.com\": {\n      \"name\": \"superfastcdn.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"supership\": {\n      \"name\": \"Supership\",\n      \"categoryId\": 4,\n      \"url\": \"https://supership.jp/en/\",\n      \"companyId\": \"supership\"\n    },\n    \"supplyframe\": {\n      \"name\": \"SupplyFrame\",\n      \"categoryId\": 4,\n      \"url\": \"https://supplyframe.com/\",\n      \"companyId\": \"supplyframe\"\n    },\n    \"surf_by_surfingbird\": {\n      \"name\": \"Surf by Surfingbird\",\n      \"categoryId\": 2,\n      \"url\": \"http://surfingbird.ru/\",\n      \"companyId\": \"surfingbird\"\n    },\n    \"survata\": {\n      \"name\": \"Survata\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.survata.com/\",\n      \"companyId\": \"survata\"\n    },\n    \"sweettooth\": {\n      \"name\": \"Sweettooth\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.sweettoothrewards.com/\",\n      \"companyId\": \"sweet_tooth_rewards\"\n    },\n    \"swiftype\": {\n      \"name\": \"Swiftype\",\n      \"categoryId\": 9,\n      \"url\": \"https://swiftype.com/\",\n      \"companyId\": \"elastic\"\n    },\n    \"swisscom\": {\n      \"name\": \"Swisscom\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"switch_concepts\": {\n      \"name\": \"Switch Concepts\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.switchconcepts.co.uk/\",\n      \"companyId\": \"switch_concepts\"\n    },\n    \"switchtv\": {\n      \"name\": \"Switch Media\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.switch.tv/\",\n      \"companyId\": \"switchtv\",\n      \"source\": \"AdGuard\"\n    },\n    \"swoop\": {\n      \"name\": \"Swoop\",\n      \"categoryId\": 4,\n      \"url\": \"http://swoop.com/\",\n      \"companyId\": \"swoop\"\n    },\n    \"sykes\": {\n      \"name\": \"Sykes\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.sykescottages.co.uk/\",\n      \"companyId\": \"sykes_cottages\"\n    },\n    \"symantec\": {\n      \"name\": \"Symantec (Norton Secured Seal)\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.symantec.com/page.jsp?id=ssl-resources&tabID=3#\",\n      \"companyId\": \"symantec\"\n    },\n    \"symphony_talent\": {\n      \"name\": \"Symphony Talent\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.symphonytalent.com/\",\n      \"companyId\": \"symphony_talent\"\n    },\n    \"synacor\": {\n      \"name\": \"Synacor\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.synacor.com/\",\n      \"companyId\": \"synacor\"\n    },\n    \"syncapse\": {\n      \"name\": \"Syncapse\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickable.com/\",\n      \"companyId\": \"syncapse\"\n    },\n    \"synergy-e\": {\n      \"name\": \"Synergy-E\",\n      \"categoryId\": 4,\n      \"url\": \"http://synergy-e.com/\",\n      \"companyId\": \"synergy-e\"\n    },\n    \"t-mobile\": {\n      \"name\": \"Deutsche Telekom\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"t8cdn.com\": {\n      \"name\": \"t8cdn.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"tableteducation.com\": {\n      \"name\": \"tableteducation.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"taboola\": {\n      \"name\": \"Taboola\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.taboola.com\",\n      \"companyId\": \"taboola\"\n    },\n    \"tacoda\": {\n      \"name\": \"Tacoda\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tacoda.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"tag_commander\": {\n      \"name\": \"Commanders Act\",\n      \"categoryId\": 5,\n      \"url\": \"https://www.commandersact.com/en/\",\n      \"companyId\": \"tag_commander\"\n    },\n    \"tagcade\": {\n      \"name\": \"Tagcade\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.pubvantage.com/\",\n      \"companyId\": \"pubvantage\"\n    },\n    \"taggify\": {\n      \"name\": \"Taggify\",\n      \"categoryId\": 4,\n      \"url\": \"http://new.taggify.net/\",\n      \"companyId\": \"taggify\"\n    },\n    \"taggy\": {\n      \"name\": \"TAGGY\",\n      \"categoryId\": 4,\n      \"url\": \"http://taggy.jp/\",\n      \"companyId\": \"taggy\"\n    },\n    \"tagman\": {\n      \"name\": \"TagMan\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.tagman.com/\",\n      \"companyId\": \"ensighten\"\n    },\n    \"tail_target\": {\n      \"name\": \"Tail\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.tail.digital/\",\n      \"companyId\": \"tail.digital\"\n    },\n    \"tailsweep\": {\n      \"name\": \"Tailsweep\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tailsweep.se/\",\n      \"companyId\": \"tailsweep\"\n    },\n    \"tamedia.ch\": {\n      \"name\": \"Tamedia\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.tamedia.ch/\",\n      \"companyId\": null\n    },\n    \"tanx\": {\n      \"name\": \"Tanx\",\n      \"categoryId\": 4,\n      \"url\": \"http://tanx.com/\",\n      \"companyId\": \"tanx\"\n    },\n    \"taobao\": {\n      \"name\": \"Taobao\",\n      \"categoryId\": 4,\n      \"url\": \"https://world.taobao.com/\",\n      \"companyId\": \"softbank\",\n      \"source\": \"AdGuard\"\n    },\n    \"tapad\": {\n      \"name\": \"Tapad\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tapad.com/\",\n      \"companyId\": \"telenor\"\n    },\n    \"tapinfluence\": {\n      \"name\": \"TapInfluence\",\n      \"categoryId\": 4,\n      \"url\": \"http://theblogfrog.com/\",\n      \"companyId\": \"tapinfluence\"\n    },\n    \"tarafdari\": {\n      \"name\": \"Tarafdari\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.tarafdari.com/\",\n      \"companyId\": \"tarafdari\"\n    },\n    \"target_2_sell\": {\n      \"name\": \"Target 2 Sell\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.target2sell.com/en/\",\n      \"companyId\": \"target_2_sell\"\n    },\n    \"target_circle\": {\n      \"name\": \"Target Circle\",\n      \"categoryId\": 6,\n      \"url\": \"http://targetcircle.com\",\n      \"companyId\": \"target_circle\"\n    },\n    \"target_fuel\": {\n      \"name\": \"Target Fuel\",\n      \"categoryId\": 6,\n      \"url\": \"http://targetfuel.com/\",\n      \"companyId\": \"target_fuel\"\n    },\n    \"tawk\": {\n      \"name\": \"Tawk\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.tawk.to/\",\n      \"companyId\": \"tawk\"\n    },\n    \"tbn.ru\": {\n      \"name\": \"TBN.ru\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.agava.ru\",\n      \"companyId\": \"agava\"\n    },\n    \"tchibo_de\": {\n      \"name\": \"tchibo.de\",\n      \"categoryId\": 8,\n      \"url\": \"http://tchibo.de/\",\n      \"companyId\": null\n    },\n    \"tdsrmbl_net\": {\n      \"name\": \"tdsrmbl.net\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"teads\": {\n      \"name\": \"Teads\",\n      \"categoryId\": 4,\n      \"url\": \"http://teads.tv/\",\n      \"companyId\": \"teads\"\n    },\n    \"tealeaf\": {\n      \"name\": \"Tealeaf\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.ibm.com/digital-marketing\",\n      \"companyId\": \"ibm\"\n    },\n    \"tealium\": {\n      \"name\": \"Tealium\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.tealium.com/\",\n      \"companyId\": \"tealium\"\n    },\n    \"teaser.cc\": {\n      \"name\": \"Teaser.cc\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.teaser.cc/\",\n      \"companyId\": \"teaser.cc\"\n    },\n    \"tedemis\": {\n      \"name\": \"Tedemis\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tedemis.com\",\n      \"companyId\": \"tedemis\"\n    },\n    \"teletech\": {\n      \"name\": \"TeleTech\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.webmetro.com/whoweare/technology.aspx\",\n      \"companyId\": \"teletech\"\n    },\n    \"telstra\": {\n      \"name\": \"Telstra\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.telstra.com.au/\",\n      \"companyId\": \"telstra\",\n      \"source\": \"AdGuard\"\n    },\n    \"tender\": {\n      \"name\": \"Tender\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.tenderapp.com/\",\n      \"companyId\": \"tender\"\n    },\n    \"tensitionschoo.club\": {\n      \"name\": \"tensitionschoo.club\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"teroti\": {\n      \"name\": \"Teroti\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.teroti.com/\",\n      \"companyId\": \"teroti\"\n    },\n    \"terren\": {\n      \"name\": \"Terren\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.webterren.com/\",\n      \"companyId\": \"terren\"\n    },\n    \"teufel.de\": {\n      \"name\": \"teufel.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.teufel.de/\",\n      \"companyId\": null\n    },\n    \"the_adex\": {\n      \"name\": \"The ADEX\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.theadex.com/\",\n      \"companyId\": \"prosieben_sat1\"\n    },\n    \"the_deck\": {\n      \"name\": \"The DECK\",\n      \"categoryId\": 4,\n      \"url\": \"http://decknetwork.net/\",\n      \"companyId\": \"the_deck\"\n    },\n    \"the_guardian\": {\n      \"name\": \"The Guardian\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.theguardian.com/\",\n      \"companyId\": \"the_guardian\"\n    },\n    \"the_reach_group\": {\n      \"name\": \"The Reach Group\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.redvertisment.com\",\n      \"companyId\": \"the_reach_group\"\n    },\n    \"the_search_agency\": {\n      \"name\": \"The Search Agency\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.thesearchagency.com/\",\n      \"companyId\": \"the_search_agency\"\n    },\n    \"the_sun\": {\n      \"name\": \"The Sun\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.thesun.co.uk/\",\n      \"companyId\": \"the_sun\"\n    },\n    \"the_weather_company\": {\n      \"name\": \"The Weather Company\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.theweathercompany.com/\",\n      \"companyId\": \"ibm\"\n    },\n    \"themoviedb\": {\n      \"name\": \"The Movie DB\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.themoviedb.org/\",\n      \"companyId\": \"themoviedb\"\n    },\n    \"thinglink\": {\n      \"name\": \"ThingLink\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.thinglink.com/\",\n      \"companyId\": \"thinglink\"\n    },\n    \"threatmetrix\": {\n      \"name\": \"ThreatMetrix\",\n      \"categoryId\": 6,\n      \"url\": \"http://threatmetrix.com/\",\n      \"companyId\": \"threatmetrix\"\n    },\n    \"tidbit\": {\n      \"name\": \"Tidbit\",\n      \"categoryId\": 2,\n      \"url\": \"http://tidbit.co.in/\",\n      \"companyId\": \"tidbit\"\n    },\n    \"tidio\": {\n      \"name\": \"Tidio\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.tidio.com/\",\n      \"companyId\": \"tidio_chat\"\n    },\n    \"tiktok_analytics\": {\n      \"name\": \"TikTok Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://analytics.tiktok.com\",\n      \"companyId\": \"bytedance_inc\"\n    },\n    \"tiller\": {\n      \"name\": \"Tiller\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.tiller.com/\",\n      \"companyId\": \"tiller\"\n    },\n    \"timezondb\": {\n      \"name\": \"TimezonDB\",\n      \"categoryId\": 4,\n      \"url\": \"https://timezonedb.com/\",\n      \"companyId\": \"timezonedb\"\n    },\n    \"tinypass\": {\n      \"name\": \"Piano\",\n      \"categoryId\": 5,\n      \"url\": \"https://piano.io/\",\n      \"companyId\": \"piano\"\n    },\n    \"tisoomi\": {\n      \"name\": \"Tisoomi\",\n      \"categoryId\": 4,\n      \"url\": \"https://tisoomi-services.com/\",\n      \"companyId\": null\n    },\n    \"tlv_media\": {\n      \"name\": \"TLV Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tlvmedia.com\",\n      \"companyId\": \"tlvmedia\"\n    },\n    \"tns\": {\n      \"name\": \"TNS\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.tnsglobal.com/\",\n      \"companyId\": \"wpp\"\n    },\n    \"tomnewsupdate.info\": {\n      \"name\": \"tomnewsupdate.info\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"tomorrow_focus\": {\n      \"name\": \"Tomorrow Focus\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tomorrow-focus.com\",\n      \"companyId\": \"hubert_burda_media\"\n    },\n    \"tonefuse\": {\n      \"name\": \"ToneFuse\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tonefuse.com/\",\n      \"companyId\": \"tonefuse\"\n    },\n    \"top_mail\": {\n      \"name\": \"Top Mail\",\n      \"categoryId\": 6,\n      \"url\": \"https://corp.megafon.com/\",\n      \"companyId\": \"megafon\"\n    },\n    \"toplist.cz\": {\n      \"name\": \"toplist.cz\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"toponclick_com\": {\n      \"name\": \"toponclick.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"topsy\": {\n      \"name\": \"Topsy\",\n      \"categoryId\": 4,\n      \"url\": \"http://topsy.com/\",\n      \"companyId\": \"topsy\"\n    },\n    \"torbit\": {\n      \"name\": \"Torbit\",\n      \"categoryId\": 6,\n      \"url\": \"http://torbit.com/\",\n      \"companyId\": \"torbit\"\n    },\n    \"toro\": {\n      \"name\": \"TORO\",\n      \"categoryId\": 4,\n      \"url\": \"http://toroadvertising.com/\",\n      \"companyId\": \"toro_advertising\"\n    },\n    \"tororango.com\": {\n      \"name\": \"tororango.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"total_media\": {\n      \"name\": \"Total Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.totalmedia.co.il/eng/\",\n      \"companyId\": \"total_media\"\n    },\n    \"touchcommerce\": {\n      \"name\": \"Nuance\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.nuance.com/omni-channel-customer-engagement/digital.html\",\n      \"companyId\": \"touchcommerce\"\n    },\n    \"tovarro.com\": {\n      \"name\": \"Tovarro\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.tovarro.com/\",\n      \"companyId\": null\n    },\n    \"tp-cdn.com\": {\n      \"name\": \"TrialPay\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.trialpay.com/\",\n      \"companyId\": null\n    },\n    \"tracc.it\": {\n      \"name\": \"Kiwe.io\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.kiwe.io/\",\n      \"companyId\": null\n    },\n    \"tracemyip\": {\n      \"name\": \"TraceMyIP\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tracemyip.org/\",\n      \"companyId\": \"tracemyip\"\n    },\n    \"traceview\": {\n      \"name\": \"TraceView\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.appneta.com/\",\n      \"companyId\": \"appneta\"\n    },\n    \"track_duck\": {\n      \"name\": \"Track Duck\",\n      \"categoryId\": 6,\n      \"url\": \"https://trackduck.com/\",\n      \"companyId\": \"track_duck\"\n    },\n    \"trackjs\": {\n      \"name\": \"TrackJS\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.trackjs.com/\",\n      \"companyId\": \"trackjs\"\n    },\n    \"trackset_conversionlab\": {\n      \"name\": \"Trackset ConversionLab\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.trackset.com/\",\n      \"companyId\": \"trackset\"\n    },\n    \"trackuity\": {\n      \"name\": \"Trackuity\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.trackuity.com/\",\n      \"companyId\": \"trackuity\"\n    },\n    \"tradedesk\": {\n      \"name\": \"TradeDesk\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.thetradedesk.com/\",\n      \"companyId\": \"the_trade_desk\"\n    },\n    \"tradedoubler\": {\n      \"name\": \"TradeDoubler\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tradedoubler.com/\",\n      \"companyId\": \"tradedoubler\"\n    },\n    \"tradelab\": {\n      \"name\": \"Tradelab\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tradelab.fr/\",\n      \"companyId\": \"tradelab\"\n    },\n    \"tradetracker\": {\n      \"name\": \"TradeTracker\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tradetracker.com\",\n      \"companyId\": \"tradetracker\"\n    },\n    \"traffective\": {\n      \"name\": \"Traffective\",\n      \"categoryId\": 4,\n      \"url\": \"https://traffective.com/\",\n      \"companyId\": null\n    },\n    \"traffic_fuel\": {\n      \"name\": \"Traffic Fuel\",\n      \"categoryId\": 4,\n      \"url\": \"https://trafficfuel.com/\",\n      \"companyId\": \"traffic_fuel\"\n    },\n    \"traffic_revenue\": {\n      \"name\": \"Traffic Revenue\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.trafficrevenue.net/\",\n      \"companyId\": \"traffic_revenue\"\n    },\n    \"traffic_stars\": {\n      \"name\": \"Traffic Stars\",\n      \"categoryId\": 3,\n      \"url\": \"https://trafficstars.com/#index_page\",\n      \"companyId\": \"traffic_stars\"\n    },\n    \"trafficbroker\": {\n      \"name\": \"TrafficBroker\",\n      \"categoryId\": 4,\n      \"url\": \"http://trafficbroker.com/\",\n      \"companyId\": \"trafficbroker\"\n    },\n    \"trafficfabrik.com\": {\n      \"name\": \"Traffic Fabrik\",\n      \"categoryId\": 3,\n      \"url\": \"https://www.trafficfabrik.com/\",\n      \"companyId\": null\n    },\n    \"trafficfactory\": {\n      \"name\": \"Traffic Factory\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.trafficfactory.biz/\",\n      \"companyId\": null\n    },\n    \"trafficforce\": {\n      \"name\": \"TrafficForce\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.trafficforce.com/\",\n      \"companyId\": \"trafficforce\"\n    },\n    \"traffichaus\": {\n      \"name\": \"TrafficHaus\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.traffichaus.com\",\n      \"companyId\": \"traffichaus\"\n    },\n    \"trafficjunky\": {\n      \"name\": \"TrafficJunky\",\n      \"categoryId\": 3,\n      \"url\": \"http://www.trafficjunky.net/\",\n      \"companyId\": \"trafficjunky\"\n    },\n    \"traffiliate\": {\n      \"name\": \"Traffiliate\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.traffiliate.com/\",\n      \"companyId\": \"dsnr_media_group\"\n    },\n    \"trafic\": {\n      \"name\": \"Trafic\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.trafic.ro/\",\n      \"companyId\": \"trafic\"\n    },\n    \"trafmag.com\": {\n      \"name\": \"TrafMag\",\n      \"categoryId\": 4,\n      \"url\": \"https://trafmag.com/\",\n      \"companyId\": \"trafmag\"\n    },\n    \"transcend\": {\n      \"name\": \"Transcend Consent\",\n      \"categoryId\": 14,\n      \"url\": \"https://transcend.io/consent/\",\n      \"companyId\": \"transcend\"\n    },\n    \"transcend_telemetry\": {\n      \"name\": \"Transcend Telemetry\",\n      \"categoryId\": 6,\n      \"url\": \"https://transcend.io\",\n      \"companyId\": \"transcend\"\n    },\n    \"transmatic\": {\n      \"name\": \"Transmatic\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.transmatico.com/en/\",\n      \"companyId\": \"transmatico\"\n    },\n    \"travel_audience\": {\n      \"name\": \"Travel Audience\",\n      \"categoryId\": 6,\n      \"url\": \"https://travelaudience.com/\",\n      \"companyId\": \"travel_audience\"\n    },\n    \"trbo\": {\n      \"name\": \"trbo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.trbo.com/\",\n      \"companyId\": \"trbo\"\n    },\n    \"treasuredata\": {\n      \"name\": \"Treasure Data\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.treasuredata.com/\",\n      \"companyId\": \"arm\"\n    },\n    \"tremor_video\": {\n      \"name\": \"Tremor Video\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.tremormedia.com/\",\n      \"companyId\": \"tremor_video\"\n    },\n    \"trendcounter\": {\n      \"name\": \"trendcounter\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.trendcounter.com/\",\n      \"companyId\": \"trendcounter\"\n    },\n    \"trendemon\": {\n      \"name\": \"TrenDemon\",\n      \"categoryId\": 6,\n      \"url\": \"http://trendemon.com\",\n      \"companyId\": \"trendemon\"\n    },\n    \"tribal_fusion\": {\n      \"name\": \"Tribal Fusion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tribalfusion.com/\",\n      \"companyId\": \"exponential_interactive\"\n    },\n    \"tribal_fusion_notice\": {\n      \"name\": \"Tribal Fusion Notice\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tribalfusion.com\",\n      \"companyId\": \"exponential_interactive\"\n    },\n    \"triblio\": {\n      \"name\": \"Triblio\",\n      \"categoryId\": 6,\n      \"url\": \"https://triblio.com/\",\n      \"companyId\": \"triblio\"\n    },\n    \"trigger_mail_marketing\": {\n      \"name\": \"Trigger Mail Marketing\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.triggeremailmarketing.com/\",\n      \"companyId\": \"trigger_mail_marketing\"\n    },\n    \"triggerbee\": {\n      \"name\": \"Triggerbee\",\n      \"categoryId\": 2,\n      \"url\": \"https://triggerbee.com/\",\n      \"companyId\": \"triggerbee\"\n    },\n    \"tripadvisor\": {\n      \"name\": \"TripAdvisor\",\n      \"categoryId\": 8,\n      \"url\": \"http://iac.com/\",\n      \"companyId\": \"iac_apps\"\n    },\n    \"triplelift\": {\n      \"name\": \"TripleLift\",\n      \"categoryId\": 4,\n      \"url\": \"http://triplelift.com/\",\n      \"companyId\": \"triplelift\"\n    },\n    \"triptease\": {\n      \"name\": \"Triptease\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.triptease.com\",\n      \"companyId\": \"triptease\"\n    },\n    \"triton_digital\": {\n      \"name\": \"Triton Digital\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.tritondigital.com/\",\n      \"companyId\": \"triton_digital\"\n    },\n    \"trovus_revelations\": {\n      \"name\": \"Trovus Revelations\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.trovus.co.uk/\",\n      \"companyId\": \"trovus_revelations\"\n    },\n    \"trsv3.com\": {\n      \"name\": \"trsv3.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"true_fit\": {\n      \"name\": \"True Fit\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.truefit.com/\",\n      \"companyId\": \"true_fit\"\n    },\n    \"trueanthem\": {\n      \"name\": \"True Anthem\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.trueanthem.com/\",\n      \"companyId\": \"trueanthem\"\n    },\n    \"trueffect\": {\n      \"name\": \"TruEffect\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.trueffect.com/\",\n      \"companyId\": \"trueffect\"\n    },\n    \"truehits.net\": {\n      \"name\": \"Truehits.net\",\n      \"categoryId\": 6,\n      \"url\": \"http://truehits.net/\",\n      \"companyId\": \"truehits.net\"\n    },\n    \"trumba\": {\n      \"name\": \"Trumba\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.trumba.com\",\n      \"companyId\": \"trumba\"\n    },\n    \"truoptik\": {\n      \"name\": \"Tru Optik\",\n      \"categoryId\": 6,\n      \"url\": \"http://truoptik.com/\",\n      \"companyId\": null\n    },\n    \"trustarc\": {\n      \"name\": \"TrustArc\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.trustarc.com/\",\n      \"companyId\": \"trustarc\"\n    },\n    \"truste_consent\": {\n      \"name\": \"Truste Consent\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.trustarc.com/\",\n      \"companyId\": \"trustarc\"\n    },\n    \"truste_notice\": {\n      \"name\": \"TRUSTe Notice\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.truste.com/\",\n      \"companyId\": \"trustarc\"\n    },\n    \"truste_seal\": {\n      \"name\": \"TRUSTe Seal\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.truste.com/\",\n      \"companyId\": \"trustarc\"\n    },\n    \"trusted_shops\": {\n      \"name\": \"Trusted Shops\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.trustedshops.com/\",\n      \"companyId\": \"trusted_shops\"\n    },\n    \"trustev\": {\n      \"name\": \"Trustev\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.trustev.com/\",\n      \"companyId\": \"trustev\"\n    },\n    \"trustlogo\": {\n      \"name\": \"TrustLogo\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.comodo.com/\",\n      \"companyId\": \"comodo\"\n    },\n    \"trustpilot\": {\n      \"name\": \"Trustpilot\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.trustpilot.com\",\n      \"companyId\": \"trustpilot\"\n    },\n    \"trustwave.com\": {\n      \"name\": \"Trustwave\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.trustwave.com/home/\",\n      \"companyId\": null\n    },\n    \"tubecorporate\": {\n      \"name\": \"Tube Corporate\",\n      \"categoryId\": 3,\n      \"url\": \"https://tubecorporate.com/\",\n      \"companyId\": null\n    },\n    \"tubecup.org\": {\n      \"name\": \"tubecup.org\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"tubemogul\": {\n      \"name\": \"TubeMogul\",\n      \"categoryId\": 4,\n      \"url\": \"http://tubemogul.com/\",\n      \"companyId\": \"tubemogul\"\n    },\n    \"tumblr_analytics\": {\n      \"name\": \"Tumblr Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.verizon.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"tumblr_buttons\": {\n      \"name\": \"Tumblr Buttons\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.tumblr.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"tumblr_dashboard\": {\n      \"name\": \"Tumblr Dashboard\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.tumblr.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"tune_in\": {\n      \"name\": \"Tune In\",\n      \"categoryId\": 0,\n      \"url\": \"http://tunein.com/\",\n      \"companyId\": \"tunein\"\n    },\n    \"turbo\": {\n      \"name\": \"Turbo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.turboadv.com/\",\n      \"companyId\": \"turbo\"\n    },\n    \"turn_inc.\": {\n      \"name\": \"Turn Inc.\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.amobee.com/company/\",\n      \"companyId\": \"singtel\"\n    },\n    \"turner\": {\n      \"name\": \"Warner Media\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.warnermedia.com/\",\n      \"companyId\": \"turner\"\n    },\n    \"turnsocial\": {\n      \"name\": \"TurnSocial\",\n      \"categoryId\": 7,\n      \"url\": \"http://turnsocial.com/\",\n      \"companyId\": \"turnsocial\"\n    },\n    \"turnto\": {\n      \"name\": \"TurnTo\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.turntonetworks.com/\",\n      \"companyId\": \"turnto_networks\"\n    },\n    \"tvsquared.com\": {\n      \"name\": \"TVSquared\",\n      \"categoryId\": 4,\n      \"url\": \"http://tvsquared.com/\",\n      \"companyId\": \"tvsquared\"\n    },\n    \"tweetboard\": {\n      \"name\": \"Tweetboard\",\n      \"categoryId\": 7,\n      \"url\": \"http://tweetboard.com/alpha/\",\n      \"companyId\": \"tweetboard\"\n    },\n    \"tweetmeme\": {\n      \"name\": \"TweetMeme\",\n      \"categoryId\": 7,\n      \"url\": \"http://tweetmeme.com/\",\n      \"companyId\": \"tweetmeme\"\n    },\n    \"twenga\": {\n      \"name\": \"Twenga Solutions\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.twenga-solutions.com/\",\n      \"companyId\": null\n    },\n    \"twiago\": {\n      \"name\": \"Twiago\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.twiago.com/\",\n      \"companyId\": \"twiago\"\n    },\n    \"twine\": {\n      \"name\": \"Twine\",\n      \"categoryId\": 6,\n      \"url\": \"http://twinedigital.com/\",\n      \"companyId\": \"twine_digital\"\n    },\n    \"twitch.tv\": {\n      \"name\": \"Twitch\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.twitch.tv/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"twitch_cdn\": {\n      \"name\": \"Twitch CDN\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.twitch.tv/\",\n      \"companyId\": \"amazon_associates\"\n    },\n    \"twitter\": {\n      \"name\": \"X (formerly Twitter)\",\n      \"categoryId\": 7,\n      \"url\": \"https://twitter.com\",\n      \"companyId\": \"twitter\",\n      \"source\": \"AdGuard\"\n    },\n    \"twitter_ads\": {\n      \"name\": \"Twitter Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"http://twitter.com/widgets\",\n      \"companyId\": \"twitter\"\n    },\n    \"twitter_analytics\": {\n      \"name\": \"Twitter Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://twitter.com\",\n      \"companyId\": \"twitter\"\n    },\n    \"twitter_badge\": {\n      \"name\": \"Twitter Badge\",\n      \"categoryId\": 7,\n      \"url\": \"http://twitter.com/widgets\",\n      \"companyId\": \"twitter\"\n    },\n    \"twitter_button\": {\n      \"name\": \"Twitter Button\",\n      \"categoryId\": 7,\n      \"url\": \"http://twitter.com\",\n      \"companyId\": \"twitter\"\n    },\n    \"twitter_conversion_tracking\": {\n      \"name\": \"Twitter Conversion Tracking\",\n      \"categoryId\": 4,\n      \"url\": \"https://twitter.com/\",\n      \"companyId\": \"twitter\"\n    },\n    \"twitter_for_business\": {\n      \"name\": \"Twitter for Business\",\n      \"categoryId\": 4,\n      \"url\": \"https://business.twitter.com/\",\n      \"companyId\": \"twitter\"\n    },\n    \"twitter_syndication\": {\n      \"name\": \"Twitter Syndication\",\n      \"categoryId\": 7,\n      \"url\": \"https://twitter.com\",\n      \"companyId\": \"twitter\"\n    },\n    \"twittercounter\": {\n      \"name\": \"TwitterCounter\",\n      \"categoryId\": 6,\n      \"url\": \"http://twittercounter.com/\",\n      \"companyId\": \"twitter_counter\"\n    },\n    \"twyn\": {\n      \"name\": \"Twyn\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.twyn.com\",\n      \"companyId\": \"twyn\"\n    },\n    \"txxx.com\": {\n      \"name\": \"txxx.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://txxx.com\",\n      \"companyId\": null\n    },\n    \"tynt\": {\n      \"name\": \"33Across\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.tynt.com/\",\n      \"companyId\": \"33across\"\n    },\n    \"typeform\": {\n      \"name\": \"Typeform\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.typeform.com/\",\n      \"companyId\": null\n    },\n    \"typepad_stats\": {\n      \"name\": \"Typepad Stats\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.typepad.com/features/statistics.ht\",\n      \"companyId\": \"typepad\"\n    },\n    \"typography.com\": {\n      \"name\": \"Webfonts by Hoefler&Co\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.typography.com/\",\n      \"companyId\": null\n    },\n    \"tyroo\": {\n      \"name\": \"Tyroo\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.tyroo.com/\",\n      \"companyId\": \"tyroo\"\n    },\n    \"tzetze\": {\n      \"name\": \"TzeTze\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.tzetze.it/\",\n      \"companyId\": \"tzetze\"\n    },\n    \"ubersetzung-app.com\": {\n      \"name\": \"ubersetzung-app.com\",\n      \"categoryId\": 12,\n      \"url\": \"https://www.ubersetzung-app.com/\",\n      \"companyId\": null\n    },\n    \"ubuntu\": {\n      \"name\": \"Ubuntu\",\n      \"categoryId\": 8,\n      \"url\": \"https://ubuntu.com/\",\n      \"companyId\": \"canonical\",\n      \"source\": \"AdGuard\"\n    },\n    \"ucfunnel\": {\n      \"name\": \"ucfunnel\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ucfunnel.com/\",\n      \"companyId\": \"ucfunnel\"\n    },\n    \"ucoz\": {\n      \"name\": \"uCoz\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.ucoz.net/\",\n      \"companyId\": \"ucoz\"\n    },\n    \"uliza\": {\n      \"name\": \"Uliza\",\n      \"categoryId\": 4,\n      \"url\": \"http://uliza.jp/index.html\",\n      \"companyId\": \"uliza\"\n    },\n    \"umbel\": {\n      \"name\": \"Umbel\",\n      \"categoryId\": 6,\n      \"url\": \"http://umbel.com\",\n      \"companyId\": \"umbel\"\n    },\n    \"umebiggestern.club\": {\n      \"name\": \"umebiggestern.club\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"unanimis\": {\n      \"name\": \"Unanimis\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.unanimis.co.uk/\",\n      \"companyId\": \"switch_concepts\"\n    },\n    \"unbounce\": {\n      \"name\": \"Unbounce\",\n      \"categoryId\": 6,\n      \"url\": \"http://unbounce.com/\",\n      \"companyId\": \"unbounce\"\n    },\n    \"unbxd\": {\n      \"name\": \"UNBXD\",\n      \"categoryId\": 6,\n      \"url\": \"http://unbxd.com/\",\n      \"companyId\": \"unbxd\"\n    },\n    \"under-box.com\": {\n      \"name\": \"under-box.com\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"undercomputer.com\": {\n      \"name\": \"undercomputer.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"underdog_media\": {\n      \"name\": \"Underdog Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.underdogmedia.com\",\n      \"companyId\": \"underdog_media\"\n    },\n    \"undertone\": {\n      \"name\": \"Undertone\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.undertone.com/\",\n      \"companyId\": \"perion\"\n    },\n    \"unica\": {\n      \"name\": \"Unica\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.unica.com/\",\n      \"companyId\": \"ibm\"\n    },\n    \"unister\": {\n      \"name\": \"Unister\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.unister.de/\",\n      \"companyId\": \"unister\"\n    },\n    \"unite\": {\n      \"name\": \"Unite\",\n      \"categoryId\": 4,\n      \"url\": \"http://unite.me/#\",\n      \"companyId\": \"unite\"\n    },\n    \"united_digital_group\": {\n      \"name\": \"United Digital Group\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.udg.de/\",\n      \"companyId\": \"united_digital_group\"\n    },\n    \"united_internet_media_gmbh\": {\n      \"name\": \"United Internet Media GmbH\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.united-internet.de/\",\n      \"companyId\": \"united_internet\"\n    },\n    \"unity\": {\n      \"name\": \"Unity\",\n      \"categoryId\": 8,\n      \"url\": \"https://unity.com/\",\n      \"companyId\": \"unity\",\n      \"source\": \"AdGuard\"\n    },\n    \"unity_ads\": {\n      \"name\": \"Unity Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://unity.com/products/unity-ads\",\n      \"companyId\": \"unity\",\n      \"source\": \"AdGuard\"\n    },\n    \"univide\": {\n      \"name\": \"Univide\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.oracle.com/\",\n      \"companyId\": \"oracle\"\n    },\n    \"unpkg.com\": {\n      \"name\": \"unpkg\",\n      \"categoryId\": 9,\n      \"url\": \"https://unpkg.com/#/\",\n      \"companyId\": null\n    },\n    \"unruly_media\": {\n      \"name\": \"Unruly Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.unrulymedia.com/\",\n      \"companyId\": \"unruly\"\n    },\n    \"untriel_finger_printing\": {\n      \"name\": \"Untriel Finger Printing\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.untriel.nl/\",\n      \"companyId\": \"untriel\"\n    },\n    \"upland_clickability_beacon\": {\n      \"name\": \"Upland Clickability Beacon\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.clickability.com/\",\n      \"companyId\": \"upland_software\"\n    },\n    \"uppr.de\": {\n      \"name\": \"uppr GmbH\",\n      \"categoryId\": 4,\n      \"url\": \"https://uppr.de/\",\n      \"companyId\": null\n    },\n    \"upravel.com\": {\n      \"name\": \"upravel.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"upsellit\": {\n      \"name\": \"UpSellit\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.upsellit.com\",\n      \"companyId\": \"upsellit\"\n    },\n    \"upsight\": {\n      \"name\": \"Upsight\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.upsight.com/\",\n      \"companyId\": \"upsight\"\n    },\n    \"uptain\": {\n      \"name\": \"Uptain\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.uptain.de/en/regaining-lost-customers/\",\n      \"companyId\": \"uptain\"\n    },\n    \"uptolike.com\": {\n      \"name\": \"Uptolike\",\n      \"categoryId\": 7,\n      \"url\": \"https://www.uptolike.com/\",\n      \"companyId\": \"uptolike\"\n    },\n    \"uptrends\": {\n      \"name\": \"Uptrends\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.uptrends.com/\",\n      \"companyId\": \"uptrends\"\n    },\n    \"urban-media.com\": {\n      \"name\": \"Urban Media GmbH\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.urban-media.com/\",\n      \"companyId\": null\n    },\n    \"urban_airship\": {\n      \"name\": \"Urban Airship\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.urbanairship.com/\",\n      \"companyId\": \"urban_airship\"\n    },\n    \"usability_tools\": {\n      \"name\": \"Usability Tools\",\n      \"categoryId\": 6,\n      \"url\": \"http://usabilitytools.com/\",\n      \"companyId\": \"usability_tools\"\n    },\n    \"usabilla\": {\n      \"name\": \"Usabilla\",\n      \"categoryId\": 2,\n      \"url\": \"https://usabilla.com/\",\n      \"companyId\": \"usabilla\"\n    },\n    \"usemax\": {\n      \"name\": \"Usemax\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.usemax.de\",\n      \"companyId\": \"usemax\"\n    },\n    \"usemessages.com\": {\n      \"name\": \"usemessages.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"usercycle\": {\n      \"name\": \"USERcycle\",\n      \"categoryId\": 6,\n      \"url\": \"http://usercycle.com/\",\n      \"companyId\": \"usercycle\"\n    },\n    \"userdive\": {\n      \"name\": \"USERDIVE\",\n      \"categoryId\": 6,\n      \"url\": \"http://userdive.com/\",\n      \"companyId\": \"userdive\"\n    },\n    \"userecho\": {\n      \"name\": \"UserEcho\",\n      \"categoryId\": 2,\n      \"url\": \"http://userecho.com\",\n      \"companyId\": \"userecho\"\n    },\n    \"userlike.com\": {\n      \"name\": \"Userlike\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.userlike.com/\",\n      \"companyId\": \"userlike\"\n    },\n    \"userpulse\": {\n      \"name\": \"UserPulse\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.userpulse.com/\",\n      \"companyId\": \"userpulse\"\n    },\n    \"userreplay\": {\n      \"name\": \"UserReplay\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.userreplay.com/\",\n      \"companyId\": \"userreplay\"\n    },\n    \"userreport\": {\n      \"name\": \"UserReport\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.userreport.com/\",\n      \"companyId\": \"userreport\"\n    },\n    \"userrules\": {\n      \"name\": \"UserRules\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.userrules.com/\",\n      \"companyId\": \"userrules_software\"\n    },\n    \"usersnap\": {\n      \"name\": \"Usersnap\",\n      \"categoryId\": 2,\n      \"url\": \"http://usersnap.com/\",\n      \"companyId\": \"usersnap\"\n    },\n    \"uservoice\": {\n      \"name\": \"UserVoice\",\n      \"categoryId\": 2,\n      \"url\": \"http://uservoice.com/\",\n      \"companyId\": \"uservoice\"\n    },\n    \"userzoom.com\": {\n      \"name\": \"UserZoom\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.userzoom.com/\",\n      \"companyId\": \"userzoom\"\n    },\n    \"usocial\": {\n      \"name\": \"Usocial\",\n      \"categoryId\": 7,\n      \"url\": \"https://usocial.pro/en\",\n      \"companyId\": \"usocial\"\n    },\n    \"utarget\": {\n      \"name\": \"uTarget\",\n      \"categoryId\": 4,\n      \"url\": \"http://utarget.ru/\",\n      \"companyId\": \"utarget\"\n    },\n    \"uuidksinc.net\": {\n      \"name\": \"uuidksinc.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"v12_group\": {\n      \"name\": \"V12 Group\",\n      \"categoryId\": 6,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"vacaneedasap.com\": {\n      \"name\": \"vacaneedasap.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"valassis\": {\n      \"name\": \"Valassis\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.brand.net/\",\n      \"companyId\": \"valassis\"\n    },\n    \"validclick\": {\n      \"name\": \"ValidClick\",\n      \"categoryId\": 4,\n      \"url\": \"http://inuvo.com/\",\n      \"companyId\": \"inuvo\"\n    },\n    \"valiton\": {\n      \"name\": \"Valiton\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.valiton.com/\",\n      \"companyId\": \"hubert_burda_media\"\n    },\n    \"valueclick_media\": {\n      \"name\": \"ValueClick Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.conversantmedia.eu/\",\n      \"companyId\": \"conversant\"\n    },\n    \"valuecommerce\": {\n      \"name\": \"ValueCommerce\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.valuecommerce.ne.jp\",\n      \"companyId\": \"valuecommerce\"\n    },\n    \"valued_opinions\": {\n      \"name\": \"Valued Opinions\",\n      \"categoryId\": 4,\n      \"url\": \"http://valuedopinions.com\",\n      \"companyId\": \"valued_opinions\"\n    },\n    \"vanksen\": {\n      \"name\": \"Vanksen\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.buzzparadise.com/\",\n      \"companyId\": \"vanksen\"\n    },\n    \"varick_media_management\": {\n      \"name\": \"Varick Media Management\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.varickmm.com/\",\n      \"companyId\": \"varick_media_management\"\n    },\n    \"vcita\": {\n      \"name\": \"Vcita\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.vcita.com/\",\n      \"companyId\": \"vcita\"\n    },\n    \"vcommission\": {\n      \"name\": \"vCommission\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vcommission.com/\",\n      \"companyId\": \"vcommission\"\n    },\n    \"vdopia\": {\n      \"name\": \"Vdopia\",\n      \"categoryId\": 4,\n      \"url\": \"http://mobile.vdopia.com/\",\n      \"companyId\": \"vdopia\"\n    },\n    \"ve_interactive\": {\n      \"name\": \"Ve Interactive\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.veinteractive.com\",\n      \"companyId\": \"ve_interactive\"\n    },\n    \"vee24\": {\n      \"name\": \"VEE24\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.vee24.com/\",\n      \"companyId\": \"vee24\"\n    },\n    \"velocecdn.com\": {\n      \"name\": \"velocecdn.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"velti_mgage_visualize\": {\n      \"name\": \"Velti mGage Visualize\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.velti.com/\",\n      \"companyId\": \"velti\"\n    },\n    \"vendemore\": {\n      \"name\": \"Vendemore\",\n      \"categoryId\": 1,\n      \"url\": \"https://vendemore.com/\",\n      \"companyId\": \"ratos\"\n    },\n    \"venturead.com\": {\n      \"name\": \"venturead.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"venyoo\": {\n      \"name\": \"Venyoo\",\n      \"categoryId\": 2,\n      \"url\": \"http://venyoo.ru/\",\n      \"companyId\": \"venyoo\"\n    },\n    \"veoxa\": {\n      \"name\": \"Veoxa\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.veoxa.com/\",\n      \"companyId\": \"veoxa\"\n    },\n    \"vergic.com\": {\n      \"name\": \"Vergic\",\n      \"categoryId\": 1,\n      \"url\": \"https://www.vergic.com/\",\n      \"companyId\": null\n    },\n    \"vero\": {\n      \"name\": \"Vero\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.getvero.com/\",\n      \"companyId\": \"vero\"\n    },\n    \"vertical_acuity\": {\n      \"name\": \"Vertical Acuity\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.verticalacuity.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"vertical_leap\": {\n      \"name\": \"Vertical Leap\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vertical-leap.co.uk/\",\n      \"companyId\": \"vertical_leap\"\n    },\n    \"verticalresponse\": {\n      \"name\": \"VerticalResponse\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.verticalresponse.com\",\n      \"companyId\": \"verticalresponse\"\n    },\n    \"verticalscope\": {\n      \"name\": \"VerticalScope\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.verticalscope.com\",\n      \"companyId\": \"verticalscope\"\n    },\n    \"vertoz\": {\n      \"name\": \"Vertoz\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vertoz.com/\",\n      \"companyId\": \"vertoz\"\n    },\n    \"veruta\": {\n      \"name\": \"Veruta\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.veruta.com/\",\n      \"companyId\": \"veruta\"\n    },\n    \"verve_mobile\": {\n      \"name\": \"Verve Mobile\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vervemobile.com/\",\n      \"companyId\": \"verve_mobile\"\n    },\n    \"vg_wort\": {\n      \"name\": \"VG Wort\",\n      \"categoryId\": 6,\n      \"url\": \"https://tom.vgwort.de/portal/showHelp\",\n      \"companyId\": \"vg_wort\"\n    },\n    \"vi\": {\n      \"name\": \"Vi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vi.ru/\",\n      \"companyId\": \"vi\"\n    },\n    \"viacom_tag_container\": {\n      \"name\": \"Viacom Tag Container\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.viacom.com/\",\n      \"companyId\": \"viacom\"\n    },\n    \"viafoura\": {\n      \"name\": \"Viafoura\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.viafoura.com/\",\n      \"companyId\": \"viafoura\"\n    },\n    \"vibrant_ads\": {\n      \"name\": \"Vibrant Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vibrantmedia.com/\",\n      \"companyId\": \"vibrant_media\"\n    },\n    \"vicomi.com\": {\n      \"name\": \"Vicomi\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.vicomi.com/\",\n      \"companyId\": \"vicomi\"\n    },\n    \"vidazoo.com\": {\n      \"name\": \"Vidazoo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.vidazoo.com/\",\n      \"companyId\": null\n    },\n    \"video_desk\": {\n      \"name\": \"Video Desk\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.videodesk.com/\",\n      \"companyId\": \"video_desk\"\n    },\n    \"video_potok\": {\n      \"name\": \"Video Potok\",\n      \"categoryId\": 0,\n      \"url\": \"http://videopotok.pro/\",\n      \"companyId\": \"videopotok\"\n    },\n    \"videoadex.com\": {\n      \"name\": \"VideoAdX\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.videoadex.com/\",\n      \"companyId\": \"digiteka\"\n    },\n    \"videology\": {\n      \"name\": \"Videology\",\n      \"categoryId\": 4,\n      \"url\": \"https://videologygroup.com/\",\n      \"companyId\": \"singtel\"\n    },\n    \"videonow\": {\n      \"name\": \"VideoNow\",\n      \"categoryId\": 4,\n      \"url\": \"https://videonow.ru/\",\n      \"companyId\": \"videonow\"\n    },\n    \"videoplayerhub.com\": {\n      \"name\": \"videoplayerhub.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"videoplaza\": {\n      \"name\": \"Videoplaza\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.videoplaza.com/\",\n      \"companyId\": \"videoplaza\"\n    },\n    \"videostep\": {\n      \"name\": \"VideoStep\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.videostep.com/\",\n      \"companyId\": \"videostep\"\n    },\n    \"vidgyor\": {\n      \"name\": \"Vidgyor\",\n      \"categoryId\": 0,\n      \"url\": \"http://vidgyor.com/\",\n      \"companyId\": \"vidgyor\"\n    },\n    \"vidible\": {\n      \"name\": \"Vidible\",\n      \"categoryId\": 4,\n      \"url\": \"http://vidible.tv/\",\n      \"companyId\": \"verizon\"\n    },\n    \"vidora\": {\n      \"name\": \"Vidora\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.vidora.com/\",\n      \"companyId\": \"vidora\"\n    },\n    \"vietad\": {\n      \"name\": \"VietAd\",\n      \"categoryId\": 4,\n      \"url\": \"http://vietad.vn/\",\n      \"companyId\": \"vietad\"\n    },\n    \"viglink\": {\n      \"name\": \"VigLink\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.viglink.com\",\n      \"companyId\": \"viglink\"\n    },\n    \"vigo\": {\n      \"name\": \"Vigo\",\n      \"categoryId\": 6,\n      \"url\": \"https://vigo.one/\",\n      \"companyId\": \"vigo\"\n    },\n    \"vimeo\": {\n      \"name\": \"Vimeo\",\n      \"categoryId\": 0,\n      \"url\": \"http://vimeo.com/\",\n      \"companyId\": \"vimeo\"\n    },\n    \"vindico_group\": {\n      \"name\": \"Vindico Group\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vindicogroup.com/\",\n      \"companyId\": \"vindico_group\"\n    },\n    \"vinted\": {\n      \"name\": \"Vinted\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.vinted.com/\",\n      \"companyId\": null\n    },\n    \"viral_ad_network\": {\n      \"name\": \"Viral Ad Network\",\n      \"categoryId\": 4,\n      \"url\": \"http://viraladnetwork.joinvan.com/\",\n      \"companyId\": \"viral_ad_network\"\n    },\n    \"viral_loops\": {\n      \"name\": \"Viral Loops\",\n      \"categoryId\": 2,\n      \"url\": \"https://viral-loops.com/\",\n      \"companyId\": \"viral-loops\"\n    },\n    \"viralgains\": {\n      \"name\": \"ViralGains\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.viralgains.com/\",\n      \"companyId\": null\n    },\n    \"viralmint\": {\n      \"name\": \"ViralMint\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.viralmint.com\",\n      \"companyId\": \"viralmint\"\n    },\n    \"virgul\": {\n      \"name\": \"Virgul\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.virgul.com/\",\n      \"companyId\": \"virgul\"\n    },\n    \"virool_player\": {\n      \"name\": \"Virool Player\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.virool.com/\",\n      \"companyId\": \"virool\"\n    },\n    \"virtusize\": {\n      \"name\": \"Virtusize\",\n      \"categoryId\": 5,\n      \"url\": \"http://www.virtusize.com/\",\n      \"companyId\": \"virtusize\"\n    },\n    \"visible_measures\": {\n      \"name\": \"Visible Measures\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.visiblemeasures.com/\",\n      \"companyId\": \"visible_measures\"\n    },\n    \"vision_critical\": {\n      \"name\": \"Vision Critical\",\n      \"categoryId\": 6,\n      \"url\": \"http://visioncritical.com/\",\n      \"companyId\": \"vision_critical\"\n    },\n    \"visit_streamer\": {\n      \"name\": \"Visit Streamer\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.visitstreamer.com/\",\n      \"companyId\": \"visit_streamer\"\n    },\n    \"visitortrack\": {\n      \"name\": \"VisitorTrack\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.netfactor.com/\",\n      \"companyId\": \"netfactor\"\n    },\n    \"visitorville\": {\n      \"name\": \"VisitorVille\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.visitorville.com\",\n      \"companyId\": \"visitorville\"\n    },\n    \"visscore\": {\n      \"name\": \"VisScore\",\n      \"categoryId\": 4,\n      \"url\": \"http://withcubed.com/\",\n      \"companyId\": \"cubed_attribution\"\n    },\n    \"visual_iq\": {\n      \"name\": \"Visual IQ\",\n      \"categoryId\": 6,\n      \"url\": \"http://visualiq.com/\",\n      \"companyId\": \"visualiq\"\n    },\n    \"visual_revenue\": {\n      \"name\": \"Visual Revenue\",\n      \"categoryId\": 6,\n      \"url\": \"http://visualrevenue.com/\",\n      \"companyId\": \"outbrain\"\n    },\n    \"visual_website_optimizer\": {\n      \"name\": \"VWO\",\n      \"categoryId\": 6,\n      \"url\": \"https://vwo.com/\",\n      \"companyId\": \"wingify\"\n    },\n    \"visualdna\": {\n      \"name\": \"VisualDNA\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.visualdna.com/\",\n      \"companyId\": \"nielsen\"\n    },\n    \"visualstudio.com\": {\n      \"name\": \"Visualstudio.com\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.visualstudio.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"visualvisitor\": {\n      \"name\": \"VisualVisitor\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.visualvisitor.com/\",\n      \"companyId\": \"visualvisitor\"\n    },\n    \"vivalu\": {\n      \"name\": \"VIVALU\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.vivalu.com/\",\n      \"companyId\": \"vivalu\"\n    },\n    \"vivistats\": {\n      \"name\": \"ViviStats\",\n      \"categoryId\": 6,\n      \"url\": \"http://en.vivistats.com/\",\n      \"companyId\": \"vivistats\"\n    },\n    \"vizury\": {\n      \"name\": \"Vizury\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vizury.com/website/\",\n      \"companyId\": \"vizury\"\n    },\n    \"vizzit\": {\n      \"name\": \"Vizzit\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vizzit.se/h/en/\",\n      \"companyId\": \"vizzit\"\n    },\n    \"vk.com\": {\n      \"name\": \"Vk.com\",\n      \"categoryId\": 7,\n      \"url\": \"https://vk.com/\",\n      \"companyId\": \"vk\",\n      \"source\": \"AdGuard\"\n    },\n    \"vkontakte\": {\n      \"name\": \"VKontakte\",\n      \"categoryId\": 7,\n      \"url\": \"https://vk.com/\",\n      \"companyId\": \"vk\",\n      \"source\": \"AdGuard\"\n    },\n    \"vkontakte_widgets\": {\n      \"name\": \"VKontakte Widgets\",\n      \"categoryId\": 7,\n      \"url\": \"https://dev.vk.com/\",\n      \"companyId\": \"vk\",\n      \"source\": \"AdGuard\"\n    },\n    \"vntsm.com\": {\n      \"name\": \"Venatus Media\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.venatusmedia.com/\",\n      \"companyId\": \"venatus\"\n    },\n    \"vodafone.de\": {\n      \"name\": \"vodafone.de\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"voicefive\": {\n      \"name\": \"VoiceFive\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.voicefive.com\",\n      \"companyId\": \"comscore\"\n    },\n    \"volusion_chat\": {\n      \"name\": \"Volusion Chat\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.volusion.com/\",\n      \"companyId\": \"volusion\"\n    },\n    \"voluum\": {\n      \"name\": \"Voluum\",\n      \"categoryId\": 4,\n      \"url\": \"https://voluum.com/\",\n      \"companyId\": \"codewise\"\n    },\n    \"vooxe.com\": {\n      \"name\": \"vooxe.com\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.vooxe.com/\",\n      \"companyId\": null\n    },\n    \"vorwerk.de\": {\n      \"name\": \"vorwerk.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://corporate.vorwerk.de/home/\",\n      \"companyId\": null\n    },\n    \"vox\": {\n      \"name\": \"Vox\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.voxmedia.com/\",\n      \"companyId\": \"vox\"\n    },\n    \"voxus\": {\n      \"name\": \"Voxus\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.voxus.tv/\",\n      \"companyId\": \"voxus\"\n    },\n    \"vpon\": {\n      \"name\": \"VPON\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.vpon.com/en/\",\n      \"companyId\": \"vpon\"\n    },\n    \"vpscash\": {\n      \"name\": \"VPSCash\",\n      \"categoryId\": 4,\n      \"url\": \"http://vpscash.nl/home\",\n      \"companyId\": \"vps_cash\"\n    },\n    \"vs\": {\n      \"name\": \"Visual Studio\",\n      \"categoryId\": 8,\n      \"url\": \"https://visualstudio.microsoft.com\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"vscode\": {\n      \"name\": \"Visual Studio Code\",\n      \"categoryId\": 8,\n      \"url\": \"https://code.visualstudio.com/\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"vtracy.de\": {\n      \"name\": \"vtracy.de\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"vungle\": {\n      \"name\": \"Vungle\",\n      \"categoryId\": 4,\n      \"url\": \"https://vungle.com/\",\n      \"companyId\": \"blackstone\",\n      \"source\": \"AdGuard\"\n    },\n    \"vuukle\": {\n      \"name\": \"Vuukle\",\n      \"categoryId\": 6,\n      \"url\": \"http://vuukle.com/\",\n      \"companyId\": \"vuukle\"\n    },\n    \"vzaar\": {\n      \"name\": \"Vzaar\",\n      \"categoryId\": 0,\n      \"url\": \"http://vzaar.com/\",\n      \"companyId\": \"vzaar\"\n    },\n    \"w3counter\": {\n      \"name\": \"W3Counter\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.w3counter.com/\",\n      \"companyId\": \"awio_web_services\"\n    },\n    \"w3roi\": {\n      \"name\": \"w3roi\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.w3roi.com/\",\n      \"companyId\": \"w3roi\"\n    },\n    \"wahoha\": {\n      \"name\": \"Wahoha\",\n      \"categoryId\": 2,\n      \"url\": \"http://wahoha.com/\",\n      \"companyId\": \"wahoha\"\n    },\n    \"walkme.com\": {\n      \"name\": \"WalkMe\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.walkme.com/\",\n      \"companyId\": \"walkme\"\n    },\n    \"wall_street_on_demand\": {\n      \"name\": \"Wall Street on Demand\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.wallst.com\",\n      \"companyId\": \"markit_on_demand\"\n    },\n    \"walmart\": {\n      \"name\": \"Walmart\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"wamcash\": {\n      \"name\": \"Wamcash\",\n      \"categoryId\": 3,\n      \"url\": \"http://wamcash.com/\",\n      \"companyId\": \"wamcash\"\n    },\n    \"wanelo\": {\n      \"name\": \"Wanelo\",\n      \"categoryId\": 2,\n      \"url\": \"https://wanelo.com/\",\n      \"companyId\": \"wanelo\"\n    },\n    \"warp.ly\": {\n      \"name\": \"Warp.ly\",\n      \"categoryId\": 6,\n      \"url\": \"https://warp.ly/\",\n      \"companyId\": \"warp.ly\"\n    },\n    \"way2traffic\": {\n      \"name\": \"Way2traffic\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.way2traffic.com/\",\n      \"companyId\": \"way2traffic\"\n    },\n    \"wayfair_com\": {\n      \"name\": \"Wayfair\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.wayfair.com/\",\n      \"companyId\": null\n    },\n    \"wdr.de\": {\n      \"name\": \"wdr.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://www1.wdr.de/index.html\",\n      \"companyId\": null\n    },\n    \"web-stat\": {\n      \"name\": \"Web-Stat\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.web-stat.net/\",\n      \"companyId\": \"web-stat\"\n    },\n    \"web.de\": {\n      \"name\": \"web.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://web.de/\",\n      \"companyId\": null\n    },\n    \"web.stat\": {\n      \"name\": \"Web.STAT\",\n      \"categoryId\": 6,\n      \"url\": \"http://webstat.net/\",\n      \"companyId\": \"web.stat\"\n    },\n    \"web_service_award\": {\n      \"name\": \"Web Service Award\",\n      \"categoryId\": 6,\n      \"url\": \"http://webserviceaward.com/english/\",\n      \"companyId\": \"web_service_award\"\n    },\n    \"web_traxs\": {\n      \"name\": \"Web Traxs\",\n      \"categoryId\": 6,\n      \"url\": \"http://websolutions.thomasnet.com/web-traxs-analytics.php\",\n      \"companyId\": \"thomasnet_websolutions\"\n    },\n    \"web_wipe_analytics\": {\n      \"name\": \"Web Wipe Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://tensquare.de\",\n      \"companyId\": \"tensquare\"\n    },\n    \"webads\": {\n      \"name\": \"WebAds\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.webads.co.uk/\",\n      \"companyId\": \"webads\"\n    },\n    \"webantenna\": {\n      \"name\": \"WebAntenna\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.bebit.co.jp/webantenna/\",\n      \"companyId\": \"webantenna\"\n    },\n    \"webclicks24_com\": {\n      \"name\": \"webclicks24.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"webclose.net\": {\n      \"name\": \"webclose.net\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"webcollage\": {\n      \"name\": \"Webcollage\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.webcollage.com/\",\n      \"companyId\": \"webcollage\"\n    },\n    \"webedia\": {\n      \"name\": \"Webedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://fr.webedia-group.com/\",\n      \"companyId\": \"fimalac_group\"\n    },\n    \"webeffective\": {\n      \"name\": \"WebEffective\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.keynote.com/\",\n      \"companyId\": \"keynote_systems\"\n    },\n    \"webengage\": {\n      \"name\": \"WebEngage\",\n      \"categoryId\": 2,\n      \"url\": \"http://webengage.com/\",\n      \"companyId\": \"webengage\"\n    },\n    \"webgains\": {\n      \"name\": \"Webgains\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"webgozar\": {\n      \"name\": \"WebGozar\",\n      \"categoryId\": 6,\n      \"url\": \"http://webgozar.com/\",\n      \"companyId\": \"webgozar\"\n    },\n    \"webhelpje\": {\n      \"name\": \"Webhelpje\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.webhelpje.nl/\",\n      \"companyId\": \"webhelpje\"\n    },\n    \"webleads_tracker\": {\n      \"name\": \"Webleads Tracker\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webleads-tracker.fr/\",\n      \"companyId\": \"webleads_tracker\"\n    },\n    \"webmecanik\": {\n      \"name\": \"Webmecanik\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webmecanik.com/en/\",\n      \"companyId\": \"webmecanik\"\n    },\n    \"weborama\": {\n      \"name\": \"Weborama\",\n      \"categoryId\": 4,\n      \"url\": \"https://weborama.com/\",\n      \"companyId\": \"weborama\"\n    },\n    \"webprospector\": {\n      \"name\": \"WebProspector\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webprospector.de/\",\n      \"companyId\": \"webprospector\"\n    },\n    \"webstat\": {\n      \"name\": \"WebSTAT\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webstat.com/\",\n      \"companyId\": \"webstat\"\n    },\n    \"webstat.se\": {\n      \"name\": \"Webstat.se\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webstat.se/\",\n      \"companyId\": \"webstat.se\"\n    },\n    \"webtrack\": {\n      \"name\": \"webtrack\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webtrack.biz/\",\n      \"companyId\": \"webtrack\"\n    },\n    \"webtraffic\": {\n      \"name\": \"Webtraffic\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webtraffic.se/\",\n      \"companyId\": \"schibsted_asa\"\n    },\n    \"webtrekk\": {\n      \"name\": \"Webtrekk\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webtrekk.com/\",\n      \"companyId\": \"webtrekk\"\n    },\n    \"webtrekk_cc\": {\n      \"name\": \"Webtrek Control Cookie\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.webtrekk.com/en/home/\",\n      \"companyId\": \"webtrekk\"\n    },\n    \"webtrends\": {\n      \"name\": \"Webtrends\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.webtrends.com/\",\n      \"companyId\": \"webtrends\"\n    },\n    \"webtrends_ads\": {\n      \"name\": \"Webtrends Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.webtrends.com\",\n      \"companyId\": \"webtrends\"\n    },\n    \"webvisor\": {\n      \"name\": \"WebVisor\",\n      \"categoryId\": 6,\n      \"url\": \"http://webvisor.ru\",\n      \"companyId\": \"yandex\"\n    },\n    \"wedcs\": {\n      \"name\": \"WEDCS\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.microsoft.com/\",\n      \"companyId\": \"microsoft\"\n    },\n    \"weebly_ads\": {\n      \"name\": \"Weebly Ads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.weebly.com\",\n      \"companyId\": \"weebly\"\n    },\n    \"weibo_widget\": {\n      \"name\": \"Weibo Widget\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.sina.com/\",\n      \"companyId\": \"sina\"\n    },\n    \"westlotto_com\": {\n      \"name\": \"westlotto.com\",\n      \"categoryId\": 8,\n      \"url\": \"http://westlotto.com/\",\n      \"companyId\": null\n    },\n    \"wetter_com\": {\n      \"name\": \"Wetter.com\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.wetter.com/\",\n      \"companyId\": null\n    },\n    \"whatbroadcast\": {\n      \"name\": \"Whatbroadcast\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.whatsbroadcast.com/\",\n      \"companyId\": \"whatsbroadcast\"\n    },\n    \"whatsapp\": {\n      \"name\": \"WhatsApp\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.whatsapp.com/\",\n      \"companyId\": \"meta\",\n      \"source\": \"AdGuard\"\n    },\n    \"whisper\": {\n      \"name\": \"Whisper\",\n      \"categoryId\": 7,\n      \"url\": \"https://whisper.sh/\",\n      \"companyId\": \"medialab\",\n      \"source\": \"AdGuard\"\n    },\n    \"whos.amung.us\": {\n      \"name\": \"Whos.amung.us\",\n      \"categoryId\": 6,\n      \"url\": \"http://whos.amung.us/\",\n      \"companyId\": \"whos.amung.us\"\n    },\n    \"whoson\": {\n      \"name\": \"WhosOn\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.whoson.com/\",\n      \"companyId\": \"whoson\"\n    },\n    \"wibbitz\": {\n      \"name\": \"Wibbitz\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.wibbitz.com/\",\n      \"companyId\": \"wibbitz\"\n    },\n    \"wibiya_toolbar\": {\n      \"name\": \"Wibiya Toolbar\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.wibiya.com/\",\n      \"companyId\": \"wibiya\"\n    },\n    \"widdit\": {\n      \"name\": \"Widdit\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.predictad.com/\",\n      \"companyId\": \"widdit\"\n    },\n    \"widerplanet\": {\n      \"name\": \"WiderPlanet\",\n      \"categoryId\": 4,\n      \"url\": \"http://widerplanet.com/\",\n      \"companyId\": \"wider_planet\"\n    },\n    \"widespace\": {\n      \"name\": \"Widespace\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.widespace.com/\",\n      \"companyId\": \"widespace\"\n    },\n    \"widgetbox\": {\n      \"name\": \"WidgetBox\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.widgetbox.com/\",\n      \"companyId\": \"widgetbox\"\n    },\n    \"wiget_media\": {\n      \"name\": \"Wiget Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://wigetmedia.com\",\n      \"companyId\": \"wiget_media\"\n    },\n    \"wigzo\": {\n      \"name\": \"Wigzo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.wigzo.com/\",\n      \"companyId\": \"wigzo\"\n    },\n    \"wikia-services.com\": {\n      \"name\": \"Wikia Services\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.wikia.com/fandom\",\n      \"companyId\": \"wikia\"\n    },\n    \"wikia_beacon\": {\n      \"name\": \"Wikia Beacon\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.wikia.com/\",\n      \"companyId\": \"wikia\"\n    },\n    \"wikia_cdn\": {\n      \"name\": \"Wikia CDN\",\n      \"categoryId\": 9,\n      \"url\": \"http://www.wikia.com/fandom\",\n      \"companyId\": \"wikia\"\n    },\n    \"wikimedia.org\": {\n      \"name\": \"WikiMedia\",\n      \"categoryId\": 9,\n      \"url\": \"https://wikimediafoundation.org/\",\n      \"companyId\": \"wikimedia_foundation\"\n    },\n    \"winaffiliates\": {\n      \"name\": \"Winaffiliates\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.winaffiliates.com/\",\n      \"companyId\": \"winaffiliates\"\n    },\n    \"windows_maps\": {\n      \"name\": \"Windows Maps\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.microsoft.com/store/apps/9wzdncrdtbvb\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"windows_notifications\": {\n      \"name\": \"The Windows Push Notification Services\",\n      \"categoryId\": 8,\n      \"url\": \"https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/windows-push-notification-services--wns--overview\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"windows_time\": {\n      \"name\": \"Windows Time Service\",\n      \"categoryId\": 8,\n      \"url\": \"https://learn.microsoft.com/en-us/windows-server/networking/windows-time-service/how-the-windows-time-service-works\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"windowsupdate\": {\n      \"name\": \"Windows Update\",\n      \"categoryId\": 9,\n      \"url\": \"https://support.microsoft.com/en-us/windows/windows-update-faq-8a903416-6f45-0718-f5c7-375e92dddeb2\",\n      \"companyId\": \"microsoft\",\n      \"source\": \"AdGuard\"\n    },\n    \"wipmania\": {\n      \"name\": \"WIPmania\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.wipmania.com/\",\n      \"companyId\": \"wipmania\"\n    },\n    \"wiqhit\": {\n      \"name\": \"WiQhit\",\n      \"categoryId\": 6,\n      \"url\": \"https://wiqhit.com/nl/\",\n      \"companyId\": \"wiqhit\"\n    },\n    \"wirecard\": {\n      \"name\": \"Wirecard\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.wirecard.com/\",\n      \"companyId\": null\n    },\n    \"wiredminds\": {\n      \"name\": \"WiredMinds\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.wiredminds.de/\",\n      \"companyId\": \"wiredminds\"\n    },\n    \"wirtualna_polska\": {\n      \"name\": \"Wirtualna Polska\",\n      \"categoryId\": 4,\n      \"url\": \"http://reklama.wp.pl/\",\n      \"companyId\": \"wirtualna_polska\"\n    },\n    \"wisepops\": {\n      \"name\": \"WisePops\",\n      \"categoryId\": 4,\n      \"url\": \"http://wisepops.com/\",\n      \"companyId\": \"wisepops\"\n    },\n    \"wishpond\": {\n      \"name\": \"Wishpond\",\n      \"categoryId\": 2,\n      \"url\": \"http://wishpond.com\",\n      \"companyId\": \"wishpond\"\n    },\n    \"wistia\": {\n      \"name\": \"Wistia\",\n      \"categoryId\": 6,\n      \"url\": \"http://wistia.com/\",\n      \"companyId\": \"wistia\"\n    },\n    \"wix.com\": {\n      \"name\": \"Wix\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.wix.com/\",\n      \"companyId\": \"wix\"\n    },\n    \"wixab\": {\n      \"name\": \"Wixab\",\n      \"categoryId\": 6,\n      \"url\": \"http://wixab.com/en/\",\n      \"companyId\": \"wixab\"\n    },\n    \"wixmp\": {\n      \"name\": \"Wix Media Platform\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.wixmp.com/\",\n      \"companyId\": \"wix\"\n    },\n    \"wnzmauurgol.com\": {\n      \"name\": \"wnzmauurgol.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"wonderpush\": {\n      \"name\": \"WonderPush\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.wonderpush.com/\",\n      \"companyId\": \"wonderpush\"\n    },\n    \"woopic.com\": {\n      \"name\": \"woopic.com\",\n      \"categoryId\": 8,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"woopra\": {\n      \"name\": \"Woopra\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.woopra.com/\",\n      \"companyId\": \"woopra\"\n    },\n    \"wordpress_ads\": {\n      \"name\": \"Wordpress Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://wordpress.com/\",\n      \"companyId\": \"automattic\"\n    },\n    \"wordpress_stats\": {\n      \"name\": \"WordPress Stats\",\n      \"categoryId\": 6,\n      \"url\": \"http://wordpress.org/extend/plugins/stats/\",\n      \"companyId\": \"automattic\"\n    },\n    \"wordstream\": {\n      \"name\": \"WordStream\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.wordstream.com/\",\n      \"companyId\": \"wordstream\"\n    },\n    \"worldnaturenet_xyz\": {\n      \"name\": \"worldnaturenet.xyz\",\n      \"categoryId\": 12,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"wp.pl\": {\n      \"name\": \"Wirtualna Polska \",\n      \"categoryId\": 4,\n      \"url\": \"https://www.wp.pl/\",\n      \"companyId\": \"wp\"\n    },\n    \"wp_engine\": {\n      \"name\": \"WP Engine\",\n      \"categoryId\": 5,\n      \"url\": \"https://wpengine.com/\",\n      \"companyId\": \"wp_engine\"\n    },\n    \"writeup_clickanalyzer\": {\n      \"name\": \"WriteUp ClickAnalyzer\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.writeup.co.jp/\",\n      \"companyId\": \"writeup\"\n    },\n    \"wurfl\": {\n      \"name\": \"WURFL\",\n      \"categoryId\": 6,\n      \"url\": \"https://web.wurfl.io/\",\n      \"companyId\": \"scientiamobile\"\n    },\n    \"wwwpromoter\": {\n      \"name\": \"WWWPromoter\",\n      \"categoryId\": 4,\n      \"url\": \"http://wwwpromoter.com/\",\n      \"companyId\": \"wwwpromoter\"\n    },\n    \"wykop\": {\n      \"name\": \"Wykop\",\n      \"categoryId\": 7,\n      \"url\": \"http://www.wykop.pl\",\n      \"companyId\": \"wykop\"\n    },\n    \"wysistat.com\": {\n      \"name\": \"WysiStat\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.wysistat.net/\",\n      \"companyId\": \"wysistat\"\n    },\n    \"wywy.com\": {\n      \"name\": \"wywy\",\n      \"categoryId\": 4,\n      \"url\": \"http://wywy.com/\",\n      \"companyId\": \"tvsquared\"\n    },\n    \"x-lift\": {\n      \"name\": \"X-lift\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.x-lift.jp/\",\n      \"companyId\": \"x-lift\"\n    },\n    \"xapads\": {\n      \"name\": \"Xapads\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.xapads.com/\",\n      \"companyId\": \"xapads\"\n    },\n    \"xen-media.com\": {\n      \"name\": \"Xen Media\",\n      \"categoryId\": 11,\n      \"url\": \"https://www.xenmedia.net/\",\n      \"companyId\": \"xenmedia\",\n      \"source\": \"AdGuard\"\n    },\n    \"xfreeservice.com\": {\n      \"name\": \"xfreeservice.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"xhamster\": {\n      \"name\": \"xHamster\",\n      \"categoryId\": 3,\n      \"url\": \"https://xhamster.com/\",\n      \"companyId\": \"xhamster\",\n      \"source\": \"AdGuard\"\n    },\n    \"xiaomi\": {\n      \"name\": \"Xiaomi\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.mi.com/\",\n      \"companyId\": \"xiaomi\",\n      \"source\": \"AdGuard\"\n    },\n    \"xing\": {\n      \"name\": \"Xing\",\n      \"categoryId\": 6,\n      \"url\": \"http://www.xing.com/\",\n      \"companyId\": \"xing\"\n    },\n    \"xmediaclicks\": {\n      \"name\": \"XmediaClicks\",\n      \"categoryId\": 3,\n      \"url\": \"http://exoclick.com/\",\n      \"companyId\": \"exoclick\"\n    },\n    \"xnxx_cdn\": {\n      \"name\": \"XNXX\",\n      \"categoryId\": 9,\n      \"url\": \"https://www.xnxx.com\",\n      \"companyId\": \"xnxx\",\n      \"source\": \"AdGuard\"\n    },\n    \"xplosion\": {\n      \"name\": \"xplosion\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.xplosion.de/\",\n      \"companyId\": \"xplosion_interactive\"\n    },\n    \"xtend\": {\n      \"name\": \"XTEND\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.xtendmedia.com/\",\n      \"companyId\": \"matomy_media\"\n    },\n    \"xvideos_com\": {\n      \"name\": \"Xvideos\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.xvideos.com\",\n      \"companyId\": \"xvideos\",\n      \"source\": \"AdGuard\"\n    },\n    \"xxxlshop.de\": {\n      \"name\": \"XXXLutz\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.xxxlutz.de/\",\n      \"companyId\": \"xxxlutz\",\n      \"source\": \"AdGuard\"\n    },\n    \"xxxlutz\": {\n      \"name\": \"XXXLutz\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.xxxlutz.de/\",\n      \"companyId\": \"xxxlutz\"\n    },\n    \"yabbi\": {\n      \"name\": \"Yabbi\",\n      \"categoryId\": 4,\n      \"url\": \"https://yabbi.me/\",\n      \"companyId\": \"yabbi\",\n      \"source\": \"AdGuard\"\n    },\n    \"yabuka\": {\n      \"name\": \"Yabuka\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yabuka.com/\",\n      \"companyId\": \"yabuka\"\n    },\n    \"yahoo\": {\n      \"name\": \"Yahoo!\",\n      \"categoryId\": 6,\n      \"url\": \"https://yahoo.com/\",\n      \"companyId\": \"apollo_global_management\",\n      \"source\": \"AdGuard\"\n    },\n    \"yahoo_ad_exchange\": {\n      \"name\": \"Yahoo! Ad Exchange\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.verizonmedia.com/advertising\",\n      \"companyId\": \"verizon\"\n    },\n    \"yahoo_ad_manager\": {\n      \"name\": \"Yahoo! Ad Manager Plus\",\n      \"categoryId\": 4,\n      \"url\": \"https://developer.yahoo.com/analytics/\",\n      \"companyId\": \"verizon\"\n    },\n    \"yahoo_advertising\": {\n      \"name\": \"Yahoo! Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.advertising.yahooinc.com/\",\n      \"companyId\": \"apollo_global_management\",\n      \"source\": \"AdGuard\"\n    },\n    \"yahoo_analytics\": {\n      \"name\": \"Yahoo! Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"http://web.analytics.yahoo.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"yahoo_commerce_central\": {\n      \"name\": \"Yahoo! Commerce Central\",\n      \"categoryId\": 4,\n      \"url\": \"http://lexity.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"yahoo_dot_tag\": {\n      \"name\": \"Yahoo! DOT tag\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.verizon.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"yahoo_japan_retargeting\": {\n      \"name\": \"Yahoo! Japan Retargeting\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yahoo.com/\",\n      \"companyId\": \"yahoo_japan\"\n    },\n    \"yahoo_overture\": {\n      \"name\": \"Yahoo! Overture\",\n      \"categoryId\": 4,\n      \"url\": \"http://searchmarketing.yahoo.com\",\n      \"companyId\": \"verizon\"\n    },\n    \"yahoo_search\": {\n      \"name\": \"Yahoo! Search\",\n      \"categoryId\": 4,\n      \"url\": \"https://search.yahooinc.com/\",\n      \"companyId\": \"apollo_global_management\",\n      \"source\": \"AdGuard\"\n    },\n    \"yahoo_small_business\": {\n      \"name\": \"Yahoo! Small Business\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pixazza.com/\",\n      \"companyId\": \"verizon\"\n    },\n    \"yandex\": {\n      \"name\": \"Yandex\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.yandex.com/\",\n      \"companyId\": \"yandex\"\n    },\n    \"yandex.api\": {\n      \"name\": \"Yandex.API\",\n      \"categoryId\": 2,\n      \"url\": \"http://api.yandex.ru/\",\n      \"companyId\": \"yandex\"\n    },\n    \"yandex_adexchange\": {\n      \"name\": \"Yandex AdExchange\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.yandex.com/\",\n      \"companyId\": \"yandex\"\n    },\n    \"yandex_advisor\": {\n      \"name\": \"Yandex.Advisor\",\n      \"categoryId\": 12,\n      \"url\": \"https://sovetnik.yandex.ru/\",\n      \"companyId\": \"yandex\"\n    },\n    \"yandex_appmetrica\": {\n      \"name\": \"Yandex AppMetrica\",\n      \"categoryId\": 101,\n      \"url\": \"https://appmetrica.yandex.com/\",\n      \"companyId\": \"yandex\",\n      \"source\": \"AdGuard\"\n    },\n    \"yandex_direct\": {\n      \"name\": \"Yandex.Direct\",\n      \"categoryId\": 6,\n      \"url\": \"https://direct.yandex.com/\",\n      \"companyId\": \"yandex\"\n    },\n    \"yandex_metrika\": {\n      \"name\": \"Yandex Metrika\",\n      \"categoryId\": 6,\n      \"url\": \"https://metrica.yandex.com/\",\n      \"companyId\": \"yandex\"\n    },\n    \"yandex_passport\": {\n      \"name\": \"Yandex Passport\",\n      \"categoryId\": 2,\n      \"url\": \"https://www.yandex.com/\",\n      \"companyId\": \"yandex\"\n    },\n    \"yapfiles.ru\": {\n      \"name\": \"yapfiles.ru\",\n      \"categoryId\": 8,\n      \"url\": \"https://www.yapfiles.ru/\",\n      \"companyId\": null\n    },\n    \"yashi\": {\n      \"name\": \"Yashi\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yashi.com/\",\n      \"companyId\": \"mass2\"\n    },\n    \"ybrant_media\": {\n      \"name\": \"Ybrant Media\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.addynamix.com/index.html\",\n      \"companyId\": \"ybrant_media\"\n    },\n    \"ycontent\": {\n      \"name\": \"Ycontent\",\n      \"categoryId\": 0,\n      \"url\": \"http://ycontent.com.br/\",\n      \"companyId\": \"ycontent\"\n    },\n    \"yektanet\": {\n      \"name\": \"Yektanet\",\n      \"categoryId\": 4,\n      \"url\": \"https://yektanet.com/\",\n      \"companyId\": \"yektanet\"\n    },\n    \"yengo\": {\n      \"name\": \"Yengo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yengo.com/\",\n      \"companyId\": \"yengo\"\n    },\n    \"yesmail\": {\n      \"name\": \"Yesmail\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yesmail.com/\",\n      \"companyId\": \"yes_mail\"\n    },\n    \"yesup_advertising\": {\n      \"name\": \"YesUp Advertising\",\n      \"categoryId\": 4,\n      \"url\": \"http://yesup.net/\",\n      \"companyId\": \"yesup\"\n    },\n    \"yesware\": {\n      \"name\": \"Yesware\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.yesware.com/\",\n      \"companyId\": \"yesware\"\n    },\n    \"yieldbot\": {\n      \"name\": \"Yieldbot\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.yieldbot.com/\",\n      \"companyId\": \"yieldbot\"\n    },\n    \"yieldify\": {\n      \"name\": \"Yieldify\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yieldify.com/\",\n      \"companyId\": \"yieldify\"\n    },\n    \"yieldlab\": {\n      \"name\": \"Yieldlab\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yieldlab.de/\",\n      \"companyId\": \"prosieben_sat1\"\n    },\n    \"yieldlove\": {\n      \"name\": \"Yieldlove\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.yieldlove.com/\",\n      \"companyId\": \"yieldlove\"\n    },\n    \"yieldmo\": {\n      \"name\": \"Yieldmo\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.yieldmo.com/\",\n      \"companyId\": \"yieldmo\"\n    },\n    \"yieldr\": {\n      \"name\": \"Yieldr Ads\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.yieldr.com/\",\n      \"companyId\": \"yieldr\"\n    },\n    \"yieldr_air\": {\n      \"name\": \"Yieldr Air\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.yieldr.com/\",\n      \"companyId\": \"yieldr\"\n    },\n    \"yieldsquare\": {\n      \"name\": \"YieldSquare\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yieldsquare.com/\",\n      \"companyId\": \"yieldsquare\"\n    },\n    \"yle\": {\n      \"name\": \"YLE\",\n      \"categoryId\": 6,\n      \"url\": \"http://yle.fi/\",\n      \"companyId\": \"yle\"\n    },\n    \"yllixmedia\": {\n      \"name\": \"YllixMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://yllix.com/\",\n      \"companyId\": \"yllixmedia\"\n    },\n    \"ymetrica1.com\": {\n      \"name\": \"ymetrica1.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"ymzrrizntbhde.com\": {\n      \"name\": \"ymzrrizntbhde.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"yo_button\": {\n      \"name\": \"Yo Button\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.justyo.co/\",\n      \"companyId\": \"yo\"\n    },\n    \"yodle\": {\n      \"name\": \"Yodle\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yodle.com/\",\n      \"companyId\": \"yodle\"\n    },\n    \"yola_analytics\": {\n      \"name\": \"Yola Analytics\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.yola.com/\",\n      \"companyId\": \"yola\"\n    },\n    \"yomedia\": {\n      \"name\": \"Yomedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.pinetech.vn/\",\n      \"companyId\": \"yomedia\"\n    },\n    \"yoochoose.net\": {\n      \"name\": \"Ibexa Personalizaton Software\",\n      \"categoryId\": 4,\n      \"url\": \"https://yoochoose.net/\",\n      \"companyId\": \"ibexa\",\n      \"source\": \"AdGuard\"\n    },\n    \"yotpo\": {\n      \"name\": \"Yotpo\",\n      \"categoryId\": 1,\n      \"url\": \"https://www.yotpo.com/\",\n      \"companyId\": \"yotpo\"\n    },\n    \"yottaa\": {\n      \"name\": \"Yottaa\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.yottaa.com/\",\n      \"companyId\": \"yottaa\"\n    },\n    \"yottly\": {\n      \"name\": \"Yottly\",\n      \"categoryId\": 4,\n      \"url\": \"https://yottly.com/\",\n      \"companyId\": \"yottly\"\n    },\n    \"youcanbookme\": {\n      \"name\": \"YouCanBookMe\",\n      \"categoryId\": 2,\n      \"url\": \"https://youcanbook.me/\",\n      \"companyId\": \"youcanbookme\"\n    },\n    \"youku\": {\n      \"name\": \"Youku\",\n      \"categoryId\": 0,\n      \"url\": \"http://www.youku.com/\",\n      \"companyId\": \"youku\"\n    },\n    \"youporn\": {\n      \"name\": \"YouPorn\",\n      \"categoryId\": 3,\n      \"url\": \"https://www.youporn.com/\",\n      \"companyId\": \"youporn\",\n      \"source\": \"AdGuard\"\n    },\n    \"youtube\": {\n      \"name\": \"YouTube\",\n      \"categoryId\": 0,\n      \"url\": \"https://www.youtube.com/\",\n      \"companyId\": \"google\"\n    },\n    \"youtube_subscription\": {\n      \"name\": \"YouTube Subscription\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.youtube.com/\",\n      \"companyId\": \"google\"\n    },\n    \"yp\": {\n      \"name\": \"YellowPages\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.yellowpages.com/\",\n      \"companyId\": \"thryv\"\n    },\n    \"ysance\": {\n      \"name\": \"YSance\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.ysance.com/en/index.html\",\n      \"companyId\": \"ysance\"\n    },\n    \"yume\": {\n      \"name\": \"YuMe\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yume.com/\",\n      \"companyId\": \"yume\"\n    },\n    \"yume,_inc.\": {\n      \"name\": \"YuMe, Inc.\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.yume.com/\",\n      \"companyId\": \"yume\"\n    },\n    \"yusp\": {\n      \"name\": \"Yusp\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.yusp.com/\",\n      \"companyId\": \"yusp\"\n    },\n    \"zadarma\": {\n      \"name\": \"Zadarma\",\n      \"categoryId\": 2,\n      \"url\": \"https://zadarma.com/\",\n      \"companyId\": \"zadarma\"\n    },\n    \"zalando_de\": {\n      \"name\": \"zalando.de\",\n      \"categoryId\": 8,\n      \"url\": \"https://zalando.de/\",\n      \"companyId\": \"zalando\"\n    },\n    \"zalo\": {\n      \"name\": \"Zalo\",\n      \"categoryId\": 2,\n      \"url\": \"https://zaloapp.com/\",\n      \"companyId\": \"zalo\"\n    },\n    \"zanox\": {\n      \"name\": \"Zanox\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.zanox.com/us/\",\n      \"companyId\": \"axel_springer\"\n    },\n    \"zaparena\": {\n      \"name\": \"zaparena\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.zaparena.com/\",\n      \"companyId\": \"zapunited\"\n    },\n    \"zappos\": {\n      \"name\": \"Zappos\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.zappos.com/\",\n      \"companyId\": \"zappos\"\n    },\n    \"zdassets.com\": {\n      \"name\": \"Zendesk CDN\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.zendesk.com/\",\n      \"companyId\": \"zendesk\"\n    },\n    \"zebestof.com\": {\n      \"name\": \"Zebestof\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.zebestof.com/en/home/\",\n      \"companyId\": \"zebestof\"\n    },\n    \"zedo\": {\n      \"name\": \"Zedo\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.zedo.com/\",\n      \"companyId\": \"zedo\"\n    },\n    \"zemanta\": {\n      \"name\": \"Zemanta\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.zemanta.com/\",\n      \"companyId\": \"zemanta\"\n    },\n    \"zencoder\": {\n      \"name\": \"Zencoder\",\n      \"categoryId\": 0,\n      \"url\": \"https://zencoder.com/en/\",\n      \"companyId\": \"zencoder\"\n    },\n    \"zendesk\": {\n      \"name\": \"Zendesk\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.zendesk.com/\",\n      \"companyId\": \"zendesk\"\n    },\n    \"zergnet\": {\n      \"name\": \"ZergNet\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.zergnet.com/info\",\n      \"companyId\": \"zergnet\"\n    },\n    \"zero.kz\": {\n      \"name\": \"ZERO.kz\",\n      \"categoryId\": 6,\n      \"url\": \"http://zero.kz/\",\n      \"companyId\": \"neolabs_zero\"\n    },\n    \"zeta\": {\n      \"name\": \"Zeta\",\n      \"categoryId\": 2,\n      \"url\": \"https://zetaglobal.com/\",\n      \"companyId\": \"zeta\"\n    },\n    \"zeusclicks\": {\n      \"name\": \"ZeusClicks\",\n      \"categoryId\": 4,\n      \"url\": \"http://zeusclicks.com/\",\n      \"companyId\": \"zeusclicks\",\n      \"source\": \"AdGuard\"\n    },\n    \"ziff_davis\": {\n      \"name\": \"Ziff Davis\",\n      \"categoryId\": 4,\n      \"url\": \"https://www.ziffdavis.com/\",\n      \"companyId\": \"ziff_davis\"\n    },\n    \"zift_solutions\": {\n      \"name\": \"Zift Solutions\",\n      \"categoryId\": 6,\n      \"url\": \"https://ziftsolutions.com/\",\n      \"companyId\": \"zift_solutions\"\n    },\n    \"zimbio.com\": {\n      \"name\": \"Zimbio\",\n      \"categoryId\": 8,\n      \"url\": \"http://www.zimbio.com/\",\n      \"companyId\": \"livinglymedia\",\n      \"source\": \"AdGuard\"\n    },\n    \"zippyshare_widget\": {\n      \"name\": \"Zippyshare Widget\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.zippyshare.com\",\n      \"companyId\": \"zippyshare\"\n    },\n    \"zmags\": {\n      \"name\": \"Zmags\",\n      \"categoryId\": 6,\n      \"url\": \"https://zmags.com/\",\n      \"companyId\": \"zmags\"\n    },\n    \"zmctrack.net\": {\n      \"name\": \"zmctrack.net\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"zog.link\": {\n      \"name\": \"zog.link\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"zoho\": {\n      \"name\": \"Zoho\",\n      \"categoryId\": 6,\n      \"url\": \"https://www.zohocorp.com/index.html\",\n      \"companyId\": \"zoho_corp\"\n    },\n    \"zononi.com\": {\n      \"name\": \"zononi.com\",\n      \"categoryId\": 3,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"zopim\": {\n      \"name\": \"Zopim\",\n      \"categoryId\": 2,\n      \"url\": \"http://www.zopim.com/\",\n      \"companyId\": \"zendesk\"\n    },\n    \"zukxd6fkxqn.com\": {\n      \"name\": \"zukxd6fkxqn.com\",\n      \"categoryId\": 11,\n      \"url\": null,\n      \"companyId\": null\n    },\n    \"zwaar\": {\n      \"name\": \"Zwaar\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.zwaar.org\",\n      \"companyId\": \"zwaar\"\n    },\n    \"zypmedia\": {\n      \"name\": \"ZypMedia\",\n      \"categoryId\": 4,\n      \"url\": \"http://www.zypmedia.com/\",\n      \"companyId\": \"zypmedia\"\n    }\n  },\n  \"trackerDomains\": {\n    \"mmtro.com\": \"1000mercis\",\n    \"creative-serving.com\": \"161media\",\n    \"p161.net\": \"161media\",\n    \"analytics.163.com\": \"163\",\n    \"1822direkt.de\": \"1822direkt.de\",\n    \"1dmp.io\": \"1dmp.io\",\n    \"opecloud.com\": \"1plusx\",\n    \"1sponsor.com\": \"1sponsor\",\n    \"tm.dentsu.de\": \"1tag\",\n    \"1and1.com\": \"1und1\",\n    \"1und1.de\": \"1und1\",\n    \"uicdn.com\": \"1und1\",\n    \"website-start.de\": \"1und1\",\n    \"24-ads.com\": \"24-ads.com\",\n    \"247-inc.net\": \"24_7\",\n    \"d1af033869koo7.cloudfront.net\": \"24_7\",\n    \"counter.24log.ru\": \"24log\",\n    \"24smi.net\": \"24smi\",\n    \"24smi.org\": \"24smi\",\n    \"2leep.com\": \"2leep\",\n    \"33across.com\": \"33across\",\n    \"3dstats.com\": \"3dstats\",\n    \"3gpp.org\": \"3gpp\",\n    \"3gppnetwork.org\": \"3gpp\",\n    \"4cdn.org\": \"4chan\",\n    \"4finance.com\": \"4finance_com\",\n    \"4wnet.com\": \"4w_marketplace\",\n    \"d3aa0ztdn3oibi.cloudfront.net\": \"500friends\",\n    \"51.la\": \"51.la\",\n    \"5min.com\": \"5min_media\",\n    \"d1lm7kd3bd3yo9.cloudfront.net\": \"6sense\",\n    \"grepdata.com\": \"6sense\",\n    \"77tracking.com\": \"77tracking\",\n    \"swm.digital\": \"7plus\",\n    \"7tv.de\": \"7tv.de\",\n    \"888media.net\": \"888media\",\n    \"hit.8digits.com\": \"8digits\",\n    \"94j7afz2nr.xyz\": \"94j7afz2nr.xyz\",\n    \"statsanalytics.com\": \"99stats\",\n    \"a3cloud.net\": \"a3cloud_net\",\n    \"a8.net\": \"a8\",\n    \"aaxads.com\": \"aaxads.com\",\n    \"abtasty.com\": \"ab_tasty\",\n    \"d1447tq2m68ekg.cloudfront.net\": \"ab_tasty\",\n    \"ab.co\": \"abc\",\n    \"abc-cdn.net.au\": \"abc\",\n    \"abc-host.net\": \"abc\",\n    \"abc-host.net.au\": \"abc\",\n    \"abc-prod.net.au\": \"abc\",\n    \"abc-stage.net.au\": \"abc\",\n    \"abc-test.net.au\": \"abc\",\n    \"abc.net.au\": \"abc\",\n    \"abcaustralia.net.au\": \"abc\",\n    \"abcradio.net.au\": \"abc\",\n    \"ablida.de\": \"ablida\",\n    \"ablida.net\": \"ablida\",\n    \"durasite.net\": \"accelia\",\n    \"accengage.net\": \"accengage\",\n    \"ax.xrea.com\": \"accessanalyzer\",\n    \"accesstrade.net\": \"accesstrade\",\n    \"agcdn.com\": \"accord_group\",\n    \"accmgr.com\": \"accordant_media\",\n    \"p-td.com\": \"accuen_media\",\n    \"acestream.net\": \"acestream.net\",\n    \"acint.net\": \"acint.net\",\n    \"acloudimages.com\": \"acloudimages\",\n    \"acpm.fr\": \"acpm.fr\",\n    \"acquia.com\": \"acquia.com\",\n    \"ziyu.net\": \"acrweb\",\n    \"actionpay.ru\": \"actionpay\",\n    \"adnwb.ru\": \"actionpay\",\n    \"adonweb.ru\": \"actionpay\",\n    \"active-agent.com\": \"active_agent\",\n    \"trackcmp.net\": \"active_campaign\",\n    \"active-srv02.de\": \"active_performance\",\n    \"active-tracking.de\": \"active_performance\",\n    \"activeconversion.com\": \"activeconversion\",\n    \"a-cast.jp\": \"activecore\",\n    \"activemeter.com\": \"activemeter\",\n    \"go.activengage.com\": \"activengage\",\n    \"actonsoftware.com\": \"acton\",\n    \"acuityplatform.com\": \"acuity_ads\",\n    \"acxiom-online.com\": \"acxiom\",\n    \"acxiom.com\": \"acxiom\",\n    \"ad-blocker.org\": \"ad-blocker.org\",\n    \"ads.ad-center.com\": \"ad-center\",\n    \"ad-delivery.net\": \"ad-delivery.net\",\n    \"ad-sys.com\": \"ad-sys\",\n    \"adagionet.com\": \"ad.agio\",\n    \"ad2click.go2cloud.org\": \"ad2click\",\n    \"ad2games.com\": \"ad2games\",\n    \"ad360.vn\": \"ad360\",\n    \"ads.ad4game.com\": \"ad4game\",\n    \"ad4mat.ar\": \"ad4mat\",\n    \"ad4mat.at\": \"ad4mat\",\n    \"ad4mat.be\": \"ad4mat\",\n    \"ad4mat.bg\": \"ad4mat\",\n    \"ad4mat.br\": \"ad4mat\",\n    \"ad4mat.ch\": \"ad4mat\",\n    \"ad4mat.co.uk\": \"ad4mat\",\n    \"ad4mat.cz\": \"ad4mat\",\n    \"ad4mat.de\": \"ad4mat\",\n    \"ad4mat.dk\": \"ad4mat\",\n    \"ad4mat.es\": \"ad4mat\",\n    \"ad4mat.fi\": \"ad4mat\",\n    \"ad4mat.fr\": \"ad4mat\",\n    \"ad4mat.gr\": \"ad4mat\",\n    \"ad4mat.hu\": \"ad4mat\",\n    \"ad4mat.it\": \"ad4mat\",\n    \"ad4mat.mx\": \"ad4mat\",\n    \"ad4mat.net\": \"ad4mat\",\n    \"ad4mat.nl\": \"ad4mat\",\n    \"ad4mat.no\": \"ad4mat\",\n    \"ad4mat.pl\": \"ad4mat\",\n    \"ad4mat.ro\": \"ad4mat\",\n    \"ad4mat.ru\": \"ad4mat\",\n    \"ad4mat.se\": \"ad4mat\",\n    \"ad4mat.tr\": \"ad4mat\",\n    \"ad6.fr\": \"ad6media\",\n    \"ad6media.co.uk\": \"ad6media\",\n    \"ad6media.com\": \"ad6media\",\n    \"ad6media.es\": \"ad6media\",\n    \"ad6media.fr\": \"ad6media\",\n    \"a2dfp.net\": \"ad_decisive\",\n    \"addynamo.net\": \"ad_dynamo\",\n    \"ebis.ne.jp\": \"ad_ebis\",\n    \"adlightning.com\": \"ad_lightning\",\n    \"admagnet.net\": \"ad_magnet\",\n    \"amimg.net\": \"ad_magnet\",\n    \"adspirit.de\": \"ad_spirit\",\n    \"adspirit.net\": \"ad_spirit\",\n    \"adac.de\": \"adac_de\",\n    \"adacado.com\": \"adacado\",\n    \"ozonemedia.com\": \"adadyn\",\n    \"adrtx.net\": \"adality_gmbh\",\n    \"adalliance.io\": \"adalliance.io\",\n    \"adalyser.com\": \"adalyser.com\",\n    \"adaos-ads.net\": \"adaos\",\n    \"adap.tv\": \"adap.tv\",\n    \"smrtlnks.com\": \"adaptiveblue_smartlinks\",\n    \"yieldoptimizer.com\": \"adara_analytics\",\n    \"adnetwork.adasiaholdings.com\": \"adasia_holdings\",\n    \"adbetclickin.pink\": \"adbetclickin.pink\",\n    \"adbetnet.com\": \"adbetnet.com\",\n    \"adblade.com\": \"adblade.com\",\n    \"adbooth.com\": \"adbooth\",\n    \"adbooth.net\": \"adbooth\",\n    \"adbox.lv\": \"adbox\",\n    \"adbrn.com\": \"adbrain\",\n    \"adbrite.com\": \"adbrite\",\n    \"adbull.com\": \"adbull\",\n    \"adbutler.com\": \"adbutler\",\n    \"adc-serv.net\": \"adc_media\",\n    \"adc-srv.net\": \"adc_media\",\n    \"adcash.com\": \"adcash\",\n    \"vuroll.in\": \"adchakra\",\n    \"acs86.com\": \"adchina\",\n    \"csbew.com\": \"adchina\",\n    \"irs09.com\": \"adchina\",\n    \"adcito.com\": \"adcito\",\n    \"adcitomedia.com\": \"adcito\",\n    \"adclear.net\": \"adclear\",\n    \"swift.adclerks.com\": \"adclerks\",\n    \"adclickmedia.com\": \"adclickmedia\",\n    \"adclickzone.go2cloud.org\": \"adclickzone\",\n    \"ad-cloud.jp\": \"adcloud\",\n    \"admarvel.s3.amazonaws.com\": \"adcolony\",\n    \"ads.admarvel.com\": \"adcolony\",\n    \"adcolony.com\": \"adcolony\",\n    \"adrdgt.com\": \"adconion\",\n    \"amgdgt.com\": \"adconion\",\n    \"adcrowd.com\": \"adcrowd\",\n    \"shop2market.com\": \"adcurve\",\n    \"addtocalendar.com\": \"add_to_calendar\",\n    \"dpmsrv.com\": \"addaptive\",\n    \"yagiay.com\": \"addefend\",\n    \"addfreestats.com\": \"addfreestats\",\n    \"addinto.com\": \"addinto\",\n    \"addshoppers.com\": \"addshoppers\",\n    \"shop.pe\": \"addshoppers\",\n    \"addthis.com\": \"addthis\",\n    \"addthiscdn.com\": \"addthis\",\n    \"addthisedge.com\": \"addthis\",\n    \"b2btracking.addvalue.de\": \"addvalue\",\n    \"addyon.com\": \"addyon\",\n    \"adeasy.ru\": \"adeasy\",\n    \"ipredictive.com\": \"adelphic\",\n    \"adengage.com\": \"adengage\",\n    \"adespresso.com\": \"adespresso\",\n    \"adexcite.com\": \"adexcite\",\n    \"adextent.com\": \"adextent\",\n    \"adf.ly\": \"adf.ly\",\n    \"adfalcon.com\": \"adfalcon\",\n    \"adfoc.us\": \"adfocus\",\n    \"js.adforgames.com\": \"adforgames\",\n    \"adform.net\": \"adform\",\n    \"adformdsp.net\": \"adform\",\n    \"seadform.net\": \"adform\",\n    \"adfox.ru\": \"adfox\",\n    \"adwolf.ru\": \"adfox\",\n    \"adfreestyle.pl\": \"adfreestyle\",\n    \"adfront.org\": \"adfront\",\n    \"adfrontiers.com\": \"adfrontiers\",\n    \"adgebra.co.in\": \"adgebra\",\n    \"adgenie.co.uk\": \"adgenie\",\n    \"ad.adgile.com\": \"adgile\",\n    \"ad.antventure.com\": \"adgile\",\n    \"adglare.net\": \"adglare.net\",\n    \"adsafety.net\": \"adglue\",\n    \"smartadcheck.de\": \"adgoal\",\n    \"smartredirect.de\": \"adgoal\",\n    \"adgorithms.com\": \"adgorithms\",\n    \"adgoto.com\": \"adgoto\",\n    \"adguard.com\": \"adguard\",\n    \"adguard.app\": \"adguard\",\n    \"adguard.info\": \"adguard\",\n    \"adguard.io\": \"adguard\",\n    \"adguard.org\": \"adguard\",\n    \"adtidy.org\": \"adguard\",\n    \"agrd.io\": \"adguard\",\n    \"agrd.eu\": \"adguard\",\n    \"adguard-dns.com\": \"adguard_dns\",\n    \"adguard-dns.io\": \"adguard_dns\",\n    \"adguard-vpn.com\": \"adguard_vpn\",\n    \"adguard-vpn.online\": \"adguard_vpn\",\n    \"adguardvpn.com\": \"adguard_vpn\",\n    \"adhands.ru\": \"adhands\",\n    \"adhese.be\": \"adhese\",\n    \"adhese.com\": \"adhese\",\n    \"adhese.net\": \"adhese\",\n    \"adhitzads.com\": \"adhitz\",\n    \"adhood.com\": \"adhood\",\n    \"afy11.net\": \"adify\",\n    \"cdn.adikteev.com\": \"adikteev\",\n    \"adimpact.com\": \"adimpact\",\n    \"adinch.com\": \"adinch\",\n    \"adition.com\": \"adition\",\n    \"adjal.com\": \"adjal\",\n    \"cdn.adjs.net\": \"adjs\",\n    \"adjug.com\": \"adjug\",\n    \"adjust.com\": \"adjust\",\n    \"adj.st\": \"adjust\",\n    \"adjust.io\": \"adjust\",\n    \"adjust.net.in\": \"adjust\",\n    \"adjust.world\": \"adjust\",\n    \"apptrace.com\": \"adjust\",\n    \"adk2.com\": \"adk2\",\n    \"cdn.adsrvmedia.com\": \"adk2\",\n    \"cdn.cdnrl.com\": \"adk2\",\n    \"adklip.com\": \"adklip\",\n    \"adkengage.com\": \"adknowledge\",\n    \"adknowledge.com\": \"adknowledge\",\n    \"bidsystem.com\": \"adknowledge\",\n    \"blogads.com\": \"adknowledge\",\n    \"cubics.com\": \"adknowledge\",\n    \"yarpp.org\": \"adknowledge\",\n    \"adsearch.adkontekst.pl\": \"adkontekst\",\n    \"netsprint.eu\": \"adkontekst.pl\",\n    \"adlabs.ru\": \"adlabs\",\n    \"clickiocdn.com\": \"adlabs\",\n    \"luxup.ru\": \"adlabs\",\n    \"mixmarket.biz\": \"adlabs\",\n    \"ad-serverparc.nl\": \"adlantic\",\n    \"adimg.net\": \"adlantis\",\n    \"adlantis.jp\": \"adlantis\",\n    \"cdn.adless.io\": \"adless\",\n    \"api.publishers.adlive.io\": \"adlive_header_bidding\",\n    \"adlooxtracking.com\": \"adloox\",\n    \"adx1.com\": \"admachine\",\n    \"adman.gr\": \"adman\",\n    \"adman.in.gr\": \"adman\",\n    \"admanmedia.com\": \"adman_media\",\n    \"admantx.com\": \"admantx.com\",\n    \"admaster.net\": \"admaster\",\n    \"cdnmaster.com\": \"admaster\",\n    \"admaster.com.cn\": \"admaster.cn\",\n    \"admasterapi.com\": \"admaster.cn\",\n    \"admatic.com.tr\": \"admatic\",\n    \"ads5.admatic.com.tr\": \"admatic\",\n    \"cdn2.admatic.com.tr\": \"admatic\",\n    \"lib-3pas.admatrix.jp\": \"admatrix\",\n    \"admaxserver.com\": \"admax\",\n    \"admaxim.com\": \"admaxim\",\n    \"admaya.in\": \"admaya\",\n    \"admedia.com\": \"admedia\",\n    \"adizio.com\": \"admedo_com\",\n    \"admedo.com\": \"admedo_com\",\n    \"admeira.ch\": \"admeira.ch\",\n    \"admeld.com\": \"admeld\",\n    \"admeo.ru\": \"admeo\",\n    \"admaym.com\": \"admeta\",\n    \"atemda.com\": \"admeta\",\n    \"admicro.vn\": \"admicro\",\n    \"vcmedia.vn\": \"admicro\",\n    \"admitad.com\": \"admitad.com\",\n    \"admixer.net\": \"admixer\",\n    \"admixer.com\": \"admixer\",\n    \"admized.com\": \"admized\",\n    \"admo.tv\": \"admo.tv\",\n    \"a.admob.com\": \"admob\",\n    \"mm.admob.com\": \"admob\",\n    \"mmv.admob.com\": \"admob\",\n    \"p.admob.com\": \"admob\",\n    \"run.admost.com\": \"admost\",\n    \"dmmotion.com\": \"admotion\",\n    \"nspmotion.com\": \"admotion\",\n    \"admulti.com\": \"admulti\",\n    \"adnegah.net\": \"adnegah\",\n    \"adnet.vn\": \"adnet\",\n    \"adnet.biz\": \"adnet.de\",\n    \"adnet.de\": \"adnet.de\",\n    \"adclick.lt\": \"adnet_media\",\n    \"adnet.lt\": \"adnet_media\",\n    \"ad.adnetwork.net\": \"adnetwork.net\",\n    \"adnetworkperformance.com\": \"adnetworkperformance.com\",\n    \"adserver.adnexio.com\": \"adnexio\",\n    \"adnium.com\": \"adnium.com\",\n    \"heias.com\": \"adnologies\",\n    \"smaclick.com\": \"adnow\",\n    \"st-n.ads3-adnow.com\": \"adnow\",\n    \"adnymics.com\": \"adnymics\",\n    \"adobe.com\": \"adobe_audience_manager\",\n    \"demdex.net\": \"adobe_audience_manager\",\n    \"everestjs.net\": \"adobe_audience_manager\",\n    \"everesttech.net\": \"adobe_audience_manager\",\n    \"adobe.io\": \"adobe_developer\",\n    \"scene7.com\": \"adobe_dynamic_media\",\n    \"adobedtm.com\": \"adobe_dynamic_tag_management\",\n    \"2o7.net\": \"adobe_experience_cloud\",\n    \"du8783wkf05yr.cloudfront.net\": \"adobe_experience_cloud\",\n    \"hitbox.com\": \"adobe_experience_cloud\",\n    \"imageg.net\": \"adobe_experience_cloud\",\n    \"nedstat.com\": \"adobe_experience_cloud\",\n    \"omtrdc.net\": \"adobe_experience_cloud\",\n    \"sitestat.com\": \"adobe_experience_cloud\",\n    \"adobedc.net\": \"adobe_experience_league\",\n    \"adobelogin.com\": \"adobe_login\",\n    \"adobetag.com\": \"adobe_tagmanager\",\n    \"typekit.com\": \"adobe_typekit\",\n    \"typekit.net\": \"adobe_typekit\",\n    \"adocean.pl\": \"adocean\",\n    \"dmtry.com\": \"adometry\",\n    \"adomik.com\": \"adomik\",\n    \"adcde.com\": \"adon_network\",\n    \"addlvr.com\": \"adon_network\",\n    \"adfeedstrk.com\": \"adon_network\",\n    \"adtrgt.com\": \"adon_network\",\n    \"bannertgt.com\": \"adon_network\",\n    \"cptgt.com\": \"adon_network\",\n    \"cpvfeed.com\": \"adon_network\",\n    \"cpvtgt.com\": \"adon_network\",\n    \"mygeek.com\": \"adon_network\",\n    \"popcde.com\": \"adon_network\",\n    \"sdfje.com\": \"adon_network\",\n    \"urtbk.com\": \"adon_network\",\n    \"adonion.com\": \"adonion\",\n    \"t.adonly.com\": \"adonly\",\n    \"adoperator.com\": \"adoperator\",\n    \"adoric.com\": \"adoric\",\n    \"adorika.com\": \"adorika\",\n    \"adorika.net\": \"adorika\",\n    \"adosia.com\": \"adosia\",\n    \"adotmob.com\": \"adotmob.com\",\n    \"adotube.com\": \"adotube\",\n    \"adparlor.com\": \"adparlor\",\n    \"adparlour.com\": \"adparlor\",\n    \"a4p.adpartner.pro\": \"adpartner\",\n    \"adpeepshosted.com\": \"adpeeps\",\n    \"adperfect.com\": \"adperfect\",\n    \"adperium.com\": \"adperium\",\n    \"adpilot.at\": \"adpilot\",\n    \"erne.co\": \"adpilot\",\n    \"adplan-ds.com\": \"adplan\",\n    \"advg.jp\": \"adplan\",\n    \"c.p-advg.com\": \"adplan\",\n    \"adplus.co.id\": \"adplus\",\n    \"adprofex.com\": \"adprofex\",\n    \"ads2.bid\": \"adprofex\",\n    \"adframesrc.com\": \"adprofy\",\n    \"adserve.adpulse.ir\": \"adpulse\",\n    \"ads.adpv.com\": \"adpv\",\n    \"adreactor.com\": \"adreactor\",\n    \"adrecord.com\": \"adrecord\",\n    \"adrecover.com\": \"adrecover\",\n    \"ad.vcm.jp\": \"adresult\",\n    \"adresult.jp\": \"adresult\",\n    \"adriver.ru\": \"adriver\",\n    \"adroll.com\": \"adroll\",\n    \"adrom.net\": \"adrom\",\n    \"txt.eu\": \"adrom\",\n    \"adru.net\": \"adru.net\",\n    \"adrunnr.com\": \"adrunnr\",\n    \"adsame.com\": \"adsame\",\n    \"adsbookie.com\": \"adsbookie\",\n    \"adscale.de\": \"adscale\",\n    \"adscience.nl\": \"adscience\",\n    \"adsco.re\": \"adsco.re\",\n    \"adsensecamp.com\": \"adsensecamp\",\n    \"adserverpub.com\": \"adserverpub\",\n    \"online.adservicemedia.dk\": \"adservice_media\",\n    \"adsfactor.net\": \"adsfactor\",\n    \"ads.doclix.com\": \"adside\",\n    \"adskeeper.co.uk\": \"adskeeper\",\n    \"ssp.adskom.com\": \"adskom\",\n    \"adslot.com\": \"adslot\",\n    \"adsnative.com\": \"adsnative\",\n    \"adsniper.ru\": \"adsniper.ru\",\n    \"adspeed.com\": \"adspeed\",\n    \"adspeed.net\": \"adspeed\",\n    \"o333o.com\": \"adspyglass\",\n    \"adstage-analytics.herokuapp.com\": \"adstage\",\n    \"code.adstanding.com\": \"adstanding\",\n    \"adstars.co.id\": \"adstars\",\n    \"ad-stir.com\": \"adstir\",\n    \"4dsply.com\": \"adsupply\",\n    \"cdn.engine.adsupply.com\": \"adsupply\",\n    \"trklnks.com\": \"adsupply\",\n    \"adswizz.com\": \"adswizz\",\n    \"adtaily.com\": \"adtaily\",\n    \"adtaily.pl\": \"adtaily\",\n    \"adtarget.me\": \"adtarget.me\",\n    \"adtech.de\": \"adtech\",\n    \"adtechus.com\": \"adtech\",\n    \"adtegrity.net\": \"adtegrity\",\n    \"adtpix.com\": \"adtegrity\",\n    \"adtelligence.de\": \"adtelligence.de\",\n    \"adentifi.com\": \"adtheorent\",\n    \"adthink.com\": \"adthink\",\n    \"advertstream.com\": \"adthink\",\n    \"audienceinsights.net\": \"adthink\",\n    \"adtiger.de\": \"adtiger\",\n    \"adtimaserver.vn\": \"adtima\",\n    \"adtng.com\": \"adtng.com\",\n    \"adtoma.com\": \"adtoma\",\n    \"adtomafusion.com\": \"adtoma\",\n    \"adtr02.com\": \"adtr02.com\",\n    \"track.adtraction.com\": \"adtraction\",\n    \"adtraxx.de\": \"adtraxx\",\n    \"adtriba.com\": \"adtriba.com\",\n    \"adtrue.com\": \"adtrue\",\n    \"adtrustmedia.com\": \"adtrustmedia\",\n    \"ad.adtube.ir\": \"adtube\",\n    \"awempire.com\": \"adult_webmaster_empire\",\n    \"dditscdn.com\": \"adult_webmaster_empire\",\n    \"livejasmin.com\": \"adult_webmaster_empire\",\n    \"adultadworld.com\": \"adultadworld\",\n    \"adworldmedia.com\": \"adultadworld\",\n    \"adup-tech.com\": \"adup-tech.com\",\n    \"advaction.ru\": \"advaction\",\n    \"aucourant.info\": \"advaction\",\n    \"schetu.net\": \"advaction\",\n    \"dqfw2hlp4tfww.cloudfront.net\": \"advalo\",\n    \"ahcdn.com\": \"advanced_hosters\",\n    \"pix-cdn.org\": \"advanced_hosters\",\n    \"s3.advarkads.com\": \"advark\",\n    \"adventori.com\": \"adventori\",\n    \"adnext.fr\": \"adverline\",\n    \"adverline.com\": \"adverline\",\n    \"surinter.net\": \"adverline\",\n    \"adversaldisplay.com\": \"adversal\",\n    \"adversalservers.com\": \"adversal\",\n    \"go.adversal.com\": \"adversal\",\n    \"adverserve.net\": \"adverserve\",\n    \"ad.adverteerdirect.nl\": \"adverteerdirect\",\n    \"adverticum.net\": \"adverticum\",\n    \"advertise.com\": \"advertise.com\",\n    \"advertisespace.com\": \"advertisespace\",\n    \"adsdk.com\": \"advertising.com\",\n    \"advertising.com\": \"advertising.com\",\n    \"aol.com\": \"advertising.com\",\n    \"atwola.com\": \"advertising.com\",\n    \"pictela.net\": \"advertising.com\",\n    \"verizonmedia.com\": \"advertising.com\",\n    \"advertlets.com\": \"advertlets\",\n    \"advertserve.com\": \"advertserve\",\n    \"advidi.com\": \"advidi\",\n    \"am10.ru\": \"advmaker.ru\",\n    \"am15.net\": \"advmaker.ru\",\n    \"advolution.de\": \"advolution\",\n    \"adwebster.com\": \"adwebster\",\n    \"ads.adwitserver.com\": \"adwit\",\n    \"adworx.at\": \"adworx.at\",\n    \"adworxs.net\": \"adworxs.net\",\n    \"adxion.com\": \"adxion\",\n    \"adxpansion.com\": \"adxpansion\",\n    \"ads.adxpose.com\": \"adxpose\",\n    \"event.adxpose.com\": \"adxpose\",\n    \"servedby.adxpose.com\": \"adxpose\",\n    \"adxprtz.com\": \"adxprtz.com\",\n    \"adyoulike.com\": \"adyoulike\",\n    \"omnitagjs.com\": \"adyoulike\",\n    \"adzerk.net\": \"adzerk\",\n    \"adzly.com\": \"adzly\",\n    \"aemediatraffic.com\": \"aemediatraffic\",\n    \"hprofits.com\": \"aemediatraffic\",\n    \"amxdt.com\": \"aerify_media\",\n    \"aerisapi.com\": \"aeris_weather\",\n    \"aerisweather.com\": \"aeris_weather\",\n    \"affectv.com\": \"affectv\",\n    \"go.affec.tv\": \"affectv\",\n    \"hybridtheory.com\": \"affectv\",\n    \"affilbox.com\": \"affilbox\",\n    \"affilbox.cz\": \"affilbox\",\n    \"track.affiliate-b.com\": \"affiliate-b\",\n    \"affiliate4you.nl\": \"affiliate4you\",\n    \"ads.affbuzzads.com\": \"affiliatebuzz\",\n    \"affiliatefuture.com\": \"affiliatefuture\",\n    \"affiliatelounge.com\": \"affiliatelounge\",\n    \"affiliation-france.com\": \"affiliation_france\",\n    \"affiliator.com\": \"affiliator\",\n    \"affiliaweb.fr\": \"affiliaweb\",\n    \"banner-rotation.com\": \"affilinet\",\n    \"webmasterplan.com\": \"affilinet\",\n    \"affimax.de\": \"affimax\",\n    \"affinity.com\": \"affinity\",\n    \"countby.com\": \"affinity.by\",\n    \"affiz.net\": \"affiz_cpm\",\n    \"pml.afftrack.com\": \"afftrack\",\n    \"afgr2.com\": \"afgr2.com\",\n    \"v2.afilio.com.br\": \"afilio\",\n    \"afsanalytics.com\": \"afs_analystics\",\n    \"ads.aftonbladet.se\": \"aftonbladet_ads\",\n    \"aftv-serving.bid\": \"aftv-serving.bid\",\n    \"agkn.com\": \"aggregate_knowledge\",\n    \"agilone.com\": \"agilone\",\n    \"adview.pl\": \"agora\",\n    \"pingagenow.com\": \"ahalogy\",\n    \"aimediagroup.com\": \"ai_media_group\",\n    \"advombat.ru\": \"aidata\",\n    \"aidata.io\": \"aidata\",\n    \"aim4media.com\": \"aim4media\",\n    \"muscache.com\": \"airbnb\",\n    \"musthird.com\": \"airbnb\",\n    \"airbrake.io\": \"airbrake\",\n    \"airpr.com\": \"airpr.com\",\n    \"ab.airpush.com\": \"airpush\",\n    \"abmr.net\": \"akamai_technologies\",\n    \"akamai.net\": \"akamai_technologies\",\n    \"akamaihd.net\": \"akamai_technologies\",\n    \"akamaized.net\": \"akamai_technologies\",\n    \"akstat.io\": \"akamai_technologies\",\n    \"edgekey.net\": \"akamai_technologies\",\n    \"edgesuite.net\": \"akamai_technologies\",\n    \"imiclk.com\": \"akamai_technologies\",\n    \"akadns.net\": \"akamai_technologies\",\n    \"akamaiedge.net\": \"akamai_technologies\",\n    \"akaquill.net\": \"akamai_technologies\",\n    \"akamoihd.net\": \"akamoihd.net\",\n    \"adn-d.sp.gmossp-sp.jp\": \"akane\",\n    \"akanoo.com\": \"akanoo\",\n    \"akavita.com\": \"akavita\",\n    \"ads.albawaba.com\": \"al_bawaba_advertising\",\n    \"serve.albacross.com\": \"albacross\",\n    \"aldi-international.com\": \"aldi-international.com\",\n    \"alenty.com\": \"alenty\",\n    \"alephd.com\": \"alephd.com\",\n    \"alexametrics.com\": \"alexa_metrics\",\n    \"d31qbv1cthcecs.cloudfront.net\": \"alexa_metrics\",\n    \"d5nxst8fruw4z.cloudfront.net\": \"alexa_metrics\",\n    \"alexa.com\": \"alexa_traffic_rank\",\n    \"algolia.com\": \"algolia.net\",\n    \"algolia.net\": \"algolia.net\",\n    \"algovid.com\": \"algovid.com\",\n    \"alibaba.com\": \"alibaba.com\",\n    \"alicdn.com\": \"alibaba.com\",\n    \"aliapp.org\": \"alibaba.com\",\n    \"alibabachengdun.com\": \"alibaba.com\",\n    \"alibabausercontent.com\": \"alibaba.com\",\n    \"aliexpress.com\": \"alibaba.com\",\n    \"alikunlun.com\": \"alibaba.com\",\n    \"aliyuncs.com\": \"alibaba.com\",\n    \"alibabacloud.com\": \"alibaba_cloud\",\n    \"alibabadns.com\": \"alibaba_cloud\",\n    \"aliyun.com\": \"alibaba_cloud\",\n    \"ucweb.com\": \"alibaba_ucbrowser\",\n    \"alipay.com\": \"alipay.com\",\n    \"alipayobjects.com\": \"alipay.com\",\n    \"websitealive.com\": \"alivechat\",\n    \"allegroimg.com\": \"allegro.pl\",\n    \"allegrostatic.com\": \"allegro.pl\",\n    \"allegrostatic.pl\": \"allegro.pl\",\n    \"ngacm.com\": \"allegro.pl\",\n    \"ngastatic.com\": \"allegro.pl\",\n    \"i.btg360.com.br\": \"allin\",\n    \"allo-pages.fr\": \"allo-pages.fr\",\n    \"allotraffic.com\": \"allotraffic\",\n    \"edge.alluremedia.com.au\": \"allure_media\",\n    \"allyes.com\": \"allyes\",\n    \"inputs.alooma.com\": \"alooma\",\n    \"arena.altitude-arena.com\": \"altitude_digital\",\n    \"amadesa.com\": \"amadesa\",\n    \"amap.com\": \"amap\",\n    \"amazon.ca\": \"amazon\",\n    \"amazon.co.jp\": \"amazon\",\n    \"amazon.co.uk\": \"amazon\",\n    \"amazon.com\": \"amazon\",\n    \"amazon.de\": \"amazon\",\n    \"amazon.es\": \"amazon\",\n    \"amazon.fr\": \"amazon\",\n    \"amazon.it\": \"amazon\",\n    \"d3io1k5o0zdpqr.cloudfront.net\": \"amazon\",\n    \"a2z.com\": \"amazon\",\n    \"aamazoncognito.com\": \"amazon\",\n    \"amazon-corp.com\": \"amazon\",\n    \"amazon-dss.com\": \"amazon\",\n    \"amazon.com.au\": \"amazon\",\n    \"amazon.com.mx\": \"amazon\",\n    \"amazon.dev\": \"amazon\",\n    \"amazon.in\": \"amazon\",\n    \"amazon.nl\": \"amazon\",\n    \"amazon.sa\": \"amazon\",\n    \"amazonbrowserapp.co.uk\": \"amazon\",\n    \"amazonbrowserapp.es\": \"amazon\",\n    \"amazoncrl.com\": \"amazon\",\n    \"firetvcaptiveportal.com\": \"amazon\",\n    \"ntp-fireos.com\": \"amazon\",\n    \"amazon-adsystem.com\": \"amazon_adsystem\",\n    \"serving-sys.com\": \"amazon_adsystem\",\n    \"sizmek.com\": \"amazon_adsystem\",\n    \"assoc-amazon.ca\": \"amazon_associates\",\n    \"assoc-amazon.co.uk\": \"amazon_associates\",\n    \"assoc-amazon.com\": \"amazon_associates\",\n    \"assoc-amazon.de\": \"amazon_associates\",\n    \"assoc-amazon.fr\": \"amazon_associates\",\n    \"assoc-amazon.jp\": \"amazon_associates\",\n    \"images-amazon.com\": \"amazon_cdn\",\n    \"media-amazon.com\": \"amazon_cdn\",\n    \"ssl-images-amazon.com\": \"amazon_cdn\",\n    \"amazontrust.com\": \"amazon_cdn\",\n    \"associates-amazon.com\": \"amazon_cdn\",\n    \"cloudfront.net\": \"amazon_cloudfront\",\n    \"ota-cloudfront.net\": \"amazon_cloudfront\",\n    \"axx-eu.amazon-adsystem.com\": \"amazon_mobile_ads\",\n    \"amazonpay.com\": \"amazon_payments\",\n    \"payments-amazon.com\": \"amazon_payments\",\n    \"amazonpay.in\": \"amazon_payments\",\n    \"aiv-cdn.net\": \"amazon_video\",\n    \"aiv-delivery.net\": \"amazon_video\",\n    \"amazonvideo.com\": \"amazon_video\",\n    \"pv-cdn.net\": \"amazon_video\",\n    \"primevideo.com\": \"amazon_video\",\n    \"amazonaws.com\": \"amazon_web_services\",\n    \"amazonwebservices.com\": \"amazon_web_services\",\n    \"awsstatic.com\": \"amazon_web_services\",\n    \"adnetwork.net.vn\": \"ambient_digital\",\n    \"adnetwork.vn\": \"ambient_digital\",\n    \"ambientplatform.vn\": \"ambient_digital\",\n    \"amgload.net\": \"amgload.net\",\n    \"amoad.com\": \"amoad\",\n    \"ad.amgdgt.com\": \"amobee\",\n    \"ads.amgdgt.com\": \"amobee\",\n    \"amobee.com\": \"amobee\",\n    \"collective-media.net\": \"amp_platform\",\n    \"amplitude.com\": \"amplitude\",\n    \"d24n15hnbwhuhn.cloudfront.net\": \"amplitude\",\n    \"ampproject.org\": \"ampproject.org\",\n    \"anametrix.net\": \"anametrix\",\n    \"ancestrycdn.com\": \"ancestry_cdn\",\n    \"ancoraplatform.com\": \"ancora\",\n    \"android.com\": \"android\",\n    \"anetwork.ir\": \"anetwork\",\n    \"aniview.com\": \"aniview.com\",\n    \"a-ads.com\": \"anonymousads\",\n    \"anormal-tracker.de\": \"anormal_tracker\",\n    \"answerscloud.com\": \"answers_cloud_service\",\n    \"anthill.vn\": \"ants\",\n    \"ants.vn\": \"ants\",\n    \"rt.analytics.anvato.net\": \"anvato\",\n    \"tkx2-prod.anvato.net\": \"anvato\",\n    \"w3.cdn.anvato.net\": \"anvato\",\n    \"player.anyclip.com\": \"anyclip\",\n    \"video-loader.com\": \"aol_be_on\",\n    \"aolcdn.com\": \"aol_cdn\",\n    \"isp.netscape.com\": \"aol_cdn\",\n    \"apa.at\": \"apa.at\",\n    \"apester.com\": \"apester\",\n    \"apicit.net\": \"apicit.net\",\n    \"carrierzone.com\": \"aplus_analytics\",\n    \"appcenter.ms\": \"appcenter\",\n    \"appcues.com\": \"appcues\",\n    \"appdynamics.com\": \"appdynamics\",\n    \"de8of677fyt0b.cloudfront.net\": \"appdynamics\",\n    \"eum-appdynamics.com\": \"appdynamics\",\n    \"jscdn.appier.net\": \"appier\",\n    \"apple.com\": \"apple\",\n    \"aaplimg.com\": \"apple\",\n    \"apple-cloudkit.com\": \"apple\",\n    \"apple-dns.net\": \"apple\",\n    \"apple-livephotoskit.com\": \"apple\",\n    \"apple-mapkit.com\": \"apple\",\n    \"apple.news\": \"apple\",\n    \"apzones.com\": \"apple\",\n    \"cdn-apple.com\": \"apple\",\n    \"icloud-content.com\": \"apple\",\n    \"icloud.com\": \"apple\",\n    \"icons.axm-usercontent-apple.com\": \"apple\",\n    \"itunes.com\": \"apple\",\n    \"me.com\": \"apple\",\n    \"mzstatic.com\": \"apple\",\n    \"safebrowsing.apple\": \"apple\",\n    \"safebrowsing.g.applimg.com\": \"apple\",\n    \"iadsdk.apple.com\": \"apple_ads\",\n    \"applifier.com\": \"applifier\",\n    \"assets.applovin.com\": \"applovin\",\n    \"applovin.com\": \"applovin\",\n    \"applvn.com\": \"applovin\",\n    \"appmetrx.com\": \"appmetrx\",\n    \"adnxs.com\": \"appnexus\",\n    \"adnxs.net\": \"appnexus\",\n    \"appsflyer.com\": \"appsflyer\",\n    \"appsflyersdk.com\": \"appsflyer\",\n    \"adne.tv\": \"apptv\",\n    \"readserver.net\": \"apptv\",\n    \"www.apture.com\": \"apture\",\n    \"arcpublishing.com\": \"arcpublishing\",\n    \"ard.de\": \"ard.de\",\n    \"areyouahuman.com\": \"are_you_a_human\",\n    \"arkoselabs.com\": \"arkoselabs.com\",\n    \"art19.com\": \"art19\",\n    \"banners.advsnx.net\": \"artimedia\",\n    \"artlebedev.ru\": \"artlebedev.ru\",\n    \"ammadv.it\": \"aruba_media_marketing\",\n    \"arubamediamarketing.it\": \"aruba_media_marketing\",\n    \"cya2.net\": \"arvato_canvas_fp\",\n    \"asambeauty.com\": \"asambeauty.com\",\n    \"ask.com\": \"ask.com\",\n    \"aspnetcdn.com\": \"aspnetcdn\",\n    \"ads.assemblyexchange.com\": \"assemblyexchange\",\n    \"cdn.astronomer.io\": \"astronomer\",\n    \"ati-host.net\": \"at_internet\",\n    \"aticdn.net\": \"at_internet\",\n    \"xiti.com\": \"at_internet\",\n    \"atedra.com\": \"atedra\",\n    \"oadts.com\": \"atg_group\",\n    \"as00.estara.com\": \"atg_optimization\",\n    \"atgsvcs.com\": \"atg_recommendations\",\n    \"adbureau.net\": \"atlas\",\n    \"atdmt.com\": \"atlas\",\n    \"atlassbx.com\": \"atlas\",\n    \"track.roiservice.com\": \"atlas_profitbuilder\",\n    \"atl-paas.net\": \"atlassian.net\",\n    \"atlassian.com\": \"atlassian.net\",\n    \"atlassian.net\": \"atlassian.net\",\n    \"d12ramskps3070.cloudfront.net\": \"atlassian.net\",\n    \"bitbucket.org\": \"atlassian.net\",\n    \"jira.com\": \"atlassian.net\",\n    \"ss-inf.net\": \"atlassian.net\",\n    \"d1xfq2052q7thw.cloudfront.net\": \"atlassian_marketplace\",\n    \"marketplace.atlassian.com\": \"atlassian_marketplace\",\n    \"atomz.com\": \"atomz_search\",\n    \"atsfi.de\": \"atsfi_de\",\n    \"cdn.attracta.com\": \"attracta\",\n    \"locayta.com\": \"attraqt\",\n    \"ads.audience2media.com\": \"audience2media\",\n    \"qwobl.net\": \"audience_ad_network\",\n    \"revsci.net\": \"audience_science\",\n    \"wunderloop.net\": \"audience_science\",\n    \"12mlbe.com\": \"audiencerate\",\n    \"audiencesquare.com\": \"audiencesquare.com\",\n    \"ad.gt\": \"audiencesquare.com\",\n    \"audigent.com\": \"audiencesquare.com\",\n    \"hadronid.net\": \"audiencesquare.com\",\n    \"auditude.com\": \"auditude\",\n    \"audtd.com\": \"audtd.com\",\n    \"cdn.augur.io\": \"augur\",\n    \"aumago.com\": \"aumago\",\n    \"clicktracks.com\": \"aurea_clicktracks\",\n    \"ausgezeichnet.org\": \"ausgezeichnet_org\",\n    \"advertising.gov.au\": \"australia.gov\",\n    \"auth0.com\": \"auth0\",\n    \"ai.autoid.com\": \"autoid\",\n    \"optimost.com\": \"autonomy\",\n    \"oc-track.autonomycloud.com\": \"autonomy_campaign\",\n    \"track.yieldsoftware.com\": \"autonomy_campaign\",\n    \"api.autopilothq.com\": \"autopilothq\",\n    \"autoscout24.com\": \"autoscout24.com\",\n    \"autoscout24.net\": \"autoscout24.com\",\n    \"avail.net\": \"avail\",\n    \"analytics.avanser.com.au\": \"avanser\",\n    \"avmws.com\": \"avant_metrics\",\n    \"avantlink.com\": \"avantlink\",\n    \"ads.avazu.net\": \"avazu_network\",\n    \"avenseo.com\": \"avenseo\",\n    \"adspdbl.com\": \"avid_media\",\n    \"avocet.io\": \"avocet\",\n    \"aweber.com\": \"aweber\",\n    \"awin.com\": \"awin\",\n    \"awin1.com\": \"awin\",\n    \"perfb.com\": \"awin\",\n    \"ad.globe7.com\": \"axill\",\n    \"azadify.com\": \"azadify\",\n    \"azure.com\": \"azure\",\n    \"azure.net\": \"azure\",\n    \"azurefd.net\": \"azure\",\n    \"trafficmanager.net\": \"azure\",\n    \"blob.core.windows.net\": \"azure_blob_storage\",\n    \"azureedge.net\": \"azureedge.net\",\n    \"b2bcontext.ru\": \"b2bcontext\",\n    \"b2bvideo.ru\": \"b2bvideo\",\n    \"babator.com\": \"babator.com\",\n    \"backbeatmedia.com\": \"back_beat_media\",\n    \"widgets.backtype.com\": \"backtype_widgets\",\n    \"bahn.de\": \"bahn_de\",\n    \"img-bahn.de\": \"bahn_de\",\n    \"baidu.com\": \"baidu_ads\",\n    \"baidustatic.com\": \"baidu_ads\",\n    \"bdimg.com\": \"baidu_static\",\n    \"bdstatic.com\": \"baidu_static\",\n    \"baletingo.com\": \"baletingo.com\",\n    \"bangdom.com\": \"bangdom.com\",\n    \"widgets.bankrate.com\": \"bankrate\",\n    \"bannerconnect.net\": \"banner_connect\",\n    \"bannerflow.com\": \"bannerflow.com\",\n    \"bannerplay.com\": \"bannerplay\",\n    \"cdn.bannersnack.com\": \"bannersnack\",\n    \"dn3y71tq7jf07.cloudfront.net\": \"barilliance\",\n    \"getbarometer.s3.amazonaws.com\": \"barometer\",\n    \"basilic.io\": \"basilic.io\",\n    \"batanga.com\": \"batanga_network\",\n    \"t4ft.de\": \"batch_media\",\n    \"bauernative.com\": \"bauer_media\",\n    \"baur.de\": \"baur.de\",\n    \"baynote.net\": \"baynote_observer\",\n    \"bazaarvoice.com\": \"bazaarvoice\",\n    \"bbci.co.uk\": \"bbci\",\n    \"tracking.bd4travel.com\": \"bd4travel\",\n    \"beopinion.com\": \"be_opinion\",\n    \"bfmio.com\": \"beachfront\",\n    \"beaconads.com\": \"beacon_ad_network\",\n    \"beampulse.com\": \"beampulse.com\",\n    \"beanstalkdata.com\": \"beanstalk_data\",\n    \"bebi.com\": \"bebi\",\n    \"beeketing.com\": \"beeketing.com\",\n    \"beeline.ru\": \"beeline.ru\",\n    \"bidr.io\": \"beeswax\",\n    \"tracker.beezup.com\": \"beezup\",\n    \"begun.ru\": \"begun\",\n    \"behavioralengine.com\": \"behavioralengine\",\n    \"belboon.de\": \"belboon_gmbh\",\n    \"cdn.belco.io\": \"belco\",\n    \"belstat.be\": \"belstat\",\n    \"belstat.com\": \"belstat\",\n    \"belstat.de\": \"belstat\",\n    \"belstat.fr\": \"belstat\",\n    \"belstat.nl\": \"belstat\",\n    \"bemobile.ua\": \"bemobile.ua\",\n    \"tag.benchplatform.com\": \"bench_platform\",\n    \"betterttv.net\": \"betterttv\",\n    \"betweendigital.com\": \"betweendigital.com\",\n    \"intencysrv.com\": \"betweendigital.com\",\n    \"bid.run\": \"bid.run\",\n    \"bidgear.com\": \"bidgear\",\n    \"bidswitch.net\": \"bidswitch\",\n    \"exe.bid\": \"bidswitch\",\n    \"bttrack.com\": \"bidtellect\",\n    \"bidtheatre.com\": \"bidtheatre\",\n    \"bidvertiser.com\": \"bidvertiser\",\n    \"bigmobileads.com\": \"big_mobile\",\n    \"bigcommerce.com\": \"bigcommerce.com\",\n    \"bigmir.net\": \"bigmir.net\",\n    \"bigpoint-payment.com\": \"bigpoint\",\n    \"bigpoint.com\": \"bigpoint\",\n    \"bigpoint.net\": \"bigpoint\",\n    \"bpcdn.net\": \"bigpoint\",\n    \"bpsecure.com\": \"bigpoint\",\n    \"bildstatic.de\": \"bild\",\n    \"ad-cdn.bilgin.pro\": \"bilgin_pro\",\n    \"pixel.bilinmedia.net\": \"bilin\",\n    \"bat.r.msn.com\": \"bing_ads\",\n    \"bing.com\": \"bing_ads\",\n    \"bing.net\": \"bing_ads\",\n    \"virtualearth.net\": \"bing_maps\",\n    \"binge.com.au\": \"binge\",\n    \"view.binlayer.com\": \"binlayer\",\n    \"widgets.binotel.com\": \"binotel\",\n    \"esendra.fi\": \"bisnode\",\n    \"bitcoinplus.com\": \"bitcoin_miner\",\n    \"bit.ly\": \"bitly\",\n    \"bitrix.de\": \"bitrix\",\n    \"bitrix.info\": \"bitrix\",\n    \"bitrix.ru\": \"bitrix\",\n    \"bitrix24.com\": \"bitrix\",\n    \"bitrix24.com.br\": \"bitrix\",\n    \"bitwarden.com\": \"bitwarden\",\n    \"traffic.adxprts.com\": \"bizcn\",\n    \"jssr.jd.com\": \"blackdragon\",\n    \"blau.de\": \"blau.de\",\n    \"bnmla.com\": \"blink_new_media\",\n    \"blismedia.com\": \"blis\",\n    \"blogad.com.tw\": \"blogad\",\n    \"blogbang.com\": \"blogbang\",\n    \"www.blogcatalog.com\": \"blogcatalog\",\n    \"track.blogcounter.de\": \"blogcounter\",\n    \"blogfoster.com\": \"blogfoster.com\",\n    \"bloggerads.net\": \"bloggerads\",\n    \"blogher.com\": \"blogher\",\n    \"blogherads.com\": \"blogher\",\n    \"blogimg.jp\": \"blogimg.jp\",\n    \"blogsmithmedia.com\": \"blogsmithmedia.com\",\n    \"blogblog.com\": \"blogspot_com\",\n    \"blogger.com\": \"blogspot_com\",\n    \"blogspot.com\": \"blogspot_com\",\n    \"brcdn.com\": \"bloomreach\",\n    \"brsrvr.com\": \"bloomreach\",\n    \"brtstats.com\": \"bloomreach\",\n    \"offerpoint.net\": \"blue_cherry_group\",\n    \"blueserving.com\": \"blue_seed\",\n    \"blueconic.net\": \"blueconic.net\",\n    \"bluecore.com\": \"bluecore\",\n    \"triggeredmail.appspot.com\": \"bluecore\",\n    \"bkrtx.com\": \"bluekai\",\n    \"bluekai.com\": \"bluekai\",\n    \"adrevolver.com\": \"bluelithium\",\n    \"bluelithium.com\": \"bluelithium\",\n    \"bmmetrix.com\": \"bluemetrix\",\n    \"japanmetrix.jp\": \"bluemetrix\",\n    \"bluenewsupdate.info\": \"bluenewsupdate.info\",\n    \"bluestreak.com\": \"bluestreak\",\n    \"bluetriangletech.com\": \"bluetriangle\",\n    \"btttag.com\": \"bluetriangle\",\n    \"bodelen.com\": \"bodelen.com\",\n    \"tracking.bol.com\": \"bol_affiliate_program\",\n    \"qb.boldapps.net\": \"bold\",\n    \"secure.apps.shappify.com\": \"bold\",\n    \"boldchat.com\": \"boldchat\",\n    \"boltdns.net\": \"boltdns.net\",\n    \"bom.gov.au\": \"bom\",\n    \"ml314.com\": \"bombora\",\n    \"bongacams.com\": \"bongacams.com\",\n    \"bonial.com\": \"bonial\",\n    \"bonialconnect.com\": \"bonial\",\n    \"bonialserviceswidget.de\": \"bonial\",\n    \"boo-box.com\": \"boo-box\",\n    \"booking.com\": \"booking.com\",\n    \"bstatic.com\": \"booking.com\",\n    \"boostbox.com.br\": \"boost_box\",\n    \"boostervideo.ru\": \"booster_video\",\n    \"bootstrapcdn.com\": \"bootstrap\",\n    \"borrango.com\": \"borrango.com\",\n    \"scan.botscanner.com\": \"botscanner\",\n    \"boudja.com\": \"boudja.com\",\n    \"bounceexchange.com\": \"bounce_exchange\",\n    \"bouncex.com\": \"bouncex\",\n    \"bouncex.net\": \"bouncex\",\n    \"j.clickdensity.com\": \"box_uk\",\n    \"boxever.com\": \"boxever\",\n    \"brainient.com\": \"brainient\",\n    \"brainsins.com\": \"brainsins\",\n    \"d2xkqxdy6ewr93.cloudfront.net\": \"brainsins\",\n    \"mobileapptracking.com\": \"branch\",\n    \"app.link\": \"branch_metrics\",\n    \"branch.io\": \"branch_metrics\",\n    \"brandaffinity.net\": \"brand_affinity\",\n    \"go.cpmadvisors.com\": \"brand_networks\",\n    \"optorb.com\": \"brand_networks\",\n    \"brandmetrics.com\": \"brandmetrics.com\",\n    \"brandreachsys.com\": \"brandreach\",\n    \"rtbidder.net\": \"brandscreen\",\n    \"brandwire.tv\": \"brandwire.tv\",\n    \"branica.com\": \"branica\",\n    \"appboycdn.com\": \"braze\",\n    \"braze.com\": \"braze\",\n    \"brealtime.com\": \"brealtime\",\n    \"bridgetrack.com\": \"bridgetrack\",\n    \"brightcove.com\": \"brightcove\",\n    \"brightcove.net\": \"brightcove_player\",\n    \"analytics.brightedge.com\": \"brightedge\",\n    \"munchkin.brightfunnel.com\": \"brightfunnel\",\n    \"brightonclick.com\": \"brightonclick.com\",\n    \"btrll.com\": \"brightroll\",\n    \"p.brilig.com\": \"brilig\",\n    \"brillen.de\": \"brillen.de\",\n    \"broadstreetads.com\": \"broadstreet\",\n    \"bm23.com\": \"bronto\",\n    \"brow.si\": \"brow.si\",\n    \"browser-statistik.de\": \"browser-statistik\",\n    \"browser-update.org\": \"browser_update\",\n    \"btncdn.com\": \"btncdn.com\",\n    \"in.bubblestat.com\": \"bubblestat\",\n    \"brighteroption.com\": \"buddy_media\",\n    \"bufferapp.com\": \"buffer_button\",\n    \"bugherd.com\": \"bugherd.com\",\n    \"bugsnag.com\": \"bugsnag\",\n    \"d2wy8f7a9ursnm.cloudfront.net\": \"bugsnag\",\n    \"bulkhentai.com\": \"bulkhentai.com\",\n    \"bumlam.com\": \"bumlam.com\",\n    \"bunchbox.co\": \"bunchbox\",\n    \"bf-ad.net\": \"burda\",\n    \"bf-tools.net\": \"burda\",\n    \"bstatic.de\": \"burda_digital_systems\",\n    \"burstbeacon.com\": \"burst_media\",\n    \"burstnet.com\": \"burst_media\",\n    \"burt.io\": \"burt\",\n    \"d3q6px0y2suh5n.cloudfront.net\": \"burt\",\n    \"rich-agent.s3.amazonaws.com\": \"burt\",\n    \"richmetrics.com\": \"burt\",\n    \"stats.businessol.com\": \"businessonline_analytics\",\n    \"bttn.io\": \"button\",\n    \"buysellads.com\": \"buysellads\",\n    \"servedby-buysellads.com\": \"buysellads\",\n    \"buzzadexchange.com\": \"buzzadexchange.com\",\n    \"buzzador.com\": \"buzzador\",\n    \"buzzfed.com\": \"buzzfeed\",\n    \"bwbx.io\": \"bwbx.io\",\n    \"bypass.jp\": \"bypass\",\n    \"c1exchange.com\": \"c1_exchange\",\n    \"c3metrics.com\": \"c3_metrics\",\n    \"c3tag.com\": \"c3_metrics\",\n    \"c8.net.ua\": \"c8_network\",\n    \"cackle.me\": \"cackle.me\",\n    \"d1cerpgff739r9.cloudfront.net\": \"cadreon\",\n    \"d1qpxk1wfeh8v1.cloudfront.net\": \"cadreon\",\n    \"callpage.io\": \"call_page\",\n    \"callbackhunter.com\": \"callbackhunter\",\n    \"callmeasurement.com\": \"callbox\",\n    \"callibri.ru\": \"callibri\",\n    \"callrail.com\": \"callrail\",\n    \"calltracking.ru\": \"calltracking\",\n    \"caltat.com\": \"caltat.com\",\n    \"cam-content.com\": \"cam-content.com\",\n    \"camakaroda.com\": \"camakaroda.com\",\n    \"s.edkay.com\": \"campus_explorer\",\n    \"canddi.com\": \"canddi\",\n    \"canonical.com\": \"canonical\",\n    \"canvas.net\": \"canvas\",\n    \"canvasnetwork.com\": \"canvas\",\n    \"du11hjcvx0uqb.cloudfront.net\": \"canvas\",\n    \"kdata.fr\": \"capitaldata\",\n    \"captora.com\": \"captora\",\n    \"edge.capturemedia.network\": \"capture_media\",\n    \"cdn.capturly.com\": \"capturly\",\n    \"route.carambo.la\": \"carambola\",\n    \"carbonads.com\": \"carbonads\",\n    \"carbonads.net\": \"carbonads\",\n    \"fusionads.net\": \"carbonads\",\n    \"cardinalcommerce.com\": \"cardinal\",\n    \"cardlytics.com\": \"cardlytics\",\n    \"cdn.carrotquest.io\": \"carrot_quest\",\n    \"api.cartstack.com\": \"cartstack\",\n    \"caspion.com\": \"caspion\",\n    \"t.castle.io\": \"castle\",\n    \"3gl.net\": \"catchpoint\",\n    \"cbox.ws\": \"cbox\",\n    \"adlog.com.com\": \"cbs_interactive\",\n    \"cbsinteractive.com\": \"cbs_interactive\",\n    \"dw.com.com\": \"cbs_interactive\",\n    \"ccmbg.com\": \"ccm_benchmark\",\n    \"admission.net\": \"cdk_digital_marketing\",\n    \"cdn-net.com\": \"cdn-net.com\",\n    \"cdn13.com\": \"cdn13.com\",\n    \"cdn77.com\": \"cdn77\",\n    \"cdn77.org\": \"cdn77\",\n    \"cdnetworks.com\": \"cdnetworks.net\",\n    \"cdnetworks.net\": \"cdnetworks.net\",\n    \"cdnnetwok.xyz\": \"cdnnetwok_xyz\",\n    \"cdnondemand.org\": \"cdnondemand.org\",\n    \"cdnsure.com\": \"cdnsure.com\",\n    \"cdnvideo.com\": \"cdnvideo.com\",\n    \"cdnwidget.com\": \"cdnwidget.com\",\n    \"cedexis-radar.net\": \"cedexis_radar\",\n    \"cedexis-test.com\": \"cedexis_radar\",\n    \"cedexis.com\": \"cedexis_radar\",\n    \"cedexis.fastlylb.net\": \"cedexis_radar\",\n    \"cedexis.net\": \"cedexis_radar\",\n    \"celebrus.com\": \"celebrus\",\n    \"celtra.com\": \"celtra\",\n    \"cendyn.adtrack.calls.net\": \"cendyn\",\n    \"centraltag.com\": \"centraltag\",\n    \"brand-server.com\": \"centro\",\n    \"speed-trap.nl\": \"cerberus_speed-trap\",\n    \"link.ixs1.net\": \"certainsource\",\n    \"hits.e.cl\": \"certifica_metric\",\n    \"certona.net\": \"certona\",\n    \"res-x.com\": \"certona\",\n    \"gsn.chameleon.ad\": \"chameleon\",\n    \"chango.ca\": \"chango\",\n    \"chango.com\": \"chango\",\n    \"channelintelligence.com\": \"channel_intelligence\",\n    \"cptrack.de\": \"channel_pilot_solutions\",\n    \"channeladvisor.com\": \"channeladvisor\",\n    \"searchmarketing.com\": \"channeladvisor\",\n    \"channelfinder.net\": \"channelfinder\",\n    \"chaordicsystems.com\": \"chaordic\",\n    \"chartbeat.com\": \"chartbeat\",\n    \"chartbeat.net\": \"chartbeat\",\n    \"chartboost.com\": \"chartboost\",\n    \"chaser.ru\": \"chaser\",\n    \"cloud.chatbeacon.io\": \"chat_beacon\",\n    \"chatango.com\": \"chatango\",\n    \"call.chatra.io\": \"chatra\",\n    \"chaturbate.com\": \"chaturbate.com\",\n    \"chatwing.com\": \"chatwing\",\n    \"checkmystats.com.au\": \"checkmystats\",\n    \"chefkoch-cdn.de\": \"chefkoch_de\",\n    \"chefkoch.de\": \"chefkoch_de\",\n    \"tracker.chinmedia.vn\": \"chin_media\",\n    \"chinesean.com\": \"chinesean\",\n    \"chitika.net\": \"chitika\",\n    \"choicestream.com\": \"choicestream\",\n    \"api.getchute.com\": \"chute\",\n    \"media.chute.io\": \"chute\",\n    \"iqcontentplatform.de\": \"circit\",\n    \"data.circulate.com\": \"circulate\",\n    \"p.cityspark.com\": \"city_spark\",\n    \"cityads.ru\": \"cityads\",\n    \"gameleads.ru\": \"cityads\",\n    \"ciuvo.com\": \"ciuvo.com\",\n    \"widget.civey.com\": \"civey_widgets\",\n    \"civicscience.com\": \"civicscience.com\",\n    \"ciweb.ciwebgroup.com\": \"ciwebgroup\",\n    \"clcknads.pro\": \"clcknads.pro\",\n    \"pulseradius.com\": \"clear_pier\",\n    \"clearbit.com\": \"clearbit.com\",\n    \"clearsale.com.br\": \"clearsale\",\n    \"tag.clrstm.com\": \"clearstream.tv\",\n    \"api.clerk.io\": \"clerk.io\",\n    \"cleverpush.com\": \"clever_push\",\n    \"wzrkt.com\": \"clever_tap\",\n    \"cleversite.ru\": \"cleversite\",\n    \"script.click360.io\": \"click360\",\n    \"clickandchat.com\": \"click_and_chat\",\n    \"software.clickback.com\": \"click_back\",\n    \"hit.clickaider.com\": \"clickaider\",\n    \"clickaine.com\": \"clickaine\",\n    \"clickbank.net\": \"clickbank\",\n    \"cbproads.com\": \"clickbank_proads\",\n    \"adtoll.com\": \"clickbooth\",\n    \"clickbooth.com\": \"clickbooth\",\n    \"clickboothlnk.com\": \"clickbooth\",\n    \"clickcease.com\": \"clickcease\",\n    \"clickcertain.com\": \"clickcertain\",\n    \"remarketstats.com\": \"clickcertain\",\n    \"clickdesk.com\": \"clickdesk\",\n    \"analytics.clickdimensions.com\": \"clickdimensions\",\n    \"clickequations.net\": \"clickequations\",\n    \"clickexperts.net\": \"clickexperts\",\n    \"doublemax.net\": \"clickforce\",\n    \"clickinc.com\": \"clickinc\",\n    \"clickintext.net\": \"clickintext\",\n    \"clickky.biz\": \"clickky\",\n    \"9nl.be\": \"clickmeter\",\n    \"9nl.com\": \"clickmeter\",\n    \"9nl.eu\": \"clickmeter\",\n    \"9nl.it\": \"clickmeter\",\n    \"9nl.me\": \"clickmeter\",\n    \"clickmeter.com\": \"clickmeter\",\n    \"clickonometrics.pl\": \"clickonometrics\",\n    \"clickpoint.com\": \"clickpoint\",\n    \"clickpoint.it\": \"clickpoint\",\n    \"clickprotector.com\": \"clickprotector\",\n    \"clickreport.com\": \"clickreport\",\n    \"doogleonduty.com\": \"clickreport\",\n    \"ctn.go2cloud.org\": \"clicks_thru_networks\",\n    \"clicksor.com\": \"clicksor\",\n    \"hatid.com\": \"clicksor\",\n    \"lzjl.com\": \"clicksor\",\n    \"myroitracking.com\": \"clicksor\",\n    \"clicktale.com\": \"clicktale\",\n    \"clicktale.net\": \"clicktale\",\n    \"clicktale.pantherssl.com\": \"clicktale\",\n    \"clicktalecdn.sslcs.cdngc.net\": \"clicktale\",\n    \"clicktripz.com\": \"clicktripz\",\n    \"clickwinks.com\": \"clickwinks\",\n    \"getclicky.com\": \"clicky\",\n    \"staticstuff.net\": \"clicky\",\n    \"clickyab.com\": \"clickyab\",\n    \"clicmanager.fr\": \"clicmanager\",\n    \"eplayer.clipsyndicate.com\": \"clip_syndicate\",\n    \"www.is1.clixgalore.com\": \"clixgalore\",\n    \"clixmetrix.com\": \"clixmetrix\",\n    \"clixsense.com\": \"clixsense\",\n    \"cloud-media.fr\": \"cloud-media.fr\",\n    \"cloudflare.com\": \"cloudflare\",\n    \"cloudflare.net\": \"cloudflare\",\n    \"cloudflare-dm-cmpimg.com\": \"cloudflare\",\n    \"cloudflare-dns.com\": \"cloudflare\",\n    \"cloudflare-ipfs.com\": \"cloudflare\",\n    \"cloudflare-quic.com\": \"cloudflare\",\n    \"cloudflare-terms-of-service-abuse.com\": \"cloudflare\",\n    \"cloudflare.tv\": \"cloudflare\",\n    \"cloudflareaccess.com\": \"cloudflare\",\n    \"cloudflareclient.com\": \"cloudflare\",\n    \"cloudflareinsights.com\": \"cloudflare\",\n    \"cloudflareok.com\": \"cloudflare\",\n    \"cloudflareportal.com\": \"cloudflare\",\n    \"cloudflareresolve.com\": \"cloudflare\",\n    \"cloudflaressl.com\": \"cloudflare\",\n    \"cloudflarestatus.com\": \"cloudflare\",\n    \"cloudflarestream.com\": \"cloudflare\",\n    \"pacloudflare.com\": \"cloudflare\",\n    \"sn-cloudflare.com\": \"cloudflare\",\n    \"videodelivery.net\": \"cloudflare\",\n    \"cloudimg.io\": \"cloudimage.io\",\n    \"cloudinary.com\": \"cloudinary\",\n    \"clovenetwork.com\": \"clove_network\",\n    \"clustrmaps.com\": \"clustrmaps\",\n    \"cnbc.com\": \"cnbc\",\n    \"cnetcontent.com\": \"cnetcontent.com\",\n    \"cnstats.ru\": \"cnstats\",\n    \"cnzz.com\": \"cnzz.com\",\n    \"umeng.com\": \"cnzz.com\",\n    \"acc-hd.de\": \"coadvertise\",\n    \"client.cobrowser.net\": \"cobrowser\",\n    \"codeonclick.com\": \"codeonclick.com\",\n    \"cogocast.net\": \"cogocast\",\n    \"coin-have.com\": \"coin_have\",\n    \"appsha1.cointraffic.io\": \"coin_traffic\",\n    \"authedmine.com\": \"coinhive\",\n    \"coin-hive.com\": \"coinhive\",\n    \"coinhive.com\": \"coinhive\",\n    \"coinurl.com\": \"coinurl\",\n    \"coll1onf.com\": \"coll1onf.com\",\n    \"coll2onf.com\": \"coll2onf.com\",\n    \"service.collarity.com\": \"collarity\",\n    \"static.clmbtech.com\": \"columbia_online\",\n    \"combotag.com\": \"combotag\",\n    \"pdk.theplatform.com\": \"comcast_technology_solutions\",\n    \"theplatform.com\": \"comcast_technology_solutions\",\n    \"comm100.cn\": \"comm100\",\n    \"comm100.com\": \"comm100\",\n    \"cdn-cs.com\": \"commerce_sciences\",\n    \"cdn.mercent.com\": \"commercehub\",\n    \"link.mercent.com\": \"commercehub\",\n    \"commercialvalue.org\": \"commercialvalue.org\",\n    \"afcyhf.com\": \"commission_junction\",\n    \"anrdoezrs.net\": \"commission_junction\",\n    \"apmebf.com\": \"commission_junction\",\n    \"awltovhc.com\": \"commission_junction\",\n    \"emjcd.com\": \"commission_junction\",\n    \"ftjcfx.com\": \"commission_junction\",\n    \"lduhtrp.net\": \"commission_junction\",\n    \"qksz.net\": \"commission_junction\",\n    \"tkqlhce.com\": \"commission_junction\",\n    \"tqlkg.com\": \"commission_junction\",\n    \"yceml.net\": \"commission_junction\",\n    \"communicatorcorp.com\": \"communicator_corp\",\n    \"wowanalytics.co.uk\": \"communigator\",\n    \"c-col.com\": \"competexl\",\n    \"c.compete.com\": \"competexl\",\n    \"complex.com\": \"complex_media_network\",\n    \"complexmedianetwork.com\": \"complex_media_network\",\n    \"comprigo.com\": \"comprigo\",\n    \"comscore.com\": \"comscore\",\n    \"zqtk.net\": \"comscore\",\n    \"conative.de\": \"conative.de\",\n    \"condenast.com\": \"condenastdigital.com\",\n    \"conduit-banners.com\": \"conduit\",\n    \"conduit-data.com\": \"conduit\",\n    \"conduit.com\": \"conduit\",\n    \"confirmit.com\": \"confirmit\",\n    \"congstar.de\": \"congstar.de\",\n    \"connatix.com\": \"connatix.com\",\n    \"connected-by.connectad.io\": \"connectad\",\n    \"cdn.connecto.io\": \"connecto\",\n    \"connexity.net\": \"connexity\",\n    \"cxt.ms\": \"connexity\",\n    \"connextra.com\": \"connextra\",\n    \"rs6.net\": \"constant_contact\",\n    \"serverbid.com\": \"consumable\",\n    \"contactatonce.com\": \"contact_at_once\",\n    \"adrolays.de\": \"contact_impact\",\n    \"c-i.as\": \"contact_impact\",\n    \"df-srv.de\": \"contact_impact\",\n    \"d1uwd25yvxu96k.cloudfront.net\": \"contactme\",\n    \"static.contactme.com\": \"contactme\",\n    \"contaxe.com\": \"contaxe\",\n    \"content.ad\": \"content.ad\",\n    \"ingestion.contentinsights.com\": \"content_insights\",\n    \"contentexchange.me\": \"contentexchange.me\",\n    \"ctfassets.net\": \"contentful_gmbh\",\n    \"contentpass.de\": \"contentpass\",\n    \"contentpass.net\": \"contentpass\",\n    \"contentsquare.net\": \"contentsquare.net\",\n    \"d1aug3dv5magti.cloudfront.net\": \"contentwrx\",\n    \"d39se0h2uvfakd.cloudfront.net\": \"contentwrx\",\n    \"c-on-text.com\": \"context\",\n    \"intext.contextad.pl\": \"context.ad\",\n    \"continum.net\": \"continum_net\",\n    \"s2.contribusourcesyndication.com\": \"contribusource\",\n    \"hits.convergetrack.com\": \"convergetrack\",\n    \"fastclick.net\": \"conversant\",\n    \"mediaplex.com\": \"conversant\",\n    \"mplxtms.com\": \"conversant\",\n    \"cm-commerce.com\": \"conversio\",\n    \"media.conversio.com\": \"conversio\",\n    \"c.conversionlogic.net\": \"conversion_logic\",\n    \"conversionruler.com\": \"conversionruler\",\n    \"conversionsbox.com\": \"conversions_box\",\n    \"conversionsondemand.com\": \"conversions_on_demand\",\n    \"ant.conversive.nl\": \"conversive\",\n    \"convertexperiments.com\": \"convert\",\n    \"d3sjgucddk68ji.cloudfront.net\": \"convertfox\",\n    \"convertro.com\": \"convertro\",\n    \"d1ivexoxmp59q7.cloudfront.net\": \"convertro\",\n    \"conviva.com\": \"conviva\",\n    \"cookieconsent.silktide.com\": \"cookie_consent\",\n    \"cookie-script.com\": \"cookie_script\",\n    \"cookiebot.com\": \"cookiebot\",\n    \"cookieq.com\": \"cookieq\",\n    \"lite.piclens.com\": \"cooliris\",\n    \"copacet.com\": \"copacet\",\n    \"raasnet.com\": \"coreaudience\",\n    \"coremotives.com\": \"coremotives\",\n    \"coull.com\": \"coull\",\n    \"cpmrocket.com\": \"cpm_rocket\",\n    \"cpmprofit.com\": \"cpmprofit\",\n    \"cpmstar.com\": \"cpmstar\",\n    \"captifymedia.com\": \"cpx.to\",\n    \"cpx.to\": \"cpx.to\",\n    \"cqcounter.com\": \"cq_counter\",\n    \"cqq5id8n.com\": \"cqq5id8n.com\",\n    \"cquotient.com\": \"cquotient.com\",\n    \"craftkeys.com\": \"craftkeys\",\n    \"ads.crakmedia.com\": \"crakmedia_network\",\n    \"craktraffic.com\": \"crakmedia_network\",\n    \"crankyads.com\": \"crankyads\",\n    \"crashlytics.com\": \"crashlytics\",\n    \"cetrk.com\": \"crazy_egg\",\n    \"crazyegg.com\": \"crazy_egg\",\n    \"dnn506yrbagrg.cloudfront.net\": \"crazy_egg\",\n    \"creafi-online-media.com\": \"creafi\",\n    \"createjs.com\": \"createjs\",\n    \"creativecommons.org\": \"creative_commons\",\n    \"brandwatch.com\": \"crimsonhexagon_com\",\n    \"crimsonhexagon.com\": \"crimsonhexagon_com\",\n    \"hexagon-analytics.com\": \"crimsonhexagon_com\",\n    \"ctnsnet.com\": \"crimtan\",\n    \"crisp.chat\": \"crisp\",\n    \"crisp.im\": \"crisp\",\n    \"criteo.com\": \"criteo\",\n    \"criteo.net\": \"criteo\",\n    \"p.crm4d.com\": \"crm4d\",\n    \"crossengage.io\": \"crossengage\",\n    \"crosspixel.net\": \"crosspixel\",\n    \"crsspxl.com\": \"crosspixel\",\n    \"crosssell.info\": \"crosssell.info\",\n    \"crossss.com\": \"crossss\",\n    \"widget.crowdignite.com\": \"crowd_ignite\",\n    \"static.crowdscience.com\": \"crowd_science\",\n    \"ss.crowdprocess.com\": \"crowdprocess\",\n    \"our.glossip.nl\": \"crowdynews\",\n    \"widget.breakingburner.com\": \"crowdynews\",\n    \"widget.crowdynews.com\": \"crowdynews\",\n    \"searchg2.crownpeak.net\": \"crownpeak\",\n    \"snippet.omm.crownpeak.com\": \"crownpeak\",\n    \"cryptoloot.pro\": \"cryptoloot_miner\",\n    \"ctnetwork.hu\": \"ctnetwork\",\n    \"adzhub.com\": \"ctrlshift\",\n    \"data.withcubed.com\": \"cubed\",\n    \"cuelinks.com\": \"cuelinks\",\n    \"cdn.cupinteractive.com\": \"cup_interactive\",\n    \"curse.com\": \"curse.com\",\n    \"cursecdn.com\": \"cursecdn.com\",\n    \"assets.customer.io\": \"customer.io\",\n    \"widget.customerly.io\": \"customerly\",\n    \"cxense.com\": \"cxense\",\n    \"cxo.name\": \"cxo.name\",\n    \"cyberwing.co.jp\": \"cyber_wing\",\n    \"cybersource.com\": \"cybersource\",\n    \"cygnus.com\": \"cygnus\",\n    \"da-ads.com\": \"da-ads.com\",\n    \"dailymail.co.uk\": \"dailymail.co.uk\",\n    \"dailymotion.com\": \"dailymotion\",\n    \"dailymotionbus.com\": \"dailymotion\",\n    \"dm-event.net\": \"dailymotion\",\n    \"dmcdn.net\": \"dailymotion\",\n    \"dmxleo.com\": \"dailymotion_advertising\",\n    \"ds1.nl\": \"daisycon\",\n    \"dantrack.net\": \"dantrack.net\",\n    \"dmclick.cn\": \"darwin_marketing\",\n    \"tags.dashboardad.net\": \"dashboard_ad\",\n    \"datacaciques.com\": \"datacaciques.com\",\n    \"datacoral.com\": \"datacoral\",\n    \"abandonaid.com\": \"datacrushers\",\n    \"datacrushers.com\": \"datacrushers\",\n    \"datadome.co\": \"datadome\",\n    \"optimahub.com\": \"datalicious_datacollector\",\n    \"supert.ag\": \"datalicious_supertag\",\n    \"inextaction.net\": \"datalogix\",\n    \"nexac.com\": \"datalogix\",\n    \"datamind.ru\": \"datamind.ru\",\n    \"datatables.net\": \"datatables\",\n    \"adunits.datawrkz.com\": \"datawrkz\",\n    \"dataxpand.script.ag\": \"dataxpand\",\n    \"tc.dataxpand.com\": \"dataxpand\",\n    \"w55c.net\": \"dataxu\",\n    \"datds.net\": \"datds.net\",\n    \"pro-market.net\": \"datonics\",\n    \"displaymarketplace.com\": \"datran\",\n    \"davebestdeals.com\": \"davebestdeals.com\",\n    \"dawandastatic.com\": \"dawandastatic.com\",\n    \"dc-storm.com\": \"dc_stormiq\",\n    \"h4k5.com\": \"dc_stormiq\",\n    \"stormcontainertag.com\": \"dc_stormiq\",\n    \"stormiq.com\": \"dc_stormiq\",\n    \"dcbap.com\": \"dcbap.com\",\n    \"dcmn.com\": \"dcmn.com\",\n    \"statslogger.rocket.persgroep.cloud\": \"de_persgroep\",\n    \"deadlinefunnel.com\": \"deadline_funnel\",\n    \"cc2.dealer.com\": \"dealer.com\",\n    \"d9lq0o81skkdj.cloudfront.net\": \"dealer.com\",\n    \"esm1.net\": \"dealer.com\",\n    \"static.dealer.com\": \"dealer.com\",\n    \"decibelinsight.net\": \"decibel_insight\",\n    \"ads.dedicatedmedia.com\": \"dedicated_media\",\n    \"api.deep.bi\": \"deep.bi\",\n    \"deepintent.com\": \"deepintent.com\",\n    \"defpush.com\": \"defpush.com\",\n    \"deichmann.com\": \"deichmann.com\",\n    \"vxml4.delacon.com.au\": \"delacon\",\n    \"tracking.percentmobile.com\": \"delivr\",\n    \"adaction.se\": \"delta_projects\",\n    \"de17a.com\": \"delta_projects\",\n    \"deluxe.script.ag\": \"deluxe\",\n    \"delvenetworks.com\": \"delve_networks\",\n    \"company-target.com\": \"demandbase\",\n    \"demandbase.com\": \"demandbase\",\n    \"dmd53.com\": \"demandmedia\",\n    \"dmtracker.com\": \"demandmedia\",\n    \"deqwas.net\": \"deqwas\",\n    \"devatics.com\": \"devatics\",\n    \"developermedia.com\": \"developer_media\",\n    \"dapxl.com\": \"deviantart.net\",\n    \"deviantart.net\": \"deviantart.net\",\n    \"my.blueadvertise.com\": \"dex_platform\",\n    \"dgm-au.com\": \"dgm\",\n    \"s2d6.com\": \"dgm\",\n    \"d31y97ze264gaa.cloudfront.net\": \"dialogtech\",\n    \"d3von6il1wr7wo.cloudfront.net\": \"dianomi\",\n    \"dianomi.com\": \"dianomi\",\n    \"dianomioffers.co.uk\": \"dianomi\",\n    \"tag.didit.com\": \"didit_blizzard\",\n    \"track.did-it.com\": \"didit_maestro\",\n    \"privacy-center.org\": \"didomi\",\n    \"digg.com\": \"digg_widget\",\n    \"digicert.com\": \"digicert_trust_seal\",\n    \"phicdn.net\": \"digicert_trust_seal\",\n    \"digidip.net\": \"digidip\",\n    \"digiglitzmarketing.go2cloud.org\": \"digiglitz\",\n    \"wtp101.com\": \"digilant\",\n    \"digioh.com\": \"digioh\",\n    \"lightboxcdn.com\": \"digioh\",\n    \"digitalgov.gov\": \"digital.gov\",\n    \"cookiereports.com\": \"digital_control_room\",\n    \"adtag.cc\": \"digital_nomads\",\n    \"adready.com\": \"digital_remedy\",\n    \"adreadytractions.com\": \"digital_remedy\",\n    \"cpxinteractive.com\": \"digital_remedy\",\n    \"directtrack.com\": \"digital_river\",\n    \"onenetworkdirect.net\": \"digital_river\",\n    \"track.digitalriver.com\": \"digital_river\",\n    \"dwin1.com\": \"digital_window\",\n    \"digiteka.net\": \"digiteka\",\n    \"ultimedia.com\": \"digiteka\",\n    \"digitru.st\": \"digitrust\",\n    \"widget.dihitt.com.br\": \"dihitt_badge\",\n    \"dimml.io\": \"dimml\",\n    \"keywordsconnect.com\": \"direct_keyword_link\",\n    \"directadvert.ru\": \"directadvert\",\n    \"directrev.com\": \"directrev\",\n    \"discordapp.com\": \"discord\",\n    \"disneyplus.com\": \"disneyplus\",\n    \"bamgrid.com\": \"disneystreaming\",\n    \"dssedge.com\": \"disneystreaming\",\n    \"dssott.com\": \"disneystreaming\",\n    \"d81mfvml8p5ml.cloudfront.net\": \"display_block\",\n    \"disqus.com\": \"disqus\",\n    \"disquscdn.com\": \"disqus\",\n    \"disqusads.com\": \"disqus_ads\",\n    \"distiltag.com\": \"distil_tag\",\n    \"districtm.ca\": \"districtm.io\",\n    \"districtm.io\": \"districtm.io\",\n    \"jsrdn.com\": \"distroscale\",\n    \"div.show\": \"div.show\",\n    \"stats.vertriebsassistent.de\": \"diva\",\n    \"tag.divvit.com\": \"divvit\",\n    \"d-msquared.com\": \"dm2\",\n    \"and.co.uk\": \"dmg_media\",\n    \"dmm.co.jp\": \"dmm\",\n    \"ctret.de\": \"dmwd\",\n    \"toolbar.dockvine.com\": \"dockvine\",\n    \"awecr.com\": \"docler\",\n    \"fwbntw.com\": \"docler\",\n    \"s.dogannet.tv\": \"dogannet\",\n    \"domain.glass\": \"domainglass\",\n    \"www.domodomain.com\": \"domodomain\",\n    \"donation-tools.org\": \"donationtools\",\n    \"doofinder.com\": \"doofinder.com\",\n    \"embed.doorbell.io\": \"doorbell.io\",\n    \"dotandad.com\": \"dotandmedia\",\n    \"trackedlink.net\": \"dotmailer\",\n    \"dotmetrics.net\": \"dotmetrics.net\",\n    \"dotomi.com\": \"dotomi\",\n    \"dtmc.com\": \"dotomi\",\n    \"dtmpub.com\": \"dotomi\",\n    \"double.net\": \"double.net\",\n    \"2mdn.net\": \"doubleclick\",\n    \"doublepimp.com\": \"doublepimp\",\n    \"doublepimpssl.com\": \"doublepimp\",\n    \"redcourtside.com\": \"doublepimp\",\n    \"xeontopa.com\": \"doublepimp\",\n    \"zerezas.com\": \"doublepimp\",\n    \"doubleverify.com\": \"doubleverify\",\n    \"wrating.com\": \"dratio\",\n    \"adsymptotic.com\": \"drawbridge\",\n    \"dreame.tech\": \"dreame_tech\",\n    \"dreametech.com\": \"dreame_tech\",\n    \"dreamlab.pl\": \"dreamlab.pl\",\n    \"drift.com\": \"drift\",\n    \"js.driftt.com\": \"drift\",\n    \"getdrip.com\": \"drip\",\n    \"dropbox.com\": \"dropbox.com\",\n    \"dropboxstatic.com\": \"dropbox.com\",\n    \"z5x.net\": \"dsnr_media_group\",\n    \"dsp-rambler.ru\": \"dsp_rambler\",\n    \"m6d.com\": \"dstillery\",\n    \"media6degrees.com\": \"dstillery\",\n    \"dtscout.com\": \"dtscout.com\",\n    \"dd-cdn.multiscreensite.com\": \"dudamobile\",\n    \"px.multiscreensite.com\": \"dudamobile\",\n    \"cdn-0.d41.co\": \"dun_and_bradstreet\",\n    \"cn01.dwstat.cn\": \"dwstat.cn\",\n    \"dynad.net\": \"dynad\",\n    \"dyntrk.com\": \"dynadmic\",\n    \"dyntracker.de\": \"dynamic_1001_gmbh\",\n    \"media01.eu\": \"dynamic_1001_gmbh\",\n    \"content.dl-rms.com\": \"dynamic_logic\",\n    \"dlqm.net\": \"dynamic_logic\",\n    \"questionmarket.com\": \"dynamic_logic\",\n    \"dynamicyield.com\": \"dynamic_yield\",\n    \"beacons.hottraffic.nl\": \"dynata\",\n    \"dynatrace.com\": \"dynatrace.com\",\n    \"dyncdn.me\": \"dyncdn.me\",\n    \"e-planning.net\": \"e-planning\",\n    \"eadv.it\": \"eadv\",\n    \"eanalyzer.de\": \"eanalyzer.de\",\n    \"early-birds.fr\": \"early_birds\",\n    \"cdn.earnify.com\": \"earnify\",\n    \"earnify.com\": \"earnify_tracker\",\n    \"easyads.bg\": \"easyads\",\n    \"easylist.club\": \"easylist_club\",\n    \"classistatic.de\": \"ebay\",\n    \"ebay-us.com\": \"ebay\",\n    \"ebay.com\": \"ebay\",\n    \"ebay.de\": \"ebay\",\n    \"ebayclassifiedsgroup.com\": \"ebay\",\n    \"ebaycommercenetwork.com\": \"ebay\",\n    \"ebaydesc.com\": \"ebay\",\n    \"ebayimg.com\": \"ebay\",\n    \"ebayrtm.com\": \"ebay\",\n    \"ebaystatic.com\": \"ebay\",\n    \"ad.about.co.kr\": \"ebay_korea\",\n    \"adcheck.about.co.kr\": \"ebay_korea\",\n    \"adn.ebay.com\": \"ebay_partner_network\",\n    \"beead.co.uk\": \"ebuzzing\",\n    \"beead.fr\": \"ebuzzing\",\n    \"beead.net\": \"ebuzzing\",\n    \"ebuzzing.com\": \"ebuzzing\",\n    \"ebz.io\": \"ebuzzing\",\n    \"echoenabled.com\": \"echo\",\n    \"eclick.vn\": \"eclick\",\n    \"econda-monitor.de\": \"econda\",\n    \"eco-tag.jp\": \"ecotag\",\n    \"alphacdn.net\": \"edgio\",\n    \"edg.io\": \"edgio\",\n    \"edgecast.com\": \"edgio\",\n    \"edgecastcdn.net\": \"edgio\",\n    \"edgecastdns.net\": \"edgio\",\n    \"sigmacdn.net\": \"edgio\",\n    \"ecustomeropinions.com\": \"edigitalresearch\",\n    \"effectivemeasure.net\": \"effective_measure\",\n    \"effiliation.com\": \"effiliation\",\n    \"egain.net\": \"egain\",\n    \"cloud-emea.analytics-egain.com\": \"egain_analytics\",\n    \"ehi-siegel.de\": \"ehi-siegel_de\",\n    \"ekmpinpoint.com\": \"ekmpinpoint\",\n    \"ekomi.de\": \"ekomi\",\n    \"elasticad.net\": \"elastic_ad\",\n    \"elasticbeanstalk.com\": \"elastic_beanstalk\",\n    \"cloudcell.com\": \"electronic_arts\",\n    \"ea.com\": \"electronic_arts\",\n    \"eamobile.com\": \"electronic_arts\",\n    \"element.io\": \"element\",\n    \"riot.im\": \"element\",\n    \"elicitapp.com\": \"elicit\",\n    \"eloqua.com\": \"eloqua\",\n    \"en25.com\": \"eloqua\",\n    \"eluxer.net\": \"eluxer_net\",\n    \"tracker.emailaptitude.com\": \"email_aptitude\",\n    \"tag.email-attitude.com\": \"email_attitude\",\n    \"app.emarketeer.com\": \"emarketeer\",\n    \"embed.ly\": \"embed.ly\",\n    \"embedly.com\": \"embed.ly\",\n    \"emediate.dk\": \"emediate\",\n    \"emediate.eu\": \"emediate\",\n    \"emediate.se\": \"emediate\",\n    \"emetriq.de\": \"emetriq\",\n    \"e2ma.net\": \"emma\",\n    \"adinsight.co.kr\": \"emnet\",\n    \"colbenson.es\": \"empathy\",\n    \"emsmobile.de\": \"emsmobile.de\",\n    \"sitecompass.com\": \"encore_metrics\",\n    \"enectoanalytics.com\": \"enecto_analytics\",\n    \"trk.enecto.com\": \"enecto_analytics\",\n    \"track.engagesciences.com\": \"engage_sciences\",\n    \"widget.engageya.com\": \"engageya_widget\",\n    \"engagio.com\": \"engagio\",\n    \"engineseeker.com\": \"engineseeker\",\n    \"enquisite.com\": \"enquisite\",\n    \"adtlgc.com\": \"enreach\",\n    \"ats.tumri.net\": \"ensemble\",\n    \"ensighten.com\": \"ensighten\",\n    \"envolve.com\": \"envolve\",\n    \"cdn.callbackkiller.com\": \"envybox\",\n    \"email-reflex.com\": \"eperflex\",\n    \"epicgameads.com\": \"epic_game_ads\",\n    \"trafficmp.com\": \"epic_marketplace\",\n    \"adshost1.com\": \"epom\",\n    \"adshost2.com\": \"epom\",\n    \"epom.com\": \"epom\",\n    \"epoq.de\": \"epoq\",\n    \"banzaiadv.it\": \"eprice\",\n    \"eproof.com\": \"eproof\",\n    \"equitystory.com\": \"eqs_group\",\n    \"eqads.com\": \"eqworks\",\n    \"ero-advertising.com\": \"eroadvertising\",\n    \"eroadvertising.com\": \"eroadvertising\",\n    \"d15qhc0lu1ghnk.cloudfront.net\": \"errorception\",\n    \"errorception.com\": \"errorception\",\n    \"eshopcomp.com\": \"eshopcomp.com\",\n    \"espncdn.com\": \"espn_cdn\",\n    \"esprit.de\": \"esprit.de\",\n    \"cybermonitor.com\": \"estat\",\n    \"estat.com\": \"estat\",\n    \"teste-s3-maycon.s3.amazonaws.com\": \"etag\",\n    \"etahub.com\": \"etahub.com\",\n    \"etargetnet.com\": \"etarget\",\n    \"ethn.io\": \"ethnio\",\n    \"pages.etology.com\": \"etology\",\n    \"sa.etp-prod.com\": \"etp\",\n    \"etracker.com\": \"etracker\",\n    \"etracker.de\": \"etracker\",\n    \"sedotracker.com\": \"etracker\",\n    \"etrigue.com\": \"etrigue\",\n    \"etsystatic.com\": \"etsystatic\",\n    \"eulerian.net\": \"eulerian\",\n    \"eultech.fnac.com\": \"eulerian\",\n    \"ew3.io\": \"eulerian\",\n    \"euroads.dk\": \"euroads\",\n    \"euroads.fi\": \"euroads\",\n    \"euroads.no\": \"euroads\",\n    \"newpromo.europacash.com\": \"europecash\",\n    \"tracker.euroweb.net\": \"euroweb_counter\",\n    \"apptegic.com\": \"evergage.com\",\n    \"evergage.com\": \"evergage.com\",\n    \"listener.everstring.com\": \"everstring\",\n    \"waterfrontmedia.com\": \"everyday_health\",\n    \"betrad.com\": \"evidon\",\n    \"evidon.com\": \"evidon\",\n    \"evisitanalyst.com\": \"evisit_analyst\",\n    \"evisitcs.com\": \"evisit_analyst\",\n    \"websiteperform.com\": \"evisit_analyst\",\n    \"ads.exactdrive.com\": \"exact_drive\",\n    \"exactag.com\": \"exactag\",\n    \"exelator.com\": \"exelate\",\n    \"dynamicoxygen.com\": \"exitjunction\",\n    \"exitjunction.com\": \"exitjunction\",\n    \"exdynsrv.com\": \"exoclick\",\n    \"exoclick.com\": \"exoclick\",\n    \"exosrv.com\": \"exoclick\",\n    \"exoticads.com\": \"exoticads.com\",\n    \"expedia.com\": \"expedia\",\n    \"trvl-px.com\": \"expedia\",\n    \"eccmp.com\": \"experian\",\n    \"audienceiq.com\": \"experian_marketing_services\",\n    \"techlightenment.com\": \"experian_marketing_services\",\n    \"expo-max.com\": \"expo-max\",\n    \"server.exposebox.com\": \"expose_box\",\n    \"sf.exposebox.com\": \"expose_box_widgets\",\n    \"express.co.uk\": \"express.co.uk\",\n    \"d1lp05q4sghme9.cloudfront.net\": \"expressvpn\",\n    \"extreme-dm.com\": \"extreme_tracker\",\n    \"eyenewton.ru\": \"eye_newton\",\n    \"eyeota.net\": \"eyeota\",\n    \"eyereturn.com\": \"eyereturnmarketing\",\n    \"eyeviewads.com\": \"eyeview\",\n    \"ezakus.net\": \"ezakus\",\n    \"f11-ads.com\": \"f11-ads.com\",\n    \"facebook.com\": \"facebook\",\n    \"facebook.net\": \"facebook\",\n    \"graph.facebook.com\": \"facebook_audience\",\n    \"fbcdn.net\": \"facebook_cdn\",\n    \"fbsbx.com\": \"facebook_cdn\",\n    \"facetz.net\": \"facetz.dca\",\n    \"adsfac.eu\": \"facilitate_digital\",\n    \"adsfac.net\": \"facilitate_digital\",\n    \"adsfac.sg\": \"facilitate_digital\",\n    \"adsfac.us\": \"facilitate_digital\",\n    \"faktor.io\": \"faktor.io\",\n    \"thefancy.com\": \"fancy_widget\",\n    \"d1q7pknmpq2wkm.cloudfront.net\": \"fanplayr\",\n    \"fap.to\": \"fap.to\",\n    \"farlightgames.com\": \"farlight_pte_ltd\",\n    \"fastly-insights.com\": \"fastly_insights\",\n    \"fastly.net\": \"fastlylb.net\",\n    \"fastlylb.net\": \"fastlylb.net\",\n    \"fastly-edge.com\": \"fastlylb.net\",\n    \"fastly-masque.net\": \"fastlylb.net\",\n    \"fastpic.ru\": \"fastpic.ru\",\n    \"fmpub.net\": \"federated_media\",\n    \"fby.s3.amazonaws.com\": \"feedbackify\",\n    \"feedbackify.com\": \"feedbackify\",\n    \"feedburner.com\": \"feedburner.com\",\n    \"feedify.de\": \"feedify\",\n    \"feedjit.com\": \"feedjit\",\n    \"log.feedjit.com\": \"feedjit\",\n    \"tracking.feedperfect.com\": \"feedperfect\",\n    \"feedsportal.com\": \"feedsportal\",\n    \"feefo.com\": \"feefo\",\n    \"fidelity-media.com\": \"fidelity_media\",\n    \"fiksu.com\": \"fiksu\",\n    \"filamentapp.s3.amazonaws.com\": \"filament.io\",\n    \"fileserve.xyz\": \"fileserve\",\n    \"tools.financeads.net\": \"financeads\",\n    \"tracker.financialcontent.com\": \"financial_content\",\n    \"findizer.fr\": \"findizer.fr\",\n    \"findologic.com\": \"findologic.com\",\n    \"app-measurement.com\": \"firebase\",\n    \"fcm.googleapis.com\": \"firebase\",\n    \"firebase.com\": \"firebase\",\n    \"firebase.google.com\": \"firebase\",\n    \"firebase.googleapis.com\": \"firebase\",\n    \"firebaseapp.com\": \"firebase\",\n    \"firebaseappcheck.googleapis.com\": \"firebase\",\n    \"firebasedynamiclinks-ipv4.googleapis.com\": \"firebase\",\n    \"firebasedynamiclinks-ipv6.googleapis.com\": \"firebase\",\n    \"firebasedynamiclinks.googleapis.com\": \"firebase\",\n    \"firebaseinappmessaging.googleapis.com\": \"firebase\",\n    \"firebaseinstallations.googleapis.com\": \"firebase\",\n    \"firebaselogging-pa.googleapis.com\": \"firebase\",\n    \"firebaselogging.googleapis.com\": \"firebase\",\n    \"firebaseperusertopics-pa.googleapis.com\": \"firebase\",\n    \"firebaseremoteconfig.googleapis.com\": \"firebase\",\n    \"firebaseio.com\": \"firebaseio.com\",\n    \"firstimpression.io\": \"first_impression\",\n    \"fitanalytics.com\": \"fit_analytics\",\n    \"fivetran.com\": \"fivetran\",\n    \"flagads.net\": \"flag_ads\",\n    \"flagcounter.com\": \"flag_counter\",\n    \"flashnews.com.au\": \"flash\",\n    \"flashtalking.com\": \"flashtalking\",\n    \"flattr.com\": \"flattr_button\",\n    \"flexlinks.com\": \"flexoffers\",\n    \"linkoffers.net\": \"flexoffers\",\n    \"flickr.com\": \"flickr_badge\",\n    \"staticflickr.com\": \"flickr_badge\",\n    \"lflipboard.com\": \"flipboard\",\n    \"flipboard.com\": \"flipboard\",\n    \"flite.com\": \"flite\",\n    \"flixcdn.com\": \"flixcdn.com\",\n    \"flix360.com\": \"flixmedia\",\n    \"flixcar.com\": \"flixmedia\",\n    \"flocktory.com\": \"flocktory.com\",\n    \"flowplayer.org\": \"flowplayer\",\n    \"adingo.jp\": \"fluct\",\n    \"clicken.us\": \"fluent\",\n    \"strcst.net\": \"fluid\",\n    \"fluidads.co\": \"fluidads\",\n    \"fluidsurveys.com\": \"fluidsurveys\",\n    \"cdn.flurry.com\": \"flurry\",\n    \"data.flurry.com\": \"flurry\",\n    \"flurry.com\": \"flurry\",\n    \"flx1.com\": \"flxone\",\n    \"flxpxl.com\": \"flxone\",\n    \"api.flyertown.ca\": \"flyertown\",\n    \"adservinghost.com\": \"fmadserving\",\n    \"adservinginternational.com\": \"fmadserving\",\n    \"special.matchtv.ru\": \"fonbet\",\n    \"kavijaseuranta.fi\": \"fonecta\",\n    \"fontawesome.com\": \"fontawesome_com\",\n    \"foodieblogroll.com\": \"foodie_blogroll\",\n    \"footprintlive.com\": \"footprint\",\n    \"footprintdns.com\": \"footprintdns.com\",\n    \"forcetrac.com\": \"forcetrac\",\n    \"fqsecure.com\": \"forensiq\",\n    \"fqtag.com\": \"forensiq\",\n    \"securepaths.com\": \"forensiq\",\n    \"4seeresults.com\": \"foresee\",\n    \"foresee.com\": \"foresee\",\n    \"cdn-static.formisimo.com\": \"formisimo\",\n    \"forter.com\": \"forter\",\n    \"fortlachanhecksof.info\": \"fortlachanhecksof.info\",\n    \"platform.foursquare.com\": \"foursquare_widget\",\n    \"fout.jp\": \"fout.jp\",\n    \"fimserve.com\": \"fox_audience_network\",\n    \"foxsports.com.au\": \"fox_sports\",\n    \"fncstatic.com\": \"foxnews_static\",\n    \"cdn.foxpush.net\": \"foxpush\",\n    \"foxpush.com\": \"foxpush\",\n    \"foxtel.com.au\": \"foxtel\",\n    \"foxtelgroupcdn.net.au\": \"foxtel\",\n    \"foxydeal.com\": \"foxydeal_com\",\n    \"yabidos.com\": \"fraudlogix\",\n    \"besucherstatistiken.com\": \"free_counter\",\n    \"compteurdevisite.com\": \"free_counter\",\n    \"contadorvisitasgratis.com\": \"free_counter\",\n    \"contatoreaccessi.com\": \"free_counter\",\n    \"freecounterstat.com\": \"free_counter\",\n    \"statcounterfree.com\": \"free_counter\",\n    \"webcontadores.com\": \"free_counter\",\n    \"fastonlineusers.com\": \"free_online_users\",\n    \"fastwebcounter.com\": \"free_online_users\",\n    \"freeonlineusers.com\": \"free_online_users\",\n    \"atoomic.com\": \"free_pagerank\",\n    \"free-pagerank.com\": \"free_pagerank\",\n    \"freedom.com\": \"freedom_mortgage\",\n    \"freegeoip.net\": \"freegeoip_net\",\n    \"freenet.de\": \"freenet_de\",\n    \"freent.de\": \"freenet_de\",\n    \"freeview.com\": \"freeview\",\n    \"freeview.com.au\": \"freeview\",\n    \"freeviewaustralia.tv\": \"freeview\",\n    \"fwmrm.net\": \"freewheel\",\n    \"heimdall.fresh8.co\": \"fresh8\",\n    \"d36mpcpuzc4ztk.cloudfront.net\": \"freshdesk\",\n    \"freshdesk.com\": \"freshdesk\",\n    \"freshplum.com\": \"freshplum\",\n    \"friendbuy.com\": \"friendbuy\",\n    \"friendfeed.com\": \"friendfeed\",\n    \"adultfriendfinder.com\": \"friendfinder_network\",\n    \"amigos.com\": \"friendfinder_network\",\n    \"board-books.com\": \"friendfinder_network\",\n    \"cams.com\": \"friendfinder_network\",\n    \"facebookofsex.com\": \"friendfinder_network\",\n    \"getiton.com\": \"friendfinder_network\",\n    \"nostringsattached.com\": \"friendfinder_network\",\n    \"pop6.com\": \"friendfinder_network\",\n    \"streamray.com\": \"friendfinder_network\",\n    \"inpref.com\": \"frosmo_optimizer\",\n    \"inpref.s3-external-3.amazonaws.com\": \"frosmo_optimizer\",\n    \"inpref.s3.amazonaws.com\": \"frosmo_optimizer\",\n    \"adflan.com\": \"fruitflan\",\n    \"fruitflan.com\": \"fruitflan\",\n    \"fstrk.net\": \"fstrk.net\",\n    \"cookie.fuel451.com\": \"fuelx\",\n    \"fullstory.com\": \"fullstory\",\n    \"track.funnelytics.io\": \"funnelytics\",\n    \"angsrvr.com\": \"fyber\",\n    \"fyber.com\": \"fyber\",\n    \"game-advertising-online.com\": \"game_advertising_online\",\n    \"gameanalytics.com\": \"gameanalytics\",\n    \"gamedistribution.com\": \"gamedistribution.com\",\n    \"gamerdna.com\": \"gamerdna\",\n    \"gannett-cdn.com\": \"gannett\",\n    \"gaug.es\": \"gaug.es\",\n    \"gpm-digital.com\": \"gazprom-media_digital\",\n    \"js.gb-world.net\": \"gb-world\",\n    \"gdeslon.ru\": \"gdeslon\",\n    \"gdmdigital.com\": \"gdm_digital\",\n    \"gntm.geeen.co.jp\": \"geeen\",\n    \"lpomax.net\": \"geeen\",\n    \"gemius.pl\": \"gemius\",\n    \"generaltracking.de\": \"generaltracking_de\",\n    \"genesismedia.com\": \"genesis\",\n    \"gssprt.jp\": \"geniee\",\n    \"rsvpgenius.com\": \"genius\",\n    \"genoo.com\": \"genoo\",\n    \"js.geoads.com\": \"geoads\",\n    \"geolify.com\": \"geolify\",\n    \"geoplugin.net\": \"geoplugin\",\n    \"geotrust.com\": \"geotrust\",\n    \"geovisite.com\": \"geovisite\",\n    \"gestionpub.com\": \"gestionpub\",\n    \"app.getresponse.com\": \"get_response\",\n    \"getsitecontrol.com\": \"get_site_control\",\n    \"getconversion.net\": \"getconversion\",\n    \"widgets.getglue.com\": \"getglue\",\n    \"adhigh.net\": \"getintent\",\n    \"static.getkudos.me\": \"getkudos\",\n    \"yottos.com\": \"getmyad\",\n    \"gsfn.us\": \"getsatisfaction\",\n    \"gettyimages.com\": \"gettyimages\",\n    \"sensic.net\": \"gfk\",\n    \"gfycat.com\": \"gfycat.com\",\n    \"a.giantrealm.com\": \"giant_realm\",\n    \"videostat.com\": \"giantmedia\",\n    \"gigaonclick.com\": \"giga\",\n    \"analytics.gigyahosting1.com\": \"gigya\",\n    \"gigcount.com\": \"gigya\",\n    \"gigya.com\": \"gigya\",\n    \"service.giosg.com\": \"giosg\",\n    \"giphy.com\": \"giphy.com\",\n    \"giraff.io\": \"giraff.io\",\n    \"github.com\": \"github\",\n    \"githubassets.com\": \"github\",\n    \"githubusercontent.com\": \"github\",\n    \"ghcr.io\": \"github\",\n    \"github.blog\": \"github\",\n    \"github.dev\": \"github\",\n    \"octocaptcha.com\": \"github\",\n    \"githubapp.com\": \"github_apps\",\n    \"github.io\": \"github_pages\",\n    \"aff3.gittigidiyor.com\": \"gittigidiyor_affiliate_program\",\n    \"gittip.com\": \"gittip\",\n    \"sitest.jp\": \"glad_cube\",\n    \"glganltcs.space\": \"glganltcs.space\",\n    \"globalwebindex.net\": \"global_web_index\",\n    \"globalnotifier.com\": \"globalnotifier.com\",\n    \"globalsign.com\": \"globalsign\",\n    \"ad.globaltakeoff.net\": \"globaltakeoff\",\n    \"glomex.cloud\": \"glomex.com\",\n    \"glomex.com\": \"glomex.com\",\n    \"glotgrx.com\": \"glotgrx.com\",\n    \"a.gmdelivery.com\": \"gm_delivery\",\n    \"gmail.com\": \"gmail\",\n    \"ad.atown.jp\": \"gmo\",\n    \"gmx.net\": \"gmx_net\",\n    \"gmxpro.net\": \"gmx_net\",\n    \"go.com\": \"go.com\",\n    \"affiliate.godaddy.com\": \"godaddy_affiliate_program\",\n    \"trafficfacts.com\": \"godaddy_site_analytics\",\n    \"seal.godaddy.com\": \"godaddy_site_seal\",\n    \"tracking.godatafeed.com\": \"godatafeed\",\n    \"counter.goingup.com\": \"goingup\",\n    \"axf8.net\": \"gomez\",\n    \"goodadvert.ru\": \"goodadvert\",\n    \"google.at\": \"google\",\n    \"google.be\": \"google\",\n    \"google.ca\": \"google\",\n    \"google.ch\": \"google\",\n    \"google.co.id\": \"google\",\n    \"google.co.in\": \"google\",\n    \"google.co.jp\": \"google\",\n    \"google.co.ma\": \"google\",\n    \"google.co.th\": \"google\",\n    \"google.co.uk\": \"google\",\n    \"google.com\": \"google\",\n    \"google.com.ar\": \"google\",\n    \"google.com.au\": \"google\",\n    \"google.com.br\": \"google\",\n    \"google.com.mx\": \"google\",\n    \"google.com.tr\": \"google\",\n    \"google.com.tw\": \"google\",\n    \"google.com.ua\": \"google\",\n    \"google.cz\": \"google\",\n    \"google.de\": \"google\",\n    \"google.dk\": \"google\",\n    \"google.dz\": \"google\",\n    \"google.es\": \"google\",\n    \"google.fi\": \"google\",\n    \"google.fr\": \"google\",\n    \"google.gr\": \"google\",\n    \"google.hu\": \"google\",\n    \"google.ie\": \"google\",\n    \"google.it\": \"google\",\n    \"google.nl\": \"google\",\n    \"google.no\": \"google\",\n    \"google.pl\": \"google\",\n    \"google.pt\": \"google\",\n    \"google.ro\": \"google\",\n    \"google.rs\": \"google\",\n    \"google.ru\": \"google\",\n    \"google.se\": \"google\",\n    \"google.tn\": \"google\",\n    \"1e100.net\": \"google\",\n    \"agnss.goog\": \"google\",\n    \"channel.status.request.url\": \"google\",\n    \"g.cn\": \"google\",\n    \"g.co\": \"google\",\n    \"google.ad\": \"google\",\n    \"google.ae\": \"google\",\n    \"google.al\": \"google\",\n    \"google.am\": \"google\",\n    \"google.as\": \"google\",\n    \"google.az\": \"google\",\n    \"google.ba\": \"google\",\n    \"google.bf\": \"google\",\n    \"google.bg\": \"google\",\n    \"google.bi\": \"google\",\n    \"google.bj\": \"google\",\n    \"google.bs\": \"google\",\n    \"google.bt\": \"google\",\n    \"google.by\": \"google\",\n    \"google.cat\": \"google\",\n    \"google.cd\": \"google\",\n    \"google.cf\": \"google\",\n    \"google.cg\": \"google\",\n    \"google.ci\": \"google\",\n    \"google.cl\": \"google\",\n    \"google.cm\": \"google\",\n    \"google.cn\": \"google\",\n    \"google.co.ao\": \"google\",\n    \"google.co.bw\": \"google\",\n    \"google.co.ck\": \"google\",\n    \"google.co.cr\": \"google\",\n    \"google.co.il\": \"google\",\n    \"google.co.ke\": \"google\",\n    \"google.co.kr\": \"google\",\n    \"google.co.ls\": \"google\",\n    \"google.co.mz\": \"google\",\n    \"google.co.nz\": \"google\",\n    \"google.co.tz\": \"google\",\n    \"google.co.ug\": \"google\",\n    \"google.co.uz\": \"google\",\n    \"google.co.ve\": \"google\",\n    \"google.co.vi\": \"google\",\n    \"google.co.za\": \"google\",\n    \"google.co.zm\": \"google\",\n    \"google.co.zw\": \"google\",\n    \"google.com.af\": \"google\",\n    \"google.com.ag\": \"google\",\n    \"google.com.ai\": \"google\",\n    \"google.com.bd\": \"google\",\n    \"google.com.bh\": \"google\",\n    \"google.com.bn\": \"google\",\n    \"google.com.bo\": \"google\",\n    \"google.com.bz\": \"google\",\n    \"google.com.co\": \"google\",\n    \"google.com.cu\": \"google\",\n    \"google.com.cy\": \"google\",\n    \"google.com.ec\": \"google\",\n    \"google.com.eg\": \"google\",\n    \"google.com.et\": \"google\",\n    \"google.com.fj\": \"google\",\n    \"google.com.gh\": \"google\",\n    \"google.com.gi\": \"google\",\n    \"google.com.gt\": \"google\",\n    \"google.com.hk\": \"google\",\n    \"google.com.jm\": \"google\",\n    \"google.com.kh\": \"google\",\n    \"google.com.kw\": \"google\",\n    \"google.com.lb\": \"google\",\n    \"google.com.my\": \"google\",\n    \"google.com.na\": \"google\",\n    \"google.com.nf\": \"google\",\n    \"google.com.ng\": \"google\",\n    \"google.com.ni\": \"google\",\n    \"google.com.np\": \"google\",\n    \"google.com.om\": \"google\",\n    \"google.com.pa\": \"google\",\n    \"google.com.pe\": \"google\",\n    \"google.com.pg\": \"google\",\n    \"google.com.ph\": \"google\",\n    \"google.com.pk\": \"google\",\n    \"google.com.pr\": \"google\",\n    \"google.com.py\": \"google\",\n    \"google.com.qa\": \"google\",\n    \"google.com.sa\": \"google\",\n    \"google.com.sb\": \"google\",\n    \"google.com.sg\": \"google\",\n    \"google.com.sl\": \"google\",\n    \"google.com.sv\": \"google\",\n    \"google.com.tj\": \"google\",\n    \"google.com.uy\": \"google\",\n    \"google.com.vc\": \"google\",\n    \"google.com.vn\": \"google\",\n    \"google.cv\": \"google\",\n    \"google.dj\": \"google\",\n    \"google.dm\": \"google\",\n    \"google.ee\": \"google\",\n    \"google.fm\": \"google\",\n    \"google.ga\": \"google\",\n    \"google.ge\": \"google\",\n    \"google.gg\": \"google\",\n    \"google.gl\": \"google\",\n    \"google.gm\": \"google\",\n    \"google.gp\": \"google\",\n    \"google.gy\": \"google\",\n    \"google.hn\": \"google\",\n    \"google.hr\": \"google\",\n    \"google.ht\": \"google\",\n    \"google.im\": \"google\",\n    \"google.in\": \"google\",\n    \"google.iq\": \"google\",\n    \"google.is\": \"google\",\n    \"google.je\": \"google\",\n    \"google.jo\": \"google\",\n    \"google.kg\": \"google\",\n    \"google.ki\": \"google\",\n    \"google.kz\": \"google\",\n    \"google.la\": \"google\",\n    \"google.li\": \"google\",\n    \"google.lk\": \"google\",\n    \"google.lt\": \"google\",\n    \"google.lu\": \"google\",\n    \"google.lv\": \"google\",\n    \"google.md\": \"google\",\n    \"google.me\": \"google\",\n    \"google.mg\": \"google\",\n    \"google.mk\": \"google\",\n    \"google.ml\": \"google\",\n    \"google.mn\": \"google\",\n    \"google.ms\": \"google\",\n    \"google.mu\": \"google\",\n    \"google.mv\": \"google\",\n    \"google.mw\": \"google\",\n    \"google.ne\": \"google\",\n    \"google.net\": \"google\",\n    \"google.nr\": \"google\",\n    \"google.nu\": \"google\",\n    \"google.org\": \"google\",\n    \"google.pn\": \"google\",\n    \"google.ps\": \"google\",\n    \"google.rw\": \"google\",\n    \"google.sc\": \"google\",\n    \"google.sh\": \"google\",\n    \"google.si\": \"google\",\n    \"google.sk\": \"google\",\n    \"google.sm\": \"google\",\n    \"google.sn\": \"google\",\n    \"google.so\": \"google\",\n    \"google.sr\": \"google\",\n    \"google.st\": \"google\",\n    \"google.td\": \"google\",\n    \"google.tg\": \"google\",\n    \"google.tk\": \"google\",\n    \"google.tl\": \"google\",\n    \"google.tm\": \"google\",\n    \"google.to\": \"google\",\n    \"google.tt\": \"google\",\n    \"google.us\": \"google\",\n    \"google.vg\": \"google\",\n    \"google.vu\": \"google\",\n    \"google.ws\": \"google\",\n    \"googleapis.cn\": \"google\",\n    \"googlecode.com\": \"google\",\n    \"googledownloads.cn\": \"google\",\n    \"googleoptimize.com\": \"google\",\n    \"googleweblight.in\": \"google\",\n    \"googlezip.net\": \"google\",\n    \"gstatic.cn\": \"google\",\n    \"news.google.com\": \"google\",\n    \"oo.gl\": \"google\",\n    \"withgoogle.com\": \"google\",\n    \"googleadservices.com\": \"google_adservices\",\n    \"google-analytics.com\": \"google_analytics\",\n    \"app-analytics-services.com\": \"google_analytics\",\n    \"ssl-google-analytics.l.google.com\": \"google_analytics\",\n    \"www-googletagmanager.l.google.com\": \"google_analytics\",\n    \"appspot.com\": \"google_appspot\",\n    \"googlehosted.com\": \"google_appspot\",\n    \"accounts.google.com\": \"google_auth\",\n    \"myaccount.google.com\": \"google_auth\",\n    \"oauth2.googleapis.com\": \"google_auth\",\n    \"ogs.google.com\": \"google_auth\",\n    \"securetoken.googleapis.com\": \"google_auth\",\n    \"beacons-google.com\": \"google_beacons\",\n    \"alt1-mtalk.google.com\": \"google_chat\",\n    \"alt2-mtalk.google.com\": \"google_chat\",\n    \"alt3-mtalk.google.com\": \"google_chat\",\n    \"alt4-mtalk.google.com\": \"google_chat\",\n    \"alt5-mtalk.google.com\": \"google_chat\",\n    \"alt6-mtalk.google.com\": \"google_chat\",\n    \"alt7-mtalk.google.com\": \"google_chat\",\n    \"alt8-mtalk.google.com\": \"google_chat\",\n    \"chat.google.com\": \"google_chat\",\n    \"mobile-gtalk.l.google.com\": \"google_chat\",\n    \"mobile-gtalk4.l.google.com\": \"google_chat\",\n    \"mtalk.google.com\": \"google_chat\",\n    \"mtalk4.google.com\": \"google_chat\",\n    \"talk.google.com\": \"google_chat\",\n    \"talk.l.google.com\": \"google_chat\",\n    \"talkx.l.google.com\": \"google_chat\",\n    \"cloud.google.com\": \"google_cloud_platform\",\n    \"gcp.gvt2.com\": \"google_cloud_platform\",\n    \"storage.googleapis.com\": \"google_cloud_storage\",\n    \"adsensecustomsearchads.com\": \"google_custom_search\",\n    \"dns.google\": \"google_dns\",\n    \"dns.google.com\": \"google_dns\",\n    \"google-public-dns-a.google.com\": \"google_dns\",\n    \"google-public-dns-b.google.com\": \"google_dns\",\n    \"domains.google\": \"google_domains\",\n    \"googledomains.com\": \"google_domains\",\n    \"nic.google\": \"google_domains\",\n    \"registry.google\": \"google_domains\",\n    \"edge.google.com\": \"google_edge\",\n    \"mail-ads.google.com\": \"google_email\",\n    \"fonts.googleapis.com\": \"google_fonts\",\n    \"cloudfunctions.net\": \"google_hosted\",\n    \"ghs.googlehosted.com\": \"google_hosted\",\n    \"ghs4.googlehosted.com\": \"google_hosted\",\n    \"ghs46.googlehosted.com\": \"google_hosted\",\n    \"ghs6.googlehosted.com\": \"google_hosted\",\n    \"googlehosted.l.googleusercontent.com\": \"google_hosted\",\n    \"run.app\": \"google_hosted\",\n    \"supl.google.com\": \"google_location\",\n    \"earth.app.goo.gl\": \"google_maps\",\n    \"geo0.ggpht.com\": \"google_maps\",\n    \"geo1.ggpht.com\": \"google_maps\",\n    \"geo2.ggpht.com\": \"google_maps\",\n    \"geo3.ggpht.com\": \"google_maps\",\n    \"kh.google.com\": \"google_maps\",\n    \"maps.app.goo.gl\": \"google_maps\",\n    \"maps.google.ca\": \"google_maps\",\n    \"maps.google.ch\": \"google_maps\",\n    \"maps.google.co.jp\": \"google_maps\",\n    \"maps.google.co.uk\": \"google_maps\",\n    \"maps.google.com\": \"google_maps\",\n    \"maps.google.com.mx\": \"google_maps\",\n    \"maps.google.es\": \"google_maps\",\n    \"maps.google.se\": \"google_maps\",\n    \"maps.gstatic.com\": \"google_maps\",\n    \"doubleclick.net\": \"google_marketing\",\n    \"invitemedia.com\": \"google_marketing\",\n    \"adsense.google.com\": \"google_marketing\",\n    \"adservice.google.ca\": \"google_marketing\",\n    \"adservice.google.co.in\": \"google_marketing\",\n    \"adservice.google.co.kr\": \"google_marketing\",\n    \"adservice.google.co.uk\": \"google_marketing\",\n    \"adservice.google.co.za\": \"google_marketing\",\n    \"adservice.google.com\": \"google_marketing\",\n    \"adservice.google.com.ar\": \"google_marketing\",\n    \"adservice.google.com.au\": \"google_marketing\",\n    \"adservice.google.com.br\": \"google_marketing\",\n    \"adservice.google.com.co\": \"google_marketing\",\n    \"adservice.google.com.gt\": \"google_marketing\",\n    \"adservice.google.com.mx\": \"google_marketing\",\n    \"adservice.google.com.pe\": \"google_marketing\",\n    \"adservice.google.com.ph\": \"google_marketing\",\n    \"adservice.google.com.pk\": \"google_marketing\",\n    \"adservice.google.com.tr\": \"google_marketing\",\n    \"adservice.google.com.tw\": \"google_marketing\",\n    \"adservice.google.com.vn\": \"google_marketing\",\n    \"adservice.google.de\": \"google_marketing\",\n    \"adservice.google.dk\": \"google_marketing\",\n    \"adservice.google.es\": \"google_marketing\",\n    \"adservice.google.fr\": \"google_marketing\",\n    \"adservice.google.nl\": \"google_marketing\",\n    \"adservice.google.no\": \"google_marketing\",\n    \"adservice.google.pl\": \"google_marketing\",\n    \"adservice.google.ru\": \"google_marketing\",\n    \"adservice.google.vg\": \"google_marketing\",\n    \"adtrafficquality.google\": \"google_marketing\",\n    \"dai.google.com\": \"google_marketing\",\n    \"doubleclick.com\": \"google_marketing\",\n    \"doubleclickbygoogle.com\": \"google_marketing\",\n    \"googlesyndication-cn.com\": \"google_marketing\",\n    \"duo.google.com\": \"google_meet\",\n    \"hangouts.clients6.google.com\": \"google_meet\",\n    \"hangouts.google.com\": \"google_meet\",\n    \"hangouts.googleapis.com\": \"google_meet\",\n    \"meet.google.com\": \"google_meet\",\n    \"meetings.googleapis.com\": \"google_meet\",\n    \"stun.l.google.com\": \"google_meet\",\n    \"stun1.l.google.com\": \"google_meet\",\n    \"ggpht.com\": \"google_photos\",\n    \"play-fe.googleapis.com\": \"google_play\",\n    \"play-lh.googleusercontent.com\": \"google_play\",\n    \"play.google.com\": \"google_play\",\n    \"play.googleapis.com\": \"google_play\",\n    \"1e100cdn.net\": \"google_servers\",\n    \"gvt1.com\": \"google_servers\",\n    \"gvt2.com\": \"google_servers\",\n    \"gvt3.com\": \"google_servers\",\n    \"googlesyndication.com\": \"google_syndication\",\n    \"googletagmanager.com\": \"google_tag_manager\",\n    \"googletagservices.com\": \"google_tag_manager\",\n    \"translate.google.com\": \"google_translate\",\n    \"googletraveladservices.com\": \"google_travel_adds\",\n    \"pki.goog\": \"google_trust_services\",\n    \"googlecommerce.com\": \"google_trusted_stores\",\n    \"googleusercontent.com\": \"google_users\",\n    \"telephony.goog\": \"google_voice\",\n    \"voice.google.com\": \"google_voice\",\n    \"gmodules.com\": \"google_widgets\",\n    \"calendar.google.com\": \"google_workspace\",\n    \"contacts.google.com\": \"google_workspace\",\n    \"currents.google.com\": \"google_workspace\",\n    \"docs.google.com\": \"google_workspace\",\n    \"drive.google.com\": \"google_workspace\",\n    \"forms.google.com\": \"google_workspace\",\n    \"gsuite.google.com\": \"google_workspace\",\n    \"jamboard.google.com\": \"google_workspace\",\n    \"keep.google.com\": \"google_workspace\",\n    \"plus.google.com\": \"google_workspace\",\n    \"sheets.google.com\": \"google_workspace\",\n    \"slides.google.com\": \"google_workspace\",\n    \"spreadsheets.google.com\": \"google_workspace\",\n    \"googleapis.com\": \"googleapis.com\",\n    \"gooal.herokuapp.com\": \"goooal\",\n    \"gooo.al\": \"goooal\",\n    \"cdn.triggertag.gorillanation.com\": \"gorilla_nation\",\n    \"evolvemediametrics.com\": \"gorilla_nation\",\n    \"d1l6p2sc9645hc.cloudfront.net\": \"gosquared\",\n    \"gosquared.com\": \"gosquared\",\n    \"gostats.com\": \"gostats\",\n    \"govmetric.com\": \"govmetric\",\n    \"servmetric.com\": \"govmetric\",\n    \"b.grabo.bg\": \"grabo_affiliate\",\n    \"trw12.com\": \"grandslammedia\",\n    \"tuberewards.com\": \"grandslammedia\",\n    \"d2bw638ufki166.cloudfront.net\": \"granify\",\n    \"granify.com\": \"granify\",\n    \"grapeshot.co.uk\": \"grapeshot\",\n    \"gscontxt.net\": \"grapeshot\",\n    \"graphcomment.com\": \"graph_comment\",\n    \"gravatar.com\": \"gravatar\",\n    \"cdn.gravitec.net\": \"gravitec\",\n    \"gravity.com\": \"gravity_insights\",\n    \"grvcdn.com\": \"gravity_insights\",\n    \"greatviews.de\": \"greatviews.de\",\n    \"gandrad.org\": \"green_and_red\",\n    \"green-red.com\": \"green_and_red\",\n    \"co2stats.com\": \"green_certified_site\",\n    \"greenstory.ca\": \"green_story\",\n    \"greentube.com\": \"greentube.com\",\n    \"gt-cdn.net\": \"greentube.com\",\n    \"greystripe.com\": \"greystripe\",\n    \"groovehq.com\": \"groove\",\n    \"groovinads.com\": \"groovinads\",\n    \"bidagent.xad.com\": \"groundtruth\",\n    \"gmads.net\": \"groupm_server\",\n    \"grmtech.net\": \"groupm_server\",\n    \"media.gsimedia.net\": \"gsi_media\",\n    \"gstatic.com\": \"gstatic\",\n    \"fx.gtop.ro\": \"gtop\",\n    \"fx.gtopstats.com\": \"gtop\",\n    \"gubagootracking.com\": \"gugaboo\",\n    \"guj.de\": \"guj.de\",\n    \"emsservice.de\": \"gujems\",\n    \"gumgum.com\": \"gumgum\",\n    \"gumroad.com\": \"gumroad\",\n    \"gunggo.com\": \"gunggo\",\n    \"h12-media.com\": \"h12_ads\",\n    \"h12-media.net\": \"h12_ads\",\n    \"hnbutton.appspot.com\": \"hacker_news_button\",\n    \"haendlerbund.de\": \"haendlerbund.de\",\n    \"halogennetwork.com\": \"halogen_network\",\n    \"d1l7z5ofrj6ab8.cloudfront.net\": \"happy_fox_chat\",\n    \"ad.harrenmedianetwork.com\": \"harren_media\",\n    \"ads.networkhm.com\": \"harren_media\",\n    \"app.hatchbuck.com\": \"hatchbuck\",\n    \"hhcdn.ru\": \"head_hunter\",\n    \"healte.de\": \"healte.de\",\n    \"d36lvucg9kzous.cloudfront.net\": \"heap\",\n    \"heapanalytics.com\": \"heap\",\n    \"heatmap.it\": \"heatmap\",\n    \"weltsport.net\": \"heimspiel\",\n    \"hellobar.com\": \"hello_bar\",\n    \"hellosociety.com\": \"hellosociety\",\n    \"here.com\": \"here\",\n    \"herokuapp.com\": \"heroku\",\n    \"heureka.cz\": \"heureka-widget\",\n    \"heybubble.com\": \"heybubble\",\n    \"heyos.com\": \"heyos\",\n    \"adlink.net\": \"hi-media_performance\",\n    \"comclick.com\": \"hi-media_performance\",\n    \"hi-mediaserver.com\": \"hi-media_performance\",\n    \"himediads.com\": \"hi-media_performance\",\n    \"himediadx.com\": \"hi-media_performance\",\n    \"hiconversion.com\": \"hiconversion\",\n    \"highwebmedia.com\": \"highwebmedia.com\",\n    \"hwcdn.net\": \"highwinds\",\n    \"hiiir.com\": \"hiiir\",\n    \"hiro.tv\": \"hiro\",\n    \"histats.com\": \"histats\",\n    \"hit-parade.com\": \"hit-parade\",\n    \"hit.ua\": \"hit.ua\",\n    \"hitslink.com\": \"hitslink\",\n    \"hitsprocessor.com\": \"hitslink\",\n    \"hitsniffer.com\": \"hitsniffer\",\n    \"hittail.com\": \"hittail\",\n    \"hivedx.com\": \"hivedx.com\",\n    \"ads.thehiveworks.com\": \"hiveworks\",\n    \"hockeyapp.net\": \"hockeyapp\",\n    \"hoholikik.club\": \"hoholikik.club\",\n    \"h-cdn.com\": \"hola_player\",\n    \"homeaway.com\": \"homeaway\",\n    \"honeybadger.io\": \"honeybadger\",\n    \"hlserve.com\": \"hooklogic\",\n    \"apiae.hopscore.com\": \"hop-cube\",\n    \"hotdogsandads.com\": \"hotdogsandads.com\",\n    \"hotjar.com\": \"hotjar\",\n    \"hotkeys.com\": \"hotkeys\",\n    \"hotlog.ru\": \"hotlog.ru\",\n    \"hotwords.com\": \"hotwords\",\n    \"hotwords.es\": \"hotwords\",\n    \"howtank.com\": \"howtank.com\",\n    \"hqentertainmentnetwork.com\": \"hqentertainmentnetwork.com\",\n    \"justservingfiles.net\": \"hqentertainmentnetwork.com\",\n    \"hsoub.com\": \"hsoub\",\n    \"hstrck.com\": \"hstrck.com\",\n    \"httpool.com\": \"httpool\",\n    \"toboads.com\": \"httpool\",\n    \"hubrus.com\": \"hubrus\",\n    \"hs-analytics.net\": \"hubspot\",\n    \"hs-scripts.com\": \"hubspot\",\n    \"hsleadflows.net\": \"hubspot\",\n    \"hubapi.com\": \"hubspot\",\n    \"hubspot.com\": \"hubspot\",\n    \"forms.hubspot.com\": \"hubspot_forms\",\n    \"hubvisor.io\": \"hubvisor.io\",\n    \"files.hucksterbot.com\": \"hucksterbot\",\n    \"hupso.com\": \"hupso\",\n    \"hurra.com\": \"hurra_tracker\",\n    \"hybrid.ai\": \"hybrid.ai\",\n    \"targetix.net\": \"hybrid.ai\",\n    \"hypeads.org\": \"hype_exchange\",\n    \"hypercomments.com\": \"hypercomments\",\n    \"hyves.nl\": \"hyves_widgets\",\n    \"hyvyd.com\": \"hyvyd\",\n    \"ib-ibi.com\": \"i-behavior\",\n    \"i-mobile.co.jp\": \"i-mobile\",\n    \"r.i.ua\": \"i.ua\",\n    \"i10c.net\": \"i10c.net\",\n    \"i2i.jp\": \"i2i.jp\",\n    \"i2idata.com\": \"i2i.jp\",\n    \"consensu.org\": \"iab_consent\",\n    \"iadvize.com\": \"iadvize\",\n    \"cmcore.com\": \"ibm_customer_experience\",\n    \"coremetrics.com\": \"ibm_customer_experience\",\n    \"coremetrics.eu\": \"ibm_customer_experience\",\n    \"tracker.icerocket.com\": \"icerocket_tracker\",\n    \"nsimg.net\": \"icf_technology\",\n    \"optimix.asia\": \"iclick\",\n    \"ic-live.com\": \"icrossing\",\n    \"icstats.nl\": \"icstats\",\n    \"icuazeczpeoohx.com\": \"icuazeczpeoohx.com\",\n    \"id-news.net\": \"id-news.net\",\n    \"idcdn.de\": \"id-news.net\",\n    \"eu-1-id5-sync.com\": \"id5-sync\",\n    \"id5-sync.com\": \"id5-sync\",\n    \"id5.io\": \"id5-sync\",\n    \"cdn.id.services\": \"id_services\",\n    \"e-generator.com\": \"ideal_media\",\n    \"idealo.com\": \"idealo_com\",\n    \"identrust.com\": \"identrust\",\n    \"ideoclick.com\": \"ideoclick\",\n    \"s.idio.co\": \"idio\",\n    \"ie8eamus.com\": \"ie8eamus.com\",\n    \"600z.com\": \"ientry\",\n    \"api.iflychat.com\": \"iflychat\",\n    \"ignitionone.com\": \"ignitionone\",\n    \"knotice.net\": \"ignitionone\",\n    \"igodigital.com\": \"igodigital\",\n    \"ad.wsod.com\": \"ihs_markit\",\n    \"collserve.com\": \"ihs_markit_online_shopper_insigh\",\n    \"ihvmcqojoj.com\": \"ihvmcqojoj.com\",\n    \"iias.eu\": \"iias.eu\",\n    \"ijento.com\": \"ijento\",\n    \"adv.imadrep.co.kr\": \"imad\",\n    \"worthathousandwords.com\": \"image_advantage\",\n    \"picadmedia.com\": \"image_space_media\",\n    \"imgix.net\": \"imgix.net\",\n    \"imgur.com\": \"imgur\",\n    \"vidigital.ru\": \"imho_vi\",\n    \"immanalytics.com\": \"immanalytics\",\n    \"immobilienscout24.de\": \"immobilienscout24_de\",\n    \"static-immobilienscout24.de\": \"immobilienscout24_de\",\n    \"imonomy.com\": \"imonomy\",\n    \"7eer.net\": \"impact_radius\",\n    \"d3cxv97fi8q177.cloudfront.net\": \"impact_radius\",\n    \"evyy.net\": \"impact_radius\",\n    \"impactradius-event.com\": \"impact_radius\",\n    \"impactradius-tag.com\": \"impact_radius\",\n    \"impactradius.com\": \"impact_radius\",\n    \"ojrq.net\": \"impact_radius\",\n    \"r7ls.net\": \"impact_radius\",\n    \"impresionesweb.com\": \"impresiones_web\",\n    \"360yield.com\": \"improve_digital\",\n    \"iljmp.com\": \"improvely\",\n    \"inbenta.com\": \"inbenta\",\n    \"inboxsdk.com\": \"inboxsdk.com\",\n    \"indeed.com\": \"indeed\",\n    \"casalemedia.com\": \"index_exchange\",\n    \"indexww.com\": \"index_exchange\",\n    \"indieclick.com\": \"indieclick\",\n    \"industrybrains.com\": \"industry_brains\",\n    \"impdesk.com\": \"infectious_media\",\n    \"impressiondesk.com\": \"infectious_media\",\n    \"zachysprod.infiniteanalytics.com\": \"infinite_analytics\",\n    \"infinity-tracking.net\": \"infinity_tracking\",\n    \"engine.influads.com\": \"influads\",\n    \"infolinks.com\": \"infolinks\",\n    \"intextscript.com\": \"infolinks\",\n    \"ioam.de\": \"infonline\",\n    \"iocnt.net\": \"infonline\",\n    \"ivwbox.de\": \"infonline\",\n    \"informer.com\": \"informer_technologies\",\n    \"infusionsoft.com\": \"infusionsoft\",\n    \"keap.com\": \"infusionsoft\",\n    \"innity.com\": \"innity\",\n    \"innity.net\": \"innity\",\n    \"innogames.com\": \"innogames.de\",\n    \"innogames.de\": \"innogames.de\",\n    \"innogamescdn.com\": \"innogames.de\",\n    \"innovid.com\": \"innovid\",\n    \"inside-graph.com\": \"inside\",\n    \"useinsider.com\": \"insider\",\n    \"insightexpressai.com\": \"insightexpress\",\n    \"inskinad.com\": \"inskin_media\",\n    \"inskinmedia.com\": \"inskin_media\",\n    \"inspectlet.com\": \"inspectlet\",\n    \"inspsearchapi.com\": \"inspsearchapi.com\",\n    \"cdninstagram.com\": \"instagram_com\",\n    \"instagram.com\": \"instagram_com\",\n    \"tcgtrkr.com\": \"instant_check_mate\",\n    \"sdad.guru\": \"instart_logic\",\n    \"insticator.com\": \"insticator\",\n    \"load.instinctiveads.com\": \"instinctive\",\n    \"intango.com\": \"intango\",\n    \"adsafeprotected.com\": \"integral_ad_science\",\n    \"iasds01.com\": \"integral_ad_science\",\n    \"integral-marketing.com\": \"integral_marketing\",\n    \"intelliad.com\": \"intelliad\",\n    \"intelliad.de\": \"intelliad\",\n    \"saas.intelligencefocus.com\": \"intelligencefocus\",\n    \"ist-track.com\": \"intelligent_reach\",\n    \"intensedebate.com\": \"intense_debate\",\n    \"intentiq.com\": \"intent_iq\",\n    \"intentmedia.net\": \"intent_media\",\n    \"intercom.com\": \"intercom\",\n    \"intercom.io\": \"intercom\",\n    \"intercomassets.com\": \"intercom\",\n    \"intercomcdn.com\": \"intercom\",\n    \"interedy.info\": \"interedy.info\",\n    \"ads.intergi.com\": \"intergi\",\n    \"intermarkets.net\": \"intermarkets.net\",\n    \"intermundomedia.com\": \"intermundo_media\",\n    \"bbelements.com\": \"internet_billboard\",\n    \"goadservices.com\": \"internet_billboard\",\n    \"ibillboard.com\": \"internet_billboard\",\n    \"mediainter.net\": \"internet_billboard\",\n    \"voice2page.com\": \"internetaudioads\",\n    \"ibpxl.com\": \"internetbrands\",\n    \"ibsrv.net\": \"internetbrands\",\n    \"interpolls.com\": \"interpolls\",\n    \"ps7894.com\": \"interyield\",\n    \"intilery-analytics.com\": \"intilery\",\n    \"im-apps.net\": \"intimate_merger\",\n    \"investingchannel.com\": \"investingchannel\",\n    \"inviziads.com\": \"inviziads\",\n    \"js12.invoca.net\": \"invoca\",\n    \"ringrevenue.com\": \"invoca\",\n    \"invodo.com\": \"invodo\",\n    \"ionicframework.com\": \"ionicframework.com\",\n    \"dsp.io\": \"iotec\",\n    \"iesnare.com\": \"iovation\",\n    \"iovation.com\": \"iovation\",\n    \"ip-label.net\": \"ip-label\",\n    \"eltoro.com\": \"ip_targeting\",\n    \"iptargeting.com\": \"ip_targeting\",\n    \"ip-tracker.org\": \"ip_tracker\",\n    \"iptrack.io\": \"ip_tracker\",\n    \"iperceptions.com\": \"iperceptions\",\n    \"dust.ipfingerprint.com\": \"ipfingerprint\",\n    \"mbww.com\": \"ipg_mediabrands\",\n    \"ipify.org\": \"ipify\",\n    \"ipinfo.io\": \"ipinfo\",\n    \"iplogger.ru\": \"iplogger\",\n    \"centraliprom.com\": \"iprom\",\n    \"iprom.net\": \"iprom\",\n    \"ipromote.com\": \"ipromote\",\n    \"clickmanage.com\": \"iprospect\",\n    \"iq.com\": \"iqiyi\",\n    \"iqiyi.com\": \"iqiyi\",\n    \"qy.net\": \"iqiyi\",\n    \"addelive.com\": \"ironsource\",\n    \"afdads.com\": \"ironsource\",\n    \"delivery47.com\": \"ironsource\",\n    \"ironsrc.com\": \"ironsource\",\n    \"ironsrc.net\": \"ironsource\",\n    \"is.com\": \"ironsource\",\n    \"soom.la\": \"ironsource\",\n    \"supersonicads.com\": \"ironsource\",\n    \"tapjoy.com\": \"ironsource\",\n    \"adsbyisocket.com\": \"isocket\",\n    \"isocket.com\": \"isocket\",\n    \"isolarcloud.com\": \"isolarcloud\",\n    \"isolarcloud.com.a.lahuashanbx.com\": \"isolarcloud\",\n    \"isolarcloud.com.w.cdngslb.com\": \"isolarcloud\",\n    \"isolarcloud.com.w.kunlunsl.com\": \"isolarcloud\",\n    \"ispot.tv\": \"ispot.tv\",\n    \"itineraire.info\": \"itineraire.info\",\n    \"autolinkmaker.itunes.apple.com\": \"itunes_link_maker\",\n    \"ity.im\": \"ity.im\",\n    \"iubenda.com\": \"iubenda.com\",\n    \"ivcbrasil.org.br\": \"ivcbrasil.org.br\",\n    \"ivitrack.com\": \"ividence\",\n    \"iwiw.hu\": \"iwiw_widgets\",\n    \"ixiaa.com\": \"ixi_digital\",\n    \"ixquick.com\": \"ixquick.com\",\n    \"cdn.izooto.com\": \"izooto\",\n    \"jlist.com\": \"j-list_affiliate_program\",\n    \"getjaco.com\": \"jaco\",\n    \"janrainbackplane.com\": \"janrain\",\n    \"rpxnow.com\": \"janrain\",\n    \"jeeng.com\": \"jeeng\",\n    \"api.jeeng.com\": \"jeeng_widgets\",\n    \"phone-analytics.com\": \"jet_interactive\",\n    \"grazie.ai\": \"jetbrains\",\n    \"intellij.net\": \"jetbrains\",\n    \"jb.gg\": \"jetbrains\",\n    \"jetbrains.ai\": \"jetbrains\",\n    \"jetbrains.com\": \"jetbrains\",\n    \"jetbrains.com.cn\": \"jetbrains\",\n    \"jetbrains.dev\": \"jetbrains\",\n    \"jetbrains.net\": \"jetbrains\",\n    \"jetbrains.org\": \"jetbrains\",\n    \"jetbrains.ru\": \"jetbrains\",\n    \"jetbrains.space\": \"jetbrains\",\n    \"kotl.in\": \"jetbrains\",\n    \"kotlinconf.com\": \"jetbrains\",\n    \"kotlinlang.org\": \"jetbrains\",\n    \"myjetbrains.com\": \"jetbrains\",\n    \"talkingkotlin.com\": \"jetbrains\",\n    \"jetlore.com\": \"jetlore\",\n    \"pixel.wp.com\": \"jetpack\",\n    \"stats.wp.com\": \"jetpack\",\n    \"jetpackdigital.com\": \"jetpack_digital\",\n    \"jimcdn.com\": \"jimdo.com\",\n    \"jimdo.com\": \"jimdo.com\",\n    \"jimstatic.com\": \"jimdo.com\",\n    \"ads.jinkads.com\": \"jink\",\n    \"jirafe.com\": \"jirafe\",\n    \"jivosite.com\": \"jivochat\",\n    \"jivox.com\": \"jivox\",\n    \"jobs2careers.com\": \"jobs_2_careers\",\n    \"joinhoney.com\": \"joinhoney\",\n    \"create.leadid.com\": \"jornaya\",\n    \"d1tprjo2w7krrh.cloudfront.net\": \"jornaya\",\n    \"cdnjquery.com\": \"jquery\",\n    \"jquery.com\": \"jquery\",\n    \"cjmooter.xcache.kinxcdn.com\": \"js_communications\",\n    \"jsdelivr.net\": \"jsdelivr\",\n    \"jsecoin.com\": \"jse_coin\",\n    \"jsuol.com.br\": \"jsuol.com.br\",\n    \"contentabc.com\": \"juggcash\",\n    \"mofos.com\": \"juggcash\",\n    \"juiceadv.com\": \"juiceadv\",\n    \"juicyads.com\": \"juicyads\",\n    \"cdn.jumplead.com\": \"jumplead\",\n    \"jumpstarttaggingsolutions.com\": \"jumpstart_tagging_solutions\",\n    \"jumptap.com\": \"jumptap\",\n    \"jump-time.net\": \"jumptime\",\n    \"jumptime.com\": \"jumptime\",\n    \"components.justanswer.com\": \"just_answer\",\n    \"justpremium.com\": \"just_premium\",\n    \"justpremium.nl\": \"just_premium\",\n    \"justrelevant.com\": \"just_relevant\",\n    \"jvc.gg\": \"jvc.gg\",\n    \"d21rhj7n383afu.cloudfront.net\": \"jw_player\",\n    \"jwpcdn.com\": \"jw_player\",\n    \"jwplatform.com\": \"jw_player\",\n    \"jwplayer.com\": \"jw_player\",\n    \"jwpltx.com\": \"jw_player\",\n    \"jwpsrv.com\": \"jw_player\",\n    \"ltassrv.com\": \"jw_player_ad_solutions\",\n    \"kaeufersiegel.de\": \"kaeufersiegel.de\",\n    \"kairion.de\": \"kairion.de\",\n    \"kctag.net\": \"kairion.de\",\n    \"kaloo.ga\": \"kaloo.ga\",\n    \"kaltura.com\": \"kaltura\",\n    \"kameleoon.com\": \"kameleoon\",\n    \"kameleoon.eu\": \"kameleoon\",\n    \"kampyle.com\": \"kampyle\",\n    \"kanoodle.com\": \"kanoodle\",\n    \"kmi-us.com\": \"kantar_media\",\n    \"tnsinternet.be\": \"kantar_media\",\n    \"karambasecurity.com\": \"karambasecurity\",\n    \"kargo.com\": \"kargo\",\n    \"kaspersky-labs.com\": \"kaspersky-labs.com\",\n    \"kataweb.it\": \"kataweb.it\",\n    \"cen.katchup.fr\": \"katchup\",\n    \"kau.li\": \"kauli\",\n    \"kavanga.ru\": \"kavanga\",\n    \"kayosports.com.au\": \"kayo_sports\",\n    \"dc8na2hxrj29i.cloudfront.net\": \"keen_io\",\n    \"keen.io\": \"keen_io\",\n    \"widget.kelkoo.com\": \"kelkoo\",\n    \"xg4ken.com\": \"kenshoo\",\n    \"keymetric.net\": \"keymetric\",\n    \"lb.keytiles.com\": \"keytiles\",\n    \"keywee.co\": \"keywee\",\n    \"keywordmax.com\": \"keywordmax\",\n    \"massrelevance.com\": \"khoros\",\n    \"tweetriver.com\": \"khoros\",\n    \"khzbeucrltin.com\": \"khzbeucrltin.com\",\n    \"ping.kickfactory.com\": \"kickfactory\",\n    \"sa-as.com\": \"kickfire\",\n    \"sniff.visistat.com\": \"kickfire\",\n    \"stats.visistat.com\": \"kickfire\",\n    \"apikik.com\": \"kik\",\n    \"kik-gateway-use1.meetme.com\": \"kik\",\n    \"kik-live.com\": \"kik\",\n    \"kik-stream.meetme.com\": \"kik\",\n    \"kik.com\": \"kik\",\n    \"king.com\": \"king.com\",\n    \"midasplayer.com\": \"king_com\",\n    \"kinja-img.com\": \"kinja.com\",\n    \"kinja-static.com\": \"kinja.com\",\n    \"kinja.com\": \"kinja.com\",\n    \"kiosked.com\": \"kiosked\",\n    \"doug1izaerwt3.cloudfront.net\": \"kissmetrics.com\",\n    \"kissmetrics.com\": \"kissmetrics.com\",\n    \"ad.103092804.com\": \"kitara_media\",\n    \"kmdisplay.com\": \"kitara_media\",\n    \"kixer.com\": \"kixer\",\n    \"klarna.com\": \"klarna.com\",\n    \"a.klaviyo.com\": \"klaviyo\",\n    \"klaviyo.com\": \"klaviyo\",\n    \"klikki.com\": \"klikki\",\n    \"scr.kliksaya.com\": \"kliksaya\",\n    \"mediapeo2.com\": \"kmeleo\",\n    \"knoopstat.nl\": \"knoopstat\",\n    \"knotch.it\": \"knotch\",\n    \"komoona.com\": \"komoona\",\n    \"kona.kontera.com\": \"kontera_contentlink\",\n    \"ktxtr.com\": \"kontextr\",\n    \"kontextua.com\": \"kontextua\",\n    \"cleanrm.net\": \"korrelate\",\n    \"korrelate.net\": \"korrelate\",\n    \"trackit.ktxlytics.io\": \"kortx\",\n    \"kaptcha.com\": \"kount\",\n    \"krxd.net\": \"krux_digital\",\n    \"d31bfnnwekbny6.cloudfront.net\": \"kupona\",\n    \"kpcustomer.de\": \"kupona\",\n    \"q-sis.de\": \"kupona\",\n    \"kxcdn.com\": \"kxcdn.com\",\n    \"cdn.kyto.com\": \"kyto\",\n    \"cd-ladsp-com.s3.amazonaws.com\": \"ladsp.com\",\n    \"ladmp.com\": \"ladsp.com\",\n    \"ladsp.com\": \"ladsp.com\",\n    \"lanistaads.com\": \"lanista_concepts\",\n    \"latimes.com\": \"latimes\",\n    \"events.launchdarkly.com\": \"launch_darkly\",\n    \"launchdarkly.com\": \"launch_darkly\",\n    \"launchbit.com\": \"launchbit\",\n    \"launchpad.net\": \"launchpad\",\n    \"launchpadcontent.net\": \"launchpad\",\n    \"layer-ad.org\": \"layer-ad.org\",\n    \"ph-live.slatic.net\": \"lazada\",\n    \"slatic.net\": \"lazada\",\n    \"lcxdigital.com\": \"lcx_digital\",\n    \"lemde.fr\": \"le_monde.fr\",\n    \"t1.llanalytics.com\": \"lead_liaison\",\n    \"leadback.ru\": \"leadback\",\n    \"leaddyno.com\": \"leaddyno\",\n    \"123-tracker.com\": \"leadforensics\",\n    \"55-trk-srv.com\": \"leadforensics\",\n    \"business-path-55.com\": \"leadforensics\",\n    \"click-to-trace.com\": \"leadforensics\",\n    \"cloud-exploration.com\": \"leadforensics\",\n    \"cloud-journey.com\": \"leadforensics\",\n    \"cloud-trail.com\": \"leadforensics\",\n    \"cloudpath82.com\": \"leadforensics\",\n    \"cloudtracer101.com\": \"leadforensics\",\n    \"discover-path.com\": \"leadforensics\",\n    \"discovertrail.net\": \"leadforensics\",\n    \"domainanalytics.net\": \"leadforensics\",\n    \"dthvdr9.com\": \"leadforensics\",\n    \"explore-123.com\": \"leadforensics\",\n    \"finger-info.net\": \"leadforensics\",\n    \"forensics1000.com\": \"leadforensics\",\n    \"ip-route.net\": \"leadforensics\",\n    \"ipadd-path.com\": \"leadforensics\",\n    \"iproute66.com\": \"leadforensics\",\n    \"lead-123.com\": \"leadforensics\",\n    \"lead-analytics-1000.com\": \"leadforensics\",\n    \"lead-watcher.com\": \"leadforensics\",\n    \"leadforensics.com\": \"leadforensics\",\n    \"ledradn.com\": \"leadforensics\",\n    \"letterbox-path.com\": \"leadforensics\",\n    \"letterboxtrail.com\": \"leadforensics\",\n    \"network-handle.com\": \"leadforensics\",\n    \"path-follower.com\": \"leadforensics\",\n    \"path-trail.com\": \"leadforensics\",\n    \"scan-trail.com\": \"leadforensics\",\n    \"site-research.net\": \"leadforensics\",\n    \"srv1010elan.com\": \"leadforensics\",\n    \"the-lead-tracker.com\": \"leadforensics\",\n    \"trace-2000.com\": \"leadforensics\",\n    \"track-web.net\": \"leadforensics\",\n    \"trackdiscovery.net\": \"leadforensics\",\n    \"trackercloud.net\": \"leadforensics\",\n    \"trackinvestigate.net\": \"leadforensics\",\n    \"trail-viewer.com\": \"leadforensics\",\n    \"trail-web.com\": \"leadforensics\",\n    \"trailbox.net\": \"leadforensics\",\n    \"trailinvestigator.com\": \"leadforensics\",\n    \"web-path.com\": \"leadforensics\",\n    \"webforensics.co.uk\": \"leadforensics\",\n    \"websiteexploration.com\": \"leadforensics\",\n    \"www-path.com\": \"leadforensics\",\n    \"gate.leadgenic.com\": \"leadgenic\",\n    \"leadhit.ru\": \"leadhit\",\n    \"js.leadin.com\": \"leadin\",\n    \"io.leadingreports.de\": \"leading_reports\",\n    \"js.leadinspector.de\": \"leadinspector\",\n    \"formalyzer.com\": \"leadlander\",\n    \"trackalyzer.com\": \"leadlander\",\n    \"analytics.leadlifesolutions.net\": \"leadlife\",\n    \"my.leadpages.net\": \"leadpages\",\n    \"leadplace.fr\": \"leadplace\",\n    \"scorecard.wspisp.net\": \"leads_by_web.com\",\n    \"www.leadscoreapp.dk\": \"leadscoreapp\",\n    \"tracker.leadsius.com\": \"leadsius\",\n    \"leady.com\": \"leady\",\n    \"leady.cz\": \"leady\",\n    \"leiki.com\": \"leiki\",\n    \"lengow.com\": \"lengow\",\n    \"lenmit.com\": \"lenmit.com\",\n    \"lentainform.com\": \"lentainform.com\",\n    \"lenua.de\": \"lenua.de\",\n    \"letreach.com\": \"let_reach\",\n    \"lencr.org\": \"lets_encrypt\",\n    \"letsencrypt.org\": \"lets_encrypt\",\n    \"js.letvcdn.com\": \"letv\",\n    \"footprint.net\": \"level3_communications\",\n    \"alphonso.tv\": \"lgads\",\n    \"lgads.tv\": \"lgads\",\n    \"lg.com\": \"lgtv\",\n    \"lge.com\": \"lgtv\",\n    \"lgsmartad.com\": \"lgtv\",\n    \"lgtvcommon.com\": \"lgtv\",\n    \"lgtvsdp.com\": \"lgtv\",\n    \"licensebuttons.net\": \"licensebuttons.net\",\n    \"lfstmedia.com\": \"lifestreet_media\",\n    \"content-recommendation.net\": \"ligatus\",\n    \"ligadx.com\": \"ligatus\",\n    \"ligatus.com\": \"ligatus\",\n    \"ligatus.de\": \"ligatus\",\n    \"veeseo.com\": \"ligatus\",\n    \"limk.com\": \"limk\",\n    \"line-apps.com\": \"line_apps\",\n    \"line-scdn.net\": \"line_apps\",\n    \"line.me\": \"line_apps\",\n    \"tongji.linezing.com\": \"linezing\",\n    \"linkbucks.com\": \"linkbucks\",\n    \"linkconnector.com\": \"linkconnector\",\n    \"bizo.com\": \"linkedin\",\n    \"licdn.com\": \"linkedin\",\n    \"linkedin.com\": \"linkedin\",\n    \"lynda.com\": \"linkedin\",\n    \"ads.linkedin.com\": \"linkedin_ads\",\n    \"snap.licdn.com\": \"linkedin_analytics\",\n    \"bizographics.com\": \"linkedin_marketing_solutions\",\n    \"platform.linkedin.com\": \"linkedin_widgets\",\n    \"linker.hr\": \"linker\",\n    \"linkprice.com\": \"linkprice\",\n    \"lp4.io\": \"linkpulse\",\n    \"linksalpha.com\": \"linksalpha\",\n    \"erovinmo.com\": \"linksmart\",\n    \"linksmart.com\": \"linksmart\",\n    \"linkstorm.net\": \"linkstorm\",\n    \"linksynergy.com\": \"linksynergy.com\",\n    \"linkup.com\": \"linkup\",\n    \"linkwi.se\": \"linkwise\",\n    \"linkwithin.com\": \"linkwithin\",\n    \"lqm.io\": \"liquidm_technology_gmbh\",\n    \"lqmcdn.com\": \"liquidm_technology_gmbh\",\n    \"liqwid.net\": \"liqwid\",\n    \"list.ru\": \"list.ru\",\n    \"listrakbi.com\": \"listrak\",\n    \"live2support.com\": \"live2support\",\n    \"live800.com\": \"live800\",\n    \"ladesk.com\": \"live_agent\",\n    \"livehelpnow.net\": \"live_help_now\",\n    \"liadm.com\": \"live_intent\",\n    \"l-stat.livejournal.net\": \"live_journal\",\n    \"liveadexchanger.com\": \"liveadexchanger.com\",\n    \"livechat.s3.amazonaws.com\": \"livechat\",\n    \"livechatinc.com\": \"livechat\",\n    \"livechatinc.net\": \"livechat\",\n    \"livechatnow.com\": \"livechatnow\",\n    \"livechatnow.net\": \"livechatnow\",\n    \"liveclicker.net\": \"liveclicker\",\n    \"livecounter.dk\": \"livecounter\",\n    \"fyre.co\": \"livefyre\",\n    \"livefyre.com\": \"livefyre\",\n    \"yadro.ru\": \"liveinternet\",\n    \"liveperson.net\": \"liveperson\",\n    \"lpsnmedia.net\": \"liveperson\",\n    \"pippio.com\": \"liveramp\",\n    \"rapleaf.com\": \"liveramp\",\n    \"rlcdn.com\": \"liveramp\",\n    \"livere.co.kr\": \"livere\",\n    \"livere.co.kr.cizion.ixcloud.net\": \"livere\",\n    \"livesportmedia.eu\": \"livesportmedia.eu\",\n    \"analytics.livestream.com\": \"livestream\",\n    \"livetex.ru\": \"livetex.ru\",\n    \"lkqd.net\": \"lkqd\",\n    \"loadbee.com\": \"loadbee.com\",\n    \"loadercdn.com\": \"loadercdn.com\",\n    \"loadsource.org\": \"loadsource.org\",\n    \"web.localytics.com\": \"localytics\",\n    \"localytics.com\": \"localytics\",\n    \"cdn2.lockerdome.com\": \"lockerdome\",\n    \"addtoany.com\": \"lockerz_share\",\n    \"pixel.loganmedia.mobi\": \"logan_media\",\n    \"ping.answerbook.com\": \"logdna\",\n    \"loggly.com\": \"loggly\",\n    \"logly.co.jp\": \"logly\",\n    \"logsss.com\": \"logsss.com\",\n    \"lomadee.com\": \"lomadee\",\n    \"assets.loomia.com\": \"loomia\",\n    \"loop11.com\": \"loop11\",\n    \"lfov.net\": \"loopfuse_oneview\",\n    \"crwdcntrl.net\": \"lotame\",\n    \"vidcpm.com\": \"lottex_inc\",\n    \"tracker.samplicio.us\": \"lucid\",\n    \"lucidmedia.com\": \"lucid_media\",\n    \"lead.adsender.us\": \"lucini\",\n    \"livestatserver.com\": \"lucky_orange\",\n    \"luckyorange.com\": \"lucky_orange\",\n    \"luckyorange.net\": \"lucky_orange\",\n    \"luckypushh.com\": \"luckypushh.com\",\n    \"adelixir.com\": \"lxr100\",\n    \"lypn.com\": \"lynchpin_analytics\",\n    \"lypn.net\": \"lynchpin_analytics\",\n    \"lytics.io\": \"lytics\",\n    \"lyuoaxruaqdo.com\": \"lyuoaxruaqdo.com\",\n    \"m-pathy.com\": \"m-pathy\",\n    \"mpnrs.com\": \"m._p._newmedia\",\n    \"m4n.nl\": \"m4n\",\n    \"madadsmedia.com\": \"mad_ads_media\",\n    \"madeleine.de\": \"madeleine.de\",\n    \"dinclinx.com\": \"madison_logic\",\n    \"madisonlogic.com\": \"madison_logic\",\n    \"madnet.ru\": \"madnet\",\n    \"eu2.madsone.com\": \"mads\",\n    \"magna.ru\": \"magna_advertise\",\n    \"d3ezl4ajpp2zy8.cloudfront.net\": \"magnetic\",\n    \"domdex.com\": \"magnetic\",\n    \"domdex.net\": \"magnetic\",\n    \"magnetisemedia.com\": \"magnetise_group\",\n    \"magnify360.com\": \"magnify360\",\n    \"magnuum.com\": \"magnuum.com\",\n    \"ad.mail.ru\": \"mail.ru_banner\",\n    \"imgsmail.ru\": \"mail.ru_group\",\n    \"mail.ru\": \"mail.ru_group\",\n    \"mradx.net\": \"mail.ru_group\",\n    \"odnoklassniki.ru\": \"mail.ru_group\",\n    \"ok.ru\": \"mail.ru_group\",\n    \"chimpstatic.com\": \"mailchimp_tracking\",\n    \"list-manage.com\": \"mailchimp_tracking\",\n    \"mailchimp.com\": \"mailchimp_tracking\",\n    \"mailerlite.com\": \"mailerlite.com\",\n    \"mailtrack.io\": \"mailtrack.io\",\n    \"mainadv.com\": \"mainadv\",\n    \"makazi.com\": \"makazi\",\n    \"makeappdev.xyz\": \"makeappdev.xyz\",\n    \"makesource.cool\": \"makesource.cool\",\n    \"widgets.mango-office.ru\": \"mango\",\n    \"manycontacts.com\": \"manycontacts\",\n    \"mapandroute.de\": \"mapandroute.de\",\n    \"mapbox.com\": \"mapbox\",\n    \"www.maploco.com\": \"maploco\",\n    \"px.marchex.io\": \"marchex\",\n    \"voicestar.com\": \"marchex\",\n    \"mmadsgadget.com\": \"marimedia\",\n    \"qadabra.com\": \"marimedia\",\n    \"qadserve.com\": \"marimedia\",\n    \"qadservice.com\": \"marimedia\",\n    \"marinsm.com\": \"marin_search_marketer\",\n    \"markandmini.com\": \"mark_+_mini\",\n    \"ak-cdn.placelocal.com\": \"market_thunder\",\n    \"dt00.net\": \"marketgid\",\n    \"dt07.net\": \"marketgid\",\n    \"marketgid.com\": \"marketgid\",\n    \"mgid.com\": \"marketgid\",\n    \"marketingautomation.si\": \"marketing_automation\",\n    \"marketo.com\": \"marketo\",\n    \"marketo.net\": \"marketo\",\n    \"mktoresp.com\": \"marketo\",\n    \"caanalytics.com\": \"markmonitor\",\n    \"mmstat.com\": \"markmonitor\",\n    \"markmonitor.com\": \"markmonitor\",\n    \"netscope.data.marktest.pt\": \"marktest\",\n    \"marshadow.io\": \"marshadow.io\",\n    \"martiniadnetwork.com\": \"martini_media\",\n    \"edigitalsurvey.com\": \"maru-edu\",\n    \"marvellousmachine.net\": \"marvellous_machine\",\n    \"mbn.com.ua\": \"master_banner_network\",\n    \"mastertarget.ru\": \"mastertarget\",\n    \"rns.matelso.de\": \"matelso\",\n    \"matheranalytics.com\": \"mather_analytics\",\n    \"mathjax.org\": \"mathjax.org\",\n    \"nzaza.com\": \"matiro\",\n    \"matomo.cloud\": \"matomo\",\n    \"matomo.org\": \"matomo\",\n    \"piwik.org\": \"matomo\",\n    \"adsmarket.com\": \"matomy_market\",\n    \"m2pub.com\": \"matomy_market\",\n    \"matrix.org\": \"matrix\",\n    \"mb01.com\": \"maxbounty\",\n    \"maxcdn.com\": \"maxcdn\",\n    \"netdna-cdn.com\": \"maxcdn\",\n    \"netdna-ssl.com\": \"maxcdn\",\n    \"maxlab.ru\": \"maxlab\",\n    \"maxmind.com\": \"maxmind\",\n    \"maxonclick.com\": \"maxonclick_com\",\n    \"mxptint.net\": \"maxpoint_interactive\",\n    \"maxymiser.hs.llnwd.net\": \"maxymiser\",\n    \"maxymiser.net\": \"maxymiser\",\n    \"m6r.eu\": \"mbr_targeting\",\n    \"pixel.adbuyer.com\": \"mbuy\",\n    \"mcabi.mcloudglobal.com\": \"mcabi\",\n    \"scanalert.com\": \"mcafee_secure\",\n    \"ywxi.net\": \"mcafee_secure\",\n    \"mconet.biz\": \"mconet\",\n    \"mdotlabs.com\": \"mdotlabs\",\n    \"media-clic.com\": \"media-clic\",\n    \"media-imdb.com\": \"media-imdb.com\",\n    \"media.net\": \"media.net\",\n    \"mediaimpact.de\": \"media_impact\",\n    \"mookie1.com\": \"media_innovation_group\",\n    \"idntfy.ru\": \"media_today\",\n    \"s1.mediaad.org\": \"mediaad\",\n    \"mlnadvertising.com\": \"mediaglu\",\n    \"fhserve.com\": \"mediahub\",\n    \"media-lab.ai\": \"medialab\",\n    \"medialab.la\": \"medialab\",\n    \"adnet.ru\": \"medialand\",\n    \"medialand.ru\": \"medialand\",\n    \"medialead.de\": \"medialead\",\n    \"mathads.com\": \"mediamath\",\n    \"mathtag.com\": \"mediamath\",\n    \"mediametrics.ru\": \"mediametrics\",\n    \"audit.median.hu\": \"median\",\n    \"mediapass.com\": \"mediapass\",\n    \"mt.mediapostcommunication.net\": \"mediapost_communications\",\n    \"mediarithmics.com\": \"mediarithmics.com\",\n    \"tns-counter.ru\": \"mediascope\",\n    \"ad.media-servers.net\": \"mediashakers\",\n    \"adsvc1107131.net\": \"mediashift\",\n    \"mediator.media\": \"mediator.media\",\n    \"mediav.com\": \"mediav\",\n    \"adnetinteractive.com\": \"mediawhiz\",\n    \"adnetinteractive.net\": \"mediawhiz\",\n    \"mediego.com\": \"medigo\",\n    \"medleyads.com\": \"medley\",\n    \"adnet.com.tr\": \"medyanet\",\n    \"e-kolay.net\": \"medyanet\",\n    \"medyanetads.com\": \"medyanet\",\n    \"cim.meebo.com\": \"meebo_bar\",\n    \"meetrics.net\": \"meetrics\",\n    \"mxcdn.net\": \"meetrics\",\n    \"research.de.com\": \"meetrics\",\n    \"counter.megaindex.ru\": \"megaindex\",\n    \"mega.co.nz\": \"meganz\",\n    \"mega.io\": \"meganz\",\n    \"mega.nz\": \"meganz\",\n    \"mein-bmi.com\": \"mein-bmi.com\",\n    \"webvisitor.melissadata.net\": \"melissa\",\n    \"meltdsp.com\": \"melt\",\n    \"mlt01.com\": \"menlo\",\n    \"mentad.com\": \"mentad\",\n    \"mercadoclics.com\": \"mercado\",\n    \"mercadolivre.com.br\": \"mercado\",\n    \"mlstatic.com\": \"mercado\",\n    \"merchantadvantage.com\": \"merchantadvantage\",\n    \"merchenta.com\": \"merchenta\",\n    \"roia.biz\": \"mercury_media\",\n    \"cdn.merklesearch.com\": \"merkle_research\",\n    \"rkdms.com\": \"merkle_rkg\",\n    \"messenger.com\": \"messenger.com\",\n    \"ad.metanetwork.com\": \"meta_network\",\n    \"metaffiliation.com\": \"metaffiliation.com\",\n    \"netaffiliation.com\": \"metaffiliation.com\",\n    \"metalyzer.com\": \"metapeople\",\n    \"mlsat02.de\": \"metapeople\",\n    \"metrigo.com\": \"metrigo\",\n    \"metriweb.be\": \"metriweb\",\n    \"miaozhen.com\": \"miaozhen\",\n    \"microad.co.jp\": \"microad\",\n    \"microad.jp\": \"microad\",\n    \"microad.net\": \"microad\",\n    \"microadinc.com\": \"microad\",\n    \"azurewebsites.net\": \"microsoft\",\n    \"cloudapp.net\": \"microsoft\",\n    \"gfx.ms\": \"microsoft\",\n    \"microsoft.com\": \"microsoft\",\n    \"microsoftonline-p.com\": \"microsoft\",\n    \"microsoftonline.com\": \"microsoft\",\n    \"microsofttranslator.com\": \"microsoft\",\n    \"msecnd.net\": \"microsoft\",\n    \"msedge.net\": \"microsoft\",\n    \"msocdn.com\": \"microsoft\",\n    \"onestore.ms\": \"microsoft\",\n    \"s-microsoft.com\": \"microsoft\",\n    \"trouter.io\": \"microsoft\",\n    \"windows.net\": \"microsoft\",\n    \"aka.ms\": \"microsoft\",\n    \"microsoftazuread-sso.com\": \"microsoft\",\n    \"bingapis.com\": \"microsoft\",\n    \"msauth.net\": \"microsoft\",\n    \"msauthimages.net\": \"microsoft\",\n    \"msftauth.net\": \"microsoft\",\n    \"msftstatic.com\": \"microsoft\",\n    \"msidentity.com\": \"microsoft\",\n    \"nelreports.net\": \"microsoft\",\n    \"windowscentral.com\": \"microsoft\",\n    \"analytics.live.com\": \"microsoft_analytics\",\n    \"a.clarity.ms\": \"microsoft_clarity\",\n    \"b.clarity.ms\": \"microsoft_clarity\",\n    \"c.clarity.ms\": \"microsoft_clarity\",\n    \"d.clarity.ms\": \"microsoft_clarity\",\n    \"e.clarity.ms\": \"microsoft_clarity\",\n    \"f.clarity.ms\": \"microsoft_clarity\",\n    \"g.clarity.ms\": \"microsoft_clarity\",\n    \"h.clarity.ms\": \"microsoft_clarity\",\n    \"i.clarity.ms\": \"microsoft_clarity\",\n    \"j.clarity.ms\": \"microsoft_clarity\",\n    \"log.clarity.ms\": \"microsoft_clarity\",\n    \"www.clarity.ms\": \"microsoft_clarity\",\n    \"mmismm.com\": \"mindset_media\",\n    \"imgfarm.com\": \"mindspark\",\n    \"mindspark.com\": \"mindspark\",\n    \"staticimgfarm.com\": \"mindspark\",\n    \"mvtracker.com\": \"mindviz_tracker\",\n    \"minewhat.com\": \"minewhat\",\n    \"mintsapp.io\": \"mints_app\",\n    \"snackly.co\": \"minute.ly\",\n    \"snippet.minute.ly\": \"minute.ly\",\n    \"apv.configuration.minute.ly\": \"minute.ly_video\",\n    \"get.mirando.de\": \"mirando\",\n    \"mirtesen.ru\": \"mirtesen.ru\",\n    \"misterbell.com\": \"mister_bell\",\n    \"mixi.jp\": \"mixi\",\n    \"mixpanel.com\": \"mixpanel\",\n    \"mxpnl.com\": \"mixpanel\",\n    \"mxpnl.net\": \"mixpanel\",\n    \"swf.mixpo.com\": \"mixpo\",\n    \"app.mluvii.com\": \"mluvii\",\n    \"mncdn.com\": \"mncdn.com\",\n    \"moatads.com\": \"moat\",\n    \"moatpixel.com\": \"moat\",\n    \"mobicow.com\": \"mobicow\",\n    \"a.mobify.com\": \"mobify\",\n    \"mobtrks.com\": \"mobtrks.com\",\n    \"ads.mocean.mobi\": \"mocean_mobile\",\n    \"ads.moceanads.com\": \"mocean_mobile\",\n    \"chat.mochapp.com\": \"mochapp\",\n    \"intelligentpixel.modernimpact.com\": \"modern_impact\",\n    \"teljari.is\": \"modernus\",\n    \"modulepush.com\": \"modulepush.com\",\n    \"mogointeractive.com\": \"mogo_interactive\",\n    \"mokonocdn.com\": \"mokono_analytics\",\n    \"devappgrant.space\": \"monero_miner\",\n    \"monetate.net\": \"monetate\",\n    \"monetize-me.com\": \"monetize_me\",\n    \"ads.themoneytizer.com\": \"moneytizer\",\n    \"mongoosemetrics.com\": \"mongoose_metrics\",\n    \"track.monitis.com\": \"monitis\",\n    \"monitus.net\": \"monitus\",\n    \"fonts.net\": \"monotype_gmbh\",\n    \"fonts.com\": \"monotype_imaging\",\n    \"cdn.monsido.com\": \"monsido\",\n    \"monster.com\": \"monster_advertising\",\n    \"mooxar.com\": \"mooxar\",\n    \"mopinion.com\": \"mopinion.com\",\n    \"mopub.com\": \"mopub\",\n    \"ad.ad-arata.com\": \"more_communication\",\n    \"moras.jp\": \"moreads\",\n    \"nedstatbasic.net\": \"motigo_webstats\",\n    \"webstats.motigo.com\": \"motigo_webstats\",\n    \"analytics.convertlanguage.com\": \"motionpoint\",\n    \"mouseflow.com\": \"mouseflow\",\n    \"mousestats.com\": \"mousestats\",\n    \"s.mousetrace.com\": \"mousetrace\",\n    \"movad.de\": \"mov.ad\",\n    \"movad.net\": \"mov.ad\",\n    \"micpn.com\": \"movable_ink\",\n    \"mvb.me\": \"movable_media\",\n    \"moz.com\": \"moz\",\n    \"firefox.com\": \"mozilla\",\n    \"mozaws.net\": \"mozilla\",\n    \"mozgcp.net\": \"mozilla\",\n    \"mozilla.com\": \"mozilla\",\n    \"mozilla.net\": \"mozilla\",\n    \"mozilla.org\": \"mozilla\",\n    \"storage.mozoo.com\": \"mozoo\",\n    \"tracker.mrpfd.com\": \"mrp\",\n    \"mrpdata.com\": \"mrpdata\",\n    \"mrpdata.net\": \"mrpdata\",\n    \"mrskincash.com\": \"mrskincash\",\n    \"a-msedge.net\": \"msedge\",\n    \"b-msedge.net\": \"msedge\",\n    \"dual-s-msedge.net\": \"msedge\",\n    \"e-msedge.net\": \"msedge\",\n    \"k-msedge.net\": \"msedge\",\n    \"l-msedge.net\": \"msedge\",\n    \"s-msedge.net\": \"msedge\",\n    \"spo-msedge.net\": \"msedge\",\n    \"t-msedge.net\": \"msedge\",\n    \"wac-msedge.net\": \"msedge\",\n    \"msn.com\": \"msn\",\n    \"s-msn.com\": \"msn\",\n    \"musculahq.appspot.com\": \"muscula\",\n    \"litix.io\": \"mux_inc\",\n    \"mybloglog.com\": \"mybloglog\",\n    \"t.p.mybuys.com\": \"mybuys\",\n    \"mycdn.me\": \"mycdn.me\",\n    \"mycliplister.com\": \"mycliplister.com\",\n    \"mycounter.com.ua\": \"mycounter.ua\",\n    \"mycounter.ua\": \"mycounter.ua\",\n    \"myfonts.net\": \"myfonts\",\n    \"mypagerank.net\": \"mypagerank\",\n    \"stat.mystat.hu\": \"mystat\",\n    \"mythings.com\": \"mythings\",\n    \"mystat-in.net\": \"mytop_counter\",\n    \"nab.com\": \"nab\",\n    \"nab.com.au\": \"nab\",\n    \"nab.net\": \"nab\",\n    \"nabgroup.com\": \"nab\",\n    \"national.com.au\": \"nab\",\n    \"nationalaustraliabank.com.au\": \"nab\",\n    \"nationalbank.com.au\": \"nab\",\n    \"nakanohito.jp\": \"nakanohito.jp\",\n    \"namogoo.coom\": \"namogoo\",\n    \"nanigans.com\": \"nanigans\",\n    \"audiencemanager.de\": \"nano_interactive\",\n    \"nanorep.com\": \"nanorep\",\n    \"narando.com\": \"narando\",\n    \"static.bam-x.com\": \"narrativ\",\n    \"narrative.io\": \"narrative_io\",\n    \"p1.ntvk1.ru\": \"natimatica\",\n    \"nativeads.com\": \"nativeads.com\",\n    \"cdn01.nativeroll.tv\": \"nativeroll\",\n    \"ntv.io\": \"nativo\",\n    \"postrelease.com\": \"nativo\",\n    \"navdmp.com\": \"navegg_dmp\",\n    \"naver.com\": \"naver.com\",\n    \"naver.net\": \"naver.com\",\n    \"s-nbcnews.com\": \"nbc_news\",\n    \"richmedia247.com\": \"ncol\",\n    \"needle.com\": \"needle\",\n    \"nekudo.com\": \"nekudo.com\",\n    \"neodatagroup.com\": \"neodata\",\n    \"ad-srv.net\": \"neory\",\n    \"contentspread.net\": \"neory\",\n    \"neory-tm.com\": \"neory\",\n    \"simptrack.com\": \"neory\",\n    \"nerfherdersolo.com\": \"nerfherdersolo_com\",\n    \"wemfbox.ch\": \"net-metrix\",\n    \"cdnma.com\": \"net-results\",\n    \"nr7.us\": \"net-results\",\n    \"netavenir.com\": \"net_avenir\",\n    \"netcommunities.com\": \"net_communities\",\n    \"visibility-stats.com\": \"net_visibility\",\n    \"netbiscuits.net\": \"netbiscuits\",\n    \"bbtrack.net\": \"netbooster_group\",\n    \"netbooster.com\": \"netbooster_group\",\n    \"netflix.com\": \"netflix\",\n    \"nflxext.com\": \"netflix\",\n    \"nflximg.net\": \"netflix\",\n    \"nflxso.net\": \"netflix\",\n    \"nflxvideo.net\": \"netflix\",\n    \"flxvpn.net\": \"netflix\",\n    \"netflix.ca\": \"netflix\",\n    \"netflix.com.au\": \"netflix\",\n    \"netflix.net\": \"netflix\",\n    \"netflixdnstest1.com\": \"netflix\",\n    \"netflixdnstest10.com\": \"netflix\",\n    \"netflixdnstest2.com\": \"netflix\",\n    \"netflixdnstest3.com\": \"netflix\",\n    \"netflixdnstest4.com\": \"netflix\",\n    \"netflixdnstest5.com\": \"netflix\",\n    \"netflixdnstest6.com\": \"netflix\",\n    \"netflixdnstest7.com\": \"netflix\",\n    \"netflixdnstest8.com\": \"netflix\",\n    \"netflixdnstest9.com\": \"netflix\",\n    \"netflixinvestor.com\": \"netflix\",\n    \"netflixstudios.com\": \"netflix\",\n    \"netflixtechblog.com\": \"netflix\",\n    \"nflximg.com\": \"netflix\",\n    \"netify.ai\": \"netify\",\n    \"netzathleten-media.de\": \"netletix\",\n    \"netminers.dk\": \"netminers\",\n    \"netmining.com\": \"netmining\",\n    \"netmng.com\": \"netmining\",\n    \"stat.netmonitor.fi\": \"netmonitor\",\n    \"glanceguide.com\": \"netratings_sitecensus\",\n    \"imrworldwide.com\": \"netratings_sitecensus\",\n    \"vizu.com\": \"netratings_sitecensus\",\n    \"netrk.net\": \"netrk.net\",\n    \"netseer.com\": \"netseer\",\n    \"netshelter.net\": \"netshelter\",\n    \"nsaudience.pl\": \"netsprint_audience\",\n    \"nwidget.networkedblogs.com\": \"networkedblogs\",\n    \"adadvisor.net\": \"neustar_adadvisor\",\n    \"d1ros97qkrwjf5.cloudfront.net\": \"new_relic\",\n    \"newrelic.com\": \"new_relic\",\n    \"nr-data.net\": \"new_relic\",\n    \"codestream.com\": \"new_relic\",\n    \"newscgp.com\": \"newscgp.com\",\n    \"nmcdn.us\": \"newsmax\",\n    \"newstogram.com\": \"newstogram\",\n    \"newsupdatedir.info\": \"newsupdatedir.info\",\n    \"newsupdatewe.info\": \"newsupdatewe.info\",\n    \"ads.newtention.net\": \"newtention\",\n    \"ads.newtentionassets.net\": \"newtention\",\n    \"nexage.com\": \"nexage\",\n    \"nexeps.com\": \"nexeps.com\",\n    \"nxtck.com\": \"next_performance\",\n    \"track.nextuser.com\": \"next_user\",\n    \"imgsrv.nextag.com\": \"nextag_roi_optimizer\",\n    \"nextclick.pl\": \"nextclick\",\n    \"nextstat.com\": \"nextstat\",\n    \"d1d8vn0fpluuz7.cloudfront.net\": \"neytiv\",\n    \"ads.ngageinc.com\": \"ngage_inc.\",\n    \"nice264.com\": \"nice264.com\",\n    \"nimblecommerce.com\": \"nimblecommerce\",\n    \"nineanalytics.io\": \"nine_direct_digital\",\n    \"cho-chin.com\": \"ninja_access_analysis\",\n    \"donburako.com\": \"ninja_access_analysis\",\n    \"hishaku.com\": \"ninja_access_analysis\",\n    \"shinobi.jp\": \"ninja_access_analysis\",\n    \"static.nirror.com\": \"nirror\",\n    \"nitropay.com\": \"nitropay\",\n    \"nk.pl\": \"nk.pl_widgets\",\n    \"noaa.gov\": \"noaa.gov\",\n    \"track.noddus.com\": \"noddus\",\n    \"contextbar.ru\": \"nolix\",\n    \"nonli.com\": \"nonli\",\n    \"non.li\": \"nonli\",\n    \"trkme.net\": \"nonstop_consulting\",\n    \"noop.style\": \"noop.style\",\n    \"nosto.com\": \"nosto.com\",\n    \"adleadevent.com\": \"notify\",\n    \"notifyfox.com\": \"notifyfox\",\n    \"notion.so\": \"notion\",\n    \"nowinteract.com\": \"now_interact\",\n    \"npario-inc.net\": \"npario\",\n    \"nplexmedia.com\": \"nplexmedia\",\n    \"nrelate.com\": \"nrelate\",\n    \"ns8.com\": \"ns8\",\n    \"nt.vc\": \"nt.vc\",\n    \"featurelink.com\": \"ntent\",\n    \"ntp.org\": \"ntppool\",\n    \"ntppool.org\": \"ntppool\",\n    \"tracer.jp\": \"nttcom_online_marketing_solutions\",\n    \"nuffnang.com\": \"nuffnang\",\n    \"nuggad.net\": \"nugg.ad\",\n    \"rotator.adjuggler.com\": \"nui_media\",\n    \"numbers.md\": \"numbers.md\",\n    \"channeliq.com\": \"numerator\",\n    \"nyacampwk.com\": \"nyacampwk.com\",\n    \"nyetm2mkch.com\": \"nyetm2mkch.com\",\n    \"nyt.com\": \"nyt.com\",\n    \"nytimes.com\": \"nyt.com\",\n    \"o12zs3u2n.com\": \"o12zs3u2n.com\",\n    \"o2.pl\": \"o2.pl\",\n    \"o2online.de\": \"o2online.de\",\n    \"oath.com\": \"oath_inc\",\n    \"observerapp.com\": \"observer\",\n    \"ocioso.com.br\": \"ocioso\",\n    \"oclasrv.com\": \"oclasrv.com\",\n    \"octapi.net\": \"octapi.net\",\n    \"service.octavius.rocks\": \"octavius\",\n    \"office.com\": \"office.com\",\n    \"office.net\": \"office.net\",\n    \"office365.com\": \"office365.com\",\n    \"oghub.io\": \"oghub.io\",\n    \"ohmystats.com\": \"oh_my_stats\",\n    \"adohana.com\": \"ohana_advertising_network\",\n    \"photorank.me\": \"olapic\",\n    \"olark.com\": \"olark\",\n    \"olx-st.com\": \"olx-st.com\",\n    \"onap.io\": \"olx-st.com\",\n    \"omarsys.com\": \"omarsys.com\",\n    \"ometria.com\": \"ometria\",\n    \"omgpm.com\": \"omg\",\n    \"omniconvert.com\": \"omniconvert.com\",\n    \"omnidsp.com\": \"omniscienta\",\n    \"oms.eu\": \"oms\",\n    \"omsnative.de\": \"oms\",\n    \"onaudience.com\": \"onaudience\",\n    \"btc-echode.api.oneall.com\": \"oneall\",\n    \"tracking.onefeed.co.uk\": \"onefeed\",\n    \"onesignal.com\": \"onesignal\",\n    \"os.tc\": \"onesignal\",\n    \"stat.onestat.com\": \"onestat\",\n    \"ocdn.eu\": \"onet.pl\",\n    \"onet.pl\": \"onet.pl\",\n    \"onetag.com\": \"onetag\",\n    \"s-onetag.com\": \"onetag\",\n    \"onetrust.com\": \"onetrust\",\n    \"fogl1onf.com\": \"onfocus.io\",\n    \"onfocus.io\": \"onfocus.io\",\n    \"onlinewebstat.com\": \"onlinewebstat\",\n    \"onlinewebstats.com\": \"onlinewebstat\",\n    \"onswipe.com\": \"onswipe\",\n    \"onthe.io\": \"onthe.io\",\n    \"moon-ray.com\": \"ontraport_autopilot\",\n    \"moonraymarketing.com\": \"ontraport_autopilot\",\n    \"ooyala.com\": \"ooyala.com\",\n    \"openadex.dk\": \"open_adexchange\",\n    \"247realmedia.com\": \"open_adstream\",\n    \"oaserve.com\": \"open_adstream\",\n    \"realmedia.com\": \"open_adstream\",\n    \"realmediadigital.com\": \"open_adstream\",\n    \"opensharecount.com\": \"open_share_count\",\n    \"chatgpt.com\": \"openai\",\n    \"oaistatic.com\": \"openai\",\n    \"oaiusercontent.com\": \"openai\",\n    \"openai.com\": \"openai\",\n    \"oloadcdn.net\": \"openload\",\n    \"openload.co\": \"openload\",\n    \"openstat.net\": \"openstat\",\n    \"spylog.com\": \"openstat\",\n    \"spylog.ru\": \"openstat\",\n    \"opentracker.net\": \"opentracker\",\n    \"openwebanalytics.com\": \"openwebanalytics\",\n    \"odnxs.net\": \"openx\",\n    \"openx.net\": \"openx\",\n    \"openx.org\": \"openx\",\n    \"openxenterprise.com\": \"openx\",\n    \"servedbyopenx.com\": \"openx\",\n    \"adsummos.net\": \"operative_media\",\n    \"opinary.com\": \"opinary\",\n    \"opinionbar.com\": \"opinionbar\",\n    \"emagazines.com\": \"oplytic\",\n    \"allawnos.com\": \"oppo\",\n    \"allawntech.com\": \"oppo\",\n    \"heytapdl.com\": \"oppo\",\n    \"heytapmobi.com\": \"oppo\",\n    \"heytapmobile.com\": \"oppo\",\n    \"oppomobile.com\": \"oppo\",\n    \"opta.net\": \"opta.net\",\n    \"optaim.com\": \"optaim\",\n    \"cookielaw.org\": \"optanaon\",\n    \"service.optify.net\": \"optify\",\n    \"optimatic.com\": \"optimatic\",\n    \"optmd.com\": \"optimax_media_delivery\",\n    \"optimicdn.com\": \"optimicdn.com\",\n    \"optimizely.com\": \"optimizely\",\n    \"episerver.net\": \"optimizely\",\n    \"optimonk.com\": \"optimonk\",\n    \"mstrlytcs.com\": \"optinmonster\",\n    \"optmnstr.com\": \"optinmonster\",\n    \"optmstr.com\": \"optinmonster\",\n    \"optnmstr.com\": \"optinmonster\",\n    \"optincollect.com\": \"optinproject.com\",\n    \"volvelle.tech\": \"optomaton\",\n    \"ora.tv\": \"ora.tv\",\n    \"oracleinfinity.io\": \"oracle_infinity\",\n    \"instantservice.com\": \"oracle_live_help\",\n    \"ts.istrack.com\": \"oracle_live_help\",\n    \"rightnowtech.com\": \"oracle_rightnow\",\n    \"rnengage.com\": \"oracle_rightnow\",\n    \"orange.fr\": \"orange\",\n    \"orangeads.fr\": \"orange\",\n    \"ads.orange142.com\": \"orange142\",\n    \"wanadoo.fr\": \"orange_france\",\n    \"otracking.com\": \"orangesoda\",\n    \"emxdgt.com\": \"orc_international\",\n    \"static.ordergroove.com\": \"order_groove\",\n    \"orelsite.ru\": \"orel_site\",\n    \"otclick-adv.ru\": \"otclick\",\n    \"othersearch.info\": \"othersearch.info\",\n    \"otm-r.com\": \"otm-r.com\",\n    \"otto.de\": \"otto.de\",\n    \"ottogroup.media\": \"otto.de\",\n    \"outbrain.com\": \"outbrain\",\n    \"outbrainimg.com\": \"outbrain\",\n    \"live.com\": \"outlook\",\n    \"cloud.microsoft\": \"outlook\",\n    \"hotmail.com\": \"outlook\",\n    \"outlook.com\": \"outlook\",\n    \"svc.ms\": \"outlook\",\n    \"overheat.it\": \"overheat.it\",\n    \"oewabox.at\": \"owa\",\n    \"owneriq.net\": \"owneriq\",\n    \"ownpage.fr\": \"ownpage\",\n    \"owox.com\": \"owox.com\",\n    \"adconnexa.com\": \"oxamedia\",\n    \"adsbwm.com\": \"oxamedia\",\n    \"oxomi.com\": \"oxomi.com\",\n    \"oztam.com.au\": \"oztam\",\n    \"pageanalytics.space\": \"pageanalytics.space\",\n    \"blockmetrics.com\": \"pagefair\",\n    \"pagefair.com\": \"pagefair\",\n    \"pagefair.net\": \"pagefair\",\n    \"btloader.com\": \"pagefair\",\n    \"ghmedia.com\": \"pagescience\",\n    \"777seo.com\": \"paid-to-promote\",\n    \"paid-to-promote.net\": \"paid-to-promote\",\n    \"ptp22.com\": \"paid-to-promote\",\n    \"ptp33.com\": \"paid-to-promote\",\n    \"paperg.com\": \"paperg\",\n    \"pardot.com\": \"pardot\",\n    \"d1z2jf7jlzjs58.cloudfront.net\": \"parsely\",\n    \"parsely.com\": \"parsely\",\n    \"partner-ads.com\": \"partner-ads\",\n    \"passionfruitads.com\": \"passionfruit\",\n    \"pathful.com\": \"pathful\",\n    \"pay-hit.com\": \"pay-hit\",\n    \"payclick.it\": \"payclick\",\n    \"app.paykickstart.com\": \"paykickstart\",\n    \"paypal.com\": \"paypal\",\n    \"paypalobjects.com\": \"paypal\",\n    \"pcvark.com\": \"pcvark.com\",\n    \"peer39.com\": \"peer39\",\n    \"peer39.net\": \"peer39\",\n    \"peer5.com\": \"peer5.com\",\n    \"peerius.com\": \"peerius\",\n    \"pendo.io\": \"pendo.io\",\n    \"pepper.com\": \"pepper.com\",\n    \"gopjn.com\": \"pepperjam\",\n    \"pjatr.com\": \"pepperjam\",\n    \"pjtra.com\": \"pepperjam\",\n    \"pntra.com\": \"pepperjam\",\n    \"pntrac.com\": \"pepperjam\",\n    \"pntrs.com\": \"pepperjam\",\n    \"player.pepsia.com\": \"pepsia\",\n    \"perfdrive.com\": \"perfdrive.com\",\n    \"perfectaudience.com\": \"perfect_audience\",\n    \"prfct.co\": \"perfect_audience\",\n    \"perfectmarket.com\": \"perfect_market\",\n    \"perfops.io\": \"perfops\",\n    \"performgroup.com\": \"perform_group\",\n    \"analytics.performable.com\": \"performable\",\n    \"performancing.com\": \"performancing_metrics\",\n    \"performax.cz\": \"performax\",\n    \"perimeterx.net\": \"perimeterx.net\",\n    \"permutive.com\": \"permutive\",\n    \"persgroep.net\": \"persgroep\",\n    \"persianstat.com\": \"persianstat\",\n    \"code.pers.io\": \"persio\",\n    \"counter.personyze.com\": \"personyze\",\n    \"petametrics.com\": \"petametrics\",\n    \"ads.pheedo.com\": \"pheedo\",\n    \"app.phonalytics.com\": \"phonalytics\",\n    \"d2bgg7rjywcwsy.cloudfront.net\": \"phunware\",\n    \"piguiqproxy.com\": \"piguiqproxy.com\",\n    \"trgt.eu\": \"pilot\",\n    \"pingdom.net\": \"pingdom\",\n    \"pinimg.com\": \"pinterest\",\n    \"pinterest.com\": \"pinterest\",\n    \"app.pipz.io\": \"pipz\",\n    \"disabled.invalid\": \"piwik\",\n    \"piwik.pro\": \"piwik_pro_analytics_suite\",\n    \"adrta.com\": \"pixalate\",\n    \"app.pixelpop.co\": \"pixel_union\",\n    \"pixfuture.net\": \"pixfuture\",\n    \"vast1.pixfuture.com\": \"pixfuture\",\n    \"piximedia.com\": \"piximedia\",\n    \"pizzaandads.com\": \"pizzaandads_com\",\n    \"ads.placester.net\": \"placester\",\n    \"d3uemyw1e5n0jw.cloudfront.net\": \"placester\",\n    \"pladform.com\": \"pladform.ru\",\n    \"tag.bi.serviceplan.com\": \"plan.net_experience_cloud\",\n    \"pfrm.co\": \"platform360\",\n    \"impact-ad.jp\": \"platformone\",\n    \"loveadvert.ru\": \"play_by_mamba\",\n    \"playbuzz.com\": \"playbuzz.com\",\n    \"pof.com\": \"plenty_of_fish\",\n    \"plex.bz\": \"plex\",\n    \"plex.direct\": \"plex\",\n    \"plex.tv\": \"plex\",\n    \"analytics.plex.tv\": \"plex_metrics\",\n    \"metrics.plex.tv\": \"plex_metrics\",\n    \"plista.com\": \"plista\",\n    \"plugrush.com\": \"plugrush\",\n    \"pluso.ru\": \"pluso.ru\",\n    \"plutusads.com\": \"plutusads\",\n    \"pmddby.com\": \"pmddby.com\",\n    \"pnamic.com\": \"pnamic.com\",\n    \"po.st\": \"po.st\",\n    \"widgets.getpocket.com\": \"pocket\",\n    \"pocketcents.com\": \"pocketcents\",\n    \"pointificsecure.com\": \"pointific\",\n    \"pointroll.com\": \"pointroll\",\n    \"poirreleast.club\": \"poirreleast.club\",\n    \"mediavoice.com\": \"polar.me\",\n    \"polar.me\": \"polar.me\",\n    \"polarmobile.com\": \"polar.me\",\n    \"polldaddy.com\": \"polldaddy\",\n    \"polyad.net\": \"polyad\",\n    \"polyfill.io\": \"polyfill.io\",\n    \"popads.net\": \"popads\",\n    \"popadscdn.net\": \"popads\",\n    \"popcash.net\": \"popcash\",\n    \"popcashjs.b-cdn.net\": \"popcash\",\n    \"desv383oqqc0.cloudfront.net\": \"popcorn_metrics\",\n    \"popin.cc\": \"popin.cc\",\n    \"cdn.popmyads.com\": \"popmyads\",\n    \"poponclick.com\": \"poponclick\",\n    \"populis.com\": \"populis\",\n    \"populisengage.com\": \"populis\",\n    \"phncdn.com\": \"pornhub\",\n    \"pornhub.com\": \"pornhub\",\n    \"prscripts.com\": \"pornwave\",\n    \"prstatics.com\": \"pornwave\",\n    \"prwidgets.com\": \"pornwave\",\n    \"barra.brasil.gov.br\": \"porta_brazil\",\n    \"postaffiliatepro.com\": \"post_affiliate_pro\",\n    \"powerlinks.com\": \"powerlinks\",\n    \"powerreviews.com\": \"powerreviews\",\n    \"powr.io\": \"powr.io\",\n    \"api.pozvonim.com\": \"pozvonim\",\n    \"prebid.org\": \"prebid\",\n    \"precisionclick.com\": \"precisionclick\",\n    \"adserver.com.br\": \"predicta\",\n    \"predicta.net\": \"predicta\",\n    \"prnx.net\": \"premonix\",\n    \"ppjol.com\": \"press\",\n    \"ppjol.net\": \"press\",\n    \"api.pressly.com\": \"pressly\",\n    \"pricegrabber.com\": \"pricegrabber\",\n    \"cdn.pricespider.com\": \"pricespider\",\n    \"pmdrecrute.com\": \"prismamediadigital.com\",\n    \"prismamediadigital.com\": \"prismamediadigital.com\",\n    \"privy.com\": \"privy.com\",\n    \"pswec.com\": \"proclivity\",\n    \"prodperfect.com\": \"prodperfect\",\n    \"lib.productsup.io\": \"productsup\",\n    \"proadsnet.com\": \"profiliad\",\n    \"profitshare.ro\": \"profitshare\",\n    \"tracking.proformics.com\": \"proformics\",\n    \"programattik.com\": \"programattik\",\n    \"projectwonderful.com\": \"project_wonderful\",\n    \"propelmarketing.com\": \"propel_marketing\",\n    \"oclaserver.com\": \"propeller_ads\",\n    \"onclasrv.com\": \"propeller_ads\",\n    \"onclickads.net\": \"propeller_ads\",\n    \"onclkds.com\": \"propeller_ads\",\n    \"propellerads.com\": \"propeller_ads\",\n    \"propellerpops.com\": \"propeller_ads\",\n    \"proper.io\": \"propermedia\",\n    \"st-a.props.id\": \"props\",\n    \"propvideo.net\": \"propvideo_net\",\n    \"tr.prospecteye.com\": \"prospecteye\",\n    \"prosperent.com\": \"prosperent\",\n    \"prostor-lite.ru\": \"prostor\",\n    \"reports.proton.me\": \"proton_ag\",\n    \"providesupport.com\": \"provide_support\",\n    \"proximic.com\": \"proximic\",\n    \"proxistore.com\": \"proxistore.com\",\n    \"pscp.tv\": \"pscp.tv\",\n    \"pstatic.net\": \"pstatic.net\",\n    \"psyma.com\": \"psyma\",\n    \"ptengine.jp\": \"pt_engine\",\n    \"pub-fit.com\": \"pub-fit\",\n    \"pub.network\": \"pub.network\",\n    \"learnpipe.com\": \"pubble\",\n    \"pubble.co\": \"pubble\",\n    \"pubdirecte.com\": \"pubdirecte\",\n    \"pubgears.com\": \"pubgears\",\n    \"publicidees.com\": \"public_ideas\",\n    \"publicidad.net\": \"publicidad.net\",\n    \"intgr.net\": \"publir\",\n    \"pubmatic.com\": \"pubmatic\",\n    \"pubnub.com\": \"pubnub.com\",\n    \"puboclic.com\": \"puboclic\",\n    \"pulpix.com\": \"pulpix.com\",\n    \"tentaculos.net\": \"pulpo_media\",\n    \"pulse360.com\": \"pulse360\",\n    \"pulseinsights.com\": \"pulse_insights\",\n    \"contextweb.com\": \"pulsepoint\",\n    \"pulsepoint.com\": \"pulsepoint\",\n    \"punchtab.com\": \"punchtab\",\n    \"purch.com\": \"purch\",\n    \"servebom.com\": \"purch\",\n    \"purechat.com\": \"pure_chat\",\n    \"cdn.pprl.io\": \"pureprofile\",\n    \"oopt.fr\": \"purlive\",\n    \"puserving.com\": \"puserving.com\",\n    \"push.world\": \"push.world\",\n    \"pushengage.com\": \"push_engage\",\n    \"pushame.com\": \"pushame.com\",\n    \"zebra.pushbullet.com\": \"pushbullet\",\n    \"pushcrew.com\": \"pushcrew\",\n    \"pusher.com\": \"pusher.com\",\n    \"pusherapp.com\": \"pusher.com\",\n    \"pushnative.com\": \"pushnative.com\",\n    \"cdn.pushnews.eu\": \"pushnews\",\n    \"pushno.com\": \"pushno.com\",\n    \"pushwhy.com\": \"pushwhy.com\",\n    \"pushwoosh.com\": \"pushwoosh.com\",\n    \"pvclouds.com\": \"pvclouds.com\",\n    \"ads.q1media.com\": \"q1media\",\n    \"q1mediahydraplatform.com\": \"q1media\",\n    \"q-divisioncdn.de\": \"q_division\",\n    \"qbaka.net\": \"qbaka\",\n    \"track.qcri.org\": \"qcri_analytics\",\n    \"collect.qeado.com\": \"qeado\",\n    \"s.lianmeng.360.cn\": \"qihoo_360\",\n    \"qq.com\": \"qq.com\",\n    \"qrius.me\": \"qrius\",\n    \"qualaroo.com\": \"qualaroo\",\n    \"qualcomm.com\": \"qualcomm\",\n    \"gpsonextra.net\": \"qualcomm_location_service\",\n    \"izatcloud.net\": \"qualcomm_location_service\",\n    \"xtracloud.net\": \"qualcomm_location_service\",\n    \"bluecava.com\": \"qualia\",\n    \"qualtrics.com\": \"qualtrics\",\n    \"quantcast.com\": \"quantcast\",\n    \"quantserve.com\": \"quantcast\",\n    \"quantcount.com\": \"quantcount\",\n    \"quantummetric.com\": \"quantum_metric\",\n    \"quartic.pl\": \"quartic.pl\",\n    \"quarticon.com\": \"quartic.pl\",\n    \"d3c3cq33003psk.cloudfront.net\": \"qubit\",\n    \"qubit.com\": \"qubit\",\n    \"easyresearch.se\": \"questback\",\n    \"queue-it.net\": \"queue-it\",\n    \"quick-counter.net\": \"quick-counter.net\",\n    \"adsonar.com\": \"quigo_adsonar\",\n    \"qnsr.com\": \"quinstreet\",\n    \"quinstreet.com\": \"quinstreet\",\n    \"thecounter.com\": \"quinstreet\",\n    \"quintelligence.com\": \"quintelligence\",\n    \"qservz.com\": \"quisma\",\n    \"quisma.com\": \"quisma\",\n    \"quora.com\": \"quora.com\",\n    \"ads-digitalkeys.com\": \"r_advertising\",\n    \"rackcdn.com\": \"rackcdn.com\",\n    \"radarurl.com\": \"radarurl\",\n    \"dsa.csdata1.com\": \"radial\",\n    \"gwallet.com\": \"radiumone\",\n    \"r1-cdn.net\": \"radiumone\",\n    \"widget.raisenow.com\": \"raisenow\",\n    \"mediaforge.com\": \"rakuten_display\",\n    \"rmtag.com\": \"rakuten_display\",\n    \"rakuten.co.jp\": \"rakuten_globalmarket\",\n    \"trafficgate.net\": \"rakuten_globalmarket\",\n    \"mtwidget04.affiliate.rakuten.co.jp\": \"rakuten_widget\",\n    \"xml.affilliate.rakuten.co.jp\": \"rakuten_widget\",\n    \"rambler.ru\": \"rambler\",\n    \"top100.ru\": \"rambler\",\n    \"rapidspike.com\": \"rapidspike\",\n    \"ravelin.com\": \"ravelin\",\n    \"rawgit.com\": \"rawgit\",\n    \"raygun.io\": \"raygun\",\n    \"count.rbc.ru\": \"rbc_counter\",\n    \"rcs.it\": \"rcs.it\",\n    \"rcsmediagroup.it\": \"rcs.it\",\n    \"d335luupugsy2.cloudfront.net\": \"rd_station\",\n    \"rea-group.com\": \"rea_group\",\n    \"reagroupdata.com.au\": \"rea_group\",\n    \"reastatic.net\": \"rea_group\",\n    \"d12ulf131zb0yj.cloudfront.net\": \"reachforce\",\n    \"reachforce.com\": \"reachforce\",\n    \"reachjunction.com\": \"reachjunction\",\n    \"cdn.rlets.com\": \"reachlocal\",\n    \"reachlocal.com\": \"reachlocal\",\n    \"reachlocallivechat.com\": \"reachlocal\",\n    \"rlcdn.net\": \"reachlocal\",\n    \"plugin.reactful.com\": \"reactful\",\n    \"reactivpub.fr\": \"reactivpub\",\n    \"skinected.com\": \"reactx\",\n    \"readrboard.com\": \"readerboard\",\n    \"readme.com\": \"readme\",\n    \"readme.io\": \"readme\",\n    \"readspeaker.com\": \"readspeaker.com\",\n    \"realclick.co.kr\": \"realclick\",\n    \"realestate.com.au\": \"realestate.com.au\",\n    \"realperson.de\": \"realperson.de\",\n    \"powermarketing.com\": \"realtime\",\n    \"realtime.co\": \"realtime\",\n    \"webspectator.com\": \"realtime\",\n    \"dcniko1cv0rz.cloudfront.net\": \"realytics\",\n    \"realytics.io\": \"realytics\",\n    \"static.rbl.ms\": \"rebel_mouse\",\n    \"recaptcha.net\": \"recaptcha\",\n    \"recettes.net\": \"recettes.net\",\n    \"static.recopick.com\": \"recopick\",\n    \"recreativ.ru\": \"recreativ\",\n    \"analytics.recruitics.com\": \"recruitics\",\n    \"analytics.cohesionapps.com\": \"red_ventures\",\n    \"cdn.cohesionapps.com\": \"red_ventures\",\n    \"redblue.de\": \"redblue_de\",\n    \"atendesoftware.pl\": \"redcdn.pl\",\n    \"redd.it\": \"reddit\",\n    \"reddit-image.s3.amazonaws.com\": \"reddit\",\n    \"reddit.com\": \"reddit\",\n    \"redditmedia.com\": \"reddit\",\n    \"redditstatic.com\": \"reddit\",\n    \"redhelper.ru\": \"redhelper\",\n    \"pixelinteractivemedia.com\": \"redlotus\",\n    \"triggit.com\": \"redlotus\",\n    \"grt01.com\": \"redtram\",\n    \"grt02.com\": \"redtram\",\n    \"redtram.com\": \"redtram\",\n    \"rdtcdn.com\": \"redtube.com\",\n    \"redtube.com\": \"redtube.com\",\n    \"reduxmedia.com\": \"redux_media\",\n    \"reduxmediagroup.com\": \"redux_media\",\n    \"reedbusiness.net\": \"reed_business_information\",\n    \"reembed.com\": \"reembed.com\",\n    \"reevoo.com\": \"reevoo.com\",\n    \"refericon.pl\": \"refericon\",\n    \"ads.referlocal.com\": \"referlocal\",\n    \"refersion.com\": \"refersion\",\n    \"refinedads.com\": \"refined_labs\",\n    \"product.reflektion.com\": \"reflektion\",\n    \"reformal.ru\": \"reformal\",\n    \"reinvigorate.net\": \"reinvigorate\",\n    \"convertglobal.com\": \"rekko\",\n    \"convertglobal.s3.amazonaws.com\": \"rekko\",\n    \"dnhgz729v27ca.cloudfront.net\": \"rekko\",\n    \"reklamstore.com\": \"reklam_store\",\n    \"ad.reklamport.com\": \"reklamport\",\n    \"delivery.reklamz.com\": \"reklamz\",\n    \"adimg.rekmob.com\": \"rekmob\",\n    \"relap.io\": \"relap\",\n    \"svtrd.com\": \"relay42\",\n    \"synovite-scripts.com\": \"relay42\",\n    \"tdn.r42tag.com\": \"relay42\",\n    \"relestar.com\": \"relestar\",\n    \"relevant4.com\": \"relevant4.com\",\n    \"remintrex.com\": \"remintrex\",\n    \"remove.video\": \"remove.video\",\n    \"rp-api.com\": \"repost.us\",\n    \"republer.com\": \"republer.com\",\n    \"resmeter.respublica.al\": \"res-meter\",\n    \"researchnow.com\": \"research_now\",\n    \"reson8.com\": \"resonate_networks\",\n    \"respondhq.com\": \"respond\",\n    \"adinsight.com\": \"responsetap\",\n    \"adinsight.eu\": \"responsetap\",\n    \"responsetap.com\": \"responsetap\",\n    \"data.resultlinks.com\": \"result_links\",\n    \"sli-system.com\": \"resultspage.com\",\n    \"retailrocket.net\": \"retailrocket.net\",\n    \"retailrocket.ru\": \"retailrocket.net\",\n    \"shopify.retargetapp.com\": \"retarget_app\",\n    \"retargeter.com\": \"retargeter_beacon\",\n    \"retargeting.cl\": \"retargeting.cl\",\n    \"d1stxfv94hrhia.cloudfront.net\": \"retention_science\",\n    \"waves.retentionscience.com\": \"retention_science\",\n    \"reutersmedia.net\": \"reuters_media\",\n    \"revcontent.com\": \"revcontent\",\n    \"socialtwist.com\": \"reve_marketing\",\n    \"revenue.com\": \"revenue\",\n    \"clkads.com\": \"revenuehits\",\n    \"clkmon.com\": \"revenuehits\",\n    \"clkrev.com\": \"revenuehits\",\n    \"clksite.com\": \"revenuehits\",\n    \"eclkspbn.com\": \"revenuehits\",\n    \"imageshack.host\": \"revenuehits\",\n    \"revenuemantra.com\": \"revenuemantra\",\n    \"revive-adserver.com\": \"revive_adserver\",\n    \"revolvermaps.com\": \"revolver_maps\",\n    \"cts.tradepub.com\": \"revresponse\",\n    \"revresponse.com\": \"revresponse\",\n    \"incontext.pl\": \"rewords\",\n    \"pl-engine.intextad.net\": \"rewords\",\n    \"addesktop.com\": \"rhythmone\",\n    \"1rx.io\": \"rhythmone_beacon\",\n    \"ria.ru\": \"ria.ru\",\n    \"rmbn.ru\": \"rich_media_banner_network\",\n    \"ics0.com\": \"richrelevance\",\n    \"richrelevance.com\": \"richrelevance\",\n    \"ringier.ch\": \"ringier.ch\",\n    \"meteorsolutions.com\": \"rio_seo\",\n    \"riskified.com\": \"riskfield.com\",\n    \"rncdn3.com\": \"rncdn3.com\",\n    \"ro2.biz\": \"ro2.biz\",\n    \"rbxcdn.com\": \"roblox\",\n    \"getrockerbox.com\": \"rockerbox\",\n    \"rocket.la\": \"rocket.ia\",\n    \"trk.sodoit.com\": \"roi_trax\",\n    \"collector.roistat.com\": \"roistat\",\n    \"rollad.ru\": \"rollad\",\n    \"d37gvrvc0wt4s1.cloudfront.net\": \"rollbar\",\n    \"get.roost.me\": \"roost\",\n    \"getrooster.com\": \"rooster\",\n    \"rqtrk.eu\": \"roq.ad\",\n    \"rotaban.ru\": \"rotaban\",\n    \"routenplaner-karten.com\": \"routenplaner-karten.com\",\n    \"rovion.com\": \"rovion\",\n    \"rsspump.com\": \"rsspump\",\n    \"creativecdn.com\": \"rtb_house\",\n    \"rvty.net\": \"rtblab\",\n    \"rtbsuperhub.com\": \"rtbsuperhub.com\",\n    \"rtl.de\": \"rtl_group\",\n    \"static-fra.de\": \"rtl_group\",\n    \"technical-service.net\": \"rtl_group\",\n    \"rtmark.net\": \"rtmark.net\",\n    \"dpclk.com\": \"rubicon\",\n    \"mobsmith.com\": \"rubicon\",\n    \"nearbyad.com\": \"rubicon\",\n    \"rubiconproject.com\": \"rubicon\",\n    \"tracker.ruhrgebiet-onlineservices.de\": \"ruhrgebiet\",\n    \"click.rummycircle.com\": \"rummycircle\",\n    \"runadtag.com\": \"run\",\n    \"rundsp.com\": \"run\",\n    \"un-syndicate.com\": \"runative\",\n    \"cdn.secretrune.com\": \"rune\",\n    \"runmewivel.com\": \"runmewivel.com\",\n    \"rhythmxchange.com\": \"rythmxchange\",\n    \"s24.com\": \"s24_com\",\n    \"s3xified.com\": \"s3xified.com\",\n    \"camp.sabavision.com\": \"sabavision\",\n    \"sageanalyst.net\": \"sagemetrics\",\n    \"sail-horizon.com\": \"sailthru_horizon\",\n    \"sail-personalize.com\": \"sailthru_horizon\",\n    \"sailthru.com\": \"sailthru_horizon\",\n    \"d16fk4ms6rqz1v.cloudfront.net\": \"salecycle\",\n    \"salecycle.com\": \"salecycle\",\n    \"api.salesfeed.com\": \"sales_feed\",\n    \"salesmanago.com\": \"sales_manago\",\n    \"salesmanago.pl\": \"sales_manago\",\n    \"force.com\": \"salesforce.com\",\n    \"salesforce.com\": \"salesforce.com\",\n    \"liveagentforsalesforce.com\": \"salesforce_live_agent\",\n    \"salesforceliveagent.com\": \"salesforce_live_agent\",\n    \"msgapp.com\": \"salesfusion\",\n    \"salespidermedia.com\": \"salespider_media\",\n    \"salesviewer.com\": \"salesviewer\",\n    \"samba.tv\": \"samba.tv\",\n    \"game-mode.net\": \"samsung\",\n    \"gos-gsp.io\": \"samsung\",\n    \"lldns.net\": \"samsung\",\n    \"pavv.co.kr\": \"samsung\",\n    \"remotesamsung.com\": \"samsung\",\n    \"samsung-gamelauncher.com\": \"samsung\",\n    \"samsung.co.kr\": \"samsung\",\n    \"samsung.com\": \"samsung\",\n    \"samsung.com.cn\": \"samsung\",\n    \"samsungcloud.com\": \"samsung\",\n    \"samsungcloudcdn.com\": \"samsung\",\n    \"samsungcloudprint.com\": \"samsung\",\n    \"samsungcloudsolution.com\": \"samsung\",\n    \"samsungcloudsolution.net\": \"samsung\",\n    \"samsungelectronics.com\": \"samsung\",\n    \"samsunghealth.com\": \"samsung\",\n    \"samsungiotcloud.com\": \"samsung\",\n    \"samsungknox.com\": \"samsung\",\n    \"samsungnyc.com\": \"samsung\",\n    \"samsungosp.com\": \"samsung\",\n    \"samsungotn.net\": \"samsung\",\n    \"samsungpositioning.com\": \"samsung\",\n    \"samsungqbe.com\": \"samsung\",\n    \"samsungrm.net\": \"samsung\",\n    \"samsungrs.com\": \"samsung\",\n    \"samsungsemi.com\": \"samsung\",\n    \"samsungsetup.com\": \"samsung\",\n    \"samsungusa.com\": \"samsung\",\n    \"secb2b.com\": \"samsung\",\n    \"smartthings.com\": \"samsung\",\n    \"adgear.com\": \"samsungads\",\n    \"adgrx.com\": \"samsungads\",\n    \"samsungacr.com\": \"samsungads\",\n    \"samsungadhub.com\": \"samsungads\",\n    \"samsungads.com\": \"samsungads\",\n    \"samsungtifa.com\": \"samsungads\",\n    \"aibixby.com\": \"samsungapps\",\n    \"findmymobile.samsung.com\": \"samsungapps\",\n    \"samsapps.cust.lldns.net\": \"samsungapps\",\n    \"samsung-omc.com\": \"samsungapps\",\n    \"samsungapps.com\": \"samsungapps\",\n    \"samsungdiroute.net\": \"samsungapps\",\n    \"samsungdive.com\": \"samsungapps\",\n    \"samsungdm.com\": \"samsungapps\",\n    \"samsungdmroute.com\": \"samsungapps\",\n    \"samsungmdec.com\": \"samsungapps\",\n    \"samsungvisioncloud.com\": \"samsungapps\",\n    \"sbixby.com\": \"samsungapps\",\n    \"ospserver.net\": \"samsungmobile\",\n    \"samsungdms.net\": \"samsungmobile\",\n    \"samsungmax.com\": \"samsungmobile\",\n    \"samsungmobile.com\": \"samsungmobile\",\n    \"secmobilesvc.com\": \"samsungmobile\",\n    \"push.samsungosp.com\": \"samsungpush\",\n    \"pushmessage.samsung.com\": \"samsungpush\",\n    \"scs.samsungqbe.com\": \"samsungpush\",\n    \"ssp.samsung.com\": \"samsungpush\",\n    \"samsungsds.com\": \"samsungsds\",\n    \"internetat.tv\": \"samsungtv\",\n    \"samsungcloud.tv\": \"samsungtv\",\n    \"tizenservice.com\": \"samsungtv\",\n    \"ilsemedia.nl\": \"sanoma.fi\",\n    \"sanoma.fi\": \"sanoma.fi\",\n    \"d13im3ek7neeqp.cloudfront.net\": \"sap_crm\",\n    \"d28ethi6slcjbm.cloudfront.net\": \"sap_crm\",\n    \"d2uevgmgh16uk4.cloudfront.net\": \"sap_crm\",\n    \"d3m83gvgzupli.cloudfront.net\": \"sap_crm\",\n    \"saas.seewhy.com\": \"sap_crm\",\n    \"leadforce1.com\": \"sap_sales_cloud\",\n    \"vlog.leadformix.com\": \"sap_sales_cloud\",\n    \"sap-xm.org\": \"sap_xm\",\n    \"sape.ru\": \"sape.ru\",\n    \"js.sl.pt\": \"sapo_ads\",\n    \"aimatch.com\": \"sas\",\n    \"sas.com\": \"sas\",\n    \"say.ac\": \"say.ac\",\n    \"ads.saymedia.com\": \"say_media\",\n    \"srv.sayyac.net\": \"sayyac\",\n    \"scarabresearch.com\": \"scarabresearch\",\n    \"schibsted.com\": \"schibsted\",\n    \"schibsted.io\": \"schibsted\",\n    \"schneevonmorgen.com\": \"schneevonmorgen.com\",\n    \"svonm.com\": \"schneevonmorgen.com\",\n    \"rockabox.co\": \"scoota\",\n    \"scorecardresearch.com\": \"scorecard_research_beacon\",\n    \"scoreresearch.com\": \"scorecard_research_beacon\",\n    \"scrsrch.com\": \"scorecard_research_beacon\",\n    \"securestudies.com\": \"scorecard_research_beacon\",\n    \"scout.scoutanalytics.net\": \"scout_analytics\",\n    \"scribblelive.com\": \"scribblelive\",\n    \"scribol.com\": \"scribol\",\n    \"analytics.snidigital.com\": \"scripps_analytics\",\n    \"scroll.com\": \"scroll\",\n    \"scupio.com\": \"scupio\",\n    \"search123.uk.com\": \"search123\",\n    \"searchforce.net\": \"searchforce\",\n    \"searchignite.com\": \"searchignite\",\n    \"srtk.net\": \"searchrev\",\n    \"tacticalrepublic.com\": \"second_media\",\n    \"sectigo.com\": \"sectigo\",\n    \"securedtouch.com\": \"securedtouch\",\n    \"securedvisit.com\": \"securedvisit\",\n    \"bacontent.de\": \"seeding_alliance\",\n    \"nativendo.de\": \"seeding_alliance\",\n    \"seedtag.com\": \"seedtag.com\",\n    \"svlu.net\": \"seevolution\",\n    \"d2dq2ahtl5zl1z.cloudfront.net\": \"segment\",\n    \"d47xnnr8b1rki.cloudfront.net\": \"segment\",\n    \"segment.com\": \"segment\",\n    \"segment.io\": \"segment\",\n    \"rutarget.ru\": \"segmento\",\n    \"segmint.net\": \"segmint\",\n    \"sekindo.com\": \"sekindo\",\n    \"sellpoint.net\": \"sellpoints\",\n    \"sellpoints.com\": \"sellpoints\",\n    \"semantiqo.com\": \"semantiqo.com\",\n    \"semasio.net\": \"semasio\",\n    \"semilo.com\": \"semilo\",\n    \"semknox.com\": \"semknox.com\",\n    \"sibautomation.com\": \"sendinblue\",\n    \"sendpulse.com\": \"sendpulse.com\",\n    \"sendsay.ru\": \"sendsay\",\n    \"track.sensedigital.in\": \"sense_digital\",\n    \"static.sensorsdata.cn\": \"sensors_data\",\n    \"sentifi.com\": \"sentifi.com\",\n    \"d3nslu0hdya83q.cloudfront.net\": \"sentry\",\n    \"getsentry.com\": \"sentry\",\n    \"ravenjs.com\": \"sentry\",\n    \"sentry.io\": \"sentry\",\n    \"sepyra.com\": \"sepyra\",\n    \"d2oh4tlt9mrke9.cloudfront.net\": \"sessioncam\",\n    \"sessioncam.com\": \"sessioncam\",\n    \"sessionly.io\": \"sessionly\",\n    \"71i.de\": \"sevenone_media\",\n    \"sexad.net\": \"sexadnetwork\",\n    \"ads.sexinyourcity.com\": \"sexinyourcity\",\n    \"sextracker.com\": \"sextracker\",\n    \"sexypartners.net\": \"sexypartners.net\",\n    \"im.cz\": \"seznam\",\n    \"imedia.cz\": \"seznam\",\n    \"szn.cz\": \"seznam\",\n    \"dtym7iokkjlif.cloudfront.net\": \"shareaholic\",\n    \"shareaholic.com\": \"shareaholic\",\n    \"shareasale.com\": \"shareasale\",\n    \"quintrics.nl\": \"sharecompany\",\n    \"sharecompany.nl\": \"sharecompany\",\n    \"sharepointonline.com\": \"sharepoint\",\n    \"onmicrosoft.com\": \"sharepoint\",\n    \"sharepoint.com\": \"sharepoint\",\n    \"sharethis.com\": \"sharethis\",\n    \"shareth.ru\": \"sharethrough\",\n    \"sharethrough.com\": \"sharethrough\",\n    \"marketingautomation.services\": \"sharpspring\",\n    \"sharpspring.com\": \"sharpspring\",\n    \"sheego.de\": \"sheego.de\",\n    \"services.sheerid.com\": \"sheerid\",\n    \"shinystat.com\": \"shinystat\",\n    \"shinystat.it\": \"shinystat\",\n    \"app.shoptarget.com.br\": \"shop_target\",\n    \"retargeter.com.br\": \"shop_target\",\n    \"shopauskunft.de\": \"shopauskunft.de\",\n    \"shopgate.com\": \"shopgate.com\",\n    \"shopify.com\": \"shopify\",\n    \"shopifycdn.com\": \"shopify\",\n    \"cdn.shopify.com\": \"shopify\",\n    \"myshopify.com\": \"shopify\",\n    \"shop.app\": \"shopify\",\n    \"shopify.co.za\": \"shopify\",\n    \"shopify.com.au\": \"shopify\",\n    \"shopify.com.mx\": \"shopify\",\n    \"shopify.dev\": \"shopify\",\n    \"shopifyapps.com\": \"shopify\",\n    \"shopifycdn.net\": \"shopify\",\n    \"shopifynetwork.com\": \"shopify\",\n    \"shopifypreview.com\": \"shopify\",\n    \"shopifysvc.com\": \"shopify_stats\",\n    \"stats.shopify.com\": \"shopify_stats\",\n    \"v.shopify.com\": \"shopify_stats\",\n    \"shopifycloud.com\": \"shopifycloud.com\",\n    \"shopperapproved.com\": \"shopper_approved\",\n    \"shoppingshadow.com\": \"shopping_com\",\n    \"tracking.shopping-flux.com\": \"shopping_flux\",\n    \"shoprunner.com\": \"shoprunner\",\n    \"shopsocially.com\": \"shopsocially\",\n    \"shopzilla.com\": \"shopzilla\",\n    \"shortnews.de\": \"shortnews\",\n    \"showrss.info\": \"showrss\",\n    \"shink.in\": \"shrink\",\n    \"shutterstock.com\": \"shutterstock\",\n    \"siblesectiveal.club\": \"siblesectiveal.club\",\n    \"d3v27wwd40f0xu.cloudfront.net\": \"sidecar\",\n    \"getsidecar.com\": \"sidecar\",\n    \"dtlilztwypawv.cloudfront.net\": \"sift_science\",\n    \"siftscience.com\": \"sift_science\",\n    \"btstatic.com\": \"signal\",\n    \"signal.co\": \"signal\",\n    \"thebrighttag.com\": \"signal\",\n    \"cdn-scripts.signifyd.com\": \"signifyd\",\n    \"signifyd.com\": \"signifyd\",\n    \"gw-services.vtrenz.net\": \"silverpop\",\n    \"mkt51.net\": \"silverpop\",\n    \"mkt912.com\": \"silverpop\",\n    \"mkt922.com\": \"silverpop\",\n    \"mkt941.com\": \"silverpop\",\n    \"pages01.net\": \"silverpop\",\n    \"pages02.net\": \"silverpop\",\n    \"pages04.net\": \"silverpop\",\n    \"pages05.net\": \"silverpop\",\n    \"similardeals.net\": \"similardeals.net\",\n    \"similarweb.com\": \"similarweb\",\n    \"similarweb.io\": \"similarweb\",\n    \"d8rk54i4mohrb.cloudfront.net\": \"simplereach\",\n    \"simplereach.com\": \"simplereach\",\n    \"simpli.fi\": \"simpli.fi\",\n    \"sina.com.cn\": \"sina\",\n    \"sinaimg.cn\": \"sina_cdn\",\n    \"reporting.singlefeed.com\": \"singlefeed\",\n    \"sddan.com\": \"sirdata\",\n    \"site24x7rum.com\": \"site24x7\",\n    \"site24x7rum.eu\": \"site24x7\",\n    \"sitebooster-fjfmworld-production.azureedge.net\": \"site_booster\",\n    \"a5.ogt.jp\": \"site_stratos\",\n    \"siteapps.com\": \"siteapps\",\n    \"sitebro.com\": \"sitebro\",\n    \"sitebro.com.tw\": \"sitebro\",\n    \"sitebro.net\": \"sitebro\",\n    \"sitebro.tw\": \"sitebro\",\n    \"siteheart.com\": \"siteheart\",\n    \"siteimprove.com\": \"siteimprove\",\n    \"siteimproveanalytics.com\": \"siteimprove_analytics\",\n    \"sitelabweb.com\": \"sitelabweb.com\",\n    \"sitemeter.com\": \"sitemeter\",\n    \"pixel.ad\": \"sitescout\",\n    \"sitescout.com\": \"sitescout\",\n    \"ad.sitemaji.com\": \"sitetag\",\n    \"sitetag.us\": \"sitetag\",\n    \"analytics.sitewit.com\": \"sitewit\",\n    \"ads.sixapart.com\": \"six_apart_advertising\",\n    \"sixt-neuwagen.de\": \"sixt-neuwagen.de\",\n    \"skadtec.com\": \"skadtec.com\",\n    \"redirectingat.com\": \"skimlinks\",\n    \"skimlinks.com\": \"skimlinks\",\n    \"skimresources.com\": \"skimlinks\",\n    \"analytics.skroutz.gr\": \"skroutz\",\n    \"skyglue.com\": \"skyglue\",\n    \"skype.com\": \"skype\",\n    \"skypeassets.com\": \"skype\",\n    \"skysa.com\": \"skysa\",\n    \"skyscnr.com\": \"skyscnr.com\",\n    \"slack-edge.com\": \"slack\",\n    \"slack-imgs.com\": \"slack\",\n    \"slack.com\": \"slack\",\n    \"slackb.com\": \"slack\",\n    \"slashdot.org\": \"slashdot_widget\",\n    \"sleeknotestaticcontent.sleeknote.com\": \"sleeknote\",\n    \"resultspage.com\": \"sli_systems\",\n    \"builder.extensionfactory.com\": \"slice_factory\",\n    \"freeskreen.com\": \"slimcutmedia\",\n    \"slingpic.com\": \"slingpic\",\n    \"smaato.net\": \"smaato\",\n    \"smart4ads.com\": \"smart4ads\",\n    \"sascdn.com\": \"smart_adserver\",\n    \"smartadserver.com\": \"smart_adserver\",\n    \"styria-digital.com\": \"smart_adserver\",\n    \"yoc-adserver.com\": \"smart_adserver\",\n    \"smartcall.kz\": \"smart_call\",\n    \"getsmartcontent.com\": \"smart_content\",\n    \"smartdevicemedia.com\": \"smart_device_media\",\n    \"x.cnt.my\": \"smart_leads\",\n    \"tracking.smartselling.cz\": \"smart_selling\",\n    \"bepolite.eu\": \"smartad\",\n    \"smartbn.ru\": \"smartbn\",\n    \"smartclick.net\": \"smartclick.net\",\n    \"smartclip.net\": \"smartclip\",\n    \"smartcontext.pl\": \"smartcontext\",\n    \"d1n00d49gkbray.cloudfront.net\": \"smarter_remarketer\",\n    \"dhxtx5wtu812h.cloudfront.net\": \"smarter_remarketer\",\n    \"smartertravel.com\": \"smarter_travel\",\n    \"travelsmarter.net\": \"smarter_travel\",\n    \"smct.co\": \"smarterclick\",\n    \"smartertrack.com\": \"smartertrack\",\n    \"smartlink.cool\": \"smartlink.cool\",\n    \"getsmartlook.com\": \"smartlook\",\n    \"smartlook.com\": \"smartlook\",\n    \"smartstream.tv\": \"smartstream.tv\",\n    \"smartsuppchat.com\": \"smartsupp_chat\",\n    \"smi2.net\": \"smi2.ru\",\n    \"smi2.ru\": \"smi2.ru\",\n    \"stat.media\": \"smi2.ru\",\n    \"cdn.smooch.io\": \"smooch\",\n    \"smowtion.com\": \"smowtion\",\n    \"smxindia.in\": \"smx_ventures\",\n    \"smyte.com\": \"smyte\",\n    \"snacktv.de\": \"snacktv\",\n    \"snap.com\": \"snap\",\n    \"addlive.io\": \"snap\",\n    \"feelinsonice.com\": \"snap\",\n    \"sc-cdn.net\": \"snap\",\n    \"sc-corp.net\": \"snap\",\n    \"sc-gw.com\": \"snap\",\n    \"sc-jpl.com\": \"snap\",\n    \"sc-prod.net\": \"snap\",\n    \"snap-dev.net\": \"snap\",\n    \"snapads.com\": \"snap\",\n    \"snapkit.com\": \"snap\",\n    \"snapengage.com\": \"snap_engage\",\n    \"sc-static.net\": \"snapchat\",\n    \"snapchat.com\": \"snapchat\",\n    \"snapcraft.io\": \"snapcraft\",\n    \"snapcraftcontent.com\": \"snapcraft\",\n    \"h-bid.com\": \"snigelweb\",\n    \"eu2.snoobi.eu\": \"snoobi\",\n    \"snoobi.com\": \"snoobi_analytics\",\n    \"d346whrrklhco7.cloudfront.net\": \"snowplow\",\n    \"d78fikflryjgj.cloudfront.net\": \"snowplow\",\n    \"dc8xl0ndzn2cb.cloudfront.net\": \"snowplow\",\n    \"playwire.com\": \"snowplow\",\n    \"snplow.net\": \"snowplow\",\n    \"go-mpulse.net\": \"soasta_mpulse\",\n    \"mpstat.us\": \"soasta_mpulse\",\n    \"tiaa-cref.org\": \"soasta_mpulse\",\n    \"sociablelabs.com\": \"sociable_labs\",\n    \"socialamp.com\": \"social_amp\",\n    \"socialannex.com\": \"social_annex\",\n    \"soclminer.com.br\": \"social_miner\",\n    \"duu8lzqdm8tsz.cloudfront.net\": \"socialbeat\",\n    \"ratevoice.com\": \"socialrms\",\n    \"sociaplus.com\": \"sociaplus.com\",\n    \"sociomantic.com\": \"sociomantic\",\n    \"images.sohu.com\": \"sohu\",\n    \"sojern.com\": \"sojern\",\n    \"sokrati.com\": \"sokrati\",\n    \"solads.media\": \"solads.media\",\n    \"solaredge.com\": \"solaredge\",\n    \"solidopinion.com\": \"solidopinion\",\n    \"pixel.solvemedia.com\": \"solve_media\",\n    \"soma2.de\": \"soma_2\",\n    \"mobileadtrading.com\": \"somoaudience\",\n    \"sonobi.com\": \"sonobi\",\n    \"sonos.com\": \"sonos\",\n    \"sophus3.com\": \"sophus3\",\n    \"deployads.com\": \"sortable\",\n    \"sndcdn.com\": \"soundcloud\",\n    \"soundcloud.com\": \"soundcloud\",\n    \"provenpixel.com\": \"sourceknowledge_pixel\",\n    \"decenthat.com\": \"sourcepoint\",\n    \"summerhamster.com\": \"sourcepoint\",\n    \"d3pkae9owd2lcf.cloudfront.net\": \"sovrn\",\n    \"lijit.com\": \"sovrn\",\n    \"onscroll.com\": \"sovrn_viewability_solutions\",\n    \"rts.sparkstudios.com\": \"spark_studios\",\n    \"sparkasse.de\": \"sparkasse.de\",\n    \"speakpipe.com\": \"speakpipe\",\n    \"adviva.net\": \"specific_media\",\n    \"specificclick.net\": \"specific_media\",\n    \"specificmedia.com\": \"specific_media\",\n    \"spectate.com\": \"spectate\",\n    \"speedshiftmedia.com\": \"speed_shift_media\",\n    \"speedcurve.com\": \"speedcurve\",\n    \"admarket.entireweb.com\": \"speedyads\",\n    \"affiliate.entireweb.com\": \"speedyads\",\n    \"sa.entireweb.com\": \"speedyads\",\n    \"speee-ad.akamaized.net\": \"speee\",\n    \"sphere.com\": \"sphere\",\n    \"surphace.com\": \"sphere\",\n    \"api.spheremall.com\": \"spheremall\",\n    \"zdwidget3-bs.sphereup.com\": \"sphereup\",\n    \"static.sspicy.ru\": \"spicy\",\n    \"spider.ad\": \"spider.ad\",\n    \"metrics.spiderads.eu\": \"spider_ads\",\n    \"spn.ee\": \"spinnakr\",\n    \"embed.spokenlayer.com\": \"spokenlayer\",\n    \"spongecell.com\": \"spongecell\",\n    \"sponsorads.de\": \"sponsorads.de\",\n    \"sportsbetaffiliates.com.au\": \"sportsbet_affiliates\",\n    \"spot.im\": \"spot.im\",\n    \"spoteffects.net\": \"spoteffect\",\n    \"scdn.co\": \"spotify\",\n    \"spotify.com\": \"spotify\",\n    \"pscdn.co\": \"spotify\",\n    \"spotifycdn.com\": \"spotify\",\n    \"spotifycdn.net\": \"spotify\",\n    \"spotilocal.com\": \"spotify\",\n    \"embed.spotify.com\": \"spotify_embed\",\n    \"spotscenered.info\": \"spotscenered.info\",\n    \"spotx.tv\": \"spotxchange\",\n    \"spotxcdn.com\": \"spotxchange\",\n    \"spotxchange.com\": \"spotxchange\",\n    \"spoutable.com\": \"spoutable\",\n    \"cdn.springboardplatform.com\": \"springboard\",\n    \"springserve.com\": \"springserve\",\n    \"pixel.sprinklr.com\": \"sprinklr\",\n    \"stat.sputnik.ru\": \"sputnik\",\n    \"email-match.com\": \"squadata\",\n    \"squarespace.com\": \"squarespace.com\",\n    \"srvtrck.com\": \"srvtrck.com\",\n    \"srvvtrk.com\": \"srvvtrk.com\",\n    \"sstatic.net\": \"sstatic.net\",\n    \"hatena.ne.jp\": \"st-hatena\",\n    \"st-hatena.com\": \"st-hatena\",\n    \"stackadapt.com\": \"stackadapt\",\n    \"stackpathdns.com\": \"stackpathdns.com\",\n    \"stailamedia.com\": \"stailamedia_com\",\n    \"stalluva.pro\": \"stalluva.pro\",\n    \"startappservice.com\": \"startapp\",\n    \"hit.stat24.com\": \"stat24\",\n    \"adstat.4u.pl\": \"stat4u\",\n    \"stat.4u.pl\": \"stat4u\",\n    \"statcounter.com\": \"statcounter\",\n    \"stathat.com\": \"stathat\",\n    \"statisfy.net\": \"statisfy\",\n    \"statsy.net\": \"statsy.net\",\n    \"statuscake.com\": \"statuscake\",\n    \"statuspage.io\": \"statuspage.io\",\n    \"stspg-customer.com\": \"statuspage.io\",\n    \"stayfriends.de\": \"stayfriends.de\",\n    \"steelhousemedia.com\": \"steelhouse\",\n    \"steepto.com\": \"steepto.com\",\n    \"stepstone.com\": \"stepstone.com\",\n    \"4stats.de\": \"stetic\",\n    \"stetic.com\": \"stetic\",\n    \"stickyadstv.com\": \"stickyads\",\n    \"stocktwits.com\": \"stocktwits\",\n    \"storify.com\": \"storify\",\n    \"storygize.net\": \"storygize\",\n    \"bizsolutions.strands.com\": \"strands_recommender\",\n    \"strava.com\": \"strava\",\n    \"mailfoogae.appspot.com\": \"streak\",\n    \"streamotion.com.au\": \"streamotion\",\n    \"streamrail.com\": \"streamrail.com\",\n    \"streamrail.net\": \"streamrail.com\",\n    \"stridespark.com\": \"stride\",\n    \"stripcdn.com\": \"stripchat.com\",\n    \"stripchat.com\": \"stripchat.com\",\n    \"stripe.com\": \"stripe.com\",\n    \"stripe.network\": \"stripe.com\",\n    \"stripst.com\": \"stripst.com\",\n    \"interactivemedia.net\": \"stroer_digital_media\",\n    \"stroeerdigitalgroup.de\": \"stroer_digital_media\",\n    \"stroeerdigitalmedia.de\": \"stroer_digital_media\",\n    \"stroeerdp.de\": \"stroer_digital_media\",\n    \"stroeermediabrands.de\": \"stroer_digital_media\",\n    \"spklw.com\": \"strossle\",\n    \"sprinklecontent.com\": \"strossle\",\n    \"strossle.it\": \"strossle\",\n    \"struq.com\": \"struq\",\n    \"stumble-upon.com\": \"stumbleupon_widgets\",\n    \"stumbleupon.com\": \"stumbleupon_widgets\",\n    \"su.pr\": \"stumbleupon_widgets\",\n    \"sub2tech.com\": \"sub2\",\n    \"ayads.co\": \"sublime_skinz\",\n    \"suggest.io\": \"suggest.io\",\n    \"sumologic.com\": \"sumologic.com\",\n    \"sumo.com\": \"sumome\",\n    \"sumome.com\": \"sumome\",\n    \"sundaysky.com\": \"sundaysky\",\n    \"supercell.com\": \"supercell\",\n    \"supercellsupport.com\": \"supercell\",\n    \"supercounters.com\": \"supercounters\",\n    \"superfastcdn.com\": \"superfastcdn.com\",\n    \"socdm.com\": \"supership\",\n    \"supplyframe.com\": \"supplyframe\",\n    \"surfingbird.ru\": \"surf_by_surfingbird\",\n    \"px.surveywall-api.survata.com\": \"survata\",\n    \"cdn.sweettooth.io\": \"sweettooth\",\n    \"swiftypecdn.com\": \"swiftype\",\n    \"swisscom.ch\": \"swisscom\",\n    \"myswitchads.com\": \"switch_concepts\",\n    \"switchadhub.com\": \"switch_concepts\",\n    \"switchads.com\": \"switch_concepts\",\n    \"switchafrica.com\": \"switch_concepts\",\n    \"switch.tv\": \"switchtv\",\n    \"shopximity.com\": \"swoop\",\n    \"swoop.com\": \"swoop\",\n    \"analytics-cdn.sykescottages.co.uk\": \"sykes\",\n    \"norton.com\": \"symantec\",\n    \"seal.verisign.com\": \"symantec\",\n    \"symantec.com\": \"symantec\",\n    \"d.hodes.com\": \"symphony_talent\",\n    \"technorati.com\": \"synacor\",\n    \"technoratimedia.com\": \"synacor\",\n    \"cn.clickable.net\": \"syncapse\",\n    \"synergy-e.com\": \"synergy-e\",\n    \"sdp-campaign.de\": \"t-mobile\",\n    \"t-online.de\": \"t-mobile\",\n    \"telekom-dienste.de\": \"t-mobile\",\n    \"telekom.com\": \"t-mobile\",\n    \"telekom.de\": \"t-mobile\",\n    \"toi.de\": \"t-mobile\",\n    \"t8cdn.com\": \"t8cdn.com\",\n    \"tableteducation.com\": \"tableteducation.com\",\n    \"basebanner.com\": \"taboola\",\n    \"taboola.com\": \"taboola\",\n    \"taboolasyndication.com\": \"taboola\",\n    \"tacoda.net\": \"tacoda\",\n    \"commander1.com\": \"tag_commander\",\n    \"tagcommander.com\": \"tag_commander\",\n    \"tags.tagcade.com\": \"tagcade\",\n    \"taggify.net\": \"taggify\",\n    \"taggyad.jp\": \"taggy\",\n    \"levexis.com\": \"tagman\",\n    \"tailtarget.com\": \"tail_target\",\n    \"tailsweep.com\": \"tailsweep\",\n    \"tamedia.ch\": \"tamedia.ch\",\n    \"tanx.com\": \"tanx\",\n    \"alipcsec.com\": \"taobao\",\n    \"taobao.com\": \"taobao\",\n    \"tapad.com\": \"tapad\",\n    \"theblogfrog.com\": \"tapinfluence\",\n    \"tarafdari.com\": \"tarafdari\",\n    \"target2sell.com\": \"target_2_sell\",\n    \"trackmytarget.com\": \"target_circle\",\n    \"cdn.targetfuel.com\": \"target_fuel\",\n    \"tawk.to\": \"tawk\",\n    \"tbn.ru\": \"tbn.ru\",\n    \"tchibo-content.de\": \"tchibo_de\",\n    \"tchibo.de\": \"tchibo_de\",\n    \"tdsrmbl.net\": \"tdsrmbl_net\",\n    \"teads.tv\": \"teads\",\n    \"tealeaf.ibmcloud.com\": \"tealeaf\",\n    \"tealium.com\": \"tealium\",\n    \"tealium.hs.llnwd.net\": \"tealium\",\n    \"tealiumiq.com\": \"tealium\",\n    \"tiqcdn.com\": \"tealium\",\n    \"teaser.cc\": \"teaser.cc\",\n    \"emailretargeting.com\": \"tedemis\",\n    \"tracking.dsmmadvantage.com\": \"teletech\",\n    \"telstra.com\": \"telstra\",\n    \"telstra.com.au\": \"telstra\",\n    \"tenderapp.com\": \"tender\",\n    \"tensitionschoo.club\": \"tensitionschoo.club\",\n    \"watch.teroti.com\": \"teroti\",\n    \"webterren.com\": \"terren\",\n    \"teufel.de\": \"teufel.de\",\n    \"theadex.com\": \"the_adex\",\n    \"connect.decknetwork.net\": \"the_deck\",\n    \"gu-web.net\": \"the_guardian\",\n    \"guardianapps.co.uk\": \"the_guardian\",\n    \"guim.co.uk\": \"the_guardian\",\n    \"deepthought.online\": \"the_reach_group\",\n    \"reachgroup.com\": \"the_reach_group\",\n    \"redintelligence.net\": \"the_reach_group\",\n    \"thesearchagency.net\": \"the_search_agency\",\n    \"thesun.co.uk\": \"the_sun\",\n    \"w-x.co\": \"the_weather_company\",\n    \"weather.com\": \"the_weather_company\",\n    \"wfxtriggers.com\": \"the_weather_company\",\n    \"tmdb.org\": \"themoviedb\",\n    \"thinglink.com\": \"thinglink\",\n    \"online-metrix.net\": \"threatmetrix\",\n    \"tidbit.co.in\": \"tidbit\",\n    \"code.tidio.co\": \"tidio\",\n    \"widget-v4.tidiochat.com\": \"tidio\",\n    \"analytics.tiktok.com\": \"tiktok_analytics\",\n    \"optimized.by.tiller.co\": \"tiller\",\n    \"vip.timezonedb.com\": \"timezondb\",\n    \"npttech.com\": \"tinypass\",\n    \"tinypass.com\": \"tinypass\",\n    \"tisoomi-services.com\": \"tisoomi\",\n    \"ad.tlvmedia.com\": \"tlv_media\",\n    \"ads.tlvmedia.com\": \"tlv_media\",\n    \"tag.tlvmedia.com\": \"tlv_media\",\n    \"research-int.se\": \"tns\",\n    \"sesamestats.com\": \"tns\",\n    \"spring-tns.net\": \"tns\",\n    \"statistik-gallup.net\": \"tns\",\n    \"tns-cs.net\": \"tns\",\n    \"tns-gallup.dk\": \"tns\",\n    \"tomnewsupdate.info\": \"tomnewsupdate.info\",\n    \"tfag.de\": \"tomorrow_focus\",\n    \"srv.clickfuse.com\": \"tonefuse\",\n    \"toplist.cz\": \"toplist.cz\",\n    \"toponclick.com\": \"toponclick_com\",\n    \"topsy.com\": \"topsy\",\n    \"insight.torbit.com\": \"torbit\",\n    \"toro-tags.com\": \"toro\",\n    \"toroadvertising.com\": \"toro\",\n    \"toroadvertisingmedia.com\": \"toro\",\n    \"tororango.com\": \"tororango.com\",\n    \"i.total-media.net\": \"total_media\",\n    \"inq.com\": \"touchcommerce\",\n    \"tovarro.com\": \"tovarro.com\",\n    \"rialpay.com\": \"tp-cdn.com\",\n    \"tp-cdn.com\": \"tp-cdn.com\",\n    \"kiwe.io\": \"tracc.it\",\n    \"tracc.it\": \"tracc.it\",\n    \"ipnoid.com\": \"tracemyip\",\n    \"tracemyip.org\": \"tracemyip\",\n    \"d2gfdmu30u15x7.cloudfront.net\": \"traceview\",\n    \"tracelytics.com\": \"traceview\",\n    \"cdn.trackduck.com\": \"track_duck\",\n    \"d2zah9y47r7bi2.cloudfront.net\": \"trackjs\",\n    \"dl1d2m8ri9v3j.cloudfront.net\": \"trackjs\",\n    \"trackjs.com\": \"trackjs\",\n    \"conversionlab.trackset.com\": \"trackset_conversionlab\",\n    \"trackuity.com\": \"trackuity\",\n    \"adsrvr.org\": \"tradedesk\",\n    \"tradedoubler.com\": \"tradedoubler\",\n    \"tradelab.fr\": \"tradelab\",\n    \"tradetracker.net\": \"tradetracker\",\n    \"cdntrf.com\": \"traffective\",\n    \"traffective.com\": \"traffective\",\n    \"my.trafficfuel.com\": \"traffic_fuel\",\n    \"trafficrevenue.net\": \"traffic_revenue\",\n    \"trafficstars.com\": \"traffic_stars\",\n    \"tsyndicate.com\": \"traffic_stars\",\n    \"trafficbroker.com\": \"trafficbroker\",\n    \"trafficfabrik.com\": \"trafficfabrik.com\",\n    \"trafficfactory.biz\": \"trafficfactory\",\n    \"trafficforce.com\": \"trafficforce\",\n    \"traffichaus.com\": \"traffichaus\",\n    \"trafficjunky.net\": \"trafficjunky\",\n    \"traffiliate.com\": \"traffiliate\",\n    \"storage.trafic.ro\": \"trafic\",\n    \"trafmag.com\": \"trafmag.com\",\n    \"api.transcend.io\": \"transcend\",\n    \"cdn.transcend.io\": \"transcend\",\n    \"sync-transcend-cdn.com\": \"transcend\",\n    \"transcend-cdn.com\": \"transcend\",\n    \"transcend.io\": \"transcend\",\n    \"telemetry.transcend.io\": \"transcend_telemetry\",\n    \"backoffice.transmatico.com\": \"transmatic\",\n    \"travelaudience.com\": \"travel_audience\",\n    \"trbo.com\": \"trbo\",\n    \"treasuredata.com\": \"treasuredata\",\n    \"scanscout.com\": \"tremor_video\",\n    \"tremorhub.com\": \"tremor_video\",\n    \"tremormedia.com\": \"tremor_video\",\n    \"tremorvideo.com\": \"tremor_video\",\n    \"videohub.tv\": \"tremor_video\",\n    \"s.tcimg.com\": \"trendcounter\",\n    \"tcimg.com\": \"trendcounter\",\n    \"trendemon.com\": \"trendemon\",\n    \"exponential.com\": \"tribal_fusion\",\n    \"tribalfusion.com\": \"tribal_fusion\",\n    \"tribl.io\": \"triblio\",\n    \"api.temails.com\": \"trigger_mail_marketing\",\n    \"t.myvisitors.se\": \"triggerbee\",\n    \"jscache.com\": \"tripadvisor\",\n    \"tacdn.com\": \"tripadvisor\",\n    \"tamgrt.com\": \"tripadvisor\",\n    \"tripadvisor.co.uk\": \"tripadvisor\",\n    \"tripadvisor.com\": \"tripadvisor\",\n    \"tripadvisor.de\": \"tripadvisor\",\n    \"3lift.com\": \"triplelift\",\n    \"d3iwjrnl4m67rd.cloudfront.net\": \"triplelift\",\n    \"triplelift.com\": \"triplelift\",\n    \"static.triptease.io\": \"triptease\",\n    \"andomedia.com\": \"triton_digital\",\n    \"tritondigital.com\": \"triton_digital\",\n    \"revelations.trovus.co.uk\": \"trovus_revelations\",\n    \"trsv3.com\": \"trsv3.com\",\n    \"truefitcorp.com\": \"true_fit\",\n    \"tru.am\": \"trueanthem\",\n    \"adlegend.com\": \"trueffect\",\n    \"addoer.com\": \"truehits.net\",\n    \"truehits.in.th\": \"truehits.net\",\n    \"truehits.net\": \"truehits.net\",\n    \"trumba.com\": \"trumba\",\n    \"truoptik.com\": \"truoptik\",\n    \"trustarc.com\": \"trustarc\",\n    \"truste.com\": \"trustarc\",\n    \"consent.truste.com\": \"truste_consent\",\n    \"choices-or.truste.com\": \"truste_notice\",\n    \"choices.truste.com\": \"truste_notice\",\n    \"privacy-policy.truste.com\": \"truste_seal\",\n    \"trustedshops.com\": \"trusted_shops\",\n    \"trustev.com\": \"trustev\",\n    \"secure.comodo.net\": \"trustlogo\",\n    \"trustlogo.com\": \"trustlogo\",\n    \"usertrust.com\": \"trustlogo\",\n    \"trustpilot.com\": \"trustpilot\",\n    \"trustwave.com\": \"trustwave.com\",\n    \"tubecorporate.com\": \"tubecorporate\",\n    \"tubecup.org\": \"tubecup.org\",\n    \"tubemogul.com\": \"tubemogul\",\n    \"sre-perim.com\": \"tumblr_analytics\",\n    \"txmblr.com\": \"tumblr_analytics\",\n    \"platform.tumblr.com\": \"tumblr_buttons\",\n    \"lib.tunein.com\": \"tune_in\",\n    \"adagio.turboadv.com\": \"turbo\",\n    \"turn.com\": \"turn_inc.\",\n    \"ngtv.io\": \"turner\",\n    \"turner.com\": \"turner\",\n    \"warnermedia.com\": \"turner\",\n    \"turnsocial.com\": \"turnsocial\",\n    \"turnto.com\": \"turnto\",\n    \"tvsquared.com\": \"tvsquared.com\",\n    \"tweetboard.com\": \"tweetboard\",\n    \"tweetmeme.com\": \"tweetmeme\",\n    \"c4tw.net\": \"twenga\",\n    \"twiago.com\": \"twiago\",\n    \"twinedigital.go2cloud.org\": \"twine\",\n    \"ext-twitch.tv\": \"twitch.tv\",\n    \"twitch.tv\": \"twitch.tv\",\n    \"jtvnw.net\": \"twitch_cdn\",\n    \"ttvnw.net\": \"twitch_cdn\",\n    \"twitchcdn.net\": \"twitch_cdn\",\n    \"twitchsvc.net\": \"twitch_cdn\",\n    \"t.co\": \"twitter\",\n    \"twimg.com\": \"twitter\",\n    \"twitter.com\": \"twitter\",\n    \"twttr.com\": \"twitter\",\n    \"x.com\": \"twitter\",\n    \"ads-twitter.com\": \"twitter_ads\",\n    \"analytics.twitter.com\": \"twitter_analytics\",\n    \"tellapart.com\": \"twitter_for_business\",\n    \"syndication.twitter.com\": \"twitter_syndication\",\n    \"twittercounter.com\": \"twittercounter\",\n    \"twyn.com\": \"twyn\",\n    \"txxx.com\": \"txxx.com\",\n    \"tynt.com\": \"tynt\",\n    \"typeform.com\": \"typeform\",\n    \"typepad.com\": \"typepad_stats\",\n    \"typography.com\": \"typography.com\",\n    \"tyroodirect.com\": \"tyroo\",\n    \"tyroodr.com\": \"tyroo\",\n    \"tzetze.it\": \"tzetze\",\n    \"ubersetzung-app.com\": \"ubersetzung-app.com\",\n    \"ubuntu.com\": \"ubuntu\",\n    \"ubuntucompanyservices.co.za\": \"ubuntu\",\n    \"aralego.net\": \"ucfunnel\",\n    \"ucfunnel.com\": \"ucfunnel\",\n    \"at.ua\": \"ucoz\",\n    \"do.am\": \"ucoz\",\n    \"ucoz.net\": \"ucoz\",\n    \"ad-api-v01.uliza.jp\": \"uliza\",\n    \"api.umbel.com\": \"umbel\",\n    \"umebiggestern.club\": \"umebiggestern.club\",\n    \"unanimis.co.uk\": \"unanimis\",\n    \"d3pkntwtp2ukl5.cloudfront.net\": \"unbounce\",\n    \"t.unbounce.com\": \"unbounce\",\n    \"d21gpk1vhmjuf5.cloudfront.net\": \"unbxd\",\n    \"tracker.unbxdapi.com\": \"unbxd\",\n    \"under-box.com\": \"under-box.com\",\n    \"undercomputer.com\": \"undercomputer.com\",\n    \"udmserve.net\": \"underdog_media\",\n    \"undertone.com\": \"undertone\",\n    \"roitesting.com\": \"unica\",\n    \"unica.com\": \"unica\",\n    \"unister-adservices.com\": \"unister\",\n    \"unister-gmbh.de\": \"unister\",\n    \"uadx.com\": \"unite\",\n    \"nonstoppartner.net\": \"united_digital_group\",\n    \"tifbs.net\": \"united_internet_media_gmbh\",\n    \"ui-portal.de\": \"united_internet_media_gmbh\",\n    \"uimserv.net\": \"united_internet_media_gmbh\",\n    \"unity.com\": \"unity\",\n    \"unity3d.com\": \"unity\",\n    \"unity3dusercontent.com\": \"unity\",\n    \"unityads.unity3d.com\": \"unity_ads\",\n    \"univide.com\": \"univide\",\n    \"unpkg.com\": \"unpkg.com\",\n    \"unrulymedia.com\": \"unruly_media\",\n    \"src.kitcode.net\": \"untriel_finger_printing\",\n    \"s.clickability.com\": \"upland_clickability_beacon\",\n    \"uppr.de\": \"uppr.de\",\n    \"upravel.com\": \"upravel.com\",\n    \"upsellit.com\": \"upsellit\",\n    \"kontagent.net\": \"upsight\",\n    \"app.uptain.de\": \"uptain\",\n    \"uptolike.com\": \"uptolike.com\",\n    \"uptrends.com\": \"uptrends\",\n    \"urban-media.com\": \"urban-media.com\",\n    \"urbanairship.com\": \"urban_airship\",\n    \"mobile.usabilitytools.com\": \"usability_tools\",\n    \"usabilla.com\": \"usabilla\",\n    \"usemax.de\": \"usemax\",\n    \"usemaxserver.de\": \"usemax\",\n    \"usemessages.com\": \"usemessages.com\",\n    \"api.usercycle.com\": \"usercycle\",\n    \"userdive.com\": \"userdive\",\n    \"userecho.com\": \"userecho\",\n    \"dq4irj27fs462.cloudfront.net\": \"userlike.com\",\n    \"userlike-cdn-widgets.s3-eu-west-1.amazonaws.com\": \"userlike.com\",\n    \"userlike.com\": \"userlike.com\",\n    \"contactusplus.com\": \"userpulse\",\n    \"user-pulse.appspot.com\": \"userpulse\",\n    \"userpulse.com\": \"userpulse\",\n    \"userreplay.net\": \"userreplay\",\n    \"sdsbucket.s3.amazonaws.com\": \"userreport\",\n    \"userreport.com\": \"userreport\",\n    \"dtkm4pd19nw6z.cloudfront.net\": \"userrules\",\n    \"api.usersnap.com\": \"usersnap\",\n    \"d3mvnvhjmkxpjz.cloudfront.net\": \"usersnap\",\n    \"uservoice.com\": \"uservoice\",\n    \"userzoom.com\": \"userzoom.com\",\n    \"usocial.pro\": \"usocial\",\n    \"utarget.ru\": \"utarget\",\n    \"uuidksinc.net\": \"uuidksinc.net\",\n    \"v12group.com\": \"v12_group\",\n    \"vacaneedasap.com\": \"vacaneedasap.com\",\n    \"ads.brand.net\": \"valassis\",\n    \"vdrn.redplum.com\": \"valassis\",\n    \"api.searchlinks.com\": \"validclick\",\n    \"js.searchlinks.com\": \"validclick\",\n    \"vinsight.de\": \"valiton\",\n    \"valueclick.net\": \"valueclick_media\",\n    \"valuecommerce.com\": \"valuecommerce\",\n    \"valuedopinions.co.uk\": \"valued_opinions\",\n    \"buzzparadise.com\": \"vanksen\",\n    \"vmmpxl.com\": \"varick_media_management\",\n    \"vcita.com\": \"vcita\",\n    \"tracking.vcommission.com\": \"vcommission\",\n    \"vdopia.com\": \"vdopia\",\n    \"veinteractive.com\": \"ve_interactive\",\n    \"vee24.com\": \"vee24\",\n    \"velocecdn.com\": \"velocecdn.com\",\n    \"mdcn.mobi\": \"velti_mgage_visualize\",\n    \"velti.com\": \"velti_mgage_visualize\",\n    \"vendemore.com\": \"vendemore\",\n    \"venturead.com\": \"venturead.com\",\n    \"api.venyoo.ru\": \"venyoo\",\n    \"veoxa.com\": \"veoxa\",\n    \"vergic.com\": \"vergic.com\",\n    \"d3qxef4rp70elm.cloudfront.net\": \"vero\",\n    \"getvero.com\": \"vero\",\n    \"verticalacuity.com\": \"vertical_acuity\",\n    \"roi.vertical-leap.co.uk\": \"vertical_leap\",\n    \"cts.vresp.com\": \"verticalresponse\",\n    \"verticalscope.com\": \"verticalscope\",\n    \"ads.vertoz.com\": \"vertoz\",\n    \"banner.vrtzads.com\": \"vertoz\",\n    \"veruta.com\": \"veruta\",\n    \"vrvm.com\": \"verve_mobile\",\n    \"vgwort.de\": \"vg_wort\",\n    \"digitaltarget.ru\": \"vi\",\n    \"btg.mtvnservices.com\": \"viacom_tag_container\",\n    \"viafoura.com\": \"viafoura\",\n    \"viafoura.net\": \"viafoura\",\n    \"intellitxt.com\": \"vibrant_ads\",\n    \"vicomi.com\": \"vicomi.com\",\n    \"vidazoo.com\": \"vidazoo.com\",\n    \"module-videodesk.com\": \"video_desk\",\n    \"vidtok.ru\": \"video_potok\",\n    \"videoadex.com\": \"videoadex.com\",\n    \"tidaltv.com\": \"videology\",\n    \"videonow.ru\": \"videonow\",\n    \"videoplayerhub.com\": \"videoplayerhub.com\",\n    \"videoplaza.tv\": \"videoplaza\",\n    \"kweb.videostep.com\": \"videostep\",\n    \"content.vidgyor.com\": \"vidgyor\",\n    \"vidible.tv\": \"vidible\",\n    \"assets.vidora.com\": \"vidora\",\n    \"vietad.vn\": \"vietad\",\n    \"viglink.com\": \"viglink\",\n    \"vigo.one\": \"vigo\",\n    \"vigo.ru\": \"vigo\",\n    \"vimeo.com\": \"vimeo\",\n    \"vimeocdn.com\": \"vimeo\",\n    \"vindicosuite.com\": \"vindico_group\",\n    \"vinted.net\": \"vinted\",\n    \"viraladnetwork.net\": \"viral_ad_network\",\n    \"app.viral-loops.com\": \"viral_loops\",\n    \"viralgains.com\": \"viralgains\",\n    \"viralmint.com\": \"viralmint\",\n    \"virgul.com\": \"virgul\",\n    \"ssp.virool.com\": \"virool_player\",\n    \"virtusize.com\": \"virtusize\",\n    \"viewablemedia.net\": \"visible_measures\",\n    \"visiblemeasures.com\": \"visible_measures\",\n    \"visioncriticalpanels.com\": \"vision_critical\",\n    \"visitstreamer.com\": \"visit_streamer\",\n    \"visitortracklog.com\": \"visitortrack\",\n    \"visitorville.com\": \"visitorville\",\n    \"d2hkbi3gan6yg6.cloudfront.net\": \"visscore\",\n    \"myvisualiq.net\": \"visual_iq\",\n    \"visualrevenue.com\": \"visual_revenue\",\n    \"d5phz18u4wuww.cloudfront.net\": \"visual_website_optimizer\",\n    \"visualwebsiteoptimizer.com\": \"visual_website_optimizer\",\n    \"wingify.com\": \"visual_website_optimizer\",\n    \"vdna-assets.com\": \"visualdna\",\n    \"visualdna.com\": \"visualdna\",\n    \"visualstudio.com\": \"visualstudio.com\",\n    \"id-visitors.com\": \"visualvisitor\",\n    \"vi-tag.net\": \"vivalu\",\n    \"vivistats.com\": \"vivistats\",\n    \"vizury.com\": \"vizury\",\n    \"vizzit.se\": \"vizzit\",\n    \"cdn-vk.com\": \"vk.com\",\n    \"vk-analytics.com\": \"vk.com\",\n    \"vkuservideo.net\": \"vk.com\",\n    \"userapi.com\": \"vkontakte\",\n    \"vk.com\": \"vkontakte\",\n    \"vkontakte.ru\": \"vkontakte\",\n    \"vntsm.com\": \"vntsm.com\",\n    \"vodafone.de\": \"vodafone.de\",\n    \"voicefive.com\": \"voicefive\",\n    \"volusion.com\": \"volusion_chat\",\n    \"cwkuki.com\": \"voluum\",\n    \"volumtrk.com\": \"voluum\",\n    \"voluumtrk3.com\": \"voluum\",\n    \"vooxe.com\": \"vooxe.com\",\n    \"vorwerk.de\": \"vorwerk.de\",\n    \"vox-cdn.com\": \"vox\",\n    \"embed.voxus.tv\": \"voxus\",\n    \"voxus-targeting-voxusmidia.netdna-ssl.com\": \"voxus\",\n    \"c-dsp.vpadn.com\": \"vpon\",\n    \"tools.vpscash.nl\": \"vpscash\",\n    \"vsassets.io\": \"vs\",\n    \"exp-tas.com\": \"vscode\",\n    \"v0cdn.net\": \"vscode\",\n    \"vscode-cdn.net\": \"vscode\",\n    \"vscode-unpkg.net\": \"vscode\",\n    \"vtracy.de\": \"vtracy.de\",\n    \"liftoff.io\": \"vungle\",\n    \"vungle.com\": \"vungle\",\n    \"vuukle.com\": \"vuukle\",\n    \"view.vzaar.com\": \"vzaar\",\n    \"w3counter.com\": \"w3counter\",\n    \"w3roi.com\": \"w3roi\",\n    \"contentwidgets.net\": \"wahoha\",\n    \"wahoha.com\": \"wahoha\",\n    \"walkme.com\": \"walkme.com\",\n    \"wsod.com\": \"wall_street_on_demand\",\n    \"walmart.com\": \"walmart\",\n    \"wamcash.com\": \"wamcash\",\n    \"cdn-saveit.wanelo.com\": \"wanelo\",\n    \"static.warp.ly\": \"warp.ly\",\n    \"way2traffic.com\": \"way2traffic\",\n    \"wayfair.com\": \"wayfair_com\",\n    \"wdr.de\": \"wdr.de\",\n    \"web-stat.com\": \"web-stat\",\n    \"web.de\": \"web.de\",\n    \"webde.de\": \"web.de\",\n    \"webstat.net\": \"web.stat\",\n    \"ssl.webserviceaward.com\": \"web_service_award\",\n    \"webtraxs.com\": \"web_traxs\",\n    \"wipe.de\": \"web_wipe_analytics\",\n    \"webads.nl\": \"webads\",\n    \"tr.webantenna.info\": \"webantenna\",\n    \"webclicks24.com\": \"webclicks24_com\",\n    \"webclose.net\": \"webclose.net\",\n    \"webcollage.net\": \"webcollage\",\n    \"goutee.top\": \"webedia\",\n    \"mediaathay.org.uk\": \"webedia\",\n    \"wbdx.fr\": \"webedia\",\n    \"webeffective.keynote.com\": \"webeffective\",\n    \"widgets.webengage.com\": \"webengage\",\n    \"webgains.com\": \"webgains\",\n    \"webgozar.com\": \"webgozar\",\n    \"webgozar.ir\": \"webgozar\",\n    \"webhelpje.be\": \"webhelpje\",\n    \"webhelpje.nl\": \"webhelpje\",\n    \"webleads-tracker.com\": \"webleads_tracker\",\n    \"automation.webmecanik.com\": \"webmecanik\",\n    \"adrcdn.com\": \"weborama\",\n    \"adrcntr.com\": \"weborama\",\n    \"weborama.com\": \"weborama\",\n    \"weborama.fr\": \"weborama\",\n    \"webprospector.de\": \"webprospector\",\n    \"webstat.com\": \"webstat\",\n    \"webstat.se\": \"webstat.se\",\n    \"stat.webtrack.biz\": \"webtrack\",\n    \"webtraffic.no\": \"webtraffic\",\n    \"webtraffic.se\": \"webtraffic\",\n    \"d1r27qvpjiaqj3.cloudfront.net\": \"webtrekk\",\n    \"mateti.net\": \"webtrekk\",\n    \"wbtrk.net\": \"webtrekk\",\n    \"wcfbc.net\": \"webtrekk\",\n    \"webtrekk-asia.net\": \"webtrekk\",\n    \"webtrekk.com\": \"webtrekk\",\n    \"webtrekk.de\": \"webtrekk\",\n    \"webtrekk.net\": \"webtrekk\",\n    \"wt-eu02.net\": \"webtrekk\",\n    \"wt-safetag.com\": \"webtrekk\",\n    \"webtrends.com\": \"webtrends\",\n    \"webtrendslive.com\": \"webtrends\",\n    \"rd.clickshift.com\": \"webtrends_ads\",\n    \"web-visor.com\": \"webvisor\",\n    \"weebly.com\": \"weebly_ads\",\n    \"widget.weibo.com\": \"weibo_widget\",\n    \"westlotto.com\": \"westlotto_com\",\n    \"wetter.com\": \"wetter_com\",\n    \"wettercomassets.com\": \"wetter_com\",\n    \"whatsbroadcast.com\": \"whatbroadcast\",\n    \"whatsapp.com\": \"whatsapp\",\n    \"whatsapp.net\": \"whatsapp\",\n    \"whisper.onelink.me\": \"whisper\",\n    \"whisper.sh\": \"whisper\",\n    \"amung.us\": \"whos.amung.us\",\n    \"whoson.com\": \"whoson\",\n    \"api.wibbitz.com\": \"wibbitz\",\n    \"cdn4.wibbitz.com\": \"wibbitz\",\n    \"cdn.wibiya.com\": \"wibiya_toolbar\",\n    \"predictad.com\": \"widdit\",\n    \"widerplanet.com\": \"widerplanet\",\n    \"widespace.com\": \"widespace\",\n    \"widgetserver.com\": \"widgetbox\",\n    \"3c45d848d99.se\": \"wiget_media\",\n    \"wigetmedia.com\": \"wiget_media\",\n    \"tracker.wigzopush.com\": \"wigzo\",\n    \"wikia-services.com\": \"wikia-services.com\",\n    \"wikia-beacon.com\": \"wikia_beacon\",\n    \"nocookie.net\": \"wikia_cdn\",\n    \"wikimedia.org\": \"wikimedia.org\",\n    \"wikipedia.org\": \"wikimedia.org\",\n    \"wikiquote.org\": \"wikimedia.org\",\n    \"tracking.winaffiliates.com\": \"winaffiliates\",\n    \"maps.windows.com\": \"windows_maps\",\n    \"client.wns.windows.com\": \"windows_notifications\",\n    \"time.windows.com\": \"windows_time\",\n    \"windowsupdate.com\": \"windowsupdate\",\n    \"api.wipmania.com\": \"wipmania\",\n    \"col1.wiqhit.com\": \"wiqhit\",\n    \"wirecard.com\": \"wirecard\",\n    \"wirecard.de\": \"wirecard\",\n    \"leadlab.click\": \"wiredminds\",\n    \"wiredminds.com\": \"wiredminds\",\n    \"wiredminds.de\": \"wiredminds\",\n    \"adtotal.pl\": \"wirtualna_polska\",\n    \"wisepops.com\": \"wisepops\",\n    \"cdn.wishpond.net\": \"wishpond\",\n    \"wistia.com\": \"wistia\",\n    \"wistia.net\": \"wistia\",\n    \"parastorage.com\": \"wix.com\",\n    \"wix.com\": \"wix.com\",\n    \"public.wixab-cloud.com\": \"wixab\",\n    \"wixmp.com\": \"wixmp\",\n    \"wnzmauurgol.com\": \"wnzmauurgol.com\",\n    \"wonderpush.com\": \"wonderpush\",\n    \"woopic.com\": \"woopic.com\",\n    \"woopra.com\": \"woopra\",\n    \"pubmine.com\": \"wordpress_ads\",\n    \"w.org\": \"wordpress_stats\",\n    \"wordpress.com\": \"wordpress_stats\",\n    \"wp.com\": \"wordpress_stats\",\n    \"tracker.wordstream.com\": \"wordstream\",\n    \"worldnaturenet.xyz\": \"worldnaturenet_xyz\",\n    \"wp.pl\": \"wp.pl\",\n    \"wpimg.pl\": \"wp.pl\",\n    \"wpengine.com\": \"wp_engine\",\n    \"clickanalyzer.jp\": \"writeup_clickanalyzer\",\n    \"wurfl.io\": \"wurfl\",\n    \"wwwpromoter.com\": \"wwwpromoter\",\n    \"imgwykop.pl\": \"wykop\",\n    \"wykop.pl\": \"wykop\",\n    \"wysistat.com\": \"wysistat.com\",\n    \"wysistat.net\": \"wysistat.com\",\n    \"wywy.com\": \"wywy.com\",\n    \"wywyuserservice.com\": \"wywy.com\",\n    \"cdn.x-lift.jp\": \"x-lift\",\n    \"xapads.com\": \"xapads\",\n    \"xen-media.com\": \"xen-media.com\",\n    \"xfreeservice.com\": \"xfreeservice.com\",\n    \"xhamster.com\": \"xhamster\",\n    \"xhamsterlive.com\": \"xhamster\",\n    \"xhamsterpremium.com\": \"xhamster\",\n    \"xhcdn.com\": \"xhamster\",\n    \"huami.com\": \"xiaomi\",\n    \"mi-img.com\": \"xiaomi\",\n    \"mi.com\": \"xiaomi\",\n    \"miui.com\": \"xiaomi\",\n    \"xiaomi.com\": \"xiaomi\",\n    \"xiaomi.net\": \"xiaomi\",\n    \"xiaomiyoupin.com\": \"xiaomi\",\n    \"xing-share.com\": \"xing\",\n    \"xing.com\": \"xing\",\n    \"xmediaclicks.com\": \"xmediaclicks\",\n    \"xnxx-cdn.com\": \"xnxx_cdn\",\n    \"xplosion.de\": \"xplosion\",\n    \"xtendmedia.com\": \"xtend\",\n    \"xvideos-cdn.com\": \"xvideos_com\",\n    \"xvideos.com\": \"xvideos_com\",\n    \"xxxlshop.de\": \"xxxlshop.de\",\n    \"xxxlutz.de\": \"xxxlutz\",\n    \"adx.com.ru\": \"yabbi\",\n    \"yabbi.me\": \"yabbi\",\n    \"yabuka.com\": \"yabuka\",\n    \"tumblr.com\": \"yahoo\",\n    \"yahoo.com\": \"yahoo\",\n    \"yahooapis.com\": \"yahoo\",\n    \"yimg.com\": \"yahoo\",\n    \"oath.cloud\": \"yahoo\",\n    \"yahoo.net\": \"yahoo\",\n    \"yahooinc.com\": \"yahoo\",\n    \"yahoodns.net\": \"yahoo\",\n    \"yads.yahoo.com\": \"yahoo_ad_exchange\",\n    \"yieldmanager.com\": \"yahoo_ad_exchange\",\n    \"pr-bh.ybp.yahoo.com\": \"yahoo_ad_manager\",\n    \"ads.yahoo.com\": \"yahoo_advertising\",\n    \"adtech.yahooinc.com\": \"yahoo_advertising\",\n    \"analytics.yahoo.com\": \"yahoo_analytics\",\n    \"np.lexity.com\": \"yahoo_commerce_central\",\n    \"storage-yahoo.jp\": \"yahoo_japan_retargeting\",\n    \"yahoo.co.jp\": \"yahoo_japan_retargeting\",\n    \"yahooapis.jp\": \"yahoo_japan_retargeting\",\n    \"yimg.jp\": \"yahoo_japan_retargeting\",\n    \"yjtag.jp\": \"yahoo_japan_retargeting\",\n    \"ov.yahoo.co.jp\": \"yahoo_overture\",\n    \"overture.com\": \"yahoo_overture\",\n    \"search.yahooinc.com\": \"yahoo_search\",\n    \"luminate.com\": \"yahoo_small_business\",\n    \"pixazza.com\": \"yahoo_small_business\",\n    \"awaps.yandex.ru\": \"yandex\",\n    \"d31j93rd8oukbv.cloudfront.net\": \"yandex\",\n    \"webvisor.org\": \"yandex\",\n    \"yandex.net\": \"yandex\",\n    \"yandex.ru\": \"yandex\",\n    \"yastatic.net\": \"yandex\",\n    \"ya.ru\": \"yandex\",\n    \"yandex.by\": \"yandex\",\n    \"yandex.com\": \"yandex\",\n    \"yandex.com.tr\": \"yandex\",\n    \"yandex.fr\": \"yandex\",\n    \"yandex.kz\": \"yandex\",\n    \"yandex.st\": \"yandex.api\",\n    \"yandexadexchange.net\": \"yandex_adexchange\",\n    \"metabar.ru\": \"yandex_advisor\",\n    \"appmetrica.yandex.com\": \"yandex_appmetrica\",\n    \"an.webvisor.org\": \"yandex_direct\",\n    \"an.yandex.ru\": \"yandex_direct\",\n    \"bs.yandex.ru\": \"yandex_direct\",\n    \"mc.yandex.ru\": \"yandex_metrika\",\n    \"passport.yandex.ru\": \"yandex_passport\",\n    \"yapfiles.ru\": \"yapfiles.ru\",\n    \"yashi.com\": \"yashi\",\n    \"ad.adserverplus.com\": \"ybrant_media\",\n    \"player.sambaads.com\": \"ycontent\",\n    \"cdn.yektanet.com\": \"yektanet\",\n    \"fetch.yektanet.com\": \"yektanet\",\n    \"yengo.com\": \"yengo\",\n    \"yengointernational.com\": \"yengo\",\n    \"link.p0.com\": \"yesmail\",\n    \"adsrevenue.net\": \"yesup_advertising\",\n    \"infinityads.com\": \"yesup_advertising\",\n    \"momentsharing.com\": \"yesup_advertising\",\n    \"multipops.com\": \"yesup_advertising\",\n    \"onlineadultadvertising.com\": \"yesup_advertising\",\n    \"paypopup.com\": \"yesup_advertising\",\n    \"popupxxx.com\": \"yesup_advertising\",\n    \"xtargeting.com\": \"yesup_advertising\",\n    \"xxxwebtraffic.com\": \"yesup_advertising\",\n    \"app.yesware.com\": \"yesware\",\n    \"yldbt.com\": \"yieldbot\",\n    \"yieldify.com\": \"yieldify\",\n    \"yieldlab.net\": \"yieldlab\",\n    \"yieldlove-ad-serving.net\": \"yieldlove\",\n    \"yieldlove.com\": \"yieldlove\",\n    \"yieldmo.com\": \"yieldmo\",\n    \"254a.com\": \"yieldr\",\n    \"collect.yldr.io\": \"yieldr_air\",\n    \"yieldsquare.com\": \"yieldsquare\",\n    \"analytics-sdk.yle.fi\": \"yle\",\n    \"yllix.com\": \"yllixmedia\",\n    \"ymetrica1.com\": \"ymetrica1.com\",\n    \"ymzrrizntbhde.com\": \"ymzrrizntbhde.com\",\n    \"yoapp.s3.amazonaws.com\": \"yo_button\",\n    \"natpal.com\": \"yodle\",\n    \"analytics.yola.net\": \"yola_analytics\",\n    \"pixel.yola.net\": \"yola_analytics\",\n    \"delivery.yomedia.vn\": \"yomedia\",\n    \"yoochoose.net\": \"yoochoose.net\",\n    \"yotpo.com\": \"yotpo\",\n    \"yottaa.net\": \"yottaa\",\n    \"yottlyscript.com\": \"yottly\",\n    \"api.youcanbook.me\": \"youcanbookme\",\n    \"youcanbook.me\": \"youcanbookme\",\n    \"player.youku.com\": \"youku\",\n    \"youporn.com\": \"youporn\",\n    \"ypncdn.com\": \"youporn\",\n    \"googlevideo.com\": \"youtube\",\n    \"youtube-nocookie.com\": \"youtube\",\n    \"youtube.com\": \"youtube\",\n    \"ytimg.com\": \"youtube\",\n    \"c.ypcdn.com\": \"yp\",\n    \"i1.ypcdn.com\": \"yp\",\n    \"yellowpages.com\": \"yp\",\n    \"prod-js.aws.y-track.com\": \"ysance\",\n    \"y-track.com\": \"ysance\",\n    \"yume.com\": \"yume\",\n    \"yumenetworks.com\": \"yume,_inc.\",\n    \"gravityrd-services.com\": \"yusp\",\n    \"api.zadarma.com\": \"zadarma\",\n    \"zalan.do\": \"zalando_de\",\n    \"zalando.de\": \"zalando_de\",\n    \"ztat.net\": \"zalando_de\",\n    \"zaloapp.com\": \"zalo\",\n    \"zanox-affiliate.de\": \"zanox\",\n    \"zanox.com\": \"zanox\",\n    \"zanox.ws\": \"zanox\",\n    \"zaparena.com\": \"zaparena\",\n    \"zapunited.com\": \"zaparena\",\n    \"track.zappos.com\": \"zappos\",\n    \"zdassets.com\": \"zdassets.com\",\n    \"zebestof.com\": \"zebestof.com\",\n    \"zedo.com\": \"zedo\",\n    \"zemanta.com\": \"zemanta\",\n    \"zencdn.net\": \"zencoder\",\n    \"zendesk.com\": \"zendesk\",\n    \"zergnet.com\": \"zergnet\",\n    \"zero.kz\": \"zero.kz\",\n    \"app.insightgrit.com\": \"zeta\",\n    \"app.ubertags.com\": \"zeta\",\n    \"cdn.boomtrain.com\": \"zeta\",\n    \"events.api.boomtrain.com\": \"zeta\",\n    \"rfihub.com\": \"zeta\",\n    \"rfihub.net\": \"zeta\",\n    \"ru4.com\": \"zeta\",\n    \"xplusone.com\": \"zeta\",\n    \"zeusclicks.com\": \"zeusclicks\",\n    \"webtest.net\": \"ziff_davis\",\n    \"zdbb.net\": \"ziff_davis\",\n    \"ziffdavis.com\": \"ziff_davis\",\n    \"ziffdavisinternational.com\": \"ziff_davis\",\n    \"ziffprod.com\": \"ziff_davis\",\n    \"ziffstatic.com\": \"ziff_davis\",\n    \"analytics.ziftsolutions.com\": \"zift_solutions\",\n    \"zimbio.com\": \"zimbio.com\",\n    \"api.zippyshare.com\": \"zippyshare_widget\",\n    \"zmags.com\": \"zmags\",\n    \"zmctrack.net\": \"zmctrack.net\",\n    \"zog.link\": \"zog.link\",\n    \"js.zohostatic.eu\": \"zoho\",\n    \"zononi.com\": \"zononi.com\",\n    \"zopim.com\": \"zopim\",\n    \"zukxd6fkxqn.com\": \"zukxd6fkxqn.com\",\n    \"zwaar.net\": \"zwaar\",\n    \"zwaar.org\": \"zwaar\",\n    \"extend.tv\": \"zypmedia\"\n  }\n}\n"
  },
  {
    "path": "client/src/helpers/trackers/trackers.ts",
    "content": "import whotracksmeWebsites from './whotracksme_web.json';\n\nimport trackersDb from './trackers.json';\nimport { REPOSITORY } from '../constants';\n\n/**\n @typedef TrackerData\n @type {object}\n @property {string} id - tracker ID.\n @property {string} name - tracker name.\n @property {string} url - tracker website url.\n @property {number} category - tracker category.\n @property {source} source - tracker data source.\n */\n\n/**\n * Tracker data sources\n */\nexport const sources = {\n    WHOTRACKSME: 1,\n    ADGUARD: 2,\n};\n\n/**\n * Gets link to tracker page on whotracks.me.\n *\n * @param trackerId\n * @return {string}\n */\nconst getWhotracksmeUrl = (trackerId: any) => {\n    const websiteId = whotracksmeWebsites.websites[trackerId];\n    if (websiteId) {\n        // Overrides links to websites.\n        return `https://whotracks.me/websites/${websiteId}.html`;\n    }\n\n    return `https://whotracks.me/trackers/${trackerId}.html`;\n};\n\n/**\n * Gets the source metadata for the specified tracker\n *\n * @param {TrackerData} trackerData tracker data\n * @returns {source} source metadata or null if no matching tracker found\n */\nexport const getSourceData = (trackerData: any) => {\n    if (!trackerData || !trackerData.source) {\n        return null;\n    }\n\n    if (trackerData.source === sources.WHOTRACKSME) {\n        return {\n            name: 'Whotracks.me',\n            url: getWhotracksmeUrl(trackerData.id),\n        };\n    }\n    if (trackerData.source === sources.ADGUARD) {\n        return {\n            name: 'AdGuard',\n            url: REPOSITORY.TRACKERS_DB,\n        };\n    }\n\n    return null;\n};\n\n/**\n * Converts the JSON string source into numeric source for AdGuard Home\n *\n * @param {TrackerData} trackerData tracker data\n * @returns {number} source number\n */\nconst convertSource = (sourceStr: any) => {\n    if (!sourceStr || sourceStr !== 'AdGuard') {\n        return sources.WHOTRACKSME;\n    }\n\n    return sources.ADGUARD;\n};\n\n/**\n * Gets tracker data from the trackers database\n *\n * @param {String} domainName domain name to check\n * @returns {TrackerData} tracker data or null if no matching tracker found\n */\nexport const getTrackerData = (domainName: any) => {\n    if (!domainName) {\n        return null;\n    }\n\n    const parts = domainName.split(/\\./g).reverse();\n    let hostToCheck = '';\n\n    // Check every subdomain\n    for (let i = 0; i < parts.length; i += 1) {\n        hostToCheck = parts[i] + (i > 0 ? '.' : '') + hostToCheck;\n        const trackerId = trackersDb.trackerDomains[hostToCheck];\n\n        if (trackerId) {\n            const trackerData = trackersDb.trackers[trackerId];\n            const categoryName = trackersDb.categories[trackerData.categoryId];\n            const source = convertSource(trackerData.source);\n            const sourceData = getSourceData(trackerData);\n\n            return {\n                id: trackerId,\n                name: trackerData.name,\n                url: trackerData.url,\n                category: categoryName,\n                source,\n                sourceData,\n            };\n        }\n    }\n\n    // No tracker found for the specified domain\n    return null;\n};\n"
  },
  {
    "path": "client/src/helpers/trackers/whotracksme_web.json",
    "content": "{\n  \"timeUpdated\": \"2021-12-19T13:50:00.512Z\",\n  \"websites\": {\n    \"netflix\": \"netflix.com\"\n  }\n}\n"
  },
  {
    "path": "client/src/helpers/twosky.ts",
    "content": "// eslint-disable-next-line import/no-relative-packages\nimport twosky from '../../../.twosky.json';\n\nexport const LANGUAGES = twosky[0].languages;\nexport const BASE_LOCALE = twosky[0].base_locale;\n"
  },
  {
    "path": "client/src/helpers/useDebounce.ts",
    "content": "import { useState, useEffect } from 'react';\n\nconst useDebounce = (value: any, delay: any) => {\n    const [debouncedValue, setDebouncedValue] = useState(value);\n\n    useEffect(() => {\n        const handler = setTimeout(() => {\n            setDebouncedValue(value);\n        }, delay);\n\n        return () => {\n            clearTimeout(handler);\n        };\n    }, [value, delay]);\n\n    return [debouncedValue, setDebouncedValue];\n};\n\nexport default useDebounce;\n"
  },
  {
    "path": "client/src/helpers/validators.ts",
    "content": "import i18next from 'i18next';\n\nimport {\n    MAX_PORT,\n    R_CIDR,\n    R_CIDR_IPV6,\n    R_HOST,\n    R_IPV4,\n    R_IPV6,\n    R_MAC,\n    R_URL_REQUIRES_PROTOCOL,\n    STANDARD_WEB_PORT,\n    UNSAFE_PORTS,\n    R_CLIENT_ID,\n    R_DOMAIN,\n    MAX_PASSWORD_LENGTH,\n    MIN_PASSWORD_LENGTH,\n    R_IPV4_SUBNET,\n    R_IPV6_SUBNET,\n} from './constants';\n\nimport { ip4ToInt, isValidAbsolutePath } from './form';\n\nimport { isIpInCidr, parseSubnetMask } from './helpers';\n\n// Validation functions\n// If the value is valid, the validation function should return undefined.\n/**\n * @param value {string|number}\n * @returns {undefined|string}\n */\nexport const validateRequiredValue = (value: any) => {\n    const formattedValue = typeof value === 'string' ? value.trim() : value;\n    if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) {\n        return undefined;\n    }\n    return i18next.t('form_error_required');\n};\n\n/**\n * @returns {undefined|string}\n * @param _\n * @param allValues\n */\nexport const validateIpv4RangeEnd = (_: any, allValues: any) => {\n    if (!allValues || !allValues.v4 || !allValues.v4.range_end || !allValues.v4.range_start) {\n        return undefined;\n    }\n\n    const { range_end, range_start } = allValues.v4;\n\n    if (ip4ToInt(range_end) <= ip4ToInt(range_start)) {\n        return i18next.t('greater_range_start_error');\n    }\n\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateIpv4 = (value: any) => {\n    if (value && !R_IPV4.test(value)) {\n        return i18next.t('form_error_ip4_format');\n    }\n    return undefined;\n};\n\n/**\n * @returns {undefined|string}\n * @param _\n * @param allValues\n */\nexport const validateNotInRange = (value: any, allValues: any) => {\n    if (!allValues.v4) {\n        return undefined;\n    }\n\n    const { range_start, range_end } = allValues.v4;\n\n    if (range_start && validateIpv4(range_start)) {\n        return undefined;\n    }\n\n    if (range_end && validateIpv4(range_end)) {\n        return undefined;\n    }\n\n    const isAboveMin = range_start && ip4ToInt(value) >= ip4ToInt(range_start);\n    const isBelowMax = range_end && ip4ToInt(value) <= ip4ToInt(range_end);\n\n    if (isAboveMin && isBelowMax) {\n        return i18next.t('out_of_range_error', {\n            start: range_start,\n            end: range_end,\n        });\n    }\n\n    return undefined;\n};\n\n/**\n * @returns {undefined|string}\n * @param _\n * @param allValues\n */\nexport const validateGatewaySubnetMask = (_: any, allValues: any) => {\n    if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) {\n        return i18next.t('gateway_or_subnet_invalid');\n    }\n\n    const { subnet_mask, gateway_ip } = allValues.v4;\n\n    if (validateIpv4(gateway_ip)) {\n        return i18next.t('gateway_or_subnet_invalid');\n    }\n\n    return parseSubnetMask(subnet_mask) ? undefined : i18next.t('gateway_or_subnet_invalid');\n};\n\n/**\n * @returns {undefined|string}\n * @param value\n * @param allValues\n */\nexport const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {\n    if (!allValues || !allValues.v4 || !value || !allValues.gateway_ip || !allValues.subnet_mask) {\n        return undefined;\n    }\n\n    const { gateway_ip, subnet_mask } = allValues.v4;\n\n    if ((gateway_ip && validateIpv4(gateway_ip)) || (subnet_mask && validateIpv4(subnet_mask))) {\n        return undefined;\n    }\n\n    const subnetPrefix = parseSubnetMask(subnet_mask);\n\n    if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) {\n        return i18next.t('subnet_error');\n    }\n\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateClientId = (value: string) => {\n    if (!value) {\n        return undefined;\n    }\n    const formattedValue = value.trim();\n    if (\n        formattedValue &&\n        !(\n            R_IPV4.test(formattedValue) ||\n            R_IPV6.test(formattedValue) ||\n            R_MAC.test(formattedValue) ||\n            R_CIDR.test(formattedValue) ||\n            R_CIDR_IPV6.test(formattedValue) ||\n            R_CLIENT_ID.test(formattedValue)\n        )\n    ) {\n        return i18next.t('form_error_client_id_format');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateConfigClientId = (value: any) => {\n    if (!value) {\n        return undefined;\n    }\n    const formattedValue = value.trim();\n    if (formattedValue && !R_CLIENT_ID.test(formattedValue)) {\n        return i18next.t('form_error_client_id_format');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateServerName = (value: any) => {\n    if (!value) {\n        return undefined;\n    }\n    const formattedValue = value ? value.trim() : value;\n    if (formattedValue && !R_DOMAIN.test(formattedValue)) {\n        return i18next.t('form_error_server_name');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateIpv6 = (value: any) => {\n    if (value && !R_IPV6.test(value)) {\n        return i18next.t('form_error_ip6_format');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateIp = (value: any) => {\n    if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) {\n        return i18next.t('form_error_ip_format');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateMac = (value: any) => {\n    if (value && !R_MAC.test(value)) {\n        return i18next.t('form_error_mac_format');\n    }\n    return undefined;\n};\n\n/**\n * @param value {number}\n * @returns {undefined|string}\n */\nexport const validatePort = (value: any) => {\n    if ((value || value === 0) && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {\n        return i18next.t('form_error_port_range');\n    }\n    return undefined;\n};\n\n/**\n * @param value {number}\n * @returns {undefined|string}\n */\nexport const validateInstallPort = (value: any) => {\n    if (value < 1 || value > MAX_PORT) {\n        return i18next.t('form_error_port');\n    }\n    return undefined;\n};\n\n/**\n * @param value {number}\n * @returns {undefined|string}\n */\nexport const validatePortTLS = (value: any) => {\n    if (value === 0) {\n        return undefined;\n    }\n    if (value && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {\n        return i18next.t('form_error_port_range');\n    }\n    return undefined;\n};\n\n/**\n * @param value {number}\n * @returns {undefined|string}\n */\nexport const validatePortQuic = validatePortTLS;\n\n/**\n * @param value {number}\n * @returns {undefined|string}\n */\nexport const validateIsSafePort = (value: any) => {\n    if (UNSAFE_PORTS.includes(value)) {\n        return i18next.t('form_error_port_unsafe');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateDomain = (value: any) => {\n    if (value && !R_HOST.test(value)) {\n        return i18next.t('form_error_domain_format');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validateAnswer = (value: any) => {\n    if (value && !R_IPV4.test(value) && !R_IPV6.test(value) && !R_HOST.test(value)) {\n        return i18next.t('form_error_answer_format');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {undefined|string}\n */\nexport const validatePath = (value: any) => {\n    if (value && !isValidAbsolutePath(value) && !R_URL_REQUIRES_PROTOCOL.test(value)) {\n        return i18next.t('form_error_url_or_path_format');\n    }\n    return undefined;\n};\n\n/**\n * @param cidr {string}\n * @returns {Function}\n */\nexport const validateIpv4InCidr = (valueIp: any, allValues: any) => {\n    if (!isIpInCidr(valueIp, allValues.cidr)) {\n        return i18next.t('form_error_subnet', { ip: valueIp, cidr: allValues.cidr });\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {number}\n */\nconst utf8StringLength = (value: any) => {\n    const encoder = new TextEncoder();\n    const view = encoder.encode(value);\n\n    return view.length;\n};\n\n/**\n * @param value {string}\n * @returns {Function}\n */\nexport const validatePasswordLength = (value: any) => {\n    if (value) {\n        const length = utf8StringLength(value);\n        if (length < MIN_PASSWORD_LENGTH || length > MAX_PASSWORD_LENGTH) {\n            // TODO: Make the i18n clearer with regards to bytes vs. characters.\n            return i18next.t('form_error_password_length', {\n                min: MIN_PASSWORD_LENGTH,\n                max: MAX_PASSWORD_LENGTH,\n            });\n        }\n    }\n\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {Function}\n */\nexport const validateIpGateway = (value: any, allValues: any) => {\n    if (value === allValues.gatewayIp) {\n        return i18next.t('form_error_gateway_ip');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {Function}\n */\nexport const validateIPv4Subnet = (value: any) => {\n    if (!R_IPV4_SUBNET.test(value)) {\n        return i18next.t('rate_limit_subnet_len_ipv4_error');\n    }\n    return undefined;\n};\n\n/**\n * @param value {string}\n * @returns {Function}\n */\nexport const validateIPv6Subnet = (value: any) => {\n    if (!R_IPV6_SUBNET.test(value)) {\n        return i18next.t('rate_limit_subnet_len_ipv6_error');\n    }\n    return undefined;\n};\n\n/**\n * @returns {undefined|string}\n * @param value\n * @param allValues\n */\nexport const validatePlainDns = (value: any, allValues: any) => {\n    const { enabled } = allValues;\n\n    if (!enabled && !value) {\n        return i18next.t('encryption_plain_dns_error');\n    }\n\n    return undefined;\n};\n"
  },
  {
    "path": "client/src/helpers/version.ts",
    "content": "/**\n * Checks if versions are equal.\n * Please note, that this method strips the \"v\" prefix.\n *\n * @param left {string} - left version\n * @param right {string} - right version\n * @return {boolean} true if versions are equal\n */\nexport const areEqualVersions = (left: any, right: any) => {\n    if (!left || !right) {\n        return false;\n    }\n\n    const leftVersion = left.replace(/^v/, '');\n    const rightVersion = right.replace(/^v/, '');\n    return leftVersion === rightVersion;\n};\n"
  },
  {
    "path": "client/src/i18n.ts",
    "content": "import i18n from 'i18next';\nimport { initReactI18next } from 'react-i18next';\nimport langDetect from 'i18next-browser-languagedetector';\nimport { setHtmlLangAttr } from './helpers/helpers';\n\nimport { LANGUAGES, BASE_LOCALE } from './helpers/twosky';\n\n// Main translations\nimport ar from './__locales/ar.json';\nimport be from './__locales/be.json';\nimport bg from './__locales/bg.json';\nimport cs from './__locales/cs.json';\nimport da from './__locales/da.json';\nimport de from './__locales/de.json';\nimport en from './__locales/en.json';\nimport es from './__locales/es.json';\nimport fa from './__locales/fa.json';\nimport fi from './__locales/fi.json';\nimport fr from './__locales/fr.json';\nimport hr from './__locales/hr.json';\nimport hu from './__locales/hu.json';\nimport id from './__locales/id.json';\nimport it from './__locales/it.json';\nimport ja from './__locales/ja.json';\nimport ko from './__locales/ko.json';\nimport nl from './__locales/nl.json';\nimport no from './__locales/no.json';\nimport pl from './__locales/pl.json';\nimport ptBR from './__locales/pt-br.json';\nimport ptPT from './__locales/pt-pt.json';\nimport ro from './__locales/ro.json';\nimport ru from './__locales/ru.json';\nimport siLk from './__locales/si-lk.json';\nimport sk from './__locales/sk.json';\nimport sl from './__locales/sl.json';\nimport srCS from './__locales/sr-cs.json';\nimport sv from './__locales/sv.json';\nimport th from './__locales/th.json';\nimport tr from './__locales/tr.json';\nimport uk from './__locales/uk.json';\nimport vi from './__locales/vi.json';\nimport zhCN from './__locales/zh-cn.json';\nimport zhHK from './__locales/zh-hk.json';\nimport zhTW from './__locales/zh-tw.json';\n\n// Services translations\nimport arServices from './__locales-services/ar.json';\nimport beServices from './__locales-services/be.json';\nimport bgServices from './__locales-services/bg.json';\nimport csServices from './__locales-services/cs.json';\nimport daServices from './__locales-services/da.json';\nimport deServices from './__locales-services/de.json';\nimport enServices from './__locales-services/en.json';\nimport esServices from './__locales-services/es.json';\nimport faServices from './__locales-services/fa.json';\nimport fiServices from './__locales-services/fi.json';\nimport frServices from './__locales-services/fr.json';\nimport hrServices from './__locales-services/hr.json';\nimport huServices from './__locales-services/hu.json';\nimport idServices from './__locales-services/id.json';\nimport itServices from './__locales-services/it.json';\nimport jaServices from './__locales-services/ja.json';\nimport koServices from './__locales-services/ko.json';\nimport nlServices from './__locales-services/nl.json';\nimport noServices from './__locales-services/no.json';\nimport plServices from './__locales-services/pl.json';\nimport ptBRServices from './__locales-services/pt-br.json';\nimport ptPTServices from './__locales-services/pt-pt.json';\nimport roServices from './__locales-services/ro.json';\nimport ruServices from './__locales-services/ru.json';\nimport siLkServices from './__locales-services/si-lk.json';\nimport skServices from './__locales-services/sk.json';\nimport slServices from './__locales-services/sl.json';\nimport srCSServices from './__locales-services/sr-cs.json';\nimport svServices from './__locales-services/sv.json';\nimport thServices from './__locales-services/th.json';\nimport trServices from './__locales-services/tr.json';\nimport ukServices from './__locales-services/uk.json';\nimport viServices from './__locales-services/vi.json';\nimport zhCNServices from './__locales-services/zh-cn.json';\nimport zhHKServices from './__locales-services/zh-hk.json';\nimport zhTWServices from './__locales-services/zh-tw.json';\n\n/**\n * Helper function to convert services object into a flat `{ key: message }` format.\n *\n * Supported formats:\n * - { message: \"...\" }\n *\n * Example:\n * Input:  { a: { message: \"one\" }, b: { message: \"two\" } }\n * Output: { a: \"one\", b: \"two\" }\n */\nconst convertServicesFormat = (\n    services: Record<string, { message: string }>,\n): Record<string, string> => {\n    return Object.fromEntries(\n        Object.entries(services).map(([key, value]) => [key, value.message])\n    );\n};\n\n// Resources\nconst resources = {\n    ar: {\n        translation: ar,\n        services: convertServicesFormat(arServices)\n    },\n    be: {\n        translation: be,\n        services: convertServicesFormat(beServices)\n    },\n    bg: {\n        translation: bg,\n        services: convertServicesFormat(bgServices)\n    },\n    cs: {\n        translation: cs,\n        services: convertServicesFormat(csServices)\n    },\n    da: {\n        translation: da,\n        services: convertServicesFormat(daServices)\n    },\n    de: {\n        translation: de,\n        services: convertServicesFormat(deServices)\n    },\n    en: {\n        translation: en,\n        services: convertServicesFormat(enServices)\n    },\n    'en-us': {\n        translation: en,\n        services: convertServicesFormat(enServices)\n    },\n    es: {\n        translation: es,\n        services: convertServicesFormat(esServices)\n    },\n    fa: {\n        translation: fa,\n        services: convertServicesFormat(faServices)\n    },\n    fi: {\n        translation: fi,\n        services: convertServicesFormat(fiServices)\n    },\n    fr: {\n        translation: fr,\n        services: convertServicesFormat(frServices)\n    },\n    hr: {\n        translation: hr,\n        services: convertServicesFormat(hrServices)\n    },\n    hu: {\n        translation: hu,\n        services: convertServicesFormat(huServices)\n    },\n    id: {\n        translation: id,\n        services: convertServicesFormat(idServices)\n    },\n    it: {\n        translation: it,\n        services: convertServicesFormat(itServices)\n    },\n    ja: {\n        translation: ja,\n        services: convertServicesFormat(jaServices)\n    },\n    ko: {\n        translation: ko,\n        services: convertServicesFormat(koServices)\n    },\n    nl: {\n        translation: nl,\n        services: convertServicesFormat(nlServices)\n    },\n    no: {\n        translation: no,\n        services: convertServicesFormat(noServices)\n    },\n    pl: {\n        translation: pl,\n        services: convertServicesFormat(plServices)\n    },\n    'pt-br': {\n        translation: ptBR,\n        services: convertServicesFormat(ptBRServices)\n    },\n    'pt-pt': {\n        translation: ptPT,\n        services: convertServicesFormat(ptPTServices)\n    },\n    ro: {\n        translation: ro,\n        services: convertServicesFormat(roServices)\n    },\n    ru: {\n        translation: ru,\n        services: convertServicesFormat(ruServices)\n    },\n    'si-lk': {\n        translation: siLk,\n        services: convertServicesFormat(siLkServices)\n    },\n    sk: {\n        translation: sk,\n        services: convertServicesFormat(skServices)\n    },\n    sl: {\n        translation: sl,\n        services: convertServicesFormat(slServices)\n    },\n    'sr-cs': {\n        translation: srCS,\n        services: convertServicesFormat(srCSServices)\n    },\n    sv: {\n        translation: sv,\n        services: convertServicesFormat(svServices)\n    },\n    th: {\n        translation: th,\n        services: convertServicesFormat(thServices)\n    },\n    tr: {\n        translation: tr,\n        services: convertServicesFormat(trServices)\n    },\n    uk: {\n        translation: uk,\n        services: convertServicesFormat(ukServices)\n    },\n    vi: {\n        translation: vi,\n        services: convertServicesFormat(viServices)\n    },\n    'zh-cn': {\n        translation: zhCN,\n        services: convertServicesFormat(zhCNServices)\n    },\n    'zh-hk': {\n        translation: zhHK,\n        services: convertServicesFormat(zhHKServices)\n    },\n    'zh-tw': {\n        translation: zhTW,\n        services: convertServicesFormat(zhTWServices)\n    },\n};\n\nconst availableLanguages = Object.keys(LANGUAGES);\n\ni18n\n    .use(langDetect)\n    .use(initReactI18next)\n    .init(\n        {\n            resources,\n            lowerCaseLng: true,\n            fallbackLng: BASE_LOCALE,\n            keySeparator: false,\n            nsSeparator: false,\n            returnEmptyString: false,\n            ns: ['translation', 'services'],\n            defaultNS: 'translation',\n            interpolation: {\n                escapeValue: false,\n            },\n            react: {\n                wait: true,\n                bindI18n: 'languageChanged loaded',\n            },\n            whitelist: availableLanguages,\n        },\n        () => {\n            if (!availableLanguages.includes(i18n.language)) {\n                i18n.changeLanguage(BASE_LOCALE);\n            }\n            setHtmlLangAttr(i18n.language);\n        }\n    );\n\ni18n.on('languageChanged', (lng) => {\n    setHtmlLangAttr(lng);\n});\n\nexport default i18n;\n"
  },
  {
    "path": "client/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\nimport configureStore from './configureStore';\nimport reducers from './reducers';\n\nimport App from './components/App';\nimport './components/App/index.css';\nimport './i18n';\nimport { RootState, initialState } from './initialState';\n\nconst store = configureStore<RootState>(reducers, initialState);\n\nReactDOM.render(\n    <Provider store={store}>\n        <App />\n    </Provider>,\n    document.getElementById('root'),\n);\n"
  },
  {
    "path": "client/src/initialState.ts",
    "content": "import {\n    BLOCKING_MODES,\n    DAY,\n    DEFAULT_LOGS_FILTER,\n    STANDARD_DNS_PORT,\n    STANDARD_WEB_PORT,\n    TIME_UNITS,\n} from './helpers/constants';\nimport { DEFAULT_BLOCKING_IPV4, DEFAULT_BLOCKING_IPV6 } from './reducers/dnsConfig';\nimport { Filter } from './helpers/helpers';\n\nexport type InstallInterface = {\n    flags: string;\n    hardware_address: string;\n    ip_addresses: string[];\n    mtu: number;\n    name: string;\n};\n\nexport type InstallData = {\n    step: number;\n    processingDefault: boolean;\n    processingSubmit: boolean;\n    processingCheck: boolean;\n    web: {\n        ip: string;\n        port: number;\n        status: string;\n        can_autofix: boolean;\n    };\n    dns: {\n        ip: string;\n        port: number;\n        status: string;\n        can_autofix: boolean;\n    };\n    staticIp: {\n        static: string;\n        ip: string;\n        error: string;\n    };\n    interfaces: InstallInterface[];\n    dnsVersion: string;\n};\n\nexport type EncryptionData = {\n    processing: boolean;\n    processingConfig: boolean;\n    processingValidate: boolean;\n    enabled: boolean;\n    serve_plain_dns: boolean;\n    dns_names: any;\n    force_https: boolean;\n    issuer: string;\n    key_type: string;\n    not_after: string;\n    not_before: string;\n    port_dns_over_tls?: number;\n    port_dns_over_quic?: number;\n    port_https?: number;\n    port_dnscrypt?: number;\n    subject: string;\n    valid_chain: boolean;\n    valid_key: boolean;\n    valid_cert: boolean;\n    valid_pair: boolean;\n    status_cert: string;\n    status_key: string;\n    private_key: string;\n    server_name: string;\n    warning_validation: string;\n    certificate_chain: string;\n    certificate_path: string;\n    private_key_path: string;\n    private_key_saved: boolean;\n    allow_unencrypted_doh?: boolean;\n    dnscrypt_config_file?: string;\n};\n\nexport type Client = {\n    blocked_services: string[];\n    blocked_services_schedule: {\n        sun?: { start: number; end: number };\n        mon?: { start: number; end: number };\n        tue?: { start: number; end: number };\n        wed?: { start: number; end: number };\n        thu?: { start: number; end: number };\n        fri?: { start: number; end: number };\n        sat?: { start: number; end: number };\n        time_zone: string;\n    };\n    filtering_enabled: boolean;\n    ids: string[];\n    ignore_querylog: boolean;\n    ignore_statistics: boolean;\n    name: string;\n    parental_enabled: boolean;\n    safe_search: Record<string, boolean>;\n    safebrowsing_enabled: boolean;\n    safesearch_enabled: boolean;\n    tags: string[];\n    upstreams: string[];\n    upstreams_cache_enabled: boolean;\n    upstreams_cache_size: number;\n    use_global_blocked_services: boolean;\n    use_global_settings: boolean;\n};\n\nexport type AutoClient = {\n    ip: string;\n    name: string;\n    source: string;\n    whois_info: any;\n};\n\nexport type DashboardData = {\n    processing: boolean;\n    isCoreRunning: boolean;\n    processingVersion: boolean;\n    processingClients: boolean;\n    processingUpdate: boolean;\n    processingProfile: boolean;\n    protectionEnabled: boolean;\n    protectionDisabledDuration: any;\n    protectionCountdownActive: boolean;\n    processingProtection: boolean;\n    httpPort: number;\n    dnsPort: number;\n    dnsAddresses: string[];\n    dnsVersion: string;\n    dnsStartTime: number | null;\n    clients: Client[];\n    autoClients: AutoClient[];\n    supportedTags: string[];\n    name: string;\n    theme: string | null;\n    checkUpdateFlag: boolean;\n    announcementUrl: string;\n    newVersion: string;\n    canAutoUpdate: boolean;\n    language: string;\n    isUpdateAvailable: boolean;\n};\n\nexport type SettingsData = {\n    processing: boolean;\n    processingTestUpstream: boolean;\n    processingDhcpStatus: boolean;\n    settingsList?: {\n        parental: {\n            enabled: boolean;\n            order: number;\n            subtitle: string;\n            title: string;\n        };\n        safebrowsing: {\n            enabled: boolean;\n            order: number;\n            subtitle: string;\n            title: string;\n        };\n        safesearch: Record<string, boolean>;\n    };\n};\n\nexport type RewritesData = {\n    processing: boolean;\n    processingAdd: boolean;\n    processingDelete: boolean;\n    processingUpdate: boolean;\n    isModalOpen: boolean;\n    modalType: string;\n    currentRewrite?: {\n        answer: string;\n        domain: string;\n        enabled: boolean;\n    };\n    list: {\n        answer: string;\n        domain: string;\n        enabled: boolean;\n    }[];\n    settings: {\n        enabled: boolean;\n    };\n};\n\nexport type NormalizedTopClients = {\n    auto: Record<string, number>;\n    configured: Record<string, number>;\n};\n\nexport type StatsData = {\n    processingGetConfig: boolean;\n    processingSetConfig: boolean;\n    processingStats: boolean;\n    processingReset: boolean;\n    interval: number;\n    customInterval?: number;\n    ignored_enabled: boolean;\n    dnsQueries: number[];\n    blockedFiltering: number[];\n    replacedParental: number[];\n    replacedSafebrowsing: number[];\n    topBlockedDomains: { name: string; count: number }[];\n    topClients: {\n        name: string;\n        count: number;\n        info: any;\n    }[];\n    normalizedTopClients?: NormalizedTopClients;\n    topQueriedDomains: { name: string; count: number }[];\n    numBlockedFiltering: number;\n    numDnsQueries: number;\n    numReplacedParental: number;\n    numReplacedSafebrowsing: number;\n    numReplacedSafesearch: number;\n    avgProcessingTime: number;\n    timeUnits: string;\n    enabled: boolean;\n    topUpstreamsAvgTime: { name: string; count: number }[];\n    topUpstreamsResponses: { name: string; count: number }[];\n};\n\nexport type ClientsData = {\n    processing: boolean;\n    processingAdding: boolean;\n    processingDeleting: boolean;\n    processingUpdating: boolean;\n    isModalOpen: boolean;\n    modalClientName: string;\n    modalType: string;\n};\n\nexport type AccessData = {\n    processing: boolean;\n    processingSet: boolean;\n    allowed_clients: string;\n    disallowed_clients: string;\n    blocked_hosts: string;\n};\n\nexport type DhcpInterface = {\n    name: string;\n    flags: string;\n    gateway_ip: string;\n    ip_addresses: string[];\n    ipv4_addresses: string[];\n    ipv6_addresses: string[];\n    hardware_address: string;\n};\n\nexport type DhcpData = {\n    processing: boolean;\n    processingStatus: boolean;\n    processingInterfaces: boolean;\n    processingDhcp: boolean;\n    processingConfig: boolean;\n    processingAdding: boolean;\n    processingDeleting: boolean;\n    processingUpdating: boolean;\n    enabled: boolean;\n    interface_name: string;\n    check?: {\n        v4?: {\n            other_server?: { found: string; error?: string };\n            static_ip?: { static: string; ip: string };\n        };\n        v6?: {\n            other_server?: { found: string; error?: string };\n            static_ip?: { static: string; ip: string };\n        };\n    };\n    v4: {\n        gateway_ip: string;\n        subnet_mask: string;\n        range_start: string;\n        range_end: string;\n        lease_duration: number;\n    };\n    v6: {\n        range_start: string;\n        lease_duration: number;\n    };\n    leases: {\n        hostname: string;\n        ip: string;\n        mac: string;\n    }[];\n    staticLeases: {\n        hostname: string;\n        ip: string;\n        mac: string;\n    }[];\n    isModalOpen: boolean;\n    leaseModalConfig?: {\n        hostname: string;\n        ip: string;\n        mac: string;\n    };\n    modalType: string;\n    dhcp_available: boolean;\n    interfaces?: DhcpInterface[];\n};\n\nexport type DnsConfigData = {\n    processingGetConfig: boolean;\n    processingSetConfig: boolean;\n    blocking_mode: string;\n    ratelimit: number;\n    blocking_ipv4: string;\n    blocking_ipv6: string;\n    blocked_response_ttl: number;\n    upstream_timeout: number;\n    edns_cs_enabled: boolean;\n    disable_ipv6: boolean;\n    dnssec_enabled: boolean;\n    upstream_dns_file: string;\n    upstream_dns: string;\n    fallback_dns: string;\n    bootstrap_dns: string;\n    local_ptr_upstreams: string;\n    ratelimit_whitelist: string;\n    upstream_mode: string;\n    resolve_clients: boolean;\n    use_private_ptr_resolvers: boolean;\n    default_local_ptr_upstreams: any[];\n    ratelimit_subnet_len_ipv4?: number;\n    ratelimit_subnet_len_ipv6?: number;\n    edns_cs_use_custom?: boolean;\n    edns_cs_custom_ip?: string;\n    cache_enabled?: boolean;\n    cache_size?: number;\n    cache_ttl_max?: number;\n    cache_ttl_min?: number;\n    cache_optimistic?: boolean;\n};\n\nexport type FilteringData = {\n    isModalOpen: boolean;\n    processingFilters: boolean;\n    processingRules: boolean;\n    processingAddFilter: boolean;\n    processingRefreshFilters: boolean;\n    processingConfigFilter: boolean;\n    processingRemoveFilter: boolean;\n    processingSetConfig: boolean;\n    processingCheck: boolean;\n    isFilterAdded: boolean;\n    filters: Filter[];\n    whitelistFilters: any[];\n    userRules: string;\n    interval: number;\n    enabled: boolean;\n    modalType: string;\n    modalFilterUrl: string;\n    check: any;\n};\n\nexport type QueryLogsData = {\n    processingGetLogs: boolean;\n    processingClear: boolean;\n    processingGetConfig: boolean;\n    processingSetConfig: boolean;\n    processingAdditionalLogs: boolean;\n    interval: any;\n    logs: any[];\n    enabled: boolean;\n    oldest: string;\n    filter: any;\n    isFiltered: boolean;\n    anonymize_client_ip: boolean;\n    isDetailed: boolean;\n    isEntireLog: boolean;\n    customInterval: any;\n    ignored_enabled: boolean;\n};\n\nexport type ServicesData = {\n    processing: boolean;\n    processingAll: boolean;\n    processingSet: boolean;\n    list: any;\n    allServices: any[];\n    allGroups: any[];\n};\n\nexport type RootState = {\n    access?: AccessData;\n    clients?: ClientsData;\n    dashboard?: DashboardData;\n    dhcp?: DhcpData;\n    dnsConfig?: DnsConfigData;\n    encryption?: EncryptionData;\n    filtering?: FilteringData;\n    queryLogs?: QueryLogsData;\n    rewrites?: RewritesData;\n    services?: ServicesData;\n    settings?: SettingsData;\n    stats?: StatsData;\n    install?: InstallData;\n    toasts: { notices: any[] };\n    loadingBar: any;\n};\n\nexport type InstallState = {\n    install: InstallData;\n    toasts: { notices: any[] };\n};\n\nexport type LoginState = {\n    login: {\n        processingLogin: false;\n        email: string;\n        password: string;\n    };\n    toasts: { notices: any[] };\n};\n\nexport const initialState: RootState = {\n    access: {\n        processing: true,\n        processingSet: false,\n        allowed_clients: '',\n        disallowed_clients: '',\n        blocked_hosts: '',\n    },\n    clients: {\n        processing: true,\n        processingAdding: false,\n        processingDeleting: false,\n        processingUpdating: false,\n        isModalOpen: false,\n        modalClientName: '',\n        modalType: '',\n    },\n    dashboard: {\n        processing: true,\n        isCoreRunning: true,\n        processingVersion: true,\n        processingClients: true,\n        processingUpdate: false,\n        processingProfile: true,\n        protectionEnabled: false,\n        protectionDisabledDuration: null,\n        protectionCountdownActive: false,\n        processingProtection: false,\n        httpPort: STANDARD_WEB_PORT,\n        dnsPort: STANDARD_DNS_PORT,\n        dnsAddresses: [],\n        dnsVersion: '',\n        dnsStartTime: null,\n        clients: [],\n        autoClients: [],\n        supportedTags: [],\n        name: '',\n        theme: undefined,\n        checkUpdateFlag: false,\n        announcementUrl: '',\n        newVersion: '',\n        canAutoUpdate: false,\n        language: '', // ???\n        isUpdateAvailable: false,\n    },\n    dhcp: {\n        processing: true,\n        processingStatus: false,\n        processingInterfaces: false,\n        processingDhcp: false,\n        processingConfig: false,\n        processingAdding: false,\n        processingDeleting: false,\n        processingUpdating: false,\n        enabled: false,\n        interface_name: '',\n        check: null,\n        v4: {\n            gateway_ip: '',\n            subnet_mask: '',\n            range_start: '',\n            range_end: '',\n            lease_duration: 0,\n        },\n        v6: {\n            range_start: '',\n            lease_duration: 0,\n        },\n        leases: [],\n        staticLeases: [],\n        isModalOpen: false,\n        leaseModalConfig: undefined,\n        modalType: '',\n        dhcp_available: false,\n    },\n    dnsConfig: {\n        processingGetConfig: false,\n        processingSetConfig: false,\n        blocking_mode: BLOCKING_MODES.default,\n        ratelimit: 20,\n        blocking_ipv4: DEFAULT_BLOCKING_IPV4,\n        blocking_ipv6: DEFAULT_BLOCKING_IPV6,\n        blocked_response_ttl: 10,\n        upstream_timeout: 10,\n        edns_cs_enabled: false,\n        disable_ipv6: false,\n        dnssec_enabled: false,\n        upstream_dns_file: '',\n        upstream_dns: '',\n        fallback_dns: '',\n        bootstrap_dns: '',\n        local_ptr_upstreams: '',\n        ratelimit_whitelist: '',\n        upstream_mode: '',\n        resolve_clients: false,\n        use_private_ptr_resolvers: false,\n        default_local_ptr_upstreams: [],\n    },\n    encryption: {\n        processing: true,\n        processingConfig: false,\n        processingValidate: false,\n        enabled: false,\n        serve_plain_dns: false,\n        dns_names: null,\n        force_https: false,\n        issuer: '',\n        key_type: '',\n        not_after: '',\n        not_before: '',\n        subject: '',\n        valid_chain: false,\n        valid_key: false,\n        valid_cert: false,\n        valid_pair: false,\n        status_cert: '',\n        status_key: '',\n        certificate_chain: '',\n        private_key: '',\n        server_name: '',\n        warning_validation: '',\n        certificate_path: '',\n        private_key_path: '',\n        private_key_saved: false,\n    },\n    filtering: {\n        isModalOpen: false,\n        processingFilters: false,\n        processingRules: false,\n        processingAddFilter: false,\n        processingRefreshFilters: false,\n        processingConfigFilter: false,\n        processingRemoveFilter: false,\n        processingSetConfig: false,\n        processingCheck: false,\n        isFilterAdded: false,\n        filters: [],\n        whitelistFilters: [],\n        userRules: '',\n        interval: 24,\n        enabled: true,\n        modalType: '',\n        modalFilterUrl: '',\n        check: {},\n    },\n    queryLogs: {\n        processingGetLogs: true,\n        processingClear: false,\n        processingGetConfig: false,\n        processingSetConfig: false,\n        processingAdditionalLogs: false,\n        interval: DAY,\n        logs: [],\n        enabled: true,\n        oldest: '',\n        filter: DEFAULT_LOGS_FILTER,\n        isFiltered: false,\n        anonymize_client_ip: false,\n        isDetailed: true,\n        isEntireLog: false,\n        customInterval: null,\n        ignored_enabled: true,\n    },\n    rewrites: {\n        processing: true,\n        processingAdd: false,\n        processingDelete: false,\n        processingUpdate: false,\n        isModalOpen: false,\n        modalType: '',\n        list: [],\n        settings: { enabled: false },\n    },\n    services: {\n        processing: true,\n        processingAll: true,\n        processingSet: false,\n        list: {},\n        allServices: [],\n        allGroups: [],\n    },\n    settings: {\n        processing: true,\n        processingTestUpstream: false,\n        processingDhcpStatus: false,\n    },\n    stats: {\n        processingGetConfig: false,\n        processingSetConfig: false,\n        processingStats: true,\n        processingReset: false,\n        interval: DAY,\n        customInterval: null,\n        ignored_enabled: true,\n        dnsQueries: [],\n        blockedFiltering: [],\n        replacedParental: [],\n        replacedSafebrowsing: [],\n        topBlockedDomains: [],\n        topClients: [],\n        topQueriedDomains: [],\n        numBlockedFiltering: 0,\n        numDnsQueries: 0,\n        numReplacedParental: 0,\n        numReplacedSafebrowsing: 0,\n        numReplacedSafesearch: 0,\n        avgProcessingTime: 0,\n        timeUnits: TIME_UNITS.HOURS,\n        enabled: true,\n        topUpstreamsAvgTime: [],\n        topUpstreamsResponses: [],\n    },\n    toasts: { notices: [] },\n    loadingBar: {},\n};\n"
  },
  {
    "path": "client/src/install/Setup/AddressList.tsx",
    "content": "import React from 'react';\n\nimport { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';\nimport { ALL_INTERFACES_IP } from '../../helpers/constants';\nimport { InstallInterface } from '../../initialState';\n\ninterface renderItemProps {\n    ip: string;\n    port: number;\n    isDns: boolean;\n}\n\nconst renderItem = ({ ip, port, isDns }: renderItemProps) => {\n    const webAddress = getWebAddress(ip, port);\n    const dnsAddress = getDnsAddress(ip, port);\n\n    return (\n        <li key={ip}>\n            {isDns ? (\n                <strong>{dnsAddress}</strong>\n            ) : (\n                <a href={webAddress} target=\"_blank\" rel=\"noopener noreferrer\">\n                    {webAddress}\n                </a>\n            )}\n        </li>\n    );\n};\n\ninterface AddressListProps {\n    interfaces: InstallInterface[];\n    address: string;\n    port: number;\n    isDns?: boolean;\n}\n\nconst AddressList = ({ address, interfaces, port, isDns }: AddressListProps) => (\n    <ul className=\"list-group pl-4\">\n        {address === ALL_INTERFACES_IP\n            ? getIpList(interfaces).map((ip: any) =>\n                  renderItem({\n                      ip,\n                      port,\n                      isDns,\n                  }),\n              )\n            : renderItem({\n                  ip: address,\n                  port,\n                  isDns,\n              })}\n    </ul>\n);\n\nexport default AddressList;\n"
  },
  {
    "path": "client/src/install/Setup/Auth.tsx",
    "content": "import React from 'react';\nimport { Controller, useForm } from 'react-hook-form';\nimport { Trans, useTranslation } from 'react-i18next';\nimport Controls from './Controls';\nimport { validatePasswordLength, validateRequiredValue } from '../../helpers/validators';\nimport { Input } from '../../components/ui/Controls/Input';\n\ntype AuthFormValues = {\n    username: string;\n    password: string;\n    confirm_password: string;\n};\n\ntype Props = {\n    onAuthSubmit: (values: AuthFormValues) => void;\n};\n\nexport const Auth = ({ onAuthSubmit }: Props) => {\n    const { t } = useTranslation();\n    const {\n        handleSubmit,\n        watch,\n        control,\n        formState: { isDirty, isValid },\n    } = useForm<AuthFormValues>({\n        mode: 'onBlur',\n        defaultValues: {\n            username: '',\n            password: '',\n            confirm_password: '',\n        },\n    });\n\n    const password = watch('password');\n\n    const validateConfirmPassword = (value: string) => {\n        if (value !== password) {\n            return t('form_error_password');\n        }\n        return undefined;\n    };\n\n    return (\n        <form className=\"setup__step\" onSubmit={handleSubmit(onAuthSubmit)}>\n            <div className=\"setup__group\">\n                <div className=\"setup__subtitle\">\n                    <Trans>install_auth_title</Trans>\n                </div>\n\n                <p className=\"setup__desc\">\n                    <Trans>install_auth_desc</Trans>\n                </p>\n\n                <div className=\"form-group\">\n                    <Controller\n                        name=\"username\"\n                        control={control}\n                        rules={{ validate: validateRequiredValue }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"text\"\n                                data-testid=\"install_username\"\n                                label={t('install_auth_username')}\n                                placeholder={t('install_auth_username_enter')}\n                                error={fieldState.error?.message}\n                                autoComplete=\"username\"\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"form-group\">\n                    <Controller\n                        name=\"password\"\n                        control={control}\n                        rules={{\n                            validate: {\n                                required: validateRequiredValue,\n                                passwordLength: validatePasswordLength,\n                            },\n                        }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"password\"\n                                data-testid=\"install_password\"\n                                label={t('install_auth_password')}\n                                placeholder={t('install_auth_password_enter')}\n                                error={fieldState.error?.message}\n                                autoComplete=\"new-password\"\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"form-group\">\n                    <Controller\n                        name=\"confirm_password\"\n                        control={control}\n                        rules={{\n                            validate: {\n                                required: validateRequiredValue,\n                                confirmPassword: validateConfirmPassword,\n                            },\n                        }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                type=\"password\"\n                                data-testid=\"install_confirm_password\"\n                                label={t('install_auth_confirm')}\n                                placeholder={t('install_auth_confirm')}\n                                error={fieldState.error?.message}\n                                autoComplete=\"new-password\"\n                            />\n                        )}\n                    />\n                </div>\n            </div>\n\n            <Controls isDirty={isDirty} isValid={isValid} />\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/install/Setup/Controls.tsx",
    "content": "import React, { Component } from 'react';\nimport { connect } from 'react-redux';\nimport { Trans } from 'react-i18next';\n\nimport * as actionCreators from '../../actions/install';\n\ninterface ControlsProps {\n    install: {\n        step: number;\n        processingSubmit: boolean;\n        dns: {\n            status: string;\n        };\n        web: {\n            status: string;\n        };\n    };\n    nextStep?: (...args: unknown[]) => unknown;\n    prevStep?: (...args: unknown[]) => unknown;\n    openDashboard?: (...args: unknown[]) => unknown;\n    submitting?: boolean;\n    invalid?: boolean;\n    pristine?: boolean;\n    ip?: string;\n    port?: number;\n}\n\nclass Controls extends Component<ControlsProps> {\n    renderPrevButton(step: any) {\n        switch (step) {\n            case 2:\n            case 3:\n                return (\n                    <button\n                        data-testid=\"install_back\"\n                        type=\"button\"\n                        className=\"btn btn-secondary btn-lg setup__button\"\n                        onClick={this.props.prevStep}>\n                        <Trans>back</Trans>\n                    </button>\n                );\n            default:\n                return false;\n        }\n    }\n\n    renderNextButton(step: any) {\n        const { nextStep, invalid, pristine, install, ip, port } = this.props;\n\n        switch (step) {\n            case 1:\n                return (\n                    <button\n                        data-testid=\"install_get_started\"\n                        type=\"button\"\n                        className=\"btn btn-success btn-lg setup__button\"\n                        onClick={nextStep}>\n                        <Trans>get_started</Trans>\n                    </button>\n                );\n            case 2:\n            case 3:\n                return (\n                    <button\n                        data-testid=\"install_next\"\n                        type=\"submit\"\n                        className=\"btn btn-success btn-lg setup__button\"\n                        disabled={invalid || pristine || install.processingSubmit}>\n                        <Trans>next</Trans>\n                    </button>\n                );\n            case 4:\n                return (\n                    <button\n                        data-testid=\"install_next\"\n                        type=\"button\"\n                        className=\"btn btn-success btn-lg setup__button\"\n                        onClick={nextStep}>\n                        <Trans>next</Trans>\n                    </button>\n                );\n            case 5:\n                return (\n                    <button\n                        data-testid=\"install_open_dashboard\"\n                        type=\"button\"\n                        className=\"btn btn-success btn-lg setup__button\"\n                        onClick={() => this.props.openDashboard(ip, port)}>\n                        <Trans>open_dashboard</Trans>\n                    </button>\n                );\n            default:\n                return false;\n        }\n    }\n\n    render() {\n        const { install } = this.props;\n\n        return (\n            <div className=\"setup__nav\">\n                <div className=\"btn-list\">\n                    {this.renderPrevButton(install.step)}\n                    {this.renderNextButton(install.step)}\n                </div>\n            </div>\n        );\n    }\n}\n\nconst mapStateToProps = (state: any) => {\n    const { install } = state;\n    return { install };\n};\n\nexport default connect(mapStateToProps, actionCreators)(Controls);\n"
  },
  {
    "path": "client/src/install/Setup/Devices.tsx",
    "content": "import React from 'react';\n\nimport { Trans } from 'react-i18next';\n\nimport { Guide } from '../../components/ui/Guide';\n\nimport Controls from './Controls';\n\nimport AddressList from './AddressList';\nimport { InstallInterface } from '../../initialState';\nimport { DnsConfig } from './Settings';\n\ntype Props = {\n    interfaces: InstallInterface[];\n    dnsConfig: DnsConfig;\n};\n\nexport const Devices = ({ interfaces, dnsConfig }: Props) => (\n    <div className=\"setup__step\">\n        <div className=\"setup__group\">\n            <div className=\"setup__subtitle\">\n                <Trans>install_devices_title</Trans>\n            </div>\n\n            <div className=\"setup__desc\">\n                <Trans>install_devices_desc</Trans>\n\n                <div className=\"mt-1\">\n                    <Trans>install_devices_address</Trans>:\n                </div>\n\n                <div className=\"mt-1\">\n                    <AddressList interfaces={interfaces} address={dnsConfig.ip} port={dnsConfig.port} isDns />\n                </div>\n            </div>\n\n            <Guide />\n        </div>\n\n        <Controls />\n    </div>\n);\n"
  },
  {
    "path": "client/src/install/Setup/Greeting.tsx",
    "content": "import React from 'react';\nimport { Trans, withTranslation } from 'react-i18next';\n\nimport Controls from './Controls';\n\nconst Greeting = () => (\n    <div className=\"setup__step\">\n        <div className=\"setup__group\">\n            <h1 className=\"setup__title\">\n                <Trans>install_welcome_title</Trans>\n            </h1>\n\n            <p className=\"setup__desc text-center\">\n                <Trans>install_welcome_desc</Trans>\n            </p>\n        </div>\n\n        <Controls />\n    </div>\n);\n\nexport default withTranslation()(Greeting);\n"
  },
  {
    "path": "client/src/install/Setup/Progress.tsx",
    "content": "import React from 'react';\nimport { Trans } from 'react-i18next';\n\nimport { INSTALL_TOTAL_STEPS } from '../../helpers/constants';\n\nconst getProgressPercent = (step: number) => (step / INSTALL_TOTAL_STEPS) * 100;\n\ntype Props = {\n    step: number;\n};\n\nexport const Progress = ({ step }: Props) => (\n    <div className=\"setup__progress\">\n        <Trans>install_step</Trans> {step}/{INSTALL_TOTAL_STEPS}\n        <div className=\"setup__progress-wrap\">\n            <div className=\"setup__progress-inner\" style={{ width: `${getProgressPercent(step)}%` }} />\n        </div>\n    </div>\n);\n"
  },
  {
    "path": "client/src/install/Setup/Settings.tsx",
    "content": "import React, { useEffect, useCallback } from 'react';\nimport { useForm, Controller } from 'react-hook-form';\nimport { Trans, useTranslation } from 'react-i18next';\nimport i18n from 'i18next';\n\nimport Controls from './Controls';\nimport AddressList from './AddressList';\n\nimport { getInterfaceIp } from '../../helpers/helpers';\nimport {\n    ALL_INTERFACES_IP,\n    ADDRESS_IN_USE_TEXT,\n    PORT_53_FAQ_LINK,\n    STATUS_RESPONSE,\n    STANDARD_DNS_PORT,\n    STANDARD_WEB_PORT,\n    MAX_PORT,\n    MIN_PORT,\n} from '../../helpers/constants';\n\nimport { validateRequiredValue } from '../../helpers/validators';\nimport { InstallInterface } from '../../initialState';\nimport { Input } from '../../components/ui/Controls/Input';\nimport { Select } from '../../components/ui/Controls/Select';\nimport { toNumber } from '../../helpers/form';\n\nconst validateInstallPort = (value: number) => {\n    if (value < MIN_PORT || value > MAX_PORT) {\n        return i18n.t('form_error_port');\n    }\n    return undefined;\n};\n\nexport type WebConfig = {\n    ip: string;\n    port: number;\n};\n\nexport type DnsConfig = {\n    ip: string;\n    port: number;\n};\n\nexport type SettingsFormValues = {\n    web: WebConfig;\n    dns: DnsConfig;\n};\n\ntype StaticIpType = {\n    ip: string;\n    static: string;\n};\n\nexport type ConfigType = {\n    web: {\n        ip: string;\n        port?: number;\n        status: string;\n        can_autofix: boolean;\n    };\n    dns: {\n        ip: string;\n        port?: number;\n        status: string;\n        can_autofix: boolean;\n    };\n    staticIp: StaticIpType;\n};\n\ntype Props = {\n    handleSubmit: (data: SettingsFormValues) => void;\n    handleChange?: (data: SettingsFormValues) => unknown;\n    handleFix: (web: WebConfig, dns: DnsConfig, set_static_ip: boolean) => void;\n    validateForm: (data: SettingsFormValues) => void;\n    config: ConfigType;\n    interfaces: InstallInterface[];\n    initialValues?: object;\n};\n\nconst renderInterfaces = (interfaces: InstallInterface[]) =>\n    Object.values(interfaces).map((option: InstallInterface) => {\n        const { name, ip_addresses, flags } = option;\n\n        if (option && ip_addresses?.length > 0) {\n            const ip = getInterfaceIp(option);\n            const isUp = flags?.includes('up');\n\n            return (\n                <option value={ip} key={name} disabled={!isUp}>\n                    {name} - {ip} {!isUp && `(${i18n.t('down')})`}\n                </option>\n            );\n        }\n\n        return null;\n    });\n\nexport const Settings = ({ handleSubmit, handleFix, validateForm, config, interfaces }: Props) => {\n    const { t } = useTranslation();\n\n    const defaultValues = {\n        web: {\n            ip: config.web.ip || ALL_INTERFACES_IP,\n            port: config.web.port || STANDARD_WEB_PORT,\n        },\n        dns: {\n            ip: config.dns.ip || ALL_INTERFACES_IP,\n            port: config.dns.port || STANDARD_DNS_PORT,\n        },\n    };\n\n    const {\n        control,\n        watch,\n        handleSubmit: reactHookFormSubmit,\n        formState: { isValid },\n    } = useForm<SettingsFormValues>({\n        defaultValues,\n        mode: 'onBlur',\n    });\n\n    const watchFields = watch();\n\n    const { status: webStatus, can_autofix: isWebFixAvailable } = config.web;\n    const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns;\n    const { staticIp } = config;\n\n    const webIpVal = watch('web.ip');\n    const webPortVal = watch('web.port');\n    const dnsIpVal = watch('dns.ip');\n    const dnsPortVal = watch('dns.port');\n\n    useEffect(() => {\n        const webPortError = validateInstallPort(webPortVal);\n        const dnsPortError = validateInstallPort(dnsPortVal);\n\n        if (webPortError || dnsPortError) {\n            return;\n        }\n\n        validateForm({\n            web: {\n                ip: webIpVal,\n                port: webPortVal,\n            },\n            dns: {\n                ip: dnsIpVal,\n                port: dnsPortVal,\n            },\n        });\n    }, [webIpVal, webPortVal, dnsIpVal, dnsPortVal]);\n\n    const handleAutofix = (type: string) => {\n        const web = {\n            ip: watchFields.web?.ip,\n            port: watchFields.web?.port,\n            autofix: false,\n        };\n        const dns = {\n            ip: watchFields.dns?.ip,\n            port: watchFields.dns?.port,\n            autofix: false,\n        };\n        const set_static_ip = false;\n\n        if (type === 'web') {\n            web.autofix = true;\n        } else {\n            dns.autofix = true;\n        }\n\n        handleFix(web, dns, set_static_ip);\n    };\n\n    const handleStaticIp = (ip: string) => {\n        const web = {\n            ip: watchFields.web?.ip,\n            port: watchFields.web?.port,\n            autofix: false,\n        };\n        const dns = {\n            ip: watchFields.dns?.ip,\n            port: watchFields.dns?.port,\n            autofix: false,\n        };\n        const set_static_ip = true;\n\n        if (window.confirm(t('confirm_static_ip', { ip }))) {\n            handleFix(web, dns, set_static_ip);\n        }\n    };\n\n    const getStaticIpMessage = useCallback(\n        (staticIp: StaticIpType) => {\n            const { static: status, ip } = staticIp;\n\n            switch (status) {\n                case STATUS_RESPONSE.NO:\n                    return (\n                        <>\n                            <div className=\"mb-2\">\n                                <Trans values={{ ip }} components={[<strong key=\"0\">text</strong>]}>\n                                    install_static_configure\n                                </Trans>\n                            </div>\n\n                            <button\n                                type=\"button\"\n                                className=\"btn btn-outline-primary btn-sm\"\n                                onClick={() => handleStaticIp(ip)}>\n                                <Trans>set_static_ip</Trans>\n                            </button>\n                        </>\n                    );\n                case STATUS_RESPONSE.ERROR:\n                    return (\n                        <div className=\"text-danger\">\n                            <Trans>install_static_error</Trans>\n                        </div>\n                    );\n                case STATUS_RESPONSE.YES:\n                    return (\n                        <div className=\"text-success\">\n                            <Trans>install_static_ok</Trans>\n                        </div>\n                    );\n                default:\n                    return null;\n            }\n        },\n        [handleStaticIp],\n    );\n\n    const onSubmit = (data: SettingsFormValues) => {\n        validateForm(data);\n        handleSubmit(data);\n    };\n\n    return (\n        <form className=\"setup__step\" onSubmit={reactHookFormSubmit(onSubmit)}>\n            <div className=\"setup__group\">\n                <div className=\"setup__subtitle\">\n                    <Trans>install_settings_title</Trans>\n                </div>\n\n                <div className=\"row\">\n                    <div className=\"col-8\">\n                        <div className=\"form-group\">\n                            <label>\n                                <Trans>install_settings_listen</Trans>\n                            </label>\n                            <Controller\n                                name=\"web.ip\"\n                                control={control}\n                                render={({ field }) => (\n                                    <Select {...field} data-testid=\"install_web_ip\">\n                                        <option value={ALL_INTERFACES_IP}>\n                                            {t('install_settings_all_interfaces')}\n                                        </option>\n                                        {renderInterfaces(interfaces)}\n                                    </Select>\n                                )}\n                            />\n                        </div>\n                    </div>\n\n                    <div className=\"col-4\">\n                        <div className=\"form-group\">\n                            <label>\n                                <Trans>install_settings_port</Trans>\n                            </label>\n                            <Controller\n                                name=\"web.port\"\n                                control={control}\n                                rules={{\n                                    validate: {\n                                        required: validateRequiredValue,\n                                        installPort: validateInstallPort,\n                                    },\n                                }}\n                                render={({ field, fieldState }) => (\n                                    <Input\n                                        {...field}\n                                        type=\"number\"\n                                        data-testid=\"install_web_port\"\n                                        placeholder={STANDARD_WEB_PORT.toString()}\n                                        error={fieldState.error?.message}\n                                        onChange={(e) => {\n                                            const { value } = e.target;\n                                            field.onChange(toNumber(value));\n                                        }}\n                                    />\n                                )}\n                            />\n                        </div>\n                    </div>\n\n                    <div className=\"col-12\">\n                        {webStatus && (\n                            <div className=\"setup__error text-danger\">\n                                {webStatus}\n                                {isWebFixAvailable && (\n                                    <button\n                                        type=\"button\"\n                                        data-testid=\"install_web_fix\"\n                                        className=\"btn btn-secondary btn-sm ml-2\"\n                                        onClick={() => handleAutofix('web')}>\n                                        <Trans>fix</Trans>\n                                    </button>\n                                )}\n                            </div>\n                        )}\n\n                        <hr className=\"divider--small\" />\n                    </div>\n                </div>\n\n                <div className=\"setup__desc\">\n                    <Trans>install_settings_interface_link</Trans>\n\n                    <div className=\"mt-1\">\n                        <AddressList\n                            interfaces={interfaces}\n                            address={watchFields.web?.ip}\n                            port={watchFields.web?.port}\n                        />\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"setup__group\">\n                <div className=\"setup__subtitle\">\n                    <Trans>install_settings_dns</Trans>\n                </div>\n\n                <div className=\"row\">\n                    <div className=\"col-8\">\n                        <div className=\"form-group\">\n                            <label>\n                                <Trans>install_settings_listen</Trans>\n                            </label>\n                            <Controller\n                                name=\"dns.ip\"\n                                control={control}\n                                render={({ field }) => (\n                                    <Select {...field} data-testid=\"install_dns_ip\">\n                                        <option value={ALL_INTERFACES_IP}>\n                                            {t('install_settings_all_interfaces')}\n                                        </option>\n                                        {renderInterfaces(interfaces)}\n                                    </Select>\n                                )}\n                            />\n                        </div>\n                    </div>\n\n                    <div className=\"col-4\">\n                        <div className=\"form-group\">\n                            <label>\n                                <Trans>install_settings_port</Trans>\n                            </label>\n                            <Controller\n                                name=\"dns.port\"\n                                control={control}\n                                rules={{\n                                    required: t('form_error_required'),\n                                    validate: {\n                                        required: validateRequiredValue,\n                                        installPort: validateInstallPort,\n                                    },\n                                }}\n                                render={({ field, fieldState }) => (\n                                    <Input\n                                        {...field}\n                                        type=\"number\"\n                                        data-testid=\"install_dns_port\"\n                                        error={fieldState.error?.message}\n                                        placeholder={STANDARD_DNS_PORT.toString()}\n                                        onChange={(e) => {\n                                            const { value } = e.target;\n                                            field.onChange(toNumber(value));\n                                        }}\n                                    />\n                                )}\n                            />\n                        </div>\n                    </div>\n\n                    <div className=\"col-12\">\n                        {dnsStatus && (\n                            <>\n                                <div className=\"setup__error text-danger\">\n                                    {dnsStatus}\n                                    {isDnsFixAvailable && (\n                                        <button\n                                            type=\"button\"\n                                            data-testid=\"install_dns_fix\"\n                                            className=\"btn btn-secondary btn-sm ml-2\"\n                                            onClick={() => handleAutofix('dns')}>\n                                            <Trans>fix</Trans>\n                                        </button>\n                                    )}\n                                </div>\n                                {isDnsFixAvailable && (\n                                    <div className=\"text-muted mb-2\">\n                                        <p className=\"mb-1\">\n                                            <Trans>autofix_warning_text</Trans>\n                                        </p>\n                                        <Trans components={[<li key=\"0\">text</li>]}>autofix_warning_list</Trans>\n                                        <p className=\"mb-1\">\n                                            <Trans>autofix_warning_result</Trans>\n                                        </p>\n                                    </div>\n                                )}\n                            </>\n                        )}\n                        {watchFields.dns?.port === STANDARD_DNS_PORT &&\n                            !isDnsFixAvailable &&\n                            dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && (\n                                <Trans\n                                    components={[\n                                        <a href={PORT_53_FAQ_LINK} key=\"0\" target=\"_blank\" rel=\"noopener noreferrer\">\n                                            link\n                                        </a>,\n                                    ]}>\n                                    port_53_faq_link\n                                </Trans>\n                            )}\n\n                        <hr className=\"divider--small\" />\n                    </div>\n                </div>\n\n                <div className=\"setup__desc\">\n                    <Trans>install_settings_dns_desc</Trans>\n\n                    <div className=\"mt-1\">\n                        <AddressList\n                            interfaces={interfaces}\n                            address={watchFields.dns?.ip}\n                            port={watchFields.dns?.port}\n                            isDns={true}\n                        />\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"setup__group\">\n                <div className=\"setup__subtitle\">\n                    <Trans>static_ip</Trans>\n                </div>\n\n                <div className=\"mb-2\">\n                    <Trans>static_ip_desc</Trans>\n                </div>\n\n                {getStaticIpMessage(staticIp)}\n            </div>\n\n            <Controls invalid={!isValid} />\n        </form>\n    );\n};\n"
  },
  {
    "path": "client/src/install/Setup/Setup.css",
    "content": "/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */\n@media screen and (max-width: 767px) {\n    input,\n    select,\n    textarea {\n        font-size: 1rem;\n    }\n}\n\n.setup {\n    min-height: calc(100vh - 345px);\n    line-height: 1.48;\n}\n\n@media screen and (min-width: 768px) {\n    .setup {\n        padding: 50px 0;\n        min-height: calc(100vh - 141px);\n    }\n}\n\n.setup__container {\n    max-width: 650px;\n    margin: 0 auto;\n    padding: 30px 20px;\n    line-height: 1.6;\n    background-color: var(--card-bgcolor);\n    box-shadow: 0 1px 4px rgba(74, 74, 74, 0.36);\n    border-radius: 3px;\n}\n\n[data-theme='dark'] .setup__container {\n    box-shadow: none;\n    border: 1px solid var(--card-border-color);\n}\n\n@media screen and (min-width: 768px) {\n    .setup__container {\n        width: 650px;\n        padding: 40px 30px;\n    }\n}\n\n.setup__logo {\n    display: block;\n    margin: 0 auto 40px;\n    max-width: 140px;\n}\n\n[data-theme='dark'] .setup__logo {\n    filter: invert(1);\n}\n\n.setup__nav {\n    text-align: center;\n}\n\n.setup__step {\n    margin-bottom: 25px;\n}\n\n.setup__title {\n    margin-bottom: 30px;\n    font-size: 28px;\n    text-align: center;\n    font-weight: 700;\n}\n\n.setup__subtitle {\n    margin-bottom: 10px;\n    font-size: 17px;\n    font-weight: 700;\n}\n\n.setup__desc {\n    margin-bottom: 20px;\n    font-size: 15px;\n}\n\n.setup__group {\n    margin-bottom: 35px;\n}\n\n.setup__group:last-child {\n    margin-bottom: 0;\n}\n\n.setup__progress {\n    font-size: 13px;\n    text-align: center;\n}\n\n.setup__progress-wrap {\n    height: 4px;\n    margin: 20px -20px -30px -20px;\n    overflow: hidden;\n    background-color: #eaeaea;\n    border-radius: 0 0 3px 3px;\n}\n\n@media screen and (min-width: 768px) {\n    .setup__progress-wrap {\n        margin: 20px -30px -40px -30px;\n    }\n}\n\n.setup__progress-inner {\n    width: 0;\n    height: 100%;\n    font-size: 1.2rem;\n    line-height: 20px;\n    color: #fff;\n    text-align: center;\n    box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n    transition: width 0.6s ease;\n    background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);\n}\n\n.btn-standard {\n    padding-left: 20px;\n    padding-right: 20px;\n}\n\n.form__message {\n    font-size: 11px;\n}\n\n.form__message--error {\n    color: var(--red);\n}\n\n.setup__button {\n    min-width: 120px;\n    padding-left: 30px;\n    padding-right: 30px;\n}\n\n.setup__error {\n    margin: -5px 0 5px;\n}\n\n.divider--small {\n    margin-top: 1rem;\n    margin-bottom: 1rem;\n}\n"
  },
  {
    "path": "client/src/install/Setup/Submit.tsx",
    "content": "import React from 'react';\n\nimport { Trans } from 'react-i18next';\n\nimport Controls from './Controls';\nimport { WebConfig } from './Settings';\n\ntype Props = {\n    webConfig: WebConfig;\n    openDashboard: (ip: string, port: number) => void;\n};\n\nexport const Submit = ({ openDashboard, webConfig }: Props) => (\n    <div className=\"setup__step\">\n        <div className=\"setup__group\">\n            <h1 className=\"setup__title\">\n                <Trans>install_submit_title</Trans>\n            </h1>\n\n            <p className=\"setup__desc\">\n                <Trans>install_submit_desc</Trans>\n            </p>\n        </div>\n\n        <Controls openDashboard={openDashboard} ip={webConfig.ip} port={webConfig.port} />\n    </div>\n);\n"
  },
  {
    "path": "client/src/install/Setup/index.tsx",
    "content": "import React, { useEffect, Fragment } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport debounce from 'lodash/debounce';\n\nimport * as actionCreators from '../../actions/install';\n\nimport { getWebAddress } from '../../helpers/helpers';\nimport { INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, DEBOUNCE_TIMEOUT } from '../../helpers/constants';\n\nimport Loading from '../../components/ui/Loading';\nimport Greeting from './Greeting';\nimport { ConfigType, DnsConfig, Settings, WebConfig } from './Settings';\nimport { Devices } from './Devices';\nimport { Submit } from './Submit';\nimport { Progress } from './Progress';\nimport { Auth } from './Auth';\nimport Toasts from '../../components/Toasts';\nimport Footer from '../../components/ui/Footer';\nimport Icons from '../../components/ui/Icons';\nimport { Logo } from '../../components/ui/svg/logo';\n\nimport './Setup.css';\nimport '../../components/ui/Tabler.css';\nimport { InstallInterface, InstallState } from '../../initialState';\n\nexport const Setup = () => {\n    const dispatch = useDispatch();\n\n    const install = useSelector((state: InstallState) => state.install);\n    const { processingDefault, step, web, dns, staticIp, interfaces } = install;\n\n    useEffect(() => {\n        dispatch(actionCreators.getDefaultAddresses());\n    }, []);\n\n    const handleFormSubmit = (values: any) => {\n        const config = { ...values };\n        delete config.staticIp;\n\n        if (web.port && dns.port) {\n            dispatch(\n                actionCreators.setAllSettings({\n                    web,\n                    dns,\n                    ...config,\n                }),\n            );\n        }\n    };\n\n    const checkConfig = debounce((values) => {\n        const { web, dns } = values;\n\n        if (values && web.port && dns.port) {\n            dispatch(actionCreators.checkConfig({ web, dns, set_static_ip: false }));\n        }\n    }, DEBOUNCE_TIMEOUT);\n\n    const handleFix = (web: WebConfig, dns: DnsConfig, set_static_ip: boolean) => {\n        dispatch(actionCreators.checkConfig({ web, dns, set_static_ip }));\n    };\n\n    const openDashboard = (ip: string, port: number) => {\n        let address = getWebAddress(ip, port);\n        if (ip === ALL_INTERFACES_IP) {\n            address = getWebAddress(window.location.hostname, port);\n        }\n        window.location.replace(address);\n    };\n\n    const handleNextStep = () => {\n        if (step < INSTALL_TOTAL_STEPS) {\n            dispatch(actionCreators.nextStep());\n        }\n    };\n\n    const renderPage = (step: number, config: ConfigType, interfaces: InstallInterface[]) => {\n        switch (step) {\n            case 1:\n                return <Greeting />;\n            case 2:\n                return (\n                    <Settings\n                        config={config}\n                        initialValues={config}\n                        interfaces={interfaces}\n                        handleSubmit={handleNextStep}\n                        validateForm={checkConfig}\n                        handleFix={handleFix}\n                    />\n                );\n            case 3:\n                return <Auth onAuthSubmit={handleFormSubmit} />;\n            case 4:\n                return <Devices interfaces={interfaces} dnsConfig={dns} />;\n            case 5:\n                return <Submit openDashboard={openDashboard} webConfig={web} />;\n            default:\n                return false;\n        }\n    };\n\n    if (processingDefault) {\n        return <Loading />;\n    }\n\n    return (\n        <>\n            <div className=\"setup\">\n                <div className=\"setup__container\">\n                    <Logo className=\"setup__logo\" />\n                    {renderPage(step, { web, dns, staticIp }, interfaces)}\n                    <Progress step={step} />\n                </div>\n            </div>\n\n            <Footer />\n\n            <Toasts />\n\n            <Icons />\n        </>\n    );\n};\n"
  },
  {
    "path": "client/src/install/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\n\nimport '../components/App/index.css';\nimport '../components/ui/ReactTable.css';\nimport configureStore from '../configureStore';\nimport reducers from '../reducers/install';\nimport '../i18n';\n\nimport { Setup } from './Setup';\nimport { InstallState } from '../initialState';\n\nconst store = configureStore<InstallState>(reducers, {});\n\nReactDOM.render(\n    <Provider store={store}>\n        <Setup />\n    </Provider>,\n    document.getElementById('root'),\n);\n"
  },
  {
    "path": "client/src/login/Login/Form.tsx",
    "content": "import React from 'react';\nimport { Controller, useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { Input } from '../../components/ui/Controls/Input';\nimport { validateRequiredValue } from '../../helpers/validators';\n\nexport type LoginFormValues = {\n    username: string;\n    password: string;\n};\n\ntype LoginFormProps = {\n    onSubmit: (data: LoginFormValues) => void;\n    processing: boolean;\n};\n\nconst Form = ({ onSubmit, processing }: LoginFormProps) => {\n    const { t } = useTranslation();\n    const {\n        handleSubmit,\n        control,\n        formState: { isValid },\n    } = useForm<LoginFormValues>({\n        mode: 'onChange',\n        defaultValues: {\n            username: '',\n            password: '',\n        },\n    });\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)} className=\"card\">\n            <div className=\"card-body p-6\">\n                <div className=\"form__group form__group--settings\">\n                    <Controller\n                        name=\"username\"\n                        control={control}\n                        rules={{ validate: validateRequiredValue }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                data-testid=\"username\"\n                                type=\"text\"\n                                label={t('username_label')}\n                                placeholder={t('username_placeholder')}\n                                error={fieldState.error?.message}\n                                autoComplete=\"username\"\n                                autoCapitalize=\"none\"\n                                disabled={processing}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"form__group form__group--settings\">\n                    <Controller\n                        name=\"password\"\n                        control={control}\n                        rules={{ validate: validateRequiredValue }}\n                        render={({ field, fieldState }) => (\n                            <Input\n                                {...field}\n                                data-testid=\"password\"\n                                type=\"password\"\n                                label={t('password_label')}\n                                placeholder={t('password_placeholder')}\n                                error={fieldState.error?.message}\n                                autoComplete=\"current-password\"\n                                disabled={processing}\n                            />\n                        )}\n                    />\n                </div>\n\n                <div className=\"form-footer\">\n                    <button\n                        data-testid=\"sign_in\"\n                        type=\"submit\"\n                        className=\"btn btn-success btn-block\"\n                        disabled={processing || !isValid}>\n                        {t('sign_in')}\n                    </button>\n                </div>\n            </div>\n        </form>\n    );\n};\n\nexport default Form;\n"
  },
  {
    "path": "client/src/login/Login/Login.css",
    "content": "/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */\n@media screen and (max-width: 767px) {\n    input,\n    select,\n    textarea {\n        font-size: 1rem;\n    }\n}\n\n.login {\n    display: flex;\n    flex-direction: column;\n    justify-content: space-between;\n    align-items: stretch;\n    min-height: 100vh;\n}\n\n[data-theme='dark'] .login__logo {\n    filter: invert(1);\n}\n\n.login__form {\n    margin: auto;\n    padding: 40px 15px 100px;\n    width: 100%;\n    max-width: 24rem;\n}\n\n.login__info {\n    position: relative;\n    text-align: center;\n}\n\n.login__message,\n.login__link {\n    font-size: 14px;\n    font-weight: 400;\n    letter-spacing: 0;\n}\n\n@media screen and (min-width: 992px) {\n    .login__message {\n        position: absolute;\n        top: 40px;\n        padding: 0 15px;\n    }\n}\n\n.form__group {\n    position: relative;\n    margin-bottom: 15px;\n}\n\n.form__message {\n    font-size: 11px;\n}\n\n.form__message--error {\n    color: var(--red);\n}\n"
  },
  {
    "path": "client/src/login/Login/index.tsx",
    "content": "import React, { useState } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { Trans } from 'react-i18next';\n\nimport * as actionCreators from '../../actions/login';\n\nimport { Logo } from '../../components/ui/svg/logo';\nimport Toasts from '../../components/Toasts';\nimport Footer from '../../components/ui/Footer';\nimport Icons from '../../components/ui/Icons';\nimport Form, { LoginFormValues } from './Form';\n\nimport './Login.css';\nimport '../../components/ui/Tabler.css';\nimport { LoginState } from '../../initialState';\n\nexport const Login = () => {\n    const dispatch = useDispatch();\n    const { processingLogin } = useSelector((state: LoginState) => state.login);\n    const [isForgotPasswordVisible, setIsForgotPasswordVisible] = useState(false);\n\n    const handleSubmit = ({ username: name, password }: LoginFormValues) => {\n        dispatch(actionCreators.processLogin({ name, password }));\n    };\n\n    const toggleText = () => {\n        setIsForgotPasswordVisible((prev) => !prev);\n    };\n\n    return (\n        <div className=\"login\">\n            <div className=\"login__form\">\n                <div className=\"text-center mb-6\">\n                    <Logo className=\"h-6 login__logo\" />\n                </div>\n\n                <Form onSubmit={handleSubmit} processing={processingLogin} />\n\n                <div className=\"login__info\">\n                    <button type=\"button\" className=\"btn btn-link login__link\" onClick={toggleText}>\n                        <Trans>forgot_password</Trans>\n                    </button>\n\n                    {isForgotPasswordVisible && (\n                        <div className=\"login__message\">\n                            <Trans\n                                components={[\n                                    <a\n                                        href=\"https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#password-reset\"\n                                        key=\"0\"\n                                        target=\"_blank\"\n                                        rel=\"noopener noreferrer\">\n                                        link\n                                    </a>,\n                                ]}>\n                                forgot_password_desc\n                            </Trans>\n                        </div>\n                    )}\n                </div>\n            </div>\n\n            <Footer />\n            <Toasts />\n            <Icons />\n        </div>\n    );\n};\n"
  },
  {
    "path": "client/src/login/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\n\nimport '../components/App/index.css';\nimport '../components/ui/ReactTable.css';\nimport configureStore from '../configureStore';\nimport reducers from '../reducers/login';\nimport '../i18n';\n\nimport { Login } from './Login';\nimport { LoginState } from '../initialState';\n\nconst store = configureStore<LoginState>(reducers, {});\n\nReactDOM.render(\n    <Provider store={store}>\n        <Login />\n    </Provider>,\n    document.getElementById('root'),\n);\n"
  },
  {
    "path": "client/src/reducers/access.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/access';\n\nconst access = handleActions(\n    {\n        [actions.getAccessListRequest.toString()]: (state: any) => ({\n            ...state,\n            processing: true,\n        }),\n        [actions.getAccessListFailure.toString()]: (state: any) => ({\n            ...state,\n            processing: false,\n        }),\n        [actions.getAccessListSuccess.toString()]: (state: any, { payload }: any) => {\n            const { allowed_clients, disallowed_clients, blocked_hosts } = payload;\n            const newState = {\n                ...state,\n                allowed_clients: allowed_clients?.join('\\n') || '',\n                disallowed_clients: disallowed_clients?.join('\\n') || '',\n                blocked_hosts: blocked_hosts?.join('\\n') || '',\n                processing: false,\n            };\n            return newState;\n        },\n\n        [actions.setAccessListRequest.toString()]: (state: any) => ({\n            ...state,\n            processingSet: true,\n        }),\n        [actions.setAccessListFailure.toString()]: (state: any) => ({\n            ...state,\n            processingSet: false,\n        }),\n        [actions.setAccessListSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingSet: false,\n        }),\n\n        [actions.toggleClientBlockRequest.toString()]: (state: any) => ({\n            ...state,\n            processingSet: true,\n        }),\n        [actions.toggleClientBlockFailure.toString()]: (state: any) => ({\n            ...state,\n            processingSet: false,\n        }),\n        [actions.toggleClientBlockSuccess.toString()]: (state: any, { payload }: any) => {\n            const { allowed_clients, disallowed_clients, blocked_hosts } = payload;\n            const newState = {\n                ...state,\n                allowed_clients: allowed_clients?.join('\\n') || '',\n                disallowed_clients: disallowed_clients?.join('\\n') || '',\n                blocked_hosts: blocked_hosts?.join('\\n') || '',\n                processingSet: false,\n            };\n            return newState;\n        },\n    },\n    {\n        processing: true,\n        processingSet: false,\n        allowed_clients: '',\n        disallowed_clients: '',\n        blocked_hosts: '',\n    },\n);\n\nexport default access;\n"
  },
  {
    "path": "client/src/reducers/clients.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/clients';\n\nconst clients = handleActions(\n    {\n        [actions.addClientRequest.toString()]: (state: any) => ({\n            ...state,\n            processingAdding: true,\n        }),\n        [actions.addClientFailure.toString()]: (state: any) => ({\n            ...state,\n            processingAdding: false,\n        }),\n        [actions.addClientSuccess.toString()]: (state: any) => {\n            const newState = {\n                ...state,\n                processingAdding: false,\n            };\n            return newState;\n        },\n\n        [actions.deleteClientRequest.toString()]: (state: any) => ({\n            ...state,\n            processingDeleting: true,\n        }),\n        [actions.deleteClientFailure.toString()]: (state: any) => ({\n            ...state,\n            processingDeleting: false,\n        }),\n        [actions.deleteClientSuccess.toString()]: (state: any) => {\n            const newState = {\n                ...state,\n                processingDeleting: false,\n            };\n            return newState;\n        },\n\n        [actions.updateClientRequest.toString()]: (state: any) => ({\n            ...state,\n            processingUpdating: true,\n        }),\n        [actions.updateClientFailure.toString()]: (state: any) => ({\n            ...state,\n            processingUpdating: false,\n        }),\n        [actions.updateClientSuccess.toString()]: (state: any) => {\n            const newState = {\n                ...state,\n                processingUpdating: false,\n            };\n            return newState;\n        },\n\n        [actions.toggleClientModal.toString()]: (state: any, { payload }: any) => {\n            if (payload) {\n                const newState = {\n                    ...state,\n                    modalType: payload.type || '',\n                    modalClientName: payload.name || '',\n                    isModalOpen: !state.isModalOpen,\n                };\n                return newState;\n            }\n\n            const newState = {\n                ...state,\n                isModalOpen: !state.isModalOpen,\n            };\n            return newState;\n        },\n    },\n    {\n        processing: true,\n        processingAdding: false,\n        processingDeleting: false,\n        processingUpdating: false,\n        isModalOpen: false,\n        modalClientName: '',\n        modalType: '',\n    },\n);\n\nexport default clients;\n"
  },
  {
    "path": "client/src/reducers/dashboard.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions';\nimport { areEqualVersions } from '../helpers/version';\nimport { STANDARD_DNS_PORT, STANDARD_WEB_PORT } from '../helpers/constants';\n\nconst dashboard = handleActions(\n    {\n        [actions.setDnsRunningStatus.toString()]: (state, { payload }: any) => ({\n            ...state,\n            isCoreRunning: payload,\n        }),\n        [actions.dnsStatusRequest.toString()]: (state: any) => ({\n            ...state,\n            processing: true,\n        }),\n        [actions.dnsStatusFailure.toString()]: (state: any) => ({\n            ...state,\n            processing: false,\n        }),\n        [actions.dnsStatusSuccess.toString()]: (state: any, { payload }: any) => {\n            const {\n                version,\n                start_time: dnsStartTime,\n                dns_port: dnsPort,\n                dns_addresses: dnsAddresses,\n                protection_enabled: protectionEnabled,\n                protection_disabled_duration: protectionDisabledDuration,\n                http_port: httpPort,\n                language,\n            } = payload;\n            const newState = {\n                ...state,\n                isCoreRunning: true,\n                processing: false,\n                dnsVersion: version,\n                dnsStartTime,\n                dnsPort,\n                dnsAddresses,\n                protectionEnabled,\n                protectionDisabledDuration,\n                language,\n                httpPort,\n            };\n\n            return newState;\n        },\n        [actions.timerStatusSuccess.toString()]: (state: any, { payload }: any) => {\n            const { protection_enabled: protectionEnabled, protection_disabled_duration: protectionDisabledDuration } =\n                payload;\n            const newState = {\n                ...state,\n                protectionEnabled,\n                protectionDisabledDuration,\n            };\n\n            return newState;\n        },\n\n        [actions.getVersionRequest.toString()]: (state: any) => ({\n            ...state,\n            processingVersion: true,\n        }),\n        [actions.getVersionFailure.toString()]: (state: any) => ({\n            ...state,\n            processingVersion: false,\n        }),\n        [actions.getVersionSuccess.toString()]: (state: any, { payload }: any) => {\n            const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion;\n\n            if (!payload.disabled && !areEqualVersions(currentVersion, payload.new_version)) {\n                const {\n                    announcement_url: announcementUrl,\n                    new_version: newVersion,\n                    can_autoupdate: canAutoUpdate,\n                } = payload;\n\n                const newState = {\n                    ...state,\n                    announcementUrl,\n                    newVersion,\n                    canAutoUpdate,\n                    isUpdateAvailable: true,\n                    processingVersion: false,\n                    checkUpdateFlag: !payload.disabled,\n                };\n                return newState;\n            }\n\n            return {\n                ...state,\n                processingVersion: false,\n                checkUpdateFlag: !payload.disabled,\n            };\n        },\n\n        [actions.getUpdateRequest.toString()]: (state: any) => ({\n            ...state,\n            processingUpdate: true,\n        }),\n        [actions.getUpdateFailure.toString()]: (state: any) => ({\n            ...state,\n            processingUpdate: false,\n        }),\n        [actions.getUpdateSuccess.toString()]: (state: any) => {\n            const newState = {\n                ...state,\n                processingUpdate: false,\n            };\n            return newState;\n        },\n\n        [actions.toggleProtectionRequest.toString()]: (state: any) => ({\n            ...state,\n            processingProtection: true,\n        }),\n        [actions.toggleProtectionFailure.toString()]: (state: any) => ({\n            ...state,\n            processingProtection: false,\n        }),\n        [actions.toggleProtectionSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                protectionEnabled: !state.protectionEnabled,\n                processingProtection: false,\n                protectionDisabledDuration: payload.disabledDuration,\n            };\n\n            return newState;\n        },\n\n        [actions.setDisableDurationTime.toString()]: (state, { payload }: any) => ({\n            ...state,\n            protectionDisabledDuration: payload.timeToEnableProtection,\n        }),\n\n        [actions.getClientsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingClients: true,\n        }),\n        [actions.getClientsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingClients: false,\n        }),\n        [actions.getClientsSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                ...payload,\n                processingClients: false,\n            };\n            return newState;\n        },\n\n        [actions.getProfileRequest.toString()]: (state: any) => ({\n            ...state,\n            processingProfile: true,\n        }),\n        [actions.getProfileFailure.toString()]: (state: any) => ({\n            ...state,\n            processingProfile: false,\n        }),\n        [actions.getProfileSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            name: payload.name,\n            theme: payload.theme,\n            processingProfile: false,\n        }),\n        [actions.changeThemeSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            theme: payload.theme,\n        }),\n    },\n    {\n        processing: true,\n        isCoreRunning: true,\n        processingVersion: true,\n        processingClients: true,\n        processingUpdate: false,\n        processingProfile: true,\n        protectionEnabled: false,\n        protectionDisabledDuration: null,\n        protectionCountdownActive: false,\n        processingProtection: false,\n        httpPort: STANDARD_WEB_PORT,\n        dnsPort: STANDARD_DNS_PORT,\n        dnsAddresses: [],\n        dnsVersion: '',\n        dnsStartTime: null,\n        clients: [],\n        autoClients: [],\n        supportedTags: [],\n        name: '',\n        theme: undefined,\n        checkUpdateFlag: false,\n    },\n);\n\nexport default dashboard;\n"
  },
  {
    "path": "client/src/reducers/dhcp.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions';\n\nimport { enrichWithConcatenatedIpAddresses } from '../helpers/helpers';\n\nconst dhcp = handleActions(\n    {\n        [actions.getDhcpStatusRequest.toString()]: (state: any) => ({\n            ...state,\n            processing: true,\n        }),\n        [actions.getDhcpStatusFailure.toString()]: (state: any) => ({\n            ...state,\n            processing: false,\n        }),\n        [actions.getDhcpStatusSuccess.toString()]: (state: any, { payload }: any) => {\n            const { static_leases: staticLeases, ...values } = payload;\n\n            const newState = {\n                ...state,\n                staticLeases,\n                processing: false,\n                ...values,\n            };\n\n            return newState;\n        },\n\n        [actions.getDhcpInterfacesRequest.toString()]: (state: any) => ({\n            ...state,\n            processingInterfaces: true,\n        }),\n        [actions.getDhcpInterfacesFailure.toString()]: (state: any) => ({\n            ...state,\n            processingInterfaces: false,\n        }),\n        [actions.getDhcpInterfacesSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                interfaces: enrichWithConcatenatedIpAddresses(payload),\n                processingInterfaces: false,\n            };\n            return newState;\n        },\n\n        [actions.findActiveDhcpRequest.toString()]: (state: any) => ({\n            ...state,\n            processingStatus: true,\n        }),\n        [actions.findActiveDhcpFailure.toString()]: (state: any) => ({\n            ...state,\n            processingStatus: false,\n        }),\n        [actions.findActiveDhcpSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                check: payload,\n                processingStatus: false,\n            };\n            return newState;\n        },\n\n        [actions.toggleDhcpRequest.toString()]: (state: any) => ({\n            ...state,\n            processingDhcp: true,\n        }),\n        [actions.toggleDhcpFailure.toString()]: (state: any) => ({\n            ...state,\n            processingDhcp: false,\n        }),\n        [actions.toggleDhcpSuccess.toString()]: (state: any) => {\n            const { enabled } = state;\n            const newState = {\n                ...state,\n                enabled: !enabled,\n                check: null,\n                processingDhcp: false,\n            };\n            return newState;\n        },\n\n        [actions.setDhcpConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingConfig: true,\n        }),\n        [actions.setDhcpConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingConfig: false,\n        }),\n        [actions.setDhcpConfigSuccess.toString()]: (state: any, { payload }: any) => {\n            const { v4, v6 } = state;\n            const newConfigV4 = { ...v4, ...payload.v4 };\n            const newConfigV6 = { ...v6, ...payload.v6 };\n\n            const newState = {\n                ...state,\n                v4: newConfigV4,\n                v6: newConfigV6,\n                interface_name: payload.interface_name,\n                processingConfig: false,\n            };\n\n            return newState;\n        },\n\n        [actions.resetDhcpRequest.toString()]: (state: any) => ({\n            ...state,\n            processingReset: true,\n        }),\n        [actions.resetDhcpFailure.toString()]: (state: any) => ({\n            ...state,\n            processingReset: false,\n        }),\n        [actions.resetDhcpSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingReset: false,\n            enabled: false,\n            v4: {},\n            v6: {},\n            interface_name: '',\n        }),\n        [actions.resetDhcpLeasesSuccess.toString()]: (state: any) => ({\n            ...state,\n            leases: [],\n            staticLeases: [],\n        }),\n\n        [actions.toggleLeaseModal.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                isModalOpen: !state.isModalOpen,\n                modalType: payload?.type || '',\n                leaseModalConfig: payload?.config,\n            };\n            return newState;\n        },\n\n        [actions.addStaticLeaseRequest.toString()]: (state: any) => ({\n            ...state,\n            processingAdding: true,\n        }),\n        [actions.addStaticLeaseFailure.toString()]: (state: any) => ({\n            ...state,\n            processingAdding: false,\n        }),\n        [actions.addStaticLeaseSuccess.toString()]: (state: any, { payload }: any) => {\n            const { ip, mac, hostname } = payload;\n            const newLease = {\n                ip,\n                mac,\n                hostname: hostname || '',\n            };\n            const leases = [...state.staticLeases, newLease];\n            const newState = {\n                ...state,\n                staticLeases: leases,\n                processingAdding: false,\n            };\n            return newState;\n        },\n\n        [actions.removeStaticLeaseRequest.toString()]: (state: any) => ({\n            ...state,\n            processingDeleting: true,\n        }),\n        [actions.removeStaticLeaseFailure.toString()]: (state: any) => ({\n            ...state,\n            processingDeleting: false,\n        }),\n        [actions.removeStaticLeaseSuccess.toString()]: (state: any, { payload }: any) => {\n            const leaseToRemove = payload.ip;\n            const leases = state.staticLeases.filter((item: any) => item.ip !== leaseToRemove);\n            const newState = {\n                ...state,\n                staticLeases: leases,\n                processingDeleting: false,\n            };\n            return newState;\n        },\n\n        [actions.updateStaticLeaseRequest.toString()]: (state: any) => ({\n            ...state,\n            processingUpdating: true,\n        }),\n        [actions.updateStaticLeaseFailure.toString()]: (state: any) => ({\n            ...state,\n            processingUpdating: false,\n        }),\n        [actions.updateStaticLeaseSuccess.toString()]: (state: any) => {\n            const newState = {\n                ...state,\n                processingUpdating: false,\n            };\n            return newState;\n        },\n    },\n    {\n        processing: true,\n        processingStatus: false,\n        processingInterfaces: false,\n        processingDhcp: false,\n        processingConfig: false,\n        processingAdding: false,\n        processingDeleting: false,\n        processingUpdating: false,\n        enabled: false,\n        interface_name: '',\n        check: null,\n        v4: {\n            gateway_ip: '',\n            subnet_mask: '',\n            range_start: '',\n            range_end: '',\n            lease_duration: 0,\n        },\n        v6: {\n            range_start: '',\n            lease_duration: 0,\n        },\n        leases: [],\n        staticLeases: [],\n        isModalOpen: false,\n        leaseModalConfig: undefined,\n        modalType: '',\n        dhcp_available: false,\n    },\n);\n\nexport default dhcp;\n"
  },
  {
    "path": "client/src/reducers/dnsConfig.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/dnsConfig';\nimport { ALL_INTERFACES_IP, BLOCKING_MODES, DNS_REQUEST_OPTIONS } from '../helpers/constants';\n\nexport const DEFAULT_BLOCKING_IPV4 = ALL_INTERFACES_IP;\nexport const DEFAULT_BLOCKING_IPV6 = '::';\n\nconst dnsConfig = handleActions(\n    {\n        [actions.getDnsConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingGetConfig: true,\n        }),\n        [actions.getDnsConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingGetConfig: false,\n        }),\n        [actions.getDnsConfigSuccess.toString()]: (state: any, { payload }: any) => {\n            const {\n                blocking_ipv4,\n                blocking_ipv6,\n                upstream_dns,\n                upstream_mode,\n                fallback_dns,\n                bootstrap_dns,\n                local_ptr_upstreams,\n                ratelimit_whitelist,\n                ...values\n            } = payload;\n\n            return {\n                ...state,\n                ...values,\n                blocking_ipv4: blocking_ipv4 || DEFAULT_BLOCKING_IPV4,\n                blocking_ipv6: blocking_ipv6 || DEFAULT_BLOCKING_IPV6,\n                upstream_dns: (upstream_dns && upstream_dns.join('\\n')) || '',\n                fallback_dns: (fallback_dns && fallback_dns.join('\\n')) || '',\n                bootstrap_dns: (bootstrap_dns && bootstrap_dns.join('\\n')) || '',\n                local_ptr_upstreams: (local_ptr_upstreams && local_ptr_upstreams.join('\\n')) || '',\n                ratelimit_whitelist: (ratelimit_whitelist && ratelimit_whitelist.join('\\n')) || '',\n                processingGetConfig: false,\n                upstream_mode: upstream_mode === '' ? DNS_REQUEST_OPTIONS.LOAD_BALANCING : upstream_mode,\n            };\n        },\n\n        [actions.setDnsConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingSetConfig: true,\n        }),\n        [actions.setDnsConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingSetConfig: false,\n        }),\n        [actions.setDnsConfigSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            ...payload,\n            processingSetConfig: false,\n        }),\n    },\n    {\n        processingGetConfig: false,\n        processingSetConfig: false,\n        blocking_mode: BLOCKING_MODES.default,\n        ratelimit: 20,\n        blocking_ipv4: DEFAULT_BLOCKING_IPV4,\n        blocking_ipv6: DEFAULT_BLOCKING_IPV6,\n        blocked_response_ttl: 10,\n        upstream_timeout: 10,\n        edns_cs_enabled: false,\n        disable_ipv6: false,\n        dnssec_enabled: false,\n        upstream_dns_file: '',\n    },\n);\n\nexport default dnsConfig;\n"
  },
  {
    "path": "client/src/reducers/encryption.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/encryption';\n\nconst encryption = handleActions(\n    {\n        [actions.getTlsStatusRequest.toString()]: (state: any) => ({\n            ...state,\n            processing: true,\n        }),\n        [actions.getTlsStatusFailure.toString()]: (state: any) => ({\n            ...state,\n            processing: false,\n        }),\n        [actions.getTlsStatusSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                ...payload,\n                /* TODO: handle property delete on api refactor */\n                server_name: payload.server_name || '',\n                processing: false,\n            };\n            return newState;\n        },\n\n        [actions.setTlsConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingConfig: true,\n        }),\n        [actions.setTlsConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingConfig: false,\n        }),\n        [actions.setTlsConfigSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                ...payload,\n                server_name: payload.server_name || '',\n                processingConfig: false,\n            };\n            return newState;\n        },\n\n        [actions.validateTlsConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingValidate: true,\n        }),\n        [actions.validateTlsConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingValidate: false,\n        }),\n        [actions.validateTlsConfigSuccess.toString()]: (state: any, { payload }: any) => {\n            const {\n                issuer = '',\n                key_type = '',\n                not_after = '',\n                not_before = '',\n                subject = '',\n                warning_validation = '',\n                dns_names = '',\n                ...values\n            } = payload;\n\n            const newState = {\n                ...state,\n                ...values,\n                issuer,\n                key_type,\n                not_after,\n                not_before,\n                subject,\n                warning_validation,\n                dns_names,\n                server_name: payload.server_name || '',\n                processingValidate: false,\n            };\n            return newState;\n        },\n    },\n    {\n        processing: true,\n        processingConfig: false,\n        processingValidate: false,\n        enabled: false,\n        serve_plain_dns: false,\n        dns_names: null,\n        force_https: false,\n        issuer: '',\n        key_type: '',\n        not_after: '',\n        not_before: '',\n        port_dns_over_tls: '',\n        port_https: '',\n        subject: '',\n        valid_chain: false,\n        valid_key: false,\n        valid_cert: false,\n        valid_pair: false,\n        status_cert: '',\n        status_key: '',\n        certificate_chain: '',\n        private_key: '',\n        server_name: '',\n        warning_validation: '',\n        certificate_path: '',\n        private_key_path: '',\n    },\n);\n\nexport default encryption;\n"
  },
  {
    "path": "client/src/reducers/filtering.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/filtering';\n\nconst filtering = handleActions(\n    {\n        [actions.setRulesRequest.toString()]: (state: any) => ({\n            ...state,\n            processingRules: true,\n        }),\n        [actions.setRulesFailure.toString()]: (state: any) => ({\n            ...state,\n            processingRules: false,\n        }),\n        [actions.setRulesSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingRules: false,\n        }),\n\n        [actions.handleRulesChange.toString()]: (state: any, { payload }: any) => {\n            const { userRules } = payload;\n            return { ...state, userRules };\n        },\n\n        [actions.getFilteringStatusRequest.toString()]: (state: any) => ({\n            ...state,\n            processingFilters: true,\n            check: {},\n        }),\n        [actions.getFilteringStatusFailure.toString()]: (state: any) => ({\n            ...state,\n            processingFilters: false,\n        }),\n        [actions.getFilteringStatusSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            ...payload,\n            processingFilters: false,\n        }),\n\n        [actions.addFilterRequest.toString()]: (state: any) => ({\n            ...state,\n            processingAddFilter: true,\n            isFilterAdded: false,\n        }),\n        [actions.addFilterFailure.toString()]: (state: any) => ({\n            ...state,\n            processingAddFilter: false,\n            isFilterAdded: false,\n        }),\n        [actions.addFilterSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingAddFilter: false,\n            isFilterAdded: true,\n        }),\n\n        [actions.toggleFilteringModal.toString()]: (state: any, { payload }: any) => {\n            if (payload) {\n                const newState = {\n                    ...state,\n                    isModalOpen: !state.isModalOpen,\n                    isFilterAdded: false,\n                    modalType: payload.type || '',\n                    modalFilterUrl: payload.url || '',\n                };\n                return newState;\n            }\n            const newState = {\n                ...state,\n                isModalOpen: !state.isModalOpen,\n                isFilterAdded: false,\n                modalType: '',\n            };\n            return newState;\n        },\n\n        [actions.toggleFilterRequest.toString()]: (state: any) => ({\n            ...state,\n            processingConfigFilter: true,\n        }),\n        [actions.toggleFilterFailure.toString()]: (state: any) => ({\n            ...state,\n            processingConfigFilter: false,\n        }),\n        [actions.toggleFilterSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingConfigFilter: false,\n        }),\n\n        [actions.editFilterRequest.toString()]: (state: any) => ({\n            ...state,\n            processingConfigFilter: true,\n        }),\n        [actions.editFilterFailure.toString()]: (state: any) => ({\n            ...state,\n            processingConfigFilter: false,\n        }),\n        [actions.editFilterSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingConfigFilter: false,\n        }),\n\n        [actions.refreshFiltersRequest.toString()]: (state: any) => ({\n            ...state,\n            processingRefreshFilters: true,\n        }),\n        [actions.refreshFiltersFailure.toString()]: (state: any) => ({\n            ...state,\n            processingRefreshFilters: false,\n        }),\n        [actions.refreshFiltersSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingRefreshFilters: false,\n        }),\n\n        [actions.removeFilterRequest.toString()]: (state: any) => ({\n            ...state,\n            processingRemoveFilter: true,\n        }),\n        [actions.removeFilterFailure.toString()]: (state: any) => ({\n            ...state,\n            processingRemoveFilter: false,\n        }),\n        [actions.removeFilterSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingRemoveFilter: false,\n        }),\n\n        [actions.setFiltersConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingSetConfig: true,\n        }),\n        [actions.setFiltersConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingSetConfig: false,\n        }),\n        [actions.setFiltersConfigSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            ...payload,\n            processingSetConfig: false,\n        }),\n\n        [actions.checkHostRequest.toString()]: (state: any) => ({\n            ...state,\n            processingCheck: true,\n        }),\n        [actions.checkHostFailure.toString()]: (state: any) => ({\n            ...state,\n            processingCheck: false,\n        }),\n        [actions.checkHostSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            check: payload,\n            processingCheck: false,\n        }),\n    },\n    {\n        isModalOpen: false,\n        processingFilters: false,\n        processingRules: false,\n        processingAddFilter: false,\n        processingRefreshFilters: false,\n        processingConfigFilter: false,\n        processingRemoveFilter: false,\n        processingSetConfig: false,\n        processingCheck: false,\n        isFilterAdded: false,\n        filters: [],\n        whitelistFilters: [],\n        userRules: '',\n        interval: 24,\n        enabled: true,\n        modalType: '',\n        modalFilterUrl: '',\n        check: {},\n    },\n);\n\nexport default filtering;\n"
  },
  {
    "path": "client/src/reducers/index.ts",
    "content": "import { combineReducers } from 'redux';\nimport { loadingBarReducer } from 'react-redux-loading-bar';\n\nimport toasts from './toasts';\nimport encryption from './encryption';\nimport clients from './clients';\nimport access from './access';\nimport rewrites from './rewrites';\nimport services from './services';\nimport stats from './stats';\nimport queryLogs from './queryLogs';\nimport dnsConfig from './dnsConfig';\nimport filtering from './filtering';\nimport settings from './settings';\nimport dashboard from './dashboard';\nimport dhcp from './dhcp';\n\nexport default combineReducers({\n    settings,\n    dashboard,\n    queryLogs,\n    filtering,\n    toasts,\n    dhcp,\n    encryption,\n    clients,\n    access,\n    rewrites,\n    services,\n    stats,\n    dnsConfig,\n    loadingBar: loadingBarReducer,\n});\n"
  },
  {
    "path": "client/src/reducers/install.ts",
    "content": "import { combineReducers } from 'redux';\n\nimport { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/install';\nimport toasts from './toasts';\nimport { ALL_INTERFACES_IP, INSTALL_FIRST_STEP, STANDARD_DNS_PORT, STANDARD_WEB_PORT } from '../helpers/constants';\n\nconst install = handleActions(\n    {\n        [actions.getDefaultAddressesRequest.toString()]: (state: any) => ({\n            ...state,\n            processingDefault: true,\n        }),\n        [actions.getDefaultAddressesFailure.toString()]: (state: any) => ({\n            ...state,\n            processingDefault: false,\n        }),\n        [actions.getDefaultAddressesSuccess.toString()]: (state: any, { payload }: any) => {\n            const { interfaces, version } = payload;\n            const web = { ...state.web, port: payload.web_port };\n            const dns = { ...state.dns, port: payload.dns_port };\n\n            const newState = {\n                ...state,\n                web,\n                dns,\n                interfaces,\n                processingDefault: false,\n                dnsVersion: version,\n            };\n\n            return newState;\n        },\n\n        [actions.nextStep.toString()]: (state: any) => ({\n            ...state,\n            step: state.step + 1,\n        }),\n        [actions.prevStep.toString()]: (state: any) => ({\n            ...state,\n            step: state.step - 1,\n        }),\n\n        [actions.setAllSettingsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingSubmit: true,\n        }),\n        [actions.setAllSettingsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingSubmit: false,\n        }),\n        [actions.setAllSettingsSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingSubmit: false,\n        }),\n\n        [actions.checkConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingCheck: true,\n        }),\n        [actions.checkConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingCheck: false,\n        }),\n        [actions.checkConfigSuccess.toString()]: (state: any, { payload }: any) => {\n            const web = { ...state.web, ...payload.web };\n            const dns = { ...state.dns, ...payload.dns };\n            const staticIp = { ...state.staticIp, ...payload.static_ip };\n\n            const newState = {\n                ...state,\n                web,\n                dns,\n                staticIp,\n                processingCheck: false,\n            };\n            return newState;\n        },\n    },\n    {\n        step: INSTALL_FIRST_STEP,\n        processingDefault: true,\n        processingSubmit: false,\n        processingCheck: false,\n        web: {\n            ip: ALL_INTERFACES_IP,\n            port: STANDARD_WEB_PORT,\n            status: '',\n            can_autofix: false,\n        },\n        dns: {\n            ip: ALL_INTERFACES_IP,\n            port: STANDARD_DNS_PORT,\n            status: '',\n            can_autofix: false,\n        },\n        staticIp: {\n            static: '',\n            ip: '',\n            error: '',\n        },\n        interfaces: {},\n        dnsVersion: '',\n    },\n);\n\nexport default combineReducers({\n    install,\n    toasts,\n});\n"
  },
  {
    "path": "client/src/reducers/login.ts",
    "content": "import { combineReducers } from 'redux';\n\nimport { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/login';\nimport toasts from './toasts';\n\nconst login = handleActions(\n    {\n        [actions.processLoginRequest.toString()]: (state: any) => ({\n            ...state,\n            processingLogin: true,\n        }),\n        [actions.processLoginFailure.toString()]: (state: any) => ({\n            ...state,\n            processingLogin: false,\n        }),\n        [actions.processLoginSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            ...payload,\n            processingLogin: false,\n        }),\n    },\n    {\n        processingLogin: false,\n        email: '',\n        password: '',\n    },\n);\n\nexport default combineReducers({\n    login,\n    toasts,\n});\n"
  },
  {
    "path": "client/src/reducers/queryLogs.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/queryLogs';\nimport { DEFAULT_LOGS_FILTER, DAY, QUERY_LOG_INTERVALS_DAYS, HOUR } from '../helpers/constants';\n\nconst queryLogs = handleActions(\n    {\n        [actions.setFilteredLogsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingGetLogs: true,\n        }),\n        [actions.setFilteredLogsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingGetLogs: false,\n        }),\n        [actions.toggleDetailedLogs.toString()]: (state, { payload }: any) => ({\n            ...state,\n            isDetailed: payload,\n        }),\n\n        [actions.setFilteredLogsSuccess.toString()]: (state: any, { payload }: any) => {\n            const { logs, oldest, filter } = payload;\n\n            const isFiltered = filter && Object.keys(filter).some((key) => filter[key]);\n\n            return {\n                ...state,\n                oldest,\n                filter,\n                isFiltered,\n                logs,\n                isEntireLog: logs.length < 1,\n                processingGetLogs: false,\n            };\n        },\n\n        [actions.setLogsFilterRequest.toString()]: (state, { payload }: any) => ({\n            ...state,\n            filter: payload,\n        }),\n\n        [actions.getLogsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingGetLogs: true,\n        }),\n        [actions.getLogsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingGetLogs: false,\n        }),\n        [actions.getLogsSuccess.toString()]: (state: any, { payload }: any) => {\n            const { logs, oldest, older_than } = payload;\n\n            return {\n                ...state,\n                oldest,\n                logs: older_than ? [...state.logs, ...logs] : logs,\n                isEntireLog: logs.length < 1,\n                processingGetLogs: false,\n            };\n        },\n\n        [actions.clearLogsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingClear: true,\n        }),\n        [actions.clearLogsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingClear: false,\n        }),\n        [actions.clearLogsSuccess.toString()]: (state: any) => ({\n            ...state,\n            logs: [],\n            processingClear: false,\n        }),\n\n        [actions.getLogsConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingGetConfig: true,\n        }),\n        [actions.getLogsConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingGetConfig: false,\n        }),\n        [actions.getLogsConfigSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            ...payload,\n\n            customInterval: !QUERY_LOG_INTERVALS_DAYS.includes(payload.interval) ? payload.interval / HOUR : null,\n\n            processingGetConfig: false,\n        }),\n\n        [actions.setLogsConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingSetConfig: true,\n        }),\n        [actions.setLogsConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingSetConfig: false,\n        }),\n        [actions.setLogsConfigSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            ...payload,\n            processingSetConfig: false,\n        }),\n\n        [actions.getAdditionalLogsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingAdditionalLogs: true,\n            processingGetLogs: true,\n        }),\n        [actions.getAdditionalLogsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingAdditionalLogs: false,\n            processingGetLogs: false,\n        }),\n        [actions.getAdditionalLogsSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingAdditionalLogs: false,\n            processingGetLogs: false,\n            isEntireLog: true,\n        }),\n    },\n    {\n        processingGetLogs: true,\n        processingClear: false,\n        processingGetConfig: false,\n        processingSetConfig: false,\n        processingAdditionalLogs: false,\n        interval: DAY,\n        logs: [],\n        enabled: true,\n        oldest: '',\n        filter: DEFAULT_LOGS_FILTER,\n        isFiltered: false,\n        anonymize_client_ip: false,\n        isDetailed: true,\n        isEntireLog: false,\n        customInterval: null,\n        ignored_enabled: true,\n    },\n);\n\nexport default queryLogs;\n"
  },
  {
    "path": "client/src/reducers/rewrites.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/rewrites';\n\nconst rewrites = handleActions(\n    {\n        [actions.getRewritesListRequest.toString()]: (state: any) => ({\n            ...state,\n            processing: true,\n        }),\n        [actions.getRewritesListFailure.toString()]: (state: any) => ({\n            ...state,\n            processing: false,\n        }),\n        [actions.getRewritesListSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                list: payload,\n                processing: false,\n            };\n            return newState;\n        },\n\n        [actions.addRewriteRequest.toString()]: (state: any) => ({\n            ...state,\n            processingAdd: true,\n        }),\n        [actions.addRewriteFailure.toString()]: (state: any) => ({\n            ...state,\n            processingAdd: false,\n        }),\n        [actions.addRewriteSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                list: [...state.list, payload],\n                processingAdd: false,\n            };\n            return newState;\n        },\n\n        [actions.deleteRewriteRequest.toString()]: (state: any) => ({\n            ...state,\n            processingDelete: true,\n        }),\n        [actions.deleteRewriteFailure.toString()]: (state: any) => ({\n            ...state,\n            processingDelete: false,\n        }),\n        [actions.deleteRewriteSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingDelete: false,\n        }),\n\n        [actions.updateRewriteRequest.toString()]: (state: any) => ({\n            ...state,\n            processingUpdate: true,\n        }),\n        [actions.updateRewriteFailure.toString()]: (state: any) => ({\n            ...state,\n            processingUpdate: false,\n        }),\n        [actions.updateRewriteSuccess.toString()]: (state: any) => {\n            const newState = {\n                ...state,\n                processingUpdate: false,\n            };\n            return newState;\n        },\n        [actions.getRewriteSettingsRequest.toString()]: (state: any) => ({\n            ...state,\n            processing: true,\n        }),\n        [actions.getRewriteSettingsFailure.toString()]: (state: any) => ({\n            ...state,\n            processing: false,\n        }),\n        [actions.getRewriteSettingsSuccess.toString()]: (state: any, { payload }: any) => {\n            const newState = {\n                ...state,\n                settings: payload,\n                processing: false,\n            };\n            return newState;\n        },\n\n        [actions.updateRewriteSettingsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingUpdate: true,\n        }),\n        [actions.updateRewriteSettingsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingUpdate: false,\n        }),\n        [actions.updateRewriteSettingsSuccess.toString()]: (state: any, { payload }: any) => ({\n            ...state,\n            settings: {\n                ...state.settings,\n                ...payload,\n            },\n            processingUpdate: false,\n        }),\n\n        [actions.toggleRewritesModal.toString()]: (state: any, { payload }: any) => {\n            if (payload) {\n                const newState = {\n                    ...state,\n                    modalType: payload.type || '',\n                    isModalOpen: !state.isModalOpen,\n                    currentRewrite: payload.currentRewrite,\n                };\n                return newState;\n            }\n\n            const newState = {\n                ...state,\n                isModalOpen: !state.isModalOpen,\n            };\n            return newState;\n        },\n    },\n    {\n        processing: true,\n        processingAdd: false,\n        processingDelete: false,\n        processingUpdate: false,\n        isModalOpen: false,\n        modalType: '',\n        currentRewrite: {},\n        list: [],\n        settings: { enabled: false },\n    },\n);\n\nexport default rewrites;\n"
  },
  {
    "path": "client/src/reducers/services.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions/services';\n\nconst services = handleActions(\n    {\n        [actions.getBlockedServicesRequest.toString()]: (state: any) => ({\n            ...state,\n            processing: true,\n        }),\n        [actions.getBlockedServicesFailure.toString()]: (state: any) => ({\n            ...state,\n            processing: false,\n        }),\n        [actions.getBlockedServicesSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            list: payload,\n            processing: false,\n        }),\n\n        [actions.getAllBlockedServicesRequest.toString()]: (state: any) => ({\n            ...state,\n            processingAll: true,\n        }),\n        [actions.getAllBlockedServicesFailure.toString()]: (state: any) => ({\n            ...state,\n            processingAll: false,\n        }),\n        [actions.getAllBlockedServicesSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            allServices: payload.blocked_services,\n            allGroups: payload.groups,\n            processingAll: false,\n        }),\n\n        [actions.updateBlockedServicesRequest.toString()]: (state: any) => ({\n            ...state,\n            processingSet: true,\n        }),\n        [actions.updateBlockedServicesFailure.toString()]: (state: any) => ({\n            ...state,\n            processingSet: false,\n        }),\n        [actions.updateBlockedServicesSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingSet: false,\n        }),\n    },\n    {\n        processing: true,\n        processingAll: true,\n        processingSet: false,\n        list: {},\n        allServices: [],\n        allGroups: [],\n    },\n);\n\nexport default services;\n"
  },
  {
    "path": "client/src/reducers/settings.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport * as actions from '../actions';\n\nconst settings = handleActions(\n    {\n        [actions.initSettingsRequest.toString()]: (state: any) => ({\n            ...state,\n            processing: true,\n        }),\n        [actions.initSettingsFailure.toString()]: (state: any) => ({\n            ...state,\n            processing: false,\n        }),\n        [actions.initSettingsSuccess.toString()]: (state: any, { payload }: any) => {\n            const { settingsList } = payload;\n            const newState = {\n                ...state,\n                settingsList,\n                processing: false,\n            };\n            return newState;\n        },\n        [actions.toggleSettingStatus.toString()]: (state: any, { payload }: any) => {\n            const { settingsList } = state;\n            const { settingKey, value } = payload;\n\n            const setting = settingsList[settingKey];\n\n            const newSetting = value || {\n                ...setting,\n                enabled: !setting.enabled,\n            };\n            const newSettingsList = {\n                ...settingsList,\n                [settingKey]: newSetting,\n            };\n            return {\n                ...state,\n                settingsList: newSettingsList,\n            };\n        },\n        [actions.testUpstreamRequest.toString()]: (state: any) => ({\n            ...state,\n            processingTestUpstream: true,\n        }),\n        [actions.testUpstreamFailure.toString()]: (state: any) => ({\n            ...state,\n            processingTestUpstream: false,\n        }),\n        [actions.testUpstreamSuccess.toString()]: (state: any) => ({\n            ...state,\n            processingTestUpstream: false,\n        }),\n    },\n    {\n        processing: true,\n        processingTestUpstream: false,\n        processingDhcpStatus: false,\n        settingsList: {},\n    },\n);\n\nexport default settings;\n"
  },
  {
    "path": "client/src/reducers/stats.ts",
    "content": "import { handleActions } from 'redux-actions';\n\nimport { normalizeTopClients } from '../helpers/helpers';\nimport { DAY, HOUR, STATS_INTERVALS_DAYS, TIME_UNITS } from '../helpers/constants';\n\nimport * as actions from '../actions/stats';\n\nconst defaultStats = {\n    dnsQueries: [],\n    blockedFiltering: [],\n    replacedParental: [],\n    replacedSafebrowsing: [],\n    topBlockedDomains: [],\n    topClients: [],\n    topQueriedDomains: [],\n    numBlockedFiltering: 0,\n    numDnsQueries: 0,\n    numReplacedParental: 0,\n    numReplacedSafebrowsing: 0,\n    numReplacedSafesearch: 0,\n    avgProcessingTime: 0,\n    timeUnits: TIME_UNITS.HOURS,\n};\n\nconst stats = handleActions(\n    {\n        [actions.getStatsConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingGetConfig: true,\n        }),\n        [actions.getStatsConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingGetConfig: false,\n        }),\n        [actions.getStatsConfigSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            ...payload,\n\n            customInterval: !STATS_INTERVALS_DAYS.includes(payload.interval) ? payload.interval / HOUR : null,\n\n            processingGetConfig: false,\n        }),\n\n        [actions.setStatsConfigRequest.toString()]: (state: any) => ({\n            ...state,\n            processingSetConfig: true,\n        }),\n        [actions.setStatsConfigFailure.toString()]: (state: any) => ({\n            ...state,\n            processingSetConfig: false,\n        }),\n        [actions.setStatsConfigSuccess.toString()]: (state, { payload }: any) => ({\n            ...state,\n            ...payload,\n            processingSetConfig: false,\n        }),\n\n        [actions.getStatsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingStats: true,\n        }),\n        [actions.getStatsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingStats: false,\n        }),\n        [actions.getStatsSuccess.toString()]: (state: any, { payload }: any) => {\n            const {\n                dns_queries: dnsQueries,\n                blocked_filtering: blockedFiltering,\n                replaced_parental: replacedParental,\n                replaced_safebrowsing: replacedSafebrowsing,\n                top_blocked_domains: topBlockedDomains,\n                top_clients: topClients,\n                top_queried_domains: topQueriedDomains,\n                num_blocked_filtering: numBlockedFiltering,\n                num_dns_queries: numDnsQueries,\n                num_replaced_parental: numReplacedParental,\n                num_replaced_safebrowsing: numReplacedSafebrowsing,\n                num_replaced_safesearch: numReplacedSafesearch,\n                avg_processing_time: avgProcessingTime,\n                top_upstreams_responses: topUpstreamsResponses,\n                top_upstrems_avg_time: topUpstreamsAvgTime,\n                time_units: timeUnits,\n            } = payload;\n\n            const newState = {\n                ...state,\n                processingStats: false,\n                dnsQueries,\n                blockedFiltering,\n                replacedParental,\n                replacedSafebrowsing,\n                topBlockedDomains,\n                topClients,\n                normalizedTopClients: normalizeTopClients(topClients),\n                topQueriedDomains,\n                numBlockedFiltering,\n                numDnsQueries,\n                numReplacedParental,\n                numReplacedSafebrowsing,\n                numReplacedSafesearch,\n                avgProcessingTime,\n                topUpstreamsResponses,\n                topUpstreamsAvgTime,\n                timeUnits,\n            };\n\n            return newState;\n        },\n\n        [actions.resetStatsRequest.toString()]: (state: any) => ({\n            ...state,\n            processingReset: true,\n        }),\n        [actions.resetStatsFailure.toString()]: (state: any) => ({\n            ...state,\n            processingReset: false,\n        }),\n        [actions.resetStatsSuccess.toString()]: (state: any) => ({\n            ...state,\n            ...defaultStats,\n            processingReset: false,\n        }),\n    },\n    {\n        processingGetConfig: false,\n        processingSetConfig: false,\n        processingStats: true,\n        processingReset: false,\n        interval: DAY,\n        customInterval: null,\n        ignored_enabled: true,\n        ...defaultStats,\n    },\n);\n\nexport default stats;\n"
  },
  {
    "path": "client/src/reducers/toasts.ts",
    "content": "import { handleActions } from 'redux-actions';\nimport { nanoid } from 'nanoid';\n\nimport { addErrorToast, addNoticeToast, addSuccessToast } from '../actions/toasts';\nimport { removeToast } from '../actions';\nimport { TOAST_TYPES } from '../helpers/constants';\n\nconst toasts = handleActions(\n    {\n        [addErrorToast.toString()]: (state: any, { payload }: any) => {\n            const message = payload.error.toString();\n            console.error(payload.error);\n\n            const errorToast = {\n                id: nanoid(),\n                message,\n                options: payload.options,\n                type: TOAST_TYPES.ERROR,\n            };\n\n            const newState = { ...state, notices: [...state.notices, errorToast] };\n            return newState;\n        },\n        [addSuccessToast.toString()]: (state: any, { payload }: any) => {\n            const successToast = {\n                id: nanoid(),\n                message: payload,\n                type: TOAST_TYPES.SUCCESS,\n            };\n\n            const newState = { ...state, notices: [...state.notices, successToast] };\n            return newState;\n        },\n        [addNoticeToast.toString()]: (state: any, { payload }: any) => {\n            const noticeToast = {\n                id: nanoid(),\n                message: payload.error.toString(),\n                options: payload.options,\n                type: TOAST_TYPES.NOTICE,\n            };\n\n            const newState = { ...state, notices: [...state.notices, noticeToast] };\n            return newState;\n        },\n        [removeToast.toString()]: (state: any, { payload }: any) => {\n            const filtered = state.notices.filter((notice: any) => notice.id !== payload);\n            const newState = { ...state, notices: filtered };\n            return newState;\n        },\n    },\n    { notices: [] },\n);\n\nexport default toasts;\n"
  },
  {
    "path": "client/src/types.d.ts",
    "content": "declare global {\n    interface Window {\n        __REDUX_DEVTOOLS_EXTENSION__?: () => any;\n    }\n}\n"
  },
  {
    "path": "client/tests/constants.ts",
    "content": "export const ADMIN_USERNAME = 'admin';\nexport const ADMIN_PASSWORD = 'superpassword';\nexport const PORT = 3000;\nexport const CONFIG_FILE_PATH = '/tmp/AdGuard.e2e.yaml';\n"
  },
  {
    "path": "client/tests/e2e/control-panel.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\nimport { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';\n\ntest.describe('Control Panel', () => {\n    test.beforeEach(async ({ page }) => {\n        await page.goto('/login.html');\n        await page.getByTestId('username').click();\n        await page.getByTestId('username').fill(ADMIN_USERNAME);\n        await page.getByTestId('password').click();\n        await page.getByTestId('password').fill(ADMIN_PASSWORD);\n        await page.keyboard.press('Tab');\n        await page.getByTestId('sign_in').click();\n        await page.waitForURL((url) => !url.href.endsWith('/login.html'));\n    });\n\n    test('should sign out successfully', async ({ page }) => {\n        await page.getByTestId('sign_out').click();\n\n        await page.waitForURL((url) => url.href.endsWith('/login.html'));\n\n        await expect(page.getByTestId('sign_in')).toBeVisible();\n    });\n\n    test('should change theme to dark and then light', async ({ page }) => {\n        await page.getByTestId('theme_dark').click();\n\n        await expect(page.locator('body[data-theme=\"dark\"]')).toBeVisible();\n\n\n        await page.getByTestId('theme_light').click();\n\n        await expect(page.locator('body:not([data-theme=\"dark\"])')).toBeVisible();\n    });\n});\n"
  },
  {
    "path": "client/tests/e2e/dhcp.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\nimport { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';\nimport { getDHCPConfig } from '../helpers/network';\n\nconst dhcpConfig = getDHCPConfig();\nconst INTERFACE_NAME = dhcpConfig.interfaceName;\nconst RANGE_START = dhcpConfig.rangeStart;\nconst RANGE_END = dhcpConfig.rangeEnd;\nconst SUBNET_MASK = dhcpConfig.subnetMask;\nconst LEASE_TIME = '86400';\n\ntest.describe('DHCP Configuration', () => {\n    test.beforeEach(async ({ page }) => {\n        await page.goto('/login.html');\n        await page.getByTestId('username').click();\n        await page.getByTestId('username').fill(ADMIN_USERNAME);\n        await page.getByTestId('password').click();\n        await page.getByTestId('password').fill(ADMIN_PASSWORD);\n        await page.keyboard.press('Tab');\n        await page.getByTestId('sign_in').click();\n        await page.waitForURL((url) => !url.href.endsWith('/login.html'));\n        await page.goto(`/#dhcp`);\n    });\n\n    test('should select the correct DHCP interface', async ({ page }) => {\n        await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);\n        expect(await page.locator('select[name=\"interface_name\"]').inputValue()).toBe(INTERFACE_NAME);\n    });\n\n    test('should configure DHCP IPv4 settings correctly', async ({ page }) => {\n        await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);\n        await page.getByTestId('v4_gateway_ip').click();\n        await page.getByTestId('v4_gateway_ip').fill('192.168.1.99');\n        await page.getByTestId('v4_subnet_mask').click();\n        await page.getByTestId('v4_subnet_mask').fill(SUBNET_MASK);\n        await page.getByTestId('v4_range_start').click();\n        await page.getByTestId('v4_range_start').fill(RANGE_START);\n        await page.getByTestId('v4_range_end').click();\n        await page.getByTestId('v4_range_end').fill(RANGE_END);\n        await page.getByTestId('v4_lease_duration').click();\n        await page.getByTestId('v4_lease_duration').fill(LEASE_TIME);\n        await page.getByTestId('v4_save').click();\n    });\n\n    test('should show error for invalid DHCP IPv4 range', async ({ page }) => {\n        await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);\n        await page.getByTestId('v4_range_start').click();\n        await page.getByTestId('v4_range_start').fill(RANGE_END);\n        await page.getByTestId('v4_range_end').click();\n        await page.getByTestId('v4_range_end').fill(RANGE_START);\n        await page.keyboard.press('Tab');\n\n        expect(await page.getByText('Must be greater than range').isVisible()).toBe(true);\n    });\n\n    test('should show error for invalid DHCP IPv4 address', async ({ page }) => {\n        await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);\n        await page.getByTestId('v4_gateway_ip').click();\n        await page.getByTestId('v4_gateway_ip').fill('192.168.1.200s');\n        await page.keyboard.press('Tab');\n\n        expect(await page.getByText('Invalid IPv4 address').isVisible()).toBe(true);\n    });\n});\n"
  },
  {
    "path": "client/tests/e2e/dns-settings.spec.ts",
    "content": "import { test, expect, type Page } from '@playwright/test';\nimport { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';\n\ntest.describe('DNS Settings', () => {\n    test.beforeEach(async ({ page }) => {\n        // Login before each test\n        await page.goto('/login.html');\n        await page.getByTestId('username').click();\n        await page.getByTestId('username').fill(ADMIN_USERNAME);\n        await page.getByTestId('password').click();\n        await page.getByTestId('password').fill(ADMIN_PASSWORD);\n        await page.keyboard.press('Tab');\n        await page.getByTestId('sign_in').click();\n        await page.waitForURL((url) => !url.href.endsWith('/login.html'));\n    });\n\n    const runDNSSettingsTest = async (page: Page, address: string) => {\n        await page.goto('/#dns');\n\n        const currentDns = await page.getByTestId('upstream_dns').inputValue();\n\n        await page.getByTestId('upstream_dns').fill(address);\n        await page.getByTestId('dns_upstream_test').click();\n\n        await page.waitForTimeout(2000);\n\n        await expect(page.getByTestId('upstream_dns')).toHaveValue(address);\n\n        await page.getByTestId('upstream_dns').fill(currentDns);\n        await page.getByTestId('dns_upstream_save').click({ force: true });\n    };\n\n    test('test for Default DNS', async ({ page }) => {\n        await runDNSSettingsTest(page, 'https://dns10.quad9.net/dns-query');\n    });\n\n    test('test for Plain DNS', async ({ page }) => {\n        await runDNSSettingsTest(page, '94.140.14.140');\n    });\n\n    test('test for DNS-over-HTTPS', async ({ page }) => {\n        await runDNSSettingsTest(page, 'https://unfiltered.adguard-dns.com/dns-query');\n    });\n\n    test('test for DNS-over-TLS', async ({ page }) => {\n        await runDNSSettingsTest(page, 'tls://unfiltered.adguard-dns.com');\n    });\n\n    test('test for DNS-over-QUIC', async ({ page }) => {\n        await runDNSSettingsTest(page, 'quic://unfiltered.adguard-dns.com');\n    });\n});\n"
  },
  {
    "path": "client/tests/e2e/filtering.spec.ts",
    "content": "import { test, expect, type Page } from '@playwright/test';\nimport { execSync } from 'child_process';\nimport { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';\n\ntest.describe('Filtering', () => {\n    test.beforeEach(async ({ page }) => {\n        // Login before each test\n        await page.goto('/login.html');\n        await page.getByTestId('username').click();\n        await page.getByTestId('username').fill(ADMIN_USERNAME);\n        await page.getByTestId('password').click();\n        await page.getByTestId('password').fill(ADMIN_PASSWORD);\n        await page.keyboard.press('Tab');\n        await page.getByTestId('sign_in').click();\n        await page.waitForURL((url) => !url.href.endsWith('/login.html'));\n    });\n\n    const runTerminalCommand = (command: string) => {\n        try {\n            console.info(`Executing command: ${command}`);\n\n            const output = execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();\n\n            console.info('Command executed successfully.');\n            console.debug(`Command output:\\n${output}`);\n\n            return output;\n        } catch (error: any) {\n            console.error(`Command execution failed with error:\\n${error.message}`);\n            throw new Error(`Failed to execute command: ${command}\\nError: ${error.message}`);\n        }\n    }\n\n    const runCustomRuleTest = async (page: Page, domain_to_block: string) => {\n        await page.goto('/#custom_rules');\n\n        await page.getByTestId('custom_rule_textarea').fill(domain_to_block);\n        await page.getByTestId('apply_custom_rule').click();\n\n        const nslookupBlockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();\n\n        console.info(`nslookup blocked CNAME result: '${nslookupBlockedResult}'`);\n\n        const currentRules = await page.getByTestId('custom_rule_textarea').inputValue();\n        console.debug(`Current rules before removal:\\n${currentRules}`);\n\n        if (currentRules.includes(domain_to_block)) {\n            const updatedRules = currentRules\n            .split('\\n')\n            .filter((line) => line.trim() !== domain_to_block.trim())\n            .join('\\n');\n\n            await page.getByTestId('custom_rule_textarea').fill(updatedRules);\n            console.info(`Rule '${domain_to_block}' removed successfully.`);\n\n            console.info('Applying the updated filtering rules after removal.');\n            await page.getByTestId('apply_custom_rule').click();\n\n            await page.waitForLoadState('domcontentloaded');\n\n            console.info(`Filtering rules successfully updated after removing '${domain_to_block}'.`);\n        } else {\n            console.warn(`Rule '${domain_to_block}' not found. No changes were made.`);\n        }\n\n        const nslookupUnblockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();\n        console.info(`nslookup unblocked CNAME result: '${nslookupUnblockedResult}'`);\n    };\n\n    test('Test blocking rule for apple.com', async ({ page }) => {\n        await runCustomRuleTest(page, 'apple.com');\n    });\n});\n"
  },
  {
    "path": "client/tests/e2e/general-settings.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\nimport { execSync } from 'child_process';\nimport { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';\n\ntest.describe('General Settings', () => {\n    test.beforeEach(async ({ page }) => {\n        await page.goto('/login.html');\n        await page.getByTestId('username').click();\n        await page.getByTestId('username').fill(ADMIN_USERNAME);\n        await page.getByTestId('password').click();\n        await page.getByTestId('password').fill(ADMIN_PASSWORD);\n        await page.keyboard.press('Tab');\n        await page.getByTestId('sign_in').click();\n        await page.waitForURL((url) => !url.href.endsWith('/login.html'));\n    });\n\n    test('should toggle browsing security feature and verify DNS changes', async ({ page }) => {\n        await page.goto('/#settings');\n\n        const browsingSecurity = await page.getByTestId('safebrowsing');\n        const browsingSecurityLabel = await browsingSecurity.locator('xpath=following-sibling::*[1]');\n\n        const initialState = await browsingSecurity.isChecked();\n\n        if (!initialState) {\n            await browsingSecurityLabel.click();\n            await expect(browsingSecurity).toBeChecked();\n        }\n\n        const resultEnabled = execSync('dig @127.0.0.1 totalvirus.com').toString();\n\n        await browsingSecurityLabel.click();\n        await expect(browsingSecurity).not.toBeChecked();\n\n        const resultDisabled = execSync('dig @127.0.0.1 totalvirus.com').toString();\n\n        expect(resultEnabled).not.toEqual(resultDisabled);\n\n        if (initialState) {\n            await browsingSecurityLabel.click();\n            await expect(browsingSecurity).toBeChecked();\n        }\n    });\n\n    test('should toggle parental control feature and verify DNS changes', async ({ page }) => {\n        await page.goto('/#settings');\n\n        const parentalControl = page.getByTestId('parental');\n        const parentalControlLabel = await parentalControl.locator('xpath=following-sibling::*[1]');\n\n        const initialState = await parentalControl.isChecked();\n\n        if (!initialState) {\n            await parentalControlLabel.click();\n            await expect(parentalControl).toBeChecked();\n        }\n\n        const resultEnabled = execSync('dig @127.0.0.1 pornhub.com').toString();\n\n        await parentalControlLabel.click();\n        await expect(parentalControl).not.toBeChecked();\n\n        const resultDisabled = execSync('dig @127.0.0.1 pornhub.com').toString();\n\n        expect(resultEnabled).not.toEqual(resultDisabled);\n\n        if (initialState) {\n            await parentalControlLabel.click();\n            await expect(parentalControl).toBeChecked();\n        }\n    });\n\n    test('should toggle safe search feature', async ({ page }) => {\n        await page.goto('/#settings');\n\n        const safeSearch = page.getByTestId('safesearch');\n        const safeSearchLabel = await safeSearch.locator('xpath=following-sibling::*[1]');\n\n        const initialState = await safeSearch.isChecked();\n\n        await safeSearchLabel.click();\n\n        await expect(safeSearch).not.toBeChecked({ checked: initialState });\n\n        await safeSearchLabel.click();\n\n        await expect(safeSearch).toBeChecked({ checked: initialState });\n    });\n});\n"
  },
  {
    "path": "client/tests/e2e/globalSetup.ts",
    "content": "import { chromium, type FullConfig } from '@playwright/test';\n\nimport { ADMIN_USERNAME, ADMIN_PASSWORD, PORT, CONFIG_FILE_PATH } from '../constants';\n\nconst BASE_URL = `http://127.0.0.1:${PORT}`;\n\nasync function checkServerAvailable(): Promise<boolean> {\n    try {\n        const response = await fetch(BASE_URL);\n        return response.ok || response.status === 302;\n    } catch {\n        return false;\n    }\n}\n\nasync function globalSetup(config: FullConfig) {\n    if (!process.env.CI) {\n        const isServerRunning = await checkServerAvailable();\n        if (!isServerRunning) {\n            console.error(\n                `\\nAdGuard Home server is not running. Start it first:\\n  sudo ./AdGuardHome --local-frontend -v -c ${CONFIG_FILE_PATH}\\n`,\n            );\n            process.exit(1);\n        }\n    }\n\n    const browser = await chromium.launch({\n        slowMo: 100,\n    });\n    const page = await browser.newPage({ baseURL: config.webServer?.url || BASE_URL });\n\n    await page.goto('/');\n\n    // Check if we're on the install page or already installed\n    const isInstallPage = page.url().includes('/install.html');\n\n    if (isInstallPage) {\n        await page.getByTestId('install_get_started').click();\n        await page.getByTestId('install_web_port').fill(PORT.toString());\n        await page.getByTestId('install_next').click();\n        await page.getByTestId('install_username').fill(ADMIN_USERNAME);\n        await page.getByTestId('install_username').blur();\n        await page.getByTestId('install_password').fill(ADMIN_PASSWORD);\n        await page.getByTestId('install_password').blur();\n        await page.getByTestId('install_confirm_password').fill(ADMIN_PASSWORD);\n        await page.getByTestId('install_confirm_password').blur();\n        await page.getByTestId('install_next').click();\n        await page.getByTestId('install_next').click();\n        await page.getByTestId('install_open_dashboard').click();\n        await page.waitForURL((url) => !url.href.endsWith('/install.html'));\n    }\n\n    await browser.close();\n}\n\nexport default globalSetup;\n"
  },
  {
    "path": "client/tests/e2e/globalTeardown.ts",
    "content": "import { existsSync, unlinkSync } from 'fs';\nimport { CONFIG_FILE_PATH } from '../constants';\n\nasync function globalTeardown() {\n    // Remove the test config file\n    if (existsSync(CONFIG_FILE_PATH)) {\n        unlinkSync(CONFIG_FILE_PATH);\n    }\n}\n\nexport default globalTeardown;\n"
  },
  {
    "path": "client/tests/e2e/login.spec.ts",
    "content": "import { test } from '@playwright/test';\nimport { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';\n\n\ntest.describe('Login', () => {\n    test('should successfully log in with valid credentials', async ({ page }) => {\n        await page.goto('/login.html');\n        await page.getByTestId('username').click();\n        await page.getByTestId('username').fill(ADMIN_USERNAME);\n        await page.getByTestId('password').click();\n        await page.getByTestId('password').fill(ADMIN_PASSWORD);\n        await page.keyboard.press('Tab');\n        await page.getByTestId('sign_in').click();\n        await page.waitForURL((url) => !url.href.endsWith('/login.html'));\n    });\n});\n"
  },
  {
    "path": "client/tests/e2e/querylog.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\nimport { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';\n\ntest.describe('QueryLog', () => {\n    test.beforeEach(async ({ page }) => {\n        await page.goto('/login.html');\n        await page.getByTestId('username').click();\n        await page.getByTestId('username').fill(ADMIN_USERNAME);\n        await page.getByTestId('password').click();\n        await page.getByTestId('password').fill(ADMIN_PASSWORD);\n        await page.keyboard.press('Tab');\n        await page.getByTestId('sign_in').click();\n        await page.waitForURL((url) => !url.href.endsWith('/login.html'));\n    });\n\n    test('Search of queryLog should work correctly', async ({ page }) => {\n        await page.route('/control/querylog', async (route) => {\n            await route.fulfill({\n                status: 200,\n                contentType: 'application/json',\n                body: JSON.stringify(\n                    {\n                        \"data\": [\n                            {\n                                \"answer\": [\n                                    {\n                                        \"type\": \"A\",\n                                        \"value\": \"77.88.44.242\",\n                                        \"ttl\": 294\n                                    },\n                                    {\n                                        \"type\": \"A\",\n                                        \"value\": \"5.255.255.242\",\n                                        \"ttl\": 294\n                                    },\n                                    {\n                                        \"type\": \"A\",\n                                        \"value\": \"77.88.55.242\",\n                                        \"ttl\": 294\n                                    }\n                                ],\n                                \"answer_dnssec\": false,\n                                \"cached\": false,\n                                \"client\": \"127.0.0.1\",\n                                \"client_info\": {\n                                    \"whois\": {},\n                                    \"name\": \"localhost\",\n                                    \"disallowed_rule\": \"127.0.0.1\",\n                                    \"disallowed\": false\n                                },\n                                \"client_proto\": \"\",\n                                \"elapsedMs\": \"78.163167\",\n                                \"question\": {\n                                    \"class\": \"IN\",\n                                    \"name\": \"ya.ru\",\n                                    \"type\": \"A\"\n                                },\n                                \"reason\": \"NotFilteredNotFound\",\n                                \"rules\": [],\n                                \"status\": \"NOERROR\",\n                                \"time\": \"2024-07-17T16:02:37.500662+02:00\",\n                                \"upstream\": \"https://dns10.quad9.net:443/dns-query\"\n                            },\n                            {\n                                \"answer\": [\n                                    {\n                                        \"type\": \"A\",\n                                        \"value\": \"77.88.55.242\",\n                                        \"ttl\": 351\n                                    },\n                                    {\n                                        \"type\": \"A\",\n                                        \"value\": \"77.88.44.242\",\n                                        \"ttl\": 351\n                                    },\n                                    {\n                                        \"type\": \"A\",\n                                        \"value\": \"5.255.255.242\",\n                                        \"ttl\": 351\n                                    }\n                                ],\n                                \"answer_dnssec\": false,\n                                \"cached\": false,\n                                \"client\": \"127.0.0.1\",\n                                \"client_info\": {\n                                    \"whois\": {},\n                                    \"name\": \"localhost\",\n                                    \"disallowed_rule\": \"127.0.0.1\",\n                                    \"disallowed\": false\n                                },\n                                \"client_proto\": \"\",\n                                \"elapsedMs\": \"5051.070708\",\n                                \"question\": {\n                                    \"class\": \"IN\",\n                                    \"name\": \"ya.ru\",\n                                    \"type\": \"A\"\n                                },\n                                \"reason\": \"NotFilteredNotFound\",\n                                \"rules\": [],\n                                \"status\": \"NOERROR\",\n                                \"time\": \"2024-07-17T16:02:37.4983+02:00\",\n                                \"upstream\": \"https://dns10.quad9.net:443/dns-query\"\n                            }\n                        ],\n                        \"oldest\": \"2024-07-17T16:02:37.4983+02:00\"\n                    }\n                ),\n            });\n        });\n\n        await page.goto('/#logs');\n\n        await page.getByTestId('querylog_search').fill('127.0.0.1');\n\n        const [request] = await Promise.all([\n            page.waitForRequest((req) => req.url().includes('/control/querylog')),\n        ]);\n\n        if (request) {\n            expect(request.url()).toContain('search=127.0.0.1');\n            expect(await page.getByTestId('querylog_cell').first().isVisible()).toBe(true);\n        }\n    });\n});\n"
  },
  {
    "path": "client/tests/e2e/rewrites.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\nimport { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';\n\nconst EXAMPLE_DOMAIN = `example.org`;\nconst EXAMPLE_UPDATED_DOMAIN = `updated.org`;\nconst EXAMPLE_ANSWER = '192.168.1.1';\n\ntest.describe('Rewrites', () => {\n    test.beforeEach(async ({ page }) => {\n        await page.goto('/login.html');\n        await page.getByTestId('username').click();\n        await page.getByTestId('username').fill(ADMIN_USERNAME);\n        await page.getByTestId('password').click();\n        await page.getByTestId('password').fill(ADMIN_PASSWORD);\n        await page.keyboard.press('Tab');\n        await page.getByTestId('sign_in').click();\n        await page.waitForURL((url) => !url.href.endsWith('/login.html'));\n        await page.goto('/#dns_rewrites');\n    });\n\n    test('should add a DNS rewrite', async ({ page }) => {\n        await page.getByTestId('add-rewrite').click();\n        await page.getByTestId('rewrites_domain').fill(EXAMPLE_DOMAIN);\n        await page.getByTestId('rewrites_answer').fill(EXAMPLE_ANSWER);\n        await page.getByTestId('rewrites_save').click();\n\n        await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_DOMAIN }).first()).toBeVisible();\n        await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_ANSWER }).first()).toBeVisible();\n    });\n\n    test('should edit a DNS rewrite', async ({ page }) => {\n        // Use the first existing rewrite instead of creating a new one\n        // Wait for the table to load\n        await expect(page.getByTestId('edit-rewrite').first()).toBeVisible({ timeout: 10000 });\n\n        // Get the current domain value before editing\n        await page.getByTestId('edit-rewrite').first().click();\n        const originalDomain = await page.getByTestId('rewrites_domain').inputValue();\n\n        // Edit the domain - use keyboard to ensure isDirty is triggered\n        const domainInput = page.getByTestId('rewrites_domain');\n        await domainInput.click();\n        await domainInput.press('Control+a');\n        await domainInput.pressSequentially(EXAMPLE_UPDATED_DOMAIN);\n        await domainInput.blur();\n        await page.getByTestId('rewrites_save').click();\n\n        // Verify the update\n        await expect(page.locator('.logs__text').filter({ hasText: EXAMPLE_UPDATED_DOMAIN }).first()).toBeVisible({ timeout: 10000 });\n\n        // Restore original value\n        await page.getByTestId('edit-rewrite').first().click();\n        const restoreInput = page.getByTestId('rewrites_domain');\n        await restoreInput.click();\n        await restoreInput.press('Control+a');\n        await restoreInput.pressSequentially(originalDomain);\n        await restoreInput.blur();\n        await page.getByTestId('rewrites_save').click();\n    });\n});\n"
  },
  {
    "path": "client/tests/helpers/network.ts",
    "content": "import { networkInterfaces } from 'os';\nimport type { NetworkInterfaceInfo } from 'node:os';\n\ninterface DHCPConfig {\n    interfaceName: string;\n    rangeStart: string;\n    rangeEnd: string;\n    subnetMask: string;\n}\n\nconst DEFAULT_SUBNET_MASK = '255.255.255.0';\nconst DEFAULT_SUBNET_MASK_OCTETS = DEFAULT_SUBNET_MASK.split('.').map(Number);\n\nfunction checkIsIPv4(addr: NetworkInterfaceInfo): boolean {\n    return addr.family === 'IPv4' && !addr.internal;\n}\n\nfunction calculateNetwork(ip: number[], mask: number[]): number[] {\n    // Calculate the network address by applying the bitwise AND operation.\n    // eslint-disable-next-line no-bitwise\n    return ip.map((octet, i) => octet & mask[i]);\n}\n\nfunction calculateBroadcast(network: number[], mask: number[]): number[] {\n    // Calculate the broadcast address by ORing the network address with the inverted mask.\n    // eslint-disable-next-line no-bitwise\n    return network.map((octet, i) => octet | (~mask[i] & 255));\n}\n\nexport function getDHCPConfig(): DHCPConfig {\n    const interfaces = networkInterfaces();\n\n    // Select the first interface that has a valid non-internal IPv4 address.\n    const ipV4Interface = Object.entries(interfaces)\n        .map(([name, addresses]) => ({ name, addresses }))\n        .find((i) => i.addresses?.some(checkIsIPv4));\n\n    if (!ipV4Interface) {\n        throw new Error('No suitable network interface found');\n    }\n\n    // Get the first valid IPv4 address from the interface.\n    const ipv4Address = ipV4Interface.addresses.find(checkIsIPv4);\n\n    const ip = ipv4Address.address.split('.').map(Number);\n    const mask = ipv4Address.netmask?.split('.').map(Number) || DEFAULT_SUBNET_MASK_OCTETS;\n\n    const network = calculateNetwork(ip, mask);\n\n    // Calculate first usable address (network address + 1)\n    const rangeStart = [...network];\n    rangeStart[3] = network[3] + 1;\n\n    // Calculate broadcast address and then the last usable address (broadcast - 1)\n    const broadcast = calculateBroadcast(network, mask);\n    const rangeEnd = [...broadcast];\n    rangeEnd[3] = broadcast[3] - 1;\n\n    return {\n        interfaceName: ipV4Interface.name,\n        rangeStart: rangeStart.join('.'),\n        rangeEnd: rangeEnd.join('.'),\n        subnetMask: ipv4Address.netmask || DEFAULT_SUBNET_MASK,\n    };\n}\n"
  },
  {
    "path": "client/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        /* Visit https://aka.ms/tsconfig to read more about this file */\n\n        /* Projects */\n        // \"incremental\": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */\n        // \"composite\": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */\n        // \"tsBuildInfoFile\": \"./.tsbuildinfo\",              /* Specify the path to .tsbuildinfo incremental compilation file. */\n        // \"disableSourceOfProjectReferenceRedirect\": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */\n        // \"disableSolutionSearching\": true,                 /* Opt a project out of multi-project reference checking when editing. */\n        // \"disableReferencedProjectLoad\": true,             /* Reduce the number of projects loaded automatically by TypeScript. */\n\n        /* Language and Environment */\n        \"target\": \"ESNext\" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,\n        // \"lib\": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */\n        \"jsx\": \"react\" /* Specify what JSX code is generated. */,\n        // \"experimentalDecorators\": true,                   /* Enable experimental support for legacy experimental decorators. */\n        // \"emitDecoratorMetadata\": true,                    /* Emit design-type metadata for decorated declarations in source files. */\n        // \"jsxFactory\": \"\",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */\n        // \"jsxFragmentFactory\": \"\",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */\n        // \"jsxImportSource\": \"\",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */\n        // \"reactNamespace\": \"\",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */\n        // \"noLib\": true,                                    /* Disable including any library files, including the default lib.d.ts. */\n        // \"useDefineForClassFields\": true,                  /* Emit ECMAScript-standard-compliant class fields. */\n        // \"moduleDetection\": \"auto\",                        /* Control what method is used to detect module-format JS files. */\n\n        /* Modules */\n        \"module\": \"ESNext\" /* Specify what module code is generated. */,\n        // \"rootDir\": \"./\",                                  /* Specify the root folder within your source files. */\n        \"moduleResolution\": \"bundler\" /* Specify how TypeScript looks up a file from a given module specifier. */,\n        // \"baseUrl\": \"./\",                                  /* Specify the base directory to resolve non-relative module names. */\n        // \"paths\": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */\n        // \"rootDirs\": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */\n        // \"typeRoots\": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */\n        // \"types\": [],                                      /* Specify type package names to be included without being referenced in a source file. */\n        // \"allowUmdGlobalAccess\": true,                     /* Allow accessing UMD globals from modules. */\n        // \"moduleSuffixes\": [],                             /* List of file name suffixes to search when resolving a module. */\n        // \"allowImportingTsExtensions\": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */\n        // \"resolvePackageJsonExports\": true,                /* Use the package.json 'exports' field when resolving package imports. */\n        // \"resolvePackageJsonImports\": true,                /* Use the package.json 'imports' field when resolving imports. */\n        // \"customConditions\": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */\n        \"resolveJsonModule\": true /* Enable importing .json files. */,\n        // \"allowArbitraryExtensions\": true,                 /* Enable importing files with any extension, provided a declaration file is present. */\n        // \"noResolve\": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */\n\n        /* JavaScript Support */\n        \"allowJs\": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,\n        // \"checkJs\": true,                                  /* Enable error reporting in type-checked JavaScript files. */\n        // \"maxNodeModuleJsDepth\": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */\n\n        /* Emit */\n        // \"declaration\": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */\n        // \"declarationMap\": true,                           /* Create sourcemaps for d.ts files. */\n        // \"emitDeclarationOnly\": true,                      /* Only output d.ts files and not JavaScript files. */\n        // \"sourceMap\": true,                                /* Create source map files for emitted JavaScript files. */\n        // \"inlineSourceMap\": true,                          /* Include sourcemap files inside the emitted JavaScript. */\n        // \"outFile\": \"./\",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */\n        // \"outDir\": \"./\",                                   /* Specify an output folder for all emitted files. */\n        // \"removeComments\": true,                           /* Disable emitting comments. */\n        // \"noEmit\": true,                                   /* Disable emitting files from a compilation. */\n        // \"importHelpers\": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */\n        // \"importsNotUsedAsValues\": \"remove\",               /* Specify emit/checking behavior for imports that are only used for types. */\n        // \"downlevelIteration\": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */\n        // \"sourceRoot\": \"\",                                 /* Specify the root path for debuggers to find the reference source code. */\n        // \"mapRoot\": \"\",                                    /* Specify the location where debugger should locate map files instead of generated locations. */\n        // \"inlineSources\": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */\n        // \"emitBOM\": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */\n        // \"newLine\": \"crlf\",                                /* Set the newline character for emitting files. */\n        // \"stripInternal\": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */\n        // \"noEmitHelpers\": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */\n        // \"noEmitOnError\": true,                            /* Disable emitting files if any type checking errors are reported. */\n        // \"preserveConstEnums\": true,                       /* Disable erasing 'const enum' declarations in generated code. */\n        // \"declarationDir\": \"./\",                           /* Specify the output directory for generated declaration files. */\n        // \"preserveValueImports\": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */\n\n        /* Interop Constraints */\n        // \"isolatedModules\": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */\n        // \"verbatimModuleSyntax\": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */\n        // \"allowSyntheticDefaultImports\": true,             /* Allow 'import x from y' when a module doesn't have a default export. */\n        \"esModuleInterop\": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,\n        // \"preserveSymlinks\": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */\n        \"forceConsistentCasingInFileNames\": true /* Ensure that casing is correct in imports. */,\n\n        /* Type Checking */\n        \"strict\": false /* Enable all strict type-checking options. */,\n        \"noImplicitAny\": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */,\n        // \"strictNullChecks\": true,                         /* When type checking, take into account 'null' and 'undefined'. */\n        // \"strictFunctionTypes\": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */\n        // \"strictBindCallApply\": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */\n        // \"strictPropertyInitialization\": true,             /* Check for class properties that are declared but not set in the constructor. */\n        // \"noImplicitThis\": true,                           /* Enable error reporting when 'this' is given the type 'any'. */\n        // \"useUnknownInCatchVariables\": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */\n        // \"alwaysStrict\": true,                             /* Ensure 'use strict' is always emitted. */\n        // \"noUnusedLocals\": true,                           /* Enable error reporting when local variables aren't read. */\n        // \"noUnusedParameters\": true,                       /* Raise an error when a function parameter isn't read. */\n        // \"exactOptionalPropertyTypes\": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */\n        // \"noImplicitReturns\": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */\n        // \"noFallthroughCasesInSwitch\": true,               /* Enable error reporting for fallthrough cases in switch statements. */\n        // \"noUncheckedIndexedAccess\": true,                 /* Add 'undefined' to a type when accessed using an index. */\n        // \"noImplicitOverride\": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */\n        // \"noPropertyAccessFromIndexSignature\": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */\n        // \"allowUnusedLabels\": true,                        /* Disable error reporting for unused labels. */\n        // \"allowUnreachableCode\": true,                     /* Disable error reporting for unreachable code. */\n\n        /* Completeness */\n        // \"skipDefaultLibCheck\": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */\n        \"skipLibCheck\": true /* Skip type checking all .d.ts files. */\n    },\n    \"include\": [\"src\", \"global.d.ts\"]\n}\n"
  },
  {
    "path": "client/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n    test: {\n        environment: 'jsdom',\n        include: ['src/__tests__/**'],\n    },\n});\n"
  },
  {
    "path": "client/webpack.common.js",
    "content": "import path from 'path';\nimport HtmlWebpackPlugin from 'html-webpack-plugin';\nimport { CleanWebpackPlugin } from 'clean-webpack-plugin';\nimport CopyPlugin from 'copy-webpack-plugin';\nimport MiniCssExtractPlugin from 'mini-css-extract-plugin';\nimport * as url from 'url';\nimport { BUILD_ENVS } from './constants.js';\n\n// eslint-disable-next-line no-underscore-dangle\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url));\n\nconst RESOURCES_PATH = path.resolve(__dirname);\nconst ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.tsx');\nconst ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/index.tsx');\nconst ENTRY_LOGIN = path.resolve(RESOURCES_PATH, 'src/login/index.tsx');\nconst HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');\nconst HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');\nconst HTML_LOGIN_PATH = path.resolve(RESOURCES_PATH, 'public/login.html');\nconst ASSETS_PATH = path.resolve(RESOURCES_PATH, 'public/assets');\n\nconst PUBLIC_PATH = path.resolve(__dirname, '../build/static');\nconst PUBLIC_ASSETS_PATH = path.resolve(PUBLIC_PATH, 'assets');\n\nconst BUILD_ENV = BUILD_ENVS[process.env.BUILD_ENV];\n\nconst isDev = BUILD_ENV === BUILD_ENVS.dev;\n\nconst config = {\n    mode: BUILD_ENV,\n    target: 'web',\n    context: RESOURCES_PATH,\n    entry: {\n        main: ENTRY_REACT,\n        install: ENTRY_INSTALL,\n        login: ENTRY_LOGIN,\n    },\n    output: {\n        path: PUBLIC_PATH,\n        filename: '[name].[chunkhash].js',\n    },\n    resolve: {\n        modules: ['node_modules'],\n        extensions: ['.js', '.jsx', '.ts', '.tsx'],\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.ya?ml$/,\n                type: 'json',\n                use: 'yaml-loader',\n            },\n            {\n                test: /\\.css$/i,\n                use: [\n                    {\n                        loader: MiniCssExtractPlugin.loader,\n                    },\n                    {\n                        loader: 'css-loader',\n                        options: {\n                            importLoaders: 1,\n                        },\n                    },\n                    {\n                        loader: 'postcss-loader',\n                    },\n                ],\n            },\n            {\n                test: /\\.tsx?$/,\n                exclude: /node_modules/,\n                use: {\n                    loader: 'ts-loader',\n                },\n            },\n        ],\n    },\n    plugins: [\n        new CleanWebpackPlugin({\n            root: PUBLIC_PATH,\n            verbose: false,\n            dry: false,\n        }),\n        new HtmlWebpackPlugin({\n            inject: true,\n            cache: false,\n            chunks: ['main'],\n            template: HTML_PATH,\n        }),\n        new HtmlWebpackPlugin({\n            inject: true,\n            cache: false,\n            chunks: ['install'],\n            filename: 'install.html',\n            template: HTML_INSTALL_PATH,\n        }),\n        new HtmlWebpackPlugin({\n            inject: true,\n            cache: false,\n            chunks: ['login'],\n            filename: 'login.html',\n            template: HTML_LOGIN_PATH,\n        }),\n        new MiniCssExtractPlugin({\n            filename: isDev ? '[name].css' : '[name].[hash].css',\n            chunkFilename: isDev ? '[id].css' : '[id].[hash].css',\n        }),\n        new CopyPlugin({\n            patterns: [\n                {\n                    from: ASSETS_PATH,\n                    to: PUBLIC_ASSETS_PATH,\n                },\n            ],\n        }),\n    ],\n};\n\nexport default config;\n"
  },
  {
    "path": "client/webpack.dev.js",
    "content": "import { merge } from 'webpack-merge';\nimport yaml from 'js-yaml';\nimport fs from 'fs';\nimport { BASE_URL } from './constants.js';\nimport common from './webpack.common.js';\n\nconst ZERO_HOST = '0.0.0.0';\nconst LOCALHOST = '127.0.0.1';\nconst DEFAULT_PORT = 80;\n\n/**\n * Get document, or throw exception on error\n * @returns {{bind_host: string, bind_port: number}}\n */\nconst importConfig = () => {\n    try {\n        const doc = yaml.safeLoad(fs.readFileSync('../AdguardHome.yaml', 'utf8'));\n        const { bind_host, bind_port } = doc;\n        return {\n            bind_host,\n            bind_port,\n        };\n    } catch (e) {\n        console.error(e);\n        return {\n            bind_host: ZERO_HOST,\n            bind_port: DEFAULT_PORT,\n        };\n    }\n};\n\nconst getDevServerConfig = (proxyUrl = BASE_URL) => {\n    const { bind_host: host, bind_port: port } = importConfig();\n    const { DEV_SERVER_PORT } = process.env;\n\n    const devServerHost = host === ZERO_HOST ? LOCALHOST : host;\n    const devServerPort = DEV_SERVER_PORT || port + 8000;\n\n    return {\n        hot: true,\n        open: true,\n        host: devServerHost,\n        port: devServerPort,\n        proxy: {\n            [proxyUrl]: `http://${devServerHost}:${port}`,\n        },\n    };\n};\n\nexport default merge(common, {\n    devtool: 'eval-source-map',\n    ...(process.env.WEBPACK_DEV_SERVER ? { devServer: getDevServerConfig(BASE_URL) } : undefined),\n});\n"
  },
  {
    "path": "client/webpack.prod.js",
    "content": "import { merge } from 'webpack-merge';\nimport common from './webpack.common.js';\n\nexport default merge(common, {\n    stats: 'minimal',\n    performance: {\n        hints: false,\n    },\n});\n"
  },
  {
    "path": "docker/build.Dockerfile",
    "content": "# A docker file for scripts/make/build-docker.sh.\n\nFROM alpine:3.23\n\nARG BUILD_DATE\nARG VERSION\nARG VCS_REF\n\nLABEL \\\n\tmaintainer=\"AdGuard Team <devteam@adguard.com>\" \\\n\torg.opencontainers.image.authors=\"AdGuard Team <devteam@adguard.com>\" \\\n\torg.opencontainers.image.created=$BUILD_DATE \\\n\torg.opencontainers.image.description=\"Network-wide ads & trackers blocking DNS server\" \\\n\torg.opencontainers.image.documentation=\"https://github.com/AdguardTeam/AdGuardHome/wiki/\" \\\n\torg.opencontainers.image.licenses=\"GPL-3.0\" \\\n\torg.opencontainers.image.revision=$VCS_REF \\\n\torg.opencontainers.image.source=\"https://github.com/AdguardTeam/AdGuardHome\" \\\n\torg.opencontainers.image.title=\"AdGuard Home\" \\\n\torg.opencontainers.image.url=\"https://adguard.com/en/adguard-home/overview.html\" \\\n\torg.opencontainers.image.vendor=\"AdGuard\" \\\n\torg.opencontainers.image.version=$VERSION\n\n# Update certificates.\nRUN apk --no-cache add ca-certificates libcap tzdata && \\\n\tmkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \\\n\tchown -R nobody: /opt/adguardhome\n\nARG DIST_DIR\nARG TARGETARCH\nARG TARGETOS\nARG TARGETVARIANT\n\nCOPY \\\n\t--chmod=0755 \\\n\t--chown=nobody:nogroup \\\n\t./${DIST_DIR}/docker/AdGuardHome_${TARGETOS}_${TARGETARCH}_${TARGETVARIANT} \\\n\t/opt/adguardhome/AdGuardHome\n\nRUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome\n\n# 53     : TCP, UDP : DNS\n# 67     :      UDP : DHCP (server)\n# 68     :      UDP : DHCP (client)\n# 80     : TCP      : HTTP (main)\n# 443    : TCP, UDP : HTTPS, DNS-over-HTTPS (incl. HTTP/3), DNSCrypt (main)\n# 853    : TCP, UDP : DNS-over-TLS, DNS-over-QUIC\n# 3000   : TCP, UDP : HTTP(S) (alt, incl. HTTP/3)\n# 5443   : TCP, UDP : DNSCrypt (alt)\n# 6060   : TCP      : HTTP (pprof)\nEXPOSE 53/tcp 53/udp \\\n\t67/udp \\\n\t68/udp \\\n\t80/tcp \\\n\t443/tcp 443/udp \\\n\t853/tcp 853/udp \\\n\t3000/tcp 3000/udp \\\n\t5443/tcp 5443/udp \\\n\t6060/tcp\n\nWORKDIR /opt/adguardhome/work\n\nENTRYPOINT [\"/opt/adguardhome/AdGuardHome\"]\n\nCMD [ \\\n\t\"--no-check-update\", \\\n\t\"-c\", \"/opt/adguardhome/conf/AdGuardHome.yaml\", \\\n\t\"-w\", \"/opt/adguardhome/work\" \\\n]\n"
  },
  {
    "path": "docker/build.Dockerfile.dockerignore",
    "content": "# Ignore everything except for explicitly allowed stuff.\n*\n!dist/docker\n"
  },
  {
    "path": "docker/ci.Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n\n# This comment is used to simplify checking local copies of the Dockerfile.\n# Bump this number every time a significant change is made to this Dockerfile.\n#\n# AdGuard-Project-Version: 11\n\n# Dockerfile guidelines:\n#\n# 1. Make sure that Docker correctly caches layers, on a second build attempt it\n#    must not run lint / test second time when it's not required.\n#\n# 2. Use BuildKit to improve the build performance (--mount=type=cache, etc).\n#\n# 3. Prefer using ARG instead of ENV when appropriate, as ARG does not create a\n#    layer in the final image.  However, be careful with what you use ARG for.\n#    Also, prefer to give ARGs sensible default values.\n#\n# 4. Use --output and the export stage if you need to get any output on the host\n#    machine.\n#\n#    NOTE:  Only use --output with FROM scratch.\n#\n# 5. Use .dockerignore to prevent unnecessary files from being sent to the\n#    Docker daemon, which can invalidate the cache.\n#\n# 6. Add a CACHE_BUSTER argument to stages to be able to rerun the stages if\n#    needed.  Keep it in sync with bamboo-specs/bamboo.yaml.\n\n# NOTE:  Keep in sync with bamboo-specs/bamboo.yaml.\nARG BASE_IMAGE=adguard/go-builder:1.26.1--1\n\n# The dependencies stage is needed to install packages and tool dependencies.\n# This is also where binaries like osslsigncode, which may be required for tests\n# in some projects, must be installed.\n#\n# Use fake BRANCH and REVISION values to both prevent git calls and also not\n# ruin the caching with ARGs.\n#\n# NOTE:  Only ADD the files required to install the dependencies.\nFROM \"$BASE_IMAGE\" AS dependencies\nADD Makefile go.mod go.sum /app/\nADD scripts /app/scripts\nWORKDIR /app\nRUN \\\n\t--mount=type=cache,id=gocache,target=/root/.cache/go-build \\\n\t--mount=type=cache,id=gopath,target=/go \\\n<<-'EOF'\nset -e -f -u -x\nmake \\\n\tBRANCH='master' \\\n\tREVISION='0000000000000000000000000000000000000000' \\\n\tVERBOSE=1 \\\n\tgo-env \\\n\tgo-deps \\\n\t;\nEOF\n\n# The linter stage is separated from the tester stage to make catching test\n# failures easier.\n#\n# Use fake BRANCH and REVISION values to both prevent git calls and also not\n# ruin the caching with ARGs.  IGNORE_NON_REPRODUCIBLE is set to 1 to make this\n# stage reproducible even when linters that query external sources fail.\nFROM dependencies AS linter\nADD . /app\nWORKDIR /app\nRUN \\\n\t--mount=type=cache,id=gocache,target=/root/.cache/go-build \\\n\t--mount=type=cache,id=gopath,target=/go \\\n<<-'EOF'\nset -e -f -u -x\nexport GOMAXPROCS=2\nmake \\\n\tBRANCH='master' \\\n\tIGNORE_NON_REPRODUCIBLE='1' \\\n\tREVISION='0000000000000000000000000000000000000000' \\\n\tVERBOSE=1 \\\n\tgo-lint \\\n\tmd-lint \\\n\tsh-lint \\\n\ttxt-lint \\\n\t;\nEOF\n\n# The test stage.  TEST_REPORTS_DIR is set to create JUnit reports for the\n# tester-exporter stage; run with --build-arg TEST_REPORTS_DIR='' if you don't\n# need them on your machine.\n#\n# Use fake BRANCH and REVISION values to both prevent git calls and also not\n# ruin the caching with ARGs.\n#\n# To run the tests:\n#\n#   docker build --target tester -t 'app' .\n#\n# Projects that have go-bench and/or go-fuzz targets should add them here as\n# well.\nFROM linter AS tester\nARG CACHE_BUSTER=0\nARG TEST_REPORTS_DIR=/test-reports\nRUN \\\n\t--mount=type=cache,id=gocache,target=/root/.cache/go-build \\\n\t--mount=type=cache,id=gopath,target=/go \\\n<<-'EOF'\nset -e -f -u -x\nexport GOMAXPROCS=2\n\nmake \\\n\tBRANCH='master' \\\n\tREVISION='0000000000000000000000000000000000000000' \\\n\tTEST_REPORTS_DIR=\"$TEST_REPORTS_DIR\" \\\n\tVERBOSE=1 \\\n\tgo-test \\\n\t;\n\nexit_code=\"$(cat \"${TEST_REPORTS_DIR}/test-exit-code.txt\")\"\nreadonly exit_code\n\nmake \\\n\tBRANCH='master' \\\n\tREVISION='0000000000000000000000000000000000000000' \\\n\tVERBOSE=1 \\\n\tgo-fuzz \\\n\tgo-bench \\\n\t;\n\nexit \"$exit_code\"\nEOF\n\n# tester-exporter exports the test result to the host machine so that it could\n# parse and analyze it.  This stage should only used in a CI.\n#\n# It the file test-report.xml, which contains test results in the JUnit format.\n#\n# Run the following command to export the test result:\n#\n#   docker build \\\n#\t   --output . \\\n#\t   --progress plain \\\n#\t   --target tester-exporter \\\n#\t   .\nFROM scratch AS tester-exporter\nARG CACHE_BUSTER=0\nARG TEST_REPORTS_DIR=/test-reports\nCOPY --from=tester \"$TEST_REPORTS_DIR\" \"$TEST_REPORTS_DIR\"\n\n# The builder stage is used to build release artifacts.  Real BRANCH and\n# REVISION must be used here.\nFROM dependencies AS builder\nARG ARCH=\"\"\nARG BRANCH=master\nARG CACHE_BUSTER=0\nARG CHANNEL=development\nARG DEPLOY_SCRIPT_PATH=not/a/real/path\nARG GPG_KEY_PASSPHRASE=not-a-real-passphrase\nARG GPG_SECRET_KEY=\"\"\nARG OS=\"\"\nARG REVISION=0000000000000000000000000000000000000000\nARG SIGN=0\nARG SIGNER_API_KEY=not-a-real-key\nARG SOURCE_DATE_EPOCH=0\nARG VERSION=\"\"\nADD . /app\nWORKDIR /app\nRUN \\\n\t--mount=type=cache,id=gocache,target=/root/.cache/go-build \\\n\t--mount=type=cache,id=gopath,target=/go \\\n<<-'EOF'\nset -e -f -u -x\n\n# Import GPG key if provided.\nif [ \"${GPG_SECRET_KEY:-}\" != '' ]; then\n    echo \"$GPG_SECRET_KEY\" | awk '{ gsub(/\\\\n/, \"\\n\"); print; }' | gpg --import --batch --yes\nfi\n\nmake \\\n\tARCH=\"${ARCH}\" \\\n\tBRANCH=\"${BRANCH}\" \\\n\tCHANNEL=\"${CHANNEL}\" \\\n\tDEPLOY_SCRIPT_PATH=\"${DEPLOY_SCRIPT_PATH}\" \\\n\tFRONTEND_PREBUILT=1 \\\n\tGPG_KEY_PASSPHRASE=\"${GPG_KEY_PASSPHRASE}\" \\\n\tOS=\"${OS}\" \\\n\tPARALLELISM=1 \\\n\tREVISION=\"${REVISION}\" \\\n\tSOURCE_DATE_EPOCH=\"$SOURCE_DATE_EPOCH\" \\\n\tSIGN=\"${SIGN}\" \\\n\tSIGNER_API_KEY=\"${SIGNER_API_KEY}\" \\\n\tVERBOSE=2 \\\n\tVERSION=\"${VERSION}\" \\\n\tbuild-release \\\n\t;\nEOF\n\n# builder-exporter exports the build artifacts to the host machine so that they\n# could be published.  This stage should only be used in a CI.\nFROM scratch AS builder-exporter\nARG CACHE_BUSTER=0\nCOPY --from=builder /app/dist /dist\n"
  },
  {
    "path": "docker/ci.Dockerfile.dockerignore",
    "content": "# This comment is used to simplify checking local copies of the file.  Bump this\n# number every time a significant change is made to this file.\n#\n# AdGuard-Project-Version: 3\n.git\n/bin/\n/test-reports/\n/tmp/\n/client/\n"
  },
  {
    "path": "docker/frontend.Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n\n# This comment is used to simplify checking local copies of the Dockerfile.\n# Bump this number every time a significant change is made to this Dockerfile.\n#\n# AdGuard-Project-Version: 11\n\n# Dockerfile guidelines:\n#\n# 1. Make sure that Docker correctly caches layers, on a second build attempt it\n#    must not run lint / test second time when it's not required.\n#\n# 2. Use BuildKit to improve the build performance (--mount=type=cache, etc).\n#\n# 3. Prefer using ARG instead of ENV when appropriate, as ARG does not create a\n#    layer in the final image.  However, be careful with what you use ARG for.\n#    Also, prefer to give ARGs sensible default values.\n#\n# 4. Use --output and the export stage if you need to get any output on the host\n#    machine.\n#\n#    NOTE:  Only use --output with FROM scratch.\n#\n# 5. Use .dockerignore to prevent unnecessary files from being sent to the\n#    Docker daemon, which can invalidate the cache.\n#\n# 6. Add a CACHE_BUSTER argument to stages to be able to rerun the stages if\n#    needed.  Keep it in sync with bamboo-specs/bamboo.yaml.\n\n# NOTE:  Keep in sync with bamboo-specs/bamboo.yaml.\nARG BASE_IMAGE=adguard/home-js-builder:4.0\n\n# The dependencies stage is needed to install packages and tool dependencies.\n# This is also where binaries like osslsigncode, which may be required for tests\n# in some projects, must be installed.\n#\n# NOTE:  Only ADD the files required to install the dependencies.\nFROM \"$BASE_IMAGE\" AS dependencies\nADD Makefile /app/\nADD scripts /app/scripts\nADD client /app/client\nWORKDIR /app\nRUN \\\n    --mount=type=cache,id=npm-root-cache,target=/root/.npm \\\n<<-'EOF'\nset -e -f -u -x\nmake \\\n\tVERBOSE=1 \\\n\tjs-deps \\\n\t;\nEOF\n\n# The linter stage is separated from the tester stage to make catching test\n# failures easier.\nFROM dependencies AS linter\nARG CACHE_BUSTER=0\nADD . /app\nWORKDIR /app\nRUN \\\n    --mount=type=cache,id=npm-root-cache,target=/root/.npm \\\n<<-'EOF'\nset -e -f -u -x\nmake \\\n\tVERBOSE=1 \\\n\tjs-typecheck \\\n\tjs-lint \\\n\t;\nEOF\n\n# The test stage.\nFROM linter AS tester\nARG CACHE_BUSTER=0\nRUN \\\n    --mount=type=cache,id=npm-root-cache,target=/root/.npm \\\n<<-'EOF'\nset -e -f -u -x\nmake \\\n\tVERBOSE=1 \\\n\tjs-test \\\n\t;\nEOF\n\n# The e2e test stage.\nFROM dependencies AS e2etester\nARG CACHE_BUSTER=0\nADD . /app\nWORKDIR /app\nRUN \\\n    --mount=type=cache,id=npm-root-cache,target=/root/.npm \\\n<<-'EOF'\nset -e -f -u -x\nmake \\\n\tCI='true' \\\n\tVERBOSE=1 \\\n\tjs-test-e2e \\\n\t;\nEOF\n\n# The builder stage.\nFROM dependencies AS builder\nARG CACHE_BUSTER=0\nADD . /app\nWORKDIR /app\nRUN \\\n    --mount=type=cache,id=npm-root-cache,target=/root/.npm \\\n<<-'EOF'\nset -e -f -u -x\nmake \\\n\tVERBOSE=1 \\\n\tjs-build \\\n\t;\nEOF\n\n# builder-exporter exports the build artifacts to the host machine so that they\n# could be published.  This stage should only be used in a CI.\nFROM scratch AS builder-exporter\nARG CACHE_BUSTER=0\nCOPY --from=builder /app/build /build\n"
  },
  {
    "path": "docker/frontend.Dockerfile.dockerignore",
    "content": "# This comment is used to simplify checking local copies of the file.  Bump this\n# number every time a significant change is made to this file.\n#\n# AdGuard-Project-Version: 2\n.git\n/bin/\n/test-reports/\n/tmp/\n/client/node_modules\n"
  },
  {
    "path": "docker/snapcraft.Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n\n# This comment is used to simplify checking local copies of the Dockerfile.\n# Bump this number every time a significant change is made to this Dockerfile.\n#\n# AdGuard-Project-Version: 11\n\n# Dockerfile guidelines:\n#\n# 1. Make sure that Docker correctly caches layers, on a second build attempt it\n#    must not run lint / test second time when it's not required.\n#\n# 2. Use BuildKit to improve the build performance (--mount=type=cache, etc).\n#\n# 3. Prefer using ARG instead of ENV when appropriate, as ARG does not create a\n#    layer in the final image.  However, be careful with what you use ARG for.\n#    Also, prefer to give ARGs sensible default values.\n#\n# 4. Use --output and the export stage if you need to get any output on the host\n#    machine.\n#\n#    NOTE:  Only use --output with FROM scratch.\n#\n# 5. Use .dockerignore to prevent unnecessary files from being sent to the\n#    Docker daemon, which can invalidate the cache.\n#\n# 6. Add a CACHE_BUSTER argument to stages to be able to rerun the stages if\n#    needed.  Keep it in sync with bamboo-specs/snapcraft.yaml.\n\n# NOTE:  Keep in sync with bamboo-specs/snapcraft.yaml.\nARG BASE_IMAGE=adguard/snap-builder:2.1\n\n# builder downloads the release artifacts and builds snap artifacts.\nFROM \"$BASE_IMAGE\" AS builder\nARG CACHE_BUSTER=0\nARG CHANNEL=development\nARG VERSION=\"\"\nADD snap /app/snap\nADD scripts /app/scripts\nWORKDIR /app\nRUN \\\n<<-'EOF'\nset -e -f -u -x\n\nexport VERBOSE='1'\n\nenv \\\n\tCHANNEL=\"${CHANNEL}\" \\\n\tsh ./scripts/snap/download.sh \\\n\t;\n\nsh ./scripts/snap/build.sh\nEOF\n\n# builder-exporter exports the build artifacts to the host machine so that they\n# could be published.  This stage should only be used in a CI.\nFROM scratch AS builder-exporter\nARG CACHE_BUSTER=0\nARG VERSION=\"\"\nCOPY --from=builder /app/AdGuardHome_amd64.snap /AdGuardHome_amd64.snap\nCOPY --from=builder /app/AdGuardHome_arm64.snap /AdGuardHome_arm64.snap\nCOPY --from=builder /app/AdGuardHome_armhf.snap /AdGuardHome_armhf.snap\nCOPY --from=builder /app/AdGuardHome_i386.snap /AdGuardHome_i386.snap\n\n# publisher uploads the release artifacts to the Snap Store.\nFROM \"$BASE_IMAGE\" AS publisher\nARG CACHE_BUSTER=0\nARG SNAPCRAFT_CHANNEL=0\nARG SNAPCRAFT_STORE_CREDENTIALS=0\nARG VERSION=\"\"\nADD snap /app/snap\nADD scripts /app/scripts\nADD AdGuardHome_amd64.snap /app/AdGuardHome_amd64.snap\nADD AdGuardHome_arm64.snap /app/AdGuardHome_arm64.snap\nADD AdGuardHome_armhf.snap /app/AdGuardHome_armhf.snap\nADD AdGuardHome_i386.snap /app/AdGuardHome_i386.snap\nWORKDIR /app\nRUN \\\n<<-'EOF'\nset -e -f -u -x\n\nenv \\\n\tSNAPCRAFT_CHANNEL=\"${SNAPCRAFT_CHANNEL}\" \\\n\tSNAPCRAFT_STORE_CREDENTIALS=\"${SNAPCRAFT_STORE_CREDENTIALS}\" \\\n\tVERBOSE='1' \\\n\tsh ./scripts/snap/upload.sh\nEOF\n"
  },
  {
    "path": "docker/snapcraft.Dockerfile.dockerignore",
    "content": "# This comment is used to simplify checking local copies of the file.  Bump this\n# number every time a significant change is made to this file.\n#\n# AdGuard-Project-Version: 2\n.git\n/bin/\n/tmp/\n/client/\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/AdguardTeam/AdGuardHome\n\ngo 1.26.1\n\nrequire (\n\tgithub.com/AdguardTeam/dnsproxy v0.81.0\n\tgithub.com/AdguardTeam/golibs v0.35.8\n\tgithub.com/AdguardTeam/urlfilter v0.23.2\n\tgithub.com/NYTimes/gziphandler v1.1.1\n\tgithub.com/ameshkov/dnscrypt/v2 v2.4.0\n\tgithub.com/bluele/gcache v0.0.2\n\tgithub.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500\n\tgithub.com/digineo/go-ipset/v2 v2.2.1\n\t// TODO(e.burkov): Update to the latest version when\n\t// github.com/fsnotify/fsnotify/issues/727 is fixed.\n\tgithub.com/fsnotify/fsnotify v1.8.0\n\t// TODO(e.burkov): This package is deprecated; find a new one or use our\n\t// own code for that.  Perhaps, use gopacket.\n\tgithub.com/go-ping/ping v1.2.0\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/gopacket v1.1.19\n\tgithub.com/google/renameio/v2 v2.0.2\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167\n\tgithub.com/kardianos/service v1.2.4\n\tgithub.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118\n\tgithub.com/mdlayher/netlink v1.8.0\n\tgithub.com/mdlayher/packet v1.1.2\n\t// TODO(a.garipov): This package is deprecated; find a new one or use our\n\t// own code for that.  Perhaps, use gopacket.\n\tgithub.com/mdlayher/raw v0.1.0\n\tgithub.com/miekg/dns v1.1.72\n\tgithub.com/quic-go/quic-go v0.59.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/ti-mo/netfilter v0.5.3\n\tgo.etcd.io/bbolt v1.4.3\n\t// TODO(e.burkov): Update to the latest version and test unmarshaling\n\t// structures with *time.Time.\n\tgo.yaml.in/yaml/v4 v4.0.0-rc.3\n\tgolang.org/x/crypto v0.49.0\n\tgolang.org/x/exp v0.0.0-20260209203927-2842357ff358\n\tgolang.org/x/net v0.52.0\n\tgolang.org/x/sys v0.42.0\n\tgopkg.in/natefinch/lumberjack.v2 v2.2.1\n\thowett.net/plist v1.0.1\n)\n\nrequire (\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.18.2 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tgithub.com/BurntSushi/toml v1.6.0 // indirect\n\tgithub.com/ameshkov/dnsstamps v1.0.3 // indirect\n\tgithub.com/anthropics/anthropic-sdk-go v1.26.0 // indirect\n\tgithub.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect\n\tgithub.com/ccojocar/zxcvbn-go v1.0.4 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fzipp/gocyclo v0.6.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golangci/misspell v0.8.0 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.18.0 // indirect\n\tgithub.com/gookit/color v1.6.0 // indirect\n\tgithub.com/gordonklaus/ineffassign v0.2.0 // indirect\n\tgithub.com/gorilla/websocket v1.5.3 // indirect\n\tgithub.com/josharian/native v1.1.0 // indirect\n\tgithub.com/jstemmer/go-junit-report/v2 v2.1.0 // indirect\n\tgithub.com/kisielk/errcheck v1.10.0 // indirect\n\tgithub.com/mdlayher/socket v0.5.1 // indirect\n\tgithub.com/openai/openai-go/v3 v3.26.0 // indirect\n\tgithub.com/patrickmn/go-cache v2.1.0+incompatible // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.25 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/quic-go/qpack v0.6.0 // indirect\n\tgithub.com/robfig/cron/v3 v3.0.1 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/securego/gosec/v2 v2.24.7 // indirect\n\tgithub.com/tidwall/gjson v1.18.0 // indirect\n\tgithub.com/tidwall/match v1.2.0 // indirect\n\tgithub.com/tidwall/pretty v1.2.1 // indirect\n\tgithub.com/tidwall/sjson v1.2.5 // indirect\n\tgithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect\n\tgithub.com/uudashr/gocognit v1.2.1 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect\n\tgo.opentelemetry.io/otel v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.42.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/exp/typeparams v0.0.0-20260312153236-7ab1446f8b90 // indirect\n\tgolang.org/x/mod v0.34.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/telemetry v0.0.0-20260312161427-1546bf4b83fe // indirect\n\tgolang.org/x/term v0.41.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/tools v0.43.0 // indirect\n\tgolang.org/x/vuln v1.1.4 // indirect\n\tgonum.org/v1/gonum v0.17.0 // indirect\n\tgoogle.golang.org/genai v1.50.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect\n\tgoogle.golang.org/grpc v1.79.2 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\thonnef.co/go/tools v0.7.0 // indirect\n\tmvdan.cc/editorconfig v0.3.0 // indirect\n\tmvdan.cc/gofumpt v0.9.2 // indirect\n\tmvdan.cc/sh/v3 v3.13.0 // indirect\n\tmvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect\n)\n\n// NOTE:  Keep in sync with .gitignore.\nignore (\n\t./agh-backup\n\t./bin\n\t./build\n\t./client\n\t./data\n\t./dist\n\t./test-reports\n\t./tmp\n\tnode_modules\n)\n\ntool (\n\tgithub.com/fzipp/gocyclo/cmd/gocyclo\n\tgithub.com/golangci/misspell/cmd/misspell\n\tgithub.com/gordonklaus/ineffassign\n\tgithub.com/jstemmer/go-junit-report/v2\n\tgithub.com/kisielk/errcheck\n\tgithub.com/securego/gosec/v2/cmd/gosec\n\tgithub.com/uudashr/gocognit/cmd/gocognit\n\tgolang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment\n\tgolang.org/x/tools/go/analysis/passes/nilness/cmd/nilness\n\tgolang.org/x/tools/go/analysis/passes/shadow/cmd/shadow\n\tgolang.org/x/vuln/cmd/govulncheck\n\thonnef.co/go/tools/cmd/staticcheck\n\tmvdan.cc/gofumpt\n\tmvdan.cc/sh/v3/cmd/shfmt\n\tmvdan.cc/unparam\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ngithub.com/AdguardTeam/dnsproxy v0.81.0 h1:derWNPHd25PbQ2eSpEpg/dw7d7VRA4dNaP5GdG+qEJ8=\ngithub.com/AdguardTeam/dnsproxy v0.81.0/go.mod h1:gwr+7Dc0e7QddQLC9JLGjL5NSKcqw0ESsNMRI5Q67Ps=\ngithub.com/AdguardTeam/golibs v0.35.8 h1:KsyF3SWwj05Ey4GiAWU6FGD9oJTDNMp1ixVdS+Nw50M=\ngithub.com/AdguardTeam/golibs v0.35.8/go.mod h1:kuLQ0yNRTl0Em2FmmXtSri7ZdVT7p62oojyc51RvP38=\ngithub.com/AdguardTeam/urlfilter v0.23.2 h1:EiS/PQZO/X2S6cduFW6BBoRLyjd6SqZj1ZiFbU1KaFE=\ngithub.com/AdguardTeam/urlfilter v0.23.2/go.mod h1:JteAKoeka1Yr2oZ3P94dqYBfPOHWyFaOcu3uZa9Yl+I=\ngithub.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=\ngithub.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=\ngithub.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o=\ngithub.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=\ngithub.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=\ngithub.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=\ngithub.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=\ngithub.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=\ngithub.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=\ngithub.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=\ngithub.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=\ngithub.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=\ngithub.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4=\ngithub.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=\ngithub.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=\ngithub.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=\ngithub.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=\ngithub.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=\ngithub.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=\ngithub.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=\ngithub.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ=\ngithub.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=\ngithub.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=\ngithub.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=\ngithub.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=\ngithub.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=\ngithub.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=\ngithub.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=\ngithub.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw=\ngithub.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=\ngithub.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=\ngithub.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=\ngithub.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=\ngithub.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=\ngithub.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=\ngithub.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=\ngithub.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=\ngithub.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=\ngithub.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 h1:MEufgJohwIjFi2n3eJv4c/8UdRLQVUwPwSWQPoER+eU=\ngithub.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=\ngithub.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=\ngithub.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=\ngithub.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=\ngithub.com/jstemmer/go-junit-report/v2 v2.1.0 h1:X3+hPYlSczH9IMIpSC9CQSZA0L+BipYafciZUWHEmsc=\ngithub.com/jstemmer/go-junit-report/v2 v2.1.0/go.mod h1:mgHVr7VUo5Tn8OLVr1cKnLuEy0M92wdRntM99h7RkgQ=\ngithub.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=\ngithub.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=\ngithub.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw=\ngithub.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE=\ngithub.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118/go.mod h1:ZFUnHIVchZ9lJoWoEGUg8Q3M4U8aNNWA3CVSUTkW4og=\ngithub.com/mdlayher/netlink v0.0.0-20190313131330-258ea9dff42c/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=\ngithub.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM=\ngithub.com/mdlayher/netlink v1.8.0/go.mod h1:UhgKXUlDQhzb09DrCl2GuRNEglHmhYoWAHid9HK3594=\ngithub.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU=\ngithub.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=\ngithub.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=\ngithub.com/mdlayher/raw v0.1.0 h1:K4PFMVy+AFsp0Zdlrts7yNhxc/uXoPVHi9RzRvtZF2Y=\ngithub.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5s9Sg=\ngithub.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=\ngithub.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=\ngithub.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=\ngithub.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=\ngithub.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=\ngithub.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=\ngithub.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE=\ngithub.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=\ngithub.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=\ngithub.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=\ngithub.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=\ngithub.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=\ngithub.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=\ngithub.com/securego/gosec/v2 v2.24.7 h1:3k5yJnrhT1TTdsG0ZsnenlfCcT+7Y/+zeCPHbL7QAn8=\ngithub.com/securego/gosec/v2 v2.24.7/go.mod h1:AdDJbjcG/XxFgVv7pW19vMNYlFM6+Q6Qy3t6lWAUcEY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=\ngithub.com/ti-mo/netfilter v0.5.3 h1:ikzduvnaUMwre5bhbNwWOd6bjqLMVb33vv0XXbK0xGQ=\ngithub.com/ti-mo/netfilter v0.5.3/go.mod h1:08SyBCg6hu1qyQk4s3DjjJKNrm3RTb32nm6AzyT972E=\ngithub.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=\ngithub.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=\ngithub.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=\ngithub.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=\ngithub.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=\ngo.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=\ngo.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=\ngo.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=\ngo.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=\ngo.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=\ngo.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=\ngo.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=\ngo.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngo.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=\ngo.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c=\ngolang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8=\ngolang.org/x/exp/typeparams v0.0.0-20260312153236-7ab1446f8b90 h1:cfW8UCYSVdPblxA7qQe3o5Iad55Vsx4BFmuGS9RNOmc=\ngolang.org/x/exp/typeparams v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/telemetry v0.0.0-20260312161427-1546bf4b83fe h1:MaXjBsxue6l0hflXDwJ/XBfUJRjiyX1PwLd7F3lYDXA=\ngolang.org/x/telemetry v0.0.0-20260312161427-1546bf4b83fe/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngolang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=\ngolang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=\ngolang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=\ngolang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=\ngolang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=\ngolang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=\ngonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=\ngoogle.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk=\ngoogle.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=\ngoogle.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=\ngopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=\nhonnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=\nhowett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=\nhowett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=\nmvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=\nmvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=\nmvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4=\nmvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s=\nmvdan.cc/sh/v3 v3.13.0 h1:dSfq/MVsY4w0Vsi6Lbs0IcQquMVqLdKLESAOZjuHdLg=\nmvdan.cc/sh/v3 v3.13.0/go.mod h1:KV1GByGPc/Ho0X1E6Uz9euhsIQEj4hwyKnodLlFLoDM=\nmvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI=\nmvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU=\n"
  },
  {
    "path": "internal/agh/agh.go",
    "content": "// Package agh contains common entities and interfaces of AdGuard Home.\npackage agh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/AdguardTeam/golibs/testutil/fakeos/fakeexec\"\n)\n\n// DefaultOutputLimit is the default limit of bytes for commands' standard\n// output and standard error.\nconst DefaultOutputLimit = 512\n\n// ConfigModifier defines an interface for updating the global configuration.\ntype ConfigModifier interface {\n\t// Apply applies changes to the global configuration.\n\tApply(ctx context.Context)\n}\n\n// EmptyConfigModifier is an empty [ConfigModifier] implementation that does\n// nothing.\ntype EmptyConfigModifier struct{}\n\n// type check\nvar _ ConfigModifier = EmptyConfigModifier{}\n\n// Apply implements the [ConfigModifier] for EmptyConfigModifier.\nfunc (em EmptyConfigModifier) Apply(ctx context.Context) {}\n\n// exitErr implements [executil.ExitCodeError] for tests to simulate non-zero\n// process exit codes.\n//\n// TODO(s.chzhen):  Consider constructing an [exec.ExitError] instead.\ntype exitErr struct {\n\tcode osutil.ExitCode\n}\n\n// newExitErr returns a properly initialized exitErr with the provided code.\nfunc newExitErr(code osutil.ExitCode) (err exitErr) {\n\treturn exitErr{code: code}\n}\n\n// type check\nvar _ executil.ExitCodeError = exitErr{}\n\n// Error implements the [executil.ExitCodeError] for exitErr.\nfunc (e exitErr) Error() (s string) {\n\treturn fmt.Sprintf(\"exit code %d\", e.code)\n}\n\n// ExitCode implements the [executil.ExitCodeError] for exitErr.\nfunc (e exitErr) ExitCode() (code osutil.ExitCode) {\n\treturn e.code\n}\n\n// ExternalCommand is a fake command used by [NewMultipleCommandConstructor].\ntype ExternalCommand struct {\n\t// Err is the error returned, if non-nil.\n\tErr error\n\n\t// Cmd contains the command path and arguments.\n\tCmd string\n\n\t// Out is written to stdout if non-empty.\n\tOut string\n\n\t// Code is returned as the exit code if non-zero.\n\tCode osutil.ExitCode\n}\n\n// keyCommand builds a key for a command lookup.\nfunc keyCommand(path string, args []string) (k string) {\n\tif len(args) == 0 {\n\t\treturn path\n\t}\n\n\treturn path + \" \" + strings.Join(args, \" \")\n}\n\n// parseCommand splits a command string into the executable path and args.\nfunc parseCommand(s string) (path string, args []string) {\n\tf := strings.Fields(s)\n\tif len(f) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\treturn f[0], f[1:]\n}\n\n// NewMultipleCommandConstructor is a helper function that returns a mock\n// [executil.CommandConstructor] for tests that supports multiple commands.\n//\n// TODO(s.chzhen):  Move to aghtest once the import cycle is resolved, since it\n// will be called from the aghnet package, which imports the whois package,\n// which in turn imports aghnet.\nfunc NewMultipleCommandConstructor(cmds ...ExternalCommand) (cs executil.CommandConstructor) {\n\ttable := make(map[string]ExternalCommand, len(cmds))\n\tfor _, ec := range cmds {\n\t\tp, a := parseCommand(ec.Cmd)\n\t\ttable[keyCommand(p, a)] = ec\n\t}\n\n\tonNew := func(_ context.Context, conf *executil.CommandConfig) (c executil.Command, err error) {\n\t\tec := table[keyCommand(conf.Path, conf.Args)]\n\n\t\tcmd := fakeexec.NewCommand()\n\t\tcmd.OnStart = func(_ context.Context) (err error) {\n\t\t\tif ec.Out != \"\" {\n\t\t\t\t_, _ = conf.Stdout.Write([]byte(ec.Out))\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\tcmd.OnWait = func(_ context.Context) (err error) {\n\t\t\tif ec.Err != nil {\n\t\t\t\treturn ec.Err\n\t\t\t}\n\n\t\t\tif ec.Code != 0 {\n\t\t\t\treturn newExitErr(ec.Code)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn cmd, nil\n\t}\n\n\treturn &fakeexec.CommandConstructor{OnNew: onNew}\n}\n\n// NewCommandConstructor is a helper function that returns a mock\n// [executil.CommandConstructor] for tests.\nfunc NewCommandConstructor(\n\t_ string,\n\tcode osutil.ExitCode,\n\tstdout string,\n\tcmdErr error,\n) (cs executil.CommandConstructor) {\n\tonNew := func(_ context.Context, conf *executil.CommandConfig) (c executil.Command, err error) {\n\t\tcmd := fakeexec.NewCommand()\n\t\tcmd.OnStart = func(_ context.Context) (err error) {\n\t\t\tif conf.Stdout != nil {\n\t\t\t\t_, _ = conf.Stdout.Write([]byte(stdout))\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\tcmd.OnWait = func(_ context.Context) (err error) {\n\t\t\tif cmdErr != nil {\n\t\t\t\treturn cmdErr\n\t\t\t}\n\n\t\t\tif code != 0 {\n\t\t\t\treturn newExitErr(code)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn cmd, nil\n\t}\n\n\treturn &fakeexec.CommandConstructor{OnNew: onNew}\n}\n"
  },
  {
    "path": "internal/aghalg/aghalg.go",
    "content": "// Package aghalg contains common generic algorithms and data structures.\n//\n// TODO(a.garipov): Move parts of this into golibs.\npackage aghalg\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\n\t\"golang.org/x/exp/constraints\"\n)\n\n// CoalesceSlice returns the first non-zero value.  It is named after function\n// COALESCE in SQL.  If values or all its elements are empty, it returns nil.\nfunc CoalesceSlice[E any, S []E](values ...S) (res S) {\n\tfor _, v := range values {\n\t\tif v != nil {\n\t\t\treturn v\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// UniqChecker allows validating uniqueness of comparable items.\n//\n// TODO(a.garipov): The Ordered constraint is only really necessary in Validate.\n// Consider ways of making this constraint comparable instead.\ntype UniqChecker[T constraints.Ordered] map[T]int64\n\n// Add adds a value to the validator.  v must not be nil.\nfunc (uc UniqChecker[T]) Add(elems ...T) {\n\tfor _, e := range elems {\n\t\tuc[e]++\n\t}\n}\n\n// Merge returns a checker containing data from both uc and other.\nfunc (uc UniqChecker[T]) Merge(other UniqChecker[T]) (merged UniqChecker[T]) {\n\tmerged = make(UniqChecker[T], len(uc)+len(other))\n\tfor elem, num := range uc {\n\t\tmerged[elem] += num\n\t}\n\n\tfor elem, num := range other {\n\t\tmerged[elem] += num\n\t}\n\n\treturn merged\n}\n\n// Validate returns an error enumerating all elements that aren't unique.\nfunc (uc UniqChecker[T]) Validate() (err error) {\n\tvar dup []T\n\tfor elem, num := range uc {\n\t\tif num > 1 {\n\t\t\tdup = append(dup, elem)\n\t\t}\n\t}\n\n\tif len(dup) == 0 {\n\t\treturn nil\n\t}\n\n\tslices.Sort(dup)\n\n\treturn fmt.Errorf(\"duplicated values: %v\", dup)\n}\n"
  },
  {
    "path": "internal/aghalg/nullbool.go",
    "content": "package aghalg\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/AdguardTeam/golibs/mathutil\"\n)\n\n// NullBool is a nullable boolean.  Use these in JSON requests and responses\n// instead of pointers to bool.\ntype NullBool uint8\n\n// NullBool values\nconst (\n\tNBNull NullBool = iota\n\tNBTrue\n\tNBFalse\n)\n\n// String implements the fmt.Stringer interface for NullBool.\nfunc (nb NullBool) String() (s string) {\n\tswitch nb {\n\tcase NBNull:\n\t\treturn \"null\"\n\tcase NBTrue:\n\t\treturn \"true\"\n\tcase NBFalse:\n\t\treturn \"false\"\n\t}\n\n\treturn fmt.Sprintf(\"!invalid NullBool %d\", uint8(nb))\n}\n\n// BoolToNullBool converts a bool into a NullBool.\nfunc BoolToNullBool(cond bool) (nb NullBool) {\n\treturn NBFalse - mathutil.BoolToNumber[NullBool](cond)\n}\n\n// type check\nvar _ json.Marshaler = NBNull\n\n// MarshalJSON implements the json.Marshaler interface for NullBool.\nfunc (nb NullBool) MarshalJSON() (b []byte, err error) {\n\treturn []byte(nb.String()), nil\n}\n\n// type check\nvar _ json.Unmarshaler = (*NullBool)(nil)\n\n// UnmarshalJSON implements the json.Unmarshaler interface for *NullBool.\nfunc (nb *NullBool) UnmarshalJSON(b []byte) (err error) {\n\tif len(b) == 0 || bytes.Equal(b, []byte(\"null\")) {\n\t\t*nb = NBNull\n\t} else if bytes.Equal(b, []byte(\"true\")) {\n\t\t*nb = NBTrue\n\t} else if bytes.Equal(b, []byte(\"false\")) {\n\t\t*nb = NBFalse\n\t} else {\n\t\treturn fmt.Errorf(\"unmarshalling json data into aghalg.NullBool: bad value %q\", b)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghalg/nullbool_test.go",
    "content": "package aghalg_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNullBool_MarshalJSON(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\twant       []byte\n\t\tin         aghalg.NullBool\n\t}{{\n\t\tname:       \"null\",\n\t\twantErrMsg: \"\",\n\t\twant:       []byte(\"null\"),\n\t\tin:         aghalg.NBNull,\n\t}, {\n\t\tname:       \"true\",\n\t\twantErrMsg: \"\",\n\t\twant:       []byte(\"true\"),\n\t\tin:         aghalg.NBTrue,\n\t}, {\n\t\tname:       \"false\",\n\t\twantErrMsg: \"\",\n\t\twant:       []byte(\"false\"),\n\t\tin:         aghalg.NBFalse,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := tc.in.MarshalJSON()\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n\n\tt.Run(\"json\", func(t *testing.T) {\n\t\tin := &struct {\n\t\t\tA aghalg.NullBool\n\t\t}{\n\t\t\tA: aghalg.NBTrue,\n\t\t}\n\n\t\tgot, err := json.Marshal(in)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, []byte(`{\"A\":true}`), got)\n\t})\n}\n\nfunc TestNullBool_UnmarshalJSON(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\tdata       []byte\n\t\twant       aghalg.NullBool\n\t}{{\n\t\tname:       \"empty\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte{},\n\t\twant:       aghalg.NBNull,\n\t}, {\n\t\tname:       \"null\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte(\"null\"),\n\t\twant:       aghalg.NBNull,\n\t}, {\n\t\tname:       \"true\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte(\"true\"),\n\t\twant:       aghalg.NBTrue,\n\t}, {\n\t\tname:       \"false\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte(\"false\"),\n\t\twant:       aghalg.NBFalse,\n\t}, {\n\t\tname:       \"invalid\",\n\t\twantErrMsg: `unmarshalling json data into aghalg.NullBool: bad value \"invalid\"`,\n\t\tdata:       []byte(\"invalid\"),\n\t\twant:       aghalg.NBNull,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar got aghalg.NullBool\n\t\t\terr := got.UnmarshalJSON(tc.data)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n\n\tt.Run(\"json\", func(t *testing.T) {\n\t\twant := aghalg.NBTrue\n\t\tvar got struct {\n\t\t\tA aghalg.NullBool\n\t\t}\n\n\t\terr := json.Unmarshal([]byte(`{\"A\":true}`), &got)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, want, got.A)\n\t})\n}\n"
  },
  {
    "path": "internal/aghalg/sortedmap.go",
    "content": "package aghalg\n\nimport (\n\t\"cmp\"\n\t\"slices\"\n)\n\n// SortedMap is a map that keeps elements in order with internal sorting\n// function.  It must be initialized with [NewSortedMap] or [NewSortedMapFunc].\ntype SortedMap[K comparable, V any] struct {\n\tvals map[K]V\n\tcmp  func(a, b K) (res int)\n\tkeys []K\n}\n\n// NewSortedMap initializes a new instance of sorted map.\nfunc NewSortedMap[K cmp.Ordered, V any]() (m *SortedMap[K, V]) {\n\treturn NewSortedMapFunc[K, V](cmp.Compare[K])\n}\n\n// NewSortedMapFunc initializes a new instance of sorted map.  cmpFunc is a\n// comparison function to keep elements in order.  cmpFunc must not be nil.\nfunc NewSortedMapFunc[K comparable, V any](cmpFunc func(a, b K) (res int)) (m *SortedMap[K, V]) {\n\treturn &SortedMap[K, V]{\n\t\tvals: map[K]V{},\n\t\tcmp:  cmpFunc,\n\t}\n}\n\n// Set adds val with key to the sorted map.  It panics if the m is nil.\nfunc (m *SortedMap[K, V]) Set(key K, val V) {\n\tm.vals[key] = val\n\n\ti, has := slices.BinarySearchFunc(m.keys, key, m.cmp)\n\tif has {\n\t\tm.keys[i] = key\n\t} else {\n\t\tm.keys = slices.Insert(m.keys, i, key)\n\t}\n}\n\n// Get returns val by key from the sorted map.\nfunc (m *SortedMap[K, V]) Get(key K) (val V, ok bool) {\n\tif m == nil {\n\t\tvar zero V\n\n\t\treturn zero, false\n\t}\n\n\tval, ok = m.vals[key]\n\n\treturn val, ok\n}\n\n// Del removes the value by key from the sorted map.\nfunc (m *SortedMap[K, V]) Del(key K) {\n\tif m == nil {\n\t\treturn\n\t}\n\n\tif _, has := m.vals[key]; !has {\n\t\treturn\n\t}\n\n\tdelete(m.vals, key)\n\ti, _ := slices.BinarySearchFunc(m.keys, key, m.cmp)\n\tm.keys = slices.Delete(m.keys, i, i+1)\n}\n\n// Clear removes all elements from the sorted map.\nfunc (m *SortedMap[K, V]) Clear() {\n\tif m == nil {\n\t\treturn\n\t}\n\n\tm.keys = m.keys[:0]\n\tclear(m.vals)\n}\n\n// Range calls cb for each element of the map, sorted by m.cmp.  If cb returns\n// false it stops.\nfunc (m *SortedMap[K, V]) Range(cb func(K, V) (cont bool)) {\n\tif m == nil {\n\t\treturn\n\t}\n\n\tfor _, k := range m.keys {\n\t\tif !cb(k, m.vals[k]) {\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/aghalg/sortedmap_test.go",
    "content": "package aghalg_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewSortedMap(t *testing.T) {\n\tvar m *aghalg.SortedMap[string, int]\n\n\tletters := []string{}\n\tfor i := range 10 {\n\t\tr := string('a' + rune(i))\n\t\tletters = append(letters, r)\n\t}\n\n\tt.Run(\"create_and_fill\", func(t *testing.T) {\n\t\tm = aghalg.NewSortedMap[string, int]()\n\n\t\tnums := []int{}\n\t\tfor i, r := range letters {\n\t\t\tm.Set(r, i)\n\t\t\tnums = append(nums, i)\n\t\t}\n\n\t\tgotLetters := []string{}\n\t\tgotNums := []int{}\n\t\tm.Range(func(k string, v int) bool {\n\t\t\tgotLetters = append(gotLetters, k)\n\t\t\tgotNums = append(gotNums, v)\n\n\t\t\treturn true\n\t\t})\n\n\t\tassert.Equal(t, letters, gotLetters)\n\t\tassert.Equal(t, nums, gotNums)\n\n\t\tn, ok := m.Get(letters[0])\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, nums[0], n)\n\t})\n\n\tt.Run(\"clear\", func(t *testing.T) {\n\t\tlastLetter := letters[len(letters)-1]\n\t\tm.Del(lastLetter)\n\n\t\t_, ok := m.Get(lastLetter)\n\t\tassert.False(t, ok)\n\n\t\tm.Clear()\n\n\t\tgotLetters := []string{}\n\t\tm.Range(func(k string, _ int) bool {\n\t\t\tgotLetters = append(gotLetters, k)\n\n\t\t\treturn true\n\t\t})\n\n\t\tassert.Len(t, gotLetters, 0)\n\t})\n}\n\nfunc TestNewSortedMap_nil(t *testing.T) {\n\tconst (\n\t\tkey = \"key\"\n\t\tval = \"val\"\n\t)\n\n\tvar m aghalg.SortedMap[string, string]\n\n\tassert.Panics(t, func() {\n\t\tm.Set(key, val)\n\t})\n\n\tassert.NotPanics(t, func() {\n\t\t_, ok := m.Get(key)\n\t\tassert.False(t, ok)\n\t})\n\n\tassert.NotPanics(t, func() {\n\t\tm.Range(func(_, _ string) (cont bool) {\n\t\t\treturn true\n\t\t})\n\t})\n\n\tassert.NotPanics(t, func() {\n\t\tm.Del(key)\n\t})\n\n\tassert.NotPanics(t, func() {\n\t\tm.Clear()\n\t})\n}\n"
  },
  {
    "path": "internal/aghhttp/aghhttp.go",
    "content": "// Package aghhttp provides some common methods to work with HTTP.\npackage aghhttp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// RegisterFunc is the function that sets the handler to handle the URL for the\n// method.\n//\n// TODO(e.burkov, a.garipov):  Get rid of it.\ntype RegisterFunc func(method, url string, handler http.HandlerFunc)\n\n// OK writes \"OK\\n\" to the response.  l and w must not be nil.\nfunc OK(ctx context.Context, l *slog.Logger, w http.ResponseWriter) {\n\tif _, err := io.WriteString(w, \"OK\\n\"); err != nil {\n\t\tl.WarnContext(ctx, \"writing ok body\", slogutil.KeyError, err)\n\t}\n}\n\n// ErrorAndLog writes a formatted HTTP error response and logs it at\n// [slog.LevelError] level.  l, r, and w must not be nil.\nfunc ErrorAndLog(\n\tctx context.Context,\n\tl *slog.Logger,\n\tr *http.Request,\n\tw http.ResponseWriter,\n\tcode int,\n\tformat string,\n\targs ...any,\n) {\n\ttext := fmt.Sprintf(format, args...)\n\tl.WarnContext(\n\t\tctx,\n\t\t\"http error\",\n\t\t\"host\", r.Host,\n\t\t\"method\", r.Method,\n\t\t\"raddr\", r.RemoteAddr,\n\t\t\"request_uri\", r.RequestURI,\n\t\t\"status\", code,\n\t\tslogutil.KeyError, text,\n\t)\n\n\thttp.Error(w, text, code)\n}\n\n// UserAgent returns the ID of the service as a User-Agent string.  It can also\n// be used as the value of the Server HTTP header.\nfunc UserAgent() (ua string) {\n\treturn fmt.Sprintf(\"AdGuardHome/%s\", version.Version())\n}\n\n// textPlainDeprMsg is the message returned to API users when they try to use\n// an API that used to accept \"text/plain\" but doesn't anymore.\nconst textPlainDeprMsg = `using this api with the text/plain content-type is deprecated; ` +\n\t`use application/json`\n\n// WriteTextPlainDeprecated responds to the request with a message about\n// deprecation and removal of a plain-text API if the request is made with the\n// \"text/plain\" Content-Type.  All arguments must not be nil.\nfunc WriteTextPlainDeprecated(\n\tctx context.Context,\n\tl *slog.Logger,\n\tw http.ResponseWriter,\n\tr *http.Request,\n) (isPlainText bool) {\n\tif r.Header.Get(httphdr.ContentType) != HdrValTextPlain {\n\t\treturn false\n\t}\n\n\tErrorAndLog(ctx, l, r, w, http.StatusUnsupportedMediaType, textPlainDeprMsg)\n\n\treturn true\n}\n"
  },
  {
    "path": "internal/aghhttp/header.go",
    "content": "package aghhttp\n\n// HTTP headers\n\n// HTTP header value constants.\nconst (\n\tHdrValApplicationJSON         = \"application/json\"\n\tHdrValStrictTransportSecurity = \"max-age=31536000; includeSubDomains\"\n\tHdrValTextPlain               = \"text/plain\"\n)\n"
  },
  {
    "path": "internal/aghhttp/json.go",
    "content": "package aghhttp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// JSON Utilities\n\n// nsecPerMsec is the number of nanoseconds in a millisecond.\nconst nsecPerMsec = float64(time.Millisecond / time.Nanosecond)\n\n// JSONDuration is a time.Duration that can be decoded from JSON and encoded\n// into JSON according to our API conventions.\ntype JSONDuration time.Duration\n\n// type check\nvar _ json.Marshaler = JSONDuration(0)\n\n// MarshalJSON implements the json.Marshaler interface for JSONDuration.  err is\n// always nil.\nfunc (d JSONDuration) MarshalJSON() (b []byte, err error) {\n\tmsec := float64(time.Duration(d)) / nsecPerMsec\n\tb = strconv.AppendFloat(nil, msec, 'f', -1, 64)\n\n\treturn b, nil\n}\n\n// type check\nvar _ json.Unmarshaler = (*JSONDuration)(nil)\n\n// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.\nfunc (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {\n\tif d == nil {\n\t\treturn fmt.Errorf(\"json duration is nil\")\n\t}\n\n\tmsec, err := strconv.ParseFloat(string(b), 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing json time: %w\", err)\n\t}\n\n\t*d = JSONDuration(int64(msec * nsecPerMsec))\n\n\treturn nil\n}\n\n// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON\n// according to our API conventions.\ntype JSONTime time.Time\n\n// type check\nvar _ json.Marshaler = JSONTime{}\n\n// MarshalJSON implements the json.Marshaler interface for JSONTime.  err is\n// always nil.\nfunc (t JSONTime) MarshalJSON() (b []byte, err error) {\n\tmsec := float64(time.Time(t).UnixNano()) / nsecPerMsec\n\tb = strconv.AppendFloat(nil, msec, 'f', -1, 64)\n\n\treturn b, nil\n}\n\n// type check\nvar _ json.Unmarshaler = (*JSONTime)(nil)\n\n// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.\nfunc (t *JSONTime) UnmarshalJSON(b []byte) (err error) {\n\tif t == nil {\n\t\treturn fmt.Errorf(\"json time is nil\")\n\t}\n\n\tmsec, err := strconv.ParseFloat(string(b), 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing json time: %w\", err)\n\t}\n\n\t*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())\n\n\treturn nil\n}\n\n// WriteJSONResponse writes headers with the code, encodes resp into w, and logs\n// any errors it encounters.  r is used to get additional information from the\n// request.  l, w, and r must not be nil.\nfunc WriteJSONResponse(\n\tctx context.Context,\n\tl *slog.Logger,\n\tw http.ResponseWriter,\n\tr *http.Request,\n\tcode int,\n\tresp any,\n) {\n\th := w.Header()\n\th.Set(httphdr.ContentType, HdrValApplicationJSON)\n\th.Set(httphdr.Server, UserAgent())\n\n\tw.WriteHeader(code)\n\n\terr := json.NewEncoder(w).Encode(resp)\n\tif err != nil {\n\t\tl.ErrorContext(\n\t\t\tctx,\n\t\t\t\"writing json response\",\n\t\t\t\"method\", r.Method,\n\t\t\t\"path\", r.URL.Path,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\t}\n}\n\n// WriteJSONResponseOK writes headers with the code 200 OK, encodes v into w,\n// and logs any errors it encounters.  r is used to get additional information\n// from the request.  l, w, and r must not be nil.\nfunc WriteJSONResponseOK(\n\tctx context.Context,\n\tl *slog.Logger,\n\tw http.ResponseWriter,\n\tr *http.Request,\n\tv any,\n) {\n\tWriteJSONResponse(ctx, l, w, r, http.StatusOK, v)\n}\n\n// ErrorCode is the error code as used by the HTTP API.  See the ErrorCode\n// definition in the OpenAPI specification.\ntype ErrorCode string\n\n// ErrorCode constants.\n//\n// TODO(a.garipov): Expand and document codes.\nconst (\n\t// ErrorCodeTMP000 is the temporary error code used for all errors.\n\tErrorCodeTMP000 = \"\"\n)\n\n// HTTPAPIErrorResp is the error response as used by the HTTP API.  See the\n// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI\n// specification.\ntype HTTPAPIErrorResp struct {\n\tCode ErrorCode `json:\"code\"`\n\tMsg  string    `json:\"msg\"`\n}\n\n// WriteJSONResponseError encodes err as a JSON error into w, and logs any\n// errors it encounters.  r is used to get additional information from the\n// request.  l, w, and r must not be nil.\nfunc WriteJSONResponseError(\n\tctx context.Context,\n\tl *slog.Logger,\n\tw http.ResponseWriter,\n\tr *http.Request,\n\terr error,\n) {\n\tl.ErrorContext(\n\t\tctx,\n\t\t\"writing json error\",\n\t\t\"method\", r.Method,\n\t\t\"path\", r.URL.Path,\n\t\tslogutil.KeyError, err,\n\t)\n\n\tWriteJSONResponse(ctx, l, w, r, http.StatusUnprocessableEntity, &HTTPAPIErrorResp{\n\t\tCode: ErrorCodeTMP000,\n\t\tMsg:  err.Error(),\n\t})\n}\n"
  },
  {
    "path": "internal/aghhttp/json_test.go",
    "content": "package aghhttp_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testJSONTime is the JSON time for tests.\nvar testJSONTime = aghhttp.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())\n\n// testJSONTimeStr is the string with the JSON encoding of testJSONTime.\nconst testJSONTimeStr = \"1234567890123.456\"\n\nfunc TestJSONTime_MarshalJSON(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\tin         aghhttp.JSONTime\n\t\twant       []byte\n\t}{{\n\t\tname:       \"unix_zero\",\n\t\twantErrMsg: \"\",\n\t\tin:         aghhttp.JSONTime(time.Unix(0, 0)),\n\t\twant:       []byte(\"0\"),\n\t}, {\n\t\tname:       \"empty\",\n\t\twantErrMsg: \"\",\n\t\tin:         aghhttp.JSONTime{},\n\t\twant:       []byte(\"-6795364578871.345\"),\n\t}, {\n\t\tname:       \"time\",\n\t\twantErrMsg: \"\",\n\t\tin:         testJSONTime,\n\t\twant:       []byte(testJSONTimeStr),\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := tc.in.MarshalJSON()\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n\n\tt.Run(\"json\", func(t *testing.T) {\n\t\tin := &struct {\n\t\t\tA aghhttp.JSONTime\n\t\t}{\n\t\t\tA: testJSONTime,\n\t\t}\n\n\t\tgot, err := json.Marshal(in)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, []byte(`{\"A\":`+testJSONTimeStr+`}`), got)\n\t})\n}\n\nfunc TestJSONTime_UnmarshalJSON(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\twant       aghhttp.JSONTime\n\t\tdata       []byte\n\t}{{\n\t\tname:       \"time\",\n\t\twantErrMsg: \"\",\n\t\twant:       testJSONTime,\n\t\tdata:       []byte(testJSONTimeStr),\n\t}, {\n\t\tname: \"bad\",\n\t\twantErrMsg: `parsing json time: strconv.ParseFloat: parsing \"{}\": ` +\n\t\t\t`invalid syntax`,\n\t\twant: aghhttp.JSONTime{},\n\t\tdata: []byte(`{}`),\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar got aghhttp.JSONTime\n\t\t\terr := got.UnmarshalJSON(tc.data)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n\n\tt.Run(\"nil\", func(t *testing.T) {\n\t\terr := (*aghhttp.JSONTime)(nil).UnmarshalJSON([]byte(\"0\"))\n\t\trequire.Error(t, err)\n\n\t\tmsg := err.Error()\n\t\tassert.Equal(t, \"json time is nil\", msg)\n\t})\n\n\tt.Run(\"json\", func(t *testing.T) {\n\t\twant := testJSONTime\n\t\tvar got struct {\n\t\t\tA aghhttp.JSONTime\n\t\t}\n\n\t\terr := json.Unmarshal([]byte(`{\"A\":`+testJSONTimeStr+`}`), &got)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, want, got.A)\n\t})\n}\n"
  },
  {
    "path": "internal/aghhttp/registrar.go",
    "content": "package aghhttp\n\nimport (\n\t\"net/http\"\n)\n\n// Registrar registers an HTTP handler for a method and path.\n//\n// TODO(s.chzhen):  Implement [httputil.Router].\ntype Registrar interface {\n\tRegister(method, path string, h http.HandlerFunc)\n}\n\n// EmptyRegistrar is an implementation of [Registrar] that does nothing.\ntype EmptyRegistrar struct{}\n\n// type check\nvar _ Registrar = EmptyRegistrar{}\n\n// Register implements the [Registrar] interface.\nfunc (EmptyRegistrar) Register(_, _ string, _ http.HandlerFunc) {}\n\n// WrapFunc is a wrapper function that builds an HTTP handler for a route.\ntype WrapFunc func(method string, h http.HandlerFunc) (wrapped http.Handler)\n\n// DefaultRegistrar is an implementation of [Registrar] that registers handlers\n// after applying a user-provided wrapper function.\ntype DefaultRegistrar struct {\n\tmux    *http.ServeMux\n\twrapFn WrapFunc\n}\n\n// NewDefaultRegistrar returns a new properly initialized *DefaultRegistrar.\n// mux and wrap must not be nil.\nfunc NewDefaultRegistrar(mux *http.ServeMux, wrap WrapFunc) (r *DefaultRegistrar) {\n\treturn &DefaultRegistrar{\n\t\tmux:    mux,\n\t\twrapFn: wrap,\n\t}\n}\n\n// type check\nvar _ Registrar = (*DefaultRegistrar)(nil)\n\n// Register implements the [Registrar] interface.\nfunc (r *DefaultRegistrar) Register(method, path string, h http.HandlerFunc) {\n\twrapped := r.wrapFn(method, h)\n\tr.mux.Handle(path, wrapped)\n}\n"
  },
  {
    "path": "internal/aghnet/addr.go",
    "content": "package aghnet\n\nimport (\n\t\"strings\"\n)\n\n// NormalizeDomain returns a lowercased version of host without the final dot,\n// unless host is \".\", in which case it returns it unchanged.  That is a special\n// case that to allow matching queries like:\n//\n//\tdig IN NS '.'\nfunc NormalizeDomain(host string) (norm string) {\n\tif host == \".\" {\n\t\treturn host\n\t}\n\n\treturn strings.ToLower(strings.TrimSuffix(host, \".\"))\n}\n"
  },
  {
    "path": "internal/aghnet/dhcp.go",
    "content": "package aghnet\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n)\n\n// CheckOtherDHCP tries to discover another DHCP server in the network.  l must\n// not be nil.\nfunc CheckOtherDHCP(\n\tctx context.Context,\n\tl *slog.Logger,\n\tifaceName string,\n) (ok4, ok6 bool, err4, err6 error) {\n\treturn checkOtherDHCP(ctx, l, ifaceName)\n}\n"
  },
  {
    "path": "internal/aghnet/dhcp_unix.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage aghnet\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/dhcpv6\"\n\t\"github.com/insomniacslk/dhcp/dhcpv6/nclient6\"\n\t\"github.com/insomniacslk/dhcp/iana\"\n)\n\n// defaultDiscoverTime is the default timeout of checking another DHCP server\n// response.\nconst defaultDiscoverTime = 3 * time.Second\n\nfunc checkOtherDHCP(\n\tctx context.Context,\n\tl *slog.Logger,\n\tifaceName string,\n) (ok4, ok6 bool, err4, err6 error) {\n\tiface, err := net.InterfaceByName(ifaceName)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"couldn't find interface by name %s: %w\", ifaceName, err)\n\t\terr4, err6 = err, err\n\n\t\treturn false, false, err4, err6\n\t}\n\n\tok4, err4 = checkOtherDHCPv4(ctx, l, iface)\n\tok6, err6 = checkOtherDHCPv6(ctx, l, iface)\n\n\treturn ok4, ok6, err4, err6\n}\n\n// ifaceIPv4Subnet returns the first suitable IPv4 subnetwork iface has.\n// iface must not be nil.\nfunc ifaceIPv4Subnet(iface *net.Interface) (subnet netip.Prefix, err error) {\n\tvar addrs []net.Addr\n\tif addrs, err = iface.Addrs(); err != nil {\n\t\treturn netip.Prefix{}, err\n\t}\n\n\tfor _, a := range addrs {\n\t\tvar ip net.IP\n\t\tvar maskLen int\n\t\tswitch a := a.(type) {\n\t\tcase *net.IPAddr:\n\t\t\tip = a.IP\n\t\t\tmaskLen, _ = ip.DefaultMask().Size()\n\t\tcase *net.IPNet:\n\t\t\tip = a.IP\n\t\t\tmaskLen, _ = a.Mask.Size()\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tif ip = ip.To4(); ip != nil {\n\t\t\treturn netip.PrefixFrom(netip.AddrFrom4([4]byte(ip)), maskLen), nil\n\t\t}\n\t}\n\n\treturn netip.Prefix{}, fmt.Errorf(\"interface %s has no ipv4 addresses\", iface.Name)\n}\n\n// checkOtherDHCPv4 sends a DHCP request to the specified network interface, and\n// waits for a response for a period defined by defaultDiscoverTime.  l must not\n// be nil.\nfunc checkOtherDHCPv4(\n\tctx context.Context,\n\tl *slog.Logger,\n\tiface *net.Interface,\n) (ok bool, err error) {\n\tvar subnet netip.Prefix\n\tif subnet, err = ifaceIPv4Subnet(iface); err != nil {\n\t\treturn false, err\n\t}\n\n\t// Resolve broadcast addr.\n\tdst := netip.AddrPortFrom(BroadcastFromPref(subnet), 67).String()\n\tvar dstAddr *net.UDPAddr\n\tif dstAddr, err = net.ResolveUDPAddr(\"udp4\", dst); err != nil {\n\t\treturn false, fmt.Errorf(\"couldn't resolve UDP address %s: %w\", dst, err)\n\t}\n\n\tvar hostname string\n\tif hostname, err = os.Hostname(); err != nil {\n\t\treturn false, fmt.Errorf(\"couldn't get hostname: %w\", err)\n\t}\n\n\treturn discover4(ctx, l, iface, dstAddr, hostname)\n}\n\n// discover4 sends a DHCPv4 discovery to the specified network interface and\n// waits for response.  iface and dstAddr must not be nil.\nfunc discover4(\n\tctx context.Context,\n\tl *slog.Logger,\n\tiface *net.Interface,\n\tdstAddr *net.UDPAddr,\n\thostname string,\n) (ok bool, err error) {\n\tvar req *dhcpv4.DHCPv4\n\tif req, err = dhcpv4.NewDiscovery(iface.HardwareAddr); err != nil {\n\t\treturn false, fmt.Errorf(\"dhcpv4.NewDiscovery: %w\", err)\n\t}\n\n\treq.Options.Update(dhcpv4.OptClientIdentifier(iface.HardwareAddr))\n\treq.Options.Update(dhcpv4.OptHostName(hostname))\n\treq.SetBroadcast()\n\n\t// Bind to 0.0.0.0:68.\n\t//\n\t// On OpenBSD binding to the port 68 competes with dhclient's binding,\n\t// so that all incoming packets are ignored and the discovering process\n\t// is spoiled.\n\t//\n\t// It's also known that listening on the specified interface's address\n\t// ignores broadcast packets when reading.\n\tvar c net.PacketConn\n\tif c, err = listenPacketReusable(iface.Name, \"udp4\", \":68\"); err != nil {\n\t\treturn false, fmt.Errorf(\"couldn't listen on :68: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, c.Close()) }()\n\n\t// Send to broadcast.\n\tif _, err = c.WriteTo(req.ToBytes(), dstAddr); err != nil {\n\t\treturn false, fmt.Errorf(\"couldn't send a packet to %s: %w\", dstAddr, err)\n\t}\n\n\tfor {\n\t\tvar next bool\n\t\tok, next, err = tryConn4(ctx, l, req, c, iface)\n\t\tif next {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\treturn ok, nil\n\t}\n}\n\n// tryConn4 reads and validates DHCPv4 response packet if it matches\n// the original request.  req and c must not be nil.\n//\n// TODO(a.garipov): Refactor further.  Inspect error handling, remove parameter\n// next, address the TODO, merge with tryConn6, etc.\nfunc tryConn4(\n\tctx context.Context,\n\tl *slog.Logger,\n\treq *dhcpv4.DHCPv4,\n\tc net.PacketConn,\n\tiface *net.Interface,\n) (ok, next bool, err error) {\n\tif err = c.SetDeadline(time.Now().Add(defaultDiscoverTime)); err != nil {\n\t\treturn false, false, fmt.Errorf(\"dhcpv4: setting deadline: %w\", err)\n\t}\n\n\t// TODO: replicate dhclient's behavior of retrying several times with\n\t// progressively longer timeouts.\n\tl.Log(ctx, slogutil.LevelTrace, \"waiting for an answer\", \"timeout\", defaultDiscoverTime)\n\n\tb := make([]byte, 1500)\n\tn, _, err := c.ReadFrom(b)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrDeadlineExceeded) {\n\t\t\tl.DebugContext(ctx, \"did not receive response\")\n\n\t\t\treturn false, false, nil\n\t\t}\n\n\t\treturn false, false, fmt.Errorf(\"receiving packet: %w\", err)\n\t}\n\n\tl.Log(ctx, slogutil.LevelTrace, \"received packet\", \"size\", n)\n\n\tresponse, err := dhcpv4.FromBytes(b[:n])\n\tif err != nil {\n\t\tl.DebugContext(ctx, \"encoding\", slogutil.KeyError, err)\n\n\t\treturn false, true, err\n\t}\n\n\tl.DebugContext(ctx, \"received message from server\", \"summary\", response.Summary())\n\n\tswitch {\n\tcase\n\t\tresponse.OpCode != dhcpv4.OpcodeBootReply,\n\t\tresponse.HWType != iana.HWTypeEthernet,\n\t\t!bytes.Equal(response.ClientHWAddr, iface.HardwareAddr),\n\t\tresponse.TransactionID != req.TransactionID,\n\t\t!response.Options.Has(dhcpv4.OptionDHCPMessageType):\n\t\tl.DebugContext(ctx, \"dhcpv4: received response does not match the request\")\n\n\t\treturn false, true, nil\n\tdefault:\n\t\tl.Log(ctx, slogutil.LevelTrace, \"packet is from an active dhcp server\")\n\n\t\treturn true, false, nil\n\t}\n}\n\n// checkOtherDHCPv6 sends a DHCP request to the specified network interface, and\n// waits for a response for a period defined by defaultDiscoverTime.  l must not\n// be nil.\nfunc checkOtherDHCPv6(\n\tctx context.Context,\n\tl *slog.Logger,\n\tiface *net.Interface,\n) (ok bool, err error) {\n\tifaceIPNet, err := IfaceIPAddrs(iface, IPVersion6)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"getting ipv6 addrs for iface %s: %w\", iface.Name, err)\n\t}\n\tif len(ifaceIPNet) == 0 {\n\t\treturn false, fmt.Errorf(\"interface %s has no ipv6 addresses\", iface.Name)\n\t}\n\n\tsrcIP := ifaceIPNet[0]\n\tsrc := netutil.JoinHostPort(srcIP.String(), 546)\n\tdst := \"[ff02::1:2]:547\"\n\n\tudpAddr, err := net.ResolveUDPAddr(\"udp6\", src)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"dhcpv6: Couldn't resolve UDP address %s: %w\", src, err)\n\t}\n\n\tif !udpAddr.IP.To16().Equal(srcIP) {\n\t\treturn false, fmt.Errorf(\"dhcpv6: Resolved UDP address is not %s: %w\", src, err)\n\t}\n\n\tdstAddr, err := net.ResolveUDPAddr(\"udp6\", dst)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"dhcpv6: Couldn't resolve UDP address %s: %w\", dst, err)\n\t}\n\n\treturn discover6(ctx, l, iface, udpAddr, dstAddr)\n}\n\n// discover6 sends a DHCPv6 discovery to the specified network interface and\n// waits for response.  iface, updAddr and dstAddr must not be nil.\nfunc discover6(\n\tctx context.Context,\n\tl *slog.Logger,\n\tiface *net.Interface,\n\tudpAddr *net.UDPAddr,\n\tdstAddr *net.UDPAddr,\n) (ok bool, err error) {\n\treq, err := dhcpv6.NewSolicit(iface.HardwareAddr)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"dhcpv6: dhcpv6.NewSolicit: %w\", err)\n\t}\n\n\tl.DebugContext(ctx, \"listening on udp6\", \"addr\", udpAddr)\n\tc, err := nclient6.NewIPv6UDPConn(iface.Name, dhcpv6.DefaultClientPort)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"dhcpv6: Couldn't listen on :546: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, c.Close()) }()\n\n\t_, err = c.WriteTo(req.ToBytes(), dstAddr)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"dhcpv6: Couldn't send a packet to %s: %w\", dstAddr, err)\n\t}\n\n\tfor {\n\t\tvar next bool\n\t\tok, next, err = tryConn6(ctx, l, req, c)\n\t\tif next {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\treturn ok, nil\n\t}\n}\n\n// tryConn6 reads and validates DHCPv6 response packet if it matches\n// the original request.  req and c must not be nil.\n//\n// TODO(a.garipov): See the comment on tryConn4.  Sigh…\nfunc tryConn6(\n\tctx context.Context,\n\tl *slog.Logger,\n\treq *dhcpv6.Message,\n\tc net.PacketConn,\n) (ok, next bool, err error) {\n\t// TODO: replicate dhclient's behavior of retrying several times with\n\t// progressively longer timeouts.\n\tl.Log(ctx, slogutil.LevelTrace, \"waiting for an answer\", \"timeout\", defaultDiscoverTime)\n\n\tb := make([]byte, 4096)\n\terr = c.SetDeadline(time.Now().Add(defaultDiscoverTime))\n\tif err != nil {\n\t\treturn false, false, fmt.Errorf(\"setting deadline: %w\", err)\n\t}\n\n\tn, _, err := c.ReadFrom(b)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrDeadlineExceeded) {\n\t\t\tl.DebugContext(ctx, \"did not receive response\")\n\n\t\t\treturn false, false, nil\n\t\t}\n\n\t\treturn false, false, fmt.Errorf(\"receiving packet: %w\", err)\n\t}\n\n\tl.Log(ctx, slogutil.LevelTrace, \"dhcpv6: received packet\", \"size\", n)\n\n\tresponse, err := dhcpv6.FromBytes(b[:n])\n\tif err != nil {\n\t\tl.DebugContext(ctx, \"encoding\", slogutil.KeyError, err)\n\n\t\treturn false, true, err\n\t}\n\n\tl.DebugContext(ctx, \"received message from server\", \"summary\", response.Summary())\n\n\tcid := req.Options.ClientID()\n\tmsg, err := response.GetInnerMessage()\n\tif err != nil {\n\t\tl.DebugContext(ctx, \"getting inner message\", slogutil.KeyError, err)\n\n\t\treturn false, true, err\n\t}\n\n\trcid := msg.Options.ClientID()\n\tif !(response.Type() == dhcpv6.MessageTypeAdvertise &&\n\t\tmsg.TransactionID == req.TransactionID &&\n\t\trcid != nil &&\n\t\tcid.Equal(rcid)) {\n\n\t\tl.DebugContext(ctx, \"received message from server does not match our request\")\n\n\t\treturn false, true, nil\n\t}\n\n\tl.Log(ctx, slogutil.LevelTrace, \"dhcpv6: the packet is from an active dhcp server\")\n\n\treturn true, false, nil\n}\n"
  },
  {
    "path": "internal/aghnet/dhcp_windows.go",
    "content": "//go:build windows\n\npackage aghnet\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n)\n\nfunc checkOtherDHCP(\n\t_ context.Context,\n\t_ *slog.Logger,\n\tifaceName string,\n) (ok4, ok6 bool, err4, err6 error) {\n\treturn false,\n\t\tfalse,\n\t\taghos.Unsupported(\"CheckIfOtherDHCPServersPresentV4\"),\n\t\taghos.Unsupported(\"CheckIfOtherDHCPServersPresentV6\")\n}\n"
  },
  {
    "path": "internal/aghnet/hostgen.go",
    "content": "package aghnet\n\nimport (\n\t\"net/netip\"\n\t\"strings\"\n)\n\n// GenerateHostname generates the hostname from ip.  In case of using IPv4 the\n// result should be like:\n//\n//\t192-168-10-1\n//\n// In case of using IPv6, the result is like:\n//\n//\tff80-f076-0000-0000-0000-0000-0000-0010\n//\n// ip must be either an IPv4 or an IPv6.\nfunc GenerateHostname(ip netip.Addr) (hostname string) {\n\tif !ip.IsValid() {\n\t\t// TODO(s.chzhen):  Get rid of it.\n\t\tpanic(\"aghnet generate hostname: invalid ip\")\n\t}\n\n\tip = ip.Unmap()\n\thostname = ip.StringExpanded()\n\n\tif ip.Is4() {\n\t\treturn strings.ReplaceAll(hostname, \".\", \"-\")\n\t}\n\n\treturn strings.ReplaceAll(hostname, \":\", \"-\")\n}\n"
  },
  {
    "path": "internal/aghnet/hostgen_test.go",
    "content": "package aghnet_test\n\nimport (\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGenerateHostName(t *testing.T) {\n\tt.Run(\"valid\", func(t *testing.T) {\n\t\ttestCases := []struct {\n\t\t\tname string\n\t\t\twant string\n\t\t\tip   netip.Addr\n\t\t}{{\n\t\t\tname: \"good_ipv4\",\n\t\t\twant: \"127-0-0-1\",\n\t\t\tip:   netip.MustParseAddr(\"127.0.0.1\"),\n\t\t}, {\n\t\t\tname: \"good_ipv6\",\n\t\t\twant: \"fe00-0000-0000-0000-0000-0000-0000-0001\",\n\t\t\tip:   netip.MustParseAddr(\"fe00::1\"),\n\t\t}, {\n\t\t\tname: \"4to6\",\n\t\t\twant: \"1-2-3-4\",\n\t\t\tip:   netip.MustParseAddr(\"::ffff:1.2.3.4\"),\n\t\t}}\n\n\t\tfor _, tc := range testCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\thostname := aghnet.GenerateHostname(tc.ip)\n\t\t\t\tassert.Equal(t, tc.want, hostname)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"invalid\", func(t *testing.T) {\n\t\tassert.Panics(t, func() { aghnet.GenerateHostname(netip.Addr{}) })\n\t})\n}\n"
  },
  {
    "path": "internal/aghnet/hostscontainer.go",
    "content": "package aghnet\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"sync/atomic\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/hostsfile\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// hostsContainerPrefix is a prefix for wrapping errors in HostsContainer's\n// methods.\nconst hostsContainerPrefix = \"hosts container\"\n\n// HostsContainer stores the relevant hosts database provided by the OS and\n// processes both A/AAAA and PTR DNS requests for those.\ntype HostsContainer struct {\n\t// logger is used for logging the operation of the hosts container.  It must\n\t// not be nil.\n\tlogger *slog.Logger\n\n\t// done is the channel to signal closing the container.\n\tdone chan struct{}\n\n\t// updates is the channel for receiving updated hosts.\n\tupdates chan *hostsfile.DefaultStorage\n\n\t// current is the last set of hosts parsed.\n\tcurrent atomic.Pointer[hostsfile.DefaultStorage]\n\n\t// fsys is the working file system to read hosts files from.  It must not be\n\t// nil.\n\tfsys fs.FS\n\n\t// watcher tracks the changes in specified files and directories.  It must\n\t// not be nil.\n\twatcher aghos.FSWatcher\n\n\t// patterns stores specified paths in the fs.Glob-compatible form.\n\tpatterns []string\n}\n\n// ErrNoHostsPaths is returned when there are no valid paths to watch passed to\n// the HostsContainer.\nconst ErrNoHostsPaths errors.Error = \"no valid paths to hosts files provided\"\n\n// NewHostsContainer creates a container of hosts that watches the paths with w.\n// paths shouldn't be empty, and each path should be relative and refer either a\n// file or a directory in fsys.  fsys should be a filesystem mounted at the\n// operating system root.  l and w must not be nil.\n//\n// TODO(e.burkov):  Add configuration.\n//\n// TODO(e.burkov):  Reconsider using fs.\nfunc NewHostsContainer(\n\tctx context.Context,\n\tl *slog.Logger,\n\tfsys fs.FS,\n\tw aghos.FSWatcher,\n\tpaths ...string,\n) (hc *HostsContainer, err error) {\n\tdefer func() { err = errors.Annotate(err, \"%s: %w\", hostsContainerPrefix) }()\n\n\tif len(paths) == 0 {\n\t\treturn nil, ErrNoHostsPaths\n\t}\n\n\tvar patterns []string\n\tpatterns, err = pathsToPatterns(fsys, paths)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if len(patterns) == 0 {\n\t\treturn nil, ErrNoHostsPaths\n\t}\n\n\thc = &HostsContainer{\n\t\tlogger:   l,\n\t\tdone:     make(chan struct{}, 1),\n\t\tupdates:  make(chan *hostsfile.DefaultStorage, 1),\n\t\tfsys:     fsys,\n\t\twatcher:  w,\n\t\tpatterns: patterns,\n\t}\n\n\tl.DebugContext(ctx, \"starting\")\n\n\t// Load initially.\n\tif err = hc.refresh(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Don't use absolute paths for watching, since those are required to be\n\t// relative to the operating system root, not the current directory.  See\n\t// TODO at NewHostsContainer.\n\trootDir := aghos.RootDir()\n\n\tfor _, p := range paths {\n\t\tp = filepath.Join(rootDir, p)\n\n\t\terr = w.Add(p)\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\t\treturn nil, fmt.Errorf(\"adding path: %w\", err)\n\t\t\t}\n\n\t\t\tl.DebugContext(ctx, \"expected path does not exist\", \"path\", p, slogutil.KeyError, err)\n\t\t}\n\t}\n\n\tgo hc.handleEvents(ctx)\n\n\treturn hc, nil\n}\n\n// Close implements the [io.Closer] interface for *HostsContainer.  It closes\n// both itself and its [aghos.FSWatcher].  Close must only be called once.\n//\n// TODO(s.chzhen):  Implement [service.Interface].\nfunc (hc *HostsContainer) Close() (err error) {\n\t// TODO(s.chzhen):  Pass context.\n\tctx := context.TODO()\n\thc.logger.DebugContext(ctx, \"closing\")\n\n\terr = errors.Annotate(hc.watcher.Shutdown(ctx), \"closing fs watcher: %w\")\n\n\t// Go on and close the container either way.\n\tclose(hc.done)\n\n\treturn err\n}\n\n// Upd returns the channel into which the updates are sent.  The updates\n// themselves must not be modified.\nfunc (hc *HostsContainer) Upd() (updates <-chan *hostsfile.DefaultStorage) {\n\treturn hc.updates\n}\n\n// type check\nvar _ hostsfile.Storage = (*HostsContainer)(nil)\n\n// ByAddr implements the [hostsfile.Storage] interface for *HostsContainer.\nfunc (hc *HostsContainer) ByAddr(addr netip.Addr) (names []string) {\n\treturn hc.current.Load().ByAddr(addr)\n}\n\n// ByName implements the [hostsfile.Storage] interface for *HostsContainer.\nfunc (hc *HostsContainer) ByName(name string) (addrs []netip.Addr) {\n\treturn hc.current.Load().ByName(name)\n}\n\n// pathsToPatterns converts paths into patterns compatible with fs.Glob.\nfunc pathsToPatterns(fsys fs.FS, paths []string) (patterns []string, err error) {\n\tfor i, p := range paths {\n\t\tvar fi fs.FileInfo\n\t\tfi, err = fs.Stat(fsys, p)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Don't put a filename here since it's already added by [fs.Stat].\n\t\t\treturn nil, fmt.Errorf(\"path at index %d: %w\", i, err)\n\t\t}\n\n\t\tif fi.IsDir() {\n\t\t\tp = path.Join(p, \"*\")\n\t\t}\n\n\t\tpatterns = append(patterns, p)\n\t}\n\n\treturn patterns, nil\n}\n\n// handleEvents concurrently handles the file system events.  It closes the\n// update channel of HostsContainer when finishes.  It is intended to be used as\n// a goroutine.\nfunc (hc *HostsContainer) handleEvents(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, hc.logger)\n\n\tdefer close(hc.updates)\n\n\teventsCh := hc.watcher.Events()\n\tok := eventsCh != nil\n\tfor ok {\n\t\tselect {\n\t\tcase _, ok = <-eventsCh:\n\t\t\tif !ok {\n\t\t\t\thc.logger.DebugContext(ctx, \"watcher closed the events channel\")\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := hc.refresh(ctx); err != nil {\n\t\t\t\thc.logger.ErrorContext(ctx, \"refreshing\", slogutil.KeyError, err)\n\t\t\t}\n\t\tcase _, ok = <-hc.done:\n\t\t\t// Go on.\n\t\t}\n\t}\n}\n\n// sendUpd tries to send the parsed data to the ch.\nfunc (hc *HostsContainer) sendUpd(ctx context.Context, recs *hostsfile.DefaultStorage) {\n\thc.logger.DebugContext(ctx, \"sending update\")\n\n\tch := hc.updates\n\tselect {\n\tcase ch <- recs:\n\t\t// Updates are delivered.  Go on.\n\tcase <-ch:\n\t\tch <- recs\n\t\thc.logger.DebugContext(ctx, \"replaced the last update\")\n\tcase ch <- recs:\n\t\t// The previous update was just read and the next one pushed.  Go on.\n\tdefault:\n\t\thc.logger.ErrorContext(ctx, \"updates channel is broken\")\n\t}\n}\n\n// refresh gets the data from specified files and propagates the updates if\n// needed.\n//\n// TODO(e.burkov):  Accept a parameter to specify the files to refresh.\nfunc (hc *HostsContainer) refresh(ctx context.Context) (err error) {\n\thc.logger.DebugContext(ctx, \"refreshing\")\n\n\t// The error is always nil here since no readers passed.\n\tstrg, _ := hostsfile.NewDefaultStorage(ctx, &hostsfile.DefaultStorageConfig{\n\t\tLogger: hc.logger,\n\t})\n\t_, err = aghos.FileWalker(func(r io.Reader) (patterns []string, cont bool, err error) {\n\t\t// Don't wrap the error since it's already informative enough as is.\n\t\treturn nil, true, hostsfile.Parse(ctx, strg, r, nil)\n\t}).Walk(hc.fsys, hc.patterns...)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\t// TODO(e.burkov):  Serialize updates using [time.Time].\n\tif !hc.current.Load().Equal(strg) {\n\t\thc.current.Store(strg)\n\t\thc.sendUpd(ctx, strg)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghnet/hostscontainer_internal_test.go",
    "content": "package aghnet\n\nimport (\n\t\"io/fs\"\n\t\"path\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/fakeio/fakefs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst nl = \"\\n\"\n\nfunc TestHostsContainer_PathsToPatterns(t *testing.T) {\n\tgsfs := fstest.MapFS{\n\t\t\"dir_0/file_1\":       &fstest.MapFile{Data: []byte{1}},\n\t\t\"dir_0/file_2\":       &fstest.MapFile{Data: []byte{2}},\n\t\t\"dir_0/dir_1/file_3\": &fstest.MapFile{Data: []byte{3}},\n\t}\n\n\ttestCases := []struct {\n\t\tname  string\n\t\tpaths []string\n\t\twant  []string\n\t}{{\n\t\tname:  \"no_paths\",\n\t\tpaths: nil,\n\t\twant:  nil,\n\t}, {\n\t\tname:  \"single_file\",\n\t\tpaths: []string{\"dir_0/file_1\"},\n\t\twant:  []string{\"dir_0/file_1\"},\n\t}, {\n\t\tname:  \"several_files\",\n\t\tpaths: []string{\"dir_0/file_1\", \"dir_0/file_2\"},\n\t\twant:  []string{\"dir_0/file_1\", \"dir_0/file_2\"},\n\t}, {\n\t\tname:  \"whole_dir\",\n\t\tpaths: []string{\"dir_0\"},\n\t\twant:  []string{\"dir_0/*\"},\n\t}, {\n\t\tname:  \"file_and_dir\",\n\t\tpaths: []string{\"dir_0/file_1\", \"dir_0/dir_1\"},\n\t\twant:  []string{\"dir_0/file_1\", \"dir_0/dir_1/*\"},\n\t}, {\n\t\tname:  \"non-existing\",\n\t\tpaths: []string{path.Join(\"dir_0\", \"file_3\")},\n\t\twant:  nil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tpatterns, err := pathsToPatterns(gsfs, tc.paths)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, patterns)\n\t\t})\n\t}\n\n\tt.Run(\"bad_file\", func(t *testing.T) {\n\t\tconst errStat errors.Error = \"bad file\"\n\n\t\tbadFS := &fakefs.StatFS{\n\t\t\tOnOpen: func(s string) (f fs.File, err error) { panic(testutil.UnexpectedCall(s)) },\n\t\t\tOnStat: func(name string) (fi fs.FileInfo, err error) {\n\t\t\t\treturn nil, errStat\n\t\t\t},\n\t\t}\n\n\t\t_, err := pathsToPatterns(badFS, []string{\"\"})\n\t\tassert.ErrorIs(t, err, errStat)\n\t})\n}\n"
  },
  {
    "path": "internal/aghnet/hostscontainer_test.go",
    "content": "package aghnet_test\n\nimport (\n\t\"context\"\n\t\"net/netip\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"testing/fstest\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/hostsfile\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewHostsContainer(t *testing.T) {\n\tconst dirname = \"dir\"\n\tconst filename = \"file1\"\n\n\tp := path.Join(dirname, filename)\n\n\ttestFS := fstest.MapFS{\n\t\tp: &fstest.MapFile{Data: []byte(\"127.0.0.1 localhost\")},\n\t}\n\n\ttestCases := []struct {\n\t\twantErr error\n\t\tname    string\n\t\tpaths   []string\n\t}{{\n\t\twantErr: nil,\n\t\tname:    \"one_file\",\n\t\tpaths:   []string{p},\n\t}, {\n\t\twantErr: aghnet.ErrNoHostsPaths,\n\t\tname:    \"no_files\",\n\t\tpaths:   []string{},\n\t}, {\n\t\twantErr: aghnet.ErrNoHostsPaths,\n\t\tname:    \"non-existent_file\",\n\t\tpaths:   []string{path.Join(dirname, filename+\"2\")},\n\t}, {\n\t\twantErr: nil,\n\t\tname:    \"whole_dir\",\n\t\tpaths:   []string{dirname},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tonAdd := func(name string) (err error) {\n\t\t\t\trelName, err := filepath.Rel(aghos.RootDir(), name)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.Contains(t, tc.paths, filepath.ToSlash(relName))\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tvar eventsCalledCounter uint32\n\t\t\teventsCh := make(chan struct{})\n\t\t\tonEvents := func() (e <-chan struct{}) {\n\t\t\t\tassert.Equal(t, uint32(1), atomic.AddUint32(&eventsCalledCounter, 1))\n\n\t\t\t\treturn eventsCh\n\t\t\t}\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\t\t\twatcher := aghtest.NewFSWatcher()\n\t\t\twatcher.OnEvents = onEvents\n\t\t\twatcher.OnAdd = onAdd\n\t\t\twatcher.OnShutdown = func(_ context.Context) (err error) { return nil }\n\n\t\t\thc, err := aghnet.NewHostsContainer(ctx, testLogger, testFS, watcher, tc.paths...)\n\t\t\tif tc.wantErr != nil {\n\t\t\t\trequire.ErrorIs(t, err, tc.wantErr)\n\n\t\t\t\tassert.Nil(t, hc)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttestutil.CleanupAndRequireSuccess(t, hc.Close)\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, hc)\n\n\t\t\tassert.NotNil(t, <-hc.Upd())\n\n\t\t\teventsCh <- struct{}{}\n\t\t\tassert.Equal(t, uint32(1), atomic.LoadUint32(&eventsCalledCounter))\n\t\t})\n\t}\n\n\tt.Run(\"nil_fs\", func(t *testing.T) {\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\trequire.Panics(t, func() {\n\t\t\twatcher := aghtest.NewFSWatcher()\n\t\t\t// Those shouldn't panic.\n\t\t\twatcher.OnAdd = func(_ string) (err error) { return nil }\n\t\t\twatcher.OnEvents = func() (e <-chan struct{}) { return nil }\n\t\t\twatcher.OnShutdown = func(_ context.Context) (err error) { return nil }\n\n\t\t\t_, _ = aghnet.NewHostsContainer(ctx, testLogger, nil, watcher, p)\n\t\t})\n\t})\n\n\tt.Run(\"nil_watcher\", func(t *testing.T) {\n\t\trequire.Panics(t, func() {\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\t_, _ = aghnet.NewHostsContainer(ctx, testLogger, testFS, nil, p)\n\t\t})\n\t})\n\n\tt.Run(\"err_watcher\", func(t *testing.T) {\n\t\tconst errOnAdd errors.Error = \"error\"\n\n\t\terrWatcher := aghtest.NewFSWatcher()\n\t\terrWatcher.OnAdd = func(_ string) (err error) { return errOnAdd }\n\t\terrWatcher.OnShutdown = func(_ context.Context) (err error) { return nil }\n\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\thc, err := aghnet.NewHostsContainer(ctx, testLogger, testFS, errWatcher, p)\n\t\trequire.ErrorIs(t, err, errOnAdd)\n\n\t\tassert.Nil(t, hc)\n\t})\n}\n\nfunc TestHostsContainer_refresh(t *testing.T) {\n\t// TODO(e.burkov):  Test the case with no actual updates.\n\n\tip := netutil.IPv4Localhost()\n\tipStr := ip.String()\n\n\tanotherIPStr := \"1.2.3.4\"\n\tanotherIP := netip.MustParseAddr(anotherIPStr)\n\n\tr1 := &hostsfile.Record{\n\t\tAddr:   ip,\n\t\tSource: \"file1\",\n\t\tNames:  []string{\"hostname\"},\n\t}\n\tr2 := &hostsfile.Record{\n\t\tAddr:   anotherIP,\n\t\tSource: \"file2\",\n\t\tNames:  []string{\"alias\"},\n\t}\n\n\tr1Data, _ := r1.MarshalText()\n\tr2Data, _ := r2.MarshalText()\n\n\ttestFS := fstest.MapFS{\"dir/file1\": &fstest.MapFile{Data: r1Data}}\n\n\t// event is a convenient alias for an empty struct{} to emit test events.\n\ttype event = struct{}\n\n\teventsCh := make(chan event, 1)\n\tt.Cleanup(func() { close(eventsCh) })\n\n\tw := aghtest.NewFSWatcher()\n\tw.OnEvents = func() (e <-chan event) { return eventsCh }\n\tw.OnAdd = func(name string) (err error) {\n\t\tassert.Equal(t, filepath.Join(aghos.RootDir(), \"dir\"), name)\n\n\t\treturn nil\n\t}\n\tw.OnShutdown = func(_ context.Context) (err error) { return nil }\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\thc, err := aghnet.NewHostsContainer(ctx, testLogger, testFS, w, \"dir\")\n\trequire.NoError(t, err)\n\ttestutil.CleanupAndRequireSuccess(t, hc.Close)\n\n\tstrg, _ := hostsfile.NewDefaultStorage(ctx, &hostsfile.DefaultStorageConfig{\n\t\tLogger: testLogger,\n\t})\n\tstrg.Add(ctx, r1)\n\n\tt.Run(\"initial_refresh\", func(t *testing.T) {\n\t\tupd, ok := testutil.RequireReceive(t, hc.Upd(), 1*time.Second)\n\t\trequire.True(t, ok)\n\n\t\tassert.True(t, strg.Equal(upd))\n\t})\n\n\tstrg.Add(ctx, r2)\n\n\tt.Run(\"second_refresh\", func(t *testing.T) {\n\t\ttestFS[\"dir/file2\"] = &fstest.MapFile{Data: r2Data}\n\t\teventsCh <- event{}\n\n\t\tupd, ok := testutil.RequireReceive(t, hc.Upd(), 1*time.Second)\n\t\trequire.True(t, ok)\n\n\t\tassert.True(t, strg.Equal(upd))\n\t})\n\n\tt.Run(\"double_refresh\", func(t *testing.T) {\n\t\t// Make a change once.\n\t\ttestFS[\"dir/file1\"] = &fstest.MapFile{Data: []byte(ipStr + \" alias\\n\")}\n\t\teventsCh <- event{}\n\n\t\t// Require the changes are written.\n\t\tcurrent, ok := testutil.RequireReceive(t, hc.Upd(), 1*time.Second)\n\t\trequire.True(t, ok)\n\n\t\trequire.Empty(t, current.ByName(\"hostname\"))\n\n\t\t// Make a change again.\n\t\ttestFS[\"dir/file2\"] = &fstest.MapFile{Data: []byte(ipStr + \" hostname\\n\")}\n\t\teventsCh <- event{}\n\n\t\t// Require the changes are written.\n\t\tcurrent, ok = testutil.RequireReceive(t, hc.Upd(), 1*time.Second)\n\t\trequire.True(t, ok)\n\n\t\trequire.NotEmpty(t, current.ByName(\"hostname\"))\n\t})\n}\n"
  },
  {
    "path": "internal/aghnet/ignore.go",
    "content": "package aghnet\n\nimport (\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/filterlist\"\n)\n\n// IgnoreEngine contains the list of rules for ignoring hostnames and matches\n// them.\n//\n// TODO(s.chzhen):  Move all urlfilter stuff to aghfilter.\ntype IgnoreEngine struct {\n\t// engine is the filtering engine that can match rules for ignoring\n\t// hostnames.\n\tengine *urlfilter.DNSEngine\n\n\t// ignored is the list of rules for ignoring hostnames.\n\tignored []string\n\n\t// enabled determines whether ignoring is enabled.\n\tenabled bool\n}\n\n// NewIgnoreEngine creates a new instance of the IgnoreEngine and stores the\n// list of rules for ignoring hostnames.  If enabled is set to false, hostnames\n// will never be ignored.\nfunc NewIgnoreEngine(ignored []string, enabled bool) (e *IgnoreEngine, err error) {\n\truleLists := []filterlist.Interface{\n\t\tfilterlist.NewString(&filterlist.StringConfig{\n\t\t\tRulesText:      strings.ToLower(strings.Join(ignored, \"\\n\")),\n\t\t\tIgnoreCosmetic: true,\n\t\t}),\n\t}\n\truleStorage, err := filterlist.NewRuleStorage(ruleLists)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &IgnoreEngine{\n\t\tengine:  urlfilter.NewDNSEngine(ruleStorage),\n\t\tignored: ignored,\n\t\tenabled: enabled,\n\t}, nil\n}\n\n// Has returns true if IgnoreEngine matches the host.\nfunc (e *IgnoreEngine) Has(host string) (ignore bool) {\n\tif e == nil || !e.enabled {\n\t\treturn false\n\t}\n\n\t_, ignore = e.engine.Match(host)\n\n\treturn ignore\n}\n\n// Values returns a copy of list of rules for ignoring hostnames.\nfunc (e *IgnoreEngine) Values() (ignored []string) {\n\treturn slices.Clone(e.ignored)\n}\n\n// IsEnabled returns true if hostnames ignoring is enabled.\nfunc (e *IgnoreEngine) IsEnabled() (enabled bool) {\n\treturn e.enabled\n}\n"
  },
  {
    "path": "internal/aghnet/ignore_test.go",
    "content": "package aghnet_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIgnoreEngine_Has(t *testing.T) {\n\thostnames := []string{\n\t\t\"*.example.com\",\n\t\t\"example.com\",\n\t\t\"|.^\",\n\t}\n\n\tengine, err := aghnet.NewIgnoreEngine(hostnames, true)\n\trequire.NotNil(t, engine)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname   string\n\t\thost   string\n\t\tignore bool\n\t}{{\n\t\tname:   \"basic\",\n\t\thost:   \"example.com\",\n\t\tignore: true,\n\t}, {\n\t\tname:   \"root\",\n\t\thost:   \".\",\n\t\tignore: true,\n\t}, {\n\t\tname:   \"wildcard\",\n\t\thost:   \"www.example.com\",\n\t\tignore: true,\n\t}, {\n\t\tname:   \"not_ignored\",\n\t\thost:   \"something.com\",\n\t\tignore: false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\trequire.Equal(t, tc.ignore, engine.Has(tc.host))\n\t}\n}\n"
  },
  {
    "path": "internal/aghnet/interfaces.go",
    "content": "package aghnet\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"time\"\n)\n\n// IPVersion is a alias for int for documentation purposes.  Use it when the\n// integer means IP version.\ntype IPVersion = int\n\n// IP version constants.\nconst (\n\tIPVersion4 IPVersion = 4\n\tIPVersion6 IPVersion = 6\n)\n\n// NetIface is the interface for network interface methods.\ntype NetIface interface {\n\tAddrs() ([]net.Addr, error)\n}\n\n// IfaceIPAddrs returns the interface's IP addresses.  iface must not be nil.\nfunc IfaceIPAddrs(iface NetIface, ipv IPVersion) (ips []net.IP, err error) {\n\tswitch ipv {\n\tcase IPVersion4, IPVersion6:\n\t\t// Go on.\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid ip version %d\", ipv)\n\t}\n\n\taddrs, err := iface.Addrs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, a := range addrs {\n\t\tif ip := ipFromAddr(a, ipv); ip != nil {\n\t\t\tips = append(ips, ip)\n\t\t}\n\t}\n\n\treturn ips, nil\n}\n\n// ipFromAddr converts addr to IP.  addr must not be nil.\nfunc ipFromAddr(addr net.Addr, ipv IPVersion) (ip net.IP) {\n\tswitch addr := addr.(type) {\n\tcase *net.IPAddr:\n\t\tip = addr.IP\n\tcase *net.IPNet:\n\t\tip = addr.IP\n\tdefault:\n\t\treturn nil\n\t}\n\n\t// Assume that net.Addr can only be valid IPv4 or IPv6.  Thus,\n\t// if it isn't an IPv4 address, it must be an IPv6 one.\n\tip4 := ip.To4()\n\tif ipv == IPVersion4 {\n\t\treturn ip4\n\t} else if ip4 == nil {\n\t\treturn ip\n\t}\n\n\treturn nil\n}\n\n// IfaceDNSIPAddrs returns IP addresses of the interface suitable to send to\n// clients as DNS addresses.  If err is nil, addrs contains either no addresses\n// or at least two.  l must not be nil.\n//\n// It makes up to maxAttempts attempts to get the addresses if there are none,\n// each time using the provided backoff.  Sometimes an interface needs a few\n// seconds to really initialize.\n//\n// See https://github.com/AdguardTeam/AdGuardHome/issues/2304.\nfunc IfaceDNSIPAddrs(\n\tctx context.Context,\n\tl *slog.Logger,\n\tiface NetIface,\n\tipv IPVersion,\n\tmaxAttempts int,\n\tbackoff time.Duration,\n) (addrs []net.IP, err error) {\n\tvar n int\n\tfor n = 1; n <= maxAttempts; n++ {\n\t\taddrs, err = IfaceIPAddrs(iface, ipv)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"getting ip addrs: %w\", err)\n\t\t}\n\n\t\tif len(addrs) > 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tl.DebugContext(ctx, \"no ip addresses\", \"attempt\", n, \"ipv\", ipv)\n\n\t\ttime.Sleep(backoff)\n\t}\n\n\tn--\n\n\tswitch len(addrs) {\n\tcase 0:\n\t\t// Don't return errors in case the users want to try and enable the DHCP\n\t\t// server later.\n\t\tt := time.Duration(n) * backoff\n\t\tl.ErrorContext(ctx, \"no ip addresses for iface\", \"attempts\", n, \"duration\", t, \"ipv\", ipv)\n\n\t\treturn nil, nil\n\tcase 1:\n\t\t// Some Android devices use 8.8.8.8 if there is not a secondary DNS\n\t\t// server.  Fix that by setting the secondary DNS address to the same\n\t\t// address.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/1708.\n\t\tl.DebugContext(ctx, \"setting secondary dns ip to itself\", \"ipv\", ipv)\n\t\taddrs = append(addrs, addrs[0])\n\tdefault:\n\t\t// Go on.\n\t}\n\n\tl.DebugContext(ctx, \"got addresses\", \"addrs\", addrs, \"attempts\", n, \"ipv\", ipv)\n\n\treturn addrs, nil\n}\n\n// interfaceName is a string containing network interface's name.  The name is\n// used in file walking methods.\ntype interfaceName string\n\n// Use interfaceName in the OS-independent code since it's actually only used in\n// several OS-dependent implementations which causes linting issues.\nvar _ = interfaceName(\"\")\n"
  },
  {
    "path": "internal/aghnet/interfaces_bsd.go",
    "content": "//go:build darwin || freebsd || openbsd\n\npackage aghnet\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"os\"\n\t\"syscall\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"golang.org/x/sys/unix\"\n)\n\n// reuseAddrCtrl is the function to be set to net.ListenConfig.Control.  It\n// configures the socket to have a reusable port binding.\nfunc reuseAddrCtrl(_, _ string, c syscall.RawConn) (err error) {\n\tcerr := c.Control(func(fd uintptr) {\n\t\t// TODO(e.burkov):  Consider using SO_REUSEPORT.\n\t\terr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)\n\t\tif err != nil {\n\t\t\terr = os.NewSyscallError(\"setsockopt\", err)\n\t\t}\n\t})\n\n\terr = errors.Join(err, cerr)\n\n\treturn errors.Annotate(err, \"setting control options: %w\")\n}\n\n// listenPacketReusable announces on the local network address additionally\n// configuring the socket to have a reusable binding.\nfunc listenPacketReusable(_, network, address string) (c net.PacketConn, err error) {\n\tvar lc net.ListenConfig\n\tlc.Control = reuseAddrCtrl\n\n\treturn lc.ListenPacket(context.Background(), network, address)\n}\n"
  },
  {
    "path": "internal/aghnet/interfaces_linux.go",
    "content": "//go:build linux\n\npackage aghnet\n\nimport (\n\t\"net\"\n\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4/nclient4\"\n)\n\n// listenPacketReusable announces on the local network address additionally\n// configuring the socket to have a reusable binding.\nfunc listenPacketReusable(ifaceName, network, address string) (c net.PacketConn, err error) {\n\tvar port uint16\n\t_, port, err = netutil.SplitHostPort(address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// TODO(e.burkov):  Inspect nclient4.NewRawUDPConn and implement here.\n\treturn nclient4.NewRawUDPConn(ifaceName, int(port))\n}\n"
  },
  {
    "path": "internal/aghnet/interfaces_test.go",
    "content": "package aghnet_test\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// fakeIface is a stub implementation of [aghnet.NetIface] interface to simplify\n// testing.\ntype fakeIface struct {\n\terr   error\n\taddrs []net.Addr\n}\n\n// Addrs implements the [aghnet.NetIface] interface for *fakeIface.\nfunc (iface *fakeIface) Addrs() (addrs []net.Addr, err error) {\n\tif iface.err != nil {\n\t\treturn nil, iface.err\n\t}\n\n\treturn iface.addrs, nil\n}\n\n// type check\nvar _ aghnet.NetIface = (*fakeIface)(nil)\n\nfunc TestIfaceIPAddrs(t *testing.T) {\n\tconst errTest errors.Error = \"test error\"\n\n\tip4 := net.IP{1, 2, 3, 4}\n\taddr4 := &net.IPNet{IP: ip4}\n\n\tip6 := net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}\n\taddr6 := &net.IPNet{IP: ip6}\n\n\ttestCases := []struct {\n\t\tiface      aghnet.NetIface\n\t\tname       string\n\t\twantErrMsg string\n\t\twant       []net.IP\n\t\tipv        aghnet.IPVersion\n\t}{{\n\t\tiface:      &fakeIface{addrs: []net.Addr{addr4}, err: nil},\n\t\tname:       \"ipv4_success\",\n\t\twantErrMsg: \"\",\n\t\twant:       []net.IP{ip4},\n\t\tipv:        aghnet.IPVersion4,\n\t}, {\n\t\tiface:      &fakeIface{addrs: []net.Addr{addr6, addr4}, err: nil},\n\t\tname:       \"ipv4_success_with_ipv6\",\n\t\twantErrMsg: \"\",\n\t\twant:       []net.IP{ip4},\n\t\tipv:        aghnet.IPVersion4,\n\t}, {\n\t\tiface:      &fakeIface{addrs: []net.Addr{addr4}, err: errTest},\n\t\tname:       \"ipv4_error\",\n\t\twantErrMsg: errTest.Error(),\n\t\twant:       nil,\n\t\tipv:        aghnet.IPVersion4,\n\t}, {\n\t\tiface:      &fakeIface{addrs: []net.Addr{addr6}, err: nil},\n\t\tname:       \"ipv6_success\",\n\t\twantErrMsg: \"\",\n\t\twant:       []net.IP{ip6},\n\t\tipv:        aghnet.IPVersion6,\n\t}, {\n\t\tiface:      &fakeIface{addrs: []net.Addr{addr6, addr4}, err: nil},\n\t\tname:       \"ipv6_success_with_ipv4\",\n\t\twantErrMsg: \"\",\n\t\twant:       []net.IP{ip6},\n\t\tipv:        aghnet.IPVersion6,\n\t}, {\n\t\tiface:      &fakeIface{addrs: []net.Addr{addr6}, err: errTest},\n\t\tname:       \"ipv6_error\",\n\t\twantErrMsg: errTest.Error(),\n\t\twant:       nil,\n\t\tipv:        aghnet.IPVersion6,\n\t}, {\n\t\tiface:      &fakeIface{addrs: nil, err: nil},\n\t\tname:       \"bad_proto\",\n\t\twantErrMsg: \"invalid ip version 10\",\n\t\twant:       nil,\n\t\tipv:        aghnet.IPVersion6 + aghnet.IPVersion4,\n\t}, {\n\t\tiface:      &fakeIface{addrs: []net.Addr{&net.IPAddr{IP: ip4}}, err: nil},\n\t\tname:       \"ipaddr_v4\",\n\t\twantErrMsg: \"\",\n\t\twant:       []net.IP{ip4},\n\t\tipv:        aghnet.IPVersion4,\n\t}, {\n\t\tiface:      &fakeIface{addrs: []net.Addr{&net.IPAddr{IP: ip6, Zone: \"\"}}, err: nil},\n\t\tname:       \"ipaddr_v6\",\n\t\twantErrMsg: \"\",\n\t\twant:       []net.IP{ip6},\n\t\tipv:        aghnet.IPVersion6,\n\t}, {\n\t\tiface:      &fakeIface{addrs: []net.Addr{&net.UnixAddr{}}, err: nil},\n\t\tname:       \"non-ipv4\",\n\t\twantErrMsg: \"\",\n\t\twant:       nil,\n\t\tipv:        aghnet.IPVersion4,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := aghnet.IfaceIPAddrs(tc.iface, tc.ipv)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\ntype waitingFakeIface struct {\n\terr   error\n\taddrs []net.Addr\n\tn     int\n}\n\n// type check\nvar _ aghnet.NetIface = (*waitingFakeIface)(nil)\n\n// Addrs implements the [aghnet.NetIface] interface for *waitingFakeIface.\nfunc (iface *waitingFakeIface) Addrs() (addrs []net.Addr, err error) {\n\tif iface.err != nil {\n\t\treturn nil, iface.err\n\t}\n\n\tif iface.n == 0 {\n\t\treturn iface.addrs, nil\n\t}\n\n\tiface.n--\n\n\treturn nil, nil\n}\n\nfunc TestIfaceDNSIPAddrs(t *testing.T) {\n\tconst errTest errors.Error = \"test error\"\n\n\tip4 := net.IP{1, 2, 3, 4}\n\taddr4 := &net.IPNet{IP: ip4}\n\n\tip6 := net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}\n\taddr6 := &net.IPNet{IP: ip6}\n\n\ttestCases := []struct {\n\t\tiface   aghnet.NetIface\n\t\twantErr error\n\t\tname    string\n\t\twant    []net.IP\n\t\tipv     aghnet.IPVersion\n\t}{{\n\t\tname:    \"ipv4_success\",\n\t\tiface:   &fakeIface{addrs: []net.Addr{addr4}, err: nil},\n\t\tipv:     aghnet.IPVersion4,\n\t\twant:    []net.IP{ip4, ip4},\n\t\twantErr: nil,\n\t}, {\n\t\tname:    \"ipv4_success_with_ipv6\",\n\t\tiface:   &fakeIface{addrs: []net.Addr{addr6, addr4}, err: nil},\n\t\tipv:     aghnet.IPVersion4,\n\t\twant:    []net.IP{ip4, ip4},\n\t\twantErr: nil,\n\t}, {\n\t\tname:    \"ipv4_error\",\n\t\tiface:   &fakeIface{addrs: []net.Addr{addr4}, err: errTest},\n\t\tipv:     aghnet.IPVersion4,\n\t\twant:    nil,\n\t\twantErr: errTest,\n\t}, {\n\t\tname:    \"ipv4_wait\",\n\t\tiface:   &waitingFakeIface{addrs: []net.Addr{addr4}, err: nil, n: 1},\n\t\tipv:     aghnet.IPVersion4,\n\t\twant:    []net.IP{ip4, ip4},\n\t\twantErr: nil,\n\t}, {\n\t\tname:    \"ipv6_success\",\n\t\tiface:   &fakeIface{addrs: []net.Addr{addr6}, err: nil},\n\t\tipv:     aghnet.IPVersion6,\n\t\twant:    []net.IP{ip6, ip6},\n\t\twantErr: nil,\n\t}, {\n\t\tname:    \"ipv6_success_with_ipv4\",\n\t\tiface:   &fakeIface{addrs: []net.Addr{addr6, addr4}, err: nil},\n\t\tipv:     aghnet.IPVersion6,\n\t\twant:    []net.IP{ip6, ip6},\n\t\twantErr: nil,\n\t}, {\n\t\tname:    \"ipv6_error\",\n\t\tiface:   &fakeIface{addrs: []net.Addr{addr6}, err: errTest},\n\t\tipv:     aghnet.IPVersion6,\n\t\twant:    nil,\n\t\twantErr: errTest,\n\t}, {\n\t\tname:    \"ipv6_wait\",\n\t\tiface:   &waitingFakeIface{addrs: []net.Addr{addr6}, err: nil, n: 1},\n\t\tipv:     aghnet.IPVersion6,\n\t\twant:    []net.IP{ip6, ip6},\n\t\twantErr: nil,\n\t}, {\n\t\tname:    \"empty\",\n\t\tiface:   &fakeIface{addrs: nil, err: nil},\n\t\tipv:     aghnet.IPVersion4,\n\t\twant:    nil,\n\t\twantErr: nil,\n\t}, {\n\t\tname:    \"many\",\n\t\tiface:   &fakeIface{addrs: []net.Addr{addr4, addr4}},\n\t\tipv:     aghnet.IPVersion4,\n\t\twant:    []net.IP{ip4, ip4},\n\t\twantErr: nil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tgot, err := aghnet.IfaceDNSIPAddrs(ctx, testLogger, tc.iface, tc.ipv, 2, 0)\n\t\t\trequire.ErrorIs(t, err, tc.wantErr)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/aghnet/ipmut.go",
    "content": "package aghnet\n\nimport (\n\t\"net\"\n\t\"sync/atomic\"\n)\n\n// IPMutFunc is the signature of a function which modifies the IP address\n// instance.  It should be safe for concurrent use.\ntype IPMutFunc func(ip net.IP)\n\n// nopIPMutFunc is the IPMutFunc that does nothing.\nfunc nopIPMutFunc(net.IP) {}\n\n// IPMut is a type-safe wrapper of atomic.Value to store the IPMutFunc.\ntype IPMut struct {\n\tf atomic.Value\n}\n\n// NewIPMut returns the new properly initialized *IPMut.  The m is guaranteed to\n// always store non-nil IPMutFunc which is safe to call.\nfunc NewIPMut(f IPMutFunc) (m *IPMut) {\n\tm = &IPMut{\n\t\tf: atomic.Value{},\n\t}\n\tm.Store(f)\n\n\treturn m\n}\n\n// Store sets the IPMutFunc to return from Func.  It's safe for concurrent use.\n// If f is nil, the stored function is the no-op one.\nfunc (m *IPMut) Store(f IPMutFunc) {\n\tif f == nil {\n\t\tf = nopIPMutFunc\n\t}\n\tm.f.Store(f)\n}\n\n// Load returns the previously stored IPMutFunc.\nfunc (m *IPMut) Load() (f IPMutFunc) {\n\treturn m.f.Load().(IPMutFunc)\n}\n"
  },
  {
    "path": "internal/aghnet/ipmut_test.go",
    "content": "package aghnet_test\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIPMut(t *testing.T) {\n\ttestIPs := []net.IP{{\n\t\t127, 0, 0, 1,\n\t}, {\n\t\t192, 168, 0, 1,\n\t}, {\n\t\t8, 8, 8, 8,\n\t}}\n\n\tt.Run(\"nil_no_mut\", func(t *testing.T) {\n\t\tipmut := aghnet.NewIPMut(nil)\n\n\t\tips := netutil.CloneIPs(testIPs)\n\t\tfor i := range ips {\n\t\t\tipmut.Load()(ips[i])\n\t\t\tassert.True(t, ips[i].Equal(testIPs[i]))\n\t\t}\n\t})\n\n\tt.Run(\"not_nil_mut\", func(t *testing.T) {\n\t\tipmut := aghnet.NewIPMut(func(ip net.IP) {\n\t\t\tfor i := range ip {\n\t\t\t\tip[i] = 0\n\t\t\t}\n\t\t})\n\t\twant := netutil.IPv4Zero()\n\n\t\tips := netutil.CloneIPs(testIPs)\n\t\tfor i := range ips {\n\t\t\tipmut.Load()(ips[i])\n\t\t\tassert.True(t, ips[i].Equal(want))\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/aghnet/net.go",
    "content": "// Package aghnet contains networking utilities.\npackage aghnet\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\n// DialContextFunc is the semantic alias for dialing functions, such as\n// [http.Transport.DialContext].\ntype DialContextFunc = func(ctx context.Context, network, addr string) (conn net.Conn, err error)\n\n// Variables and functions to substitute in tests.\nvar (\n\t// netInterfaceAddrs is the function to get the available network\n\t// interfaces.\n\tnetInterfaceAddrs = net.InterfaceAddrs\n\n\t// rootDirFS is the filesystem pointing to the root directory.\n\trootDirFS = osutil.RootDirFS()\n)\n\n// ErrNoStaticIPInfo is returned by IfaceHasStaticIP when no information about\n// whether the IP is static is available.\nconst ErrNoStaticIPInfo errors.Error = \"no information about static ip\"\n\n// IfaceHasStaticIP reports whether the interface has a static IP.  If the\n// status is indeterminate, it returns false with an error matching\n// [ErrNoStaticIPInfo].  cmdCons must not be nil.\nfunc IfaceHasStaticIP(\n\tctx context.Context,\n\tcmdCons executil.CommandConstructor,\n\tifaceName string,\n) (has bool, err error) {\n\treturn ifaceHasStaticIP(ctx, cmdCons, ifaceName)\n}\n\n// IfaceSetStaticIP sets a static IP address for network interface.  l and\n// cmdCons must not be nil.\nfunc IfaceSetStaticIP(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n\tifaceName string,\n) (err error) {\n\treturn ifaceSetStaticIP(ctx, l, cmdCons, ifaceName)\n}\n\n// GatewayIP returns the gateway IP address for the interface.  l and cmdCons\n// must not be nil.\n//\n// TODO(e.burkov):  Investigate if the gateway address may be fetched in another\n// way since not every machine has the software installed.\nfunc GatewayIP(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n\tifaceName string,\n) (ip netip.Addr) {\n\tstdout := bytes.Buffer{}\n\terr := executil.Run(\n\t\tctx,\n\t\tcmdCons,\n\t\t&executil.CommandConfig{\n\t\t\tPath:   \"ip\",\n\t\t\tArgs:   []string{\"route\", \"show\", \"dev\", ifaceName},\n\t\t\tStdout: &stdout,\n\t\t},\n\t)\n\tif err != nil {\n\t\tif code, ok := executil.ExitCodeFromError(err); ok {\n\t\t\terr = fmt.Errorf(\"unexpected exit code %d: %w\", code, err)\n\t\t}\n\n\t\tl.DebugContext(ctx, \"fetching gateway ip\", slogutil.KeyError, err)\n\n\t\treturn netip.Addr{}\n\t}\n\n\tfields := bytes.Fields(stdout.Bytes())\n\tif len(fields) < 3 || !bytes.Equal(fields[0], []byte(\"default\")) {\n\t\treturn netip.Addr{}\n\t}\n\n\tif err = ip.UnmarshalText(fields[2]); err != nil {\n\t\treturn netip.Addr{}\n\t}\n\n\treturn ip\n}\n\n// CanBindPrivilegedPorts checks if current process can bind to privileged\n// ports.  l must not be nil.\nfunc CanBindPrivilegedPorts(ctx context.Context, l *slog.Logger) (can bool, err error) {\n\treturn canBindPrivilegedPorts(ctx, l)\n}\n\n// NetInterface represents an entry of network interfaces map.\ntype NetInterface struct {\n\t// Addresses are the network interface addresses.\n\tAddresses []netip.Addr `json:\"ip_addresses,omitempty\"`\n\t// Subnets are the IP networks for this network interface.\n\tSubnets      []netip.Prefix   `json:\"-\"`\n\tName         string           `json:\"name\"`\n\tHardwareAddr net.HardwareAddr `json:\"hardware_address\"`\n\tFlags        net.Flags        `json:\"flags\"`\n\tMTU          int              `json:\"mtu\"`\n}\n\n// MarshalJSON implements the json.Marshaler interface for NetInterface.\nfunc (iface NetInterface) MarshalJSON() ([]byte, error) {\n\ttype netInterface NetInterface\n\treturn json.Marshal(&struct {\n\t\tHardwareAddr string `json:\"hardware_address\"`\n\t\tFlags        string `json:\"flags\"`\n\t\tnetInterface\n\t}{\n\t\tHardwareAddr: iface.HardwareAddr.String(),\n\t\tFlags:        iface.Flags.String(),\n\t\tnetInterface: netInterface(iface),\n\t})\n}\n\n// NetInterfaceFrom converts a [net.Interface] to [NetInterface], populating\n// name, MAC address, flags, MTU, IP addresses, and subnets.  iface must not be\n// nil.\nfunc NetInterfaceFrom(iface *net.Interface) (niface *NetInterface, err error) {\n\tniface = &NetInterface{\n\t\tName:         iface.Name,\n\t\tHardwareAddr: iface.HardwareAddr,\n\t\tFlags:        iface.Flags,\n\t\tMTU:          iface.MTU,\n\t}\n\n\taddrs, err := iface.Addrs()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"getting addresses for interface %q: %w\", iface.Name, err)\n\t}\n\n\tfor i, addr := range addrs {\n\t\tif err = populateAddrs(addr, niface); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"populating at index %d: %w\", i, err)\n\t\t}\n\t}\n\n\treturn niface, nil\n}\n\n// populateAddrs fills *NetInterface IP addresses and subnets.  addr and niface\n// must not be nil.\nfunc populateAddrs(addr net.Addr, niface *NetInterface) (err error) {\n\tn, err := ipNetFromAddr(addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tip, ok := netip.AddrFromSlice(n.IP)\n\tif !ok {\n\t\treturn fmt.Errorf(\"bad address %s\", n.IP)\n\t}\n\n\tip = ip.Unmap()\n\n\t// Skip link-local IPv4 addresses\n\tif isLinkLocalV4(ip) {\n\t\treturn nil\n\t}\n\n\tif ip.IsLinkLocalUnicast() {\n\t\tip = ip.WithZone(niface.Name)\n\t}\n\n\tones, _ := n.Mask.Size()\n\tp := netip.PrefixFrom(ip, ones)\n\n\tniface.Addresses = append(niface.Addresses, ip)\n\tniface.Subnets = append(niface.Subnets, p)\n\n\treturn nil\n}\n\n// ipNetFromAddr converts net.Addr to *net.IPNet and its IP to v4 if necessary.\nfunc ipNetFromAddr(addr net.Addr) (ip *net.IPNet, err error) {\n\tipNet, ok := addr.(*net.IPNet)\n\tif !ok {\n\t\t// Should be *net.IPNet, this is weird.\n\t\treturn nil, fmt.Errorf(\"bad type for interface net.Addr %T(%[1]v)\", ipNet)\n\t}\n\n\t// TODO(f.setrakov): Explore whether this logic can be safely removed.\n\tif ip4 := ipNet.IP.To4(); ip4 != nil {\n\t\tipNet.IP = ip4\n\t}\n\n\treturn ipNet, nil\n}\n\n// isLinkLocalV4 checks if ip is link-local unicast IPv4 address.\nfunc isLinkLocalV4(ip netip.Addr) (ok bool) {\n\treturn ip.Is4() && ip.IsLinkLocalUnicast()\n}\n\n// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and\n// WEB only we do not return link-local addresses here.\n//\n// TODO(e.burkov):  Can't properly test the function since it's nontrivial to\n// substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used.\nfunc GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) {\n\tifaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"getting interfaces: %w\", err)\n\t} else if len(ifaces) == 0 {\n\t\treturn nil, errors.Error(\"no legible interfaces\")\n\t}\n\n\tfor i := range ifaces {\n\t\tvar niface *NetInterface\n\t\tniface, err = NetInterfaceFrom(&ifaces[i])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t} else if len(niface.Addresses) != 0 {\n\t\t\t// Discard interfaces with no addresses.\n\t\t\tnifaces = append(nifaces, niface)\n\t\t}\n\t}\n\n\treturn nifaces, nil\n}\n\n// InterfaceByIP returns the name of the interface bound to ip.\n//\n// TODO(a.garipov, e.burkov): This function is technically incorrect, since one\n// IP address can be shared by multiple interfaces in some configurations.\n//\n// TODO(e.burkov):  See TODO on GetValidNetInterfacesForWeb.\nfunc InterfaceByIP(ip netip.Addr) (ifaceName string) {\n\tifaces, err := GetValidNetInterfacesForWeb()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tfor _, iface := range ifaces {\n\t\tfor _, addr := range iface.Addresses {\n\t\t\tif ip == addr {\n\t\t\t\treturn iface.Name\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// GetSubnet returns the subnet corresponding to the interface of zero prefix if\n// the search fails.  l must not be nil.\n//\n// TODO(e.burkov):  See TODO on GetValidNetInterfacesForWeb.\nfunc GetSubnet(ctx context.Context, l *slog.Logger, ifaceName string) (p netip.Prefix) {\n\tnetIfaces, err := GetValidNetInterfacesForWeb()\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"could not get network interfaces info\", slogutil.KeyError, err)\n\n\t\treturn p\n\t}\n\n\tfor _, netIface := range netIfaces {\n\t\tif netIface.Name == ifaceName && len(netIface.Subnets) > 0 {\n\t\t\treturn netIface.Subnets[0]\n\t\t}\n\t}\n\n\treturn p\n}\n\n// CheckPort checks if the port is available for binding.  network is expected\n// to be one of \"udp\" and \"tcp\".\nfunc CheckPort(network string, ipp netip.AddrPort) (err error) {\n\tvar c io.Closer\n\taddr := ipp.String()\n\tswitch network {\n\tcase \"tcp\":\n\t\tc, err = net.Listen(network, addr)\n\tcase \"udp\":\n\t\tc, err = net.ListenPacket(network, addr)\n\tdefault:\n\t\treturn nil\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn closePortChecker(c)\n}\n\n// IsAddrInUse checks if err is about unsuccessful address binding.\nfunc IsAddrInUse(err error) (ok bool) {\n\tvar sysErr syscall.Errno\n\tif !errors.As(err, &sysErr) {\n\t\treturn false\n\t}\n\n\treturn isAddrInUse(sysErr)\n}\n\n// CollectAllIfacesAddrs returns the slice of all network interfaces IP\n// addresses without port number.\nfunc CollectAllIfacesAddrs() (addrs []netip.Addr, err error) {\n\tvar ifaceAddrs []net.Addr\n\tifaceAddrs, err = netInterfaceAddrs()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"getting interfaces addresses: %w\", err)\n\t}\n\n\tfor _, addr := range ifaceAddrs {\n\t\tvar p netip.Prefix\n\t\tp, err = netip.ParsePrefix(addr.String())\n\t\tif err != nil {\n\t\t\t// Don't wrap the error since it's informative enough as is.\n\t\t\treturn nil, err\n\t\t}\n\n\t\taddrs = append(addrs, p.Addr())\n\t}\n\n\treturn addrs, nil\n}\n\n// ParseAddrPort parses an [netip.AddrPort] from s, which should be either a\n// valid IP, optionally with port, or a valid URL with plain IP address.  The\n// defaultPort is used if s doesn't contain port number.\nfunc ParseAddrPort(s string, defaultPort uint16) (ipp netip.AddrPort, err error) {\n\tu, err := url.Parse(s)\n\tif err == nil && u.Host != \"\" {\n\t\ts = u.Host\n\t}\n\n\tipp, err = netip.ParseAddrPort(s)\n\tif err != nil {\n\t\tip, parseErr := netip.ParseAddr(s)\n\t\tif parseErr != nil {\n\t\t\treturn ipp, errors.Join(err, parseErr)\n\t\t}\n\n\t\treturn netip.AddrPortFrom(ip, defaultPort), nil\n\t}\n\n\treturn ipp, nil\n}\n\n// ParseSubnet parses s either as a CIDR prefix itself, or as an IP address,\n// returning the corresponding single-IP CIDR prefix.\n//\n// TODO(e.burkov):  Taken from dnsproxy, move to golibs.\nfunc ParseSubnet(s string) (p netip.Prefix, err error) {\n\tif strings.Contains(s, \"/\") {\n\t\tp, err = netip.ParsePrefix(s)\n\t\tif err != nil {\n\t\t\treturn netip.Prefix{}, err\n\t\t}\n\t} else {\n\t\tvar ip netip.Addr\n\t\tip, err = netip.ParseAddr(s)\n\t\tif err != nil {\n\t\t\treturn netip.Prefix{}, err\n\t\t}\n\n\t\tp = netip.PrefixFrom(ip, ip.BitLen())\n\t}\n\n\treturn p, nil\n}\n\n// ParseBootstraps returns the slice of upstream resolvers parsed from addrs.\n// It additionally returns the closers for each resolver, that should be closed\n// after use.\nfunc ParseBootstraps(\n\taddrs []string,\n\topts *upstream.Options,\n) (boots []*upstream.UpstreamResolver, err error) {\n\tboots = make([]*upstream.UpstreamResolver, 0, len(boots))\n\tfor i, b := range addrs {\n\t\tvar r *upstream.UpstreamResolver\n\t\tr, err = upstream.NewUpstreamResolver(b, opts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"bootstrap at index %d: %w\", i, err)\n\t\t}\n\n\t\tboots = append(boots, r)\n\t}\n\n\treturn boots, nil\n}\n\n// BroadcastFromPref calculates the broadcast IP address for p.\nfunc BroadcastFromPref(p netip.Prefix) (bc netip.Addr) {\n\tbc = p.Addr().Unmap()\n\tif !bc.IsValid() {\n\t\treturn netip.Addr{}\n\t}\n\n\tmaskLen, addrLen := p.Bits(), bc.BitLen()\n\tif maskLen == addrLen {\n\t\treturn bc\n\t}\n\n\tipBytes := bc.AsSlice()\n\tfor i := maskLen; i < addrLen; i++ {\n\t\tipBytes[i/8] |= 1 << (7 - (i % 8))\n\t}\n\tbc, _ = netip.AddrFromSlice(ipBytes)\n\n\treturn bc\n}\n"
  },
  {
    "path": "internal/aghnet/net_bsd.go",
    "content": "//go:build darwin || freebsd || openbsd\n\npackage aghnet\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n)\n\nfunc canBindPrivilegedPorts(_ context.Context, _ *slog.Logger) (can bool, err error) {\n\treturn aghos.HaveAdminRights()\n}\n"
  },
  {
    "path": "internal/aghnet/net_darwin.go",
    "content": "//go:build darwin\n\npackage aghnet\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"regexp\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\n// hardwarePortInfo contains information about the current state of the internet\n// connection obtained from macOS networksetup.\ntype hardwarePortInfo struct {\n\tname      string\n\tip        string\n\tsubnet    string\n\tgatewayIP string\n\tstatic    bool\n}\n\n// ifaceHasStaticIP reports whether ifaceName is configured with a static IP.\n// cmdCons must not be nil.\nfunc ifaceHasStaticIP(\n\tctx context.Context,\n\tcmdCons executil.CommandConstructor,\n\tifaceName string,\n) (ok bool, err error) {\n\tportInfo, err := getCurrentHardwarePortInfo(ctx, cmdCons, ifaceName)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn portInfo.static, nil\n}\n\n// getCurrentHardwarePortInfo returns information for the specified network\n// interface.  cmdCons must not be nil.\nfunc getCurrentHardwarePortInfo(\n\tctx context.Context,\n\tcmdCons executil.CommandConstructor,\n\tifaceName string,\n) (hardwarePortInfo, error) {\n\t// First, find the hardware port name.\n\tm := getNetworkSetupHardwareReports(ctx, cmdCons)\n\thardwarePort, ok := m[ifaceName]\n\tif !ok {\n\t\treturn hardwarePortInfo{}, fmt.Errorf(\"could not find hardware port for %s\", ifaceName)\n\t}\n\n\treturn getHardwarePortInfo(ctx, cmdCons, hardwarePort)\n}\n\n// hardwareReportsReg is the regular expression matching the lines of\n// networksetup command output lines containing the interface information.\nvar hardwareReportsReg = regexp.MustCompile(\"Hardware Port: (.*?)\\nDevice: (.*?)\\n\")\n\n// getNetworkSetupHardwareReports returns a map of interface names to hardware\n// port names.  It returns nil if parsing fails.  cmdCons must not be nil.\n//\n// TODO(e.burkov):  There should be more proper approach than parsing the\n// command output.  For example, see\n// https://developer.apple.com/documentation/systemconfiguration.\nfunc getNetworkSetupHardwareReports(\n\tctx context.Context,\n\tcmdCons executil.CommandConstructor,\n) (reports map[string]string) {\n\t_, out, err := aghos.RunCommand(ctx, cmdCons, \"networksetup\", \"-listallhardwareports\")\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treports = make(map[string]string)\n\n\tmatches := hardwareReportsReg.FindAllSubmatch(out, -1)\n\tfor _, m := range matches {\n\t\treports[string(m[2])] = string(m[1])\n\t}\n\n\treturn reports\n}\n\n// hardwarePortReg is the regular expression matching the lines of networksetup\n// command output lines containing the port information.\nvar hardwarePortReg = regexp.MustCompile(\"IP address: (.*?)\\nSubnet mask: (.*?)\\nRouter: (.*?)\\n\")\n\n// getHardwarePortInfo returns IP, subnet, gateway, and static/dynamic status\n// for the given hardware port.  cmdCons must not be nil.\nfunc getHardwarePortInfo(\n\tctx context.Context,\n\tcmdCons executil.CommandConstructor,\n\thardwarePort string,\n) (h hardwarePortInfo, err error) {\n\t_, out, err := aghos.RunCommand(ctx, cmdCons, \"networksetup\", \"-getinfo\", hardwarePort)\n\tif err != nil {\n\t\treturn h, err\n\t}\n\n\tmatch := hardwarePortReg.FindSubmatch(out)\n\tif len(match) != 4 {\n\t\treturn h, errors.Error(\"could not find hardware port info\")\n\t}\n\n\treturn hardwarePortInfo{\n\t\tname:      hardwarePort,\n\t\tip:        string(match[1]),\n\t\tsubnet:    string(match[2]),\n\t\tgatewayIP: string(match[3]),\n\t\tstatic:    bytes.Index(out, []byte(\"Manual Configuration\")) == 0,\n\t}, nil\n}\n\n// ifaceSetStaticIP sets a static IP on ifaceName.  cmdCons must not be nil.\nfunc ifaceSetStaticIP(\n\tctx context.Context,\n\t_ *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n\tifaceName string,\n) (err error) {\n\tportInfo, err := getCurrentHardwarePortInfo(ctx, cmdCons, ifaceName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif portInfo.static {\n\t\treturn errors.Error(\"ip address is already static\")\n\t}\n\n\tdnsAddrs, err := getEtcResolvConfServers()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\targs := append([]string{\"-setdnsservers\", portInfo.name}, dnsAddrs...)\n\n\t// Setting DNS servers is necessary when configuring a static IP\n\tcode, _, err := aghos.RunCommand(ctx, cmdCons, \"networksetup\", args...)\n\tif err != nil {\n\t\treturn err\n\t} else if code != 0 {\n\t\treturn fmt.Errorf(\"failed to set DNS servers, code=%d\", code)\n\t}\n\n\t// Actually configures hardware port to have static IP\n\tcode, _, err = aghos.RunCommand(\n\t\tctx,\n\t\tcmdCons,\n\t\t\"networksetup\",\n\t\t\"-setmanual\",\n\t\tportInfo.name,\n\t\tportInfo.ip,\n\t\tportInfo.subnet,\n\t\tportInfo.gatewayIP,\n\t)\n\tif err != nil {\n\t\treturn err\n\t} else if code != 0 {\n\t\treturn fmt.Errorf(\"failed to set DNS servers, code=%d\", code)\n\t}\n\n\treturn nil\n}\n\n// etcResolvConfReg is the regular expression matching the lines of resolv.conf\n// file containing a name server information.\nvar etcResolvConfReg = regexp.MustCompile(\"nameserver ([a-zA-Z0-9.:]+)\")\n\n// getEtcResolvConfServers returns a list of nameservers configured in\n// /etc/resolv.conf.\nfunc getEtcResolvConfServers() (addrs []string, err error) {\n\tconst filename = \"etc/resolv.conf\"\n\n\t_, err = aghos.FileWalker(func(r io.Reader) (_ []string, _ bool, err error) {\n\t\tsc := bufio.NewScanner(r)\n\t\tfor sc.Scan() {\n\t\t\tmatches := etcResolvConfReg.FindAllStringSubmatch(sc.Text(), -1)\n\t\t\tif len(matches) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, m := range matches {\n\t\t\t\taddrs = append(addrs, m[1])\n\t\t\t}\n\t\t}\n\n\t\treturn nil, false, sc.Err()\n\t}).Walk(rootDirFS, filename)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing etc/resolv.conf file: %w\", err)\n\t} else if len(addrs) == 0 {\n\t\treturn nil, fmt.Errorf(\"found no dns servers in %s\", filename)\n\t}\n\n\treturn addrs, nil\n}\n"
  },
  {
    "path": "internal/aghnet/net_darwin_internal_test.go",
    "content": "//go:build darwin\n\npackage aghnet\n\nimport (\n\t\"io/fs\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/fakeio/fakefs\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIfaceHasStaticIP(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\tcmdCons    executil.CommandConstructor\n\t\tifaceName  string\n\t\twantHas    assert.BoolAssertionFunc\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"success\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -getinfo hwport\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"IP address: 1.2.3.4\\nSubnet mask: 255.255.255.0\\nRouter: 1.2.3.1\\n\",\n\t\t\tCode: 0,\n\t\t}),\n\t\tifaceName:  \"en0\",\n\t\twantHas:    assert.False,\n\t\twantErrMsg: ``,\n\t}, {\n\t\tname: \"success_static\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd: \"networksetup -getinfo hwport\",\n\t\t\tErr: nil,\n\t\t\tOut: \"Manual Configuration\\nIP address: 1.2.3.4\\n\" +\n\t\t\t\t\"Subnet mask: 255.255.255.0\\nRouter: 1.2.3.1\\n\",\n\t\t\tCode: 0,\n\t\t}),\n\t\tifaceName:  \"en0\",\n\t\twantHas:    assert.True,\n\t\twantErrMsg: ``,\n\t}, {\n\t\tname: \"reports_error\",\n\t\tcmdCons: agh.NewCommandConstructor(\n\t\t\t\"networksetup -listallhardwareports\",\n\t\t\t0,\n\t\t\t\"\",\n\t\t\terrors.Error(\"can't list\"),\n\t\t),\n\t\tifaceName:  \"en0\",\n\t\twantHas:    assert.False,\n\t\twantErrMsg: `could not find hardware port for en0`,\n\t}, {\n\t\tname: \"port_error\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -getinfo hwport\",\n\t\t\tErr:  errors.Error(\"can't get\"),\n\t\t\tOut:  ``,\n\t\t\tCode: 0,\n\t\t}),\n\t\tifaceName:  \"en0\",\n\t\twantHas:    assert.False,\n\t\twantErrMsg: `command \"networksetup\" failed: running: can't get: `,\n\t}, {\n\t\tname: \"port_bad_output\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -getinfo hwport\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"nothing meaningful\",\n\t\t\tCode: 0,\n\t\t}),\n\t\tifaceName:  \"en0\",\n\t\twantHas:    assert.False,\n\t\twantErrMsg: `could not find hardware port info`,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\thas, err := IfaceHasStaticIP(ctx, tc.cmdCons, tc.ifaceName)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\ttc.wantHas(t, has)\n\t\t})\n\t}\n}\n\nfunc TestIfaceSetStaticIP(t *testing.T) {\n\tsuccFsys := fstest.MapFS{\n\t\t\"etc/resolv.conf\": &fstest.MapFile{\n\t\t\tData: []byte(`nameserver 1.1.1.1`),\n\t\t},\n\t}\n\tpanicFsys := &fakefs.FS{\n\t\tOnOpen: func(name string) (_ fs.File, _ error) { panic(testutil.UnexpectedCall(name)) },\n\t}\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tcmdCons    executil.CommandConstructor\n\t\tfsys       fs.FS\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"success\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -getinfo hwport\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"IP address: 1.2.3.4\\nSubnet mask: 255.255.255.0\\nRouter: 1.2.3.1\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -setdnsservers hwport 1.1.1.1\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"\",\n\t\t\tCode: 0,\n\t\t}),\n\t\tfsys:       succFsys,\n\t\twantErrMsg: ``,\n\t}, {\n\t\tname: \"static_already\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd: \"networksetup -getinfo hwport\",\n\t\t\tErr: nil,\n\t\t\tOut: \"Manual Configuration\\nIP address: 1.2.3.4\\n\" +\n\t\t\t\t\"Subnet mask: 255.255.255.0\\nRouter: 1.2.3.1\\n\",\n\t\t\tCode: 0,\n\t\t}),\n\t\tfsys:       panicFsys,\n\t\twantErrMsg: `ip address is already static`,\n\t}, {\n\t\tname: \"reports_error\",\n\t\tcmdCons: agh.NewCommandConstructor(\n\t\t\t\"networksetup -listallhardwareports\",\n\t\t\t0,\n\t\t\t\"\",\n\t\t\terrors.Error(\"can't list\"),\n\t\t),\n\t\tfsys:       panicFsys,\n\t\twantErrMsg: `could not find hardware port for en0`,\n\t}, {\n\t\tname: \"resolv_conf_error\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -getinfo hwport\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"IP address: 1.2.3.4\\nSubnet mask: 255.255.255.0\\nRouter: 1.2.3.1\\n\",\n\t\t\tCode: 0,\n\t\t},\n\t\t),\n\t\tfsys: fstest.MapFS{\n\t\t\t\"etc/resolv.conf\": &fstest.MapFile{\n\t\t\t\tData: []byte(\"this resolv.conf is invalid\"),\n\t\t\t},\n\t\t},\n\t\twantErrMsg: `found no dns servers in etc/resolv.conf`,\n\t}, {\n\t\tname: \"set_dns_error\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -getinfo hwport\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"IP address: 1.2.3.4\\nSubnet mask: 255.255.255.0\\nRouter: 1.2.3.1\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -setdnsservers hwport 1.1.1.1\",\n\t\t\tErr:  errors.Error(\"can't set\"),\n\t\t\tOut:  \"\",\n\t\t\tCode: 0,\n\t\t}),\n\t\tfsys:       succFsys,\n\t\twantErrMsg: `command \"networksetup\" failed: running: can't set: `,\n\t}, {\n\t\tname: \"set_manual_error\",\n\t\tcmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -listallhardwareports\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"Hardware Port: hwport\\nDevice: en0\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -getinfo hwport\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"IP address: 1.2.3.4\\nSubnet mask: 255.255.255.0\\nRouter: 1.2.3.1\\n\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -setdnsservers hwport 1.1.1.1\",\n\t\t\tErr:  nil,\n\t\t\tOut:  \"\",\n\t\t\tCode: 0,\n\t\t}, agh.ExternalCommand{\n\t\t\tCmd:  \"networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1\",\n\t\t\tErr:  errors.Error(\"can't set\"),\n\t\t\tOut:  \"\",\n\t\t\tCode: 0,\n\t\t}),\n\t\tfsys:       succFsys,\n\t\twantErrMsg: `command \"networksetup\" failed: running: can't set: `,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tsubstRootDirFS(t, tc.fsys)\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := IfaceSetStaticIP(ctx, testLogger, tc.cmdCons, \"en0\")\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/aghnet/net_freebsd.go",
    "content": "//go:build freebsd\n\npackage aghnet\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\nfunc ifaceHasStaticIP(\n\t_ context.Context,\n\t_ executil.CommandConstructor,\n\tifaceName string,\n) (ok bool, err error) {\n\tconst rcConfFilename = \"etc/rc.conf\"\n\n\twalker := aghos.FileWalker(interfaceName(ifaceName).rcConfStaticConfig)\n\n\treturn walker.Walk(rootDirFS, rcConfFilename)\n}\n\n// rcConfStaticConfig checks if the interface is configured by /etc/rc.conf to\n// have a static IP.\nfunc (n interfaceName) rcConfStaticConfig(r io.Reader) (_ []string, cont bool, err error) {\n\ts := bufio.NewScanner(r)\n\tfor pref := fmt.Sprintf(\"ifconfig_%s=\", n); s.Scan(); {\n\t\tline := strings.TrimSpace(s.Text())\n\t\tif !strings.HasPrefix(line, pref) {\n\t\t\tcontinue\n\t\t}\n\n\t\tcfgLeft, cfgRight := len(pref)+1, len(line)-1\n\t\tif cfgLeft >= cfgRight {\n\t\t\tcontinue\n\t\t}\n\n\t\t// TODO(e.burkov):  Expand the check to cover possible\n\t\t// configurations from man rc.conf(5).\n\t\tfields := strings.Fields(line[cfgLeft:cfgRight])\n\t\tswitch {\n\t\tcase\n\t\t\tlen(fields) < 2,\n\t\t\t!strings.EqualFold(fields[0], \"inet\"),\n\t\t\t!netutil.IsValidIPString(fields[1]):\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn nil, false, s.Err()\n\t\t}\n\t}\n\n\treturn nil, true, s.Err()\n}\n\nfunc ifaceSetStaticIP(\n\t_ context.Context,\n\t_ *slog.Logger,\n\t_ executil.CommandConstructor,\n\t_ string,\n) (err error) {\n\treturn aghos.Unsupported(\"setting static ip\")\n}\n"
  },
  {
    "path": "internal/aghnet/net_freebsd_internal_test.go",
    "content": "//go:build freebsd\n\npackage aghnet\n\nimport (\n\t\"io/fs\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIfaceHasStaticIP(t *testing.T) {\n\tconst (\n\t\tifaceName = `em0`\n\t\trcConf    = \"etc/rc.conf\"\n\t)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\trootFsys fs.FS\n\t\twantHas  assert.BoolAssertionFunc\n\t}{{\n\t\tname: \"simple\",\n\t\trootFsys: fstest.MapFS{rcConf: &fstest.MapFile{\n\t\t\tData: []byte(`ifconfig_` + ifaceName + `=\"inet 127.0.0.253 netmask 0xffffffff\"` + nl),\n\t\t}},\n\t\twantHas: assert.True,\n\t}, {\n\t\tname: \"case_insensitiveness\",\n\t\trootFsys: fstest.MapFS{rcConf: &fstest.MapFile{\n\t\t\tData: []byte(`ifconfig_` + ifaceName + `=\"InEt 127.0.0.253 NeTmAsK 0xffffffff\"` + nl),\n\t\t}},\n\t\twantHas: assert.True,\n\t}, {\n\t\tname: \"comments_and_trash\",\n\t\trootFsys: fstest.MapFS{rcConf: &fstest.MapFile{\n\t\t\tData: []byte(`# comment 1` + nl +\n\t\t\t\t`` + nl +\n\t\t\t\t`# comment 2` + nl +\n\t\t\t\t`ifconfig_` + ifaceName + `=\"inet 127.0.0.253 netmask 0xffffffff\"` + nl,\n\t\t\t),\n\t\t}},\n\t\twantHas: assert.True,\n\t}, {\n\t\tname: \"aliases\",\n\t\trootFsys: fstest.MapFS{rcConf: &fstest.MapFile{\n\t\t\tData: []byte(`ifconfig_` + ifaceName + `_alias=\"inet 127.0.0.1/24\"` + nl +\n\t\t\t\t`ifconfig_` + ifaceName + `=\"inet 127.0.0.253 netmask 0xffffffff\"` + nl,\n\t\t\t),\n\t\t}},\n\t\twantHas: assert.True,\n\t}, {\n\t\tname: \"incorrect_config\",\n\t\trootFsys: fstest.MapFS{rcConf: &fstest.MapFile{\n\t\t\tData: []byte(\n\t\t\t\t`ifconfig_` + ifaceName + `=\"inet6 127.0.0.253 netmask 0xffffffff\"` + nl +\n\t\t\t\t\t`ifconfig_` + ifaceName + `=\"inet 256.256.256.256 netmask 0xffffffff\"` + nl +\n\t\t\t\t\t`ifconfig_` + ifaceName + `=\"\"` + nl,\n\t\t\t),\n\t\t}},\n\t\twantHas: assert.False,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tsubstRootDirFS(t, tc.rootFsys)\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\thas, err := IfaceHasStaticIP(ctx, testCmdCons, ifaceName)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.wantHas(t, has)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/aghnet/net_internal_test.go",
    "content": "package aghnet\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testCmdCons is the common command constructor for tests.\nvar testCmdCons = executil.EmptyCommandConstructor{}\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\n// substRootDirFS replaces the aghos.RootDirFS function used throughout the\n// package with fsys for tests ran under t.\nfunc substRootDirFS(tb testing.TB, fsys fs.FS) {\n\ttb.Helper()\n\n\tprev := rootDirFS\n\ttb.Cleanup(func() { rootDirFS = prev })\n\trootDirFS = fsys\n}\n\n// RunCmdFunc is the signature of aghos.RunCommand function.\ntype RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)\n\n// ifaceAddrsFunc is the signature of net.InterfaceAddrs function.\ntype ifaceAddrsFunc func() (ifaces []net.Addr, err error)\n\n// substNetInterfaceAddrs replaces the the net.InterfaceAddrs function used\n// throughout the package with f for tests ran under t.\nfunc substNetInterfaceAddrs(tb testing.TB, f ifaceAddrsFunc) {\n\ttb.Helper()\n\n\tprev := netInterfaceAddrs\n\ttb.Cleanup(func() { netInterfaceAddrs = prev })\n\tnetInterfaceAddrs = f\n}\n\nfunc TestGatewayIP(t *testing.T) {\n\tt.Parallel()\n\n\tconst ifaceName = \"ifaceName\"\n\tconst cmd = \"ip route show dev \" + ifaceName\n\n\ttestCases := []struct {\n\t\tcmdCons executil.CommandConstructor\n\t\twant    netip.Addr\n\t\tname    string\n\t}{{\n\t\tcmdCons: agh.NewCommandConstructor(cmd, 0, `default via 1.2.3.4 onlink`, nil),\n\t\twant:    netip.MustParseAddr(\"1.2.3.4\"),\n\t\tname:    \"success_v4\",\n\t}, {\n\t\tcmdCons: agh.NewCommandConstructor(cmd, 0, `default via ::ffff onlink`, nil),\n\t\twant:    netip.MustParseAddr(\"::ffff\"),\n\t\tname:    \"success_v6\",\n\t}, {\n\t\tcmdCons: agh.NewCommandConstructor(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),\n\t\twant:    netip.Addr{},\n\t\tname:    \"bad_output\",\n\t}, {\n\t\tcmdCons: agh.NewCommandConstructor(cmd, 0, \"\", errors.Error(\"can't run command\")),\n\t\twant:    netip.Addr{},\n\t\tname:    \"err_runcmd\",\n\t}, {\n\t\tcmdCons: agh.NewCommandConstructor(cmd, 1, \"\", nil),\n\t\twant:    netip.Addr{},\n\t\tname:    \"bad_code\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tassert.Equal(t, tc.want, GatewayIP(ctx, testLogger, tc.cmdCons, ifaceName))\n\t\t})\n\t}\n}\n\nfunc TestInterfaceByIP(t *testing.T) {\n\tifaces, err := GetValidNetInterfacesForWeb()\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, ifaces)\n\n\tfor _, iface := range ifaces {\n\t\tt.Run(iface.Name, func(t *testing.T) {\n\t\t\trequire.NotEmpty(t, iface.Addresses)\n\n\t\t\tfor _, ip := range iface.Addresses {\n\t\t\t\tifaceName := InterfaceByIP(ip)\n\t\t\t\trequire.Equal(t, iface.Name, ifaceName)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBroadcastFromIPNet(t *testing.T) {\n\tknown4 := netip.MustParseAddr(\"192.168.0.1\")\n\tfullBroadcast4 := netip.MustParseAddr(\"255.255.255.255\")\n\n\tknown6 := netip.MustParseAddr(\"102:304:506:708:90a:b0c:d0e:f10\")\n\n\ttestCases := []struct {\n\t\tpref netip.Prefix\n\t\twant netip.Addr\n\t\tname string\n\t}{{\n\t\tpref: netip.PrefixFrom(known4, 0),\n\t\twant: fullBroadcast4,\n\t\tname: \"full\",\n\t}, {\n\t\tpref: netip.PrefixFrom(known4, 20),\n\t\twant: netip.MustParseAddr(\"192.168.15.255\"),\n\t\tname: \"full\",\n\t}, {\n\t\tpref: netip.PrefixFrom(known6, netutil.IPv6BitLen),\n\t\twant: known6,\n\t\tname: \"ipv6_no_mask\",\n\t}, {\n\t\tpref: netip.PrefixFrom(known4, netutil.IPv4BitLen),\n\t\twant: known4,\n\t\tname: \"ipv4_no_mask\",\n\t}, {\n\t\tpref: netip.PrefixFrom(netip.IPv4Unspecified(), 0),\n\t\twant: fullBroadcast4,\n\t\tname: \"unspecified\",\n\t}, {\n\t\tpref: netip.Prefix{},\n\t\twant: netip.Addr{},\n\t\tname: \"invalid\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.want, BroadcastFromPref(tc.pref))\n\t\t})\n\t}\n}\n\nfunc TestCheckPort(t *testing.T) {\n\tladdr := netip.AddrPortFrom(netutil.IPv4Localhost(), 0)\n\n\tt.Run(\"tcp_bound\", func(t *testing.T) {\n\t\tl, err := net.Listen(\"tcp\", laddr.String())\n\t\trequire.NoError(t, err)\n\t\ttestutil.CleanupAndRequireSuccess(t, l.Close)\n\n\t\tipp := testutil.RequireTypeAssert[*net.TCPAddr](t, l.Addr()).AddrPort()\n\t\trequire.Equal(t, laddr.Addr(), ipp.Addr())\n\t\trequire.NotZero(t, ipp.Port())\n\n\t\terr = CheckPort(\"tcp\", ipp)\n\t\ttarget := &net.OpError{}\n\t\trequire.ErrorAs(t, err, &target)\n\n\t\tassert.Equal(t, \"listen\", target.Op)\n\t})\n\n\tt.Run(\"udp_bound\", func(t *testing.T) {\n\t\tconn, err := net.ListenPacket(\"udp\", laddr.String())\n\t\trequire.NoError(t, err)\n\t\ttestutil.CleanupAndRequireSuccess(t, conn.Close)\n\n\t\tipp := testutil.RequireTypeAssert[*net.UDPAddr](t, conn.LocalAddr()).AddrPort()\n\t\trequire.Equal(t, laddr.Addr(), ipp.Addr())\n\t\trequire.NotZero(t, ipp.Port())\n\n\t\terr = CheckPort(\"udp\", ipp)\n\t\ttarget := &net.OpError{}\n\t\trequire.ErrorAs(t, err, &target)\n\n\t\tassert.Equal(t, \"listen\", target.Op)\n\t})\n\n\tt.Run(\"bad_network\", func(t *testing.T) {\n\t\terr := CheckPort(\"bad_network\", netip.AddrPortFrom(netip.Addr{}, 0))\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"can_bind\", func(t *testing.T) {\n\t\terr := CheckPort(\"udp\", netip.AddrPortFrom(netip.IPv4Unspecified(), 0))\n\t\tassert.NoError(t, err)\n\t})\n}\n\nfunc TestCollectAllIfacesAddrs(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\taddrs      []net.Addr\n\t\twantAddrs  []netip.Addr\n\t}{{\n\t\tname:       \"success\",\n\t\twantErrMsg: ``,\n\t\taddrs: []net.Addr{&net.IPNet{\n\t\t\tIP:   net.IP{1, 2, 3, 4},\n\t\t\tMask: net.CIDRMask(24, netutil.IPv4BitLen),\n\t\t}, &net.IPNet{\n\t\t\tIP:   net.IP{4, 3, 2, 1},\n\t\t\tMask: net.CIDRMask(16, netutil.IPv4BitLen),\n\t\t}},\n\t\twantAddrs: []netip.Addr{\n\t\t\tnetip.MustParseAddr(\"1.2.3.4\"),\n\t\t\tnetip.MustParseAddr(\"4.3.2.1\"),\n\t\t},\n\t}, {\n\t\tname:       \"not_cidr\",\n\t\twantErrMsg: `netip.ParsePrefix(\"1.2.3.4\"): no '/'`,\n\t\taddrs: []net.Addr{&net.IPAddr{\n\t\t\tIP: net.IP{1, 2, 3, 4},\n\t\t}},\n\t\twantAddrs: nil,\n\t}, {\n\t\tname:       \"empty\",\n\t\twantErrMsg: ``,\n\t\taddrs:      []net.Addr{},\n\t\twantAddrs:  nil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tsubstNetInterfaceAddrs(t, func() ([]net.Addr, error) { return tc.addrs, nil })\n\n\t\t\taddrs, err := CollectAllIfacesAddrs()\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.wantAddrs, addrs)\n\t\t})\n\t}\n\n\tt.Run(\"internal_error\", func(t *testing.T) {\n\t\tconst errAddrs errors.Error = \"can't get addresses\"\n\n\t\tsubstNetInterfaceAddrs(t, func() ([]net.Addr, error) { return nil, errAddrs })\n\n\t\t_, err := CollectAllIfacesAddrs()\n\t\tassert.ErrorIs(t, err, errAddrs)\n\t})\n}\n\nfunc TestIsAddrInUse(t *testing.T) {\n\tt.Run(\"addr_in_use\", func(t *testing.T) {\n\t\tl, err := net.Listen(\"tcp\", \"0.0.0.0:0\")\n\t\trequire.NoError(t, err)\n\t\ttestutil.CleanupAndRequireSuccess(t, l.Close)\n\n\t\t_, err = net.Listen(l.Addr().Network(), l.Addr().String())\n\t\tassert.True(t, IsAddrInUse(err))\n\t})\n\n\tt.Run(\"another\", func(t *testing.T) {\n\t\tconst anotherErr errors.Error = \"not addr in use\"\n\n\t\tassert.False(t, IsAddrInUse(anotherErr))\n\t})\n}\n\nfunc TestNetInterface_MarshalJSON(t *testing.T) {\n\tconst want = `{` +\n\t\t`\"hardware_address\":\"aa:bb:cc:dd:ee:ff\",` +\n\t\t`\"flags\":\"up|multicast\",` +\n\t\t`\"ip_addresses\":[\"1.2.3.4\",\"aaaa::1\"],` +\n\t\t`\"name\":\"iface0\",` +\n\t\t`\"mtu\":1500` +\n\t\t`}` + \"\\n\"\n\n\tip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})\n\trequire.True(t, ok)\n\n\tip6, ok := netip.AddrFromSlice([]byte{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})\n\trequire.True(t, ok)\n\n\tnet4 := netip.PrefixFrom(ip4, 24)\n\tnet6 := netip.PrefixFrom(ip6, 8)\n\n\tiface := &NetInterface{\n\t\tAddresses:    []netip.Addr{ip4, ip6},\n\t\tSubnets:      []netip.Prefix{net4, net6},\n\t\tName:         \"iface0\",\n\t\tHardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},\n\t\tFlags:        net.FlagUp | net.FlagMulticast,\n\t\tMTU:          1500,\n\t}\n\n\tb := &bytes.Buffer{}\n\terr := json.NewEncoder(b).Encode(iface)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, want, b.String())\n}\n"
  },
  {
    "path": "internal/aghnet/net_linux.go",
    "content": "//go:build linux\n\npackage aghnet\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n\t\"github.com/google/renameio/v2/maybe\"\n\t\"golang.org/x/sys/unix\"\n)\n\n// dhcpсdConf is the name of /etc/dhcpcd.conf file in the root filesystem.\nconst dhcpcdConf = \"etc/dhcpcd.conf\"\n\nfunc canBindPrivilegedPorts(ctx context.Context, l *slog.Logger) (can bool, err error) {\n\tres, err := unix.PrctlRetInt(\n\t\tunix.PR_CAP_AMBIENT,\n\t\tunix.PR_CAP_AMBIENT_IS_SET,\n\t\tunix.CAP_NET_BIND_SERVICE,\n\t\t0,\n\t\t0,\n\t)\n\tif err != nil {\n\t\tif errors.Is(err, unix.EINVAL) {\n\t\t\t// Older versions of Linux kernel do not support this.  Print a\n\t\t\t// warning and check admin rights.\n\t\t\tl.WarnContext(\n\t\t\t\tctx,\n\t\t\t\t\"checking capability cap_net_bind_service\",\n\t\t\t\tslogutil.KeyError, err,\n\t\t\t)\n\t\t} else {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\t// Don't check the error because it's always nil on Linux.\n\tadm, _ := aghos.HaveAdminRights()\n\n\treturn res == 1 || adm, nil\n}\n\n// dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to\n// have a static IP.\nfunc (n interfaceName) dhcpcdStaticConfig(r io.Reader) (subsources []string, cont bool, err error) {\n\ts := bufio.NewScanner(r)\n\tif !findIfaceLine(s, string(n)) {\n\t\treturn nil, true, s.Err()\n\t}\n\n\tfor s.Scan() {\n\t\tline := strings.TrimSpace(s.Text())\n\t\tfields := strings.Fields(line)\n\t\tif len(fields) >= 2 &&\n\t\t\tfields[0] == \"static\" &&\n\t\t\tstrings.HasPrefix(fields[1], \"ip_address=\") {\n\t\t\treturn nil, false, s.Err()\n\t\t}\n\n\t\tif len(fields) > 0 && fields[0] == \"interface\" {\n\t\t\t// Another interface found.\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn nil, true, s.Err()\n}\n\n// ifacesStaticConfig checks if the interface is configured by any file of\n// /etc/network/interfaces format to have a static IP.\nfunc (n interfaceName) ifacesStaticConfig(r io.Reader) (sub []string, cont bool, err error) {\n\ts := bufio.NewScanner(r)\n\tfor s.Scan() {\n\t\tline := strings.TrimSpace(s.Text())\n\t\tif len(line) == 0 || line[0] == '#' {\n\t\t\tcontinue\n\t\t}\n\n\t\t// TODO(e.burkov): As man page interfaces(5) says, a line may be\n\t\t// extended across multiple lines by making the last character a\n\t\t// backslash.  Provide extended lines support.\n\n\t\tfields := strings.Fields(line)\n\t\tfieldsNum := len(fields)\n\n\t\t// Man page interfaces(5) declares that interface definition should\n\t\t// consist of the key word \"iface\" followed by interface name, and\n\t\t// method at fourth field.\n\t\tif fieldsNum >= 4 &&\n\t\t\tfields[0] == \"iface\" && fields[1] == string(n) && fields[3] == \"static\" {\n\t\t\treturn nil, false, nil\n\t\t}\n\n\t\tif fieldsNum >= 2 && fields[0] == \"source\" {\n\t\t\tsub = append(sub, fields[1])\n\t\t}\n\t}\n\n\treturn sub, true, s.Err()\n}\n\nfunc ifaceHasStaticIP(\n\t_ context.Context,\n\t_ executil.CommandConstructor,\n\tifaceName string,\n) (has bool, err error) {\n\t// TODO(a.garipov): Currently, this function returns the first definitive\n\t// result.  So if /etc/dhcpcd.conf has and /etc/network/interfaces has no\n\t// static IP configuration, it will return true.  Perhaps this is not the\n\t// most desirable behavior.\n\n\tiface := interfaceName(ifaceName)\n\n\tfor _, pair := range [...]struct {\n\t\taghos.FileWalker\n\t\tfilename string\n\t}{{\n\t\tFileWalker: iface.dhcpcdStaticConfig,\n\t\tfilename:   dhcpcdConf,\n\t}, {\n\t\tFileWalker: iface.ifacesStaticConfig,\n\t\tfilename:   \"etc/network/interfaces\",\n\t}} {\n\t\thas, err = pair.Walk(rootDirFS, pair.filename)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t} else if has {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, ErrNoStaticIPInfo\n}\n\n// findIfaceLine scans s until it finds the line that declares an interface with\n// the given name.  If findIfaceLine can't find the line, it returns false.\nfunc findIfaceLine(s *bufio.Scanner, name string) (ok bool) {\n\tfor s.Scan() {\n\t\tline := strings.TrimSpace(s.Text())\n\t\tfields := strings.Fields(line)\n\t\tif len(fields) == 2 && fields[0] == \"interface\" && fields[1] == name {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ifaceSetStaticIP configures the system to retain its current IP on the\n// interface through dhcpcd.conf.  l and cmdCons must not be nil.\nfunc ifaceSetStaticIP(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n\tifaceName string,\n) (err error) {\n\tipNet := GetSubnet(ctx, l, ifaceName)\n\tif !ipNet.Addr().IsValid() {\n\t\treturn errors.Error(\"can't get IP address\")\n\t}\n\n\tbody, err := os.ReadFile(dhcpcdConf)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tgatewayIP := GatewayIP(ctx, l, cmdCons, ifaceName)\n\tadd := dhcpcdConfIface(ifaceName, ipNet, gatewayIP)\n\n\tbody = append(body, []byte(add)...)\n\terr = maybe.WriteFile(dhcpcdConf, body, 0o644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"writing conf: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that\n// configure the interface to have a static IP.\nfunc dhcpcdConfIface(ifaceName string, subnet netip.Prefix, gateway netip.Addr) (conf string) {\n\tb := &strings.Builder{}\n\tstringutil.WriteToBuilder(\n\t\tb,\n\t\t\"\\n# \",\n\t\tifaceName,\n\t\t\" added by AdGuard Home.\\ninterface \",\n\t\tifaceName,\n\t\t\"\\nstatic ip_address=\",\n\t\tsubnet.String(),\n\t\t\"\\n\",\n\t)\n\n\tif gateway != (netip.Addr{}) {\n\t\tstringutil.WriteToBuilder(b, \"static routers=\", gateway.String(), \"\\n\")\n\t}\n\n\tstringutil.WriteToBuilder(b, \"static domain_name_servers=\", subnet.Addr().String(), \"\\n\\n\")\n\n\treturn b.String()\n}\n"
  },
  {
    "path": "internal/aghnet/net_linux_internal_test.go",
    "content": "//go:build linux\n\npackage aghnet\n\nimport (\n\t\"io/fs\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHasStaticIP(t *testing.T) {\n\tconst ifaceName = \"wlan0\"\n\n\tconst (\n\t\tdhcpcd    = \"etc/dhcpcd.conf\"\n\t\tnetifaces = \"etc/network/interfaces\"\n\t)\n\n\ttestCases := []struct {\n\t\trootFsys   fs.FS\n\t\tname       string\n\t\twantHas    assert.BoolAssertionFunc\n\t\twantErrMsg string\n\t}{{\n\t\trootFsys: fstest.MapFS{\n\t\t\tdhcpcd: &fstest.MapFile{\n\t\t\t\tData: []byte(`#comment` + nl +\n\t\t\t\t\t`# comment` + nl +\n\t\t\t\t\t`interface eth0` + nl +\n\t\t\t\t\t`static ip_address=192.168.0.1/24` + nl +\n\t\t\t\t\t`# interface ` + ifaceName + nl +\n\t\t\t\t\t`static ip_address=192.168.1.1/24` + nl +\n\t\t\t\t\t`# comment` + nl,\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\tname:       \"dhcpcd_has_not\",\n\t\twantHas:    assert.False,\n\t\twantErrMsg: `no information about static ip`,\n\t}, {\n\t\trootFsys: fstest.MapFS{\n\t\t\tdhcpcd: &fstest.MapFile{\n\t\t\t\tData: []byte(`#comment` + nl +\n\t\t\t\t\t`# comment` + nl +\n\t\t\t\t\t`interface ` + ifaceName + nl +\n\t\t\t\t\t`static ip_address=192.168.0.1/24` + nl +\n\t\t\t\t\t`# interface ` + ifaceName + nl +\n\t\t\t\t\t`static ip_address=192.168.1.1/24` + nl +\n\t\t\t\t\t`# comment` + nl,\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\tname:       \"dhcpcd_has\",\n\t\twantHas:    assert.True,\n\t\twantErrMsg: ``,\n\t}, {\n\t\trootFsys: fstest.MapFS{\n\t\t\tnetifaces: &fstest.MapFile{\n\t\t\t\tData: []byte(`allow-hotplug ` + ifaceName + nl +\n\t\t\t\t\t`#iface enp0s3 inet static` + nl +\n\t\t\t\t\t`#  address 192.168.0.200` + nl +\n\t\t\t\t\t`#  netmask 255.255.255.0` + nl +\n\t\t\t\t\t`#  gateway 192.168.0.1` + nl +\n\t\t\t\t\t`iface ` + ifaceName + ` inet dhcp` + nl,\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\tname:       \"netifaces_has_not\",\n\t\twantHas:    assert.False,\n\t\twantErrMsg: `no information about static ip`,\n\t}, {\n\t\trootFsys: fstest.MapFS{\n\t\t\tnetifaces: &fstest.MapFile{\n\t\t\t\tData: []byte(`allow-hotplug ` + ifaceName + nl +\n\t\t\t\t\t`iface ` + ifaceName + ` inet static` + nl +\n\t\t\t\t\t`  address 192.168.0.200` + nl +\n\t\t\t\t\t`  netmask 255.255.255.0` + nl +\n\t\t\t\t\t`  gateway 192.168.0.1` + nl +\n\t\t\t\t\t`#iface ` + ifaceName + ` inet dhcp` + nl,\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\tname:       \"netifaces_has\",\n\t\twantHas:    assert.True,\n\t\twantErrMsg: ``,\n\t}, {\n\t\trootFsys: fstest.MapFS{\n\t\t\tnetifaces: &fstest.MapFile{\n\t\t\t\tData: []byte(`source hello` + nl +\n\t\t\t\t\t`#iface ` + ifaceName + ` inet static` + nl,\n\t\t\t\t),\n\t\t\t},\n\t\t\t\"hello\": &fstest.MapFile{\n\t\t\t\tData: []byte(`iface ` + ifaceName + ` inet static` + nl),\n\t\t\t},\n\t\t},\n\t\tname:       \"netifaces_another_file\",\n\t\twantHas:    assert.True,\n\t\twantErrMsg: ``,\n\t}, {\n\t\trootFsys: fstest.MapFS{\n\t\t\tnetifaces: &fstest.MapFile{\n\t\t\t\tData: []byte(`source hello` + nl +\n\t\t\t\t\t`iface ` + ifaceName + ` inet static` + nl,\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\tname:       \"netifaces_ignore_another\",\n\t\twantHas:    assert.True,\n\t\twantErrMsg: ``,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tsubstRootDirFS(t, tc.rootFsys)\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\thas, err := IfaceHasStaticIP(ctx, testCmdCons, ifaceName)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\ttc.wantHas(t, has)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/aghnet/net_openbsd.go",
    "content": "//go:build openbsd\n\npackage aghnet\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\nfunc ifaceHasStaticIP(\n\t_ context.Context,\n\t_ executil.CommandConstructor,\n\tifaceName string,\n) (ok bool, err error) {\n\tfilename := fmt.Sprintf(\"etc/hostname.%s\", ifaceName)\n\n\treturn aghos.FileWalker(hostnameIfStaticConfig).Walk(rootDirFS, filename)\n}\n\n// hostnameIfStaticConfig checks if the interface is configured by\n// /etc/hostname.* to have a static IP.\nfunc hostnameIfStaticConfig(r io.Reader) (_ []string, ok bool, err error) {\n\ts := bufio.NewScanner(r)\n\tfor s.Scan() {\n\t\tline := strings.TrimSpace(s.Text())\n\t\tfields := strings.Fields(line)\n\t\tswitch {\n\t\tcase\n\t\t\tlen(fields) < 2,\n\t\t\tfields[0] != \"inet\",\n\t\t\t!netutil.IsValidIPString(fields[1]):\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn nil, false, s.Err()\n\t\t}\n\t}\n\n\treturn nil, true, s.Err()\n}\n\nfunc ifaceSetStaticIP(\n\t_ context.Context,\n\t_ *slog.Logger,\n\t_ executil.CommandConstructor,\n\t_ string,\n) (err error) {\n\treturn aghos.Unsupported(\"setting static ip\")\n}\n"
  },
  {
    "path": "internal/aghnet/net_openbsd_internal_test.go",
    "content": "//go:build openbsd\n\npackage aghnet\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIfaceHasStaticIP(t *testing.T) {\n\tconst ifaceName = \"em0\"\n\n\tconfFile := fmt.Sprintf(\"etc/hostname.%s\", ifaceName)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\trootFsys fs.FS\n\t\twantHas  assert.BoolAssertionFunc\n\t}{{\n\t\tname: \"simple\",\n\t\trootFsys: fstest.MapFS{\n\t\t\tconfFile: &fstest.MapFile{\n\t\t\t\tData: []byte(`inet 127.0.0.253` + nl),\n\t\t\t},\n\t\t},\n\t\twantHas: assert.True,\n\t}, {\n\t\tname: \"case_sensitiveness\",\n\t\trootFsys: fstest.MapFS{\n\t\t\tconfFile: &fstest.MapFile{\n\t\t\t\tData: []byte(`InEt 127.0.0.253` + nl),\n\t\t\t},\n\t\t},\n\t\twantHas: assert.False,\n\t}, {\n\t\tname: \"comments_and_trash\",\n\t\trootFsys: fstest.MapFS{\n\t\t\tconfFile: &fstest.MapFile{\n\t\t\t\tData: []byte(`# comment 1` + nl + nl +\n\t\t\t\t\t`# inet 127.0.0.253` + nl +\n\t\t\t\t\t`inet` + nl,\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\twantHas: assert.False,\n\t}, {\n\t\tname: \"incorrect_config\",\n\t\trootFsys: fstest.MapFS{\n\t\t\tconfFile: &fstest.MapFile{\n\t\t\t\tData: []byte(`inet6 127.0.0.253` + nl + `inet 256.256.256.256` + nl),\n\t\t\t},\n\t\t},\n\t\twantHas: assert.False,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tsubstRootDirFS(t, tc.rootFsys)\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\thas, err := IfaceHasStaticIP(ctx, testCmdCons, ifaceName)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.wantHas(t, has)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/aghnet/net_test.go",
    "content": "package aghnet_test\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// testTimeout is a common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\nfunc TestMain(m *testing.M) {\n\ttestutil.DiscardLogOutput(m)\n}\n\nfunc TestParseAddrPort(t *testing.T) {\n\tconst defaultPort = 1\n\n\tv4addr := netip.MustParseAddr(\"1.2.3.4\")\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tinput      string\n\t\twantErrMsg string\n\t\twant       netip.AddrPort\n\t}{{\n\t\tname:       \"success_ip\",\n\t\tinput:      v4addr.String(),\n\t\twantErrMsg: \"\",\n\t\twant:       netip.AddrPortFrom(v4addr, defaultPort),\n\t}, {\n\t\tname:       \"success_ip_port\",\n\t\tinput:      netutil.JoinHostPort(v4addr.String(), 5),\n\t\twantErrMsg: \"\",\n\t\twant:       netip.AddrPortFrom(v4addr, 5),\n\t}, {\n\t\tname: \"success_url\",\n\t\tinput: (&url.URL{\n\t\t\tScheme: \"tcp\",\n\t\t\tHost:   v4addr.String(),\n\t\t}).String(),\n\t\twantErrMsg: \"\",\n\t\twant:       netip.AddrPortFrom(v4addr, defaultPort),\n\t}, {\n\t\tname: \"success_url_port\",\n\t\tinput: (&url.URL{\n\t\t\tScheme: \"tcp\",\n\t\t\tHost:   netutil.JoinHostPort(v4addr.String(), 5),\n\t\t}).String(),\n\t\twantErrMsg: \"\",\n\t\twant:       netip.AddrPortFrom(v4addr, 5),\n\t}, {\n\t\tname:  \"error_invalid_ip\",\n\t\tinput: \"256.256.256.256\",\n\t\twantErrMsg: `not an ip:port\nParseAddr(\"256.256.256.256\"): IPv4 field has value >255`,\n\t\twant: netip.AddrPort{},\n\t}, {\n\t\tname:  \"error_invalid_port\",\n\t\tinput: net.JoinHostPort(v4addr.String(), \"-5\"),\n\t\twantErrMsg: `invalid port \"-5\" parsing \"1.2.3.4:-5\"\nParseAddr(\"1.2.3.4:-5\"): unexpected character (at \":-5\")`,\n\t\twant: netip.AddrPort{},\n\t}, {\n\t\tname:  \"error_invalid_url\",\n\t\tinput: \"tcp:://1.2.3.4\",\n\t\twantErrMsg: `invalid port \"//1.2.3.4\" parsing \"tcp:://1.2.3.4\"\nParseAddr(\"tcp:://1.2.3.4\"): each colon-separated field must have at least ` +\n\t\t\t`one digit (at \"tcp:://1.2.3.4\")`,\n\t\twant: netip.AddrPort{},\n\t}, {\n\t\tname:  \"empty\",\n\t\tinput: \"\",\n\t\twant:  netip.AddrPort{},\n\t\twantErrMsg: `not an ip:port\nParseAddr(\"\"): unable to parse IP`,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tap, err := aghnet.ParseAddrPort(tc.input, defaultPort)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, ap)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/aghnet/net_unix.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage aghnet\n\nimport (\n\t\"io\"\n\t\"syscall\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// closePortChecker closes c.  c must be non-nil.\nfunc closePortChecker(c io.Closer) (err error) {\n\treturn c.Close()\n}\n\nfunc isAddrInUse(err syscall.Errno) (ok bool) {\n\treturn errors.Is(err, syscall.EADDRINUSE)\n}\n"
  },
  {
    "path": "internal/aghnet/net_windows.go",
    "content": "//go:build windows\n\npackage aghnet\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"log/slog\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc canBindPrivilegedPorts(_ context.Context, _ *slog.Logger) (can bool, err error) {\n\treturn true, nil\n}\n\nfunc ifaceHasStaticIP(\n\t_ context.Context,\n\t_ executil.CommandConstructor,\n\t_ string,\n) (ok bool, err error) {\n\treturn false, aghos.Unsupported(\"checking static ip\")\n}\n\nfunc ifaceSetStaticIP(\n\t_ context.Context,\n\t_ *slog.Logger,\n\t_ executil.CommandConstructor,\n\t_ string,\n) (err error) {\n\treturn aghos.Unsupported(\"setting static ip\")\n}\n\n// closePortChecker closes c.  c must be non-nil.\nfunc closePortChecker(c io.Closer) (err error) {\n\tif err = c.Close(); err != nil {\n\t\treturn err\n\t}\n\n\t// It seems that net.Listener.Close() doesn't close file descriptors right\n\t// away.  We wait for some time and hope that this fd will be closed.\n\t//\n\t// TODO(e.burkov):  Investigate the purpose of the line and perhaps use more\n\t// reliable approach.\n\ttime.Sleep(100 * time.Millisecond)\n\n\treturn nil\n}\n\nfunc isAddrInUse(err syscall.Errno) (ok bool) {\n\treturn errors.Is(err, windows.WSAEADDRINUSE)\n}\n"
  },
  {
    "path": "internal/aghnet/upstream.go",
    "content": "package aghnet\n\nimport \"github.com/AdguardTeam/dnsproxy/upstream\"\n\n// UpstreamHTTPVersions returns the HTTP versions for upstream configuration\n// depending on configuration.\nfunc UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {\n\tif !http3 {\n\t\treturn upstream.DefaultHTTPVersions\n\t}\n\n\treturn []upstream.HTTPVersion{\n\t\tupstream.HTTPVersion3,\n\t\tupstream.HTTPVersion2,\n\t\tupstream.HTTPVersion11,\n\t}\n}\n\n// IsCommentOrEmpty returns true if s starts with a \"#\" character or is empty.\n// This function is useful for filtering out non-upstream lines from upstream\n// configs.\nfunc IsCommentOrEmpty(s string) (ok bool) {\n\treturn len(s) == 0 || s[0] == '#'\n}\n"
  },
  {
    "path": "internal/aghnet/upstream_test.go",
    "content": "package aghnet_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsCommentOrEmpty(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\twant assert.BoolAssertionFunc\n\t\tstr  string\n\t}{{\n\t\twant: assert.True,\n\t\tstr:  \"\",\n\t}, {\n\t\twant: assert.True,\n\t\tstr:  \"# comment\",\n\t}, {\n\t\twant: assert.False,\n\t\tstr:  \"1.2.3.4\",\n\t}} {\n\t\ttc.want(t, aghnet.IsCommentOrEmpty(tc.str))\n\t}\n}\n"
  },
  {
    "path": "internal/aghos/filewalker.go",
    "content": "package aghos\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// FileWalker is the signature of a function called for files in the file tree.\n// As opposed to filepath.Walk it only walk the files (not directories) matching\n// the provided pattern and those returned by function itself.  All patterns\n// should be valid for fs.Glob.  If FileWalker returns false for cont then\n// walking terminates.  Prefer using bufio.Scanner to read the r since the input\n// is not limited.\n//\n// TODO(e.burkov, a.garipov):  Move into another package like aghfs.\n//\n// TODO(e.burkov):  Think about passing filename or any additional data.\ntype FileWalker func(r io.Reader) (patterns []string, cont bool, err error)\n\n// checkFile tries to open and process a single file located on sourcePath in\n// the specified fsys.  The path is skipped if it's a directory.\nfunc checkFile(\n\tfsys fs.FS,\n\tc FileWalker,\n\tsourcePath string,\n) (patterns []string, cont bool, err error) {\n\tvar f fs.File\n\tf, err = fsys.Open(sourcePath)\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t// Ignore non-existing files since this may only happen when the\n\t\t\t// file was removed after filepath.Glob matched it.\n\t\t\treturn nil, true, nil\n\t\t}\n\n\t\treturn nil, false, err\n\t}\n\tdefer func() { err = errors.WithDeferred(err, f.Close()) }()\n\n\tvar fi fs.FileInfo\n\tif fi, err = f.Stat(); err != nil {\n\t\treturn nil, true, err\n\t} else if fi.IsDir() {\n\t\t// Skip the directories.\n\t\treturn nil, true, nil\n\t}\n\n\treturn c(f)\n}\n\n// handlePatterns parses the patterns in fsys and ignores duplicates using\n// srcSet.  srcSet must be non-nil.\nfunc handlePatterns(\n\tfsys fs.FS,\n\tsrcSet *container.MapSet[string],\n\tpatterns ...string,\n) (sub []string, err error) {\n\tsub = make([]string, 0, len(patterns))\n\tfor _, p := range patterns {\n\t\tvar matches []string\n\t\tmatches, err = fs.Glob(fsys, p)\n\t\tif err != nil {\n\t\t\t// Enrich error with the pattern because filepath.Glob\n\t\t\t// doesn't do it.\n\t\t\treturn nil, fmt.Errorf(\"invalid pattern %q: %w\", p, err)\n\t\t}\n\n\t\tfor _, m := range matches {\n\t\t\tif srcSet.Has(m) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsrcSet.Add(m)\n\t\t\tsub = append(sub, m)\n\t\t}\n\t}\n\n\treturn sub, nil\n}\n\n// Walk starts walking the files in fsys defined by patterns from initial.\n// It only returns true if fw signed to stop walking.\nfunc (fw FileWalker) Walk(fsys fs.FS, initial ...string) (ok bool, err error) {\n\t// The slice of sources keeps the order in which the files are walked since\n\t// srcSet.Values() returns strings in undefined order.\n\tsrcSet := container.NewMapSet[string]()\n\tvar src []string\n\tsrc, err = handlePatterns(fsys, srcSet, initial...)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar filename string\n\tdefer func() { err = errors.Annotate(err, \"checking %q: %w\", filename) }()\n\n\t// TODO(e.burkov):  Redo this loop, as it modifies the very same slice it\n\t// iterates over.\n\tfor i := 0; i < len(src); i++ {\n\t\tvar patterns []string\n\t\tvar cont bool\n\t\tfilename = src[i]\n\t\tpatterns, cont, err = checkFile(fsys, fw, src[i])\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif !cont {\n\t\t\treturn true, nil\n\t\t}\n\n\t\tvar subsrc []string\n\t\tsubsrc, err = handlePatterns(fsys, srcSet, patterns...)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tsrc = append(src, subsrc...)\n\t}\n\n\treturn false, nil\n}\n"
  },
  {
    "path": "internal/aghos/filewalker_internal_test.go",
    "content": "package aghos\n\nimport (\n\t\"io/fs\"\n\t\"path\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// errFS is an fs.FS implementation, method Open of which always returns\n// errFSOpen.\ntype errFS struct{}\n\n// errFSOpen is returned from errFS.Open.\nconst errFSOpen errors.Error = \"test open error\"\n\n// Open implements the fs.FS interface for *errFS.  fsys is always nil and err\n// is always errFSOpen.\nfunc (efs *errFS) Open(name string) (fsys fs.File, err error) {\n\treturn nil, errFSOpen\n}\n\nfunc TestWalkerFunc_CheckFile(t *testing.T) {\n\temptyFS := fstest.MapFS{}\n\n\tt.Run(\"non-existing\", func(t *testing.T) {\n\t\t_, ok, err := checkFile(emptyFS, nil, \"lol\")\n\t\trequire.NoError(t, err)\n\n\t\tassert.True(t, ok)\n\t})\n\n\tt.Run(\"invalid_argument\", func(t *testing.T) {\n\t\t_, ok, err := checkFile(&errFS{}, nil, \"\")\n\t\trequire.ErrorIs(t, err, errFSOpen)\n\n\t\tassert.False(t, ok)\n\t})\n\n\tt.Run(\"ignore_dirs\", func(t *testing.T) {\n\t\tconst dirName = \"dir\"\n\n\t\ttestFS := fstest.MapFS{\n\t\t\tpath.Join(dirName, \"file\"): &fstest.MapFile{Data: []byte{}},\n\t\t}\n\n\t\tpatterns, ok, err := checkFile(testFS, nil, dirName)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, patterns)\n\t\tassert.True(t, ok)\n\t})\n}\n"
  },
  {
    "path": "internal/aghos/filewalker_test.go",
    "content": "package aghos_test\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"path\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Common file-walker constants.\nconst (\n\tattribute = \"000\"\n\tnl        = \"\\n\"\n)\n\n// newFileWalker returns a new file-walker function that reads patterns from an\n// [io.Reader].\nfunc newFileWalker() (fw aghos.FileWalker) {\n\treturn func(r io.Reader) (patterns []string, cont bool, err error) {\n\t\ts := bufio.NewScanner(r)\n\t\tfor s.Scan() {\n\t\t\tline := s.Text()\n\t\t\tif line == attribute {\n\t\t\t\treturn nil, false, nil\n\t\t\t}\n\n\t\t\tif len(line) != 0 {\n\t\t\t\tpatterns = append(patterns, path.Join(\".\", line))\n\t\t\t}\n\t\t}\n\n\t\treturn patterns, true, s.Err()\n\t}\n}\n\nfunc TestFileWalker_Walk(t *testing.T) {\n\ttestCases := []struct {\n\t\ttestFS      fstest.MapFS\n\t\twant        assert.BoolAssertionFunc\n\t\tinitPattern string\n\t\tname        string\n\t}{{\n\t\tname: \"simple\",\n\t\ttestFS: fstest.MapFS{\n\t\t\t\"simple_0001.txt\": &fstest.MapFile{Data: []byte(attribute + nl)},\n\t\t},\n\t\tinitPattern: \"simple_0001.txt\",\n\t\twant:        assert.True,\n\t}, {\n\t\tname: \"chain\",\n\t\ttestFS: fstest.MapFS{\n\t\t\t\"chain_0001.txt\": &fstest.MapFile{Data: []byte(`chain_0002.txt` + nl)},\n\t\t\t\"chain_0002.txt\": &fstest.MapFile{Data: []byte(`chain_0003.txt` + nl)},\n\t\t\t\"chain_0003.txt\": &fstest.MapFile{Data: []byte(attribute + nl)},\n\t\t},\n\t\tinitPattern: \"chain_0001.txt\",\n\t\twant:        assert.True,\n\t}, {\n\t\tname: \"several\",\n\t\ttestFS: fstest.MapFS{\n\t\t\t\"several_0001.txt\": &fstest.MapFile{Data: []byte(`several_*` + nl)},\n\t\t\t\"several_0002.txt\": &fstest.MapFile{Data: []byte(`several_0001.txt` + nl)},\n\t\t\t\"several_0003.txt\": &fstest.MapFile{Data: []byte(attribute + nl)},\n\t\t},\n\t\tinitPattern: \"several_0001.txt\",\n\t\twant:        assert.True,\n\t}, {\n\t\tname: \"no\",\n\t\ttestFS: fstest.MapFS{\n\t\t\t\"no_0001.txt\": &fstest.MapFile{Data: []byte(nl)},\n\t\t\t\"no_0002.txt\": &fstest.MapFile{Data: []byte(nl)},\n\t\t\t\"no_0003.txt\": &fstest.MapFile{Data: []byte(nl)},\n\t\t},\n\t\tinitPattern: \"no_*\",\n\t\twant:        assert.False,\n\t}, {\n\t\tname: \"subdirectory\",\n\t\ttestFS: fstest.MapFS{\n\t\t\tpath.Join(\"dir\", \"subdir_0002.txt\"): &fstest.MapFile{\n\t\t\t\tData: []byte(attribute + nl),\n\t\t\t},\n\t\t\t\"subdir_0001.txt\": &fstest.MapFile{Data: []byte(`dir/*`)},\n\t\t},\n\t\tinitPattern: \"subdir_0001.txt\",\n\t\twant:        assert.True,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tfw := newFileWalker()\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tok, err := fw.Walk(tc.testFS, tc.initPattern)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.want(t, ok)\n\t\t})\n\t}\n\n\tt.Run(\"pattern_malformed\", func(t *testing.T) {\n\t\tf := fstest.MapFS{}\n\t\tok, err := newFileWalker().Walk(f, \"[]\")\n\t\trequire.Error(t, err)\n\n\t\tassert.False(t, ok)\n\t\tassert.ErrorIs(t, err, path.ErrBadPattern)\n\t})\n\n\tt.Run(\"bad_filename\", func(t *testing.T) {\n\t\tconst filename = \"bad_filename.txt\"\n\n\t\tf := fstest.MapFS{\n\t\t\tfilename: &fstest.MapFile{Data: []byte(\"[]\")},\n\t\t}\n\t\tok, err := aghos.FileWalker(func(r io.Reader) (patterns []string, cont bool, err error) {\n\t\t\ts := bufio.NewScanner(r)\n\t\t\tfor s.Scan() {\n\t\t\t\tpatterns = append(patterns, s.Text())\n\t\t\t}\n\n\t\t\treturn patterns, true, s.Err()\n\t\t}).Walk(f, filename)\n\t\trequire.Error(t, err)\n\n\t\tassert.False(t, ok)\n\t\tassert.ErrorIs(t, err, path.ErrBadPattern)\n\t})\n\n\tt.Run(\"itself_error\", func(t *testing.T) {\n\t\tconst rerr errors.Error = \"returned error\"\n\n\t\tf := fstest.MapFS{\n\t\t\t\"mockfile.txt\": &fstest.MapFile{Data: []byte(`mockdata`)},\n\t\t}\n\n\t\tok, err := aghos.FileWalker(func(r io.Reader) (patterns []string, ok bool, err error) {\n\t\t\treturn nil, true, rerr\n\t\t}).Walk(f, \"*\")\n\t\trequire.ErrorIs(t, err, rerr)\n\n\t\tassert.False(t, ok)\n\t})\n}\n"
  },
  {
    "path": "internal/aghos/fswatcher.go",
    "content": "package aghos\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/service\"\n\t\"github.com/fsnotify/fsnotify\"\n)\n\n// Event is a convenient alias for an empty struct to signal that watched file\n// event happened.\ntype Event = struct{}\n\n// FSWatcher tracks all the file system events and notifies about those.\n//\n// TODO(e.burkov, a.garipov): Move into another package like aghfs.\ntype FSWatcher interface {\n\tservice.Interface\n\n\t// Events returns the channel to notify about the file system events.\n\tEvents() (e <-chan Event)\n\n\t// Add starts tracking the file.  It returns an error if the file can't be\n\t// tracked.  Adding the same file multiple times must not result in an\n\t// error.\n\tAdd(name string) (err error)\n\n\t// Remove stops tracking the file.  Removing a non-tracked file must not\n\t// result in an error.\n\tRemove(name string) (err error)\n}\n\n// OSWatcherConfig is the configuration structure for [NewOSWatcher].\n//\n// TODO(e.burkov):  Consider using [os.Root].\ntype OSWatcherConfig struct {\n\t// Logger is used for logging the operations of watcher.  It must not be\n\t// nil.\n\tLogger *slog.Logger\n}\n\n// OSWatcher tracks the file system provided by the OS.\n//\n// TODO(e.burkov):  Add tests.\ntype OSWatcher struct {\n\t// logger is used for logging the operations of watcher.\n\tlogger *slog.Logger\n\n\t// filesMu protects files.\n\tfilesMu *sync.RWMutex\n\n\t// watcher is the actual notifier.\n\twatcher *fsnotify.Watcher\n\n\t// events is the channel to notify.\n\tevents chan Event\n\n\t// files maps directories to the files tracked in them.  If the tracked file\n\t// is a directory, it is mapped to itself.\n\tfiles map[string]*container.MapSet[string]\n}\n\n// osWatcherPref is a prefix for logging and wrapping errors in osWathcer's\n// methods.\nconst osWatcherPref = \"os_watcher\"\n\n// NewOSWatcher creates an [FSWatcher] that tracks the file system of the OS.  c\n// must not be nil.\nfunc NewOSWatcher(c *OSWatcherConfig) (w *OSWatcher, err error) {\n\tdefer func() { err = errors.Annotate(err, \"%s: %w\", osWatcherPref) }()\n\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating watcher: %w\", err)\n\t}\n\n\treturn &OSWatcher{\n\t\tlogger:  c.Logger,\n\t\tfilesMu: &sync.RWMutex{},\n\t\twatcher: watcher,\n\t\tevents:  make(chan Event, 1),\n\t\tfiles:   map[string]*container.MapSet[string]{},\n\t}, nil\n}\n\n// type check\nvar _ FSWatcher = (*OSWatcher)(nil)\n\n// Start implements the [service.Interface] interface for *OSWatcher.\nfunc (w *OSWatcher) Start(ctx context.Context) (err error) {\n\tgo w.handleErrors(ctx)\n\tgo w.handleEvents(ctx)\n\n\treturn nil\n}\n\n// Shutdown implements the [service.Interface] interface for *OSWatcher.\nfunc (w *OSWatcher) Shutdown(_ context.Context) (err error) {\n\treturn errors.Annotate(w.watcher.Close(), \"%s: %w\", osWatcherPref)\n}\n\n// Events implements the [FSWatcher] interface for *OSWatcher.\nfunc (w *OSWatcher) Events() (e <-chan Event) {\n\treturn w.events\n}\n\n// Add implements the [FSWatcher] interface for *OSWatcher.  It's safe for\n// concurrent use.\n//\n// TODO(e.burkov):  Make it accept non-existing files to detect it's creating.\nfunc (w *OSWatcher) Add(name string) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"%s: %w\", osWatcherPref) }()\n\n\tfi, err := os.Stat(name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checking file %q: %w\", name, err)\n\t}\n\n\t// Watch the directory and filter the events by the file name, since the\n\t// common recomendation to the fsnotify package is to watch the directory\n\t// instead of the file itself.\n\t//\n\t// See https://pkg.go.dev/github.com/fsnotify/fsnotify@v1.7.0#readme-watching-a-file-doesn-t-work-well.\n\tdirName := name\n\tif !fi.IsDir() {\n\t\tdirName = filepath.Dir(name)\n\t}\n\n\tw.filesMu.Lock()\n\tdefer w.filesMu.Unlock()\n\n\tnames := w.files[dirName]\n\tif names == nil {\n\t\tnames = container.NewMapSet[string]()\n\t\tw.files[dirName] = names\n\t}\n\tnames.Add(name)\n\n\terr = w.watcher.Add(dirName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"adding %q: %w\", dirName, err)\n\t}\n\n\treturn nil\n}\n\n// Remove implements the [FSWatcher] interface for *OSWatcher.  It's safe for\n// concurrent use.\nfunc (w *OSWatcher) Remove(name string) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"%s: %w\", osWatcherPref) }()\n\n\tdirName := filepath.Dir(name)\n\n\tw.filesMu.Lock()\n\tdefer w.filesMu.Unlock()\n\n\tnames, ok := w.files[name]\n\tif ok {\n\t\tdirName = name\n\t} else {\n\t\tnames = w.files[dirName]\n\t}\n\n\tif !names.Has(name) {\n\t\t// Name is not tracked.\n\t\treturn nil\n\t}\n\n\tnames.Delete(name)\n\tif names.Len() > 0 {\n\t\t// Some files are still tracked in the directory.\n\t\treturn nil\n\t}\n\n\t// No more files tracked in the directory, unwatch it.\n\tdelete(w.files, dirName)\n\n\terr = w.watcher.Remove(dirName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"removing %q: %w\", dirName, err)\n\t}\n\n\treturn nil\n}\n\n// handleEvents notifies about the received file system's event if needed.  It\n// is intended to be used as a goroutine.\nfunc (w *OSWatcher) handleEvents(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, w.logger)\n\n\tdefer close(w.events)\n\n\tch := w.watcher.Events\n\tfor e := range ch {\n\t\tif !w.isTrackedEvent(e) {\n\t\t\tcontinue\n\t\t}\n\n\t\tskipDuplicates(ch)\n\n\t\tselect {\n\t\tcase w.events <- Event{}:\n\t\t\t// Go on.\n\t\tdefault:\n\t\t\tw.logger.DebugContext(ctx, \"events buffer is full\")\n\t\t}\n\t}\n}\n\n// isTrackedEvent returns true if the event is about change of a file that is\n// tracked.\nfunc (w *OSWatcher) isTrackedEvent(e fsnotify.Event) (isDir bool) {\n\t// changeEvent is a combination of events that indicate a file change.\n\tconst changeEvent = fsnotify.Write | fsnotify.Create | fsnotify.Rename | fsnotify.Remove\n\n\tif !e.Has(changeEvent) {\n\t\treturn false\n\t}\n\n\tdirName := filepath.Dir(e.Name)\n\n\tw.filesMu.RLock()\n\tdefer w.filesMu.RUnlock()\n\n\tnames, isDir := w.files[e.Name]\n\tif !isDir {\n\t\tnames = w.files[dirName]\n\t}\n\n\treturn names.Has(e.Name)\n}\n\n// skipDuplicates drains the given channel of events, assuming that some events\n// might occur multiple times.\n//\n// TODO(e.burkov):  Check if this is still needed.\nfunc skipDuplicates(ch <-chan fsnotify.Event) {\n\tfor {\n\t\tselect {\n\t\tcase <-ch:\n\t\t\t// Go on.\n\t\tdefault:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// handleErrors handles accompanying errors.  It used to be called in a separate\n// goroutine.\nfunc (w *OSWatcher) handleErrors(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, w.logger)\n\n\tfor err := range w.watcher.Errors {\n\t\tw.logger.ErrorContext(ctx, \"handling error\", slogutil.KeyError, err)\n\t}\n}\n\n// EmptyFSWatcher is a no-op implementation of the [FSWatcher] interface.  It\n// may be used on systems not supporting filesystem events.\ntype EmptyFSWatcher struct{}\n\n// type check\nvar _ FSWatcher = EmptyFSWatcher{}\n\n// Start implements the [FSWatcher] interface for EmptyFSWatcher.  It always\n// returns nil error.\nfunc (EmptyFSWatcher) Start(_ context.Context) (err error) {\n\treturn nil\n}\n\n// Shutdown implements the [FSWatcher] interface for EmptyFSWatcher.  It always\n// returns nil error.\nfunc (EmptyFSWatcher) Shutdown(_ context.Context) (err error) {\n\treturn nil\n}\n\n// Events implements the [FSWatcher] interface for EmptyFSWatcher.  It always\n// returns nil channel.\nfunc (EmptyFSWatcher) Events() (e <-chan Event) {\n\treturn nil\n}\n\n// Add implements the [FSWatcher] interface for EmptyFSWatcher.  It always\n// returns nil error.\nfunc (EmptyFSWatcher) Add(_ string) (err error) {\n\treturn nil\n}\n\n// Remove implements the [FSWatcher] interface for EmptyFSWatcher.  It always\n// returns nil error.\nfunc (EmptyFSWatcher) Remove(_ string) (err error) {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghos/os.go",
    "content": "// Package aghos contains utilities for functions requiring system calls and\n// other OS-specific APIs.  OS-specific network handling should go to aghnet\n// instead.\npackage aghos\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\n// Default file, binary, and directory permissions.\nconst (\n\tDefaultPermDir  fs.FileMode = 0o700\n\tDefaultPermExe  fs.FileMode = 0o700\n\tDefaultPermFile fs.FileMode = 0o600\n)\n\n// Unsupported is a helper that returns a wrapped [errors.ErrUnsupported].\nfunc Unsupported(op string) (err error) {\n\treturn fmt.Errorf(\"%s: not supported on %s: %w\", op, runtime.GOOS, errors.ErrUnsupported)\n}\n\n// SetRlimit sets user-specified limit of how many fd's we can use.\n//\n// See https://github.com/AdguardTeam/AdGuardHome/internal/issues/659.\nfunc SetRlimit(val uint64) (err error) {\n\treturn setRlimit(val)\n}\n\n// HaveAdminRights checks if the current user has root (administrator) rights.\nfunc HaveAdminRights() (bool, error) {\n\treturn haveAdminRights()\n}\n\n// MaxCmdOutputSize is the maximum length of performed shell command output in\n// bytes.\nconst MaxCmdOutputSize = 64 * 1024\n\n// RunCommand runs shell command.\n//\n// TODO(s.chzhen):  Consider removing this after addressing the current behavior\n// where a non-zero exit code is returned together with a nil error.\nfunc RunCommand(\n\tctx context.Context,\n\tcmdCons executil.CommandConstructor,\n\tcommand string,\n\targuments ...string,\n) (code int, output []byte, err error) {\n\tstdoutBuf := bytes.Buffer{}\n\tstderrBuf := bytes.Buffer{}\n\n\terr = executil.Run(\n\t\tctx,\n\t\tcmdCons,\n\t\t&executil.CommandConfig{\n\t\t\tPath:   command,\n\t\t\tArgs:   arguments,\n\t\t\tStdout: ioutil.NewTruncatedWriter(&stdoutBuf, MaxCmdOutputSize),\n\t\t\tStderr: &stderrBuf,\n\t\t},\n\t)\n\n\tif err == nil {\n\t\treturn osutil.ExitCodeSuccess, stdoutBuf.Bytes(), nil\n\t}\n\n\tcode, ok := executil.ExitCodeFromError(err)\n\tif ok {\n\t\t// Mirror the old behavior and return a nil-error on non-zero code\n\t\t// status.\n\t\treturn code, stderrBuf.Bytes(), nil\n\t}\n\n\tcode = osutil.ExitCodeFailure\n\n\treturn code, nil, fmt.Errorf(\"command %q failed: %w: %s\", command, err, &stdoutBuf)\n}\n\n// psArgs holds the default ps arguments to avoid per-call slice allocations.\n//\n// Don't use -C flag here since it's a feature of linux's ps\n// implementation.  Use POSIX-compatible flags instead.\n//\n// See https://github.com/AdguardTeam/AdGuardHome/issues/3457.\nvar psArgs = []string{\"-A\", \"-o\", \"pid=\", \"-o\", \"comm=\"}\n\n// PIDByCommand searches for process named command and returns its PID ignoring\n// the PIDs from except.  If no processes found, the error returned.  l must not\n// be nil.\nfunc PIDByCommand(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcommand string,\n\texcept ...int,\n) (pid int, err error) {\n\tconst psCmd = \"ps\"\n\n\tl.DebugContext(ctx, \"executing\", \"cmd\", psCmd, \"args\", psArgs)\n\n\tstdoutBuf := bytes.Buffer{}\n\n\t// TODO(s.chzhen):  Catch stderr.\n\t//\n\t// TODO(s.chzhen):  Consider streaming the output if needed.  Using\n\t// [io.Pipe] here is unnecessary; it complicates lifecycle management\n\t// because the output must be read concurrently, and the PipeWriter must be\n\t// explicitly closed to signal EOF.  Since this command's output is small, a\n\t// bytes.Buffer via executil.Run is sufficient.\n\trunErr := executil.Run(\n\t\tctx,\n\t\texecutil.SystemCommandConstructor{},\n\t\t&executil.CommandConfig{\n\t\t\tPath:   psCmd,\n\t\t\tArgs:   psArgs,\n\t\t\tStdout: &stdoutBuf,\n\t\t},\n\t)\n\n\tvar instNum int\n\tpid, instNum, err = parsePSOutput(&stdoutBuf, command, except)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tswitch instNum {\n\tcase 0:\n\t\t// TODO(e.burkov):  Use constant error.\n\t\treturn 0, fmt.Errorf(\"no %s instances found\", command)\n\tcase 1:\n\t\t// Go on.\n\tdefault:\n\t\tl.WarnContext(ctx, \"instances found\", \"num\", instNum, \"command\", command)\n\t}\n\n\tif runErr != nil {\n\t\tif code, ok := executil.ExitCodeFromError(runErr); ok {\n\t\t\treturn 0, fmt.Errorf(\"ps finished with code %d\", code)\n\t\t}\n\n\t\treturn 0, fmt.Errorf(\"executing the command: %w\", runErr)\n\t}\n\n\treturn pid, nil\n}\n\n// parsePSOutput scans the output of ps searching the largest PID of the process\n// associated with cmdName ignoring PIDs from ignore.  A valid line from r\n// should look like these:\n//\n//\t 123 ./example-cmd\n//\t1230 some/base/path/example-cmd\n//\t3210 example-cmd\nfunc parsePSOutput(r io.Reader, cmdName string, ignore []int) (largest, instNum int, err error) {\n\ts := bufio.NewScanner(r)\n\tfor s.Scan() {\n\t\tfields := strings.Fields(s.Text())\n\t\tif len(fields) != 2 || path.Base(fields[1]) != cmdName {\n\t\t\tcontinue\n\t\t}\n\n\t\tcur, aerr := strconv.Atoi(fields[0])\n\t\tif aerr != nil || cur < 0 || slices.Contains(ignore, cur) {\n\t\t\tcontinue\n\t\t}\n\n\t\tinstNum++\n\t\tlargest = max(largest, cur)\n\t}\n\tif err = s.Err(); err != nil {\n\t\treturn 0, 0, fmt.Errorf(\"scanning stdout: %w\", err)\n\t}\n\n\treturn largest, instNum, nil\n}\n\n// IsOpenWrt returns true if host OS is OpenWrt.\nfunc IsOpenWrt() (ok bool) {\n\treturn isOpenWrt()\n}\n\n// SendShutdownSignal sends the shutdown signal to the channel.\nfunc SendShutdownSignal(c chan<- os.Signal) {\n\tsendShutdownSignal(c)\n}\n\n// RootDir returns the root directory for the current OS.\n//\n// TODO(e.burkov):  Deprecate [osutil.RootDirFS] and move it there.\nfunc RootDir() (dir string) {\n\treturn rootDir()\n}\n"
  },
  {
    "path": "internal/aghos/os_bsd.go",
    "content": "//go:build darwin || openbsd\n\npackage aghos\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\nfunc setRlimit(val uint64) (err error) {\n\tvar rlim syscall.Rlimit\n\trlim.Max = val\n\trlim.Cur = val\n\n\treturn syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)\n}\n\nfunc haveAdminRights() (bool, error) {\n\treturn os.Getuid() == 0, nil\n}\n\nfunc isOpenWrt() (ok bool) {\n\treturn false\n}\n"
  },
  {
    "path": "internal/aghos/os_freebsd.go",
    "content": "//go:build freebsd\n\npackage aghos\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\nfunc setRlimit(val uint64) (err error) {\n\tvar rlim syscall.Rlimit\n\trlim.Max = int64(val)\n\trlim.Cur = int64(val)\n\n\treturn syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)\n}\n\nfunc haveAdminRights() (bool, error) {\n\treturn os.Getuid() == 0, nil\n}\n\nfunc isOpenWrt() (ok bool) {\n\treturn false\n}\n"
  },
  {
    "path": "internal/aghos/os_internal_test.go",
    "content": "package aghos\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLargestLabeled(t *testing.T) {\n\tconst (\n\t\tcomm = `command-name`\n\t\tnl   = \"\\n\"\n\t)\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tdata        []byte\n\t\twantPID     int\n\t\twantInstNum int\n\t}{{\n\t\tname: \"success\",\n\t\tdata: []byte(nl +\n\t\t\t`  123     not-a-` + comm + nl +\n\t\t\t`  321    ` + comm + nl,\n\t\t),\n\t\twantPID:     321,\n\t\twantInstNum: 1,\n\t}, {\n\t\tname: \"several\",\n\t\tdata: []byte(nl +\n\t\t\t`1 ` + comm + nl +\n\t\t\t`5 /` + comm + nl +\n\t\t\t`2 /some/path/` + comm + nl +\n\t\t\t`4 ./` + comm + nl +\n\t\t\t`3 ` + comm + nl +\n\t\t\t`10 .` + comm + nl,\n\t\t),\n\t\twantPID:     5,\n\t\twantInstNum: 5,\n\t}, {\n\t\tname: \"no_any\",\n\t\tdata: []byte(nl +\n\t\t\t`1 ` + `not-a-` + comm + nl +\n\t\t\t`2 ` + `not-a-` + comm + nl +\n\t\t\t`3 ` + `not-a-` + comm + nl,\n\t\t),\n\t\twantPID:     0,\n\t\twantInstNum: 0,\n\t}, {\n\t\tname: \"weird_input\",\n\t\tdata: []byte(nl +\n\t\t\t`abc  ` + comm + nl +\n\t\t\t`-1   ` + comm + nl,\n\t\t),\n\t\twantPID:     0,\n\t\twantInstNum: 0,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tr := bytes.NewReader(tc.data)\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tpid, instNum, err := parsePSOutput(r, comm, nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.wantPID, pid)\n\t\t\tassert.Equal(t, tc.wantInstNum, instNum)\n\t\t})\n\t}\n\n\tt.Run(\"scanner_fail\", func(t *testing.T) {\n\t\tlr := ioutil.LimitReader(bytes.NewReader([]byte{1, 2, 3}), 0)\n\n\t\ttarget := &ioutil.LimitError{}\n\t\t_, _, err := parsePSOutput(lr, \"\", nil)\n\t\trequire.ErrorAs(t, err, &target)\n\n\t\tassert.EqualValues(t, 0, target.Limit)\n\t})\n\n\tt.Run(\"ignore\", func(t *testing.T) {\n\t\tr := bytes.NewReader([]byte(nl +\n\t\t\t`1 ` + comm + nl +\n\t\t\t`2 ` + comm + nl +\n\t\t\t`3` + comm + nl,\n\t\t))\n\n\t\tpid, instances, err := parsePSOutput(r, comm, []int{1, 3})\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 2, pid)\n\t\tassert.Equal(t, 1, instances)\n\t})\n}\n"
  },
  {
    "path": "internal/aghos/os_linux.go",
    "content": "//go:build linux\n\npackage aghos\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"syscall\"\n\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n)\n\nfunc setRlimit(val uint64) (err error) {\n\tvar rlim syscall.Rlimit\n\trlim.Max = val\n\trlim.Cur = val\n\n\treturn syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)\n}\n\nfunc haveAdminRights() (bool, error) {\n\t// The error is nil because the platform-independent function signature\n\t// requires returning an error.\n\treturn os.Getuid() == 0, nil\n}\n\nfunc isOpenWrt() (ok bool) {\n\tconst etcReleasePattern = \"etc/*release*\"\n\n\tvar err error\n\tok, err = FileWalker(func(r io.Reader) (_ []string, cont bool, err error) {\n\t\tconst osNameData = \"openwrt\"\n\n\t\t// This use of ReadAll is now safe, because FileWalker's Walk()\n\t\t// have limited r.\n\t\tvar data []byte\n\t\tdata, err = io.ReadAll(r)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\n\t\treturn nil, !stringutil.ContainsFold(string(data), osNameData), nil\n\t}).Walk(osutil.RootDirFS(), etcReleasePattern)\n\n\treturn err == nil && ok\n}\n"
  },
  {
    "path": "internal/aghos/os_unix.go",
    "content": "//go:build unix\n\npackage aghos\n\nimport (\n\t\"os\"\n)\n\nfunc sendShutdownSignal(_ chan<- os.Signal) {\n\t// On Unix we are already notified by the system.\n}\n\nfunc rootDir() (dir string) {\n\treturn \"/\"\n}\n"
  },
  {
    "path": "internal/aghos/os_windows.go",
    "content": "//go:build windows\n\npackage aghos\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc setRlimit(_ uint64) (err error) {\n\treturn Unsupported(\"setrlimit\")\n}\n\nfunc haveAdminRights() (bool, error) {\n\tvar token windows.Token\n\th := windows.CurrentProcess()\n\terr := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tinfo := make([]byte, 4)\n\tvar returnedLen uint32\n\terr = windows.GetTokenInformation(token, windows.TokenElevation, &info[0], uint32(len(info)), &returnedLen)\n\ttoken.Close()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif info[0] == 0 {\n\t\treturn false, nil\n\t}\n\treturn true, nil\n}\n\nfunc isOpenWrt() (ok bool) {\n\treturn false\n}\n\nfunc sendShutdownSignal(c chan<- os.Signal) {\n\tc <- os.Interrupt\n}\n\nfunc rootDir() (dir string) {\n\t// TODO(e.burkov): Use a better way if golang/go#44279 is ever resolved.\n\tsysDir, err := windows.GetSystemDirectory()\n\tif err != nil {\n\t\t// Assume that C:\\ is the safe default.\n\t\treturn `C:\\`\n\t}\n\n\treturn filepath.Join(filepath.VolumeName(sysDir), `\\`)\n}\n"
  },
  {
    "path": "internal/aghos/service.go",
    "content": "package aghos\n\n// PreCheckActionStart performs the service start action pre-check.\nfunc PreCheckActionStart() (err error) {\n\treturn preCheckActionStart()\n}\n"
  },
  {
    "path": "internal/aghos/service_darwin.go",
    "content": "//go:build darwin\n\npackage aghos\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/golibs/log\"\n)\n\n// preCheckActionStart performs the service start action pre-check.  It warns\n// user that the service should be installed into Applications directory.\nfunc preCheckActionStart() (err error) {\n\texe, err := os.Executable()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting executable path: %v\", err)\n\t}\n\n\texe, err = filepath.EvalSymlinks(exe)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"evaluating executable symlinks: %v\", err)\n\t}\n\n\tif !strings.HasPrefix(exe, \"/Applications/\") {\n\t\tlog.Info(\"warning: service must be started from within the /Applications directory\")\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "internal/aghos/service_others.go",
    "content": "//go:build !darwin\n\npackage aghos\n\n// preCheckActionStart performs the service start action pre-check.\nfunc preCheckActionStart() (err error) {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghos/syslog.go",
    "content": "package aghos\n\n// ConfigureSyslog reroutes standard logger output to syslog.\nfunc ConfigureSyslog(serviceName string) (err error) {\n\treturn configureSyslog(serviceName)\n}\n"
  },
  {
    "path": "internal/aghos/syslog_others.go",
    "content": "//go:build !windows\n\npackage aghos\n\nimport (\n\t\"log/syslog\"\n\n\t\"github.com/AdguardTeam/golibs/log\"\n)\n\n// configureSyslog sets standard log output to syslog.\nfunc configureSyslog(serviceName string) (err error) {\n\tw, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tlog.SetOutput(w)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghos/syslog_windows.go",
    "content": "//go:build windows\n\npackage aghos\n\nimport (\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"golang.org/x/sys/windows\"\n\t\"golang.org/x/sys/windows/svc/eventlog\"\n)\n\ntype eventLogWriter struct {\n\tel *eventlog.Log\n}\n\n// Write implements io.Writer interface for eventLogWriter.\nfunc (w *eventLogWriter) Write(b []byte) (int, error) {\n\treturn len(b), w.el.Info(1, string(b))\n}\n\n// configureSyslog sets standard log output to event log.\nfunc configureSyslog(serviceName string) (err error) {\n\t// Note that the eventlog src is the same as the service name, otherwise we\n\t// will get \"the description for event id cannot be found\" warning in every\n\t// log record.\n\n\t// Continue if we receive \"registry key already exists\" or if we get\n\t// ERROR_ACCESS_DENIED so that we can log without administrative permissions\n\t// for pre-existing eventlog sources.\n\terr = eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error)\n\tif err != nil &&\n\t\t!strings.Contains(err.Error(), \"registry key already exists\") &&\n\t\terr != windows.ERROR_ACCESS_DENIED {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tel, err := eventlog.Open(serviceName)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tlog.SetOutput(&eventLogWriter{el: el})\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghos/user.go",
    "content": "package aghos\n\n// SetGroup sets the effective group ID of the calling process.\nfunc SetGroup(groupName string) (err error) {\n\treturn setGroup(groupName)\n}\n\n// SetUser sets the effective user ID of the calling process.\nfunc SetUser(userName string) (err error) {\n\treturn setUser(userName)\n}\n"
  },
  {
    "path": "internal/aghos/user_unix.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage aghos\n\nimport (\n\t\"fmt\"\n\t\"os/user\"\n\t\"strconv\"\n\t\"syscall\"\n)\n\nfunc setGroup(groupName string) (err error) {\n\tg, err := user.LookupGroup(groupName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"looking up group: %w\", err)\n\t}\n\n\tgid, err := strconv.Atoi(g.Gid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing gid: %w\", err)\n\t}\n\n\terr = syscall.Setgid(gid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"setting gid: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc setUser(userName string) (err error) {\n\tu, err := user.Lookup(userName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"looking up user: %w\", err)\n\t}\n\n\tuid, err := strconv.Atoi(u.Uid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing uid: %w\", err)\n\t}\n\n\terr = syscall.Setuid(uid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"setting uid: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghos/user_windows.go",
    "content": "//go:build windows\n\npackage aghos\n\n// TODO(a.garipov): Think of a way to implement these.  Perhaps by using\n// syscall.CreateProcessAsUser or something from the golang.org/x/sys module.\n\nfunc setGroup(_ string) (err error) {\n\treturn Unsupported(\"setgid\")\n}\n\nfunc setUser(_ string) (err error) {\n\treturn Unsupported(\"setuid\")\n}\n"
  },
  {
    "path": "internal/aghrenameio/renameio.go",
    "content": "// Package aghrenameio is a wrapper around package github.com/google/renameio/v2\n// that provides a similar stream-based API for both Unix and Windows systems.\n// While the Windows API is not technically atomic, it still provides a\n// consistent stream-based interface, and atomic renames of files do not seem to\n// be possible in all cases anyway.\n//\n// See https://github.com/google/renameio/issues/1.\n//\n// TODO(a.garipov): Consider moving to golibs/renameioutil once tried and\n// tested.\npackage aghrenameio\n\nimport (\n\t\"io/fs\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// PendingFile is the interface for pending temporary files.\ntype PendingFile interface {\n\t// Cleanup closes the file, and removes it without performing the renaming.\n\t// To close and rename the file, use CloseReplace.\n\tCleanup() (err error)\n\n\t// CloseReplace closes the temporary file and replaces the destination file\n\t// with it, possibly atomically.\n\t//\n\t// This method is not safe for concurrent use by multiple goroutines.\n\tCloseReplace() (err error)\n\n\t// Write writes len(b) bytes from b to the File.  It returns the number of\n\t// bytes written and an error, if any.  Write returns a non-nil error when n\n\t// != len(b).\n\tWrite(b []byte) (n int, err error)\n}\n\n// NewPendingFile is a wrapper around [renameio.NewPendingFile] on Unix systems\n// and [os.CreateTemp] on Windows.\nfunc NewPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {\n\treturn newPendingFile(filePath, mode)\n}\n\n// WithDeferredCleanup is a helper that performs the necessary cleanups and\n// finalizations of the temporary files based on the returned error.\nfunc WithDeferredCleanup(returned error, file PendingFile) (err error) {\n\t// Make sure that any error returned from here is marked as a deferred one.\n\tif returned != nil {\n\t\treturn errors.WithDeferred(returned, file.Cleanup())\n\t}\n\n\treturn errors.WithDeferred(nil, file.CloseReplace())\n}\n"
  },
  {
    "path": "internal/aghrenameio/renameio_test.go",
    "content": "package aghrenameio_test\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testPerm is the common permission mode for tests.\nconst testPerm fs.FileMode = 0o644\n\n// Common file data for tests.\nvar (\n\tinitialData = []byte(\"initial data\\n\")\n\tnewData     = []byte(\"new data\\n\")\n)\n\nfunc TestPendingFile(t *testing.T) {\n\tt.Parallel()\n\n\ttargetPath := newInitialFile(t)\n\tf, err := aghrenameio.NewPendingFile(targetPath, testPerm)\n\trequire.NoError(t, err)\n\n\t_, err = f.Write(newData)\n\trequire.NoError(t, err)\n\n\terr = f.CloseReplace()\n\trequire.NoError(t, err)\n\n\tgotData, err := os.ReadFile(targetPath)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, newData, gotData)\n}\n\n// newInitialFile is a test helper that returns the path to the file containing\n// [initialData].\nfunc newInitialFile(tb testing.TB) (targetPath string) {\n\ttb.Helper()\n\n\tdir := tb.TempDir()\n\ttargetPath = filepath.Join(dir, \"target\")\n\n\terr := os.WriteFile(targetPath, initialData, 0o644)\n\trequire.NoError(tb, err)\n\n\treturn targetPath\n}\n\nfunc TestWithDeferredCleanup(t *testing.T) {\n\tt.Parallel()\n\n\tconst testError errors.Error = \"test error\"\n\n\ttestCases := []struct {\n\t\terror      error\n\t\tname       string\n\t\twantErrMsg string\n\t\twantData   []byte\n\t}{{\n\t\tname:       \"success\",\n\t\terror:      nil,\n\t\twantErrMsg: \"\",\n\t\twantData:   newData,\n\t}, {\n\t\tname:       \"error\",\n\t\terror:      testError,\n\t\twantErrMsg: testError.Error(),\n\t\twantData:   initialData,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttargetPath := newInitialFile(t)\n\t\t\tf, err := aghrenameio.NewPendingFile(targetPath, testPerm)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t_, err = f.Write(newData)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = aghrenameio.WithDeferredCleanup(tc.error, f)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tgotData, err := os.ReadFile(targetPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.wantData, gotData)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/aghrenameio/renameio_unix.go",
    "content": "//go:build unix\n\npackage aghrenameio\n\nimport (\n\t\"io/fs\"\n\n\t\"github.com/google/renameio/v2\"\n)\n\n// pendingFile is a wrapper around [*renameio.PendingFile] making it an\n// [io.WriteCloser].\ntype pendingFile struct {\n\tfile *renameio.PendingFile\n}\n\n// type check\nvar _ PendingFile = pendingFile{}\n\n// Cleanup implements the [PendingFile] interface for pendingFile.\nfunc (f pendingFile) Cleanup() (err error) {\n\treturn f.file.Cleanup()\n}\n\n// CloseReplace implements the [PendingFile] interface for pendingFile.\nfunc (f pendingFile) CloseReplace() (err error) {\n\treturn f.file.CloseAtomicallyReplace()\n}\n\n// Write implements the [PendingFile] interface for pendingFile.\nfunc (f pendingFile) Write(b []byte) (n int, err error) {\n\treturn f.file.Write(b)\n}\n\n// NewPendingFile is a wrapper around [renameio.NewPendingFile].\n//\n// f.Close must be called to finish the renaming.\nfunc newPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {\n\tfile, err := renameio.NewPendingFile(filePath, renameio.WithPermissions(mode))\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\treturn pendingFile{\n\t\tfile: file,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/aghrenameio/renameio_windows.go",
    "content": "//go:build windows\n\npackage aghrenameio\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// pendingFile is a wrapper around [*os.File] calling [os.Rename] in its Close\n// method.\ntype pendingFile struct {\n\tfile       *os.File\n\ttargetPath string\n}\n\n// type check\nvar _ PendingFile = (*pendingFile)(nil)\n\n// Cleanup implements the [PendingFile] interface for *pendingFile.\nfunc (f *pendingFile) Cleanup() (err error) {\n\tcloseErr := f.file.Close()\n\terr = os.Remove(f.file.Name())\n\n\t// Put closeErr into the deferred error because that's where it is usually\n\t// expected.\n\treturn errors.WithDeferred(err, closeErr)\n}\n\n// CloseReplace implements the [PendingFile] interface for *pendingFile.\nfunc (f *pendingFile) CloseReplace() (err error) {\n\terr = f.file.Close()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"closing: %w\", err)\n\t}\n\n\terr = os.Rename(f.file.Name(), f.targetPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"renaming: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Write implements the [PendingFile] interface for *pendingFile.\nfunc (f *pendingFile) Write(b []byte) (n int, err error) {\n\treturn f.file.Write(b)\n}\n\n// NewPendingFile is a wrapper around [os.CreateTemp].\n//\n// f.Close must be called to finish the renaming.\nfunc newPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {\n\t// Use the same directory as the file itself, because moves across\n\t// filesystems can be especially problematic.\n\tfile, err := os.CreateTemp(filepath.Dir(filePath), \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"opening pending file: %w\", err)\n\t}\n\n\t// TODO(e.burkov):  The [os.Chmod] implementation is useless on Windows,\n\t// investigate if it can be removed.\n\terr = os.Chmod(file.Name(), mode)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"preparing pending file: %w\", err)\n\t}\n\n\treturn &pendingFile{\n\t\tfile:       file,\n\t\ttargetPath: filePath,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/aghslog/aghslog.go",
    "content": "// Package aghslog contains logging constants and helpers.\npackage aghslog\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// PrefixDNSProxy is the prefix for DNS proxy logs.\nconst PrefixDNSProxy = \"dnsproxy\"\n\nconst (\n\t// KeyClientName is the log attribute for the client name.\n\tKeyClientName = \"client_name\"\n\n\t// KeyUpstreamType is the log attribute for the upstream types.  See the\n\t// UpstreamType* constants below.\n\tKeyUpstreamType = \"upstream_type\"\n)\n\nconst (\n\t// UpstreamTypeBootstrap is the log attribute value for bootstrap upstreams.\n\tUpstreamTypeBootstrap = \"bootstrap\"\n\n\t// UpstreamTypeCustom is the log attribute value for custom upstreams.\n\tUpstreamTypeCustom = \"custom\"\n\n\t// UpstreamTypeFallback is the log attribute value for fallback upstreams.\n\tUpstreamTypeFallback = \"fallback\"\n\n\t// UpstreamTypeMain is the log attribute value for main upstreams.\n\tUpstreamTypeMain = \"main\"\n\n\t// UpstreamTypeLocal is the log attribute value for upstreams used for\n\t// resolving PTR records for local addresses.\n\tUpstreamTypeLocal = \"local\"\n\n\t// UpstreamTypeService is the log attribute value for upstreams used for\n\t// safe browsing and parental services.\n\tUpstreamTypeService = \"service\"\n\n\t// UpstreamTypeTest is the log attribute value for upstreams used for\n\t// testing and validation.\n\tUpstreamTypeTest = \"test\"\n)\n\n// NewForUpstream returns a new logger with a prefix for logs related to a\n// specific upstream type.\nfunc NewForUpstream(baseLogger *slog.Logger, typ string) (l *slog.Logger) {\n\treturn baseLogger.With(slogutil.KeyPrefix, PrefixDNSProxy, KeyUpstreamType, typ)\n}\n"
  },
  {
    "path": "internal/aghtest/aghtest.go",
    "content": "// Package aghtest contains utilities for testing.\npackage aghtest\n\nimport (\n\t\"crypto/sha256\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\t// ReqHost is the common request host for filtering tests.\n\tReqHost = \"www.host.example\"\n\n\t// ReqFQDN is the common request FQDN for filtering tests.\n\tReqFQDN = ReqHost + \".\"\n)\n\n// HostToIPs is a helper that generates one IPv4 and one IPv6 address from host.\nfunc HostToIPs(host string) (ipv4, ipv6 netip.Addr) {\n\thash := sha256.Sum256([]byte(host))\n\n\treturn netip.AddrFrom4([4]byte(hash[:4])), netip.AddrFrom16([16]byte(hash[4:20]))\n}\n\n// StartHTTPServer is a helper that starts the HTTP server, which is configured\n// to return data on every request, and returns the client and server URL.\nfunc StartHTTPServer(tb testing.TB, data []byte) (c *http.Client, u *url.URL) {\n\ttb.Helper()\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t_, _ = w.Write(data)\n\t}))\n\ttb.Cleanup(srv.Close)\n\n\tu, err := url.Parse(srv.URL)\n\trequire.NoError(tb, err)\n\n\treturn srv.Client(), u\n}\n\n// testTimeout is a timeout for tests.\n//\n// TODO(e.burkov):  Move into agdctest.\nconst testTimeout = 1 * time.Second\n\n// StartLocalhostUpstream is a test helper that starts a DNS server on\n// localhost.\nfunc StartLocalhostUpstream(tb testing.TB, h dns.Handler) (addr *url.URL) {\n\ttb.Helper()\n\n\tstartCh := make(chan netip.AddrPort)\n\tdefer close(startCh)\n\terrCh := make(chan error)\n\n\tsrv := &dns.Server{\n\t\tAddr:         \"127.0.0.1:0\",\n\t\tNet:          string(proxy.ProtoTCP),\n\t\tHandler:      h,\n\t\tReadTimeout:  testTimeout,\n\t\tWriteTimeout: testTimeout,\n\t}\n\tsrv.NotifyStartedFunc = func() {\n\t\taddrPort := srv.Listener.Addr()\n\t\tstartCh <- netutil.NetAddrToAddrPort(addrPort)\n\t}\n\n\tgo func() { errCh <- srv.ListenAndServe() }()\n\n\tselect {\n\tcase addrPort := <-startCh:\n\t\taddr = &url.URL{\n\t\t\tScheme: string(proxy.ProtoTCP),\n\t\t\tHost:   addrPort.String(),\n\t\t}\n\n\t\ttestutil.CleanupAndRequireSuccess(tb, func() (err error) { return <-errCh })\n\t\ttestutil.CleanupAndRequireSuccess(tb, srv.Shutdown)\n\tcase err := <-errCh:\n\t\trequire.NoError(tb, err)\n\tcase <-time.After(testTimeout):\n\t\trequire.FailNow(tb, \"timeout exceeded\")\n\t}\n\n\treturn addr\n}\n"
  },
  {
    "path": "internal/aghtest/interface.go",
    "content": "package aghtest\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\tnextagh \"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/rdns\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// FSWatcher is a fake [aghos.FSWatcher] implementation for tests.\ntype FSWatcher struct {\n\tOnStart    func(ctx context.Context) (err error)\n\tOnShutdown func(ctx context.Context) (err error)\n\tOnEvents   func() (e <-chan aghos.Event)\n\tOnAdd      func(name string) (err error)\n\tOnRemove   func(name string) (err error)\n}\n\n// NewFSWatcher returns a new *FSWatcher all methods of which panic.\nfunc NewFSWatcher() (w *FSWatcher) {\n\treturn &FSWatcher{\n\t\tOnStart:    func(ctx context.Context) (_ error) { panic(testutil.UnexpectedCall(ctx)) },\n\t\tOnShutdown: func(ctx context.Context) (_ error) { panic(testutil.UnexpectedCall(ctx)) },\n\t\tOnEvents:   func() (_ <-chan aghos.Event) { panic(testutil.UnexpectedCall()) },\n\t\tOnAdd:      func(name string) (_ error) { panic(testutil.UnexpectedCall(name)) },\n\t\tOnRemove:   func(name string) (_ error) { panic(testutil.UnexpectedCall(name)) },\n\t}\n}\n\n// type check\nvar _ aghos.FSWatcher = (*FSWatcher)(nil)\n\n// Start implements the [aghos.FSWatcher] interface for *FSWatcher.\nfunc (w *FSWatcher) Start(ctx context.Context) (err error) {\n\treturn w.OnStart(ctx)\n}\n\n// Shutdown implements the [aghos.FSWatcher] interface for *FSWatcher.\nfunc (w *FSWatcher) Shutdown(ctx context.Context) (err error) {\n\treturn w.OnShutdown(ctx)\n}\n\n// Events implements the [aghos.FSWatcher] interface for *FSWatcher.\nfunc (w *FSWatcher) Events() (e <-chan aghos.Event) {\n\treturn w.OnEvents()\n}\n\n// Add implements the [aghos.FSWatcher] interface for *FSWatcher.\nfunc (w *FSWatcher) Add(name string) (err error) {\n\treturn w.OnAdd(name)\n}\n\n// Remove implements the [aghos.FSWatcher] interface for *FSWatcher.\nfunc (w *FSWatcher) Remove(name string) (err error) {\n\treturn w.OnRemove(name)\n}\n\n// ServiceWithConfig is a fake [nextagh.ServiceWithConfig] implementation for\n// tests.\ntype ServiceWithConfig[ConfigType any] struct {\n\tOnStart    func(ctx context.Context) (err error)\n\tOnShutdown func(ctx context.Context) (err error)\n\tOnConfig   func() (c ConfigType)\n}\n\n// type check\nvar _ nextagh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)\n\n// Start implements the [nextagh.ServiceWithConfig] interface for\n// *ServiceWithConfig.\nfunc (s *ServiceWithConfig[_]) Start(ctx context.Context) (err error) {\n\treturn s.OnStart(ctx)\n}\n\n// Shutdown implements the [nextagh.ServiceWithConfig] interface for\n// *ServiceWithConfig.\nfunc (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {\n\treturn s.OnShutdown(ctx)\n}\n\n// Config implements the [nextagh.ServiceWithConfig] interface for\n// *ServiceWithConfig.\nfunc (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {\n\treturn s.OnConfig()\n}\n\n// AddressProcessor is a fake [client.AddressProcessor] implementation for\n// tests.\ntype AddressProcessor struct {\n\tOnProcess func(ctx context.Context, ip netip.Addr)\n\tOnClose   func() (err error)\n}\n\n// Process implements the [client.AddressProcessor] interface for\n// *AddressProcessor.\nfunc (p *AddressProcessor) Process(ctx context.Context, ip netip.Addr) {\n\tp.OnProcess(ctx, ip)\n}\n\n// Close implements the [client.AddressProcessor] interface for\n// *AddressProcessor.\nfunc (p *AddressProcessor) Close() (err error) {\n\treturn p.OnClose()\n}\n\n// AddressUpdater is a fake [client.AddressUpdater] implementation for tests.\ntype AddressUpdater struct {\n\tOnUpdateAddress func(ctx context.Context, ip netip.Addr, host string, info *whois.Info)\n}\n\n// UpdateAddress implements the [client.AddressUpdater] interface for\n// *AddressUpdater.\nfunc (p *AddressUpdater) UpdateAddress(\n\tctx context.Context,\n\tip netip.Addr,\n\thost string,\n\tinfo *whois.Info,\n) {\n\tp.OnUpdateAddress(ctx, ip, host, info)\n}\n\n// Exchanger is a fake [rdns.Exchanger] implementation for tests.\ntype Exchanger struct {\n\tOnExchange func(ctx context.Context, ip netip.Addr) (host string, ttl time.Duration, err error)\n}\n\n// type check\nvar _ rdns.Exchanger = (*Exchanger)(nil)\n\n// Exchange implements [rdns.Exchanger] interface for *Exchanger.\nfunc (e *Exchanger) Exchange(\n\tctx context.Context,\n\tip netip.Addr,\n) (host string, ttl time.Duration, err error) {\n\treturn e.OnExchange(ctx, ip)\n}\n\n// UpstreamMock is a fake [upstream.Upstream] implementation for tests.\n//\n// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and\n// rename it to just Upstream.\ntype UpstreamMock struct {\n\tOnAddress  func() (addr string)\n\tOnExchange func(req *dns.Msg) (resp *dns.Msg, err error)\n\tOnClose    func() (err error)\n}\n\n// type check\nvar _ upstream.Upstream = (*UpstreamMock)(nil)\n\n// Address implements the [upstream.Upstream] interface for *UpstreamMock.\nfunc (u *UpstreamMock) Address() (addr string) {\n\treturn u.OnAddress()\n}\n\n// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.\nfunc (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {\n\treturn u.OnExchange(req)\n}\n\n// Close implements the [upstream.Upstream] interface for *UpstreamMock.\nfunc (u *UpstreamMock) Close() (err error) {\n\treturn u.OnClose()\n}\n\n// ConfigModifier is a fake [agh.ConfigModifier] implementation for tests.\ntype ConfigModifier struct {\n\tOnApply func(ctx context.Context)\n}\n\n// type check\nvar _ agh.ConfigModifier = (*ConfigModifier)(nil)\n\n// Apply implements the [agh.ConfigModifier] interface for *ConfigModifier.\nfunc (m *ConfigModifier) Apply(ctx context.Context) {\n\tm.OnApply(ctx)\n}\n\n// Registrar is a fake [aghhttp.Registrar] implementation for tests.\ntype Registrar struct {\n\tOnRegister func(method, path string, h http.HandlerFunc)\n}\n\n// type check\nvar _ aghhttp.Registrar = (*Registrar)(nil)\n\n// Register implements the [aghhttp.Registrar] interface for *Registrar.\nfunc (m *Registrar) Register(method, path string, h http.HandlerFunc) {\n\tm.OnRegister(method, path, h)\n}\n"
  },
  {
    "path": "internal/aghtest/interface_test.go",
    "content": "package aghtest_test\n\nimport (\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n)\n\n// type check\n//\n// TODO(s.chzhen):  Resolve the import cycles and move it to aghtest.\nvar (\n\t_ client.AddressProcessor = (*aghtest.AddressProcessor)(nil)\n\t_ client.AddressUpdater   = (*aghtest.AddressUpdater)(nil)\n)\n"
  },
  {
    "path": "internal/aghtest/upstream.go",
    "content": "package aghtest\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/miekg/dns\"\n)\n\n// Additional Upstream Testing Utilities\n\n// Upstream is a mock implementation of upstream.Upstream.\n//\n// TODO(a.garipov): Replace with UpstreamMock and rename it to just Upstream.\ntype Upstream struct {\n\t// CName is a map of hostname to canonical name.\n\tCName map[string][]string\n\t// IPv4 is a map of hostname to IPv4.\n\tIPv4 map[string][]net.IP\n\t// IPv6 is a map of hostname to IPv6.\n\tIPv6 map[string][]net.IP\n}\n\nvar _ upstream.Upstream = (*Upstream)(nil)\n\n// Exchange implements the [upstream.Upstream] interface for *Upstream.\n//\n// TODO(a.garipov): Split further into handlers.\nfunc (u *Upstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) {\n\tresp = new(dns.Msg).SetReply(m)\n\n\tif len(m.Question) == 0 {\n\t\treturn nil, fmt.Errorf(\"question should not be empty\")\n\t}\n\n\tq := m.Question[0]\n\tname := q.Name\n\tfor _, cname := range u.CName[name] {\n\t\tresp.Answer = append(resp.Answer, &dns.CNAME{\n\t\t\tHdr:    dns.RR_Header{Name: name, Rrtype: dns.TypeCNAME},\n\t\t\tTarget: cname,\n\t\t})\n\t}\n\n\tqtype := q.Qtype\n\thdr := dns.RR_Header{\n\t\tName:   name,\n\t\tRrtype: qtype,\n\t}\n\n\tswitch qtype {\n\tcase dns.TypeA:\n\t\tfor _, ip := range u.IPv4[name] {\n\t\t\tresp.Answer = append(resp.Answer, &dns.A{Hdr: hdr, A: ip})\n\t\t}\n\tcase dns.TypeAAAA:\n\t\tfor _, ip := range u.IPv6[name] {\n\t\t\tresp.Answer = append(resp.Answer, &dns.AAAA{Hdr: hdr, AAAA: ip})\n\t\t}\n\t}\n\tif len(resp.Answer) == 0 {\n\t\tresp.SetRcode(m, dns.RcodeNameError)\n\t}\n\n\treturn resp, nil\n}\n\n// Address implements [upstream.Upstream] interface for *Upstream.\nfunc (u *Upstream) Address() string {\n\treturn \"todo.upstream.example\"\n}\n\n// Close implements [upstream.Upstream] interface for *Upstream.\nfunc (u *Upstream) Close() (err error) {\n\treturn nil\n}\n\n// MatchedResponse is a test helper that returns a response with answer if req\n// has question type qt, and target targ.  Otherwise, it returns nil.\n//\n// req must not be nil and req.Question must have a length of 1.  Answer is\n// interpreted in the following ways:\n//\n//   - For A and AAAA queries, answer must be an IP address of the corresponding\n//     protocol version.\n//\n//   - For PTR queries, answer should be a domain name in the response.\n//\n// If the answer does not correspond to the question type, MatchedResponse panics.\n// Panics are used instead of [testing.TB], because the helper is intended to\n// use in [UpstreamMock.OnExchange] callbacks, which are usually called in a\n// separate goroutine.\n//\n// TODO(a.garipov): Consider adding version with DNS class as well.\nfunc MatchedResponse(req *dns.Msg, qt uint16, targ, answer string) (resp *dns.Msg) {\n\tif req == nil || len(req.Question) != 1 {\n\t\tpanic(fmt.Errorf(\"bad req: %+v\", req))\n\t}\n\n\tq := req.Question[0]\n\ttarg = dns.Fqdn(targ)\n\tif q.Qclass != dns.ClassINET || q.Qtype != qt || q.Name != targ {\n\t\treturn nil\n\t}\n\n\trespHdr := dns.RR_Header{\n\t\tName:   targ,\n\t\tRrtype: qt,\n\t\tClass:  dns.ClassINET,\n\t\tTtl:    60,\n\t}\n\n\tresp = new(dns.Msg).SetReply(req)\n\tswitch qt {\n\tcase dns.TypeA:\n\t\tresp.Answer = mustAnsA(respHdr, answer)\n\tcase dns.TypeAAAA:\n\t\tresp.Answer = mustAnsAAAA(respHdr, answer)\n\tcase dns.TypePTR:\n\t\tresp.Answer = []dns.RR{&dns.PTR{\n\t\t\tHdr: respHdr,\n\t\t\tPtr: answer,\n\t\t}}\n\tdefault:\n\t\tpanic(fmt.Errorf(\"aghtest: bad question type: %s\", dns.Type(qt)))\n\t}\n\n\treturn resp\n}\n\n// mustAnsA returns valid answer records if s is a valid IPv4 address.\n// Otherwise, mustAnsA panics.\nfunc mustAnsA(respHdr dns.RR_Header, s string) (ans []dns.RR) {\n\tip, err := netip.ParseAddr(s)\n\tif err != nil || !ip.Is4() {\n\t\tpanic(fmt.Errorf(\"aghtest: bad A answer: %+v\", s))\n\t}\n\n\treturn []dns.RR{&dns.A{\n\t\tHdr: respHdr,\n\t\tA:   ip.AsSlice(),\n\t}}\n}\n\n// mustAnsAAAA returns valid answer records if s is a valid IPv6 address.\n// Otherwise, mustAnsAAAA panics.\nfunc mustAnsAAAA(respHdr dns.RR_Header, s string) (ans []dns.RR) {\n\tip, err := netip.ParseAddr(s)\n\tif err != nil || !ip.Is6() {\n\t\tpanic(fmt.Errorf(\"aghtest: bad AAAA answer: %+v\", s))\n\t}\n\n\treturn []dns.RR{&dns.AAAA{\n\t\tHdr:  respHdr,\n\t\tAAAA: ip.AsSlice(),\n\t}}\n}\n\n// NewUpstreamMock returns an [*UpstreamMock], fields OnAddress and OnClose of\n// which are set to stubs that return \"upstream.example\" and nil respectively.\n// The field OnExchange is set to onExc.\nfunc NewUpstreamMock(onExc func(req *dns.Msg) (resp *dns.Msg, err error)) (u *UpstreamMock) {\n\treturn &UpstreamMock{\n\t\tOnAddress:  func() (addr string) { return \"upstream.example\" },\n\t\tOnExchange: onExc,\n\t\tOnClose:    func() (err error) { return nil },\n\t}\n}\n\n// NewBlockUpstream returns an [*UpstreamMock] that works like an upstream that\n// supports hash-based safe-browsing/adult-blocking feature.  If shouldBlock is\n// true, hostname's actual hash is returned, blocking it.  Otherwise, it returns\n// a different hash.\nfunc NewBlockUpstream(hostname string, shouldBlock bool) (u *UpstreamMock) {\n\thash := sha256.Sum256([]byte(hostname))\n\thashStr := hex.EncodeToString(hash[:])\n\tif !shouldBlock {\n\t\thashStr = hex.EncodeToString(hash[:])[:2] + strings.Repeat(\"ab\", 28)\n\t}\n\n\tans := &dns.TXT{\n\t\tHdr: dns.RR_Header{\n\t\t\tName:   \"\",\n\t\t\tRrtype: dns.TypeTXT,\n\t\t\tClass:  dns.ClassINET,\n\t\t\tTtl:    60,\n\t\t},\n\t\tTxt: []string{hashStr},\n\t}\n\trespTmpl := &dns.Msg{\n\t\tAnswer: []dns.RR{ans},\n\t}\n\n\treturn &UpstreamMock{\n\t\tOnAddress: func() (addr string) { return \"sbpc.upstream.example\" },\n\t\tOnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {\n\t\t\tresp = respTmpl.Copy()\n\t\t\tresp.SetReply(req)\n\t\t\tresp.Answer[0].(*dns.TXT).Hdr.Name = req.Question[0].Name\n\n\t\t\treturn resp, nil\n\t\t},\n\t\tOnClose: func() (err error) { return nil },\n\t}\n}\n\n// ErrUpstream is the error returned from the [*UpstreamMock] created by\n// [NewErrorUpstream].\nconst ErrUpstream errors.Error = \"test upstream error\"\n\n// NewErrorUpstream returns an [*UpstreamMock] that returns [ErrUpstream] from\n// its Exchange method.\nfunc NewErrorUpstream() (u *UpstreamMock) {\n\treturn &UpstreamMock{\n\t\tOnAddress: func() (addr string) { return \"error.upstream.example\" },\n\t\tOnExchange: func(_ *dns.Msg) (resp *dns.Msg, err error) {\n\t\t\treturn nil, ErrUpstream\n\t\t},\n\t\tOnClose: func() (err error) { return nil },\n\t}\n}\n"
  },
  {
    "path": "internal/aghtls/aghtls.go",
    "content": "// Package aghtls contains utilities for work with TLS.\npackage aghtls\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/golibs/netutil\"\n)\n\n// Init populates the cipherSuites map with the name-to-ID mapping of cipher\n// suites from crypto/tls.  It must be called only once, and it must be called\n// before any function that calls [ParseCiphers].\n//\n// TODO(a.garipov): Propose a similar API to crypto/tls.\nfunc Init(ctx context.Context, l *slog.Logger) {\n\tsuites := tls.CipherSuites()\n\tcipherSuites = make(map[string]uint16, len(suites))\n\tfor _, s := range suites {\n\t\tcipherSuites[s.Name] = s.ID\n\t}\n\n\tl.DebugContext(ctx, \"known ciphers\", \"ciphers\", cipherSuites)\n}\n\n// cipherSuites are a name-to-ID mapping of cipher suites from crypto/tls.  It\n// is filled by init.  It must not be modified.\nvar cipherSuites map[string]uint16\n\n// ParseCiphers parses a slice of cipher suites from cipher names.\nfunc ParseCiphers(cipherNames []string) (cipherIDs []uint16, err error) {\n\tif cipherNames == nil {\n\t\treturn nil, nil\n\t}\n\n\tcipherIDs = make([]uint16, 0, len(cipherNames))\n\tfor _, name := range cipherNames {\n\t\tid, ok := cipherSuites[name]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"unknown cipher %q\", name)\n\t\t}\n\n\t\tcipherIDs = append(cipherIDs, id)\n\t}\n\n\treturn cipherIDs, nil\n}\n\n// SaferCipherSuites returns a set of default cipher suites with vulnerable and\n// weak cipher suites removed.\nfunc SaferCipherSuites() (safe []uint16) {\n\tfor _, s := range tls.CipherSuites() {\n\t\tswitch s.ID {\n\t\tcase\n\t\t\ttls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,\n\t\t\ttls.TLS_RSA_WITH_AES_128_CBC_SHA,\n\t\t\ttls.TLS_RSA_WITH_AES_256_CBC_SHA,\n\t\t\ttls.TLS_RSA_WITH_AES_128_CBC_SHA256,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,\n\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,\n\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256:\n\t\t\t// Less safe 3DES and CBC suites, go on.\n\t\tdefault:\n\t\t\tsafe = append(safe, s.ID)\n\t\t}\n\t}\n\n\treturn safe\n}\n\n// CertificateHasIP returns true if cert has at least a single IP address among\n// its subjectAltNames.\nfunc CertificateHasIP(cert *x509.Certificate) (ok bool) {\n\treturn len(cert.IPAddresses) > 0 || slices.ContainsFunc(cert.DNSNames, netutil.IsValidIPString)\n}\n"
  },
  {
    "path": "internal/aghtls/aghtls_test.go",
    "content": "package aghtls_test\n\nimport (\n\t\"crypto/tls\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// testTimeout is a common timeout for tests and contexts.\nconst testTimeout time.Duration = 1 * time.Second\n\nfunc TestParseCiphers(t *testing.T) {\n\taghtls.Init(testutil.ContextWithTimeout(t, testTimeout), slogutil.NewDiscardLogger())\n\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\twant       []uint16\n\t\tin         []string\n\t}{{\n\t\tname:       \"nil\",\n\t\twantErrMsg: \"\",\n\t\twant:       nil,\n\t\tin:         nil,\n\t}, {\n\t\tname:       \"empty\",\n\t\twantErrMsg: \"\",\n\t\twant:       []uint16{},\n\t\tin:         []string{},\n\t}, {}, {\n\t\tname:       \"one\",\n\t\twantErrMsg: \"\",\n\t\twant:       []uint16{tls.TLS_AES_128_GCM_SHA256},\n\t\tin:         []string{\"TLS_AES_128_GCM_SHA256\"},\n\t}, {\n\t\tname:       \"several\",\n\t\twantErrMsg: \"\",\n\t\twant:       []uint16{tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384},\n\t\tin:         []string{\"TLS_AES_128_GCM_SHA256\", \"TLS_AES_256_GCM_SHA384\"},\n\t}, {\n\t\tname:       \"bad\",\n\t\twantErrMsg: `unknown cipher \"bad_cipher\"`,\n\t\twant:       nil,\n\t\tin:         []string{\"bad_cipher\"},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := aghtls.ParseCiphers(tc.in)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/aghtls/defaultmanager.go",
    "content": "package aghtls\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// DefaultManagerConfig is the configuration structure for [NewDefaultManager].\ntype DefaultManagerConfig struct {\n\t// Logger is used for logging the operation of the manager.  It must not be\n\t// nil.\n\tLogger *slog.Logger\n\n\t// Watcher is used to watch the TLS certificate and key files.  It must not\n\t// be nil.\n\tWatcher aghos.FSWatcher\n}\n\n// DefaultManager is the default implementation of the [Manager] interface.\n//\n// TODO(e.burkov):  Add tests.\ntype DefaultManager struct {\n\tlogger  *slog.Logger\n\tpairMu  *sync.Mutex\n\tupdates chan UpdateSignal\n\twatcher aghos.FSWatcher\n\tpair    TLSPair\n}\n\n// NewDefaultManager returns a new properly initialized default manager.\nfunc NewDefaultManager(c *DefaultManagerConfig) (mgr *DefaultManager) {\n\treturn &DefaultManager{\n\t\tlogger: c.Logger,\n\t\tpairMu: &sync.Mutex{},\n\t\tpair:   TLSPair{},\n\t\t// Buffer the channel to avoid missing updates.\n\t\tupdates: make(chan UpdateSignal, 1),\n\t\twatcher: c.Watcher,\n\t}\n}\n\n// type check\nvar _ Manager = (*DefaultManager)(nil)\n\n// Set implements the [Manager] interface for *DefaultManager.\nfunc (mgr *DefaultManager) Set(ctx context.Context, certKey TLSPair) (err error) {\n\tmgr.logger.DebugContext(ctx, \"setting\", \"cert\", certKey.CertPath, \"key\", certKey.KeyPath)\n\n\tvar errs []error\n\n\tmgr.pairMu.Lock()\n\tdefer mgr.pairMu.Unlock()\n\n\told := mgr.pair\n\n\terrs = mgr.appendUnwatchErr(errs, \"old cert\", old.CertPath)\n\terrs = mgr.appendUnwatchErr(errs, \"old key\", old.KeyPath)\n\terrs = mgr.appendWatchErr(errs, \"new cert\", certKey.CertPath)\n\terrs = mgr.appendWatchErr(errs, \"new key\", certKey.KeyPath)\n\n\tmgr.pair = certKey\n\n\treturn errors.Join(errs...)\n}\n\n// appendUnwatchErr stops watching a file at path p described by what and\n// appends an error to the errs slice, if any.  Empty p is ignored.\nfunc (mgr *DefaultManager) appendUnwatchErr(errs []error, what, p string) (result []error) {\n\tif p == \"\" {\n\t\treturn errs\n\t}\n\n\terr := mgr.watcher.Remove(p)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"unwatching %s %s: %w\", what, p, err))\n\t}\n\n\treturn errs\n}\n\n// appendWatchErr starts watching a file at path p described by what and\n// appends an error to the errs slice, if any.  Empty p is ignored.\nfunc (mgr *DefaultManager) appendWatchErr(errs []error, what, p string) (result []error) {\n\tif p == \"\" {\n\t\treturn errs\n\t}\n\n\terr := mgr.watcher.Add(p)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"watching %s %s: %w\", what, p, err))\n\t}\n\n\treturn errs\n}\n\n// Refresh implements the [service.Refresher] interface for *DefaultManager.\nfunc (mgr *DefaultManager) Refresh(ctx context.Context) (err error) {\n\tmgr.logger.DebugContext(ctx, \"refreshing\")\n\n\tselect {\n\tcase mgr.updates <- UpdateSignal{}:\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\treturn fmt.Errorf(\"refreshing: %w\", ctx.Err())\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// Start implements the [service.Interface] interface for *DefaultManager.\nfunc (mgr *DefaultManager) Start(ctx context.Context) (err error) {\n\terr = mgr.watcher.Start(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting watcher: %w\", err)\n\t}\n\n\tgo mgr.handleEvents(ctx)\n\n\treturn nil\n}\n\n// Shutdown implements the [service.Interface] interface for *DefaultManager.\nfunc (mgr *DefaultManager) Shutdown(ctx context.Context) (err error) {\n\tdefer close(mgr.updates)\n\n\terr = mgr.watcher.Shutdown(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"shutting down watcher: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Updates implements the [Manager] interface for *DefaultManager.\nfunc (mgr *DefaultManager) Updates(ctx context.Context) (updates <-chan UpdateSignal) {\n\treturn mgr.updates\n}\n\n// handleEvents handles changes of the tracked files.  It is intended to be run\n// in a separate goroutine.\nfunc (mgr *DefaultManager) handleEvents(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, mgr.logger)\n\n\teventsCh := mgr.watcher.Events()\n\tif eventsCh == nil {\n\t\tmgr.logger.DebugContext(ctx, \"watcher does not emit events\")\n\n\t\treturn\n\t}\n\n\tfor range eventsCh {\n\t\terr := mgr.Refresh(ctx)\n\t\tif err != nil {\n\t\t\tmgr.logger.ErrorContext(ctx, \"refreshing\", slogutil.KeyError, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/aghtls/manager.go",
    "content": "package aghtls\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/service\"\n)\n\n// TLSPair is a pair of paths to a certificate and a key.\ntype TLSPair struct {\n\t// CertPath is the path to the certificate.  If empty, the certificate will\n\t// not be tracked.\n\tCertPath string\n\n\t// KeyPath is the path to the key.  If empty, the key will not be tracked.\n\tKeyPath string\n}\n\n// UpdateSignal is the signal that the TLS certificate and key have been\n// updated.\ntype UpdateSignal struct{}\n\n// Manager manages TLS certificates and keys updates.\ntype Manager interface {\n\tservice.Interface\n\tservice.Refresher\n\n\t// Set sets the TLS certificate and key.  certKey may have unset fields,\n\t// in which case the corresponding files will not be tracked.\n\tSet(ctx context.Context, certKey TLSPair) (err error)\n\n\t// Updates returns a channel that emits signals when the TLS certificate\n\t// and/or key have been updated.\n\t//\n\t// TODO(e.burkov):  Move reloading logic to the manager and get rid of this\n\t// method.\n\tUpdates(ctx context.Context) (updates <-chan UpdateSignal)\n}\n\n// EmptyManager is an empty implementation of the [Manager] interface.\ntype EmptyManager struct{}\n\n// type check\nvar _ Manager = (*EmptyManager)(nil)\n\n// Start implements the [service.Interface] interface for EmptyManager.  It\n// always returns nil.\nfunc (EmptyManager) Start(_ context.Context) (err error) { return nil }\n\n// Shutdown implements the [service.Interface] interface for EmptyManager.  It\n// always returns nil.\nfunc (EmptyManager) Shutdown(_ context.Context) (err error) { return nil }\n\n// Refresh implements the [service.Refresher] interface for EmptyManager.  It\n// always returns nil.\nfunc (EmptyManager) Refresh(_ context.Context) (err error) { return nil }\n\n// Set implements the [Manager] interface for EmptyManager.  It always returns\n// nil.\nfunc (EmptyManager) Set(_ context.Context, _ TLSPair) (err error) { return nil }\n\n// Updates implements the [Manager] interface for EmptyManager.  It always\n// returns a nil channel.\nfunc (EmptyManager) Updates(_ context.Context) (updates <-chan UpdateSignal) { return nil }\n"
  },
  {
    "path": "internal/aghtls/root.go",
    "content": "package aghtls\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"log/slog\"\n)\n\n// SystemRootCAs tries to load root certificates from the operating system.  It\n// returns nil in case nothing is found so that Go' crypto/x509 can use its\n// default algorithm to find system root CA list.\n//\n// See https://github.com/AdguardTeam/AdGuardHome/issues/1311.\nfunc SystemRootCAs(ctx context.Context, l *slog.Logger) (roots *x509.CertPool) {\n\treturn rootCAs(ctx, l)\n}\n"
  },
  {
    "path": "internal/aghtls/root_linux.go",
    "content": "//go:build linux\n\npackage aghtls\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\nfunc rootCAs(ctx context.Context, l *slog.Logger) (roots *x509.CertPool) {\n\t// Directories with the system root certificates, which aren't supported by\n\t// Go's crypto/x509.\n\tdirs := []string{\n\t\t// Entware.\n\t\t\"/opt/etc/ssl/certs\",\n\t}\n\n\troots = x509.NewCertPool()\n\tfor _, dir := range dirs {\n\t\tif addCertsFromDir(ctx, l, roots, dir) {\n\t\t\treturn roots\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// addCertsFromDir appends all readable PEM files from dir to pool.  It returns\n// true if at least one certificate was accepted.\nfunc addCertsFromDir(\n\tctx context.Context,\n\tl *slog.Logger,\n\tpool *x509.CertPool,\n\tdir string,\n) (ok bool) {\n\tdirEnts, err := os.ReadDir(dir)\n\tif err != nil {\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\t// TODO(a.garipov): Improve error handling here and in other places.\n\t\t\tl.ErrorContext(ctx, \"opening directory\", slogutil.KeyError, err)\n\t\t}\n\n\t\treturn false\n\t}\n\n\tvar rootsAdded bool\n\tfor _, de := range dirEnts {\n\t\tvar certData []byte\n\t\trootFile := filepath.Join(dir, de.Name())\n\t\tcertData, err = os.ReadFile(rootFile)\n\t\tif err != nil {\n\t\t\tl.ErrorContext(ctx, \"reading root cert\", slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif !pool.AppendCertsFromPEM(certData) {\n\t\t\tl.ErrorContext(ctx, \"adding root cert\", \"file\", rootFile, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\trootsAdded = true\n\t}\n\n\treturn rootsAdded\n}\n"
  },
  {
    "path": "internal/aghtls/root_others.go",
    "content": "//go:build !linux\n\npackage aghtls\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"log/slog\"\n)\n\nfunc rootCAs(_ context.Context, _ *slog.Logger) (roots *x509.CertPool) {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghuser/aghuser.go",
    "content": "package aghuser\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// Login is the type for web user logins.\ntype Login string\n\n// NewLogin returns a web user login.  The length of s must not be greater than\n// [math.MaxUint16].\n//\n// TODO(s.chzhen): Add more constraints as needed.\nfunc NewLogin(s string) (l Login, err error) {\n\tif s == \"\" {\n\t\treturn \"\", errors.ErrEmptyValue\n\t}\n\n\treturn Login(s), nil\n}\n\n// Password is an interface that defines methods for handling web user\n// passwords.\ntype Password interface {\n\t// Authenticate returns true if the provided password is allowed.\n\tAuthenticate(ctx context.Context, password string) (ok bool)\n\n\t// Hash returns a hashed representation of the web user password.\n\tHash() (b []byte)\n}\n\n// DefaultPassword is the default bcrypt implementation of the [Password]\n// interface.\ntype DefaultPassword struct {\n\thash []byte\n}\n\n// NewDefaultPassword returns the new properly initialized *DefaultPassword.\nfunc NewDefaultPassword(hash string) (p *DefaultPassword) {\n\treturn &DefaultPassword{\n\t\thash: []byte(hash),\n\t}\n}\n\n// type check\nvar _ Password = (*DefaultPassword)(nil)\n\n// Authenticate implements the [Password] interface for *DefaultPassword.\nfunc (p *DefaultPassword) Authenticate(ctx context.Context, passwd string) (ok bool) {\n\treturn bcrypt.CompareHashAndPassword([]byte(p.hash), []byte(passwd)) == nil\n}\n\n// Hash implements the [Password] interface for *DefaultPassword.\nfunc (p *DefaultPassword) Hash() (b []byte) {\n\treturn p.hash\n}\n"
  },
  {
    "path": "internal/aghuser/aghuser_test.go",
    "content": "package aghuser_test\n\nimport \"time\"\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n"
  },
  {
    "path": "internal/aghuser/db.go",
    "content": "package aghuser\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"maps\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// DB is an interface that defines methods for interacting with user\n// information.  All methods must be safe for concurrent use.\n//\n// TODO(s.chzhen):  Use this.\n//\n// TODO(s.chzhen):  Consider updating methods to return a clone.\ntype DB interface {\n\t// All retrieves all users from the database, sorted by login.\n\t//\n\t// TODO(s.chzhen):  Consider function signature change to reflect the\n\t// in-memory implementation, as it currently always returns nil for error.\n\tAll(ctx context.Context) (users []*User, err error)\n\n\t// ByLogin retrieves a user by their login.  u must not be modified.\n\t//\n\t// TODO(s.chzhen):  Remove this once user sessions support [UserID].\n\tByLogin(ctx context.Context, login Login) (u *User, err error)\n\n\t// ByUUID retrieves a user by their unique identifier.  u must not be\n\t// modified.\n\t//\n\t// TODO(s.chzhen):  Use this.\n\tByUUID(ctx context.Context, id UserID) (u *User, err error)\n\n\t// Create adds a new user to the database.  If the credentials already\n\t// exist, it returns the [errors.ErrDuplicated] error.  It also can return\n\t// an error from the cryptographic randomness reader.  u must not be\n\t// modified.\n\tCreate(ctx context.Context, u *User) (err error)\n}\n\n// DefaultDB is the default in-memory implementation of the [DB] interface.\ntype DefaultDB struct {\n\t// mu protects all properties below.\n\tmu *sync.Mutex\n\n\t// loginToUserID maps a web user login to their UserID.  The values must not\n\t// be empty.\n\t//\n\t// TODO(s.chzhen):  Remove this once user sessions support [UserID].\n\tloginToUserID map[Login]UserID\n\n\t// userIDToUser maps a UserID to a web user.  The values must not be nil.\n\t// It must be synchronized with loginToUserID, meaning all UserIDs stored in\n\t// loginToUserID must also be stored in this map.\n\tuserIDToUser map[UserID]*User\n}\n\n// NewDefaultDB returns the new properly initialized *DefaultDB.\nfunc NewDefaultDB() (db *DefaultDB) {\n\treturn &DefaultDB{\n\t\tmu:            &sync.Mutex{},\n\t\tloginToUserID: map[Login]UserID{},\n\t\tuserIDToUser:  map[UserID]*User{},\n\t}\n}\n\n// type check\nvar _ DB = (*DefaultDB)(nil)\n\n// All implements the [DB] interface for *DefaultDB.\nfunc (db *DefaultDB) All(ctx context.Context) (users []*User, err error) {\n\tdb.mu.Lock()\n\tdefer db.mu.Unlock()\n\n\tif len(db.userIDToUser) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tusers = slices.SortedStableFunc(\n\t\tmaps.Values(db.userIDToUser),\n\t\tfunc(a, b *User) (res int) {\n\t\t\t// TODO(s.chzhen):  Consider adding a custom comparer.\n\t\t\treturn cmp.Compare(a.Login, b.Login)\n\t\t},\n\t)\n\n\treturn users, nil\n}\n\n// ByLogin implements the [DB] interface for *DefaultDB.\nfunc (db *DefaultDB) ByLogin(ctx context.Context, login Login) (u *User, err error) {\n\tdb.mu.Lock()\n\tdefer db.mu.Unlock()\n\n\tid, ok := db.loginToUserID[login]\n\tif !ok {\n\t\treturn nil, nil\n\t}\n\n\tu, ok = db.userIDToUser[id]\n\tif !ok {\n\t\t// Should not happen.\n\t\tpanic(fmt.Errorf(\"no web user present with login %q\", login))\n\t}\n\n\treturn u, nil\n}\n\n// ByUUID implements the [DB] interface for *DefaultDB.\nfunc (db *DefaultDB) ByUUID(ctx context.Context, id UserID) (u *User, err error) {\n\tdb.mu.Lock()\n\tdefer db.mu.Unlock()\n\n\tu, ok := db.userIDToUser[id]\n\tif !ok {\n\t\treturn nil, nil\n\t}\n\n\treturn u, nil\n}\n\n// Create implements the [DB] interface for *DefaultDB.\nfunc (db *DefaultDB) Create(ctx context.Context, u *User) (err error) {\n\tdb.mu.Lock()\n\tdefer db.mu.Unlock()\n\n\tif u.ID == (UserID{}) {\n\t\treturn fmt.Errorf(\"userid: %w\", errors.ErrEmptyValue)\n\t}\n\n\t_, ok := db.userIDToUser[u.ID]\n\tif ok {\n\t\treturn fmt.Errorf(\"userid: %w\", errors.ErrDuplicated)\n\t}\n\n\t_, ok = db.loginToUserID[u.Login]\n\tif ok {\n\t\treturn fmt.Errorf(\"login: %w\", errors.ErrDuplicated)\n\t}\n\n\tdb.userIDToUser[u.ID] = u\n\tdb.loginToUserID[u.Login] = u.ID\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghuser/db_test.go",
    "content": "package aghuser_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghuser\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nfunc TestDB(t *testing.T) {\n\tdb := aghuser.NewDefaultDB()\n\n\tconst (\n\t\tuserWithIDPassRaw = \"user_with_id_password\"\n\t\tuserSecondPassRaw = \"user_second_password\"\n\t)\n\n\tuserWithIDPassHash, err := bcrypt.GenerateFromPassword(\n\t\t[]byte(userWithIDPassRaw),\n\t\tbcrypt.DefaultCost,\n\t)\n\trequire.NoError(t, err)\n\n\tuserSecondPassHash, err := bcrypt.GenerateFromPassword(\n\t\t[]byte(userSecondPassRaw),\n\t\tbcrypt.DefaultCost,\n\t)\n\trequire.NoError(t, err)\n\n\tuserWithIDPass := aghuser.NewDefaultPassword(string(userWithIDPassHash))\n\tuserSecondPass := aghuser.NewDefaultPassword(string(userSecondPassHash))\n\n\tvar (\n\t\tuserWithID = &aghuser.User{\n\t\t\tID:       aghuser.MustNewUserID(),\n\t\t\tLogin:    \"user_with_id\",\n\t\t\tPassword: userWithIDPass,\n\t\t}\n\t\tuserSecond = &aghuser.User{\n\t\t\tID:       aghuser.MustNewUserID(),\n\t\t\tLogin:    \"user_second\",\n\t\t\tPassword: userSecondPass,\n\t\t}\n\t\tuserDuplicateLogin = &aghuser.User{\n\t\t\tID:       aghuser.MustNewUserID(),\n\t\t\tLogin:    userWithID.Login,\n\t\t\tPassword: userWithIDPass,\n\t\t}\n\t)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\terr = db.Create(ctx, userWithID)\n\trequire.NoError(t, err)\n\n\terr = db.Create(ctx, userSecond)\n\trequire.NoError(t, err)\n\n\terr = db.Create(ctx, userDuplicateLogin)\n\tassert.ErrorIs(t, err, errors.ErrDuplicated)\n\n\tgot, err := db.ByUUID(ctx, userWithID.ID)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, userWithID, got)\n\tassert.True(t, got.Password.Authenticate(ctx, userWithIDPassRaw))\n\n\tgot, err = db.ByLogin(ctx, userSecond.Login)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, userSecond, got)\n\tassert.True(t, got.Password.Authenticate(ctx, userSecondPassRaw))\n\n\tusers, err := db.All(ctx)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, users, 2)\n\tassert.Equal(t, []*aghuser.User{userSecond, userWithID}, users)\n}\n"
  },
  {
    "path": "internal/aghuser/session.go",
    "content": "package aghuser\n\nimport (\n\t\"crypto/rand\"\n\t\"time\"\n)\n\n// SessionTokenLength is the length of the web user session token.\nconst SessionTokenLength = 16\n\n// SessionToken is the type for the web user session token.\ntype SessionToken [SessionTokenLength]byte\n\n// NewSessionToken returns a cryptographically secure randomly generated web\n// user session token.  If an error occurs during random generation, it will\n// cause the program to crash.\nfunc NewSessionToken() (t SessionToken) {\n\t_, _ = rand.Read(t[:])\n\n\treturn t\n}\n\n// Session represents a web user session.\ntype Session struct {\n\t// Expire indicates when the session will expire.\n\tExpire time.Time\n\n\t// UserLogin is the login of the web user associated with the session.\n\t//\n\t// TODO(s.chzhen):  Remove this field and associate the user by UserID.\n\tUserLogin Login\n\n\t// Token is the session token.\n\tToken SessionToken\n\n\t// UserID is the identifier of the web user associated with the session.\n\tUserID UserID\n}\n"
  },
  {
    "path": "internal/aghuser/sessionstorage.go",
    "content": "package aghuser\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"go.etcd.io/bbolt\"\n\tberrors \"go.etcd.io/bbolt/errors\"\n)\n\n// SessionStorage is an interface that defines methods for handling web user\n// sessions.  All methods must be safe for concurrent use.\n//\n// TODO(s.chzhen):  Add DeleteAll method.\ntype SessionStorage interface {\n\t// New creates a new session for the web user.\n\tNew(ctx context.Context, u *User) (s *Session, err error)\n\n\t// FindByToken returns the stored session for the web user based on the session\n\t// token.\n\t//\n\t// TODO(s.chzhen):  Consider function signature change to reflect the\n\t// in-memory implementation, as it currently always returns nil for error.\n\tFindByToken(ctx context.Context, t SessionToken) (s *Session, err error)\n\n\t// DeleteByToken removes a stored web user session by the provided token.\n\tDeleteByToken(ctx context.Context, t SessionToken) (err error)\n\n\t// Close releases the web user sessions database resources.\n\tClose() (err error)\n}\n\n// DefaultSessionStorageConfig represents the web user session storage\n// configuration structure.\ntype DefaultSessionStorageConfig struct {\n\t// Logger is used for logging the operation of the session storage.  It must\n\t// not be nil.\n\tLogger *slog.Logger\n\n\t// Clock is used to get the current time.  It must not be nil.\n\tClock timeutil.Clock\n\n\t// UserDB contains the web user information such as ID, login, and password.\n\t// It must not be nil.\n\tUserDB DB\n\n\t// DBPath is the path to the database file where session data is stored.  It\n\t// must not be empty.\n\tDBPath string\n\n\t// SessionTTL is the default Time-To-Live duration for web user sessions.\n\t// It specifies how long a session should last and is a required field.\n\tSessionTTL time.Duration\n}\n\n// DefaultSessionStorage is the default bbolt database implementation of the\n// [SessionStorage] interface.\ntype DefaultSessionStorage struct {\n\t// db is an instance of the bbolt database where web user sessions are\n\t// stored by [SessionToken] in the [bucketNameSessions] bucket.\n\tdb *bbolt.DB\n\n\t// logger is used for logging the operation of the session storage.\n\tlogger *slog.Logger\n\n\t// mu protects sessions.\n\tmu *sync.Mutex\n\n\t// clock is used to get the current time.\n\tclock timeutil.Clock\n\n\t// userDB contains the web user information such as ID, login, and password.\n\tuserDB DB\n\n\t// sessions maps a session token to a web user session.\n\tsessions map[SessionToken]*Session\n\n\t// sessionTTL is the default Time-To-Live value for web user sessions.\n\tsessionTTL time.Duration\n}\n\n// NewDefaultSessionStorage returns the new properly initialized\n// *DefaultSessionStorage.\nfunc NewDefaultSessionStorage(\n\tctx context.Context,\n\tconf *DefaultSessionStorageConfig,\n) (ds *DefaultSessionStorage, err error) {\n\tds = &DefaultSessionStorage{\n\t\tclock:      conf.Clock,\n\t\tuserDB:     conf.UserDB,\n\t\tlogger:     conf.Logger,\n\t\tmu:         &sync.Mutex{},\n\t\tsessions:   map[SessionToken]*Session{},\n\t\tsessionTTL: conf.SessionTTL,\n\t}\n\n\tdbFilename := conf.DBPath\n\t// TODO(s.chzhen):  Pass logger with options.\n\tds.db, err = bbolt.Open(dbFilename, aghos.DefaultPermFile, nil)\n\tif err != nil {\n\t\tds.logger.ErrorContext(ctx, \"opening db %q: %w\", dbFilename, err)\n\t\tif errors.Is(err, berrors.ErrInvalid) {\n\t\t\tconst s = \"AdGuard Home cannot be initialized due to an incompatible file system.\\n\" +\n\t\t\t\t\"Please read the explanation here: https://adguard-dns.io/kb/adguard-home/getting-started/#limitations\"\n\t\t\tslogutil.PrintLines(ctx, ds.logger, slog.LevelError, \"\", s)\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\terr = ds.loadSessions(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"loading sessions: %w\", err)\n\t}\n\n\treturn ds, nil\n}\n\n// loadSessions loads web user sessions from the bbolt database.\nfunc (ds *DefaultSessionStorage) loadSessions(ctx context.Context) (err error) {\n\ttx, err := ds.db.Begin(true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting transaction: %w\", err)\n\t}\n\n\tneedRollback := true\n\tdefer func() {\n\t\tif needRollback {\n\t\t\terr = errors.WithDeferred(err, tx.Rollback())\n\t\t}\n\t}()\n\n\tbkt := tx.Bucket([]byte(bboltBucketSessions))\n\tif bkt == nil {\n\t\treturn nil\n\t}\n\n\tremoved, err := ds.processSessions(ctx, bkt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"processing sessions: %w\", err)\n\t}\n\n\tif removed == 0 {\n\t\tds.logger.DebugContext(ctx, \"loading sessions from db\", \"stored\", len(ds.sessions))\n\n\t\treturn nil\n\t}\n\n\tneedRollback = false\n\terr = tx.Commit()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"committing transaction: %w\", err)\n\t}\n\n\tds.logger.DebugContext(\n\t\tctx,\n\t\t\"loading sessions from db\",\n\t\t\"stored\", len(ds.sessions),\n\t\t\"removed\", removed,\n\t)\n\n\treturn nil\n}\n\n// processSessions iterates over the sessions bucket and loads or removes\n// sessions as needed.\nfunc (ds *DefaultSessionStorage) processSessions(\n\tctx context.Context,\n\tbkt *bbolt.Bucket,\n) (removed int, err error) {\n\tinvalidSessions := [][]byte{}\n\n\terr = bkt.ForEach(ds.bboltSessionHandler(ctx, &invalidSessions))\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"iterating over sessions: %w\", err)\n\t}\n\n\tvar errs []error\n\tfor _, s := range invalidSessions {\n\t\tif err = bkt.Delete(s); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif err = errors.Join(errs...); err != nil {\n\t\treturn 0, fmt.Errorf(\"deleting sessions: %w\", err)\n\t}\n\n\treturn len(invalidSessions), nil\n}\n\n// bboltSessionHandler returns a function for [bbolt.Bucket.ForEach] that\n// iterates over stored sessions, deserializes them, and logs any errors\n// encountered.  The returned error is always nil, as these errors are\n// considered non-critical to stop the iteration process.\nfunc (ds *DefaultSessionStorage) bboltSessionHandler(\n\tctx context.Context,\n\tinvalidSessions *[][]byte,\n) (fn func(k, v []byte) (err error)) {\n\tnow := ds.clock.Now()\n\n\treturn func(k, v []byte) (err error) {\n\t\ts, err := bboltDecode(v)\n\t\tif err != nil {\n\t\t\t*invalidSessions = append(*invalidSessions, k)\n\t\t\tds.logger.DebugContext(ctx, \"deserializing session\", slogutil.KeyError, err)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tif now.After(s.Expire) {\n\t\t\t*invalidSessions = append(*invalidSessions, k)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tu, err := ds.userDB.ByLogin(ctx, s.UserLogin)\n\t\tif err != nil {\n\t\t\t// Should not happen, as it currently always returns nil for error.\n\t\t\tpanic(err)\n\t\t}\n\n\t\tif u == nil {\n\t\t\t*invalidSessions = append(*invalidSessions, k)\n\t\t\tds.logger.DebugContext(ctx, \"no saved user by name\", \"name\", s.UserLogin)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tt := SessionToken(k)\n\t\ts.Token = t\n\t\ts.UserID = u.ID\n\t\tds.sessions[t] = s\n\n\t\treturn nil\n\t}\n}\n\n// bboltBucketSessions is the name of the bucket storing web user sessions in\n// the bbolt database.\nconst bboltBucketSessions = \"sessions-2\"\n\nconst (\n\t// bboltSessionExpireLen is the length of the expire field in the binary\n\t// entry stored in bbolt.\n\tbboltSessionExpireLen = 4\n\n\t// bboltSessionNameLen is the length of the name field in the binary entry\n\t// stored in bbolt.\n\tbboltSessionNameLen = 2\n)\n\n// bboltDecode deserializes decodes a binary data into a session.\nfunc bboltDecode(data []byte) (s *Session, err error) {\n\tif len(data) < bboltSessionExpireLen+bboltSessionNameLen {\n\t\treturn nil, fmt.Errorf(\"length of the data is less than expected: got %d\", len(data))\n\t}\n\n\texpireData := data[:bboltSessionExpireLen]\n\tnameLenData := data[bboltSessionExpireLen : bboltSessionExpireLen+bboltSessionNameLen]\n\tnameData := data[bboltSessionExpireLen+bboltSessionNameLen:]\n\n\tnameLen := binary.BigEndian.Uint16(nameLenData)\n\tif len(nameData) != int(nameLen) {\n\t\treturn nil, fmt.Errorf(\"login: expected length %d, got %d\", nameLen, len(nameData))\n\t}\n\n\texpire := binary.BigEndian.Uint32(expireData)\n\n\treturn &Session{\n\t\tExpire:    time.Unix(int64(expire), 0),\n\t\tUserLogin: Login(nameData),\n\t}, nil\n}\n\n// bboltEncode serializes a session properties into a binary data.\nfunc bboltEncode(s *Session) (data []byte) {\n\tdata = make([]byte, bboltSessionExpireLen+bboltSessionNameLen+len(s.UserLogin))\n\n\texpireData := data[:bboltSessionExpireLen]\n\tnameLenData := data[bboltSessionExpireLen : bboltSessionExpireLen+bboltSessionNameLen]\n\tnameData := data[bboltSessionExpireLen+bboltSessionNameLen:]\n\n\texpire := uint32(s.Expire.Unix())\n\tbinary.BigEndian.PutUint32(expireData, expire)\n\tbinary.BigEndian.PutUint16(nameLenData, uint16(len(s.UserLogin)))\n\tcopy(nameData, []byte(s.UserLogin))\n\n\treturn data\n}\n\n// type check\nvar _ SessionStorage = (*DefaultSessionStorage)(nil)\n\n// New implements the [SessionStorage] interface for *DefaultSessionStorage.\nfunc (ds *DefaultSessionStorage) New(ctx context.Context, u *User) (s *Session, err error) {\n\ts = &Session{\n\t\tToken:     NewSessionToken(),\n\t\tUserID:    u.ID,\n\t\tUserLogin: u.Login,\n\t\tExpire:    ds.clock.Now().Add(ds.sessionTTL),\n\t}\n\n\terr = ds.store(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"storing session: %w\", err)\n\t}\n\n\tds.mu.Lock()\n\tdefer ds.mu.Unlock()\n\n\tds.sessions[s.Token] = s\n\n\treturn s, nil\n}\n\n// store saves a web user session in the bbolt database.\nfunc (ds *DefaultSessionStorage) store(s *Session) (err error) {\n\ttx, err := ds.db.Begin(true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting transaction: %w\", err)\n\t}\n\n\tneedRollback := true\n\tdefer func() {\n\t\tif needRollback {\n\t\t\terr = errors.WithDeferred(err, tx.Rollback())\n\t\t}\n\t}()\n\n\tbkt, err := tx.CreateBucketIfNotExists([]byte(bboltBucketSessions))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating bucket: %w\", err)\n\t}\n\n\terr = bkt.Put(s.Token[:], bboltEncode(s))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"putting data: %w\", err)\n\t}\n\n\tneedRollback = false\n\terr = tx.Commit()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"committing transaction: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// FindByToken implements the [SessionStorage] interface for *DefaultSessionStorage.\nfunc (ds *DefaultSessionStorage) FindByToken(ctx context.Context, t SessionToken) (s *Session, err error) {\n\tds.mu.Lock()\n\tdefer ds.mu.Unlock()\n\n\ts, ok := ds.sessions[t]\n\tif !ok {\n\t\treturn nil, nil\n\t}\n\n\tnow := ds.clock.Now()\n\tif now.After(s.Expire) {\n\t\terr = ds.deleteByToken(ctx, t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"expired session: %w\", err)\n\t\t}\n\n\t\treturn nil, nil\n\t}\n\n\treturn s, nil\n}\n\n// DeleteByToken implements the [SessionStorage] interface for\n// *DefaultSessionStorage.\nfunc (ds *DefaultSessionStorage) DeleteByToken(ctx context.Context, t SessionToken) (err error) {\n\tds.mu.Lock()\n\tdefer ds.mu.Unlock()\n\n\t// Don't wrap the error because it's informative enough as is.\n\treturn ds.deleteByToken(ctx, t)\n}\n\n// deleteByToken removes stored session by token.  ds.mu is expected to be\n// locked.\nfunc (ds *DefaultSessionStorage) deleteByToken(ctx context.Context, t SessionToken) (err error) {\n\terr = ds.remove(ctx, t)\n\tif err != nil {\n\t\tds.logger.ErrorContext(ctx, \"deleting session\", slogutil.KeyError, err)\n\n\t\treturn err\n\t}\n\n\tdelete(ds.sessions, t)\n\n\treturn nil\n}\n\n// remove deletes a web user session from the bbolt database.\nfunc (ds *DefaultSessionStorage) remove(ctx context.Context, t SessionToken) (err error) {\n\ttx, err := ds.db.Begin(true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting transaction: %w\", err)\n\t}\n\n\tneedRollback := true\n\tdefer func() {\n\t\tif needRollback {\n\t\t\terr = errors.WithDeferred(err, tx.Rollback())\n\t\t}\n\t}()\n\n\tbkt := tx.Bucket([]byte(bboltBucketSessions))\n\tif bkt == nil {\n\t\treturn errors.Error(\"no bucket\")\n\t}\n\n\terr = bkt.Delete(t[:])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"removing data: %w\", err)\n\t}\n\n\tneedRollback = false\n\terr = tx.Commit()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"committing transaction: %w\", err)\n\t}\n\n\tds.logger.DebugContext(ctx, \"removed session from db\")\n\n\treturn err\n}\n\n// Close implements the [SessionStorage] interface for *DefaultSessionStorage.\nfunc (ds *DefaultSessionStorage) Close() (err error) {\n\terr = ds.db.Close()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"closing db: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/aghuser/sessionstorage_test.go",
    "content": "package aghuser_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghuser\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/faketime\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// addSession is a helper function that saves and returns a session for a newly\n// generated [aghuser.User] by login.\nfunc addSession(\n\ttb testing.TB,\n\tctx context.Context,\n\tds aghuser.SessionStorage,\n\tlogin aghuser.Login,\n) (s *aghuser.Session) {\n\ttb.Helper()\n\n\ts, err := ds.New(ctx, &aghuser.User{\n\t\tID:    aghuser.MustNewUserID(),\n\t\tLogin: login,\n\t})\n\trequire.NoError(tb, err)\n\trequire.NotNil(tb, s)\n\n\tvar got *aghuser.Session\n\tgot, err = ds.FindByToken(ctx, s.Token)\n\trequire.NoError(tb, err)\n\trequire.NotNil(tb, got)\n\n\tassert.Equal(tb, login, got.UserLogin)\n\n\treturn s\n}\n\nfunc TestDefaultSessionStorage(t *testing.T) {\n\tconst (\n\t\tuserLoginFirst  aghuser.Login = \"user_one\"\n\t\tuserLoginSecond aghuser.Login = \"user_two\"\n\t)\n\n\tvar (\n\t\tctx    = testutil.ContextWithTimeout(t, testTimeout)\n\t\tlogger = slogutil.NewDiscardLogger()\n\t)\n\n\tconst (\n\t\tsessionTTL = time.Minute\n\t\ttimeStep   = time.Second\n\t)\n\n\t// Set up a mock clock to test expired sessions. Each call to [clock.Now]\n\t// will return the [date] incremented by [timeStep].\n\tdate := time.Now()\n\tclock := &faketime.Clock{\n\t\tOnNow: func() (now time.Time) {\n\t\t\tdate = date.Add(timeStep)\n\n\t\t\treturn date\n\t\t},\n\t}\n\n\tdbFile, err := os.CreateTemp(t.TempDir(), \"sessions.db\")\n\trequire.NoError(t, err)\n\ttestutil.CleanupAndRequireSuccess(t, dbFile.Close)\n\n\tuserDB := aghuser.NewDefaultDB()\n\n\terr = userDB.Create(ctx, &aghuser.User{\n\t\tLogin: userLoginFirst,\n\t\tID:    aghuser.MustNewUserID(),\n\t})\n\trequire.NoError(t, err)\n\n\terr = userDB.Create(ctx, &aghuser.User{\n\t\tLogin: userLoginSecond,\n\t\tID:    aghuser.MustNewUserID(),\n\t})\n\trequire.NoError(t, err)\n\n\tvar (\n\t\tds *aghuser.DefaultSessionStorage\n\n\t\tsessionFirst  *aghuser.Session\n\t\tsessionSecond *aghuser.Session\n\t)\n\n\trequire.True(t, t.Run(\"prepare_session_storage\", func(t *testing.T) {\n\t\tds, err = aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{\n\t\t\tClock:      clock,\n\t\t\tUserDB:     userDB,\n\t\t\tLogger:     logger,\n\t\t\tDBPath:     dbFile.Name(),\n\t\t\tSessionTTL: sessionTTL,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tsessionFirst = addSession(t, ctx, ds, userLoginFirst)\n\n\t\t// Advance time to ensure the first session expires before creating the\n\t\t// second session.\n\t\tdate = date.Add(time.Hour)\n\n\t\tsessionSecond = addSession(t, ctx, ds, userLoginSecond)\n\n\t\terr = ds.Close()\n\t\trequire.NoError(t, err)\n\t}))\n\n\trequire.True(t, t.Run(\"load_sessions\", func(t *testing.T) {\n\t\tds, err = aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{\n\t\t\tClock:      clock,\n\t\t\tUserDB:     userDB,\n\t\t\tLogger:     logger,\n\t\t\tDBPath:     dbFile.Name(),\n\t\t\tSessionTTL: sessionTTL,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tvar got *aghuser.Session\n\t\tgot, err = ds.FindByToken(ctx, sessionFirst.Token)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Nil(t, got)\n\n\t\tgot, err = ds.FindByToken(ctx, sessionSecond.Token)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, got)\n\n\t\tassert.Equal(t, userLoginSecond, got.UserLogin)\n\n\t\terr = ds.DeleteByToken(ctx, sessionSecond.Token)\n\t\trequire.NoError(t, err)\n\n\t\tgot, err = ds.FindByToken(ctx, sessionSecond.Token)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Nil(t, got)\n\t}))\n\n\trequire.True(t, t.Run(\"expired_session\", func(t *testing.T) {\n\t\ttestutil.CleanupAndRequireSuccess(t, ds.Close)\n\n\t\tsessionFirst = addSession(t, ctx, ds, userLoginFirst)\n\n\t\tdate = date.Add(time.Hour)\n\n\t\tvar got *aghuser.Session\n\t\tgot, err = ds.FindByToken(ctx, sessionFirst.Token)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Nil(t, got)\n\t}))\n}\n"
  },
  {
    "path": "internal/aghuser/user.go",
    "content": "// Package aghuser contains types and logic for dealing with AdGuard Home's web\n// users.\npackage aghuser\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/google/uuid\"\n)\n\n// UserID is the type for the unique IDs of web users.\ntype UserID uuid.UUID\n\n// NewUserID returns a new web user unique identifier.  Any error returned is an\n// error from the cryptographic randomness reader.\nfunc NewUserID() (uid UserID, err error) {\n\tuuidv7, err := uuid.NewV7()\n\n\treturn UserID(uuidv7), err\n}\n\n// MustNewUserID is a wrapper around [NewUserID] that panics if there is an\n// error.  It is currently only used in tests.\nfunc MustNewUserID() (uid UserID) {\n\tuid, err := NewUserID()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unexpected uuidv7 error: %w\", err))\n\t}\n\n\treturn uid\n}\n\n// User represents a web user.\ntype User struct {\n\t// Password stores the password information for the web user.  It must not\n\t// be nil.\n\tPassword Password\n\n\t// Login is the login name of the web user.  It must not be empty.\n\tLogin Login\n\n\t// ID is the unique identifier for the web user.  It must not be empty.\n\tID UserID\n}\n"
  },
  {
    "path": "internal/arpdb/arpdb.go",
    "content": "// Package arpdb implements the Network Neighborhood Database.\npackage arpdb\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/AdguardTeam/golibs/service\"\n)\n\n// Variables and functions to substitute in tests.\nvar (\n\t// rootDirFS is the filesystem pointing to the root directory.\n\trootDirFS = osutil.RootDirFS()\n)\n\n// Interface stores and refreshes the network neighborhood reported by ARP\n// (Address Resolution Protocol).\ntype Interface interface {\n\t// Refresher updates the stored data.  It must be safe for concurrent use.\n\tservice.Refresher\n\n\t// Neighbors returns the last set of data reported by ARP.  Both the method\n\t// and it's result must be safe for concurrent use.\n\tNeighbors() (ns []Neighbor)\n}\n\n// New returns the [Interface] properly initialized for the OS.\nfunc New(logger *slog.Logger) (arp Interface) {\n\treturn newARPDB(logger, executil.SystemCommandConstructor{})\n}\n\n// Empty is the [Interface] implementation that does nothing.\ntype Empty struct{}\n\n// type check\nvar _ Interface = Empty{}\n\n// Refresh implements the [Interface] interface for EmptyARPContainer.  It does\n// nothing and always returns nil error.\nfunc (Empty) Refresh(_ context.Context) (err error) { return nil }\n\n// Neighbors implements the [Interface] interface for EmptyARPContainer.  It\n// always returns nil.\nfunc (Empty) Neighbors() (ns []Neighbor) { return nil }\n\n// Neighbor is the pair of IP address and MAC address reported by ARP.\ntype Neighbor struct {\n\t// Name is the hostname of the neighbor.  Empty name is valid since not each\n\t// implementation of ARP is able to retrieve that.\n\tName string\n\n\t// IP contains either IPv4 or IPv6.\n\tIP netip.Addr\n\n\t// MAC contains the hardware address.\n\tMAC net.HardwareAddr\n}\n\n// newNeighbor returns the new initialized [Neighbor] by parsing string\n// representations of IP and MAC addresses.\nfunc newNeighbor(host, ipStr, macStr string) (n *Neighbor, err error) {\n\tdefer func() { err = errors.Annotate(err, \"getting arp neighbor: %w\") }()\n\n\tip, err := netip.ParseAddr(ipStr)\n\tif err != nil {\n\t\t// Don't wrap the error, as it will get annotated.\n\t\treturn nil, err\n\t}\n\n\tmac, err := net.ParseMAC(macStr)\n\tif err != nil {\n\t\t// Don't wrap the error, as it will get annotated.\n\t\treturn nil, err\n\t}\n\n\treturn &Neighbor{\n\t\tName: host,\n\t\tIP:   ip,\n\t\tMAC:  mac,\n\t}, nil\n}\n\n// Clone returns the deep copy of n.\nfunc (n Neighbor) Clone() (clone Neighbor) {\n\treturn Neighbor{\n\t\tName: n.Name,\n\t\tIP:   n.IP,\n\t\tMAC:  slices.Clone(n.MAC),\n\t}\n}\n\n// validatedHostname returns h if it's a valid hostname, or an empty string\n// otherwise, logging the validation error.\nfunc validatedHostname(logger *slog.Logger, h string) (host string) {\n\terr := netutil.ValidateHostname(h)\n\tif err != nil {\n\t\tlogger.Debug(\"parsing host of arp output\", slogutil.KeyError, err)\n\n\t\treturn \"\"\n\t}\n\n\treturn h\n}\n\n// neighs is the helper type that stores neighbors to avoid copying its methods\n// among all the [Interface] implementations.\ntype neighs struct {\n\tmu *sync.RWMutex\n\tns []Neighbor\n}\n\n// len returns the length of the neighbors slice.  It's safe for concurrent use.\nfunc (ns *neighs) len() (l int) {\n\tns.mu.RLock()\n\tdefer ns.mu.RUnlock()\n\n\treturn len(ns.ns)\n}\n\n// clone returns a deep copy of the underlying neighbors slice.  It's safe for\n// concurrent use.\nfunc (ns *neighs) clone() (cloned []Neighbor) {\n\tns.mu.RLock()\n\tdefer ns.mu.RUnlock()\n\n\tcloned = make([]Neighbor, len(ns.ns))\n\tfor i, n := range ns.ns {\n\t\tcloned[i] = n.Clone()\n\t}\n\n\treturn cloned\n}\n\n// reset replaces the underlying slice with the new one.  It's safe for\n// concurrent use.\nfunc (ns *neighs) reset(with []Neighbor) {\n\tns.mu.Lock()\n\tdefer ns.mu.Unlock()\n\n\tns.ns = with\n}\n\n// parseNeighsFunc parses the text from sc as if it'd be an output of some\n// ARP-related command.  lenHint is a hint for the size of the allocated slice\n// of Neighbors.\n//\n// TODO(s.chzhen):  Return []*Neighbor instead.\ntype parseNeighsFunc func(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor)\n\n// cmdARPDB is the implementation of the [Interface] that uses command line to\n// retrieve data.\ntype cmdARPDB struct {\n\tlogger  *slog.Logger\n\tcmdCons executil.CommandConstructor\n\tparse   parseNeighsFunc\n\tns      *neighs\n\tcmd     string\n\targs    []string\n}\n\n// type check\nvar _ Interface = (*cmdARPDB)(nil)\n\n// Refresh implements the [Interface] interface for *cmdARPDB.\nfunc (arp *cmdARPDB) Refresh(ctx context.Context) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"cmd arpdb: %w\") }()\n\n\tvar stdout bytes.Buffer\n\terr = executil.Run(\n\t\tctx,\n\t\tarp.cmdCons,\n\t\t&executil.CommandConfig{\n\t\t\tPath:   arp.cmd,\n\t\t\tArgs:   arp.args,\n\t\t\tStdout: &stdout,\n\t\t},\n\t)\n\tif err != nil {\n\t\tif code, ok := executil.ExitCodeFromError(err); ok {\n\t\t\treturn fmt.Errorf(\"running command: unexpected exit code %d\", code)\n\t\t}\n\n\t\treturn fmt.Errorf(\"running command: %w\", err)\n\t}\n\n\tsc := bufio.NewScanner(&stdout)\n\tns := arp.parse(arp.logger, sc, arp.ns.len())\n\tif err = sc.Err(); err != nil {\n\t\t// TODO(e.burkov):  This error seems unreachable.  Investigate.\n\t\treturn fmt.Errorf(\"scanning the output: %w\", err)\n\t}\n\n\tarp.ns.reset(ns)\n\n\treturn nil\n}\n\n// Neighbors implements the [Interface] interface for *cmdARPDB.\nfunc (arp *cmdARPDB) Neighbors() (ns []Neighbor) {\n\treturn arp.ns.clone()\n}\n\n// arpdbs is the [Interface] that combines several [Interface] implementations\n// and consequently switches between those.\ntype arpdbs struct {\n\t// arps is the set of [Interface] implementations to range through.\n\tarps []Interface\n\tneighs\n}\n\n// newARPDBs returns a properly initialized *arpdbs.  It begins refreshing from\n// the first of arps.\nfunc newARPDBs(arps ...Interface) (arp *arpdbs) {\n\treturn &arpdbs{\n\t\tarps: arps,\n\t\tneighs: neighs{\n\t\t\tmu: &sync.RWMutex{},\n\t\t\tns: make([]Neighbor, 0),\n\t\t},\n\t}\n}\n\n// type check\nvar _ Interface = (*arpdbs)(nil)\n\n// Refresh implements the [Interface] interface for *arpdbs.\nfunc (arp *arpdbs) Refresh(ctx context.Context) (err error) {\n\tvar errs []error\n\n\tfor _, a := range arp.arps {\n\t\terr = a.Refresh(ctx)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tarp.reset(a.Neighbors())\n\n\t\treturn nil\n\t}\n\n\treturn errors.Annotate(errors.Join(errs...), \"each arpdb failed: %w\")\n}\n\n// Neighbors implements the [Interface] interface for *arpdbs.\n//\n// TODO(e.burkov):  Think of a way to avoid cloning the slice twice.\nfunc (arp *arpdbs) Neighbors() (ns []Neighbor) {\n\treturn arp.clone()\n}\n"
  },
  {
    "path": "internal/arpdb/arpdb_bsd.go",
    "content": "//go:build darwin || freebsd\n\npackage arpdb\n\nimport (\n\t\"bufio\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\nfunc newARPDB(logger *slog.Logger, cmdCons executil.CommandConstructor) (arp *cmdARPDB) {\n\treturn &cmdARPDB{\n\t\tlogger:  logger,\n\t\tcmdCons: cmdCons,\n\t\tparse:   parseArpA,\n\t\tns: &neighs{\n\t\t\tmu: &sync.RWMutex{},\n\t\t\tns: make([]Neighbor, 0),\n\t\t},\n\t\tcmd: \"arp\",\n\t\t// Use -n flag to avoid resolving the hostnames of the neighbors.  By\n\t\t// default ARP attempts to resolve the hostnames via DNS.  See man 8\n\t\t// arp.\n\t\t//\n\t\t// See also https://github.com/AdguardTeam/AdGuardHome/issues/3157.\n\t\targs: []string{\"-a\", \"-n\"},\n\t}\n}\n\n// parseArpA parses the output of the \"arp -a -n\" command on macOS and FreeBSD.\n// The expected input format:\n//\n//\thost.name (192.168.0.1) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]\nfunc parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {\n\tns = make([]Neighbor, 0, lenHint)\n\tfor sc.Scan() {\n\t\tln := sc.Text()\n\n\t\tfields := strings.Fields(ln)\n\t\tif len(fields) < 4 {\n\t\t\tcontinue\n\t\t}\n\n\t\tipStr := fields[1]\n\t\tif len(ipStr) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\thost := validatedHostname(logger, fields[0])\n\t\tn, err := newNeighbor(host, ipStr[1:len(ipStr)-1], fields[3])\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"parsing arp output\", \"line\", ln, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tns = append(ns, *n)\n\t}\n\n\treturn ns\n}\n"
  },
  {
    "path": "internal/arpdb/arpdb_bsd_internal_test.go",
    "content": "//go:build darwin || freebsd\n\npackage arpdb\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n)\n\nconst arpAOutput = `\ninvalid.mac (1.2.3.4) at 12:34:56:78:910 on el0 ifscope [ethernet]\ninvalid.ip  (1.2.3.4.5) at ab:cd:ef:ab:cd:12 on ek0 ifscope [ethernet]\ninvalid.fmt 1 at 12:cd:ef:ab:cd:ef on er0 ifscope [ethernet]\nhostname.one (192.168.1.2) at ab:cd:ef:ab:cd:ef on en0 ifscope [ethernet]\nhostname.two (::ffff:ffff) at ef:cd:ab:ef:cd:ab on em0 expires in 1198 seconds [ethernet]\n? (::1234) at aa:bb:cc:dd:ee:ff on ej0 expires in 1918 seconds [ethernet]\n`\n\nvar wantNeighs = []Neighbor{{\n\tName: \"hostname.one\",\n\tIP:   netip.MustParseAddr(\"192.168.1.2\"),\n\tMAC:  net.HardwareAddr{0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF},\n}, {\n\tName: \"hostname.two\",\n\tIP:   netip.MustParseAddr(\"::ffff:ffff\"),\n\tMAC:  net.HardwareAddr{0xEF, 0xCD, 0xAB, 0xEF, 0xCD, 0xAB},\n}, {\n\tName: \"\",\n\tIP:   netip.MustParseAddr(\"::1234\"),\n\tMAC:  net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},\n}}\n"
  },
  {
    "path": "internal/arpdb/arpdb_internal_test.go",
    "content": "package arpdb\n\nimport (\n\t\"context\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is a common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testdata is the filesystem containing data for testing the package.\nvar testdata fs.FS = os.DirFS(\"./testdata\")\n\n// RunCmdFunc is the signature of aghos.RunCommand function.\ntype RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)\n\nfunc Test_New(t *testing.T) {\n\tvar a Interface\n\trequire.NotPanics(t, func() { a = New(slogutil.NewDiscardLogger()) })\n\n\tassert.NotNil(t, a)\n}\n\n// TODO(s.chzhen):  Consider moving mocks into aghtest.\n\n// TestARPDB is the mock implementation of [Interface] to use in tests.\ntype TestARPDB struct {\n\tOnRefresh   func(ctx context.Context) (err error)\n\tOnNeighbors func() (ns []Neighbor)\n}\n\n// type check\nvar _ Interface = (*TestARPDB)(nil)\n\n// Refresh implements the [Interface] interface for *TestARPDB.\nfunc (arp *TestARPDB) Refresh(ctx context.Context) (err error) {\n\treturn arp.OnRefresh(ctx)\n}\n\n// Neighbors implements the [Interface] interface for *TestARPDB.\nfunc (arp *TestARPDB) Neighbors() (ns []Neighbor) {\n\treturn arp.OnNeighbors()\n}\n\nfunc Test_NewARPDBs(t *testing.T) {\n\tknownIP := netip.MustParseAddr(\"1.2.3.4\")\n\tknownMAC := net.HardwareAddr{0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF}\n\n\tsuccRefrCount, failRefrCount := 0, 0\n\tclnp := func() {\n\t\tsuccRefrCount, failRefrCount = 0, 0\n\t}\n\n\tsuccDB := &TestARPDB{\n\t\tOnRefresh: func(_ context.Context) (err error) { succRefrCount++; return nil },\n\t\tOnNeighbors: func() (ns []Neighbor) {\n\t\t\treturn []Neighbor{{Name: \"abc\", IP: knownIP, MAC: knownMAC}}\n\t\t},\n\t}\n\tfailDB := &TestARPDB{\n\t\tOnRefresh: func(_ context.Context) (err error) {\n\t\t\tfailRefrCount++\n\n\t\t\treturn errors.Error(\"refresh failed\")\n\t\t},\n\t\tOnNeighbors: func() (ns []Neighbor) { return nil },\n\t}\n\n\tt.Run(\"begin_with_success\", func(t *testing.T) {\n\t\tt.Cleanup(clnp)\n\n\t\ta := newARPDBs(succDB, failDB)\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 1, succRefrCount)\n\t\tassert.Zero(t, failRefrCount)\n\t\tassert.NotEmpty(t, a.Neighbors())\n\t})\n\n\tt.Run(\"begin_with_fail\", func(t *testing.T) {\n\t\tt.Cleanup(clnp)\n\n\t\ta := newARPDBs(failDB, succDB)\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 1, succRefrCount)\n\t\tassert.Equal(t, 1, failRefrCount)\n\t\tassert.NotEmpty(t, a.Neighbors())\n\t})\n\n\tt.Run(\"fail_only\", func(t *testing.T) {\n\t\tt.Cleanup(clnp)\n\n\t\twantMsg := \"each arpdb failed: refresh failed\\nrefresh failed\"\n\n\t\ta := newARPDBs(failDB, failDB)\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.Error(t, err)\n\n\t\ttestutil.AssertErrorMsg(t, wantMsg, err)\n\n\t\tassert.Equal(t, 2, failRefrCount)\n\t\tassert.Empty(t, a.Neighbors())\n\t})\n\n\tt.Run(\"fail_after_success\", func(t *testing.T) {\n\t\tt.Cleanup(clnp)\n\n\t\tshouldFail := false\n\t\tunstableDB := &TestARPDB{\n\t\t\tOnRefresh: func(_ context.Context) (err error) {\n\t\t\t\tif shouldFail {\n\t\t\t\t\terr = errors.Error(\"unstable failed\")\n\t\t\t\t}\n\t\t\t\tshouldFail = !shouldFail\n\n\t\t\t\treturn err\n\t\t\t},\n\t\t\tOnNeighbors: func() (ns []Neighbor) {\n\t\t\t\tif !shouldFail {\n\t\t\t\t\treturn failDB.OnNeighbors()\n\t\t\t\t}\n\n\t\t\t\treturn succDB.OnNeighbors()\n\t\t\t},\n\t\t}\n\t\ta := newARPDBs(unstableDB, succDB)\n\n\t\t// Unstable ARPDB should refresh successfully.\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Zero(t, succRefrCount)\n\t\tassert.NotEmpty(t, a.Neighbors())\n\n\t\t// Unstable ARPDB should fail and the succDB should be used.\n\t\terr = a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 1, succRefrCount)\n\t\tassert.NotEmpty(t, a.Neighbors())\n\n\t\t// Unstable ARPDB should refresh successfully again.\n\t\terr = a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 1, succRefrCount)\n\t\tassert.NotEmpty(t, a.Neighbors())\n\t})\n\n\tt.Run(\"empty\", func(t *testing.T) {\n\t\ta := newARPDBs()\n\t\trequire.NoError(t, a.Refresh(testutil.ContextWithTimeout(t, testTimeout)))\n\n\t\tassert.Empty(t, a.Neighbors())\n\t})\n}\n\nfunc TestCmdARPDB_arpa(t *testing.T) {\n\ta := &cmdARPDB{\n\t\tlogger: slogutil.NewDiscardLogger(),\n\t\tcmd:    \"cmd\",\n\t\tparse:  parseArpA,\n\t\tns: &neighs{\n\t\t\tmu: &sync.RWMutex{},\n\t\t\tns: make([]Neighbor, 0),\n\t\t},\n\t}\n\n\tt.Run(\"arp_a\", func(t *testing.T) {\n\t\ta.cmdCons = agh.NewCommandConstructor(\"cmd\", 0, arpAOutput, nil)\n\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, wantNeighs, a.Neighbors())\n\t})\n\n\tt.Run(\"runcmd_error\", func(t *testing.T) {\n\t\ta.cmdCons = agh.NewCommandConstructor(\"cmd\", 0, \"\", errors.Error(\"can't run\"))\n\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\ttestutil.AssertErrorMsg(t, \"cmd arpdb: running command: running: can't run\", err)\n\t})\n\n\tt.Run(\"bad_code\", func(t *testing.T) {\n\t\ta.cmdCons = agh.NewCommandConstructor(\"cmd\", 1, \"\", nil)\n\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\ttestutil.AssertErrorMsg(\n\t\t\tt,\n\t\t\t\"cmd arpdb: running command: unexpected exit code 1\",\n\t\t\terr,\n\t\t)\n\t})\n\n\tt.Run(\"empty\", func(t *testing.T) {\n\t\ta.cmdCons = agh.NewCommandConstructor(\"cmd\", 0, \"\", nil)\n\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, a.Neighbors())\n\t})\n}\n\nfunc TestEmptyARPDB(t *testing.T) {\n\ta := Empty{}\n\n\tt.Run(\"refresh\", func(t *testing.T) {\n\t\tvar err error\n\t\trequire.NotPanics(t, func() {\n\t\t\terr = a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\t})\n\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"neighbors\", func(t *testing.T) {\n\t\tvar ns []Neighbor\n\t\trequire.NotPanics(t, func() {\n\t\t\tns = a.Neighbors()\n\t\t})\n\n\t\tassert.Empty(t, ns)\n\t})\n}\n"
  },
  {
    "path": "internal/arpdb/arpdb_linux.go",
    "content": "//go:build linux\n\npackage arpdb\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n)\n\nfunc newARPDB(logger *slog.Logger, cmdCons executil.CommandConstructor) (arp *arpdbs) {\n\t// Use the common storage among the implementations.\n\tns := &neighs{\n\t\tmu: &sync.RWMutex{},\n\t\tns: make([]Neighbor, 0),\n\t}\n\n\tvar parseF parseNeighsFunc\n\tif aghos.IsOpenWrt() {\n\t\tparseF = parseArpAWrt\n\t} else {\n\t\tparseF = parseArpA\n\t}\n\n\treturn newARPDBs(\n\t\t// Try /proc/net/arp first.\n\t\t&fsysARPDB{\n\t\t\tns:       ns,\n\t\t\tfsys:     rootDirFS,\n\t\t\tfilename: \"proc/net/arp\",\n\t\t},\n\t\t// Then, try \"arp -a -n\".\n\t\t&cmdARPDB{\n\t\t\tlogger:  logger,\n\t\t\tcmdCons: cmdCons,\n\t\t\tparse:   parseF,\n\t\t\tns:      ns,\n\t\t\tcmd:     \"arp\",\n\t\t\t// Use -n flag to avoid resolving the hostnames of the neighbors.\n\t\t\t// By default ARP attempts to resolve the hostnames via DNS.  See\n\t\t\t// man 8 arp.\n\t\t\t//\n\t\t\t// See also https://github.com/AdguardTeam/AdGuardHome/issues/3157.\n\t\t\targs: []string{\"-a\", \"-n\"},\n\t\t},\n\t\t// Finally, try \"ip neigh\".\n\t\t&cmdARPDB{\n\t\t\tlogger:  logger,\n\t\t\tcmdCons: cmdCons,\n\t\t\tparse:   parseIPNeigh,\n\t\t\tns:      ns,\n\t\t\tcmd:     \"ip\",\n\t\t\targs:    []string{\"neigh\"},\n\t\t},\n\t)\n}\n\n// fsysARPDB accesses the ARP cache file to update the database.\ntype fsysARPDB struct {\n\tns       *neighs\n\tfsys     fs.FS\n\tfilename string\n}\n\n// type check\nvar _ Interface = (*fsysARPDB)(nil)\n\n// Refresh implements the [Interface] interface for *fsysARPDB.\nfunc (arp *fsysARPDB) Refresh(_ context.Context) (err error) {\n\tvar f fs.File\n\tf, err = arp.fsys.Open(arp.filename)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"opening %q: %w\", arp.filename, err)\n\t}\n\n\tsc := bufio.NewScanner(f)\n\t// Skip the header.\n\tif !sc.Scan() {\n\t\treturn nil\n\t} else if err = sc.Err(); err != nil {\n\t\treturn err\n\t}\n\n\tns := make([]Neighbor, 0, arp.ns.len())\n\tfor sc.Scan() {\n\t\tn := parseNeighbor(sc.Text())\n\t\tif n != nil {\n\t\t\tns = append(ns, *n)\n\t\t}\n\t}\n\n\tarp.ns.reset(ns)\n\n\treturn nil\n}\n\n// parseNeighbor parses line into *Neighbor.\nfunc parseNeighbor(line string) (n *Neighbor) {\n\tfields := stringutil.SplitTrimmed(line, \" \")\n\tif len(fields) != 6 {\n\t\treturn nil\n\t}\n\n\tip, err := netip.ParseAddr(fields[0])\n\tif err != nil || ip.IsUnspecified() {\n\t\treturn nil\n\t}\n\n\tmac, err := net.ParseMAC(fields[3])\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn &Neighbor{\n\t\tIP:  ip,\n\t\tMAC: mac,\n\t}\n}\n\n// Neighbors implements the [Interface] interface for *fsysARPDB.\nfunc (arp *fsysARPDB) Neighbors() (ns []Neighbor) {\n\treturn arp.ns.clone()\n}\n\n// parseArpAWrt parses the output of the \"arp -a -n\" command on OpenWrt.  The\n// expected input format:\n//\n//\tIP address     HW type  Flags  HW address         Mask  Device\n//\t192.168.11.98  0x1      0x2    5a:92:df:a9:7e:28  *     wan\nfunc parseArpAWrt(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {\n\t// Skip the header.\n\tif !sc.Scan() {\n\t\treturn nil\n\t}\n\n\tns = make([]Neighbor, 0, lenHint)\n\tfor sc.Scan() {\n\t\tln := sc.Text()\n\n\t\tfields := strings.Fields(ln)\n\t\tif len(fields) < 4 {\n\t\t\tcontinue\n\t\t}\n\n\t\tn, err := newNeighbor(\"\", fields[0], fields[3])\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"parsing arp output\", \"line\", ln, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tns = append(ns, *n)\n\t}\n\n\treturn ns\n}\n\n// parseArpA parses the output of the \"arp -a -n\" command on Linux.  The\n// expected input format:\n//\n//\thostname (192.168.1.1) at ab:cd:ef:ab:cd:ef [ether] on enp0s3\nfunc parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {\n\tns = make([]Neighbor, 0, lenHint)\n\tfor sc.Scan() {\n\t\tln := sc.Text()\n\n\t\tfields := strings.Fields(ln)\n\t\tif len(fields) < 4 {\n\t\t\tcontinue\n\t\t}\n\n\t\tipStr := fields[1]\n\t\tif len(ipStr) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\thost := validatedHostname(logger, fields[0])\n\t\tn, err := newNeighbor(host, ipStr[1:len(ipStr)-1], fields[3])\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"parsing arp output\", \"line\", ln, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tns = append(ns, *n)\n\t}\n\n\treturn ns\n}\n\n// parseIPNeigh parses the output of the \"ip neigh\" command on Linux.  The\n// expected input format:\n//\n//\t192.168.1.1 dev enp0s3 lladdr ab:cd:ef:ab:cd:ef REACHABLE\nfunc parseIPNeigh(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {\n\tns = make([]Neighbor, 0, lenHint)\n\tfor sc.Scan() {\n\t\tln := sc.Text()\n\n\t\tfields := strings.Fields(ln)\n\t\tif len(fields) < 5 {\n\t\t\tcontinue\n\t\t}\n\n\t\tn, err := newNeighbor(\"\", fields[0], fields[4])\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"parsing arp output\", \"line\", ln, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tns = append(ns, *n)\n\t}\n\n\treturn ns\n}\n"
  },
  {
    "path": "internal/arpdb/arpdb_linux_internal_test.go",
    "content": "//go:build linux\n\npackage arpdb\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"sync\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst arpAOutputWrt = `\nIP address    HW type     Flags       HW address            Mask     Device\n1.2.3.4.5     0x1         0x2         aa:bb:cc:dd:ee:ff     *        wan\n1.2.3.4       0x1         0x2         12:34:56:78:910       *        wan\n192.168.1.2   0x1         0x2         ab:cd:ef:ab:cd:ef     *        wan\n::ffff:ffff   0x1         0x2         ef:cd:ab:ef:cd:ab     *        wan`\n\nconst arpAOutput = `\ninvalid.mac (1.2.3.4) at 12:34:56:78:910 on el0 ifscope [ethernet]\ninvalid.ip  (1.2.3.4.5) at ab:cd:ef:ab:cd:12 on ek0 ifscope [ethernet]\ninvalid.fmt 1 at 12:cd:ef:ab:cd:ef on er0 ifscope [ethernet]\n? (192.168.1.2) at ab:cd:ef:ab:cd:ef on en0 ifscope [ethernet]\n? (::ffff:ffff) at ef:cd:ab:ef:cd:ab on em0 expires in 100 seconds [ethernet]`\n\nconst ipNeighOutput = `\n1.2.3.4.5 dev enp0s3 lladdr aa:bb:cc:dd:ee:ff DELAY\n1.2.3.4 dev enp0s3 lladdr 12:34:56:78:910 DELAY\n192.168.1.2 dev enp0s3 lladdr ab:cd:ef:ab:cd:ef DELAY\n::ffff:ffff dev enp0s3 lladdr ef:cd:ab:ef:cd:ab router STALE`\n\nvar wantNeighs = []Neighbor{{\n\tIP:  netip.MustParseAddr(\"192.168.1.2\"),\n\tMAC: net.HardwareAddr{0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF},\n}, {\n\tIP:  netip.MustParseAddr(\"::ffff:ffff\"),\n\tMAC: net.HardwareAddr{0xEF, 0xCD, 0xAB, 0xEF, 0xCD, 0xAB},\n}}\n\nfunc TestFSysARPDB(t *testing.T) {\n\trequire.NoError(t, fstest.TestFS(testdata, \"proc_net_arp\"))\n\n\ta := &fsysARPDB{\n\t\tns: &neighs{\n\t\t\tmu: &sync.RWMutex{},\n\t\t\tns: make([]Neighbor, 0),\n\t\t},\n\t\tfsys:     testdata,\n\t\tfilename: \"proc_net_arp\",\n\t}\n\n\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\trequire.NoError(t, err)\n\n\tns := a.Neighbors()\n\tassert.Equal(t, wantNeighs, ns)\n}\n\nfunc TestCmdARPDB_linux(t *testing.T) {\n\tt.Run(\"wrt\", func(t *testing.T) {\n\t\ta := &cmdARPDB{\n\t\t\tlogger:  slogutil.NewDiscardLogger(),\n\t\t\tcmdCons: agh.NewCommandConstructor(\"arp -a\", 0, arpAOutputWrt, nil),\n\t\t\tparse:   parseArpAWrt,\n\t\t\tcmd:     \"arp\",\n\t\t\targs:    []string{\"-a\"},\n\t\t\tns: &neighs{\n\t\t\t\tmu: &sync.RWMutex{},\n\t\t\t\tns: make([]Neighbor, 0),\n\t\t\t},\n\t\t}\n\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, wantNeighs, a.Neighbors())\n\t})\n\n\tt.Run(\"ip_neigh\", func(t *testing.T) {\n\t\ta := &cmdARPDB{\n\t\t\tlogger:  slogutil.NewDiscardLogger(),\n\t\t\tcmdCons: agh.NewCommandConstructor(\"ip neigh\", 0, ipNeighOutput, nil),\n\t\t\tparse:   parseIPNeigh,\n\t\t\tcmd:     \"ip\",\n\t\t\targs:    []string{\"neigh\"},\n\t\t\tns: &neighs{\n\t\t\t\tmu: &sync.RWMutex{},\n\t\t\t\tns: make([]Neighbor, 0),\n\t\t\t},\n\t\t}\n\t\terr := a.Refresh(testutil.ContextWithTimeout(t, testTimeout))\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, wantNeighs, a.Neighbors())\n\t})\n}\n"
  },
  {
    "path": "internal/arpdb/arpdb_openbsd.go",
    "content": "//go:build openbsd\n\npackage arpdb\n\nimport (\n\t\"bufio\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\nfunc newARPDB(logger *slog.Logger, cmdCons executil.CommandConstructor) (arp *cmdARPDB) {\n\treturn &cmdARPDB{\n\t\tlogger:  logger,\n\t\tcmdCons: cmdCons,\n\t\tparse:   parseArpA,\n\t\tns: &neighs{\n\t\t\tmu: &sync.RWMutex{},\n\t\t\tns: make([]Neighbor, 0),\n\t\t},\n\t\tcmd: \"arp\",\n\t\t// Use -n flag to avoid resolving the hostnames of the neighbors.  By\n\t\t// default ARP attempts to resolve the hostnames via DNS.  See man 8\n\t\t// arp.\n\t\t//\n\t\t// See also https://github.com/AdguardTeam/AdGuardHome/issues/3157.\n\t\targs: []string{\"-a\", \"-n\"},\n\t}\n}\n\n// parseArpA parses the output of the \"arp -a -n\" command on OpenBSD.  The\n// expected input format:\n//\n//\tHost        Ethernet Address  Netif Expire    Flags\n//\t192.168.1.1 ab:cd:ef:ab:cd:ef   em0 19m59s\nfunc parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {\n\t// Skip the header.\n\tif !sc.Scan() {\n\t\treturn nil\n\t}\n\n\tns = make([]Neighbor, 0, lenHint)\n\tfor sc.Scan() {\n\t\tln := sc.Text()\n\n\t\tfields := strings.Fields(ln)\n\t\tif len(fields) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tn, err := newNeighbor(\"\", fields[0], fields[1])\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"parsing arp output\", \"line\", ln, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tns = append(ns, *n)\n\t}\n\n\treturn ns\n}\n"
  },
  {
    "path": "internal/arpdb/arpdb_openbsd_internal_test.go",
    "content": "//go:build openbsd\n\npackage arpdb\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n)\n\nconst arpAOutput = `\nHost        Ethernet Address  Netif Expire    Flags\n1.2.3.4.5   aa:bb:cc:dd:ee:ff   em0 permanent\n1.2.3.4     12:34:56:78:910     em0 permanent\n192.168.1.2 ab:cd:ef:ab:cd:ef   em0 19m56s\n::ffff:ffff ef:cd:ab:ef:cd:ab   em0 permanent l\n`\n\nvar wantNeighs = []Neighbor{{\n\tIP:  netip.MustParseAddr(\"192.168.1.2\"),\n\tMAC: net.HardwareAddr{0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF},\n}, {\n\tIP:  netip.MustParseAddr(\"::ffff:ffff\"),\n\tMAC: net.HardwareAddr{0xEF, 0xCD, 0xAB, 0xEF, 0xCD, 0xAB},\n}}\n"
  },
  {
    "path": "internal/arpdb/arpdb_windows.go",
    "content": "//go:build windows\n\npackage arpdb\n\nimport (\n\t\"bufio\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\nfunc newARPDB(logger *slog.Logger, cmdCons executil.CommandConstructor) (arp *cmdARPDB) {\n\treturn &cmdARPDB{\n\t\tlogger:  logger,\n\t\tcmdCons: cmdCons,\n\t\tparse:   parseArpA,\n\t\tns: &neighs{\n\t\t\tmu: &sync.RWMutex{},\n\t\t\tns: make([]Neighbor, 0),\n\t\t},\n\t\tcmd:  \"arp\",\n\t\targs: []string{\"/a\"},\n\t}\n}\n\n// parseArpA parses the output of the \"arp /a\" command on Windows.  The expected\n// input format (the first line is empty):\n//\n//\tInterface: 192.168.56.16 --- 0x7\n//\t  Internet Address      Physical Address      Type\n//\t  192.168.56.1          0a-00-27-00-00-00     dynamic\n//\t  192.168.56.255        ff-ff-ff-ff-ff-ff     static\nfunc parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {\n\tns = make([]Neighbor, 0, lenHint)\n\tfor sc.Scan() {\n\t\tln := sc.Text()\n\t\tif ln == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tfields := strings.Fields(ln)\n\t\tif len(fields) != 3 {\n\t\t\tcontinue\n\t\t}\n\n\t\tn, err := newNeighbor(\"\", fields[0], fields[1])\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"parsing arp output\", \"line\", ln, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tns = append(ns, *n)\n\t}\n\n\treturn ns\n}\n"
  },
  {
    "path": "internal/arpdb/arpdb_windows_internal_test.go",
    "content": "//go:build windows\n\npackage arpdb\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n)\n\nconst arpAOutput = `\n\nInterface: 192.168.1.1 --- 0x7\n  Internet Address      Physical Address      Type\n  192.168.1.2           ab-cd-ef-ab-cd-ef     dynamic\n  ::ffff:ffff           ef-cd-ab-ef-cd-ab     static`\n\nvar wantNeighs = []Neighbor{{\n\tIP:  netip.MustParseAddr(\"192.168.1.2\"),\n\tMAC: net.HardwareAddr{0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF},\n}, {\n\tIP:  netip.MustParseAddr(\"::ffff:ffff\"),\n\tMAC: net.HardwareAddr{0xEF, 0xCD, 0xAB, 0xEF, 0xCD, 0xAB},\n}}\n"
  },
  {
    "path": "internal/arpdb/testdata/proc_net_arp",
    "content": "IP address     HW type     Flags       HW address            Mask     Device\n192.168.1.2    0x1         0x2         ab:cd:ef:ab:cd:ef     *        wan\n::ffff:ffff    0x1         0x0         ef:cd:ab:ef:cd:ab     *        br-lan\n0.0.0.0        0x0         0x0         00:00:00:00:00:00     *        unspec\n1.2.3.4.5      0x1         0x2         aa:bb:cc:dd:ee:ff     *        wan\n1.2.3.4        0x1         0x2         12:34:56:78:910       *        wan\n"
  },
  {
    "path": "internal/client/addrproc.go",
    "content": "package client\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/rdns\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n)\n\n// ErrClosed is returned from [AddressProcessor.Close] if it's closed more than\n// once.\nconst ErrClosed errors.Error = \"use of closed address processor\"\n\n// AddressProcessor is the interface for types that can process clients.\ntype AddressProcessor interface {\n\tProcess(ctx context.Context, ip netip.Addr)\n\tClose() (err error)\n}\n\n// EmptyAddrProc is an [AddressProcessor] that does nothing.\ntype EmptyAddrProc struct{}\n\n// type check\nvar _ AddressProcessor = EmptyAddrProc{}\n\n// Process implements the [AddressProcessor] interface for EmptyAddrProc.\nfunc (EmptyAddrProc) Process(_ context.Context, _ netip.Addr) {}\n\n// Close implements the [AddressProcessor] interface for EmptyAddrProc.\nfunc (EmptyAddrProc) Close() (_ error) { return nil }\n\n// DefaultAddrProcConfig is the configuration structure for address processors.\ntype DefaultAddrProcConfig struct {\n\t// BaseLogger is used to create loggers with custom prefixes for sources of\n\t// information about runtime clients.  It must not be nil.\n\tBaseLogger *slog.Logger\n\n\t// DialContext is used to create TCP connections to WHOIS servers.\n\t// DialContext must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true.\n\tDialContext aghnet.DialContextFunc\n\n\t// Exchanger is used to perform rDNS queries.  Exchanger must not be nil if\n\t// [DefaultAddrProcConfig.UseRDNS] is true.\n\tExchanger rdns.Exchanger\n\n\t// PrivateSubnets are used to determine if an incoming IP address is\n\t// private.  It must not be nil.\n\tPrivateSubnets netutil.SubnetSet\n\n\t// AddressUpdater is used to update the information about a client's IP\n\t// address.  It must not be nil.\n\tAddressUpdater AddressUpdater\n\n\t// InitialAddresses are the addresses that are queued for processing\n\t// immediately by [NewDefaultAddrProc].\n\tInitialAddresses []netip.Addr\n\n\t// CatchPanics, if true, makes the address processor catch and log panics.\n\t//\n\t// TODO(a.garipov): Consider better ways to do this or apply this method to\n\t// other parts of the codebase.\n\tCatchPanics bool\n\n\t// UseRDNS, if true, enables resolving of client IP addresses using reverse\n\t// DNS.\n\tUseRDNS bool\n\n\t// UsePrivateRDNS, if true, enables resolving of private client IP addresses\n\t// using reverse DNS.  See [DefaultAddrProcConfig.PrivateSubnets].\n\tUsePrivateRDNS bool\n\n\t// UseWHOIS, if true, enables resolving of client IP addresses using WHOIS.\n\tUseWHOIS bool\n}\n\n// AddressUpdater is the interface for storages of DNS clients that can update\n// information about them.\n//\n// TODO(a.garipov): Consider using the actual client storage once it is moved\n// into this package.\ntype AddressUpdater interface {\n\t// UpdateAddress updates information about an IP address, setting host (if\n\t// not empty) and WHOIS information (if not nil).\n\tUpdateAddress(ctx context.Context, ip netip.Addr, host string, info *whois.Info)\n}\n\n// DefaultAddrProc processes incoming client addresses with rDNS and WHOIS, if\n// configured, and updates that information in a client storage.\ntype DefaultAddrProc struct {\n\t// logger is used to log the operation of address processor.\n\tlogger *slog.Logger\n\n\t// clientIPsMu serializes closure of clientIPs and access to isClosed.\n\tclientIPsMu *sync.Mutex\n\n\t// clientIPs is the channel queueing client processing tasks.\n\tclientIPs chan netip.Addr\n\n\t// rdns is used to perform rDNS lookups of clients' IP addresses.\n\trdns rdns.Interface\n\n\t// whois is used to perform WHOIS lookups of clients' IP addresses.\n\twhois whois.Interface\n\n\t// addrUpdater is used to update the information about a client's IP\n\t// address.\n\taddrUpdater AddressUpdater\n\n\t// privateSubnets are used to determine if an incoming IP address is\n\t// private.\n\tprivateSubnets netutil.SubnetSet\n\n\t// isClosed is set to true once the address processor is closed.\n\tisClosed bool\n\n\t// usePrivateRDNS, if true, enables resolving of private client IP addresses\n\t// using reverse DNS.\n\tusePrivateRDNS bool\n}\n\nconst (\n\t// defaultQueueSize is the size of queue of IPs for rDNS and WHOIS\n\t// processing.\n\tdefaultQueueSize = 255\n\n\t// defaultCacheSize is the maximum size of the cache for rDNS and WHOIS\n\t// processing.  It must be greater than zero.\n\tdefaultCacheSize = 10_000\n\n\t// defaultIPTTL is the Time to Live duration for IP addresses cached by\n\t// rDNS and WHOIS.\n\tdefaultIPTTL = 1 * time.Hour\n)\n\n// NewDefaultAddrProc returns a new running client address processor.  c must\n// not be nil.\nfunc NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {\n\tp = &DefaultAddrProc{\n\t\tlogger:         c.BaseLogger.With(slogutil.KeyPrefix, \"addrproc\"),\n\t\tclientIPsMu:    &sync.Mutex{},\n\t\tclientIPs:      make(chan netip.Addr, defaultQueueSize),\n\t\trdns:           &rdns.Empty{},\n\t\taddrUpdater:    c.AddressUpdater,\n\t\twhois:          &whois.Empty{},\n\t\tprivateSubnets: c.PrivateSubnets,\n\t\tusePrivateRDNS: c.UsePrivateRDNS,\n\t}\n\n\tif c.UseRDNS {\n\t\tp.rdns = rdns.New(&rdns.Config{\n\t\t\tLogger:    c.BaseLogger.With(slogutil.KeyPrefix, \"rdns\"),\n\t\t\tExchanger: c.Exchanger,\n\t\t\tCacheSize: defaultCacheSize,\n\t\t\tCacheTTL:  defaultIPTTL,\n\t\t})\n\t}\n\n\tif c.UseWHOIS {\n\t\tp.whois = newWHOIS(c.BaseLogger.With(slogutil.KeyPrefix, \"whois\"), c.DialContext)\n\t}\n\n\t// TODO(s.chzhen):  Pass context.\n\tctx := context.TODO()\n\n\tgo p.process(ctx, c.CatchPanics)\n\n\tfor _, ip := range c.InitialAddresses {\n\t\tp.Process(ctx, ip)\n\t}\n\n\treturn p\n}\n\n// newWHOIS returns a whois.Interface instance using the given function for\n// dialing.\nfunc newWHOIS(logger *slog.Logger, dialFunc aghnet.DialContextFunc) (w whois.Interface) {\n\t// TODO(s.chzhen):  Consider making configurable.\n\tconst (\n\t\t// defaultTimeout is the timeout for WHOIS requests.\n\t\tdefaultTimeout = 5 * time.Second\n\n\t\t// defaultMaxConnReadSize is an upper limit in bytes for reading from a\n\t\t// net.Conn.\n\t\tdefaultMaxConnReadSize = 64 * 1024\n\n\t\t// defaultMaxRedirects is the maximum redirects count.\n\t\tdefaultMaxRedirects = 5\n\n\t\t// defaultMaxInfoLen is the maximum length of whois.Info fields.\n\t\tdefaultMaxInfoLen = 250\n\t)\n\n\treturn whois.New(&whois.Config{\n\t\tLogger:          logger,\n\t\tDialContext:     dialFunc,\n\t\tServerAddr:      whois.DefaultServer,\n\t\tPort:            whois.DefaultPort,\n\t\tTimeout:         defaultTimeout,\n\t\tCacheSize:       defaultCacheSize,\n\t\tMaxConnReadSize: defaultMaxConnReadSize,\n\t\tMaxRedirects:    defaultMaxRedirects,\n\t\tMaxInfoLen:      defaultMaxInfoLen,\n\t\tCacheTTL:        defaultIPTTL,\n\t})\n}\n\n// type check\nvar _ AddressProcessor = (*DefaultAddrProc)(nil)\n\n// Process implements the [AddressProcessor] interface for *DefaultAddrProc.\nfunc (p *DefaultAddrProc) Process(ctx context.Context, ip netip.Addr) {\n\tp.clientIPsMu.Lock()\n\tdefer p.clientIPsMu.Unlock()\n\n\tif p.isClosed {\n\t\treturn\n\t}\n\n\tselect {\n\tcase p.clientIPs <- ip:\n\t\t// Go on.\n\tdefault:\n\t\tp.logger.DebugContext(ctx, \"ip channel is full\", \"len\", len(p.clientIPs))\n\t}\n}\n\n// process processes the incoming client IP-address information.  It is intended\n// to be used as a goroutine.  Once clientIPs is closed, process exits.\nfunc (p *DefaultAddrProc) process(ctx context.Context, catchPanics bool) {\n\tif catchPanics {\n\t\tdefer slogutil.RecoverAndLog(ctx, p.logger)\n\t}\n\n\tp.logger.InfoContext(ctx, \"processing addresses\")\n\n\tfor ip := range p.clientIPs {\n\t\thost := p.processRDNS(ctx, ip)\n\t\tinfo := p.processWHOIS(ctx, ip)\n\n\t\tp.addrUpdater.UpdateAddress(ctx, ip, host, info)\n\t}\n\n\tp.logger.InfoContext(ctx, \"finished processing addresses\")\n}\n\n// processRDNS resolves the clients' IP addresses using reverse DNS.  host is\n// empty if there were errors or if the information hasn't changed.\nfunc (p *DefaultAddrProc) processRDNS(ctx context.Context, ip netip.Addr) (host string) {\n\tstart := time.Now()\n\tp.logger.DebugContext(ctx, \"processing rdns\", \"ip\", ip)\n\tdefer func() {\n\t\tp.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"finished processing rdns\",\n\t\t\t\"ip\", ip,\n\t\t\t\"host\", host,\n\t\t\t\"elapsed\", time.Since(start),\n\t\t)\n\t}()\n\n\tok := p.shouldResolve(ip)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\thost, changed := p.rdns.Process(ctx, ip)\n\tif !changed {\n\t\thost = \"\"\n\t}\n\n\treturn host\n}\n\n// shouldResolve returns false if ip is a loopback address, or ip is private and\n// resolving of private addresses is disabled.\nfunc (p *DefaultAddrProc) shouldResolve(ip netip.Addr) (ok bool) {\n\treturn !ip.IsLoopback() && (p.usePrivateRDNS || !p.privateSubnets.Contains(ip))\n}\n\n// processWHOIS looks up the information about clients' IP addresses in the\n// WHOIS databases.  info is nil if there were errors or if the information\n// hasn't changed.\nfunc (p *DefaultAddrProc) processWHOIS(ctx context.Context, ip netip.Addr) (info *whois.Info) {\n\tstart := time.Now()\n\tp.logger.DebugContext(ctx, \"processing whois\", \"ip\", ip)\n\tdefer func() {\n\t\tp.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"finished processing whois\",\n\t\t\t\"ip\", ip,\n\t\t\t\"whois\", info,\n\t\t\t\"elapsed\", time.Since(start),\n\t\t)\n\t}()\n\n\t// TODO(s.chzhen):  Move the timeout logic from WHOIS configuration to the\n\t// context.\n\tinfo, changed := p.whois.Process(ctx, ip)\n\tif !changed {\n\t\tinfo = nil\n\t}\n\n\treturn info\n}\n\n// Close implements the [AddressProcessor] interface for *DefaultAddrProc.\nfunc (p *DefaultAddrProc) Close() (err error) {\n\tp.clientIPsMu.Lock()\n\tdefer p.clientIPsMu.Unlock()\n\n\tif p.isClosed {\n\t\treturn ErrClosed\n\t}\n\n\tclose(p.clientIPs)\n\tp.isClosed = true\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/client/addrproc_test.go",
    "content": "package client_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/fakenet\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestEmptyAddrProc(t *testing.T) {\n\tt.Parallel()\n\n\tp := client.EmptyAddrProc{}\n\n\tassert.NotPanics(t, func() {\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\tp.Process(ctx, testIP)\n\t})\n\n\tassert.NotPanics(t, func() {\n\t\terr := p.Close()\n\t\tassert.NoError(t, err)\n\t})\n}\n\nfunc TestDefaultAddrProc_Process_rDNS(t *testing.T) {\n\tt.Parallel()\n\n\tprivateIP := netip.MustParseAddr(\"192.168.0.1\")\n\n\ttestCases := []struct {\n\t\trdnsErr    error\n\t\tip         netip.Addr\n\t\tname       string\n\t\thost       string\n\t\tusePrivate bool\n\t\twantUpd    bool\n\t}{{\n\t\trdnsErr:    nil,\n\t\tip:         testIP,\n\t\tname:       \"success\",\n\t\thost:       testHost,\n\t\tusePrivate: false,\n\t\twantUpd:    true,\n\t}, {\n\t\trdnsErr:    nil,\n\t\tip:         testIP,\n\t\tname:       \"no_host\",\n\t\thost:       \"\",\n\t\tusePrivate: false,\n\t\twantUpd:    false,\n\t}, {\n\t\trdnsErr:    nil,\n\t\tip:         netip.MustParseAddr(\"127.0.0.1\"),\n\t\tname:       \"localhost\",\n\t\thost:       \"\",\n\t\tusePrivate: false,\n\t\twantUpd:    false,\n\t}, {\n\t\trdnsErr:    nil,\n\t\tip:         privateIP,\n\t\tname:       \"private_ignored\",\n\t\thost:       \"\",\n\t\tusePrivate: false,\n\t\twantUpd:    false,\n\t}, {\n\t\trdnsErr:    nil,\n\t\tip:         privateIP,\n\t\tname:       \"private_processed\",\n\t\thost:       \"private.example\",\n\t\tusePrivate: true,\n\t\twantUpd:    true,\n\t}, {\n\t\trdnsErr:    errors.Error(\"rdns error\"),\n\t\tip:         testIP,\n\t\tname:       \"rdns_error\",\n\t\thost:       \"\",\n\t\tusePrivate: false,\n\t\twantUpd:    false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tupdIPCh := make(chan netip.Addr, 1)\n\t\t\tupdHostCh := make(chan string, 1)\n\t\t\tupdInfoCh := make(chan *whois.Info, 1)\n\t\t\tonExchange := func(\n\t\t\t\t_ context.Context,\n\t\t\t\tip netip.Addr,\n\t\t\t) (host string, ttl time.Duration, err error) {\n\t\t\t\treturn tc.host, 0, tc.rdnsErr\n\t\t\t}\n\n\t\t\tp := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{\n\t\t\t\tBaseLogger: slogutil.NewDiscardLogger(),\n\t\t\t\tDialContext: func(\n\t\t\t\t\tctx context.Context,\n\t\t\t\t\tnetwork,\n\t\t\t\t\taddr string,\n\t\t\t\t) (conn net.Conn, err error) {\n\t\t\t\t\tpanic(testutil.UnexpectedCall(ctx, network, addr))\n\t\t\t\t},\n\t\t\t\tExchanger: &aghtest.Exchanger{\n\t\t\t\t\tOnExchange: onExchange,\n\t\t\t\t},\n\t\t\t\tPrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\t\t\tAddressUpdater: &aghtest.AddressUpdater{\n\t\t\t\t\tOnUpdateAddress: newOnUpdateAddress(tc.wantUpd, updIPCh, updHostCh, updInfoCh),\n\t\t\t\t},\n\t\t\t\tCatchPanics:    false,\n\t\t\t\tUseRDNS:        true,\n\t\t\t\tUsePrivateRDNS: tc.usePrivate,\n\t\t\t\tUseWHOIS:       false,\n\t\t\t})\n\t\t\ttestutil.CleanupAndRequireSuccess(t, p.Close)\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tp.Process(ctx, tc.ip)\n\n\t\t\tif !tc.wantUpd {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgotIP, _ := testutil.RequireReceive(t, updIPCh, testTimeout)\n\t\t\tassert.Equal(t, tc.ip, gotIP)\n\n\t\t\tgotHost, _ := testutil.RequireReceive(t, updHostCh, testTimeout)\n\t\t\tassert.Equal(t, tc.host, gotHost)\n\n\t\t\tgotInfo, _ := testutil.RequireReceive(t, updInfoCh, testTimeout)\n\t\t\tassert.Nil(t, gotInfo)\n\t\t})\n\t}\n}\n\n// newOnUpdateAddress is a test helper that returns a new OnUpdateAddress\n// callback using the provided channels if an update is expected and panicking\n// otherwise.\nfunc newOnUpdateAddress(\n\twant bool,\n\tips chan<- netip.Addr,\n\thosts chan<- string,\n\tinfos chan<- *whois.Info,\n) (f func(ctx context.Context, ip netip.Addr, host string, info *whois.Info)) {\n\treturn func(ctx context.Context, ip netip.Addr, host string, info *whois.Info) {\n\t\tif !want && (host != \"\" || info != nil) {\n\t\t\tpanic(fmt.Errorf(\"got unexpected update for %v with %q and %v\", ip, host, info))\n\t\t}\n\n\t\tips <- ip\n\t\thosts <- host\n\t\tinfos <- info\n\t}\n}\n\nfunc TestDefaultAddrProc_Process_WHOIS(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\twantInfo *whois.Info\n\t\texchErr  error\n\t\tname     string\n\t\twantUpd  bool\n\t}{{\n\t\twantInfo: &whois.Info{\n\t\t\tCity: testWHOISCity,\n\t\t},\n\t\texchErr: nil,\n\t\tname:    \"success\",\n\t\twantUpd: true,\n\t}, {\n\t\twantInfo: nil,\n\t\texchErr:  nil,\n\t\tname:     \"no_info\",\n\t\twantUpd:  false,\n\t}, {\n\t\twantInfo: nil,\n\t\texchErr:  errors.Error(\"whois error\"),\n\t\tname:     \"whois_error\",\n\t\twantUpd:  false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\twhoisConn := &fakenet.Conn{\n\t\t\t\tOnClose: func() (err error) { return nil },\n\t\t\t\tOnRead: func(b []byte) (n int, err error) {\n\t\t\t\t\tif tc.wantInfo == nil {\n\t\t\t\t\t\treturn 0, tc.exchErr\n\t\t\t\t\t}\n\n\t\t\t\t\tdata := \"city: \" + tc.wantInfo.City + \"\\n\"\n\t\t\t\t\tcopy(b, data)\n\n\t\t\t\t\treturn len(data), io.EOF\n\t\t\t\t},\n\t\t\t\tOnSetDeadline: func(_ time.Time) (err error) { return nil },\n\t\t\t\tOnWrite:       func(b []byte) (n int, err error) { return len(b), nil },\n\t\t\t}\n\n\t\t\tupdIPCh := make(chan netip.Addr, 1)\n\t\t\tupdHostCh := make(chan string, 1)\n\t\t\tupdInfoCh := make(chan *whois.Info, 1)\n\n\t\t\tonExchange := func(\n\t\t\t\tctx context.Context,\n\t\t\t\taddr netip.Addr,\n\t\t\t) (_ string, _ time.Duration, _ error) {\n\t\t\t\tpanic(testutil.UnexpectedCall(ctx, addr))\n\t\t\t}\n\n\t\t\tp := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{\n\t\t\t\tBaseLogger: slogutil.NewDiscardLogger(),\n\t\t\t\tDialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {\n\t\t\t\t\treturn whoisConn, nil\n\t\t\t\t},\n\t\t\t\tExchanger: &aghtest.Exchanger{\n\t\t\t\t\tOnExchange: onExchange,\n\t\t\t\t},\n\t\t\t\tPrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\t\t\tAddressUpdater: &aghtest.AddressUpdater{\n\t\t\t\t\tOnUpdateAddress: newOnUpdateAddress(tc.wantUpd, updIPCh, updHostCh, updInfoCh),\n\t\t\t\t},\n\t\t\t\tCatchPanics:    false,\n\t\t\t\tUseRDNS:        false,\n\t\t\t\tUsePrivateRDNS: false,\n\t\t\t\tUseWHOIS:       true,\n\t\t\t})\n\t\t\ttestutil.CleanupAndRequireSuccess(t, p.Close)\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tp.Process(ctx, testIP)\n\n\t\t\tif !tc.wantUpd {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgotIP, _ := testutil.RequireReceive(t, updIPCh, testTimeout)\n\t\t\tassert.Equal(t, testIP, gotIP)\n\n\t\t\tgotHost, _ := testutil.RequireReceive(t, updHostCh, testTimeout)\n\t\t\tassert.Empty(t, gotHost)\n\n\t\t\tgotInfo, _ := testutil.RequireReceive(t, updInfoCh, testTimeout)\n\t\t\tassert.Equal(t, tc.wantInfo, gotInfo)\n\t\t})\n\t}\n}\n\nfunc TestDefaultAddrProc_Close(t *testing.T) {\n\tt.Parallel()\n\n\tp := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{\n\t\tBaseLogger: slogutil.NewDiscardLogger(),\n\t})\n\n\terr := p.Close()\n\tassert.NoError(t, err)\n\n\terr = p.Close()\n\tassert.ErrorIs(t, err, client.ErrClosed)\n}\n"
  },
  {
    "path": "internal/client/client.go",
    "content": "// Package client contains types and logic dealing with AdGuard Home's DNS\n// clients.\n//\n// TODO(a.garipov): Expand.\npackage client\n\nimport (\n\t\"encoding\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n)\n\n// ClientID is a unique identifier for a persistent client used in\n// DNS-over-HTTPS, DNS-over-TLS, and DNS-over-QUIC queries.\n//\n// TODO(s.chzhen):  Use everywhere.\ntype ClientID string\n\n// ValidateClientID returns an error if id is not a valid ClientID.\n//\n// TODO(s.chzhen):  Consider implementing [validate.Interface] for ClientID.\nfunc ValidateClientID(id string) (err error) {\n\terr = netutil.ValidateHostnameLabel(id)\n\tif err != nil {\n\t\t// Replace the domain name label wrapper with our own.\n\t\treturn fmt.Errorf(\"invalid clientid %q: %w\", id, errors.Unwrap(err))\n\t}\n\n\treturn nil\n}\n\n// isValidClientID returns false if id is not a valid ClientID.\nfunc isValidClientID(id string) (ok bool) {\n\treturn netutil.IsValidHostnameLabel(id)\n}\n\n// Source represents the source from which the information about the client has\n// been obtained.\ntype Source uint8\n\n// Clients information sources.  The order determines the priority.\nconst (\n\tSourceWHOIS Source = iota + 1\n\tSourceARP\n\tSourceRDNS\n\tSourceDHCP\n\tSourceHostsFile\n\tSourcePersistent\n)\n\n// type check\nvar _ fmt.Stringer = Source(0)\n\n// String returns a human-readable name of cs.\nfunc (cs Source) String() (s string) {\n\tswitch cs {\n\tcase SourceWHOIS:\n\t\treturn \"WHOIS\"\n\tcase SourceARP:\n\t\treturn \"ARP\"\n\tcase SourceRDNS:\n\t\treturn \"rDNS\"\n\tcase SourceDHCP:\n\t\treturn \"DHCP\"\n\tcase SourceHostsFile:\n\t\treturn \"etc/hosts\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// type check\nvar _ encoding.TextMarshaler = Source(0)\n\n// MarshalText implements encoding.TextMarshaler for the Source.\nfunc (cs Source) MarshalText() (text []byte, err error) {\n\treturn []byte(cs.String()), nil\n}\n\n// Runtime is a client information from different sources.\ntype Runtime struct {\n\t// ip is an IP address of a client.\n\tip netip.Addr\n\n\t// whois is the filtered WHOIS information of a client.\n\twhois *whois.Info\n\n\t// arp is the ARP information of a client.  nil indicates that there is no\n\t// information from the source.  Empty non-nil slice indicates that the data\n\t// from the source is present, but empty.\n\tarp []string\n\n\t// rdns is the RDNS information of a client.  nil indicates that there is no\n\t// information from the source.  Empty non-nil slice indicates that the data\n\t// from the source is present, but empty.\n\trdns []string\n\n\t// dhcp is the DHCP information of a client.  nil indicates that there is no\n\t// information from the source.  Empty non-nil slice indicates that the data\n\t// from the source is present, but empty.\n\tdhcp []string\n\n\t// hostsFile is the information from the hosts file.  nil indicates that\n\t// there is no information from the source.  Empty non-nil slice indicates\n\t// that the data from the source is present, but empty.\n\thostsFile []string\n}\n\n// NewRuntime constructs a new runtime client.  ip must be valid IP address.\n//\n// TODO(s.chzhen):  Validate IP address.\nfunc NewRuntime(ip netip.Addr) (r *Runtime) {\n\treturn &Runtime{\n\t\tip: ip,\n\t}\n}\n\n// Info returns a client information from the highest-priority source.\nfunc (r *Runtime) Info() (cs Source, host string) {\n\tinfo := []string{}\n\n\tswitch {\n\tcase r.hostsFile != nil:\n\t\tcs, info = SourceHostsFile, r.hostsFile\n\tcase r.dhcp != nil:\n\t\tcs, info = SourceDHCP, r.dhcp\n\tcase r.rdns != nil:\n\t\tcs, info = SourceRDNS, r.rdns\n\tcase r.arp != nil:\n\t\tcs, info = SourceARP, r.arp\n\tcase r.whois != nil:\n\t\tcs = SourceWHOIS\n\t}\n\n\tif len(info) == 0 {\n\t\treturn cs, \"\"\n\t}\n\n\t// TODO(s.chzhen):  Return the full information.\n\treturn cs, info[0]\n}\n\n// setInfo sets a host as a client information from the cs.\nfunc (r *Runtime) setInfo(cs Source, hosts []string) {\n\t// TODO(s.chzhen):  Use contract where hosts must contain non-empty host.\n\tif len(hosts) == 1 && hosts[0] == \"\" {\n\t\thosts = []string{}\n\t}\n\n\tswitch cs {\n\tcase SourceARP:\n\t\tr.arp = hosts\n\tcase SourceRDNS:\n\t\tr.rdns = hosts\n\tcase SourceDHCP:\n\t\tr.dhcp = hosts\n\tcase SourceHostsFile:\n\t\tr.hostsFile = hosts\n\t}\n}\n\n// WHOIS returns a copy of WHOIS client information.\nfunc (r *Runtime) WHOIS() (info *whois.Info) {\n\treturn r.whois.Clone()\n}\n\n// setWHOIS sets a WHOIS client information.  info must be non-nil.\nfunc (r *Runtime) setWHOIS(info *whois.Info) {\n\tr.whois = info\n}\n\n// unset clears a cs information.\nfunc (r *Runtime) unset(cs Source) {\n\tswitch cs {\n\tcase SourceWHOIS:\n\t\tr.whois = nil\n\tcase SourceARP:\n\t\tr.arp = nil\n\tcase SourceRDNS:\n\t\tr.rdns = nil\n\tcase SourceDHCP:\n\t\tr.dhcp = nil\n\tcase SourceHostsFile:\n\t\tr.hostsFile = nil\n\t}\n}\n\n// isEmpty returns true if there is no information from any source.\nfunc (r *Runtime) isEmpty() (ok bool) {\n\treturn r.whois == nil &&\n\t\tr.arp == nil &&\n\t\tr.rdns == nil &&\n\t\tr.dhcp == nil &&\n\t\tr.hostsFile == nil\n}\n\n// Addr returns an IP address of the client.\nfunc (r *Runtime) Addr() (ip netip.Addr) {\n\treturn r.ip\n}\n\n// clone returns a deep copy of the runtime client.  If r is nil, c is nil.\nfunc (r *Runtime) clone() (c *Runtime) {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\treturn &Runtime{\n\t\tip:        r.ip,\n\t\twhois:     r.whois.Clone(),\n\t\tarp:       slices.Clone(r.arp),\n\t\trdns:      slices.Clone(r.rdns),\n\t\tdhcp:      slices.Clone(r.dhcp),\n\t\thostsFile: slices.Clone(r.hostsFile),\n\t}\n}\n"
  },
  {
    "path": "internal/client/client_test.go",
    "content": "package client_test\n\nimport (\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n)\n\nfunc TestMain(m *testing.M) {\n\ttestutil.DiscardLogOutput(m)\n}\n\n// testHost is the common hostname for tests.\nconst testHost = \"client.example\"\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testWHOISCity is the common city for tests.\nconst testWHOISCity = \"Brussels\"\n\n// testIP is the common IP address for tests.\nvar testIP = netip.MustParseAddr(\"1.2.3.4\")\n"
  },
  {
    "path": "internal/client/index.go",
    "content": "package client\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n)\n\n// macKey contains MAC as byte array of 6, 8, or 20 bytes.\ntype macKey any\n\n// macToKey converts mac into key of type macKey, which is used as the key of\n// the [clientIndex.macToUID].  mac must be valid MAC address.\nfunc macToKey(mac net.HardwareAddr) (key macKey) {\n\tswitch len(mac) {\n\tcase 6:\n\t\treturn [6]byte(mac)\n\tcase 8:\n\t\treturn [8]byte(mac)\n\tcase 20:\n\t\treturn [20]byte(mac)\n\tdefault:\n\t\tpanic(fmt.Errorf(\"invalid mac address %#v\", mac))\n\t}\n}\n\n// index stores all information about persistent clients.\ntype index struct {\n\t// subnetToUID maps subnet to UID.\n\tsubnetToUID *aghalg.SortedMap[netip.Prefix, UID]\n\n\t// nameToUID maps client name to UID.\n\tnameToUID map[string]UID\n\n\t// clientIDToUID maps ClientID to UID.\n\tclientIDToUID map[ClientID]UID\n\n\t// ipToUID maps IP address to UID.\n\tipToUID map[netip.Addr]UID\n\n\t// macToUID maps MAC address to UID.\n\tmacToUID map[macKey]UID\n\n\t// uidToClient maps UID to the persistent client.\n\tuidToClient map[UID]*Persistent\n}\n\n// newIndex initializes the new instance of client index.\nfunc newIndex() (ci *index) {\n\treturn &index{\n\t\tsubnetToUID:   aghalg.NewSortedMapFunc[netip.Prefix, UID](subnetCompare),\n\t\tnameToUID:     map[string]UID{},\n\t\tclientIDToUID: map[ClientID]UID{},\n\t\tipToUID:       map[netip.Addr]UID{},\n\t\tmacToUID:      map[macKey]UID{},\n\t\tuidToClient:   map[UID]*Persistent{},\n\t}\n}\n\n// add stores information about a persistent client in the index.  c must be\n// non-nil, have a UID, and contain at least one identifier.\nfunc (ci *index) add(c *Persistent) {\n\tif (c.UID == UID{}) {\n\t\tpanic(\"client must contain uid\")\n\t}\n\n\tci.nameToUID[c.Name] = c.UID\n\n\tfor _, id := range c.ClientIDs {\n\t\tci.clientIDToUID[id] = c.UID\n\t}\n\n\tfor _, ip := range c.IPs {\n\t\tci.ipToUID[ip] = c.UID\n\t}\n\n\tfor _, pref := range c.Subnets {\n\t\tci.subnetToUID.Set(pref, c.UID)\n\t}\n\n\tfor _, mac := range c.MACs {\n\t\tk := macToKey(mac)\n\t\tci.macToUID[k] = c.UID\n\t}\n\n\tci.uidToClient[c.UID] = c\n}\n\n// clashesUID returns existing persistent client with the same UID as c.  Note\n// that this is only possible when configuration contains duplicate fields.\nfunc (ci *index) clashesUID(c *Persistent) (err error) {\n\tp, ok := ci.uidToClient[c.UID]\n\tif ok {\n\t\treturn fmt.Errorf(\"another client %q uses the same uid\", p.Name)\n\t}\n\n\treturn nil\n}\n\n// clashes returns an error if the index contains a different persistent client\n// with at least a single identifier contained by c.  c must be non-nil.\nfunc (ci *index) clashes(c *Persistent) (err error) {\n\tif p := ci.clashesName(c); p != nil {\n\t\treturn fmt.Errorf(\"another client uses the same name %q\", p.Name)\n\t}\n\n\tfor _, id := range c.ClientIDs {\n\t\texisting, ok := ci.clientIDToUID[id]\n\t\tif ok && existing != c.UID {\n\t\t\tp := ci.uidToClient[existing]\n\n\t\t\treturn fmt.Errorf(\"another client %q uses the same ClientID %q\", p.Name, id)\n\t\t}\n\t}\n\n\tp, ip := ci.clashesIP(c)\n\tif p != nil {\n\t\treturn fmt.Errorf(\"another client %q uses the same IP %q\", p.Name, ip)\n\t}\n\n\tp, s := ci.clashesSubnet(c)\n\tif p != nil {\n\t\treturn fmt.Errorf(\"another client %q uses the same subnet %q\", p.Name, s)\n\t}\n\n\tp, mac := ci.clashesMAC(c)\n\tif p != nil {\n\t\treturn fmt.Errorf(\"another client %q uses the same MAC %q\", p.Name, mac)\n\t}\n\n\treturn nil\n}\n\n// clashesName returns existing persistent client with the same name as c or\n// nil.  c must be non-nil.\nfunc (ci *index) clashesName(c *Persistent) (existing *Persistent) {\n\texisting, ok := ci.findByName(c.Name)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tif existing.UID != c.UID {\n\t\treturn existing\n\t}\n\n\treturn nil\n}\n\n// clashesIP returns a previous client with the same IP address as c.  c must be\n// non-nil.\nfunc (ci *index) clashesIP(c *Persistent) (p *Persistent, ip netip.Addr) {\n\tfor _, ip := range c.IPs {\n\t\texisting, ok := ci.ipToUID[ip]\n\t\tif ok && existing != c.UID {\n\t\t\treturn ci.uidToClient[existing], ip\n\t\t}\n\t}\n\n\treturn nil, netip.Addr{}\n}\n\n// clashesSubnet returns a previous client with the same subnet as c.  c must be\n// non-nil.\nfunc (ci *index) clashesSubnet(c *Persistent) (p *Persistent, s netip.Prefix) {\n\tfor _, s = range c.Subnets {\n\t\tvar existing UID\n\t\tvar ok bool\n\n\t\tci.subnetToUID.Range(func(p netip.Prefix, uid UID) (cont bool) {\n\t\t\tif s == p {\n\t\t\t\texisting = uid\n\t\t\t\tok = true\n\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\treturn true\n\t\t})\n\n\t\tif ok && existing != c.UID {\n\t\t\treturn ci.uidToClient[existing], s\n\t\t}\n\t}\n\n\treturn nil, netip.Prefix{}\n}\n\n// clashesMAC returns a previous client with the same MAC address as c.  c must\n// be non-nil.\nfunc (ci *index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr) {\n\tfor _, mac = range c.MACs {\n\t\tk := macToKey(mac)\n\t\texisting, ok := ci.macToUID[k]\n\t\tif ok && existing != c.UID {\n\t\t\treturn ci.uidToClient[existing], mac\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\n// find finds persistent client by string representation of the ClientID, IP\n// address, or MAC.\nfunc (ci *index) find(id string) (c *Persistent, ok bool) {\n\tc, ok = ci.findByClientID(ClientID(id))\n\tif ok {\n\t\treturn c, true\n\t}\n\n\tip, err := netip.ParseAddr(id)\n\tif err == nil {\n\t\t// MAC addresses can be successfully parsed as IP addresses.\n\t\tc, ok = ci.findByIP(ip)\n\t\tif ok {\n\t\t\treturn c, true\n\t\t}\n\t}\n\n\tmac, err := net.ParseMAC(id)\n\tif err == nil {\n\t\treturn ci.findByMAC(mac)\n\t}\n\n\treturn nil, false\n}\n\n// findByClientID finds persistent client by ClientID.\nfunc (ci *index) findByClientID(clientID ClientID) (c *Persistent, ok bool) {\n\tuid, ok := ci.clientIDToUID[clientID]\n\tif ok {\n\t\treturn ci.uidToClient[uid], true\n\t}\n\n\treturn nil, false\n}\n\n// findByName finds persistent client by name.\nfunc (ci *index) findByName(name string) (c *Persistent, found bool) {\n\tuid, found := ci.nameToUID[name]\n\tif found {\n\t\treturn ci.uidToClient[uid], true\n\t}\n\n\treturn nil, false\n}\n\n// findByIP finds persistent client by IP address.\nfunc (ci *index) findByIP(ip netip.Addr) (c *Persistent, found bool) {\n\tuid, found := ci.ipToUID[ip]\n\tif found {\n\t\treturn ci.uidToClient[uid], true\n\t}\n\n\tipWithoutZone := ip.WithZone(\"\")\n\tci.subnetToUID.Range(func(pref netip.Prefix, id UID) (cont bool) {\n\t\t// Remove zone before checking because prefixes strip zones.\n\t\tif pref.Contains(ipWithoutZone) {\n\t\t\tuid, found = id, true\n\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t})\n\n\tif found {\n\t\treturn ci.uidToClient[uid], true\n\t}\n\n\treturn nil, false\n}\n\n// findByCIDR searches for a persistent client with the provided subnet as an\n// identifier.  Note that this function looks for an exact match of subnets,\n// rather than checking if one subnet contains another.\nfunc (ci *index) findByCIDR(subnet netip.Prefix) (c *Persistent, ok bool) {\n\tvar uid UID\n\tfor pref, id := range ci.subnetToUID.Range {\n\t\tif subnet == pref {\n\t\t\tuid, ok = id, true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif ok {\n\t\treturn ci.uidToClient[uid], true\n\t}\n\n\treturn nil, false\n}\n\n// findByMAC finds persistent client by MAC.\nfunc (ci *index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {\n\tk := macToKey(mac)\n\tuid, found := ci.macToUID[k]\n\tif found {\n\t\treturn ci.uidToClient[uid], true\n\t}\n\n\treturn nil, false\n}\n\n// findByIPWithoutZone finds a persistent client by IP address without zone.  It\n// strips the IPv6 zone index from the stored IP addresses before comparing,\n// because querylog entries don't have it.  See TODO on [querylog.logEntry.IP].\n//\n// Note that multiple clients can have the same IP address with different zones.\n// Therefore, the result of this method is indeterminate.\nfunc (ci *index) findByIPWithoutZone(ip netip.Addr) (c *Persistent) {\n\tif (ip == netip.Addr{}) {\n\t\treturn nil\n\t}\n\n\tfor addr, uid := range ci.ipToUID {\n\t\tif addr.WithZone(\"\") == ip {\n\t\t\treturn ci.uidToClient[uid]\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// remove removes information about persistent client from the index.  c must be\n// non-nil.\nfunc (ci *index) remove(c *Persistent) {\n\tdelete(ci.nameToUID, c.Name)\n\n\tfor _, id := range c.ClientIDs {\n\t\tdelete(ci.clientIDToUID, id)\n\t}\n\n\tfor _, ip := range c.IPs {\n\t\tdelete(ci.ipToUID, ip)\n\t}\n\n\tfor _, pref := range c.Subnets {\n\t\tci.subnetToUID.Del(pref)\n\t}\n\n\tfor _, mac := range c.MACs {\n\t\tk := macToKey(mac)\n\t\tdelete(ci.macToUID, k)\n\t}\n\n\tdelete(ci.uidToClient, c.UID)\n}\n\n// size returns the number of persistent clients.\nfunc (ci *index) size() (n int) {\n\treturn len(ci.uidToClient)\n}\n\n// rangeByName is like [Index.Range] but sorts the persistent clients by name\n// before iterating ensuring a predictable order.\nfunc (ci *index) rangeByName(f func(c *Persistent) (cont bool)) {\n\tclients := slices.SortedStableFunc(\n\t\tmaps.Values(ci.uidToClient),\n\t\tfunc(a, b *Persistent) (res int) {\n\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t},\n\t)\n\n\tfor _, c := range clients {\n\t\tif !f(c) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/client/index_internal_test.go",
    "content": "package client\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// newIDIndex is a helper function that returns a client index filled with\n// persistent clients from the m.  It also generates a UID for each client.\nfunc newIDIndex(m []*Persistent) (ci *index) {\n\tci = newIndex()\n\n\tfor _, c := range m {\n\t\tc.UID = MustNewUID()\n\t\tci.add(c)\n\t}\n\n\treturn ci\n}\n\n// TODO(s.chzhen):  Remove.\nfunc TestClientIndex_Find(t *testing.T) {\n\tconst (\n\t\tcliIPNone = \"1.2.3.4\"\n\t\tcliIP1    = \"1.1.1.1\"\n\t\tcliIP2    = \"2.2.2.2\"\n\n\t\tcliIPv6 = \"1:2:3::4\"\n\n\t\tcliSubnet   = \"2.2.2.0/24\"\n\t\tcliSubnetIP = \"2.2.2.222\"\n\n\t\tcliID  = \"client-id\"\n\t\tcliMAC = \"11:11:11:11:11:11\"\n\n\t\tlinkLocalIP     = \"fe80::abcd:abcd:abcd:ab%eth0\"\n\t\tlinkLocalSubnet = \"fe80::/16\"\n\t)\n\n\tvar (\n\t\tclientWithBothFams = &Persistent{\n\t\t\tName: \"client1\",\n\t\t\tIPs: []netip.Addr{\n\t\t\t\tnetip.MustParseAddr(cliIP1),\n\t\t\t\tnetip.MustParseAddr(cliIPv6),\n\t\t\t},\n\t\t}\n\n\t\tclientWithSubnet = &Persistent{\n\t\t\tName:    \"client2\",\n\t\t\tIPs:     []netip.Addr{netip.MustParseAddr(cliIP2)},\n\t\t\tSubnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},\n\t\t}\n\n\t\tclientWithMAC = &Persistent{\n\t\t\tName: \"client_with_mac\",\n\t\t\tMACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},\n\t\t}\n\n\t\tclientWithID = &Persistent{\n\t\t\tName:      \"client_with_id\",\n\t\t\tClientIDs: []ClientID{cliID},\n\t\t}\n\n\t\tclientLinkLocal = &Persistent{\n\t\t\tName:    \"client_link_local\",\n\t\t\tSubnets: []netip.Prefix{netip.MustParsePrefix(linkLocalSubnet)},\n\t\t}\n\t)\n\n\tclients := []*Persistent{\n\t\tclientWithBothFams,\n\t\tclientWithSubnet,\n\t\tclientWithMAC,\n\t\tclientWithID,\n\t\tclientLinkLocal,\n\t}\n\tci := newIDIndex(clients)\n\n\ttestCases := []struct {\n\t\twant *Persistent\n\t\tname string\n\t\tids  []string\n\t}{{\n\t\tname: \"ipv4_ipv6\",\n\t\tids:  []string{cliIP1, cliIPv6},\n\t\twant: clientWithBothFams,\n\t}, {\n\t\tname: \"ipv4_subnet\",\n\t\tids:  []string{cliIP2, cliSubnetIP},\n\t\twant: clientWithSubnet,\n\t}, {\n\t\tname: \"mac\",\n\t\tids:  []string{cliMAC},\n\t\twant: clientWithMAC,\n\t}, {\n\t\tname: \"client_id\",\n\t\tids:  []string{cliID},\n\t\twant: clientWithID,\n\t}, {\n\t\tname: \"client_link_local_subnet\",\n\t\tids:  []string{linkLocalIP},\n\t\twant: clientLinkLocal,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tfor _, id := range tc.ids {\n\t\t\t\tc, ok := ci.find(id)\n\t\t\t\trequire.True(t, ok)\n\n\t\t\t\tassert.Equal(t, tc.want, c)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"not_found\", func(t *testing.T) {\n\t\t_, ok := ci.find(cliIPNone)\n\t\tassert.False(t, ok)\n\t})\n}\n\nfunc TestClientIndex_Clashes(t *testing.T) {\n\tconst (\n\t\tcliIP1      = \"1.1.1.1\"\n\t\tcliSubnet   = \"2.2.2.0/24\"\n\t\tcliSubnetIP = \"2.2.2.222\"\n\t\tcliID       = \"client-id\"\n\t\tcliMAC      = \"11:11:11:11:11:11\"\n\t)\n\n\tclients := []*Persistent{{\n\t\tName: \"client_with_ip\",\n\t\tIPs:  []netip.Addr{netip.MustParseAddr(cliIP1)},\n\t}, {\n\t\tName:    \"client_with_subnet\",\n\t\tSubnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},\n\t}, {\n\t\tName: \"client_with_mac\",\n\t\tMACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},\n\t}, {\n\t\tName:      \"client_with_id\",\n\t\tClientIDs: []ClientID{cliID},\n\t}}\n\n\tci := newIDIndex(clients)\n\n\ttestCases := []struct {\n\t\tclient *Persistent\n\t\tname   string\n\t}{{\n\t\tname:   \"ipv4\",\n\t\tclient: clients[0],\n\t}, {\n\t\tname:   \"subnet\",\n\t\tclient: clients[1],\n\t}, {\n\t\tname:   \"mac\",\n\t\tclient: clients[2],\n\t}, {\n\t\tname:   \"client_id\",\n\t\tclient: clients[3],\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tclone := tc.client.ShallowClone()\n\t\t\tclone.UID = MustNewUID()\n\n\t\t\terr := ci.clashes(clone)\n\t\t\trequire.Error(t, err)\n\n\t\t\tci.remove(tc.client)\n\t\t\terr = ci.clashes(clone)\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestMACToKey(t *testing.T) {\n\ttestCases := []struct {\n\t\twant any\n\t\tname string\n\t\tin   string\n\t}{{\n\t\tname: \"column6\",\n\t\tin:   \"00:00:5e:00:53:01\",\n\t\twant: [6]byte(errors.Must(net.ParseMAC(\"00:00:5e:00:53:01\"))),\n\t}, {\n\t\tname: \"column8\",\n\t\tin:   \"02:00:5e:10:00:00:00:01\",\n\t\twant: [8]byte(errors.Must(net.ParseMAC(\"02:00:5e:10:00:00:00:01\"))),\n\t}, {\n\t\tname: \"column20\",\n\t\tin:   \"00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01\",\n\t\twant: [20]byte(errors.Must(net.ParseMAC(\"00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01\"))),\n\t}, {\n\t\tname: \"hyphen6\",\n\t\tin:   \"00-00-5e-00-53-01\",\n\t\twant: [6]byte(errors.Must(net.ParseMAC(\"00-00-5e-00-53-01\"))),\n\t}, {\n\t\tname: \"hyphen8\",\n\t\tin:   \"02-00-5e-10-00-00-00-01\",\n\t\twant: [8]byte(errors.Must(net.ParseMAC(\"02-00-5e-10-00-00-00-01\"))),\n\t}, {\n\t\tname: \"hyphen20\",\n\t\tin:   \"00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01\",\n\t\twant: [20]byte(errors.Must(net.ParseMAC(\"00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01\"))),\n\t}, {\n\t\tname: \"dot6\",\n\t\tin:   \"0000.5e00.5301\",\n\t\twant: [6]byte(errors.Must(net.ParseMAC(\"0000.5e00.5301\"))),\n\t}, {\n\t\tname: \"dot8\",\n\t\tin:   \"0200.5e10.0000.0001\",\n\t\twant: [8]byte(errors.Must(net.ParseMAC(\"0200.5e10.0000.0001\"))),\n\t}, {\n\t\tname: \"dot20\",\n\t\tin:   \"0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001\",\n\t\twant: [20]byte(errors.Must(net.ParseMAC(\"0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001\"))),\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmac := errors.Must(net.ParseMAC(tc.in))\n\n\t\t\tkey := macToKey(mac)\n\t\t\tassert.Equal(t, tc.want, key)\n\t\t})\n\t}\n\n\tassert.Panics(t, func() {\n\t\tmac := net.HardwareAddr([]byte{1, 2, 3})\n\t\t_ = macToKey(mac)\n\t})\n}\n\nfunc TestIndex_FindByIPWithoutZone(t *testing.T) {\n\tvar (\n\t\tip         = netip.MustParseAddr(\"fe80::a098:7654:32ef:ff1\")\n\t\tipWithZone = netip.MustParseAddr(\"fe80::1ff:fe23:4567:890a%eth2\")\n\t)\n\n\tvar (\n\t\tclientNoZone = &Persistent{\n\t\t\tName: \"client\",\n\t\t\tIPs:  []netip.Addr{ip},\n\t\t}\n\n\t\tclientWithZone = &Persistent{\n\t\t\tName: \"client_with_zone\",\n\t\t\tIPs:  []netip.Addr{ipWithZone},\n\t\t}\n\t)\n\n\tci := newIDIndex([]*Persistent{\n\t\tclientNoZone,\n\t\tclientWithZone,\n\t})\n\n\ttestCases := []struct {\n\t\tip   netip.Addr\n\t\twant *Persistent\n\t\tname string\n\t}{{\n\t\tname: \"without_zone\",\n\t\tip:   ip,\n\t\twant: clientNoZone,\n\t}, {\n\t\tname: \"with_zone\",\n\t\tip:   ipWithZone,\n\t\twant: clientWithZone,\n\t}, {\n\t\tname: \"zero_address\",\n\t\tip:   netip.Addr{},\n\t\twant: nil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := ci.findByIPWithoutZone(tc.ip.WithZone(\"\"))\n\t\t\trequire.Equal(t, tc.want, c)\n\t\t})\n\t}\n}\n\nfunc TestClientIndex_RangeByName(t *testing.T) {\n\tsortedClients := []*Persistent{{\n\t\tName:      \"clientA\",\n\t\tClientIDs: []ClientID{\"A\"},\n\t}, {\n\t\tName:      \"clientB\",\n\t\tClientIDs: []ClientID{\"B\"},\n\t}, {\n\t\tName:      \"clientC\",\n\t\tClientIDs: []ClientID{\"C\"},\n\t}, {\n\t\tName:      \"clientD\",\n\t\tClientIDs: []ClientID{\"D\"},\n\t}, {\n\t\tName:      \"clientE\",\n\t\tClientIDs: []ClientID{\"E\"},\n\t}}\n\n\ttestCases := []struct {\n\t\tname string\n\t\twant []*Persistent\n\t}{{\n\t\tname: \"basic\",\n\t\twant: sortedClients,\n\t}, {\n\t\tname: \"nil\",\n\t\twant: nil,\n\t}, {\n\t\tname: \"one_element\",\n\t\twant: sortedClients[:1],\n\t}, {\n\t\tname: \"two_elements\",\n\t\twant: sortedClients[:2],\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tci := newIDIndex(tc.want)\n\n\t\t\tvar got []*Persistent\n\t\t\tci.rangeByName(func(c *Persistent) (cont bool) {\n\t\t\t\tgot = append(got, c)\n\n\t\t\t\treturn true\n\t\t\t})\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc TestIndex_FindByName(t *testing.T) {\n\tconst (\n\t\tclientExistingName        = \"client_existing\"\n\t\tclientAnotherExistingName = \"client_another_existing\"\n\t\tnonExistingClientName     = \"client_non_existing\"\n\t)\n\n\tvar (\n\t\tclientExisting = &Persistent{\n\t\t\tName: clientExistingName,\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"192.0.2.1\")},\n\t\t}\n\n\t\tclientAnotherExisting = &Persistent{\n\t\t\tName: clientAnotherExistingName,\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"192.0.2.2\")},\n\t\t}\n\t)\n\n\tclients := []*Persistent{\n\t\tclientExisting,\n\t\tclientAnotherExisting,\n\t}\n\tci := newIDIndex(clients)\n\n\ttestCases := []struct {\n\t\twant       *Persistent\n\t\tfound      assert.BoolAssertionFunc\n\t\tname       string\n\t\tclientName string\n\t}{{\n\t\twant:       clientExisting,\n\t\tfound:      assert.True,\n\t\tname:       \"existing\",\n\t\tclientName: clientExistingName,\n\t}, {\n\t\twant:       clientAnotherExisting,\n\t\tfound:      assert.True,\n\t\tname:       \"another_existing\",\n\t\tclientName: clientAnotherExistingName,\n\t}, {\n\t\twant:       nil,\n\t\tfound:      assert.False,\n\t\tname:       \"non_existing\",\n\t\tclientName: nonExistingClientName,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc, ok := ci.findByName(tc.clientName)\n\t\t\tassert.Equal(t, tc.want, c)\n\t\t\ttc.found(t, ok)\n\t\t})\n\t}\n}\n\nfunc TestIndex_FindByMAC(t *testing.T) {\n\tvar (\n\t\tcliMAC               = errors.Must(net.ParseMAC(\"11:11:11:11:11:11\"))\n\t\tcliAnotherMAC        = errors.Must(net.ParseMAC(\"22:22:22:22:22:22\"))\n\t\tnonExistingClientMAC = errors.Must(net.ParseMAC(\"33:33:33:33:33:33\"))\n\t)\n\n\tvar (\n\t\tclientExisting = &Persistent{\n\t\t\tName: \"client\",\n\t\t\tMACs: []net.HardwareAddr{cliMAC},\n\t\t}\n\n\t\tclientAnotherExisting = &Persistent{\n\t\t\tName: \"another_client\",\n\t\t\tMACs: []net.HardwareAddr{cliAnotherMAC},\n\t\t}\n\t)\n\n\tclients := []*Persistent{\n\t\tclientExisting,\n\t\tclientAnotherExisting,\n\t}\n\tci := newIDIndex(clients)\n\n\ttestCases := []struct {\n\t\twant      *Persistent\n\t\tfound     assert.BoolAssertionFunc\n\t\tname      string\n\t\tclientMAC net.HardwareAddr\n\t}{{\n\t\twant:      clientExisting,\n\t\tfound:     assert.True,\n\t\tname:      \"existing\",\n\t\tclientMAC: cliMAC,\n\t}, {\n\t\twant:      clientAnotherExisting,\n\t\tfound:     assert.True,\n\t\tname:      \"another_existing\",\n\t\tclientMAC: cliAnotherMAC,\n\t}, {\n\t\twant:      nil,\n\t\tfound:     assert.False,\n\t\tname:      \"non_existing\",\n\t\tclientMAC: nonExistingClientMAC,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc, ok := ci.findByMAC(tc.clientMAC)\n\t\t\tassert.Equal(t, tc.want, c)\n\t\t\ttc.found(t, ok)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/client/persistent.go",
    "content": "package client\n\nimport (\n\t\"context\"\n\t\"encoding\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghslog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/google/uuid\"\n)\n\n// UID is the type for the unique IDs of persistent clients.\ntype UID uuid.UUID\n\n// NewUID returns a new persistent client UID.  Any error returned is an error\n// from the cryptographic randomness reader.\nfunc NewUID() (uid UID, err error) {\n\tuuidv7, err := uuid.NewV7()\n\n\treturn UID(uuidv7), err\n}\n\n// MustNewUID is a wrapper around [NewUID] that panics if there is an error.\nfunc MustNewUID() (uid UID) {\n\tuid, err := NewUID()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unexpected uuidv7 error: %w\", err))\n\t}\n\n\treturn uid\n}\n\n// type check\nvar _ encoding.TextMarshaler = UID{}\n\n// MarshalText implements the [encoding.TextMarshaler] for UID.\nfunc (uid UID) MarshalText() ([]byte, error) {\n\treturn uuid.UUID(uid).MarshalText()\n}\n\n// type check\nvar _ encoding.TextUnmarshaler = (*UID)(nil)\n\n// UnmarshalText implements the [encoding.TextUnmarshaler] interface for UID.\nfunc (uid *UID) UnmarshalText(data []byte) error {\n\treturn (*uuid.UUID)(uid).UnmarshalText(data)\n}\n\n// Persistent contains information about persistent clients.\ntype Persistent struct {\n\t// SafeSearch handles search engine hosts rewrites.\n\tSafeSearch filtering.SafeSearch\n\n\t// BlockedServices is the configuration of blocked services of a client.  It\n\t// must not be nil after initialization.\n\tBlockedServices *filtering.BlockedServices\n\n\t// Name of the persistent client.  Must not be empty.\n\tName string\n\n\t// Tags is a list of client tags that categorize the client.\n\tTags []string\n\n\t// Upstreams is a list of custom upstream DNS servers for the client.  If\n\t// it's empty, the custom upstream cache is disabled, regardless of the\n\t// value of UpstreamsCacheEnabled.\n\tUpstreams []string\n\n\t// IPs is a list of IP addresses that identify the client.  The client must\n\t// have at least one ID (IP, subnet, MAC, or ClientID).\n\tIPs []netip.Addr\n\n\t// Subnets identifying the client.  The client must have at least one ID\n\t// (IP, subnet, MAC, or ClientID).\n\t//\n\t// TODO(s.chzhen):  Use netutil.Prefix.\n\tSubnets []netip.Prefix\n\n\t// MACs identifying the client.  The client must have at least one ID (IP,\n\t// subnet, MAC, or ClientID).\n\tMACs []net.HardwareAddr\n\n\t// ClientIDs identifying the client.  The client must have at least one ID\n\t// (IP, subnet, MAC, or ClientID).\n\tClientIDs []ClientID\n\n\t// UID is the unique identifier of the persistent client.\n\tUID UID\n\n\t// UpstreamsCacheSize defines the size of the custom upstream cache.\n\tUpstreamsCacheSize uint32\n\n\t// UpstreamsCacheEnabled specifies whether the custom upstream cache is\n\t// used.  If true, the list of Upstreams should not be empty.\n\tUpstreamsCacheEnabled bool\n\n\t// UseOwnSettings specifies whether custom filtering settings are used.\n\tUseOwnSettings bool\n\n\t// FilteringEnabled specifies whether filtering is enabled.\n\tFilteringEnabled bool\n\n\t// SafeBrowsingEnabled specifies whether safe browsing is enabled.\n\tSafeBrowsingEnabled bool\n\n\t// ParentalEnabled specifies whether parental control is enabled.\n\tParentalEnabled bool\n\n\t// UseOwnBlockedServices specifies whether custom services are blocked.\n\tUseOwnBlockedServices bool\n\n\t// IgnoreQueryLog specifies whether the client requests are logged.\n\tIgnoreQueryLog bool\n\n\t// IgnoreStatistics  specifies whether the client requests are counted.\n\tIgnoreStatistics bool\n\n\t// SafeSearchConf is the safe search filtering configuration.\n\t//\n\t// TODO(d.kolyshev): Make SafeSearchConf a pointer.\n\tSafeSearchConf filtering.SafeSearchConfig\n}\n\n// validate returns an error if persistent client information contains errors.\n// allTags must be sorted.\nfunc (c *Persistent) validate(ctx context.Context, l *slog.Logger, allTags []string) (err error) {\n\tswitch {\n\tcase c.Name == \"\":\n\t\treturn errors.Error(\"empty name\")\n\tcase c.idendifiersLen() == 0:\n\t\treturn errors.Error(\"id required\")\n\tcase c.UID == UID{}:\n\t\treturn errors.Error(\"uid required\")\n\t}\n\n\tconf, err := proxy.ParseUpstreamsConfig(c.Upstreams, &upstream.Options{\n\t\tLogger: l.With(aghslog.KeyUpstreamType, aghslog.UpstreamTypeTest),\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid upstream servers: %w\", err)\n\t}\n\n\terr = conf.Close()\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"client: closing upstream config\", slogutil.KeyError, err)\n\t}\n\n\tfor _, t := range c.Tags {\n\t\t_, ok := slices.BinarySearch(allTags, t)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"invalid tag: %q\", t)\n\t\t}\n\t}\n\n\t// TODO(s.chzhen):  Move to the constructor.\n\tslices.Sort(c.Tags)\n\n\treturn nil\n}\n\n// SetIDs parses a list of strings into typed fields and returns an error if\n// there is one.\nfunc (c *Persistent) SetIDs(ids []string) (err error) {\n\tfor _, id := range ids {\n\t\terr = c.setID(id)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tslices.SortFunc(c.IPs, netip.Addr.Compare)\n\n\t// TODO(s.chzhen):  Use netip.PrefixCompare in Go 1.23.\n\tslices.SortFunc(c.Subnets, subnetCompare)\n\tslices.SortFunc(c.MACs, slices.Compare[net.HardwareAddr])\n\tslices.Sort(c.ClientIDs)\n\n\treturn nil\n}\n\n// subnetCompare is a comparison function for the two subnets.  It returns -1 if\n// x sorts before y, 1 if x sorts after y, and 0 if their relative sorting\n// position is the same.\n//\n// TODO(s.chzhen):  Use netip.Prefix.Compare in Go 1.26.\nfunc subnetCompare(x, y netip.Prefix) (cmp int) {\n\tif x == y {\n\t\treturn 0\n\t}\n\n\txAddr, xBits := x.Addr(), x.Bits()\n\tyAddr, yBits := y.Addr(), y.Bits()\n\tif xBits == yBits {\n\t\treturn xAddr.Compare(yAddr)\n\t}\n\n\tif xBits > yBits {\n\t\treturn -1\n\t} else {\n\t\treturn 1\n\t}\n}\n\n// setID parses id into typed field if there is no error.\nfunc (c *Persistent) setID(id string) (err error) {\n\tif id == \"\" {\n\t\treturn errors.Error(\"clientid is empty\")\n\t}\n\n\tvar ip netip.Addr\n\tif ip, err = netip.ParseAddr(id); err == nil {\n\t\tc.IPs = append(c.IPs, ip)\n\n\t\treturn nil\n\t}\n\n\tvar subnet netip.Prefix\n\tif subnet, err = netip.ParsePrefix(id); err == nil {\n\t\tc.Subnets = append(c.Subnets, subnet)\n\n\t\treturn nil\n\t}\n\n\tvar mac net.HardwareAddr\n\tif mac, err = net.ParseMAC(id); err == nil {\n\t\tc.MACs = append(c.MACs, mac)\n\n\t\treturn nil\n\t}\n\n\terr = ValidateClientID(id)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tc.ClientIDs = append(c.ClientIDs, ClientID(strings.ToLower(id)))\n\n\treturn nil\n}\n\n// Identifiers returns a list of client identifiers containing at least one\n// element.\nfunc (c *Persistent) Identifiers() (ids []string) {\n\tids = make([]string, 0, c.idendifiersLen())\n\n\tfor _, ip := range c.IPs {\n\t\tids = append(ids, ip.String())\n\t}\n\n\tfor _, subnet := range c.Subnets {\n\t\tids = append(ids, subnet.String())\n\t}\n\n\tfor _, mac := range c.MACs {\n\t\tids = append(ids, mac.String())\n\t}\n\n\tfor _, cid := range c.ClientIDs {\n\t\tids = append(ids, string(cid))\n\t}\n\n\treturn ids\n}\n\n// identifiersLen returns the number of client identifiers.\nfunc (c *Persistent) idendifiersLen() (n int) {\n\treturn len(c.IPs) + len(c.Subnets) + len(c.MACs) + len(c.ClientIDs)\n}\n\n// EqualIDs returns true if the ids of the current and previous clients are the\n// same.\nfunc (c *Persistent) EqualIDs(prev *Persistent) (equal bool) {\n\treturn slices.Equal(c.IPs, prev.IPs) &&\n\t\tslices.Equal(c.Subnets, prev.Subnets) &&\n\t\tslices.EqualFunc(c.MACs, prev.MACs, slices.Equal[net.HardwareAddr]) &&\n\t\tslices.Equal(c.ClientIDs, prev.ClientIDs)\n}\n\n// ShallowClone returns a deep copy of the client, except upstreamConfig,\n// safeSearchConf, SafeSearch fields, because it's difficult to copy them.\nfunc (c *Persistent) ShallowClone() (clone *Persistent) {\n\tclone = &Persistent{}\n\t*clone = *c\n\n\tclone.BlockedServices = c.BlockedServices.Clone()\n\tclone.Tags = slices.Clone(c.Tags)\n\tclone.Upstreams = slices.Clone(c.Upstreams)\n\n\tclone.IPs = slices.Clone(c.IPs)\n\tclone.Subnets = slices.Clone(c.Subnets)\n\tclone.MACs = slices.Clone(c.MACs)\n\tclone.ClientIDs = slices.Clone(c.ClientIDs)\n\n\treturn clone\n}\n"
  },
  {
    "path": "internal/client/persistent_internal_test.go",
    "content": "package client\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPersistent_EqualIDs(t *testing.T) {\n\tconst (\n\t\tip  = \"0.0.0.0\"\n\t\tip1 = \"1.1.1.1\"\n\t\tip2 = \"2.2.2.2\"\n\n\t\tcidr  = \"0.0.0.0/0\"\n\t\tcidr1 = \"1.1.1.1/11\"\n\t\tcidr2 = \"2.2.2.2/22\"\n\n\t\tmac  = \"00-00-00-00-00-00\"\n\t\tmac1 = \"11-11-11-11-11-11\"\n\t\tmac2 = \"22-22-22-22-22-22\"\n\n\t\tcli  = \"client0\"\n\t\tcli1 = \"client1\"\n\t\tcli2 = \"client2\"\n\t)\n\n\ttestCases := []struct {\n\t\twant    assert.BoolAssertionFunc\n\t\tname    string\n\t\tids     []string\n\t\tprevIDs []string\n\t}{{\n\t\tname:    \"single_ip\",\n\t\tids:     []string{ip1},\n\t\tprevIDs: []string{ip1},\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"single_ip_not_equal\",\n\t\tids:     []string{ip1},\n\t\tprevIDs: []string{ip2},\n\t\twant:    assert.False,\n\t}, {\n\t\tname:    \"ips_not_equal\",\n\t\tids:     []string{ip1, ip2},\n\t\tprevIDs: []string{ip1, ip},\n\t\twant:    assert.False,\n\t}, {\n\t\tname:    \"ips_mixed_equal\",\n\t\tids:     []string{ip1, ip2},\n\t\tprevIDs: []string{ip2, ip1},\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"single_subnet\",\n\t\tids:     []string{cidr1},\n\t\tprevIDs: []string{cidr1},\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"subnets_not_equal\",\n\t\tids:     []string{ip1, ip2, cidr1, cidr2},\n\t\tprevIDs: []string{ip1, ip2, cidr1, cidr},\n\t\twant:    assert.False,\n\t}, {\n\t\tname:    \"subnets_mixed_equal\",\n\t\tids:     []string{ip1, ip2, cidr1, cidr2},\n\t\tprevIDs: []string{cidr2, cidr1, ip2, ip1},\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"single_mac\",\n\t\tids:     []string{mac1},\n\t\tprevIDs: []string{mac1},\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"single_mac_not_equal\",\n\t\tids:     []string{mac1},\n\t\tprevIDs: []string{mac2},\n\t\twant:    assert.False,\n\t}, {\n\t\tname:    \"macs_not_equal\",\n\t\tids:     []string{ip1, ip2, cidr1, cidr2, mac1, mac2},\n\t\tprevIDs: []string{ip1, ip2, cidr1, cidr2, mac1, mac},\n\t\twant:    assert.False,\n\t}, {\n\t\tname:    \"macs_mixed_equal\",\n\t\tids:     []string{ip1, ip2, cidr1, cidr2, mac1, mac2},\n\t\tprevIDs: []string{mac2, mac1, cidr2, cidr1, ip2, ip1},\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"single_client_id\",\n\t\tids:     []string{cli1},\n\t\tprevIDs: []string{cli1},\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"single_client_id_not_equal\",\n\t\tids:     []string{cli1},\n\t\tprevIDs: []string{cli2},\n\t\twant:    assert.False,\n\t}, {\n\t\tname:    \"client_ids_not_equal\",\n\t\tids:     []string{ip1, ip2, cidr1, cidr2, mac1, mac2, cli1, cli2},\n\t\tprevIDs: []string{ip1, ip2, cidr1, cidr2, mac1, mac2, cli1, cli},\n\t\twant:    assert.False,\n\t}, {\n\t\tname:    \"client_ids_mixed_equal\",\n\t\tids:     []string{ip1, ip2, cidr1, cidr2, mac1, mac2, cli1, cli2},\n\t\tprevIDs: []string{cli2, cli1, mac2, mac1, cidr2, cidr1, ip2, ip1},\n\t\twant:    assert.True,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := &Persistent{}\n\t\t\terr := c.SetIDs(tc.ids)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tprev := &Persistent{}\n\t\t\terr = prev.SetIDs(tc.prevIDs)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.want(t, c.EqualIDs(prev))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/client/runtimeindex.go",
    "content": "package client\n\nimport \"net/netip\"\n\n// runtimeIndex stores information about runtime clients.\ntype runtimeIndex struct {\n\t// index maps IP address to runtime client.\n\tindex map[netip.Addr]*Runtime\n}\n\n// newRuntimeIndex returns initialized runtime index.\nfunc newRuntimeIndex() (ri *runtimeIndex) {\n\treturn &runtimeIndex{\n\t\tindex: map[netip.Addr]*Runtime{},\n\t}\n}\n\n// client returns the saved runtime client by ip.  If no such client exists,\n// returns nil.\nfunc (ri *runtimeIndex) client(ip netip.Addr) (rc *Runtime) {\n\treturn ri.index[ip]\n}\n\n// add saves the runtime client in the index.  IP address of a client must be\n// unique.  See [Runtime.Client].  rc must not be nil.\nfunc (ri *runtimeIndex) add(rc *Runtime) {\n\tip := rc.Addr()\n\tri.index[ip] = rc\n}\n\n// rangeClients calls f for each runtime client in an undefined order.\nfunc (ri *runtimeIndex) rangeClients(f func(rc *Runtime) (cont bool)) {\n\tfor _, rc := range ri.index {\n\t\tif !f(rc) {\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// setInfo sets the client information from cs for runtime client stored by ip.\n// If no such client exists, it creates one.\nfunc (ri *runtimeIndex) setInfo(ip netip.Addr, cs Source, hosts []string) (rc *Runtime) {\n\trc = ri.index[ip]\n\tif rc == nil {\n\t\trc = NewRuntime(ip)\n\t\tri.add(rc)\n\t}\n\n\trc.setInfo(cs, hosts)\n\n\treturn rc\n}\n\n// clearSource removes information from the specified source from all clients.\nfunc (ri *runtimeIndex) clearSource(src Source) {\n\tfor _, rc := range ri.index {\n\t\trc.unset(src)\n\t}\n}\n\n// removeEmpty removes empty runtime clients and returns the number of removed\n// clients.\nfunc (ri *runtimeIndex) removeEmpty() (n int) {\n\tfor ip, rc := range ri.index {\n\t\tif rc.isEmpty() {\n\t\t\tdelete(ri.index, ip)\n\t\t\tn++\n\t\t}\n\t}\n\n\treturn n\n}\n"
  },
  {
    "path": "internal/client/storage.go",
    "content": "package client\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/arpdb\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/hostsfile\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// allowedTags is the list of available client tags.\nvar allowedTags = []string{\n\t\"device_audio\",\n\t\"device_camera\",\n\t\"device_gameconsole\",\n\t\"device_laptop\",\n\t\"device_nas\", // Network-attached Storage\n\t\"device_other\",\n\t\"device_pc\",\n\t\"device_phone\",\n\t\"device_printer\",\n\t\"device_securityalarm\",\n\t\"device_tablet\",\n\t\"device_tv\",\n\n\t\"os_android\",\n\t\"os_ios\",\n\t\"os_linux\",\n\t\"os_macos\",\n\t\"os_other\",\n\t\"os_windows\",\n\n\t\"user_admin\",\n\t\"user_child\",\n\t\"user_regular\",\n}\n\n// DHCP is an interface for accessing DHCP lease data the [Storage] needs.\ntype DHCP interface {\n\t// Leases returns all the DHCP leases.\n\tLeases() (leases []*dhcpsvc.Lease)\n\n\t// HostByIP returns the hostname of the DHCP client with the given IP\n\t// address.  host will be empty if there is no such client, due to an\n\t// assumption that a DHCP client must always have a hostname.\n\tHostByIP(ip netip.Addr) (host string)\n\n\t// MACByIP returns the MAC address for the given IP address leased.  It\n\t// returns nil if there is no such client, due to an assumption that a DHCP\n\t// client must always have a MAC address.\n\tMACByIP(ip netip.Addr) (mac net.HardwareAddr)\n}\n\n// EmptyDHCP is the empty [DHCP] implementation that does nothing.\ntype EmptyDHCP struct{}\n\n// type check\nvar _ DHCP = EmptyDHCP{}\n\n// Leases implements the [DHCP] interface for emptyDHCP.\nfunc (EmptyDHCP) Leases() (leases []*dhcpsvc.Lease) { return nil }\n\n// HostByIP implements the [DHCP] interface for emptyDHCP.\nfunc (EmptyDHCP) HostByIP(_ netip.Addr) (host string) { return \"\" }\n\n// MACByIP implements the [DHCP] interface for emptyDHCP.\nfunc (EmptyDHCP) MACByIP(_ netip.Addr) (mac net.HardwareAddr) { return nil }\n\n// HostsContainer is an interface for receiving updates to the system hosts\n// file.\ntype HostsContainer interface {\n\tUpd() (updates <-chan *hostsfile.DefaultStorage)\n}\n\n// StorageConfig is the client storage configuration structure.\ntype StorageConfig struct {\n\t// BaseLogger is used to create loggers for other entities.  It should not\n\t// have a prefix and must not be nil.\n\tBaseLogger *slog.Logger\n\n\t// Logger is used for logging the operation of the client storage.  It must\n\t// not be nil.\n\tLogger *slog.Logger\n\n\t// Clock is used by [upstreamManager] to retrieve the current time.  It must\n\t// not be nil.\n\tClock timeutil.Clock\n\n\t// DHCP is used to match IPs against MACs of persistent clients and update\n\t// [SourceDHCP] runtime client information.  It must not be nil.\n\tDHCP DHCP\n\n\t// EtcHosts is used to update [SourceHostsFile] runtime client information.\n\tEtcHosts HostsContainer\n\n\t// ARPDB is used to update [SourceARP] runtime client information.\n\tARPDB arpdb.Interface\n\n\t// InitialClients is a list of persistent clients parsed from the\n\t// configuration file.  Each client must not be nil.\n\tInitialClients []*Persistent\n\n\t// ARPClientsUpdatePeriod defines how often [SourceARP] runtime client\n\t// information is updated.\n\tARPClientsUpdatePeriod time.Duration\n\n\t// RuntimeSourceDHCP specifies whether to update [SourceDHCP] information\n\t// of runtime clients.\n\tRuntimeSourceDHCP bool\n}\n\n// Storage contains information about persistent and runtime clients.\ntype Storage struct {\n\t// logger is used for logging the operation of the client storage.  It must\n\t// not be nil.\n\tlogger *slog.Logger\n\n\t// mu protects indexes of persistent and runtime clients.\n\tmu *sync.Mutex\n\n\t// index contains information about persistent clients.\n\tindex *index\n\n\t// runtimeIndex contains information about runtime clients.\n\truntimeIndex *runtimeIndex\n\n\t// upstreamManager stores and updates custom client upstream configurations.\n\tupstreamManager *upstreamManager\n\n\t// dhcp is used to update [SourceDHCP] runtime client information.\n\tdhcp DHCP\n\n\t// etcHosts is used to update [SourceHostsFile] runtime client information.\n\tetcHosts HostsContainer\n\n\t// arpDB is used to update [SourceARP] runtime client information.\n\tarpDB arpdb.Interface\n\n\t// done is the shutdown signaling channel.\n\tdone chan struct{}\n\n\t// allowedTags is a sorted list of all allowed tags.  It must not be\n\t// modified after initialization.\n\t//\n\t// TODO(s.chzhen):  Use custom type.\n\tallowedTags []string\n\n\t// arpClientsUpdatePeriod defines how often [SourceARP] runtime client\n\t// information is updated.  It must be greater than zero.\n\tarpClientsUpdatePeriod time.Duration\n\n\t// runtimeSourceDHCP specifies whether to update [SourceDHCP] information\n\t// of runtime clients.\n\truntimeSourceDHCP bool\n}\n\n// NewStorage returns initialized client storage.  conf must not be nil.\nfunc NewStorage(ctx context.Context, conf *StorageConfig) (s *Storage, err error) {\n\ttags := slices.Clone(allowedTags)\n\tslices.Sort(tags)\n\n\ts = &Storage{\n\t\tlogger:                 conf.Logger,\n\t\tmu:                     &sync.Mutex{},\n\t\tindex:                  newIndex(),\n\t\truntimeIndex:           newRuntimeIndex(),\n\t\tupstreamManager:        newUpstreamManager(conf.BaseLogger, conf.Clock),\n\t\tdhcp:                   conf.DHCP,\n\t\tetcHosts:               conf.EtcHosts,\n\t\tarpDB:                  conf.ARPDB,\n\t\tdone:                   make(chan struct{}),\n\t\tallowedTags:            tags,\n\t\tarpClientsUpdatePeriod: conf.ARPClientsUpdatePeriod,\n\t\truntimeSourceDHCP:      conf.RuntimeSourceDHCP,\n\t}\n\n\tfor i, p := range conf.InitialClients {\n\t\terr = s.Add(ctx, p)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"adding client %q at index %d: %w\", p.Name, i, err)\n\t\t}\n\t}\n\n\ts.ReloadARP(ctx)\n\n\treturn s, nil\n}\n\n// Start starts the goroutines for updating the runtime client information.\n//\n// TODO(s.chzhen):  Pass context.\nfunc (s *Storage) Start(ctx context.Context) (err error) {\n\tgo s.periodicARPUpdate(ctx)\n\tgo s.handleHostsUpdates(ctx)\n\n\treturn nil\n}\n\n// Shutdown gracefully stops the client storage.\n//\n// TODO(s.chzhen):  Pass context.\nfunc (s *Storage) Shutdown(_ context.Context) (err error) {\n\tclose(s.done)\n\n\treturn s.upstreamManager.close()\n}\n\n// periodicARPUpdate periodically reloads runtime clients from ARP.  It is\n// intended to be used as a goroutine.\nfunc (s *Storage) periodicARPUpdate(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, s.logger)\n\n\tt := time.NewTicker(s.arpClientsUpdatePeriod)\n\n\tfor {\n\t\tselect {\n\t\tcase <-t.C:\n\t\t\ts.ReloadARP(ctx)\n\t\tcase <-s.done:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// ReloadARP reloads runtime clients from ARP, if configured.\nfunc (s *Storage) ReloadARP(ctx context.Context) {\n\tif s.arpDB != nil {\n\t\ts.addFromSystemARP(ctx)\n\t}\n}\n\n// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a\n// command.\nfunc (s *Storage) addFromSystemARP(ctx context.Context) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif err := s.arpDB.Refresh(ctx); err != nil {\n\t\ts.arpDB = arpdb.Empty{}\n\t\ts.logger.ErrorContext(ctx, \"refreshing arp container\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tns := s.arpDB.Neighbors()\n\tif len(ns) == 0 {\n\t\ts.logger.DebugContext(ctx, \"refreshing arp container: the update is empty\")\n\n\t\treturn\n\t}\n\n\tsrc := SourceARP\n\ts.runtimeIndex.clearSource(src)\n\n\tfor _, n := range ns {\n\t\ts.runtimeIndex.setInfo(n.IP, src, []string{n.Name})\n\t}\n\n\tremoved := s.runtimeIndex.removeEmpty()\n\n\ts.logger.DebugContext(\n\t\tctx,\n\t\t\"updating client aliases from arp neighborhood\",\n\t\t\"added\", len(ns),\n\t\t\"removed\", removed,\n\t)\n}\n\n// handleHostsUpdates receives the updates from the hosts container and adds\n// them to the clients storage.  It is intended to be used as a goroutine.\nfunc (s *Storage) handleHostsUpdates(ctx context.Context) {\n\tif s.etcHosts == nil {\n\t\treturn\n\t}\n\n\tdefer slogutil.RecoverAndLog(ctx, s.logger)\n\n\tfor {\n\t\tselect {\n\t\tcase upd, ok := <-s.etcHosts.Upd():\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.addFromHostsFile(ctx, upd)\n\t\tcase <-s.done:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// addFromHostsFile fills the client-hostname pairing index from the system's\n// hosts files.\nfunc (s *Storage) addFromHostsFile(ctx context.Context, hosts *hostsfile.DefaultStorage) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tsrc := SourceHostsFile\n\ts.runtimeIndex.clearSource(src)\n\n\tadded := 0\n\thosts.RangeNames(func(addr netip.Addr, names []string) (cont bool) {\n\t\t// Only the first name of the first record is considered a canonical\n\t\t// hostname for the IP address.\n\t\t//\n\t\t// TODO(e.burkov):  Consider using all the names from all the records.\n\t\ts.runtimeIndex.setInfo(addr, src, []string{names[0]})\n\t\tadded++\n\n\t\treturn true\n\t})\n\n\tremoved := s.runtimeIndex.removeEmpty()\n\ts.logger.DebugContext(\n\t\tctx,\n\t\t\"updating client aliases from system hosts file\",\n\t\t\"added\", added,\n\t\t\"removed\", removed,\n\t)\n}\n\n// type check\nvar _ AddressUpdater = (*Storage)(nil)\n\n// UpdateAddress implements the [AddressUpdater] interface for *Storage\nfunc (s *Storage) UpdateAddress(ctx context.Context, ip netip.Addr, host string, info *whois.Info) {\n\t// Common fast path optimization.\n\tif host == \"\" && info == nil {\n\t\treturn\n\t}\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif host != \"\" {\n\t\ts.runtimeIndex.setInfo(ip, SourceRDNS, []string{host})\n\t}\n\n\tif info != nil {\n\t\ts.setWHOISInfo(ctx, ip, info)\n\t}\n}\n\n// UpdateDHCP updates [SourceDHCP] runtime client information.\nfunc (s *Storage) UpdateDHCP(ctx context.Context) {\n\tif s.dhcp == nil || !s.runtimeSourceDHCP {\n\t\treturn\n\t}\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tsrc := SourceDHCP\n\ts.runtimeIndex.clearSource(src)\n\n\tadded := 0\n\tfor _, l := range s.dhcp.Leases() {\n\t\ts.runtimeIndex.setInfo(l.IP, src, []string{l.Hostname})\n\t\tadded++\n\t}\n\n\tremoved := s.runtimeIndex.removeEmpty()\n\ts.logger.DebugContext(\n\t\tctx,\n\t\t\"updating client aliases from dhcp\",\n\t\t\"added\", added,\n\t\t\"removed\", removed,\n\t)\n}\n\n// setWHOISInfo sets the WHOIS information for a runtime client.\nfunc (s *Storage) setWHOISInfo(ctx context.Context, ip netip.Addr, wi *whois.Info) {\n\t_, ok := s.index.findByIP(ip)\n\tif ok {\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"persistent client is already created, ignore whois info\",\n\t\t\t\"ip\", ip,\n\t\t)\n\n\t\treturn\n\t}\n\n\trc := s.runtimeIndex.client(ip)\n\tif rc == nil {\n\t\trc = NewRuntime(ip)\n\t\ts.runtimeIndex.add(rc)\n\t}\n\n\trc.setWHOIS(wi)\n\n\ts.logger.DebugContext(ctx, \"set whois info for runtime client\", \"ip\", ip, \"whois\", wi)\n}\n\n// Add stores persistent client information or returns an error.\nfunc (s *Storage) Add(ctx context.Context, p *Persistent) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"adding client: %w\") }()\n\n\terr = p.validate(ctx, s.logger, s.allowedTags)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\terr = s.index.clashesUID(p)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\terr = s.index.clashes(p)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\ts.index.add(p)\n\ts.upstreamManager.updateCustomUpstreamConfig(p)\n\n\ts.logger.DebugContext(\n\t\tctx,\n\t\t\"client added\",\n\t\t\"name\", p.Name,\n\t\t\"ids\", p.Identifiers(),\n\t\t\"clients_count\", s.index.size(),\n\t)\n\n\treturn nil\n}\n\n// FindParams represents the parameters for searching a client.  At least one\n// field must be non-empty.\ntype FindParams struct {\n\t// ClientID is a unique identifier for the client used in DoH, DoT, and DoQ\n\t// DNS queries.\n\tClientID ClientID\n\n\t// RemoteIP is the IP address used as a client search parameter.\n\tRemoteIP netip.Addr\n\n\t// Subnet is the CIDR used as a client search parameter.\n\tSubnet netip.Prefix\n\n\t// MAC is the physical hardware address used as a client search parameter.\n\tMAC net.HardwareAddr\n\n\t// UID is the unique ID of persistent client used as a search parameter.\n\t//\n\t// TODO(s.chzhen):  Use this.\n\tUID UID\n}\n\n// ErrBadIdentifier is returned by [FindParams.Set] when it cannot parse the\n// provided client identifier.\nconst ErrBadIdentifier errors.Error = \"bad client identifier\"\n\n// Set clears the stored search parameters and parses the string representation\n// of the search parameter into typed parameter, storing it.  In some cases, it\n// may result in storing both an IP address and a MAC address because they might\n// have identical string representations.  It returns [ErrBadIdentifier] if id\n// cannot be parsed.\n//\n// TODO(s.chzhen):  Add support for UID.\nfunc (p *FindParams) Set(id string) (err error) {\n\t*p = FindParams{}\n\n\tisFound := false\n\n\tif netutil.IsValidIPString(id) {\n\t\t// It is safe to use [netip.MustParseAddr] because it has already been\n\t\t// validated that id contains the string representation of the IP\n\t\t// address.\n\t\tp.RemoteIP = netip.MustParseAddr(id)\n\n\t\t// Even if id can be parsed as an IP address, it may be a MAC address.\n\t\t// So do not return prematurely, continue parsing.\n\t\tisFound = true\n\t}\n\n\tif netutil.IsValidMACString(id) {\n\t\tp.MAC, err = net.ParseMAC(id)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"parsing mac from %q: %w\", id, err))\n\t\t}\n\n\t\tisFound = true\n\t}\n\n\tif isFound {\n\t\treturn nil\n\t}\n\n\tif netutil.IsValidIPPrefixString(id) {\n\t\t// It is safe to use [netip.MustParsePrefix] because it has already been\n\t\t// validated that id contains the string representation of IP prefix.\n\t\tp.Subnet = netip.MustParsePrefix(id)\n\n\t\treturn nil\n\t}\n\n\tif !isValidClientID(id) {\n\t\treturn ErrBadIdentifier\n\t}\n\n\tp.ClientID = ClientID(id)\n\n\treturn nil\n}\n\n// Find represents the parameters for searching a client.  params must not be\n// nil and must have at least one non-empty field.\nfunc (s *Storage) Find(params *FindParams) (p *Persistent, ok bool) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tisClientID := params.ClientID != \"\"\n\tisRemoteIP := params.RemoteIP != (netip.Addr{})\n\tisSubnet := params.Subnet != (netip.Prefix{})\n\tisMAC := params.MAC != nil\n\n\tfor {\n\t\tswitch {\n\t\tcase isClientID:\n\t\t\tisClientID = false\n\t\t\tp, ok = s.index.findByClientID(params.ClientID)\n\t\tcase isRemoteIP:\n\t\t\tisRemoteIP = false\n\t\t\tp, ok = s.findByIP(params.RemoteIP)\n\t\tcase isSubnet:\n\t\t\tisSubnet = false\n\t\t\tp, ok = s.index.findByCIDR(params.Subnet)\n\t\tcase isMAC:\n\t\t\tisMAC = false\n\t\t\tp, ok = s.index.findByMAC(params.MAC)\n\t\tdefault:\n\t\t\treturn nil, false\n\t\t}\n\n\t\tif ok {\n\t\t\treturn p.ShallowClone(), true\n\t\t}\n\t}\n}\n\n// findByIP finds persistent client by IP address.  s.mu is expected to be\n// locked.\nfunc (s *Storage) findByIP(addr netip.Addr) (p *Persistent, ok bool) {\n\tp, ok = s.index.findByIP(addr)\n\tif ok {\n\t\treturn p, true\n\t}\n\n\tfoundMAC := s.dhcp.MACByIP(addr)\n\tif foundMAC != nil {\n\t\treturn s.index.findByMAC(foundMAC)\n\t}\n\n\treturn nil, false\n}\n\n// FindLoose is like [Storage.Find] but it also tries to find a persistent\n// client by IP address without zone.  It strips the IPv6 zone index from the\n// stored IP addresses before comparing, because querylog entries don't have it.\n// See TODO on [querylog.logEntry.IP].\n//\n// Note that multiple clients can have the same IP address with different zones.\n// Therefore, the result of this method is indeterminate.\n//\n// TODO(s.chzhen):  Consider accepting [FindParams].\nfunc (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tp, ok = s.index.find(id)\n\tif ok {\n\t\treturn p.ShallowClone(), ok\n\t}\n\n\tfoundMAC := s.dhcp.MACByIP(ip)\n\tif foundMAC != nil {\n\t\treturn s.index.findByMAC(foundMAC)\n\t}\n\n\tp = s.index.findByIPWithoutZone(ip)\n\tif p != nil {\n\t\treturn p.ShallowClone(), true\n\t}\n\n\treturn nil, false\n}\n\n// RemoveByName removes persistent client information.  ok is false if no such\n// client exists by that name.\nfunc (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tp, ok := s.index.findByName(name)\n\tif !ok {\n\t\treturn false\n\t}\n\n\ts.index.remove(p)\n\n\terr := s.upstreamManager.remove(p.UID)\n\tif err != nil {\n\t\ts.logger.DebugContext(ctx, \"closing client upstreams\", \"name\", name, slogutil.KeyError, err)\n\t}\n\n\treturn true\n}\n\n// Update finds the stored persistent client by its name and updates its\n// information from p.\nfunc (s *Storage) Update(ctx context.Context, name string, p *Persistent) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"updating client: %w\") }()\n\n\terr = p.validate(ctx, s.logger, s.allowedTags)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tstored, ok := s.index.findByName(name)\n\tif !ok {\n\t\treturn fmt.Errorf(\"client %q is not found\", name)\n\t}\n\n\t// Client p has a newly generated UID, so replace it with the stored one.\n\t//\n\t// TODO(s.chzhen):  Remove when frontend starts handling UIDs.\n\tp.UID = stored.UID\n\n\terr = s.index.clashes(p)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\ts.index.remove(stored)\n\ts.index.add(p)\n\n\ts.upstreamManager.updateCustomUpstreamConfig(p)\n\n\treturn nil\n}\n\n// RangeByName calls f for each persistent client sorted by name, unless cont is\n// false.\nfunc (s *Storage) RangeByName(f func(c *Persistent) (cont bool)) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\ts.index.rangeByName(f)\n}\n\n// Size returns the number of persistent clients.\nfunc (s *Storage) Size() (n int) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\treturn s.index.size()\n}\n\n// ClientRuntime returns a copy of the saved runtime client by ip.  If no such\n// client exists, returns nil.\nfunc (s *Storage) ClientRuntime(ip netip.Addr) (rc *Runtime) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\trc = s.runtimeIndex.client(ip)\n\tif !s.runtimeSourceDHCP {\n\t\treturn rc.clone()\n\t}\n\n\t// SourceHostsFile > SourceDHCP, so return immediately if the client is from\n\t// the hosts file.\n\tif rc != nil && rc.hostsFile != nil {\n\t\treturn rc.clone()\n\t}\n\n\t// Otherwise, check the DHCP server and add the client information if there\n\t// is any.\n\thost := s.dhcp.HostByIP(ip)\n\tif host == \"\" {\n\t\treturn rc.clone()\n\t}\n\n\trc = s.runtimeIndex.setInfo(ip, SourceDHCP, []string{host})\n\n\treturn rc.clone()\n}\n\n// RangeRuntime calls f for each runtime client in an undefined order.\nfunc (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\ts.runtimeIndex.rangeClients(f)\n}\n\n// AllowedTags returns the list of available client tags.  tags must not be\n// modified.\nfunc (s *Storage) AllowedTags() (tags []string) {\n\treturn s.allowedTags\n}\n\n// CustomUpstreamConfig implements the [dnsforward.ClientsContainer] interface\n// for *Storage\nfunc (s *Storage) CustomUpstreamConfig(\n\tid string,\n\taddr netip.Addr,\n) (prxConf *proxy.CustomUpstreamConfig) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tc, ok := s.index.findByClientID(ClientID(id))\n\tif !ok {\n\t\tc, ok = s.findByIP(addr)\n\t}\n\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn s.upstreamManager.customUpstreamConfig(c.UID, c.Name)\n}\n\n// UpdateCommonUpstreamConfig implements the [dnsforward.ClientsContainer]\n// interface for *Storage\nfunc (s *Storage) UpdateCommonUpstreamConfig(conf *CommonUpstreamConfig) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\ts.upstreamManager.updateCommonUpstreamConfig(conf)\n}\n\n// ClearUpstreamCache implements the [dnsforward.ClientsContainer] interface for\n// *Storage\nfunc (s *Storage) ClearUpstreamCache() {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\ts.upstreamManager.clearUpstreamCache()\n}\n\n// ApplyClientFiltering retrieves persistent client information using the\n// ClientID or client IP address, and applies it to the filtering settings.\n// setts must not be nil.\nfunc (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filtering.Settings) {\n\tc, ok := s.index.findByClientID(ClientID(id))\n\tif !ok {\n\t\tc, ok = s.index.findByIP(addr)\n\t}\n\n\tif !ok {\n\t\tfoundMAC := s.dhcp.MACByIP(addr)\n\t\tif foundMAC != nil {\n\t\t\tc, ok = s.index.findByMAC(foundMAC)\n\t\t}\n\t}\n\n\tif !ok {\n\t\ts.logger.Debug(\"no client filtering settings found\", \"clientid\", id, \"addr\", addr)\n\n\t\treturn\n\t}\n\n\ts.logger.Debug(\"applying custom client filtering settings\", \"client_name\", c.Name)\n\n\tif c.UseOwnBlockedServices {\n\t\tsetts.BlockedServices = c.BlockedServices.Clone()\n\t}\n\n\tsetts.ClientName = c.Name\n\tsetts.ClientTags = slices.Clone(c.Tags)\n\tif !c.UseOwnSettings {\n\t\treturn\n\t}\n\n\tsetts.FilteringEnabled = c.FilteringEnabled\n\tsetts.SafeSearchEnabled = c.SafeSearchConf.Enabled\n\tsetts.ClientSafeSearch = c.SafeSearch\n\tsetts.SafeBrowsingEnabled = c.SafeBrowsingEnabled\n\tsetts.ParentalEnabled = c.ParentalEnabled\n}\n"
  },
  {
    "path": "internal/client/storage_test.go",
    "content": "package client_test\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\t\"runtime\"\n\t\"slices\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/arpdb\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpd\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dnsforward\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/hostsfile\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/service\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/faketime\"\n\t\"github.com/AdguardTeam/golibs/testutil/servicetest\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\n// newTestStorage is a helper function that returns initialized storage.\nfunc newTestStorage(tb testing.TB, clock timeutil.Clock) (s *client.Storage) {\n\ttb.Helper()\n\n\tctx := testutil.ContextWithTimeout(tb, testTimeout)\n\ts, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger: testLogger,\n\t\tLogger:     testLogger,\n\t\tClock:      clock,\n\t})\n\trequire.NoError(tb, err)\n\n\treturn s\n}\n\n// type check\nvar _ dnsforward.ClientsContainer = (*client.Storage)(nil)\n\n// testHostsContainer is a mock implementation of the [client.HostsContainer]\n// interface.\ntype testHostsContainer struct {\n\tonUpd func() (updates <-chan *hostsfile.DefaultStorage)\n}\n\n// type check\nvar _ client.HostsContainer = (*testHostsContainer)(nil)\n\n// Upd implements the [client.HostsContainer] interface for *testHostsContainer.\nfunc (c *testHostsContainer) Upd() (updates <-chan *hostsfile.DefaultStorage) {\n\treturn c.onUpd()\n}\n\n// Interface stores and refreshes the network neighborhood reported by ARP\n// (Address Resolution Protocol).\ntype Interface interface {\n\t// Refresher updates the stored data.  It must be safe for concurrent use.\n\tservice.Refresher\n\n\t// Neighbors returns the last set of data reported by ARP.  Both the method\n\t// and it's result must be safe for concurrent use.\n\tNeighbors() (ns []arpdb.Neighbor)\n}\n\n// testARPDB is a mock implementation of the [arpdb.Interface].\ntype testARPDB struct {\n\tonRefresh   func(ctx context.Context) (err error)\n\tonNeighbors func() (ns []arpdb.Neighbor)\n}\n\n// type check\nvar _ arpdb.Interface = (*testARPDB)(nil)\n\n// Refresh implements the [arpdb.Interface] interface for *testARP.\nfunc (c *testARPDB) Refresh(ctx context.Context) (err error) {\n\treturn c.onRefresh(ctx)\n}\n\n// Neighbors implements the [arpdb.Interface] interface for *testARP.\nfunc (c *testARPDB) Neighbors() (ns []arpdb.Neighbor) {\n\treturn c.onNeighbors()\n}\n\n// testDHCP is a mock implementation of the [client.DHCP].\ntype testDHCP struct {\n\tOnLeases func() (leases []*dhcpsvc.Lease)\n\tOnHostBy func(ip netip.Addr) (host string)\n\tOnMACBy  func(ip netip.Addr) (mac net.HardwareAddr)\n}\n\n// type check\nvar _ client.DHCP = (*testDHCP)(nil)\n\n// Lease implements the [client.DHCP] interface for *testDHCP.\nfunc (t *testDHCP) Leases() (leases []*dhcpsvc.Lease) { return t.OnLeases() }\n\n// HostByIP implements the [client.DHCP] interface for *testDHCP.\nfunc (t *testDHCP) HostByIP(ip netip.Addr) (host string) { return t.OnHostBy(ip) }\n\n// MACByIP implements the [client.DHCP] interface for *testDHCP.\nfunc (t *testDHCP) MACByIP(ip netip.Addr) (mac net.HardwareAddr) { return t.OnMACBy(ip) }\n\n// compareRuntimeInfo is a helper function that returns true if the runtime\n// client has provided info.\nfunc compareRuntimeInfo(rc *client.Runtime, src client.Source, host string) (ok bool) {\n\ts, h := rc.Info()\n\tif s != src {\n\t\treturn false\n\t} else if h != host {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc TestStorage_Add_hostsfile(t *testing.T) {\n\tvar (\n\t\tcliIP1   = netip.MustParseAddr(\"1.1.1.1\")\n\t\tcliName1 = \"client_one\"\n\n\t\tcliIP2   = netip.MustParseAddr(\"2.2.2.2\")\n\t\tcliName2 = \"client_two\"\n\t)\n\n\thostCh := make(chan *hostsfile.DefaultStorage)\n\th := &testHostsContainer{\n\t\tonUpd: func() (updates <-chan *hostsfile.DefaultStorage) { return hostCh },\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tstorage, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger:             testLogger,\n\t\tLogger:                 testLogger,\n\t\tDHCP:                   client.EmptyDHCP{},\n\t\tEtcHosts:               h,\n\t\tARPClientsUpdatePeriod: testTimeout / 10,\n\t})\n\trequire.NoError(t, err)\n\n\tservicetest.RequireRun(t, storage, testTimeout)\n\n\tt.Run(\"add_hosts\", func(t *testing.T) {\n\t\tvar s *hostsfile.DefaultStorage\n\t\ts, err = hostsfile.NewDefaultStorage(ctx, &hostsfile.DefaultStorageConfig{\n\t\t\tLogger: testLogger,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\ts.Add(ctx, &hostsfile.Record{\n\t\t\tAddr:  cliIP1,\n\t\t\tNames: []string{cliName1},\n\t\t})\n\n\t\ttestutil.RequireSend(t, hostCh, s, testTimeout)\n\n\t\trequire.EventuallyWithT(t, func(ct *assert.CollectT) {\n\t\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\t\trequire.NotNil(ct, cli1)\n\n\t\t\tassert.True(ct, compareRuntimeInfo(cli1, client.SourceHostsFile, cliName1))\n\t\t}, testTimeout, testTimeout/10)\n\t})\n\n\tt.Run(\"update_hosts\", func(t *testing.T) {\n\t\tvar s *hostsfile.DefaultStorage\n\t\ts, err = hostsfile.NewDefaultStorage(ctx, &hostsfile.DefaultStorageConfig{\n\t\t\tLogger: testLogger,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\ts.Add(ctx, &hostsfile.Record{\n\t\t\tAddr:  cliIP2,\n\t\t\tNames: []string{cliName2},\n\t\t})\n\n\t\ttestutil.RequireSend(t, hostCh, s, testTimeout)\n\n\t\trequire.EventuallyWithT(t, func(ct *assert.CollectT) {\n\t\t\tcli2 := storage.ClientRuntime(cliIP2)\n\t\t\trequire.NotNil(ct, cli2)\n\n\t\t\tassert.True(ct, compareRuntimeInfo(cli2, client.SourceHostsFile, cliName2))\n\n\t\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\t\trequire.Nil(ct, cli1)\n\t\t}, testTimeout, testTimeout/10)\n\t})\n}\n\nfunc TestStorage_Add_arp(t *testing.T) {\n\tvar (\n\t\tmu        sync.Mutex\n\t\tneighbors []arpdb.Neighbor\n\n\t\tcliIP1   = netip.MustParseAddr(\"1.1.1.1\")\n\t\tcliName1 = \"client_one\"\n\n\t\tcliIP2   = netip.MustParseAddr(\"2.2.2.2\")\n\t\tcliName2 = \"client_two\"\n\t)\n\n\ta := &testARPDB{\n\t\tonRefresh: func(_ context.Context) (err error) { return nil },\n\t\tonNeighbors: func() (ns []arpdb.Neighbor) {\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\n\t\t\treturn neighbors\n\t\t},\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tstorage, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger:             testLogger,\n\t\tLogger:                 testLogger,\n\t\tDHCP:                   client.EmptyDHCP{},\n\t\tARPDB:                  a,\n\t\tARPClientsUpdatePeriod: testTimeout / 10,\n\t})\n\trequire.NoError(t, err)\n\n\tservicetest.RequireRun(t, storage, testTimeout)\n\n\tt.Run(\"add_hosts\", func(t *testing.T) {\n\t\tfunc() {\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\n\t\t\tneighbors = []arpdb.Neighbor{{\n\t\t\t\tName: cliName1,\n\t\t\t\tIP:   cliIP1,\n\t\t\t}}\n\t\t}()\n\n\t\trequire.EventuallyWithT(t, func(ct *assert.CollectT) {\n\t\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\t\trequire.NotNil(ct, cli1)\n\n\t\t\tassert.True(ct, compareRuntimeInfo(cli1, client.SourceARP, cliName1))\n\t\t}, testTimeout, testTimeout/10)\n\t})\n\n\tt.Run(\"update_hosts\", func(t *testing.T) {\n\t\tfunc() {\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\n\t\t\tneighbors = []arpdb.Neighbor{{\n\t\t\t\tName: cliName2,\n\t\t\t\tIP:   cliIP2,\n\t\t\t}}\n\t\t}()\n\n\t\trequire.EventuallyWithT(t, func(ct *assert.CollectT) {\n\t\t\tcli2 := storage.ClientRuntime(cliIP2)\n\t\t\trequire.NotNil(ct, cli2)\n\n\t\t\tassert.True(ct, compareRuntimeInfo(cli2, client.SourceARP, cliName2))\n\n\t\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\t\trequire.Nil(ct, cli1)\n\t\t}, testTimeout, testTimeout/10)\n\t})\n}\n\nfunc TestStorage_Add_whois(t *testing.T) {\n\tvar (\n\t\tcliIP1 = netip.MustParseAddr(\"1.1.1.1\")\n\n\t\tcliIP2   = netip.MustParseAddr(\"2.2.2.2\")\n\t\tcliName2 = \"client_two\"\n\n\t\tcliIP3   = netip.MustParseAddr(\"3.3.3.3\")\n\t\tcliName3 = \"client_three\"\n\t)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tstorage, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger: testLogger,\n\t\tLogger:     testLogger,\n\t\tDHCP:       client.EmptyDHCP{},\n\t})\n\trequire.NoError(t, err)\n\n\twhois := &whois.Info{\n\t\tCountry: \"AU\",\n\t\tOrgname: \"Example Org\",\n\t}\n\n\tt.Run(\"new_client\", func(t *testing.T) {\n\t\tstorage.UpdateAddress(ctx, cliIP1, \"\", whois)\n\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\trequire.NotNil(t, cli1)\n\n\t\tassert.Equal(t, whois, cli1.WHOIS())\n\t})\n\n\tt.Run(\"existing_runtime_client\", func(t *testing.T) {\n\t\tstorage.UpdateAddress(ctx, cliIP2, cliName2, nil)\n\t\tstorage.UpdateAddress(ctx, cliIP2, \"\", whois)\n\n\t\tcli2 := storage.ClientRuntime(cliIP2)\n\t\trequire.NotNil(t, cli2)\n\n\t\tassert.True(t, compareRuntimeInfo(cli2, client.SourceRDNS, cliName2))\n\n\t\tassert.Equal(t, whois, cli2.WHOIS())\n\t})\n\n\tt.Run(\"can't_set_persistent_client\", func(t *testing.T) {\n\t\terr = storage.Add(ctx, &client.Persistent{\n\t\t\tName: cliName3,\n\t\t\tUID:  client.MustNewUID(),\n\t\t\tIPs:  []netip.Addr{cliIP3},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tstorage.UpdateAddress(ctx, cliIP3, \"\", whois)\n\t\trc := storage.ClientRuntime(cliIP3)\n\t\trequire.Nil(t, rc)\n\t})\n}\n\nfunc TestClientsDHCP(t *testing.T) {\n\tvar (\n\t\tcliIP1   = netip.MustParseAddr(\"1.1.1.1\")\n\t\tcliName1 = \"one.dhcp\"\n\n\t\tcliIP2   = netip.MustParseAddr(\"2.2.2.2\")\n\t\tcliMAC2  = errors.Must(net.ParseMAC(\"22:22:22:22:22:22\"))\n\t\tcliName2 = \"two.dhcp\"\n\n\t\tcliIP3   = netip.MustParseAddr(\"3.3.3.3\")\n\t\tcliMAC3  = errors.Must(net.ParseMAC(\"33:33:33:33:33:33\"))\n\t\tcliName3 = \"three.dhcp\"\n\n\t\tprsCliIP   = netip.MustParseAddr(\"4.3.2.1\")\n\t\tprsCliMAC  = errors.Must(net.ParseMAC(\"AA:AA:AA:AA:AA:AA\"))\n\t\tprsCliName = \"persistent.dhcp\"\n\n\t\totherARPCliName = \"other.arp\"\n\t\totherARPCliIP   = netip.MustParseAddr(\"192.0.2.1\")\n\t)\n\n\tipToHost := map[netip.Addr]string{\n\t\tcliIP1: cliName1,\n\t}\n\tipToMAC := map[netip.Addr]net.HardwareAddr{\n\t\tprsCliIP: prsCliMAC,\n\t}\n\n\tleases := []*dhcpsvc.Lease{{\n\t\tIP:       cliIP2,\n\t\tHostname: cliName2,\n\t\tHWAddr:   cliMAC2,\n\t}, {\n\t\tIP:       cliIP3,\n\t\tHostname: cliName3,\n\t\tHWAddr:   cliMAC3,\n\t}}\n\n\tarpCh := make(chan []arpdb.Neighbor, 1)\n\tarpDB := &testARPDB{\n\t\tonRefresh: func(_ context.Context) (err error) { return nil },\n\t\tonNeighbors: func() (ns []arpdb.Neighbor) {\n\t\t\tselect {\n\t\t\tcase ns = <-arpCh:\n\t\t\t\treturn ns\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t},\n\t}\n\n\tdhcp := &testDHCP{\n\t\tOnLeases: func() (ls []*dhcpsvc.Lease) {\n\t\t\treturn leases\n\t\t},\n\t\tOnHostBy: func(ip netip.Addr) (host string) {\n\t\t\treturn ipToHost[ip]\n\t\t},\n\t\tOnMACBy: func(ip netip.Addr) (mac net.HardwareAddr) {\n\t\t\treturn ipToMAC[ip]\n\t\t},\n\t}\n\n\tetcHostsCh := make(chan *hostsfile.DefaultStorage, 1)\n\tetcHosts := &testHostsContainer{\n\t\tonUpd: func() (updates <-chan *hostsfile.DefaultStorage) {\n\t\t\treturn etcHostsCh\n\t\t},\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tstorage, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger:             testLogger,\n\t\tLogger:                 testLogger,\n\t\tARPDB:                  arpDB,\n\t\tDHCP:                   dhcp,\n\t\tEtcHosts:               etcHosts,\n\t\tRuntimeSourceDHCP:      true,\n\t\tARPClientsUpdatePeriod: testTimeout / 10,\n\t})\n\trequire.NoError(t, err)\n\n\tservicetest.RequireRun(t, storage, testTimeout)\n\n\trequire.True(t, t.Run(\"find_runtime_lower_priority\", func(t *testing.T) {\n\t\t// Add a lower-priority client.\n\t\tns := []arpdb.Neighbor{{\n\t\t\tName: cliName1,\n\t\t\tIP:   cliIP1,\n\t\t}}\n\n\t\ttestutil.RequireSend(t, arpCh, ns, testTimeout)\n\n\t\tstorage.ReloadARP(testutil.ContextWithTimeout(t, testTimeout))\n\n\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\trequire.NotNil(t, cli1)\n\n\t\tassert.True(t, compareRuntimeInfo(cli1, client.SourceDHCP, cliName1))\n\n\t\t// Remove the matching client.\n\t\t//\n\t\t// TODO(a.garipov):  Consider adding ways of explicitly clearing runtime\n\t\t// sources by source.\n\t\tns = []arpdb.Neighbor{{\n\t\t\tName: otherARPCliName,\n\t\t\tIP:   otherARPCliIP,\n\t\t}}\n\n\t\ttestutil.RequireSend(t, arpCh, ns, testTimeout)\n\n\t\tstorage.ReloadARP(testutil.ContextWithTimeout(t, testTimeout))\n\t}))\n\n\trequire.True(t, t.Run(\"find_runtime\", func(t *testing.T) {\n\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\trequire.NotNil(t, cli1)\n\n\t\tassert.True(t, compareRuntimeInfo(cli1, client.SourceDHCP, cliName1))\n\t}))\n\n\trequire.True(t, t.Run(\"find_runtime_higher_priority\", func(t *testing.T) {\n\t\t// Add a higher-priority client.\n\t\ts, strgErr := hostsfile.NewDefaultStorage(ctx, &hostsfile.DefaultStorageConfig{\n\t\t\tLogger: testLogger,\n\t\t})\n\t\trequire.NoError(t, strgErr)\n\n\t\ts.Add(ctx, &hostsfile.Record{\n\t\t\tAddr:  cliIP1,\n\t\t\tNames: []string{cliName1},\n\t\t})\n\n\t\ttestutil.RequireSend(t, etcHostsCh, s, testTimeout)\n\n\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\trequire.NotNil(t, cli1)\n\n\t\trequire.EventuallyWithT(t, func(ct *assert.CollectT) {\n\t\t\tcli := storage.ClientRuntime(cliIP1)\n\t\t\trequire.NotNil(ct, cli)\n\n\t\t\tassert.True(ct, compareRuntimeInfo(cli, client.SourceHostsFile, cliName1))\n\t\t}, testTimeout, testTimeout/10)\n\n\t\t// Remove the matching client.\n\t\t//\n\t\t// TODO(a.garipov):  Consider adding ways of explicitly clearing runtime\n\t\t// sources by source.\n\t\ts, strgErr = hostsfile.NewDefaultStorage(ctx, &hostsfile.DefaultStorageConfig{\n\t\t\tLogger: testLogger,\n\t\t})\n\t\trequire.NoError(t, strgErr)\n\n\t\ttestutil.RequireSend(t, etcHostsCh, s, testTimeout)\n\n\t\trequire.EventuallyWithT(t, func(ct *assert.CollectT) {\n\t\t\tcli := storage.ClientRuntime(cliIP1)\n\t\t\trequire.NotNil(ct, cli)\n\n\t\t\tassert.True(ct, compareRuntimeInfo(cli, client.SourceDHCP, cliName1))\n\t\t}, testTimeout, testTimeout/10)\n\t}))\n\n\trequire.True(t, t.Run(\"find_persistent\", func(t *testing.T) {\n\t\terr = storage.Add(ctx, &client.Persistent{\n\t\t\tName: prsCliName,\n\t\t\tUID:  client.MustNewUID(),\n\t\t\tMACs: []net.HardwareAddr{prsCliMAC},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tparams := &client.FindParams{}\n\t\terr = params.Set(prsCliIP.String())\n\t\trequire.NoError(t, err)\n\n\t\tprsCli, ok := storage.Find(params)\n\t\trequire.True(t, ok)\n\n\t\tassert.Equal(t, prsCliName, prsCli.Name)\n\t}))\n\n\trequire.True(t, t.Run(\"leases\", func(t *testing.T) {\n\t\tdelete(ipToHost, cliIP1)\n\t\tstorage.UpdateDHCP(ctx)\n\n\t\tcli1 := storage.ClientRuntime(cliIP1)\n\t\trequire.Nil(t, cli1)\n\n\t\tfor i, l := range leases {\n\t\t\tcli := storage.ClientRuntime(l.IP)\n\t\t\trequire.NotNil(t, cli)\n\n\t\t\tsrc, host := cli.Info()\n\t\t\tassert.Equal(t, client.SourceDHCP, src)\n\t\t\tassert.Equal(t, leases[i].Hostname, host)\n\t\t}\n\t}))\n\n\trequire.True(t, t.Run(\"range\", func(t *testing.T) {\n\t\ts := 0\n\t\tstorage.RangeRuntime(func(rc *client.Runtime) (cont bool) {\n\t\t\tif src, _ := rc.Info(); src == client.SourceDHCP {\n\t\t\t\ts++\n\t\t\t}\n\n\t\t\treturn true\n\t\t})\n\n\t\tassert.Equal(t, len(leases), s)\n\t}))\n}\n\nfunc TestClientsAddExisting(t *testing.T) {\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tt.Run(\"simple\", func(t *testing.T) {\n\t\tstorage, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\t\tBaseLogger: testLogger,\n\t\t\tLogger:     testLogger,\n\t\t\tDHCP:       client.EmptyDHCP{},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tip := netip.MustParseAddr(\"1.1.1.1\")\n\n\t\t// Add a client.\n\t\terr = storage.Add(ctx, &client.Persistent{\n\t\t\tName:    \"client1\",\n\t\t\tUID:     client.MustNewUID(),\n\t\t\tIPs:     []netip.Addr{ip, netip.MustParseAddr(\"1:2:3::4\")},\n\t\t\tSubnets: []netip.Prefix{netip.MustParsePrefix(\"2.2.2.0/24\")},\n\t\t\tMACs:    []net.HardwareAddr{{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Now add an auto-client with the same IP.\n\t\tstorage.UpdateAddress(ctx, ip, \"test\", nil)\n\t\trc := storage.ClientRuntime(ip)\n\t\tassert.True(t, compareRuntimeInfo(rc, client.SourceRDNS, \"test\"))\n\t})\n\n\tt.Run(\"complicated\", func(t *testing.T) {\n\t\t// TODO(a.garipov): Properly decouple the DHCP server from the client\n\t\t// storage.\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tt.Skip(\"skipping dhcp test on windows\")\n\t\t}\n\n\t\t// First, init a DHCP server with a single static lease.\n\t\tconfig := &dhcpd.ServerConfig{\n\t\t\tLogger:  testLogger,\n\t\t\tEnabled: true,\n\t\t\tDataDir: t.TempDir(),\n\t\t\tConf4: dhcpd.V4ServerConf{\n\t\t\t\tEnabled:    true,\n\t\t\t\tGatewayIP:  netip.MustParseAddr(\"1.2.3.1\"),\n\t\t\t\tSubnetMask: netip.MustParseAddr(\"255.255.255.0\"),\n\t\t\t\tRangeStart: netip.MustParseAddr(\"1.2.3.2\"),\n\t\t\t\tRangeEnd:   netip.MustParseAddr(\"1.2.3.10\"),\n\t\t\t},\n\t\t}\n\n\t\tctx = testutil.ContextWithTimeout(t, testTimeout)\n\t\tdhcpServer, err := dhcpd.Create(ctx, config)\n\t\trequire.NoError(t, err)\n\n\t\tstorage, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\t\tBaseLogger: testLogger,\n\t\t\tLogger:     testLogger,\n\t\t\tDHCP:       dhcpServer,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tip := netip.MustParseAddr(\"1.2.3.4\")\n\n\t\terr = dhcpServer.AddStaticLease(&dhcpsvc.Lease{\n\t\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\t\tIP:       ip,\n\t\t\tHostname: \"testhost\",\n\t\t\tExpiry:   time.Now().Add(time.Hour),\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Add a new client with the same IP as for a client with MAC.\n\t\terr = storage.Add(ctx, &client.Persistent{\n\t\t\tName: \"client2\",\n\t\t\tUID:  client.MustNewUID(),\n\t\t\tIPs:  []netip.Addr{ip},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Add a new client with the IP from the first client's IP range.\n\t\terr = storage.Add(ctx, &client.Persistent{\n\t\t\tName: \"client3\",\n\t\t\tUID:  client.MustNewUID(),\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"2.2.2.2\")},\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n}\n\n// newStorage is a helper function that returns a client storage filled with\n// persistent clients from the m.  It also generates a UID for each client.\nfunc newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {\n\ttb.Helper()\n\n\tctx := testutil.ContextWithTimeout(tb, testTimeout)\n\ts, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger: testLogger,\n\t\tLogger:     testLogger,\n\t\tDHCP:       client.EmptyDHCP{},\n\t})\n\trequire.NoError(tb, err)\n\n\tfor _, c := range m {\n\t\tc.UID = client.MustNewUID()\n\t\trequire.NoError(tb, s.Add(ctx, c))\n\t}\n\n\trequire.Equal(tb, len(m), s.Size())\n\n\treturn s\n}\n\nfunc TestStorage_Add(t *testing.T) {\n\tconst (\n\t\texistingName     = \"existing_name\"\n\t\texistingClientID = \"existing_client_id\"\n\n\t\tallowedTag    = \"user_admin\"\n\t\tnotAllowedTag = \"not_allowed_tag\"\n\t)\n\n\tvar (\n\t\texistingClientUID = client.MustNewUID()\n\t\texistingIP        = netip.MustParseAddr(\"1.2.3.4\")\n\t\texistingSubnet    = netip.MustParsePrefix(\"1.2.3.0/24\")\n\t)\n\n\texistingClient := &client.Persistent{\n\t\tName:      existingName,\n\t\tIPs:       []netip.Addr{existingIP},\n\t\tSubnets:   []netip.Prefix{existingSubnet},\n\t\tClientIDs: []client.ClientID{existingClientID},\n\t\tUID:       existingClientUID,\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\ts := newTestStorage(t, timeutil.SystemClock{})\n\ttags := s.AllowedTags()\n\trequire.NotZero(t, len(tags))\n\trequire.True(t, slices.IsSorted(tags))\n\n\t_, ok := slices.BinarySearch(tags, allowedTag)\n\trequire.True(t, ok)\n\n\t_, ok = slices.BinarySearch(tags, notAllowedTag)\n\trequire.False(t, ok)\n\n\terr := s.Add(ctx, existingClient)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tcli        *client.Persistent\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"basic\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"basic\",\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"1.1.1.1\")},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"duplicate_uid\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"no_uid\",\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"2.2.2.2\")},\n\t\t\tUID:  existingClientUID,\n\t\t},\n\t\twantErrMsg: `adding client: another client \"existing_name\" uses the same uid`,\n\t}, {\n\t\tname: \"duplicate_name\",\n\t\tcli: &client.Persistent{\n\t\t\tName: existingName,\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"3.3.3.3\")},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `adding client: another client uses the same name \"existing_name\"`,\n\t}, {\n\t\tname: \"duplicate_ip\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"duplicate_ip\",\n\t\t\tIPs:  []netip.Addr{existingIP},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `adding client: another client \"existing_name\" uses the same IP \"1.2.3.4\"`,\n\t}, {\n\t\tname: \"duplicate_subnet\",\n\t\tcli: &client.Persistent{\n\t\t\tName:    \"duplicate_subnet\",\n\t\t\tSubnets: []netip.Prefix{existingSubnet},\n\t\t\tUID:     client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `adding client: another client \"existing_name\" ` +\n\t\t\t`uses the same subnet \"1.2.3.0/24\"`,\n\t}, {\n\t\tname: \"duplicate_client_id\",\n\t\tcli: &client.Persistent{\n\t\t\tName:      \"duplicate_client_id\",\n\t\t\tClientIDs: []client.ClientID{existingClientID},\n\t\t\tUID:       client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `adding client: another client \"existing_name\" ` +\n\t\t\t`uses the same ClientID \"existing_client_id\"`,\n\t}, {\n\t\tname: \"not_allowed_tag\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"not_allowed_tag\",\n\t\t\tTags: []string{notAllowedTag},\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"4.4.4.4\")},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `adding client: invalid tag: \"not_allowed_tag\"`,\n\t}, {\n\t\tname: \"allowed_tag\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"allowed_tag\",\n\t\t\tTags: []string{allowedTag},\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"5.5.5.5\")},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"\",\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"6.6.6.6\")},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: \"adding client: empty name\",\n\t}, {\n\t\tname: \"no_id\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"no_id\",\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: \"adding client: id required\",\n\t}, {\n\t\tname: \"no_uid\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"no_uid\",\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"7.7.7.7\")},\n\t\t},\n\t\twantErrMsg: \"adding client: uid required\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr = s.Add(ctx, tc.cli)\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\nfunc TestStorage_RemoveByName(t *testing.T) {\n\tconst (\n\t\texistingName = \"existing_name\"\n\t)\n\n\texistingClient := &client.Persistent{\n\t\tName: existingName,\n\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"1.2.3.4\")},\n\t\tUID:  client.MustNewUID(),\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\ts := newTestStorage(t, timeutil.SystemClock{})\n\terr := s.Add(ctx, existingClient)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\twant    assert.BoolAssertionFunc\n\t\tname    string\n\t\tcliName string\n\t}{{\n\t\tname:    \"existing_client\",\n\t\tcliName: existingName,\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"non_existing_client\",\n\t\tcliName: \"non_existing_client\",\n\t\twant:    assert.False,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttc.want(t, s.RemoveByName(ctx, tc.cliName))\n\t\t})\n\t}\n\n\tt.Run(\"duplicate_remove\", func(t *testing.T) {\n\t\ts = newTestStorage(t, timeutil.SystemClock{})\n\t\terr = s.Add(ctx, existingClient)\n\t\trequire.NoError(t, err)\n\n\t\tassert.True(t, s.RemoveByName(ctx, existingName))\n\t\tassert.False(t, s.RemoveByName(ctx, existingName))\n\t})\n}\n\nfunc TestStorage_Find(t *testing.T) {\n\tconst (\n\t\tcliIPNone = \"1.2.3.4\"\n\t\tcliIP1    = \"1.1.1.1\"\n\t\tcliIP2    = \"2.2.2.2\"\n\n\t\tcliIPv6 = \"1:2:3::4\"\n\n\t\tcliSubnet   = \"2.2.2.0/24\"\n\t\tcliSubnetIP = \"2.2.2.222\"\n\n\t\tcliID  = \"client-id\"\n\t\tcliMAC = \"11:11:11:11:11:11\"\n\n\t\tlinkLocalIP     = \"fe80::abcd:abcd:abcd:ab%eth0\"\n\t\tlinkLocalSubnet = \"fe80::/16\"\n\t)\n\n\tvar (\n\t\tclientWithBothFams = &client.Persistent{\n\t\t\tName: \"client1\",\n\t\t\tIPs: []netip.Addr{\n\t\t\t\tnetip.MustParseAddr(cliIP1),\n\t\t\t\tnetip.MustParseAddr(cliIPv6),\n\t\t\t},\n\t\t}\n\n\t\tclientWithSubnet = &client.Persistent{\n\t\t\tName:    \"client2\",\n\t\t\tIPs:     []netip.Addr{netip.MustParseAddr(cliIP2)},\n\t\t\tSubnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},\n\t\t}\n\n\t\tclientWithMAC = &client.Persistent{\n\t\t\tName: \"client_with_mac\",\n\t\t\tMACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},\n\t\t}\n\n\t\tclientWithID = &client.Persistent{\n\t\t\tName:      \"client_with_id\",\n\t\t\tClientIDs: []client.ClientID{cliID},\n\t\t}\n\n\t\tclientLinkLocal = &client.Persistent{\n\t\t\tName:    \"client_link_local\",\n\t\t\tSubnets: []netip.Prefix{netip.MustParsePrefix(linkLocalSubnet)},\n\t\t}\n\t)\n\n\tclients := []*client.Persistent{\n\t\tclientWithBothFams,\n\t\tclientWithSubnet,\n\t\tclientWithMAC,\n\t\tclientWithID,\n\t\tclientLinkLocal,\n\t}\n\ts := newStorage(t, clients)\n\n\ttestCases := []struct {\n\t\twant *client.Persistent\n\t\tname string\n\t\tids  []string\n\t}{{\n\t\tname: \"ipv4_ipv6\",\n\t\tids:  []string{cliIP1, cliIPv6},\n\t\twant: clientWithBothFams,\n\t}, {\n\t\tname: \"ipv4_subnet\",\n\t\tids:  []string{cliIP2, cliSubnetIP},\n\t\twant: clientWithSubnet,\n\t}, {\n\t\tname: \"mac\",\n\t\tids:  []string{cliMAC},\n\t\twant: clientWithMAC,\n\t}, {\n\t\tname: \"client_id\",\n\t\tids:  []string{cliID},\n\t\twant: clientWithID,\n\t}, {\n\t\tname: \"client_link_local_subnet\",\n\t\tids:  []string{linkLocalIP},\n\t\twant: clientLinkLocal,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tfor _, id := range tc.ids {\n\t\t\t\tparams := &client.FindParams{}\n\t\t\t\terr := params.Set(id)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tc, ok := s.Find(params)\n\t\t\t\trequire.True(t, ok)\n\n\t\t\t\tassert.Equal(t, tc.want, c)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"not_found\", func(t *testing.T) {\n\t\tparams := &client.FindParams{}\n\t\terr := params.Set(cliIPNone)\n\t\trequire.NoError(t, err)\n\n\t\t_, ok := s.Find(params)\n\t\tassert.False(t, ok)\n\t})\n}\n\nfunc TestStorage_FindLoose(t *testing.T) {\n\tconst (\n\t\tnonExistingClientID = \"client_id\"\n\t)\n\n\tvar (\n\t\tip         = netip.MustParseAddr(\"fe80::a098:7654:32ef:ff1\")\n\t\tipWithZone = netip.MustParseAddr(\"fe80::1ff:fe23:4567:890a%eth2\")\n\t)\n\n\tvar (\n\t\tclientNoZone = &client.Persistent{\n\t\t\tName: \"client\",\n\t\t\tIPs:  []netip.Addr{ip},\n\t\t}\n\n\t\tclientWithZone = &client.Persistent{\n\t\t\tName: \"client_with_zone\",\n\t\t\tIPs:  []netip.Addr{ipWithZone},\n\t\t}\n\t)\n\n\ts := newStorage(\n\t\tt,\n\t\t[]*client.Persistent{\n\t\t\tclientNoZone,\n\t\t\tclientWithZone,\n\t\t},\n\t)\n\n\ttestCases := []struct {\n\t\tip      netip.Addr\n\t\twant    assert.BoolAssertionFunc\n\t\twantCli *client.Persistent\n\t\tname    string\n\t}{{\n\t\tname:    \"without_zone\",\n\t\tip:      ip,\n\t\twantCli: clientNoZone,\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"with_zone\",\n\t\tip:      ipWithZone,\n\t\twantCli: clientWithZone,\n\t\twant:    assert.True,\n\t}, {\n\t\tname:    \"zero_address\",\n\t\tip:      netip.Addr{},\n\t\twantCli: nil,\n\t\twant:    assert.False,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc, ok := s.FindLoose(tc.ip.WithZone(\"\"), nonExistingClientID)\n\t\t\tassert.Equal(t, tc.wantCli, c)\n\t\t\ttc.want(t, ok)\n\t\t})\n\t}\n}\n\nfunc TestStorage_Update(t *testing.T) {\n\tconst (\n\t\tclientName          = \"client_name\"\n\t\tobstructingName     = \"obstructing_name\"\n\t\tobstructingClientID = \"obstructing_client_id\"\n\t)\n\n\tvar (\n\t\tobstructingIP     = netip.MustParseAddr(\"1.2.3.4\")\n\t\tobstructingSubnet = netip.MustParsePrefix(\"1.2.3.0/24\")\n\t)\n\n\tobstructingClient := &client.Persistent{\n\t\tName:      obstructingName,\n\t\tIPs:       []netip.Addr{obstructingIP},\n\t\tSubnets:   []netip.Prefix{obstructingSubnet},\n\t\tClientIDs: []client.ClientID{obstructingClientID},\n\t}\n\n\tclientToUpdate := &client.Persistent{\n\t\tName: clientName,\n\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"1.1.1.1\")},\n\t}\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tcli        *client.Persistent\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"basic\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"basic\",\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"1.1.1.1\")},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"duplicate_name\",\n\t\tcli: &client.Persistent{\n\t\t\tName: obstructingName,\n\t\t\tIPs:  []netip.Addr{netip.MustParseAddr(\"3.3.3.3\")},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `updating client: another client uses the same name \"obstructing_name\"`,\n\t}, {\n\t\tname: \"duplicate_ip\",\n\t\tcli: &client.Persistent{\n\t\t\tName: \"duplicate_ip\",\n\t\t\tIPs:  []netip.Addr{obstructingIP},\n\t\t\tUID:  client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `updating client: another client \"obstructing_name\" uses the same IP \"1.2.3.4\"`,\n\t}, {\n\t\tname: \"duplicate_subnet\",\n\t\tcli: &client.Persistent{\n\t\t\tName:    \"duplicate_subnet\",\n\t\t\tSubnets: []netip.Prefix{obstructingSubnet},\n\t\t\tUID:     client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `updating client: another client \"obstructing_name\" ` +\n\t\t\t`uses the same subnet \"1.2.3.0/24\"`,\n\t}, {\n\t\tname: \"duplicate_client_id\",\n\t\tcli: &client.Persistent{\n\t\t\tName:      \"duplicate_client_id\",\n\t\t\tClientIDs: []client.ClientID{obstructingClientID},\n\t\t\tUID:       client.MustNewUID(),\n\t\t},\n\t\twantErrMsg: `updating client: another client \"obstructing_name\" ` +\n\t\t\t`uses the same ClientID \"obstructing_client_id\"`,\n\t}}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := newStorage(\n\t\t\t\tt,\n\t\t\t\t[]*client.Persistent{\n\t\t\t\t\tclientToUpdate,\n\t\t\t\t\tobstructingClient,\n\t\t\t\t},\n\t\t\t)\n\n\t\t\terr := s.Update(ctx, clientName, tc.cli)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\nfunc TestStorage_RangeByName(t *testing.T) {\n\tsortedClients := []*client.Persistent{{\n\t\tName:      \"clientA\",\n\t\tClientIDs: []client.ClientID{\"A\"},\n\t}, {\n\t\tName:      \"clientB\",\n\t\tClientIDs: []client.ClientID{\"B\"},\n\t}, {\n\t\tName:      \"clientC\",\n\t\tClientIDs: []client.ClientID{\"C\"},\n\t}, {\n\t\tName:      \"clientD\",\n\t\tClientIDs: []client.ClientID{\"D\"},\n\t}, {\n\t\tName:      \"clientE\",\n\t\tClientIDs: []client.ClientID{\"E\"},\n\t}}\n\n\ttestCases := []struct {\n\t\tname string\n\t\twant []*client.Persistent\n\t}{{\n\t\tname: \"basic\",\n\t\twant: sortedClients,\n\t}, {\n\t\tname: \"nil\",\n\t\twant: nil,\n\t}, {\n\t\tname: \"one_element\",\n\t\twant: sortedClients[:1],\n\t}, {\n\t\tname: \"two_elements\",\n\t\twant: sortedClients[:2],\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := newStorage(t, tc.want)\n\n\t\t\tvar got []*client.Persistent\n\t\t\ts.RangeByName(func(c *client.Persistent) (cont bool) {\n\t\t\t\tgot = append(got, c)\n\n\t\t\t\treturn true\n\t\t\t})\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc TestStorage_CustomUpstreamConfig(t *testing.T) {\n\tconst (\n\t\texistingClientID    = \"existing_client_id\"\n\t\tnonExistingClientID = \"non_existing_client_id\"\n\t)\n\n\tvar (\n\t\texistingIP    = netip.MustParseAddr(\"192.0.2.1\")\n\t\tnonExistingIP = netip.MustParseAddr(\"192.0.2.255\")\n\n\t\tdhcpCliIP  = netip.MustParseAddr(\"192.0.2.2\")\n\t\tdhcpCliMAC = errors.Must(net.ParseMAC(\"02:00:00:00:00:00\"))\n\n\t\ttestUpstreamTimeout = time.Second\n\t)\n\n\tdate := time.Now()\n\tclock := &faketime.Clock{\n\t\tOnNow: func() (now time.Time) {\n\t\t\tdate = date.Add(time.Second)\n\n\t\t\treturn date\n\t\t},\n\t}\n\n\tipToMAC := map[netip.Addr]net.HardwareAddr{\n\t\tdhcpCliIP: dhcpCliMAC,\n\t}\n\n\tdhcp := &testDHCP{\n\t\tOnLeases: func() (_ []*dhcpsvc.Lease) { panic(testutil.UnexpectedCall()) },\n\t\tOnHostBy: func(ip netip.Addr) (_ string) { panic(testutil.UnexpectedCall(ip)) },\n\t\tOnMACBy: func(ip netip.Addr) (mac net.HardwareAddr) {\n\t\t\treturn ipToMAC[ip]\n\t\t},\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\ts, err := client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger: testLogger,\n\t\tLogger:     testLogger,\n\t\tClock:      clock,\n\t\tDHCP:       dhcp,\n\t})\n\trequire.NoError(t, err)\n\n\ts.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{\n\t\tUpstreamTimeout: testUpstreamTimeout,\n\t})\n\n\ttestutil.CleanupAndRequireSuccess(t, func() (err error) {\n\t\treturn s.Shutdown(testutil.ContextWithTimeout(t, testTimeout))\n\t})\n\n\terr = s.Add(ctx, &client.Persistent{\n\t\tName:      \"client_first\",\n\t\tIPs:       []netip.Addr{existingIP},\n\t\tClientIDs: []client.ClientID{existingClientID},\n\t\tUID:       client.MustNewUID(),\n\t\tUpstreams: []string{\"192.0.2.0\"},\n\t})\n\trequire.NoError(t, err)\n\n\terr = s.Add(ctx, &client.Persistent{\n\t\tName:      \"client_second\",\n\t\tMACs:      []net.HardwareAddr{dhcpCliMAC},\n\t\tUID:       client.MustNewUID(),\n\t\tUpstreams: []string{\"192.0.2.0\"},\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tcliAddr     netip.Addr\n\t\twantNilConf assert.ValueAssertionFunc\n\t\tname        string\n\t\tcliID       string\n\t}{{\n\t\tname:        \"client_id\",\n\t\tcliID:       existingClientID,\n\t\tcliAddr:     netip.Addr{},\n\t\twantNilConf: assert.NotNil,\n\t}, {\n\t\tname:        \"client_addr\",\n\t\tcliID:       \"\",\n\t\tcliAddr:     existingIP,\n\t\twantNilConf: assert.NotNil,\n\t}, {\n\t\tname:        \"client_dhcp\",\n\t\tcliID:       \"\",\n\t\tcliAddr:     dhcpCliIP,\n\t\twantNilConf: assert.NotNil,\n\t}, {\n\t\tname:        \"non_existing_client_id\",\n\t\tcliID:       nonExistingClientID,\n\t\tcliAddr:     netip.Addr{},\n\t\twantNilConf: assert.Nil,\n\t}, {\n\t\tname:        \"non_existing_client_addr\",\n\t\tcliID:       \"\",\n\t\tcliAddr:     nonExistingIP,\n\t\twantNilConf: assert.Nil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tconf := s.CustomUpstreamConfig(tc.cliID, tc.cliAddr)\n\t\t\ttc.wantNilConf(t, conf)\n\t\t})\n\t}\n\n\tt.Run(\"update_common_config\", func(t *testing.T) {\n\t\tconf := s.CustomUpstreamConfig(existingClientID, existingIP)\n\t\trequire.NotNil(t, conf)\n\n\t\ts.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{\n\t\t\tUpstreamTimeout: testUpstreamTimeout * 2,\n\t\t})\n\n\t\tupdConf := s.CustomUpstreamConfig(existingClientID, existingIP)\n\t\trequire.NotNil(t, updConf)\n\n\t\tassert.NotEqual(t, conf, updConf)\n\t})\n\n\tt.Run(\"same_custom_config\", func(t *testing.T) {\n\t\tfirstConf := s.CustomUpstreamConfig(existingClientID, existingIP)\n\t\trequire.NotNil(t, firstConf)\n\n\t\tsecondConf := s.CustomUpstreamConfig(existingClientID, existingIP)\n\t\trequire.NotNil(t, secondConf)\n\n\t\tassert.Same(t, firstConf, secondConf)\n\t})\n}\n\nfunc BenchmarkFindParams_Set(b *testing.B) {\n\tconst (\n\t\ttestIPStr    = \"192.0.2.1\"\n\t\ttestCIDRStr  = \"192.0.2.0/24\"\n\t\ttestMACStr   = \"02:00:00:00:00:00\"\n\t\ttestClientID = \"clientid\"\n\t)\n\n\tbenchCases := []struct {\n\t\twantErr error\n\t\tparams  *client.FindParams\n\t\tname    string\n\t\tid      string\n\t}{{\n\t\twantErr: nil,\n\t\tparams: &client.FindParams{\n\t\t\tClientID: testClientID,\n\t\t},\n\t\tname: \"client_id\",\n\t\tid:   testClientID,\n\t}, {\n\t\twantErr: nil,\n\t\tparams: &client.FindParams{\n\t\t\tRemoteIP: netip.MustParseAddr(testIPStr),\n\t\t},\n\t\tname: \"ip_address\",\n\t\tid:   testIPStr,\n\t}, {\n\t\twantErr: nil,\n\t\tparams: &client.FindParams{\n\t\t\tSubnet: netip.MustParsePrefix(testCIDRStr),\n\t\t},\n\t\tname: \"subnet\",\n\t\tid:   testCIDRStr,\n\t}, {\n\t\twantErr: nil,\n\t\tparams: &client.FindParams{\n\t\t\tMAC: errors.Must(net.ParseMAC(testMACStr)),\n\t\t},\n\t\tname: \"mac_address\",\n\t\tid:   testMACStr,\n\t}, {\n\t\twantErr: client.ErrBadIdentifier,\n\t\tparams:  &client.FindParams{},\n\t\tname:    \"bad_id\",\n\t\tid:      \"!@#$%^&*()_+\",\n\t}}\n\n\tfor _, bc := range benchCases {\n\t\tb.Run(bc.name, func(b *testing.B) {\n\t\t\tparams := &client.FindParams{}\n\t\t\tvar err error\n\n\t\t\tb.ReportAllocs()\n\t\t\tfor b.Loop() {\n\t\t\t\terr = params.Set(bc.id)\n\t\t\t}\n\n\t\t\tassert.ErrorIs(b, err, bc.wantErr)\n\t\t\tassert.Equal(b, bc.params, params)\n\t\t})\n\t}\n\n\t// Most recent results:\n\t//\n\t//\tgoos: linux\n\t//\tgoarch: amd64\n\t//\tpkg: github.com/AdguardTeam/AdGuardHome/internal/client\n\t//\tcpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz\n\t//\tBenchmarkFindParams_Set/client_id-8         \t49463488\t        24.27 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkFindParams_Set/ip_address-8        \t18740977\t        62.22 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkFindParams_Set/subnet-8            \t10848192\t       110.0 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkFindParams_Set/mac_address-8       \t 8148494\t       133.2 ns/op\t       8 B/op\t       1 allocs/op\n\t//\tBenchmarkFindParams_Set/bad_id-8            \t73894278\t        16.29 ns/op\t       0 B/op\t       0 allocs/op\n}\n\nfunc BenchmarkStorage_Find(b *testing.B) {\n\tconst (\n\t\tcliID  = \"cid\"\n\t\tcliMAC = \"02:00:00:00:00:00\"\n\t)\n\n\tconst (\n\t\tcliNameWithID   = \"client_with_id\"\n\t\tcliNameWithIP   = \"client_with_ip\"\n\t\tcliNameWithCIDR = \"client_with_cidr\"\n\t\tcliNameWithMAC  = \"client_with_mac\"\n\t)\n\n\tvar (\n\t\tcliIP   = netip.MustParseAddr(\"192.0.2.1\")\n\t\tcliCIDR = netip.MustParsePrefix(\"192.0.2.0/24\")\n\t)\n\n\tvar (\n\t\tclientWithID = &client.Persistent{\n\t\t\tName:      cliNameWithID,\n\t\t\tClientIDs: []client.ClientID{cliID},\n\t\t}\n\t\tclientWithIP = &client.Persistent{\n\t\t\tName: cliNameWithIP,\n\t\t\tIPs:  []netip.Addr{cliIP},\n\t\t}\n\t\tclientWithCIDR = &client.Persistent{\n\t\t\tName:    cliNameWithCIDR,\n\t\t\tSubnets: []netip.Prefix{cliCIDR},\n\t\t}\n\t\tclientWithMAC = &client.Persistent{\n\t\t\tName: cliNameWithMAC,\n\t\t\tMACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},\n\t\t}\n\t)\n\n\tclients := []*client.Persistent{\n\t\tclientWithID,\n\t\tclientWithIP,\n\t\tclientWithCIDR,\n\t\tclientWithMAC,\n\t}\n\ts := newStorage(b, clients)\n\n\tbenchCases := []struct {\n\t\tparams   *client.FindParams\n\t\tname     string\n\t\twantName string\n\t}{{\n\t\tparams: &client.FindParams{\n\t\t\tClientID: cliID,\n\t\t},\n\t\tname:     \"client_id\",\n\t\twantName: cliNameWithID,\n\t}, {\n\t\tparams: &client.FindParams{\n\t\t\tRemoteIP: cliIP,\n\t\t},\n\t\tname:     \"ip_address\",\n\t\twantName: cliNameWithIP,\n\t}, {\n\t\tparams: &client.FindParams{\n\t\t\tSubnet: cliCIDR,\n\t\t},\n\t\tname:     \"subnet\",\n\t\twantName: cliNameWithCIDR,\n\t}, {\n\t\tparams: &client.FindParams{\n\t\t\tMAC: errors.Must(net.ParseMAC(cliMAC)),\n\t\t},\n\t\tname:     \"mac_address\",\n\t\twantName: cliNameWithMAC,\n\t}}\n\n\tfor _, bc := range benchCases {\n\t\tb.Run(bc.name, func(b *testing.B) {\n\t\t\tvar p *client.Persistent\n\t\t\tvar ok bool\n\n\t\t\tb.ReportAllocs()\n\t\t\tfor b.Loop() {\n\t\t\t\tp, ok = s.Find(bc.params)\n\t\t\t}\n\n\t\t\tassert.True(b, ok)\n\t\t\tassert.NotNil(b, p)\n\t\t\tassert.Equal(b, bc.wantName, p.Name)\n\t\t})\n\t}\n\n\t// Most recent results:\n\t//\n\t//\tgoos: linux\n\t//\tgoarch: amd64\n\t//\tpkg: github.com/AdguardTeam/AdGuardHome/internal/client\n\t//\tcpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz\n\t//\tBenchmarkStorage_Find/client_id-8         \t 7070107\t       154.4 ns/op\t     240 B/op\t       2 allocs/op\n\t//\tBenchmarkStorage_Find/ip_address-8        \t 6831823\t       168.6 ns/op\t     248 B/op\t       2 allocs/op\n\t//\tBenchmarkStorage_Find/subnet-8            \t 7209050\t       167.5 ns/op\t     256 B/op\t       2 allocs/op\n\t//\tBenchmarkStorage_Find/mac_address-8       \t 5776131\t       199.7 ns/op\t     256 B/op\t       3 allocs/op\n}\n"
  },
  {
    "path": "internal/client/upstreammanager.go",
    "content": "package client\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghslog\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// CommonUpstreamConfig contains common settings for custom client upstream\n// configurations.\ntype CommonUpstreamConfig struct {\n\tBootstrap               upstream.Resolver\n\tUpstreamTimeout         time.Duration\n\tBootstrapPreferIPv6     bool\n\tEDNSClientSubnetEnabled bool\n\tUseHTTP3Upstreams       bool\n}\n\n// customUpstreamConfig contains custom client upstream configuration and the\n// timestamp of the latest configuration update.\ntype customUpstreamConfig struct {\n\t// proxyConf is the constructed upstream configuration for the [proxy],\n\t// derived from the fields below.  It is initialized on demand with\n\t// [newCustomUpstreamConfig].\n\tproxyConf *proxy.CustomUpstreamConfig\n\n\t// commonConfUpdate is the timestamp of the latest configuration update,\n\t// used to check against [upstreamManager.confUpdate] to determine if the\n\t// configuration is up to date.\n\tcommonConfUpdate time.Time\n\n\t// upstreams is the cached list of custom upstream DNS servers used for the\n\t// configuration of proxyConf.\n\tupstreams []string\n\n\t// upstreamsCacheSize is the cached value of the cache size of the\n\t// upstreams, used for the configuration of proxyConf.\n\tupstreamsCacheSize uint32\n\n\t// upstreamsCacheEnabled is the cached value indicating whether the cache of\n\t// the upstreams is enabled for the configuration of proxyConf.\n\tupstreamsCacheEnabled bool\n\n\t// isChanged indicates whether the proxyConf needs to be updated.\n\tisChanged bool\n}\n\n// upstreamManager stores and updates custom client upstream configurations.\ntype upstreamManager struct {\n\t// baseLogger is used to create loggers for client upstream configurations.\n\t// It should not have a prefix and must not be nil.\n\tbaseLogger *slog.Logger\n\n\t// logger is used for logging the operation of the upstream manager.  It\n\t// must not be nil.\n\tlogger *slog.Logger\n\n\t// uidToCustomConf maps persistent client UID to the custom client upstream\n\t// configuration.  Stored UIDs must be in sync with the [index.uidToClient].\n\tuidToCustomConf map[UID]*customUpstreamConfig\n\n\t// commonConf is the common upstream configuration.\n\tcommonConf *CommonUpstreamConfig\n\n\t// clock is used to get the current time.  It must not be nil.\n\tclock timeutil.Clock\n\n\t// confUpdate is the timestamp of the latest common upstream configuration\n\t// update.\n\tconfUpdate time.Time\n}\n\n// newUpstreamManager returns the new properly initialized upstream manager.\nfunc newUpstreamManager(baseLogger *slog.Logger, clock timeutil.Clock) (m *upstreamManager) {\n\treturn &upstreamManager{\n\t\tbaseLogger:      baseLogger,\n\t\tlogger:          baseLogger.With(slogutil.KeyPrefix, \"upstream_manager\"),\n\t\tuidToCustomConf: make(map[UID]*customUpstreamConfig),\n\t\tclock:           clock,\n\t}\n}\n\n// updateCommonUpstreamConfig updates the common upstream configuration and the\n// timestamp of the latest configuration update.\nfunc (m *upstreamManager) updateCommonUpstreamConfig(conf *CommonUpstreamConfig) {\n\tm.commonConf = conf\n\tm.confUpdate = m.clock.Now()\n}\n\n// updateCustomUpstreamConfig updates the stored custom client upstream\n// configuration associated with the persistent client.  It also sets\n// [customUpstreamConfig.isChanged] to true so [customUpstreamConfig.proxyConf]\n// can be updated later in [upstreamManager.customUpstreamConfig].\nfunc (m *upstreamManager) updateCustomUpstreamConfig(c *Persistent) {\n\tcliConf, ok := m.uidToCustomConf[c.UID]\n\tif !ok {\n\t\tcliConf = &customUpstreamConfig{\n\t\t\tcommonConfUpdate: m.confUpdate,\n\t\t}\n\n\t\tm.uidToCustomConf[c.UID] = cliConf\n\t}\n\n\t// TODO(s.chzhen):  Compare before cloning.\n\tcliConf.upstreams = slices.Clone(c.Upstreams)\n\tcliConf.upstreamsCacheSize = c.UpstreamsCacheSize\n\tcliConf.upstreamsCacheEnabled = c.UpstreamsCacheEnabled\n\tcliConf.isChanged = true\n}\n\n// customUpstreamConfig returns the custom client upstream configuration.\nfunc (m *upstreamManager) customUpstreamConfig(\n\tuid UID,\n\tclientName string,\n) (proxyConf *proxy.CustomUpstreamConfig) {\n\tcliConf, ok := m.uidToCustomConf[uid]\n\tif !ok {\n\t\t// TODO(s.chzhen):  Consider panic.\n\t\tm.logger.Error(\"no associated custom client upstream config\")\n\n\t\treturn nil\n\t}\n\n\tif !m.isConfigChanged(cliConf) {\n\t\treturn cliConf.proxyConf\n\t}\n\n\tif cliConf.proxyConf != nil {\n\t\terr := cliConf.proxyConf.Close()\n\t\tif err != nil {\n\t\t\t// TODO(s.chzhen):  Pass context.\n\t\t\tm.logger.Debug(\"closing custom upstream config\", slogutil.KeyError, err)\n\t\t}\n\t}\n\n\tcliLogger := aghslog.NewForUpstream(m.baseLogger, aghslog.UpstreamTypeCustom).With(\n\t\taghslog.KeyClientName,\n\t\tclientName,\n\t)\n\tproxyConf = newCustomUpstreamConfig(cliConf, m.commonConf, cliLogger)\n\tcliConf.proxyConf = proxyConf\n\tcliConf.commonConfUpdate = m.confUpdate\n\tcliConf.isChanged = false\n\n\treturn proxyConf\n}\n\n// isConfigChanged returns true if the update is necessary for the custom client\n// upstream configuration.\nfunc (m *upstreamManager) isConfigChanged(cliConf *customUpstreamConfig) (ok bool) {\n\treturn !m.confUpdate.Equal(cliConf.commonConfUpdate) || cliConf.isChanged\n}\n\n// clearUpstreamCache clears the upstream cache for each stored custom client\n// upstream configuration.\nfunc (m *upstreamManager) clearUpstreamCache() {\n\tfor _, c := range m.uidToCustomConf {\n\t\tif c.proxyConf != nil {\n\t\t\tc.proxyConf.ClearCache()\n\t\t}\n\t}\n}\n\n// remove deletes the custom client upstream configuration and closes\n// [customUpstreamConfig.proxyConf] if necessary.\nfunc (m *upstreamManager) remove(uid UID) (err error) {\n\tcliConf, ok := m.uidToCustomConf[uid]\n\tif !ok {\n\t\t// TODO(s.chzhen):  Consider panic.\n\t\treturn errors.Error(\"no associated custom client upstream config\")\n\t}\n\n\tdelete(m.uidToCustomConf, uid)\n\n\tif cliConf.proxyConf != nil {\n\t\treturn cliConf.proxyConf.Close()\n\t}\n\n\treturn nil\n}\n\n// close shuts down each stored custom client upstream configuration.\nfunc (m *upstreamManager) close() (err error) {\n\tvar errs []error\n\tfor _, c := range m.uidToCustomConf {\n\t\tif c.proxyConf == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\terrs = append(errs, c.proxyConf.Close())\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// newCustomUpstreamConfig returns the new properly initialized custom proxy\n// upstream configuration for the client.  cliConf, conf, and cliLogger must not\n// be nil.\nfunc newCustomUpstreamConfig(\n\tcliConf *customUpstreamConfig,\n\tconf *CommonUpstreamConfig,\n\tcliLogger *slog.Logger,\n) (proxyConf *proxy.CustomUpstreamConfig) {\n\tupstreams := stringutil.FilterOut(cliConf.upstreams, aghnet.IsCommentOrEmpty)\n\tif len(upstreams) == 0 {\n\t\treturn nil\n\t}\n\n\tupsConf, err := proxy.ParseUpstreamsConfig(\n\t\tupstreams,\n\t\t&upstream.Options{\n\t\t\tLogger:       cliLogger,\n\t\t\tBootstrap:    conf.Bootstrap,\n\t\t\tTimeout:      conf.UpstreamTimeout,\n\t\t\tHTTPVersions: aghnet.UpstreamHTTPVersions(conf.UseHTTP3Upstreams),\n\t\t\tPreferIPv6:   conf.BootstrapPreferIPv6,\n\t\t},\n\t)\n\tif err != nil {\n\t\t// Should not happen because upstreams are already validated.  See\n\t\t// [Persistent.validate].\n\t\tpanic(fmt.Errorf(\"creating custom upstream config: %w\", err))\n\t}\n\n\treturn proxy.NewCustomUpstreamConfig(\n\t\tupsConf,\n\t\tcliConf.upstreamsCacheEnabled,\n\t\tint(cliConf.upstreamsCacheSize),\n\t\tconf.EDNSClientSubnetEnabled,\n\t)\n}\n"
  },
  {
    "path": "internal/configmigrate/configmigrate.go",
    "content": "// Package configmigrate provides a way to upgrade the YAML configuration file.\npackage configmigrate\n\n// LastSchemaVersion is the most recent schema version.\nconst LastSchemaVersion uint = 33\n"
  },
  {
    "path": "internal/configmigrate/configmigrate_internal_test.go",
    "content": "package configmigrate\n\nimport (\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n"
  },
  {
    "path": "internal/configmigrate/configmigrate_test.go",
    "content": "package configmigrate_test\n\nimport (\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n"
  },
  {
    "path": "internal/configmigrate/migrations_internal_test.go",
    "content": "package configmigrate\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dnsforward\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// emptyMigrator is a helper function that returns initialized with empty values\n// *Migrator and no-op implementations for tests.\nfunc emptyMigrator() (m *Migrator) {\n\treturn New(&Config{\n\t\tLogger:     testLogger,\n\t\tWorkingDir: \"\",\n\t\tDataDir:    \"\",\n\t})\n}\n\nfunc TestUpgradeSchema1to2(t *testing.T) {\n\tt.Parallel()\n\n\tdiskConf := testDiskConf(1)\n\n\tm := emptyMigrator()\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\terr := m.migrateTo2(ctx, diskConf)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, diskConf[\"schema_version\"], 2)\n\n\t_, ok := diskConf[\"coredns\"]\n\trequire.False(t, ok)\n\n\tnewDNSConf, ok := diskConf[\"dns\"]\n\trequire.True(t, ok)\n\n\toldDNSConf := testDNSConf(1)\n\tassert.Equal(t, oldDNSConf, newDNSConf)\n\n\toldExcludedEntries := []string{\"coredns\", \"schema_version\"}\n\tnewExcludedEntries := []string{\"dns\", \"schema_version\"}\n\toldDiskConf := testDiskConf(1)\n\tassertEqualExcept(t, oldDiskConf, diskConf, oldExcludedEntries, newExcludedEntries)\n}\n\nfunc TestUpgradeSchema2to3(t *testing.T) {\n\tt.Parallel()\n\n\tdiskConf := testDiskConf(2)\n\n\tm := emptyMigrator()\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\terr := m.migrateTo3(ctx, diskConf)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, diskConf[\"schema_version\"], 3)\n\n\tdnsMap, ok := diskConf[\"dns\"]\n\trequire.True(t, ok)\n\n\tnewDNSConf, ok := dnsMap.(yobj)\n\trequire.True(t, ok)\n\n\tbootstrapDNS := newDNSConf[\"bootstrap_dns\"]\n\tswitch v := bootstrapDNS.(type) {\n\tcase yarr:\n\t\trequire.Len(t, v, 1)\n\t\trequire.Equal(t, \"8.8.8.8:53\", v[0])\n\tdefault:\n\t\tt.Fatalf(\"wrong type for bootstrap dns: %T\", v)\n\t}\n\n\texcludedEntries := []string{\"bootstrap_dns\"}\n\toldDNSConf := testDNSConf(2)\n\tassertEqualExcept(t, oldDNSConf, newDNSConf, excludedEntries, excludedEntries)\n\n\texcludedEntries = []string{\"dns\", \"schema_version\"}\n\toldDiskConf := testDiskConf(2)\n\tassertEqualExcept(t, oldDiskConf, diskConf, excludedEntries, excludedEntries)\n}\n\nfunc TestUpgradeSchema5to6(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 6\n\n\ttestCases := []struct {\n\t\tin      yobj\n\t\twant    yobj\n\t\twantErr string\n\t\tname    string\n\t}{{\n\t\tin: yobj{\n\t\t\t\"clients\": yarr{},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\":        yarr{},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErr: \"\",\n\t\tname:    \"no_clients\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yarr{yobj{\"ip\": \"127.0.0.1\"}},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\": yarr{yobj{\n\t\t\t\t\"ids\": yarr{\"127.0.0.1\"},\n\t\t\t\t\"ip\":  \"127.0.0.1\",\n\t\t\t}},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErr: \"\",\n\t\tname:    \"client_ip\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yarr{yobj{\"mac\": \"mac\"}},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\": yarr{yobj{\n\t\t\t\t\"ids\": yarr{\"mac\"},\n\t\t\t\t\"mac\": \"mac\",\n\t\t\t}},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErr: \"\",\n\t\tname:    \"client_mac\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yarr{yobj{\"ip\": \"127.0.0.1\", \"mac\": \"mac\"}},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\": yarr{yobj{\n\t\t\t\t\"ids\": yarr{\"127.0.0.1\", \"mac\"},\n\t\t\t\t\"ip\":  \"127.0.0.1\",\n\t\t\t\t\"mac\": \"mac\",\n\t\t\t}},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErr: \"\",\n\t\tname:    \"client_ip_mac\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yarr{yobj{\"ip\": 1, \"mac\": \"mac\"}},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\":        yarr{yobj{\"ip\": 1, \"mac\": \"mac\"}},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErr: `client at index 0: unexpected type of \"ip\": int`,\n\t\tname:    \"inv_client_ip\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yarr{yobj{\"ip\": \"127.0.0.1\", \"mac\": 1}},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\":        yarr{yobj{\"ip\": \"127.0.0.1\", \"mac\": 1}},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErr: `client at index 0: unexpected type of \"mac\": int`,\n\t\tname:    \"inv_client_mac\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo6(ctx, tc.in)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErr, err)\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema7to8(t *testing.T) {\n\tt.Parallel()\n\n\tconst host = \"1.2.3.4\"\n\toldConf := yobj{\n\t\t\"dns\": yobj{\n\t\t\t\"bind_host\": host,\n\t\t},\n\t\t\"schema_version\": 7,\n\t}\n\n\tm := emptyMigrator()\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\terr := m.migrateTo8(ctx, oldConf)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, oldConf[\"schema_version\"], 8)\n\n\tdnsVal, ok := oldConf[\"dns\"]\n\trequire.True(t, ok)\n\n\tnewDNSConf, ok := dnsVal.(yobj)\n\trequire.True(t, ok)\n\n\tnewBindHosts, ok := newDNSConf[\"bind_hosts\"].(yarr)\n\trequire.True(t, ok)\n\trequire.Len(t, newBindHosts, 1)\n\tassert.Equal(t, host, newBindHosts[0])\n}\n\nfunc TestUpgradeSchema8to9(t *testing.T) {\n\tt.Parallel()\n\n\tconst tld = \"foo\"\n\n\tt.Run(\"with_autohost_tld\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\toldConf := yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"autohost_tld\": tld,\n\t\t\t},\n\t\t\t\"schema_version\": 8,\n\t\t}\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo9(ctx, oldConf)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, oldConf[\"schema_version\"], 9)\n\n\t\tdnsVal, ok := oldConf[\"dns\"]\n\t\trequire.True(t, ok)\n\n\t\tnewDNSConf, ok := dnsVal.(yobj)\n\t\trequire.True(t, ok)\n\n\t\tlocalDomainName, ok := newDNSConf[\"local_domain_name\"].(string)\n\t\trequire.True(t, ok)\n\n\t\tassert.Equal(t, tld, localDomainName)\n\t})\n\n\tt.Run(\"without_autohost_tld\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\toldConf := yobj{\n\t\t\t\"dns\":            yobj{},\n\t\t\t\"schema_version\": 8,\n\t\t}\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo9(ctx, oldConf)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, oldConf[\"schema_version\"], 9)\n\n\t\tdnsVal, ok := oldConf[\"dns\"]\n\t\trequire.True(t, ok)\n\n\t\tnewDNSConf, ok := dnsVal.(yobj)\n\t\trequire.True(t, ok)\n\n\t\t// Should be nil in order to be set to the default value by the\n\t\t// following config rewrite.\n\t\t_, ok = newDNSConf[\"local_domain_name\"]\n\t\trequire.False(t, ok)\n\t})\n}\n\n// assertEqualExcept removes entries from configs and compares them.\nfunc assertEqualExcept(tb testing.TB, oldConf, newConf yobj, oldKeys, newKeys []string) {\n\ttb.Helper()\n\n\tfor _, k := range oldKeys {\n\t\tdelete(oldConf, k)\n\t}\n\tfor _, k := range newKeys {\n\t\tdelete(newConf, k)\n\t}\n\n\tassert.Equal(tb, oldConf, newConf)\n}\n\nfunc testDiskConf(schemaVersion int) (diskConf yobj) {\n\tfilters := []filtering.FilterYAML{{\n\t\tURL:        \"https://filters.adtidy.org/android/filters/111_optimized.txt\",\n\t\tName:       \"Latvian filter\",\n\t\tRulesCount: 100,\n\t}, {\n\t\tURL:        \"https://easylist.to/easylistgermany/easylistgermany.txt\",\n\t\tName:       \"Germany filter\",\n\t\tRulesCount: 200,\n\t}}\n\tdiskConf = yobj{\n\t\t\"language\":       \"en\",\n\t\t\"filters\":        filters,\n\t\t\"user_rules\":     []string{},\n\t\t\"schema_version\": schemaVersion,\n\t\t\"bind_host\":      \"0.0.0.0\",\n\t\t\"bind_port\":      80,\n\t\t\"auth_name\":      \"name\",\n\t\t\"auth_pass\":      \"pass\",\n\t}\n\n\tdnsConf := testDNSConf(schemaVersion)\n\tif schemaVersion > 1 {\n\t\tdiskConf[\"dns\"] = dnsConf\n\t} else {\n\t\tdiskConf[\"coredns\"] = dnsConf\n\t}\n\n\treturn diskConf\n}\n\n// testDNSConf creates a DNS config for test the way [go.yaml.in/yaml/v4] would\n// unmarshal it.  In YAML, keys aren't guaranteed to always only be strings.\nfunc testDNSConf(schemaVersion int) (dnsConf yobj) {\n\tdnsConf = yobj{\n\t\t\"port\":                 53,\n\t\t\"blocked_response_ttl\": 10,\n\t\t\"querylog_enabled\":     true,\n\t\t\"ratelimit\":            20,\n\t\t\"bootstrap_dns\":        \"8.8.8.8:53\",\n\t\t\"parental_sensitivity\": 13,\n\t\t\"ratelimit_whitelist\":  []string{},\n\t\t\"upstream_dns\":         []string{\"tls://1.1.1.1\", \"tls://1.0.0.1\", \"8.8.8.8\"},\n\t\t\"filtering_enabled\":    true,\n\t\t\"refuse_any\":           true,\n\t\t\"parental_enabled\":     true,\n\t\t\"bind_host\":            \"0.0.0.0\",\n\t\t\"protection_enabled\":   true,\n\t\t\"safesearch_enabled\":   true,\n\t\t\"safebrowsing_enabled\": true,\n\t}\n\n\tif schemaVersion > 2 {\n\t\tdnsConf[\"bootstrap_dns\"] = []string{\"8.8.8.8:53\"}\n\t}\n\n\treturn dnsConf\n}\n\nfunc TestAddQUICPort(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tups  string\n\t\twant string\n\t}{{\n\t\tname: \"simple_ip\",\n\t\tups:  \"8.8.8.8\",\n\t\twant: \"8.8.8.8\",\n\t}, {\n\t\tname: \"url_ipv4\",\n\t\tups:  \"quic://8.8.8.8\",\n\t\twant: \"quic://8.8.8.8:784\",\n\t}, {\n\t\tname: \"url_ipv4_with_port\",\n\t\tups:  \"quic://8.8.8.8:25565\",\n\t\twant: \"quic://8.8.8.8:25565\",\n\t}, {\n\t\tname: \"url_ipv6\",\n\t\tups:  \"quic://[::1]\",\n\t\twant: \"quic://[::1]:784\",\n\t}, {\n\t\tname: \"url_ipv6_invalid\",\n\t\tups:  \"quic://::1\",\n\t\twant: \"quic://::1\",\n\t}, {\n\t\tname: \"url_ipv6_with_port\",\n\t\tups:  \"quic://[::1]:25565\",\n\t\twant: \"quic://[::1]:25565\",\n\t}, {\n\t\tname: \"url_hostname\",\n\t\tups:  \"quic://example.com\",\n\t\twant: \"quic://example.com:784\",\n\t}, {\n\t\tname: \"url_hostname_with_port\",\n\t\tups:  \"quic://example.com:25565\",\n\t\twant: \"quic://example.com:25565\",\n\t}, {\n\t\tname: \"url_hostname_with_endpoint\",\n\t\tups:  \"quic://example.com/some-endpoint\",\n\t\twant: \"quic://example.com:784/some-endpoint\",\n\t}, {\n\t\tname: \"url_hostname_with_port_endpoint\",\n\t\tups:  \"quic://example.com:25565/some-endpoint\",\n\t\twant: \"quic://example.com:25565/some-endpoint\",\n\t}, {\n\t\tname: \"non-quic_proto\",\n\t\tups:  \"tls://example.com\",\n\t\twant: \"tls://example.com\",\n\t}, {\n\t\tname: \"comment\",\n\t\tups:  \"# comment\",\n\t\twant: \"# comment\",\n\t}, {\n\t\tname: \"blank\",\n\t\tups:  \"\",\n\t\twant: \"\",\n\t}, {\n\t\tname: \"with_domain_ip\",\n\t\tups:  \"[/example.domain/]8.8.8.8\",\n\t\twant: \"[/example.domain/]8.8.8.8\",\n\t}, {\n\t\tname: \"with_domain_url\",\n\t\tups:  \"[/example.domain/]quic://example.com\",\n\t\twant: \"[/example.domain/]quic://example.com:784\",\n\t}, {\n\t\tname: \"invalid_domain\",\n\t\tups:  \"[/exmaple.domain]quic://example.com\",\n\t\twant: \"[/exmaple.domain]quic://example.com\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\twithPort := addQUICPort(tc.ups, 784)\n\n\t\t\tassert.Equal(t, tc.want, withPort)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema9to10(t *testing.T) {\n\tt.Parallel()\n\n\tconst ultimateAns = 42\n\n\ttestCases := []struct {\n\t\tups     any\n\t\twant    any\n\t\twantErr string\n\t\tname    string\n\t}{{\n\t\tups:     yarr{\"quic://8.8.8.8\"},\n\t\twant:    yarr{\"quic://8.8.8.8:784\"},\n\t\twantErr: \"\",\n\t\tname:    \"success\",\n\t}, {\n\t\tups:     ultimateAns,\n\t\twant:    nil,\n\t\twantErr: `unexpected type of \"upstream_dns\": int`,\n\t\tname:    \"bad_yarr_type\",\n\t}, {\n\t\tups:     yarr{ultimateAns},\n\t\twant:    nil,\n\t\twantErr: `unexpected type of upstream field: int`,\n\t\tname:    \"bad_upstream_type\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tconf := yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"upstream_dns\": tc.ups,\n\t\t\t},\n\t\t\t\"schema_version\": 9,\n\t\t}\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo10(ctx, conf)\n\n\t\t\tif tc.wantErr != \"\" {\n\t\t\t\ttestutil.AssertErrorMsg(t, tc.wantErr, err)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, conf[\"schema_version\"], 10)\n\n\t\t\tdnsVal, ok := conf[\"dns\"]\n\t\t\trequire.True(t, ok)\n\n\t\t\tnewDNSConf, ok := dnsVal.(yobj)\n\t\t\trequire.True(t, ok)\n\n\t\t\tfixedUps, ok := newDNSConf[\"upstream_dns\"].(yarr)\n\t\t\trequire.True(t, ok)\n\n\t\t\tassert.Equal(t, tc.want, fixedUps)\n\t\t})\n\t}\n\n\tt.Run(\"no_dns\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo10(ctx, yobj{})\n\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"bad_dns\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo10(ctx, yobj{\n\t\t\t\"dns\": ultimateAns,\n\t\t})\n\n\t\ttestutil.AssertErrorMsg(t, `unexpected type of \"dns\": int`, err)\n\t})\n}\n\nfunc TestUpgradeSchema10to11(t *testing.T) {\n\tt.Parallel()\n\n\tcheck := func(t *testing.T, conf yobj) {\n\t\trlimit, _ := conf[\"rlimit_nofile\"].(int)\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo11(ctx, conf)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, conf[\"schema_version\"], 11)\n\n\t\t_, ok := conf[\"rlimit_nofile\"]\n\t\tassert.False(t, ok)\n\n\t\tosVal, ok := conf[\"os\"]\n\t\trequire.True(t, ok)\n\n\t\tnewOSConf, ok := osVal.(yobj)\n\t\trequire.True(t, ok)\n\n\t\t_, ok = newOSConf[\"group\"]\n\t\tassert.True(t, ok)\n\n\t\t_, ok = newOSConf[\"user\"]\n\t\tassert.True(t, ok)\n\n\t\trlimitVal, ok := newOSConf[\"rlimit_nofile\"].(int)\n\t\trequire.True(t, ok)\n\n\t\tassert.Equal(t, rlimit, rlimitVal)\n\t}\n\n\tconst rlimit = 42\n\tt.Run(\"with_rlimit\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := yobj{\n\t\t\t\"rlimit_nofile\":  rlimit,\n\t\t\t\"schema_version\": 10,\n\t\t}\n\t\tcheck(t, conf)\n\t})\n\n\tt.Run(\"without_rlimit\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := yobj{\n\t\t\t\"schema_version\": 10,\n\t\t}\n\t\tcheck(t, conf)\n\t})\n}\n\nfunc TestUpgradeSchema11to12(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tivl     any\n\t\twant    any\n\t\twantErr string\n\t\tname    string\n\t}{{\n\t\tivl:     1,\n\t\twant:    timeutil.Duration(timeutil.Day),\n\t\twantErr: \"\",\n\t\tname:    \"success\",\n\t}, {\n\t\tivl:     0.25,\n\t\twant:    0,\n\t\twantErr: `unexpected type of \"querylog_interval\": float64`,\n\t\tname:    \"fail\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tconf := yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"querylog_interval\": tc.ivl,\n\t\t\t},\n\t\t\t\"schema_version\": 11,\n\t\t}\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo12(ctx, conf)\n\n\t\t\tif tc.wantErr != \"\" {\n\t\t\t\trequire.Error(t, err)\n\n\t\t\t\tassert.Equal(t, tc.wantErr, err.Error())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, conf[\"schema_version\"], 12)\n\n\t\t\tdnsVal, ok := conf[\"dns\"]\n\t\t\trequire.True(t, ok)\n\n\t\t\tvar newDNSConf yobj\n\t\t\tnewDNSConf, ok = dnsVal.(yobj)\n\t\t\trequire.True(t, ok)\n\n\t\t\tvar newIvl timeutil.Duration\n\t\t\tnewIvl, ok = newDNSConf[\"querylog_interval\"].(timeutil.Duration)\n\t\t\trequire.True(t, ok)\n\n\t\t\tassert.Equal(t, tc.want, newIvl)\n\t\t})\n\t}\n\n\tt.Run(\"no_dns\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo12(ctx, yobj{})\n\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"bad_dns\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo12(ctx, yobj{\n\t\t\t\"dns\": 0,\n\t\t})\n\n\t\ttestutil.AssertErrorMsg(t, `unexpected type of \"dns\": int`, err)\n\t})\n\n\tt.Run(\"no_field\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := yobj{\n\t\t\t\"dns\": yobj{},\n\t\t}\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo12(ctx, conf)\n\t\trequire.NoError(t, err)\n\n\t\tdns, ok := conf[\"dns\"]\n\t\trequire.True(t, ok)\n\n\t\tvar dnsVal yobj\n\t\tdnsVal, ok = dns.(yobj)\n\t\trequire.True(t, ok)\n\n\t\tvar ivl any\n\t\tivl, ok = dnsVal[\"querylog_interval\"]\n\t\trequire.True(t, ok)\n\n\t\tvar ivlVal timeutil.Duration\n\t\tivlVal, ok = ivl.(timeutil.Duration)\n\t\trequire.True(t, ok)\n\n\t\tassert.Equal(t, 90*24*time.Hour, time.Duration(ivlVal))\n\t})\n}\n\nfunc TestUpgradeSchema12to13(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 13\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tin:   yobj{},\n\t\twant: yobj{\"schema_version\": newSchemaVer},\n\t\tname: \"no_dns\",\n\t}, {\n\t\tin: yobj{\"dns\": yobj{}},\n\t\twant: yobj{\n\t\t\t\"dns\":            yobj{},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"no_dhcp\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"local_domain_name\": \"lan\",\n\t\t\t},\n\t\t\t\"dhcp\":           yobj{},\n\t\t\t\"schema_version\": newSchemaVer - 1,\n\t\t},\n\t\twant: yobj{\n\t\t\t\"dns\": yobj{},\n\t\t\t\"dhcp\": yobj{\n\t\t\t\t\"local_domain_name\": \"lan\",\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"good\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo13(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema13to14(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 14\n\n\ttestClient := yobj{\n\t\t\"name\":                \"agh-client\",\n\t\t\"ids\":                 []string{\"id1\"},\n\t\t\"use_global_settings\": true,\n\t}\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tin: yobj{},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t\t// The clients field will be added anyway.\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": yarr{},\n\t\t\t\t\"runtime_sources\": yobj{\n\t\t\t\t\t\"whois\": true,\n\t\t\t\t\t\"arp\":   true,\n\t\t\t\t\t\"rdns\":  false,\n\t\t\t\t\t\"dhcp\":  true,\n\t\t\t\t\t\"hosts\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tname: \"no_clients\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yarr{testClient},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": yarr{testClient},\n\t\t\t\t\"runtime_sources\": yobj{\n\t\t\t\t\t\"whois\": true,\n\t\t\t\t\t\"arp\":   true,\n\t\t\t\t\t\"rdns\":  false,\n\t\t\t\t\t\"dhcp\":  true,\n\t\t\t\t\t\"hosts\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tname: \"no_dns\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yarr{testClient},\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"resolve_clients\": true,\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": yarr{testClient},\n\t\t\t\t\"runtime_sources\": yobj{\n\t\t\t\t\t\"whois\": true,\n\t\t\t\t\t\"arp\":   true,\n\t\t\t\t\t\"rdns\":  true,\n\t\t\t\t\t\"dhcp\":  true,\n\t\t\t\t\t\"hosts\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"dns\": yobj{},\n\t\t},\n\t\tname: \"good\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo14(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema14to15(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 15\n\n\tdefaultWantObj := yobj{\n\t\t\"querylog\": map[string]any{\n\t\t\t\"enabled\":      true,\n\t\t\t\"file_enabled\": true,\n\t\t\t\"interval\":     \"2160h\",\n\t\t\t\"size_memory\":  1000,\n\t\t\t\"ignored\":      []any{},\n\t\t},\n\t\t\"dns\":            map[string]any{},\n\t\t\"schema_version\": newSchemaVer,\n\t}\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tin: yobj{\n\t\t\t\"dns\": map[string]any{\n\t\t\t\t\"querylog_enabled\":      true,\n\t\t\t\t\"querylog_file_enabled\": true,\n\t\t\t\t\"querylog_interval\":     \"2160h\",\n\t\t\t\t\"querylog_size_memory\":  1000,\n\t\t\t},\n\t\t},\n\t\twant: defaultWantObj,\n\t\tname: \"basic\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"dns\": map[string]any{},\n\t\t},\n\t\twant: defaultWantObj,\n\t\tname: \"default_values\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo15(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema15to16(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 16\n\n\tdefaultWantObj := yobj{\n\t\t\"statistics\": map[string]any{\n\t\t\t\"enabled\":  true,\n\t\t\t\"interval\": 1,\n\t\t\t\"ignored\":  []any{},\n\t\t},\n\t\t\"dns\":            map[string]any{},\n\t\t\"schema_version\": newSchemaVer,\n\t}\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tin: yobj{\n\t\t\t\"dns\": map[string]any{\n\t\t\t\t\"statistics_interval\": 1,\n\t\t\t},\n\t\t},\n\t\twant: defaultWantObj,\n\t\tname: \"basic\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"dns\": map[string]any{},\n\t\t},\n\t\twant: defaultWantObj,\n\t\tname: \"default_values\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"dns\": map[string]any{\n\t\t\t\t\"statistics_interval\": 0,\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"statistics\": map[string]any{\n\t\t\t\t\"enabled\":  false,\n\t\t\t\t\"interval\": 1,\n\t\t\t\t\"ignored\":  []any{},\n\t\t\t},\n\t\t\t\"dns\":            map[string]any{},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"stats_disabled\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo16(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema16to17(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 17\n\n\tdefaultWantObj := yobj{\n\t\t\"dns\": map[string]any{\n\t\t\t\"edns_client_subnet\": map[string]any{\n\t\t\t\t\"enabled\":    false,\n\t\t\t\t\"use_custom\": false,\n\t\t\t\t\"custom_ip\":  \"\",\n\t\t\t},\n\t\t},\n\t\t\"schema_version\": newSchemaVer,\n\t}\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tin: yobj{\n\t\t\t\"dns\": map[string]any{\n\t\t\t\t\"edns_client_subnet\": false,\n\t\t\t},\n\t\t},\n\t\twant: defaultWantObj,\n\t\tname: \"basic\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"dns\": map[string]any{},\n\t\t},\n\t\twant: defaultWantObj,\n\t\tname: \"default_values\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"dns\": map[string]any{\n\t\t\t\t\"edns_client_subnet\": true,\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"dns\": map[string]any{\n\t\t\t\t\"edns_client_subnet\": map[string]any{\n\t\t\t\t\t\"enabled\":    true,\n\t\t\t\t\t\"use_custom\": false,\n\t\t\t\t\t\"custom_ip\":  \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"is_true\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo17(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema17to18(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 18\n\n\tdefaultWantObj := yobj{\n\t\t\"dns\": yobj{\n\t\t\t\"safe_search\": yobj{\n\t\t\t\t\"enabled\":    true,\n\t\t\t\t\"bing\":       true,\n\t\t\t\t\"duckduckgo\": true,\n\t\t\t\t\"google\":     true,\n\t\t\t\t\"pixabay\":    true,\n\t\t\t\t\"yandex\":     true,\n\t\t\t\t\"youtube\":    true,\n\t\t\t},\n\t\t},\n\t\t\"schema_version\": newSchemaVer,\n\t}\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tin:   yobj{\"dns\": yobj{}},\n\t\twant: defaultWantObj,\n\t\tname: \"default_values\",\n\t}, {\n\t\tin:   yobj{\"dns\": yobj{\"safesearch_enabled\": true}},\n\t\twant: defaultWantObj,\n\t\tname: \"enabled\",\n\t}, {\n\t\tin: yobj{\"dns\": yobj{\"safesearch_enabled\": false}},\n\t\twant: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"safe_search\": map[string]any{\n\t\t\t\t\t\"enabled\":    false,\n\t\t\t\t\t\"bing\":       true,\n\t\t\t\t\t\"duckduckgo\": true,\n\t\t\t\t\t\"google\":     true,\n\t\t\t\t\t\"pixabay\":    true,\n\t\t\t\t\t\"yandex\":     true,\n\t\t\t\t\t\"youtube\":    true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"disabled\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo18(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema18to19(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 19\n\n\tdefaultWantObj := yobj{\n\t\t\"clients\": yobj{\n\t\t\t\"persistent\": yarr{yobj{\n\t\t\t\t\"name\": \"localhost\",\n\t\t\t\t\"safe_search\": yobj{\n\t\t\t\t\t\"enabled\":    true,\n\t\t\t\t\t\"bing\":       true,\n\t\t\t\t\t\"duckduckgo\": true,\n\t\t\t\t\t\"google\":     true,\n\t\t\t\t\t\"pixabay\":    true,\n\t\t\t\t\t\"yandex\":     true,\n\t\t\t\t\t\"youtube\":    true,\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\t\"schema_version\": newSchemaVer,\n\t}\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tin: yobj{\n\t\t\t\"clients\": yobj{},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\":        yobj{},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"no_clients\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": yarr{yobj{\"name\": \"localhost\"}},\n\t\t\t},\n\t\t},\n\t\twant: defaultWantObj,\n\t\tname: \"default_values\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": yarr{yobj{\"name\": \"localhost\", \"safesearch_enabled\": true}},\n\t\t\t},\n\t\t},\n\t\twant: defaultWantObj,\n\t\tname: \"enabled\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": yarr{yobj{\"name\": \"localhost\", \"safesearch_enabled\": false}},\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\": yobj{\"persistent\": yarr{yobj{\n\t\t\t\t\"name\": \"localhost\",\n\t\t\t\t\"safe_search\": yobj{\n\t\t\t\t\t\"enabled\":    false,\n\t\t\t\t\t\"bing\":       true,\n\t\t\t\t\t\"duckduckgo\": true,\n\t\t\t\t\t\"google\":     true,\n\t\t\t\t\t\"pixabay\":    true,\n\t\t\t\t\t\"yandex\":     true,\n\t\t\t\t\t\"youtube\":    true,\n\t\t\t\t},\n\t\t\t}}},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"disabled\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo19(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema19to20(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tivl     any\n\t\twant    any\n\t\twantErr string\n\t\tname    string\n\t}{{\n\t\tivl:     1,\n\t\twant:    timeutil.Duration(timeutil.Day),\n\t\twantErr: \"\",\n\t\tname:    \"success\",\n\t}, {\n\t\tivl:     0,\n\t\twant:    timeutil.Duration(timeutil.Day),\n\t\twantErr: \"\",\n\t\tname:    \"success\",\n\t}, {\n\t\tivl:     0.25,\n\t\twant:    0,\n\t\twantErr: `unexpected type of \"interval\": float64`,\n\t\tname:    \"fail\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tconf := yobj{\n\t\t\t\"statistics\": yobj{\n\t\t\t\t\"interval\": tc.ivl,\n\t\t\t},\n\t\t\t\"schema_version\": 19,\n\t\t}\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo20(ctx, conf)\n\n\t\t\tif tc.wantErr != \"\" {\n\t\t\t\trequire.Error(t, err)\n\n\t\t\t\tassert.Equal(t, tc.wantErr, err.Error())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, conf[\"schema_version\"], 20)\n\n\t\t\tstatsVal, ok := conf[\"statistics\"]\n\t\t\trequire.True(t, ok)\n\n\t\t\tvar stats yobj\n\t\t\tstats, ok = statsVal.(yobj)\n\t\t\trequire.True(t, ok)\n\n\t\t\tvar newIvl timeutil.Duration\n\t\t\tnewIvl, ok = stats[\"interval\"].(timeutil.Duration)\n\t\t\trequire.True(t, ok)\n\n\t\t\tassert.Equal(t, tc.want, newIvl)\n\t\t})\n\t}\n\n\tt.Run(\"no_stats\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo20(ctx, yobj{})\n\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"bad_stats\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo20(ctx, yobj{\n\t\t\t\"statistics\": 0,\n\t\t})\n\n\t\ttestutil.AssertErrorMsg(t, `unexpected type of \"statistics\": int`, err)\n\t})\n\n\tt.Run(\"no_field\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := yobj{\n\t\t\t\"statistics\": yobj{},\n\t\t}\n\n\t\tm := emptyMigrator()\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr := m.migrateTo20(ctx, conf)\n\t\trequire.NoError(t, err)\n\n\t\tstatsVal, ok := conf[\"statistics\"]\n\t\trequire.True(t, ok)\n\n\t\tvar stats yobj\n\t\tstats, ok = statsVal.(yobj)\n\t\trequire.True(t, ok)\n\n\t\tvar ivl any\n\t\tivl, ok = stats[\"interval\"]\n\t\trequire.True(t, ok)\n\n\t\tvar ivlVal timeutil.Duration\n\t\tivlVal, ok = ivl.(timeutil.Duration)\n\t\trequire.True(t, ok)\n\n\t\tassert.Equal(t, 24*time.Hour, time.Duration(ivlVal))\n\t})\n}\n\nfunc TestUpgradeSchema20to21(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 21\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tname: \"nothing\",\n\t\tin:   yobj{},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"no_clients\",\n\t\tin: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"blocked_services\": yarr{\"ok\"},\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"blocked_services\": yobj{\n\t\t\t\t\t\"ids\": yarr{\"ok\"},\n\t\t\t\t\t\"schedule\": yobj{\n\t\t\t\t\t\t\"time_zone\": \"Local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo21(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema21to22(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 22\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tin: yobj{\n\t\t\t\"clients\": yobj{},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\":        yobj{},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"nothing\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": []any{yobj{\"name\": \"localhost\", \"blocked_services\": yarr{}}},\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": []any{yobj{\n\t\t\t\t\t\"name\": \"localhost\",\n\t\t\t\t\t\"blocked_services\": yobj{\n\t\t\t\t\t\t\"ids\": yarr{},\n\t\t\t\t\t\t\"schedule\": yobj{\n\t\t\t\t\t\t\t\"time_zone\": \"Local\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"no_services\",\n\t}, {\n\t\tin: yobj{\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": []any{yobj{\"name\": \"localhost\", \"blocked_services\": yarr{\"ok\"}}},\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"clients\": yobj{\n\t\t\t\t\"persistent\": []any{yobj{\n\t\t\t\t\t\"name\": \"localhost\",\n\t\t\t\t\t\"blocked_services\": yobj{\n\t\t\t\t\t\t\"ids\": yarr{\"ok\"},\n\t\t\t\t\t\t\"schedule\": yobj{\n\t\t\t\t\t\t\t\"time_zone\": \"Local\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\tname: \"services\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo22(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema22to23(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 23\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tname: \"empty\",\n\t\tin:   yobj{},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"ok\",\n\t\tin: yobj{\n\t\t\t\"bind_host\":       \"1.2.3.4\",\n\t\t\t\"bind_port\":       8081,\n\t\t\t\"web_session_ttl\": 720,\n\t\t},\n\t\twant: yobj{\n\t\t\t\"http\": yobj{\n\t\t\t\t\"address\":     \"1.2.3.4:8081\",\n\t\t\t\t\"session_ttl\": \"720h\",\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"v6_address\",\n\t\tin: yobj{\n\t\t\t\"bind_host\":       \"2001:db8::1\",\n\t\t\t\"bind_port\":       8081,\n\t\t\t\"web_session_ttl\": 720,\n\t\t},\n\t\twant: yobj{\n\t\t\t\"http\": yobj{\n\t\t\t\t\"address\":     \"[2001:db8::1]:8081\",\n\t\t\t\t\"session_ttl\": \"720h\",\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo23(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema23to24(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 24\n\n\ttestCases := []struct {\n\t\tin         yobj\n\t\twant       yobj\n\t\tname       string\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"empty\",\n\t\tin:   yobj{},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"ok\",\n\t\tin: yobj{\n\t\t\t\"log_file\":        \"/test/path.log\",\n\t\t\t\"log_max_backups\": 1,\n\t\t\t\"log_max_size\":    2,\n\t\t\t\"log_max_age\":     3,\n\t\t\t\"log_compress\":    true,\n\t\t\t\"log_localtime\":   true,\n\t\t\t\"verbose\":         true,\n\t\t},\n\t\twant: yobj{\n\t\t\t\"log\": yobj{\n\t\t\t\t\"file\":        \"/test/path.log\",\n\t\t\t\t\"max_backups\": 1,\n\t\t\t\t\"max_size\":    2,\n\t\t\t\t\"max_age\":     3,\n\t\t\t\t\"compress\":    true,\n\t\t\t\t\"local_time\":  true,\n\t\t\t\t\"verbose\":     true,\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"invalid\",\n\t\tin: yobj{\n\t\t\t\"log_file\":        \"/test/path.log\",\n\t\t\t\"log_max_backups\": 1,\n\t\t\t\"log_max_size\":    2,\n\t\t\t\"log_max_age\":     3,\n\t\t\t\"log_compress\":    \"\",\n\t\t\t\"log_localtime\":   true,\n\t\t\t\"verbose\":         true,\n\t\t},\n\t\twant: yobj{\n\t\t\t\"log_compress\":   \"\",\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErrMsg: `unexpected type of \"log_compress\": string`,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo24(ctx, tc.in)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema24to25(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 25\n\n\ttestCases := []struct {\n\t\tin         yobj\n\t\twant       yobj\n\t\tname       string\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"empty\",\n\t\tin:   yobj{},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"ok\",\n\t\tin: yobj{\n\t\t\t\"http\": yobj{\n\t\t\t\t\"address\":     \"0.0.0.0:3000\",\n\t\t\t\t\"session_ttl\": \"720h\",\n\t\t\t},\n\t\t\t\"debug_pprof\": true,\n\t\t},\n\t\twant: yobj{\n\t\t\t\"http\": yobj{\n\t\t\t\t\"address\":     \"0.0.0.0:3000\",\n\t\t\t\t\"session_ttl\": \"720h\",\n\t\t\t\t\"pprof\": yobj{\n\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\"port\":    6060,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"ok_disabled\",\n\t\tin: yobj{\n\t\t\t\"http\": yobj{\n\t\t\t\t\"address\":     \"0.0.0.0:3000\",\n\t\t\t\t\"session_ttl\": \"720h\",\n\t\t\t},\n\t\t\t\"debug_pprof\": false,\n\t\t},\n\t\twant: yobj{\n\t\t\t\"http\": yobj{\n\t\t\t\t\"address\":     \"0.0.0.0:3000\",\n\t\t\t\t\"session_ttl\": \"720h\",\n\t\t\t\t\"pprof\": yobj{\n\t\t\t\t\t\"enabled\": false,\n\t\t\t\t\t\"port\":    6060,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"invalid\",\n\t\tin: yobj{\n\t\t\t\"http\": yobj{\n\t\t\t\t\"address\":     \"0.0.0.0:3000\",\n\t\t\t\t\"session_ttl\": \"720h\",\n\t\t\t},\n\t\t\t\"debug_pprof\": 1,\n\t\t},\n\t\twant: yobj{\n\t\t\t\"http\": yobj{\n\t\t\t\t\"address\":     \"0.0.0.0:3000\",\n\t\t\t\t\"session_ttl\": \"720h\",\n\t\t\t},\n\t\t\t\"debug_pprof\":    1,\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t\twantErrMsg: `unexpected type of \"debug_pprof\": int`,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo25(ctx, tc.in)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema25to26(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 26\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tname: \"empty\",\n\t\tin:   yobj{},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"ok\",\n\t\tin: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"filtering_enabled\":       true,\n\t\t\t\t\"filters_update_interval\": 24,\n\t\t\t\t\"parental_enabled\":        false,\n\t\t\t\t\"safebrowsing_enabled\":    false,\n\t\t\t\t\"safebrowsing_cache_size\": 1048576,\n\t\t\t\t\"safesearch_cache_size\":   1048576,\n\t\t\t\t\"parental_cache_size\":     1048576,\n\t\t\t\t\"safe_search\": yobj{\n\t\t\t\t\t\"enabled\":    false,\n\t\t\t\t\t\"bing\":       true,\n\t\t\t\t\t\"duckduckgo\": true,\n\t\t\t\t\t\"google\":     true,\n\t\t\t\t\t\"pixabay\":    true,\n\t\t\t\t\t\"yandex\":     true,\n\t\t\t\t\t\"youtube\":    true,\n\t\t\t\t},\n\t\t\t\t\"rewrites\": yarr{},\n\t\t\t\t\"blocked_services\": yobj{\n\t\t\t\t\t\"schedule\": yobj{\n\t\t\t\t\t\t\"time_zone\": \"Local\",\n\t\t\t\t\t},\n\t\t\t\t\t\"ids\": yarr{},\n\t\t\t\t},\n\t\t\t\t\"protection_enabled\":        true,\n\t\t\t\t\"blocking_mode\":             \"custom_ip\",\n\t\t\t\t\"blocking_ipv4\":             \"1.2.3.4\",\n\t\t\t\t\"blocking_ipv6\":             \"1:2:3::4\",\n\t\t\t\t\"blocked_response_ttl\":      10,\n\t\t\t\t\"protection_disabled_until\": nil,\n\t\t\t\t\"parental_block_host\":       \"p.dns.adguard.com\",\n\t\t\t\t\"safebrowsing_block_host\":   \"s.dns.adguard.com\",\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"dns\": yobj{},\n\t\t\t\"filtering\": yobj{\n\t\t\t\t\"filtering_enabled\":       true,\n\t\t\t\t\"filters_update_interval\": 24,\n\t\t\t\t\"parental_enabled\":        false,\n\t\t\t\t\"safebrowsing_enabled\":    false,\n\t\t\t\t\"safebrowsing_cache_size\": 1048576,\n\t\t\t\t\"safesearch_cache_size\":   1048576,\n\t\t\t\t\"parental_cache_size\":     1048576,\n\t\t\t\t\"safe_search\": yobj{\n\t\t\t\t\t\"enabled\":    false,\n\t\t\t\t\t\"bing\":       true,\n\t\t\t\t\t\"duckduckgo\": true,\n\t\t\t\t\t\"google\":     true,\n\t\t\t\t\t\"pixabay\":    true,\n\t\t\t\t\t\"yandex\":     true,\n\t\t\t\t\t\"youtube\":    true,\n\t\t\t\t},\n\t\t\t\t\"rewrites\": yarr{},\n\t\t\t\t\"blocked_services\": yobj{\n\t\t\t\t\t\"schedule\": yobj{\n\t\t\t\t\t\t\"time_zone\": \"Local\",\n\t\t\t\t\t},\n\t\t\t\t\t\"ids\": yarr{},\n\t\t\t\t},\n\t\t\t\t\"protection_enabled\":        true,\n\t\t\t\t\"blocking_mode\":             \"custom_ip\",\n\t\t\t\t\"blocking_ipv4\":             \"1.2.3.4\",\n\t\t\t\t\"blocking_ipv6\":             \"1:2:3::4\",\n\t\t\t\t\"blocked_response_ttl\":      10,\n\t\t\t\t\"protection_disabled_until\": nil,\n\t\t\t\t\"parental_block_host\":       \"p.dns.adguard.com\",\n\t\t\t\t\"safebrowsing_block_host\":   \"s.dns.adguard.com\",\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo26(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema26to27(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 27\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tname: \"empty\",\n\t\tin:   yobj{},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"single_dot\",\n\t\tin: yobj{\n\t\t\t\"querylog\": yobj{\n\t\t\t\t\"ignored\": yarr{\n\t\t\t\t\t\".\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"statistics\": yobj{\n\t\t\t\t\"ignored\": yarr{\n\t\t\t\t\t\".\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"querylog\": yobj{\n\t\t\t\t\"ignored\": yarr{\n\t\t\t\t\t\"|.^\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"statistics\": yobj{\n\t\t\t\t\"ignored\": yarr{\n\t\t\t\t\t\"|.^\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"mixed\",\n\t\tin: yobj{\n\t\t\t\"querylog\": yobj{\n\t\t\t\t\"ignored\": yarr{\n\t\t\t\t\t\".\",\n\t\t\t\t\t\"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"statistics\": yobj{\n\t\t\t\t\"ignored\": yarr{\n\t\t\t\t\t\".\",\n\t\t\t\t\t\"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"querylog\": yobj{\n\t\t\t\t\"ignored\": yarr{\n\t\t\t\t\t\"|.^\",\n\t\t\t\t\t\"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"statistics\": yobj{\n\t\t\t\t\"ignored\": yarr{\n\t\t\t\t\t\"|.^\",\n\t\t\t\t\t\"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo27(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestUpgradeSchema27to28(t *testing.T) {\n\tt.Parallel()\n\n\tconst newSchemaVer = 28\n\n\ttestCases := []struct {\n\t\tin   yobj\n\t\twant yobj\n\t\tname string\n\t}{{\n\t\tname: \"empty\",\n\t\tin:   yobj{},\n\t\twant: yobj{\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"load_balance\",\n\t\tin: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"all_servers\":  false,\n\t\t\t\t\"fastest_addr\": false,\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"upstream_mode\": dnsforward.UpstreamModeLoadBalance,\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"parallel\",\n\t\tin: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"all_servers\":  true,\n\t\t\t\t\"fastest_addr\": false,\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"upstream_mode\": dnsforward.UpstreamModeParallel,\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"parallel_fastest\",\n\t\tin: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"all_servers\":  true,\n\t\t\t\t\"fastest_addr\": true,\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"upstream_mode\": dnsforward.UpstreamModeParallel,\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}, {\n\t\tname: \"load_balance\",\n\t\tin: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"all_servers\":  false,\n\t\t\t\t\"fastest_addr\": true,\n\t\t\t},\n\t\t},\n\t\twant: yobj{\n\t\t\t\"dns\": yobj{\n\t\t\t\t\"upstream_mode\": dnsforward.UpstreamModeFastestAddr,\n\t\t\t},\n\t\t\t\"schema_version\": newSchemaVer,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tm := emptyMigrator()\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\terr := m.migrateTo28(ctx, tc.in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, tc.in)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/configmigrate/migrator.go",
    "content": "package configmigrate\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\tyaml \"go.yaml.in/yaml/v4\"\n)\n\n// Config is a the configuration for initializing a [Migrator].\ntype Config struct {\n\t// Logger is used to log the operation of configuration migrator.  It must\n\t// not be nil.\n\tLogger *slog.Logger\n\n\t// WorkingDir is the absolute path to the working directory of AdGuardHome.\n\tWorkingDir string\n\n\t// DataDir is the absolute path to the data directory of AdGuardHome.\n\tDataDir string\n}\n\n// Migrator performs the YAML configuration file migrations.\ntype Migrator struct {\n\tlogger     *slog.Logger\n\tworkingDir string\n\tdataDir    string\n}\n\n// New creates a new Migrator.\nfunc New(c *Config) (m *Migrator) {\n\treturn &Migrator{\n\t\tlogger:     c.Logger,\n\t\tworkingDir: c.WorkingDir,\n\t\tdataDir:    c.DataDir,\n\t}\n}\n\n// Migrate preforms necessary upgrade operations to upgrade file to target\n// schema version, if needed.  It returns the body of the upgraded config file,\n// whether the file was upgraded, and an error, if any.  If upgraded is false,\n// the body is the same as the input.\nfunc (m *Migrator) Migrate(\n\tctx context.Context,\n\tbody []byte,\n\ttarget uint,\n) (newBody []byte, upgraded bool, err error) {\n\tdiskConf := yobj{}\n\terr = yaml.Unmarshal(body, &diskConf)\n\tif err != nil {\n\t\treturn body, false, fmt.Errorf(\"parsing config file for upgrade: %w\", err)\n\t}\n\n\tcurrentInt, _, err := fieldVal[int](diskConf, \"schema_version\")\n\tif err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn body, false, err\n\t}\n\n\tcurrent := uint(currentInt)\n\tm.logger.DebugContext(ctx, \"got\", \"schema_version\", current)\n\n\tif err = validateVersion(current, target); err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn body, false, err\n\t} else if current == target {\n\t\treturn body, false, nil\n\t}\n\n\tif err = m.upgradeConfigSchema(ctx, current, target, diskConf); err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn body, false, err\n\t}\n\n\tbuf := bytes.NewBuffer(newBody)\n\tenc := yaml.NewEncoder(buf)\n\tenc.SetIndent(2)\n\n\tif err = enc.Encode(diskConf); err != nil {\n\t\treturn body, false, fmt.Errorf(\"generating new config: %w\", err)\n\t}\n\n\treturn buf.Bytes(), true, nil\n}\n\n// validateVersion validates the current and desired schema versions.\nfunc validateVersion(current, target uint) (err error) {\n\tswitch {\n\tcase current > target:\n\t\treturn fmt.Errorf(\"unknown current schema version %d\", current)\n\tcase target > LastSchemaVersion:\n\t\treturn fmt.Errorf(\"unknown target schema version %d\", target)\n\tcase target < current:\n\t\treturn fmt.Errorf(\"target schema version %d lower than current %d\", target, current)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// migrateFunc is a function that upgrades a config and returns an error.\ntype migrateFunc = func(ctx context.Context, diskConf yobj) (err error)\n\n// upgradeConfigSchema upgrades the configuration schema in diskConf from\n// current to target version.  current must be less than target, and both must\n// be non-negative and less or equal to [LastSchemaVersion].\nfunc (m *Migrator) upgradeConfigSchema(\n\tctx context.Context,\n\tcurrent, target uint,\n\tdiskConf yobj,\n) (err error) {\n\tupgrades := [LastSchemaVersion]migrateFunc{\n\t\t0:  m.migrateTo1,\n\t\t1:  m.migrateTo2,\n\t\t2:  m.migrateTo3,\n\t\t3:  m.migrateTo4,\n\t\t4:  m.migrateTo5,\n\t\t5:  m.migrateTo6,\n\t\t6:  m.migrateTo7,\n\t\t7:  m.migrateTo8,\n\t\t8:  m.migrateTo9,\n\t\t9:  m.migrateTo10,\n\t\t10: m.migrateTo11,\n\t\t11: m.migrateTo12,\n\t\t12: m.migrateTo13,\n\t\t13: m.migrateTo14,\n\t\t14: m.migrateTo15,\n\t\t15: m.migrateTo16,\n\t\t16: m.migrateTo17,\n\t\t17: m.migrateTo18,\n\t\t18: m.migrateTo19,\n\t\t19: m.migrateTo20,\n\t\t20: m.migrateTo21,\n\t\t21: m.migrateTo22,\n\t\t22: m.migrateTo23,\n\t\t23: m.migrateTo24,\n\t\t24: m.migrateTo25,\n\t\t25: m.migrateTo26,\n\t\t26: m.migrateTo27,\n\t\t27: m.migrateTo28,\n\t\t28: m.migrateTo29,\n\t\t29: m.migrateTo30,\n\t\t30: m.migrateTo31,\n\t\t31: m.migrateTo32,\n\t\t32: m.migrateTo33,\n\t}\n\n\tfor i, migrate := range upgrades[current:target] {\n\t\tcur := current + uint(i)\n\t\tnext := current + uint(i) + 1\n\n\t\tm.logger.InfoContext(ctx, \"upgrade yaml\", \"from\", cur, \"to\", next)\n\n\t\tif err = migrate(ctx, diskConf); err != nil {\n\t\t\treturn fmt.Errorf(\"migrating schema %d to %d: %w\", cur, next, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/migrator_test.go",
    "content": "package configmigrate_test\n\nimport (\n\t\"bytes\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/configmigrate\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/require\"\n\tyaml \"go.yaml.in/yaml/v4\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// testdata is a virtual filesystem containing test data.\nvar testdata = os.DirFS(\"testdata\")\n\n// getField returns the value located at the given indexes in the given object.\n// It fails the test if the value is not found or of the expected type.  The\n// indexes can be either strings or integers, and are interpreted as map keys or\n// array indexes, respectively.\nfunc getField[T any](t require.TestingT, obj any, indexes ...any) (val T) {\n\tfor _, index := range indexes {\n\t\tswitch index := index.(type) {\n\t\tcase string:\n\t\t\trequire.IsType(t, map[string]any(nil), obj)\n\t\t\ttypedObj := obj.(map[string]any)\n\n\t\t\trequire.Contains(t, typedObj, index)\n\t\t\tobj = typedObj[index]\n\t\tcase int:\n\t\t\trequire.IsType(t, []any(nil), obj)\n\t\t\ttypedObj := obj.([]any)\n\n\t\t\trequire.Less(t, index, len(typedObj))\n\t\t\tobj = typedObj[index]\n\t\tdefault:\n\t\t\tt.Errorf(\"unexpected index type: %T\", index)\n\t\t\tt.FailNow()\n\t\t}\n\t}\n\n\trequire.IsType(t, val, obj)\n\n\treturn obj.(T)\n}\n\nfunc TestMigrateConfig_Migrate(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tinputFileName  = \"input.yml\"\n\t\toutputFileName = \"output.yml\"\n\t)\n\n\ttestCases := []struct {\n\t\tyamlEqFunc    func(t require.TestingT, expected, actual string, msgAndArgs ...any)\n\t\tname          string\n\t\ttargetVersion uint\n\t}{{\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v1\",\n\t\ttargetVersion: 1,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v2\",\n\t\ttargetVersion: 2,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v3\",\n\t\ttargetVersion: 3,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v4\",\n\t\ttargetVersion: 4,\n\t}, {\n\t\t// Compare passwords separately because bcrypt hashes those with a\n\t\t// different salt every time.\n\t\tyamlEqFunc: func(t require.TestingT, expected, actual string, msgAndArgs ...any) {\n\t\t\tif h, ok := t.(interface{ Helper() }); ok {\n\t\t\t\th.Helper()\n\t\t\t}\n\n\t\t\tvar want, got map[string]any\n\t\t\terr := yaml.Unmarshal([]byte(expected), &want)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = yaml.Unmarshal([]byte(actual), &got)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgotPass := getField[string](t, got, \"users\", 0, \"password\")\n\t\t\twantPass := getField[string](t, want, \"users\", 0, \"password\")\n\t\t\trequire.NoError(t, bcrypt.CompareHashAndPassword([]byte(gotPass), []byte(wantPass)))\n\n\t\t\tdelete(getField[map[string]any](t, got, \"users\", 0), \"password\")\n\t\t\tdelete(getField[map[string]any](t, want, \"users\", 0), \"password\")\n\n\t\t\trequire.Equal(t, want, got, msgAndArgs...)\n\t\t},\n\t\tname:          \"v5\",\n\t\ttargetVersion: 5,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v6\",\n\t\ttargetVersion: 6,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v7\",\n\t\ttargetVersion: 7,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v8\",\n\t\ttargetVersion: 8,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v9\",\n\t\ttargetVersion: 9,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v10\",\n\t\ttargetVersion: 10,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v11\",\n\t\ttargetVersion: 11,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v12\",\n\t\ttargetVersion: 12,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v13\",\n\t\ttargetVersion: 13,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v14\",\n\t\ttargetVersion: 14,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v15\",\n\t\ttargetVersion: 15,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v16\",\n\t\ttargetVersion: 16,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v17\",\n\t\ttargetVersion: 17,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v18\",\n\t\ttargetVersion: 18,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v19\",\n\t\ttargetVersion: 19,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v20\",\n\t\ttargetVersion: 20,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v21\",\n\t\ttargetVersion: 21,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v22\",\n\t\ttargetVersion: 22,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v23\",\n\t\ttargetVersion: 23,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v24\",\n\t\ttargetVersion: 24,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v25\",\n\t\ttargetVersion: 25,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v26\",\n\t\ttargetVersion: 26,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v27\",\n\t\ttargetVersion: 27,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v28\",\n\t\ttargetVersion: 28,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v30\",\n\t\ttargetVersion: 30,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v31\",\n\t\ttargetVersion: 31,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v32\",\n\t\ttargetVersion: 32,\n\t}, {\n\t\tyamlEqFunc:    require.YAMLEq,\n\t\tname:          \"v33\",\n\t\ttargetVersion: 33,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tbody, err := fs.ReadFile(testdata, path.Join(t.Name(), inputFileName))\n\t\t\trequire.NoError(t, err)\n\n\t\t\twantBody, err := fs.ReadFile(testdata, path.Join(t.Name(), outputFileName))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tmigrator := configmigrate.New(&configmigrate.Config{\n\t\t\t\tLogger:     testLogger,\n\t\t\t\tWorkingDir: t.Name(),\n\t\t\t\tDataDir:    filepath.Join(t.Name(), \"data\"),\n\t\t\t})\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tnewBody, upgraded, err := migrator.Migrate(ctx, body, tc.targetVersion)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.True(t, upgraded)\n\n\t\t\ttc.yamlEqFunc(t, string(wantBody), string(newBody))\n\t\t})\n\t}\n}\n\n// TODO(a.garipov):  Consider ways of merging into the previous one.\nfunc TestMigrateConfig_Migrate_v29(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tpathUnix       = `/path/to/file.txt`\n\t\tuserDirPatUnix = `TestMigrateConfig_Migrate/v29/data/userfilters/*`\n\n\t\tpathWindows       = `C:\\path\\to\\file.txt`\n\t\tuserDirPatWindows = `TestMigrateConfig_Migrate\\v29\\data\\userfilters\\*`\n\t)\n\n\tpathToReplace := pathUnix\n\tpatternToReplace := userDirPatUnix\n\tif runtime.GOOS == \"windows\" {\n\t\tpathToReplace = pathWindows\n\t\tpatternToReplace = userDirPatWindows\n\t}\n\n\tbody, err := fs.ReadFile(testdata, \"TestMigrateConfig_Migrate/v29/input.yml\")\n\trequire.NoError(t, err)\n\n\tbody = bytes.ReplaceAll(body, []byte(\"FILEPATH\"), []byte(pathToReplace))\n\n\twantBody, err := fs.ReadFile(testdata, \"TestMigrateConfig_Migrate/v29/output.yml\")\n\trequire.NoError(t, err)\n\n\twantBody = bytes.ReplaceAll(wantBody, []byte(\"FILEPATH\"), []byte(pathToReplace))\n\twantBody = bytes.ReplaceAll(wantBody, []byte(\"USERFILTERSPATH\"), []byte(patternToReplace))\n\n\tmigrator := configmigrate.New(&configmigrate.Config{\n\t\tLogger:     testLogger,\n\t\tWorkingDir: t.Name(),\n\t\tDataDir:    \"TestMigrateConfig_Migrate/v29/data\",\n\t})\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tnewBody, upgraded, err := migrator.Migrate(ctx, body, 29)\n\trequire.NoError(t, err)\n\trequire.True(t, upgraded)\n\n\trequire.YAMLEq(t, string(wantBody), string(newBody))\n}\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v1/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ncoredns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v1/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ncoredns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nschema_version: 1\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v10/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  local_domain_name: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 9\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v10/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  local_domain_name: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 10\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v11/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  local_domain_name: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 10\nuser_rules: []\nrlimit_nofile: 123\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v11/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  local_domain_name: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 11\nuser_rules: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v12/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  local_domain_name: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  querylog_interval: 30\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 11\nuser_rules: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v12/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  local_domain_name: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  querylog_interval: 720h\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 12\nuser_rules: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v13/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  local_domain_name: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  querylog_interval: 720h\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 12\nuser_rules: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v13/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  querylog_interval: 720h\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 13\nuser_rules: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v14/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  querylog_interval: 720h\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  resolve_clients: true\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 13\nuser_rules: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v14/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  querylog_interval: 720h\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 14\nuser_rules: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v15/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  querylog_file_enabled: true\n  querylog_interval: 720h\n  querylog_size_memory: 1000\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 14\nuser_rules: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v15/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 15\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v16/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  statistics_interval: 10\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 15\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v16/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 16\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 10\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v17/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet: true\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 16\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 10\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v17/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 17\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 10\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v18/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 17\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 10\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v18/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: false\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 18\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 10\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v19/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safesearch_enabled: true\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 18\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 10\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v19/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 19\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 10\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v2/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ncoredns:\n  bind_host: 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns: 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nschema_version: 1\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v2/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ndns:\n  bind_host: 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns: 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nschema_version: 2\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v20/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 19\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 10\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v20/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 20\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v21/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n  - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 20\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v21/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 21\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v22/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n    - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 21\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v22/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 22\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v23/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nweb_session_ttl: 3\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 22\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v23/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 23\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v24/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 23\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: \"\"\n  rlimit_nofile: 123\n  user: \"\"\nlog_file: \"\"\nlog_max_backups: 0\nlog_max_size: 100\nlog_max_age: 3\nlog_compress: true\nlog_localtime: false\nverbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v24/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 24\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v25/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\ndebug_pprof: true\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 24\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v25/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 25\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v26/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 25\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v26/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 26\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored: []\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored: []\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v27/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 26\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '.'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '.'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v27/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 27\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v28/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  all_servers: true\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 27\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v28/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  upstream_mode: parallel\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 28\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v29/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 28\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v29/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_fs_patterns:\n      - USERFILTERSPATH\n      - FILEPATH\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 29\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v3/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns: 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nschema_version: 2\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v3/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nschema_version: 3\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v30/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  cache_size: 4194304\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_fs_patterns: []\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 29\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v30/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  cache_enabled: true\n  cache_size: 4194304\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safe_fs_patterns: []\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 30\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v31/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  cache_size: 4194304\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  rewrites:\n    - domain: test.example\n      answer: 192.0.2.0\n  safe_fs_patterns: []\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 29\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v31/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  cache_enabled: true\n  cache_size: 4194304\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  rewrites:\n    - domain: test.example\n      answer: 192.0.2.0\n      enabled: true\n  safe_fs_patterns: []\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 31\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v32/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  cache_enabled: true\n  cache_size: 4194304\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  rewrites:\n    - domain: test.example\n      answer: 192.0.2.0\n      enabled: true\n  safe_fs_patterns: []\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 31\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v32/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  cache_enabled: true\n  cache_size: 4194304\n  cache_optimistic_answer_ttl: 30s\n  cache_optimistic_max_age: 12h\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  rewrites:\n    - domain: test.example\n      answer: 192.0.2.0\n      enabled: true\n  safe_fs_patterns: []\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 32\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v33/input.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  cache_enabled: true\n  cache_size: 4194304\n  cache_optimistic_answer_ttl: 30s\n  cache_optimistic_max_age: 12h\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  rewrites:\n    - domain: test.example\n      answer: 192.0.2.0\n      enabled: true\n  safe_fs_patterns: []\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 32\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v33/output.yml",
    "content": "http:\n  address: 127.0.0.1:3000\n  session_ttl: 3h\n  pprof:\n    enabled: true\n    port: 6060\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  parental_sensitivity: 0\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  - quic://8.8.8.8:784\n  bootstrap_dns:\n  - 8.8.8.8:53\n  cache_enabled: true\n  cache_size: 4194304\n  cache_optimistic_answer_ttl: 30s\n  cache_optimistic_max_age: 12h\n  edns_client_subnet:\n    enabled:    true\n    use_custom: false\n    custom_ip:  \"\"\nfiltering:\n  filtering_enabled: true\n  parental_enabled: false\n  safebrowsing_enabled: false\n  rewrites:\n    - domain: test.example\n      answer: 192.0.2.0\n      enabled: true\n  safe_fs_patterns: []\n  safe_search:\n    enabled:    false\n    bing:       true\n    duckduckgo: true\n    google:     true\n    pixabay:    true\n    yandex:     true\n    youtube:    true\n  protection_enabled: true\n  blocked_services:\n    schedule:\n      time_zone: Local\n    ids:\n    - 500px\n  blocked_response_ttl: 10\nfilters:\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: FILEPATH\n  name: Local Filter\n  enabled: false\nclients:\n  persistent:\n  - name: localhost\n    ids:\n    - 127.0.0.1\n    - aa:aa:aa:aa:aa:aa\n    use_global_settings: true\n    use_global_blocked_services: true\n    filtering_enabled: false\n    parental_enabled: false\n    safebrowsing_enabled: false\n    safe_search:\n      enabled:    true\n      bing:       true\n      duckduckgo: true\n      google:     true\n      pixabay:    true\n      yandex:     true\n      youtube:    true\n    blocked_services:\n      schedule:\n        time_zone: Local\n      ids:\n      - 500px\n  runtime_sources:\n    whois: true\n    arp:   true\n    rdns:  true\n    dhcp:  true\n    hosts: true\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  local_domain_name: local\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 33\nuser_rules: []\nquerylog:\n  enabled: true\n  file_enabled: true\n  interval: 720h\n  size_memory: 1000\n  ignored:\n  - '|.^'\n  ignored_enabled: true\nstatistics:\n  enabled: true\n  interval: 240h\n  ignored:\n  - '|.^'\n  ignored_enabled: true\nos:\n  group: ''\n  rlimit_nofile: 123\n  user: ''\nlog:\n  file: \"\"\n  max_backups: 0\n  max_size: 100\n  max_age: 3\n  compress: true\n  local_time: false\n  verbose: true\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v4/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ip: 127.0.0.1\n  mac: \"\"\n  use_global_settings: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\nschema_version: 3\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v4/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ip: 127.0.0.1\n  mac: \"\"\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\nschema_version: 4\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v5/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nauth_name: testuser\nauth_pass: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ip: 127.0.0.1\n  mac: \"\"\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\nschema_version: 4\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v5/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ip: 127.0.0.1\n  mac: \"\"\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\nschema_version: 5\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v6/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ip: 127.0.0.1\n  mac: aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\nschema_version: 5\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v6/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  ip: 127.0.0.1\n  mac: aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\nschema_version: 6\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v7/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  gateway_ip: 192.168.0.1\n  subnet_mask: 255.255.255.0\n  range_start: 192.168.0.10\n  range_end: 192.168.0.250\n  lease_duration: 1234\n  icmp_timeout_msec: 10\nschema_version: 6\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v7/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 7\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v8/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_host: 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 7\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v8/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 8\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v9/input.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  autohost_tld: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 8\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/testdata/TestMigrateConfig_Migrate/v9/output.yml",
    "content": "bind_host: 127.0.0.1\nbind_port: 3000\nusers:\n- name: testuser\n  password: testpassword\ndns:\n  bind_hosts:\n  - 127.0.0.1\n  port: 53\n  local_domain_name: local\n  protection_enabled: true\n  filtering_enabled: true\n  safebrowsing_enabled: false\n  safesearch_enabled: false\n  parental_enabled: false\n  parental_sensitivity: 0\n  blocked_response_ttl: 10\n  querylog_enabled: true\n  upstream_dns:\n  - tls://1.1.1.1\n  - tls://1.0.0.1\n  bootstrap_dns:\n  - 8.8.8.8:53\nfilters:\n- url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n  name: \"\"\n  enabled: true\n- url: https://adaway.org/hosts.txt\n  name: AdAway\n  enabled: false\n- url: https://hosts-file.net/ad_servers.txt\n  name: hpHosts - Ad and Tracking servers only\n  enabled: false\n- url: http://www.malwaredomainlist.com/hostslist/hosts.txt\n  name: MalwareDomainList.com Hosts List\n  enabled: false\nclients:\n- name: localhost\n  ids:\n  - 127.0.0.1\n  - aa:aa:aa:aa:aa:aa\n  use_global_settings: true\n  use_global_blocked_services: true\n  filtering_enabled: false\n  parental_enabled: false\n  safebrowsing_enabled: false\n  safesearch_enabled: false\ndhcp:\n  enabled: false\n  interface_name: vboxnet0\n  dhcpv4:\n    gateway_ip: 192.168.0.1\n    subnet_mask: 255.255.255.0\n    range_start: 192.168.0.10\n    range_end: 192.168.0.250\n    lease_duration: 1234\n    icmp_timeout_msec: 10\nschema_version: 9\nuser_rules: []\n"
  },
  {
    "path": "internal/configmigrate/v1.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// migrateTo1 performs the following changes:\n//\n//\t# BEFORE:\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 1\n//\t# …\n//\n// It also deletes the unused dnsfilter.txt file, since the following versions\n// store filters in data/filters/.\nfunc (m *Migrator) migrateTo1(ctx context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 1\n\n\tdnsFilterPath := filepath.Join(m.workingDir, \"dnsfilter.txt\")\n\tm.logger.InfoContext(ctx, \"deleting file as we do not need it anymore\", \"path\", dnsFilterPath)\n\terr = os.Remove(dnsFilterPath)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tm.logger.InfoContext(ctx, \"failed to delete\", slogutil.KeyError, err)\n\n\t\t// Go on.\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v10.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/golibs/netutil\"\n)\n\n// migrateTo10 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 9\n//\t'dns':\n//\t  'upstream_dns':\n//\t   - 'quic://some-upstream.com'\n//\t  'local_ptr_upstreams':\n//\t   - 'quic://some-upstream.com'\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 10\n//\t'dns':\n//\t  'upstream_dns':\n//\t   - 'quic://some-upstream.com:784'\n//\t  'local_ptr_upstreams':\n//\t   - 'quic://some-upstream.com:784'\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo10(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 10\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tconst quicPort = 784\n\n\tups, ok, err := fieldVal[yarr](dns, \"upstream_dns\")\n\tif err != nil {\n\t\treturn err\n\t} else if ok {\n\t\tif err = addQUICPorts(ups, quicPort); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdns[\"upstream_dns\"] = ups\n\t}\n\n\tups, ok, err = fieldVal[yarr](dns, \"local_ptr_upstreams\")\n\tif err != nil {\n\t\treturn err\n\t} else if ok {\n\t\tif err = addQUICPorts(ups, quicPort); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdns[\"local_ptr_upstreams\"] = ups\n\t}\n\n\treturn nil\n}\n\n// addQUICPorts inserts a port into each QUIC upstream's hostname in ups if\n// those are missing.\nfunc addQUICPorts(ups yarr, port int) (err error) {\n\tfor i, uVal := range ups {\n\t\tu, ok := uVal.(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unexpected type of upstream field: %T\", uVal)\n\t\t}\n\n\t\tups[i] = addQUICPort(u, port)\n\t}\n\n\treturn nil\n}\n\n// addQUICPort inserts a port into QUIC upstream's hostname if it is missing.\nfunc addQUICPort(ups string, port int) (withPort string) {\n\tif ups == \"\" || ups[0] == '#' {\n\t\treturn ups\n\t}\n\n\tvar doms string\n\twithPort = ups\n\tif strings.HasPrefix(ups, \"[/\") {\n\t\tdomsAndUps := strings.Split(strings.TrimPrefix(ups, \"[/\"), \"/]\")\n\t\tif len(domsAndUps) != 2 {\n\t\t\treturn ups\n\t\t}\n\n\t\tdoms, withPort = \"[/\"+domsAndUps[0]+\"/]\", domsAndUps[1]\n\t}\n\n\tif !strings.Contains(withPort, \"://\") {\n\t\treturn ups\n\t}\n\n\tupsURL, err := url.Parse(withPort)\n\tif err != nil || upsURL.Scheme != \"quic\" {\n\t\treturn ups\n\t}\n\n\tvar host string\n\thost, err = netutil.SplitHost(upsURL.Host)\n\tif err != nil || host != upsURL.Host {\n\t\treturn ups\n\t}\n\n\tupsURL.Host = strings.Join([]string{host, strconv.Itoa(port)}, \":\")\n\n\treturn doms + upsURL.String()\n}\n"
  },
  {
    "path": "internal/configmigrate/v11.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo11 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 10\n//\t'rlimit_nofile': 42\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 11\n//\t'os':\n//\t  'group': ''\n//\t  'rlimit_nofile': 42\n//\t  'user': ''\n//\t# …\nfunc (m *Migrator) migrateTo11(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 11\n\n\trlimit, _, err := fieldVal[int](diskConf, \"rlimit_nofile\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdelete(diskConf, \"rlimit_nofile\")\n\tdiskConf[\"os\"] = yobj{\n\t\t\"group\":         \"\",\n\t\t\"rlimit_nofile\": rlimit,\n\t\t\"user\":          \"\",\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v12.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// migrateTo12 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 11\n//\t'querylog_interval': 90\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 12\n//\t'querylog_interval': '2160h'\n//\t# …\nfunc (m *Migrator) migrateTo12(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 12\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tconst field = \"querylog_interval\"\n\n\tqlogIvl, ok, err := fieldVal[int](dns, field)\n\tif !ok {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Set the initial value from home.initConfig function.\n\t\tqlogIvl = 90\n\t}\n\n\tdns[field] = timeutil.Duration(time.Duration(qlogIvl) * timeutil.Day)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v13.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo13 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 12\n//\t'dns':\n//\t  'local_domain_name': 'lan'\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 13\n//\t'dhcp':\n//\t  'local_domain_name': 'lan'\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo13(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 13\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tdhcp, ok, err := fieldVal[yobj](diskConf, \"dhcp\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\treturn moveSameVal[string](dns, dhcp, \"local_domain_name\")\n}\n"
  },
  {
    "path": "internal/configmigrate/v14.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo14 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 13\n//\t'dns':\n//\t  'resolve_clients': true\n//\t  # …\n//\t'clients':\n//\t- 'name': 'client-name'\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 14\n//\t'dns':\n//\t  # …\n//\t'clients':\n//\t  'persistent':\n//\t  - 'name': 'client-name'\n//\t    # …\n//\t  'runtime_sources':\n//\t    'whois': true\n//\t    'arp': true\n//\t    'rdns': true\n//\t    'dhcp': true\n//\t    'hosts': true\n//\t# …\nfunc (m *Migrator) migrateTo14(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 14\n\n\tpersistent, ok, err := fieldVal[yarr](diskConf, \"clients\")\n\tif !ok {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tpersistent = yarr{}\n\t}\n\n\truntimeClients := yobj{\n\t\t\"whois\": true,\n\t\t\"arp\":   true,\n\t\t\"rdns\":  false,\n\t\t\"dhcp\":  true,\n\t\t\"hosts\": true,\n\t}\n\tdiskConf[\"clients\"] = yobj{\n\t\t\"persistent\":      persistent,\n\t\t\"runtime_sources\": runtimeClients,\n\t}\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif err != nil {\n\t\treturn err\n\t} else if !ok {\n\t\treturn nil\n\t}\n\n\treturn moveVal[bool](dns, runtimeClients, \"resolve_clients\", \"rdns\")\n}\n"
  },
  {
    "path": "internal/configmigrate/v15.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// migrateTo15 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 14\n//\t'dns':\n//\t  # …\n//\t  'querylog_enabled': true\n//\t  'querylog_file_enabled': true\n//\t  'querylog_interval': '2160h'\n//\t  'querylog_size_memory': 1000\n//\t'querylog':\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 15\n//\t'dns':\n//\t  # …\n//\t'querylog':\n//\t  'enabled': true\n//\t  'file_enabled': true\n//\t  'interval': '2160h'\n//\t  'size_memory': 1000\n//\t  'ignored': []\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo15(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 15\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tqlog := map[string]any{\n\t\t\"ignored\":      yarr{},\n\t\t\"enabled\":      true,\n\t\t\"file_enabled\": true,\n\t\t\"interval\":     \"2160h\",\n\t\t\"size_memory\":  1000,\n\t}\n\tdiskConf[\"querylog\"] = qlog\n\n\treturn errors.Join(\n\t\tmoveVal[bool](dns, qlog, \"querylog_enabled\", \"enabled\"),\n\t\tmoveVal[bool](dns, qlog, \"querylog_file_enabled\", \"file_enabled\"),\n\t\tmoveVal[any](dns, qlog, \"querylog_interval\", \"interval\"),\n\t\tmoveVal[int](dns, qlog, \"querylog_size_memory\", \"size_memory\"),\n\t)\n}\n"
  },
  {
    "path": "internal/configmigrate/v16.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo16 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 15\n//\t'dns':\n//\t  # …\n//\t  'statistics_interval': 1\n//\t'statistics':\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 16\n//\t'dns':\n//\t  # …\n//\t'statistics':\n//\t  'enabled': true\n//\t  'interval': 1\n//\t  'ignored': []\n//\t  # …\n//\t# …\n//\n// If statistics were disabled:\n//\n//\t# BEFORE:\n//\t'schema_version': 15\n//\t'dns':\n//\t  # …\n//\t  'statistics_interval': 0\n//\t'statistics':\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 16\n//\t'dns':\n//\t  # …\n//\t'statistics':\n//\t  'enabled': false\n//\t  'interval': 1\n//\t  'ignored': []\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo16(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 16\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tstats := yobj{\n\t\t\"enabled\":  true,\n\t\t\"interval\": 1,\n\t\t\"ignored\":  yarr{},\n\t}\n\tdiskConf[\"statistics\"] = stats\n\n\tconst field = \"statistics_interval\"\n\n\tstatsIvl, ok, err := fieldVal[int](dns, field)\n\tif !ok {\n\t\treturn err\n\t}\n\n\tif statsIvl == 0 {\n\t\t// Set the interval to the default value of one day to make sure\n\t\t// that it passes the validations.\n\t\tstats[\"enabled\"] = false\n\t} else {\n\t\tstats[\"interval\"] = statsIvl\n\t}\n\tdelete(dns, field)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v17.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo17 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 16\n//\t'dns':\n//\t  'edns_client_subnet': false\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 17\n//\t'dns':\n//\t  'edns_client_subnet':\n//\t    'enabled': false\n//\t    'use_custom': false\n//\t    'custom_ip': \"\"\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo17(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 17\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tconst field = \"edns_client_subnet\"\n\n\tenabled, _, _ := fieldVal[bool](dns, field)\n\tdns[field] = yobj{\n\t\t\"enabled\":    enabled,\n\t\t\"use_custom\": false,\n\t\t\"custom_ip\":  \"\",\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v18.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo18 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 17\n//\t'dns':\n//\t  'safesearch_enabled': true\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 18\n//\t'dns':\n//\t  'safe_search':\n//\t    'enabled': true\n//\t    'bing': true\n//\t    'duckduckgo': true\n//\t    'google': true\n//\t    'pixabay': true\n//\t    'yandex': true\n//\t    'youtube': true\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo18(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 18\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tsafeSearch := yobj{\n\t\t\"enabled\":    true,\n\t\t\"bing\":       true,\n\t\t\"duckduckgo\": true,\n\t\t\"google\":     true,\n\t\t\"pixabay\":    true,\n\t\t\"yandex\":     true,\n\t\t\"youtube\":    true,\n\t}\n\tdns[\"safe_search\"] = safeSearch\n\n\treturn moveVal[bool](dns, safeSearch, \"safesearch_enabled\", \"enabled\")\n}\n"
  },
  {
    "path": "internal/configmigrate/v19.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// migrateTo19 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 18\n//\t'clients':\n//\t  'persistent':\n//\t  - 'name': 'client-name'\n//\t    'safesearch_enabled': true\n//\t    # …\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 19\n//\t'clients':\n//\t  'persistent':\n//\t  - 'name': 'client-name'\n//\t    'safe_search':\n//\t      'enabled': true\n//\t\t  'bing': true\n//\t\t  'duckduckgo': true\n//\t\t  'google': true\n//\t\t  'pixabay': true\n//\t\t  'yandex': true\n//\t\t  'youtube': true\n//\t    # …\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo19(ctx context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 19\n\n\tclients, ok, err := fieldVal[yobj](diskConf, \"clients\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tpersistent, ok, _ := fieldVal[yarr](clients, \"persistent\")\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tfor _, p := range persistent {\n\t\tvar c yobj\n\t\tc, ok = p.(yobj)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tsafeSearch := yobj{\n\t\t\t\"enabled\":    true,\n\t\t\t\"bing\":       true,\n\t\t\t\"duckduckgo\": true,\n\t\t\t\"google\":     true,\n\t\t\t\"pixabay\":    true,\n\t\t\t\"yandex\":     true,\n\t\t\t\"youtube\":    true,\n\t\t}\n\n\t\terr = moveVal[bool](c, safeSearch, \"safesearch_enabled\", \"enabled\")\n\t\tif err != nil {\n\t\t\tm.logger.DebugContext(ctx, \"migrating to\", \"version\", 19, slogutil.KeyError, err)\n\t\t}\n\n\t\tc[\"safe_search\"] = safeSearch\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v2.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// migrateTo2 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 1\n//\t'coredns':\n//\t  # …\n//\n//\t# AFTER:\n//\t'schema_version': 2\n//\t'dns':\n//\t  # …\n//\n// It also deletes the Corefile file, since it isn't used anymore.\nfunc (m *Migrator) migrateTo2(ctx context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 2\n\n\tcoreFilePath := filepath.Join(m.workingDir, \"Corefile\")\n\tm.logger.InfoContext(ctx, \"deleting file as we do not need it anymore\", \"path\", coreFilePath)\n\terr = os.Remove(coreFilePath)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tm.logger.WarnContext(ctx, \"failed to delete\", slogutil.KeyError, err)\n\n\t\t// Go on.\n\t}\n\n\treturn moveVal[any](diskConf, diskConf, \"coredns\", \"dns\")\n}\n"
  },
  {
    "path": "internal/configmigrate/v20.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// migrateTo20 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 19\n//\t'statistics':\n//\t  'interval': 1\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 20\n//\t'statistics':\n//\t  'interval': 24h\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo20(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 20\n\n\tstats, ok, err := fieldVal[yobj](diskConf, \"statistics\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tconst field = \"interval\"\n\n\tivl, ok, err := fieldVal[int](stats, field)\n\tif err != nil {\n\t\treturn err\n\t} else if !ok || ivl == 0 {\n\t\tivl = 1\n\t}\n\n\tstats[field] = timeutil.Duration(time.Duration(ivl) * timeutil.Day)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v21.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo21 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 20\n//\t'dns':\n//\t  'blocked_services':\n//\t  - 'svc_name'\n//\t  - # …\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 21\n//\t'dns':\n//\t  'blocked_services':\n//\t    'ids':\n//\t    - 'svc_name'\n//\t    - # …\n//\t    'schedule':\n//\t      'time_zone': 'Local'\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo21(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 21\n\n\tconst field = \"blocked_services\"\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tsvcs := yobj{\n\t\t\"schedule\": yobj{\n\t\t\t\"time_zone\": \"Local\",\n\t\t},\n\t}\n\n\terr = moveVal[yarr](dns, svcs, field, \"ids\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdns[field] = svcs\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v22.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// migrateTo22 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 21\n//\t'persistent':\n//\t  - 'name': 'client_name'\n//\t    'blocked_services':\n//\t    - 'svc_name'\n//\t    - # …\n//\t    # …\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 22\n//\t'persistent':\n//\t  - 'name': 'client_name'\n//\t    'blocked_services':\n//\t      'ids':\n//\t      - 'svc_name'\n//\t      - # …\n//\t      'schedule':\n//\t        'time_zone': 'Local'\n//\t    # …\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo22(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 22\n\n\tconst field = \"blocked_services\"\n\n\tclients, ok, err := fieldVal[yobj](diskConf, \"clients\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tpersistent, ok, err := fieldVal[yarr](clients, \"persistent\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tfor i, p := range persistent {\n\t\tvar c yobj\n\t\tc, ok = p.(yobj)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"persistent client at index %d: unexpected type %T\", i, p)\n\t\t}\n\n\t\tvar services yarr\n\t\tservices, ok, err = fieldVal[yarr](c, field)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"persistent client at index %d: %w\", i, err)\n\t\t} else if !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tc[field] = yobj{\n\t\t\t\"ids\": services,\n\t\t\t\"schedule\": yobj{\n\t\t\t\t\"time_zone\": \"Local\",\n\t\t\t},\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v23.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// migrateTo23 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 22\n//\t'bind_host': '1.2.3.4'\n//\t'bind_port': 8080\n//\t'web_session_ttl': 720\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 23\n//\t'http':\n//\t  'address': '1.2.3.4:8080'\n//\t  'session_ttl': '720h'\n//\t# …\nfunc (m *Migrator) migrateTo23(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 23\n\n\tbindHost, ok, err := fieldVal[string](diskConf, \"bind_host\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tbindHostAddr, err := netip.ParseAddr(bindHost)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid bind_host value: %s\", bindHost)\n\t}\n\n\tbindPort, _, err := fieldVal[int](diskConf, \"bind_port\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsessionTTL, _, err := fieldVal[int](diskConf, \"web_session_ttl\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdiskConf[\"http\"] = yobj{\n\t\t\"address\":     netip.AddrPortFrom(bindHostAddr, uint16(bindPort)).String(),\n\t\t\"session_ttl\": timeutil.Duration(time.Duration(sessionTTL) * time.Hour).String(),\n\t}\n\n\tdelete(diskConf, \"bind_host\")\n\tdelete(diskConf, \"bind_port\")\n\tdelete(diskConf, \"web_session_ttl\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v24.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// migrateTo24 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 23\n//\t'log_file': \"\"\n//\t'log_max_backups': 0\n//\t'log_max_size': 100\n//\t'log_max_age': 3\n//\t'log_compress': false\n//\t'log_localtime': false\n//\t'verbose': false\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 24\n//\t'log':\n//\t  'file': \"\"\n//\t  'max_backups': 0\n//\t  'max_size': 100\n//\t  'max_age': 3\n//\t  'compress': false\n//\t  'local_time': false\n//\t  'verbose': false\n//\t# …\nfunc (m *Migrator) migrateTo24(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 24\n\n\tlogObj := yobj{}\n\terr = errors.Join(\n\t\tmoveVal[string](diskConf, logObj, \"log_file\", \"file\"),\n\t\tmoveVal[int](diskConf, logObj, \"log_max_backups\", \"max_backups\"),\n\t\tmoveVal[int](diskConf, logObj, \"log_max_size\", \"max_size\"),\n\t\tmoveVal[int](diskConf, logObj, \"log_max_age\", \"max_age\"),\n\t\tmoveVal[bool](diskConf, logObj, \"log_compress\", \"compress\"),\n\t\tmoveVal[bool](diskConf, logObj, \"log_localtime\", \"local_time\"),\n\t\tmoveVal[bool](diskConf, logObj, \"verbose\", \"verbose\"),\n\t)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif len(logObj) != 0 {\n\t\tdiskConf[\"log\"] = logObj\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v25.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo25 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 24\n//\t'debug_pprof': true\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 25\n//\t'http':\n//\t  'pprof':\n//\t    'enabled': true\n//\t    'port': 6060\n//\t# …\nfunc (m *Migrator) migrateTo25(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 25\n\n\thttpObj, ok, err := fieldVal[yobj](diskConf, \"http\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tpprofObj := yobj{\n\t\t\"enabled\": false,\n\t\t\"port\":    6060,\n\t}\n\n\terr = moveVal[bool](diskConf, pprofObj, \"debug_pprof\", \"enabled\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpObj[\"pprof\"] = pprofObj\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v26.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// migrateTo26 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 25\n//\t'dns':\n//\t  'filtering_enabled': true\n//\t  'filters_update_interval': 24\n//\t  'parental_enabled': false\n//\t  'safebrowsing_enabled': false\n//\t  'safebrowsing_cache_size': 1048576\n//\t  'safesearch_cache_size': 1048576\n//\t  'parental_cache_size': 1048576\n//\t  'safe_search':\n//\t    'enabled': false\n//\t    'bing': true\n//\t    'duckduckgo': true\n//\t    'google': true\n//\t    'pixabay': true\n//\t    'yandex': true\n//\t    'youtube': true\n//\t  'rewrites': []\n//\t  'blocked_services':\n//\t    'schedule':\n//\t      'time_zone': 'Local'\n//\t    'ids': []\n//\t  'protection_enabled':        true,\n//\t  'blocking_mode':             'custom_ip',\n//\t  'blocking_ipv4':             '1.2.3.4',\n//\t  'blocking_ipv6':             '1:2:3::4',\n//\t  'blocked_response_ttl':      10,\n//\t  'protection_disabled_until': 'null',\n//\t  'parental_block_host':       'p.dns.adguard.com',\n//\t  'safebrowsing_block_host':   's.dns.adguard.com',\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 26\n//\t'filtering':\n//\t  'filtering_enabled': true\n//\t  'filters_update_interval': 24\n//\t  'parental_enabled': false\n//\t  'safebrowsing_enabled': false\n//\t  'safebrowsing_cache_size': 1048576\n//\t  'safesearch_cache_size': 1048576\n//\t  'parental_cache_size': 1048576\n//\t  'safe_search':\n//\t    'enabled': false\n//\t    'bing': true\n//\t    'duckduckgo': true\n//\t    'google': true\n//\t    'pixabay': true\n//\t    'yandex': true\n//\t    'youtube': true\n//\t  'rewrites': []\n//\t  'blocked_services':\n//\t    'schedule':\n//\t      'time_zone': 'Local'\n//\t    'ids': []\n//\t  'protection_enabled':        true,\n//\t  'blocking_mode':             'custom_ip',\n//\t  'blocking_ipv4':             '1.2.3.4',\n//\t  'blocking_ipv6':             '1:2:3::4',\n//\t  'blocked_response_ttl':      10,\n//\t  'protection_disabled_until': 'null',\n//\t  'parental_block_host':       'p.dns.adguard.com',\n//\t  'safebrowsing_block_host':   's.dns.adguard.com',\n//\t'dns'\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo26(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 26\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tfilteringObj := yobj{}\n\terr = errors.Join(\n\t\tmoveSameVal[bool](dns, filteringObj, \"filtering_enabled\"),\n\t\tmoveSameVal[int](dns, filteringObj, \"filters_update_interval\"),\n\t\tmoveSameVal[bool](dns, filteringObj, \"parental_enabled\"),\n\t\tmoveSameVal[bool](dns, filteringObj, \"safebrowsing_enabled\"),\n\t\tmoveSameVal[int](dns, filteringObj, \"safebrowsing_cache_size\"),\n\t\tmoveSameVal[int](dns, filteringObj, \"safesearch_cache_size\"),\n\t\tmoveSameVal[int](dns, filteringObj, \"parental_cache_size\"),\n\t\tmoveSameVal[yobj](dns, filteringObj, \"safe_search\"),\n\t\tmoveSameVal[yarr](dns, filteringObj, \"rewrites\"),\n\t\tmoveSameVal[yobj](dns, filteringObj, \"blocked_services\"),\n\t\tmoveSameVal[bool](dns, filteringObj, \"protection_enabled\"),\n\t\tmoveSameVal[string](dns, filteringObj, \"blocking_mode\"),\n\t\tmoveSameVal[string](dns, filteringObj, \"blocking_ipv4\"),\n\t\tmoveSameVal[string](dns, filteringObj, \"blocking_ipv6\"),\n\t\tmoveSameVal[int](dns, filteringObj, \"blocked_response_ttl\"),\n\t\tmoveSameVal[any](dns, filteringObj, \"protection_disabled_until\"),\n\t\tmoveSameVal[string](dns, filteringObj, \"parental_block_host\"),\n\t\tmoveSameVal[string](dns, filteringObj, \"safebrowsing_block_host\"),\n\t)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif len(filteringObj) != 0 {\n\t\tdiskConf[\"filtering\"] = filteringObj\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v27.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo27 performs the following changes:\n//\n//\t# BEFORE:\n//\t'querylog':\n//\t  'ignored':\n//\t  - '.'\n//\t  - # …\n//\t  # …\n//\t'statistics':\n//\t  'ignored':\n//\t  - '.'\n//\t  - # …\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'querylog':\n//\t  'ignored':\n//\t  - '|.^'\n//\t  - # …\n//\t  # …\n//\t'statistics':\n//\t  'ignored':\n//\t  - '|.^'\n//\t  - # …\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo27(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 27\n\n\tkeys := []string{\"querylog\", \"statistics\"}\n\tfor _, k := range keys {\n\t\terr = replaceDot(diskConf, k)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// replaceDot replaces rules blocking root domain \".\" with AdBlock style syntax\n// \"|.^\".\nfunc replaceDot(diskConf yobj, key string) (err error) {\n\tvar obj yobj\n\tvar ok bool\n\tobj, ok, err = fieldVal[yobj](diskConf, key)\n\tif err != nil {\n\t\treturn err\n\t} else if !ok {\n\t\treturn nil\n\t}\n\n\tvar ignored yarr\n\tignored, ok, err = fieldVal[yarr](obj, \"ignored\")\n\tif err != nil {\n\t\treturn err\n\t} else if !ok {\n\t\treturn nil\n\t}\n\n\tfor i, hostVal := range ignored {\n\t\tvar host string\n\t\thost, ok = hostVal.(string)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif host == \".\" {\n\t\t\tignored[i] = \"|.^\"\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v28.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dnsforward\"\n)\n\n// migrateTo28 performs the following changes:\n//\n//\t# BEFORE:\n//\t'dns':\n//\t  'all_servers': true\n//\t  'fastest_addr': true\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'dns':\n//\t  'upstream_mode': 'parallel'\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo28(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 28\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tallServers, _, _ := fieldVal[bool](dns, \"all_servers\")\n\tfastestAddr, _, _ := fieldVal[bool](dns, \"fastest_addr\")\n\n\tvar upstreamModeType dnsforward.UpstreamMode\n\tif allServers {\n\t\tupstreamModeType = dnsforward.UpstreamModeParallel\n\t} else if fastestAddr {\n\t\tupstreamModeType = dnsforward.UpstreamModeFastestAddr\n\t} else {\n\t\tupstreamModeType = dnsforward.UpstreamModeLoadBalance\n\t}\n\n\tdns[\"upstream_mode\"] = upstreamModeType\n\n\tdelete(dns, \"all_servers\")\n\tdelete(dns, \"fastest_addr\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v29.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n)\n\n// migrateTo29 performs the following changes:\n//\n//\t# BEFORE:\n//\t'filters':\n//\t  - 'enabled': true\n//\t    'url': /path/to/file.txt\n//\t    'name': My FS Filter\n//\t    'id': 1234\n//\n//\t# AFTER:\n//\t'filters':\n//\t  - 'enabled': true\n//\t    'url': /path/to/file.txt\n//\t    'name': My FS Filter\n//\t    'id': 1234\n//\t# …\n//\t'filtering':\n//\t  'safe_fs_patterns':\n//\t    - '/opt/AdGuardHome/data/userfilters/*'\n//\t    - '/path/to/file.txt'\n//\t  # …\nfunc (m Migrator) migrateTo29(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 29\n\n\tfilterVals, ok, err := fieldVal[[]any](diskConf, \"filters\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tpaths := []string{\n\t\tfilepath.Join(m.dataDir, \"userfilters\", \"*\"),\n\t}\n\n\tfor i, v := range filterVals {\n\t\tvar f yobj\n\t\tf, ok = v.(yobj)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"filters: at index %d: expected object, got %T\", i, v)\n\t\t}\n\n\t\tvar u string\n\t\tu, ok, _ = fieldVal[string](f, \"url\")\n\t\tif ok && filepath.IsAbs(u) {\n\t\t\tpaths = append(paths, u)\n\t\t}\n\t}\n\n\tfltConf, ok, err := fieldVal[yobj](diskConf, \"filtering\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tfltConf[\"safe_fs_patterns\"] = paths\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v3.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo3 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 2\n//\t'dns':\n//\t  'bootstrap_dns': '1.1.1.1'\n//\t  # …\n//\n//\t# AFTER:\n//\t'schema_version': 3\n//\t'dns':\n//\t  'bootstrap_dns':\n//\t  - '1.1.1.1'\n//\t  # …\nfunc (m *Migrator) migrateTo3(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 3\n\n\tdnsConfig, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tbootstrapDNS, ok, err := fieldVal[any](dnsConfig, \"bootstrap_dns\")\n\tif ok {\n\t\tdnsConfig[\"bootstrap_dns\"] = yarr{bootstrapDNS}\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "internal/configmigrate/v30.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo30 performs the following changes:\n//\n//\t# BEFORE:\n//\t'dns':\n//\t  'cache_size': 123456\n//\t  # …\n//\n//\t# AFTER:\n//\t'dns':\n//\t  'cache_size': 123456\n//\t  'cache_enabled': true\n//\t  # …\n//\n// If cache_size is zero, then cache_enabled should be false.\nfunc (m Migrator) migrateTo30(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 30\n\n\tdnsConf, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tcacheSize, ok, err := fieldVal[int](dnsConf, \"cache_size\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tdnsConf[\"cache_enabled\"] = cacheSize > 0\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v31.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo31 performs the following changes:\n//\n//\t# BEFORE:\n//\t'filtering':\n//\t  'rewrites':\n//\t    - 'domain': test.example\n//\t      'answer': 192.0.2.0\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'filtering':\n//\t  'rewrites':\n//\t    - 'domain': test.example\n//\t      'answer': 192.0.2.0\n//\t      'enabled': true\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo31(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 31\n\n\tfltConf, ok, err := fieldVal[yobj](diskConf, \"filtering\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\trewrites, ok, err := fieldVal[yarr](fltConf, \"rewrites\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tfor i := range rewrites {\n\t\tif r, isYobj := rewrites[i].(yobj); isYobj {\n\t\t\tr[\"enabled\"] = true\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v32.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo32 performs the following changes:\n//\n//\t# BEFORE:\n//\t'dns':\n//\t  'cache_enabled': true\n//\t  'cache_optimistic': true\n//\t  # …\n//\n//\t# AFTER:\n//\t'dns':\n//\t  'cache_enabled': true\n//\t  'cache_optimistic': true\n//\t  'cache_optimistic_answer_ttl': '30s'\n//\t  'cache_optimistic_max_age': '12h'\n//\t  # …\n//\n// If cache_size is zero, then cache_enabled should be false.\nfunc (m Migrator) migrateTo32(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 32\n\n\tdnsConf, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tdnsConf[\"cache_optimistic_answer_ttl\"] = \"30s\"\n\tdnsConf[\"cache_optimistic_max_age\"] = \"12h\"\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v33.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo33 performs the following changes:\n//\n//\t# BEFORE:\n//\t'querylog':\n//\t  # …\n//\t  'ignored':\n//\t  - '|.^'\n//\t'statistics':\n//\t  # …\n//\t  'ignored':\n//\t  - '|.^'\n//\n//\t# AFTER:\n//\t'querylog':\n//\t  # …\n//\t  'ignored':\n//\t  - '|.^'\n//\t  'ignored_enabled': true\n//\t'statistics':\n//\t  # …\n//\t  'ignored':\n//\t  - '|.^'\n//\t  'ignored_enabled': true\n//\n// If ignored is empty, then ignored_enabled should be false.\nfunc (m Migrator) migrateTo33(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 33\n\n\tqueryLogConf, ok, err := fieldVal[yobj](diskConf, \"querylog\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tignored, ok, err := fieldVal[yarr](queryLogConf, \"ignored\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tqueryLogConf[\"ignored_enabled\"] = len(ignored) > 0\n\n\tstatisticsConf, ok, err := fieldVal[yobj](diskConf, \"statistics\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tignored, ok, err = fieldVal[yarr](statisticsConf, \"ignored\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tstatisticsConf[\"ignored_enabled\"] = len(ignored) > 0\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v4.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo4 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 3\n//\t'clients':\n//\t- # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 4\n//\t'clients':\n//\t- 'use_global_blocked_services': true\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo4(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 4\n\n\tclients, ok, _ := fieldVal[yarr](diskConf, \"clients\")\n\tif ok {\n\t\tfor i := range clients {\n\t\t\tif c, isYobj := clients[i].(yobj); isYobj {\n\t\t\t\tc[\"use_global_blocked_services\"] = true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v5.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// migrateTo5 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 4\n//\t'auth_name': …\n//\t'auth_pass': …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 5\n//\t'users':\n//\t- 'name': …\n//\t  'password': <hashed>\n//\t# …\nfunc (m *Migrator) migrateTo5(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 5\n\n\tuser := yobj{}\n\n\tif err = moveVal[string](diskConf, user, \"auth_name\", \"name\"); err != nil {\n\t\treturn err\n\t}\n\n\tpass, ok, err := fieldVal[string](diskConf, \"auth_pass\")\n\tif !ok {\n\t\treturn err\n\t}\n\tdelete(diskConf, \"auth_pass\")\n\n\thash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"generating password hash: %w\", err)\n\t}\n\n\tuser[\"password\"] = string(hash)\n\tdiskConf[\"users\"] = yarr{user}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v6.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// migrateTo6 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 5\n//\t'clients':\n//\t - # …\n//\t   'ip': '127.0.0.1'\n//\t   'mac': 'AA:AA:AA:AA:AA:AA'\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 6\n//\t'clients':\n//\t - # …\n//\t   'ip': '127.0.0.1'\n//\t   'mac': 'AA:AA:AA:AA:AA:AA'\n//\t   'ids':\n//\t   - '127.0.0.1'\n//\t   - 'AA:AA:AA:AA:AA:AA'\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo6(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 6\n\n\tclients, ok, err := fieldVal[yarr](diskConf, \"clients\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tfor i, client := range clients {\n\t\tvar c yobj\n\t\tc, ok = client.(yobj)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unexpected type of client at index %d: %T\", i, client)\n\t\t}\n\n\t\tids := yarr{}\n\t\tfor _, id := range []string{\"ip\", \"mac\"} {\n\t\t\tval, _, valErr := fieldVal[string](c, id)\n\t\t\tif valErr != nil {\n\t\t\t\treturn fmt.Errorf(\"client at index %d: %w\", i, valErr)\n\t\t\t} else if val != \"\" {\n\t\t\t\tids = append(ids, val)\n\t\t\t}\n\t\t}\n\n\t\tc[\"ids\"] = ids\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v7.go",
    "content": "package configmigrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// migrateTo7 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 6\n//\t'dhcp':\n//\t  'enabled': false\n//\t  'interface_name': vboxnet0\n//\t  'gateway_ip': '192.168.56.1'\n//\t  'subnet_mask': '255.255.255.0'\n//\t  'range_start': '192.168.56.10'\n//\t  'range_end': '192.168.56.240'\n//\t  'lease_duration': 86400\n//\t  'icmp_timeout_msec': 1000\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 7\n//\t'dhcp':\n//\t  'enabled': false\n//\t  'interface_name': vboxnet0\n//\t  'dhcpv4':\n//\t    'gateway_ip': '192.168.56.1'\n//\t    'subnet_mask': '255.255.255.0'\n//\t    'range_start': '192.168.56.10'\n//\t    'range_end': '192.168.56.240'\n//\t    'lease_duration': 86400\n//\t    'icmp_timeout_msec': 1000\n//\t# …\nfunc (m *Migrator) migrateTo7(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 7\n\n\tdhcp, ok, _ := fieldVal[yobj](diskConf, \"dhcp\")\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tdhcpv4 := yobj{}\n\terr = errors.Join(\n\t\tmoveSameVal[string](dhcp, dhcpv4, \"gateway_ip\"),\n\t\tmoveSameVal[string](dhcp, dhcpv4, \"subnet_mask\"),\n\t\tmoveSameVal[string](dhcp, dhcpv4, \"range_start\"),\n\t\tmoveSameVal[string](dhcp, dhcpv4, \"range_end\"),\n\t\tmoveSameVal[int](dhcp, dhcpv4, \"lease_duration\"),\n\t\tmoveSameVal[int](dhcp, dhcpv4, \"icmp_timeout_msec\"),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdhcp[\"dhcpv4\"] = dhcpv4\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v8.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo8 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 7\n//\t'dns':\n//\t  'bind_host': '127.0.0.1'\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 8\n//\t'dns':\n//\t  'bind_hosts':\n//\t  - '127.0.0.1'\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo8(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 8\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tbindHost, ok, err := fieldVal[string](dns, \"bind_host\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\tdelete(dns, \"bind_host\")\n\tdns[\"bind_hosts\"] = yarr{bindHost}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/configmigrate/v9.go",
    "content": "package configmigrate\n\nimport \"context\"\n\n// migrateTo9 performs the following changes:\n//\n//\t# BEFORE:\n//\t'schema_version': 8\n//\t'dns':\n//\t  'autohost_tld': 'lan'\n//\t  # …\n//\t# …\n//\n//\t# AFTER:\n//\t'schema_version': 9\n//\t'dns':\n//\t  'local_domain_name': 'lan'\n//\t  # …\n//\t# …\nfunc (m *Migrator) migrateTo9(_ context.Context, diskConf yobj) (err error) {\n\tdiskConf[\"schema_version\"] = 9\n\n\tdns, ok, err := fieldVal[yobj](diskConf, \"dns\")\n\tif !ok {\n\t\treturn err\n\t}\n\n\treturn moveVal[string](dns, dns, \"autohost_tld\", \"local_domain_name\")\n}\n"
  },
  {
    "path": "internal/configmigrate/yaml.go",
    "content": "package configmigrate\n\nimport (\n\t\"fmt\"\n)\n\ntype (\n\t// yarr is the convenience alias for YAML array.\n\tyarr = []any\n\n\t// yobj is the convenience alias for YAML key-value object.\n\tyobj = map[string]any\n)\n\n// fieldVal returns the value of type T for key from obj.  Use [any] if the\n// field's type doesn't matter.\nfunc fieldVal[T any](obj yobj, key string) (v T, ok bool, err error) {\n\tval, ok := obj[key]\n\tif !ok {\n\t\treturn v, false, nil\n\t}\n\n\tif val == nil {\n\t\treturn v, true, nil\n\t}\n\n\tv, ok = val.(T)\n\tif !ok {\n\t\treturn v, false, fmt.Errorf(\"unexpected type of %q: %T\", key, val)\n\t}\n\n\treturn v, true, nil\n}\n\n// moveVal copies the value for srcKey from src into dst for dstKey and deletes\n// it from src.\nfunc moveVal[T any](src, dst yobj, srcKey, dstKey string) (err error) {\n\tnewVal, ok, err := fieldVal[T](src, srcKey)\n\tif !ok {\n\t\treturn err\n\t}\n\n\tdst[dstKey] = newVal\n\tdelete(src, srcKey)\n\n\treturn nil\n}\n\n// moveSameVal moves the value for key from src into dst.\nfunc moveSameVal[T any](src, dst yobj, key string) (err error) {\n\treturn moveVal[T](src, dst, key, key)\n}\n"
  },
  {
    "path": "internal/dhcpd/README.md",
    "content": "# Testing DHCP Server\n\nContents:\n\n- [Test setup with Virtual Box](#vbox)\n- [Quick test with DHCPTest](#dhcptest)\n\n## <a href=\"#vbox\" id=\"vbox\" name=\"vbox\">Test setup with Virtual Box</a>\n\n### Prerequisites\n\nTo set up a test environment for DHCP server you will need:\n\n- Linux AG Home host machine (Virtual)\n- Virtual Box\n- Virtual machine (guest OS doesn't matter)\n\n### Configure Virtual Box\n\n1. Install Virtual Box and run the following command to create a Host-Only network:\n\n    ```sh\n    VBoxManage hostonlyif create\n    ```\n\n    You can check its status by `ip a` command.\n\n    You can also set up Host-Only network using Virtual Box menu in *File → Host Network Manager.*\n\n2. Create your virtual machine and set up its network in *VM Settings → Network → Host-only Adapter.*\n\n3. Start your VM, install an OS. Configure your network interface to use DHCP and the OS should ask for a IP address from our DHCP server.\n\n4. To see the current IP addresses on client OS you can use `ip a` command on Linux or `ipconfig` on Windows.\n\n5. To force the client OS to request an IP from DHCP server again, you can use `dhclient` on Linux or `ipconfig /release` on Windows.\n\n### Configure server\n\n1. Edit server configuration file `AdGuardHome.yaml`, for example:\n\n    ```yaml\n    dhcp:\n        enabled: true\n        interface_name: vboxnet0\n        local_domain_name: lan\n        dhcpv4:\n            gateway_ip: 192.168.56.1\n            subnet_mask: 255.255.255.0\n            range_start: 192.168.56.2\n            range_end: 192.168.56.2\n            lease_duration: 86400\n            icmp_timeout_msec: 1000\n            options: []\n        dhcpv6:\n            range_start: 2001::1\n            lease_duration: 86400\n            ra_slaac_only: false\n            ra_allow_slaac: false\n    ```\n\n2. Start the server:\n\n    ```sh\n    ./AdGuardHome -v\n    ```\n\n    There should be a message in log which shows that DHCP server is ready:\n\n    ```none\n    [info] dhcpv4: listening\n    ```\n\n## <a href=\"#dhcptest\" id=\"dhcptest\" name=\"dhcptest\">Quick test with DHCPTest utility</a>\n\n### Prerequisites\n\n- [DHCP test utility][dhcptest-gh].\n\n### Quick test\n\nThe DHCP server could be tested for DISCOVER-OFFER packets with in interactive mode.\n\n[dhcptest-gh]: https://github.com/CyberShadow/dhcptest\n"
  },
  {
    "path": "internal/dhcpd/bitset.go",
    "content": "package dhcpd\n\nconst bitsPerWord = 64\n\n// bitSet is a sparse bitSet.  A nil *bitSet is an empty bitSet.\ntype bitSet struct {\n\twords map[uint64]uint64\n}\n\n// newBitSet returns a new bitset.\nfunc newBitSet() (s *bitSet) {\n\treturn &bitSet{\n\t\twords: map[uint64]uint64{},\n\t}\n}\n\n// isSet returns true if the bit n is set.\nfunc (s *bitSet) isSet(n uint64) (ok bool) {\n\tif s == nil {\n\t\treturn false\n\t}\n\n\twordIdx := n / bitsPerWord\n\tbitIdx := n % bitsPerWord\n\n\tvar word uint64\n\tword, ok = s.words[wordIdx]\n\n\treturn ok && word&(1<<bitIdx) != 0\n}\n\n// set sets or unsets a bit.\nfunc (s *bitSet) set(n uint64, ok bool) {\n\tif s == nil {\n\t\treturn\n\t}\n\n\twordIdx := n / bitsPerWord\n\tbitIdx := n % bitsPerWord\n\n\tword := s.words[wordIdx]\n\tif ok {\n\t\tword |= 1 << bitIdx\n\t} else {\n\t\tword &^= 1 << bitIdx\n\t}\n\n\ts.words[wordIdx] = word\n}\n"
  },
  {
    "path": "internal/dhcpd/bitset_internal_test.go",
    "content": "package dhcpd\n\nimport (\n\t\"math\"\n\t\"testing\"\n\t\"testing/quick\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBitSet(t *testing.T) {\n\tt.Run(\"nil\", func(t *testing.T) {\n\t\tvar s *bitSet\n\n\t\tok := s.isSet(0)\n\t\tassert.False(t, ok)\n\n\t\tassert.NotPanics(t, func() {\n\t\t\ts.set(0, true)\n\t\t})\n\n\t\tok = s.isSet(0)\n\t\tassert.False(t, ok)\n\n\t\tassert.NotPanics(t, func() {\n\t\t\ts.set(0, false)\n\t\t})\n\n\t\tok = s.isSet(0)\n\t\tassert.False(t, ok)\n\t})\n\n\tt.Run(\"non_nil\", func(t *testing.T) {\n\t\ts := newBitSet()\n\n\t\tok := s.isSet(0)\n\t\tassert.False(t, ok)\n\n\t\ts.set(0, true)\n\n\t\tok = s.isSet(0)\n\t\tassert.True(t, ok)\n\n\t\ts.set(0, false)\n\n\t\tok = s.isSet(0)\n\t\tassert.False(t, ok)\n\t})\n\n\tt.Run(\"non_nil_long\", func(t *testing.T) {\n\t\ts := newBitSet()\n\n\t\ts.set(0, true)\n\t\ts.set(math.MaxUint64, true)\n\t\tassert.Len(t, s.words, 2)\n\n\t\tok := s.isSet(0)\n\t\tassert.True(t, ok)\n\n\t\tok = s.isSet(math.MaxUint64)\n\t\tassert.True(t, ok)\n\t})\n\n\tt.Run(\"compare_to_map\", func(t *testing.T) {\n\t\tm := map[uint64]struct{}{}\n\t\ts := newBitSet()\n\n\t\tmapFunc := func(setNew, checkOld, delOld uint64) (ok bool) {\n\t\t\tm[setNew] = struct{}{}\n\t\t\tdelete(m, delOld)\n\t\t\t_, ok = m[checkOld]\n\n\t\t\treturn ok\n\t\t}\n\n\t\tsetFunc := func(setNew, checkOld, delOld uint64) (ok bool) {\n\t\t\ts.set(setNew, true)\n\t\t\ts.set(delOld, false)\n\t\t\tok = s.isSet(checkOld)\n\n\t\t\treturn ok\n\t\t}\n\n\t\terr := quick.CheckEqual(mapFunc, setFunc, &quick.Config{\n\t\t\tMaxCount:      10_000,\n\t\t\tMaxCountScale: 10,\n\t\t})\n\t\tassert.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "internal/dhcpd/broadcast_bsd.go",
    "content": "//go:build freebsd || openbsd\n\npackage dhcpd\n\nimport (\n\t\"net\"\n)\n\n// broadcast sends resp to the broadcast address specific for network interface.\nfunc (c *dhcpConn) broadcast(respData []byte, peer *net.UDPAddr) (n int, err error) {\n\t// Despite the fact that server4.NewIPv4UDPConn explicitly sets socket\n\t// options to allow broadcasting, it also binds the connection to a specific\n\t// interface.  On FreeBSD and OpenBSD net.UDPConn.WriteTo causes errors\n\t// while writing to the addresses that belong to another interface.  So, use\n\t// the broadcast address specific for the interface bound.\n\tpeer.IP = c.bcastIP\n\n\treturn c.udpConn.WriteTo(respData, peer)\n}\n"
  },
  {
    "path": "internal/dhcpd/broadcast_bsd_internal_test.go",
    "content": "//go:build freebsd || openbsd\n\npackage dhcpd\n\nimport (\n\t\"bytes\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDHCPConn_Broadcast(t *testing.T) {\n\tb := &bytes.Buffer{}\n\tvar peer *net.UDPAddr\n\n\tudpConn := &fakePacketConn{\n\t\twriteTo: func(p []byte, addr net.Addr) (n int, err error) {\n\t\t\tudpPeer, ok := addr.(*net.UDPAddr)\n\t\t\trequire.True(t, ok)\n\n\t\t\tpeer = cloneUDPAddr(udpPeer)\n\n\t\t\tn, err = b.Write(p)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn n, nil\n\t\t},\n\t}\n\tconn := &dhcpConn{\n\t\tudpConn: udpConn,\n\t\tbcastIP: net.IP{1, 2, 3, 255},\n\t}\n\tdefaultPeer := &net.UDPAddr{\n\t\tIP: net.IP{1, 2, 3, 4},\n\t\t// Use neither client nor server port.\n\t\tPort: 1234,\n\t}\n\trespData := (&dhcpv4.DHCPv4{}).ToBytes()\n\n\t_, _ = conn.broadcast(respData, cloneUDPAddr(defaultPeer))\n\n\tassert.EqualValues(t, respData, b.Bytes())\n\tassert.Equal(t, &net.UDPAddr{\n\t\tIP:   conn.bcastIP,\n\t\tPort: defaultPeer.Port,\n\t}, peer)\n}\n"
  },
  {
    "path": "internal/dhcpd/broadcast_others.go",
    "content": "//go:build darwin || linux\n\npackage dhcpd\n\nimport (\n\t\"net\"\n)\n\n// broadcast sends resp to the broadcast address specific for network interface.\nfunc (c *dhcpConn) broadcast(respData []byte, peer *net.UDPAddr) (n int, err error) {\n\t// This write to 0xffffffff reverts some behavior changes made in\n\t// https://github.com/AdguardTeam/AdGuardHome/issues/3289.  The DHCP\n\t// server should broadcast the message to 0xffffffff but it's\n\t// inconsistent with the actual mental model of DHCP implementation\n\t// which requires the network interface selection to bind to.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/3480 and\n\t// https://github.com/AdguardTeam/AdGuardHome/issues/3366.\n\t//\n\t// See also https://github.com/AdguardTeam/AdGuardHome/issues/3539.\n\tif n, err = c.udpConn.WriteTo(respData, peer); err != nil {\n\t\treturn n, err\n\t}\n\n\t// Broadcast the message one more time using the interface-specific\n\t// broadcast address.\n\tpeer.IP = c.bcastIP\n\n\treturn c.udpConn.WriteTo(respData, peer)\n}\n"
  },
  {
    "path": "internal/dhcpd/broadcast_others_internal_test.go",
    "content": "//go:build darwin || linux\n\npackage dhcpd\n\nimport (\n\t\"bytes\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDHCPConn_Broadcast(t *testing.T) {\n\tb := &bytes.Buffer{}\n\tvar peers []*net.UDPAddr\n\n\tudpConn := &fakePacketConn{\n\t\twriteTo: func(p []byte, addr net.Addr) (n int, err error) {\n\t\t\tudpPeer, ok := addr.(*net.UDPAddr)\n\t\t\trequire.True(t, ok)\n\n\t\t\tpeers = append(peers, cloneUDPAddr(udpPeer))\n\n\t\t\tn, err = b.Write(p)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn n, nil\n\t\t},\n\t}\n\tconn := &dhcpConn{\n\t\tudpConn: udpConn,\n\t\tbcastIP: net.IP{1, 2, 3, 255},\n\t}\n\tdefaultPeer := &net.UDPAddr{\n\t\tIP: net.IP{1, 2, 3, 4},\n\t\t// Use neither client nor server port.\n\t\tPort: 1234,\n\t}\n\trespData := (&dhcpv4.DHCPv4{}).ToBytes()\n\n\t_, _ = conn.broadcast(respData, cloneUDPAddr(defaultPeer))\n\n\t// The same response is written twice but for different peers.\n\tassert.EqualValues(t, append(respData, respData...), b.Bytes())\n\n\trequire.Len(t, peers, 2)\n\n\tassert.Equal(t, cloneUDPAddr(defaultPeer), peers[0])\n\tassert.Equal(t, &net.UDPAddr{\n\t\tIP:   conn.bcastIP,\n\t\tPort: defaultPeer.Port,\n\t}, peers[1])\n}\n"
  },
  {
    "path": "internal/dhcpd/config.go",
    "content": "package dhcpd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\n// ServerConfig is the configuration for the DHCP server.  The order of YAML\n// fields is important, since the YAML configuration file follows it.\ntype ServerConfig struct {\n\t// Logger is used for logging the operation of the DHCP server.  It must not\n\t// be nil.\n\tLogger *slog.Logger `yaml:\"-\"`\n\n\t// CommandConstructor is used to run external commands.  It must not be nil.\n\tCommandConstructor executil.CommandConstructor `yaml:\"-\"`\n\n\t// ConfModifier is used to update the global configuration.  It must not be\n\t// nil.\n\tConfModifier agh.ConfigModifier `yaml:\"-\"`\n\n\t// Register an HTTP handler\n\tHTTPReg aghhttp.Registrar `yaml:\"-\"`\n\n\tEnabled       bool   `yaml:\"enabled\"`\n\tInterfaceName string `yaml:\"interface_name\"`\n\n\t// LocalDomainName is the domain name used for DHCP hosts.  For example, a\n\t// DHCP client with the hostname \"myhost\" can be addressed as \"myhost.lan\"\n\t// when LocalDomainName is \"lan\".\n\t//\n\t// TODO(e.burkov):  Probably, remove this field.  See the TODO on\n\t// [Interface.Enabled].\n\tLocalDomainName string `yaml:\"local_domain_name\"`\n\n\tConf4 V4ServerConf `yaml:\"dhcpv4\"`\n\tConf6 V6ServerConf `yaml:\"dhcpv6\"`\n\n\t// WorkDir is used to store DHCP leases.\n\t//\n\t// Deprecated:  Remove it when migration of DHCP leases will not be needed.\n\tWorkDir string `yaml:\"-\"`\n\n\t// DataDir is used to store DHCP leases.\n\tDataDir string `yaml:\"-\"`\n\n\t// dbFilePath is the path to the file with stored DHCP leases.\n\tdbFilePath string `yaml:\"-\"`\n}\n\n// DHCPServer - DHCP server interface\ntype DHCPServer interface {\n\t// ResetLeases resets leases.\n\tResetLeases(leases []*dhcpsvc.Lease) (err error)\n\t// GetLeases returns deep clones of the current leases.\n\tGetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease)\n\t// AddStaticLease - add a static lease\n\tAddStaticLease(l *dhcpsvc.Lease) (err error)\n\t// RemoveStaticLease - remove a static lease\n\tRemoveStaticLease(l *dhcpsvc.Lease) (err error)\n\n\t// UpdateStaticLease updates IP, hostname of the lease.\n\tUpdateStaticLease(l *dhcpsvc.Lease) (err error)\n\n\t// FindMACbyIP returns a MAC address by the IP address of its lease, if\n\t// there is one.\n\tFindMACbyIP(ip netip.Addr) (mac net.HardwareAddr)\n\n\t// HostByIP returns a hostname by the IP address of its lease, if there is\n\t// one.\n\tHostByIP(ip netip.Addr) (host string)\n\n\t// IPByHost returns an IP address by the hostname of its lease, if there is\n\t// one.\n\tIPByHost(host string) (ip netip.Addr)\n\n\t// WriteDiskConfig4 - copy disk configuration\n\tWriteDiskConfig4(c *V4ServerConf)\n\t// WriteDiskConfig6 - copy disk configuration\n\tWriteDiskConfig6(c *V6ServerConf)\n\n\t// Start - start server\n\tStart(ctx context.Context) (err error)\n\t// Stop - stop server\n\tStop() (err error)\n\tgetLeasesRef() []*dhcpsvc.Lease\n}\n\n// V4ServerConf - server configuration\ntype V4ServerConf struct {\n\t// Logger is used for logging the operation of the DHCPv4 server.  It must\n\t// not be nil.\n\tLogger *slog.Logger `yaml:\"-\" json:\"-\"`\n\n\tEnabled       bool   `yaml:\"-\" json:\"-\"`\n\tInterfaceName string `yaml:\"-\" json:\"-\"`\n\n\tGatewayIP  netip.Addr `yaml:\"gateway_ip\" json:\"gateway_ip\"`\n\tSubnetMask netip.Addr `yaml:\"subnet_mask\" json:\"subnet_mask\"`\n\t// broadcastIP is the broadcasting address pre-calculated from the\n\t// configured gateway IP and subnet mask.\n\tbroadcastIP netip.Addr\n\n\t// The first & the last IP address for dynamic leases\n\t// Bytes [0..2] of the last allowed IP address must match the first IP\n\tRangeStart netip.Addr `yaml:\"range_start\" json:\"range_start\"`\n\tRangeEnd   netip.Addr `yaml:\"range_end\" json:\"range_end\"`\n\n\tLeaseDuration uint32 `yaml:\"lease_duration\" json:\"lease_duration\"` // in seconds\n\n\t// IP conflict detector: time (ms) to wait for ICMP reply\n\t// 0: disable\n\tICMPTimeout uint32 `yaml:\"icmp_timeout_msec\" json:\"-\"`\n\n\t// Custom Options.\n\t//\n\t// Option with arbitrary hexadecimal data:\n\t//     DEC_CODE hex HEX_DATA\n\t// where DEC_CODE is a decimal DHCPv4 option code in range [1..255]\n\t//\n\t// Option with IP data (only 1 IP is supported):\n\t//     DEC_CODE ip IP_ADDR\n\tOptions []string `yaml:\"options\" json:\"-\"`\n\n\tipRange *ipRange\n\n\tleaseTime  time.Duration // the time during which a dynamic lease is considered valid\n\tdnsIPAddrs []netip.Addr  // IPv4 addresses to return to DHCP clients as DNS server addresses\n\n\t// subnet contains the DHCP server's subnet.  The IP is the IP of the\n\t// gateway.\n\tsubnet netip.Prefix\n\n\t// notify is a way to signal to other components that leases have been\n\t// changed.  notify must be called outside of locked sections, since the\n\t// clients might want to get the new data.\n\t//\n\t// TODO(a.garipov): This is utter madness and must be refactored.  It just\n\t// begs for deadlock bugs and other nastiness.\n\tnotify func(uint32)\n}\n\n// errNilConfig is an error returned by validation method if the config is nil.\nconst errNilConfig errors.Error = \"nil config\"\n\n// ensureV4 returns an unmapped version of ip.  An error is returned if the\n// passed ip is not an IPv4.\nfunc ensureV4(ip netip.Addr, kind string) (ip4 netip.Addr, err error) {\n\tip4 = ip.Unmap()\n\tif !ip4.IsValid() || !ip4.Is4() {\n\t\treturn netip.Addr{}, fmt.Errorf(\"%v is not an IPv4 %s\", ip, kind)\n\t}\n\n\treturn ip4, nil\n}\n\n// Validate returns an error if c is not a valid configuration.\n//\n// TODO(e.burkov):  Don't set the config fields when the server itself will stop\n// containing the config.\nfunc (c *V4ServerConf) Validate() (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv4: %w\") }()\n\n\tif c == nil {\n\t\treturn errNilConfig\n\t}\n\n\tgatewayIP, err := ensureV4(c.GatewayIP, \"address\")\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is and there is\n\t\t// an annotation deferred already.\n\t\treturn err\n\t}\n\n\tsubnetMask, err := ensureV4(c.SubnetMask, \"subnet mask\")\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is and there is\n\t\t// an annotation deferred already.\n\t\treturn err\n\t}\n\tmaskLen, _ := net.IPMask(subnetMask.AsSlice()).Size()\n\n\tc.subnet = netip.PrefixFrom(gatewayIP, maskLen)\n\tc.broadcastIP = aghnet.BroadcastFromPref(c.subnet)\n\n\trangeStart, err := ensureV4(c.RangeStart, \"address\")\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is and there is\n\t\t// an annotation deferred already.\n\t\treturn err\n\t}\n\n\trangeEnd, err := ensureV4(c.RangeEnd, \"address\")\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is and there is\n\t\t// an annotation deferred already.\n\t\treturn err\n\t}\n\n\tc.ipRange, err = newIPRange(rangeStart.AsSlice(), rangeEnd.AsSlice())\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is and there is\n\t\t// an annotation deferred already.\n\t\treturn err\n\t}\n\n\tif c.ipRange.contains(gatewayIP.AsSlice()) {\n\t\treturn fmt.Errorf(\"gateway ip %v in the ip range: %v-%v\",\n\t\t\tgatewayIP,\n\t\t\tc.RangeStart,\n\t\t\tc.RangeEnd,\n\t\t)\n\t}\n\n\tif !c.subnet.Contains(rangeStart) {\n\t\treturn fmt.Errorf(\"range start %v is outside network %v\",\n\t\t\tc.RangeStart,\n\t\t\tc.subnet,\n\t\t)\n\t}\n\n\tif !c.subnet.Contains(rangeEnd) {\n\t\treturn fmt.Errorf(\"range end %v is outside network %v\",\n\t\t\tc.RangeEnd,\n\t\t\tc.subnet,\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// V6ServerConf - server configuration\ntype V6ServerConf struct {\n\t// Logger is used for logging the operation of the DHCPv6 server.  It must\n\t// not be nil.\n\tLogger *slog.Logger `yaml:\"-\" json:\"-\"`\n\n\tEnabled       bool   `yaml:\"-\" json:\"-\"`\n\tInterfaceName string `yaml:\"-\" json:\"-\"`\n\n\t// The first IP address for dynamic leases\n\t// The last allowed IP address ends with 0xff byte\n\tRangeStart net.IP `yaml:\"range_start\" json:\"range_start\"`\n\n\tLeaseDuration uint32 `yaml:\"lease_duration\" json:\"lease_duration\"` // in seconds\n\n\tRASLAACOnly  bool `yaml:\"ra_slaac_only\" json:\"-\"`  // send ICMPv6.RA packets without MO flags\n\tRAAllowSLAAC bool `yaml:\"ra_allow_slaac\" json:\"-\"` // send ICMPv6.RA packets with MO flags\n\n\tipStart    net.IP        // starting IP address for dynamic leases\n\tleaseTime  time.Duration // the time during which a dynamic lease is considered valid\n\tdnsIPAddrs []net.IP      // IPv6 addresses to return to DHCP clients as DNS server addresses\n\n\t// Server calls this function when leases data changes\n\tnotify func(uint32)\n}\n"
  },
  {
    "path": "internal/dhcpd/conn_bsd.go",
    "content": "//go:build darwin || freebsd || openbsd\n\npackage dhcpd\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4/server4\"\n\t\"github.com/mdlayher/ethernet\"\n\n\t//lint:ignore SA1019 See the TODO in go.mod.\n\t\"github.com/mdlayher/raw\"\n)\n\n// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to\n// the unconfigured host.\ntype dhcpUnicastAddr struct {\n\t// raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without\n\t// actually implementing all methods.  It also contains the client's\n\t// hardware address.\n\traw.Addr\n\n\t// yiaddr is an IP address just allocated by server for the host.\n\tyiaddr net.IP\n}\n\n// dhcpConn is the net.PacketConn capable of handling both net.UDPAddr and\n// net.HardwareAddr.\ntype dhcpConn struct {\n\t// udpConn is the connection for UDP addresses.\n\tudpConn net.PacketConn\n\t// bcastIP is the broadcast address specific for the configured\n\t// interface's subnet.\n\tbcastIP net.IP\n\n\t// rawConn is the connection for MAC addresses.\n\trawConn net.PacketConn\n\t// srcMAC is the hardware address of the configured network interface.\n\tsrcMAC net.HardwareAddr\n\t// srcIP is the IP address  of the configured network interface.\n\tsrcIP net.IP\n}\n\n// newDHCPConn creates the special connection for DHCP server.\nfunc (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) {\n\tvar ucast net.PacketConn\n\tif ucast, err = raw.ListenPacket(iface, uint16(ethernet.EtherTypeIPv4), nil); err != nil {\n\t\treturn nil, fmt.Errorf(\"creating raw udp connection: %w\", err)\n\t}\n\n\t// Create the UDP connection.\n\tvar bcast net.PacketConn\n\tbcast, err = server4.NewIPv4UDPConn(iface.Name, &net.UDPAddr{\n\t\t// TODO(e.burkov):  Listening on zeroes makes the server handle\n\t\t// requests from all the interfaces.  Inspect the ways to\n\t\t// specify the interface-specific listening addresses.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.\n\t\tIP:   net.IP{0, 0, 0, 0},\n\t\tPort: dhcpv4.ServerPort,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating ipv4 udp connection: %w\", err)\n\t}\n\n\treturn &dhcpConn{\n\t\tudpConn: bcast,\n\t\tbcastIP: s.conf.broadcastIP.AsSlice(),\n\t\trawConn: ucast,\n\t\tsrcMAC:  iface.HardwareAddr,\n\t\tsrcIP:   s.conf.dnsIPAddrs[0].AsSlice(),\n\t}, nil\n}\n\n// WriteTo implements net.PacketConn for *dhcpConn.  It selects the underlying\n// connection to write to based on the type of addr.\nfunc (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {\n\tswitch addr := addr.(type) {\n\tcase *dhcpUnicastAddr:\n\t\t// Unicast the message to the client's MAC address.  Use the raw\n\t\t// connection.\n\t\t//\n\t\t// Note: unicasting is performed on the only network interface\n\t\t// that is configured.  For now it may be not what users expect\n\t\t// so additionally broadcast the message via UDP connection.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.\n\t\tvar rerr error\n\t\tn, rerr = c.unicast(p, addr)\n\n\t\t_, uerr := c.broadcast(p, &net.UDPAddr{\n\t\t\tIP:   netutil.IPv4bcast(),\n\t\t\tPort: dhcpv4.ClientPort,\n\t\t})\n\n\t\treturn n, wrapErrs(\"writing to\", uerr, rerr)\n\tcase *net.UDPAddr:\n\t\tif addr.IP.Equal(net.IPv4bcast) {\n\t\t\t// Broadcast the message for the client which supports\n\t\t\t// it.  Use the UDP connection.\n\t\t\treturn c.broadcast(p, addr)\n\t\t}\n\n\t\t// Unicast the message to the client's IP address.  Use the UDP\n\t\t// connection.\n\t\treturn c.udpConn.WriteTo(p, addr)\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"addr has an unexpected type %T\", addr)\n\t}\n}\n\n// ReadFrom implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {\n\treturn c.udpConn.ReadFrom(p)\n}\n\n// unicast wraps respData with required frames and writes it to the peer.\nfunc (c *dhcpConn) unicast(respData []byte, peer *dhcpUnicastAddr) (n int, err error) {\n\tvar data []byte\n\tdata, err = c.buildEtherPkt(respData, peer)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn c.rawConn.WriteTo(data, &peer.Addr)\n}\n\n// Close implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) Close() (err error) {\n\trerr := c.rawConn.Close()\n\tif errors.Is(rerr, os.ErrClosed) {\n\t\t// Ignore the error since the actual file is closed already.\n\t\trerr = nil\n\t}\n\n\treturn wrapErrs(\"closing\", c.udpConn.Close(), rerr)\n}\n\n// LocalAddr implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) LocalAddr() (a net.Addr) {\n\treturn c.udpConn.LocalAddr()\n}\n\n// SetDeadline implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) SetDeadline(t time.Time) (err error) {\n\treturn wrapErrs(\"setting deadline on\", c.udpConn.SetDeadline(t), c.rawConn.SetDeadline(t))\n}\n\n// SetReadDeadline implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) SetReadDeadline(t time.Time) error {\n\treturn wrapErrs(\n\t\t\"setting reading deadline on\",\n\t\tc.udpConn.SetReadDeadline(t),\n\t\tc.rawConn.SetReadDeadline(t),\n\t)\n}\n\n// SetWriteDeadline implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) SetWriteDeadline(t time.Time) error {\n\treturn wrapErrs(\n\t\t\"setting writing deadline on\",\n\t\tc.udpConn.SetWriteDeadline(t),\n\t\tc.rawConn.SetWriteDeadline(t),\n\t)\n}\n\n// ipv4DefaultTTL is the default Time to Live value in seconds as recommended by\n// RFC-1700.\n//\n// See https://datatracker.ietf.org/doc/html/rfc1700.\nconst ipv4DefaultTTL = 64\n\n// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames.\n// Validation of the payload is a caller's responsibility.\nfunc (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) {\n\tudpLayer := &layers.UDP{\n\t\tSrcPort: dhcpv4.ServerPort,\n\t\tDstPort: dhcpv4.ClientPort,\n\t}\n\n\tipv4Layer := &layers.IPv4{\n\t\tVersion:  uint8(layers.IPProtocolIPv4),\n\t\tFlags:    layers.IPv4DontFragment,\n\t\tTTL:      ipv4DefaultTTL,\n\t\tProtocol: layers.IPProtocolUDP,\n\t\tSrcIP:    c.srcIP,\n\t\tDstIP:    peer.yiaddr,\n\t}\n\n\t// Ignore the error since it's only returned for invalid network layer's\n\t// type.\n\t_ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)\n\n\tethLayer := &layers.Ethernet{\n\t\tSrcMAC:       c.srcMAC,\n\t\tDstMAC:       peer.HardwareAddr,\n\t\tEthernetType: layers.EthernetTypeIPv4,\n\t}\n\n\tbuf := gopacket.NewSerializeBuffer()\n\tsetts := gopacket.SerializeOptions{\n\t\tFixLengths:       true,\n\t\tComputeChecksums: true,\n\t}\n\n\terr = gopacket.SerializeLayers(\n\t\tbuf,\n\t\tsetts,\n\t\tethLayer,\n\t\tipv4Layer,\n\t\tudpLayer,\n\t\tgopacket.Payload(payload),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"serializing layers: %w\", err)\n\t}\n\n\treturn buf.Bytes(), nil\n}\n\n// send writes resp for peer to conn considering the req's parameters according\n// to RFC-2131.\n//\n// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.\nfunc (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {\n\tswitch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {\n\tcase giaddr != nil && !giaddr.IsUnspecified():\n\t\t// Send any return messages to the server port on the BOOTP relay agent\n\t\t// whose address appears in giaddr.\n\t\tpeer = &net.UDPAddr{\n\t\t\tIP:   giaddr,\n\t\t\tPort: dhcpv4.ServerPort,\n\t\t}\n\t\tif mtype == dhcpv4.MessageTypeNak {\n\t\t\t// Set the broadcast bit in the DHCPNAK, so that the relay agent\n\t\t\t// broadcasts it to the client, because the client may not have a\n\t\t\t// correct network address or subnet mask, and the client may not be\n\t\t\t// answering ARP requests.\n\t\t\tresp.SetBroadcast()\n\t\t}\n\tcase mtype == dhcpv4.MessageTypeNak:\n\t\t// Broadcast any DHCPNAK messages to 0xffffffff.\n\tcase ciaddr != nil && !ciaddr.IsUnspecified():\n\t\t// Unicast DHCPOFFER and DHCPACK messages to the address in ciaddr.\n\t\tpeer = &net.UDPAddr{\n\t\t\tIP:   ciaddr,\n\t\t\tPort: dhcpv4.ClientPort,\n\t\t}\n\tcase !req.IsBroadcast() && req.ClientHWAddr != nil:\n\t\t// Unicast DHCPOFFER and DHCPACK messages to the client's hardware\n\t\t// address and yiaddr.\n\t\tpeer = &dhcpUnicastAddr{\n\t\t\tAddr:   raw.Addr{HardwareAddr: req.ClientHWAddr},\n\t\t\tyiaddr: resp.YourIPAddr,\n\t\t}\n\tdefault:\n\t\t// Go on since peer is already set to broadcast.\n\t}\n\n\tpktData := resp.ToBytes()\n\n\tlog.Debug(\"dhcpv4: sending %d bytes to %s: %s\", len(pktData), peer, resp.Summary())\n\n\t_, err := conn.WriteTo(pktData, peer)\n\tif err != nil {\n\t\tlog.Error(\"dhcpv4: conn.Write to %s failed: %s\", peer, err)\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/conn_bsd_internal_test.go",
    "content": "//go:build darwin || freebsd || openbsd\n\npackage dhcpd\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t//lint:ignore SA1019 See the TODO in go.mod.\n\t\"github.com/mdlayher/raw\"\n)\n\nfunc TestDHCPConn_WriteTo_common(t *testing.T) {\n\trespData := (&dhcpv4.DHCPv4{}).ToBytes()\n\tudpAddr := &net.UDPAddr{\n\t\tIP:   net.IP{1, 2, 3, 4},\n\t\tPort: dhcpv4.ClientPort,\n\t}\n\n\tt.Run(\"unicast_ip\", func(t *testing.T) {\n\t\twriteTo := func(_ []byte, addr net.Addr) (_ int, _ error) {\n\t\t\tassert.Equal(t, udpAddr, addr)\n\n\t\t\treturn 0, nil\n\t\t}\n\n\t\tconn := &dhcpConn{udpConn: &fakePacketConn{writeTo: writeTo}}\n\n\t\t_, err := conn.WriteTo(respData, udpAddr)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"unexpected_addr_type\", func(t *testing.T) {\n\t\ttype unexpectedAddrType struct {\n\t\t\tnet.Addr\n\t\t}\n\n\t\tconn := &dhcpConn{}\n\t\tn, err := conn.WriteTo(nil, &unexpectedAddrType{})\n\t\trequire.Error(t, err)\n\n\t\ttestutil.AssertErrorMsg(t, \"addr has an unexpected type *dhcpd.unexpectedAddrType\", err)\n\t\tassert.Zero(t, n)\n\t})\n}\n\nfunc TestBuildEtherPkt(t *testing.T) {\n\tconn := &dhcpConn{\n\t\tsrcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6},\n\t\tsrcIP:  net.IP{1, 2, 3, 4},\n\t}\n\tpeer := &dhcpUnicastAddr{\n\t\tAddr:   raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},\n\t\tyiaddr: net.IP{4, 3, 2, 1},\n\t}\n\tpayload := (&dhcpv4.DHCPv4{}).ToBytes()\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tpkt, err := conn.buildEtherPkt(payload, peer)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEmpty(t, pkt)\n\n\t\tactualPkt := gopacket.NewPacket(pkt, layers.LayerTypeEthernet, gopacket.DecodeOptions{\n\t\t\tNoCopy: true,\n\t\t})\n\t\trequire.NotNil(t, actualPkt)\n\n\t\twantTypes := []gopacket.LayerType{\n\t\t\tlayers.LayerTypeEthernet,\n\t\t\tlayers.LayerTypeIPv4,\n\t\t\tlayers.LayerTypeUDP,\n\t\t\tlayers.LayerTypeDHCPv4,\n\t\t}\n\t\tactualLayers := actualPkt.Layers()\n\t\trequire.Len(t, actualLayers, len(wantTypes))\n\n\t\tfor i, wantType := range wantTypes {\n\t\t\tlayer := actualLayers[i]\n\t\t\trequire.NotNil(t, layer)\n\n\t\t\tassert.Equal(t, wantType, layer.LayerType())\n\t\t}\n\t})\n\n\tt.Run(\"bad_payload\", func(t *testing.T) {\n\t\t// Create an invalid DHCP packet.\n\t\tinvalidPayload := []byte{1, 2, 3, 4}\n\t\tpkt, err := conn.buildEtherPkt(invalidPayload, peer)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEmpty(t, pkt)\n\t})\n\n\tt.Run(\"serializing_error\", func(t *testing.T) {\n\t\t// Create a peer with invalid MAC.\n\t\tbadPeer := &dhcpUnicastAddr{\n\t\t\tAddr:   raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},\n\t\t\tyiaddr: net.IP{4, 3, 2, 1},\n\t\t}\n\n\t\tpkt, err := conn.buildEtherPkt(payload, badPeer)\n\t\trequire.Error(t, err)\n\n\t\tassert.Empty(t, pkt)\n\t})\n}\n\nfunc TestV4Server_Send(t *testing.T) {\n\ts := &v4Server{}\n\n\tvar (\n\t\tdefaultIP = net.IP{99, 99, 99, 99}\n\t\tknownIP   = net.IP{4, 2, 4, 2}\n\t\tknownMAC  = net.HardwareAddr{6, 5, 4, 3, 2, 1}\n\t)\n\n\tdefaultPeer := &net.UDPAddr{\n\t\tIP: defaultIP,\n\t\t// Use neither client nor server port to check it actually\n\t\t// changed.\n\t\tPort: dhcpv4.ClientPort + dhcpv4.ServerPort,\n\t}\n\tdefaultResp := &dhcpv4.DHCPv4{}\n\n\ttestCases := []struct {\n\t\twant net.Addr\n\t\treq  *dhcpv4.DHCPv4\n\t\tresp *dhcpv4.DHCPv4\n\t\tname string\n\t}{{\n\t\tname: \"giaddr\",\n\t\treq:  &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},\n\t\tresp: defaultResp,\n\t\twant: &net.UDPAddr{\n\t\t\tIP:   knownIP,\n\t\t\tPort: dhcpv4.ServerPort,\n\t\t},\n\t}, {\n\t\tname: \"nak\",\n\t\treq:  &dhcpv4.DHCPv4{},\n\t\tresp: &dhcpv4.DHCPv4{\n\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeNak),\n\t\t\t),\n\t\t},\n\t\twant: defaultPeer,\n\t}, {\n\t\tname: \"ciaddr\",\n\t\treq:  &dhcpv4.DHCPv4{ClientIPAddr: knownIP},\n\t\tresp: &dhcpv4.DHCPv4{},\n\t\twant: &net.UDPAddr{\n\t\t\tIP:   knownIP,\n\t\t\tPort: dhcpv4.ClientPort,\n\t\t},\n\t}, {\n\t\tname: \"chaddr\",\n\t\treq:  &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},\n\t\tresp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},\n\t\twant: &dhcpUnicastAddr{\n\t\t\tAddr:   raw.Addr{HardwareAddr: knownMAC},\n\t\t\tyiaddr: knownIP,\n\t\t},\n\t}, {\n\t\tname: \"who_are_you\",\n\t\treq:  &dhcpv4.DHCPv4{},\n\t\tresp: &dhcpv4.DHCPv4{},\n\t\twant: defaultPeer,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tconn := &fakePacketConn{\n\t\t\t\twriteTo: func(_ []byte, addr net.Addr) (_ int, _ error) {\n\t\t\t\t\tassert.Equal(t, tc.want, addr)\n\n\t\t\t\t\treturn 0, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)\n\t\t})\n\t}\n\n\tt.Run(\"giaddr_nak\", func(t *testing.T) {\n\t\treq := &dhcpv4.DHCPv4{\n\t\t\tGatewayIPAddr: knownIP,\n\t\t}\n\t\t// Ensure the request is for unicast.\n\t\treq.SetUnicast()\n\t\tresp := &dhcpv4.DHCPv4{\n\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeNak),\n\t\t\t),\n\t\t}\n\t\twant := &net.UDPAddr{\n\t\t\tIP:   req.GatewayIPAddr,\n\t\t\tPort: dhcpv4.ServerPort,\n\t\t}\n\n\t\tconn := &fakePacketConn{\n\t\t\twriteTo: func(_ []byte, addr net.Addr) (n int, err error) {\n\t\t\t\tassert.Equal(t, want, addr)\n\n\t\t\t\treturn 0, nil\n\t\t\t},\n\t\t}\n\n\t\ts.send(cloneUDPAddr(defaultPeer), conn, req, resp)\n\t\tassert.True(t, resp.IsBroadcast())\n\t})\n}\n"
  },
  {
    "path": "internal/dhcpd/conn_linux.go",
    "content": "//go:build linux\n\npackage dhcpd\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4/server4\"\n\t\"github.com/mdlayher/ethernet\"\n\t\"github.com/mdlayher/packet\"\n)\n\n// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to\n// the unconfigured host.\ntype dhcpUnicastAddr struct {\n\t// packet.Addr is embedded here to make *dhcpUcastAddr a net.Addr without\n\t// actually implementing all methods.  It also contains the client's\n\t// hardware address.\n\tpacket.Addr\n\n\t// yiaddr is an IP address just allocated by server for the host.\n\tyiaddr net.IP\n}\n\n// dhcpConn is the net.PacketConn capable of handling both net.UDPAddr and\n// net.HardwareAddr.\ntype dhcpConn struct {\n\t// udpConn is the connection for UDP addresses.\n\tudpConn net.PacketConn\n\t// bcastIP is the broadcast address specific for the configured\n\t// interface's subnet.\n\tbcastIP net.IP\n\n\t// rawConn is the connection for MAC addresses.\n\trawConn net.PacketConn\n\t// srcMAC is the hardware address of the configured network interface.\n\tsrcMAC net.HardwareAddr\n\t// srcIP is the IP address  of the configured network interface.\n\tsrcIP net.IP\n}\n\n// newDHCPConn creates the special connection for DHCP server.\nfunc (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) {\n\tvar ucast net.PacketConn\n\tif ucast, err = packet.Listen(iface, packet.Raw, int(ethernet.EtherTypeIPv4), nil); err != nil {\n\t\treturn nil, fmt.Errorf(\"creating raw udp connection: %w\", err)\n\t}\n\n\t// Create the UDP connection.\n\tvar bcast net.PacketConn\n\tbcast, err = server4.NewIPv4UDPConn(iface.Name, &net.UDPAddr{\n\t\t// TODO(e.burkov):  Listening on zeroes makes the server handle\n\t\t// requests from all the interfaces.  Inspect the ways to\n\t\t// specify the interface-specific listening addresses.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.\n\t\tIP:   net.IP{0, 0, 0, 0},\n\t\tPort: dhcpv4.ServerPort,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating ipv4 udp connection: %w\", err)\n\t}\n\n\treturn &dhcpConn{\n\t\tudpConn: bcast,\n\t\tbcastIP: s.conf.broadcastIP.AsSlice(),\n\t\trawConn: ucast,\n\t\tsrcMAC:  iface.HardwareAddr,\n\t\tsrcIP:   s.conf.dnsIPAddrs[0].AsSlice(),\n\t}, nil\n}\n\n// WriteTo implements net.PacketConn for *dhcpConn.  It selects the underlying\n// connection to write to based on the type of addr.\nfunc (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {\n\tswitch addr := addr.(type) {\n\tcase *dhcpUnicastAddr:\n\t\t// Unicast the message to the client's MAC address.  Use the raw\n\t\t// connection.\n\t\t//\n\t\t// Note: unicasting is performed on the only network interface\n\t\t// that is configured.  For now it may be not what users expect\n\t\t// so additionally broadcast the message via UDP connection.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.\n\t\tvar rerr error\n\t\tn, rerr = c.unicast(p, addr)\n\n\t\t_, uerr := c.broadcast(p, &net.UDPAddr{\n\t\t\tIP:   netutil.IPv4bcast(),\n\t\t\tPort: dhcpv4.ClientPort,\n\t\t})\n\n\t\treturn n, wrapErrs(\"writing to\", uerr, rerr)\n\tcase *net.UDPAddr:\n\t\tif addr.IP.Equal(net.IPv4bcast) {\n\t\t\t// Broadcast the message for the client which supports\n\t\t\t// it.  Use the UDP connection.\n\t\t\treturn c.broadcast(p, addr)\n\t\t}\n\n\t\t// Unicast the message to the client's IP address.  Use the UDP\n\t\t// connection.\n\t\treturn c.udpConn.WriteTo(p, addr)\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"addr has an unexpected type %T\", addr)\n\t}\n}\n\n// ReadFrom implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {\n\treturn c.udpConn.ReadFrom(p)\n}\n\n// unicast wraps respData with required frames and writes it to the peer.\nfunc (c *dhcpConn) unicast(respData []byte, peer *dhcpUnicastAddr) (n int, err error) {\n\tvar data []byte\n\tdata, err = c.buildEtherPkt(respData, peer)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn c.rawConn.WriteTo(data, &peer.Addr)\n}\n\n// Close implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) Close() (err error) {\n\trerr := c.rawConn.Close()\n\tif errors.Is(rerr, os.ErrClosed) {\n\t\t// Ignore the error since the actual file is closed already.\n\t\trerr = nil\n\t}\n\n\treturn wrapErrs(\"closing\", c.udpConn.Close(), rerr)\n}\n\n// LocalAddr implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) LocalAddr() (a net.Addr) {\n\treturn c.udpConn.LocalAddr()\n}\n\n// SetDeadline implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) SetDeadline(t time.Time) (err error) {\n\treturn wrapErrs(\"setting deadline on\", c.udpConn.SetDeadline(t), c.rawConn.SetDeadline(t))\n}\n\n// SetReadDeadline implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) SetReadDeadline(t time.Time) error {\n\treturn wrapErrs(\n\t\t\"setting reading deadline on\",\n\t\tc.udpConn.SetReadDeadline(t),\n\t\tc.rawConn.SetReadDeadline(t),\n\t)\n}\n\n// SetWriteDeadline implements net.PacketConn for *dhcpConn.\nfunc (c *dhcpConn) SetWriteDeadline(t time.Time) error {\n\treturn wrapErrs(\n\t\t\"setting writing deadline on\",\n\t\tc.udpConn.SetWriteDeadline(t),\n\t\tc.rawConn.SetWriteDeadline(t),\n\t)\n}\n\n// ipv4DefaultTTL is the default Time to Live value in seconds as recommended by\n// RFC-1700.\n//\n// See https://datatracker.ietf.org/doc/html/rfc1700.\nconst ipv4DefaultTTL = 64\n\n// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames.\n// Validation of the payload is a caller's responsibility.\nfunc (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) {\n\tudpLayer := &layers.UDP{\n\t\tSrcPort: dhcpv4.ServerPort,\n\t\tDstPort: dhcpv4.ClientPort,\n\t}\n\n\tipv4Layer := &layers.IPv4{\n\t\tVersion:  uint8(layers.IPProtocolIPv4),\n\t\tFlags:    layers.IPv4DontFragment,\n\t\tTTL:      ipv4DefaultTTL,\n\t\tProtocol: layers.IPProtocolUDP,\n\t\tSrcIP:    c.srcIP,\n\t\tDstIP:    peer.yiaddr,\n\t}\n\n\t// Ignore the error since it's only returned for invalid network layer's\n\t// type.\n\t_ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)\n\n\tethLayer := &layers.Ethernet{\n\t\tSrcMAC:       c.srcMAC,\n\t\tDstMAC:       peer.HardwareAddr,\n\t\tEthernetType: layers.EthernetTypeIPv4,\n\t}\n\n\tbuf := gopacket.NewSerializeBuffer()\n\tsetts := gopacket.SerializeOptions{\n\t\tFixLengths:       true,\n\t\tComputeChecksums: true,\n\t}\n\n\terr = gopacket.SerializeLayers(\n\t\tbuf,\n\t\tsetts,\n\t\tethLayer,\n\t\tipv4Layer,\n\t\tudpLayer,\n\t\tgopacket.Payload(payload),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"serializing layers: %w\", err)\n\t}\n\n\treturn buf.Bytes(), nil\n}\n\n// send writes resp for peer to conn considering the req's parameters according\n// to RFC-2131.\n//\n// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.\nfunc (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {\n\tswitch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {\n\tcase giaddr != nil && !giaddr.IsUnspecified():\n\t\t// Send any return messages to the server port on the BOOTP relay agent\n\t\t// whose address appears in giaddr.\n\t\tpeer = &net.UDPAddr{\n\t\t\tIP:   giaddr,\n\t\t\tPort: dhcpv4.ServerPort,\n\t\t}\n\t\tif mtype == dhcpv4.MessageTypeNak {\n\t\t\t// Set the broadcast bit in the DHCPNAK, so that the relay agent\n\t\t\t// broadcasts it to the client, because the client may not have a\n\t\t\t// correct network address or subnet mask, and the client may not be\n\t\t\t// answering ARP requests.\n\t\t\tresp.SetBroadcast()\n\t\t}\n\tcase mtype == dhcpv4.MessageTypeNak:\n\t\t// Broadcast any DHCPNAK messages to 0xffffffff.\n\tcase ciaddr != nil && !ciaddr.IsUnspecified():\n\t\t// Unicast DHCPOFFER and DHCPACK messages to the address in ciaddr.\n\t\tpeer = &net.UDPAddr{\n\t\t\tIP:   ciaddr,\n\t\t\tPort: dhcpv4.ClientPort,\n\t\t}\n\tcase !req.IsBroadcast() && req.ClientHWAddr != nil:\n\t\t// Unicast DHCPOFFER and DHCPACK messages to the client's hardware\n\t\t// address and yiaddr.\n\t\tpeer = &dhcpUnicastAddr{\n\t\t\tAddr:   packet.Addr{HardwareAddr: req.ClientHWAddr},\n\t\t\tyiaddr: resp.YourIPAddr,\n\t\t}\n\tdefault:\n\t\t// Go on since peer is already set to broadcast.\n\t}\n\n\tpktData := resp.ToBytes()\n\n\tlog.Debug(\"dhcpv4: sending %d bytes to %s: %s\", len(pktData), peer, resp.Summary())\n\n\t_, err := conn.WriteTo(pktData, peer)\n\tif err != nil {\n\t\tlog.Error(\"dhcpv4: conn.Write to %s failed: %s\", peer, err)\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/conn_linux_internal_test.go",
    "content": "//go:build linux\n\npackage dhcpd\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/mdlayher/packet\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDHCPConn_WriteTo_common(t *testing.T) {\n\trespData := (&dhcpv4.DHCPv4{}).ToBytes()\n\tudpAddr := &net.UDPAddr{\n\t\tIP:   net.IP{1, 2, 3, 4},\n\t\tPort: dhcpv4.ClientPort,\n\t}\n\n\tt.Run(\"unicast_ip\", func(t *testing.T) {\n\t\twriteTo := func(_ []byte, addr net.Addr) (_ int, _ error) {\n\t\t\tassert.Equal(t, udpAddr, addr)\n\n\t\t\treturn 0, nil\n\t\t}\n\n\t\tconn := &dhcpConn{udpConn: &fakePacketConn{writeTo: writeTo}}\n\n\t\t_, err := conn.WriteTo(respData, udpAddr)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"unexpected_addr_type\", func(t *testing.T) {\n\t\ttype unexpectedAddrType struct {\n\t\t\tnet.Addr\n\t\t}\n\n\t\tconn := &dhcpConn{}\n\t\tn, err := conn.WriteTo(nil, &unexpectedAddrType{})\n\t\trequire.Error(t, err)\n\n\t\ttestutil.AssertErrorMsg(t, \"addr has an unexpected type *dhcpd.unexpectedAddrType\", err)\n\t\tassert.Zero(t, n)\n\t})\n}\n\nfunc TestBuildEtherPkt(t *testing.T) {\n\tconn := &dhcpConn{\n\t\tsrcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6},\n\t\tsrcIP:  net.IP{1, 2, 3, 4},\n\t}\n\tpeer := &dhcpUnicastAddr{\n\t\tAddr:   packet.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},\n\t\tyiaddr: net.IP{4, 3, 2, 1},\n\t}\n\tpayload := (&dhcpv4.DHCPv4{}).ToBytes()\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tpkt, err := conn.buildEtherPkt(payload, peer)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEmpty(t, pkt)\n\n\t\tactualPkt := gopacket.NewPacket(pkt, layers.LayerTypeEthernet, gopacket.DecodeOptions{\n\t\t\tNoCopy: true,\n\t\t})\n\t\trequire.NotNil(t, actualPkt)\n\n\t\twantTypes := []gopacket.LayerType{\n\t\t\tlayers.LayerTypeEthernet,\n\t\t\tlayers.LayerTypeIPv4,\n\t\t\tlayers.LayerTypeUDP,\n\t\t\tlayers.LayerTypeDHCPv4,\n\t\t}\n\t\tactualLayers := actualPkt.Layers()\n\t\trequire.Len(t, actualLayers, len(wantTypes))\n\n\t\tfor i, wantType := range wantTypes {\n\t\t\tlayer := actualLayers[i]\n\t\t\trequire.NotNil(t, layer)\n\n\t\t\tassert.Equal(t, wantType, layer.LayerType())\n\t\t}\n\t})\n\n\tt.Run(\"bad_payload\", func(t *testing.T) {\n\t\t// Create an invalid DHCP packet.\n\t\tinvalidPayload := []byte{1, 2, 3, 4}\n\t\tpkt, err := conn.buildEtherPkt(invalidPayload, peer)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEmpty(t, pkt)\n\t})\n\n\tt.Run(\"serializing_error\", func(t *testing.T) {\n\t\t// Create a peer with invalid MAC.\n\t\tbadPeer := &dhcpUnicastAddr{\n\t\t\tAddr:   packet.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},\n\t\t\tyiaddr: net.IP{4, 3, 2, 1},\n\t\t}\n\n\t\tpkt, err := conn.buildEtherPkt(payload, badPeer)\n\t\trequire.Error(t, err)\n\n\t\tassert.Empty(t, pkt)\n\t})\n}\n\nfunc TestV4Server_Send(t *testing.T) {\n\ts := &v4Server{}\n\n\tvar (\n\t\tdefaultIP = net.IP{99, 99, 99, 99}\n\t\tknownIP   = net.IP{4, 2, 4, 2}\n\t\tknownMAC  = net.HardwareAddr{6, 5, 4, 3, 2, 1}\n\t)\n\n\tdefaultPeer := &net.UDPAddr{\n\t\tIP: defaultIP,\n\t\t// Use neither client nor server port to check it actually\n\t\t// changed.\n\t\tPort: dhcpv4.ClientPort + dhcpv4.ServerPort,\n\t}\n\tdefaultResp := &dhcpv4.DHCPv4{}\n\n\ttestCases := []struct {\n\t\twant net.Addr\n\t\treq  *dhcpv4.DHCPv4\n\t\tresp *dhcpv4.DHCPv4\n\t\tname string\n\t}{{\n\t\tname: \"giaddr\",\n\t\treq:  &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},\n\t\tresp: defaultResp,\n\t\twant: &net.UDPAddr{\n\t\t\tIP:   knownIP,\n\t\t\tPort: dhcpv4.ServerPort,\n\t\t},\n\t}, {\n\t\tname: \"nak\",\n\t\treq:  &dhcpv4.DHCPv4{},\n\t\tresp: &dhcpv4.DHCPv4{\n\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeNak),\n\t\t\t),\n\t\t},\n\t\twant: defaultPeer,\n\t}, {\n\t\tname: \"ciaddr\",\n\t\treq:  &dhcpv4.DHCPv4{ClientIPAddr: knownIP},\n\t\tresp: &dhcpv4.DHCPv4{},\n\t\twant: &net.UDPAddr{\n\t\t\tIP:   knownIP,\n\t\t\tPort: dhcpv4.ClientPort,\n\t\t},\n\t}, {\n\t\tname: \"chaddr\",\n\t\treq:  &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},\n\t\tresp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},\n\t\twant: &dhcpUnicastAddr{\n\t\t\tAddr:   packet.Addr{HardwareAddr: knownMAC},\n\t\t\tyiaddr: knownIP,\n\t\t},\n\t}, {\n\t\tname: \"who_are_you\",\n\t\treq:  &dhcpv4.DHCPv4{},\n\t\tresp: &dhcpv4.DHCPv4{},\n\t\twant: defaultPeer,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tconn := &fakePacketConn{\n\t\t\t\twriteTo: func(_ []byte, addr net.Addr) (_ int, _ error) {\n\t\t\t\t\tassert.Equal(t, tc.want, addr)\n\n\t\t\t\t\treturn 0, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)\n\t\t})\n\t}\n\n\tt.Run(\"giaddr_nak\", func(t *testing.T) {\n\t\treq := &dhcpv4.DHCPv4{\n\t\t\tGatewayIPAddr: knownIP,\n\t\t}\n\t\t// Ensure the request is for unicast.\n\t\treq.SetUnicast()\n\t\tresp := &dhcpv4.DHCPv4{\n\t\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeNak),\n\t\t\t),\n\t\t}\n\t\twant := &net.UDPAddr{\n\t\t\tIP:   req.GatewayIPAddr,\n\t\t\tPort: dhcpv4.ServerPort,\n\t\t}\n\n\t\tconn := &fakePacketConn{\n\t\t\twriteTo: func(_ []byte, addr net.Addr) (n int, err error) {\n\t\t\t\tassert.Equal(t, want, addr)\n\n\t\t\t\treturn 0, nil\n\t\t\t},\n\t\t}\n\n\t\ts.send(cloneUDPAddr(defaultPeer), conn, req, resp)\n\t\tassert.True(t, resp.IsBroadcast())\n\t})\n}\n"
  },
  {
    "path": "internal/dhcpd/conn_unix.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// wrapErrs is a helper to wrap the errors from two independent underlying\n// connections.\nfunc wrapErrs(action string, udpConnErr, rawConnErr error) (err error) {\n\tswitch {\n\tcase udpConnErr != nil && rawConnErr != nil:\n\t\treturn fmt.Errorf(\"%s both connections: %s\", action, errors.Join(udpConnErr, rawConnErr))\n\tcase udpConnErr != nil:\n\t\treturn fmt.Errorf(\"%s udp connection: %w\", action, udpConnErr)\n\tcase rawConnErr != nil:\n\t\treturn fmt.Errorf(\"%s raw connection: %w\", action, rawConnErr)\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/db.go",
    "content": "// On-disk database for lease table\n\npackage dhcpd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/google/renameio/v2/maybe\"\n)\n\nconst (\n\t// dataFilename contains saved leases.\n\tdataFilename = \"leases.json\"\n\n\t// dataVersion is the current version of the stored DHCP leases structure.\n\tdataVersion = 1\n)\n\n// dataLeases is the structure of the stored DHCP leases.\ntype dataLeases struct {\n\t// Version is the current version of the structure.\n\tVersion int `json:\"version\"`\n\n\t// Leases is the list containing stored DHCP leases.\n\tLeases []*dbLease `json:\"leases\"`\n}\n\n// dbLease is the structure of stored lease.\ntype dbLease struct {\n\tExpiry   string     `json:\"expires\"`\n\tIP       netip.Addr `json:\"ip\"`\n\tHostname string     `json:\"hostname\"`\n\tHWAddr   string     `json:\"mac\"`\n\tIsStatic bool       `json:\"static\"`\n}\n\n// fromLease converts *dhcpsvc.Lease to *dbLease.\nfunc fromLease(l *dhcpsvc.Lease) (dl *dbLease) {\n\tvar expiryStr string\n\tif !l.IsStatic {\n\t\t// The front-end is waiting for RFC 3999 format of the time value.  It\n\t\t// also shouldn't got an Expiry field for static leases.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.\n\t\texpiryStr = l.Expiry.Format(time.RFC3339)\n\t}\n\n\treturn &dbLease{\n\t\tExpiry:   expiryStr,\n\t\tHostname: l.Hostname,\n\t\tHWAddr:   l.HWAddr.String(),\n\t\tIP:       l.IP,\n\t\tIsStatic: l.IsStatic,\n\t}\n}\n\n// toLease converts *dbLease to *dhcpsvc.Lease.\nfunc (dl *dbLease) toLease() (l *dhcpsvc.Lease, err error) {\n\tmac, err := net.ParseMAC(dl.HWAddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing hardware address: %w\", err)\n\t}\n\n\texpiry := time.Time{}\n\tif !dl.IsStatic {\n\t\texpiry, err = time.Parse(time.RFC3339, dl.Expiry)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing expiry time: %w\", err)\n\t\t}\n\t}\n\n\treturn &dhcpsvc.Lease{\n\t\tExpiry:   expiry,\n\t\tIP:       dl.IP,\n\t\tHostname: dl.Hostname,\n\t\tHWAddr:   mac,\n\t\tIsStatic: dl.IsStatic,\n\t}, nil\n}\n\n// dbLoad loads stored leases.\nfunc (s *server) dbLoad() (err error) {\n\tdata, err := os.ReadFile(s.conf.dbFilePath)\n\tif err != nil {\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"reading db: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tdl := &dataLeases{}\n\terr = json.Unmarshal(data, dl)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"decoding db: %w\", err)\n\t}\n\n\tleases := dl.Leases\n\tleases4 := []*dhcpsvc.Lease{}\n\tleases6 := []*dhcpsvc.Lease{}\n\n\tfor _, l := range leases {\n\t\tvar lease *dhcpsvc.Lease\n\t\tlease, err = l.toLease()\n\t\tif err != nil {\n\t\t\tlog.Info(\"dhcp: invalid lease: %s\", err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif lease.IP.Is4() {\n\t\t\tleases4 = append(leases4, lease)\n\t\t} else {\n\t\t\tleases6 = append(leases6, lease)\n\t\t}\n\t}\n\n\terr = s.srv4.ResetLeases(leases4)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"resetting dhcpv4 leases: %w\", err)\n\t}\n\n\tif s.srv6 != nil {\n\t\terr = s.srv6.ResetLeases(leases6)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"resetting dhcpv6 leases: %w\", err)\n\t\t}\n\t}\n\n\tlog.Info(\n\t\t\"dhcp: loaded leases v4:%d  v6:%d  total-read:%d from DB\",\n\t\tlen(leases4),\n\t\tlen(leases6),\n\t\tlen(leases),\n\t)\n\n\treturn nil\n}\n\n// dbStore stores DHCP leases.\nfunc (s *server) dbStore() (err error) {\n\t// Use an empty slice here as opposed to nil so that it doesn't write\n\t// \"null\" into the database file if leases are empty.\n\tleases := []*dbLease{}\n\n\tfor _, l := range s.srv4.getLeasesRef() {\n\t\tleases = append(leases, fromLease(l))\n\t}\n\n\tif s.srv6 != nil {\n\t\tfor _, l := range s.srv6.getLeasesRef() {\n\t\t\tleases = append(leases, fromLease(l))\n\t\t}\n\t}\n\n\treturn writeDB(s.conf.dbFilePath, leases)\n}\n\n// writeDB writes leases to file at path.\nfunc writeDB(path string, leases []*dbLease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"writing db: %w\") }()\n\n\tslices.SortFunc(leases, func(a, b *dbLease) (res int) {\n\t\treturn strings.Compare(a.Hostname, b.Hostname)\n\t})\n\n\tdl := &dataLeases{\n\t\tVersion: dataVersion,\n\t\tLeases:  leases,\n\t}\n\n\tbuf, err := json.Marshal(dl)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = maybe.WriteFile(path, buf, aghos.DefaultPermFile)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tlog.Info(\"dhcp: stored %d leases in %q\", len(leases), path)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dhcpd/dhcpd.go",
    "content": "// Package dhcpd provides a DHCP server.\npackage dhcpd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\nconst (\n\t// DefaultDHCPLeaseTTL is the default time-to-live for leases.\n\tDefaultDHCPLeaseTTL = uint32(timeutil.Day / time.Second)\n\n\t// DefaultDHCPTimeoutICMP is the default timeout for waiting ICMP responses.\n\tDefaultDHCPTimeoutICMP = 1000\n)\n\n// Currently used defaults for ifaceDNSAddrs.\nconst (\n\tdefaultMaxAttempts int           = 10\n\tdefaultBackoff     time.Duration = 500 * time.Millisecond\n)\n\n// OnLeaseChangedT is a callback for lease changes.\ntype OnLeaseChangedT func(flags int)\n\n// flags for onLeaseChanged()\nconst (\n\tLeaseChangedAdded = iota\n\tLeaseChangedAddedStatic\n\tLeaseChangedRemovedStatic\n\tLeaseChangedRemovedAll\n\n\tLeaseChangedDBStore\n)\n\n// GetLeasesFlags are the flags for GetLeases.\ntype GetLeasesFlags uint8\n\n// GetLeasesFlags values\nconst (\n\tLeasesDynamic GetLeasesFlags = 0b01\n\tLeasesStatic  GetLeasesFlags = 0b10\n\n\tLeasesAll = LeasesDynamic | LeasesStatic\n)\n\n// Interface is the DHCP server that deals with both IP address families.\ntype Interface interface {\n\tStart(ctx context.Context) (err error)\n\tStop() (err error)\n\n\t// Enabled returns true if the DHCP server is running.\n\t//\n\t// TODO(e.burkov):  Currently, we need this method to determine whether the\n\t// local domain suffix should be considered while resolving A/AAAA requests.\n\t// This is because other parts of the code aren't aware of the DNS suffixes\n\t// in DHCP clients names and caller is responsible for trimming it.  This\n\t// behavior should be changed in the future.\n\tEnabled() (ok bool)\n\n\t// Leases returns all the leases in the database.\n\tLeases() (leases []*dhcpsvc.Lease)\n\n\t// MacByIP returns the MAC address of a client with ip.  It returns nil if\n\t// there is no such client, due to an assumption that a DHCP client must\n\t// always have a HardwareAddr.\n\tMACByIP(ip netip.Addr) (mac net.HardwareAddr)\n\n\t// HostByIP returns the hostname of the DHCP client with the given IP\n\t// address.  The address will be netip.Addr{} if there is no such client,\n\t// due to an assumption that a DHCP client must always have an IP address.\n\tHostByIP(ip netip.Addr) (host string)\n\n\t// IPByHost returns the IP address of the DHCP client with the given\n\t// hostname.  The address will be netip.Addr{} if there is no such client,\n\t// due to an assumption that a DHCP client must always have an IP address.\n\tIPByHost(host string) (ip netip.Addr)\n\n\tWriteDiskConfig(c *ServerConfig)\n}\n\n// server is the DHCP service that handles DHCPv4, DHCPv6, and HTTP API.\ntype server struct {\n\tsrv4 DHCPServer\n\tsrv6 DHCPServer\n\n\t// TODO(a.garipov): Either create a separate type for the internal config or\n\t// just put the config values into Server.\n\tconf *ServerConfig\n\n\t// Called when the leases DB is modified\n\tonLeaseChanged []OnLeaseChangedT\n}\n\n// type check\nvar _ Interface = (*server)(nil)\n\n// Create initializes and returns the DHCP server handling both address\n// families.  It also registers the corresponding HTTP API endpoints.\nfunc Create(ctx context.Context, conf *ServerConfig) (s *server, err error) {\n\ts = &server{\n\t\tconf: &ServerConfig{\n\t\t\tLogger:             conf.Logger,\n\t\t\tCommandConstructor: conf.CommandConstructor,\n\t\t\tConfModifier:       conf.ConfModifier,\n\n\t\t\tHTTPReg: conf.HTTPReg,\n\n\t\t\tEnabled:       conf.Enabled,\n\t\t\tInterfaceName: conf.InterfaceName,\n\n\t\t\tLocalDomainName: conf.LocalDomainName,\n\n\t\t\tdbFilePath: filepath.Join(conf.DataDir, dataFilename),\n\t\t},\n\t}\n\n\t// TODO(e.burkov):  Don't register handlers, see TODO on\n\t// [aghhttp.RegisterFunc].\n\ts.registerHandlers()\n\n\tv4Enabled, v6Enabled, err := s.setServers(ctx, conf)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\ts.conf.Conf4 = conf.Conf4\n\ts.conf.Conf6 = conf.Conf6\n\n\tif s.conf.Enabled && !v4Enabled && !v6Enabled {\n\t\treturn nil, fmt.Errorf(\"neither dhcpv4 nor dhcpv6 srv is configured\")\n\t}\n\n\t// Migrate leases db if needed.\n\terr = migrateDB(conf)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\t// Don't delay database loading until the DHCP server is started,\n\t// because we need static leases functionality available beforehand.\n\terr = s.dbLoad()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"loading db: %w\", err)\n\t}\n\n\treturn s, nil\n}\n\n// setServers updates DHCPv4 and DHCPv6 servers created from the provided\n// configuration conf.  It returns the status of both the DHCPv4 and the DHCPv6\n// servers, which is always false for corresponding server on any error.\nfunc (s *server) setServers(\n\tctx context.Context,\n\tconf *ServerConfig,\n) (v4Enabled, v6Enabled bool, err error) {\n\tv4conf := conf.Conf4\n\tv4conf.Logger = s.conf.Logger.With(\"ip_version\", \"4\")\n\tv4conf.InterfaceName = s.conf.InterfaceName\n\tv4conf.notify = s.onNotify\n\tv4conf.Enabled = s.conf.Enabled && v4conf.RangeStart.IsValid()\n\n\ts.srv4, err = v4Create(&v4conf)\n\tif err != nil {\n\t\tif v4conf.Enabled {\n\t\t\treturn false, false, fmt.Errorf(\"creating dhcpv4 srv: %w\", err)\n\t\t}\n\n\t\ts.conf.Logger.WarnContext(ctx, \"creating dhcpv4 server\", slogutil.KeyError, err)\n\t}\n\n\tv6conf := conf.Conf6\n\tv6conf.Logger = s.conf.Logger.With(\"ip_version\", \"6\")\n\tv6conf.InterfaceName = s.conf.InterfaceName\n\tv6conf.notify = s.onNotify\n\tv6conf.Enabled = s.conf.Enabled && len(v6conf.RangeStart) != 0\n\n\ts.srv6, err = v6Create(v6conf)\n\tif err != nil {\n\t\treturn v4conf.Enabled, false, fmt.Errorf(\"creating dhcpv6 srv: %w\", err)\n\t}\n\n\treturn v4conf.Enabled, v6conf.Enabled, nil\n}\n\n// Enabled returns true when the server is enabled.\nfunc (s *server) Enabled() (ok bool) {\n\treturn s.conf.Enabled\n}\n\n// resetLeases resets all leases in the lease database.\nfunc (s *server) resetLeases() (err error) {\n\terr = s.srv4.ResetLeases(nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif s.srv6 != nil {\n\t\terr = s.srv6.ResetLeases(nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn s.dbStore()\n}\n\n// server calls this function after DB is updated\nfunc (s *server) onNotify(flags uint32) {\n\tif flags == LeaseChangedDBStore {\n\t\terr := s.dbStore()\n\t\tif err != nil {\n\t\t\t// TODO(s.chzhen):  Pass context.\n\t\t\tctx := context.TODO()\n\t\t\ts.conf.Logger.ErrorContext(ctx, \"updating db\", slogutil.KeyError, err)\n\t\t}\n\n\t\treturn\n\t}\n\n\ts.notify(int(flags))\n}\n\nfunc (s *server) notify(flags int) {\n\tfor _, f := range s.onLeaseChanged {\n\t\tf(flags)\n\t}\n}\n\n// WriteDiskConfig - write configuration\nfunc (s *server) WriteDiskConfig(c *ServerConfig) {\n\tc.Enabled = s.conf.Enabled\n\tc.InterfaceName = s.conf.InterfaceName\n\tc.LocalDomainName = s.conf.LocalDomainName\n\n\ts.srv4.WriteDiskConfig4(&c.Conf4)\n\ts.srv6.WriteDiskConfig6(&c.Conf6)\n}\n\n// Start will listen on port 67 and serve DHCP requests.\nfunc (s *server) Start(ctx context.Context) (err error) {\n\terr = s.srv4.Start(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = s.srv6.Start(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Stop closes the listening UDP socket\nfunc (s *server) Stop() (err error) {\n\terr = s.srv4.Stop()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = s.srv6.Stop()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Leases returns the list of active DHCP leases.\nfunc (s *server) Leases() (leases []*dhcpsvc.Lease) {\n\treturn append(s.srv4.GetLeases(LeasesAll), s.srv6.GetLeases(LeasesAll)...)\n}\n\n// MACByIP returns a MAC address by the IP address of its lease, if there is\n// one.\nfunc (s *server) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {\n\tif ip.Is4() {\n\t\treturn s.srv4.FindMACbyIP(ip)\n\t}\n\n\treturn s.srv6.FindMACbyIP(ip)\n}\n\n// HostByIP implements the [Interface] interface for *server.\n//\n// TODO(e.burkov):  Implement this method for DHCPv6.\nfunc (s *server) HostByIP(ip netip.Addr) (host string) {\n\tif ip.Is4() {\n\t\treturn s.srv4.HostByIP(ip)\n\t}\n\n\treturn \"\"\n}\n\n// IPByHost implements the [Interface] interface for *server.\n//\n// TODO(e.burkov):  Implement this method for DHCPv6.\nfunc (s *server) IPByHost(host string) (ip netip.Addr) {\n\treturn s.srv4.IPByHost(host)\n}\n\n// AddStaticLease - add static v4 lease\nfunc (s *server) AddStaticLease(l *dhcpsvc.Lease) error {\n\treturn s.srv4.AddStaticLease(l)\n}\n"
  },
  {
    "path": "internal/dhcpd/dhcpd_internal_test.go",
    "content": "package dhcpd\n\nimport (\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// testTimeout is a common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n"
  },
  {
    "path": "internal/dhcpd/dhcpd_unix_internal_test.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMain(m *testing.M) {\n\ttestutil.DiscardLogOutput(m)\n}\n\nfunc testNotify(flags uint32) {\n}\n\n// Leases database store/load.\nfunc TestDB(t *testing.T) {\n\tvar err error\n\ts := server{\n\t\tconf: &ServerConfig{\n\t\t\tdbFilePath: filepath.Join(t.TempDir(), dataFilename),\n\t\t},\n\t}\n\n\ts.srv4, err = v4Create(&V4ServerConf{\n\t\tEnabled:    true,\n\t\tRangeStart: netip.MustParseAddr(\"192.168.10.100\"),\n\t\tRangeEnd:   netip.MustParseAddr(\"192.168.10.200\"),\n\t\tGatewayIP:  netip.MustParseAddr(\"192.168.10.1\"),\n\t\tSubnetMask: netip.MustParseAddr(\"255.255.255.0\"),\n\t\tnotify:     testNotify,\n\t})\n\trequire.NoError(t, err)\n\n\ts.srv6, err = v6Create(V6ServerConf{})\n\trequire.NoError(t, err)\n\n\tleases := []*dhcpsvc.Lease{{\n\t\tExpiry:   time.Now().Add(time.Hour),\n\t\tHostname: \"static-1.local\",\n\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\tIP:       netip.MustParseAddr(\"192.168.10.100\"),\n\t}, {\n\t\tHostname: \"static-2.local\",\n\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB},\n\t\tIP:       netip.MustParseAddr(\"192.168.10.101\"),\n\t}}\n\n\tsrv4, ok := s.srv4.(*v4Server)\n\trequire.True(t, ok)\n\n\terr = srv4.addLease(leases[0])\n\trequire.NoError(t, err)\n\n\terr = s.srv4.AddStaticLease(leases[1])\n\trequire.NoError(t, err)\n\n\terr = s.dbStore()\n\trequire.NoError(t, err)\n\n\terr = s.srv4.ResetLeases(nil)\n\trequire.NoError(t, err)\n\n\terr = s.dbLoad()\n\trequire.NoError(t, err)\n\n\tll := s.srv4.GetLeases(LeasesAll)\n\trequire.Len(t, ll, len(leases))\n\n\tassert.Equal(t, leases[0].HWAddr, ll[0].HWAddr)\n\tassert.Equal(t, leases[0].IP, ll[0].IP)\n\tassert.Equal(t, leases[0].Expiry.Unix(), ll[0].Expiry.Unix())\n\n\tassert.Equal(t, leases[1].HWAddr, ll[1].HWAddr)\n\tassert.Equal(t, leases[1].IP, ll[1].IP)\n\tassert.True(t, ll[1].IsStatic)\n}\n\nfunc TestV4Server_badRange(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\tgatewayIP  netip.Addr\n\t\tsubnetMask netip.Addr\n\t\twantErrMsg string\n\t}{{\n\t\tname:       \"gateway_in_range\",\n\t\tgatewayIP:  netip.MustParseAddr(\"192.168.10.120\"),\n\t\tsubnetMask: netip.MustParseAddr(\"255.255.255.0\"),\n\t\twantErrMsg: \"dhcpv4: gateway ip 192.168.10.120 in the ip range: \" +\n\t\t\t\"192.168.10.20-192.168.10.200\",\n\t}, {\n\t\tname:       \"outside_range_start\",\n\t\tgatewayIP:  netip.MustParseAddr(\"192.168.10.1\"),\n\t\tsubnetMask: netip.MustParseAddr(\"255.255.255.240\"),\n\t\twantErrMsg: \"dhcpv4: range start 192.168.10.20 is outside network \" +\n\t\t\t\"192.168.10.1/28\",\n\t}, {\n\t\tname:       \"outside_range_end\",\n\t\tgatewayIP:  netip.MustParseAddr(\"192.168.10.1\"),\n\t\tsubnetMask: netip.MustParseAddr(\"255.255.255.224\"),\n\t\twantErrMsg: \"dhcpv4: range end 192.168.10.200 is outside network \" +\n\t\t\t\"192.168.10.1/27\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tconf := V4ServerConf{\n\t\t\t\tEnabled:    true,\n\t\t\t\tRangeStart: netip.MustParseAddr(\"192.168.10.20\"),\n\t\t\t\tRangeEnd:   netip.MustParseAddr(\"192.168.10.200\"),\n\t\t\t\tGatewayIP:  tc.gatewayIP,\n\t\t\t\tSubnetMask: tc.subnetMask,\n\t\t\t\tnotify:     testNotify,\n\t\t\t}\n\n\t\t\t_, err := v4Create(&conf)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\n// cloneUDPAddr returns a deep copy of a.\nfunc cloneUDPAddr(a *net.UDPAddr) (clone *net.UDPAddr) {\n\treturn &net.UDPAddr{\n\t\tIP:   slices.Clone(a.IP),\n\t\tPort: a.Port,\n\t\tZone: a.Zone,\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/http_unix.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"os\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\ntype v4ServerConfJSON struct {\n\tGatewayIP     netip.Addr `json:\"gateway_ip\"`\n\tSubnetMask    netip.Addr `json:\"subnet_mask\"`\n\tRangeStart    netip.Addr `json:\"range_start\"`\n\tRangeEnd      netip.Addr `json:\"range_end\"`\n\tLeaseDuration uint32     `json:\"lease_duration\"`\n}\n\nfunc (j *v4ServerConfJSON) toServerConf() *V4ServerConf {\n\tif j == nil {\n\t\treturn &V4ServerConf{}\n\t}\n\n\treturn &V4ServerConf{\n\t\tGatewayIP:     j.GatewayIP,\n\t\tSubnetMask:    j.SubnetMask,\n\t\tRangeStart:    j.RangeStart,\n\t\tRangeEnd:      j.RangeEnd,\n\t\tLeaseDuration: j.LeaseDuration,\n\t}\n}\n\ntype v6ServerConfJSON struct {\n\tRangeStart    netip.Addr `json:\"range_start\"`\n\tLeaseDuration uint32     `json:\"lease_duration\"`\n}\n\nfunc v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {\n\tif j == nil {\n\t\treturn V6ServerConf{}\n\t}\n\n\treturn V6ServerConf{\n\t\tRangeStart:    j.RangeStart.AsSlice(),\n\t\tLeaseDuration: j.LeaseDuration,\n\t}\n}\n\n// dhcpStatusResponse is the response for /control/dhcp/status endpoint.\ntype dhcpStatusResponse struct {\n\tIfaceName    string          `json:\"interface_name\"`\n\tV4           V4ServerConf    `json:\"v4\"`\n\tV6           V6ServerConf    `json:\"v6\"`\n\tLeases       []*leaseDynamic `json:\"leases\"`\n\tStaticLeases []*leaseStatic  `json:\"static_leases\"`\n\tEnabled      bool            `json:\"enabled\"`\n}\n\n// leaseStatic is the JSON form of static DHCP lease.\ntype leaseStatic struct {\n\tHWAddr   string     `json:\"mac\"`\n\tIP       netip.Addr `json:\"ip\"`\n\tHostname string     `json:\"hostname\"`\n}\n\n// leasesToStatic converts list of leases to their JSON form.\nfunc leasesToStatic(leases []*dhcpsvc.Lease) (static []*leaseStatic) {\n\tstatic = make([]*leaseStatic, len(leases))\n\n\tfor i, l := range leases {\n\t\tstatic[i] = &leaseStatic{\n\t\t\tHWAddr:   l.HWAddr.String(),\n\t\t\tIP:       l.IP,\n\t\t\tHostname: l.Hostname,\n\t\t}\n\t}\n\n\treturn static\n}\n\n// toLease converts leaseStatic to Lease or returns error.\nfunc (l *leaseStatic) toLease() (lease *dhcpsvc.Lease, err error) {\n\taddr, err := net.ParseMAC(l.HWAddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"couldn't parse MAC address: %w\", err)\n\t}\n\n\treturn &dhcpsvc.Lease{\n\t\tHWAddr:   addr,\n\t\tIP:       l.IP,\n\t\tHostname: l.Hostname,\n\t\tIsStatic: true,\n\t}, nil\n}\n\n// leaseDynamic is the JSON form of dynamic DHCP lease.\ntype leaseDynamic struct {\n\tHWAddr   string     `json:\"mac\"`\n\tIP       netip.Addr `json:\"ip\"`\n\tHostname string     `json:\"hostname\"`\n\tExpiry   string     `json:\"expires\"`\n}\n\n// leasesToDynamic converts list of leases to their JSON form.\nfunc leasesToDynamic(leases []*dhcpsvc.Lease) (dynamic []*leaseDynamic) {\n\tdynamic = make([]*leaseDynamic, len(leases))\n\n\tfor i, l := range leases {\n\t\tdynamic[i] = &leaseDynamic{\n\t\t\tHWAddr:   l.HWAddr.String(),\n\t\t\tIP:       l.IP,\n\t\t\tHostname: l.Hostname,\n\t\t\t// The front-end is waiting for RFC 3999 format of the time\n\t\t\t// value.\n\t\t\t//\n\t\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.\n\t\t\tExpiry: l.Expiry.Format(time.RFC3339),\n\t\t}\n\t}\n\n\treturn dynamic\n}\n\nfunc (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {\n\tstatus := &dhcpStatusResponse{\n\t\tEnabled:   s.conf.Enabled,\n\t\tIfaceName: s.conf.InterfaceName,\n\t\tV4:        V4ServerConf{},\n\t\tV6:        V6ServerConf{},\n\t}\n\n\ts.srv4.WriteDiskConfig4(&status.V4)\n\ts.srv6.WriteDiskConfig6(&status.V6)\n\n\tleases := s.Leases()\n\tslices.SortFunc(leases, func(a, b *dhcpsvc.Lease) (res int) {\n\t\tif a.IsStatic == b.IsStatic {\n\t\t\treturn 0\n\t\t} else if a.IsStatic {\n\t\t\treturn -1\n\t\t} else {\n\t\t\treturn 1\n\t\t}\n\t})\n\n\tdynamicIdx := slices.IndexFunc(leases, func(l *dhcpsvc.Lease) (ok bool) {\n\t\treturn !l.IsStatic\n\t})\n\n\tif dynamicIdx == -1 {\n\t\tdynamicIdx = len(leases)\n\t}\n\n\tstatus.Leases = leasesToDynamic(leases[dynamicIdx:])\n\tstatus.StaticLeases = leasesToStatic(leases[:dynamicIdx])\n\n\taghhttp.WriteJSONResponseOK(r.Context(), s.conf.Logger, w, r, status)\n}\n\nfunc (s *server) enableDHCP(ctx context.Context, ifaceName string) (code int, err error) {\n\tvar hasStaticIP bool\n\tcmdCons := s.conf.CommandConstructor\n\thasStaticIP, err = aghnet.IfaceHasStaticIP(ctx, cmdCons, ifaceName)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrPermission) {\n\t\t\t// ErrPermission may happen here on Linux systems where AdGuard Home\n\t\t\t// is installed using Snap.  That doesn't necessarily mean that the\n\t\t\t// machine doesn't have a static IP, so we can assume that it has\n\t\t\t// and go on.  If the machine doesn't, we'll get an error later.\n\t\t\t//\n\t\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2667.\n\t\t\t//\n\t\t\t// TODO(a.garipov): I was thinking about moving this into\n\t\t\t// IfaceHasStaticIP, but then we wouldn't be able to log it.  Think\n\t\t\t// about it more.\n\t\t\tlog.Info(\"error while checking static ip: %s; \"+\n\t\t\t\t\"assuming machine has static ip and going on\", err)\n\t\t\thasStaticIP = true\n\t\t} else if errors.Is(err, aghnet.ErrNoStaticIPInfo) {\n\t\t\t// Couldn't obtain a definitive answer.  Assume static IP an go on.\n\t\t\tlog.Info(\"can't check for static ip; \" +\n\t\t\t\t\"assuming machine has static ip and going on\")\n\t\t\thasStaticIP = true\n\t\t} else {\n\t\t\terr = fmt.Errorf(\"checking static ip: %w\", err)\n\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\t}\n\n\tif !hasStaticIP {\n\t\terr = aghnet.IfaceSetStaticIP(ctx, s.conf.Logger, cmdCons, ifaceName)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"setting static ip: %w\", err)\n\n\t\t\treturn http.StatusInternalServerError, err\n\t\t}\n\t}\n\n\terr = s.Start(ctx)\n\tif err != nil {\n\t\treturn http.StatusBadRequest, fmt.Errorf(\"starting dhcp server: %w\", err)\n\t}\n\n\treturn 0, nil\n}\n\ntype dhcpServerConfigJSON struct {\n\tV4            *v4ServerConfJSON `json:\"v4\"`\n\tV6            *v6ServerConfJSON `json:\"v6\"`\n\tInterfaceName string            `json:\"interface_name\"`\n\tEnabled       aghalg.NullBool   `json:\"enabled\"`\n}\n\nfunc (s *server) handleDHCPSetConfigV4(\n\tconf *dhcpServerConfigJSON,\n) (srv DHCPServer, enabled bool, err error) {\n\tif conf.V4 == nil {\n\t\treturn nil, false, nil\n\t}\n\n\tv4Conf := conf.V4.toServerConf()\n\tv4Conf.Enabled = conf.Enabled == aghalg.NBTrue\n\tif !v4Conf.RangeStart.IsValid() {\n\t\tv4Conf.Enabled = false\n\t}\n\n\tv4Conf.InterfaceName = conf.InterfaceName\n\tv4Conf.Logger = s.conf.Logger.With(\"ip_version\", \"4\")\n\n\t// Set the default values for the fields not configurable via web API.\n\tc4 := &V4ServerConf{\n\t\tnotify:      s.onNotify,\n\t\tICMPTimeout: s.conf.Conf4.ICMPTimeout,\n\t\tOptions:     s.conf.Conf4.Options,\n\t}\n\n\ts.srv4.WriteDiskConfig4(c4)\n\tv4Conf.notify = c4.notify\n\tv4Conf.ICMPTimeout = c4.ICMPTimeout\n\tv4Conf.Options = c4.Options\n\n\tsrv4, err := v4Create(v4Conf)\n\n\treturn srv4, srv4.enabled(), err\n}\n\nfunc (s *server) handleDHCPSetConfigV6(\n\tconf *dhcpServerConfigJSON,\n) (srv6 DHCPServer, enabled bool, err error) {\n\tif conf.V6 == nil {\n\t\treturn nil, false, nil\n\t}\n\n\tv6Conf := v6JSONToServerConf(conf.V6)\n\tv6Conf.Enabled = conf.Enabled == aghalg.NBTrue\n\tif len(v6Conf.RangeStart) == 0 {\n\t\tv6Conf.Enabled = false\n\t}\n\n\t// Don't overwrite the RA/SLAAC settings from the config file.\n\t//\n\t// TODO(a.garipov): Perhaps include them into the request to allow\n\t// changing them from the HTTP API?\n\tv6Conf.RASLAACOnly = s.conf.Conf6.RASLAACOnly\n\tv6Conf.RAAllowSLAAC = s.conf.Conf6.RAAllowSLAAC\n\n\tenabled = v6Conf.Enabled\n\tv6Conf.InterfaceName = conf.InterfaceName\n\tv6Conf.Logger = s.conf.Logger.With(\"ip_version\", \"6\")\n\tv6Conf.notify = s.onNotify\n\n\tsrv6, err = v6Create(v6Conf)\n\n\treturn srv6, enabled, err\n}\n\n// createServers returns DHCPv4 and DHCPv6 servers created from the provided\n// configuration conf.\nfunc (s *server) createServers(conf *dhcpServerConfigJSON) (srv4, srv6 DHCPServer, err error) {\n\tsrv4, v4Enabled, err := s.handleDHCPSetConfigV4(conf)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"bad dhcpv4 configuration: %w\", err)\n\t}\n\n\tsrv6, v6Enabled, err := s.handleDHCPSetConfigV6(conf)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"bad dhcpv6 configuration: %w\", err)\n\t}\n\n\tif conf.Enabled == aghalg.NBTrue && !v4Enabled && !v6Enabled {\n\t\treturn nil, nil, fmt.Errorf(\"dhcpv4 or dhcpv6 configuration must be complete\")\n\t}\n\n\treturn srv4, srv6, nil\n}\n\n// handleDHCPSetConfig is the handler for the POST /control/dhcp/set_config\n// HTTP API.\nfunc (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.conf.Logger\n\n\tconf := &dhcpServerConfigJSON{}\n\tconf.Enabled = aghalg.BoolToNullBool(s.conf.Enabled)\n\tconf.InterfaceName = s.conf.InterfaceName\n\n\terr := json.NewDecoder(r.Body).Decode(conf)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"failed to parse new dhcp config json: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tsrv4, srv6, err := s.createServers(conf)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\terr = s.Stop()\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"stopping dhcp: %s\", err)\n\n\t\treturn\n\t}\n\n\ts.setConfFromJSON(conf, srv4, srv6)\n\ts.conf.ConfModifier.Apply(ctx)\n\n\terr = s.dbLoad()\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"loading leases db: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tif s.conf.Enabled {\n\t\tvar code int\n\t\tcode, err = s.enableDHCP(ctx, conf.InterfaceName)\n\t\tif err != nil {\n\t\t\taghhttp.ErrorAndLog(ctx, l, r, w, code, \"enabling dhcp: %s\", err)\n\t\t}\n\t}\n}\n\n// setConfFromJSON sets configuration parameters in s from the new configuration\n// decoded from JSON.\nfunc (s *server) setConfFromJSON(conf *dhcpServerConfigJSON, srv4, srv6 DHCPServer) {\n\tif conf.Enabled != aghalg.NBNull {\n\t\ts.conf.Enabled = conf.Enabled == aghalg.NBTrue\n\t}\n\n\tif conf.InterfaceName != \"\" {\n\t\ts.conf.InterfaceName = conf.InterfaceName\n\t}\n\n\tif srv4 != nil {\n\t\ts.srv4 = srv4\n\t}\n\n\tif srv6 != nil {\n\t\ts.srv6 = srv6\n\t}\n}\n\ntype netInterfaceJSON struct {\n\tName         string       `json:\"name\"`\n\tHardwareAddr string       `json:\"hardware_address\"`\n\tFlags        string       `json:\"flags\"`\n\tGatewayIP    netip.Addr   `json:\"gateway_ip\"`\n\tAddrs4       []netip.Addr `json:\"ipv4_addresses\"`\n\tAddrs6       []netip.Addr `json:\"ipv6_addresses\"`\n}\n\n// handleDHCPInterfaces is the handler for the GET /control/dhcp/interfaces\n// HTTP API.\nfunc (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.conf.Logger\n\n\tresp := map[string]*netInterfaceJSON{}\n\n\tifaces, err := net.Interfaces()\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"Couldn't get interfaces: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tfor _, iface := range ifaces {\n\t\tif iface.Flags&net.FlagLoopback != 0 {\n\t\t\t// It's a loopback, skip it.\n\t\t\tcontinue\n\t\t}\n\n\t\tif iface.Flags&net.FlagBroadcast == 0 {\n\t\t\t// This interface doesn't support broadcast, skip it.\n\t\t\tcontinue\n\t\t}\n\n\t\tjsonIface, iErr := newNetInterfaceJSON(ctx, s.conf.Logger, iface, s.conf.CommandConstructor)\n\t\tif iErr != nil {\n\t\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"%s\", iErr)\n\n\t\t\treturn\n\t\t}\n\n\t\tif jsonIface != nil {\n\t\t\tresp[iface.Name] = jsonIface\n\t\t}\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, resp)\n}\n\n// newNetInterfaceJSON creates a JSON object from a [net.Interface] iface.\n// l and cmdCons must not be nil.\nfunc newNetInterfaceJSON(\n\tctx context.Context,\n\tl *slog.Logger,\n\tiface net.Interface,\n\tcmdCons executil.CommandConstructor,\n) (out *netInterfaceJSON, err error) {\n\taddrs, err := iface.Addrs()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"failed to get addresses for interface %s: %w\",\n\t\t\tiface.Name,\n\t\t\terr,\n\t\t)\n\t}\n\n\tout = &netInterfaceJSON{\n\t\tName:         iface.Name,\n\t\tHardwareAddr: iface.HardwareAddr.String(),\n\t}\n\n\tif iface.Flags != 0 {\n\t\tout.Flags = iface.Flags.String()\n\t}\n\n\t// We don't want link-local addresses in JSON, so skip them.\n\tfor _, addr := range addrs {\n\t\tipNet, ok := addr.(*net.IPNet)\n\t\tif !ok {\n\t\t\t// Not an IPNet, should not happen.\n\t\t\treturn nil, fmt.Errorf(\"got iface.Addrs() element %[1]s that is not\"+\n\t\t\t\t\" net.IPNet, it is %[1]T\", addr)\n\t\t}\n\n\t\t// Ignore link-local.\n\t\t//\n\t\t// TODO(e.burkov):  Try to listen DHCP on LLA as well.\n\t\tif ipNet.IP.IsLinkLocalUnicast() {\n\t\t\tcontinue\n\t\t}\n\n\t\tvAddr, iErr := netutil.IPToAddrNoMapped(ipNet.IP)\n\t\tif iErr != nil {\n\t\t\t// Not an IPNet, should not happen.\n\t\t\treturn nil, fmt.Errorf(\"failed to convert IP address %[1]s: %w\", addr, iErr)\n\t\t}\n\n\t\tif vAddr.Is4() {\n\t\t\tout.Addrs4 = append(out.Addrs4, vAddr)\n\t\t} else {\n\t\t\tout.Addrs6 = append(out.Addrs6, vAddr)\n\t\t}\n\t}\n\n\tif len(out.Addrs4)+len(out.Addrs6) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tout.GatewayIP = aghnet.GatewayIP(ctx, l, cmdCons, iface.Name)\n\n\treturn out, nil\n}\n\n// dhcpSearchOtherResult contains information about other DHCP server for\n// specific network interface.\ntype dhcpSearchOtherResult struct {\n\tFound string `json:\"found,omitempty\"`\n\tError string `json:\"error,omitempty\"`\n}\n\n// dhcpStaticIPStatus contains information about static IP address for DHCP\n// server.\ntype dhcpStaticIPStatus struct {\n\tStatic string `json:\"static\"`\n\tIP     string `json:\"ip,omitempty\"`\n\tError  string `json:\"error,omitempty\"`\n}\n\n// dhcpSearchV4Result contains information about DHCPv4 server for specific\n// network interface.\ntype dhcpSearchV4Result struct {\n\tOtherServer dhcpSearchOtherResult `json:\"other_server\"`\n\tStaticIP    dhcpStaticIPStatus    `json:\"static_ip\"`\n}\n\n// dhcpSearchV6Result contains information about DHCPv6 server for specific\n// network interface.\ntype dhcpSearchV6Result struct {\n\tOtherServer dhcpSearchOtherResult `json:\"other_server\"`\n}\n\n// dhcpSearchResult is a response for /control/dhcp/find_active_dhcp endpoint.\ntype dhcpSearchResult struct {\n\tV4 dhcpSearchV4Result `json:\"v4\"`\n\tV6 dhcpSearchV6Result `json:\"v6\"`\n}\n\n// findActiveServerReq is the JSON structure for the request to find active DHCP\n// servers.\ntype findActiveServerReq struct {\n\tInterface string `json:\"interface\"`\n}\n\n// handleDHCPFindActiveServer performs the following tasks:\n//  1. searches for another DHCP server in the network;\n//  2. check if a static IP is configured for the network interface;\n//  3. responds with the results.\nfunc (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.conf.Logger\n\n\tif aghhttp.WriteTextPlainDeprecated(ctx, l, w, r) {\n\t\treturn\n\t}\n\n\treq := &findActiveServerReq{}\n\terr := json.NewDecoder(r.Body).Decode(req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"reading req: %s\", err)\n\n\t\treturn\n\t}\n\n\tifaceName := req.Interface\n\tif ifaceName == \"\" {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"empty interface name\")\n\n\t\treturn\n\t}\n\n\tresult := &dhcpSearchResult{\n\t\tV4: dhcpSearchV4Result{\n\t\t\tOtherServer: dhcpSearchOtherResult{\n\t\t\t\tFound: \"no\",\n\t\t\t},\n\t\t\tStaticIP: dhcpStaticIPStatus{\n\t\t\t\tStatic: \"yes\",\n\t\t\t},\n\t\t},\n\t\tV6: dhcpSearchV6Result{\n\t\t\tOtherServer: dhcpSearchOtherResult{\n\t\t\t\tFound: \"no\",\n\t\t\t},\n\t\t},\n\t}\n\n\tcmdCons := s.conf.CommandConstructor\n\tif isStaticIP, serr := aghnet.IfaceHasStaticIP(ctx, cmdCons, ifaceName); serr != nil {\n\t\tresult.V4.StaticIP.Static = \"error\"\n\t\tresult.V4.StaticIP.Error = serr.Error()\n\t} else if !isStaticIP {\n\t\tresult.V4.StaticIP.Static = \"no\"\n\t\t// TODO(e.burkov):  The returned IP should only be of version 4.\n\t\tresult.V4.StaticIP.IP = aghnet.GetSubnet(ctx, s.conf.Logger, ifaceName).String()\n\t}\n\n\ts.setOtherDHCPResult(ctx, ifaceName, result)\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, result)\n}\n\n// setOtherDHCPResult sets the results of the check for another DHCP server in\n// result.  result must not be nil.\nfunc (s *server) setOtherDHCPResult(\n\tctx context.Context,\n\tifaceName string,\n\tresult *dhcpSearchResult,\n) {\n\tfound4, found6, err4, err6 := aghnet.CheckOtherDHCP(ctx, s.conf.Logger, ifaceName)\n\tif err4 != nil {\n\t\tresult.V4.OtherServer.Found = \"error\"\n\t\tresult.V4.OtherServer.Error = err4.Error()\n\t} else if found4 {\n\t\tresult.V4.OtherServer.Found = \"yes\"\n\t}\n\n\tif err6 != nil {\n\t\tresult.V6.OtherServer.Found = \"error\"\n\t\tresult.V6.OtherServer.Error = err6.Error()\n\t} else if found6 {\n\t\tresult.V6.OtherServer.Found = \"yes\"\n\t}\n}\n\n// parseLease parses a lease from r.  If there is no error returns DHCPServer\n// and *Lease.  r must be non-nil.\nfunc (s *server) parseLease(r io.Reader) (srv DHCPServer, lease *dhcpsvc.Lease, err error) {\n\tl := &leaseStatic{}\n\terr = json.NewDecoder(r).Decode(l)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"decoding json: %w\", err)\n\t}\n\n\tif !l.IP.IsValid() {\n\t\treturn nil, nil, errors.Error(\"invalid ip\")\n\t}\n\n\tl.IP = l.IP.Unmap()\n\n\tlease, err = l.toLease()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"parsing: %w\", err)\n\t}\n\n\tif lease.IP.Is4() {\n\t\tsrv = s.srv4\n\t} else {\n\t\tsrv = s.srv6\n\t}\n\n\treturn srv, lease, nil\n}\n\n// handleDHCPAddStaticLease is the handler for the POST\n// /control/dhcp/add_static_lease HTTP API.\nfunc (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.conf.Logger\n\n\tsrv, lease, err := s.parseLease(r.Body)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tif err = srv.AddStaticLease(lease); err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\t}\n}\n\n// handleDHCPRemoveStaticLease is the handler for the POST\n// /control/dhcp/remove_static_lease HTTP API.\nfunc (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.conf.Logger\n\n\tsrv, lease, err := s.parseLease(r.Body)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tif err = srv.RemoveStaticLease(lease); err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\t}\n}\n\n// handleDHCPUpdateStaticLease is the handler for the POST\n// /control/dhcp/update_static_lease HTTP API.\nfunc (s *server) handleDHCPUpdateStaticLease(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.conf.Logger\n\n\tsrv, lease, err := s.parseLease(r.Body)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tif err = srv.UpdateStaticLease(lease); err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\t}\n}\n\nfunc (s *server) handleReset(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.conf.Logger\n\n\terr := s.Stop()\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"stopping dhcp: %s\", err)\n\n\t\treturn\n\t}\n\n\terr = os.Remove(s.conf.dbFilePath)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tl.ErrorContext(ctx, \"failed to remove database file\", slogutil.KeyError, err)\n\t}\n\n\ts.conf = &ServerConfig{\n\t\tLogger:             l,\n\t\tCommandConstructor: s.conf.CommandConstructor,\n\t\tConfModifier:       s.conf.ConfModifier,\n\n\t\tHTTPReg: s.conf.HTTPReg,\n\n\t\tLocalDomainName: s.conf.LocalDomainName,\n\n\t\tDataDir:    s.conf.DataDir,\n\t\tdbFilePath: s.conf.dbFilePath,\n\t}\n\n\tv4conf := &V4ServerConf{\n\t\tLogger:        s.conf.Logger.With(\"ip_version\", \"4\"),\n\t\tLeaseDuration: DefaultDHCPLeaseTTL,\n\t\tICMPTimeout:   DefaultDHCPTimeoutICMP,\n\t\tnotify:        s.onNotify,\n\t}\n\ts.srv4, _ = v4Create(v4conf)\n\n\tv6conf := V6ServerConf{\n\t\tLogger:        s.conf.Logger.With(\"ip_version\", \"6\"),\n\t\tLeaseDuration: DefaultDHCPLeaseTTL,\n\t\tnotify:        s.onNotify,\n\t}\n\ts.srv6, _ = v6Create(v6conf)\n\n\ts.conf.ConfModifier.Apply(ctx)\n}\n\nfunc (s *server) handleResetLeases(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\terr := s.resetLeases()\n\tif err != nil {\n\t\tmsg := \"resetting leases: %s\"\n\t\taghhttp.ErrorAndLog(ctx, s.conf.Logger, r, w, http.StatusInternalServerError, msg, err)\n\n\t\treturn\n\t}\n}\n\nfunc (s *server) registerHandlers() {\n\tif s.conf.HTTPReg == nil {\n\t\treturn\n\t}\n\n\ts.conf.HTTPReg.Register(http.MethodGet, \"/control/dhcp/status\", s.handleDHCPStatus)\n\ts.conf.HTTPReg.Register(http.MethodGet, \"/control/dhcp/interfaces\", s.handleDHCPInterfaces)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/set_config\", s.handleDHCPSetConfig)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/find_active_dhcp\", s.handleDHCPFindActiveServer)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/add_static_lease\", s.handleDHCPAddStaticLease)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/remove_static_lease\", s.handleDHCPRemoveStaticLease)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/update_static_lease\", s.handleDHCPUpdateStaticLease)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/reset\", s.handleReset)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/reset_leases\", s.handleResetLeases)\n}\n"
  },
  {
    "path": "internal/dhcpd/http_unix_internal_test.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// defaultResponse is a helper that returns the response with default\n// configuration.\nfunc defaultResponse() *dhcpStatusResponse {\n\tconf4 := defaultV4ServerConf()\n\tconf4.LeaseDuration = 86400\n\n\tresp := &dhcpStatusResponse{\n\t\tV4:           *conf4,\n\t\tV6:           V6ServerConf{},\n\t\tLeases:       []*leaseDynamic{},\n\t\tStaticLeases: []*leaseStatic{},\n\t\tEnabled:      true,\n\t}\n\n\treturn resp\n}\n\n// handleLease is the helper function that calls handler with provided static\n// lease as body and returns modified response recorder.\nfunc handleLease(tb testing.TB, lease *leaseStatic, handler http.HandlerFunc) (w *httptest.ResponseRecorder) {\n\ttb.Helper()\n\n\tw = httptest.NewRecorder()\n\n\tb := &bytes.Buffer{}\n\terr := json.NewEncoder(b).Encode(lease)\n\trequire.NoError(tb, err)\n\n\tvar r *http.Request\n\tr, err = http.NewRequest(http.MethodPost, \"\", b)\n\trequire.NoError(tb, err)\n\n\thandler(w, r)\n\n\treturn w\n}\n\n// checkStatus is a helper that asserts the response of\n// [*server.handleDHCPStatus].\nfunc checkStatus(t *testing.T, s *server, want *dhcpStatusResponse) {\n\tw := httptest.NewRecorder()\n\n\tb := &bytes.Buffer{}\n\terr := json.NewEncoder(b).Encode(&want)\n\trequire.NoError(t, err)\n\n\tr, err := http.NewRequest(http.MethodPost, \"\", b)\n\trequire.NoError(t, err)\n\n\ts.handleDHCPStatus(w, r)\n\tassert.Equal(t, http.StatusOK, w.Code)\n\n\tassert.JSONEq(t, b.String(), w.Body.String())\n}\n\nfunc TestServer_handleDHCPStatus(t *testing.T) {\n\tconst (\n\t\tstaticName = \"static-client\"\n\t\tstaticMAC  = \"aa:aa:aa:aa:aa:aa\"\n\t)\n\n\tstaticIP := netip.MustParseAddr(\"192.168.10.10\")\n\n\tstaticLease := &leaseStatic{\n\t\tHWAddr:   staticMAC,\n\t\tIP:       staticIP,\n\t\tHostname: staticName,\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\ts, err := Create(ctx, &ServerConfig{\n\t\tLogger:       testLogger,\n\t\tEnabled:      true,\n\t\tConf4:        *defaultV4ServerConf(),\n\t\tDataDir:      t.TempDir(),\n\t\tConfModifier: agh.EmptyConfigModifier{},\n\t})\n\trequire.NoError(t, err)\n\n\tok := t.Run(\"status\", func(t *testing.T) {\n\t\tresp := defaultResponse()\n\n\t\tcheckStatus(t, s, resp)\n\t})\n\trequire.True(t, ok)\n\n\tok = t.Run(\"add_static_lease\", func(t *testing.T) {\n\t\tw := handleLease(t, staticLease, s.handleDHCPAddStaticLease)\n\t\tassert.Equal(t, http.StatusOK, w.Code)\n\n\t\tresp := defaultResponse()\n\t\tresp.StaticLeases = []*leaseStatic{staticLease}\n\n\t\tcheckStatus(t, s, resp)\n\t})\n\trequire.True(t, ok)\n\n\tok = t.Run(\"add_invalid_lease\", func(t *testing.T) {\n\t\tw := handleLease(t, staticLease, s.handleDHCPAddStaticLease)\n\t\tassert.Equal(t, http.StatusBadRequest, w.Code)\n\t})\n\trequire.True(t, ok)\n\n\tok = t.Run(\"remove_static_lease\", func(t *testing.T) {\n\t\tw := handleLease(t, staticLease, s.handleDHCPRemoveStaticLease)\n\t\tassert.Equal(t, http.StatusOK, w.Code)\n\n\t\tresp := defaultResponse()\n\n\t\tcheckStatus(t, s, resp)\n\t})\n\trequire.True(t, ok)\n\n\tok = t.Run(\"set_config\", func(t *testing.T) {\n\t\tw := httptest.NewRecorder()\n\n\t\tresp := defaultResponse()\n\t\tresp.Enabled = false\n\n\t\tb := &bytes.Buffer{}\n\t\terr = json.NewEncoder(b).Encode(&resp)\n\t\trequire.NoError(t, err)\n\n\t\tvar r *http.Request\n\t\tr, err = http.NewRequest(http.MethodPost, \"\", b)\n\t\trequire.NoError(t, err)\n\n\t\ts.handleDHCPSetConfig(w, r)\n\t\tassert.Equal(t, http.StatusOK, w.Code)\n\n\t\tcheckStatus(t, s, resp)\n\t})\n\trequire.True(t, ok)\n}\n\nfunc TestServer_HandleUpdateStaticLease(t *testing.T) {\n\tconst (\n\t\tleaseV4Name = \"static-client-v4\"\n\t\tleaseV4MAC  = \"44:44:44:44:44:44\"\n\n\t\tleaseV6Name = \"static-client-v6\"\n\t\tleaseV6MAC  = \"66:66:66:66:66:66\"\n\t)\n\n\tleaseV4IP := netip.MustParseAddr(\"192.168.10.10\")\n\tleaseV6IP := netip.MustParseAddr(\"2001::6\")\n\n\tconst (\n\t\tleaseV4Pos = iota\n\t\tleaseV6Pos\n\t)\n\n\tleases := []*leaseStatic{\n\t\tleaseV4Pos: {\n\t\t\tHWAddr:   leaseV4MAC,\n\t\t\tIP:       leaseV4IP,\n\t\t\tHostname: leaseV4Name,\n\t\t},\n\t\tleaseV6Pos: {\n\t\t\tHWAddr:   leaseV6MAC,\n\t\t\tIP:       leaseV6IP,\n\t\t\tHostname: leaseV6Name,\n\t\t},\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\ts, err := Create(ctx, &ServerConfig{\n\t\tLogger:       testLogger,\n\t\tEnabled:      true,\n\t\tConf4:        *defaultV4ServerConf(),\n\t\tConf6:        V6ServerConf{},\n\t\tDataDir:      t.TempDir(),\n\t\tConfModifier: agh.EmptyConfigModifier{},\n\t})\n\trequire.NoError(t, err)\n\n\tfor _, l := range leases {\n\t\tw := handleLease(t, l, s.handleDHCPAddStaticLease)\n\t\tassert.Equal(t, http.StatusOK, w.Code)\n\t}\n\n\ttestCases := []struct {\n\t\tname  string\n\t\tpos   int\n\t\tlease *leaseStatic\n\t}{{\n\t\tname: \"update_v4_name\",\n\t\tpos:  leaseV4Pos,\n\t\tlease: &leaseStatic{\n\t\t\tHWAddr:   leaseV4MAC,\n\t\t\tIP:       leaseV4IP,\n\t\t\tHostname: \"updated-client-v4\",\n\t\t},\n\t}, {\n\t\tname: \"update_v4_ip\",\n\t\tpos:  leaseV4Pos,\n\t\tlease: &leaseStatic{\n\t\t\tHWAddr:   leaseV4MAC,\n\t\t\tIP:       netip.MustParseAddr(\"192.168.10.200\"),\n\t\t\tHostname: \"updated-client-v4\",\n\t\t},\n\t}, {\n\t\tname: \"update_v6_name\",\n\t\tpos:  leaseV6Pos,\n\t\tlease: &leaseStatic{\n\t\t\tHWAddr:   leaseV6MAC,\n\t\t\tIP:       leaseV6IP,\n\t\t\tHostname: \"updated-client-v6\",\n\t\t},\n\t}, {\n\t\tname: \"update_v6_ip\",\n\t\tpos:  leaseV6Pos,\n\t\tlease: &leaseStatic{\n\t\t\tHWAddr:   leaseV6MAC,\n\t\t\tIP:       netip.MustParseAddr(\"2001::666\"),\n\t\t\tHostname: \"updated-client-v6\",\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tw := handleLease(t, tc.lease, s.handleDHCPUpdateStaticLease)\n\t\t\tassert.Equal(t, http.StatusOK, w.Code)\n\n\t\t\tresp := defaultResponse()\n\t\t\tleases[tc.pos] = tc.lease\n\t\t\tresp.StaticLeases = leases\n\n\t\t\tcheckStatus(t, s, resp)\n\t\t})\n\t}\n}\n\nfunc TestServer_HandleUpdateStaticLease_validation(t *testing.T) {\n\tconst (\n\t\tleaseV4Name = \"static-client-v4\"\n\t\tleaseV4MAC  = \"44:44:44:44:44:44\"\n\n\t\tanotherV4Name = \"another-client-v4\"\n\t\tanotherV4MAC  = \"55:55:55:55:55:55\"\n\t)\n\n\tleaseV4IP := netip.MustParseAddr(\"192.168.10.10\")\n\tanotherV4IP := netip.MustParseAddr(\"192.168.10.20\")\n\n\tleases := []*leaseStatic{{\n\t\tHWAddr:   leaseV4MAC,\n\t\tIP:       leaseV4IP,\n\t\tHostname: leaseV4Name,\n\t}, {\n\t\tHWAddr:   anotherV4MAC,\n\t\tIP:       anotherV4IP,\n\t\tHostname: anotherV4Name,\n\t}}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\ts, err := Create(ctx, &ServerConfig{\n\t\tLogger:       testLogger,\n\t\tEnabled:      true,\n\t\tConf4:        *defaultV4ServerConf(),\n\t\tConf6:        V6ServerConf{},\n\t\tDataDir:      t.TempDir(),\n\t\tConfModifier: agh.EmptyConfigModifier{},\n\t})\n\trequire.NoError(t, err)\n\n\tfor _, l := range leases {\n\t\tw := handleLease(t, l, s.handleDHCPAddStaticLease)\n\t\tassert.Equal(t, http.StatusOK, w.Code)\n\t}\n\n\ttestCases := []struct {\n\t\tlease *leaseStatic\n\t\tname  string\n\t\twant  string\n\t}{{\n\t\tname: \"v4_unknown_mac\",\n\t\tlease: &leaseStatic{\n\t\t\tHWAddr:   \"aa:aa:aa:aa:aa:aa\",\n\t\t\tIP:       leaseV4IP,\n\t\t\tHostname: leaseV4Name,\n\t\t},\n\t\twant: \"dhcpv4: updating static lease: can't find lease aa:aa:aa:aa:aa:aa\\n\",\n\t}, {\n\t\tname: \"update_v4_same_ip\",\n\t\tlease: &leaseStatic{\n\t\t\tHWAddr:   leaseV4MAC,\n\t\t\tIP:       anotherV4IP,\n\t\t\tHostname: leaseV4Name,\n\t\t},\n\t\twant: \"dhcpv4: updating static lease: ip address is not unique\\n\",\n\t}, {\n\t\tname: \"update_v4_same_name\",\n\t\tlease: &leaseStatic{\n\t\t\tHWAddr:   leaseV4MAC,\n\t\t\tIP:       leaseV4IP,\n\t\t\tHostname: anotherV4Name,\n\t\t},\n\t\twant: \"dhcpv4: updating static lease: hostname is not unique\\n\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tw := handleLease(t, tc.lease, s.handleDHCPUpdateStaticLease)\n\t\t\tassert.Equal(t, http.StatusBadRequest, w.Code)\n\t\t\tassert.Equal(t, tc.want, w.Body.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/http_windows.go",
    "content": "//go:build windows\n\npackage dhcpd\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n)\n\n// jsonError is a generic JSON error response.\n//\n// TODO(a.garipov): Merge together with the implementations in .../home and\n// other packages after refactoring the web handler registering.\ntype jsonError struct {\n\t// Message is the error message, an opaque string.\n\tMessage string `json:\"message\"`\n}\n\n// notImplemented is a handler that replies to any request with an HTTP 501 Not\n// Implemented status and a JSON error with the provided message msg.\n//\n// TODO(a.garipov): Either take the logger from the server after we've\n// refactored logging or make this not a method of *Server.\nfunc (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\taghhttp.WriteJSONResponse(ctx, s.conf.Logger, w, r, http.StatusNotImplemented, &jsonError{\n\t\tMessage: aghos.Unsupported(\"dhcp\").Error(),\n\t})\n}\n\n// registerHandlers sets the handlers for DHCP HTTP API that always respond with\n// an HTTP 501, since DHCP server doesn't work on Windows yet.\n//\n// TODO(a.garipov): This needs refactoring.  We shouldn't even try and\n// initialize a DHCP server on Windows, but there are currently too many\n// interconnected parts--such as HTTP handlers and frontend--to make that work\n// properly.\nfunc (s *server) registerHandlers() {\n\ts.conf.HTTPReg.Register(http.MethodGet, \"/control/dhcp/status\", s.notImplemented)\n\ts.conf.HTTPReg.Register(http.MethodGet, \"/control/dhcp/interfaces\", s.notImplemented)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/set_config\", s.notImplemented)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/find_active_dhcp\", s.notImplemented)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/add_static_lease\", s.notImplemented)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/remove_static_lease\", s.notImplemented)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/update_static_lease\", s.notImplemented)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/reset\", s.notImplemented)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dhcp/reset_leases\", s.notImplemented)\n}\n"
  },
  {
    "path": "internal/dhcpd/http_windows_internal_test.go",
    "content": "//go:build windows\n\npackage dhcpd\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestServer_notImplemented(t *testing.T) {\n\ts := &server{conf: &ServerConfig{\n\t\tLogger: testLogger,\n\t}}\n\n\tw := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, \"/unsupported\", nil)\n\trequire.NoError(t, err)\n\n\ts.notImplemented(w, r)\n\tassert.Equal(t, http.StatusNotImplemented, w.Code)\n\n\twantStr := fmt.Sprintf(\"{%q:%q}\", \"message\", aghos.Unsupported(\"dhcp\"))\n\tassert.JSONEq(t, wantStr, w.Body.String())\n}\n"
  },
  {
    "path": "internal/dhcpd/iprange.go",
    "content": "package dhcpd\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"math/big\"\n\t\"net\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// ipRange is an inclusive range of IP addresses.  A nil range is a range that\n// doesn't contain any IP addresses.\n//\n// It is safe for concurrent use.\n//\n// TODO(a.garipov): Perhaps create an optimized version with uint32 for IPv4\n// ranges?  Or use one of uint128 packages?\n//\n// TODO(e.burkov):  Use netip.Addr.\ntype ipRange struct {\n\tstart *big.Int\n\tend   *big.Int\n}\n\n// maxRangeLen is the maximum IP range length.  The bitsets used in servers only\n// accept uints, which can have the size of 32 bit.\nconst maxRangeLen = math.MaxUint32\n\n// newIPRange creates a new IP address range.  start must be less than end.  The\n// resulting range must not be greater than maxRangeLen.\nfunc newIPRange(start, end net.IP) (r *ipRange, err error) {\n\tdefer func() { err = errors.Annotate(err, \"invalid ip range: %w\") }()\n\n\t// Make sure that both are 16 bytes long to simplify handling in\n\t// methods.\n\tstart, end = start.To16(), end.To16()\n\n\tstartInt := (&big.Int{}).SetBytes(start)\n\tendInt := (&big.Int{}).SetBytes(end)\n\tdiff := (&big.Int{}).Sub(endInt, startInt)\n\n\tif diff.Sign() <= 0 {\n\t\treturn nil, fmt.Errorf(\"start is greater than or equal to end\")\n\t} else if !diff.IsUint64() || diff.Uint64() > maxRangeLen {\n\t\treturn nil, fmt.Errorf(\"range is too large\")\n\t}\n\n\tr = &ipRange{\n\t\tstart: startInt,\n\t\tend:   endInt,\n\t}\n\n\treturn r, nil\n}\n\n// contains returns true if r contains ip.\nfunc (r *ipRange) contains(ip net.IP) (ok bool) {\n\tif r == nil {\n\t\treturn false\n\t}\n\n\tipInt := (&big.Int{}).SetBytes(ip.To16())\n\n\treturn r.containsInt(ipInt)\n}\n\n// containsInt returns true if r contains ipInt.  For internal use only.\nfunc (r *ipRange) containsInt(ipInt *big.Int) (ok bool) {\n\treturn ipInt.Cmp(r.start) >= 0 && ipInt.Cmp(r.end) <= 0\n}\n\n// ipPredicate is a function that is called on every IP address in\n// (*ipRange).find.  ip is given in the 16-byte form.\ntype ipPredicate func(ip net.IP) (ok bool)\n\n// find finds the first IP address in r for which p returns true.  ip is in the\n// 16-byte form.\nfunc (r *ipRange) find(p ipPredicate) (ip net.IP) {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tip = make(net.IP, net.IPv6len)\n\t_1 := big.NewInt(1)\n\tfor i := (&big.Int{}).Set(r.start); i.Cmp(r.end) <= 0; i.Add(i, _1) {\n\t\ti.FillBytes(ip)\n\t\tif p(ip) {\n\t\t\treturn ip\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// offset returns the offset of ip from the beginning of r.  It returns 0 and\n// false if ip is not in r.\nfunc (r *ipRange) offset(ip net.IP) (offset uint64, ok bool) {\n\tif r == nil {\n\t\treturn 0, false\n\t}\n\n\tip = ip.To16()\n\tipInt := (&big.Int{}).SetBytes(ip)\n\tif !r.containsInt(ipInt) {\n\t\treturn 0, false\n\t}\n\n\toffsetInt := (&big.Int{}).Sub(ipInt, r.start)\n\n\t// Assume that the range was checked against maxRangeLen during\n\t// construction.\n\treturn offsetInt.Uint64(), true\n}\n\n// String implements the fmt.Stringer interface for *ipRange.\nfunc (r *ipRange) String() (s string) {\n\treturn fmt.Sprintf(\"%s-%s\", r.start, r.end)\n}\n"
  },
  {
    "path": "internal/dhcpd/iprange_internal_test.go",
    "content": "package dhcpd\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewIPRange(t *testing.T) {\n\tstart4 := net.IP{0, 0, 0, 1}\n\tend4 := net.IP{0, 0, 0, 3}\n\tstart6 := net.IP{\n\t\t0x01, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x01,\n\t}\n\tend6 := net.IP{\n\t\t0x01, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x03,\n\t}\n\tend6Large := net.IP{\n\t\t0x02, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x03,\n\t}\n\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\tstart      net.IP\n\t\tend        net.IP\n\t}{{\n\t\tname:       \"success_ipv4\",\n\t\twantErrMsg: \"\",\n\t\tstart:      start4,\n\t\tend:        end4,\n\t}, {\n\t\tname:       \"success_ipv6\",\n\t\twantErrMsg: \"\",\n\t\tstart:      start6,\n\t\tend:        end6,\n\t}, {\n\t\tname:       \"start_gt_end\",\n\t\twantErrMsg: \"invalid ip range: start is greater than or equal to end\",\n\t\tstart:      end4,\n\t\tend:        start4,\n\t}, {\n\t\tname:       \"start_eq_end\",\n\t\twantErrMsg: \"invalid ip range: start is greater than or equal to end\",\n\t\tstart:      start4,\n\t\tend:        start4,\n\t}, {\n\t\tname:       \"too_large\",\n\t\twantErrMsg: \"invalid ip range: range is too large\",\n\t\tstart:      start6,\n\t\tend:        end6Large,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t_, err := newIPRange(tc.start, tc.end)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\nfunc TestIPRange_Contains(t *testing.T) {\n\tstart, end := net.IP{0, 0, 0, 1}, net.IP{0, 0, 0, 3}\n\tr, err := newIPRange(start, end)\n\trequire.NoError(t, err)\n\n\tassert.True(t, r.contains(start))\n\tassert.True(t, r.contains(net.IP{0, 0, 0, 2}))\n\tassert.True(t, r.contains(end))\n\n\tassert.False(t, r.contains(net.IP{0, 0, 0, 0}))\n\tassert.False(t, r.contains(net.IP{0, 0, 0, 4}))\n}\n\nfunc TestIPRange_Find(t *testing.T) {\n\tstart, end := net.IP{0, 0, 0, 1}, net.IP{0, 0, 0, 5}\n\tr, err := newIPRange(start, end)\n\trequire.NoError(t, err)\n\n\twant := net.IPv4(0, 0, 0, 2)\n\tgot := r.find(func(ip net.IP) (ok bool) {\n\t\treturn ip[len(ip)-1]%2 == 0\n\t})\n\n\tassert.Equal(t, want, got)\n\n\tgot = r.find(func(ip net.IP) (ok bool) {\n\t\treturn ip[len(ip)-1]%10 == 0\n\t})\n\tassert.Nil(t, got)\n}\n\nfunc TestIPRange_Offset(t *testing.T) {\n\tstart, end := net.IP{0, 0, 0, 1}, net.IP{0, 0, 0, 5}\n\tr, err := newIPRange(start, end)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tin         net.IP\n\t\twantOffset uint64\n\t\twantOK     bool\n\t}{{\n\t\tname:       \"in\",\n\t\tin:         net.IP{0, 0, 0, 2},\n\t\twantOffset: 1,\n\t\twantOK:     true,\n\t}, {\n\t\tname:       \"in_start\",\n\t\tin:         start,\n\t\twantOffset: 0,\n\t\twantOK:     true,\n\t}, {\n\t\tname:       \"in_end\",\n\t\tin:         end,\n\t\twantOffset: 4,\n\t\twantOK:     true,\n\t}, {\n\t\tname:       \"out_after\",\n\t\tin:         net.IP{0, 0, 0, 6},\n\t\twantOffset: 0,\n\t\twantOK:     false,\n\t}, {\n\t\tname:       \"out_before\",\n\t\tin:         net.IP{0, 0, 0, 0},\n\t\twantOffset: 0,\n\t\twantOK:     false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toffset, ok := r.offset(tc.in)\n\t\t\tassert.Equal(t, tc.wantOffset, offset)\n\t\t\tassert.Equal(t, tc.wantOK, ok)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/migrate.go",
    "content": "package dhcpd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n)\n\nconst (\n\t// leaseExpireStatic is used to define the Expiry field for static\n\t// leases.\n\t//\n\t// Deprecated:  Remove it when migration of DHCP leases will be not needed.\n\tleaseExpireStatic = 1\n\n\t// dbFilename contains saved leases.\n\t//\n\t// Deprecated:  Use dataFilename.\n\tdbFilename = \"leases.db\"\n)\n\n// leaseJSON is the structure of stored lease in a legacy database.\n//\n// Deprecated:  Use [dbLease].\ntype leaseJSON struct {\n\tHWAddr   []byte `json:\"mac\"`\n\tIP       []byte `json:\"ip\"`\n\tHostname string `json:\"host\"`\n\tExpiry   int64  `json:\"exp\"`\n}\n\n// readOldDB reads the old database from the given path.\nfunc readOldDB(path string) (leases []*leaseJSON, err error) {\n\t// #nosec G304 -- Trust this path, since it's taken from the old file name\n\t// relative to the working directory and should generally be considered\n\t// safe.\n\tfile, err := os.Open(path)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\t// Nothing to migrate.\n\t\treturn nil, nil\n\t} else if err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\tdefer func() { err = errors.WithDeferred(err, file.Close()) }()\n\n\tleases = []*leaseJSON{}\n\terr = json.NewDecoder(file).Decode(&leases)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding old db: %w\", err)\n\t}\n\n\treturn leases, nil\n}\n\n// migrateDB migrates stored leases if necessary.\nfunc migrateDB(conf *ServerConfig) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"migrating db: %w\") }()\n\n\toldLeasesPath := filepath.Join(conf.WorkDir, dbFilename)\n\tdataDirPath := filepath.Join(conf.DataDir, dataFilename)\n\n\toldLeases, err := readOldDB(oldLeasesPath)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t} else if oldLeases == nil {\n\t\t// Nothing to migrate.\n\t\treturn nil\n\t}\n\n\tleases := make([]*dbLease, 0, len(oldLeases))\n\tfor _, l := range oldLeases {\n\t\tl.IP = normalizeIP(l.IP)\n\t\tip, ok := netip.AddrFromSlice(l.IP)\n\t\tif !ok {\n\t\t\tlog.Info(\"dhcp: invalid IP: %s\", l.IP)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tleases = append(leases, &dbLease{\n\t\t\tExpiry:   time.Unix(l.Expiry, 0).Format(time.RFC3339),\n\t\t\tHostname: l.Hostname,\n\t\t\tHWAddr:   net.HardwareAddr(l.HWAddr).String(),\n\t\t\tIP:       ip,\n\t\t\tIsStatic: l.Expiry == leaseExpireStatic,\n\t\t})\n\t}\n\n\terr = writeDB(dataDirPath, leases)\n\tif err != nil {\n\t\t// Don't wrap the error since an annotation deferred already.\n\t\treturn err\n\t}\n\n\treturn os.Remove(oldLeasesPath)\n}\n\n// normalizeIP converts the given IP address to IPv4 if it's IPv4-mapped IPv6,\n// or leaves it as is otherwise.\nfunc normalizeIP(ip net.IP) (normalized net.IP) {\n\tnormalized = ip.To4()\n\tif normalized != nil {\n\t\treturn normalized\n\t}\n\n\treturn ip\n}\n"
  },
  {
    "path": "internal/dhcpd/migrate_internal_test.go",
    "content": "package dhcpd\n\nimport (\n\t\"encoding/json\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testData = `[\n{\"mac\":\"ESIzRFVm\",\"ip\":\"AQIDBA==\",\"host\":\"test1\",\"exp\":1},\n{\"mac\":\"ZlVEMyIR\",\"ip\":\"BAMCAQ==\",\"host\":\"test2\",\"exp\":1231231231}\n]`\n\nfunc TestMigrateDB(t *testing.T) {\n\tdir := t.TempDir()\n\n\toldLeasesPath := filepath.Join(dir, dbFilename)\n\tdataDirPath := filepath.Join(dir, dataFilename)\n\n\terr := os.WriteFile(oldLeasesPath, []byte(testData), 0o644)\n\trequire.NoError(t, err)\n\n\twantLeases := []*dbLease{{\n\t\tExpiry:   time.Unix(1, 0).Format(time.RFC3339),\n\t\tHostname: \"test1\",\n\t\tHWAddr:   \"11:22:33:44:55:66\",\n\t\tIP:       netip.MustParseAddr(\"1.2.3.4\"),\n\t\tIsStatic: true,\n\t}, {\n\t\tExpiry:   time.Unix(1231231231, 0).Format(time.RFC3339),\n\t\tHostname: \"test2\",\n\t\tHWAddr:   \"66:55:44:33:22:11\",\n\t\tIP:       netip.MustParseAddr(\"4.3.2.1\"),\n\t\tIsStatic: false,\n\t}}\n\n\tconf := &ServerConfig{\n\t\tWorkDir: dir,\n\t\tDataDir: dir,\n\t}\n\n\terr = migrateDB(conf)\n\trequire.NoError(t, err)\n\n\t_, err = os.Stat(oldLeasesPath)\n\trequire.ErrorIs(t, err, os.ErrNotExist)\n\n\tvar data []byte\n\tdata, err = os.ReadFile(dataDirPath)\n\trequire.NoError(t, err)\n\n\tdl := &dataLeases{}\n\terr = json.Unmarshal(data, dl)\n\trequire.NoError(t, err)\n\n\tleases := dl.Leases\n\n\tfor i, wantLease := range wantLeases {\n\t\tassert.Equal(t, wantLease.Hostname, leases[i].Hostname)\n\t\tassert.Equal(t, wantLease.HWAddr, leases[i].HWAddr)\n\t\tassert.Equal(t, wantLease.IP, leases[i].IP)\n\t\tassert.Equal(t, wantLease.IsStatic, leases[i].IsStatic)\n\n\t\trequire.Equal(t, wantLease.Expiry, leases[i].Expiry)\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/options_unix.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n)\n\n// The aliases for DHCP option types available for explicit declaration.\n//\n// TODO(e.burkov):  Add an option for classless routes.\nconst (\n\ttypDel  = \"del\"\n\ttypBool = \"bool\"\n\ttypDur  = \"dur\"\n\ttypHex  = \"hex\"\n\ttypIP   = \"ip\"\n\ttypIPs  = \"ips\"\n\ttypText = \"text\"\n\ttypU8   = \"u8\"\n\ttypU16  = \"u16\"\n)\n\n// parseDHCPOptionHex parses a DHCP option as a hex-encoded string.\nfunc parseDHCPOptionHex(s string) (val dhcpv4.OptionValue, err error) {\n\tvar data []byte\n\tdata, err = hex.DecodeString(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding hex: %w\", err)\n\t}\n\n\treturn dhcpv4.OptionGeneric{Data: data}, nil\n}\n\n// parseDHCPOptionIP parses a DHCP option as a single IP address.\nfunc parseDHCPOptionIP(s string) (val dhcpv4.OptionValue, err error) {\n\tvar ip net.IP\n\t// All DHCPv4 options require IPv4, so don't put the 16-byte version.\n\t// Otherwise, the clients will receive weird data that looks like four IPv4\n\t// addresses.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2688.\n\tif ip, err = netutil.ParseIPv4(s); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn dhcpv4.IP(ip), nil\n}\n\n// parseDHCPOptionIPs parses a DHCP option as a comma-separates list of IP\n// addresses.\nfunc parseDHCPOptionIPs(s string) (val dhcpv4.OptionValue, err error) {\n\tvar ips dhcpv4.IPs\n\tvar ip dhcpv4.OptionValue\n\tfor i, ipStr := range strings.Split(s, \",\") {\n\t\tip, err = parseDHCPOptionIP(ipStr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing ip at index %d: %w\", i, err)\n\t\t}\n\n\t\tips = append(ips, net.IP(ip.(dhcpv4.IP)))\n\t}\n\n\treturn ips, nil\n}\n\n// parseDHCPOptionDur parses a DHCP option as a duration in a human-readable\n// form.\nfunc parseDHCPOptionDur(s string) (val dhcpv4.OptionValue, err error) {\n\tvar v timeutil.Duration\n\terr = v.UnmarshalText([]byte(s))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding dur: %w\", err)\n\t}\n\n\treturn dhcpv4.Duration(v), nil\n}\n\n// parseDHCPOptionUint parses a DHCP option as an unsigned integer.  bitSize is\n// expected to be 8 or 16.\nfunc parseDHCPOptionUint(s string, bitSize int) (val dhcpv4.OptionValue, err error) {\n\tvar v uint64\n\tv, err = strconv.ParseUint(s, 10, bitSize)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding u%d: %w\", bitSize, err)\n\t}\n\n\tswitch bitSize {\n\tcase 8:\n\t\treturn dhcpv4.OptionGeneric{Data: []byte{uint8(v)}}, nil\n\tcase 16:\n\t\treturn dhcpv4.Uint16(v), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported size of integer %d\", bitSize)\n\t}\n}\n\n// parseDHCPOptionBool parses a DHCP option as a boolean value.  See\n// [strconv.ParseBool] for available values.\nfunc parseDHCPOptionBool(s string) (val dhcpv4.OptionValue, err error) {\n\tvar v bool\n\tv, err = strconv.ParseBool(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding bool: %w\", err)\n\t}\n\n\trawVal := [1]byte{}\n\tif v {\n\t\trawVal[0] = 1\n\t}\n\n\treturn dhcpv4.OptionGeneric{Data: rawVal[:]}, nil\n}\n\n// parseDHCPOptionVal parses a DHCP option value considering typ.\nfunc parseDHCPOptionVal(typ, valStr string) (val dhcpv4.OptionValue, err error) {\n\tswitch typ {\n\tcase typBool:\n\t\tval, err = parseDHCPOptionBool(valStr)\n\tcase typDel:\n\t\tval = dhcpv4.OptionGeneric{Data: nil}\n\tcase typDur:\n\t\tval, err = parseDHCPOptionDur(valStr)\n\tcase typHex:\n\t\tval, err = parseDHCPOptionHex(valStr)\n\tcase typIP:\n\t\tval, err = parseDHCPOptionIP(valStr)\n\tcase typIPs:\n\t\tval, err = parseDHCPOptionIPs(valStr)\n\tcase typText:\n\t\tval = dhcpv4.String(valStr)\n\tcase typU8:\n\t\tval, err = parseDHCPOptionUint(valStr, 8)\n\tcase typU16:\n\t\tval, err = parseDHCPOptionUint(valStr, 16)\n\tdefault:\n\t\terr = fmt.Errorf(\"unknown option type %q\", typ)\n\t}\n\n\treturn val, err\n}\n\n// parseDHCPOption parses an option.  For the del option value is ignored.  The\n// examples of possible option strings:\n//\n//   - 1  bool true\n//   - 2  del\n//   - 3  dur  2h5s\n//   - 4  hex  736f636b733a2f2f70726f78792e6578616d706c652e6f7267\n//   - 5  ip   192.168.1.1\n//   - 6  ips  192.168.1.1,192.168.1.2\n//   - 7  text http://192.168.1.1/wpad.dat\n//   - 8  u8   255\n//   - 9  u16  65535\nfunc parseDHCPOption(s string) (code dhcpv4.OptionCode, val dhcpv4.OptionValue, err error) {\n\tdefer func() { err = errors.Annotate(err, \"invalid option string %q: %w\", s) }()\n\n\ts = strings.TrimSpace(s)\n\tparts := strings.SplitN(s, \" \", 3)\n\n\tvar valStr string\n\tif pl := len(parts); pl < 3 {\n\t\tif pl < 2 || parts[1] != typDel {\n\t\t\treturn nil, nil, errors.Error(\"bad option format\")\n\t\t}\n\t} else {\n\t\tvalStr = parts[2]\n\t}\n\n\tvar code64 uint64\n\tcode64, err = strconv.ParseUint(parts[0], 10, 8)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"parsing option code: %w\", err)\n\t}\n\n\tval, err = parseDHCPOptionVal(parts[1], valStr)\n\tif err != nil {\n\t\t// Don't wrap an error since it's informative enough as is and there\n\t\t// also the deferred annotation.\n\t\treturn nil, nil, err\n\t}\n\n\treturn dhcpv4.GenericOptionCode(code64), val, nil\n}\n\n// prepareOptions builds the set of DHCP options according to host requirements\n// document and values from conf.\nfunc (s *v4Server) prepareOptions() {\n\t// Set default values of host configuration parameters listed in Appendix A\n\t// of RFC-2131.\n\ts.implicitOpts = dhcpv4.OptionsFromList(\n\t\t// IP-Layer Per Host\n\n\t\t// An Internet host that includes embedded gateway code MUST have a\n\t\t// configuration switch to disable the gateway function, and this switch\n\t\t// MUST default to the non-gateway mode.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.5.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionIPForwarding, []byte{0x0}),\n\n\t\t// A host that supports non-local source-routing MUST have a\n\t\t// configurable switch to disable forwarding, and this switch MUST\n\t\t// default to disabled.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.5.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionNonLocalSourceRouting, []byte{0x0}),\n\n\t\t// Do not set the Policy Filter Option since it only makes sense when\n\t\t// the non-local source routing is enabled.\n\n\t\t// The minimum legal value is 576.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc2132#section-4.4.\n\t\tdhcpv4.Option{\n\t\t\tCode:  dhcpv4.OptionMaximumDatagramAssemblySize,\n\t\t\tValue: dhcpv4.Uint16(576),\n\t\t},\n\n\t\t// Set the current recommended default time to live for the Internet\n\t\t// Protocol which is 64.\n\t\t//\n\t\t// See https://www.iana.org/assignments/ip-parameters/ip-parameters.xhtml#ip-parameters-2.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionDefaultIPTTL, []byte{0x40}),\n\n\t\t// For example, after the PTMU estimate is decreased, the timeout should\n\t\t// be set to 10 minutes; once this timer expires and a larger MTU is\n\t\t// attempted, the timeout can be set to a much smaller value.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1191#section-6.6.\n\t\tdhcpv4.Option{\n\t\t\tCode:  dhcpv4.OptionPathMTUAgingTimeout,\n\t\t\tValue: dhcpv4.Duration(10 * time.Minute),\n\t\t},\n\n\t\t// There is a table describing the MTU values representing all major\n\t\t// data-link technologies in use in the Internet so that each set of\n\t\t// similar MTUs is associated with a plateau value equal to the lowest\n\t\t// MTU in the group.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1191#section-7.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionPathMTUPlateauTable, []byte{\n\t\t\t0x0, 0x44,\n\t\t\t0x1, 0x28,\n\t\t\t0x1, 0xFC,\n\t\t\t0x3, 0xEE,\n\t\t\t0x5, 0xD4,\n\t\t\t0x7, 0xD2,\n\t\t\t0x11, 0x0,\n\t\t\t0x1F, 0xE6,\n\t\t\t0x45, 0xFA,\n\t\t}),\n\n\t\t// IP-Layer Per Interface\n\n\t\t// Don't set the Interface MTU because client may choose the value on\n\t\t// their own since it's listed in the [Host Requirements RFC].  It also\n\t\t// seems the values listed there sometimes appear obsolete, see\n\t\t// https://github.com/AdguardTeam/AdGuardHome/issues/5281.\n\t\t//\n\t\t// [Host Requirements RFC]: https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.\n\n\t\t// Set the All Subnets Are Local Option to false since commonly the\n\t\t// connected hosts aren't expected to be multihomed.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionAllSubnetsAreLocal, []byte{0x00}),\n\n\t\t// Set the Perform Mask Discovery Option to false to provide the subnet\n\t\t// mask by options only.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.2.9.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionPerformMaskDiscovery, []byte{0x00}),\n\n\t\t// A system MUST NOT send an Address Mask Reply unless it is an\n\t\t// authoritative agent for address masks.  An authoritative agent may be\n\t\t// a host or a gateway, but it MUST be explicitly configured as a\n\t\t// address mask agent.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.2.9.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionMaskSupplier, []byte{0x00}),\n\n\t\t// Set the Perform Router Discovery Option to true as per Router\n\t\t// Discovery Document.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionPerformRouterDiscovery, []byte{0x01}),\n\n\t\t// The all-routers address is preferred wherever possible.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.\n\t\tdhcpv4.Option{\n\t\t\tCode:  dhcpv4.OptionRouterSolicitationAddress,\n\t\t\tValue: dhcpv4.IP(netutil.IPv4allrouter()),\n\t\t},\n\n\t\t// Don't set the Static Routes Option since it should be set up by\n\t\t// system administrator.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.1.2.\n\n\t\t// A datagram with the destination address of limited broadcast will be\n\t\t// received by every host on the connected physical network but will not\n\t\t// be forwarded outside that network.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.3.\n\t\tdhcpv4.OptBroadcastAddress(netutil.IPv4bcast()),\n\n\t\t// Link-Layer Per Interface\n\n\t\t// If the system does not dynamically negotiate use of the trailer\n\t\t// protocol on a per-destination basis, the default configuration MUST\n\t\t// disable the protocol.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.1.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionTrailerEncapsulation, []byte{0x00}),\n\n\t\t// For proxy ARP situations, the timeout needs to be on the order of a\n\t\t// minute.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.2.1.\n\t\tdhcpv4.Option{\n\t\t\tCode:  dhcpv4.OptionArpCacheTimeout,\n\t\t\tValue: dhcpv4.Duration(time.Minute),\n\t\t},\n\n\t\t// An Internet host that implements sending both the RFC-894 and the\n\t\t// RFC-1042 encapsulations MUST provide a configuration switch to select\n\t\t// which is sent, and this switch MUST default to RFC-894.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.3.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionEthernetEncapsulation, []byte{0x00}),\n\n\t\t// TCP Per Host\n\n\t\t// A fixed value must be at least big enough for the Internet diameter,\n\t\t// i.e., the longest possible path.  A reasonable value is about twice\n\t\t// the diameter, to allow for continued Internet growth.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.7.\n\t\tdhcpv4.Option{\n\t\t\tCode:  dhcpv4.OptionDefaulTCPTTL,\n\t\t\tValue: dhcpv4.Duration(60 * time.Second),\n\t\t},\n\n\t\t// The interval MUST be configurable and MUST default to no less than\n\t\t// two hours.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.\n\t\tdhcpv4.Option{\n\t\t\tCode:  dhcpv4.OptionTCPKeepaliveInterval,\n\t\t\tValue: dhcpv4.Duration(2 * time.Hour),\n\t\t},\n\n\t\t// Unfortunately, some misbehaved TCP implementations fail to respond to\n\t\t// a probe segment unless it contains data.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.\n\t\tdhcpv4.OptGeneric(dhcpv4.OptionTCPKeepaliveGarbage, []byte{0x01}),\n\n\t\t// Values From Configuration\n\t\tdhcpv4.OptRouter(s.conf.GatewayIP.AsSlice()),\n\n\t\tdhcpv4.OptSubnetMask(s.conf.SubnetMask.AsSlice()),\n\t)\n\n\t// Set values for explicitly configured options.\n\ts.explicitOpts = dhcpv4.Options{}\n\tfor i, o := range s.conf.Options {\n\t\tcode, val, err := parseDHCPOption(o)\n\t\tif err != nil {\n\t\t\tlog.Error(\"dhcpv4: bad option string at index %d: %s\", i, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\ts.explicitOpts.Update(dhcpv4.Option{Code: code, Value: val})\n\t\t// Remove those from the implicit options.\n\t\tdelete(s.implicitOpts, code.Code())\n\t}\n\n\tlog.Debug(\"dhcpv4: implicit options:\\n%s\", s.implicitOpts.Summary(nil))\n\tlog.Debug(\"dhcpv4: explicit options:\\n%s\", s.explicitOpts.Summary(nil))\n\n\tif len(s.explicitOpts) == 0 {\n\t\ts.explicitOpts = nil\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/options_unix_internal_test.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseOpt(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\tin         string\n\t\twantCode   dhcpv4.OptionCode\n\t\twantVal    dhcpv4.OptionValue\n\t\twantErrMsg string\n\t}{{\n\t\tname:     \"hex_success\",\n\t\tin:       \"6 hex c0a80101c0a80102\",\n\t\twantCode: dhcpv4.GenericOptionCode(6),\n\t\twantVal: dhcpv4.OptionGeneric{Data: []byte{\n\t\t\t0xC0, 0xA8, 0x01, 0x01,\n\t\t\t0xC0, 0xA8, 0x01, 0x02,\n\t\t}},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"ip_success\",\n\t\tin:         \"6 ip 1.2.3.4\",\n\t\twantCode:   dhcpv4.GenericOptionCode(6),\n\t\twantVal:    dhcpv4.IP(net.IP{0x01, 0x02, 0x03, 0x04}),\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:     \"ips_success\",\n\t\tin:       \"6 ips 192.168.1.1,192.168.1.2\",\n\t\twantCode: dhcpv4.GenericOptionCode(6),\n\t\twantVal: dhcpv4.IPs([]net.IP{\n\t\t\t{0xC0, 0xA8, 0x01, 0x01},\n\t\t\t{0xC0, 0xA8, 0x01, 0x02},\n\t\t}),\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"text_success\",\n\t\tin:         \"252 text http://192.168.1.1/\",\n\t\twantCode:   dhcpv4.GenericOptionCode(252),\n\t\twantVal:    dhcpv4.String(\"http://192.168.1.1/\"),\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"del_success\",\n\t\tin:         \"61 del\",\n\t\twantCode:   dhcpv4.GenericOptionCode(dhcpv4.OptionClientIdentifier),\n\t\twantVal:    dhcpv4.OptionGeneric{Data: nil},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"bool_success\",\n\t\tin:         \"19 bool true\",\n\t\twantCode:   dhcpv4.GenericOptionCode(dhcpv4.OptionIPForwarding),\n\t\twantVal:    dhcpv4.OptionGeneric{Data: []byte{0x01}},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"bool_success_false\",\n\t\tin:         \"19 bool F\",\n\t\twantCode:   dhcpv4.GenericOptionCode(dhcpv4.OptionIPForwarding),\n\t\twantVal:    dhcpv4.OptionGeneric{Data: []byte{0x00}},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"dur_success\",\n\t\tin:         \"24 dur 2h5s\",\n\t\twantCode:   dhcpv4.GenericOptionCode(dhcpv4.OptionPathMTUAgingTimeout),\n\t\twantVal:    dhcpv4.Duration(2*time.Hour + 5*time.Second),\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"u8_success\",\n\t\tin:         \"23 u8 64\",\n\t\twantCode:   dhcpv4.GenericOptionCode(dhcpv4.OptionDefaultIPTTL),\n\t\twantVal:    dhcpv4.OptionGeneric{Data: []byte{0x40}},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"u16_success\",\n\t\tin:         \"22 u16 1234\",\n\t\twantCode:   dhcpv4.GenericOptionCode(dhcpv4.OptionMaximumDatagramAssemblySize),\n\t\twantVal:    dhcpv4.Uint16(1234),\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"bad_parts\",\n\t\tin:         \"6 ip\",\n\t\twantCode:   nil,\n\t\twantVal:    nil,\n\t\twantErrMsg: `invalid option string \"6 ip\": bad option format`,\n\t}, {\n\t\tname:     \"bad_code\",\n\t\tin:       \"256 ip 1.1.1.1\",\n\t\twantCode: nil,\n\t\twantVal:  nil,\n\t\twantErrMsg: `invalid option string \"256 ip 1.1.1.1\": parsing option code: ` +\n\t\t\t`strconv.ParseUint: parsing \"256\": value out of range`,\n\t}, {\n\t\tname:       \"bad_type\",\n\t\tin:         \"6 bad 1.1.1.1\",\n\t\twantCode:   nil,\n\t\twantVal:    nil,\n\t\twantErrMsg: `invalid option string \"6 bad 1.1.1.1\": unknown option type \"bad\"`,\n\t}, {\n\t\tname:     \"hex_error\",\n\t\tin:       \"6 hex ZZZ\",\n\t\twantCode: nil,\n\t\twantVal:  nil,\n\t\twantErrMsg: `invalid option string \"6 hex ZZZ\": decoding hex: ` +\n\t\t\t`encoding/hex: invalid byte: U+005A 'Z'`,\n\t}, {\n\t\tname:       \"ip_error\",\n\t\tin:         \"6 ip 1.2.3.x\",\n\t\twantCode:   nil,\n\t\twantVal:    nil,\n\t\twantErrMsg: \"invalid option string \\\"6 ip 1.2.3.x\\\": bad ipv4 address \\\"1.2.3.x\\\"\",\n\t}, {\n\t\tname:       \"ip_error_v6\",\n\t\tin:         \"6 ip ::1234\",\n\t\twantCode:   nil,\n\t\twantVal:    nil,\n\t\twantErrMsg: \"invalid option string \\\"6 ip ::1234\\\": bad ipv4 address \\\"::1234\\\"\",\n\t}, {\n\t\tname:     \"ips_error\",\n\t\tin:       \"6 ips 192.168.1.1,192.168.1.x\",\n\t\twantCode: nil,\n\t\twantVal:  nil,\n\t\twantErrMsg: \"invalid option string \\\"6 ips 192.168.1.1,192.168.1.x\\\": \" +\n\t\t\t\"parsing ip at index 1: bad ipv4 address \\\"192.168.1.x\\\"\",\n\t}, {\n\t\tname:     \"bool_error\",\n\t\tin:       \"19 bool yes\",\n\t\twantCode: nil,\n\t\twantVal:  nil,\n\t\twantErrMsg: \"invalid option string \\\"19 bool yes\\\": decoding bool: \" +\n\t\t\t\"strconv.ParseBool: parsing \\\"yes\\\": invalid syntax\",\n\t}, {\n\t\tname:     \"dur_error\",\n\t\tin:       \"24 dur 3y\",\n\t\twantCode: nil,\n\t\twantVal:  nil,\n\t\twantErrMsg: `invalid option string \"24 dur 3y\": decoding dur: time: ` +\n\t\t\t`unknown unit \"y\" in duration \"3y\"`,\n\t}, {\n\t\tname:     \"u8_error\",\n\t\tin:       \"23 u8 256\",\n\t\twantCode: nil,\n\t\twantVal:  nil,\n\t\twantErrMsg: \"invalid option string \\\"23 u8 256\\\": decoding u8: \" +\n\t\t\t\"strconv.ParseUint: parsing \\\"256\\\": value out of range\",\n\t}, {\n\t\tname:     \"u16_error\",\n\t\tin:       \"23 u16 65536\",\n\t\twantCode: nil,\n\t\twantVal:  nil,\n\t\twantErrMsg: \"invalid option string \\\"23 u16 65536\\\": decoding u16: \" +\n\t\t\t\"strconv.ParseUint: parsing \\\"65536\\\": value out of range\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcode, val, err := parseDHCPOption(tc.in)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.wantCode, code)\n\t\t\tassert.Equal(t, tc.wantVal, val)\n\t\t})\n\t}\n}\n\nfunc TestPrepareOptions(t *testing.T) {\n\toneIP, otherIP := net.IP{1, 2, 3, 4}, net.IP{5, 6, 7, 8}\n\n\ttestCases := []struct {\n\t\tname         string\n\t\twantExplicit dhcpv4.Options\n\t\topts         []string\n\t}{{\n\t\tname:         \"all_default\",\n\t\twantExplicit: nil,\n\t\topts:         nil,\n\t}, {\n\t\tname: \"configured_ip\",\n\t\twantExplicit: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptBroadcastAddress(oneIP),\n\t\t),\n\t\topts: []string{\n\t\t\tfmt.Sprintf(\"%d ip %s\", dhcpv4.OptionBroadcastAddress, oneIP),\n\t\t},\n\t}, {\n\t\tname: \"configured_ips\",\n\t\twantExplicit: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.Option{\n\t\t\t\tCode:  dhcpv4.OptionDomainNameServer,\n\t\t\t\tValue: dhcpv4.IPs{oneIP, otherIP},\n\t\t\t},\n\t\t),\n\t\topts: []string{\n\t\t\tfmt.Sprintf(\"%d ips %s,%s\", dhcpv4.OptionDomainNameServer, oneIP, otherIP),\n\t\t},\n\t}, {\n\t\tname:         \"configured_bad\",\n\t\twantExplicit: nil,\n\t\topts: []string{\n\t\t\t\"19 bool yes\",\n\t\t\t\"24 dur 3y\",\n\t\t\t\"23 u8 256\",\n\t\t\t\"23 u16 65536\",\n\t\t\t\"20 hex\",\n\t\t\t\"23 hex abc\",\n\t\t\t\"32 ips 1,2,3,4\",\n\t\t\t\"28 256.256.256.256\",\n\t\t},\n\t}, {\n\t\tname: \"configured_del\",\n\t\twantExplicit: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptBroadcastAddress(nil),\n\t\t),\n\t\topts: []string{\n\t\t\t\"28 del\",\n\t\t},\n\t}, {\n\t\tname: \"rewritten_del\",\n\t\twantExplicit: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptBroadcastAddress(netutil.IPv4bcast()),\n\t\t),\n\t\topts: []string{\n\t\t\t\"28 del\",\n\t\t\t\"28 ip 255.255.255.255\",\n\t\t},\n\t}, {\n\t\tname: \"configured_and_del\",\n\t\twantExplicit: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.Option{\n\t\t\t\tCode:  dhcpv4.OptionGeoConf,\n\t\t\t\tValue: dhcpv4.String(\"cba\"),\n\t\t\t},\n\t\t),\n\t\topts: []string{\n\t\t\t\"123 text abc\",\n\t\t\t\"123 del\",\n\t\t\t\"123 text cba\",\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\ts := &v4Server{\n\t\t\tconf: &V4ServerConf{\n\t\t\t\tOptions: tc.opts,\n\t\t\t},\n\t\t}\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts.prepareOptions()\n\n\t\t\tassert.Equal(t, tc.wantExplicit, s.explicitOpts)\n\n\t\t\tfor c := range s.explicitOpts {\n\t\t\t\tassert.NotContains(t, s.implicitOpts, c)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpd/routeradv.go",
    "content": "package dhcpd\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"golang.org/x/net/icmp\"\n\t\"golang.org/x/net/ipv6\"\n)\n\n// raCtx is a context for the Router Advertisement logic.\ntype raCtx struct {\n\t// raAllowSLAAC is used to determine if the ICMP Router Advertisement\n\t// messages should be sent.\n\t//\n\t// If both raAllowSLAAC and raSLAACOnly are false, the Router Advertisement\n\t// messages aren't sent.\n\traAllowSLAAC bool\n\n\t// raSLAACOnly is used to determine if the ICMP Router Advertisement\n\t// messages should set M and O flags, see RFC 4861, section 4.2.\n\t//\n\t// If both raAllowSLAAC and raSLAACOnly are false, the Router Advertisement\n\t// messages aren't sent.\n\traSLAACOnly bool\n\n\t// ipAddr is an IP address used within the Source Link-Layer Address option.\n\t// See RFC 4861, section 4.6.1.\n\tipAddr net.IP\n\n\t// dnsIPAddr is an IP address used within the DNS Server option.\n\tdnsIPAddr net.IP\n\n\t// prefixIPAddr is an IP address used within the Prefix Information option.\n\t// See RFC 4861, section 4.6.2.\n\tprefixIPAddr net.IP\n\n\t// ifaceName is the name of the interface used as a scope of the IP\n\t// addresses.\n\tifaceName string\n\n\t// iface is the network interface used to send the ICMPv6 packets.\n\tiface *net.Interface\n\n\t// packetSendPeriod is the interval between sending the ICMPv6 packets.\n\tpacketSendPeriod time.Duration\n\n\t// conn is the ICMPv6 socket.\n\tconn *icmp.PacketConn\n\n\t// stop is used to stop the packet sending loop.\n\tstop atomic.Value\n}\n\ntype icmpv6RA struct {\n\tmanagedAddressConfiguration bool\n\totherConfiguration          bool\n\tprefix                      net.IP\n\tprefixLen                   int\n\tsourceLinkLayerAddress      net.HardwareAddr\n\trecursiveDNSServer          net.IP\n\tmtu                         uint32\n}\n\n// hwAddrToLinkLayerAddr clones the hardware address and returns it as a byte\n// slice suitable for the Source Link-Layer Address option in the ICMPv6\n// Router Advertisement packet.\n//\n// TODO(e.burkov):  Check if it's safe to use the original slice.\nfunc hwAddrToLinkLayerAddr(hwa net.HardwareAddr) (lla []byte, err error) {\n\terr = netutil.ValidateMAC(hwa)\n\tif err != nil {\n\t\t// Don't wrap the error, because it already contains enough\n\t\t// context.\n\t\treturn nil, err\n\t}\n\n\treturn slices.Clone(hwa), nil\n}\n\n// Create an ICMPv6.RouterAdvertisement packet with all necessary options.\n// Data scheme:\n//\n//\tICMPv6:\n//\t- type[1]\n//\t- code[1]\n//\t- chksum[2]\n//\t- body (RouterAdvertisement):\n//\t  - Cur Hop Limit[1]\n//\t  - Flags[1]: MO......\n//\t  - Router Lifetime[2]\n//\t  - Reachable Time[4]\n//\t  - Retrans Timer[4]\n//\t  - Option=Prefix Information(3):\n//\t    - Type[1]\n//\t    - Length * 8bytes[1]\n//\t    - Prefix Length[1]\n//\t    - Flags[1]: LA......\n//\t    - Valid Lifetime[4]\n//\t    - Preferred Lifetime[4]\n//\t    - Reserved[4]\n//\t    - Prefix[16]\n//\t  - Option=MTU(5):\n//\t    - Type[1]\n//\t    - Length * 8bytes[1]\n//\t    - Reserved[2]\n//\t    - MTU[4]\n//\t  - Option=Source link-layer address(1):\n//\t    - Link-Layer Address[8/24]\n//\t  - Option=Recursive DNS Server(25):\n//\t    - Type[1]\n//\t    - Length * 8bytes[1]\n//\t    - Reserved[2]\n//\t    - Lifetime[4]\n//\t    - Addresses of IPv6 Recursive DNS Servers[16]\n//\n// TODO(a.garipov): Replace with an existing implementation from a dependency.\nfunc createICMPv6RAPacket(params icmpv6RA) (data []byte, err error) {\n\tlla, err := hwAddrToLinkLayerAddr(params.sourceLinkLayerAddress)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"converting source link-layer address: %w\", err)\n\t}\n\n\t// Calculate length of the source link-layer address option.  As per RFC\n\t// 4861, section 4.6.1, the length should be in units of 8 octets, including\n\t// the type and length fields.\n\t//\n\t// See https://datatracker.ietf.org/doc/html/rfc4861#section-4.6.1.\n\tsrcLLAOptLen := len(lla) + 2\n\t// Make sure the value is rounded up to the nearest multiple of 8.\n\tsrcLLAOptLenValue := (srcLLAOptLen + 7) / 8\n\tsrcLLAPadLen := srcLLAOptLenValue*8 - srcLLAOptLen\n\n\t// TODO(a.garipov): Don't use a magic constant here.  Refactor the code\n\t// and make all constants named instead of all those comments.\n\tdata = make([]byte, 80+srcLLAOptLen+srcLLAPadLen)\n\ti := 0\n\n\t// ICMPv6:\n\n\tdata[i] = 134 // type\n\tdata[i+1] = 0 // code\n\tdata[i+2] = 0 // chksum\n\tdata[i+3] = 0\n\ti += 4\n\n\t// RouterAdvertisement:\n\n\tdata[i] = 64 // Cur Hop Limit[1]\n\ti++\n\n\tdata[i] = 0 // Flags[1]: MO......\n\tif params.managedAddressConfiguration {\n\t\tdata[i] |= 0x80\n\t}\n\tif params.otherConfiguration {\n\t\tdata[i] |= 0x40\n\t}\n\ti++\n\n\tbinary.BigEndian.PutUint16(data[i:], 1800) // Router Lifetime[2]\n\ti += 2\n\tbinary.BigEndian.PutUint32(data[i:], 0) // Reachable Time[4]\n\ti += 4\n\tbinary.BigEndian.PutUint32(data[i:], 0) // Retrans Timer[4]\n\ti += 4\n\n\t// Option=Prefix Information:\n\n\tdata[i] = 3   // Type\n\tdata[i+1] = 4 // Length\n\ti += 2\n\tdata[i] = byte(params.prefixLen) // Prefix Length[1]\n\ti++\n\tdata[i] = 0xc0 // Flags[1]\n\ti++\n\tbinary.BigEndian.PutUint32(data[i:], 3600) // Valid Lifetime[4]\n\ti += 4\n\tbinary.BigEndian.PutUint32(data[i:], 3600) // Preferred Lifetime[4]\n\ti += 4\n\tbinary.BigEndian.PutUint32(data[i:], 0) // Reserved[4]\n\ti += 4\n\tcopy(data[i:], params.prefix[:8]) // Prefix[16]\n\tbinary.BigEndian.PutUint32(data[i+8:], 0)\n\tbinary.BigEndian.PutUint32(data[i+12:], 0)\n\ti += 16\n\n\t// Option=MTU:\n\n\tdata[i] = 5   // Type\n\tdata[i+1] = 1 // Length\n\ti += 2\n\tbinary.BigEndian.PutUint16(data[i:], 0) // Reserved[2]\n\ti += 2\n\tbinary.BigEndian.PutUint32(data[i:], params.mtu) // MTU[4]\n\ti += 4\n\n\t// Option=Source link-layer address:\n\n\tdata[i] = 1                         // Type\n\tdata[i+1] = byte(srcLLAOptLenValue) // Length\n\ti += 2\n\tcopy(data[i:], lla) // Link-Layer Address[8/24]\n\ti += len(lla) + srcLLAPadLen\n\n\t// Option=Recursive DNS Server:\n\n\tdata[i] = 25  // Type\n\tdata[i+1] = 3 // Length\n\ti += 2\n\tbinary.BigEndian.PutUint16(data[i:], 0) // Reserved[2]\n\ti += 2\n\tbinary.BigEndian.PutUint32(data[i:], 3600) // Lifetime[4]\n\ti += 4\n\tcopy(data[i:], params.recursiveDNSServer) // Addresses of IPv6 Recursive DNS Servers[16]\n\n\treturn data, nil\n}\n\n// Init initializes RA module.\nfunc (ra *raCtx) Init() (err error) {\n\tra.stop.Store(0)\n\tra.conn = nil\n\tif !ra.raAllowSLAAC && !ra.raSLAACOnly {\n\t\treturn nil\n\t}\n\n\tlog.Debug(\"dhcpv6 ra: source IP address: %s  DNS IP address: %s\", ra.ipAddr, ra.dnsIPAddr)\n\n\tparams := icmpv6RA{\n\t\tmanagedAddressConfiguration: !ra.raSLAACOnly,\n\t\totherConfiguration:          !ra.raSLAACOnly,\n\t\tmtu:                         uint32(ra.iface.MTU),\n\t\tprefixLen:                   64,\n\t\trecursiveDNSServer:          ra.dnsIPAddr,\n\t\tsourceLinkLayerAddress:      ra.iface.HardwareAddr,\n\t}\n\tparams.prefix = make([]byte, 16)\n\tcopy(params.prefix, ra.prefixIPAddr[:8]) // /64\n\n\tvar data []byte\n\tdata, err = createICMPv6RAPacket(params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating packet: %w\", err)\n\t}\n\n\tipAndScope := ra.ipAddr.String() + \"%\" + ra.ifaceName\n\tra.conn, err = icmp.ListenPacket(\"ip6:ipv6-icmp\", ipAndScope)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dhcpv6 ra: icmp.ListenPacket: %w\", err)\n\t}\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = errors.WithDeferred(err, ra.Close())\n\t\t}\n\t}()\n\n\tcon6 := ra.conn.IPv6PacketConn()\n\n\tif err = con6.SetHopLimit(255); err != nil {\n\t\treturn fmt.Errorf(\"dhcpv6 ra: SetHopLimit: %w\", err)\n\t}\n\n\tif err = con6.SetMulticastHopLimit(255); err != nil {\n\t\treturn fmt.Errorf(\"dhcpv6 ra: SetMulticastHopLimit: %w\", err)\n\t}\n\n\tmsg := &ipv6.ControlMessage{\n\t\tHopLimit: 255,\n\t\tSrc:      ra.ipAddr,\n\t\tIfIndex:  ra.iface.Index,\n\t}\n\taddr := &net.UDPAddr{\n\t\tIP: net.ParseIP(\"ff02::1\"),\n\t}\n\n\tgo func() {\n\t\tlog.Debug(\"dhcpv6 ra: starting to send periodic RouterAdvertisement packets\")\n\t\tfor ra.stop.Load() == 0 {\n\t\t\t_, err = con6.WriteTo(data, msg, addr)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(\"dhcpv6 ra: WriteTo: %s\", err)\n\t\t\t}\n\t\t\ttime.Sleep(ra.packetSendPeriod)\n\t\t}\n\t\tlog.Debug(\"dhcpv6 ra: loop exit\")\n\t}()\n\n\treturn nil\n}\n\n// Close closes the module.\nfunc (ra *raCtx) Close() (err error) {\n\tlog.Debug(\"dhcpv6 ra: closing\")\n\n\tra.stop.Store(1)\n\n\tif ra.conn != nil {\n\t\treturn ra.conn.Close()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dhcpd/routeradv_internal_test.go",
    "content": "package dhcpd\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCreateICMPv6RAPacket(t *testing.T) {\n\traConf := icmpv6RA{\n\t\tmanagedAddressConfiguration: false,\n\t\totherConfiguration:          true,\n\t\tmtu:                         1500,\n\t\tprefix:                      net.ParseIP(\"1234::\"),\n\t\tprefixLen:                   64,\n\t\trecursiveDNSServer:          net.ParseIP(\"fe80::800:27ff:fe00:0\"),\n\t\tsourceLinkLayerAddress:      []byte{0x0A, 0x00, 0x27, 0x00, 0x00, 0x00},\n\t}\n\n\tpkt, err := createICMPv6RAPacket(raConf)\n\trequire.NoError(t, err)\n\n\ticmpPkt := &layers.ICMPv6{}\n\terr = icmpPkt.DecodeFromBytes(pkt, gopacket.NilDecodeFeedback)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, layers.LayerTypeICMPv6RouterAdvertisement, icmpPkt.NextLayerType())\n\traPkt := &layers.ICMPv6RouterAdvertisement{}\n\terr = raPkt.DecodeFromBytes(icmpPkt.LayerPayload(), gopacket.NilDecodeFeedback)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, raConf.managedAddressConfiguration, raPkt.ManagedAddressConfig())\n\tassert.Equal(t, raConf.otherConfiguration, raPkt.OtherConfig())\n\n\twantOpts := layers.ICMPv6Options{{\n\t\tType: layers.ICMPv6OptPrefixInfo,\n\t\tData: []uint8{\n\t\t\t0x40, 0xC0, 0x00, 0x00, 0x0E, 0x10, 0x00, 0x00,\n\t\t\t0x0E, 0x10, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34,\n\t\t\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t\t\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t\t},\n\t}, {\n\t\tType: layers.ICMPv6OptMTU,\n\t\tData: []uint8{0x00, 0x00, 0x00, 0x00, 0x05, 0xDC},\n\t}, {\n\t\tType: layers.ICMPv6OptSourceAddress,\n\t\tData: []uint8{0x0A, 0x00, 0x27, 0x00, 0x00, 0x0},\n\t}, {\n\t\t// Package layers declares no constant for Recursive DNS Server option.\n\t\tType: layers.ICMPv6Opt(25),\n\t\tData: []uint8{\n\t\t\t0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0xFE, 0x80,\n\t\t\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00,\n\t\t\t0x27, 0xFF, 0xFE, 0x00, 0x00, 0x00,\n\t\t},\n\t}}\n\tassert.Equal(t, wantOpts, raPkt.Options)\n}\n"
  },
  {
    "path": "internal/dhcpd/v46_windows.go",
    "content": "//go:build windows\n\npackage dhcpd\n\n// 'u-root/u-root' package, a dependency of 'insomniacslk/dhcp' package, doesn't build on Windows\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n)\n\ntype winServer struct{}\n\n// type check\nvar _ DHCPServer = winServer{}\n\nfunc (winServer) ResetLeases(_ []*dhcpsvc.Lease) (err error)           { return nil }\nfunc (winServer) GetLeases(_ GetLeasesFlags) (leases []*dhcpsvc.Lease) { return nil }\nfunc (winServer) getLeasesRef() []*dhcpsvc.Lease                       { return nil }\nfunc (winServer) AddStaticLease(_ *dhcpsvc.Lease) (err error)          { return nil }\nfunc (winServer) RemoveStaticLease(_ *dhcpsvc.Lease) (err error)       { return nil }\nfunc (winServer) UpdateStaticLease(_ *dhcpsvc.Lease) (err error)       { return nil }\nfunc (winServer) FindMACbyIP(_ netip.Addr) (mac net.HardwareAddr)      { return nil }\nfunc (winServer) WriteDiskConfig4(_ *V4ServerConf)                     {}\nfunc (winServer) WriteDiskConfig6(_ *V6ServerConf)                     {}\nfunc (winServer) Start(_ context.Context) (err error)                  { return nil }\nfunc (winServer) Stop() (err error)                                    { return nil }\nfunc (winServer) HostByIP(_ netip.Addr) (host string)                  { return \"\" }\nfunc (winServer) IPByHost(_ string) (ip netip.Addr)                    { return netip.Addr{} }\n\nfunc v4Create(_ *V4ServerConf) (s DHCPServer, err error) { return winServer{}, nil }\nfunc v6Create(_ V6ServerConf) (s DHCPServer, err error)  { return winServer{}, nil }\n"
  },
  {
    "path": "internal/dhcpd/v4_unix.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4/server4\"\n\n\t//lint:ignore SA1019 See the TODO in go.mod.\n\t\"github.com/go-ping/ping\"\n)\n\n// v4Server is a DHCPv4 server.\n//\n// TODO(a.garipov): Think about unifying this and v6Server.\ntype v4Server struct {\n\tconf *V4ServerConf\n\n\tsrv *server4.Server\n\n\t// implicitOpts are the options listed in Appendix A of RFC 2131 initialized\n\t// with default values.  It must not have intersections with [explicitOpts].\n\timplicitOpts dhcpv4.Options\n\n\t// explicitOpts are the options parsed from the configuration.  It must not\n\t// have intersections with [implicitOpts].\n\texplicitOpts dhcpv4.Options\n\n\t// leasesLock protects leases, hostsIndex, ipIndex, and leasedOffsets.\n\tleasesLock sync.Mutex\n\n\t// leasedOffsets contains offsets from conf.ipRange.start that have been\n\t// leased.\n\tleasedOffsets *bitSet\n\n\t// leases contains all dynamic and static leases.\n\tleases []*dhcpsvc.Lease\n\n\t// hostsIndex is the set of all hostnames of all known DHCP clients.\n\thostsIndex map[string]*dhcpsvc.Lease\n\n\t// ipIndex is an index of leases by their IP addresses.\n\tipIndex map[netip.Addr]*dhcpsvc.Lease\n}\n\nfunc (s *v4Server) enabled() (ok bool) {\n\treturn s.conf != nil && s.conf.Enabled\n}\n\n// WriteDiskConfig4 - write configuration\nfunc (s *v4Server) WriteDiskConfig4(c *V4ServerConf) {\n\tif s.conf != nil {\n\t\t*c = *s.conf\n\t}\n}\n\n// WriteDiskConfig6 - write configuration\nfunc (s *v4Server) WriteDiskConfig6(c *V6ServerConf) {\n}\n\n// normalizeHostname normalizes a hostname sent by the client.  If err is not\n// nil, norm is an empty string.\nfunc normalizeHostname(hostname string) (norm string, err error) {\n\tdefer func() { err = errors.Annotate(err, \"normalizing %q: %w\", hostname) }()\n\n\tif hostname == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\tnorm = strings.ToLower(hostname)\n\tparts := strings.FieldsFunc(norm, func(c rune) (ok bool) {\n\t\treturn c != '.' && !netutil.IsValidHostOuterRune(c)\n\t})\n\n\tif len(parts) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no valid parts\")\n\t}\n\n\tnorm = strings.Join(parts, \"-\")\n\tnorm = strings.TrimSuffix(norm, \"-\")\n\n\treturn norm, nil\n}\n\n// validHostnameForClient accepts the hostname sent by the client and its IP and\n// returns either a normalized version of that hostname, or a new hostname\n// generated from the IP address, or an empty string.\nfunc (s *v4Server) validHostnameForClient(cliHostname string, ip netip.Addr) (hostname string) {\n\thostname, err := normalizeHostname(cliHostname)\n\tif err != nil {\n\t\tlog.Info(\"dhcpv4: %s\", err)\n\t}\n\n\tif hostname == \"\" {\n\t\thostname = aghnet.GenerateHostname(ip)\n\t}\n\n\terr = netutil.ValidateHostname(hostname)\n\tif err != nil {\n\t\tlog.Info(\"dhcpv4: %s\", err)\n\t\thostname = \"\"\n\t}\n\n\treturn hostname\n}\n\n// HostByIP implements the [Interface] interface for *v4Server.\nfunc (s *v4Server) HostByIP(ip netip.Addr) (host string) {\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tif l, ok := s.ipIndex[ip]; ok {\n\t\treturn l.Hostname\n\t}\n\n\treturn \"\"\n}\n\n// IPByHost implements the [Interface] interface for *v4Server.\nfunc (s *v4Server) IPByHost(host string) (ip netip.Addr) {\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tif l, ok := s.hostsIndex[host]; ok {\n\t\treturn l.IP\n\t}\n\n\treturn netip.Addr{}\n}\n\n// ResetLeases resets leases.\nfunc (s *v4Server) ResetLeases(leases []*dhcpsvc.Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv4: %w\") }()\n\n\tif s.conf == nil {\n\t\treturn nil\n\t}\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\ts.leasedOffsets = newBitSet()\n\ts.hostsIndex = make(map[string]*dhcpsvc.Lease, len(leases))\n\ts.ipIndex = make(map[netip.Addr]*dhcpsvc.Lease, len(leases))\n\ts.leases = nil\n\n\tfor _, l := range leases {\n\t\tif !l.IsStatic {\n\t\t\tl.Hostname = s.validHostnameForClient(l.Hostname, l.IP)\n\t\t}\n\t\terr = s.addLease(l)\n\t\tif err != nil {\n\t\t\t// TODO(a.garipov): Wrap and bubble up the error.\n\t\t\tlog.Error(\"dhcpv4: reset: re-adding a lease for %s (%s): %s\", l.IP, l.HWAddr, err)\n\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// getLeasesRef returns the actual leases slice.  For internal use only.\nfunc (s *v4Server) getLeasesRef() []*dhcpsvc.Lease {\n\treturn s.leases\n}\n\n// isBlocklisted returns true if this lease holds a blocklisted IP.\n//\n// TODO(a.garipov): Make a method of *Lease?\nfunc (s *v4Server) isBlocklisted(l *dhcpsvc.Lease) (ok bool) {\n\tif len(l.HWAddr) == 0 {\n\t\treturn false\n\t}\n\n\tfor _, b := range l.HWAddr {\n\t\tif b != 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// GetLeases returns the list of current DHCP leases.  It is safe for concurrent\n// use.\nfunc (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease) {\n\t// The function shouldn't return nil, because zero-length slice behaves\n\t// differently in cases like marshalling.  Our front-end also requires\n\t// a non-nil value in the response.\n\tleases = []*dhcpsvc.Lease{}\n\n\tgetDynamic := flags&LeasesDynamic != 0\n\tgetStatic := flags&LeasesStatic != 0\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tnow := time.Now()\n\tfor _, l := range s.leases {\n\t\tif getDynamic && l.Expiry.After(now) && !s.isBlocklisted(l) {\n\t\t\tleases = append(leases, l.Clone())\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif getStatic && l.IsStatic {\n\t\t\tleases = append(leases, l.Clone())\n\t\t}\n\t}\n\n\treturn leases\n}\n\n// FindMACbyIP implements the [Interface] for *v4Server.\nfunc (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {\n\tif !ip.Is4() {\n\t\treturn nil\n\t}\n\n\tnow := time.Now()\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tif l, ok := s.ipIndex[ip]; ok {\n\t\tif l.IsStatic || l.Expiry.After(now) {\n\t\t\treturn l.HWAddr\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// defaultHwAddrLen is the default length of a hardware (MAC) address.\nconst defaultHwAddrLen = 6\n\n// Add the specified IP to the black list for a time period\nfunc (s *v4Server) blocklistLease(l *dhcpsvc.Lease) {\n\tl.HWAddr = make(net.HardwareAddr, defaultHwAddrLen)\n\tl.Hostname = \"\"\n\tl.Expiry = time.Now().Add(s.conf.leaseTime)\n}\n\n// rmLeaseByIndex removes a lease by its index in the leases slice.\nfunc (s *v4Server) rmLeaseByIndex(i int) {\n\tn := len(s.leases)\n\tif i >= n {\n\t\t// TODO(a.garipov): Better error handling.\n\t\tlog.Debug(\"dhcpv4: can't remove lease at index %d: no such lease\", i)\n\n\t\treturn\n\t}\n\n\tl := s.leases[i]\n\ts.leases = append(s.leases[:i], s.leases[i+1:]...)\n\n\tr := s.conf.ipRange\n\tleaseIP := net.IP(l.IP.AsSlice())\n\toffset, ok := r.offset(leaseIP)\n\tif ok {\n\t\ts.leasedOffsets.set(offset, false)\n\t}\n\n\tdelete(s.hostsIndex, l.Hostname)\n\tdelete(s.ipIndex, l.IP)\n\n\tlog.Debug(\"dhcpv4: removed lease %s (%s)\", l.IP, l.HWAddr)\n}\n\n// Remove a dynamic lease with the same properties\n// Return error if a static lease is found\n//\n// TODO(s.chzhen):  Refactor the code.\nfunc (s *v4Server) rmDynamicLease(lease *dhcpsvc.Lease) (err error) {\n\tfor i, l := range s.leases {\n\t\tisStatic := l.IsStatic\n\n\t\tif bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP == lease.IP {\n\t\t\tif isStatic {\n\t\t\t\treturn errors.Error(\"static lease already exists\")\n\t\t\t}\n\n\t\t\ts.rmLeaseByIndex(i)\n\t\t\tif i == len(s.leases) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tl = s.leases[i]\n\t\t}\n\n\t\tif !isStatic && l.Hostname == lease.Hostname {\n\t\t\tl.Hostname = \"\"\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst (\n\t// ErrDupHostname is returned by addLease, validateStaticLease when the\n\t// modified lease has a not empty non-unique hostname.\n\tErrDupHostname = errors.Error(\"hostname is not unique\")\n\n\t// ErrDupIP is returned by addLease, validateStaticLease when the modified\n\t// lease has a non-unique IP address.\n\tErrDupIP = errors.Error(\"ip address is not unique\")\n)\n\n// addLease adds a dynamic or static lease.\nfunc (s *v4Server) addLease(l *dhcpsvc.Lease) (err error) {\n\tr := s.conf.ipRange\n\tleaseIP := net.IP(l.IP.AsSlice())\n\toffset, inOffset := r.offset(leaseIP)\n\n\tif l.IsStatic {\n\t\t// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is\n\t\t// disabled.\n\t\tif sn := s.conf.subnet; !sn.Contains(l.IP) {\n\t\t\treturn fmt.Errorf(\"subnet %s does not contain the ip %q\", sn, l.IP)\n\t\t}\n\t} else if !inOffset {\n\t\treturn fmt.Errorf(\"lease %s (%s) out of range, not adding\", l.IP, l.HWAddr)\n\t}\n\n\t// TODO(e.burkov):  l must have a valid hostname here, investigate.\n\tif l.Hostname != \"\" {\n\t\tif _, ok := s.hostsIndex[l.Hostname]; ok {\n\t\t\treturn ErrDupHostname\n\t\t}\n\n\t\ts.hostsIndex[l.Hostname] = l\n\t}\n\ts.ipIndex[l.IP] = l\n\n\ts.leases = append(s.leases, l)\n\ts.leasedOffsets.set(offset, true)\n\n\treturn nil\n}\n\n// rmLease removes a lease with the same properties.\nfunc (s *v4Server) rmLease(lease *dhcpsvc.Lease) (err error) {\n\tif len(s.leases) == 0 {\n\t\treturn nil\n\t}\n\n\tfor i, l := range s.leases {\n\t\tif l.IP == lease.IP {\n\t\t\tif !bytes.Equal(l.HWAddr, lease.HWAddr) || l.Hostname != lease.Hostname {\n\t\t\t\treturn fmt.Errorf(\"lease for ip %s is different: %+v\", lease.IP, l)\n\t\t\t}\n\n\t\t\ts.rmLeaseByIndex(i)\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn errors.Error(\"lease not found\")\n}\n\n// ErrUnconfigured is returned from the server's method when it requires the\n// server to be configured and it's not.\nconst ErrUnconfigured errors.Error = \"server is unconfigured\"\n\n// AddStaticLease implements the DHCPServer interface for *v4Server.  It is\n// safe for concurrent use.\nfunc (s *v4Server) AddStaticLease(l *dhcpsvc.Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv4: adding static lease: %w\") }()\n\n\tif s.conf == nil {\n\t\treturn ErrUnconfigured\n\t}\n\n\tl.IP = l.IP.Unmap()\n\n\tif !l.IP.Is4() {\n\t\treturn fmt.Errorf(\"invalid IP %q: only IPv4 is supported\", l.IP)\n\t} else if gwIP := s.conf.GatewayIP; gwIP == l.IP {\n\t\treturn fmt.Errorf(\"can't assign the gateway IP %q to the lease\", gwIP)\n\t}\n\n\tl.IsStatic = true\n\n\terr = netutil.ValidateMAC(l.HWAddr)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif hostname := l.Hostname; hostname != \"\" {\n\t\thostname, err = normalizeHostname(hostname)\n\t\tif err != nil {\n\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\n\t\terr = netutil.ValidateHostname(hostname)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"validating hostname: %w\", err)\n\t\t}\n\n\t\t// Don't check for hostname uniqueness, since we try to emulate dnsmasq\n\t\t// here, which means that rmDynamicLease below will simply empty the\n\t\t// hostname of the dynamic lease if there even is one.  In case a static\n\t\t// lease with the same name already exists, addLease will return an\n\t\t// error and the lease won't be added.\n\n\t\tl.Hostname = hostname\n\t}\n\n\terr = s.updateStaticLease(l)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\ts.conf.notify(LeaseChangedDBStore)\n\ts.conf.notify(LeaseChangedAddedStatic)\n\n\treturn nil\n}\n\n// UpdateStaticLease updates IP, hostname of the static lease.\nfunc (s *v4Server) UpdateStaticLease(l *dhcpsvc.Lease) (err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = errors.Annotate(err, \"dhcpv4: updating static lease: %w\")\n\n\t\t\treturn\n\t\t}\n\n\t\ts.conf.notify(LeaseChangedDBStore)\n\t\ts.conf.notify(LeaseChangedRemovedStatic)\n\t}()\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tfound := s.findLease(l.HWAddr)\n\tif found == nil {\n\t\treturn fmt.Errorf(\"can't find lease %s\", l.HWAddr)\n\t}\n\n\terr = s.validateStaticLease(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = s.rmLease(found)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"removing previous lease for %s (%s): %w\", l.IP, l.HWAddr, err)\n\t}\n\n\terr = s.addLease(l)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"adding updated static lease for %s (%s): %w\", l.IP, l.HWAddr, err)\n\t}\n\n\treturn nil\n}\n\n// validateStaticLease returns an error if the static lease is invalid.\nfunc (s *v4Server) validateStaticLease(l *dhcpsvc.Lease) (err error) {\n\thostname, err := normalizeHostname(l.Hostname)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = netutil.ValidateHostname(hostname)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating hostname: %w\", err)\n\t}\n\n\tdup, ok := s.hostsIndex[hostname]\n\tif ok && !bytes.Equal(dup.HWAddr, l.HWAddr) {\n\t\treturn ErrDupHostname\n\t}\n\n\tdup, ok = s.ipIndex[l.IP]\n\tif ok && !bytes.Equal(dup.HWAddr, l.HWAddr) {\n\t\treturn ErrDupIP\n\t}\n\n\tl.Hostname = hostname\n\n\tif gwIP := s.conf.GatewayIP; gwIP == l.IP {\n\t\treturn fmt.Errorf(\"can't assign the gateway IP %q to the lease\", gwIP)\n\t}\n\n\tif sn := s.conf.subnet; !sn.Contains(l.IP) {\n\t\treturn fmt.Errorf(\"subnet %s does not contain the ip %q\", sn, l.IP)\n\t}\n\n\treturn nil\n}\n\n// updateStaticLease safe removes dynamic lease with the same properties and\n// then adds a static lease l.\nfunc (s *v4Server) updateStaticLease(l *dhcpsvc.Lease) (err error) {\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\terr = s.rmDynamicLease(l)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"removing dynamic leases for %s (%s): %w\", l.IP, l.HWAddr, err)\n\t}\n\n\terr = s.addLease(l)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"adding static lease for %s (%s): %w\", l.IP, l.HWAddr, err)\n\t}\n\n\treturn nil\n}\n\n// RemoveStaticLease removes a static lease.  It is safe for concurrent use.\nfunc (s *v4Server) RemoveStaticLease(l *dhcpsvc.Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv4: %w\") }()\n\n\tif s.conf == nil {\n\t\treturn ErrUnconfigured\n\t}\n\n\tif !l.IP.Is4() {\n\t\treturn fmt.Errorf(\"invalid IP\")\n\t}\n\n\terr = netutil.ValidateMAC(l.HWAddr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating lease: %w\", err)\n\t}\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\ts.conf.notify(LeaseChangedDBStore)\n\t\ts.conf.notify(LeaseChangedRemovedStatic)\n\t}()\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\treturn s.rmLease(l)\n}\n\n// addrAvailable sends an ICP request to the specified IP address.  It returns\n// true if the remote host doesn't reply, which probably means that the IP\n// address is available.\n//\n// TODO(a.garipov): I'm not sure that this is the best way to do this.\nfunc (s *v4Server) addrAvailable(target net.IP) (avail bool) {\n\tif s.conf.ICMPTimeout == 0 {\n\t\treturn true\n\t}\n\n\tpinger, err := ping.NewPinger(target.String())\n\tif err != nil {\n\t\tlog.Error(\"dhcpv4: ping.NewPinger(): %s\", err)\n\n\t\treturn true\n\t}\n\n\tpinger.SetPrivileged(true)\n\tpinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond\n\tpinger.Count = 1\n\treply := false\n\tpinger.OnRecv = func(_ *ping.Packet) {\n\t\treply = true\n\t}\n\n\tlog.Debug(\"dhcpv4: sending icmp echo to %s\", target)\n\n\terr = pinger.Run()\n\tif err != nil {\n\t\tlog.Error(\"dhcpv4: pinger.Run(): %s\", err)\n\n\t\treturn true\n\t}\n\n\tif reply {\n\t\tlog.Info(\"dhcpv4: ip conflict: %s is already used by another device\", target)\n\n\t\treturn false\n\t}\n\n\tlog.Debug(\"dhcpv4: icmp procedure is complete: %q\", target)\n\n\treturn true\n}\n\n// findLease finds a lease by its MAC-address.\nfunc (s *v4Server) findLease(mac net.HardwareAddr) (l *dhcpsvc.Lease) {\n\tfor _, l = range s.leases {\n\t\tif bytes.Equal(mac, l.HWAddr) {\n\t\t\treturn l\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// nextIP generates a new free IP.\nfunc (s *v4Server) nextIP() (ip net.IP) {\n\tr := s.conf.ipRange\n\tip = r.find(func(next net.IP) (ok bool) {\n\t\toffset, ok := r.offset(next)\n\t\tif !ok {\n\t\t\t// Shouldn't happen.\n\t\t\treturn false\n\t\t}\n\n\t\treturn !s.leasedOffsets.isSet(offset)\n\t})\n\n\treturn ip.To4()\n}\n\n// Find an expired lease and return its index or -1\nfunc (s *v4Server) findExpiredLease() int {\n\tnow := time.Now()\n\tfor i, lease := range s.leases {\n\t\tif !lease.IsStatic && lease.Expiry.Before(now) {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n\n// reserveLease reserves a lease for a client by its MAC-address.  It returns\n// nil if it couldn't allocate a new lease.\nfunc (s *v4Server) reserveLease(mac net.HardwareAddr) (l *dhcpsvc.Lease, err error) {\n\tl = &dhcpsvc.Lease{HWAddr: slices.Clone(mac)}\n\n\tnextIP := s.nextIP()\n\tif nextIP == nil {\n\t\ti := s.findExpiredLease()\n\t\tif i < 0 {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tcopy(s.leases[i].HWAddr, mac)\n\n\t\treturn s.leases[i], nil\n\t}\n\n\tnetIP, ok := netip.AddrFromSlice(nextIP)\n\tif !ok {\n\t\treturn nil, errors.Error(\"invalid ip\")\n\t}\n\n\tl.IP = netIP\n\n\terr = s.addLease(l)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn l, nil\n}\n\n// commitLease refreshes l's values.  It takes the desired hostname into account\n// when setting it into the lease, but generates a unique one if the provided\n// can't be used.\nfunc (s *v4Server) commitLease(l *dhcpsvc.Lease, hostname string) {\n\tprev := l.Hostname\n\thostname = s.validHostnameForClient(hostname, l.IP)\n\n\tif _, ok := s.hostsIndex[hostname]; ok {\n\t\tlog.Info(\"dhcpv4: hostname %q already exists\", hostname)\n\n\t\tif prev == \"\" {\n\t\t\t// The lease is just allocated due to DHCPDISCOVER.\n\t\t\thostname = aghnet.GenerateHostname(l.IP)\n\t\t} else {\n\t\t\thostname = prev\n\t\t}\n\t}\n\tif l.Hostname != hostname {\n\t\tl.Hostname = hostname\n\t}\n\n\tl.Expiry = time.Now().Add(s.conf.leaseTime)\n\tif prev != \"\" && prev != l.Hostname {\n\t\tdelete(s.hostsIndex, prev)\n\t}\n\tif l.Hostname != \"\" {\n\t\ts.hostsIndex[l.Hostname] = l\n\t}\n\ts.ipIndex[l.IP] = l\n}\n\n// allocateLease allocates a new lease for the MAC address.  If there are no IP\n// addresses left, both l and err are nil.\nfunc (s *v4Server) allocateLease(mac net.HardwareAddr) (l *dhcpsvc.Lease, err error) {\n\tfor {\n\t\tl, err = s.reserveLease(mac)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"reserving a lease: %w\", err)\n\t\t} else if l == nil {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tleaseIP := l.IP.AsSlice()\n\t\tif s.addrAvailable(leaseIP) {\n\t\t\treturn l, nil\n\t\t}\n\n\t\ts.blocklistLease(l)\n\t}\n}\n\n// handleDiscover is the handler for the DHCP Discover request.\nfunc (s *v4Server) handleDiscover(req, resp *dhcpv4.DHCPv4) (l *dhcpsvc.Lease, err error) {\n\tmac := req.ClientHWAddr\n\n\tdefer s.conf.notify(LeaseChangedDBStore)\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tl = s.findLease(mac)\n\tif l != nil {\n\t\treqIP := req.RequestedIPAddress()\n\t\tleaseIP := net.IP(l.IP.AsSlice())\n\t\tif len(reqIP) != 0 && !reqIP.Equal(leaseIP) {\n\t\t\tlog.Debug(\"dhcpv4: different RequestedIP: %s != %s\", reqIP, leaseIP)\n\t\t}\n\n\t\tresp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))\n\n\t\treturn l, nil\n\t}\n\n\tl, err = s.allocateLease(mac)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if l == nil {\n\t\tlog.Debug(\"dhcpv4: no more ip addresses\")\n\n\t\treturn nil, nil\n\t}\n\n\tresp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))\n\n\treturn l, nil\n}\n\n// OptionFQDN returns a DHCPv4 option for sending the FQDN to the client\n// requested another hostname.\n//\n// See https://datatracker.ietf.org/doc/html/rfc4702.\nfunc OptionFQDN(fqdn string) (opt dhcpv4.Option) {\n\toptData := []byte{\n\t\t// Set only S and O DHCP client FQDN option flags.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc4702#section-2.1.\n\t\t1<<0 | 1<<1,\n\t\t// The RCODE fields should be set to 0xFF in the server responses.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc4702#section-2.2.\n\t\t0xFF,\n\t\t0xFF,\n\t}\n\toptData = append(optData, fqdn...)\n\n\treturn dhcpv4.OptGeneric(dhcpv4.OptionFQDN, optData)\n}\n\n// checkLease checks if the pair of mac and ip is already leased.  The mismatch\n// is true when the existing lease has the same hardware address but differs in\n// its IP address.\nfunc (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (l *dhcpsvc.Lease, mismatch bool) {\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tnetIP, ok := netip.AddrFromSlice(ip)\n\tif !ok {\n\t\tlog.Info(\"check lease: invalid IP: %s\", ip)\n\n\t\treturn nil, false\n\t}\n\n\tfor _, l = range s.leases {\n\t\tif !bytes.Equal(l.HWAddr, mac) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif l.IP == netIP {\n\t\t\treturn l, false\n\t\t}\n\n\t\tlog.Debug(\n\t\t\t`dhcpv4: mismatched OptionRequestedIPAddress in req msg for %s`,\n\t\t\tmac,\n\t\t)\n\n\t\treturn nil, true\n\t}\n\n\treturn nil, false\n}\n\n// handleSelecting handles the DHCPREQUEST generated during SELECTING state.\nfunc (s *v4Server) handleSelecting(\n\treq *dhcpv4.DHCPv4,\n\treqIP net.IP,\n\tsid net.IP,\n) (l *dhcpsvc.Lease, needsReply bool) {\n\t// Client inserts the address of the selected server in server identifier,\n\t// ciaddr MUST be zero.\n\tmac := req.ClientHWAddr\n\n\tif !sid.Equal(s.conf.dnsIPAddrs[0].AsSlice()) {\n\t\tlog.Debug(\"dhcpv4: bad server identifier in req msg for %s: %s\", mac, sid)\n\n\t\treturn nil, false\n\t} else if ciaddr := req.ClientIPAddr; ciaddr != nil && !ciaddr.IsUnspecified() {\n\t\tlog.Debug(\"dhcpv4: non-zero ciaddr in selecting req msg for %s\", mac)\n\n\t\treturn nil, false\n\t}\n\n\t// Requested IP address MUST be filled in with the yiaddr value from the\n\t// chosen DHCPOFFER.\n\tif ip4 := reqIP.To4(); ip4 == nil {\n\t\tlog.Debug(\"dhcpv4: bad requested address in req msg for %s: %s\", mac, reqIP)\n\n\t\treturn nil, false\n\t}\n\n\tvar mismatch bool\n\tif l, mismatch = s.checkLease(mac, reqIP); mismatch {\n\t\treturn nil, true\n\t} else if l == nil {\n\t\tlog.Debug(\"dhcpv4: no reserved lease for %s\", mac)\n\t}\n\n\treturn l, true\n}\n\n// handleInitReboot handles the DHCPREQUEST generated during INIT-REBOOT state.\nfunc (s *v4Server) handleInitReboot(\n\treq *dhcpv4.DHCPv4,\n\treqIP net.IP,\n) (l *dhcpsvc.Lease, needsReply bool) {\n\tmac := req.ClientHWAddr\n\n\tip4 := reqIP.To4()\n\tif ip4 == nil {\n\t\tlog.Debug(\"dhcpv4: bad requested address in req msg for %s: %s\", mac, reqIP)\n\n\t\treturn nil, false\n\t}\n\n\t// ciaddr MUST be zero.  The client is seeking to verify a previously\n\t// allocated, cached configuration.\n\tif ciaddr := req.ClientIPAddr; ciaddr != nil && !ciaddr.IsUnspecified() {\n\t\tlog.Debug(\"dhcpv4: non-zero ciaddr in init-reboot req msg for %s\", mac)\n\n\t\treturn nil, false\n\t}\n\n\tif !s.conf.subnet.Contains(netip.AddrFrom4([4]byte(ip4))) {\n\t\t// If the DHCP server detects that the client is on the wrong net then\n\t\t// the server SHOULD send a DHCPNAK message to the client.\n\t\tlog.Debug(\"dhcpv4: wrong subnet in init-reboot req msg for %s: %s\", mac, reqIP)\n\n\t\treturn nil, true\n\t}\n\n\tvar mismatch bool\n\tif l, mismatch = s.checkLease(mac, reqIP); mismatch {\n\t\treturn nil, true\n\t} else if l == nil {\n\t\t// If the DHCP server has no record of this client, then it MUST remain\n\t\t// silent, and MAY output a warning to the network administrator.\n\t\tlog.Info(\"dhcpv4: warning: no existing lease for %s\", mac)\n\n\t\treturn nil, false\n\t}\n\n\treturn l, true\n}\n\n// handleRenew handles the DHCPREQUEST generated during RENEWING or REBINDING\n// state.\nfunc (s *v4Server) handleRenew(req *dhcpv4.DHCPv4) (l *dhcpsvc.Lease, needsReply bool) {\n\tmac := req.ClientHWAddr\n\n\t// ciaddr MUST be filled in with client's IP address.\n\tciaddr := req.ClientIPAddr\n\tif ciaddr == nil || ciaddr.IsUnspecified() || ciaddr.To4() == nil {\n\t\tlog.Debug(\"dhcpv4: bad ciaddr in renew req msg for %s: %s\", mac, ciaddr)\n\n\t\treturn nil, false\n\t}\n\n\tvar mismatch bool\n\tif l, mismatch = s.checkLease(mac, ciaddr); mismatch {\n\t\treturn nil, true\n\t} else if l == nil {\n\t\t// If the DHCP server has no record of this client, then it MUST remain\n\t\t// silent, and MAY output a warning to the network administrator.\n\t\tlog.Info(\"dhcpv4: warning: no existing lease for %s\", mac)\n\n\t\treturn nil, false\n\t}\n\n\treturn l, true\n}\n\n// handleByRequestType handles the DHCPREQUEST according to the state during\n// which it's generated by client.\nfunc (s *v4Server) handleByRequestType(req *dhcpv4.DHCPv4) (lease *dhcpsvc.Lease, needsReply bool) {\n\treqIP, sid := req.RequestedIPAddress(), req.ServerIdentifier()\n\n\tif sid != nil && !sid.IsUnspecified() {\n\t\t// If the DHCPREQUEST message contains a server identifier option, the\n\t\t// message is in response to a DHCPOFFER message.  Otherwise, the\n\t\t// message is a request to verify or extend an existing lease.\n\t\treturn s.handleSelecting(req, reqIP, sid)\n\t}\n\n\tif reqIP != nil && !reqIP.IsUnspecified() {\n\t\t// Requested IP address option MUST be filled in with client's notion of\n\t\t// its previously assigned address.\n\t\treturn s.handleInitReboot(req, reqIP)\n\t}\n\n\t// Server identifier MUST NOT be filled in, requested IP address option MUST\n\t// NOT be filled in.\n\treturn s.handleRenew(req)\n}\n\n// handleRequest is the handler for a DHCPREQUEST message.\n//\n// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2.\nfunc (s *v4Server) handleRequest(req, resp *dhcpv4.DHCPv4) (lease *dhcpsvc.Lease, needsReply bool) {\n\tlease, needsReply = s.handleByRequestType(req)\n\tif lease == nil {\n\t\treturn nil, needsReply\n\t}\n\n\tresp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))\n\n\thostname := req.HostName()\n\tisRequested := hostname != \"\" || req.ParameterRequestList().Has(dhcpv4.OptionHostName)\n\n\tdefer func() {\n\t\ts.conf.notify(LeaseChangedAdded)\n\t\ts.conf.notify(LeaseChangedDBStore)\n\t}()\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tif lease.IsStatic {\n\t\tif lease.Hostname != \"\" {\n\t\t\t// TODO(e.burkov):  This option is used to update the server's DNS\n\t\t\t// mapping.  The option should only be answered when it has been\n\t\t\t// requested.\n\t\t\tresp.UpdateOption(OptionFQDN(lease.Hostname))\n\t\t}\n\n\t\treturn lease, needsReply\n\t}\n\n\ts.commitLease(lease, hostname)\n\n\tif isRequested {\n\t\tresp.UpdateOption(dhcpv4.OptHostName(lease.Hostname))\n\t}\n\n\treturn lease, needsReply\n}\n\n// handleDecline is the handler for the DHCP Decline request.\nfunc (s *v4Server) handleDecline(req, resp *dhcpv4.DHCPv4) (err error) {\n\ts.conf.notify(LeaseChangedDBStore)\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tmac := req.ClientHWAddr\n\treqIP := req.RequestedIPAddress()\n\tif reqIP == nil {\n\t\treqIP = req.ClientIPAddr\n\t}\n\n\toldLease := s.findLeaseForIP(reqIP, mac)\n\tif oldLease == nil {\n\t\tlog.Info(\"dhcpv4: lease with IP %s for %s not found\", reqIP, mac)\n\n\t\treturn nil\n\t}\n\n\terr = s.rmDynamicLease(oldLease)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"removing old lease for %s: %w\", mac, err)\n\t}\n\n\tnewLease, err := s.allocateLease(mac)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"allocating new lease for %s: %w\", mac, err)\n\t} else if newLease == nil {\n\t\tlog.Info(\"dhcpv4: allocating new lease for %s: no more IP addresses\", mac)\n\n\t\tresp.YourIPAddr = make([]byte, 4)\n\t\tresp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))\n\n\t\treturn nil\n\t}\n\n\tnewLease.Hostname = oldLease.Hostname\n\tnewLease.Expiry = time.Now().Add(s.conf.leaseTime)\n\n\terr = s.addLease(newLease)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"adding new lease for %s: %w\", mac, err)\n\t}\n\n\tlog.Info(\"dhcpv4: changed IP from %s to %s for %s\", reqIP, newLease.IP, mac)\n\n\tresp.YourIPAddr = newLease.IP.AsSlice()\n\tresp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))\n\n\treturn nil\n}\n\n// findLeaseForIP returns a lease for provided ip and mac.\nfunc (s *v4Server) findLeaseForIP(ip net.IP, mac net.HardwareAddr) (l *dhcpsvc.Lease) {\n\tnetIP, ok := netip.AddrFromSlice(ip)\n\tif !ok {\n\t\tlog.Info(\"dhcpv4: invalid IP: %s\", ip)\n\n\t\treturn nil\n\t}\n\n\tfor _, il := range s.leases {\n\t\tif bytes.Equal(il.HWAddr, mac) && il.IP == netIP {\n\t\t\treturn il\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// handleRelease is the handler for the DHCP Release request.\nfunc (s *v4Server) handleRelease(req, resp *dhcpv4.DHCPv4) (err error) {\n\tmac := req.ClientHWAddr\n\treqIP := req.RequestedIPAddress()\n\tif reqIP == nil {\n\t\treqIP = req.ClientIPAddr\n\t}\n\n\t// TODO(a.garipov): Add a separate notification type for dynamic lease\n\t// removal?\n\tdefer s.conf.notify(LeaseChangedDBStore)\n\n\tn := 0\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tnetIP, ok := netip.AddrFromSlice(reqIP)\n\tif !ok {\n\t\tlog.Info(\"dhcpv4: invalid IP: %s\", reqIP)\n\n\t\treturn nil\n\t}\n\n\tfor _, l := range s.leases {\n\t\tif !bytes.Equal(l.HWAddr, mac) || l.IP != netIP {\n\t\t\tcontinue\n\t\t}\n\n\t\terr = s.rmDynamicLease(l)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"removing dynamic lease for %s: %w\", mac, err)\n\t\t}\n\n\t\tn++\n\t}\n\n\tlog.Info(\"dhcpv4: released %d dynamic leases for %s\", n, mac)\n\n\tresp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))\n\n\treturn nil\n}\n\n// messageHandler describes a DHCPv4 message handler function.\ntype messageHandler func(\n\ts *v4Server,\n\treq *dhcpv4.DHCPv4,\n\tresp *dhcpv4.DHCPv4,\n) (rCode int, l *dhcpsvc.Lease, err error)\n\n// messageHandlers is a map of handlers for various messages with message types\n// keys.\nvar messageHandlers = map[dhcpv4.MessageType]messageHandler{\n\tdhcpv4.MessageTypeDiscover: func(\n\t\ts *v4Server,\n\t\treq *dhcpv4.DHCPv4,\n\t\tresp *dhcpv4.DHCPv4,\n\t) (rCode int, l *dhcpsvc.Lease, err error) {\n\t\tl, err = s.handleDiscover(req, resp)\n\t\tif err != nil {\n\t\t\treturn 0, nil, fmt.Errorf(\"handling discover: %s\", err)\n\t\t}\n\n\t\tif l == nil {\n\t\t\treturn 0, nil, nil\n\t\t}\n\n\t\treturn 1, l, nil\n\t},\n\tdhcpv4.MessageTypeRequest: func(\n\t\ts *v4Server,\n\t\treq *dhcpv4.DHCPv4,\n\t\tresp *dhcpv4.DHCPv4,\n\t) (rCode int, l *dhcpsvc.Lease, err error) {\n\t\tvar toReply bool\n\t\tl, toReply = s.handleRequest(req, resp)\n\t\tif l == nil {\n\t\t\tif toReply {\n\t\t\t\treturn 0, nil, nil\n\t\t\t}\n\n\t\t\t// Drop the packet.\n\t\t\treturn -1, nil, nil\n\t\t}\n\n\t\treturn 1, l, nil\n\t},\n\tdhcpv4.MessageTypeDecline: func(\n\t\ts *v4Server,\n\t\treq *dhcpv4.DHCPv4,\n\t\tresp *dhcpv4.DHCPv4,\n\t) (rCode int, l *dhcpsvc.Lease, err error) {\n\t\terr = s.handleDecline(req, resp)\n\t\tif err != nil {\n\t\t\treturn 0, nil, fmt.Errorf(\"handling decline: %s\", err)\n\t\t}\n\n\t\treturn 1, nil, nil\n\t},\n\tdhcpv4.MessageTypeRelease: func(\n\t\ts *v4Server,\n\t\treq *dhcpv4.DHCPv4,\n\t\tresp *dhcpv4.DHCPv4,\n\t) (rCode int, l *dhcpsvc.Lease, err error) {\n\t\terr = s.handleRelease(req, resp)\n\t\tif err != nil {\n\t\t\treturn 0, nil, fmt.Errorf(\"handling release: %s\", err)\n\t\t}\n\n\t\treturn 1, nil, nil\n\t},\n}\n\n// handle processes request, it finds a lease associated with MAC address and\n// prepares response.\n//\n// Possible return values are:\n//   - \"1\": OK,\n//   - \"0\": error, reply with Nak,\n//   - \"-1\": error, don't reply.\nfunc (s *v4Server) handle(req, resp *dhcpv4.DHCPv4) (rCode int) {\n\tvar err error\n\n\t// Include server's identifier option since any reply should contain it.\n\t//\n\t// See https://datatracker.ietf.org/doc/html/rfc2131#page-29.\n\tresp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0].AsSlice()))\n\n\thandler := messageHandlers[req.MessageType()]\n\tif handler == nil {\n\t\ts.updateOptions(req, resp)\n\n\t\treturn 1\n\t}\n\n\trCode, l, err := handler(s, req, resp)\n\tif err != nil {\n\t\tlog.Error(\"dhcpv4: %s\", err)\n\n\t\treturn 0\n\t}\n\n\tif rCode != 1 {\n\t\treturn rCode\n\t}\n\n\tif l != nil {\n\t\tresp.YourIPAddr = l.IP.AsSlice()\n\t}\n\n\ts.updateOptions(req, resp)\n\n\treturn 1\n}\n\n// updateOptions updates the options of the response in accordance with the\n// request and RFC 2131.\n//\n// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.\nfunc (s *v4Server) updateOptions(req, resp *dhcpv4.DHCPv4) {\n\t// Set IP address lease time for all DHCPOFFER messages and DHCPACK messages\n\t// replied for DHCPREQUEST.\n\t//\n\t// TODO(e.burkov):  Inspect why this is always set to configured value.\n\tresp.UpdateOption(dhcpv4.OptIPAddressLeaseTime(s.conf.leaseTime))\n\n\t// If the server recognizes the parameter as a parameter defined in the Host\n\t// Requirements Document, the server MUST include the default value for that\n\t// parameter.\n\tfor _, code := range req.ParameterRequestList() {\n\t\tif val := s.implicitOpts.Get(code); val != nil {\n\t\t\tresp.UpdateOption(dhcpv4.OptGeneric(code, val))\n\t\t}\n\t}\n\n\t// If the server has been explicitly configured with a default value for the\n\t// parameter or the parameter has a non-default value on the client's\n\t// subnet, the server MUST include that value in an appropriate option.\n\tfor code, val := range s.explicitOpts {\n\t\tif val != nil {\n\t\t\tresp.Options[code] = val\n\t\t} else {\n\t\t\t// Delete options explicitly configured to be removed.\n\t\t\tdelete(resp.Options, code)\n\t\t}\n\t}\n}\n\n// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Discover,ClientID,ReqIP,HostName) -> server(255.255.255.255:67)\n// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Type=Offer,ServerID,SubnetMask,LeaseTime) <- server(<IP>:67)\n// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Request,ClientID,ReqIP||ClientIP,HostName,ServerID,ParamReqList) -> server(255.255.255.255:67)\n// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Type=ACK,ServerID,SubnetMask,LeaseTime) <- server(<IP>:67)\nfunc (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) {\n\tlog.Debug(\"dhcpv4: received message: %s\", req.Summary())\n\n\tswitch req.MessageType() {\n\tcase\n\t\tdhcpv4.MessageTypeDiscover,\n\t\tdhcpv4.MessageTypeRequest,\n\t\tdhcpv4.MessageTypeDecline,\n\t\tdhcpv4.MessageTypeRelease:\n\t\t// Go on.\n\tdefault:\n\t\tlog.Debug(\"dhcpv4: unsupported message type %d\", req.MessageType())\n\n\t\treturn\n\t}\n\n\tresp, err := dhcpv4.NewReplyFromRequest(req)\n\tif err != nil {\n\t\tlog.Debug(\"dhcpv4: dhcpv4.New: %s\", err)\n\n\t\treturn\n\t}\n\n\terr = netutil.ValidateMAC(req.ClientHWAddr)\n\tif err != nil {\n\t\tlog.Error(\"dhcpv4: invalid ClientHWAddr: %s\", err)\n\n\t\treturn\n\t}\n\n\tr := s.handle(req, resp)\n\tif r < 0 {\n\t\treturn\n\t} else if r == 0 {\n\t\tresp.Options.Update(dhcpv4.OptMessageType(dhcpv4.MessageTypeNak))\n\t}\n\n\ts.send(peer, conn, req, resp)\n}\n\n// Start starts the IPv4 DHCP server.\nfunc (s *v4Server) Start(ctx context.Context) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv4: %w\") }()\n\n\tif !s.enabled() {\n\t\treturn nil\n\t}\n\n\tifaceName := s.conf.InterfaceName\n\tiface, err := net.InterfaceByName(ifaceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"finding interface %s by name: %w\", ifaceName, err)\n\t}\n\n\tlog.Debug(\"dhcpv4: starting...\")\n\n\tdnsIPAddrs, err := aghnet.IfaceDNSIPAddrs(\n\t\tctx,\n\t\ts.conf.Logger,\n\t\tiface,\n\t\taghnet.IPVersion4,\n\t\tdefaultMaxAttempts,\n\t\tdefaultBackoff,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"interface %s: %w\", ifaceName, err)\n\t}\n\n\tif len(dnsIPAddrs) == 0 {\n\t\t// No available IP addresses which may appear later.\n\t\treturn nil\n\t}\n\n\ts.configureDNSIPAddrs(dnsIPAddrs)\n\n\tvar c net.PacketConn\n\tif c, err = s.newDHCPConn(iface); err != nil {\n\t\treturn err\n\t}\n\n\ts.srv, err = server4.NewServer(\n\t\tiface.Name,\n\t\tnil,\n\t\ts.packetHandler,\n\t\tserver4.WithConn(c),\n\t\tserver4.WithDebugLogger(),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Info(\"dhcpv4: listening\")\n\n\tgo func() {\n\t\tif sErr := s.srv.Serve(); errors.Is(sErr, net.ErrClosed) {\n\t\t\tlog.Info(\"dhcpv4: server is closed\")\n\t\t} else if sErr != nil {\n\t\t\tlog.Error(\"dhcpv4: srv.Serve: %s\", sErr)\n\t\t}\n\t}()\n\n\t// Signal to the clients containers in packages home and dnsforward that\n\t// it should reload the DHCP clients.\n\ts.conf.notify(LeaseChangedAdded)\n\n\treturn nil\n}\n\n// configureDNSIPAddrs updates v4Server configuration with provided slice of\n// dns IP addresses.\nfunc (s *v4Server) configureDNSIPAddrs(dnsIPAddrs []net.IP) {\n\t// Update the value of Domain Name Server option separately from others if\n\t// not assigned yet since its value is available only at server's start.\n\t//\n\t// TODO(e.burkov):  Initialize as implicit option with the rest of default\n\t// options when it will be possible to do before the call to Start.\n\tif !s.explicitOpts.Has(dhcpv4.OptionDomainNameServer) {\n\t\ts.implicitOpts.Update(dhcpv4.OptDNS(dnsIPAddrs...))\n\t}\n\n\tfor _, ip := range dnsIPAddrs {\n\t\tvAddr, err := netutil.IPToAddr(ip, netutil.AddrFamilyIPv4)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\ts.conf.dnsIPAddrs = append(s.conf.dnsIPAddrs, vAddr)\n\t}\n}\n\n// Stop - stop server\nfunc (s *v4Server) Stop() (err error) {\n\tif s.srv == nil {\n\t\treturn nil\n\t}\n\n\tlog.Debug(\"dhcpv4: stopping\")\n\terr = s.srv.Close()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"closing dhcpv4 srv: %w\", err)\n\t}\n\n\t// Signal to the clients containers in packages home and dnsforward that\n\t// it should remove all DHCP clients.\n\ts.conf.notify(LeaseChangedRemovedAll)\n\n\ts.srv = nil\n\n\treturn nil\n}\n\n// Create DHCPv4 server\nfunc v4Create(conf *V4ServerConf) (srv *v4Server, err error) {\n\ts := &v4Server{\n\t\thostsIndex: map[string]*dhcpsvc.Lease{},\n\t\tipIndex:    map[netip.Addr]*dhcpsvc.Lease{},\n\t}\n\n\terr = conf.Validate()\n\tif err != nil {\n\t\t// TODO(a.garipov): Don't use a disabled server in other places or just\n\t\t// use an interface.\n\t\treturn s, err\n\t}\n\n\ts.conf = &V4ServerConf{}\n\t*s.conf = *conf\n\n\t// TODO(a.garipov, d.seregin): Check that every lease is inside the IPRange.\n\ts.leasedOffsets = newBitSet()\n\n\tif conf.LeaseDuration == 0 {\n\t\ts.conf.leaseTime = timeutil.Day\n\t\ts.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())\n\t} else {\n\t\ts.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)\n\t}\n\n\ts.prepareOptions()\n\n\treturn s, nil\n}\n"
  },
  {
    "path": "internal/dhcpd/v4_unix_internal_test.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/insomniacslk/dhcp/dhcpv4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tDefaultRangeStart = netip.MustParseAddr(\"192.168.10.100\")\n\tDefaultRangeEnd   = netip.MustParseAddr(\"192.168.10.200\")\n\tDefaultGatewayIP  = netip.MustParseAddr(\"192.168.10.1\")\n\tDefaultSelfIP     = netip.MustParseAddr(\"192.168.10.2\")\n\tDefaultSubnetMask = netip.MustParseAddr(\"255.255.255.0\")\n)\n\n// defaultV4ServerConf returns the default configuration for *v4Server to use in\n// tests.\nfunc defaultV4ServerConf() (conf *V4ServerConf) {\n\treturn &V4ServerConf{\n\t\tEnabled:    true,\n\t\tRangeStart: DefaultRangeStart,\n\t\tRangeEnd:   DefaultRangeEnd,\n\t\tGatewayIP:  DefaultGatewayIP,\n\t\tSubnetMask: DefaultSubnetMask,\n\t\tnotify:     testNotify,\n\t\tdnsIPAddrs: []netip.Addr{DefaultSelfIP},\n\t}\n}\n\n// defaultSrv prepares the default DHCPServer to use in tests.  The underlying\n// type of s is *v4Server.\nfunc defaultSrv(tb testing.TB) (s DHCPServer) {\n\ttb.Helper()\n\n\tvar err error\n\ts, err = v4Create(defaultV4ServerConf())\n\trequire.NoError(tb, err)\n\n\treturn s\n}\n\nfunc TestV4Server_leasing(t *testing.T) {\n\tconst (\n\t\tstaticName  = \"static-client\"\n\t\tanotherName = \"another-client\"\n\t)\n\n\tstaticIP := netip.MustParseAddr(\"192.168.10.10\")\n\tanotherIP := DefaultRangeStart\n\tstaticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\tanotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}\n\n\ts := defaultSrv(t)\n\n\tt.Run(\"add_static\", func(t *testing.T) {\n\t\terr := s.AddStaticLease(&dhcpsvc.Lease{\n\t\t\tHostname: staticName,\n\t\t\tHWAddr:   staticMAC,\n\t\t\tIP:       staticIP,\n\t\t\tIsStatic: true,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tt.Run(\"same_name\", func(t *testing.T) {\n\t\t\terr = s.AddStaticLease(&dhcpsvc.Lease{\n\t\t\t\tHostname: staticName,\n\t\t\t\tHWAddr:   anotherMAC,\n\t\t\t\tIP:       anotherIP,\n\t\t\t\tIsStatic: true,\n\t\t\t})\n\t\t\tassert.ErrorIs(t, err, ErrDupHostname)\n\t\t})\n\n\t\tt.Run(\"same_mac\", func(t *testing.T) {\n\t\t\twantErrMsg := \"dhcpv4: adding static lease: removing \" +\n\t\t\t\t\"dynamic leases for \" + anotherIP.String() +\n\t\t\t\t\" (\" + staticMAC.String() + \"): static lease already exists\"\n\n\t\t\terr = s.AddStaticLease(&dhcpsvc.Lease{\n\t\t\t\tHostname: anotherName,\n\t\t\t\tHWAddr:   staticMAC,\n\t\t\t\tIP:       anotherIP,\n\t\t\t\tIsStatic: true,\n\t\t\t})\n\t\t\ttestutil.AssertErrorMsg(t, wantErrMsg, err)\n\t\t})\n\n\t\tt.Run(\"same_ip\", func(t *testing.T) {\n\t\t\twantErrMsg := \"dhcpv4: adding static lease: removing \" +\n\t\t\t\t\"dynamic leases for \" + staticIP.String() +\n\t\t\t\t\" (\" + anotherMAC.String() + \"): static lease already exists\"\n\n\t\t\terr = s.AddStaticLease(&dhcpsvc.Lease{\n\t\t\t\tHostname: anotherName,\n\t\t\t\tHWAddr:   anotherMAC,\n\t\t\t\tIP:       staticIP,\n\t\t\t\tIsStatic: true,\n\t\t\t})\n\t\t\ttestutil.AssertErrorMsg(t, wantErrMsg, err)\n\t\t})\n\t})\n\n\tt.Run(\"add_dynamic\", func(t *testing.T) {\n\t\ts4, ok := s.(*v4Server)\n\t\trequire.True(t, ok)\n\n\t\tdiscoverAnOffer := func(\n\t\t\tt *testing.T,\n\t\t\tname string,\n\t\t\tnetIP netip.Addr,\n\t\t\tmac net.HardwareAddr,\n\t\t) (resp *dhcpv4.DHCPv4) {\n\t\t\ttestutil.CleanupAndRequireSuccess(t, func() (err error) {\n\t\t\t\treturn s.ResetLeases(s.GetLeases(LeasesStatic))\n\t\t\t})\n\n\t\t\tip := net.IP(netIP.AsSlice())\n\t\t\treq, err := dhcpv4.NewDiscovery(\n\t\t\t\tmac,\n\t\t\t\tdhcpv4.WithOption(dhcpv4.OptHostName(name)),\n\t\t\t\tdhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(ip)),\n\t\t\t\tdhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte{1, 2, 3, 4, 5, 6, 8})),\n\t\t\t\tdhcpv4.WithGatewayIP(DefaultGatewayIP.AsSlice()),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresp = &dhcpv4.DHCPv4{}\n\t\t\tres := s4.handle(req, resp)\n\t\t\trequire.Positive(t, res)\n\t\t\trequire.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())\n\n\t\t\tresp.ClientHWAddr = mac\n\n\t\t\treturn resp\n\t\t}\n\n\t\tt.Run(\"same_name\", func(t *testing.T) {\n\t\t\tresp := discoverAnOffer(t, staticName, anotherIP, anotherMAC)\n\n\t\t\treq, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(\n\t\t\t\tdhcpv4.OptHostName(staticName),\n\t\t\t))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres := s4.handle(req, resp)\n\t\t\trequire.Positive(t, res)\n\n\t\t\tvar netIP netip.Addr\n\t\t\tnetIP, ok = netip.AddrFromSlice(resp.YourIPAddr)\n\t\t\trequire.True(t, ok)\n\n\t\t\tassert.Equal(t, aghnet.GenerateHostname(netIP), resp.HostName())\n\t\t})\n\n\t\tt.Run(\"same_mac\", func(t *testing.T) {\n\t\t\tresp := discoverAnOffer(t, anotherName, anotherIP, staticMAC)\n\n\t\t\treq, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(\n\t\t\t\tdhcpv4.OptHostName(anotherName),\n\t\t\t))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres := s4.handle(req, resp)\n\t\t\trequire.Positive(t, res)\n\n\t\t\tfqdnOptData := resp.Options.Get(dhcpv4.OptionFQDN)\n\t\t\trequire.Len(t, fqdnOptData, 3+len(staticName))\n\t\t\tassert.Equal(t, []uint8(staticName), fqdnOptData[3:])\n\n\t\t\tip := net.IP(staticIP.AsSlice())\n\t\t\tassert.Equal(t, ip, resp.YourIPAddr)\n\t\t})\n\n\t\tt.Run(\"same_ip\", func(t *testing.T) {\n\t\t\tresp := discoverAnOffer(t, anotherName, staticIP, anotherMAC)\n\n\t\t\treq, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(\n\t\t\t\tdhcpv4.OptHostName(anotherName),\n\t\t\t))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres := s4.handle(req, resp)\n\t\t\trequire.Positive(t, res)\n\n\t\t\tassert.NotEqual(t, staticIP, resp.YourIPAddr)\n\t\t})\n\t})\n}\n\nfunc TestV4Server_AddRemove_static(t *testing.T) {\n\ts := defaultSrv(t)\n\n\tls := s.GetLeases(LeasesStatic)\n\trequire.Empty(t, ls)\n\n\ttestCases := []struct {\n\t\tlease      *dhcpsvc.Lease\n\t\tname       string\n\t\twantErrMsg string\n\t}{{\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: \"success.local\",\n\t\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\t\tIP:       netip.MustParseAddr(\"192.168.10.150\"),\n\t\t},\n\t\tname:       \"success\",\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: \"probably-router.local\",\n\t\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\t\tIP:       DefaultGatewayIP,\n\t\t},\n\t\tname: \"with_gateway_ip\",\n\t\twantErrMsg: \"dhcpv4: adding static lease: \" +\n\t\t\t`can't assign the gateway IP \"192.168.10.1\" to the lease`,\n\t}, {\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: \"ip6.local\",\n\t\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\t\tIP:       netip.MustParseAddr(\"ffff::1\"),\n\t\t},\n\t\tname: \"ipv6\",\n\t\twantErrMsg: `dhcpv4: adding static lease: ` +\n\t\t\t`invalid IP \"ffff::1\": only IPv4 is supported`,\n\t}, {\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: \"bad-mac.local\",\n\t\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA},\n\t\t\tIP:       netip.MustParseAddr(\"192.168.10.150\"),\n\t\t},\n\t\tname: \"bad_mac\",\n\t\twantErrMsg: `dhcpv4: adding static lease: bad mac address \"aa:aa\": ` +\n\t\t\t`bad mac address length 2, allowed: [6 8 20]`,\n\t}, {\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: \"bad-lbl-.local\",\n\t\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\t\tIP:       netip.MustParseAddr(\"192.168.10.150\"),\n\t\t},\n\t\tname: \"bad_hostname\",\n\t\twantErrMsg: `dhcpv4: adding static lease: validating hostname: ` +\n\t\t\t`bad hostname \"bad-lbl-.local\": ` +\n\t\t\t`bad hostname label \"bad-lbl-\": bad hostname label rune '-'`,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := s.AddStaticLease(tc.lease)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t\tif tc.wantErrMsg != \"\" {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terr = s.RemoveStaticLease(&dhcpsvc.Lease{\n\t\t\t\tIP:     tc.lease.IP,\n\t\t\t\tHWAddr: tc.lease.HWAddr,\n\t\t\t})\n\t\t\tdiffErrMsg := fmt.Sprintf(\"dhcpv4: lease for ip %s is different: %+v\", tc.lease.IP, tc.lease)\n\t\t\ttestutil.AssertErrorMsg(t, diffErrMsg, err)\n\n\t\t\t// Remove static lease.\n\t\t\terr = s.RemoveStaticLease(tc.lease)\n\t\t\trequire.NoError(t, err)\n\t\t})\n\n\t\tls = s.GetLeases(LeasesStatic)\n\t\trequire.Emptyf(t, ls, \"after %s\", tc.name)\n\t}\n}\n\nfunc TestV4_AddReplace(t *testing.T) {\n\tsIface := defaultSrv(t)\n\n\ts, ok := sIface.(*v4Server)\n\trequire.True(t, ok)\n\n\tdynLeases := []dhcpsvc.Lease{{\n\t\tHostname: \"dynamic-1.local\",\n\t\tHWAddr:   net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\tIP:       netip.MustParseAddr(\"192.168.10.150\"),\n\t}, {\n\t\tHostname: \"dynamic-2.local\",\n\t\tHWAddr:   net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\tIP:       netip.MustParseAddr(\"192.168.10.151\"),\n\t}}\n\n\tfor i := range dynLeases {\n\t\terr := s.addLease(&dynLeases[i])\n\t\trequire.NoError(t, err)\n\t}\n\n\tstLeases := []*dhcpsvc.Lease{{\n\t\tHostname: \"static-1.local\",\n\t\tHWAddr:   net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\tIP:       netip.MustParseAddr(\"192.168.10.150\"),\n\t}, {\n\t\tHostname: \"static-2.local\",\n\t\tHWAddr:   net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\tIP:       netip.MustParseAddr(\"192.168.10.152\"),\n\t}}\n\n\tfor _, l := range stLeases {\n\t\terr := s.AddStaticLease(l)\n\t\trequire.NoError(t, err)\n\t}\n\n\tls := s.GetLeases(LeasesStatic)\n\trequire.Len(t, ls, 2)\n\n\tfor i, l := range ls {\n\t\tassert.Equal(t, stLeases[i].IP, l.IP)\n\t\tassert.Equal(t, stLeases[i].HWAddr, l.HWAddr)\n\t\tassert.True(t, l.IsStatic)\n\t}\n}\n\nfunc TestV4Server_handle_optionsPriority(t *testing.T) {\n\tdefaultIP := netip.MustParseAddr(\"192.168.1.1\")\n\tknownIP := net.IP{1, 2, 3, 4}\n\n\t// prepareSrv creates a *v4Server and sets the opt6IPs in the initial\n\t// configuration of the server as the value for DHCP option 6.\n\tprepareSrv := func(t *testing.T, opt6IPs []net.IP) (s *v4Server) {\n\t\tt.Helper()\n\n\t\tconf := defaultV4ServerConf()\n\t\tif len(opt6IPs) > 0 {\n\t\t\tb := &strings.Builder{}\n\t\t\tstringutil.WriteToBuilder(b, \"6 ips \", opt6IPs[0].String())\n\t\t\tfor _, ip := range opt6IPs[1:] {\n\t\t\t\tstringutil.WriteToBuilder(b, \",\", ip.String())\n\t\t\t}\n\t\t\tconf.Options = []string{b.String()}\n\t\t} else {\n\t\t\tdefer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP.AsSlice())) }()\n\t\t}\n\n\t\tvar err error\n\t\ts, err = v4Create(conf)\n\t\trequire.NoError(t, err)\n\n\t\ts.conf.dnsIPAddrs = []netip.Addr{defaultIP}\n\n\t\treturn s\n\t}\n\n\t// checkResp creates a discovery message with DHCP option 6 requested amd\n\t// asserts the response to contain wantIPs in this option.\n\tcheckResp := func(t *testing.T, s *v4Server, wantIPs []net.IP) {\n\t\tt.Helper()\n\n\t\tmac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\t\treq, err := dhcpv4.NewDiscovery(mac, dhcpv4.WithRequestedOptions(\n\t\t\tdhcpv4.OptionDomainNameServer,\n\t\t))\n\t\trequire.NoError(t, err)\n\n\t\tvar resp *dhcpv4.DHCPv4\n\t\tresp, err = dhcpv4.NewReplyFromRequest(req)\n\t\trequire.NoError(t, err)\n\n\t\tres := s.handle(req, resp)\n\t\trequire.Equal(t, 1, res)\n\n\t\to := resp.GetOneOption(dhcpv4.OptionDomainNameServer)\n\t\trequire.NotEmpty(t, o)\n\n\t\twantData := []byte{}\n\t\tfor _, ip := range wantIPs {\n\t\t\twantData = append(wantData, ip...)\n\t\t}\n\t\tassert.Equal(t, o, wantData)\n\t}\n\n\tt.Run(\"default\", func(t *testing.T) {\n\t\ts := prepareSrv(t, nil)\n\n\t\tcheckResp(t, s, []net.IP{defaultIP.AsSlice()})\n\t})\n\n\tt.Run(\"explicitly_configured\", func(t *testing.T) {\n\t\ts := prepareSrv(t, []net.IP{knownIP, knownIP})\n\n\t\tcheckResp(t, s, []net.IP{knownIP, knownIP})\n\t})\n}\n\nfunc TestV4Server_updateOptions(t *testing.T) {\n\ttestIP := net.IP{1, 2, 3, 4}\n\n\tdontWant := func(c dhcpv4.OptionCode) (opt dhcpv4.Option) {\n\t\treturn dhcpv4.OptGeneric(c, nil)\n\t}\n\n\ttestCases := []struct {\n\t\tname     string\n\t\twantOpts dhcpv4.Options\n\t\treqMods  []dhcpv4.Modifier\n\t\tconfOpts []string\n\t}{{\n\t\tname: \"requested_default\",\n\t\twantOpts: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptBroadcastAddress(netutil.IPv4bcast()),\n\t\t),\n\t\treqMods: []dhcpv4.Modifier{\n\t\t\tdhcpv4.WithRequestedOptions(dhcpv4.OptionBroadcastAddress),\n\t\t},\n\t\tconfOpts: nil,\n\t}, {\n\t\tname: \"requested_non-default\",\n\t\twantOpts: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptBroadcastAddress(testIP),\n\t\t),\n\t\treqMods: []dhcpv4.Modifier{\n\t\t\tdhcpv4.WithRequestedOptions(dhcpv4.OptionBroadcastAddress),\n\t\t},\n\t\tconfOpts: []string{\n\t\t\tfmt.Sprintf(\"%d ip %s\", dhcpv4.OptionBroadcastAddress, testIP),\n\t\t},\n\t}, {\n\t\tname: \"non-requested_default\",\n\t\twantOpts: dhcpv4.OptionsFromList(\n\t\t\tdontWant(dhcpv4.OptionBroadcastAddress),\n\t\t),\n\t\treqMods:  nil,\n\t\tconfOpts: nil,\n\t}, {\n\t\tname: \"non-requested_non-default\",\n\t\twantOpts: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptBroadcastAddress(testIP),\n\t\t),\n\t\treqMods: nil,\n\t\tconfOpts: []string{\n\t\t\tfmt.Sprintf(\"%d ip %s\", dhcpv4.OptionBroadcastAddress, testIP),\n\t\t},\n\t}, {\n\t\tname: \"requested_deleted\",\n\t\twantOpts: dhcpv4.OptionsFromList(\n\t\t\tdontWant(dhcpv4.OptionBroadcastAddress),\n\t\t),\n\t\treqMods: []dhcpv4.Modifier{\n\t\t\tdhcpv4.WithRequestedOptions(dhcpv4.OptionBroadcastAddress),\n\t\t},\n\t\tconfOpts: []string{\n\t\t\tfmt.Sprintf(\"%d del\", dhcpv4.OptionBroadcastAddress),\n\t\t},\n\t}, {\n\t\tname: \"requested_non-default_deleted\",\n\t\twantOpts: dhcpv4.OptionsFromList(\n\t\t\tdontWant(dhcpv4.OptionBroadcastAddress),\n\t\t),\n\t\treqMods: []dhcpv4.Modifier{\n\t\t\tdhcpv4.WithRequestedOptions(dhcpv4.OptionBroadcastAddress),\n\t\t},\n\t\tconfOpts: []string{\n\t\t\tfmt.Sprintf(\"%d ip %s\", dhcpv4.OptionBroadcastAddress, testIP),\n\t\t\tfmt.Sprintf(\"%d del\", dhcpv4.OptionBroadcastAddress),\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\treq, err := dhcpv4.New(tc.reqMods...)\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := dhcpv4.NewReplyFromRequest(req)\n\t\trequire.NoError(t, err)\n\n\t\tconf := defaultV4ServerConf()\n\t\tconf.Options = tc.confOpts\n\n\t\ts, err := v4Create(conf)\n\t\trequire.NoError(t, err)\n\t\trequire.IsType(t, (*v4Server)(nil), s)\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts.updateOptions(req, resp)\n\n\t\t\tfor c, v := range tc.wantOpts {\n\t\t\t\tif v == nil {\n\t\t\t\t\tassert.NotContains(t, resp.Options, c)\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, v, resp.Options.Get(dhcpv4.GenericOptionCode(c)))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestV4StaticLease_Get(t *testing.T) {\n\tsIface := defaultSrv(t)\n\n\ts, ok := sIface.(*v4Server)\n\trequire.True(t, ok)\n\n\tdnsAddr := netip.MustParseAddr(\"192.168.10.1\")\n\ts.conf.dnsIPAddrs = []netip.Addr{dnsAddr}\n\ts.implicitOpts.Update(dhcpv4.OptDNS(dnsAddr.AsSlice()))\n\n\tl := &dhcpsvc.Lease{\n\t\tHostname: \"static-1.local\",\n\t\tHWAddr:   net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t\tIP:       netip.MustParseAddr(\"192.168.10.150\"),\n\t}\n\terr := s.AddStaticLease(l)\n\trequire.NoError(t, err)\n\n\tvar req, resp *dhcpv4.DHCPv4\n\tmac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\n\tt.Run(\"discover\", func(t *testing.T) {\n\t\treq, err = dhcpv4.NewDiscovery(mac, dhcpv4.WithRequestedOptions(\n\t\t\tdhcpv4.OptionDomainNameServer,\n\t\t))\n\t\trequire.NoError(t, err)\n\n\t\tresp, err = dhcpv4.NewReplyFromRequest(req)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 1, s.handle(req, resp))\n\t})\n\n\t// Don't continue if we got any errors in the previous subtest.\n\trequire.NoError(t, err)\n\n\tt.Run(\"offer\", func(t *testing.T) {\n\t\tassert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())\n\t\tassert.Equal(t, mac, resp.ClientHWAddr)\n\n\t\tip := net.IP(l.IP.AsSlice())\n\t\tassert.True(t, ip.Equal(resp.YourIPAddr))\n\n\t\tassert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))\n\t\tassert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))\n\n\t\tones, _ := resp.SubnetMask().Size()\n\t\tassert.Equal(t, s.conf.subnet.Bits(), ones)\n\t\tassert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())\n\t})\n\n\tt.Run(\"request\", func(t *testing.T) {\n\t\treq, err = dhcpv4.NewRequestFromOffer(resp)\n\t\trequire.NoError(t, err)\n\n\t\tresp, err = dhcpv4.NewReplyFromRequest(req)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 1, s.handle(req, resp))\n\t})\n\n\trequire.NoError(t, err)\n\n\tt.Run(\"ack\", func(t *testing.T) {\n\t\tassert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())\n\t\tassert.Equal(t, mac, resp.ClientHWAddr)\n\n\t\tip := net.IP(l.IP.AsSlice())\n\t\tassert.True(t, ip.Equal(resp.YourIPAddr))\n\n\t\tassert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))\n\t\tassert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))\n\n\t\tones, _ := resp.SubnetMask().Size()\n\t\tassert.Equal(t, s.conf.subnet.Bits(), ones)\n\t\tassert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())\n\t})\n\n\tdnsAddrs := resp.DNS()\n\trequire.Len(t, dnsAddrs, 1)\n\n\tassert.True(t, dnsAddrs[0].Equal(s.conf.GatewayIP.AsSlice()))\n\n\tt.Run(\"check_lease\", func(t *testing.T) {\n\t\tls := s.GetLeases(LeasesStatic)\n\t\trequire.Len(t, ls, 1)\n\n\t\tassert.Equal(t, l.IP, ls[0].IP)\n\t\tassert.Equal(t, mac, ls[0].HWAddr)\n\t})\n}\n\nfunc TestV4DynamicLease_Get(t *testing.T) {\n\tconf := defaultV4ServerConf()\n\tconf.Options = []string{\n\t\t\"81 hex 303132\",\n\t\t\"82 ip 1.2.3.4\",\n\t}\n\n\ts, err := v4Create(conf)\n\trequire.NoError(t, err)\n\n\ts.conf.dnsIPAddrs = []netip.Addr{netip.MustParseAddr(\"192.168.10.1\")}\n\ts.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs[0].AsSlice()))\n\n\tvar req, resp *dhcpv4.DHCPv4\n\tmac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\n\tt.Run(\"discover\", func(t *testing.T) {\n\t\treq, err = dhcpv4.NewDiscovery(mac, dhcpv4.WithRequestedOptions(\n\t\t\tdhcpv4.OptionFQDN,\n\t\t\tdhcpv4.OptionRelayAgentInformation,\n\t\t))\n\t\trequire.NoError(t, err)\n\n\t\tresp, err = dhcpv4.NewReplyFromRequest(req)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 1, s.handle(req, resp))\n\t})\n\n\t// Don't continue if we got any errors in the previous subtest.\n\trequire.NoError(t, err)\n\n\tt.Run(\"offer\", func(t *testing.T) {\n\t\tassert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())\n\t\tassert.Equal(t, mac, resp.ClientHWAddr)\n\n\t\tassert.True(t, resp.YourIPAddr.Equal(s.conf.RangeStart.AsSlice()))\n\t\tassert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))\n\n\t\trouter := resp.Router()\n\t\trequire.Len(t, router, 1)\n\n\t\tassert.True(t, router[0].Equal(s.conf.GatewayIP.AsSlice()))\n\n\t\tones, _ := resp.SubnetMask().Size()\n\t\tassert.Equal(t, s.conf.subnet.Bits(), ones)\n\t\tassert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())\n\t\tassert.Equal(t, []byte(\"012\"), resp.Options.Get(dhcpv4.OptionFQDN))\n\n\t\trai := resp.RelayAgentInfo()\n\t\trequire.NotNil(t, rai)\n\t\tassert.Equal(t, net.IP{1, 2, 3, 4}, net.IP(rai.ToBytes()))\n\t})\n\n\tt.Run(\"request\", func(t *testing.T) {\n\t\treq, err = dhcpv4.NewRequestFromOffer(resp)\n\t\trequire.NoError(t, err)\n\n\t\tresp, err = dhcpv4.NewReplyFromRequest(req)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, 1, s.handle(req, resp))\n\t})\n\n\trequire.NoError(t, err)\n\n\tt.Run(\"ack\", func(t *testing.T) {\n\t\tassert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())\n\t\tassert.Equal(t, mac, resp.ClientHWAddr)\n\t\tassert.True(t, resp.YourIPAddr.Equal(s.conf.RangeStart.AsSlice()))\n\n\t\trouter := resp.Router()\n\t\trequire.Len(t, router, 1)\n\n\t\tassert.True(t, router[0].Equal(s.conf.GatewayIP.AsSlice()))\n\n\t\tassert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))\n\n\t\tones, _ := resp.SubnetMask().Size()\n\t\tassert.Equal(t, s.conf.subnet.Bits(), ones)\n\t\tassert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())\n\t})\n\n\tdnsAddrs := resp.DNS()\n\trequire.Len(t, dnsAddrs, 1)\n\n\tassert.True(t, net.IP{192, 168, 10, 1}.Equal(dnsAddrs[0]))\n\n\t// check lease\n\tt.Run(\"check_lease\", func(t *testing.T) {\n\t\tls := s.GetLeases(LeasesDynamic)\n\t\trequire.Len(t, ls, 1)\n\n\t\tip := netip.MustParseAddr(\"192.168.10.100\")\n\t\tassert.Equal(t, ip, ls[0].IP)\n\t\tassert.Equal(t, mac, ls[0].HWAddr)\n\t})\n}\n\nfunc TestNormalizeHostname(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\thostname   string\n\t\twantErrMsg string\n\t\twant       string\n\t}{{\n\t\tname:       \"success\",\n\t\thostname:   \"example.com\",\n\t\twantErrMsg: \"\",\n\t\twant:       \"example.com\",\n\t}, {\n\t\tname:       \"success_empty\",\n\t\thostname:   \"\",\n\t\twantErrMsg: \"\",\n\t\twant:       \"\",\n\t}, {\n\t\tname:       \"success_spaces\",\n\t\thostname:   \"my device 01\",\n\t\twantErrMsg: \"\",\n\t\twant:       \"my-device-01\",\n\t}, {\n\t\tname:       \"success_underscores\",\n\t\thostname:   \"my_device_01\",\n\t\twantErrMsg: \"\",\n\t\twant:       \"my-device-01\",\n\t}, {\n\t\tname:       \"error_part\",\n\t\thostname:   \"device !!!\",\n\t\twantErrMsg: \"\",\n\t\twant:       \"device\",\n\t}, {\n\t\tname:       \"error_part_spaces\",\n\t\thostname:   \"device ! ! !\",\n\t\twantErrMsg: \"\",\n\t\twant:       \"device\",\n\t}, {\n\t\tname:       \"error\",\n\t\thostname:   \"!!!\",\n\t\twantErrMsg: `normalizing \"!!!\": no valid parts`,\n\t\twant:       \"\",\n\t}, {\n\t\tname:       \"error_spaces\",\n\t\thostname:   \"! ! !\",\n\t\twantErrMsg: `normalizing \"! ! !\": no valid parts`,\n\t\twant:       \"\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := normalizeHostname(tc.hostname)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\n// fakePacketConn is a mock implementation of net.PacketConn to simplify\n// testing.\ntype fakePacketConn struct {\n\t// writeTo is used to substitute net.PacketConn's WriteTo method.\n\twriteTo func(p []byte, addr net.Addr) (n int, err error)\n\t// net.PacketConn is embedded here simply to make *fakePacketConn a\n\t// net.PacketConn without actually implementing all methods.\n\tnet.PacketConn\n}\n\n// WriteTo implements net.PacketConn interface for *fakePacketConn.\nfunc (fc *fakePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {\n\treturn fc.writeTo(p, addr)\n}\n\nfunc TestV4Server_FindMACbyIP(t *testing.T) {\n\tconst (\n\t\tstaticName  = \"static-client\"\n\t\tanotherName = \"another-client\"\n\t)\n\n\tstaticIP := netip.MustParseAddr(\"192.168.10.10\")\n\tstaticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\n\tanotherIP := netip.MustParseAddr(\"192.168.100.100\")\n\tanotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}\n\n\ts := &v4Server{\n\t\tleases: []*dhcpsvc.Lease{{\n\t\t\tHostname: staticName,\n\t\t\tHWAddr:   staticMAC,\n\t\t\tIP:       staticIP,\n\t\t\tIsStatic: true,\n\t\t}, {\n\t\t\tExpiry:   time.Unix(10, 0),\n\t\t\tHostname: anotherName,\n\t\t\tHWAddr:   anotherMAC,\n\t\t\tIP:       anotherIP,\n\t\t}},\n\t}\n\ts.ipIndex = map[netip.Addr]*dhcpsvc.Lease{\n\t\tstaticIP:  s.leases[0],\n\t\tanotherIP: s.leases[1],\n\t}\n\ts.hostsIndex = map[string]*dhcpsvc.Lease{\n\t\tstaticName:  s.leases[0],\n\t\tanotherName: s.leases[1],\n\t}\n\n\ttestCases := []struct {\n\t\twant net.HardwareAddr\n\t\tip   netip.Addr\n\t\tname string\n\t}{{\n\t\tname: \"basic\",\n\t\tip:   staticIP,\n\t\twant: staticMAC,\n\t}, {\n\t\tname: \"not_found\",\n\t\tip:   netip.MustParseAddr(\"1.2.3.4\"),\n\t\twant: nil,\n\t}, {\n\t\tname: \"expired\",\n\t\tip:   anotherIP,\n\t\twant: nil,\n\t}, {\n\t\tname: \"v6\",\n\t\tip:   netip.MustParseAddr(\"ffff::1\"),\n\t\twant: nil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmac := s.FindMACbyIP(tc.ip)\n\n\t\t\trequire.Equal(t, tc.want, mac)\n\t\t})\n\t}\n}\n\nfunc TestV4Server_handleDecline(t *testing.T) {\n\tconst (\n\t\tdynamicName = \"dynamic-client\"\n\t\tanotherName = \"another-client\"\n\t)\n\n\tdynamicIP := netip.MustParseAddr(\"192.168.10.200\")\n\tdynamicMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\n\ts := defaultSrv(t)\n\n\ts4, ok := s.(*v4Server)\n\trequire.True(t, ok)\n\n\ts4.leases = []*dhcpsvc.Lease{{\n\t\tHostname: dynamicName,\n\t\tHWAddr:   dynamicMAC,\n\t\tIP:       dynamicIP,\n\t}}\n\n\treq, err := dhcpv4.New(\n\t\tdhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(net.IP(dynamicIP.AsSlice()))),\n\t)\n\trequire.NoError(t, err)\n\n\treq.ClientIPAddr = net.IP(dynamicIP.AsSlice())\n\treq.ClientHWAddr = dynamicMAC\n\n\tresp := &dhcpv4.DHCPv4{}\n\terr = s4.handleDecline(req, resp)\n\trequire.NoError(t, err)\n\n\twantResp := &dhcpv4.DHCPv4{\n\t\tYourIPAddr: net.IP(s4.conf.RangeStart.AsSlice()),\n\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeAck),\n\t\t),\n\t}\n\n\trequire.Equal(t, wantResp, resp)\n}\n\nfunc TestV4Server_handleRelease(t *testing.T) {\n\tconst (\n\t\tdynamicName = \"dynamic-client\"\n\t\tanotherName = \"another-client\"\n\t)\n\n\tdynamicIP := netip.MustParseAddr(\"192.168.10.200\")\n\tdynamicMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\n\ts := defaultSrv(t)\n\n\ts4, ok := s.(*v4Server)\n\trequire.True(t, ok)\n\n\ts4.leases = []*dhcpsvc.Lease{{\n\t\tHostname: dynamicName,\n\t\tHWAddr:   dynamicMAC,\n\t\tIP:       dynamicIP,\n\t}}\n\n\treq, err := dhcpv4.New(\n\t\tdhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(net.IP(dynamicIP.AsSlice()))),\n\t)\n\trequire.NoError(t, err)\n\n\treq.ClientIPAddr = net.IP(dynamicIP.AsSlice())\n\treq.ClientHWAddr = dynamicMAC\n\n\tresp := &dhcpv4.DHCPv4{}\n\terr = s4.handleRelease(req, resp)\n\trequire.NoError(t, err)\n\n\twantResp := &dhcpv4.DHCPv4{\n\t\tOptions: dhcpv4.OptionsFromList(\n\t\t\tdhcpv4.OptMessageType(dhcpv4.MessageTypeAck),\n\t\t),\n\t}\n\n\trequire.Equal(t, wantResp, resp)\n}\n"
  },
  {
    "path": "internal/dhcpd/v6_unix.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/insomniacslk/dhcp/dhcpv6\"\n\t\"github.com/insomniacslk/dhcp/dhcpv6/server6\"\n\t\"github.com/insomniacslk/dhcp/iana\"\n)\n\nconst valueIAID = \"ADGH\" // value for IANA.ID\n\n// v6Server is a DHCPv6 server.\n//\n// TODO(a.garipov): Think about unifying this and v4Server.\ntype v6Server struct {\n\tra   raCtx\n\tconf V6ServerConf\n\tsid  dhcpv6.DUID\n\tsrv  *server6.Server\n\n\tleases     []*dhcpsvc.Lease\n\tleasesLock sync.Mutex\n\tipAddrs    [256]byte\n}\n\n// WriteDiskConfig4 - write configuration\nfunc (s *v6Server) WriteDiskConfig4(c *V4ServerConf) {\n}\n\n// WriteDiskConfig6 - write configuration\nfunc (s *v6Server) WriteDiskConfig6(c *V6ServerConf) {\n\t*c = s.conf\n}\n\n// Return TRUE if IP address is within range [start..0xff]\nfunc ip6InRange(start, ip net.IP) bool {\n\tif len(start) != 16 {\n\t\treturn false\n\t}\n\t//lint:ignore SA1021 TODO(e.burkov): Ignore this for now, think about\n\t// using masks.\n\tif !bytes.Equal(start[:15], ip[:15]) {\n\t\treturn false\n\t}\n\treturn start[15] <= ip[15]\n}\n\n// HostByIP implements the [Interface] interface for *v6Server.\nfunc (s *v6Server) HostByIP(ip netip.Addr) (host string) {\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tfor _, l := range s.leases {\n\t\tif l.IP == ip {\n\t\t\treturn l.Hostname\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// IPByHost implements the [Interface] interface for *v6Server.\nfunc (s *v6Server) IPByHost(host string) (ip netip.Addr) {\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tfor _, l := range s.leases {\n\t\tif l.Hostname == host {\n\t\t\treturn l.IP\n\t\t}\n\t}\n\n\treturn netip.Addr{}\n}\n\n// ResetLeases resets leases.\nfunc (s *v6Server) ResetLeases(leases []*dhcpsvc.Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv6: %w\") }()\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\ts.leases = nil\n\tfor _, l := range leases {\n\t\tip := net.IP(l.IP.AsSlice())\n\t\tif !l.IsStatic && !ip6InRange(s.conf.ipStart, ip) {\n\n\t\t\tlog.Debug(\"dhcpv6: skipping a lease with IP %v: not within current IP range\", l.IP)\n\n\t\t\tcontinue\n\t\t}\n\n\t\ts.addLease(l)\n\t}\n\n\treturn nil\n}\n\n// GetLeases returns the list of current DHCP leases.  It is safe for concurrent\n// use.\nfunc (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease) {\n\t// The function shouldn't return nil value because zero-length slice\n\t// behaves differently in cases like marshalling.  Our front-end also\n\t// requires non-nil value in the response.\n\tleases = []*dhcpsvc.Lease{}\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tfor _, l := range s.leases {\n\t\tif l.IsStatic {\n\t\t\tif (flags & LeasesStatic) != 0 {\n\t\t\t\tleases = append(leases, l.Clone())\n\t\t\t}\n\t\t} else {\n\t\t\tif (flags & LeasesDynamic) != 0 {\n\t\t\t\tleases = append(leases, l.Clone())\n\t\t\t}\n\t\t}\n\t}\n\n\treturn leases\n}\n\n// getLeasesRef returns the actual leases slice.  For internal use only.\nfunc (s *v6Server) getLeasesRef() []*dhcpsvc.Lease {\n\treturn s.leases\n}\n\n// FindMACbyIP implements the [Interface] for *v6Server.\nfunc (s *v6Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {\n\tnow := time.Now()\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tif !ip.Is6() {\n\t\treturn nil\n\t}\n\n\tfor _, l := range s.leases {\n\t\tif l.IP == ip {\n\t\t\tif l.IsStatic || l.Expiry.After(now) {\n\t\t\t\treturn l.HWAddr\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Remove (swap) lease by index\nfunc (s *v6Server) leaseRemoveSwapByIndex(i int) {\n\tleaseIP := s.leases[i].IP.As16()\n\ts.ipAddrs[leaseIP[15]] = 0\n\tlog.Debug(\"dhcpv6: removed lease %s\", s.leases[i].HWAddr)\n\n\tn := len(s.leases)\n\tif i != n-1 {\n\t\ts.leases[i] = s.leases[n-1] // swap with the last element\n\t}\n\ts.leases = s.leases[:n-1]\n}\n\n// Remove a dynamic lease with the same properties\n// Return error if a static lease is found\nfunc (s *v6Server) rmDynamicLease(lease *dhcpsvc.Lease) (err error) {\n\tfor i := 0; i < len(s.leases); i++ {\n\t\tl := s.leases[i]\n\n\t\tif bytes.Equal(l.HWAddr, lease.HWAddr) {\n\t\t\tif l.IsStatic {\n\t\t\t\treturn fmt.Errorf(\"static lease already exists\")\n\t\t\t}\n\n\t\t\ts.leaseRemoveSwapByIndex(i)\n\t\t\tif i == len(s.leases) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tl = s.leases[i]\n\t\t}\n\n\t\tif l.IP == lease.IP {\n\t\t\tif l.IsStatic {\n\t\t\t\treturn fmt.Errorf(\"static lease already exists\")\n\t\t\t}\n\n\t\t\ts.leaseRemoveSwapByIndex(i)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// AddStaticLease adds a static lease.  It is safe for concurrent use.\nfunc (s *v6Server) AddStaticLease(l *dhcpsvc.Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv6: %w\") }()\n\n\tif !l.IP.Is6() {\n\t\treturn fmt.Errorf(\"invalid IP\")\n\t}\n\n\terr = netutil.ValidateMAC(l.HWAddr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating lease: %w\", err)\n\t}\n\n\tl.IsStatic = true\n\n\ts.leasesLock.Lock()\n\terr = s.rmDynamicLease(l)\n\tif err != nil {\n\t\ts.leasesLock.Unlock()\n\n\t\treturn err\n\t}\n\n\ts.addLease(l)\n\ts.conf.notify(LeaseChangedDBStore)\n\ts.leasesLock.Unlock()\n\n\ts.conf.notify(LeaseChangedAddedStatic)\n\n\treturn nil\n}\n\n// UpdateStaticLease updates IP, hostname of the static lease.\nfunc (s *v6Server) UpdateStaticLease(l *dhcpsvc.Lease) (err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = errors.Annotate(err, \"dhcpv6: updating static lease: %w\")\n\n\t\t\treturn\n\t\t}\n\n\t\ts.conf.notify(LeaseChangedDBStore)\n\t\ts.conf.notify(LeaseChangedRemovedStatic)\n\t}()\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tfound := s.findLease(l.HWAddr)\n\tif found == nil {\n\t\treturn fmt.Errorf(\"can't find lease %s\", l.HWAddr)\n\t}\n\n\terr = s.rmLease(found)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"removing previous lease for %s (%s): %w\", l.IP, l.HWAddr, err)\n\t}\n\n\ts.addLease(l)\n\n\treturn nil\n}\n\n// RemoveStaticLease removes a static lease.  It is safe for concurrent use.\nfunc (s *v6Server) RemoveStaticLease(l *dhcpsvc.Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv6: %w\") }()\n\n\tif !l.IP.Is6() {\n\t\treturn fmt.Errorf(\"invalid IP\")\n\t}\n\n\terr = netutil.ValidateMAC(l.HWAddr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating lease: %w\", err)\n\t}\n\n\ts.leasesLock.Lock()\n\terr = s.rmLease(l)\n\tif err != nil {\n\t\ts.leasesLock.Unlock()\n\t\treturn err\n\t}\n\ts.conf.notify(LeaseChangedDBStore)\n\ts.leasesLock.Unlock()\n\ts.conf.notify(LeaseChangedRemovedStatic)\n\treturn nil\n}\n\n// Add a lease\nfunc (s *v6Server) addLease(l *dhcpsvc.Lease) {\n\ts.leases = append(s.leases, l)\n\tip := l.IP.As16()\n\ts.ipAddrs[ip[15]] = 1\n\tlog.Debug(\"dhcpv6: added lease %s <-> %s\", l.IP, l.HWAddr)\n}\n\n// Remove a lease with the same properties\nfunc (s *v6Server) rmLease(lease *dhcpsvc.Lease) (err error) {\n\tfor i, l := range s.leases {\n\t\tif l.IP == lease.IP {\n\t\t\tif !bytes.Equal(l.HWAddr, lease.HWAddr) ||\n\t\t\t\tl.Hostname != lease.Hostname {\n\t\t\t\treturn fmt.Errorf(\"lease not found\")\n\t\t\t}\n\n\t\t\ts.leaseRemoveSwapByIndex(i)\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"lease not found\")\n}\n\n// Find lease by MAC.\nfunc (s *v6Server) findLease(mac net.HardwareAddr) (lease *dhcpsvc.Lease) {\n\tfor i := range s.leases {\n\t\tif bytes.Equal(mac, s.leases[i].HWAddr) {\n\t\t\treturn s.leases[i]\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Find an expired lease and return its index or -1\nfunc (s *v6Server) findExpiredLease() int {\n\tnow := time.Now().Unix()\n\tfor i, lease := range s.leases {\n\t\tif !lease.IsStatic && lease.Expiry.Unix() <= now {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\n// Get next free IP\nfunc (s *v6Server) findFreeIP() net.IP {\n\tfor i := s.conf.ipStart[15]; ; i++ {\n\t\tif s.ipAddrs[i] == 0 {\n\t\t\tip := make([]byte, 16)\n\t\t\tcopy(ip, s.conf.ipStart)\n\t\t\tip[15] = i\n\t\t\treturn ip\n\t\t}\n\t\tif i == 0xff {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// Reserve lease for MAC\nfunc (s *v6Server) reserveLease(mac net.HardwareAddr) *dhcpsvc.Lease {\n\tl := dhcpsvc.Lease{\n\t\tHWAddr: make([]byte, len(mac)),\n\t}\n\n\tcopy(l.HWAddr, mac)\n\n\ts.leasesLock.Lock()\n\tdefer s.leasesLock.Unlock()\n\n\tip := s.findFreeIP()\n\tif ip == nil {\n\t\ti := s.findExpiredLease()\n\t\tif i < 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\tcopy(s.leases[i].HWAddr, mac)\n\n\t\treturn s.leases[i]\n\t}\n\n\tnetIP, ok := netip.AddrFromSlice(ip)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tl.IP = netIP\n\n\ts.addLease(&l)\n\n\treturn &l\n}\n\nfunc (s *v6Server) commitDynamicLease(l *dhcpsvc.Lease) {\n\tl.Expiry = time.Now().Add(s.conf.leaseTime)\n\n\ts.leasesLock.Lock()\n\ts.conf.notify(LeaseChangedDBStore)\n\ts.leasesLock.Unlock()\n\ts.conf.notify(LeaseChangedAdded)\n}\n\n// Check Client ID\nfunc (s *v6Server) checkCID(msg *dhcpv6.Message) error {\n\tif msg.Options.ClientID() == nil {\n\t\treturn fmt.Errorf(\"dhcpv6: no ClientID option in request\")\n\t}\n\n\treturn nil\n}\n\n// Check ServerID policy\nfunc (s *v6Server) checkSID(msg *dhcpv6.Message) error {\n\tsid := msg.Options.ServerID()\n\n\tswitch msg.Type() {\n\tcase dhcpv6.MessageTypeSolicit,\n\t\tdhcpv6.MessageTypeConfirm,\n\t\tdhcpv6.MessageTypeRebind:\n\n\t\tif sid != nil {\n\t\t\treturn fmt.Errorf(\"dhcpv6: drop packet: ServerID option in message %s\", msg.Type().String())\n\t\t}\n\tcase dhcpv6.MessageTypeRequest,\n\t\tdhcpv6.MessageTypeRenew,\n\t\tdhcpv6.MessageTypeRelease,\n\t\tdhcpv6.MessageTypeDecline:\n\t\tif sid == nil {\n\t\t\treturn fmt.Errorf(\"dhcpv6: drop packet: no ServerID option in message %s\", msg.Type().String())\n\t\t}\n\n\t\tif !sid.Equal(s.sid) {\n\t\t\treturn fmt.Errorf(\"dhcpv6: drop packet: mismatched ServerID option in message %s: %s\",\n\t\t\t\tmsg.Type().String(), sid.String())\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// . IAAddress must be equal to the lease's IP\nfunc (s *v6Server) checkIA(msg *dhcpv6.Message, lease *dhcpsvc.Lease) error {\n\tswitch msg.Type() {\n\tcase dhcpv6.MessageTypeRequest,\n\t\tdhcpv6.MessageTypeConfirm,\n\t\tdhcpv6.MessageTypeRenew,\n\t\tdhcpv6.MessageTypeRebind:\n\n\t\toia := msg.Options.OneIANA()\n\t\tif oia == nil {\n\t\t\treturn fmt.Errorf(\"no IANA option in %s\", msg.Type().String())\n\t\t}\n\n\t\toiaAddr := oia.Options.OneAddress()\n\t\tif oiaAddr == nil {\n\t\t\treturn fmt.Errorf(\"no IANA.Addr option in %s\", msg.Type().String())\n\t\t}\n\n\t\tleaseIP := net.IP(lease.IP.AsSlice())\n\t\tif !oiaAddr.IPv6Addr.Equal(leaseIP) {\n\t\t\treturn fmt.Errorf(\"invalid IANA.Addr option in %s\", msg.Type().String())\n\t\t}\n\t}\n\treturn nil\n}\n\n// Store lease in DB (if necessary) and return lease life time\nfunc (s *v6Server) commitLease(msg *dhcpv6.Message, lease *dhcpsvc.Lease) time.Duration {\n\tlifetime := s.conf.leaseTime\n\n\tswitch msg.Type() {\n\tcase dhcpv6.MessageTypeSolicit:\n\t\t//\n\n\tcase dhcpv6.MessageTypeConfirm:\n\t\tlifetime = time.Until(lease.Expiry)\n\n\tcase dhcpv6.MessageTypeRequest,\n\t\tdhcpv6.MessageTypeRenew,\n\t\tdhcpv6.MessageTypeRebind:\n\n\t\tif !lease.IsStatic {\n\t\t\ts.commitDynamicLease(lease)\n\t\t}\n\t}\n\treturn lifetime\n}\n\n// Find a lease associated with MAC and prepare response\nfunc (s *v6Server) process(msg *dhcpv6.Message, req, resp dhcpv6.DHCPv6) bool {\n\tswitch msg.Type() {\n\tcase dhcpv6.MessageTypeSolicit,\n\t\tdhcpv6.MessageTypeRequest,\n\t\tdhcpv6.MessageTypeConfirm,\n\t\tdhcpv6.MessageTypeRenew,\n\t\tdhcpv6.MessageTypeRebind:\n\t\t// continue\n\n\tdefault:\n\t\treturn false\n\t}\n\n\tmac, err := dhcpv6.ExtractMAC(req)\n\tif err != nil {\n\t\tlog.Debug(\"dhcpv6: dhcpv6.ExtractMAC: %s\", err)\n\n\t\treturn false\n\t}\n\n\tvar lease *dhcpsvc.Lease\n\tfunc() {\n\t\ts.leasesLock.Lock()\n\t\tdefer s.leasesLock.Unlock()\n\n\t\tlease = s.findLease(mac)\n\t}()\n\n\tif lease == nil {\n\t\tlog.Debug(\"dhcpv6: no lease for: %s\", mac)\n\n\t\tswitch msg.Type() {\n\n\t\tcase dhcpv6.MessageTypeSolicit:\n\t\t\tlease = s.reserveLease(mac)\n\t\t\tif lease == nil {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\terr = s.checkIA(msg, lease)\n\tif err != nil {\n\t\tlog.Debug(\"dhcpv6: %s\", err)\n\n\t\treturn false\n\t}\n\n\tlifetime := s.commitLease(msg, lease)\n\n\toia := &dhcpv6.OptIANA{\n\t\tT1: lifetime / 2,\n\t\tT2: time.Duration(float32(lifetime) / 1.5),\n\t}\n\troia := msg.Options.OneIANA()\n\tif roia != nil {\n\t\tcopy(oia.IaId[:], roia.IaId[:])\n\t} else {\n\t\tcopy(oia.IaId[:], []byte(valueIAID))\n\t}\n\toiaAddr := &dhcpv6.OptIAAddress{\n\t\tIPv6Addr:          net.IP(lease.IP.AsSlice()),\n\t\tPreferredLifetime: lifetime,\n\t\tValidLifetime:     lifetime,\n\t}\n\toia.Options = dhcpv6.IdentityOptions{\n\t\tOptions: []dhcpv6.Option{oiaAddr},\n\t}\n\tresp.AddOption(oia)\n\n\tif msg.IsOptionRequested(dhcpv6.OptionDNSRecursiveNameServer) {\n\t\tresp.UpdateOption(dhcpv6.OptDNS(s.conf.dnsIPAddrs...))\n\t}\n\n\tfqdn := msg.GetOneOption(dhcpv6.OptionFQDN)\n\tif fqdn != nil {\n\t\tresp.AddOption(fqdn)\n\t}\n\n\tresp.AddOption(&dhcpv6.OptStatusCode{\n\t\tStatusCode:    iana.StatusSuccess,\n\t\tStatusMessage: \"success\",\n\t})\n\treturn true\n}\n\n// 1.\n// fe80::* (client) --(Solicit + ClientID+IANA())-> ff02::1:2\n// server -(Advertise + ClientID+ServerID+IANA(IAAddress)> fe80::*\n// fe80::* --(Request + ClientID+ServerID+IANA(IAAddress))-> ff02::1:2\n// server -(Reply + ClientID+ServerID+IANA(IAAddress)+DNS)> fe80::*\n//\n// 2.\n// fe80::* --(Confirm|Renew|Rebind + ClientID+IANA(IAAddress))-> ff02::1:2\n// server -(Reply + ClientID+ServerID+IANA(IAAddress)+DNS)> fe80::*\n//\n// 3.\n// fe80::* --(Release + ClientID+ServerID+IANA(IAAddress))-> ff02::1:2\nfunc (s *v6Server) packetHandler(conn net.PacketConn, peer net.Addr, req dhcpv6.DHCPv6) {\n\tmsg, err := req.GetInnerMessage()\n\tif err != nil {\n\t\tlog.Error(\"dhcpv6: %s\", err)\n\n\t\treturn\n\t}\n\n\tlog.Debug(\"dhcpv6: received: %s\", req.Summary())\n\n\terr = s.checkCID(msg)\n\tif err != nil {\n\t\tlog.Debug(\"%s\", err)\n\t\treturn\n\t}\n\n\terr = s.checkSID(msg)\n\tif err != nil {\n\t\tlog.Debug(\"%s\", err)\n\t\treturn\n\t}\n\n\tvar resp dhcpv6.DHCPv6\n\n\tswitch msg.Type() {\n\tcase dhcpv6.MessageTypeSolicit:\n\t\tif msg.GetOneOption(dhcpv6.OptionRapidCommit) == nil {\n\t\t\tresp, err = dhcpv6.NewAdvertiseFromSolicit(msg)\n\n\t\t\tbreak\n\t\t}\n\n\t\tresp, err = dhcpv6.NewReplyFromMessage(msg)\n\tcase dhcpv6.MessageTypeRequest,\n\t\tdhcpv6.MessageTypeConfirm,\n\t\tdhcpv6.MessageTypeRenew,\n\t\tdhcpv6.MessageTypeRebind,\n\t\tdhcpv6.MessageTypeRelease,\n\t\tdhcpv6.MessageTypeInformationRequest:\n\t\tresp, err = dhcpv6.NewReplyFromMessage(msg)\n\tdefault:\n\t\tlog.Error(\"dhcpv6: message type %d not supported\", msg.Type())\n\n\t\treturn\n\t}\n\tif err != nil {\n\t\tlog.Error(\"dhcpv6: %s\", err)\n\n\t\treturn\n\t}\n\n\tresp.AddOption(dhcpv6.OptServerID(s.sid))\n\n\t_ = s.process(msg, req, resp)\n\n\tlog.Debug(\"dhcpv6: sending: %s\", resp.Summary())\n\n\t_, err = conn.WriteTo(resp.ToBytes(), peer)\n\tif err != nil {\n\t\tlog.Error(\"dhcpv6: conn.Write to %s failed: %s\", peer, err)\n\n\t\treturn\n\t}\n}\n\n// configureDNSIPAddrs updates v6Server configuration with the slice of DNS IP\n// addresses of provided interface iface.  Initializes RA module.\nfunc (s *v6Server) configureDNSIPAddrs(\n\tctx context.Context,\n\tiface *net.Interface,\n) (ok bool, err error) {\n\tdnsIPAddrs, err := aghnet.IfaceDNSIPAddrs(\n\t\tctx,\n\t\ts.conf.Logger,\n\t\tiface,\n\t\taghnet.IPVersion6,\n\t\tdefaultMaxAttempts,\n\t\tdefaultBackoff,\n\t)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"interface %s: %w\", iface.Name, err)\n\t}\n\n\tif len(dnsIPAddrs) == 0 {\n\t\treturn false, nil\n\t}\n\n\ts.conf.dnsIPAddrs = dnsIPAddrs\n\n\treturn true, s.initRA(iface)\n}\n\n// initRA initializes RA module.\nfunc (s *v6Server) initRA(iface *net.Interface) (err error) {\n\t// Choose the source IP address - should be link-local-unicast.\n\ts.ra.ipAddr = s.conf.dnsIPAddrs[0]\n\tfor _, ip := range s.conf.dnsIPAddrs {\n\t\tif ip.IsLinkLocalUnicast() {\n\t\t\ts.ra.ipAddr = ip\n\t\t\tbreak\n\t\t}\n\t}\n\n\ts.ra.raAllowSLAAC = s.conf.RAAllowSLAAC\n\ts.ra.raSLAACOnly = s.conf.RASLAACOnly\n\ts.ra.dnsIPAddr = s.ra.ipAddr\n\ts.ra.prefixIPAddr = s.conf.ipStart\n\ts.ra.ifaceName = s.conf.InterfaceName\n\ts.ra.iface = iface\n\ts.ra.packetSendPeriod = 1 * time.Second\n\n\treturn s.ra.Init()\n}\n\n// Start starts the IPv6 DHCP server.\nfunc (s *v6Server) Start(ctx context.Context) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"dhcpv6: %w\") }()\n\n\tif !s.conf.Enabled {\n\t\treturn nil\n\t}\n\n\tifaceName := s.conf.InterfaceName\n\tiface, err := net.InterfaceByName(ifaceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"finding interface %s by name: %w\", ifaceName, err)\n\t}\n\n\tlog.Debug(\"dhcpv6: starting...\")\n\n\tok, err := s.configureDNSIPAddrs(ctx, iface)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif !ok {\n\t\t// No available IP addresses which may appear later.\n\t\treturn nil\n\t}\n\n\t// Don't initialize DHCPv6 server if we must force the clients to use SLAAC.\n\tif s.conf.RASLAACOnly {\n\t\tlog.Debug(\"not starting dhcpv6 server due to ra_slaac_only=true\")\n\n\t\treturn nil\n\t}\n\n\terr = netutil.ValidateMAC(iface.HardwareAddr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating interface %s: %w\", iface.Name, err)\n\t}\n\n\ts.sid = &dhcpv6.DUIDLLT{\n\t\tHWType:        iana.HWTypeEthernet,\n\t\tLinkLayerAddr: iface.HardwareAddr,\n\t\tTime:          dhcpv6.GetTime(),\n\t}\n\n\ts.srv, err = server6.NewServer(iface.Name, nil, s.packetHandler, server6.WithDebugLogger())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Debug(\"dhcpv6: listening...\")\n\n\tgo func() {\n\t\tif sErr := s.srv.Serve(); errors.Is(sErr, net.ErrClosed) {\n\t\t\tlog.Info(\"dhcpv6: server is closed\")\n\t\t} else if sErr != nil {\n\t\t\tlog.Error(\"dhcpv6: srv.Serve: %s\", sErr)\n\t\t}\n\t}()\n\n\treturn nil\n}\n\n// Stop - stop server\nfunc (s *v6Server) Stop() (err error) {\n\terr = s.ra.Close()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"closing ra ctx: %w\", err)\n\t}\n\n\t// DHCPv6 server may not be initialized if ra_slaac_only=true\n\tif s.srv == nil {\n\t\treturn nil\n\t}\n\n\tlog.Debug(\"dhcpv6: stopping\")\n\terr = s.srv.Close()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"closing dhcpv6 srv: %w\", err)\n\t}\n\n\t// now server.Serve() will return\n\ts.srv = nil\n\n\treturn nil\n}\n\n// Create DHCPv6 server\nfunc v6Create(conf V6ServerConf) (DHCPServer, error) {\n\ts := &v6Server{}\n\ts.conf = conf\n\n\tif !conf.Enabled {\n\t\treturn s, nil\n\t}\n\n\ts.conf.ipStart = conf.RangeStart\n\tif s.conf.ipStart == nil || s.conf.ipStart.To16() == nil {\n\t\treturn s, fmt.Errorf(\"dhcpv6: invalid range-start IP: %s\", conf.RangeStart)\n\t}\n\n\tif conf.LeaseDuration == 0 {\n\t\ts.conf.leaseTime = timeutil.Day\n\t\ts.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())\n\t} else {\n\t\ts.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)\n\t}\n\n\treturn s, nil\n}\n"
  },
  {
    "path": "internal/dhcpd/v6_unix_internal_test.go",
    "content": "//go:build darwin || freebsd || linux || openbsd\n\npackage dhcpd\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/insomniacslk/dhcp/dhcpv6\"\n\t\"github.com/insomniacslk/dhcp/iana\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc notify6(flags uint32) {\n}\n\nfunc TestV6_AddRemove_static(t *testing.T) {\n\ts, err := v6Create(V6ServerConf{\n\t\tEnabled:    true,\n\t\tRangeStart: net.ParseIP(\"2001::1\"),\n\t\tnotify:     notify6,\n\t})\n\trequire.NoError(t, err)\n\n\trequire.Empty(t, s.GetLeases(LeasesStatic))\n\n\t// Add static lease.\n\tl := &dhcpsvc.Lease{\n\t\tIP:     netip.MustParseAddr(\"2001::1\"),\n\t\tHWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t}\n\terr = s.AddStaticLease(l)\n\trequire.NoError(t, err)\n\n\t// Try to add the same static lease.\n\terr = s.AddStaticLease(l)\n\trequire.Error(t, err)\n\n\tls := s.GetLeases(LeasesStatic)\n\trequire.Len(t, ls, 1)\n\n\tassert.Equal(t, l.IP, ls[0].IP)\n\tassert.Equal(t, l.HWAddr, ls[0].HWAddr)\n\tassert.True(t, ls[0].IsStatic)\n\n\t// Try to remove non-existent static lease.\n\terr = s.RemoveStaticLease(&dhcpsvc.Lease{\n\t\tIP:     netip.MustParseAddr(\"2001::2\"),\n\t\tHWAddr: l.HWAddr,\n\t})\n\trequire.Error(t, err)\n\n\t// Remove static lease.\n\terr = s.RemoveStaticLease(l)\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, s.GetLeases(LeasesStatic))\n}\n\nfunc TestV6_AddReplace(t *testing.T) {\n\tsIface, err := v6Create(V6ServerConf{\n\t\tEnabled:    true,\n\t\tRangeStart: net.ParseIP(\"2001::1\"),\n\t\tnotify:     notify6,\n\t})\n\trequire.NoError(t, err)\n\n\ts, ok := sIface.(*v6Server)\n\trequire.True(t, ok)\n\n\t// Add dynamic leases.\n\tdynLeases := []*dhcpsvc.Lease{{\n\t\tIP:     netip.MustParseAddr(\"2001::1\"),\n\t\tHWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t}, {\n\t\tIP:     netip.MustParseAddr(\"2001::2\"),\n\t\tHWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t}}\n\n\tfor _, l := range dynLeases {\n\t\ts.addLease(l)\n\t}\n\n\tstLeases := []*dhcpsvc.Lease{{\n\t\tIP:     netip.MustParseAddr(\"2001::1\"),\n\t\tHWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t}, {\n\t\tIP:     netip.MustParseAddr(\"2001::3\"),\n\t\tHWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t}}\n\n\tfor _, l := range stLeases {\n\t\terr = s.AddStaticLease(l)\n\t\trequire.NoError(t, err)\n\t}\n\n\tls := s.GetLeases(LeasesStatic)\n\trequire.Len(t, ls, 2)\n\n\tfor i, l := range ls {\n\t\tassert.Equal(t, stLeases[i].IP, l.IP)\n\t\tassert.Equal(t, stLeases[i].HWAddr, l.HWAddr)\n\t\tassert.True(t, l.IsStatic)\n\t}\n}\n\nfunc TestV6GetLease(t *testing.T) {\n\tvar err error\n\tsIface, err := v6Create(V6ServerConf{\n\t\tEnabled:    true,\n\t\tRangeStart: net.ParseIP(\"2001::1\"),\n\t\tnotify:     notify6,\n\t})\n\trequire.NoError(t, err)\n\ts, ok := sIface.(*v6Server)\n\n\trequire.True(t, ok)\n\n\tdnsAddr := net.ParseIP(\"2000::1\")\n\ts.conf.dnsIPAddrs = []net.IP{dnsAddr}\n\ts.sid = &dhcpv6.DUIDLL{\n\t\tHWType:        iana.HWTypeEthernet,\n\t\tLinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t}\n\n\tl := &dhcpsvc.Lease{\n\t\tIP:     netip.MustParseAddr(\"2001::1\"),\n\t\tHWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t}\n\terr = s.AddStaticLease(l)\n\trequire.NoError(t, err)\n\n\tvar req, resp, msg *dhcpv6.Message\n\tmac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\tt.Run(\"solicit\", func(t *testing.T) {\n\t\treq, err = dhcpv6.NewSolicit(mac)\n\t\trequire.NoError(t, err)\n\n\t\tmsg, err = req.GetInnerMessage()\n\t\trequire.NoError(t, err)\n\n\t\tresp, err = dhcpv6.NewAdvertiseFromSolicit(msg)\n\t\trequire.NoError(t, err)\n\n\t\tassert.True(t, s.process(msg, req, resp))\n\t})\n\trequire.NoError(t, err)\n\n\tresp.AddOption(dhcpv6.OptServerID(s.sid))\n\n\tvar oia *dhcpv6.OptIANA\n\tvar oiaAddr *dhcpv6.OptIAAddress\n\tt.Run(\"advertise\", func(t *testing.T) {\n\t\trequire.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())\n\n\t\toia = resp.Options.OneIANA()\n\t\toiaAddr = oia.Options.OneAddress()\n\n\t\tip := net.IP(l.IP.AsSlice())\n\t\tassert.Equal(t, ip, oiaAddr.IPv6Addr)\n\t\tassert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())\n\t})\n\n\tt.Run(\"request\", func(t *testing.T) {\n\t\treq, err = dhcpv6.NewRequestFromAdvertise(resp)\n\t\trequire.NoError(t, err)\n\n\t\tmsg, err = req.GetInnerMessage()\n\t\trequire.NoError(t, err)\n\n\t\tresp, err = dhcpv6.NewReplyFromMessage(msg)\n\t\trequire.NoError(t, err)\n\n\t\tassert.True(t, s.process(msg, req, resp))\n\t})\n\trequire.NoError(t, err)\n\n\tt.Run(\"reply\", func(t *testing.T) {\n\t\trequire.Equal(t, dhcpv6.MessageTypeReply, resp.Type())\n\n\t\toia = resp.Options.OneIANA()\n\t\toiaAddr = oia.Options.OneAddress()\n\n\t\tip := net.IP(l.IP.AsSlice())\n\t\tassert.Equal(t, ip, oiaAddr.IPv6Addr)\n\t\tassert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())\n\t})\n\n\tdnsAddrs := resp.Options.DNS()\n\trequire.Len(t, dnsAddrs, 1)\n\tassert.Equal(t, dnsAddr, dnsAddrs[0])\n\n\tt.Run(\"lease\", func(t *testing.T) {\n\t\tls := s.GetLeases(LeasesStatic)\n\t\trequire.Len(t, ls, 1)\n\n\t\tassert.Equal(t, l.IP, ls[0].IP)\n\t\tassert.Equal(t, l.HWAddr, ls[0].HWAddr)\n\t})\n}\n\nfunc TestV6GetDynamicLease(t *testing.T) {\n\tsIface, err := v6Create(V6ServerConf{\n\t\tEnabled:    true,\n\t\tRangeStart: net.ParseIP(\"2001::2\"),\n\t\tnotify:     notify6,\n\t})\n\trequire.NoError(t, err)\n\n\ts, ok := sIface.(*v6Server)\n\trequire.True(t, ok)\n\n\tdnsAddr := net.ParseIP(\"2000::1\")\n\ts.conf.dnsIPAddrs = []net.IP{dnsAddr}\n\ts.sid = &dhcpv6.DUIDLL{\n\t\tHWType:        iana.HWTypeEthernet,\n\t\tLinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},\n\t}\n\n\tvar req, resp, msg *dhcpv6.Message\n\tmac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\tt.Run(\"solicit\", func(t *testing.T) {\n\t\treq, err = dhcpv6.NewSolicit(mac)\n\t\trequire.NoError(t, err)\n\n\t\tmsg, err = req.GetInnerMessage()\n\t\trequire.NoError(t, err)\n\n\t\tresp, err = dhcpv6.NewAdvertiseFromSolicit(msg)\n\t\trequire.NoError(t, err)\n\n\t\tassert.True(t, s.process(msg, req, resp))\n\t})\n\trequire.NoError(t, err)\n\n\tresp.AddOption(dhcpv6.OptServerID(s.sid))\n\n\tvar oia *dhcpv6.OptIANA\n\tvar oiaAddr *dhcpv6.OptIAAddress\n\tt.Run(\"advertise\", func(t *testing.T) {\n\t\trequire.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())\n\n\t\toia = resp.Options.OneIANA()\n\t\toiaAddr = oia.Options.OneAddress()\n\t\tassert.Equal(t, \"2001::2\", oiaAddr.IPv6Addr.String())\n\t})\n\n\tt.Run(\"request\", func(t *testing.T) {\n\t\treq, err = dhcpv6.NewRequestFromAdvertise(resp)\n\t\trequire.NoError(t, err)\n\n\t\tmsg, err = req.GetInnerMessage()\n\t\trequire.NoError(t, err)\n\n\t\tresp, err = dhcpv6.NewReplyFromMessage(msg)\n\t\trequire.NoError(t, err)\n\n\t\tassert.True(t, s.process(msg, req, resp))\n\t})\n\trequire.NoError(t, err)\n\n\tt.Run(\"reply\", func(t *testing.T) {\n\t\trequire.Equal(t, dhcpv6.MessageTypeReply, resp.Type())\n\n\t\toia = resp.Options.OneIANA()\n\t\toiaAddr = oia.Options.OneAddress()\n\t\tassert.Equal(t, \"2001::2\", oiaAddr.IPv6Addr.String())\n\t})\n\n\tdnsAddrs := resp.Options.DNS()\n\trequire.Len(t, dnsAddrs, 1)\n\n\tassert.Equal(t, dnsAddr, dnsAddrs[0])\n\n\tt.Run(\"lease\", func(t *testing.T) {\n\t\tls := s.GetLeases(LeasesDynamic)\n\t\trequire.Len(t, ls, 1)\n\n\t\tassert.Equal(t, \"2001::2\", ls[0].IP.String())\n\t\tassert.Equal(t, mac, ls[0].HWAddr)\n\t})\n}\n\nfunc TestIP6InRange(t *testing.T) {\n\tstart := net.ParseIP(\"2001::2\")\n\n\ttestCases := []struct {\n\t\tip   net.IP\n\t\twant bool\n\t}{{\n\t\tip:   net.ParseIP(\"2001::1\"),\n\t\twant: false,\n\t}, {\n\t\tip:   net.ParseIP(\"2002::2\"),\n\t\twant: false,\n\t}, {\n\t\tip:   start,\n\t\twant: true,\n\t}, {\n\t\tip:   net.ParseIP(\"2001::3\"),\n\t\twant: true,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.ip.String(), func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.want, ip6InRange(start, tc.ip))\n\t\t})\n\t}\n}\n\nfunc TestV6_FindMACbyIP(t *testing.T) {\n\tconst (\n\t\tstaticName  = \"static-client\"\n\t\tanotherName = \"another-client\"\n\t)\n\n\tstaticIP := netip.MustParseAddr(\"2001::1\")\n\tstaticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}\n\n\tanotherIP := netip.MustParseAddr(\"2001::100\")\n\tanotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}\n\n\ts := &v6Server{\n\t\tleases: []*dhcpsvc.Lease{{\n\t\t\tHostname: staticName,\n\t\t\tHWAddr:   staticMAC,\n\t\t\tIP:       staticIP,\n\t\t\tIsStatic: true,\n\t\t}, {\n\t\t\tExpiry:   time.Unix(10, 0),\n\t\t\tHostname: anotherName,\n\t\t\tHWAddr:   anotherMAC,\n\t\t\tIP:       anotherIP,\n\t\t}},\n\t}\n\n\ts.leases = []*dhcpsvc.Lease{{\n\t\tHostname: staticName,\n\t\tHWAddr:   staticMAC,\n\t\tIP:       staticIP,\n\t\tIsStatic: true,\n\t}, {\n\t\tExpiry:   time.Unix(10, 0),\n\t\tHostname: anotherName,\n\t\tHWAddr:   anotherMAC,\n\t\tIP:       anotherIP,\n\t}}\n\n\ttestCases := []struct {\n\t\twant net.HardwareAddr\n\t\tip   netip.Addr\n\t\tname string\n\t}{{\n\t\tname: \"basic\",\n\t\tip:   staticIP,\n\t\twant: staticMAC,\n\t}, {\n\t\tname: \"not_found\",\n\t\tip:   netip.MustParseAddr(\"ffff::1\"),\n\t\twant: nil,\n\t}, {\n\t\tname: \"expired\",\n\t\tip:   anotherIP,\n\t\twant: nil,\n\t}, {\n\t\tname: \"v4\",\n\t\tip:   netip.MustParseAddr(\"1.2.3.4\"),\n\t\twant: nil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmac := s.FindMACbyIP(tc.ip)\n\n\t\t\trequire.Equal(t, tc.want, mac)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpsvc/addresschecker.go",
    "content": "package dhcpsvc\n\nimport \"net/netip\"\n\n// addressChecker checks addresses for availability.\ntype addressChecker interface {\n\t// IsAvailable returns true if the address is available in the current\n\t// subnet.  Any error is a network error.\n\tIsAvailable(ip netip.Addr) (ok bool, err error)\n}\n\n// noopAddressChecker is an implementation of [addressChecker] that doesn't\n// perform any checks.\ntype noopAddressChecker struct{}\n\n// IsAvailable implements the [addressChecker] interface for noopAddressChecker.\nfunc (c noopAddressChecker) IsAvailable(ip netip.Addr) (ok bool, err error) {\n\treturn true, nil\n}\n\n// TODO(e.burkov):  Add ICMP implementation of [addressChecker], as required by\n// https://datatracker.ietf.org/doc/html/rfc2131#section-2.2.\n"
  },
  {
    "path": "internal/dhcpsvc/bitset.go",
    "content": "package dhcpsvc\n\nconst bitsPerWord = 64\n\n// bitSet is a sparse bitSet.  A nil *bitSet is an empty bitSet.\ntype bitSet struct {\n\twords map[uint64]uint64\n}\n\n// newBitSet returns a new bitset.\nfunc newBitSet() (s *bitSet) {\n\treturn &bitSet{\n\t\twords: map[uint64]uint64{},\n\t}\n}\n\n// isSet returns true if the bit n is set.\nfunc (s *bitSet) isSet(n uint64) (ok bool) {\n\tif s == nil {\n\t\treturn false\n\t}\n\n\twordIdx := n / bitsPerWord\n\tbitIdx := n % bitsPerWord\n\n\tvar word uint64\n\tword, ok = s.words[wordIdx]\n\n\treturn ok && word&(1<<bitIdx) != 0\n}\n\n// set sets or unsets a bit.\nfunc (s *bitSet) set(n uint64, ok bool) {\n\tif s == nil {\n\t\treturn\n\t}\n\n\twordIdx := n / bitsPerWord\n\tbitIdx := n % bitsPerWord\n\n\tword := s.words[wordIdx]\n\tif ok {\n\t\tword |= 1 << bitIdx\n\t} else {\n\t\tword &^= 1 << bitIdx\n\t}\n\n\ts.words[wordIdx] = word\n}\n"
  },
  {
    "path": "internal/dhcpsvc/bitset_internal_test.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"math\"\n\t\"testing\"\n\t\"testing/quick\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBitSet(t *testing.T) {\n\tt.Run(\"nil\", func(t *testing.T) {\n\t\tvar s *bitSet\n\n\t\tok := s.isSet(0)\n\t\tassert.False(t, ok)\n\n\t\tassert.NotPanics(t, func() {\n\t\t\ts.set(0, true)\n\t\t})\n\n\t\tok = s.isSet(0)\n\t\tassert.False(t, ok)\n\n\t\tassert.NotPanics(t, func() {\n\t\t\ts.set(0, false)\n\t\t})\n\n\t\tok = s.isSet(0)\n\t\tassert.False(t, ok)\n\t})\n\n\tt.Run(\"non_nil\", func(t *testing.T) {\n\t\ts := newBitSet()\n\n\t\tok := s.isSet(0)\n\t\tassert.False(t, ok)\n\n\t\ts.set(0, true)\n\n\t\tok = s.isSet(0)\n\t\tassert.True(t, ok)\n\n\t\ts.set(0, false)\n\n\t\tok = s.isSet(0)\n\t\tassert.False(t, ok)\n\t})\n\n\tt.Run(\"non_nil_long\", func(t *testing.T) {\n\t\ts := newBitSet()\n\n\t\ts.set(0, true)\n\t\ts.set(math.MaxUint64, true)\n\t\tassert.Len(t, s.words, 2)\n\n\t\tok := s.isSet(0)\n\t\tassert.True(t, ok)\n\n\t\tok = s.isSet(math.MaxUint64)\n\t\tassert.True(t, ok)\n\t})\n\n\tt.Run(\"compare_to_map\", func(t *testing.T) {\n\t\tm := map[uint64]struct{}{}\n\t\ts := newBitSet()\n\n\t\tmapFunc := func(setNew, checkOld, delOld uint64) (ok bool) {\n\t\t\tm[setNew] = struct{}{}\n\t\t\tdelete(m, delOld)\n\t\t\t_, ok = m[checkOld]\n\n\t\t\treturn ok\n\t\t}\n\n\t\tsetFunc := func(setNew, checkOld, delOld uint64) (ok bool) {\n\t\t\ts.set(setNew, true)\n\t\t\ts.set(delOld, false)\n\t\t\tok = s.isSet(checkOld)\n\n\t\t\treturn ok\n\t\t}\n\n\t\terr := quick.CheckEqual(mapFunc, setFunc, &quick.Config{\n\t\t\tMaxCount:      10_000,\n\t\t\tMaxCountScale: 10,\n\t\t})\n\t\tassert.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "internal/dhcpsvc/config.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"os\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n)\n\n// Config is the configuration for the DHCP service.\ntype Config struct {\n\t// Interfaces stores configurations of DHCP server specific for the network\n\t// interface identified by its name.  It must not be empty and must only\n\t// contain valid interface names and configurations.\n\tInterfaces map[string]*InterfaceConfig\n\n\t// NetworkDeviceManager is the manager of network devices.  It must not be\n\t// nil.\n\t//\n\t// TODO(e.burkov):  Set.\n\tNetworkDeviceManager NetworkDeviceManager\n\n\t// Logger will be used to log the DHCP events.  It must not be nil.\n\tLogger *slog.Logger\n\n\t// LocalDomainName is the top-level domain name to use for resolving DHCP\n\t// clients' hostnames.  It must be a valid domain name.\n\tLocalDomainName string\n\n\t// DBFilePath is the path to the database file containing the DHCP leases.\n\t// It must not be empty.\n\tDBFilePath string\n\n\t// ICMPTimeout is the timeout for checking another DHCP server's presence.\n\t// It must be non-negative.  If it is zero, the check will be skipped.\n\tICMPTimeout time.Duration\n\n\t// Enabled is the state of the service, whether it is enabled or not.\n\tEnabled bool\n}\n\n// type check\nvar _ validate.Interface = (*Config)(nil)\n\n// Validate implements the [validate.Interface] for *Config.\nfunc (conf *Config) Validate() (err error) {\n\tswitch {\n\tcase conf == nil:\n\t\treturn errors.ErrNoValue\n\tcase !conf.Enabled:\n\t\treturn nil\n\t}\n\n\terrs := []error{\n\t\tvalidate.NotNegative(\"conf.ICMPTimeout\", conf.ICMPTimeout),\n\t\tvalidate.NotEmpty(\"conf.DBFilePath\", conf.DBFilePath),\n\t\tvalidate.NotNil(\"conf.Logger\", conf.Logger),\n\t\tvalidate.NotNilInterface(\"conf.NetworkDeviceManager\", conf.NetworkDeviceManager),\n\t}\n\n\terr = netutil.ValidateDomainName(conf.LocalDomainName)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"conf.LocalDomainName: %w\", err))\n\t}\n\n\t// This is a best-effort check for the file accessibility.  The file will be\n\t// checked again when it is opened later.\n\tif _, err = os.Stat(conf.DBFilePath); err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\terrs = append(errs, fmt.Errorf(\"conf.DBFilePath %q: %w\", conf.DBFilePath, err))\n\t}\n\n\tif len(conf.Interfaces) == 0 {\n\t\terr = fmt.Errorf(\"conf.Interfaces: %w\", errors.ErrEmptyValue)\n\t\terrs = append(errs, err)\n\n\t\treturn errors.Join(errs...)\n\t}\n\n\tfor _, iface := range slices.Sorted(maps.Keys(conf.Interfaces)) {\n\t\tifaceConf := conf.Interfaces[iface]\n\t\terrs = validate.Append(errs, \"conf.Interfaces.\"+iface, ifaceConf)\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// InterfaceConfig is the configuration of a single DHCP interface.\ntype InterfaceConfig struct {\n\t// IPv4 is the configuration of DHCP protocol for IPv4.\n\tIPv4 *IPv4Config\n\n\t// IPv6 is the configuration of DHCP protocol for IPv6.\n\tIPv6 *IPv6Config\n}\n\n// type check\nvar _ validate.Interface = (*InterfaceConfig)(nil)\n\n// Validate implements the [validate.Interface] interface for *InterfaceConfig.\nfunc (ic *InterfaceConfig) Validate() (err error) {\n\tif ic == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\treturn errors.Join(\n\t\terrors.Annotate(ic.IPv4.Validate(), \"IPv4: %w\"),\n\t\terrors.Annotate(ic.IPv6.Validate(), \"IPv6: %w\"),\n\t)\n}\n"
  },
  {
    "path": "internal/dhcpsvc/config_test.go",
    "content": "package dhcpsvc_test\n\nimport (\n\t\"net/netip\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n)\n\n// TODO(e.burkov):  Move string IP address representations into constants and\n// use in the tests below.\n\nfunc TestIPv4Config_Validate(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tconf       *dhcpsvc.IPv4Config\n\t\twantErrMsg string\n\t}{{\n\t\tname:       \"nil\",\n\t\tconf:       nil,\n\t\twantErrMsg: \"no value\",\n\t}, {\n\t\tname:       \"disabled\",\n\t\tconf:       &dhcpsvc.IPv4Config{Enabled: false},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"nil_clock\",\n\t\tconf: &dhcpsvc.IPv4Config{\n\t\t\tEnabled:       true,\n\t\t\tClock:         nil,\n\t\t\tGatewayIP:     testIPv4Conf.GatewayIP,\n\t\t\tSubnetMask:    testIPv4Conf.SubnetMask,\n\t\t\tRangeStart:    testIPv4Conf.RangeStart,\n\t\t\tRangeEnd:      testIPv4Conf.RangeEnd,\n\t\t\tLeaseDuration: testIPv4Conf.LeaseDuration,\n\t\t},\n\t\twantErrMsg: \"clock: no value\",\n\t}, {\n\t\tname: \"bad_lease_duration\",\n\t\tconf: &dhcpsvc.IPv4Config{\n\t\t\tEnabled:       true,\n\t\t\tClock:         testIPv4Conf.Clock,\n\t\t\tGatewayIP:     testIPv4Conf.GatewayIP,\n\t\t\tSubnetMask:    testIPv4Conf.SubnetMask,\n\t\t\tRangeStart:    testIPv4Conf.RangeStart,\n\t\t\tRangeEnd:      testIPv4Conf.RangeEnd,\n\t\t\tLeaseDuration: 0,\n\t\t},\n\t\twantErrMsg: \"lease duration: not positive: 0s\",\n\t}, {\n\t\tname: \"bad_gateway_ip\",\n\t\tconf: &dhcpsvc.IPv4Config{\n\t\t\tEnabled:       true,\n\t\t\tClock:         testIPv4Conf.Clock,\n\t\t\tGatewayIP:     netip.MustParseAddr(testRangeStartV6Str),\n\t\t\tSubnetMask:    testIPv4Conf.SubnetMask,\n\t\t\tRangeStart:    testIPv4Conf.RangeStart,\n\t\t\tRangeEnd:      testIPv4Conf.RangeEnd,\n\t\t\tLeaseDuration: testIPv4Conf.LeaseDuration,\n\t\t},\n\t\twantErrMsg: \"gateway ip \" + testRangeStartV6Str + \" must be a valid ipv4\" + \"\\n\" +\n\t\t\t\"range start \" + testRangeStartV4Str + \" is not within \" + testRangeStartV6Str + \"/24\",\n\t}, {\n\t\tname: \"bad_subnet_mask\",\n\t\tconf: &dhcpsvc.IPv4Config{\n\t\t\tEnabled:       true,\n\t\t\tClock:         testIPv4Conf.Clock,\n\t\t\tGatewayIP:     testIPv4Conf.GatewayIP,\n\t\t\tSubnetMask:    netip.MustParseAddr(testRangeStartV6Str),\n\t\t\tRangeStart:    testIPv4Conf.RangeStart,\n\t\t\tRangeEnd:      testIPv4Conf.RangeEnd,\n\t\t\tLeaseDuration: testIPv4Conf.LeaseDuration,\n\t\t},\n\t\twantErrMsg: \"subnet mask \" + testRangeStartV6Str + \" must be a valid ipv4 cidr mask\",\n\t}, {\n\t\tname: \"bad_range_start\",\n\t\tconf: &dhcpsvc.IPv4Config{\n\t\t\tEnabled:       true,\n\t\t\tClock:         testIPv4Conf.Clock,\n\t\t\tGatewayIP:     testIPv4Conf.GatewayIP,\n\t\t\tSubnetMask:    testIPv4Conf.SubnetMask,\n\t\t\tRangeStart:    netip.MustParseAddr(testRangeStartV6Str),\n\t\t\tRangeEnd:      testIPv4Conf.RangeEnd,\n\t\t\tLeaseDuration: testIPv4Conf.LeaseDuration,\n\t\t},\n\t\twantErrMsg: \"range start \" + testRangeStartV6Str + \" must be a valid ipv4\" + \"\\n\" +\n\t\t\t\"range start \" + testRangeStartV6Str + \" is not within \" +\n\t\t\ttestGatewayIPv4Str + \"/24\" + \"\\n\" + \"invalid ip range: \" + testRangeStartV6Str +\n\t\t\t\" and \" + testRangeEndV4Str + \" must be within the same address family\",\n\t}, {\n\t\tname: \"bad_range_end\",\n\t\tconf: &dhcpsvc.IPv4Config{\n\t\t\tEnabled:       true,\n\t\t\tClock:         testIPv4Conf.Clock,\n\t\t\tGatewayIP:     testIPv4Conf.GatewayIP,\n\t\t\tSubnetMask:    testIPv4Conf.SubnetMask,\n\t\t\tRangeStart:    testIPv4Conf.RangeStart,\n\t\t\tRangeEnd:      netip.MustParseAddr(testRangeStartV6Str),\n\t\t\tLeaseDuration: testIPv4Conf.LeaseDuration,\n\t\t},\n\t\twantErrMsg: \"range end \" + testRangeStartV6Str + \" must be a valid ipv4\" + \"\\n\" +\n\t\t\t\"range end \" + testRangeStartV6Str + \" is not within \" + testGatewayIPv4Str + \"/24\" +\n\t\t\t\"\\n\" + \"invalid ip range: \" + testRangeStartV4Str + \" and \" + testRangeStartV6Str +\n\t\t\t\" must be within the same address family\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, tc.conf.Validate())\n\t\t})\n\t}\n}\n\nfunc TestIPv6Config_Validate(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tconf       *dhcpsvc.IPv6Config\n\t\twantErrMsg string\n\t}{{\n\t\tname:       \"nil\",\n\t\tconf:       nil,\n\t\twantErrMsg: \"no value\",\n\t}, {\n\t\tname:       \"disabled\",\n\t\tconf:       &dhcpsvc.IPv6Config{Enabled: false},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"bad_range_start\",\n\t\tconf: &dhcpsvc.IPv6Config{\n\t\t\tEnabled:       true,\n\t\t\tRangeStart:    testIPv4Conf.GatewayIP,\n\t\t\tLeaseDuration: 1 * time.Hour,\n\t\t},\n\t\twantErrMsg: \"range start \" + testGatewayIPv4Str + \" should be a valid ipv6\",\n\t}, {\n\t\tname: \"bad_lease_duration\",\n\t\tconf: &dhcpsvc.IPv6Config{\n\t\t\tEnabled:       true,\n\t\t\tRangeStart:    netip.MustParseAddr(testRangeStartV6Str),\n\t\t\tLeaseDuration: 0,\n\t\t},\n\t\twantErrMsg: \"lease duration 0s must be positive\",\n\t}, {\n\t\tname: \"valid\",\n\t\tconf: &dhcpsvc.IPv6Config{\n\t\t\tEnabled:       true,\n\t\t\tRangeStart:    netip.MustParseAddr(testRangeStartV6Str),\n\t\t\tLeaseDuration: 1 * time.Hour,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, tc.conf.Validate())\n\t\t})\n\t}\n}\n\nfunc TestConfig_Validate(t *testing.T) {\n\tt.Parallel()\n\n\tvalid := &dhcpsvc.Config{\n\t\tInterfaces:           testInterfaceConf,\n\t\tNetworkDeviceManager: dhcpsvc.EmptyNetworkDeviceManager{},\n\t\tLogger:               testLogger,\n\t\tLocalDomainName:      testLocalTLD,\n\t\tDBFilePath:           filepath.Join(t.TempDir(), testDBLeasesFilename),\n\t\tICMPTimeout:          1 * time.Second,\n\t\tEnabled:              true,\n\t}\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tconf       *dhcpsvc.Config\n\t\twantErrMsg string\n\t}{{\n\t\tname:       \"disabled\",\n\t\tconf:       &dhcpsvc.Config{Enabled: false},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname:       \"nil\",\n\t\tconf:       nil,\n\t\twantErrMsg: \"no value\",\n\t}, {\n\t\tname:       \"valid\",\n\t\tconf:       valid,\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"bad_icmp_timeout\",\n\t\tconf: &dhcpsvc.Config{\n\t\t\tInterfaces:           valid.Interfaces,\n\t\t\tNetworkDeviceManager: valid.NetworkDeviceManager,\n\t\t\tLogger:               valid.Logger,\n\t\t\tLocalDomainName:      valid.LocalDomainName,\n\t\t\tDBFilePath:           valid.DBFilePath,\n\t\t\tICMPTimeout:          -1 * time.Second,\n\t\t\tEnabled:              valid.Enabled,\n\t\t},\n\t\twantErrMsg: \"conf.ICMPTimeout: negative value: -1s\",\n\t}, {\n\t\tname: \"bad_db_filepath\",\n\t\tconf: &dhcpsvc.Config{\n\t\t\tInterfaces:           valid.Interfaces,\n\t\t\tNetworkDeviceManager: valid.NetworkDeviceManager,\n\t\t\tLogger:               valid.Logger,\n\t\t\tLocalDomainName:      valid.LocalDomainName,\n\t\t\tDBFilePath:           \"\",\n\t\t\tICMPTimeout:          valid.ICMPTimeout,\n\t\t\tEnabled:              valid.Enabled,\n\t\t},\n\t\twantErrMsg: \"conf.DBFilePath: empty value\",\n\t}, {\n\t\tname: \"no_interfaces\",\n\t\tconf: &dhcpsvc.Config{\n\t\t\tInterfaces:           nil,\n\t\t\tNetworkDeviceManager: valid.NetworkDeviceManager,\n\t\t\tLogger:               valid.Logger,\n\t\t\tLocalDomainName:      valid.LocalDomainName,\n\t\t\tDBFilePath:           valid.DBFilePath,\n\t\t\tICMPTimeout:          valid.ICMPTimeout,\n\t\t\tEnabled:              valid.Enabled,\n\t\t},\n\t\twantErrMsg: \"conf.Interfaces: empty value\",\n\t}, {\n\t\tname: \"nil_network_manager\",\n\t\tconf: &dhcpsvc.Config{\n\t\t\tInterfaces:           valid.Interfaces,\n\t\t\tNetworkDeviceManager: nil,\n\t\t\tLogger:               valid.Logger,\n\t\t\tLocalDomainName:      valid.LocalDomainName,\n\t\t\tDBFilePath:           valid.DBFilePath,\n\t\t\tICMPTimeout:          valid.ICMPTimeout,\n\t\t\tEnabled:              valid.Enabled,\n\t\t},\n\t\twantErrMsg: \"conf.NetworkDeviceManager: no value\",\n\t}, {\n\t\tname: \"no_logger\",\n\t\tconf: &dhcpsvc.Config{\n\t\t\tInterfaces:           valid.Interfaces,\n\t\t\tNetworkDeviceManager: valid.NetworkDeviceManager,\n\t\t\tLogger:               nil,\n\t\t\tLocalDomainName:      valid.LocalDomainName,\n\t\t\tDBFilePath:           valid.DBFilePath,\n\t\t\tICMPTimeout:          valid.ICMPTimeout,\n\t\t\tEnabled:              valid.Enabled,\n\t\t},\n\t\twantErrMsg: \"conf.Logger: no value\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, tc.conf.Validate())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpsvc/db.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/google/renameio/v2/maybe\"\n)\n\n// dataVersion is the current version of the stored DHCP leases structure.\nconst dataVersion = 1\n\n// databasePerm is the permissions for the database file.\nconst databasePerm fs.FileMode = 0o640\n\n// dataLeases is the structure of the stored DHCP leases.\ntype dataLeases struct {\n\t// Leases is the list containing stored DHCP leases.\n\tLeases []*dbLease `json:\"leases\"`\n\n\t// Version is the current version of the structure.\n\tVersion int `json:\"version\"`\n}\n\n// dbLease is the structure of stored lease.\ntype dbLease struct {\n\tExpiry   string     `json:\"expires\"`\n\tIP       netip.Addr `json:\"ip\"`\n\tHostname string     `json:\"hostname\"`\n\tHWAddr   string     `json:\"mac\"`\n\tIsStatic bool       `json:\"static\"`\n}\n\n// compareNames returns the result of comparing the hostnames of dl and other\n// lexicographically.\nfunc (dl *dbLease) compareNames(other *dbLease) (res int) {\n\treturn strings.Compare(dl.Hostname, other.Hostname)\n}\n\n// toDBLease converts *Lease to *dbLease.\nfunc toDBLease(l *Lease) (dl *dbLease) {\n\tvar expiryStr string\n\tif !l.IsStatic {\n\t\t// The front-end is waiting for RFC 3999 format of the time value.  It\n\t\t// also shouldn't got an Expiry field for static leases.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.\n\t\texpiryStr = l.Expiry.Format(time.RFC3339)\n\t}\n\n\treturn &dbLease{\n\t\tExpiry:   expiryStr,\n\t\tHostname: l.Hostname,\n\t\tHWAddr:   l.HWAddr.String(),\n\t\tIP:       l.IP,\n\t\tIsStatic: l.IsStatic,\n\t}\n}\n\n// toInternal converts dl to *Lease.\nfunc (dl *dbLease) toInternal() (l *Lease, err error) {\n\tmac, err := net.ParseMAC(dl.HWAddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing hardware address: %w\", err)\n\t}\n\n\texpiry := time.Time{}\n\tif !dl.IsStatic {\n\t\texpiry, err = time.Parse(time.RFC3339, dl.Expiry)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing expiry time: %w\", err)\n\t\t}\n\t}\n\n\treturn &Lease{\n\t\tExpiry:   expiry,\n\t\tIP:       dl.IP,\n\t\tHostname: dl.Hostname,\n\t\tHWAddr:   mac,\n\t\tIsStatic: dl.IsStatic,\n\t}, nil\n}\n\n// dbLoad loads stored leases.  It must only be called before the service has\n// been started.\nfunc (idx *leaseIndex) dbLoad(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tifaces4 dhcpInterfacesV4,\n\tifaces6 dhcpInterfacesV6,\n) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"loading db: %w\") }()\n\n\tfile, err := os.Open(idx.dbFilePath)\n\tif err != nil {\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"reading db: %w\", err)\n\t\t}\n\n\t\tlogger.DebugContext(ctx, \"no db file found\")\n\n\t\treturn nil\n\t}\n\tdefer func() {\n\t\terr = errors.WithDeferred(err, file.Close())\n\t}()\n\n\tdl := &dataLeases{}\n\terr = json.NewDecoder(file).Decode(dl)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"decoding db: %w\", err)\n\t}\n\n\tidx.addDBLeases(ctx, logger, dl.Leases, ifaces4, ifaces6)\n\n\treturn nil\n}\n\n// addDBLeases adds leases to the server.\nfunc (idx *leaseIndex) addDBLeases(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tleases []*dbLease,\n\tifaces4 dhcpInterfacesV4,\n\tifaces6 dhcpInterfacesV6,\n) {\n\tvar v4, v6 uint\n\tfor i, l := range leases {\n\t\tlease, err := l.toInternal()\n\t\tif err != nil {\n\t\t\tlogger.WarnContext(ctx, \"converting lease\", \"idx\", i, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tiface, err := ifaceForAddr(l.IP, ifaces4, ifaces6)\n\t\tif err != nil {\n\t\t\tlogger.WarnContext(ctx, \"searching lease iface\", \"idx\", i, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\terr = idx.add(ctx, logger, lease, iface)\n\t\tif err != nil {\n\t\t\tlogger.WarnContext(ctx, \"adding lease\", \"idx\", i, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif l.IP.Is4() {\n\t\t\tv4++\n\t\t} else {\n\t\t\tv6++\n\t\t}\n\t}\n\n\t// TODO(e.burkov):  Group by interface.\n\tlogger.InfoContext(ctx, \"loaded leases\", \"v4\", v4, \"v6\", v6, \"total\", len(leases))\n}\n\n// writeDB writes leases to the database file.  It expects the\n// [DHCPServer.leasesMu] to be locked.\nfunc (idx *leaseIndex) dbStore(ctx context.Context, logger *slog.Logger) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"writing db: %w\") }()\n\n\tdl := &dataLeases{\n\t\t// Avoid writing null into the database file if there are no leases.\n\t\tLeases:  make([]*dbLease, 0, idx.len()),\n\t\tVersion: dataVersion,\n\t}\n\n\tfor l := range idx.rangeLeases {\n\t\tlease := toDBLease(l)\n\t\ti, _ := slices.BinarySearchFunc(dl.Leases, lease, (*dbLease).compareNames)\n\t\tdl.Leases = slices.Insert(dl.Leases, i, lease)\n\t}\n\n\tbuf, err := json.Marshal(dl)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = maybe.WriteFile(idx.dbFilePath, buf, databasePerm)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tlogger.InfoContext(ctx, \"stored leases\", \"num\", len(dl.Leases), \"file\", idx.dbFilePath)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dhcpsvc/db_internal_test.go",
    "content": "package dhcpsvc\n\n// DatabasePerm is the permissions for the test database file.\nconst DatabasePerm = databasePerm\n"
  },
  {
    "path": "internal/dhcpsvc/dhcpsvc.go",
    "content": "// Package dhcpsvc contains the AdGuard Home DHCP service.\n//\n// TODO(e.burkov): Add tests.\npackage dhcpsvc\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n)\n\nconst (\n\t// keyInterface is the key for logging the network interface name.\n\tkeyInterface = \"iface\"\n\n\t// keyFamily is the key for logging the handled address family.\n\tkeyFamily = \"family\"\n)\n\n// Interface is a DHCP service.\n//\n// TODO(e.burkov):  Separate HostByIP, MACByIP, IPByHost into a separate\n// interface.  This is also applicable to Enabled method.\n//\n// TODO(e.burkov):  Reconsider the requirements for the leases validity.\ntype Interface interface {\n\tagh.ServiceWithConfig[*Config]\n\n\t// Enabled returns true if DHCP provides information about clients.\n\tEnabled() (ok bool)\n\n\t// HostByIP returns the hostname of the DHCP client with the given IP\n\t// address.  The address will be netip.Addr{} if there is no such client,\n\t// due to an assumption that a DHCP client must always have an IP address.\n\tHostByIP(ip netip.Addr) (host string)\n\n\t// MACByIP returns the MAC address for the given IP address leased.  It\n\t// returns nil if there is no such client, due to an assumption that a DHCP\n\t// client must always have a MAC address.\n\t//\n\t// TODO(e.burkov):  Think of a contract for the returned value.\n\tMACByIP(ip netip.Addr) (mac net.HardwareAddr)\n\n\t// IPByHost returns the IP address of the DHCP client with the given\n\t// hostname.  The hostname will be an empty string if there is no such\n\t// client, due to an assumption that a DHCP client must always have a\n\t// hostname, either set or generated.\n\tIPByHost(host string) (ip netip.Addr)\n\n\t// Leases returns all the active DHCP leases.  The returned slice should be\n\t// a clone.  The order of leases is undefined.\n\t//\n\t// TODO(e.burkov):  Consider implementing iterating methods with appropriate\n\t// signatures instead of cloning the whole list.\n\tLeases() (ls []*Lease)\n\n\t// AddLease adds a new DHCP lease.  l must be valid.  It returns an error if\n\t// l already exists.\n\tAddLease(ctx context.Context, l *Lease) (err error)\n\n\t// UpdateStaticLease replaces an existing static DHCP lease.  l must be\n\t// valid.  It returns an error if the lease with the given hardware address\n\t// doesn't exist or if other values match another existing lease.\n\tUpdateStaticLease(ctx context.Context, l *Lease) (err error)\n\n\t// RemoveLease removes an existing DHCP lease.  l must be valid.  It returns\n\t// an error if there is no lease equal to l.\n\tRemoveLease(ctx context.Context, l *Lease) (err error)\n\n\t// Reset removes all the DHCP leases.\n\t//\n\t// TODO(e.burkov):  If it's really needed?\n\tReset(ctx context.Context) (err error)\n}\n\n// Empty is an [Interface] implementation that does nothing.\ntype Empty struct{}\n\n// type check\nvar _ agh.ServiceWithConfig[*Config] = Empty{}\n\n// Start implements the [Service] interface for Empty.\nfunc (Empty) Start(_ context.Context) (err error) { return nil }\n\n// Shutdown implements the [Service] interface for Empty.\nfunc (Empty) Shutdown(_ context.Context) (err error) { return nil }\n\n// Config implements the [ServiceWithConfig] interface for Empty.\nfunc (Empty) Config() (conf *Config) { return nil }\n\n// type check\nvar _ Interface = Empty{}\n\n// Enabled implements the [Interface] interface for Empty.\nfunc (Empty) Enabled() (ok bool) { return false }\n\n// HostByIP implements the [Interface] interface for Empty.\nfunc (Empty) HostByIP(_ netip.Addr) (host string) { return \"\" }\n\n// MACByIP implements the [Interface] interface for Empty.\nfunc (Empty) MACByIP(_ netip.Addr) (mac net.HardwareAddr) { return nil }\n\n// IPByHost implements the [Interface] interface for Empty.\nfunc (Empty) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} }\n\n// Leases implements the [Interface] interface for Empty.\nfunc (Empty) Leases() (leases []*Lease) { return nil }\n\n// AddLease implements the [Interface] interface for Empty.\nfunc (Empty) AddLease(_ context.Context, _ *Lease) (err error) { return nil }\n\n// UpdateStaticLease implements the [Interface] interface for Empty.\nfunc (Empty) UpdateStaticLease(_ context.Context, _ *Lease) (err error) { return nil }\n\n// RemoveLease implements the [Interface] interface for Empty.\nfunc (Empty) RemoveLease(_ context.Context, _ *Lease) (err error) { return nil }\n\n// Reset implements the [Interface] interface for Empty.\nfunc (Empty) Reset(_ context.Context) (err error) { return nil }\n"
  },
  {
    "path": "internal/dhcpsvc/dhcpsvc_test.go",
    "content": "package dhcpsvc_test\n\nimport (\n\t\"cmp\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/faketime\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TODO(e.burkov):  Use addresses and prefixes from [RFC 5737].\n//\n// [RFC 5737]: https://datatracker.ietf.org/doc/html/rfc5737\n\n// testLocalTLD is a common local TLD for tests.\nconst testLocalTLD = \"local\"\n\n// testIfaceName is the name of the test network interface.\nconst testIfaceName = \"iface0\"\n\n// testDBLeasesFilename is the common name of a leases database file for tests.\nconst testDBLeasesFilename = \"leases.json\"\n\n// testTimeout is a common timeout for tests and contexts.\nconst testTimeout = 10 * time.Second\n\n// testLeaseTTL is the lease duration used in tests.\nconst testLeaseTTL = 24 * time.Hour\n\n// testXid is a common transaction ID for DHCPv4 tests.\nconst testXid = 1\n\n// testLogger is a common logger for tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\n// testdata is a filesystem containing data for tests.\nvar testdata = os.DirFS(\"testdata\")\n\n// testCurrentTime is the fixed time returned by [testClock] to ensure\n// reproducible tests.\nvar testCurrentTime = time.Date(2025, 1, 1, 1, 1, 1, 0, time.UTC)\n\n// testClock is the test [timeutil.Clock] that always returns [testCurrentTime].\nvar testClock = &faketime.Clock{\n\tOnNow: func() (now time.Time) {\n\t\treturn testCurrentTime\n\t},\n}\n\nconst (\n\t// testGatewayIPv4Str is the string representation of the gateway IPv4\n\t// address used in tests.\n\ttestGatewayIPv4Str = \"192.0.2.1\"\n\n\t// testSubnetMaskV4Str is the string representation of the subnet mask for\n\t// the IPv4 interface used in tests.\n\ttestSubnetMaskV4Str = \"255.255.255.0\"\n\n\t// testRangeStartV4Str is the string representation of the range start of\n\t// the IPv4 interface used in tests.\n\ttestRangeStartV4Str = \"192.0.2.100\"\n\n\t// testRangeEndV4Str is the string representation of the range end of the\n\t// IPv4 interface used in tests.\n\ttestRangeEndV4Str = \"192.0.2.200\"\n\n\t// testIfaceAddrV4Str is the string representation of the interface's IPv4\n\t// address used in tests.\n\ttestIfaceAddrV4Str = \"192.0.2.2\"\n\n\t// testAnotherGatewayIPv4Str is the string representation of the second\n\t// gateway IPv4 address used in tests.\n\ttestAnotherGatewayIPv4Str = \"198.51.100.1\"\n\n\t// testAnotherSubnetMaskV4Str is the string representation of the subnet\n\t// mask for the second IPv4 interface used in tests.\n\ttestAnotherSubnetMaskV4Str = \"255.255.255.0\"\n\n\t// testAnotherRangeStartV4Str is the string representation of the range\n\t// start of the second IPv4 interface used in tests.\n\ttestAnotherRangeStartV4Str = \"198.51.100.100\"\n\n\t// testAnotherRangeEndV4Str is the string representation of the range end\n\t// of the second IPv4 interface used in tests.\n\ttestAnotherRangeEndV4Str = \"198.51.100.200\"\n)\n\nconst (\n\t// testRangeStartV6Str is the string representation of the range start of\n\t// the IPv6 interface used in tests.\n\ttestRangeStartV6Str = \"2001:db8::1\"\n\n\t// testAnotherRangeStartV6Str is the string representation of the range\n\t// start of the second IPv6 interface used in tests.\n\ttestAnotherRangeStartV6Str = \"2001:db9::1\"\n)\n\nvar (\n\t// testIPv4Conf is a common valid IPv4 part of the interface configuration\n\t// for tests.\n\ttestIPv4Conf = &dhcpsvc.IPv4Config{\n\t\tClock:         testClock,\n\t\tGatewayIP:     netip.MustParseAddr(testGatewayIPv4Str),\n\t\tSubnetMask:    netip.MustParseAddr(testSubnetMaskV4Str),\n\t\tRangeStart:    netip.MustParseAddr(testRangeStartV4Str),\n\t\tRangeEnd:      netip.MustParseAddr(testRangeEndV4Str),\n\t\tLeaseDuration: testLeaseTTL,\n\t\tEnabled:       true,\n\t}\n\n\t// testIfaceAddr is a common valid IPv4 address of the test network\n\t// interface, compliant with [testIPv4Conf], i.e. outside of the range,\n\t// within the subnet, not equal to the gateway.\n\ttestIfaceAddr = netip.MustParseAddr(testIfaceAddrV4Str)\n\n\t// testIfaceHWAddr is a common valid hardware address of the test network\n\t// interface.\n\ttestIfaceHWAddr = net.HardwareAddr{0x01, 0x01, 0x01, 0x01, 0x01, 0x01}\n)\n\n// testIPv6Conf is a common valid IPv6 part of the interface configuration for\n// tests.\nvar testIPv6Conf = &dhcpsvc.IPv6Config{\n\tEnabled:       true,\n\tRangeStart:    netip.MustParseAddr(testRangeStartV6Str),\n\tLeaseDuration: testLeaseTTL,\n\tRAAllowSLAAC:  true,\n\tRASLAACOnly:   true,\n}\n\n// testInterfaceConf is a common valid set of interface configurations for\n// tests.\nvar testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{\n\ttestIfaceName: {\n\t\tIPv4: testIPv4Conf,\n\t\tIPv6: testIPv6Conf,\n\t},\n\t\"iface1\": {\n\t\tIPv4: &dhcpsvc.IPv4Config{\n\t\t\tEnabled:       true,\n\t\t\tClock:         timeutil.SystemClock{},\n\t\t\tGatewayIP:     netip.MustParseAddr(testAnotherGatewayIPv4Str),\n\t\t\tSubnetMask:    netip.MustParseAddr(testAnotherSubnetMaskV4Str),\n\t\t\tRangeStart:    netip.MustParseAddr(testAnotherRangeStartV4Str),\n\t\t\tRangeEnd:      netip.MustParseAddr(testAnotherRangeEndV4Str),\n\t\t\tLeaseDuration: 1 * time.Hour,\n\t\t},\n\t\tIPv6: &dhcpsvc.IPv6Config{\n\t\t\tEnabled:       true,\n\t\t\tRangeStart:    netip.MustParseAddr(testAnotherRangeStartV6Str),\n\t\t\tLeaseDuration: 1 * time.Hour,\n\t\t\tRAAllowSLAAC:  true,\n\t\t\tRASLAACOnly:   true,\n\t\t},\n\t},\n}\n\n// disabledIPv6Config is a configuration of IPv6 part of the interfaces\n// configuration that is disabled.\nvar disabledIPv6Config = &dhcpsvc.IPv6Config{Enabled: false}\n\n// fullLayersStack is the complete stack of layers expected to appear in the\n// DHCP response packets.\nvar fullLayersStack = []gopacket.LayerType{\n\tlayers.LayerTypeEthernet,\n\tlayers.LayerTypeIPv4,\n\tlayers.LayerTypeUDP,\n\tlayers.LayerTypeDHCPv4,\n}\n\n// newTempDB copies the leases database file located in the testdata FS, under\n// tb.Name()/leases.json, to a temporary directory and returns the path to the\n// copied file.\nfunc newTempDB(tb testing.TB) (dst string) {\n\ttb.Helper()\n\n\tdata, err := fs.ReadFile(testdata, path.Join(tb.Name(), testDBLeasesFilename))\n\trequire.NoError(tb, err)\n\n\tdst = filepath.Join(tb.TempDir(), testDBLeasesFilename)\n\n\terr = os.WriteFile(dst, data, 0o640)\n\trequire.NoError(tb, err)\n\n\treturn dst\n}\n\n// newTestDHCPServer creates a new DHCPServer for testing.  It uses the default\n// values of config in case it's nil or some of its fields aren't set.\nfunc newTestDHCPServer(tb testing.TB, conf *dhcpsvc.Config) (srv *dhcpsvc.DHCPServer) {\n\ttb.Helper()\n\n\tconf = cmp.Or(conf, &dhcpsvc.Config{\n\t\tEnabled: true,\n\t})\n\n\tconf.NetworkDeviceManager = cmp.Or[dhcpsvc.NetworkDeviceManager](\n\t\tconf.NetworkDeviceManager,\n\t\tdhcpsvc.EmptyNetworkDeviceManager{},\n\t)\n\tconf.Logger = cmp.Or(conf.Logger, testLogger)\n\tconf.LocalDomainName = cmp.Or(conf.LocalDomainName, testLocalTLD)\n\tif conf.DBFilePath == \"\" {\n\t\tconf.DBFilePath = filepath.Join(tb.TempDir(), \"leases.json\")\n\t}\n\tconf.ICMPTimeout = cmp.Or(conf.ICMPTimeout, testTimeout)\n\tif conf.Interfaces == nil {\n\t\tconf.Interfaces = testInterfaceConf\n\t}\n\n\tsrv, err := dhcpsvc.New(testutil.ContextWithTimeout(tb, testTimeout), conf)\n\trequire.NoError(tb, err)\n\n\treturn srv\n}\n"
  },
  {
    "path": "internal/dhcpsvc/errors.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"fmt\"\n)\n\n// newMustErr returns an error that indicates that valName must be as must\n// describes.\n//\n// TODO(e.burkov):  Use [validate] and remove this function.\nfunc newMustErr(valName, must string, val fmt.Stringer) (err error) {\n\treturn fmt.Errorf(\"%s %s must %s\", valName, val, must)\n}\n"
  },
  {
    "path": "internal/dhcpsvc/handle.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n)\n\n// serveEther4 handles the incoming ethernet packets and dispatches them to the\n// appropriate handler.  It's used to run in a separate goroutine as it blocks\n// until packets channel is closed.  iface and nd must not be nil.  nd must have\n// at least a single address returned by its Addresses method.\nfunc (srv *DHCPServer) serveEther4(ctx context.Context, iface *dhcpInterfaceV4, nd NetworkDevice) {\n\tdefer slogutil.RecoverAndLog(ctx, srv.logger)\n\n\tsrc := gopacket.NewPacketSource(nd, nd.LinkType())\n\n\tfor pkt := range src.Packets() {\n\t\tetherLayer, ok := pkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)\n\t\tif !ok {\n\t\t\tactual := pkt.Layers()\n\t\t\tsrv.logger.DebugContext(ctx, \"skipping non-ethernet packet\", \"layers\", actual)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tipLayer, ok := pkt.Layer(layers.LayerTypeIPv4).(*layers.IPv4)\n\t\tif !ok {\n\t\t\tactual := pkt.Layers()\n\t\t\tsrv.logger.DebugContext(ctx, \"skipping non-ipv4 packet\", \"layers\", actual)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tfd := &frameData{\n\t\t\tether:  etherLayer,\n\t\t\tip:     ipLayer,\n\t\t\tdevice: nd,\n\t\t}\n\n\t\terr := srv.serveV4(ctx, iface, pkt, fd)\n\t\tif err != nil {\n\t\t\tsrv.logger.ErrorContext(ctx, \"serving\", slogutil.KeyError, err)\n\t\t}\n\t}\n}\n\n// TODO(e.burkov):  Add DHCPServer.serveEther6.\n"
  },
  {
    "path": "internal/dhcpsvc/handler4.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n)\n\n// serveV4 handles the ethernet packet of IPv4 type. iface and pkt must not be\n// nil.  iface and fd must not be nil.  pkt must be an IPv4 packet.\nfunc (srv *DHCPServer) serveV4(\n\tctx context.Context,\n\tiface *dhcpInterfaceV4,\n\tpkt gopacket.Packet,\n\tfd *frameData,\n) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"serving dhcpv4: %w\") }()\n\n\treq, ok := pkt.Layer(layers.LayerTypeDHCPv4).(*layers.DHCPv4)\n\tif !ok {\n\t\t// TODO(e.burkov):  Consider adding some debug information about the\n\t\t// actual received packet.\n\t\tsrv.logger.DebugContext(ctx, \"skipping non-dhcpv4 packet\")\n\n\t\treturn nil\n\t}\n\n\t// TODO(e.burkov):  Handle duplicate Xid.\n\n\tif req.Operation != layers.DHCPOpRequest {\n\t\tsrv.logger.DebugContext(ctx, \"skipping non-request dhcpv4 packet\", \"op\", req.Operation)\n\n\t\treturn nil\n\t}\n\n\ttyp, ok := msg4Type(req)\n\tif !ok {\n\t\t// The \"DHCP message type\" option - must be included in every DHCP\n\t\t// message.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc2131#section-3.\n\t\treturn fmt.Errorf(\"message type: %w\", errors.ErrNoValue)\n\t}\n\n\treturn iface.handleDHCPv4(ctx, typ, req, fd)\n}\n\n// handleDHCPv4 handles the DHCPv4 message of the given type.  The DHCPDISCOVER\n// messages are handled by all interfaces concurrently, as those offer addresses\n// for the independent networks.  The DHCPREQUEST, DHCPRELEASE, and DHCPDECLINE\n// messages are handled by the appropriate interface according to the client's\n// choice.  req and fd must not be nil, typ should be one of:\n//   - [layers.DHCPMsgTypeDiscover]\n//   - [layers.DHCPMsgTypeRequest]\n//   - [layers.DHCPMsgTypeRelease]\n//   - [layers.DHCPMsgTypeDecline]\nfunc (iface *dhcpInterfaceV4) handleDHCPv4(\n\tctx context.Context,\n\ttyp layers.DHCPMsgType,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n) (err error) {\n\tswitch typ {\n\tcase layers.DHCPMsgTypeDiscover:\n\t\tiface.handleDiscover(ctx, req, fd)\n\tcase layers.DHCPMsgTypeRequest:\n\t\tiface.handleRequest(ctx, req, fd)\n\tcase layers.DHCPMsgTypeRelease:\n\t\tiface.handleRelease(ctx, req)\n\tcase layers.DHCPMsgTypeDecline:\n\t\tiface.handleDecline(ctx, req)\n\tdefault:\n\t\t// TODO(e.burkov):  Handle DHCPINFORM.\n\t\treturn fmt.Errorf(\"dhcpv4: request type: %w: %v\", errors.ErrBadEnumValue, typ)\n\t}\n\n\treturn nil\n}\n\n// handleRequest handles the DHCPv4 message of DHCPREQUEST type.  req must be a\n// DHCPREQUEST message.  req and fd must not be nil.\n//\n// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2.\n//\n// TODO(e.burkov):  Remove allocated leases after client have chosen one.\nfunc (iface *dhcpInterfaceV4) handleRequest(\n\tctx context.Context,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n) {\n\tsrvID, hasSrvID := serverID4(req)\n\treqIP, hasReqIP := requestedIPv4(req)\n\n\tl := iface.common.logger\n\n\tswitch {\n\tcase hasSrvID && !srvID.IsUnspecified():\n\t\t// If the DHCPREQUEST message contains a server identifier option, the\n\t\t// message is in response to a DHCPOFFER message.  Otherwise, the\n\t\t// message is a request to verify or extend an existing lease.\n\t\tif !slices.Contains(fd.device.Addresses(), srvID) {\n\t\t\tl.DebugContext(ctx, \"skipping selecting request\", \"serverid\", srvID)\n\n\t\t\treturn\n\t\t}\n\n\t\tiface.handleSelecting(ctx, req, fd, reqIP)\n\tcase hasReqIP && !reqIP.IsUnspecified():\n\t\t// Requested IP address option MUST be filled in with client's notion of\n\t\t// its previously assigned address.\n\t\tif !iface.subnet.Contains(reqIP) {\n\t\t\t// If the DHCP server detects that the client is on the wrong net\n\t\t\t// then the server SHOULD send a DHCPNAK message to the client.\n\t\t\tl.DebugContext(ctx, \"declining init-reboot request\", \"requestedip\", reqIP)\n\t\t\tiface.respondNAK(ctx, req, fd)\n\n\t\t\treturn\n\t\t}\n\n\t\tiface.handleInitReboot(ctx, req, fd, reqIP)\n\tdefault:\n\t\t// Server identifier MUST NOT be filled in, requested IP address option\n\t\t// MUST NOT be filled in, 'ciaddr' MUST be filled in with client's\n\t\t// notion of its previously assigned address.\n\t\tip, ok := netip.AddrFromSlice(req.ClientIP.To4())\n\t\tif !ok || !iface.subnet.Contains(ip) {\n\t\t\tl.DebugContext(ctx, \"skipping renew request\", \"clientip\", ip)\n\n\t\t\treturn\n\t\t}\n\n\t\tiface.handleRenew(ctx, req, fd, ip)\n\t}\n}\n\n// handleDiscover handles messages of type DHCPDISCOVER.  req must be a\n// DHCPDISCOVER message, fd must not be nil.\nfunc (iface *dhcpInterfaceV4) handleDiscover(\n\tctx context.Context,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n) {\n\tl := iface.common.logger\n\tmac := req.ClientHWAddr\n\tmk := macToKey(mac)\n\n\t// Check if there's an existing lease for this MAC address.\n\tiface.common.indexMu.Lock()\n\tdefer iface.common.indexMu.Unlock()\n\n\tlease, hasLease := iface.common.leases[mk]\n\tif hasLease {\n\t\treqIP, hasReqIP := requestedIPv4(req)\n\t\tif hasReqIP && reqIP != lease.IP {\n\t\t\tl.DebugContext(ctx, \"different requested ip\", \"requested\", reqIP, \"lease\", lease.IP)\n\t\t}\n\n\t\tlease.updateExpiry(iface.clock, iface.common.leaseTTL)\n\t\tiface.respondOffer(ctx, req, fd, lease)\n\n\t\treturn\n\t}\n\n\tlease, err := iface.allocateLease(ctx, mac)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"allocating a lease\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\t// Send DHCPOFFER with new lease.\n\tiface.respondOffer(ctx, req, fd, lease)\n}\n\n// handleSelecting handles messages of type DHCPREQUEST in SELECTING state.  req\n// must be a DHCPREQUEST message, reqIP must be a valid IPv4 address, fd must\n// not be nil.\nfunc (iface *dhcpInterfaceV4) handleSelecting(\n\tctx context.Context,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n\treqIP netip.Addr,\n) {\n\tl := iface.common.logger\n\n\tciaddr, ok := netip.AddrFromSlice(req.ClientIP)\n\tif ok && !ciaddr.IsUnspecified() {\n\t\tl.DebugContext(ctx, \"non-zero ciaddr in selecting request\", \"ciaddr\", ciaddr)\n\n\t\treturn\n\t}\n\n\tmac := req.ClientHWAddr\n\tmk := macToKey(mac)\n\n\tiface.common.indexMu.Lock()\n\tdefer iface.common.indexMu.Unlock()\n\n\tlease, hasLease := iface.common.leases[mk]\n\tif !hasLease {\n\t\tl.DebugContext(ctx, \"no reserved lease\", \"clienthwaddr\", mac)\n\t\tiface.respondNAK(ctx, req, fd)\n\n\t\treturn\n\t}\n\n\tif lease.IP != reqIP {\n\t\tl.DebugContext(ctx, \"selecting request mismatched\", \"requested\", reqIP, \"lease\", lease.IP)\n\t\tiface.respondNAK(ctx, req, fd)\n\n\t\treturn\n\t}\n\n\t// Commit the lease and send ACK.\n\tlease.Hostname = hostname4(req)\n\terr := iface.updateLease(ctx, lease)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"selecting request failed\", slogutil.KeyError, err)\n\t\tiface.respondNAK(ctx, req, fd)\n\n\t\treturn\n\t}\n\n\tiface.respondACK(ctx, req, fd, lease)\n}\n\n// handleInitReboot handles messages of type DHCPREQUEST in INIT-REBOOT state.\n// req must be a DHCPREQUEST message, reqIP must be a valid IPv4 address, fd\n// must not be nil.\nfunc (iface *dhcpInterfaceV4) handleInitReboot(\n\tctx context.Context,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n\treqIP netip.Addr,\n) {\n\tl := iface.common.logger\n\n\t// ciaddr must be zero.  The client is seeking to verify a previously\n\t// allocated, cached configuration.\n\tciaddr, _ := netip.AddrFromSlice(req.ClientIP)\n\tif ciaddr.IsValid() && !ciaddr.IsUnspecified() {\n\t\tl.DebugContext(ctx, \"unexpected ciaddr in init-reboot request\", \"ciaddr\", ciaddr)\n\n\t\treturn\n\t}\n\n\t// Check if the lease exists and matches.\n\tmac := req.ClientHWAddr\n\tmk := macToKey(mac)\n\n\tiface.common.indexMu.Lock()\n\tdefer iface.common.indexMu.Unlock()\n\n\tlease, hasLease := iface.common.leases[mk]\n\tif !hasLease {\n\t\t// If the DHCP server has no record of this client, then it MUST remain\n\t\t// silent, and MAY output a warning to the network administrator.\n\t\tl.WarnContext(ctx, \"no existing lease\", \"mac\", mac)\n\n\t\treturn\n\t}\n\n\tif lease.IP != reqIP {\n\t\tl.WarnContext(ctx, \"init-reboot request mismatched\", \"requested\", reqIP, \"lease\", lease.IP)\n\t\tiface.respondNAK(ctx, req, fd)\n\n\t\treturn\n\t}\n\n\tiface.updateAndRespond(ctx, l, req, lease, fd)\n}\n\n// handleRenew handles messages of type DHCPREQUEST in RENEWING or REBINDING\n// state.  req must be a DHCPREQUEST message, ip should be a previously leased\n// address, fd must not be nil.\nfunc (iface *dhcpInterfaceV4) handleRenew(\n\tctx context.Context,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n\tip netip.Addr,\n) {\n\tl := iface.common.logger\n\n\t// Check if the lease exists and matches.\n\tmac := req.ClientHWAddr\n\tmk := macToKey(mac)\n\n\tiface.common.indexMu.Lock()\n\tdefer iface.common.indexMu.Unlock()\n\n\tlease, hasLease := iface.common.leases[mk]\n\tif !hasLease {\n\t\t// If the DHCP server has no record of this client, then it MUST remain\n\t\t// silent, and MAY output a warning to the network administrator.\n\t\tl.InfoContext(ctx, \"no existing lease\", \"mac\", mac)\n\n\t\t// TODO(e.burkov):  Investigate if we should respond with NAK.\n\t\treturn\n\t}\n\n\tif lease.IP != ip {\n\t\tl.DebugContext(ctx, \"renew request mismatched\", \"ciaddr\", ip, \"lease\", lease.IP)\n\t\tiface.respondNAK(ctx, req, fd)\n\n\t\treturn\n\t}\n\n\tiface.updateAndRespond(ctx, l, req, lease, fd)\n}\n\n// handleDecline handles messages of type DHCPDECLINE.  req must be a\n// DHCPDECLINE message.\n//\n// TODO(e.burkov):  Log the message option, as the request should include one.\n//\n// TODO(e.burkov):  Consider DRY'ing this with [dhcpInterfaceV4.handleRelease].\nfunc (iface *dhcpInterfaceV4) handleDecline(ctx context.Context, req *layers.DHCPv4) {\n\tl := iface.common.logger\n\n\treqIP, hasReqIP := requestedIPv4(req)\n\tif !hasReqIP {\n\t\tl.DebugContext(ctx, \"skipping decline message without requested ip\")\n\n\t\treturn\n\t}\n\n\tif !iface.subnet.Contains(reqIP) {\n\t\tl.DebugContext(ctx, \"skipping decline message\", \"requestedip\", reqIP)\n\n\t\treturn\n\t}\n\n\t// Check if the lease exists and matches.\n\tmac := req.ClientHWAddr\n\tmk := macToKey(mac)\n\n\tiface.common.indexMu.Lock()\n\tdefer iface.common.indexMu.Unlock()\n\n\tlease, hasLease := iface.common.leases[mk]\n\tif !hasLease {\n\t\tl.ErrorContext(ctx, \"decline message for non-existing lease\", \"mac\", mac)\n\n\t\treturn\n\t}\n\n\tif lease.IP != reqIP {\n\t\tl.ErrorContext(ctx, \"decline mismatch\", \"ip\", reqIP, \"lease\", lease.IP)\n\n\t\treturn\n\t}\n\n\tl.WarnContext(ctx, \"lease reported to be unavailable\", \"ip\", lease.IP)\n\n\terr := iface.common.blockLease(ctx, lease, iface.clock)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"blocking lease\", slogutil.KeyError, err)\n\t}\n}\n\n// handleRelease handles messages of type DHCPRELEASE.  req must be a\n// DHCPRELEASE message.\nfunc (iface *dhcpInterfaceV4) handleRelease(ctx context.Context, req *layers.DHCPv4) {\n\tl := iface.common.logger\n\n\tip, _ := netip.AddrFromSlice(req.ClientIP.To4())\n\tif !iface.subnet.Contains(ip) {\n\t\tl.DebugContext(ctx, \"skipping release message\", \"clientip\", ip)\n\n\t\treturn\n\t}\n\n\t// Check if the lease exists and matches.\n\tmac := req.ClientHWAddr\n\tmk := macToKey(mac)\n\n\tiface.common.indexMu.Lock()\n\tdefer iface.common.indexMu.Unlock()\n\n\tlease, hasLease := iface.common.leases[mk]\n\tif !hasLease {\n\t\tl.WarnContext(ctx, \"release message for non-existing lease\", \"mac\", mac)\n\n\t\treturn\n\t}\n\n\tif lease.IP != ip {\n\t\tl.WarnContext(ctx, \"release mismatch\", \"ip\", ip, \"lease\", lease.IP)\n\n\t\treturn\n\t}\n\n\terr := iface.common.index.remove(ctx, l, lease, iface.common)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"removing lease\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpsvc/handler4_test.go",
    "content": "package dhcpsvc_test\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/servicetest\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testIPv4InterfacesConf is the test interfaces configuration for the DHCPv4\n// part of the [DHCPServer].\nvar testIPv4InterfacesConf = map[string]*dhcpsvc.InterfaceConfig{\n\ttestIfaceName: {\n\t\tIPv4: testIPv4Conf,\n\t\tIPv6: disabledIPv6Config,\n\t},\n}\n\nfunc TestDHCPServer_ServeEther4_discover(t *testing.T) {\n\tt.Parallel()\n\n\t// NOTE: Keep in sync with testdata.\n\tconst (\n\t\t// leaseHostnameStatic is the hostname for the static lease.\n\t\tleaseHostnameStatic = \"static4\"\n\n\t\t// leaseHostnameDynamic is the hostname for the dynamic lease.\n\t\tleaseHostnameDynamic = \"dynamic4\"\n\n\t\t// leaseHostnameExpired is the hostname for the expired lease.\n\t\tleaseHostnameExpired = \"expired4\"\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\t// hwAddrUnknown is the MAC address for an unknown client.\n\t\thwAddrUnknown = net.HardwareAddr{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}\n\n\t\t// hwAddrStatic is the MAC address for a known static lease.\n\t\thwAddrStatic = net.HardwareAddr{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}\n\n\t\t// hwAddrDynamic is the MAC address for a known dynamic lease.\n\t\thwAddrDynamic = net.HardwareAddr{0x2, 0x3, 0x4, 0x5, 0x6, 0x7}\n\n\t\t// hwAddrExpired is the MAC address for a known expired lease.\n\t\thwAddrExpired = net.HardwareAddr{0x3, 0x4, 0x5, 0x6, 0x7, 0x8}\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tdynamicLeaseExpiry := time.Date(2025, 1, 1, 10, 1, 1, 0, time.UTC)\n\tdynamicLeaseTTL := dynamicLeaseExpiry.Sub(testCurrentTime)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tin       gopacket.Packet\n\t\twantOpts layers.DHCPOptions\n\t}{{\n\t\tname: \"new\",\n\t\tin:   newDHCPDISCOVER(t, hwAddrUnknown),\n\t\twantOpts: layers.DHCPOptions{\n\t\t\tnewOptMessageType(t, layers.DHCPMsgTypeOffer),\n\t\t\tnewOptServerID(t, testIfaceAddr),\n\t\t\tnewOptLeaseTime(t, testLeaseTTL),\n\t\t},\n\t}, {\n\t\tname: \"existing_static\",\n\t\tin:   newDHCPDISCOVER(t, hwAddrStatic),\n\t\twantOpts: layers.DHCPOptions{\n\t\t\tnewOptMessageType(t, layers.DHCPMsgTypeOffer),\n\t\t\tnewOptServerID(t, testIfaceAddr),\n\t\t\tnewOptLeaseTime(t, testLeaseTTL),\n\t\t\tnewOptHostname(t, leaseHostnameStatic),\n\t\t},\n\t}, {\n\t\tname: \"existing_dynamic\",\n\t\tin:   newDHCPDISCOVER(t, hwAddrDynamic),\n\t\twantOpts: layers.DHCPOptions{\n\t\t\tnewOptMessageType(t, layers.DHCPMsgTypeOffer),\n\t\t\tnewOptServerID(t, testIfaceAddr),\n\t\t\tnewOptLeaseTime(t, dynamicLeaseTTL),\n\t\t\tnewOptHostname(t, leaseHostnameDynamic),\n\t\t},\n\t}, {\n\t\tname: \"existing_dynamic_expired\",\n\t\tin:   newDHCPDISCOVER(t, hwAddrExpired),\n\t\twantOpts: layers.DHCPOptions{\n\t\t\tnewOptMessageType(t, layers.DHCPMsgTypeOffer),\n\t\t\tnewOptServerID(t, testIfaceAddr),\n\t\t\tnewOptLeaseTime(t, testLeaseTTL),\n\t\t\tnewOptHostname(t, leaseHostnameExpired),\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\treq := testutil.RequireTypeAssert[*layers.DHCPv4](t, tc.in.Layer(layers.LayerTypeDHCPv4))\n\n\t\tndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)\n\t\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\t\tInterfaces:           testIPv4InterfacesConf,\n\t\t\tNetworkDeviceManager: ndMgr,\n\t\t\tDBFilePath:           newTempDB(t),\n\t\t\tEnabled:              true,\n\t\t})\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tservicetest.RequireRun(t, srv, testTimeout)\n\n\t\t\ttestutil.RequireSend(t, inCh, tc.in, testTimeout)\n\n\t\t\trespData, ok := testutil.RequireReceive(t, outCh, testTimeout)\n\t\t\trequire.True(t, ok)\n\n\t\t\tassertValidOffer(t, req, respData, tc.wantOpts)\n\t\t})\n\t}\n}\n\nfunc TestDHCPServer_ServeEther4_discoverExpired(t *testing.T) {\n\tt.Parallel()\n\n\t// hwAddrUnknown is the MAC address for an unknown client, not related to\n\t// any existing lease.\n\t//\n\t// NOTE: Keep in sync with testdata.\n\thwAddrUnknown := net.HardwareAddr{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}\n\n\tpkt := newDHCPDISCOVER(t, hwAddrUnknown)\n\treq := testutil.RequireTypeAssert[*layers.DHCPv4](t, pkt.Layer(layers.LayerTypeDHCPv4))\n\n\tndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)\n\n\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\tInterfaces:           testIPv4InterfacesConf,\n\t\tNetworkDeviceManager: ndMgr,\n\t\tDBFilePath:           newTempDB(t),\n\t\tEnabled:              true,\n\t})\n\tservicetest.RequireRun(t, srv, testTimeout)\n\n\ttestutil.RequireSend(t, inCh, pkt, testTimeout)\n\n\trespData, ok := testutil.RequireReceive(t, outCh, testTimeout)\n\trequire.True(t, ok)\n\n\tassertValidOffer(t, req, respData, layers.DHCPOptions{\n\t\tnewOptMessageType(t, layers.DHCPMsgTypeOffer),\n\t\tnewOptServerID(t, testIfaceAddr),\n\t\tnewOptLeaseTime(t, testLeaseTTL),\n\t})\n}\n\nfunc TestDHCPServer_ServeEther4_release(t *testing.T) {\n\tt.Parallel()\n\n\t// NOTE: Keep in sync with testdata.\n\tleaseExpiry := time.Date(2025, 1, 1, 10, 1, 1, 0, time.UTC)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\t// hwAddrSuccess is the MAC address for a lease to be released\n\t\t// successfully.\n\t\thwAddrSuccess = net.HardwareAddr{0x02, 0x03, 0x04, 0x05, 0x06, 0x07}\n\n\t\t// ipSuccess matches the lease IP.\n\t\tipSuccess = netip.MustParseAddr(\"192.0.2.102\")\n\n\t\t// ipMismatch is the IP of the lease used in the mismatch cases.\n\t\tipMismatch = netip.MustParseAddr(\"192.0.2.103\")\n\n\t\t// hwAddrMismatch is the MAC address for a lease with mismatched IP.\n\t\thwAddrMismatch = net.HardwareAddr{0x03, 0x04, 0x05, 0x06, 0x07, 0x08}\n\n\t\t// ipMismatchReq is the IP requested for release, which differs from the\n\t\t// lease IP.\n\t\tipMismatchReq = netip.MustParseAddr(\"192.0.2.104\")\n\n\t\t// hwAddrUnknown is an unknown MAC.\n\t\thwAddrUnknown = net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}\n\t)\n\n\tanotherSubnetAddr := netip.MustParseAddr(testAnotherGatewayIPv4Str)\n\n\tvar (\n\t\tleaseSuccess = &dhcpsvc.Lease{\n\t\t\tExpiry:   leaseExpiry,\n\t\t\tIP:       ipSuccess,\n\t\t\tHostname: \"success\",\n\t\t\tHWAddr:   hwAddrSuccess,\n\t\t\tIsStatic: false,\n\t\t}\n\t\tleaseMismatch = &dhcpsvc.Lease{\n\t\t\tExpiry:   leaseExpiry,\n\t\t\tIP:       ipMismatch,\n\t\t\tHostname: \"mismatch\",\n\t\t\tHWAddr:   hwAddrMismatch,\n\t\t\tIsStatic: false,\n\t\t}\n\t)\n\n\ttestCases := []struct {\n\t\tname       string\n\t\treq        gopacket.Packet\n\t\twantLeases []*dhcpsvc.Lease\n\t}{{\n\t\tname: \"success\",\n\t\treq:  newDHCPRELEASE(t, hwAddrSuccess, ipSuccess, testIfaceHWAddr, testIfaceAddr),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseMismatch,\n\t\t},\n\t}, {\n\t\tname: \"not_found\",\n\t\treq:  newDHCPRELEASE(t, hwAddrUnknown, ipSuccess, testIfaceHWAddr, testIfaceAddr),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseSuccess,\n\t\t\tleaseMismatch,\n\t\t},\n\t}, {\n\t\tname: \"mismatch_ip\",\n\t\treq:  newDHCPRELEASE(t, hwAddrMismatch, ipMismatchReq, testIfaceHWAddr, testIfaceAddr),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseSuccess,\n\t\t\tleaseMismatch,\n\t\t},\n\t}, {\n\t\tname: \"bad_subnet\",\n\t\treq:  newDHCPRELEASE(t, hwAddrSuccess, anotherSubnetAddr, testIfaceHWAddr, testIfaceAddr),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseSuccess,\n\t\t\tleaseMismatch,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tndMgr, inCh, _ := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)\n\t\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\t\tInterfaces:           testIPv4InterfacesConf,\n\t\t\tNetworkDeviceManager: ndMgr,\n\t\t\tDBFilePath:           newTempDB(t),\n\t\t\tEnabled:              true,\n\t\t})\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tservicetest.RequireRun(t, srv, testTimeout)\n\n\t\t\ttestutil.RequireSend(t, inCh, tc.req, testTimeout)\n\n\t\t\t// TODO(e.burkov):  Improve the test to ensure that the DHCPDISCOVER\n\t\t\t// actually receives the released address.\n\t\t\tassert.EventuallyWithT(t, func(ct *assert.CollectT) {\n\t\t\t\tassert.Equal(ct, tc.wantLeases, srv.Leases())\n\t\t\t}, testTimeout/2, testTimeout/20)\n\t\t})\n\t}\n}\n\nfunc TestDHCPServer_ServeEther4_requestSelecting(t *testing.T) {\n\tt.Parallel()\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\thwAddrUnknown = net.HardwareAddr{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}\n\t\thwAddrStatic  = net.HardwareAddr{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}\n\n\t\tipStatic = netip.MustParseAddr(\"192.0.2.101\")\n\t\tipWrong  = netip.MustParseAddr(\"192.0.2.200\")\n\n\t\tipOtherSubnet = netip.MustParseAddr(testAnotherGatewayIPv4Str)\n\t)\n\n\ttestCases := []struct {\n\t\tdiscover     gopacket.Packet\n\t\tconf         *dhcpRequestConfig\n\t\tname         string\n\t\twantOpts     layers.DHCPOptions\n\t\twantResponse layers.DHCPMsgType\n\t}{{\n\t\tdiscover: newDHCPDISCOVER(t, hwAddrUnknown),\n\t\tconf: &dhcpRequestConfig{\n\t\t\trequestedIP:  testIPv4Conf.RangeStart,\n\t\t\tclientHWAddr: hwAddrUnknown,\n\t\t\tserverID:     testIfaceAddr,\n\t\t},\n\t\tname: \"success\",\n\t\twantOpts: layers.DHCPOptions{\n\t\t\tnewOptMessageType(t, layers.DHCPMsgTypeAck),\n\t\t\tnewOptServerID(t, testIfaceAddr),\n\t\t\tnewOptLeaseTime(t, testLeaseTTL),\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeAck,\n\t}, {\n\t\tdiscover: newDHCPDISCOVER(t, hwAddrStatic),\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\tserverID:     ipOtherSubnet,\n\t\t\trequestedIP:  ipStatic,\n\t\t},\n\t\tname:         \"wrong_server_id\",\n\t\twantOpts:     nil,\n\t\twantResponse: layers.DHCPMsgTypeUnspecified,\n\t}, {\n\t\tdiscover: nil,\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrUnknown,\n\t\t\tserverID:     testIfaceAddr,\n\t\t\trequestedIP:  ipWrong,\n\t\t},\n\t\tname:         \"no_lease\",\n\t\twantOpts:     nil,\n\t\twantResponse: layers.DHCPMsgTypeNak,\n\t}, {\n\t\tdiscover: newDHCPDISCOVER(t, hwAddrStatic),\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\tserverID:     testIfaceAddr,\n\t\t\trequestedIP:  ipWrong,\n\t\t},\n\t\tname:         \"wrong_ip\",\n\t\twantOpts:     nil,\n\t\twantResponse: layers.DHCPMsgTypeNak,\n\t}, {\n\t\tdiscover: newDHCPDISCOVER(t, hwAddrStatic),\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\tserverID:     testIfaceAddr,\n\t\t\trequestedIP:  ipStatic,\n\t\t\tclientIP:     ipStatic,\n\t\t},\n\t\tname:         \"nonzero_ciaddr\",\n\t\twantOpts:     nil,\n\t\twantResponse: layers.DHCPMsgTypeUnspecified,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)\n\t\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\t\tLogger:               slogutil.NewDiscardLogger(),\n\t\t\tInterfaces:           testIPv4InterfacesConf,\n\t\t\tNetworkDeviceManager: ndMgr,\n\t\t\tDBFilePath:           newTempDB(t),\n\t\t\tEnabled:              true,\n\t\t})\n\n\t\tpkt := newDHCPREQUEST(t, tc.conf)\n\t\treq := testutil.RequireTypeAssert[*layers.DHCPv4](t, pkt.Layer(layers.LayerTypeDHCPv4))\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tservicetest.RequireRun(t, srv, testTimeout)\n\n\t\t\tif tc.discover != nil {\n\t\t\t\ttestutil.RequireSend(t, inCh, tc.discover, testTimeout)\n\n\t\t\t\t_, ok := testutil.RequireReceive(t, outCh, testTimeout)\n\t\t\t\trequire.True(t, ok)\n\t\t\t}\n\n\t\t\ttestutil.RequireSend(t, inCh, pkt, testTimeout)\n\n\t\t\tswitch tc.wantResponse {\n\t\t\tcase layers.DHCPMsgTypeUnspecified:\n\t\t\t\tassertNoResponse(t, outCh, testTimeout/10)\n\t\t\tcase layers.DHCPMsgTypeAck:\n\t\t\t\tassertValidACK(t, req, outCh, tc.wantOpts)\n\t\t\tcase layers.DHCPMsgTypeNak:\n\t\t\t\tassertValidNAK(t, req, outCh, testIPv4Conf.GatewayIP)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDHCPServer_ServeEther4_requestInitReboot(t *testing.T) {\n\tt.Parallel()\n\n\t// NOTE: Keep in sync with testdata.\n\tconst (\n\t\tleaseHostnameStatic = \"static4\"\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\thwAddrUnknown = net.HardwareAddr{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}\n\t\thwAddrStatic  = net.HardwareAddr{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}\n\n\t\tipStatic  = netip.MustParseAddr(\"192.0.2.101\")\n\t\tipDynamic = netip.MustParseAddr(\"192.0.2.102\")\n\n\t\tipOtherSubnet = netip.MustParseAddr(testAnotherGatewayIPv4Str)\n\t)\n\n\ttestCases := []struct {\n\t\tconf         *dhcpRequestConfig\n\t\tname         string\n\t\twantOpts     layers.DHCPOptions\n\t\twantResponse layers.DHCPMsgType\n\t}{{\n\t\tname: \"success\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\trequestedIP:  ipStatic,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeAck,\n\t\twantOpts: layers.DHCPOptions{\n\t\t\tnewOptMessageType(t, layers.DHCPMsgTypeAck),\n\t\t\tnewOptServerID(t, testIfaceAddr),\n\t\t\tnewOptLeaseTime(t, testLeaseTTL),\n\t\t\tnewOptHostname(t, leaseHostnameStatic),\n\t\t},\n\t}, {\n\t\tname: \"wrong_subnet\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\trequestedIP:  ipOtherSubnet,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeNak,\n\t}, {\n\t\tname: \"no_lease\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrUnknown,\n\t\t\trequestedIP:  ipStatic,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeUnspecified,\n\t}, {\n\t\tname: \"wrong_ip\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\trequestedIP:  ipDynamic,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeNak,\n\t}, {\n\t\tname: \"nonzero_ciaddr\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\trequestedIP:  ipStatic,\n\t\t\tclientIP:     ipStatic,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeUnspecified,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)\n\t\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\t\tInterfaces:           testIPv4InterfacesConf,\n\t\t\tNetworkDeviceManager: ndMgr,\n\t\t\tDBFilePath:           newTempDB(t),\n\t\t\tEnabled:              true,\n\t\t})\n\n\t\tpkt := newDHCPREQUEST(t, tc.conf)\n\t\treq := testutil.RequireTypeAssert[*layers.DHCPv4](t, pkt.Layer(layers.LayerTypeDHCPv4))\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tservicetest.RequireRun(t, srv, testTimeout)\n\n\t\t\ttestutil.RequireSend(t, inCh, pkt, testTimeout)\n\n\t\t\tswitch tc.wantResponse {\n\t\t\tcase layers.DHCPMsgTypeUnspecified:\n\t\t\t\tassertNoResponse(t, outCh, testTimeout/10)\n\t\t\tcase layers.DHCPMsgTypeAck:\n\t\t\t\tassertValidACK(t, req, outCh, tc.wantOpts)\n\t\t\tcase layers.DHCPMsgTypeNak:\n\t\t\t\tassertValidNAK(t, req, outCh, testIPv4Conf.GatewayIP)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDHCPServer_ServeEther4_requestRenew(t *testing.T) {\n\tt.Parallel()\n\n\t// NOTE: Keep in sync with testdata.\n\tconst (\n\t\tleaseHostnameStatic = \"static4\"\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\thwAddrUnknown = net.HardwareAddr{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}\n\t\thwAddrStatic  = net.HardwareAddr{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}\n\t\thwAddrDynamic = net.HardwareAddr{0x2, 0x3, 0x4, 0x5, 0x6, 0x7}\n\n\t\tipStatic  = netip.MustParseAddr(\"192.0.2.101\")\n\t\tipDynamic = netip.MustParseAddr(\"192.0.2.102\")\n\n\t\tipOtherSubnet = netip.MustParseAddr(testAnotherGatewayIPv4Str)\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tdynamicLeaseExpiry := time.Date(2025, 1, 1, 10, 1, 1, 0, time.UTC)\n\tdynamicLeaseTTL := dynamicLeaseExpiry.Sub(testCurrentTime)\n\n\ttestCases := []struct {\n\t\tconf         *dhcpRequestConfig\n\t\tname         string\n\t\twantOpts     layers.DHCPOptions\n\t\twantResponse layers.DHCPMsgType\n\t}{{\n\t\tname: \"success\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrDynamic,\n\t\t\tclientIP:     ipDynamic,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeAck,\n\t\twantOpts: layers.DHCPOptions{\n\t\t\tnewOptMessageType(t, layers.DHCPMsgTypeAck),\n\t\t\tnewOptServerID(t, testIfaceAddr),\n\t\t\tnewOptLeaseTime(t, dynamicLeaseTTL),\n\t\t\tnewOptHostname(t, \"dynamic4\"),\n\t\t},\n\t}, {\n\t\tname: \"static\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\tclientIP:     ipStatic,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeAck,\n\t\twantOpts: layers.DHCPOptions{\n\t\t\tnewOptMessageType(t, layers.DHCPMsgTypeAck),\n\t\t\tnewOptServerID(t, testIfaceAddr),\n\t\t\tnewOptLeaseTime(t, testLeaseTTL),\n\t\t\tnewOptHostname(t, leaseHostnameStatic),\n\t\t},\n\t}, {\n\t\tname: \"wrong_subnet\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\tclientIP:     ipOtherSubnet,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeUnspecified,\n\t}, {\n\t\tname: \"no_lease\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrUnknown,\n\t\t\tclientIP:     ipStatic,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeUnspecified,\n\t}, {\n\t\tname: \"wrong_ip\",\n\t\tconf: &dhcpRequestConfig{\n\t\t\tclientHWAddr: hwAddrStatic,\n\t\t\tclientIP:     ipDynamic,\n\t\t},\n\t\twantResponse: layers.DHCPMsgTypeNak,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tndMgr, inCh, outCh := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)\n\t\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\t\tInterfaces:           testIPv4InterfacesConf,\n\t\t\tNetworkDeviceManager: ndMgr,\n\t\t\tDBFilePath:           newTempDB(t),\n\t\t\tEnabled:              true,\n\t\t})\n\n\t\tpkt := newDHCPREQUEST(t, tc.conf)\n\t\treq := testutil.RequireTypeAssert[*layers.DHCPv4](t, pkt.Layer(layers.LayerTypeDHCPv4))\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tservicetest.RequireRun(t, srv, testTimeout)\n\n\t\t\ttestutil.RequireSend(t, inCh, pkt, testTimeout)\n\n\t\t\tswitch tc.wantResponse {\n\t\t\tcase layers.DHCPMsgTypeUnspecified:\n\t\t\t\tassertNoResponse(t, outCh, testTimeout/10)\n\t\t\tcase layers.DHCPMsgTypeAck:\n\t\t\t\tassertValidACK(t, req, outCh, tc.wantOpts)\n\t\t\tcase layers.DHCPMsgTypeNak:\n\t\t\t\tassertValidNAK(t, req, outCh, testIPv4Conf.GatewayIP)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDHCPServer_ServeEther4_decline(t *testing.T) {\n\tt.Parallel()\n\n\t// NOTE: Keep in sync with testdata.\n\tleaseExpiry := time.Date(2025, 1, 1, 10, 1, 1, 0, time.UTC)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\t// hwAddrSuccess is the MAC address for a lease to be declined\n\t\t// successfully.\n\t\thwAddrSuccess = net.HardwareAddr{0x02, 0x03, 0x04, 0x05, 0x06, 0x07}\n\n\t\t// ipSuccess matches the lease IP.\n\t\tipSuccess = netip.MustParseAddr(\"192.0.2.102\")\n\n\t\t// ipMismatch is the IP of the lease used in the mismatch cases.\n\t\tipMismatch = netip.MustParseAddr(\"192.0.2.103\")\n\n\t\t// hwAddrMismatch is the MAC address for a lease with mismatched IP.\n\t\thwAddrMismatch = net.HardwareAddr{0x03, 0x04, 0x05, 0x06, 0x07, 0x08}\n\n\t\t// ipMismatchReq is the IP requested for decline, which differs from\n\t\t// the lease IP.\n\t\tipMismatchReq = netip.MustParseAddr(\"192.0.2.104\")\n\n\t\t// hwAddrUnknown is an unknown MAC.\n\t\thwAddrUnknown = net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}\n\t)\n\n\tanotherSubnetAddr := netip.MustParseAddr(testAnotherGatewayIPv4Str)\n\n\tvar (\n\t\tleaseSuccess = &dhcpsvc.Lease{\n\t\t\tExpiry:   leaseExpiry,\n\t\t\tIP:       ipSuccess,\n\t\t\tHostname: \"success\",\n\t\t\tHWAddr:   hwAddrSuccess,\n\t\t\tIsStatic: false,\n\t\t}\n\t\tleaseMismatch = &dhcpsvc.Lease{\n\t\t\tExpiry:   leaseExpiry,\n\t\t\tIP:       ipMismatch,\n\t\t\tHostname: \"mismatch\",\n\t\t\tHWAddr:   hwAddrMismatch,\n\t\t\tIsStatic: false,\n\t\t}\n\t)\n\n\ttestCases := []struct {\n\t\tname       string\n\t\treq        gopacket.Packet\n\t\twantLeases []*dhcpsvc.Lease\n\t}{{\n\t\tname: \"success\",\n\t\treq:  newDHCPDECLINE(t, hwAddrSuccess, ipSuccess),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseMismatch,\n\t\t},\n\t}, {\n\t\tname: \"not_found\",\n\t\treq:  newDHCPDECLINE(t, hwAddrUnknown, ipSuccess),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseSuccess,\n\t\t\tleaseMismatch,\n\t\t},\n\t}, {\n\t\tname: \"mismatch_ip\",\n\t\treq:  newDHCPDECLINE(t, hwAddrMismatch, ipMismatchReq),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseSuccess,\n\t\t\tleaseMismatch,\n\t\t},\n\t}, {\n\t\tname: \"bad_subnet\",\n\t\treq:  newDHCPDECLINE(t, hwAddrSuccess, anotherSubnetAddr),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseSuccess,\n\t\t\tleaseMismatch,\n\t\t},\n\t}, {\n\t\tname: \"no_requested_ip\",\n\t\treq:  newDHCPDECLINE(t, hwAddrSuccess, netip.Addr{}),\n\t\twantLeases: []*dhcpsvc.Lease{\n\t\t\tleaseSuccess,\n\t\t\tleaseMismatch,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tndMgr, inCh, _ := newTestNetworkDeviceManager(t, testIfaceName, testIfaceAddr)\n\t\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\t\tInterfaces:           testIPv4InterfacesConf,\n\t\t\tNetworkDeviceManager: ndMgr,\n\t\t\tDBFilePath:           newTempDB(t),\n\t\t\tEnabled:              true,\n\t\t})\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tservicetest.RequireRun(t, srv, testTimeout)\n\n\t\t\ttestutil.RequireSend(t, inCh, tc.req, testTimeout)\n\n\t\t\tassert.EventuallyWithT(t, func(ct *assert.CollectT) {\n\t\t\t\tassert.Equal(ct, tc.wantLeases, srv.Leases())\n\t\t\t}, testTimeout/2, testTimeout/20)\n\t\t})\n\t}\n}\n\n// TODO(e.burkov):  Add tests for wrong packets.\n\n// dhcpRequestConfig contains the configuration for creating a DHCPREQUEST\n// packet.\ntype dhcpRequestConfig struct {\n\t// serverID is the server identifier option value.  If zero, the option is\n\t// not included.\n\tserverID netip.Addr\n\n\t// requestedIP is the requested IP address option value.  If zero, the\n\t// option is not included.\n\trequestedIP netip.Addr\n\n\t// clientIP is the ciaddr field value.  If zero, it's set to 0.0.0.0.\n\tclientIP netip.Addr\n\n\t// hostname is the hostname option value.  If empty, the option is not\n\t// included.\n\thostname string\n\n\t// clientHWAddr is the MAC address of the client.\n\tclientHWAddr net.HardwareAddr\n}\n\n// newDHCPREQUEST creates a new DHCPREQUEST packet for testing.\nfunc newDHCPREQUEST(tb testing.TB, conf *dhcpRequestConfig) (pkt gopacket.Packet) {\n\ttb.Helper()\n\n\teth := &layers.Ethernet{\n\t\tSrcMAC:       conf.clientHWAddr,\n\t\tDstMAC:       net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},\n\t\tEthernetType: layers.EthernetTypeIPv4,\n\t}\n\n\tsrcIP := net.IPv4zero.To4()\n\tif conf.clientIP.IsValid() {\n\t\tsrcIP = conf.clientIP.AsSlice()\n\t}\n\n\tip := &layers.IPv4{\n\t\tVersion:  4,\n\t\tTTL:      dhcpsvc.IPv4DefaultTTL,\n\t\tSrcIP:    srcIP,\n\t\tDstIP:    net.IPv4bcast.To4(),\n\t\tProtocol: layers.IPProtocolUDP,\n\t}\n\tudp := &layers.UDP{\n\t\tSrcPort: dhcpsvc.ClientPortV4,\n\t\tDstPort: dhcpsvc.ServerPortV4,\n\t}\n\t_ = udp.SetNetworkLayerForChecksum(ip)\n\n\topts := layers.DHCPOptions{\n\t\tlayers.NewDHCPOption(\n\t\t\tlayers.DHCPOptMessageType,\n\t\t\t[]byte{byte(layers.DHCPMsgTypeRequest)},\n\t\t),\n\t}\n\n\tif conf.serverID.IsValid() {\n\t\topts = append(opts, layers.NewDHCPOption(\n\t\t\tlayers.DHCPOptServerID,\n\t\t\tconf.serverID.AsSlice(),\n\t\t))\n\t}\n\n\tif conf.requestedIP.IsValid() {\n\t\topts = append(opts, layers.NewDHCPOption(\n\t\t\tlayers.DHCPOptRequestIP,\n\t\t\tconf.requestedIP.AsSlice(),\n\t\t))\n\t}\n\n\tif conf.hostname != \"\" {\n\t\topts = append(opts, layers.NewDHCPOption(\n\t\t\tlayers.DHCPOptHostname,\n\t\t\t[]byte(conf.hostname),\n\t\t))\n\t}\n\n\tciaddr := net.IPv4zero.To4()\n\tif conf.clientIP.IsValid() {\n\t\tciaddr = conf.clientIP.AsSlice()\n\t}\n\n\tdhcp := &layers.DHCPv4{\n\t\tOperation:    layers.DHCPOpRequest,\n\t\tHardwareType: layers.LinkTypeEthernet,\n\t\tHardwareLen:  dhcpsvc.EUI48AddrLen,\n\t\tXid:          testXid,\n\t\tClientIP:     ciaddr,\n\t\tClientHWAddr: conf.clientHWAddr,\n\t\tOptions:      opts,\n\t}\n\n\treturn newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)\n}\n\n// newDHCPDISCOVER creates a new DHCPDISCOVER packet for testing.\n//\n// TODO(e.burkov):  Add parameters.\nfunc newDHCPDISCOVER(tb testing.TB, clientHWAddr net.HardwareAddr) (pkt gopacket.Packet) {\n\ttb.Helper()\n\n\teth := &layers.Ethernet{\n\t\tSrcMAC:       clientHWAddr,\n\t\tDstMAC:       net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},\n\t\tEthernetType: layers.EthernetTypeIPv4,\n\t}\n\tip := &layers.IPv4{\n\t\tVersion:  4,\n\t\tTTL:      dhcpsvc.IPv4DefaultTTL,\n\t\tSrcIP:    net.IPv4zero.To4(),\n\t\tDstIP:    net.IPv4bcast.To4(),\n\t\tProtocol: layers.IPProtocolUDP,\n\t}\n\tudp := &layers.UDP{\n\t\tSrcPort: dhcpsvc.ClientPortV4,\n\t\tDstPort: dhcpsvc.ServerPortV4,\n\t}\n\t_ = udp.SetNetworkLayerForChecksum(ip)\n\n\tdhcp := &layers.DHCPv4{\n\t\tOperation:    layers.DHCPOpRequest,\n\t\tHardwareType: layers.LinkTypeEthernet,\n\t\tHardwareLen:  dhcpsvc.EUI48AddrLen,\n\t\tXid:          testXid,\n\t\tClientHWAddr: clientHWAddr,\n\t\tOptions: layers.DHCPOptions{\n\t\t\tnewOptMessageType(tb, layers.DHCPMsgTypeDiscover),\n\t\t},\n\t}\n\n\treturn newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)\n}\n\n// newDHCPRELEASE creates a new DHCPRELEASE packet for testing.\nfunc newDHCPRELEASE(\n\ttb testing.TB,\n\tclientHWAddr net.HardwareAddr,\n\tclientIP netip.Addr,\n\tserverHWAddr net.HardwareAddr,\n\tserverIP netip.Addr,\n) (pkt gopacket.Packet) {\n\ttb.Helper()\n\n\teth := &layers.Ethernet{\n\t\tSrcMAC:       clientHWAddr,\n\t\tDstMAC:       serverHWAddr,\n\t\tEthernetType: layers.EthernetTypeIPv4,\n\t}\n\tip := &layers.IPv4{\n\t\tVersion:  4,\n\t\tTTL:      dhcpsvc.IPv4DefaultTTL,\n\t\tSrcIP:    clientIP.AsSlice(),\n\t\tDstIP:    serverIP.AsSlice(),\n\t\tProtocol: layers.IPProtocolUDP,\n\t}\n\tudp := &layers.UDP{\n\t\tSrcPort: dhcpsvc.ClientPortV4,\n\t\tDstPort: dhcpsvc.ServerPortV4,\n\t}\n\t_ = udp.SetNetworkLayerForChecksum(ip)\n\n\tdhcp := &layers.DHCPv4{\n\t\tOperation:    layers.DHCPOpRequest,\n\t\tHardwareType: layers.LinkTypeEthernet,\n\t\tHardwareLen:  dhcpsvc.EUI48AddrLen,\n\t\tXid:          testXid,\n\t\tClientHWAddr: clientHWAddr,\n\t\tClientIP:     clientIP.AsSlice(),\n\t\tOptions: layers.DHCPOptions{\n\t\t\tnewOptMessageType(tb, layers.DHCPMsgTypeRelease),\n\t\t},\n\t}\n\n\treturn newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)\n}\n\n// newDHCPDECLINE creates a new DHCPDECLINE packet for testing.\nfunc newDHCPDECLINE(\n\ttb testing.TB,\n\tclientHWAddr net.HardwareAddr,\n\trequestedIP netip.Addr,\n) (pkt gopacket.Packet) {\n\ttb.Helper()\n\n\teth := &layers.Ethernet{\n\t\tSrcMAC:       clientHWAddr,\n\t\tDstMAC:       net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},\n\t\tEthernetType: layers.EthernetTypeIPv4,\n\t}\n\tip := &layers.IPv4{\n\t\tVersion:  4,\n\t\tTTL:      dhcpsvc.IPv4DefaultTTL,\n\t\tSrcIP:    net.IPv4zero.To4(),\n\t\tDstIP:    net.IPv4bcast.To4(),\n\t\tProtocol: layers.IPProtocolUDP,\n\t}\n\tudp := &layers.UDP{\n\t\tSrcPort: dhcpsvc.ClientPortV4,\n\t\tDstPort: dhcpsvc.ServerPortV4,\n\t}\n\t_ = udp.SetNetworkLayerForChecksum(ip)\n\n\topts := layers.DHCPOptions{\n\t\tnewOptMessageType(tb, layers.DHCPMsgTypeDecline),\n\t}\n\n\tif requestedIP.IsValid() {\n\t\topts = append(opts, layers.NewDHCPOption(\n\t\t\tlayers.DHCPOptRequestIP,\n\t\t\trequestedIP.AsSlice(),\n\t\t))\n\t}\n\n\tdhcp := &layers.DHCPv4{\n\t\tOperation:    layers.DHCPOpRequest,\n\t\tHardwareType: layers.LinkTypeEthernet,\n\t\tHardwareLen:  dhcpsvc.EUI48AddrLen,\n\t\tXid:          testXid,\n\t\tClientHWAddr: clientHWAddr,\n\t\tOptions:      opts,\n\t}\n\n\treturn newTestPacket(tb, layers.LinkTypeEthernet, eth, ip, udp, dhcp)\n}\n\n// newTestPacket creates a valid packet from ls using first as first layer\n// decoder.\nfunc newTestPacket(\n\ttb testing.TB,\n\tfirst gopacket.Decoder,\n\tls ...gopacket.SerializableLayer,\n) (pkg gopacket.Packet) {\n\ttb.Helper()\n\n\tbuf := gopacket.NewSerializeBuffer()\n\n\topts := gopacket.SerializeOptions{\n\t\tFixLengths:       true,\n\t\tComputeChecksums: true,\n\t}\n\terr := gopacket.SerializeLayers(buf, opts, ls...)\n\trequire.NoError(tb, err)\n\n\treturn gopacket.NewPacket(buf.Bytes(), first, gopacket.Default)\n}\n\n// requireEthernet requires data to contain an Ethernet layer and all layers\n// from ls.  First of ls must be of type [layers.LayerTypeEthernet].\nfunc requireEthernet(\n\ttb testing.TB,\n\tdata []byte,\n\tls ...gopacket.DecodingLayer,\n) (types []gopacket.LayerType) {\n\ttb.Helper()\n\n\tparser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, ls...)\n\n\terr := parser.DecodeLayers(data, &types)\n\trequire.NoError(tb, err)\n\n\treturn types\n}\n\n// assertValidOffer asserts that respData contains a complete DHCPOFFER response\n// with the expected options, wrapped with all layers down to Ethernet.\nfunc assertValidOffer(\n\ttb testing.TB,\n\tdiscover *layers.DHCPv4,\n\trespData []byte,\n\twantOpts layers.DHCPOptions,\n) {\n\ttb.Helper()\n\n\tresp := &layers.DHCPv4{}\n\ttypes := requireEthernet(tb, respData, &layers.Ethernet{}, &layers.IPv4{}, &layers.UDP{}, resp)\n\trequire.Equal(tb, fullLayersStack, types)\n\n\tassert.Equal(tb, layers.DHCPOpReply, resp.Operation, \"operation\")\n\tassert.Equal(tb, discover.HardwareType, resp.HardwareType, \"hardware type\")\n\tassert.Equal(tb, discover.HardwareLen, resp.HardwareLen, \"hardware length\")\n\tassert.Equal(tb, discover.Xid, resp.Xid, \"xid\")\n\tassert.Equal(tb, discover.ClientHWAddr, resp.ClientHWAddr, \"client hardware address\")\n\tassert.Equal(tb, wantOpts, resp.Options, \"options\")\n}\n\n// assertValidACK asserts that respData contains a complete DHCPACK response\n// with the expected options, wrapped with all layers down to Ethernet.\nfunc assertValidACK(\n\ttb testing.TB,\n\trequest *layers.DHCPv4,\n\toutCh <-chan []byte,\n\twantOpts layers.DHCPOptions,\n) {\n\ttb.Helper()\n\n\trespData, ok := testutil.RequireReceive(tb, outCh, testTimeout)\n\trequire.True(tb, ok)\n\n\tresp := &layers.DHCPv4{}\n\ttypes := requireEthernet(tb, respData, &layers.Ethernet{}, &layers.IPv4{}, &layers.UDP{}, resp)\n\trequire.Equal(tb, fullLayersStack, types)\n\n\tassert.Equal(tb, layers.DHCPOpReply, resp.Operation, \"operation\")\n\tassert.Equal(tb, request.HardwareType, resp.HardwareType, \"hardware type\")\n\tassert.Equal(tb, request.HardwareLen, resp.HardwareLen, \"hardware length\")\n\tassert.Equal(tb, request.Xid, resp.Xid, \"xid\")\n\tassert.Equal(tb, request.ClientHWAddr, resp.ClientHWAddr, \"client hardware address\")\n\tassert.Equal(tb, wantOpts, resp.Options, \"options\")\n}\n\n// assertValidNAK asserts that respData contains a complete DHCPNAK response\n// wrapped with all layers down to Ethernet.\nfunc assertValidNAK(\n\ttb testing.TB,\n\trequest *layers.DHCPv4,\n\toutCh <-chan []byte,\n\tserverIP netip.Addr,\n) {\n\ttb.Helper()\n\n\trespData, ok := testutil.RequireReceive(tb, outCh, testTimeout)\n\trequire.True(tb, ok)\n\n\tresp := &layers.DHCPv4{}\n\ttypes := requireEthernet(tb, respData, &layers.Ethernet{}, &layers.IPv4{}, &layers.UDP{}, resp)\n\trequire.Equal(tb, fullLayersStack, types)\n\n\tassert.Equal(tb, layers.DHCPOpReply, resp.Operation, \"operation\")\n\tassert.Equal(tb, request.HardwareType, resp.HardwareType, \"hardware type\")\n\tassert.Equal(tb, request.HardwareLen, resp.HardwareLen, \"hardware length\")\n\tassert.Equal(tb, request.Xid, resp.Xid, \"xid\")\n\tassert.Equal(tb, request.ClientHWAddr, resp.ClientHWAddr, \"client hardware address\")\n\n\twantOpts := layers.DHCPOptions{\n\t\tnewOptMessageType(tb, layers.DHCPMsgTypeNak),\n\t\tnewOptServerID(tb, serverIP),\n\t}\n\tassert.Equal(tb, wantOpts, resp.Options, \"options\")\n}\n\n// assertNoResponse asserts that no response is received on the channel within\n// the timeout.\nfunc assertNoResponse(tb testing.TB, outCh <-chan []byte, timeout time.Duration) {\n\ttb.Helper()\n\n\tvar resp []byte\n\trequire.Panics(tb, func() {\n\t\tresp, _ = testutil.RequireReceive(testutil.PanicT{}, outCh, timeout)\n\t})\n\n\trequire.Nil(tb, resp)\n}\n"
  },
  {
    "path": "internal/dhcpsvc/handler6.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n)\n\n// serveV6 handles the ethernet packet of IPv6 type. iface and pkt must not be\n// nil.  ctx must contain a [frameData] accessible with [frameDataFromContext].\n//\n//lint:ignore U1000 TODO(e.burkov): Use.\nfunc (srv *DHCPServer) serveV6(\n\tctx context.Context,\n\t_ *dhcpInterfaceV6,\n\tpkt gopacket.Packet,\n) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"serving dhcpv6: %w\") }()\n\n\tmsg, ok := pkt.Layer(layers.LayerTypeDHCPv6).(*layers.DHCPv6)\n\tif !ok {\n\t\t// TODO(e.burkov):  Consider adding some debug information about the\n\t\t// actual received packet.\n\t\tsrv.logger.DebugContext(ctx, \"skipping non-dhcpv6 packet\")\n\n\t\treturn nil\n\t}\n\n\t// TODO(e.burkov):  Handle duplicate TransactionID.\n\n\treturn srv.handleDHCPv6(ctx, msg.MsgType, msg)\n}\n\n// handleDHCPv6 handles the DHCPv6 message of the given type.\nfunc (srv *DHCPServer) handleDHCPv6(\n\t_ context.Context,\n\ttyp layers.DHCPv6MsgType,\n\t_ *layers.DHCPv6,\n) (err error) {\n\tswitch typ {\n\tcase\n\t\tlayers.DHCPv6MsgTypeSolicit,\n\t\tlayers.DHCPv6MsgTypeRequest,\n\t\tlayers.DHCPv6MsgTypeConfirm,\n\t\tlayers.DHCPv6MsgTypeRenew,\n\t\tlayers.DHCPv6MsgTypeRebind,\n\t\tlayers.DHCPv6MsgTypeInformationRequest,\n\t\tlayers.DHCPv6MsgTypeRelease,\n\t\tlayers.DHCPv6MsgTypeDecline:\n\t\t// TODO(e.burkov):  Handle messages.\n\tdefault:\n\t\treturn fmt.Errorf(\"dhcpv6: request type: %w: %v\", errors.ErrBadEnumValue, typ)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dhcpsvc/interface.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// macKey contains hardware address as byte array of 6, 8, or 20 bytes.\n//\n// TODO(e.burkov):  Move to aghnet or even to netutil.\ntype macKey any\n\n// macToKey converts mac into macKey, which is used as the key for the lease\n// maps.  mac must be a valid hardware address of length 6, 8, or 20 bytes, see\n// [netutil.ValidateMAC].\nfunc macToKey(mac net.HardwareAddr) (key macKey) {\n\tswitch len(mac) {\n\tcase 6:\n\t\treturn [6]byte(mac)\n\tcase 8:\n\t\treturn [8]byte(mac)\n\tcase 20:\n\t\treturn [20]byte(mac)\n\tdefault:\n\t\tpanic(fmt.Errorf(\"invalid mac address %#v\", mac))\n\t}\n}\n\n// netInterface is a common part of any interface within the DHCP server.\n//\n// TODO(e.burkov):  Add other methods as [DHCPServer] evolves.\ntype netInterface struct {\n\t// logger logs the events related to the network interface.\n\t//\n\t// TODO(e.burkov):  Consider removing it and using the value from context.\n\tlogger *slog.Logger\n\n\t// indexMu protects the index, leases, and leasedOffsets.\n\tindexMu *sync.RWMutex\n\n\t// leasedOffsets contains offsets from conf.ipRange.start that have been\n\t// leased.\n\tleasedOffsets *bitSet\n\n\t// index stores the DHCP leases for quick lookups.\n\tindex *leaseIndex\n\n\t// leases is the set of DHCP leases assigned to this interface.\n\tleases map[macKey]*Lease\n\n\t// addrSpace is the IPv4 address space allocated for leasing.\n\taddrSpace ipRange\n\n\t// name is the name of the network interface.\n\tname string\n\n\t// leaseTTL is the default Time-To-Live value for leases.\n\tleaseTTL time.Duration\n}\n\n// reset clears all the slices in iface for reuse.\nfunc (iface *netInterface) reset() {\n\tclear(iface.leases)\n}\n\n// addLease inserts the given lease into iface.  It returns an error if the\n// lease can't be inserted.\nfunc (iface *netInterface) addLease(l *Lease) (err error) {\n\tmk := macToKey(l.HWAddr)\n\t_, found := iface.leases[mk]\n\tif found {\n\t\treturn fmt.Errorf(\"lease for mac %s already exists\", l.HWAddr)\n\t}\n\n\tiface.leases[mk] = l\n\n\toff, _ := iface.addrSpace.offset(l.IP)\n\tiface.leasedOffsets.set(off, true)\n\n\treturn nil\n}\n\n// updateLease replaces an existing lease within iface with the given one.  It\n// returns an error if there is no lease with such hardware address.\nfunc (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {\n\tmk := macToKey(l.HWAddr)\n\tprev, found := iface.leases[mk]\n\tif !found {\n\t\treturn nil, fmt.Errorf(\"no lease for mac %s\", l.HWAddr)\n\t}\n\n\tiface.leases[mk] = l\n\n\treturn prev, nil\n}\n\n// removeLease removes an existing lease from iface.  It returns an error if\n// there is no lease equal to l.  l must not be nil.\nfunc (iface *netInterface) removeLease(l *Lease) (err error) {\n\tmk := macToKey(l.HWAddr)\n\t_, found := iface.leases[mk]\n\tif !found {\n\t\treturn fmt.Errorf(\"no lease for mac %s\", l.HWAddr)\n\t}\n\n\tdelete(iface.leases, mk)\n\n\toff, _ := iface.addrSpace.offset(l.IP)\n\tiface.leasedOffsets.set(off, false)\n\n\treturn nil\n}\n\n// blockLease marks l as blocked for a configured TTL, as reported by\n// [Lease.IsBlocked].  indexMu must be locked,  It also removes the lease from\n// iface.  l must not be nil.\nfunc (iface *netInterface) blockLease(\n\tctx context.Context,\n\tl *Lease,\n\tclock timeutil.Clock,\n) (err error) {\n\tl.HWAddr = blockedHardwareAddr\n\tl.Hostname = \"\"\n\tl.Expiry = clock.Now().Add(iface.leaseTTL)\n\tl.IsStatic = false\n\n\terr = iface.index.dbStore(ctx, iface.logger)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"storing index: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// nextIP generates a new free IP.\nfunc (iface *netInterface) nextIP() (ip netip.Addr) {\n\tr := iface.addrSpace\n\tip = r.find(func(next netip.Addr) (ok bool) {\n\t\toffset, ok := r.offset(next)\n\t\tif !ok {\n\t\t\tpanic(fmt.Errorf(\"next: %s: %w\", next, errors.ErrOutOfRange))\n\t\t}\n\n\t\treturn !iface.leasedOffsets.isSet(offset)\n\t})\n\n\treturn ip\n}\n\n// findExpiredLease returns the first found lease that has expired.  indexMu\n// must be locked.\nfunc (iface *netInterface) findExpiredLease(now time.Time) (l *Lease) {\n\tfor _, lease := range iface.leases {\n\t\tif !lease.IsStatic && lease.Expiry.Before(now) {\n\t\t\treturn lease\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dhcpsvc/iprange.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"math\"\n\t\"math/big\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// ipRange is an inclusive range of IP addresses.  A zero range doesn't contain\n// any IP addresses.\n//\n// It is safe for concurrent use.\ntype ipRange struct {\n\tstart netip.Addr\n\tend   netip.Addr\n}\n\n// maxRangeLen is the maximum IP range length.  The bitsets used in servers only\n// accept uints, which can have the size of 32 bit.\n//\n// TODO(a.garipov, e.burkov):  Reconsider the value for IPv6.\nconst maxRangeLen = math.MaxUint32\n\n// newIPRange creates a new IP address range.  start must be less than end.  The\n// resulting range must not be greater than maxRangeLen.\nfunc newIPRange(start, end netip.Addr) (r ipRange, err error) {\n\tdefer func() { err = errors.Annotate(err, \"invalid ip range: %w\") }()\n\n\tswitch false {\n\tcase start.Is4() == end.Is4():\n\t\treturn ipRange{}, fmt.Errorf(\"%s and %s must be within the same address family\", start, end)\n\tcase start.Less(end):\n\t\treturn ipRange{}, fmt.Errorf(\"start %s is greater than or equal to end %s\", start, end)\n\tdefault:\n\t\tdiff := (&big.Int{}).Sub(\n\t\t\t(&big.Int{}).SetBytes(end.AsSlice()),\n\t\t\t(&big.Int{}).SetBytes(start.AsSlice()),\n\t\t)\n\n\t\tif !diff.IsUint64() || diff.Uint64() > maxRangeLen {\n\t\t\treturn ipRange{}, fmt.Errorf(\"range length must be within %d\", uint32(maxRangeLen))\n\t\t}\n\t}\n\n\treturn ipRange{\n\t\tstart: start,\n\t\tend:   end,\n\t}, nil\n}\n\n// contains returns true if r contains ip.\nfunc (r ipRange) contains(ip netip.Addr) (ok bool) {\n\t// Assume that the end was checked to be within the same address family as\n\t// the start during construction.\n\treturn r.start.Is4() == ip.Is4() && !ip.Less(r.start) && !r.end.Less(ip)\n}\n\n// ipPredicate is a function that is called on every IP address in\n// [ipRange.find].\ntype ipPredicate func(ip netip.Addr) (ok bool)\n\n// find finds the first IP address in r for which p returns true.  It returns an\n// empty [netip.Addr] if there are no addresses that satisfy p.\n//\n// TODO(e.burkov):  Use.\nfunc (r ipRange) find(p ipPredicate) (ip netip.Addr) {\n\tfor ip = r.start; !r.end.Less(ip); ip = ip.Next() {\n\t\tif p(ip) {\n\t\t\treturn ip\n\t\t}\n\t}\n\n\treturn netip.Addr{}\n}\n\n// offset returns the offset of ip from the beginning of r.  It returns 0 and\n// false if ip is not in r.\nfunc (r ipRange) offset(ip netip.Addr) (offset uint64, ok bool) {\n\tif !r.contains(ip) {\n\t\treturn 0, false\n\t}\n\n\tstartData, ipData := r.start.As16(), ip.As16()\n\tbe := binary.BigEndian\n\n\t// Assume that the range length was checked against maxRangeLen during\n\t// construction.\n\treturn be.Uint64(ipData[8:]) - be.Uint64(startData[8:]), true\n}\n\n// String implements the fmt.Stringer interface for *ipRange.\nfunc (r ipRange) String() (s string) {\n\treturn fmt.Sprintf(\"%s-%s\", r.start, r.end)\n}\n"
  },
  {
    "path": "internal/dhcpsvc/iprange_internal_test.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"net/netip\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\t// testRangeStartV4Str is the string representation of the start of the test\n\t// range for IPv4.\n\ttestRangeStartV4Str = \"192.0.2.1\"\n\n\t// testRangeEndV4Str is the string representation of the end of the test\n\t// range for IPv4.\n\ttestRangeEndV4Str = \"192.0.2.5\"\n\n\t// testRangeStartV6Str is the string representation of the start of the\n\t// test range for IPv6.\n\ttestRangeStartV6Str = \"2001:db8::1\"\n\n\t// testRangeEndV6Str is the string representation of the end of the test\n\t// range for IPv6.\n\ttestRangeEndV6Str = \"2001:db8::3\"\n\n\t// testRangeEndV6LargeStr is the string representation of the end of the\n\t// test range for IPv6 that is too large.\n\ttestRangeEndV6LargeStr = \"2001:db9::4\"\n)\n\nvar (\n\t// testRangeStartV4 is the start of the test range for IPv4.\n\ttestRangeStartV4 = netip.MustParseAddr(testRangeStartV4Str)\n\n\t// testRangeEndV4 is the end of the test range for IPv4.\n\ttestRangeEndV4 = netip.MustParseAddr(testRangeEndV4Str)\n\n\t// testRangeStartV6 is the start of the test range for IPv6.\n\ttestRangeStartV6 = netip.MustParseAddr(testRangeStartV6Str)\n\n\t// testRangeEndV6 is the end of the test range for IPv6.\n\ttestRangeEndV6 = netip.MustParseAddr(testRangeEndV6Str)\n\n\t// testRangeEndV6Large is the end of the test range for IPv6 that is too\n\t// large.\n\ttestRangeEndV6Large = netip.MustParseAddr(testRangeEndV6LargeStr)\n)\n\nfunc TestNewIPRange(t *testing.T) {\n\ttestCases := []struct {\n\t\tstart      netip.Addr\n\t\tend        netip.Addr\n\t\tname       string\n\t\twantErrMsg string\n\t}{{\n\t\tstart:      testRangeStartV4,\n\t\tend:        testRangeEndV4,\n\t\tname:       \"success_ipv4\",\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tstart:      testRangeStartV6,\n\t\tend:        testRangeEndV6,\n\t\tname:       \"success_ipv6\",\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tstart: testRangeEndV4,\n\t\tend:   testRangeStartV4,\n\t\tname:  \"start_gt_end\",\n\t\twantErrMsg: \"invalid ip range: start \" + testRangeEndV4Str +\n\t\t\t\" is greater than or equal to end \" + testRangeStartV4Str,\n\t}, {\n\t\tstart: testRangeStartV4,\n\t\tend:   testRangeStartV4,\n\t\tname:  \"start_eq_end\",\n\t\twantErrMsg: \"invalid ip range: start \" + testRangeStartV4Str +\n\t\t\t\" is greater than or equal to end \" + testRangeStartV4Str,\n\t}, {\n\t\tstart: testRangeStartV6,\n\t\tend:   testRangeEndV6Large,\n\t\tname:  \"too_large\",\n\t\twantErrMsg: \"invalid ip range: range length must be within \" +\n\t\t\tstrconv.FormatUint(maxRangeLen, 10),\n\t}, {\n\t\tstart: testRangeStartV4,\n\t\tend:   testRangeEndV6,\n\t\tname:  \"different_family\",\n\t\twantErrMsg: \"invalid ip range: \" + testRangeStartV4Str + \" and \" +\n\t\t\ttestRangeEndV6Str + \" must be within the same address family\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t_, err := newIPRange(tc.start, tc.end)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\nfunc TestIPRange_Contains(t *testing.T) {\n\tr, err := newIPRange(testRangeStartV4, testRangeEndV4)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tin   netip.Addr\n\t\twant assert.BoolAssertionFunc\n\t\tname string\n\t}{{\n\t\tin:   testRangeStartV4,\n\t\twant: assert.True,\n\t\tname: \"start\",\n\t}, {\n\t\tin:   testRangeEndV4,\n\t\twant: assert.True,\n\t\tname: \"end\",\n\t}, {\n\t\tin:   testRangeStartV4.Next(),\n\t\twant: assert.True,\n\t\tname: \"within\",\n\t}, {\n\t\tin:   testRangeStartV4.Prev(),\n\t\twant: assert.False,\n\t\tname: \"before\",\n\t}, {\n\t\tin:   testRangeEndV4.Next(),\n\t\twant: assert.False,\n\t\tname: \"after\",\n\t}, {\n\t\tin:   testRangeStartV6,\n\t\twant: assert.False,\n\t\tname: \"another_family\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttc.want(t, r.contains(tc.in))\n\t\t})\n\t}\n}\n\nfunc TestIPRange_Find(t *testing.T) {\n\tr, err := newIPRange(testRangeStartV4, testRangeEndV4)\n\trequire.NoError(t, err)\n\n\tnum, ok := r.offset(testRangeEndV4)\n\trequire.True(t, ok)\n\n\ttestCases := []struct {\n\t\tpredicate ipPredicate\n\t\twant      netip.Addr\n\t\tname      string\n\t}{{\n\t\tpredicate: func(ip netip.Addr) (ok bool) {\n\t\t\tipData := ip.AsSlice()\n\n\t\t\treturn ipData[len(ipData)-1]%2 == 0\n\t\t},\n\t\twant: testRangeStartV4.Next(),\n\t\tname: \"even\",\n\t}, {\n\t\tpredicate: func(ip netip.Addr) (ok bool) {\n\t\t\tipData := ip.AsSlice()\n\n\t\t\treturn ipData[len(ipData)-1]%10 == 0\n\t\t},\n\t\twant: netip.Addr{},\n\t\tname: \"none\",\n\t}, {\n\t\tpredicate: func(ip netip.Addr) (ok bool) {\n\t\t\treturn true\n\t\t},\n\t\twant: testRangeStartV4,\n\t\tname: \"first\",\n\t}, {\n\t\tpredicate: func(ip netip.Addr) (ok bool) {\n\t\t\toff, _ := r.offset(ip)\n\n\t\t\treturn off == num\n\t\t},\n\t\twant: testRangeEndV4,\n\t\tname: \"last\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := r.find(tc.predicate)\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc TestIPRange_Offset(t *testing.T) {\n\tr, err := newIPRange(testRangeStartV4, testRangeEndV4)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\twantOK     assert.BoolAssertionFunc\n\t\tin         netip.Addr\n\t\tname       string\n\t\twantOffset uint64\n\t}{{\n\t\twantOK:     assert.True,\n\t\tin:         testRangeStartV4.Next(),\n\t\tname:       \"in\",\n\t\twantOffset: 1,\n\t}, {\n\t\twantOK:     assert.True,\n\t\tin:         testRangeStartV4,\n\t\tname:       \"in_start\",\n\t\twantOffset: 0,\n\t}, {\n\t\twantOK:     assert.True,\n\t\tin:         testRangeEndV4,\n\t\tname:       \"in_end\",\n\t\twantOffset: 4,\n\t}, {\n\t\twantOK:     assert.False,\n\t\tin:         testRangeEndV4.Next(),\n\t\tname:       \"out_after\",\n\t\twantOffset: 0,\n\t}, {\n\t\twantOK:     assert.False,\n\t\tin:         testRangeStartV4.Prev(),\n\t\tname:       \"out_before\",\n\t\twantOffset: 0,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toffset, ok := r.offset(tc.in)\n\t\t\tassert.Equal(t, tc.wantOffset, offset)\n\t\t\ttc.wantOK(t, ok)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpsvc/lease.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"bytes\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// Lease is a DHCP lease.\n//\n// TODO(e.burkov):  Consider moving it to [agh], since it also may be needed in\n// [websvc].\n//\n// TODO(e.burkov):  Add validation method.\ntype Lease struct {\n\t// IP is the IP address leased to the client.  It must not be empty.\n\tIP netip.Addr\n\n\t// Expiry is the expiration time of the lease or its blocking expiration\n\t// time.\n\tExpiry time.Time\n\n\t// Hostname of the client.  It may be empty if the lease is blocked.\n\tHostname string\n\n\t// HWAddr is the physical hardware (MAC) address.  It must not be nil.\n\tHWAddr net.HardwareAddr\n\n\t// IsStatic defines if the lease is static.\n\tIsStatic bool\n}\n\n// Clone returns a deep copy of l.\nfunc (l *Lease) Clone() (clone *Lease) {\n\tif l == nil {\n\t\treturn nil\n\t}\n\n\treturn &Lease{\n\t\tExpiry:   l.Expiry,\n\t\tHostname: l.Hostname,\n\t\tHWAddr:   slices.Clone(l.HWAddr),\n\t\tIP:       l.IP,\n\t\tIsStatic: l.IsStatic,\n\t}\n}\n\n// EUI48AddrLen is the length of a valid EUI-48 hardware address.\nconst EUI48AddrLen = 6\n\n// blockedHardwareAddr is the hardware address used to mark a lease as blocked.\nvar blockedHardwareAddr = make(net.HardwareAddr, EUI48AddrLen)\n\n// IsBlocked returns true if the lease is blocked.\nfunc (l *Lease) IsBlocked() (blocked bool) {\n\treturn bytes.Equal(l.HWAddr, blockedHardwareAddr)\n}\n\n// updateExpiry updates the lease expiry time if the current time is past the\n// expiry.  For static leases, this operation is a no-op.\nfunc (l *Lease) updateExpiry(clock timeutil.Clock, ttl time.Duration) {\n\tif l.IsStatic {\n\t\treturn\n\t}\n\n\tnow := clock.Now()\n\tif now.Before(l.Expiry) {\n\t\treturn\n\t}\n\n\tl.Expiry = now.Add(ttl)\n}\n"
  },
  {
    "path": "internal/dhcpsvc/lease_internal_test.go",
    "content": "package dhcpsvc\n\n// BlockedHardwareAddr is the hardware address used to mark a lease as blocked.\n// It's exported for testing purposes.\nvar BlockedHardwareAddr = blockedHardwareAddr\n"
  },
  {
    "path": "internal/dhcpsvc/leaseindex.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strings\"\n)\n\n// leaseIndex is the set of leases indexed by their identifiers for quick\n// lookup.\n//\n// TODO(e.burkov):  Use for all lease-related operations, including\n// interface-specific ones.\ntype leaseIndex struct {\n\t// byAddr is a lookup shortcut for leases by their IP addresses.\n\tbyAddr map[netip.Addr]*Lease\n\n\t// byName is a lookup shortcut for leases by their hostnames.\n\t//\n\t// TODO(e.burkov):  Use a slice of leases with the same hostname?\n\tbyName map[string]*Lease\n\n\t// dbFilePath is the path to the database file containing the DHCP leases.\n\t//\n\t// TODO(e.burkov):  Consider extracting the database logic into a separate\n\t// interface to prevent packages that only need lease data from depending on\n\t// the entire index and to simplify testing.\n\tdbFilePath string\n}\n\n// newLeaseIndex returns a new index for [Lease]s.\nfunc newLeaseIndex(dbFilePath string) (idx *leaseIndex) {\n\treturn &leaseIndex{\n\t\tbyAddr:     map[netip.Addr]*Lease{},\n\t\tbyName:     map[string]*Lease{},\n\t\tdbFilePath: dbFilePath,\n\t}\n}\n\n// leaseByAddr returns a lease by its IP address.\nfunc (idx *leaseIndex) leaseByAddr(addr netip.Addr) (l *Lease, ok bool) {\n\tl, ok = idx.byAddr[addr]\n\n\treturn l, ok\n}\n\n// leaseByName returns a lease by its hostname.\nfunc (idx *leaseIndex) leaseByName(name string) (l *Lease, ok bool) {\n\t// TODO(e.burkov):  Probably, use a case-insensitive comparison and store in\n\t// slice.  This would require a benchmark.\n\tl, ok = idx.byName[strings.ToLower(name)]\n\n\treturn l, ok\n}\n\n// clear removes all leases from idx.  It doesn't clear interfaces' leases.\nfunc (idx *leaseIndex) clear(ctx context.Context, logger *slog.Logger) (err error) {\n\tclear(idx.byAddr)\n\tclear(idx.byName)\n\n\terr = idx.dbStore(ctx, logger)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// add adds l into idx and into iface.  l must be valid, iface should be\n// responsible for l's IP.  It returns an error if l duplicates at least a\n// single value of another lease.\nfunc (idx *leaseIndex) add(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tl *Lease,\n\tiface *netInterface,\n) (err error) {\n\tloweredName := strings.ToLower(l.Hostname)\n\n\tif _, ok := idx.byAddr[l.IP]; ok {\n\t\treturn fmt.Errorf(\"lease for ip %s already exists\", l.IP)\n\t} else if _, ok = idx.byName[loweredName]; ok {\n\t\treturn fmt.Errorf(\"lease for hostname %s already exists\", l.Hostname)\n\t}\n\n\terr = iface.addLease(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tidx.byAddr[l.IP] = l\n\tidx.byName[loweredName] = l\n\n\terr = idx.dbStore(ctx, logger)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// remove removes l from idx and from iface.  l must be valid, iface should\n// contain the same lease or the lease itself.  It returns an error if the lease\n// not found.\n//\n// TODO(e.burkov):  Consider using the iface's logger after simplifying\n// relations between index and interfaces.\nfunc (idx *leaseIndex) remove(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tl *Lease,\n\tiface *netInterface,\n) (err error) {\n\tloweredName := strings.ToLower(l.Hostname)\n\n\tif _, ok := idx.byAddr[l.IP]; !ok {\n\t\treturn fmt.Errorf(\"no lease for ip %s\", l.IP)\n\t} else if _, ok = idx.byName[loweredName]; !ok {\n\t\treturn fmt.Errorf(\"no lease for hostname %s\", l.Hostname)\n\t}\n\n\terr = iface.removeLease(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdelete(idx.byAddr, l.IP)\n\tdelete(idx.byName, loweredName)\n\n\terr = idx.dbStore(ctx, logger)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// update updates l in idx and in iface.  l must be valid, iface should be\n// responsible for l's IP.  It returns an error if l duplicates at least a\n// single value of another lease, except for the updated lease itself.\nfunc (idx *leaseIndex) update(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tl *Lease,\n\tiface *netInterface,\n) (err error) {\n\tloweredName := strings.ToLower(l.Hostname)\n\n\texisting, ok := idx.byAddr[l.IP]\n\tif ok && !slices.Equal(l.HWAddr, existing.HWAddr) {\n\t\treturn fmt.Errorf(\"lease for ip %s already exists\", l.IP)\n\t}\n\n\texisting, ok = idx.byName[loweredName]\n\tif ok && !slices.Equal(l.HWAddr, existing.HWAddr) {\n\t\treturn fmt.Errorf(\"lease for hostname %s already exists\", l.Hostname)\n\t}\n\n\tprev, err := iface.updateLease(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = idx.dbStore(ctx, logger)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tdelete(idx.byAddr, prev.IP)\n\tdelete(idx.byName, strings.ToLower(prev.Hostname))\n\n\tidx.byAddr[l.IP] = l\n\tidx.byName[loweredName] = l\n\n\treturn nil\n}\n\n// rangeLeases calls f for each lease in idx in an unspecified order until f\n// returns false.\nfunc (idx *leaseIndex) rangeLeases(f func(l *Lease) (cont bool)) {\n\tfor _, l := range idx.byName {\n\t\tif !f(l) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// len returns the number of leases in idx.\nfunc (idx *leaseIndex) len() (l uint) {\n\treturn uint(len(idx.byAddr))\n}\n"
  },
  {
    "path": "internal/dhcpsvc/networkdevice.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n)\n\n// NetworkDeviceConfig is the configuration for a network device.\ntype NetworkDeviceConfig struct {\n\t// Name is the name of the network device.  It must be a valid interface\n\t// name on the system.\n\tName string\n}\n\n// Validate implements the [validate.Interface] interface for\n// *NetworkDeviceConfig.\nfunc (conf *NetworkDeviceConfig) Validate() (err error) {\n\tif conf == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\treturn validate.NotEmpty(\"Name\", conf.Name)\n}\n\n// NetworkDeviceManager creates and manages network devices.\ntype NetworkDeviceManager interface {\n\t// Open opens a network device.  conf must be valid.\n\t//\n\t// An attempt to open the same device multiple times may return an error.\n\tOpen(ctx context.Context, conf *NetworkDeviceConfig) (dev NetworkDevice, err error)\n}\n\n// EmptyNetworkDeviceManager is an empty implementation of\n// [NetworkDeviceManager].\ntype EmptyNetworkDeviceManager struct{}\n\n// type check\nvar _ NetworkDeviceManager = EmptyNetworkDeviceManager{}\n\n// Open implements the [NetworkDeviceManager] interface for\n// [EmptyNetworkDeviceManager].  It always returns [EmptyNetworkDevice].\nfunc (EmptyNetworkDeviceManager) Open(\n\t_ context.Context,\n\t_ *NetworkDeviceConfig,\n) (nd NetworkDevice, err error) {\n\treturn nil, nil\n}\n\n// NetworkDevice provides an ability of reading and writing packets to a network\n// interface.  It used to generalize implementations for different platforms and\n// to simplify testing.\n//\n// It's based on [pcap.Handle].\ntype NetworkDevice interface {\n\tgopacket.PacketDataSource\n\n\t// No methods of a device should be called after Close.\n\tio.Closer\n\n\t// Addresses returns all IP addresses assigned to the device.\n\tAddresses() (ips []netip.Addr)\n\n\t// LinkType returns the link type of the network interface.\n\tLinkType() (lt layers.LinkType)\n\n\t// WritePacketData writes a serialized packet to the network interface.\n\tWritePacketData(data []byte) (err error)\n}\n\n// EmptyNetworkDevice is an empty implementation of NetworkDevice.\ntype EmptyNetworkDevice struct{}\n\n// type check\nvar _ NetworkDevice = EmptyNetworkDevice{}\n\n// ReadPacketData implements the [gopacket.PacketDataSource] interface for\n// [EmptyNetworkDevice].  It always returns no data, empty capture info and a\n// nil error.\nfunc (EmptyNetworkDevice) ReadPacketData() (data []byte, ci gopacket.CaptureInfo, err error) {\n\treturn nil, gopacket.CaptureInfo{}, nil\n}\n\n// Close implements the [io.Closer] interface for [EmptyNetworkDevice].  It\n// always returns nil.\nfunc (EmptyNetworkDevice) Close() (err error) {\n\treturn nil\n}\n\n// Addresses implements the [NetworkDevice] interface for [EmptyNetworkDevice].\n// It always returns nil.\nfunc (EmptyNetworkDevice) Addresses() (ips []netip.Addr) {\n\treturn nil\n}\n\n// LinkType implements the [NetworkDevice] interface for [EmptyNetworkDevice].\n// It always returns [layers.LinkTypeNull].\nfunc (EmptyNetworkDevice) LinkType() (lt layers.LinkType) {\n\treturn layers.LinkTypeNull\n}\n\n// WritePacketData implements the [NetworkDevice] interface for\n// [EmptyNetworkDevice].  It always returns nil.\nfunc (EmptyNetworkDevice) WritePacketData(_ []byte) (err error) {\n\treturn nil\n}\n\n// frameData stores the Ethernet and IPv4 layers of the incoming packet, and\n// the network device that the packet was received from.\ntype frameData struct {\n\tether  *layers.Ethernet\n\tip     *layers.IPv4\n\tdevice NetworkDevice\n}\n"
  },
  {
    "path": "internal/dhcpsvc/networkdevice_test.go",
    "content": "package dhcpsvc_test\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/netip\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testNetworkDeviceManager is a mock implementation of the\n// [dhcpsvc.NetworkDeviceManager] interface.\n//\n// TODO(e.burkov):  Move to aghtest.\ntype testNetworkDeviceManager struct {\n\tonOpen func(\n\t\tctx context.Context,\n\t\tconf *dhcpsvc.NetworkDeviceConfig,\n\t) (nd dhcpsvc.NetworkDevice, err error)\n}\n\n// type check\nvar _ dhcpsvc.NetworkDeviceManager = (*testNetworkDeviceManager)(nil)\n\n// Open implements the [dhcpsvc.NetworkDeviceManager] interface for\n// *testNetworkDeviceManager.\nfunc (ndm *testNetworkDeviceManager) Open(\n\tctx context.Context,\n\tconf *dhcpsvc.NetworkDeviceConfig,\n) (dev dhcpsvc.NetworkDevice, err error) {\n\treturn ndm.onOpen(ctx, conf)\n}\n\n// testNetworkDevice is a mock implementation of the [dhcpsvc.NetworkDevice]\n// interface.\n//\n// TODO(e.burkov):  Move to aghtest.\ntype testNetworkDevice struct {\n\tonReadPacketData  func() (data []byte, ci gopacket.CaptureInfo, err error)\n\tonClose           func() (err error)\n\tonAddresses       func() (ips []netip.Addr)\n\tonLinkType        func() (lt layers.LinkType)\n\tonWritePacketData func(data []byte) (err error)\n}\n\n// type check\nvar _ dhcpsvc.NetworkDevice = (*testNetworkDevice)(nil)\n\n// ReadPacketData implements the [gopacket.PacketDataSource] interface for\n// *testNetworkDevice.\nfunc (nd *testNetworkDevice) ReadPacketData() (data []byte, ci gopacket.CaptureInfo, err error) {\n\treturn nd.onReadPacketData()\n}\n\n// Close implements the [io.Closer] interface for *testNetworkDevice.\nfunc (nd *testNetworkDevice) Close() (err error) {\n\treturn nd.onClose()\n}\n\n// Addresses implements the [dhcpsvc.NetworkDevice] interface for\n// *testNetworkDevice.\nfunc (nd *testNetworkDevice) Addresses() (ips []netip.Addr) {\n\treturn nd.onAddresses()\n}\n\n// WritePacketData implements the [dhcpsvc.NetworkDevice] interface for\n// *testNetworkDevice.\nfunc (nd *testNetworkDevice) WritePacketData(data []byte) (err error) {\n\treturn nd.onWritePacketData(data)\n}\n\n// LinkType implements the [dhcpsvc.NetworkDevice] interface for\n// *testNetworkDevice.\nfunc (nd *testNetworkDevice) LinkType() (lt layers.LinkType) {\n\treturn nd.onLinkType()\n}\n\n// newTestNetworkDeviceManager creates a network device manager for testing.  It\n// requires that device opened have a deviceName.  The device itself has a link\n// type [layers.LinkTypeEthernet].  Incoming packets are received from inCh and\n// outgoing packets are sent to outCh.\nfunc newTestNetworkDeviceManager(\n\ttb testing.TB,\n\tdeviceName string,\n\taddr netip.Addr,\n) (ndMgr dhcpsvc.NetworkDeviceManager, inCh chan gopacket.Packet, outCh chan []byte) {\n\ttb.Helper()\n\n\tinCh = make(chan gopacket.Packet)\n\toutCh = make(chan []byte)\n\n\tisOpened := atomic.Bool{}\n\n\tpt := testutil.PanicT{}\n\taddrs := []netip.Addr{addr}\n\n\tdev := &testNetworkDevice{\n\t\tonReadPacketData: func() (data []byte, ci gopacket.CaptureInfo, err error) {\n\t\t\tpkt, ok := testutil.RequireReceive(pt, inCh, testTimeout)\n\t\t\trequire.Equal(pt, isOpened.Load(), ok)\n\n\t\t\tif !ok {\n\t\t\t\treturn nil, gopacket.CaptureInfo{}, io.EOF\n\t\t\t}\n\n\t\t\tdata = pkt.Data()\n\t\t\tci = gopacket.CaptureInfo{\n\t\t\t\tLength:        len(data),\n\t\t\t\tCaptureLength: len(data),\n\t\t\t}\n\n\t\t\treturn data, ci, nil\n\t\t},\n\t\tonClose: func() (err error) {\n\t\t\tisOpened.Store(false)\n\t\t\tclose(inCh)\n\n\t\t\treturn nil\n\t\t},\n\t\tonAddresses: func() (ips []netip.Addr) {\n\t\t\treturn addrs\n\t\t},\n\t\tonLinkType: func() (lt layers.LinkType) {\n\t\t\treturn layers.LinkTypeEthernet\n\t\t},\n\t\tonWritePacketData: func(data []byte) (err error) {\n\t\t\ttestutil.RequireSend(pt, outCh, data, testTimeout)\n\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tndMgr = &testNetworkDeviceManager{\n\t\tonOpen: func(\n\t\t\t_ context.Context,\n\t\t\tconf *dhcpsvc.NetworkDeviceConfig,\n\t\t) (nd dhcpsvc.NetworkDevice, err error) {\n\t\t\tisOpened.Store(true)\n\t\t\trequire.Equal(pt, deviceName, conf.Name)\n\n\t\t\treturn dev, nil\n\t\t},\n\t}\n\n\treturn ndMgr, inCh, outCh\n}\n"
  },
  {
    "path": "internal/dhcpsvc/options4.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/google/gopacket/layers\"\n)\n\n// implicitOptions returns the implicit options for the interface, sorted by\n// code.\nfunc (c *IPv4Config) implicitOptions() (opts layers.DHCPOptions) {\n\t// Set default values of host configuration parameters listed in Appendix A\n\t// of RFC-2131.\n\topts = make(layers.DHCPOptions, 0, 20)\n\n\topts = c.appendConfOptions(opts)\n\topts = appendIPPerHostOptions(opts)\n\topts = appendIPPerInterfaceOptions(opts)\n\topts = appendLinkPerInterfaceOptions(opts)\n\topts = appendTCPPerHostOptions(opts)\n\n\tslices.SortFunc(opts, compareV4OptionCodes)\n\n\treturn opts\n}\n\n// appendConfOptions appends the DHCPv4 options depending on the configuration\n// to orig.\nfunc (c *IPv4Config) appendConfOptions(orig layers.DHCPOptions) (res layers.DHCPOptions) {\n\treturn append(\n\t\torig,\n\t\tlayers.NewDHCPOption(layers.DHCPOptSubnetMask, c.SubnetMask.AsSlice()),\n\t\tlayers.NewDHCPOption(layers.DHCPOptRouter, c.GatewayIP.AsSlice()),\n\t)\n}\n\n// appendIPPerHostOptions appends the IP-layer per host DHCPv4 options to orig.\nfunc appendIPPerHostOptions(orig layers.DHCPOptions) (res layers.DHCPOptions) {\n\treturn append(\n\t\torig,\n\t\t// An Internet host that includes embedded gateway code MUST have a\n\t\t// configuration switch to disable the gateway function, and this switch\n\t\t// MUST default to the non-gateway mode.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.5.\n\t\tlayers.NewDHCPOption(layers.DHCPOptIPForwarding, []byte{0x0}),\n\n\t\t// A host that supports non-local source-routing MUST have a\n\t\t// configurable switch to disable forwarding, and this switch MUST\n\t\t// default to disabled.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.5.\n\t\tlayers.NewDHCPOption(layers.DHCPOptSourceRouting, []byte{0x0}),\n\n\t\t// Do not set the Policy Filter Option since it only makes sense when\n\t\t// the non-local source routing is enabled.\n\n\t\t// The minimum legal value is 576.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc2132#section-4.4.\n\t\tlayers.NewDHCPOption(layers.DHCPOptDatagramMTU, []byte{0x2, 0x40}),\n\n\t\t// Set the current recommended default time to live for the Internet\n\t\t// Protocol which is 64.\n\t\t//\n\t\t// See https://www.iana.org/assignments/ip-parameters/ip-parameters.xhtml#ip-parameters-2.\n\t\tlayers.NewDHCPOption(layers.DHCPOptDefaultTTL, []byte{0x40}),\n\n\t\t// For example, after the PTMU estimate is decreased, the timeout should\n\t\t// be set to 10 minutes; once this timer expires and a larger MTU is\n\t\t// attempted, the timeout can be set to a much smaller value.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1191#section-6.6.\n\t\tlayers.NewDHCPOption(layers.DHCPOptPathMTUAgingTimeout, []byte{0x0, 0x0, 0x2, 0x58}),\n\n\t\t// There is a table describing the MTU values representing all major\n\t\t// data-link technologies in use in the Internet so that each set of\n\t\t// similar MTUs is associated with a plateau value equal to the lowest\n\t\t// MTU in the group.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1191#section-7.\n\t\tlayers.NewDHCPOption(layers.DHCPOptPathPlateuTableOption, []byte{\n\t\t\t0x0, 0x44,\n\t\t\t0x1, 0x28,\n\t\t\t0x1, 0xFC,\n\t\t\t0x3, 0xEE,\n\t\t\t0x5, 0xD4,\n\t\t\t0x7, 0xD2,\n\t\t\t0x11, 0x0,\n\t\t\t0x1F, 0xE6,\n\t\t\t0x45, 0xFA,\n\t\t}),\n\t)\n}\n\n// appendIPPerInterfaceOptions appends the IP-layer per interface DHCPv4 options\n// to orig.\nfunc appendIPPerInterfaceOptions(orig layers.DHCPOptions) (res layers.DHCPOptions) {\n\treturn append(\n\t\torig,\n\n\t\t// Don't set the Interface MTU because client may choose the value on\n\t\t// their own since it's listed in the [Host Requirements RFC].  It also\n\t\t// seems the values listed there sometimes appear obsolete, see\n\t\t// https://github.com/AdguardTeam/AdGuardHome/issues/5281.\n\t\t//\n\t\t// [Host Requirements RFC]: https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.\n\n\t\t// Set the All Subnets Are Local Option to false since commonly the\n\t\t// connected hosts aren't expected to be multihomed.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.\n\t\tlayers.NewDHCPOption(layers.DHCPOptAllSubsLocal, []byte{0x0}),\n\n\t\t// Set the Perform Mask Discovery Option to false to provide the subnet\n\t\t// mask by options only.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.2.9.\n\t\tlayers.NewDHCPOption(layers.DHCPOptMaskDiscovery, []byte{0x0}),\n\n\t\t// A system MUST NOT send an Address Mask Reply unless it is an\n\t\t// authoritative agent for address masks.  An authoritative agent may be\n\t\t// a host or a gateway, but it MUST be explicitly configured as a\n\t\t// address mask agent.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.2.9.\n\t\tlayers.NewDHCPOption(layers.DHCPOptMaskSupplier, []byte{0x0}),\n\n\t\t// Set the Perform Router Discovery Option to true as per Router\n\t\t// Discovery Document.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.\n\t\tlayers.NewDHCPOption(layers.DHCPOptRouterDiscovery, []byte{0x1}),\n\n\t\t// The all-routers address is preferred wherever possible.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.\n\t\tlayers.NewDHCPOption(layers.DHCPOptSolicitAddr, netutil.IPv4allrouter()),\n\n\t\t// Don't set the Static Routes Option since it should be set up by\n\t\t// system administrator.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.1.2.\n\n\t\t// A datagram with the destination address of limited broadcast will be\n\t\t// received by every host on the connected physical network but will not\n\t\t// be forwarded outside that network.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.3.\n\t\tlayers.NewDHCPOption(layers.DHCPOptBroadcastAddr, netutil.IPv4bcast()),\n\t)\n}\n\n// appendLinkPerInterfaceOptions appends the link-layer per interface DHCPv4\n// options to orig.\nfunc appendLinkPerInterfaceOptions(orig layers.DHCPOptions) (res layers.DHCPOptions) {\n\treturn append(\n\t\torig,\n\n\t\t// If the system does not dynamically negotiate use of the trailer\n\t\t// protocol on a per-destination basis, the default configuration MUST\n\t\t// disable the protocol.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.1.\n\t\tlayers.NewDHCPOption(layers.DHCPOptARPTrailers, []byte{0x0}),\n\n\t\t// For proxy ARP situations, the timeout needs to be on the order of a\n\t\t// minute.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.2.1.\n\t\tlayers.NewDHCPOption(layers.DHCPOptARPTimeout, []byte{0x0, 0x0, 0x0, 0x3C}),\n\n\t\t// An Internet host that implements sending both the RFC-894 and the\n\t\t// RFC-1042 encapsulations MUST provide a configuration switch to select\n\t\t// which is sent, and this switch MUST default to RFC-894.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.3.\n\t\tlayers.NewDHCPOption(layers.DHCPOptEthernetEncap, []byte{0x0}),\n\t)\n}\n\n// appendTCPPerHostOptions appends the TCP per host DHCPv4 options to orig.\nfunc appendTCPPerHostOptions(orig layers.DHCPOptions) (res layers.DHCPOptions) {\n\treturn append(\n\t\torig,\n\n\t\t// A fixed value must be at least big enough for the Internet diameter,\n\t\t// i.e., the longest possible path.  A reasonable value is about twice\n\t\t// the diameter, to allow for continued Internet growth.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.7.\n\t\tlayers.NewDHCPOption(layers.DHCPOptTCPTTL, []byte{0x0, 0x0, 0x0, 0x3C}),\n\n\t\t// The interval MUST be configurable and MUST default to no less than\n\t\t// two hours.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.\n\t\tlayers.NewDHCPOption(layers.DHCPOptTCPKeepAliveInt, []byte{0x0, 0x0, 0x1C, 0x20}),\n\n\t\t// Unfortunately, some misbehaved TCP implementations fail to respond to\n\t\t// a probe segment unless it contains data.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.\n\t\tlayers.NewDHCPOption(layers.DHCPOptTCPKeepAliveGarbage, []byte{0x1}),\n\t)\n}\n\n// options returns the implicit and explicit options for the interface.  The two\n// lists are disjoint and the implicit options are initialized with default\n// values.  All options within exp which have a nil Data field should be treated\n// as instruction to remove those from responses.\n//\n// TODO(e.burkov):  DRY with the IPv6 version.\nfunc (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp layers.DHCPOptions) {\n\t// Set values of implicit options.\n\timp = c.implicitOptions()\n\n\t// Set values for explicitly configured options.\n\tfor _, o := range c.Options {\n\t\ti, found := slices.BinarySearchFunc(imp, o, compareV4OptionCodes)\n\t\tif found {\n\t\t\timp = slices.Delete(imp, i, i+1)\n\t\t}\n\n\t\ti, found = slices.BinarySearchFunc(exp, o, compareV4OptionCodes)\n\t\tif found {\n\t\t\texp[i].Data, exp[i].Length = o.Data, o.Length\n\t\t} else {\n\t\t\texp = slices.Insert(exp, i, o)\n\t\t}\n\t}\n\n\tl.DebugContext(ctx, \"options\", \"implicit\", imp, \"explicit\", exp)\n\n\treturn imp, exp\n}\n\n// compareV4OptionCodes compares option codes of a and b.\nfunc compareV4OptionCodes(a, b layers.DHCPOption) (res int) {\n\treturn int(a.Type) - int(b.Type)\n}\n\n// updateOptions updates the options of the response in accordance with the\n// requested parameters.  req and resp must not be nil.\n//\n// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.\nfunc (iface *dhcpInterfaceV4) updateOptions(req, resp *layers.DHCPv4) {\n\t// If the server recognizes the parameter as a parameter defined in the Host\n\t// Requirements Document, the server MUST include the default value for that\n\t// parameter.\n\toptWithCode := layers.DHCPOption{}\n\tfor _, code := range requestedOptions(req) {\n\t\toptWithCode.Type = code\n\t\ti, has := slices.BinarySearchFunc(iface.implicitOpts, optWithCode, compareV4OptionCodes)\n\t\tif has {\n\t\t\t// The client MAY list the options in order of preference. The DHCP\n\t\t\t// server is not required to return the options in the requested\n\t\t\t// order, but MUST try to insert the requested options in the order\n\t\t\t// requested by the client.\n\t\t\t//\n\t\t\t// See https://datatracker.ietf.org/doc/html/rfc2132#section-9.8.\n\t\t\tresp.Options = append(resp.Options, iface.implicitOpts[i])\n\t\t}\n\t}\n\n\t// If the server has been explicitly configured with a default value for the\n\t// parameter or the parameter has a non-default value on the client's\n\t// subnet, the server MUST include that value in an appropriate option.\n\tfor _, opt := range iface.explicitOpts {\n\t\tif opt.Data != nil {\n\t\t\tresp.Options = append(resp.Options, opt)\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// Remove options explicitly configured to be removed, in case they are\n\t\t// already set.\n\t\tresp.Options = slices.DeleteFunc(resp.Options, func(o layers.DHCPOption) (ok bool) {\n\t\t\treturn o.Type == opt.Type\n\t\t})\n\t}\n}\n\n// appendLeaseTime appends the lease time option to the response.  lease must\n// not be nil.\nfunc (iface *dhcpInterfaceV4) appendLeaseTime(resp *layers.DHCPv4, lease *Lease) {\n\tvar dur time.Duration\n\tif lease.IsStatic {\n\t\tdur = iface.common.leaseTTL\n\t} else {\n\t\tdur = lease.Expiry.Sub(iface.clock.Now())\n\t}\n\n\tleaseTimeData := binary.BigEndian.AppendUint32(nil, uint32(dur.Seconds()))\n\n\tresp.Options = append(\n\t\tresp.Options,\n\t\tlayers.NewDHCPOption(layers.DHCPOptLeaseTime, leaseTimeData),\n\t)\n}\n\n// msg4Type returns the message type of msg, if it's present within the options.\nfunc msg4Type(msg *layers.DHCPv4) (typ layers.DHCPMsgType, ok bool) {\n\tfor _, opt := range msg.Options {\n\t\tif opt.Type == layers.DHCPOptMessageType && len(opt.Data) > 0 {\n\t\t\treturn layers.DHCPMsgType(opt.Data[0]), true\n\t\t}\n\t}\n\n\treturn 0, false\n}\n\n// requestedIPv4 returns the IPv4 address, requested by client in the DHCP\n// message, if any.\n//\n// TODO(e.burkov):  DRY with other IP-from-option helpers.\nfunc requestedIPv4(msg *layers.DHCPv4) (ip netip.Addr, ok bool) {\n\tfor _, opt := range msg.Options {\n\t\tif opt.Type == layers.DHCPOptRequestIP && len(opt.Data) == net.IPv4len {\n\t\t\treturn netip.AddrFromSlice(opt.Data)\n\t\t}\n\t}\n\n\treturn netip.Addr{}, false\n}\n\n// serverID4 returns the server ID of the DHCP message, if any.\nfunc serverID4(msg *layers.DHCPv4) (ip netip.Addr, ok bool) {\n\tfor _, opt := range msg.Options {\n\t\tif opt.Type == layers.DHCPOptServerID && len(opt.Data) == net.IPv4len {\n\t\t\treturn netip.AddrFromSlice(opt.Data)\n\t\t}\n\t}\n\n\treturn netip.Addr{}, false\n}\n\n// hostname4 returns the hostname from the DHCPv4 message, if any.\nfunc hostname4(msg *layers.DHCPv4) (hostname string) {\n\tfor _, opt := range msg.Options {\n\t\tif opt.Type == layers.DHCPOptHostname && len(opt.Data) > 0 {\n\t\t\treturn string(opt.Data)\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// requestedOptions returns the list of options requested in DHCPv4 message, if\n// any.\n//\n// TODO(e.burkov):  Use [iter.Seq1].\nfunc requestedOptions(msg *layers.DHCPv4) (opts []layers.DHCPOpt) {\n\tfor _, opt := range msg.Options {\n\t\tl := len(opt.Data)\n\t\tif opt.Type != layers.DHCPOptParamsRequest || l == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\topts = make([]layers.DHCPOpt, 0, l)\n\t\tfor _, code := range opt.Data {\n\t\t\topts = append(opts, layers.DHCPOpt(code))\n\t\t}\n\n\t\treturn opts\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dhcpsvc/options4_internal_test.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst (\n\t// testIPv4Str is the string representation of the test IPv4 address.\n\ttestIPv4Str = \"192.0.2.1\"\n\n\t// testAnotherIPv4Str is the string representation of the test another IPv4\n\t// address.\n\ttestAnotherIPv4Str = \"198.51.100.1\"\n\n\t// broadcastIPv4Str is the string representation of the broadcast IPv4\n\t// address.\n\tbroadcastIPv4Str = \"255.255.255.255\"\n)\n\n// broadcastAddr is the broadcast IPv4 address.\nvar broadcastAddr = netip.MustParseAddr(broadcastIPv4Str)\n\nfunc TestIPv4Config_Options(t *testing.T) {\n\tvar (\n\t\tipv4        = netip.MustParseAddr(testIPv4Str)\n\t\tanotherIPv4 = netip.MustParseAddr(testAnotherIPv4Str)\n\t\tsubnetMask  = netip.PrefixFrom(broadcastAddr, broadcastAddr.BitLen()/2).Masked().Addr()\n\n\t\toptSubnetMask = layers.NewDHCPOption(layers.DHCPOptSubnetMask, subnetMask.AsSlice())\n\t\toptDNS        = layers.NewDHCPOption(\n\t\t\tlayers.DHCPOptDNS,\n\t\t\tappend(ipv4.AsSlice(), anotherIPv4.AsSlice()...),\n\t\t)\n\t\toptBroadcast   = layers.NewDHCPOption(layers.DHCPOptBroadcastAddr, ipv4.AsSlice())\n\t\toptStaticRoute = layers.NewDHCPOption(layers.DHCPOptClasslessStaticRoute, []byte(\"cba\"))\n\t)\n\ttestCases := []struct {\n\t\tname         string\n\t\tconf         *IPv4Config\n\t\twantExplicit layers.DHCPOptions\n\t}{{\n\t\tname: \"all_default\",\n\t\tconf: &IPv4Config{\n\t\t\tOptions: nil,\n\t\t},\n\t\twantExplicit: nil,\n\t}, {\n\t\tname: \"configured_ip\",\n\t\tconf: &IPv4Config{\n\t\t\tOptions: layers.DHCPOptions{optBroadcast},\n\t\t},\n\t\twantExplicit: layers.DHCPOptions{optBroadcast},\n\t}, {\n\t\tname: \"configured_ips\",\n\t\tconf: &IPv4Config{\n\t\t\tOptions: layers.DHCPOptions{optDNS},\n\t\t},\n\t\twantExplicit: layers.DHCPOptions{optDNS},\n\t}, {\n\t\tname: \"configured_del\",\n\t\tconf: &IPv4Config{\n\t\t\tOptions: layers.DHCPOptions{\n\t\t\t\tlayers.NewDHCPOption(layers.DHCPOptBroadcastAddr, nil),\n\t\t\t},\n\t\t},\n\t\twantExplicit: layers.DHCPOptions{\n\t\t\tlayers.NewDHCPOption(layers.DHCPOptBroadcastAddr, nil),\n\t\t},\n\t}, {\n\t\tname: \"rewritten_del\",\n\t\tconf: &IPv4Config{\n\t\t\tOptions: layers.DHCPOptions{\n\t\t\t\tlayers.NewDHCPOption(layers.DHCPOptBroadcastAddr, nil),\n\t\t\t\toptBroadcast,\n\t\t\t},\n\t\t},\n\t\twantExplicit: layers.DHCPOptions{optBroadcast},\n\t}, {\n\t\tname: \"configured_and_del\",\n\t\tconf: &IPv4Config{\n\t\t\tOptions: layers.DHCPOptions{\n\t\t\t\tlayers.NewDHCPOption(layers.DHCPOptClasslessStaticRoute, []byte(\"a\")),\n\t\t\t\tlayers.NewDHCPOption(layers.DHCPOptClasslessStaticRoute, nil),\n\t\t\t\toptStaticRoute,\n\t\t\t},\n\t\t},\n\t\twantExplicit: layers.DHCPOptions{optStaticRoute},\n\t}, {\n\t\tname: \"replace_config_value\",\n\t\tconf: &IPv4Config{\n\t\t\tSubnetMask: netip.PrefixFrom(broadcastAddr, 3*broadcastAddr.BitLen()/4).Masked().Addr(),\n\t\t\tOptions:    layers.DHCPOptions{optSubnetMask},\n\t\t},\n\t\twantExplicit: layers.DHCPOptions{optSubnetMask},\n\t}}\n\n\tctx := testutil.ContextWithTimeout(t, time.Second)\n\tl := slogutil.NewDiscardLogger()\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\timp, exp := tc.conf.options(ctx, l)\n\t\t\tassert.Equal(t, tc.wantExplicit, exp)\n\n\t\t\tfor c := range exp {\n\t\t\t\tassert.NotContains(t, imp, c)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dhcpsvc/options4_test.go",
    "content": "package dhcpsvc_test\n\nimport (\n\t\"encoding/binary\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/gopacket/layers\"\n)\n\n// newOptHostname creates a DHCP hostname (12) option.\nfunc newOptHostname(tb testing.TB, hostname string) (opt layers.DHCPOption) {\n\ttb.Helper()\n\n\treturn layers.NewDHCPOption(layers.DHCPOptHostname, []byte(hostname))\n}\n\n// newOptLeaseTime creates a DHCP lease time (51) option.\nfunc newOptLeaseTime(tb testing.TB, dur time.Duration) (opt layers.DHCPOption) {\n\ttb.Helper()\n\n\tsecs := uint32(dur.Seconds())\n\tvar buf [4]byte\n\tbinary.BigEndian.PutUint32(buf[:], secs)\n\n\treturn layers.NewDHCPOption(layers.DHCPOptLeaseTime, buf[:])\n}\n\n// newOptMessageType creates a DHCP message type (53) option.\nfunc newOptMessageType(tb testing.TB, msgType layers.DHCPMsgType) (opt layers.DHCPOption) {\n\ttb.Helper()\n\n\treturn layers.NewDHCPOption(layers.DHCPOptMessageType, []byte{byte(msgType)})\n}\n\n// newOptServerID creates a DHCP server identifier (54) option.\nfunc newOptServerID(tb testing.TB, serverIP netip.Addr) (opt layers.DHCPOption) {\n\ttb.Helper()\n\n\treturn layers.NewDHCPOption(layers.DHCPOptServerID, serverIP.AsSlice())\n}\n"
  },
  {
    "path": "internal/dhcpsvc/server.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n)\n\n// DHCPServer is a DHCP server for both IPv4 and IPv6 address families.\n//\n// TODO(e.burkov):  Rename to Default.\ntype DHCPServer struct {\n\t// enabled indicates whether the DHCP server is enabled and can provide\n\t// information about its clients.\n\tenabled *atomic.Bool\n\n\t// logger logs common DHCP events.\n\tlogger *slog.Logger\n\n\t// deviceManager is the manager of network devices.\n\tdeviceManager NetworkDeviceManager\n\n\t// devices are the network devices opened in [DHCPServer.Start], mapped to\n\t// their names.  Those are closed in [DHCPServer.Shutdown].\n\t//\n\t// TODO(e.burkov):  Consider storing those within interfaces.\n\tdevices container.KeyValues[string, NetworkDevice]\n\n\t// localTLD is the top-level domain name to use for resolving DHCP clients'\n\t// hostnames.\n\tlocalTLD string\n\n\t// leasesMu protects the leases index as well as leases in the interfaces.\n\tleasesMu *sync.RWMutex\n\n\t// leases stores the DHCP leases for quick lookups.\n\tleases *leaseIndex\n\n\t// interfaces4 is the set of IPv4 interfaces sorted by interface name.\n\tinterfaces4 dhcpInterfacesV4\n\n\t// interfaces6 is the set of IPv6 interfaces sorted by interface name.\n\tinterfaces6 dhcpInterfacesV6\n\n\t// icmpTimeout is the timeout for checking another DHCP server's presence.\n\ticmpTimeout time.Duration\n}\n\n// New creates a new DHCP server with the given configuration.  conf must be\n// valid.\n//\n// TODO(e.burkov):  Use.\nfunc New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {\n\tl := conf.Logger\n\tif !conf.Enabled {\n\t\tl.DebugContext(ctx, \"disabled\")\n\n\t\t// TODO(e.burkov):  Perhaps return [Empty]?\n\t\treturn nil, nil\n\t}\n\n\tenabled := &atomic.Bool{}\n\tenabled.Store(conf.Enabled)\n\n\tsrv = &DHCPServer{\n\t\tenabled:       enabled,\n\t\tlogger:        l,\n\t\tdeviceManager: conf.NetworkDeviceManager,\n\t\tlocalTLD:      conf.LocalDomainName,\n\t\tleasesMu:      &sync.RWMutex{},\n\t\tleases:        newLeaseIndex(conf.DBFilePath),\n\t\ticmpTimeout:   conf.ICMPTimeout,\n\t}\n\n\tsrv.interfaces4, srv.interfaces6 = srv.newInterfaces(ctx, l, conf.Interfaces)\n\n\terr = srv.leases.dbLoad(ctx, l, srv.interfaces4, srv.interfaces6)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\treturn srv, nil\n}\n\n// newInterfaces creates interfaces for the given map of interface names to\n// their configurations.  ifaces must be valid, baseLogger must not be nil.\nfunc (srv *DHCPServer) newInterfaces(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tifaces map[string]*InterfaceConfig,\n) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6) {\n\t// TODO(e.burkov):  Add validations scoped to the network interfaces set.\n\tv4 = make(dhcpInterfacesV4, 0, len(ifaces))\n\tv6 = make(dhcpInterfacesV6, 0, len(ifaces))\n\n\tfor _, name := range slices.Sorted(maps.Keys(ifaces)) {\n\t\tiface := ifaces[name]\n\t\tifaceLogger := baseLogger.With(keyInterface, name)\n\n\t\tiface4 := srv.newDHCPInterfaceV4(\n\t\t\tctx,\n\t\t\tifaceLogger.With(keyFamily, netutil.AddrFamilyIPv4),\n\t\t\tname,\n\t\t\tiface.IPv4,\n\t\t)\n\t\tif iface4 != nil {\n\t\t\tv4 = append(v4, iface4)\n\t\t}\n\n\t\tiface6 := srv.newDHCPInterfaceV6(\n\t\t\tctx,\n\t\t\tifaceLogger.With(keyFamily, netutil.AddrFamilyIPv6),\n\t\t\tname,\n\t\t\tiface.IPv6,\n\t\t)\n\t\tif iface6 != nil {\n\t\t\tv6 = append(v6, iface6)\n\t\t}\n\t}\n\n\treturn v4, v6\n}\n\n// type check\n//\n// TODO(e.burkov):  Uncomment when the [Interface] interface is implemented.\n// var _ Interface = (*DHCPServer)(nil)\n\n// Start implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) Start(ctx context.Context) (err error) {\n\tsrv.logger.DebugContext(ctx, \"starting dhcp server\")\n\n\tvar errs []error\n\tfor _, iface := range srv.interfaces4 {\n\t\tnetDevName := iface.common.name\n\n\t\tvar netDev NetworkDevice\n\t\tnetDev, err = srv.deviceManager.Open(ctx, &NetworkDeviceConfig{\n\t\t\tName: netDevName,\n\t\t})\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tsrv.devices = append(srv.devices, container.KeyValue[string, NetworkDevice]{\n\t\t\tKey:   netDevName,\n\t\t\tValue: netDev,\n\t\t})\n\n\t\tgo srv.serveEther4(context.WithoutCancel(ctx), iface, netDev)\n\t}\n\n\t// TODO(e.burkov):  Serve EthernetTypeIPv6.\n\n\treturn errors.Join(errs...)\n}\n\n// Shutdown implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) Shutdown(ctx context.Context) (err error) {\n\tsrv.logger.DebugContext(ctx, \"shutting down dhcp server\")\n\n\tvar errs []error\n\tfor _, kv := range srv.devices {\n\t\tnetDevName, netDev := kv.Key, kv.Value\n\n\t\terr = netDev.Close()\n\t\tif err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"closing device %q: %w\", netDevName, err))\n\t\t}\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// Enabled implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) Enabled() (ok bool) {\n\treturn srv.enabled.Load()\n}\n\n// Leases implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) Leases() (leases []*Lease) {\n\tsrv.leasesMu.RLock()\n\tdefer srv.leasesMu.RUnlock()\n\n\tfor l := range srv.leases.rangeLeases {\n\t\tif l.IsBlocked() {\n\t\t\tcontinue\n\t\t}\n\n\t\tleases = append(leases, l.Clone())\n\t}\n\n\treturn leases\n}\n\n// HostByIP implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) HostByIP(ip netip.Addr) (host string) {\n\tsrv.leasesMu.RLock()\n\tdefer srv.leasesMu.RUnlock()\n\n\tif l, ok := srv.leases.leaseByAddr(ip); ok {\n\t\treturn l.Hostname\n\t}\n\n\treturn \"\"\n}\n\n// MACByIP implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {\n\tsrv.leasesMu.RLock()\n\tdefer srv.leasesMu.RUnlock()\n\n\tif l, ok := srv.leases.leaseByAddr(ip); ok {\n\t\treturn l.HWAddr\n\t}\n\n\treturn nil\n}\n\n// IPByHost implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {\n\tsrv.leasesMu.RLock()\n\tdefer srv.leasesMu.RUnlock()\n\n\tif l, ok := srv.leases.leaseByName(host); ok {\n\t\treturn l.IP\n\t}\n\n\treturn netip.Addr{}\n}\n\n// Reset implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) Reset(ctx context.Context) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"resetting leases: %w\") }()\n\n\tsrv.leasesMu.Lock()\n\tdefer srv.leasesMu.Unlock()\n\n\tfor _, iface := range srv.interfaces4 {\n\t\tiface.common.reset()\n\t}\n\tfor _, iface := range srv.interfaces6 {\n\t\tiface.common.reset()\n\t}\n\terr = srv.leases.clear(ctx, srv.logger)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\tsrv.logger.DebugContext(ctx, \"reset leases\")\n\n\treturn nil\n}\n\n// AddLease implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"adding lease: %w\") }()\n\n\taddr := l.IP\n\tiface, err := ifaceForAddr(addr, srv.interfaces4, srv.interfaces6)\n\tif err != nil {\n\t\t// Don't wrap the error since it's already informative enough as is.\n\t\treturn err\n\t}\n\n\tsrv.leasesMu.Lock()\n\tdefer srv.leasesMu.Unlock()\n\n\terr = srv.leases.add(ctx, srv.logger, l, iface)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\tiface.logger.DebugContext(\n\t\tctx, \"added lease\",\n\t\t\"hostname\", l.Hostname,\n\t\t\"ip\", l.IP,\n\t\t\"mac\", l.HWAddr,\n\t\t\"is_static\", l.IsStatic,\n\t)\n\n\treturn nil\n}\n\n// UpdateStaticLease implements the [Interface] interface for *DHCPServer.\n//\n// TODO(e.burkov):  Support moving leases between interfaces.\nfunc (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"updating static lease: %w\") }()\n\n\taddr := l.IP\n\tiface, err := ifaceForAddr(addr, srv.interfaces4, srv.interfaces6)\n\tif err != nil {\n\t\t// Don't wrap the error since it's already informative enough as is.\n\t\treturn err\n\t}\n\n\tsrv.leasesMu.Lock()\n\tdefer srv.leasesMu.Unlock()\n\n\terr = srv.leases.update(ctx, srv.logger, l, iface)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\tiface.logger.DebugContext(\n\t\tctx, \"updated lease\",\n\t\t\"hostname\", l.Hostname,\n\t\t\"ip\", l.IP,\n\t\t\"mac\", l.HWAddr,\n\t\t\"is_static\", l.IsStatic,\n\t)\n\n\treturn nil\n}\n\n// RemoveLease implements the [Interface] interface for *DHCPServer.\nfunc (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"removing lease: %w\") }()\n\n\taddr := l.IP\n\tiface, err := ifaceForAddr(addr, srv.interfaces4, srv.interfaces6)\n\tif err != nil {\n\t\t// Don't wrap the error since it's already informative enough as is.\n\t\treturn err\n\t}\n\n\tsrv.leasesMu.Lock()\n\tdefer srv.leasesMu.Unlock()\n\n\terr = srv.leases.remove(ctx, srv.logger, l, iface)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\tiface.logger.DebugContext(\n\t\tctx, \"removed lease\",\n\t\t\"hostname\", l.Hostname,\n\t\t\"ip\", l.IP,\n\t\t\"mac\", l.HWAddr,\n\t\t\"is_static\", l.IsStatic,\n\t)\n\n\treturn nil\n}\n\n// removeLeaseByAddr removes the lease with the given IP address from the\n// server.  It returns an error if the lease can't be removed.\n//\n//lint:ignore U1000 TODO(e.burkov):  Use.\nfunc (srv *DHCPServer) removeLeaseByAddr(ctx context.Context, addr netip.Addr) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"removing lease by address: %w\") }()\n\n\tiface, err := ifaceForAddr(addr, srv.interfaces4, srv.interfaces6)\n\tif err != nil {\n\t\t// Don't wrap the error since it's already informative enough as is.\n\t\treturn err\n\t}\n\n\tsrv.leasesMu.Lock()\n\tdefer srv.leasesMu.Unlock()\n\n\tl, ok := srv.leases.leaseByAddr(addr)\n\tif !ok {\n\t\treturn fmt.Errorf(\"no lease for ip %s\", addr)\n\t}\n\n\terr = srv.leases.remove(ctx, srv.logger, l, iface)\n\tif err != nil {\n\t\t// Don't wrap the error since there is already an annotation deferred.\n\t\treturn err\n\t}\n\n\tiface.logger.DebugContext(\n\t\tctx, \"removed lease\",\n\t\t\"hostname\", l.Hostname,\n\t\t\"ip\", l.IP,\n\t\t\"mac\", l.HWAddr,\n\t\t\"is_static\", l.IsStatic,\n\t)\n\n\treturn nil\n}\n\n// ifaceForAddr returns the handled network interface for the given IP address,\n// or an error if no such interface exists.\n//\n// TODO(e.burkov):  Use a proper golibs error.\nfunc ifaceForAddr(\n\taddr netip.Addr,\n\tifaces4 dhcpInterfacesV4,\n\tifaces6 dhcpInterfacesV6,\n) (iface *netInterface, err error) {\n\tvar ok bool\n\tif addr.Is4() {\n\t\tiface, ok = ifaces4.find(addr)\n\t} else {\n\t\tiface, ok = ifaces6.find(addr)\n\t}\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"no interface for ip %s\", addr)\n\t}\n\n\treturn iface, nil\n}\n"
  },
  {
    "path": "internal/dhcpsvc/server_test.go",
    "content": "package dhcpsvc_test\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDHCPServer_AddLease(t *testing.T) {\n\tleasesPath := filepath.Join(t.TempDir(), \"leases.json\")\n\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\tDBFilePath: leasesPath,\n\t\tEnabled:    true,\n\t})\n\n\t// NOTE: Keep in sync with testdata.\n\tconst (\n\t\texistHost = \"host1\"\n\t\tnewHost   = \"host2\"\n\t\tipv6Host  = \"host3\"\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\texistIP = netip.MustParseAddr(\"192.0.2.2\")\n\t\tnewIP   = netip.MustParseAddr(\"192.0.2.3\")\n\t\tnewIPv6 = netip.MustParseAddr(\"2001:db8::2\")\n\n\t\texistMAC = errors.Must(net.ParseMAC(\"01:02:03:04:05:06\"))\n\t\tnewMAC   = errors.Must(net.ParseMAC(\"06:05:04:03:02:01\"))\n\t\tipv6MAC  = errors.Must(net.ParseMAC(\"02:03:04:05:06:07\"))\n\t)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\trequire.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{\n\t\tHostname: existHost,\n\t\tIP:       existIP,\n\t\tHWAddr:   existMAC,\n\t\tIsStatic: true,\n\t}))\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tlease      *dhcpsvc.Lease\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"outside_range\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: newHost,\n\t\t\tIP:       netip.MustParseAddr(\"1.2.3.4\"),\n\t\t\tHWAddr:   newMAC,\n\t\t},\n\t\twantErrMsg: \"adding lease: no interface for ip 1.2.3.4\",\n\t}, {\n\t\tname: \"duplicate_ip\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: newHost,\n\t\t\tIP:       existIP,\n\t\t\tHWAddr:   newMAC,\n\t\t},\n\t\twantErrMsg: \"adding lease: lease for ip \" + existIP.String() +\n\t\t\t\" already exists\",\n\t}, {\n\t\tname: \"duplicate_hostname\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: existHost,\n\t\t\tIP:       newIP,\n\t\t\tHWAddr:   newMAC,\n\t\t},\n\t\twantErrMsg: \"adding lease: lease for hostname \" + existHost +\n\t\t\t\" already exists\",\n\t}, {\n\t\tname: \"duplicate_hostname_case\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: strings.ToUpper(existHost),\n\t\t\tIP:       newIP,\n\t\t\tHWAddr:   newMAC,\n\t\t},\n\t\twantErrMsg: \"adding lease: lease for hostname \" +\n\t\t\tstrings.ToUpper(existHost) + \" already exists\",\n\t}, {\n\t\tname: \"duplicate_mac\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: newHost,\n\t\t\tIP:       newIP,\n\t\t\tHWAddr:   existMAC,\n\t\t},\n\t\twantErrMsg: \"adding lease: lease for mac \" + existMAC.String() +\n\t\t\t\" already exists\",\n\t}, {\n\t\tname: \"valid\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: newHost,\n\t\t\tIP:       newIP,\n\t\t\tHWAddr:   newMAC,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"valid_v6\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: ipv6Host,\n\t\t\tIP:       newIPv6,\n\t\t\tHWAddr:   ipv6MAC,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tctx = testutil.ContextWithTimeout(t, testTimeout)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease))\n\t\t})\n\t}\n\n\tassert.NotEmpty(t, srv.Leases())\n\tassert.FileExists(t, leasesPath)\n}\n\nfunc TestDHCPServer_index(t *testing.T) {\n\tleasesPath := newTempDB(t)\n\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\tDBFilePath: leasesPath,\n\t\tEnabled:    true,\n\t})\n\n\t// NOTE: Keep in sync with testdata.\n\tconst (\n\t\thost1 = \"host1\"\n\t\thost2 = \"host2\"\n\t\thost3 = \"host3\"\n\t\thost4 = \"host4\"\n\t\thost5 = \"host5\"\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\tip1 = netip.MustParseAddr(\"192.0.2.2\")\n\t\tip2 = netip.MustParseAddr(\"192.0.2.3\")\n\t\tip3 = netip.MustParseAddr(\"198.51.100.3\")\n\t\tip4 = netip.MustParseAddr(\"198.51.100.4\")\n\n\t\tmac1 = errors.Must(net.ParseMAC(\"01:02:03:04:05:06\"))\n\t\tmac2 = errors.Must(net.ParseMAC(\"06:05:04:03:02:01\"))\n\t\tmac3 = errors.Must(net.ParseMAC(\"02:03:04:05:06:07\"))\n\t)\n\n\tt.Run(\"ip_idx\", func(t *testing.T) {\n\t\tassert.Equal(t, ip1, srv.IPByHost(host1))\n\t\tassert.Equal(t, ip2, srv.IPByHost(host2))\n\t\tassert.Equal(t, ip3, srv.IPByHost(host3))\n\t\tassert.Equal(t, ip4, srv.IPByHost(host4))\n\t\tassert.Zero(t, srv.IPByHost(host5))\n\t})\n\n\tt.Run(\"name_idx\", func(t *testing.T) {\n\t\tassert.Equal(t, host1, srv.HostByIP(ip1))\n\t\tassert.Equal(t, host2, srv.HostByIP(ip2))\n\t\tassert.Equal(t, host3, srv.HostByIP(ip3))\n\t\tassert.Equal(t, host4, srv.HostByIP(ip4))\n\t\tassert.Zero(t, srv.HostByIP(netip.Addr{}))\n\t})\n\n\tt.Run(\"mac_idx\", func(t *testing.T) {\n\t\tassert.Equal(t, mac1, srv.MACByIP(ip1))\n\t\tassert.Equal(t, mac2, srv.MACByIP(ip2))\n\t\tassert.Equal(t, mac3, srv.MACByIP(ip3))\n\t\tassert.Equal(t, mac1, srv.MACByIP(ip4))\n\t\tassert.Zero(t, srv.MACByIP(netip.Addr{}))\n\t})\n}\n\nfunc TestDHCPServer_UpdateStaticLease(t *testing.T) {\n\tleasesPath := newTempDB(t)\n\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\tDBFilePath: leasesPath,\n\t\tEnabled:    true,\n\t})\n\n\t// NOTE: Keep in sync with testdata.\n\tconst (\n\t\thost1 = \"host1\"\n\t\thost2 = \"host2\"\n\t\thost3 = \"host3\"\n\t\thost4 = \"host4\"\n\t\thost5 = \"host5\"\n\t\thost6 = \"host6\"\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\tip1 = netip.MustParseAddr(\"192.0.2.2\")\n\t\tip2 = netip.MustParseAddr(\"192.0.2.3\")\n\t\tip3 = netip.MustParseAddr(\"192.0.2.4\")\n\t\tip4 = netip.MustParseAddr(\"2001:db8::2\")\n\n\t\tmac1 = errors.Must(net.ParseMAC(\"01:02:03:04:05:06\"))\n\t\tmac2 = errors.Must(net.ParseMAC(\"06:05:04:03:02:01\"))\n\t\tmac3 = errors.Must(net.ParseMAC(\"06:05:04:03:02:02\"))\n\t)\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tlease      *dhcpsvc.Lease\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"outside_range\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host1,\n\t\t\tIP:       netip.MustParseAddr(\"1.2.3.4\"),\n\t\t\tHWAddr:   mac1,\n\t\t},\n\t\twantErrMsg: \"updating static lease: no interface for ip 1.2.3.4\",\n\t}, {\n\t\tname: \"not_found\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host3,\n\t\t\tIP:       ip3,\n\t\t\tHWAddr:   mac2,\n\t\t},\n\t\twantErrMsg: \"updating static lease: no lease for mac \" + mac2.String(),\n\t}, {\n\t\tname: \"duplicate_ip\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host1,\n\t\t\tIP:       ip2,\n\t\t\tHWAddr:   mac1,\n\t\t},\n\t\twantErrMsg: \"updating static lease: lease for ip \" + ip2.String() +\n\t\t\t\" already exists\",\n\t}, {\n\t\tname: \"duplicate_hostname\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host2,\n\t\t\tIP:       ip1,\n\t\t\tHWAddr:   mac1,\n\t\t},\n\t\twantErrMsg: \"updating static lease: lease for hostname \" + host2 +\n\t\t\t\" already exists\",\n\t}, {\n\t\tname: \"duplicate_hostname_case\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: strings.ToUpper(host2),\n\t\t\tIP:       ip1,\n\t\t\tHWAddr:   mac1,\n\t\t},\n\t\twantErrMsg: \"updating static lease: lease for hostname \" +\n\t\t\tstrings.ToUpper(host2) + \" already exists\",\n\t}, {\n\t\tname: \"valid\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host3,\n\t\t\tIP:       ip3,\n\t\t\tHWAddr:   mac1,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"valid_v6\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host6,\n\t\t\tIP:       ip4,\n\t\t\tHWAddr:   mac3,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease))\n\t\t})\n\t}\n\n\tassert.FileExists(t, leasesPath)\n}\n\nfunc TestDHCPServer_RemoveLease(t *testing.T) {\n\tleasesPath := newTempDB(t)\n\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\tDBFilePath: leasesPath,\n\t\tEnabled:    true,\n\t})\n\n\t// NOTE: Keep in sync with testdata.\n\tconst (\n\t\thost1 = \"host1\"\n\t\thost2 = \"host2\"\n\t\thost3 = \"host3\"\n\t)\n\n\t// NOTE: Keep in sync with testdata.\n\tvar (\n\t\texistIP = netip.MustParseAddr(\"192.0.2.2\")\n\t\tnewIP   = netip.MustParseAddr(\"192.0.2.3\")\n\t\tnewIPv6 = netip.MustParseAddr(\"2001:db8::2\")\n\n\t\texistMAC = errors.Must(net.ParseMAC(\"01:02:03:04:05:06\"))\n\t\tnewMAC   = errors.Must(net.ParseMAC(\"02:03:04:05:06:07\"))\n\t\tipv6MAC  = errors.Must(net.ParseMAC(\"06:05:04:03:02:01\"))\n\t)\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tlease      *dhcpsvc.Lease\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"not_found_mac\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host1,\n\t\t\tIP:       existIP,\n\t\t\tHWAddr:   newMAC,\n\t\t},\n\t\twantErrMsg: \"removing lease: no lease for mac \" + newMAC.String(),\n\t}, {\n\t\tname: \"not_found_ip\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host1,\n\t\t\tIP:       newIP,\n\t\t\tHWAddr:   existMAC,\n\t\t},\n\t\twantErrMsg: \"removing lease: no lease for ip \" + newIP.String(),\n\t}, {\n\t\tname: \"not_found_host\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host2,\n\t\t\tIP:       existIP,\n\t\t\tHWAddr:   existMAC,\n\t\t},\n\t\twantErrMsg: \"removing lease: no lease for hostname \" + host2,\n\t}, {\n\t\tname: \"valid\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host1,\n\t\t\tIP:       existIP,\n\t\t\tHWAddr:   existMAC,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"valid_v6\",\n\t\tlease: &dhcpsvc.Lease{\n\t\t\tHostname: host3,\n\t\t\tIP:       newIPv6,\n\t\t\tHWAddr:   ipv6MAC,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(ctx, tc.lease))\n\t\t})\n\t}\n\n\tassert.FileExists(t, leasesPath)\n\tassert.Empty(t, srv.Leases())\n}\n\nfunc TestDHCPServer_Reset(t *testing.T) {\n\tleasesPath := newTempDB(t)\n\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\tDBFilePath: leasesPath,\n\t\tEnabled:    true,\n\t})\n\n\tconst leasesNum = 4\n\n\trequire.Len(t, srv.Leases(), leasesNum)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\trequire.NoError(t, srv.Reset(ctx))\n\n\tassert.FileExists(t, leasesPath)\n\tassert.Empty(t, srv.Leases())\n}\n\nfunc TestServer_Leases(t *testing.T) {\n\tleasesPath := newTempDB(t)\n\tsrv := newTestDHCPServer(t, &dhcpsvc.Config{\n\t\tDBFilePath: leasesPath,\n\t\tEnabled:    true,\n\t})\n\n\texpiry, err := time.Parse(time.RFC3339, \"2042-01-02T03:04:05Z\")\n\trequire.NoError(t, err)\n\n\twantLeases := []*dhcpsvc.Lease{{\n\t\tExpiry:   expiry,\n\t\tIP:       netip.MustParseAddr(\"192.0.2.3\"),\n\t\tHostname: \"example.host\",\n\t\tHWAddr:   errors.Must(net.ParseMAC(\"AA:AA:AA:AA:AA:AA\")),\n\t\tIsStatic: false,\n\t}, {\n\t\tExpiry:   time.Time{},\n\t\tIP:       netip.MustParseAddr(\"192.0.2.4\"),\n\t\tHostname: \"example.static.host\",\n\t\tHWAddr:   errors.Must(net.ParseMAC(\"BB:BB:BB:BB:BB:BB\")),\n\t\tIsStatic: true,\n\t}}\n\tassert.ElementsMatch(t, wantLeases, srv.Leases())\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.2\",\n      \"hostname\": \"host1\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"2001:db8::2\",\n      \"hostname\": \"host3\",\n      \"mac\": \"06:05:04:03:02:01\",\n      \"static\": true\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.2\",\n      \"hostname\": \"host1\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.3\",\n      \"hostname\": \"host2\",\n      \"mac\": \"06:05:04:03:02:01\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"2001:db8::2\",\n      \"hostname\": \"host3\",\n      \"mac\": \"02:03:04:05:06:07\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"2001:db8::3\",\n      \"hostname\": \"host4\",\n      \"mac\": \"06:05:04:03:02:02\",\n      \"static\": true\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_ServeEther4_decline/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"2025-01-01T10:01:01Z\",\n      \"ip\": \"192.0.2.102\",\n      \"hostname\": \"success\",\n      \"mac\": \"02:03:04:05:06:07\",\n      \"static\": false\n    },\n    {\n      \"expires\": \"2025-01-01T10:01:01Z\",\n      \"ip\": \"192.0.2.103\",\n      \"hostname\": \"mismatch\",\n      \"mac\": \"03:04:05:06:07:08\",\n      \"static\": false\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_ServeEther4_discover/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"2025-01-01T10:01:01Z\",\n      \"ip\": \"192.0.2.102\",\n      \"hostname\": \"dynamic4\",\n      \"mac\": \"02:03:04:05:06:07\",\n      \"static\": false\n    },\n    {\n      \"expires\": \"2025-01-01T01:01:01Z\",\n      \"ip\": \"192.0.2.103\",\n      \"hostname\": \"expired4\",\n      \"mac\": \"03:04:05:06:07:08\",\n      \"static\": false\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.101\",\n      \"hostname\": \"static4\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_ServeEther4_discoverExpired/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"2025-01-01T01:01:00Z\",\n      \"ip\": \"192.0.2.100\",\n      \"hostname\": \"dynamic4\",\n      \"mac\": \"02:03:04:05:06:07\",\n      \"static\": false\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_ServeEther4_release/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"2025-01-01T10:01:01Z\",\n      \"ip\": \"192.0.2.102\",\n      \"hostname\": \"success\",\n      \"mac\": \"02:03:04:05:06:07\",\n      \"static\": false\n    },\n    {\n      \"expires\": \"2025-01-01T10:01:01Z\",\n      \"ip\": \"192.0.2.103\",\n      \"hostname\": \"mismatch\",\n      \"mac\": \"03:04:05:06:07:08\",\n      \"static\": false\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_ServeEther4_requestInitReboot/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.101\",\n      \"hostname\": \"static4\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"2025-01-01T10:01:01Z\",\n      \"ip\": \"192.0.2.102\",\n      \"hostname\": \"dynamic4\",\n      \"mac\": \"02:03:04:05:06:07\",\n      \"static\": false\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_ServeEther4_requestRenew/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.101\",\n      \"hostname\": \"static4\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"2025-01-01T10:01:01Z\",\n      \"ip\": \"192.0.2.102\",\n      \"hostname\": \"dynamic4\",\n      \"mac\": \"02:03:04:05:06:07\",\n      \"static\": false\n    },\n    {\n      \"expires\": \"2025-01-01T01:01:01Z\",\n      \"ip\": \"192.0.2.103\",\n      \"hostname\": \"expired4\",\n      \"mac\": \"03:04:05:06:07:08\",\n      \"static\": false\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_ServeEther4_requestSelecting/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"2025-01-01T01:01:01Z\",\n      \"ip\": \"192.0.2.103\",\n      \"hostname\": \"expired4\",\n      \"mac\": \"03:04:05:06:07:08\",\n      \"static\": false\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.101\",\n      \"hostname\": \"static4\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.2\",\n      \"hostname\": \"host1\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.3\",\n      \"hostname\": \"host2\",\n      \"mac\": \"01:02:03:04:05:07\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"2001:db8::2\",\n      \"hostname\": \"host4\",\n      \"mac\": \"06:05:04:03:02:02\",\n      \"static\": true\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.2\",\n      \"hostname\": \"host1\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"192.0.2.3\",\n      \"hostname\": \"host2\",\n      \"mac\": \"06:05:04:03:02:01\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"198.51.100.3\",\n      \"hostname\": \"host3\",\n      \"mac\": \"02:03:04:05:06:07\",\n      \"static\": true\n    },\n    {\n      \"expires\": \"\",\n      \"ip\": \"198.51.100.4\",\n      \"hostname\": \"host4\",\n      \"mac\": \"01:02:03:04:05:06\",\n      \"static\": true\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/testdata/TestServer_Leases/leases.json",
    "content": "{\n  \"leases\": [\n    {\n      \"expires\": \"2042-01-02T03:04:05Z\",\n      \"ip\": \"192.0.2.3\",\n      \"hostname\": \"example.host\",\n      \"mac\": \"AA:AA:AA:AA:AA:AA\",\n      \"static\": false\n    },\n    {\n      \"ip\": \"192.0.2.4\",\n      \"hostname\": \"example.static.host\",\n      \"mac\": \"BB:BB:BB:BB:BB:BB\",\n      \"static\": true\n    }\n  ],\n  \"version\": 1\n}\n"
  },
  {
    "path": "internal/dhcpsvc/v4.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n)\n\n// IPv4Config is the interface-specific configuration for DHCPv4.\ntype IPv4Config struct {\n\t// Clock is used to get current time.  It should not be nil.\n\tClock timeutil.Clock\n\n\t// GatewayIP is the IPv4 address of the network's gateway.  It is used as\n\t// the default gateway for DHCP clients and also used for calculating the\n\t// network-specific broadcast address.  It should be a valid IPv4 address,\n\t// should be within the subnet, and should be outside the address range.\n\tGatewayIP netip.Addr\n\n\t// SubnetMask is the IPv4 subnet mask of the network.  It should be a valid\n\t// IPv4 CIDR (i.e. all 1s followed by all 0s).\n\tSubnetMask netip.Addr\n\n\t// RangeStart is the first address in the range to assign to DHCP clients.\n\t// It should be a valid IPv4 address, should be within the subnet, and\n\t// should be less or equal to RangeEnd.\n\tRangeStart netip.Addr\n\n\t// RangeEnd is the last address in the range to assign to DHCP clients.  It\n\t// should be a valid IPv4 address, should be within the subnet, and should\n\t// be greater or equal to RangeStart.\n\tRangeEnd netip.Addr\n\n\t// Options is the list of explicitly configured DHCP options to send to\n\t// clients.  Options with nil Data field are removed from responses.\n\t//\n\t// TODO(e.burkov):  Validate.\n\tOptions layers.DHCPOptions\n\n\t// LeaseDuration is the TTL of a DHCP lease.  It should be positive.\n\tLeaseDuration time.Duration\n\n\t// Enabled is the state of the DHCPv4 service, whether it is enabled or not\n\t// on the specific interface.\n\tEnabled bool\n}\n\n// type check\nvar _ validate.Interface = (*IPv4Config)(nil)\n\n// Validate implements the [validate.Interface] interface for *IPv4Config.\nfunc (c *IPv4Config) Validate() (err error) {\n\tif c == nil {\n\t\treturn errors.ErrNoValue\n\t} else if !c.Enabled {\n\t\t// Don't validate the configuration for disabled interface.\n\t\treturn nil\n\t}\n\n\terrs := []error{\n\t\tvalidate.NotNilInterface(\"clock\", c.Clock),\n\t\tvalidate.Positive(\"lease duration\", c.LeaseDuration),\n\t}\n\n\terrs = c.validateSubnet(errs)\n\n\treturn errors.Join(errs...)\n}\n\n// validateSubnet validates the subnet configuration.\n//\n// TODO(e.burkov):  Use [validate].\nfunc (c *IPv4Config) validateSubnet(orig []error) (errs []error) {\n\terrs = orig\n\n\tif !c.GatewayIP.Is4() {\n\t\terr := newMustErr(\"gateway ip\", \"be a valid ipv4\", c.GatewayIP)\n\t\terrs = append(errs, err)\n\t}\n\n\tif !c.SubnetMask.Is4() {\n\t\terr := newMustErr(\"subnet mask\", \"be a valid ipv4 cidr mask\", c.SubnetMask)\n\t\terrs = append(errs, err)\n\t}\n\n\tif !c.RangeStart.Is4() {\n\t\terr := newMustErr(\"range start\", \"be a valid ipv4\", c.RangeStart)\n\t\terrs = append(errs, err)\n\t}\n\n\tif !c.RangeEnd.Is4() {\n\t\terr := newMustErr(\"range end\", \"be a valid ipv4\", c.RangeEnd)\n\t\terrs = append(errs, err)\n\t}\n\n\tmaskLen, _ := net.IPMask(c.SubnetMask.AsSlice()).Size()\n\tsubnet := netip.PrefixFrom(c.GatewayIP, maskLen)\n\n\tswitch {\n\tcase !subnet.Contains(c.RangeStart):\n\t\terrs = append(errs, fmt.Errorf(\"range start %s is not within %s\", c.RangeStart, subnet))\n\tcase !subnet.Contains(c.RangeEnd):\n\t\terrs = append(errs, fmt.Errorf(\"range end %s is not within %s\", c.RangeEnd, subnet))\n\t}\n\n\taddrSpace, err := newIPRange(c.RangeStart, c.RangeEnd)\n\tif err != nil {\n\t\terrs = append(errs, err)\n\t} else if addrSpace.contains(c.GatewayIP) {\n\t\terrs = append(errs, fmt.Errorf(\"gateway ip %s in the ip range %s\", c.GatewayIP, addrSpace))\n\t}\n\n\treturn errs\n}\n\n// dhcpInterfaceV4 is a DHCP interface for IPv4 address family.\ntype dhcpInterfaceV4 struct {\n\t// common is the common part of any network interface within the DHCP\n\t// server.\n\tcommon *netInterface\n\n\t// clock used to get current time.\n\t//\n\t// TODO(e.burkov):  Move to [netInterface].\n\tclock timeutil.Clock\n\n\t// addrChecker checks addresses for availability.\n\taddrChecker addressChecker\n\n\t// gateway is the IP address of the network gateway.\n\tgateway netip.Addr\n\n\t// subnet is the network subnet of the interface.\n\tsubnet netip.Prefix\n\n\t// implicitOpts are the options listed in Appendix A of RFC 2131 and\n\t// initialized with default values.  It must not have intersections with\n\t// explicitOpts.\n\timplicitOpts layers.DHCPOptions\n\n\t// explicitOpts are the user-configured options.  It must not have\n\t// intersections with implicitOpts.  Options with nil Data field are removed\n\t// from responses.\n\texplicitOpts layers.DHCPOptions\n}\n\n// newDHCPInterfaceV4 creates a new DHCP interface for IPv4 address family with\n// the given configuration.  If the interface is disabled, it returns nil.\n// baseLogger must not be nil, name must be a valid network interface name, conf\n// must be valid.\nfunc (srv *DHCPServer) newDHCPInterfaceV4(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tname string,\n\tconf *IPv4Config,\n) (iface *dhcpInterfaceV4) {\n\tif !conf.Enabled {\n\t\tbaseLogger.DebugContext(ctx, \"disabled\")\n\n\t\treturn nil\n\t}\n\n\t// TODO(e.burkov):  Add a helper for converting [netip.Addr] to subnet mask\n\t// to [netutil].\n\tmaskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()\n\taddrSpace, _ := newIPRange(conf.RangeStart, conf.RangeEnd)\n\n\tiface = &dhcpInterfaceV4{\n\t\t// TODO(e.burkov):  Use an ICMP implementation.\n\t\taddrChecker: noopAddressChecker{},\n\t\tgateway:     conf.GatewayIP,\n\t\tclock:       conf.Clock,\n\t\tsubnet:      netip.PrefixFrom(conf.GatewayIP, maskLen),\n\t\tcommon: &netInterface{\n\t\t\tlogger:        baseLogger,\n\t\t\tindexMu:       srv.leasesMu,\n\t\t\tindex:         srv.leases,\n\t\t\tleases:        map[macKey]*Lease{},\n\t\t\tleasedOffsets: newBitSet(),\n\t\t\tname:          name,\n\t\t\taddrSpace:     addrSpace,\n\t\t\tleaseTTL:      conf.LeaseDuration,\n\t\t},\n\t}\n\tiface.implicitOpts, iface.explicitOpts = conf.options(ctx, baseLogger)\n\n\treturn iface\n}\n\n// updateLease updates l in the database.  l must be valid and not expired.\nfunc (iface *dhcpInterfaceV4) updateLease(\n\tctx context.Context,\n\tl *Lease,\n) (err error) {\n\treturn iface.common.index.update(ctx, iface.common.logger, l, iface.common)\n}\n\n// respondOffer sends a DHCPOFFER message to the client.  req, fd, and l must\n// not be nil.\nfunc (iface *dhcpInterfaceV4) respondOffer(\n\tctx context.Context,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n\tl *Lease,\n) {\n\tresp := iface.buildResponse(req, l, fd.device, layers.DHCPMsgTypeOffer)\n\n\terr := respond4(fd, resp)\n\tif err != nil {\n\t\tiface.common.logger.ErrorContext(ctx, \"writing offer\", \"error\", err)\n\t}\n}\n\n// respondACK sends a DHCPACK message to the client.  req, fd, and l must not be\n// nil.\n//\n// TODO(e.burkov):  Implement according to RFC, answer to DHCPINFORM\n// differently, when it's supported.\nfunc (iface *dhcpInterfaceV4) respondACK(\n\tctx context.Context,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n\tl *Lease,\n) {\n\tresp := iface.buildResponse(req, l, fd.device, layers.DHCPMsgTypeAck)\n\tif err := respond4(fd, resp); err != nil {\n\t\tiface.common.logger.ErrorContext(ctx, \"writing ack\", \"error\", err)\n\t}\n}\n\n// v4OptionMessageTypeNAK is a DHCP option for DHCPNAK message type.\nvar v4OptionMessageTypeNAK = layers.NewDHCPOption(\n\tlayers.DHCPOptMessageType,\n\t[]byte{byte(layers.DHCPMsgTypeNak)},\n)\n\n// respondNAK constructs and sends a DHCPNAK message to the client.  req, fd,\n// and resp must not be nil.\n//\n// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.\nfunc (iface *dhcpInterfaceV4) respondNAK(\n\tctx context.Context,\n\treq *layers.DHCPv4,\n\tfd *frameData,\n) {\n\tresp := &layers.DHCPv4{\n\t\tOperation:    layers.DHCPOpReply,\n\t\tHardwareType: layers.LinkTypeEthernet,\n\t\tHardwareLen:  uint8(len(req.ClientHWAddr)),\n\t\tXid:          req.Xid,\n\t\tRelayAgentIP: req.RelayAgentIP,\n\t\tClientHWAddr: req.ClientHWAddr,\n\t\tOptions: layers.DHCPOptions{\n\t\t\tv4OptionMessageTypeNAK,\n\t\t\tlayers.NewDHCPOption(layers.DHCPOptServerID, iface.gateway.AsSlice()),\n\t\t\t// TODO(e.burkov):  According to RFC 2131 we should add a message.\n\t\t},\n\t}\n\n\tif err := respond4(fd, resp); err != nil {\n\t\tiface.common.logger.ErrorContext(ctx, \"writing nak\", \"error\", err)\n\t}\n}\n\n// buildResponse builds a DHCP response message with the given message type.\n// req and l must not be nil.  msgType must be one of:\n//   - [layers.DHCPMsgTypeOffer]\n//   - [layers.DHCPMsgTypeAck]\nfunc (iface *dhcpInterfaceV4) buildResponse(\n\treq *layers.DHCPv4,\n\tl *Lease,\n\tnd NetworkDevice,\n\tmsgType layers.DHCPMsgType,\n) (resp *layers.DHCPv4) {\n\tresp = &layers.DHCPv4{\n\t\tOperation:    layers.DHCPOpReply,\n\t\tHardwareType: layers.LinkTypeEthernet,\n\t\tHardwareLen:  uint8(len(req.ClientHWAddr)),\n\t\tXid:          req.Xid,\n\t\tClientHWAddr: req.ClientHWAddr,\n\t\tYourClientIP: l.IP.AsSlice(),\n\t}\n\n\tresp.Options = append(\n\t\tresp.Options,\n\t\tlayers.NewDHCPOption(layers.DHCPOptMessageType, []byte{byte(msgType)}),\n\t\tlayers.NewDHCPOption(layers.DHCPOptServerID, nd.Addresses()[0].AsSlice()),\n\t)\n\n\tiface.appendLeaseTime(resp, l)\n\tiface.updateOptions(req, resp)\n\n\t// Add hostname option if the lease has a hostname.\n\t//\n\t// TODO(e.burkov):  Lease should always has a hostname, investigate when\n\t// it isn't the case.\n\tif l.Hostname != \"\" {\n\t\tresp.Options = append(\n\t\t\tresp.Options,\n\t\t\tlayers.NewDHCPOption(layers.DHCPOptHostname, []byte(l.Hostname)),\n\t\t)\n\t}\n\n\treturn resp\n}\n\n// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.\ntype dhcpInterfacesV4 []*dhcpInterfaceV4\n\n// find returns the first network interface within ifaces containing ip.  It\n// returns false if there is no such interface.  ip must be valid.\nfunc (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {\n\ti := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {\n\t\treturn iface.subnet.Contains(ip)\n\t})\n\tif i < 0 {\n\t\treturn nil, false\n\t}\n\n\treturn ifaces[i].common, true\n}\n\n// allocateLease allocates a new lease for the MAC address.  If there are no IP\n// addresses left, both l and err are nil.  mac must be a valid according to\n// [netutil.ValidateMAC].\n//\n// TODO(e.burkov):  Pass the precalculated macKey.\nfunc (iface *dhcpInterfaceV4) allocateLease(\n\tctx context.Context,\n\tmac net.HardwareAddr,\n) (l *Lease, err error) {\n\tfor {\n\t\tl, err = iface.reserveLease(ctx, mac)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvar ok bool\n\t\tok, err = iface.addrChecker.IsAvailable(l.IP)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"checking address availability: %w\", err)\n\t\t}\n\n\t\tif ok {\n\t\t\tiface.common.leases[macToKey(mac)] = l\n\n\t\t\toff, _ := iface.common.addrSpace.offset(l.IP)\n\t\t\tiface.common.leasedOffsets.set(off, true)\n\n\t\t\treturn l, nil\n\t\t}\n\n\t\tiface.common.logger.DebugContext(ctx, \"address not available\", \"ip\", l.IP)\n\n\t\terr = iface.common.blockLease(ctx, l, iface.clock)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"blocking unavailable address: %w\", err)\n\t\t}\n\t}\n}\n\n// reserveLease reserves a lease for a client by its MAC-address.  l is nil if a\n// new lease can't be allocated.  mac must be a valid according to\n// [netutil.ValidateMAC].  index mutex must be locked.\nfunc (iface *dhcpInterfaceV4) reserveLease(\n\tctx context.Context,\n\tmac net.HardwareAddr,\n) (l *Lease, err error) {\n\tnextIP := iface.common.nextIP()\n\tif nextIP != (netip.Addr{}) {\n\t\tl = &Lease{\n\t\t\tHWAddr: slices.Clone(mac),\n\t\t\tIP:     nextIP,\n\t\t\tExpiry: iface.clock.Now().Add(iface.common.leaseTTL),\n\t\t}\n\n\t\treturn l, nil\n\t}\n\n\tl = iface.common.findExpiredLease(iface.clock.Now())\n\tif l == nil {\n\t\treturn nil, errors.Error(\"no addresses available to lease\")\n\t}\n\n\t// TODO(e.burkov):  Move validation from index methods into server's\n\t// methods and use index here.\n\tdelete(iface.common.leases, macToKey(l.HWAddr))\n\n\tidx := iface.common.index\n\tdelete(idx.byAddr, l.IP)\n\tdelete(idx.byName, strings.ToLower(l.Hostname))\n\n\terr = idx.dbStore(ctx, iface.common.logger)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tl.HWAddr = slices.Clone(mac)\n\tl.Hostname = \"\"\n\tl.IsStatic = false\n\tl.updateExpiry(iface.clock, iface.common.leaseTTL)\n\n\tiface.common.leases[macToKey(mac)] = l\n\n\treturn l, nil\n}\n\n// updateAndRespond updates the lease and sends a DHCPACK or DHCPNAK response to\n// the client according to the update result.  req must be a DHCPREQUEST\n// message, lease, l, and fd must not be nil.\nfunc (iface *dhcpInterfaceV4) updateAndRespond(\n\tctx context.Context,\n\tl *slog.Logger,\n\treq *layers.DHCPv4,\n\tlease *Lease,\n\tfd *frameData,\n) {\n\tlease.Hostname = cmp.Or(hostname4(req), lease.Hostname)\n\n\terr := iface.updateLease(ctx, lease)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"init-reboot request failed\", slogutil.KeyError, err)\n\t\tiface.respondNAK(ctx, req, fd)\n\n\t\treturn\n\t}\n\n\tiface.respondACK(ctx, req, fd, lease)\n}\n\nconst (\n\t// IPv4DefaultTTL is the default Time to Live value in seconds as\n\t// recommended by RFC 1700.\n\tIPv4DefaultTTL = 64\n\n\t// IPProtoVersion is the IP internetwork general protocol version number as\n\t// defined by RFC 1700.\n\tIPProtoVersion = 4\n)\n\n// Port numbers for DHCPv4.\n//\n// See RFC 2131 Section 4.1.\nconst (\n\t// ServerPortV4 is the standard DHCPv4 server port.\n\tServerPortV4 layers.UDPPort = 67\n\n\t// ClientPortV4 is the standard DHCPv4 client port.\n\tClientPortV4 layers.UDPPort = 68\n)\n\n// respond4 sends a DHCPv4 response.  fd and resp must not be nil.\nfunc respond4(fd *frameData, resp *layers.DHCPv4) (err error) {\n\t// TODO(e.burkov):  Use pools for buffer and layers.\n\tbuf := gopacket.NewSerializeBuffer()\n\n\teth := &layers.Ethernet{\n\t\tSrcMAC:       fd.ether.SrcMAC,\n\t\tDstMAC:       fd.ether.DstMAC,\n\t\tEthernetType: layers.EthernetTypeIPv4,\n\t}\n\tip := &layers.IPv4{\n\t\tVersion:  IPProtoVersion,\n\t\tTTL:      IPv4DefaultTTL,\n\t\tSrcIP:    net.IPv4zero.To4(),\n\t\tDstIP:    net.IPv4bcast.To4(),\n\t\tProtocol: layers.IPProtocolUDP,\n\t}\n\tudp := &layers.UDP{\n\t\tSrcPort: ServerPortV4,\n\t\tDstPort: ClientPortV4,\n\t}\n\t_ = udp.SetNetworkLayerForChecksum(ip)\n\n\topts := gopacket.SerializeOptions{\n\t\tFixLengths:       true,\n\t\tComputeChecksums: true,\n\t}\n\n\terr = gopacket.SerializeLayers(buf, opts, eth, ip, udp, resp)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constructing dhcp v4 response: %w\", err)\n\t}\n\n\treturn fd.device.WritePacketData(buf.Bytes())\n}\n"
  },
  {
    "path": "internal/dhcpsvc/v6.go",
    "content": "package dhcpsvc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n\t\"github.com/google/gopacket/layers\"\n)\n\n// IPv6Config is the interface-specific configuration for DHCPv6.\ntype IPv6Config struct {\n\t// RangeStart is the first address in the range to assign to DHCP clients.\n\t// It should be a valid IPv6 address.\n\tRangeStart netip.Addr\n\n\t// Options is the list of explicit DHCP options to send to clients.  The\n\t// options with zero length are treated as deletions of the corresponding\n\t// options, either implicit or explicit.\n\tOptions layers.DHCPv6Options\n\n\t// LeaseDuration is the TTL of a DHCP lease.  It should be positive.\n\tLeaseDuration time.Duration\n\n\t// RASlaacOnly defines whether the DHCP clients should only use SLAAC for\n\t// address assignment.\n\tRASLAACOnly bool\n\n\t// RAAllowSlaac defines whether the DHCP clients may use SLAAC for address\n\t// assignment.\n\tRAAllowSLAAC bool\n\n\t// Enabled is the state of the DHCPv6 service, whether it is enabled or not\n\t// on the specific interface.\n\tEnabled bool\n}\n\n// type check\nvar _ validate.Interface = (*IPv6Config)(nil)\n\n// Validate implements the [validate.Interface] interface for *IPv6Config.\nfunc (c *IPv6Config) Validate() (err error) {\n\tif c == nil {\n\t\treturn errors.ErrNoValue\n\t} else if !c.Enabled {\n\t\treturn nil\n\t}\n\n\tvar errs []error\n\n\tif !c.RangeStart.Is6() {\n\t\terr = fmt.Errorf(\"range start %s should be a valid ipv6\", c.RangeStart)\n\t\terrs = append(errs, err)\n\t}\n\n\tif c.LeaseDuration <= 0 {\n\t\terr = fmt.Errorf(\"lease duration %s must be positive\", c.LeaseDuration)\n\t\terrs = append(errs, err)\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// dhcpInterfaceV6 is a DHCP interface for IPv6 address family.\ntype dhcpInterfaceV6 struct {\n\t// common is the common part of any network interface within the DHCP\n\t// server.\n\tcommon *netInterface\n\n\t// rangeStart is the first IP address in the range.\n\trangeStart netip.Addr\n\n\t// implicitOpts are the DHCPv6 options listed in RFC 8415 (and others) and\n\t// initialized with default values.  It must not have intersections with\n\t// explicitOpts.\n\timplicitOpts layers.DHCPv6Options\n\n\t// explicitOpts are the user-configured options.  It must not have\n\t// intersections with implicitOpts.\n\texplicitOpts layers.DHCPv6Options\n\n\t// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO\n\t// flags.\n\traSLAACOnly bool\n\n\t// raAllowSLAAC defines if DHCP should send ICMPv6.RA packets with MO flags.\n\traAllowSLAAC bool\n}\n\n// newDHCPInterfaceV6 creates a new DHCP interface for IPv6 address family with\n// the given configuration.  If the interface is disabled, it returns nil.  conf\n// must be valid.\nfunc (srv *DHCPServer) newDHCPInterfaceV6(\n\tctx context.Context,\n\tl *slog.Logger,\n\tname string,\n\tconf *IPv6Config,\n) (iface *dhcpInterfaceV6) {\n\tif !conf.Enabled {\n\t\tl.DebugContext(ctx, \"disabled\")\n\n\t\treturn nil\n\t}\n\n\tiface = &dhcpInterfaceV6{\n\t\trangeStart: conf.RangeStart,\n\t\tcommon: &netInterface{\n\t\t\tlogger:   l,\n\t\t\tleases:   map[macKey]*Lease{},\n\t\t\tindexMu:  srv.leasesMu,\n\t\t\tindex:    srv.leases,\n\t\t\tname:     name,\n\t\t\tleaseTTL: conf.LeaseDuration,\n\t\t},\n\t\traSLAACOnly:  conf.RASLAACOnly,\n\t\traAllowSLAAC: conf.RAAllowSLAAC,\n\t}\n\tiface.implicitOpts, iface.explicitOpts = conf.options(ctx, l)\n\n\treturn iface\n}\n\n// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.\ntype dhcpInterfacesV6 []*dhcpInterfaceV6\n\n// find returns the first network interface within ifaces containing ip.  It\n// returns false if there is no such interface.\nfunc (ifaces dhcpInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {\n\t// prefLen is the length of prefix to match ip against.\n\t//\n\t// TODO(e.burkov):  DHCPv6 inherits the weird behavior of legacy\n\t// implementation where the allocated range constrained by the first address\n\t// and the first address with last byte set to 0xff.  Proper prefixes should\n\t// be used instead.\n\tconst prefLen = netutil.IPv6BitLen - 8\n\n\ti := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV6) (contains bool) {\n\t\treturn !ip.Less(iface.rangeStart) &&\n\t\t\tnetip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)\n\t})\n\tif i < 0 {\n\t\treturn nil, false\n\t}\n\n\treturn ifaces[i].common, true\n}\n\n// options returns the implicit and explicit options for the interface.  The two\n// lists are disjoint and the implicit options are initialized with default\n// values.\n//\n// TODO(e.burkov):  Add implicit options according to RFC.\nfunc (c *IPv6Config) options(ctx context.Context, l *slog.Logger) (imp, exp layers.DHCPv6Options) {\n\t// Set default values of host configuration parameters listed in RFC 8415.\n\timp = layers.DHCPv6Options{}\n\tslices.SortFunc(imp, compareV6OptionCodes)\n\n\t// Set values for explicitly configured options.\n\tfor _, e := range c.Options {\n\t\ti, found := slices.BinarySearchFunc(imp, e, compareV6OptionCodes)\n\t\tif found {\n\t\t\timp = slices.Delete(imp, i, i+1)\n\t\t}\n\n\t\texp = append(exp, e)\n\t}\n\n\tl.DebugContext(ctx, \"options\", \"implicit\", imp, \"explicit\", exp)\n\n\treturn imp, exp\n}\n\n// compareV6OptionCodes compares option codes of a and b.\nfunc compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) {\n\treturn int(a.Code) - int(b.Code)\n}\n"
  },
  {
    "path": "internal/dnsforward/access.go",
    "content": "package dnsforward\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/filterlist\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n)\n\n// accessManager controls IP and client blocking that takes place before all\n// other processing.  An accessManager is safe for concurrent use.\ntype accessManager struct {\n\tallowedIPs *container.MapSet[netip.Addr]\n\tblockedIPs *container.MapSet[netip.Addr]\n\n\tallowedClientIDs *container.MapSet[string]\n\tblockedClientIDs *container.MapSet[string]\n\n\t// TODO(s.chzhen):  Use [aghnet.IgnoreEngine].\n\tblockedHostsEng *urlfilter.DNSEngine\n\n\t// TODO(a.garipov): Create a type for an efficient tree set of IP networks.\n\tallowedNets []netip.Prefix\n\tblockedNets []netip.Prefix\n}\n\n// processAccessClients is a helper for processing a list of client strings,\n// which may be an IP address, a CIDR, or a ClientID.\nfunc processAccessClients(\n\tclientStrs []string,\n\tips *container.MapSet[netip.Addr],\n\tnets *[]netip.Prefix,\n\tclientIDs *container.MapSet[string],\n) (err error) {\n\tfor i, s := range clientStrs {\n\t\tvar ip netip.Addr\n\t\tvar ipnet netip.Prefix\n\t\tif ip, err = netip.ParseAddr(s); err == nil {\n\t\t\tips.Add(ip)\n\t\t} else if ipnet, err = netip.ParsePrefix(s); err == nil {\n\t\t\t*nets = append(*nets, ipnet)\n\t\t} else {\n\t\t\terr = client.ValidateClientID(s)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"value %q at index %d: bad ip, cidr, or clientid\", s, i)\n\t\t\t}\n\n\t\t\tclientIDs.Add(s)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// newAccessCtx creates a new accessCtx.\nfunc newAccessCtx(allowed, blocked, blockedHosts []string) (a *accessManager, err error) {\n\ta = &accessManager{\n\t\tallowedIPs: container.NewMapSet[netip.Addr](),\n\t\tblockedIPs: container.NewMapSet[netip.Addr](),\n\n\t\tallowedClientIDs: container.NewMapSet[string](),\n\t\tblockedClientIDs: container.NewMapSet[string](),\n\t}\n\n\terr = processAccessClients(allowed, a.allowedIPs, &a.allowedNets, a.allowedClientIDs)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"adding allowed: %w\", err)\n\t}\n\n\terr = processAccessClients(blocked, a.blockedIPs, &a.blockedNets, a.blockedClientIDs)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"adding blocked: %w\", err)\n\t}\n\n\tb := &strings.Builder{}\n\tfor _, h := range blockedHosts {\n\t\tstringutil.WriteToBuilder(b, strings.ToLower(h), \"\\n\")\n\t}\n\n\tlists := []filterlist.Interface{\n\t\tfilterlist.NewString(&filterlist.StringConfig{\n\t\t\tID:             0,\n\t\t\tRulesText:      b.String(),\n\t\t\tIgnoreCosmetic: true,\n\t\t}),\n\t}\n\n\trulesStrg, err := filterlist.NewRuleStorage(lists)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"adding blocked hosts: %w\", err)\n\t}\n\n\ta.blockedHostsEng = urlfilter.NewDNSEngine(rulesStrg)\n\n\treturn a, nil\n}\n\n// allowlistMode returns true if this *accessCtx is in the allowlist mode.\nfunc (a *accessManager) allowlistMode() (ok bool) {\n\treturn a.allowedIPs.Len() != 0 || a.allowedClientIDs.Len() != 0 || len(a.allowedNets) != 0\n}\n\n// isBlockedClientID returns true if the ClientID should be blocked.\nfunc (a *accessManager) isBlockedClientID(id string) (ok bool) {\n\tallowlistMode := a.allowlistMode()\n\tif id == \"\" {\n\t\t// In allowlist mode, consider requests without ClientIDs blocked by\n\t\t// default.\n\t\treturn allowlistMode\n\t}\n\n\tif allowlistMode {\n\t\treturn !a.allowedClientIDs.Has(id)\n\t}\n\n\treturn a.blockedClientIDs.Has(id)\n}\n\n// isBlockedHost returns true if host should be blocked.\nfunc (a *accessManager) isBlockedHost(host string, qt rules.RRType) (ok bool) {\n\t_, ok = a.blockedHostsEng.MatchRequest(&urlfilter.DNSRequest{\n\t\tHostname: host,\n\t\tDNSType:  qt,\n\t})\n\n\treturn ok\n}\n\n// isBlockedIP returns the status of the IP address blocking as well as the rule\n// that blocked it.\nfunc (a *accessManager) isBlockedIP(ip netip.Addr) (blocked bool, rule string) {\n\tblocked = true\n\tips := a.blockedIPs\n\tipnets := a.blockedNets\n\n\tif a.allowlistMode() {\n\t\t// Enable allowlist mode and use the allowlist sets.\n\t\tblocked = false\n\t\tips = a.allowedIPs\n\t\tipnets = a.allowedNets\n\t}\n\n\tif ips.Has(ip) {\n\t\treturn blocked, ip.String()\n\t}\n\n\tfor _, ipnet := range ipnets {\n\t\t// Remove zone before checking because prefixes stip zones.\n\t\t//\n\t\t// TODO(d.kolyshev):  Cover with tests.\n\t\tif ipnet.Contains(ip.WithZone(\"\")) {\n\t\t\treturn blocked, ipnet.String()\n\t\t}\n\t}\n\n\treturn !blocked, \"\"\n}\n\ntype accessListJSON struct {\n\tAllowedClients    []string `json:\"allowed_clients\"`\n\tDisallowedClients []string `json:\"disallowed_clients\"`\n\tBlockedHosts      []string `json:\"blocked_hosts\"`\n}\n\nfunc (s *Server) accessListJSON() (j accessListJSON) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\treturn accessListJSON{\n\t\tAllowedClients:    slices.Clone(s.conf.AllowedClients),\n\t\tDisallowedClients: slices.Clone(s.conf.DisallowedClients),\n\t\tBlockedHosts:      slices.Clone(s.conf.BlockedHosts),\n\t}\n}\n\n// handleAccessList handles requests to the GET /control/access/list endpoint.\nfunc (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {\n\taghhttp.WriteJSONResponseOK(r.Context(), s.logger, w, r, s.accessListJSON())\n}\n\n// validateAccessSet checks the internal accessListJSON lists.  To search for\n// duplicates, we cannot compare the new stringutil.Set and []string, because\n// creating a set for a large array can be an unnecessary algorithmic complexity\nfunc validateAccessSet(list *accessListJSON) (err error) {\n\tallowed, err := validateStrUniq(list.AllowedClients)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating allowed clients: %w\", err)\n\t}\n\n\tdisallowed, err := validateStrUniq(list.DisallowedClients)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating disallowed clients: %w\", err)\n\t}\n\n\t_, err = validateStrUniq(list.BlockedHosts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating blocked hosts: %w\", err)\n\t}\n\n\tallowed = allowed.Intersection(allowed, disallowed)\n\tif allowed.Len() == 0 {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\n\t\t\"items in allowed and disallowed clients intersect: %s\",\n\t\tcontainer.MapSetToString(allowed),\n\t)\n}\n\n// validateStrUniq returns an informative error if clients are not unique.\nfunc validateStrUniq(clients []string) (m *container.MapSet[string], err error) {\n\tvar dup []string\n\tm = container.NewMapSet[string]()\n\tfor _, client := range clients {\n\t\tif m.Has(client) {\n\t\t\tdup = append(dup, client)\n\t\t}\n\n\t\tm.Add(client)\n\t}\n\n\tif len(dup) != 0 {\n\t\tslices.Sort(dup)\n\n\t\treturn nil, fmt.Errorf(\"duplicated values: %v\", dup)\n\t}\n\n\treturn m, nil\n}\n\n// handleAccessSet handles requests to the POST /control/access/set endpoint.\nfunc (s *Server) handleAccessSet(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.logger\n\n\tlist := &accessListJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&list)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"decoding request: %s\", err)\n\n\t\treturn\n\t}\n\n\terr = validateAccessSet(list)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tvar a *accessManager\n\ta, err = newAccessCtx(list.AllowedClients, list.DisallowedClients, list.BlockedHosts)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"creating access ctx: %s\", err)\n\n\t\treturn\n\t}\n\n\tdefer l.DebugContext(\n\t\tctx,\n\t\t\"updated access lists\",\n\t\t\"allowed\", len(list.AllowedClients),\n\t\t\"disallowed\", len(list.DisallowedClients),\n\t\t\"blocked_hosts\", len(list.BlockedHosts),\n\t)\n\n\tdefer s.conf.ConfModifier.Apply(ctx)\n\n\ts.serverLock.Lock()\n\tdefer s.serverLock.Unlock()\n\n\ts.conf.AllowedClients = list.AllowedClients\n\ts.conf.DisallowedClients = list.DisallowedClients\n\ts.conf.BlockedHosts = list.BlockedHosts\n\ts.access = a\n}\n"
  },
  {
    "path": "internal/dnsforward/access_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIsBlockedClientID(t *testing.T) {\n\tclientID := \"client-1\"\n\tclients := []string{clientID}\n\n\ta, err := newAccessCtx(clients, nil, nil)\n\trequire.NoError(t, err)\n\n\tassert.False(t, a.isBlockedClientID(clientID))\n\n\ta, err = newAccessCtx(nil, clients, nil)\n\trequire.NoError(t, err)\n\n\tassert.True(t, a.isBlockedClientID(clientID))\n}\n\nfunc TestIsBlockedHost(t *testing.T) {\n\ta, err := newAccessCtx(nil, nil, []string{\n\t\t\"host1\",\n\t\t\"*.host.com\",\n\t\t\"||host3.com^\",\n\t\t\"||*^$dnstype=HTTPS\",\n\t\t\"|.^\",\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\twant assert.BoolAssertionFunc\n\t\tname string\n\t\thost string\n\t\tqt   rules.RRType\n\t}{{\n\t\twant: assert.True,\n\t\tname: \"plain_match\",\n\t\thost: \"host1\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.False,\n\t\tname: \"plain_mismatch\",\n\t\thost: \"host2\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.True,\n\t\tname: \"subdomain_match_short\",\n\t\thost: \"asdf.host.com\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.True,\n\t\tname: \"subdomain_match_long\",\n\t\thost: \"qwer.asdf.host.com\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.False,\n\t\tname: \"subdomain_mismatch_no_lead\",\n\t\thost: \"host.com\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.False,\n\t\tname: \"subdomain_mismatch_bad_asterisk\",\n\t\thost: \"asdf.zhost.com\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.True,\n\t\tname: \"rule_match_simple\",\n\t\thost: \"host3.com\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.True,\n\t\tname: \"rule_match_complex\",\n\t\thost: \"asdf.host3.com\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.False,\n\t\tname: \"rule_mismatch\",\n\t\thost: \".host3.com\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.True,\n\t\tname: \"by_qtype\",\n\t\thost: \"site-with-https-record.example\",\n\t\tqt:   dns.TypeHTTPS,\n\t}, {\n\t\twant: assert.False,\n\t\tname: \"by_qtype_other\",\n\t\thost: \"site-with-https-record.example\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: assert.True,\n\t\tname: \"ns_root\",\n\t\thost: \".\",\n\t\tqt:   dns.TypeNS,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttc.want(t, a.isBlockedHost(tc.host, tc.qt))\n\t\t})\n\t}\n}\n\nfunc TestIsBlockedIP(t *testing.T) {\n\tclients := []string{\n\t\t\"1.2.3.4\",\n\t\t\"5.6.7.8/24\",\n\t}\n\n\tallowCtx, err := newAccessCtx(clients, nil, nil)\n\trequire.NoError(t, err)\n\n\tblockCtx, err := newAccessCtx(nil, clients, nil)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tip          netip.Addr\n\t\tname        string\n\t\twantRule    string\n\t\twantBlocked bool\n\t}{{\n\t\tip:          netip.MustParseAddr(\"1.2.3.4\"),\n\t\tname:        \"match_ip\",\n\t\twantRule:    \"1.2.3.4\",\n\t\twantBlocked: true,\n\t}, {\n\t\tip:          netip.MustParseAddr(\"5.6.7.100\"),\n\t\tname:        \"match_cidr\",\n\t\twantRule:    \"5.6.7.8/24\",\n\t\twantBlocked: true,\n\t}, {\n\t\tip:          netip.MustParseAddr(\"9.2.3.4\"),\n\t\tname:        \"no_match_ip\",\n\t\twantRule:    \"\",\n\t\twantBlocked: false,\n\t}, {\n\t\tip:          netip.MustParseAddr(\"9.6.7.100\"),\n\t\tname:        \"no_match_cidr\",\n\t\twantRule:    \"\",\n\t\twantBlocked: false,\n\t}}\n\n\tt.Run(\"allow\", func(t *testing.T) {\n\t\tfor _, tc := range testCases {\n\t\t\tblocked, rule := allowCtx.isBlockedIP(tc.ip)\n\t\t\tassert.Equal(t, !tc.wantBlocked, blocked)\n\t\t\tassert.Equal(t, tc.wantRule, rule)\n\t\t}\n\t})\n\n\tt.Run(\"block\", func(t *testing.T) {\n\t\tfor _, tc := range testCases {\n\t\t\tblocked, rule := blockCtx.isBlockedIP(tc.ip)\n\t\t\tassert.Equal(t, tc.wantBlocked, blocked)\n\t\t\tassert.Equal(t, tc.wantRule, rule)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/dnsforward/clientid.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n)\n\n// clientIDFromClientServerName extracts and validates a ClientID.  hostSrvName\n// is the server name of the host.  cliSrvName is the server name as sent by the\n// client.  When strict is true, and client and host server name don't match,\n// clientIDFromClientServerName will return an error.\nfunc clientIDFromClientServerName(\n\thostSrvName string,\n\tcliSrvName string,\n\tstrict bool,\n) (clientID string, err error) {\n\tif hostSrvName == cliSrvName {\n\t\treturn \"\", nil\n\t}\n\n\tif !netutil.IsImmediateSubdomain(cliSrvName, hostSrvName) {\n\t\tif !strict {\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\treturn \"\", fmt.Errorf(\n\t\t\t\"client server name %q doesn't match host server name %q\",\n\t\t\tcliSrvName,\n\t\t\thostSrvName,\n\t\t)\n\t}\n\n\tclientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1]\n\terr = client.ValidateClientID(clientID)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn \"\", err\n\t}\n\n\treturn strings.ToLower(clientID), nil\n}\n\n// clientIDFromDNSContextHTTPS extracts the ClientID from the path of the\n// client's DNS-over-HTTPS request.\nfunc clientIDFromDNSContextHTTPS(pctx *proxy.DNSContext) (clientID string, err error) {\n\tr := pctx.HTTPRequest\n\tif r == nil {\n\t\treturn \"\", fmt.Errorf(\n\t\t\t\"proxy ctx http request of proto %s is nil\",\n\t\t\tpctx.Proto,\n\t\t)\n\t}\n\n\torigPath := r.URL.Path\n\tparts := strings.Split(path.Clean(origPath), \"/\")\n\tif parts[0] == \"\" {\n\t\tparts = parts[1:]\n\t}\n\n\tif len(parts) == 0 || parts[0] != \"dns-query\" {\n\t\treturn \"\", fmt.Errorf(\"clientid check: invalid path %q\", origPath)\n\t}\n\n\tswitch len(parts) {\n\tcase 1:\n\t\t// Just /dns-query, no ClientID.\n\t\treturn \"\", nil\n\tcase 2:\n\t\tclientID = parts[1]\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"clientid check: invalid path %q: extra parts\", origPath)\n\t}\n\n\terr = client.ValidateClientID(clientID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"clientid check: %w\", err)\n\t}\n\n\treturn strings.ToLower(clientID), nil\n}\n\n// tlsConn is a narrow interface for *tls.Conn to simplify testing.\ntype tlsConn interface {\n\tConnectionState() (cs tls.ConnectionState)\n}\n\n// clientServerName returns the TLS server name based on the protocol.  For\n// DNS-over-HTTPS requests, it will return the hostname part of the Host header\n// if there is one.  l and pctx must not be nil.\nfunc clientServerName(\n\tctx context.Context,\n\tl *slog.Logger,\n\tpctx *proxy.DNSContext,\n\tproto proxy.Proto,\n) (srvName string, err error) {\n\tfrom := \"tls conn\"\n\n\tswitch proto {\n\tcase proxy.ProtoHTTPS:\n\t\tvar fromHost bool\n\t\tsrvName, fromHost, err = clientServerNameFromHTTP(pctx.HTTPRequest)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"from http: %w\", err)\n\t\t}\n\n\t\tif fromHost {\n\t\t\tfrom = \"host header\"\n\t\t}\n\tcase proxy.ProtoQUIC:\n\t\tsrvName = pctx.QUICConnection.ConnectionState().TLS.ServerName\n\tcase proxy.ProtoTLS:\n\t\tconn := pctx.Conn\n\t\ttc, ok := conn.(tlsConn)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"pctx conn of proto %s is %T, want *tls.Conn\", proto, conn)\n\t\t}\n\n\t\tsrvName = tc.ConnectionState().ServerName\n\t}\n\n\tl.DebugContext(ctx, \"got client server name\", \"name\", srvName, \"from\", from)\n\n\treturn srvName, nil\n}\n\n// clientServerNameFromHTTP returns the TLS server name or the value of the host\n// header depending on the protocol.  fromHost is true if srvName comes from the\n// \"Host\" HTTP header.\nfunc clientServerNameFromHTTP(r *http.Request) (srvName string, fromHost bool, err error) {\n\tif connState := r.TLS; connState != nil {\n\t\treturn connState.ServerName, false, nil\n\t}\n\n\tif r.Host == \"\" {\n\t\treturn \"\", false, nil\n\t}\n\n\tsrvName, err = netutil.SplitHost(r.Host)\n\tif err != nil {\n\t\treturn \"\", false, fmt.Errorf(\"parsing host: %w\", err)\n\t}\n\n\treturn srvName, true, nil\n}\n"
  },
  {
    "path": "internal/dnsforward/clientid_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// testTLSConn is a tlsConn for tests.\ntype testTLSConn struct {\n\t// Conn is embedded here simply to make testTLSConn a net.Conn without\n\t// actually implementing all methods.\n\tnet.Conn\n\n\tserverName string\n}\n\n// ConnectionState implements the tlsConn interface for testTLSConn.\nfunc (c testTLSConn) ConnectionState() (cs tls.ConnectionState) {\n\tcs.ServerName = c.serverName\n\n\treturn cs\n}\n\nfunc TestServer_clientIDFromDNSContext(t *testing.T) {\n\ttestCases := []struct {\n\t\tname         string\n\t\tproto        proxy.Proto\n\t\tconfSrvName  string\n\t\tcliSrvName   string\n\t\twantClientID string\n\t\twantErrMsg   string\n\t\tinclHTTPTLS  bool\n\t\tstrictSNI    bool\n\t}{{\n\t\tname:         \"udp\",\n\t\tproto:        proxy.ProtoUDP,\n\t\tconfSrvName:  \"\",\n\t\tcliSrvName:   \"\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    false,\n\t}, {\n\t\tname:         \"tls_no_clientid\",\n\t\tproto:        proxy.ProtoTLS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    true,\n\t}, {\n\t\tname:         \"tls_no_client_server_name\",\n\t\tproto:        proxy.ProtoTLS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"\",\n\t\twantClientID: \"\",\n\t\twantErrMsg: `clientid check: client server name \"\" ` +\n\t\t\t`doesn't match host server name \"example.com\"`,\n\t\tinclHTTPTLS: false,\n\t\tstrictSNI:   true,\n\t}, {\n\t\tname:         \"tls_no_client_server_name_no_strict\",\n\t\tproto:        proxy.ProtoTLS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    false,\n\t}, {\n\t\tname:         \"tls_clientid\",\n\t\tproto:        proxy.ProtoTLS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"cli.example.com\",\n\t\twantClientID: \"cli\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    true,\n\t}, {\n\t\tname:         \"tls_clientid_hostname_error\",\n\t\tproto:        proxy.ProtoTLS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"cli.example.net\",\n\t\twantClientID: \"\",\n\t\twantErrMsg: `clientid check: client server name \"cli.example.net\" ` +\n\t\t\t`doesn't match host server name \"example.com\"`,\n\t\tinclHTTPTLS: false,\n\t\tstrictSNI:   true,\n\t}, {\n\t\tname:         \"tls_invalid_clientid\",\n\t\tproto:        proxy.ProtoTLS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"!!!.example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg: `clientid check: invalid clientid \"!!!\": ` +\n\t\t\t`bad hostname label rune '!'`,\n\t\tinclHTTPTLS: false,\n\t\tstrictSNI:   true,\n\t}, {\n\t\tname:        \"tls_clientid_too_long\",\n\t\tproto:       proxy.ProtoTLS,\n\t\tconfSrvName: \"example.com\",\n\t\tcliSrvName: `abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmno` +\n\t\t\t`pqrstuvwxyz0123456789.example.com`,\n\t\twantClientID: \"\",\n\t\twantErrMsg: `clientid check: invalid clientid \"abcdefghijklmno` +\n\t\t\t`pqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789\": ` +\n\t\t\t`hostname label is too long: got 72, max 63`,\n\t\tinclHTTPTLS: false,\n\t\tstrictSNI:   true,\n\t}, {\n\t\tname:         \"quic_clientid\",\n\t\tproto:        proxy.ProtoQUIC,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"cli.example.com\",\n\t\twantClientID: \"cli\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    true,\n\t}, {\n\t\tname:         \"tls_clientid_issue3437\",\n\t\tproto:        proxy.ProtoTLS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"cli.myexample.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg: `clientid check: client server name \"cli.myexample.com\" ` +\n\t\t\t`doesn't match host server name \"example.com\"`,\n\t\tinclHTTPTLS: false,\n\t\tstrictSNI:   true,\n\t}, {\n\t\tname:         \"tls_case\",\n\t\tproto:        proxy.ProtoTLS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"InSeNsItIvE.example.com\",\n\t\twantClientID: \"insensitive\",\n\t\twantErrMsg:   ``,\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    true,\n\t}, {\n\t\tname:         \"quic_case\",\n\t\tproto:        proxy.ProtoQUIC,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"InSeNsItIvE.example.com\",\n\t\twantClientID: \"insensitive\",\n\t\twantErrMsg:   ``,\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    true,\n\t}, {\n\t\tname:         \"https_no_clientid\",\n\t\tproto:        proxy.ProtoHTTPS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  true,\n\t\tstrictSNI:    true,\n\t}, {\n\t\tname:         \"https_clientid\",\n\t\tproto:        proxy.ProtoHTTPS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"cli.example.com\",\n\t\twantClientID: \"cli\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  true,\n\t\tstrictSNI:    true,\n\t}, {\n\t\tname:         \"https_issue5518\",\n\t\tproto:        proxy.ProtoHTTPS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"cli.example.com\",\n\t\twantClientID: \"cli\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    true,\n\t}, {\n\t\tname:         \"https_no_host\",\n\t\tproto:        proxy.ProtoHTTPS,\n\t\tconfSrvName:  \"example.com\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   \"\",\n\t\tinclHTTPTLS:  false,\n\t\tstrictSNI:    true,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttlsConf := &TLSConfig{\n\t\t\t\tServerName:     tc.confSrvName,\n\t\t\t\tStrictSNICheck: tc.strictSNI,\n\t\t\t}\n\n\t\t\tsrv := &Server{\n\t\t\t\tconf:       ServerConfig{TLSConf: tlsConf},\n\t\t\t\tbaseLogger: testLogger,\n\t\t\t\tlogger:     testLogger,\n\t\t\t}\n\n\t\t\tvar (\n\t\t\t\tconn    net.Conn\n\t\t\t\thttpReq *http.Request\n\t\t\t)\n\n\t\t\tswitch tc.proto {\n\t\t\tcase proxy.ProtoHTTPS:\n\t\t\t\thttpReq = newHTTPReq(tc.cliSrvName, tc.inclHTTPTLS)\n\t\t\tcase proxy.ProtoQUIC:\n\t\t\t\t// TODO(a.garipov):  Find ways of testing this with the new\n\t\t\t\t// quic-go API.\n\t\t\t\tt.Skipf(\"skipped during the quic-go api update\")\n\t\t\tcase proxy.ProtoTLS:\n\t\t\t\tconn = testTLSConn{\n\t\t\t\t\tserverName: tc.cliSrvName,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpctx := &proxy.DNSContext{\n\t\t\t\tProto:       tc.proto,\n\t\t\t\tConn:        conn,\n\t\t\t\tHTTPRequest: httpReq,\n\t\t\t}\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tclientID, err := srv.clientIDFromDNSContext(ctx, pctx)\n\t\t\tassert.Equal(t, tc.wantClientID, clientID)\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\n// newHTTPReq is a helper to create HTTP requests for tests.\nfunc newHTTPReq(cliSrvName string, inclTLS bool) (r *http.Request) {\n\tu := &url.URL{\n\t\tPath: \"/dns-query\",\n\t}\n\n\tr = &http.Request{\n\t\tProtoMajor: 1,\n\t\tProtoMinor: 1,\n\t\tURL:        u,\n\t\tHost:       cliSrvName,\n\t}\n\n\tif inclTLS {\n\t\tr.TLS = &tls.ConnectionState{\n\t\t\tServerName: cliSrvName,\n\t\t}\n\t}\n\n\treturn r\n}\n\nfunc TestClientIDFromDNSContextHTTPS(t *testing.T) {\n\ttestCases := []struct {\n\t\tname         string\n\t\tpath         string\n\t\tcliSrvName   string\n\t\twantClientID string\n\t\twantErrMsg   string\n\t}{{\n\t\tname:         \"no_clientid\",\n\t\tpath:         \"/dns-query\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   \"\",\n\t}, {\n\t\tname:         \"no_clientid_slash\",\n\t\tpath:         \"/dns-query/\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   \"\",\n\t}, {\n\t\tname:         \"clientid\",\n\t\tpath:         \"/dns-query/cli\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"cli\",\n\t\twantErrMsg:   \"\",\n\t}, {\n\t\tname:         \"clientid_slash\",\n\t\tpath:         \"/dns-query/cli/\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"cli\",\n\t\twantErrMsg:   \"\",\n\t}, {\n\t\tname:         \"clientid_case\",\n\t\tpath:         \"/dns-query/InSeNsItIvE\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"insensitive\",\n\t\twantErrMsg:   ``,\n\t}, {\n\t\tname:         \"bad_url\",\n\t\tpath:         \"/foo\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   `clientid check: invalid path \"/foo\"`,\n\t}, {\n\t\tname:         \"extra\",\n\t\tpath:         \"/dns-query/cli/foo\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   `clientid check: invalid path \"/dns-query/cli/foo\": extra parts`,\n\t}, {\n\t\tname:         \"invalid_clientid\",\n\t\tpath:         \"/dns-query/!!!\",\n\t\tcliSrvName:   \"example.com\",\n\t\twantClientID: \"\",\n\t\twantErrMsg:   `clientid check: invalid clientid \"!!!\": bad hostname label rune '!'`,\n\t}, {\n\t\tname:         \"both_ids\",\n\t\tpath:         \"/dns-query/right\",\n\t\tcliSrvName:   \"wrong.example.com\",\n\t\twantClientID: \"right\",\n\t\twantErrMsg:   \"\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tconnState := &tls.ConnectionState{\n\t\t\t\tServerName: tc.cliSrvName,\n\t\t\t}\n\n\t\t\tr := &http.Request{\n\t\t\t\tURL: &url.URL{\n\t\t\t\t\tPath: tc.path,\n\t\t\t\t},\n\t\t\t\tTLS: connState,\n\t\t\t}\n\n\t\t\tpctx := &proxy.DNSContext{\n\t\t\t\tProto:       proxy.ProtoHTTPS,\n\t\t\t\tHTTPRequest: r,\n\t\t\t}\n\n\t\t\tclientID, err := clientIDFromDNSContextHTTPS(pctx)\n\t\t\tassert.Equal(t, tc.wantClientID, clientID)\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dnsforward/clientscontainer.go",
    "content": "package dnsforward\n\nimport (\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n)\n\n// ClientsContainer provides information about preconfigured DNS clients.\ntype ClientsContainer interface {\n\t// CustomUpstreamConfig returns the custom client upstream configuration, if\n\t// any.  It prioritizes ClientID over client IP address to identify the\n\t// client.\n\tCustomUpstreamConfig(clientID string, cliAddr netip.Addr) (conf *proxy.CustomUpstreamConfig)\n\n\t// UpdateCommonUpstreamConfig updates the common upstream configuration.\n\tUpdateCommonUpstreamConfig(conf *client.CommonUpstreamConfig)\n\n\t// ClearUpstreamCache clears the upstream cache for each stored custom\n\t// client upstream configuration.\n\tClearUpstreamCache()\n}\n\n// EmptyClientsContainer is an [ClientsContainer] implementation that does nothing.\ntype EmptyClientsContainer struct{}\n\n// type check\nvar _ ClientsContainer = EmptyClientsContainer{}\n\n// CustomUpstreamConfig implements the [ClientsContainer] interface for\n// EmptyClientsContainer.\nfunc (EmptyClientsContainer) CustomUpstreamConfig(\n\tclientID string,\n\tcliAddr netip.Addr,\n) (conf *proxy.CustomUpstreamConfig) {\n\treturn nil\n}\n\n// UpdateCommonUpstreamConfig implements the [ClientsContainer] interface for\n// EmptyClientsContainer.\nfunc (EmptyClientsContainer) UpdateCommonUpstreamConfig(conf *client.CommonUpstreamConfig) {}\n\n// ClearUpstreamCache implements the [ClientsContainer] interface for\n// EmptyClientsContainer.\nfunc (EmptyClientsContainer) ClearUpstreamCache() {}\n"
  },
  {
    "path": "internal/dnsforward/config.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghslog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/ratelimit\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n\t\"github.com/ameshkov/dnscrypt/v2\"\n)\n\n// Config represents the DNS filtering configuration of AdGuard Home.  The zero\n// Config is empty and ready for use.\ntype Config struct {\n\t// Callbacks for other modules\n\n\t// ClientsContainer stores the information about special handling of some\n\t// DNS clients.\n\tClientsContainer ClientsContainer `yaml:\"-\"`\n\n\t// Anti-DNS amplification\n\n\t// Ratelimit is the maximum number of requests per second from a given IP\n\t// (0 to disable).\n\tRatelimit uint32 `yaml:\"ratelimit\"`\n\n\t// RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for\n\t// rate limiting requests.\n\tRatelimitSubnetLenIPv4 uint `yaml:\"ratelimit_subnet_len_ipv4\"`\n\n\t// RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for\n\t// rate limiting requests.\n\tRatelimitSubnetLenIPv6 uint `yaml:\"ratelimit_subnet_len_ipv6\"`\n\n\t// RatelimitWhitelist is the list of whitelisted client IP addresses.\n\tRatelimitWhitelist []netip.Addr `yaml:\"ratelimit_whitelist\"`\n\n\t// RefuseAny, if true, refuse ANY requests.\n\tRefuseAny bool `yaml:\"refuse_any\"`\n\n\t// Upstream DNS servers configuration\n\n\t// UpstreamDNS is the list of upstream DNS servers.\n\tUpstreamDNS []string `yaml:\"upstream_dns\"`\n\n\t// UpstreamDNSFileName, if set, points to the file which contains upstream\n\t// DNS servers.\n\tUpstreamDNSFileName string `yaml:\"upstream_dns_file\"`\n\n\t// BootstrapDNS is the list of bootstrap DNS servers for DoH and DoT\n\t// resolvers (plain DNS only).\n\tBootstrapDNS []string `yaml:\"bootstrap_dns\"`\n\n\t// FallbackDNS is the list of fallback DNS servers used when upstream DNS\n\t// servers are not responding.\n\tFallbackDNS []string `yaml:\"fallback_dns\"`\n\n\t// UpstreamMode determines the logic through which upstreams will be used.\n\tUpstreamMode UpstreamMode `yaml:\"upstream_mode\"`\n\n\t// FastestTimeout replaces the default timeout for dialing IP addresses\n\t// when FastestAddr is true.\n\tFastestTimeout timeutil.Duration `yaml:\"fastest_timeout\"`\n\n\t// Access settings\n\n\t// AllowedClients is the slice of IP addresses, CIDR networks, and\n\t// ClientIDs of allowed clients.  If not empty, only these clients are\n\t// allowed, and [Config.DisallowedClients] are ignored.\n\tAllowedClients []string `yaml:\"allowed_clients\"`\n\n\t// DisallowedClients is the slice of IP addresses, CIDR networks, and\n\t// ClientIDs of disallowed clients.\n\tDisallowedClients []string `yaml:\"disallowed_clients\"`\n\n\t// BlockedHosts is the list of hosts that should be blocked.\n\tBlockedHosts []string `yaml:\"blocked_hosts\"`\n\n\t// TrustedProxies is the list of CIDR networks with proxy servers addresses\n\t// from which the DoH requests should be handled.  The value of nil or an\n\t// empty slice for this field makes Proxy not trust any address.\n\tTrustedProxies []netutil.Prefix `yaml:\"trusted_proxies\"`\n\n\t// DNS cache settings\n\n\t// CacheEnabled defines if the DNS cache should be used.\n\tCacheEnabled bool `yaml:\"cache_enabled\"`\n\n\t// CacheSize is the DNS cache size (in bytes).\n\tCacheSize uint32 `yaml:\"cache_size\"`\n\n\t// CacheMinTTL is the override TTL value (minimum) received from upstream\n\t// server.\n\tCacheMinTTL uint32 `yaml:\"cache_ttl_min\"`\n\n\t// CacheMaxTTL is the override TTL value (maximum) received from upstream\n\t// server.\n\tCacheMaxTTL uint32 `yaml:\"cache_ttl_max\"`\n\n\t// CacheOptimistic defines if optimistic cache mechanism should be used.\n\tCacheOptimistic bool `yaml:\"cache_optimistic\"`\n\n\t// CacheOptimisticAnswerTTL is the default TTL for expired cached responses.\n\tCacheOptimisticAnswerTTL timeutil.Duration `yaml:\"cache_optimistic_answer_ttl\"`\n\n\t// CacheOptimisticMaxAge is the maximum time entries remain in the cache\n\t// when cache is optimistic.\n\tCacheOptimisticMaxAge timeutil.Duration `yaml:\"cache_optimistic_max_age\"`\n\n\t// Other settings\n\n\t// BogusNXDomain is the list of IP addresses, responses with them will be\n\t// transformed to NXDOMAIN.\n\tBogusNXDomain []string `yaml:\"bogus_nxdomain\"`\n\n\t// AAAADisabled, if true, respond with an empty answer to all AAAA\n\t// requests.\n\tAAAADisabled bool `yaml:\"aaaa_disabled\"`\n\n\t// EnableDNSSEC, if true, set AD flag in outcoming DNS request.\n\tEnableDNSSEC bool `yaml:\"enable_dnssec\"`\n\n\t// EDNSClientSubnet is the settings list for EDNS Client Subnet.\n\tEDNSClientSubnet *EDNSClientSubnet `yaml:\"edns_client_subnet\"`\n\n\t// MaxGoroutines is the max number of parallel goroutines for processing\n\t// incoming requests.\n\tMaxGoroutines uint `yaml:\"max_goroutines\"`\n\n\t// HandleDDR, if true, handle DDR requests\n\tHandleDDR bool `yaml:\"handle_ddr\"`\n\n\t// IpsetList is the ipset configuration that allows AdGuard Home to add IP\n\t// addresses of the specified domain names to an ipset list.  Syntax:\n\t//\n\t//\tDOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...\n\t//\n\t// This field is ignored if [IpsetListFileName] is set.\n\tIpsetList []string `yaml:\"ipset\"`\n\n\t// IpsetListFileName, if set, points to the file with ipset configuration.\n\t// The format is the same as in [IpsetList].\n\tIpsetListFileName string `yaml:\"ipset_file\"`\n\n\t// BootstrapPreferIPv6, if true, instructs the bootstrapper to prefer IPv6\n\t// addresses to IPv4 ones for DoH, DoQ, and DoT.\n\tBootstrapPreferIPv6 bool `yaml:\"bootstrap_prefer_ipv6\"`\n}\n\n// EDNSClientSubnet is the settings list for EDNS Client Subnet.\ntype EDNSClientSubnet struct {\n\t// CustomIP for EDNS Client Subnet.\n\tCustomIP netip.Addr `yaml:\"custom_ip\"`\n\n\t// Enabled defines if EDNS Client Subnet is enabled.\n\tEnabled bool `yaml:\"enabled\"`\n\n\t// UseCustom defines if CustomIP should be used.\n\tUseCustom bool `yaml:\"use_custom\"`\n}\n\n// TLSConfig contains the TLS configuration settings for DNSCrypt,\n// DNS-over-HTTPS (DoH), DNS-over-TLS (DoT), DNS-over-QUIC (DoQ), and Discovery\n// of Designated Resolvers (DDR).\ntype TLSConfig struct {\n\t// DNSCryptConf contains the configuration settings for a DNSCrypt server.\n\t// It is nil if the DNSCrypt server is disabled.\n\tDNSCryptConf *DNSCryptConfig\n\n\t// Cert is the TLS certificate used for TLS connections.  It is nil if\n\t// encryption is disabled.\n\tCert *tls.Certificate\n\n\t// TLSListenAddrs are the addresses to listen on for DoT connections.  Each\n\t// item in the list must be non-nil if Cert is not nil.\n\tTLSListenAddrs []*net.TCPAddr\n\n\t// QUICListenAddrs are the addresses to listen on for DoQ connections.  Each\n\t// item in the list must be non-nil if Cert is not nil.\n\tQUICListenAddrs []*net.UDPAddr\n\n\t// HTTPSListenAddrs should be the addresses AdGuard Home is listening on for\n\t// DoH connections.  These addresses are announced with DDR.  Each item in\n\t// the list must be non-nil.\n\tHTTPSListenAddrs []netip.AddrPort\n\n\t// ServerName is the hostname of the server.  Currently, it is only being\n\t// used for ClientID checking and Discovery of Designated Resolvers (DDR).\n\tServerName string\n\n\t// StrictSNICheck controls if the connections with SNI mismatching the\n\t// certificate's ones should be rejected.\n\tStrictSNICheck bool\n}\n\n// DNSCryptConfig contains the configuration settings for a DNSCrypt server.\ntype DNSCryptConfig struct {\n\t// ResolverCert is the certificate used for DNSCrypt connections.  It is not\n\t// nil if there is at least one UDP or TCP address present.\n\tResolverCert *dnscrypt.Cert\n\n\t// UDPListenAddrs are the addresses to listen on for DNSCrypt UDP\n\t// connections.\n\tUDPListenAddrs []*net.UDPAddr\n\n\t// TCPListenAddrs are the addresses to listen on for DNSCrypt TCP\n\t// connections.\n\tTCPListenAddrs []*net.TCPAddr\n\n\t// ProviderName is the name of the DNSCrypt provider.  It is not empty if\n\t// there is at least one UDP or TCP address present.\n\tProviderName string\n}\n\n// ServerConfig represents server configuration.\n// The zero ServerConfig is empty and ready for use.\ntype ServerConfig struct {\n\t// UDPListenAddrs is the list of addresses to listen for DNS-over-UDP.\n\tUDPListenAddrs []*net.UDPAddr\n\n\t// TCPListenAddrs is the list of addresses to listen for DNS-over-TCP.\n\tTCPListenAddrs []*net.TCPAddr\n\n\t// UpstreamConfig is the general configuration of upstream DNS servers.\n\tUpstreamConfig *proxy.UpstreamConfig\n\n\t// PrivateRDNSUpstreamConfig is the configuration of upstream DNS servers\n\t// for private reverse DNS.\n\tPrivateRDNSUpstreamConfig *proxy.UpstreamConfig\n\n\t// AddrProcConf defines the configuration for the client IP processor.\n\t// If nil, [client.EmptyAddrProc] is used.\n\t//\n\t// TODO(a.garipov): The use of [client.EmptyAddrProc] is a crutch for tests.\n\t// Remove that.\n\tAddrProcConf *client.DefaultAddrProcConfig\n\n\t// TLSConf is the TLS configuration for DNS-over-TLS, DNS-over-QUIC, and\n\t// HTTPS.  It must not be nil.\n\tTLSConf *TLSConfig\n\n\tConfig\n\tTLSAllowUnencryptedDoH bool\n\n\t// UpstreamTimeout is the timeout for querying upstream servers.\n\tUpstreamTimeout time.Duration\n\n\tTLSv12Roots *x509.CertPool // list of root CAs for TLSv1.2\n\n\t// TLSCiphers are the IDs of TLS cipher suites to use.\n\tTLSCiphers []uint16\n\n\t// ConfModifier is used to update the global configuration.  It must not be\n\t// nil.\n\tConfModifier agh.ConfigModifier\n\n\t// Register an HTTP handler\n\tHTTPReg aghhttp.Registrar\n\n\t// LocalPTRResolvers is a slice of addresses to be used as upstreams for\n\t// resolving PTR queries for local addresses.\n\tLocalPTRResolvers []string\n\n\t// DNS64Prefixes is a slice of NAT64 prefixes to be used for DNS64.\n\tDNS64Prefixes []netip.Prefix\n\n\t// UsePrivateRDNS defines if the PTR requests for unknown addresses from\n\t// locally-served networks should be resolved via private PTR resolvers.\n\tUsePrivateRDNS bool\n\n\t// UseDNS64 defines if DNS64 is enabled for incoming requests.\n\tUseDNS64 bool\n\n\t// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.\n\tServeHTTP3 bool\n\n\t// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS\n\t// upstreams.\n\tUseHTTP3Upstreams bool\n\n\t// ServePlainDNS defines if plain DNS is allowed for incoming requests.\n\tServePlainDNS bool\n\n\t// PendingRequestsEnabled defines if duplicate requests should be forwarded\n\t// to upstreams along with the original one.\n\tPendingRequestsEnabled bool\n}\n\n// UpstreamMode is a enumeration of upstream mode representations.  See\n// [proxy.UpstreamModeType].\n//\n// TODO(d.kolyshev): Consider using [proxy.UpstreamMode].\ntype UpstreamMode string\n\nconst (\n\tUpstreamModeLoadBalance UpstreamMode = \"load_balance\"\n\tUpstreamModeParallel    UpstreamMode = \"parallel\"\n\tUpstreamModeFastestAddr UpstreamMode = \"fastest_addr\"\n)\n\n// newProxyConfig creates and validates configuration for the main proxy.\n//\n// TODO(d.kolyshev):  Improve maintainability.\nfunc (s *Server) newProxyConfig(ctx context.Context) (conf *proxy.Config, err error) {\n\tsrvConf := s.conf\n\ttrustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies)\n\n\tratelimitMw, err := newRatelimitMw(s.baseLogger, srvConf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ratelimit middleware: %w\", err)\n\t}\n\n\tlogMw := newLogMiddleware(s.baseLogger, slogutil.LevelTrace)\n\n\thttpConf := &proxy.HTTPConfig{\n\t\tServerHeader:    aghhttp.UserAgent(),\n\t\tInsecureEnabled: s.conf.TLSAllowUnencryptedDoH,\n\t}\n\n\tconf = &proxy.Config{\n\t\tLogger:                    s.baseLogger.With(slogutil.KeyPrefix, aghslog.PrefixDNSProxy),\n\t\tRefuseAny:                 srvConf.RefuseAny,\n\t\tTrustedProxies:            netutil.SliceSubnetSet(trustedPrefixes),\n\t\tCacheMinTTL:               srvConf.CacheMinTTL,\n\t\tCacheMaxTTL:               srvConf.CacheMaxTTL,\n\t\tCacheOptimistic:           srvConf.CacheOptimistic,\n\t\tCacheOptimisticAnswerTTL:  time.Duration(srvConf.CacheOptimisticAnswerTTL),\n\t\tCacheOptimisticMaxAge:     time.Duration(srvConf.CacheOptimisticMaxAge),\n\t\tUpstreamConfig:            srvConf.UpstreamConfig,\n\t\tPrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,\n\t\tRequestHandler:            ratelimitMw.Wrap(logMw.Wrap(s.Wrap(s))),\n\t\tEnableEDNSClientSubnet:    srvConf.EDNSClientSubnet.Enabled,\n\t\tMaxGoroutines:             srvConf.MaxGoroutines,\n\t\tUseDNS64:                  srvConf.UseDNS64,\n\t\tDNS64Prefs:                srvConf.DNS64Prefixes,\n\t\tUsePrivateRDNS:            srvConf.UsePrivateRDNS,\n\t\tPrivateSubnets:            s.privateNets,\n\t\tMessageConstructor:        s,\n\t\tPendingRequests: &proxy.PendingRequestsConfig{\n\t\t\tEnabled: srvConf.PendingRequestsEnabled,\n\t\t},\n\t\tHTTPConfig: httpConf,\n\t}\n\n\tif srvConf.EDNSClientSubnet.UseCustom {\n\t\t// TODO(s.chzhen):  Use netip.Addr instead of net.IP inside dnsproxy.\n\t\tconf.EDNSAddr = net.IP(srvConf.EDNSClientSubnet.CustomIP.AsSlice())\n\t}\n\n\terr = setProxyUpstreamMode(conf, srvConf.UpstreamMode, time.Duration(srvConf.FastestTimeout))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"upstream mode: %w\", err)\n\t}\n\n\tconf.BogusNXDomain, err = parseBogusNXDOMAIN(srvConf.BogusNXDomain)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bogus_nxdomain: %w\", err)\n\t}\n\n\terr = s.prepareTLS(ctx, conf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"validating tls: %w\", err)\n\t}\n\n\terr = s.preparePlain(ctx, conf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"validating plain: %w\", err)\n\t}\n\n\tconf, err = prepareCacheConfig(conf,\n\t\tsrvConf.CacheEnabled,\n\t\tsrvConf.CacheSize,\n\t\tsrvConf.CacheMinTTL,\n\t\tsrvConf.CacheMaxTTL,\n\t)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\treturn conf, nil\n}\n\n// newRatelimitMw returns the ratelimit middleware.  In case of invalid\n// ratelimit configuration returns an error. l must not be nil.\nfunc newRatelimitMw(\n\tl *slog.Logger,\n\tconf ServerConfig,\n) (mw proxy.Middleware, err error) {\n\tif conf.Ratelimit == 0 {\n\t\treturn proxy.MiddlewareFunc(proxy.PassThrough), nil\n\t}\n\n\trlConf := &ratelimit.Config{\n\t\tLogger:        l.With(slogutil.KeyPrefix, \"ratelimit\"),\n\t\tRatelimit:     uint(conf.Ratelimit),\n\t\tSubnetLenIPv4: conf.RatelimitSubnetLenIPv4,\n\t\tSubnetLenIPv6: conf.RatelimitSubnetLenIPv6,\n\t}\n\tif err = rlConf.Validate(); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid configuration: %w\", err)\n\t}\n\n\treturn ratelimit.NewMiddleware(rlConf), nil\n}\n\n// prepareCacheConfig prepares the cache configuration and returns an error if\n// there is one.\nfunc prepareCacheConfig(\n\tconf *proxy.Config,\n\tisEnabled bool,\n\tsize uint32,\n\tminTTL uint32,\n\tmaxTTL uint32,\n) (prepared *proxy.Config, err error) {\n\tif isEnabled {\n\t\tcacheSize := int(size)\n\t\terr = validate.Positive(\"cache_size\", cacheSize)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cache_enabled is true: %w\", err)\n\t\t}\n\n\t\tconf.CacheEnabled = true\n\t\tconf.CacheSizeBytes = cacheSize\n\t}\n\n\terr = validateCacheTTL(minTTL, maxTTL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"validating cache ttl: %w\", err)\n\t}\n\n\treturn conf, nil\n}\n\n// parseBogusNXDOMAIN parses the bogus NXDOMAIN strings into valid subnets.\nfunc parseBogusNXDOMAIN(confBogusNXDOMAIN []string) (subnets []netip.Prefix, err error) {\n\tfor i, s := range confBogusNXDOMAIN {\n\t\tvar subnet netip.Prefix\n\t\tsubnet, err = aghnet.ParseSubnet(s)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"subnet at index %d: %w\", i, err)\n\t\t}\n\n\t\tsubnets = append(subnets, subnet)\n\t}\n\n\treturn subnets, nil\n}\n\n// initDefaultSettings initializes default settings if nothing\n// is configured\nfunc (s *Server) initDefaultSettings() {\n\tif len(s.conf.UpstreamDNS) == 0 {\n\t\ts.conf.UpstreamDNS = defaultDNS\n\t}\n\n\tif len(s.conf.BootstrapDNS) == 0 {\n\t\ts.conf.BootstrapDNS = defaultBootstrap\n\t}\n\n\tif s.conf.UDPListenAddrs == nil {\n\t\ts.conf.UDPListenAddrs = defaultUDPListenAddrs\n\t}\n\n\tif s.conf.TCPListenAddrs == nil {\n\t\ts.conf.TCPListenAddrs = defaultTCPListenAddrs\n\t}\n\n\tif len(s.conf.BlockedHosts) == 0 {\n\t\ts.conf.BlockedHosts = defaultBlockedHosts\n\t}\n\n\tif s.conf.UpstreamTimeout == 0 {\n\t\ts.conf.UpstreamTimeout = DefaultTimeout\n\t}\n}\n\n// prepareIpsetListSettings reads and prepares the ipset configuration either\n// from a file or from the data in the configuration file.\nfunc (s *Server) prepareIpsetListSettings(ctx context.Context) (ipsets []string, err error) {\n\tfn := s.conf.IpsetListFileName\n\tif fn == \"\" {\n\t\treturn s.conf.IpsetList, nil\n\t}\n\n\t// #nosec G304 -- Trust the path explicitly given by the user.\n\tdata, err := os.ReadFile(fn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tipsets = stringutil.SplitTrimmed(string(data), \"\\n\")\n\tipsets = slices.DeleteFunc(ipsets, aghnet.IsCommentOrEmpty)\n\n\ts.logger.DebugContext(ctx, \"using ipset rules from file\", \"num\", len(ipsets), \"file\", fn)\n\n\treturn ipsets, nil\n}\n\n// loadUpstreams parses upstream DNS servers from the configured file or from\n// the configuration itself.  l must not be nil.\nfunc (conf *ServerConfig) loadUpstreams(\n\tctx context.Context,\n\tl *slog.Logger,\n) (upstreams []string, err error) {\n\tif conf.UpstreamDNSFileName == \"\" {\n\t\treturn stringutil.FilterOut(conf.UpstreamDNS, aghnet.IsCommentOrEmpty), nil\n\t}\n\n\tvar data []byte\n\t// #nosec G703 -- Trust the path explicitly given by the user.\n\tdata, err = os.ReadFile(conf.UpstreamDNSFileName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading upstream from file: %w\", err)\n\t}\n\n\tupstreams = stringutil.SplitTrimmed(string(data), \"\\n\")\n\n\tl.DebugContext(ctx, \"got upstreams\", \"number\", len(upstreams), \"filename\", conf.UpstreamDNSFileName)\n\n\treturn stringutil.FilterOut(upstreams, aghnet.IsCommentOrEmpty), nil\n}\n\n// collectListenAddr adds addrPort to addrs.  It also adds its port to\n// unspecPorts if its address is unspecified.\nfunc collectListenAddr(\n\taddrPort netip.AddrPort,\n\taddrs *container.MapSet[netip.AddrPort],\n\tunspecPorts *container.MapSet[uint16],\n) {\n\tif addrPort == (netip.AddrPort{}) {\n\t\treturn\n\t}\n\n\taddrs.Add(addrPort)\n\tif addrPort.Addr().IsUnspecified() {\n\t\tunspecPorts.Add(addrPort.Port())\n\t}\n}\n\n// collectDNSAddrs returns configured set of listening addresses.  It also\n// returns a set of ports of each unspecified listening address.\nfunc (conf *ServerConfig) collectDNSAddrs() (\n\taddrs *container.MapSet[netip.AddrPort],\n\tunspecPorts *container.MapSet[uint16],\n) {\n\taddrs = container.NewMapSet[netip.AddrPort]()\n\tunspecPorts = container.NewMapSet[uint16]()\n\n\tfor _, laddr := range conf.TCPListenAddrs {\n\t\tcollectListenAddr(laddr.AddrPort(), addrs, unspecPorts)\n\t}\n\n\tfor _, laddr := range conf.UDPListenAddrs {\n\t\tcollectListenAddr(laddr.AddrPort(), addrs, unspecPorts)\n\t}\n\n\treturn addrs, unspecPorts\n}\n\n// defaultPlainDNSPort is the default port for plain DNS.\nconst defaultPlainDNSPort uint16 = 53\n\n// addrPortSet is a set of [netip.AddrPort] values.\ntype addrPortSet interface {\n\t// Has returns true if addrPort is in the set.\n\tHas(addrPort netip.AddrPort) (ok bool)\n}\n\n// type check\nvar _ addrPortSet = emptyAddrPortSet{}\n\n// emptyAddrPortSet is the [addrPortSet] containing no values.\ntype emptyAddrPortSet struct{}\n\n// Has implements the [addrPortSet] interface for [emptyAddrPortSet].\nfunc (emptyAddrPortSet) Has(_ netip.AddrPort) (ok bool) { return false }\n\n// combinedAddrPortSet is the [addrPortSet] defined by some IP addresses along\n// with ports, any combination of which is considered being in the set.\ntype combinedAddrPortSet struct {\n\t// TODO(e.burkov):  Use container.SliceSet when available.\n\tports *container.MapSet[uint16]\n\taddrs *container.MapSet[netip.Addr]\n}\n\n// type check\nvar _ addrPortSet = (*combinedAddrPortSet)(nil)\n\n// Has implements the [addrPortSet] interface for [*combinedAddrPortSet].\nfunc (m *combinedAddrPortSet) Has(addrPort netip.AddrPort) (ok bool) {\n\treturn m.ports.Has(addrPort.Port()) && m.addrs.Has(addrPort.Addr())\n}\n\n// filterOutAddrs filters out all the upstreams that match um.  It returns all\n// the closing errors joined.\nfunc filterOutAddrs(upsConf *proxy.UpstreamConfig, set addrPortSet) (err error) {\n\tvar errs []error\n\tdelFunc := func(u upstream.Upstream) (ok bool) {\n\t\t// TODO(e.burkov):  We should probably consider the protocol of u to\n\t\t// only filter out the listening addresses of the same protocol.\n\t\taddr, parseErr := aghnet.ParseAddrPort(u.Address(), defaultPlainDNSPort)\n\t\tif parseErr != nil || !set.Has(addr) {\n\t\t\t// Don't filter out the upstream if it either cannot be parsed, or\n\t\t\t// does not match m.\n\t\t\treturn false\n\t\t}\n\n\t\terrs = append(errs, u.Close())\n\n\t\treturn true\n\t}\n\n\tupsConf.Upstreams = slices.DeleteFunc(upsConf.Upstreams, delFunc)\n\tfor d, ups := range upsConf.DomainReservedUpstreams {\n\t\tupsConf.DomainReservedUpstreams[d] = slices.DeleteFunc(ups, delFunc)\n\t}\n\tfor d, ups := range upsConf.SpecifiedDomainUpstreams {\n\t\tupsConf.SpecifiedDomainUpstreams[d] = slices.DeleteFunc(ups, delFunc)\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// ourAddrsSet returns an addrPortSet that contains all the configured listening\n// addresses.  l must not be nil.\nfunc (conf *ServerConfig) ourAddrsSet(ctx context.Context, l *slog.Logger) (m addrPortSet, err error) {\n\taddrs, unspecPorts := conf.collectDNSAddrs()\n\tswitch {\n\tcase addrs.Len() == 0:\n\t\tl.DebugContext(ctx, \"no listen addresses\")\n\n\t\treturn emptyAddrPortSet{}, nil\n\tcase unspecPorts.Len() == 0:\n\t\tl.DebugContext(ctx, \"filtering out addresses\", \"addresses\", addrs)\n\n\t\treturn addrs, nil\n\tdefault:\n\t\tvar ifaceAddrs []netip.Addr\n\t\tifaceAddrs, err = aghnet.CollectAllIfacesAddrs()\n\t\tif err != nil {\n\t\t\t// Don't wrap the error since it's informative enough as is.\n\t\t\treturn nil, err\n\t\t}\n\n\t\tl.DebugContext(\n\t\t\tctx,\n\t\t\t\"filtering out addresses\",\n\t\t\t\"addresses\", ifaceAddrs,\n\t\t\t\"ports\", unspecPorts,\n\t\t)\n\n\t\treturn &combinedAddrPortSet{\n\t\t\tports: unspecPorts,\n\t\t\taddrs: container.NewMapSet(ifaceAddrs...),\n\t\t}, nil\n\t}\n}\n\n// prepareDNSCrypt sets up the DNSCrypt configuration for the DNS proxy.\nfunc (s *Server) prepareDNSCrypt(proxyConf *proxy.Config) {\n\tdnsCryptConf := s.conf.TLSConf.DNSCryptConf\n\tif dnsCryptConf == nil {\n\t\treturn\n\t}\n\n\tproxyConf.DNSCryptUDPListenAddr = dnsCryptConf.UDPListenAddrs\n\tproxyConf.DNSCryptTCPListenAddr = dnsCryptConf.TCPListenAddrs\n\tproxyConf.DNSCryptProviderName = dnsCryptConf.ProviderName\n\tproxyConf.DNSCryptResolverCert = dnsCryptConf.ResolverCert\n}\n\n// prepareTLS sets up the TLS configuration for the DNS proxy.\nfunc (s *Server) prepareTLS(ctx context.Context, proxyConf *proxy.Config) (err error) {\n\ts.prepareDNSCrypt(proxyConf)\n\n\tif s.conf.TLSConf.Cert == nil {\n\t\treturn nil\n\t}\n\n\tif s.conf.TLSConf.TLSListenAddrs == nil && s.conf.TLSConf.QUICListenAddrs == nil {\n\t\treturn nil\n\t}\n\n\tproxyConf.TLSListenAddr = s.conf.TLSConf.TLSListenAddrs\n\tproxyConf.QUICListenAddr = s.conf.TLSConf.QUICListenAddrs\n\n\tcert, err := x509.ParseCertificate(s.conf.TLSConf.Cert.Certificate[0])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"x509.ParseCertificate(): %w\", err)\n\t}\n\n\ts.hasIPAddrs = aghtls.CertificateHasIP(cert)\n\n\tif s.conf.TLSConf.StrictSNICheck {\n\t\tif len(cert.DNSNames) != 0 {\n\t\t\ts.dnsNames = cert.DNSNames\n\t\t\ts.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"using certificate's SAN as DNS names\",\n\t\t\t\t\"dns_names\", cert.DNSNames,\n\t\t\t)\n\t\t\tslices.Sort(s.dnsNames)\n\t\t} else {\n\t\t\ts.dnsNames = []string{cert.Subject.CommonName}\n\t\t\ts.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"using certificate's CN as DNS name\",\n\t\t\t\t\"common_name\",\n\t\t\t\tcert.Subject.CommonName,\n\t\t\t)\n\t\t}\n\t}\n\n\tproxyConf.TLSConfig = &tls.Config{\n\t\tGetCertificate: s.onGetCertificate,\n\t\tCipherSuites:   s.conf.TLSCiphers,\n\t\tMinVersion:     tls.VersionTLS12,\n\t}\n\n\treturn nil\n}\n\n// isWildcard returns true if host is a wildcard hostname.\nfunc isWildcard(host string) (ok bool) {\n\treturn strings.HasPrefix(host, \"*.\")\n}\n\n// matchesDomainWildcard returns true if host matches the domain wildcard\n// pattern pat.\nfunc matchesDomainWildcard(host, pat string) (ok bool) {\n\treturn isWildcard(pat) && strings.HasSuffix(host, pat[1:])\n}\n\n// anyNameMatches returns true if sni, the client's SNI value, matches any of\n// the DNS names and patterns from certificate.  dnsNames must be sorted.\nfunc anyNameMatches(dnsNames []string, sni string) (ok bool) {\n\t// Check sni is either a valid hostname or a valid IP address.\n\tif !netutil.IsValidHostname(sni) && !netutil.IsValidIPString(sni) {\n\t\treturn false\n\t}\n\n\tif _, ok = slices.BinarySearch(dnsNames, sni); ok {\n\t\treturn true\n\t}\n\n\tfor _, dn := range dnsNames {\n\t\tif matchesDomainWildcard(sni, dn) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Called by 'tls' package when Client Hello is received\n// If the server name (from SNI) supplied by client is incorrect - we terminate the ongoing TLS handshake.\nfunc (s *Server) onGetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\tif s.conf.TLSConf.StrictSNICheck && !anyNameMatches(s.dnsNames, ch.ServerName) {\n\t\t// TODO(s.chzhen):  Pass context.\n\t\ts.logger.WarnContext(\n\t\t\tcontext.TODO(),\n\t\t\t\"unknown SNI in Client Hello\",\n\t\t\t\"server_name\", ch.ServerName,\n\t\t)\n\n\t\treturn nil, fmt.Errorf(\"invalid SNI\")\n\t}\n\n\treturn s.conf.TLSConf.Cert, nil\n}\n\n// preparePlain prepares the plain-DNS configuration for the DNS proxy.\n// preparePlain assumes that prepareTLS has already been called.\nfunc (s *Server) preparePlain(ctx context.Context, proxyConf *proxy.Config) (err error) {\n\tif s.conf.ServePlainDNS {\n\t\tproxyConf.UDPListenAddr = s.conf.UDPListenAddrs\n\t\tproxyConf.TCPListenAddr = s.conf.TCPListenAddrs\n\n\t\treturn nil\n\t}\n\n\tlenEncrypted := len(proxyConf.DNSCryptTCPListenAddr) +\n\t\tlen(proxyConf.DNSCryptUDPListenAddr) +\n\t\tlen(proxyConf.HTTPConfig.ListenAddresses) +\n\t\tlen(proxyConf.QUICListenAddr) +\n\t\tlen(proxyConf.TLSListenAddr)\n\tif lenEncrypted == 0 {\n\t\t// TODO(a.garipov): Support full disabling of all DNS.\n\t\treturn errors.Error(\"disabling plain dns requires at least one encrypted protocol\")\n\t}\n\n\ts.logger.WarnContext(ctx, \"plain dns is disabled\")\n\n\treturn nil\n}\n\n// UpdatedProtectionStatus updates protection state, if the protection was\n// disabled temporarily.  Returns the updated state of protection.\nfunc (s *Server) UpdatedProtectionStatus(\n\tctx context.Context,\n) (enabled bool, disabledUntil *time.Time) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\tenabled, disabledUntil = s.dnsFilter.ProtectionStatus()\n\tif disabledUntil == nil {\n\t\treturn enabled, nil\n\t}\n\n\tif time.Now().Before(*disabledUntil) {\n\t\treturn false, disabledUntil\n\t}\n\n\t// Update the values in a separate goroutine, unless an update is already in\n\t// progress.  Since this method is called very often, and this update is a\n\t// relatively rare situation, do not lock s.serverLock for writing, as that\n\t// can lead to freezes.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/5661.\n\tif s.protectionUpdateInProgress.CompareAndSwap(false, true) {\n\t\tgo s.enableProtectionAfterPause(ctx)\n\t}\n\n\treturn true, nil\n}\n\n// enableProtectionAfterPause sets the protection configuration to enabled\n// values.  It is intended to be used as a goroutine.\nfunc (s *Server) enableProtectionAfterPause(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, s.logger)\n\n\tdefer s.protectionUpdateInProgress.Store(false)\n\n\tdefer s.conf.ConfModifier.Apply(ctx)\n\n\ts.serverLock.Lock()\n\tdefer s.serverLock.Unlock()\n\n\ts.dnsFilter.SetProtectionStatus(true, nil)\n\n\ts.logger.InfoContext(ctx, \"protection is restarted after pause\")\n}\n\n// validateCacheTTL returns an error if the configuration of the cache TTL\n// invalid.\n//\n// TODO(s.chzhen):  Move to dnsproxy.\nfunc validateCacheTTL(minTTL, maxTTL uint32) (err error) {\n\tif minTTL == 0 && maxTTL == 0 {\n\t\treturn nil\n\t}\n\n\tif maxTTL > 0 && minTTL > maxTTL {\n\t\treturn errors.Error(\"cache_ttl_min must be less than or equal to cache_ttl_max\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dnsforward/config_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAnyNameMatches(t *testing.T) {\n\tdnsNames := []string{\"host1\", \"*.host2\", \"1.2.3.4\"}\n\tslices.Sort(dnsNames)\n\n\ttestCases := []struct {\n\t\tname    string\n\t\tdnsName string\n\t\twant    bool\n\t}{{\n\t\tname:    \"match\",\n\t\tdnsName: \"host1\",\n\t\twant:    true,\n\t}, {\n\t\tname:    \"match\",\n\t\tdnsName: \"a.host2\",\n\t\twant:    true,\n\t}, {\n\t\tname:    \"match\",\n\t\tdnsName: \"b.a.host2\",\n\t\twant:    true,\n\t}, {\n\t\tname:    \"match\",\n\t\tdnsName: \"1.2.3.4\",\n\t\twant:    true,\n\t}, {\n\t\tname:    \"mismatch_bad_ip\",\n\t\tdnsName: \"1.2.3.256\",\n\t\twant:    false,\n\t}, {\n\t\tname:    \"mismatch\",\n\t\tdnsName: \"host2\",\n\t\twant:    false,\n\t}, {\n\t\tname:    \"mismatch\",\n\t\tdnsName: \"\",\n\t\twant:    false,\n\t}, {\n\t\tname:    \"mismatch\",\n\t\tdnsName: \"*.host2\",\n\t\twant:    false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.want, anyNameMatches(dnsNames, tc.dnsName))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dnsforward/configvalidator.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// upstreamConfigValidator parses each section of an upstream configuration into\n// a corresponding [*proxy.UpstreamConfig] and checks the actual DNS\n// availability of each upstream.\ntype upstreamConfigValidator struct {\n\t// generalUpstreamResults contains upstream results of a general section.\n\tgeneralUpstreamResults map[string]*upstreamResult\n\n\t// fallbackUpstreamResults contains upstream results of a fallback section.\n\tfallbackUpstreamResults map[string]*upstreamResult\n\n\t// privateUpstreamResults contains upstream results of a private section.\n\tprivateUpstreamResults map[string]*upstreamResult\n\n\t// generalParseResults contains parsing results of a general section.\n\tgeneralParseResults []*parseResult\n\n\t// fallbackParseResults contains parsing results of a fallback section.\n\tfallbackParseResults []*parseResult\n\n\t// privateParseResults contains parsing results of a private section.\n\tprivateParseResults []*parseResult\n}\n\n// upstreamResult is a result of parsing of an [upstream.Upstream] within an\n// [proxy.UpstreamConfig].\ntype upstreamResult struct {\n\t// server is the parsed upstream.\n\tserver upstream.Upstream\n\n\t// err is the upstream check error.\n\terr error\n\n\t// isSpecific is true if the upstream is domain-specific.\n\tisSpecific bool\n}\n\n// parseResult contains a original piece of upstream configuration and a\n// corresponding error.\ntype parseResult struct {\n\terr      *proxy.ParseError\n\toriginal string\n}\n\n// newUpstreamConfigValidator parses the upstream configuration and returns a\n// validator for it.  cv already contains the parsed upstreams along with errors\n// related.\nfunc newUpstreamConfigValidator(\n\tctx context.Context,\n\tgeneral []string,\n\tfallback []string,\n\tprivate []string,\n\topts *upstream.Options,\n) (cv *upstreamConfigValidator) {\n\tcv = &upstreamConfigValidator{\n\t\tgeneralUpstreamResults:  map[string]*upstreamResult{},\n\t\tfallbackUpstreamResults: map[string]*upstreamResult{},\n\t\tprivateUpstreamResults:  map[string]*upstreamResult{},\n\t}\n\n\tconf, err := proxy.ParseUpstreamsConfig(general, opts)\n\tcv.generalParseResults = collectErrResults(ctx, opts.Logger, general, err)\n\tinsertConfResults(conf, cv.generalUpstreamResults)\n\n\tconf, err = proxy.ParseUpstreamsConfig(fallback, opts)\n\tcv.fallbackParseResults = collectErrResults(ctx, opts.Logger, fallback, err)\n\tinsertConfResults(conf, cv.fallbackUpstreamResults)\n\n\tconf, err = proxy.ParseUpstreamsConfig(private, opts)\n\tcv.privateParseResults = collectErrResults(ctx, opts.Logger, private, err)\n\tinsertConfResults(conf, cv.privateUpstreamResults)\n\n\treturn cv\n}\n\n// collectErrResults parses err and returns parsing results containing the\n// original upstream configuration line and the corresponding error.  err can be\n// nil.  l must not be nil.\nfunc collectErrResults(ctx context.Context, l *slog.Logger, lines []string, err error) (results []*parseResult) {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\t// limit is a maximum length for upstream configuration lines.\n\tconst limit = 80\n\n\twrapper, ok := err.(errors.WrapperSlice)\n\tif !ok {\n\t\tl.DebugContext(ctx, \"unwrapping\", slogutil.KeyError, err)\n\n\t\treturn nil\n\t}\n\n\terrs := wrapper.Unwrap()\n\tresults = make([]*parseResult, 0, len(errs))\n\tfor i, e := range errs {\n\t\tvar parseErr *proxy.ParseError\n\t\tif !errors.As(e, &parseErr) {\n\t\t\tl.DebugContext(ctx, \"inserting unexpected error\", \"index\", i, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tidx := parseErr.Idx\n\t\tline := []rune(lines[idx])\n\t\tif len(line) > limit {\n\t\t\tline = line[:limit]\n\t\t\tline[limit-1] = '…'\n\t\t}\n\n\t\tresults = append(results, &parseResult{\n\t\t\toriginal: string(line),\n\t\t\terr:      parseErr,\n\t\t})\n\t}\n\n\treturn results\n}\n\n// insertConfResults parses conf and inserts the upstream result into results.\n// It can insert multiple results as well as none.\nfunc insertConfResults(conf *proxy.UpstreamConfig, results map[string]*upstreamResult) {\n\tinsertListResults(conf.Upstreams, results, false)\n\n\tfor _, ups := range conf.DomainReservedUpstreams {\n\t\tinsertListResults(ups, results, true)\n\t}\n\n\tfor _, ups := range conf.SpecifiedDomainUpstreams {\n\t\tinsertListResults(ups, results, true)\n\t}\n}\n\n// insertListResults constructs upstream results from the upstream list and\n// inserts them into results.  It can insert multiple results as well as none.\nfunc insertListResults(ups []upstream.Upstream, results map[string]*upstreamResult, specific bool) {\n\tfor _, u := range ups {\n\t\taddr := u.Address()\n\t\t_, ok := results[addr]\n\t\tif ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tresults[addr] = &upstreamResult{\n\t\t\tserver:     u,\n\t\t\tisSpecific: specific,\n\t\t}\n\t}\n}\n\n// check tries to exchange with each successfully parsed upstream and enriches\n// the results with the healthcheck errors.  It should not be called after the\n// [upsConfValidator.close] method, since it makes no sense to check the closed\n// upstreams.  l must not be nil.\nfunc (cv *upstreamConfigValidator) check(ctx context.Context, l *slog.Logger) {\n\tconst (\n\t\t// testTLD is the special-use fully-qualified domain name for testing\n\t\t// the DNS server reachability.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc6761#section-6.2.\n\t\ttestTLD = \"test.\"\n\n\t\t// inAddrARPATLD is the special-use fully-qualified domain name for PTR\n\t\t// IP address resolution.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc1035#section-3.5.\n\t\tinAddrARPATLD = \"in-addr.arpa.\"\n\t)\n\n\tcommonChecker := &healthchecker{\n\t\thostname: testTLD,\n\t\tqtype:    dns.TypeA,\n\t\tansEmpty: true,\n\t}\n\n\tarpaChecker := &healthchecker{\n\t\thostname: inAddrARPATLD,\n\t\tqtype:    dns.TypePTR,\n\t\tansEmpty: false,\n\t}\n\n\twg := &sync.WaitGroup{}\n\n\tfor _, res := range cv.generalUpstreamResults {\n\t\twg.Go(func() { checkSrv(ctx, l, res, commonChecker) })\n\t}\n\tfor _, res := range cv.fallbackUpstreamResults {\n\t\twg.Go(func() { checkSrv(ctx, l, res, commonChecker) })\n\t}\n\tfor _, res := range cv.privateUpstreamResults {\n\t\twg.Go(func() { checkSrv(ctx, l, res, arpaChecker) })\n\t}\n\n\twg.Wait()\n}\n\n// checkSrv runs hc on the server from res, if any, and stores any occurred\n// error in res.  It is used to be run in a separate goroutine.  l must not be\n// nil.\nfunc checkSrv(ctx context.Context, l *slog.Logger, res *upstreamResult, hc *healthchecker) {\n\tdefer slogutil.RecoverAndLog(ctx, l)\n\n\tres.err = hc.check(res.server)\n\tif res.err != nil && res.isSpecific {\n\t\tres.err = domainSpecificTestError{Err: res.err}\n\t}\n}\n\n// close closes all the upstreams that were successfully parsed.  It enriches\n// the results with deferred closing errors.\nfunc (cv *upstreamConfigValidator) close() {\n\tall := []map[string]*upstreamResult{\n\t\tcv.generalUpstreamResults,\n\t\tcv.fallbackUpstreamResults,\n\t\tcv.privateUpstreamResults,\n\t}\n\n\tfor _, m := range all {\n\t\tfor _, r := range m {\n\t\t\tr.err = errors.WithDeferred(r.err, r.server.Close())\n\t\t}\n\t}\n}\n\n// sections of the upstream configuration according to the text label of the\n// localization.\n//\n// Keep in sync with client/src/__locales/en.json.\n//\n// TODO(s.chzhen):  Refactor.\nconst (\n\tgeneralTextLabel  = \"upstream_dns\"\n\tfallbackTextLabel = \"fallback_dns_title\"\n\tprivateTextLabel  = \"local_ptr_title\"\n)\n\n// status returns all the data collected during parsing, healthcheck, and\n// closing of the upstreams.  The returned map is keyed by the original upstream\n// configuration piece and contains the corresponding error or \"OK\" if there was\n// no error.  l must not be nil.\nfunc (cv *upstreamConfigValidator) status(\n\tctx context.Context,\n\tl *slog.Logger,\n) (results map[string]string) {\n\t// Names of the upstream configuration sections for logging.\n\tconst (\n\t\tgeneralSection  = \"general\"\n\t\tfallbackSection = \"fallback\"\n\t\tprivateSection  = \"private\"\n\t)\n\n\tresults = map[string]string{}\n\n\tfor original, res := range cv.generalUpstreamResults {\n\t\tupstreamResultToStatus(ctx, l, generalSection, string(original), res, results)\n\t}\n\tfor original, res := range cv.fallbackUpstreamResults {\n\t\tupstreamResultToStatus(ctx, l, fallbackSection, string(original), res, results)\n\t}\n\tfor original, res := range cv.privateUpstreamResults {\n\t\tupstreamResultToStatus(ctx, l, privateSection, string(original), res, results)\n\t}\n\n\tparseResultToStatus(ctx, l, generalTextLabel, generalSection, cv.generalParseResults, results)\n\tparseResultToStatus(\n\t\tctx,\n\t\tl,\n\t\tfallbackTextLabel,\n\t\tfallbackSection,\n\t\tcv.fallbackParseResults,\n\t\tresults,\n\t)\n\tparseResultToStatus(ctx, l, privateTextLabel, privateSection, cv.privateParseResults, results)\n\n\treturn results\n}\n\n// upstreamResultToStatus puts \"OK\" or an error message from res into resMap.\n// section is the name of the upstream configuration section, i.e. \"general\",\n// \"fallback\", or \"private\", and only used for logging.  l must not be nil.\n//\n// TODO(e.burkov):  Currently, the HTTP handler expects that all the results are\n// put together in a single map, which may lead to collisions, see AG-27539.\n// Improve the results compilation.\nfunc upstreamResultToStatus(\n\tctx context.Context,\n\tl *slog.Logger,\n\tsection string,\n\toriginal string,\n\tres *upstreamResult,\n\tresMap map[string]string,\n) {\n\tval := \"OK\"\n\tif res.err != nil {\n\t\tval = res.err.Error()\n\t}\n\n\tprevVal := resMap[original]\n\tswitch prevVal {\n\tcase \"\":\n\t\tresMap[original] = val\n\tcase val:\n\t\tl.DebugContext(ctx, \"duplicating config line\", \"section\", section, \"address\", original)\n\tdefault:\n\t\tl.WarnContext(\n\t\t\tctx,\n\t\t\t\"config line had different result\",\n\t\t\t\"section\", section,\n\t\t\t\"result\", val,\n\t\t\t\"original\", original,\n\t\t\t\"previous\", prevVal,\n\t\t)\n\t}\n}\n\n// parseResultToStatus puts parsing error messages from results into resMap.\n// section is the name of the upstream configuration section, i.e. \"general\",\n// \"fallback\", or \"private\", and only used for logging.  l must not be nil.\n//\n// Parsing error message has the following format:\n//\n//\tsectionTextLabel line: parsing error\n//\n// Where sectionTextLabel is a section text label of a localization and line is\n// a line number.\nfunc parseResultToStatus(\n\tctx context.Context,\n\tl *slog.Logger,\n\ttextLabel string,\n\tsection string,\n\tresults []*parseResult,\n\tresMap map[string]string,\n) {\n\tfor _, res := range results {\n\t\toriginal := res.original\n\t\t_, ok := resMap[original]\n\t\tif ok {\n\t\t\tl.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"duplicating parsing error\",\n\t\t\t\t\"section\",\n\t\t\t\tsection,\n\t\t\t\t\"original\",\n\t\t\t\toriginal,\n\t\t\t)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tresMap[original] = fmt.Sprintf(\"%s %d: parsing error\", textLabel, res.err.Idx+1)\n\t}\n}\n\n// domainSpecificTestError is a wrapper for errors returned by checkDNS to mark\n// the tested upstream domain-specific and therefore consider its errors\n// non-critical.\n//\n// TODO(a.garipov):  Some common mechanism of distinguishing between errors and\n// warnings (non-critical errors) is desired.\ntype domainSpecificTestError struct {\n\t// Err is the actual error occurred during healthcheck test.\n\tErr error\n}\n\n// type check\nvar _ error = domainSpecificTestError{}\n\n// Error implements the [error] interface for domainSpecificTestError.\nfunc (err domainSpecificTestError) Error() (msg string) {\n\treturn fmt.Sprintf(\"WARNING: %s\", err.Err)\n}\n\n// type check\nvar _ errors.Wrapper = domainSpecificTestError{}\n\n// Unwrap implements the [errors.Wrapper] interface for domainSpecificTestError.\nfunc (err domainSpecificTestError) Unwrap() (wrapped error) {\n\treturn err.Err\n}\n\n// healthchecker checks the upstream's status by exchanging with it.\ntype healthchecker struct {\n\t// hostname is the name of the host to put into healthcheck DNS request.\n\thostname string\n\n\t// qtype is the type of DNS request to use for healthcheck.\n\tqtype uint16\n\n\t// ansEmpty defines if the answer section within the response is expected to\n\t// be empty.\n\tansEmpty bool\n}\n\n// check exchanges with u and validates the response.\nfunc (h *healthchecker) check(u upstream.Upstream) (err error) {\n\treq := &dns.Msg{\n\t\tMsgHdr: dns.MsgHdr{\n\t\t\tId:               dns.Id(),\n\t\t\tRecursionDesired: true,\n\t\t},\n\t\tQuestion: []dns.Question{{\n\t\t\tName:   h.hostname,\n\t\t\tQtype:  h.qtype,\n\t\t\tQclass: dns.ClassINET,\n\t\t}},\n\t}\n\n\treply, err := u.Exchange(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"couldn't communicate with upstream: %w\", err)\n\t} else if h.ansEmpty && len(reply.Answer) > 0 {\n\t\treturn errors.Error(\"wrong response\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dnsforward/context.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// ctxKey is the type for context keys.\ntype ctxKey int\n\n// Context key values.\nconst (\n\tctxKeyClientID ctxKey = iota\n)\n\n// contextWithClientID returns a new context with the given ID.\nfunc contextWithClientID(parent context.Context, id string) (ctx context.Context) {\n\treturn context.WithValue(parent, ctxKeyClientID, id)\n}\n\n// clientIDFromContext returns ID for this request, if any.\nfunc clientIDFromContext(ctx context.Context) (id string, ok bool) {\n\tv := ctx.Value(ctxKeyClientID)\n\tif v == nil {\n\t\treturn id, false\n\t}\n\n\tid, ok = v.(string)\n\tif !ok {\n\t\tpanic(fmt.Errorf(\"bad type for ctxKeyClientID: %T(%[1]v)\", v))\n\t}\n\n\treturn id, true\n}\n"
  },
  {
    "path": "internal/dnsforward/dialcontext.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n)\n\n// DialContext is an [aghnet.DialContextFunc] that uses s to resolve hostnames.\n// addr should be a valid host:port address, where host could be a domain name\n// or an IP address.\nfunc (s *Server) DialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) {\n\ts.logger.DebugContext(ctx, \"dialing\", \"addr\", addr, \"network\", network)\n\n\thost, portStr, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdialer := &net.Dialer{\n\t\t// TODO(a.garipov): Consider making configurable.\n\t\tTimeout: time.Minute * 5,\n\t}\n\n\tif netutil.IsValidIPString(host) {\n\t\treturn dialer.DialContext(ctx, network, addr)\n\t}\n\n\tport, err := strconv.Atoi(portStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid port %s: %w\", portStr, err)\n\t}\n\n\tips, err := s.Resolve(ctx, network, host)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"resolving %q: %w\", host, err)\n\t} else if len(ips) == 0 {\n\t\treturn nil, fmt.Errorf(\"no addresses for host %q\", host)\n\t}\n\n\ts.logger.DebugContext(ctx, \"resolved\", \"host\", host, \"ips\", ips)\n\n\tvar dialErrs []error\n\tfor _, ip := range ips {\n\t\taddrPort := netip.AddrPortFrom(ip, uint16(port))\n\t\tconn, err = dialer.DialContext(ctx, network, addrPort.String())\n\t\tif err != nil {\n\t\t\tdialErrs = append(dialErrs, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\treturn conn, nil\n\t}\n\n\treturn nil, errors.Join(dialErrs...)\n}\n"
  },
  {
    "path": "internal/dnsforward/dns64.go",
    "content": "package dnsforward\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n)\n\n// setupDNS64 initializes DNS64 settings, the NAT64 prefixes in particular.  If\n// the DNS64 feature is enabled and no prefixes are configured, the default\n// Well-Known Prefix is used, just like Section 5.2 of RFC 6147 prescribes.  Any\n// configured set of prefixes discards the default Well-Known prefix unless it\n// is specified explicitly.  Each prefix also validated to be a valid IPv6\n// CIDR with a maximum length of 96 bits.  The first specified prefix is then\n// used to synthesize AAAA records.\nfunc (s *Server) setupDNS64() {\n\tif !s.conf.UseDNS64 {\n\t\treturn\n\t}\n\n\tif len(s.conf.DNS64Prefixes) == 0 {\n\t\t// dns64WellKnownPref is the default prefix to use in an algorithmic\n\t\t// mapping for DNS64.\n\t\t//\n\t\t// See https://datatracker.ietf.org/doc/html/rfc6052#section-2.1.\n\t\tdns64WellKnownPref := netip.MustParsePrefix(\"64:ff9b::/96\")\n\n\t\ts.dns64Pref = dns64WellKnownPref\n\t} else {\n\t\ts.dns64Pref = s.conf.DNS64Prefixes[0]\n\t}\n}\n\n// mapDNS64 maps ip to IPv6 address using configured DNS64 prefix.  ip must be a\n// valid IPv4.  It panics, if there are no configured DNS64 prefixes, because\n// synthesis should not be performed unless DNS64 function enabled.\nfunc (s *Server) mapDNS64(ip netip.Addr) (mapped net.IP) {\n\tpref := s.dns64Pref.Masked().Addr().As16()\n\tipData := ip.As4()\n\n\tmapped = make(net.IP, net.IPv6len)\n\tcopy(mapped[:proxy.NAT64PrefixLength], pref[:])\n\tcopy(mapped[proxy.NAT64PrefixLength:], ipData[:])\n\n\treturn mapped\n}\n"
  },
  {
    "path": "internal/dnsforward/dns64_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// maxDNS64SynTTL is the maximum TTL for synthesized DNS64 responses with no SOA\n// records in seconds.\n//\n// If the SOA RR was not delivered with the negative response to the AAAA query,\n// then the DNS64 SHOULD use the TTL of the original A RR or 600 seconds,\n// whichever is shorter.\n//\n// See https://datatracker.ietf.org/doc/html/rfc6147#section-5.1.7.\nconst maxDNS64SynTTL uint32 = 600\n\n// newRR is a helper that creates a new dns.RR with the given name, qtype, ttl\n// and value.  It fails the test if the qtype is not supported or the type of\n// value doesn't match the qtype.\nfunc newRR(tb testing.TB, name string, qtype uint16, ttl uint32, val any) (rr dns.RR) {\n\ttb.Helper()\n\n\tswitch qtype {\n\tcase dns.TypeA:\n\t\trr = &dns.A{A: testutil.RequireTypeAssert[net.IP](tb, val)}\n\tcase dns.TypeAAAA:\n\t\trr = &dns.AAAA{AAAA: testutil.RequireTypeAssert[net.IP](tb, val)}\n\tcase dns.TypeCNAME:\n\t\trr = &dns.CNAME{Target: testutil.RequireTypeAssert[string](tb, val)}\n\tcase dns.TypeSOA:\n\t\trr = &dns.SOA{\n\t\t\tNs:      \"ns.\" + name,\n\t\t\tMbox:    \"hostmaster.\" + name,\n\t\t\tSerial:  1,\n\t\t\tRefresh: 1,\n\t\t\tRetry:   1,\n\t\t\tExpire:  1,\n\t\t\tMinttl:  1,\n\t\t}\n\tcase dns.TypePTR:\n\t\trr = &dns.PTR{Ptr: testutil.RequireTypeAssert[string](tb, val)}\n\tdefault:\n\t\ttb.Fatalf(\"unsupported qtype: %d\", qtype)\n\t}\n\n\t*rr.Header() = dns.RR_Header{\n\t\tName:   name,\n\t\tRrtype: qtype,\n\t\tClass:  dns.ClassINET,\n\t\tTtl:    ttl,\n\t}\n\n\treturn rr\n}\n\nfunc TestServer_ServeDNS_dns64(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tipv4Domain    = \"ipv4.only.\"\n\t\tipv6Domain    = \"ipv6.only.\"\n\t\tsoaDomain     = \"ipv4.soa.\"\n\t\tmappedDomain  = \"filterable.ipv6.\"\n\t\tanotherDomain = \"another.domain.\"\n\n\t\tpointedDomain = \"local1234.ipv4.\"\n\t\tglobDomain    = \"real1234.ipv4.\"\n\t)\n\n\tsomeIPv4 := net.IP{1, 2, 3, 4}\n\tsomeIPv6 := net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}\n\tmappedIPv6 := net.ParseIP(\"64:ff9b::102:304\")\n\n\tptr64Domain, err := netutil.IPToReversedAddr(mappedIPv6)\n\trequire.NoError(t, err)\n\tptr64Domain = dns.Fqdn(ptr64Domain)\n\n\tptrGlobDomain, err := netutil.IPToReversedAddr(someIPv4)\n\trequire.NoError(t, err)\n\tptrGlobDomain = dns.Fqdn(ptrGlobDomain)\n\n\tconst (\n\t\tsectionAnswer = iota\n\t\tsectionAuthority\n\t\tsectionAdditional\n\n\t\tsectionsNum\n\t)\n\n\t// answerMap is a convenience alias for describing the upstream response for\n\t// a given question type.\n\ttype answerMap = map[uint16][sectionsNum][]dns.RR\n\n\tpt := testutil.PanicT{}\n\n\ttestCases := []struct {\n\t\tname    string\n\t\tqname   string\n\t\tupsAns  answerMap\n\t\twantAns []dns.RR\n\t\tqtype   uint16\n\t}{{\n\t\tname:  \"simple_a\",\n\t\tqname: ipv4Domain,\n\t\tupsAns: answerMap{\n\t\t\tdns.TypeA: {\n\t\t\t\tsectionAnswer: {newRR(t, ipv4Domain, dns.TypeA, 3600, someIPv4)},\n\t\t\t},\n\t\t\tdns.TypeAAAA: {},\n\t\t},\n\t\twantAns: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     ipv4Domain,\n\t\t\t\tRrtype:   dns.TypeA,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      3600,\n\t\t\t\tRdlength: 4,\n\t\t\t},\n\t\t\tA: someIPv4,\n\t\t}},\n\t\tqtype: dns.TypeA,\n\t}, {\n\t\tname:  \"simple_aaaa\",\n\t\tqname: ipv6Domain,\n\t\tupsAns: answerMap{\n\t\t\tdns.TypeA: {},\n\t\t\tdns.TypeAAAA: {\n\t\t\t\tsectionAnswer: {newRR(t, ipv6Domain, dns.TypeAAAA, 3600, someIPv6)},\n\t\t\t},\n\t\t},\n\t\twantAns: []dns.RR{&dns.AAAA{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     ipv6Domain,\n\t\t\t\tRrtype:   dns.TypeAAAA,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      3600,\n\t\t\t\tRdlength: 16,\n\t\t\t},\n\t\t\tAAAA: someIPv6,\n\t\t}},\n\t\tqtype: dns.TypeAAAA,\n\t}, {\n\t\tname:  \"actual_dns64\",\n\t\tqname: ipv4Domain,\n\t\tupsAns: answerMap{\n\t\t\tdns.TypeA: {\n\t\t\t\tsectionAnswer: {newRR(t, ipv4Domain, dns.TypeA, 3600, someIPv4)},\n\t\t\t},\n\t\t\tdns.TypeAAAA: {},\n\t\t},\n\t\twantAns: []dns.RR{&dns.AAAA{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     ipv4Domain,\n\t\t\t\tRrtype:   dns.TypeAAAA,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      maxDNS64SynTTL,\n\t\t\t\tRdlength: 16,\n\t\t\t},\n\t\t\tAAAA: mappedIPv6,\n\t\t}},\n\t\tqtype: dns.TypeAAAA,\n\t}, {\n\t\tname:  \"actual_dns64_soattl\",\n\t\tqname: soaDomain,\n\t\tupsAns: answerMap{\n\t\t\tdns.TypeA: {\n\t\t\t\tsectionAnswer: {newRR(t, soaDomain, dns.TypeA, 3600, someIPv4)},\n\t\t\t},\n\t\t\tdns.TypeAAAA: {\n\t\t\t\tsectionAuthority: {newRR(t, soaDomain, dns.TypeSOA, maxDNS64SynTTL+50, nil)},\n\t\t\t},\n\t\t},\n\t\twantAns: []dns.RR{&dns.AAAA{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     soaDomain,\n\t\t\t\tRrtype:   dns.TypeAAAA,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      maxDNS64SynTTL + 50,\n\t\t\t\tRdlength: 16,\n\t\t\t},\n\t\t\tAAAA: mappedIPv6,\n\t\t}},\n\t\tqtype: dns.TypeAAAA,\n\t}, {\n\t\tname:  \"filtered\",\n\t\tqname: mappedDomain,\n\t\tupsAns: answerMap{\n\t\t\tdns.TypeA: {},\n\t\t\tdns.TypeAAAA: {\n\t\t\t\tsectionAnswer: {\n\t\t\t\t\tnewRR(t, mappedDomain, dns.TypeAAAA, 3600, net.ParseIP(\"64:ff9b::506:708\")),\n\t\t\t\t\tnewRR(t, mappedDomain, dns.TypeCNAME, 3600, anotherDomain),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\twantAns: []dns.RR{&dns.CNAME{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     mappedDomain,\n\t\t\t\tRrtype:   dns.TypeCNAME,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      3600,\n\t\t\t\tRdlength: 16,\n\t\t\t},\n\t\t\tTarget: anotherDomain,\n\t\t}},\n\t\tqtype: dns.TypeAAAA,\n\t}, {\n\t\tname:   \"ptr\",\n\t\tqname:  ptr64Domain,\n\t\tupsAns: nil,\n\t\twantAns: []dns.RR{&dns.PTR{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     ptr64Domain,\n\t\t\t\tRrtype:   dns.TypePTR,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      3600,\n\t\t\t\tRdlength: 16,\n\t\t\t},\n\t\t\tPtr: pointedDomain,\n\t\t}},\n\t\tqtype: dns.TypePTR,\n\t}, {\n\t\tname:  \"ptr_glob\",\n\t\tqname: ptrGlobDomain,\n\t\tupsAns: answerMap{\n\t\t\tdns.TypePTR: {\n\t\t\t\tsectionAnswer: {newRR(t, ptrGlobDomain, dns.TypePTR, 3600, globDomain)},\n\t\t\t},\n\t\t},\n\t\twantAns: []dns.RR{&dns.PTR{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     ptrGlobDomain,\n\t\t\t\tRrtype:   dns.TypePTR,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      3600,\n\t\t\t\tRdlength: 15,\n\t\t\t},\n\t\t\tPtr: globDomain,\n\t\t}},\n\t\tqtype: dns.TypePTR,\n\t}}\n\n\tlocalRR := newRR(t, ptr64Domain, dns.TypePTR, 3600, pointedDomain)\n\tlocalUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {\n\t\trequire.Len(pt, m.Question, 1)\n\t\trequire.Equal(pt, m.Question[0].Name, ptr64Domain)\n\n\t\tresp := (&dns.Msg{}).SetReply(m)\n\t\tresp.Answer = []dns.RR{localRR}\n\n\t\trequire.NoError(t, w.WriteMsg(resp))\n\t})\n\tlocalUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()\n\n\tclient := &dns.Client{\n\t\tNet:     string(proxy.ProtoTCP),\n\t\tTimeout: testTimeout,\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tupsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\t\t\tq := req.Question[0]\n\n\t\t\t\trequire.Contains(pt, tc.upsAns, q.Qtype)\n\t\t\t\tanswer := tc.upsAns[q.Qtype]\n\n\t\t\t\tresp := (&dns.Msg{}).SetReply(req)\n\t\t\t\tresp.Answer = answer[sectionAnswer]\n\t\t\t\tresp.Ns = answer[sectionAuthority]\n\t\t\t\tresp.Extra = answer[sectionAdditional]\n\n\t\t\t\trequire.NoError(pt, w.WriteMsg(resp))\n\t\t\t})\n\t\t\tupsAddr := aghtest.StartLocalhostUpstream(t, upsHdlr).String()\n\n\t\t\t// TODO(e.burkov):  It seems [proxy.Proxy] isn't intended to be\n\t\t\t// reused right after stop, due to a data race in [proxy.Proxy.Init]\n\t\t\t// method when setting an OOB size.  As a temporary workaround,\n\t\t\t// recreate the whole server for each test case.\n\t\t\ts := createTestServer(t, &filtering.Config{\n\t\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t\t}, ServerConfig{\n\t\t\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\t\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\t\t\tUseDNS64:       true,\n\t\t\t\tTLSConf:        &TLSConfig{},\n\t\t\t\tConfig: Config{\n\t\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t\t\tUpstreamDNS:      []string{upsAddr},\n\t\t\t\t},\n\t\t\t\tUsePrivateRDNS:    true,\n\t\t\t\tLocalPTRResolvers: []string{localUpsAddr},\n\t\t\t\tServePlainDNS:     true,\n\t\t\t})\n\n\t\t\tstartDeferStop(t, s)\n\n\t\t\treq := (&dns.Msg{}).SetQuestion(tc.qname, tc.qtype)\n\n\t\t\tresp, _, excErr := client.Exchange(req, s.proxy().Addr(proxy.ProtoTCP).String())\n\t\t\trequire.NoError(t, excErr)\n\n\t\t\trequire.Equal(t, tc.wantAns, resp.Answer)\n\t\t})\n\t}\n}\n\nfunc TestServer_dns64WithDisabledRDNS(t *testing.T) {\n\tt.Parallel()\n\n\t// Shouldn't go to upstream at all.\n\tpanicHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {\n\t\tpanic(testutil.UnexpectedCall(w, m))\n\t})\n\tupsAddr := aghtest.StartLocalhostUpstream(t, panicHdlr).String()\n\tlocalUpsAddr := aghtest.StartLocalhostUpstream(t, panicHdlr).String()\n\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tUseDNS64:       true,\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\tUpstreamDNS:      []string{upsAddr},\n\t\t},\n\t\tUsePrivateRDNS:    false,\n\t\tLocalPTRResolvers: []string{localUpsAddr},\n\t\tServePlainDNS:     true,\n\t})\n\tstartDeferStop(t, s)\n\n\tmappedIPv6 := net.ParseIP(\"64:ff9b::102:304\")\n\tarpa, err := netutil.IPToReversedAddr(mappedIPv6)\n\trequire.NoError(t, err)\n\n\treq := (&dns.Msg{}).SetQuestion(dns.Fqdn(arpa), dns.TypePTR)\n\n\tcli := &dns.Client{\n\t\tNet:     string(proxy.ProtoTCP),\n\t\tTimeout: testTimeout,\n\t}\n\n\tresp, _, err := cli.Exchange(req, s.proxy().Addr(proxy.ProtoTCP).String())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, dns.RcodeNameError, resp.Rcode)\n}\n"
  },
  {
    "path": "internal/dnsforward/dnsforward.go",
    "content": "// Package dnsforward contains a DNS forwarding server.\npackage dnsforward\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghslog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/querylog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/rdns\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/stats\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/sysresolv\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// DefaultTimeout is the default upstream timeout\nconst DefaultTimeout = 10 * time.Second\n\n// defaultLocalTimeout is the default timeout for resolving addresses from\n// locally-served networks.  It is assumed that local resolvers should work much\n// faster than ordinary upstreams.\nconst defaultLocalTimeout = 1 * time.Second\n\nvar defaultDNS = []string{\n\t\"https://dns10.quad9.net/dns-query\",\n}\nvar defaultBootstrap = []string{\"9.9.9.10\", \"149.112.112.10\", \"2620:fe::10\", \"2620:fe::fe:10\"}\n\n// Often requested by all kinds of DNS probes\nvar defaultBlockedHosts = []string{\"version.bind\", \"id.server\", \"hostname.bind\"}\n\nvar (\n\t// defaultUDPListenAddrs are the default UDP addresses for the server.\n\tdefaultUDPListenAddrs = []*net.UDPAddr{{Port: 53}}\n\n\t// defaultTCPListenAddrs are the default TCP addresses for the server.\n\tdefaultTCPListenAddrs = []*net.TCPAddr{{Port: 53}}\n)\n\nvar webRegistered bool\n\n// DHCP is an interface for accessing DHCP lease data needed in this package.\ntype DHCP interface {\n\t// HostByIP returns the hostname of the DHCP client with the given IP\n\t// address.  The address will be netip.Addr{} if there is no such client,\n\t// due to an assumption that a DHCP client must always have an IP address.\n\tHostByIP(ip netip.Addr) (host string)\n\n\t// IPByHost returns the IP address of the DHCP client with the given\n\t// hostname.  The hostname will be an empty string if there is no such\n\t// client, due to an assumption that a DHCP client must always have a\n\t// hostname, either set by the client or assigned automatically.\n\tIPByHost(host string) (ip netip.Addr)\n\n\t// Enabled returns true if DHCP provides information about clients.\n\tEnabled() (ok bool)\n}\n\n// SystemResolvers is an interface for accessing the OS-provided resolvers.\ntype SystemResolvers interface {\n\t// Addrs returns the list of system resolvers' addresses.  Callers must\n\t// clone the returned slice before modifying it.  Implementations of Addrs\n\t// must be safe for concurrent use.\n\tAddrs() (addrs []netip.AddrPort)\n}\n\n// Server is the main way to start a DNS server.\n//\n// Example:\n//\n//\ts := dnsforward.Server{}\n//\terr := s.Start(nil) // will start a DNS server listening on default port 53, in a goroutine\n//\terr := s.Reconfigure(ServerConfig{UDPListenAddr: &net.UDPAddr{Port: 53535}}) // will reconfigure running DNS server to listen on UDP port 53535\n//\terr := s.Stop() // will stop listening on port 53535 and cancel all goroutines\n//\terr := s.Start(nil) // will start listening again, on port 53535, in a goroutine\n//\n// The zero Server is empty and ready for use.\ntype Server struct {\n\t// addrProc, if not nil, is used to process clients' IP addresses with rDNS,\n\t// WHOIS, etc.\n\taddrProc client.AddressProcessor\n\n\t// bootstrap is the resolver for upstreams' hostnames.\n\tbootstrap upstream.Resolver\n\n\t// dhcpServer is the DHCP server for accessing lease data.\n\tdhcpServer DHCP\n\n\t// etcHosts contains the current data from the system's hosts files.\n\tetcHosts upstream.Resolver\n\n\t// privateNets is the configured set of IP networks considered private.\n\tprivateNets netutil.SubnetSet\n\n\t// queryLog is the query log for client's DNS requests, responses and\n\t// filtering results.\n\tqueryLog querylog.QueryLog\n\n\t// stats is the statistics collector for client's DNS usage data.\n\tstats stats.Interface\n\n\t// sysResolvers used to fetch system resolvers to use by default for private\n\t// PTR resolving.\n\tsysResolvers SystemResolvers\n\n\t// access drops disallowed clients.\n\taccess *accessManager\n\n\t// anonymizer masks the client's IP addresses if needed.\n\tanonymizer *aghnet.IPMut\n\n\t// baseLogger is used to create loggers for other entities.  It should not\n\t// have a prefix and must not be nil.\n\tbaseLogger *slog.Logger\n\n\t// logger is used to log the operation of the DNS server.  It is created\n\t// during initialization in [NewServer].\n\tlogger *slog.Logger\n\n\t// dnsFilter is the DNS filter for filtering client's DNS requests and\n\t// responses.\n\tdnsFilter *filtering.DNSFilter\n\n\t// dnsProxy is the DNS proxy for forwarding client's DNS requests.\n\tdnsProxy *proxy.Proxy\n\n\t// internalProxy resolves internal requests from the application itself.  It\n\t// isn't started and so no listen ports are required.\n\tinternalProxy *proxy.Proxy\n\n\t// ipset processes DNS requests using ipset data.  It must not be nil after\n\t// initialization.  See [newIpsetHandler].\n\tipset *ipsetHandler\n\n\t// dns64Pref is the NAT64 prefix used for DNS64 response mapping.  The major\n\t// part of DNS64 happens inside the [proxy] package, but there still are\n\t// some places where response mapping is needed (e.g. DHCP).\n\tdns64Pref netip.Prefix\n\n\t// localDomainSuffix is the suffix used to detect internal hosts.  It\n\t// must be a valid domain name plus dots on each side.\n\tlocalDomainSuffix string\n\n\t// bootResolvers are the resolvers that should be used for\n\t// bootstrapping along with [etcHosts].\n\t//\n\t// TODO(e.burkov):  Use [proxy.UpstreamConfig] when it will implement the\n\t// [upstream.Resolver] interface.\n\tbootResolvers []*upstream.UpstreamResolver\n\n\t// dnsNames are the DNS names from certificate (SAN) or CN value from\n\t// Subject.\n\tdnsNames []string\n\n\t// conf is the current configuration of the server.\n\tconf ServerConfig\n\n\t// serverLock protects Server.\n\tserverLock sync.RWMutex\n\n\t// protectionUpdateInProgress is used to make sure that only one goroutine\n\t// updating the protection configuration after a pause is running at a time.\n\tprotectionUpdateInProgress atomic.Bool\n\n\t// isRunning is true if the DNS server is running.\n\tisRunning bool\n\n\t// hasIPAddrs is set during the certificate parsing and is true if the\n\t// configured certificate contains at least a single IP address.\n\thasIPAddrs bool\n}\n\n// defaultLocalDomainSuffix is the default suffix used to detect internal hosts\n// when no suffix is provided.\n//\n// See the documentation for Server.localDomainSuffix.\nconst defaultLocalDomainSuffix = \"lan\"\n\n// DNSCreateParams are parameters to create a new server.\ntype DNSCreateParams struct {\n\tDNSFilter   *filtering.DNSFilter\n\tStats       stats.Interface\n\tQueryLog    querylog.QueryLog\n\tDHCPServer  DHCP\n\tPrivateNets netutil.SubnetSet\n\tAnonymizer  *aghnet.IPMut\n\tEtcHosts    *aghnet.HostsContainer\n\n\t// Logger is used as a base logger.  It must not be nil.\n\tLogger *slog.Logger\n\n\tLocalDomain string\n}\n\n// NewServer creates a new instance of the dnsforward.Server\n// Note: this function must be called only once\n//\n// TODO(a.garipov): How many constructors and initializers does this thing have?\n// Refactor!\nfunc NewServer(p DNSCreateParams) (s *Server, err error) {\n\tvar localDomainSuffix string\n\tif p.LocalDomain == \"\" {\n\t\tlocalDomainSuffix = defaultLocalDomainSuffix\n\t} else {\n\t\terr = netutil.ValidateDomainName(p.LocalDomain)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"local domain: %w\", err)\n\t\t}\n\n\t\tlocalDomainSuffix = p.LocalDomain\n\t}\n\n\tif p.Anonymizer == nil {\n\t\tp.Anonymizer = aghnet.NewIPMut(nil)\n\t}\n\n\tvar etcHosts upstream.Resolver\n\tif p.EtcHosts != nil {\n\t\tetcHosts = upstream.NewHostsResolver(p.EtcHosts)\n\t}\n\n\ts = &Server{\n\t\tdnsFilter:   p.DNSFilter,\n\t\tdhcpServer:  p.DHCPServer,\n\t\tstats:       p.Stats,\n\t\tqueryLog:    p.QueryLog,\n\t\tprivateNets: p.PrivateNets,\n\t\tbaseLogger:  p.Logger,\n\t\tlogger:      p.Logger.With(slogutil.KeyPrefix, \"dnsforward\"),\n\t\t// TODO(e.burkov):  Use some case-insensitive string comparison.\n\t\tlocalDomainSuffix: strings.ToLower(localDomainSuffix),\n\t\tetcHosts:          etcHosts,\n\t\tanonymizer:        p.Anonymizer,\n\t\tconf: ServerConfig{\n\t\t\tServePlainDNS: true,\n\t\t},\n\t}\n\n\ts.sysResolvers, err = sysresolv.NewSystemResolvers(nil, defaultPlainDNSPort)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initializing system resolvers: %w\", err)\n\t}\n\n\tif runtime.GOARCH == \"mips\" || runtime.GOARCH == \"mipsle\" {\n\t\t// Use plain DNS on MIPS, encryption is too slow\n\t\tdefaultDNS = defaultBootstrap\n\t}\n\n\treturn s, nil\n}\n\n// Close gracefully closes the server.  It is safe for concurrent use.\n//\n// TODO(e.burkov): A better approach would be making Stop method waiting for all\n// its workers finished.  But it would require the upstream.Upstream to have the\n// Close method to prevent from hanging while waiting for unresponsive server to\n// respond.\nfunc (s *Server) Close(ctx context.Context) {\n\ts.serverLock.Lock()\n\tdefer s.serverLock.Unlock()\n\n\t// TODO(s.chzhen):  Remove it.\n\ts.stats = nil\n\ts.queryLog = nil\n\ts.dnsProxy = nil\n\n\tif err := s.ipset.close(); err != nil {\n\t\ts.logger.ErrorContext(ctx, \"closing ipset\", slogutil.KeyError, err)\n\t}\n}\n\n// WriteDiskConfig - write configuration\nfunc (s *Server) WriteDiskConfig(c *Config) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\tsc := s.conf.Config\n\t*c = sc\n\tc.RatelimitWhitelist = slices.Clone(sc.RatelimitWhitelist)\n\tc.BootstrapDNS = slices.Clone(sc.BootstrapDNS)\n\tc.FallbackDNS = slices.Clone(sc.FallbackDNS)\n\tc.AllowedClients = slices.Clone(sc.AllowedClients)\n\tc.DisallowedClients = slices.Clone(sc.DisallowedClients)\n\tc.BlockedHosts = slices.Clone(sc.BlockedHosts)\n\tc.TrustedProxies = slices.Clone(sc.TrustedProxies)\n\tc.UpstreamDNS = slices.Clone(sc.UpstreamDNS)\n}\n\n// LocalPTRResolvers returns the current local PTR resolver configuration.\nfunc (s *Server) LocalPTRResolvers() (localPTRResolvers []string) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\treturn slices.Clone(s.conf.LocalPTRResolvers)\n}\n\n// AddrProcConfig returns the current address processing configuration.  Only\n// fields c.UsePrivateRDNS, c.UseRDNS, and c.UseWHOIS are filled.\nfunc (s *Server) AddrProcConfig() (c *client.DefaultAddrProcConfig) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\treturn &client.DefaultAddrProcConfig{\n\t\tUsePrivateRDNS: s.conf.UsePrivateRDNS,\n\t\tUseRDNS:        s.conf.AddrProcConf.UseRDNS,\n\t\tUseWHOIS:       s.conf.AddrProcConf.UseWHOIS,\n\t}\n}\n\n// UpstreamTimeout returns the current upstream timeout configuration.\nfunc (s *Server) UpstreamTimeout() (t time.Duration) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\treturn s.conf.UpstreamTimeout\n}\n\n// Resolve gets IP addresses by host name from an upstream server.  No\n// request/response filtering is performed.  Query log and Stats are not\n// updated.  This method may be called before [Server.Start].\nfunc (s *Server) Resolve(ctx context.Context, net, host string) (addr []netip.Addr, err error) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\treturn s.internalProxy.LookupNetIP(ctx, net, host)\n}\n\nconst (\n\t// ErrRDNSNoData is returned by [RDNSExchanger.Exchange] when the answer\n\t// section of response is either NODATA or has no PTR records.\n\tErrRDNSNoData errors.Error = \"no ptr data in response\"\n\n\t// ErrRDNSFailed is returned by [RDNSExchanger.Exchange] if the received\n\t// response is not a NOERROR or NXDOMAIN.\n\tErrRDNSFailed errors.Error = \"failed to resolve ptr\"\n)\n\n// type check\nvar _ rdns.Exchanger = (*Server)(nil)\n\n// Exchange implements the [rdns.Exchanger] interface for *Server.\nfunc (s *Server) Exchange(\n\tctx context.Context,\n\tip netip.Addr,\n) (host string, ttl time.Duration, err error) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\t// TODO(e.burkov):  Migrate to [netip.Addr] already.\n\tarpa, err := netutil.IPToReversedAddr(ip.AsSlice())\n\tif err != nil {\n\t\treturn \"\", 0, fmt.Errorf(\"reversing ip: %w\", err)\n\t}\n\n\tarpa = dns.Fqdn(arpa)\n\treq := &dns.Msg{\n\t\tMsgHdr: dns.MsgHdr{\n\t\t\tId:               dns.Id(),\n\t\t\tRecursionDesired: true,\n\t\t},\n\t\tCompress: true,\n\t\tQuestion: []dns.Question{{\n\t\t\tName:   arpa,\n\t\t\tQtype:  dns.TypePTR,\n\t\t\tQclass: dns.ClassINET,\n\t\t}},\n\t}\n\n\tdctx := &proxy.DNSContext{\n\t\tProto:           proxy.ProtoUDP,\n\t\tReq:             req,\n\t\tIsPrivateClient: true,\n\t}\n\n\tvar errMsg string\n\tif s.privateNets.Contains(ip) {\n\t\tif !s.conf.UsePrivateRDNS {\n\t\t\treturn \"\", 0, nil\n\t\t}\n\n\t\terrMsg = \"resolving a private address: %w\"\n\t\tdctx.RequestedPrivateRDNS = netip.PrefixFrom(ip, ip.BitLen())\n\t} else {\n\t\terrMsg = \"resolving an address: %w\"\n\t}\n\tif err = s.internalProxy.Resolve(ctx, dctx); err != nil {\n\t\treturn \"\", 0, fmt.Errorf(errMsg, err)\n\t}\n\n\treturn hostFromPTR(ctx, s.logger, dctx.Res)\n}\n\n// hostFromPTR returns domain name from the PTR response or error.  l must not\n// be nil.\nfunc hostFromPTR(\n\tctx context.Context,\n\tl *slog.Logger,\n\tresp *dns.Msg,\n) (host string, ttl time.Duration, err error) {\n\t// Distinguish between NODATA response and a failed request.\n\tif resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeNameError {\n\t\treturn \"\", 0, fmt.Errorf(\n\t\t\t\"received %s response: %w\",\n\t\t\tdns.RcodeToString[resp.Rcode],\n\t\t\tErrRDNSFailed,\n\t\t)\n\t}\n\n\tvar ttlSec uint32\n\n\tl.DebugContext(ctx, \"resolving ptr\", \"num_answers\", len(resp.Answer))\n\n\tfor _, ans := range resp.Answer {\n\t\tptr, ok := ans.(*dns.PTR)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Respect zero TTL records since some DNS servers use it to\n\t\t// locally-resolved addresses.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/6046.\n\t\tif ptr.Hdr.Ttl >= ttlSec {\n\t\t\thost = ptr.Ptr\n\t\t\tttlSec = ptr.Hdr.Ttl\n\t\t}\n\t}\n\n\tif host != \"\" {\n\t\t// NOTE:  Don't use [aghnet.NormalizeDomain] to retain original letter\n\t\t// case.\n\t\thost = strings.TrimSuffix(host, \".\")\n\t\tttl = time.Duration(ttlSec) * time.Second\n\n\t\treturn host, ttl, nil\n\t}\n\n\treturn \"\", 0, ErrRDNSNoData\n}\n\n// Start starts the DNS server.  It must only be called after [Server.Prepare].\nfunc (s *Server) Start(ctx context.Context) error {\n\ts.serverLock.Lock()\n\tdefer s.serverLock.Unlock()\n\n\treturn s.startLocked(ctx)\n}\n\n// startLocked starts the DNS server without locking.  s.serverLock is expected\n// to be locked.\nfunc (s *Server) startLocked(ctx context.Context) error {\n\terr := s.dnsProxy.Start(ctx)\n\tif err == nil {\n\t\ts.isRunning = true\n\t}\n\n\treturn err\n}\n\n// Prepare initializes parameters of s using data from conf.  conf must not be\n// nil.\nfunc (s *Server) Prepare(ctx context.Context, conf *ServerConfig) (err error) {\n\ts.conf = *conf\n\n\t// dnsFilter can be nil during application update.\n\tif s.dnsFilter != nil {\n\t\tmode, bIPv4, bIPv6 := s.dnsFilter.BlockingMode()\n\t\terr = validateBlockingMode(mode, bIPv4, bIPv6)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"checking blocking mode: %w\", err)\n\t\t}\n\t}\n\n\ts.initDefaultSettings()\n\n\terr = s.prepareInternalDNS(ctx)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tproxyConfig, err := s.newProxyConfig(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"preparing proxy: %w\", err)\n\t}\n\n\ts.setupDNS64()\n\n\ts.access, err = newAccessCtx(\n\t\ts.conf.AllowedClients,\n\t\ts.conf.DisallowedClients,\n\t\ts.conf.BlockedHosts,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"preparing access: %w\", err)\n\t}\n\n\tproxyConfig.Fallbacks, err = s.setupFallbackDNS()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"setting up fallback dns servers: %w\", err)\n\t}\n\n\tdnsProxy, err := proxy.New(proxyConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating proxy: %w\", err)\n\t}\n\n\ts.dnsProxy = dnsProxy\n\n\ts.setupAddrProc()\n\n\ts.registerHandlers()\n\n\treturn nil\n}\n\n// prepareUpstreamSettings sets upstream DNS server settings.\nfunc (s *Server) prepareUpstreamSettings(ctx context.Context, boot upstream.Resolver) (err error) {\n\t// Load upstreams either from the file, or from the settings\n\tvar upstreams []string\n\tupstreams, err = s.conf.loadUpstreams(ctx, s.logger)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loading upstreams: %w\", err)\n\t}\n\n\tuc, err := newUpstreamConfig(ctx, s.logger, upstreams, defaultDNS, &upstream.Options{\n\t\tLogger:       aghslog.NewForUpstream(s.baseLogger, aghslog.UpstreamTypeMain),\n\t\tBootstrap:    boot,\n\t\tTimeout:      s.conf.UpstreamTimeout,\n\t\tHTTPVersions: aghnet.UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),\n\t\tPreferIPv6:   s.conf.BootstrapPreferIPv6,\n\t\t// Use a customized set of RootCAs, because Go's default mechanism of\n\t\t// loading TLS roots does not always work properly on some routers so\n\t\t// we're loading roots manually and pass it here.\n\t\t//\n\t\t// See [aghtls.SystemRootCAs].\n\t\t//\n\t\t// TODO(a.garipov): Investigate if that's true.\n\t\tRootCAs:      s.conf.TLSv12Roots,\n\t\tCipherSuites: s.conf.TLSCiphers,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"preparing upstream config: %w\", err)\n\t}\n\n\ts.conf.UpstreamConfig = uc\n\ts.conf.ClientsContainer.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{\n\t\tBootstrap:               boot,\n\t\tUpstreamTimeout:         s.conf.UpstreamTimeout,\n\t\tBootstrapPreferIPv6:     s.conf.BootstrapPreferIPv6,\n\t\tEDNSClientSubnetEnabled: s.conf.EDNSClientSubnet.Enabled,\n\t\tUseHTTP3Upstreams:       s.conf.UseHTTP3Upstreams,\n\t})\n\n\treturn nil\n}\n\n// PrivateRDNSError is returned when the private rDNS upstreams are\n// invalid but enabled.\n//\n// TODO(e.burkov):  Consider allowing to use incomplete private rDNS upstreams\n// configuration in proxy when the private rDNS function is enabled.  In theory,\n// proxy supports the case when no upstreams provided to resolve the private\n// request, since it already supports this for DNS64-prefixed PTR requests.\ntype PrivateRDNSError struct {\n\terr error\n}\n\n// Error implements the [errors.Error] interface.\nfunc (e *PrivateRDNSError) Error() (s string) {\n\treturn e.err.Error()\n}\n\nfunc (e *PrivateRDNSError) Unwrap() (err error) {\n\treturn e.err\n}\n\n// prepareLocalResolvers initializes the private RDNS upstream configuration\n// according to the server's settings.  It assumes s.serverLock is locked or the\n// Server not running.\nfunc (s *Server) prepareLocalResolvers(ctx context.Context) (uc *proxy.UpstreamConfig, err error) {\n\tif !s.conf.UsePrivateRDNS {\n\t\treturn nil, nil\n\t}\n\n\tvar ownAddrs addrPortSet\n\townAddrs, err = s.conf.ourAddrsSet(ctx, s.logger)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\topts := &upstream.Options{\n\t\tLogger:    aghslog.NewForUpstream(s.baseLogger, aghslog.UpstreamTypeLocal),\n\t\tBootstrap: s.bootstrap,\n\t\tTimeout:   defaultLocalTimeout,\n\t\t// TODO(e.burkov): Should we verify server's certificates?\n\t\tPreferIPv6: s.conf.BootstrapPreferIPv6,\n\t}\n\n\taddrs := s.conf.LocalPTRResolvers\n\tuc, err = newPrivateConfig(ctx, s.logger, addrs, ownAddrs, s.sysResolvers, s.privateNets, opts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"preparing resolvers: %w\", err)\n\t}\n\n\treturn uc, nil\n}\n\n// prepareInternalDNS initializes the internal state of s before initializing\n// the primary DNS proxy instance.  It assumes s.serverLock is locked or the\n// Server not running.\nfunc (s *Server) prepareInternalDNS(ctx context.Context) (err error) {\n\tipsetList, err := s.prepareIpsetListSettings(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"preparing ipset settings: %w\", err)\n\t}\n\n\tipsetLogger := s.baseLogger.With(slogutil.KeyPrefix, \"ipset\")\n\ts.ipset, err = newIpsetHandler(context.TODO(), ipsetLogger, ipsetList)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tbootOpts := &upstream.Options{\n\t\tLogger:       aghslog.NewForUpstream(s.baseLogger, aghslog.UpstreamTypeBootstrap),\n\t\tTimeout:      DefaultTimeout,\n\t\tHTTPVersions: aghnet.UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),\n\t}\n\n\ts.bootstrap, s.bootResolvers, err = newBootstrap(s.conf.BootstrapDNS, s.etcHosts, bootOpts)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = s.prepareUpstreamSettings(ctx, s.bootstrap)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\ts.conf.PrivateRDNSUpstreamConfig, err = s.prepareLocalResolvers(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = s.prepareInternalProxy()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"preparing internal proxy: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// setupFallbackDNS initializes the fallback DNS servers.\nfunc (s *Server) setupFallbackDNS() (uc *proxy.UpstreamConfig, err error) {\n\tfallbacks := s.conf.FallbackDNS\n\tfallbacks = stringutil.FilterOut(fallbacks, aghnet.IsCommentOrEmpty)\n\tif len(fallbacks) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tuc, err = proxy.ParseUpstreamsConfig(fallbacks, &upstream.Options{\n\t\tLogger: aghslog.NewForUpstream(s.baseLogger, aghslog.UpstreamTypeFallback),\n\t\t// TODO(s.chzhen):  Investigate if other options are needed.\n\t\tTimeout:    s.conf.UpstreamTimeout,\n\t\tPreferIPv6: s.conf.BootstrapPreferIPv6,\n\t\t// TODO(e.burkov):  Use bootstrap.\n\t})\n\tif err != nil {\n\t\t// Do not wrap the error because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\treturn uc, nil\n}\n\n// setupAddrProc initializes the address processor.  It assumes s.serverLock is\n// locked or the Server not running.\nfunc (s *Server) setupAddrProc() {\n\t// TODO(a.garipov): This is a crutch for tests; remove.\n\tif s.conf.AddrProcConf == nil {\n\t\ts.conf.AddrProcConf = &client.DefaultAddrProcConfig{}\n\t}\n\tif s.conf.AddrProcConf.AddressUpdater == nil {\n\t\ts.addrProc = client.EmptyAddrProc{}\n\t} else {\n\t\tc := s.conf.AddrProcConf\n\t\tc.BaseLogger = s.baseLogger\n\t\tc.DialContext = s.DialContext\n\t\tc.PrivateSubnets = s.privateNets\n\t\tc.UsePrivateRDNS = s.conf.UsePrivateRDNS\n\t\ts.addrProc = client.NewDefaultAddrProc(s.conf.AddrProcConf)\n\n\t\t// Clear the initial addresses to not resolve them again.\n\t\t//\n\t\t// TODO(a.garipov): Consider ways of removing this once more client\n\t\t// logic is moved to package client.\n\t\tc.InitialAddresses = nil\n\t}\n}\n\n// validateBlockingMode returns an error if the blocking mode data aren't valid.\nfunc validateBlockingMode(\n\tmode filtering.BlockingMode,\n\tblockingIPv4, blockingIPv6 netip.Addr,\n) (err error) {\n\tswitch mode {\n\tcase\n\t\tfiltering.BlockingModeDefault,\n\t\tfiltering.BlockingModeNXDOMAIN,\n\t\tfiltering.BlockingModeREFUSED,\n\t\tfiltering.BlockingModeNullIP:\n\t\treturn nil\n\tcase filtering.BlockingModeCustomIP:\n\t\tif !blockingIPv4.Is4() {\n\t\t\treturn fmt.Errorf(\"blocking_ipv4 must be valid ipv4 on custom_ip blocking_mode\")\n\t\t} else if !blockingIPv6.Is6() {\n\t\t\treturn fmt.Errorf(\"blocking_ipv6 must be valid ipv6 on custom_ip blocking_mode\")\n\t\t}\n\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"bad blocking mode %q\", mode)\n\t}\n}\n\n// prepareInternalProxy initializes the DNS proxy that is used for internal DNS\n// queries, such as public clients PTR resolving and updater hostname resolving.\nfunc (s *Server) prepareInternalProxy() (err error) {\n\tsrvConf := s.conf\n\tconf := &proxy.Config{\n\t\tLogger:                    s.baseLogger.With(slogutil.KeyPrefix, aghslog.PrefixDNSProxy),\n\t\tCacheEnabled:              true,\n\t\tCacheSizeBytes:            4096,\n\t\tPrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,\n\t\tUpstreamConfig:            srvConf.UpstreamConfig,\n\t\tMaxGoroutines:             srvConf.MaxGoroutines,\n\t\tUseDNS64:                  srvConf.UseDNS64,\n\t\tDNS64Prefs:                srvConf.DNS64Prefixes,\n\t\tUsePrivateRDNS:            srvConf.UsePrivateRDNS,\n\t\tPrivateSubnets:            s.privateNets,\n\t\tMessageConstructor:        s,\n\t}\n\n\terr = setProxyUpstreamMode(conf, srvConf.UpstreamMode, time.Duration(srvConf.FastestTimeout))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid upstream mode: %w\", err)\n\t}\n\n\ts.internalProxy, err = proxy.New(conf)\n\n\treturn err\n}\n\n// Stop stops the DNS server.\nfunc (s *Server) Stop(ctx context.Context) error {\n\ts.serverLock.Lock()\n\tdefer s.serverLock.Unlock()\n\n\ts.stopLocked(ctx)\n\n\treturn nil\n}\n\n// stopLocked stops the DNS server without locking.  s.serverLock is expected to\n// be locked.\nfunc (s *Server) stopLocked(ctx context.Context) {\n\t// TODO(e.burkov, a.garipov):  Return critical errors, not just log them.\n\t// This will require filtering all the non-critical errors in\n\t// [upstream.Upstream] implementations.\n\n\tif s.dnsProxy != nil {\n\t\terr := s.dnsProxy.Shutdown(ctx)\n\t\tif err != nil {\n\t\t\ts.logger.ErrorContext(ctx, \"closing primary resolvers\", slogutil.KeyError, err)\n\t\t}\n\t}\n\n\tfor _, b := range s.bootResolvers {\n\t\tlogCloserErr(ctx, b, \"closing bootstrap\", s.logger.With(\"address\", b.Address()))\n\t}\n\n\ts.isRunning = false\n}\n\n// logCloserErr logs the error returned by c, if any.  l and c must not be nil.\nfunc logCloserErr(ctx context.Context, c io.Closer, msg string, l *slog.Logger) {\n\tif c == nil {\n\t\treturn\n\t}\n\n\terr := c.Close()\n\tif err != nil {\n\t\tl.ErrorContext(ctx, msg, slogutil.KeyError, err)\n\t}\n}\n\n// IsRunning returns true if the DNS server is running.\nfunc (s *Server) IsRunning() bool {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\treturn s.isRunning\n}\n\n// srvClosedErr is returned when the method can't complete without inaccessible\n// data from the closing server.\nconst srvClosedErr errors.Error = \"server is closed\"\n\n// proxy returns a pointer to the current DNS proxy instance.  If p is nil, the\n// server is closing.\n//\n// See https://github.com/AdguardTeam/AdGuardHome/issues/3655.\nfunc (s *Server) proxy() (p *proxy.Proxy) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\treturn s.dnsProxy\n}\n\n// Reconfigure applies the new configuration to the DNS server.\n//\n// TODO(a.garipov): This whole piece of API is weird and needs to be remade.\nfunc (s *Server) Reconfigure(ctx context.Context, conf *ServerConfig) error {\n\ts.serverLock.Lock()\n\tdefer s.serverLock.Unlock()\n\n\ts.logger.InfoContext(ctx, \"starting reconfiguring server\")\n\tdefer s.logger.InfoContext(ctx, \"finished reconfiguring server\")\n\n\ts.stopLocked(ctx)\n\n\t// It seems that net.Listener.Close() doesn't close file descriptors right\n\t// away.  We wait for some time and hope that this fd will be closed.\n\ttime.Sleep(100 * time.Millisecond)\n\n\tif s.addrProc != nil {\n\t\terr := s.addrProc.Close()\n\t\tif err != nil {\n\t\t\ts.logger.ErrorContext(ctx, \"closing address processor\", slogutil.KeyError, err)\n\t\t}\n\t}\n\n\tif conf == nil {\n\t\tconf = &s.conf\n\t}\n\n\t// TODO(e.burkov):  It seems an error here brings the server down, which is\n\t// not reliable enough.\n\terr := s.Prepare(ctx, conf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not reconfigure the server: %w\", err)\n\t}\n\n\terr = s.startLocked(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not reconfigure the server: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// ServeHTTP is a HTTP handler method we use to provide DNS-over-HTTPS.\nfunc (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif prx := s.proxy(); prx != nil {\n\t\tprx.ServeHTTP(w, r)\n\t}\n}\n\n// IsBlockedClient returns true if the client is blocked by the current access\n// settings.\nfunc (s *Server) IsBlockedClient(ip netip.Addr, clientID string) (blocked bool, rule string) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\tblockedByIP := false\n\tif ip != (netip.Addr{}) {\n\t\tblockedByIP, rule = s.access.isBlockedIP(ip)\n\t}\n\n\tallowlistMode := s.access.allowlistMode()\n\tblockedByClientID := s.access.isBlockedClientID(clientID)\n\n\t// TODO(s.chzhen):  Pass context.\n\tctx := context.TODO()\n\n\t// Allow if at least one of the checks allows in allowlist mode, but block\n\t// if at least one of the checks blocks in blocklist mode.\n\tif allowlistMode && blockedByIP && blockedByClientID {\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"client is not in access allowlist\",\n\t\t\t\"ip\", ip,\n\t\t\t\"client_id\", clientID,\n\t\t)\n\n\t\t// Return now without substituting the empty rule for the\n\t\t// clientID because the rule can't be empty here.\n\t\treturn true, rule\n\t} else if !allowlistMode && (blockedByIP || blockedByClientID) {\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"client is in access blocklist\",\n\t\t\t\"ip\", ip,\n\t\t\t\"client_id\", clientID,\n\t\t)\n\n\t\tblocked = true\n\t}\n\n\treturn blocked, cmp.Or(rule, clientID)\n}\n"
  },
  {
    "path": "internal/dnsforward/dnsforward_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"cmp\"\n\t\"crypto/ecdsa\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/netip\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"testing/fstest\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/schedule\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\nfunc TestMain(m *testing.M) {\n\ttestutil.DiscardLogOutput(m)\n}\n\n// testTimeout is the common timeout for tests.\n//\n// TODO(a.garipov): Use more.\nconst testTimeout = 1 * time.Second\n\n// testQuestionTarget is the common question target for tests.\n//\n// TODO(a.garipov): Use more.\nconst testQuestionTarget = \"target.example\"\n\nconst (\n\ttlsServerName     = \"testdns.adguard.com\"\n\ttestMessagesCount = 10\n)\n\n// testClientAddrPort is the common net.Addr for tests.\n//\n// TODO(a.garipov): Use more.\nvar testClientAddrPort = netip.MustParseAddrPort(\"1.2.3.4:12345\")\n\n// type check\nvar _ ClientsContainer = (*clientsContainer)(nil)\n\n// clientsContainer is a mock [ClientsContainer] implementation for tests.\ntype clientsContainer struct {\n\tOnCustomUpstreamConfig func(\n\t\tclientID string,\n\t\tcliAddr netip.Addr,\n\t) (conf *proxy.CustomUpstreamConfig)\n\n\tOnUpdateCommonUpstreamConfig func(conf *client.CommonUpstreamConfig)\n\n\tOnClearUpstreamCache func()\n}\n\n// CustomUpstreamConfig implements the [ClientsContainer] interface for\n// *clientsContainer.\nfunc (c *clientsContainer) CustomUpstreamConfig(\n\tclientID string,\n\tcliAddr netip.Addr,\n) (conf *proxy.CustomUpstreamConfig) {\n\treturn c.OnCustomUpstreamConfig(clientID, cliAddr)\n}\n\n// UpdateCommonUpstreamConfig implements the [ClientsContainer] interface for\n// *clientsContainer.\nfunc (c *clientsContainer) UpdateCommonUpstreamConfig(conf *client.CommonUpstreamConfig) {\n\tc.OnUpdateCommonUpstreamConfig(conf)\n}\n\n// ClearUpstreamCache implements the [ClientsContainer] interface for\n// *clientsContainer.\nfunc (c *clientsContainer) ClearUpstreamCache() {\n\tc.OnClearUpstreamCache()\n}\n\n// startDeferStop starts the server and stops it when the test ends.\n//\n// TODO(e.burkov):  Replace with [servicetest.RequireRun].\nfunc startDeferStop(tb testing.TB, s *Server) {\n\ttb.Helper()\n\n\terr := s.Start(testutil.ContextWithTimeout(tb, testTimeout))\n\trequire.NoError(tb, err)\n\ttestutil.CleanupAndRequireSuccess(tb, func() (err error) {\n\t\treturn s.Stop(testutil.ContextWithTimeout(tb, testTimeout))\n\t})\n}\n\n// applyEmptyClientFiltering is a helper function for tests with\n// [filtering.Config] that does nothing.\nfunc applyEmptyClientFiltering(_ string, _ netip.Addr, _ *filtering.Settings) {}\n\n// emptyFilteringBlockedServices is a helper function that returns an empty\n// filtering blocked services for tests.\nfunc emptyFilteringBlockedServices() (bsvc *filtering.BlockedServices) {\n\treturn &filtering.BlockedServices{\n\t\tSchedule: schedule.EmptyWeekly(),\n\t}\n}\n\n// createTestServer is a helper function that returns a properly initialized\n// *Server for use in tests, given the provided parameters.  It also populates\n// the filtering configuration with default parameters.\nfunc createTestServer(\n\ttb testing.TB,\n\tfilterConf *filtering.Config,\n\tforwardConf ServerConfig,\n) (s *Server) {\n\ttb.Helper()\n\n\tfilterConf.Logger = cmp.Or(filterConf.Logger, testLogger)\n\n\trules := `||nxdomain.example.org\n||NULL.example.org^\n127.0.0.1\thost.example.org\n@@||whitelist.example.org^\n||127.0.0.255`\n\tfilters := []filtering.Filter{{\n\t\tID:   0,\n\t\tData: []byte(rules),\n\t}}\n\n\tfilterConf.BlockedServices = cmp.Or(filterConf.BlockedServices, emptyFilteringBlockedServices())\n\n\tif filterConf.ApplyClientFiltering == nil {\n\t\tfilterConf.ApplyClientFiltering = applyEmptyClientFiltering\n\t}\n\n\tf, err := filtering.New(filterConf, filters)\n\trequire.NoError(tb, err)\n\n\tf.SetEnabled(true)\n\n\tdhcp := &testDHCP{\n\t\tOnEnabled:  func() (ok bool) { return false },\n\t\tOnHostByIP: func(ip netip.Addr) (host string) { return \"\" },\n\t\tOnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },\n\t}\n\ts, err = NewServer(DNSCreateParams{\n\t\tDHCPServer:  dhcp,\n\t\tDNSFilter:   f,\n\t\tPrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\tLogger:      testLogger,\n\t})\n\trequire.NoError(tb, err)\n\n\terr = s.Prepare(testutil.ContextWithTimeout(tb, testTimeout), &forwardConf)\n\trequire.NoError(tb, err)\n\n\treturn s\n}\n\nfunc createServerTLSConfig(tb testing.TB) (*tls.Config, []byte, []byte) {\n\ttb.Helper()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoErrorf(tb, err, \"cannot generate RSA key: %s\", err)\n\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\trequire.NoErrorf(tb, err, \"failed to generate serial number: %s\", err)\n\n\tnotBefore := time.Now()\n\tnotAfter := notBefore.Add(5 * 365 * timeutil.Day)\n\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"AdGuard Tests\"},\n\t\t},\n\t\tNotBefore: notBefore,\n\t\tNotAfter:  notAfter,\n\n\t\tKeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t}\n\ttemplate.DNSNames = append(template.DNSNames, tlsServerName)\n\n\tderBytes, err := x509.CreateCertificate(\n\t\trand.Reader,\n\t\t&template,\n\t\t&template,\n\t\tpublicKey(privateKey),\n\t\tprivateKey,\n\t)\n\trequire.NoErrorf(tb, err, \"failed to create certificate: %s\", err)\n\n\tcertPem := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: derBytes})\n\tkeyPem := pem.EncodeToMemory(\n\t\t&pem.Block{Type: \"RSA PRIVATE KEY\", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)},\n\t)\n\n\tcert, err := tls.X509KeyPair(certPem, keyPem)\n\trequire.NoErrorf(tb, err, \"failed to create certificate: %s\", err)\n\n\treturn &tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t\tServerName:   tlsServerName,\n\t\tMinVersion:   tls.VersionTLS12,\n\t}, certPem, keyPem\n}\n\nfunc createTestTLS(tb testing.TB, tlsConf *TLSConfig) (s *Server, certPem []byte) {\n\ttb.Helper()\n\n\tvar keyPem []byte\n\t_, certPem, keyPem = createServerTLSConfig(tb)\n\n\tcert, err := tls.X509KeyPair(certPem, keyPem)\n\trequire.NoError(tb, err)\n\n\ttlsConf.Cert = &cert\n\n\ts = createTestServer(tb, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        tlsConf,\n\t\tConfig: Config{\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t})\n\n\terr = s.Prepare(testutil.ContextWithTimeout(tb, testTimeout), &s.conf)\n\trequire.NoErrorf(tb, err, \"failed to prepare server: %s\", err)\n\n\treturn s, certPem\n}\n\nconst googleDomainName = \"google-public-dns-a.google.com.\"\n\nfunc createGoogleATestMessage() *dns.Msg {\n\treturn createTestMessage(googleDomainName)\n}\n\nfunc newGoogleUpstream() (u upstream.Upstream) {\n\treturn &aghtest.UpstreamMock{\n\t\tOnAddress: func() (addr string) { return \"google.upstream.example\" },\n\t\tOnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {\n\t\t\treturn cmp.Or(\n\t\t\t\taghtest.MatchedResponse(req, dns.TypeA, googleDomainName, \"8.8.8.8\"),\n\t\t\t\tnew(dns.Msg).SetRcode(req, dns.RcodeNameError),\n\t\t\t), nil\n\t\t},\n\t\tOnClose: func() (err error) { return nil },\n\t}\n}\n\nfunc createTestMessage(host string) *dns.Msg {\n\treturn &dns.Msg{\n\t\tMsgHdr: dns.MsgHdr{\n\t\t\tId:               dns.Id(),\n\t\t\tRecursionDesired: true,\n\t\t},\n\t\tQuestion: []dns.Question{{\n\t\t\tName:   host,\n\t\t\tQtype:  dns.TypeA,\n\t\t\tQclass: dns.ClassINET,\n\t\t}},\n\t}\n}\n\nfunc createTestMessageWithType(host string, qtype uint16) *dns.Msg {\n\treq := createTestMessage(host)\n\treq.Question[0].Qtype = qtype\n\n\treturn req\n}\n\n// newResp returns the new DNS response with response code set to rcode, req\n// used as request, and rrs added.\nfunc newResp(rcode int, req *dns.Msg, ans []dns.RR) (resp *dns.Msg) {\n\tresp = (&dns.Msg{}).SetRcode(req, rcode)\n\tresp.RecursionAvailable = true\n\tresp.Compress = true\n\tresp.Answer = ans\n\n\treturn resp\n}\n\nfunc assertGoogleAResponse(tb testing.TB, reply *dns.Msg) {\n\ttb.Helper()\n\n\tassertResponse(tb, reply, netip.AddrFrom4([4]byte{8, 8, 8, 8}))\n}\n\nfunc assertResponse(tb testing.TB, reply *dns.Msg, ip netip.Addr) {\n\ttb.Helper()\n\n\tif !assert.Len(tb, reply.Answer, 1, \"wrong number of answers\") {\n\t\treturn\n\t}\n\n\ta, ok := reply.Answer[0].(*dns.A)\n\tif !assert.True(tb, ok, \"wrong answer type instead of A: %T\", reply.Answer[0]) {\n\t\treturn\n\t}\n\n\tassert.Equal(tb, net.IP(ip.AsSlice()), a.A)\n}\n\n// sendTestMessagesAsync sends messages in parallel to check for race issues.\n//\n//lint:ignore U1000 it's called from the function which is skipped for now.\nfunc sendTestMessagesAsync(tb testing.TB, conn *dns.Conn) {\n\ttb.Helper()\n\n\twg := &sync.WaitGroup{}\n\n\tfor range testMessagesCount {\n\t\tmsg := createGoogleATestMessage()\n\n\t\twg.Go(func() {\n\t\t\terr := conn.WriteMsg(msg)\n\t\t\trequire.NoErrorf(tb, err, \"cannot write message: %s\", err)\n\n\t\t\tres, err := conn.ReadMsg()\n\t\t\trequire.NoErrorf(tb, err, \"cannot read response to message: %s\", err)\n\n\t\t\tassertGoogleAResponse(tb, res)\n\t\t})\n\t}\n\n\twg.Wait()\n\n\tif tb.Failed() {\n\t\ttb.FailNow()\n\t}\n}\n\nfunc sendTestMessages(tb testing.TB, conn *dns.Conn) {\n\ttb.Helper()\n\n\tfor i := range testMessagesCount {\n\t\treq := createGoogleATestMessage()\n\t\terr := conn.WriteMsg(req)\n\t\tassert.NoErrorf(tb, err, \"cannot write message #%d: %s\", i, err)\n\n\t\tres, err := conn.ReadMsg()\n\t\tassert.NoErrorf(tb, err, \"cannot read response to message #%d: %s\", i, err)\n\t\tassertGoogleAResponse(tb, res)\n\t}\n}\n\nfunc TestServer(t *testing.T) {\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t})\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}\n\tstartDeferStop(t, s)\n\n\ttestCases := []struct {\n\t\tname  string\n\t\tnet   string\n\t\tproto proxy.Proto\n\t}{{\n\t\tname:  \"message_over_udp\",\n\t\tnet:   \"\",\n\t\tproto: proxy.ProtoUDP,\n\t}, {\n\t\tname:  \"message_over_tcp\",\n\t\tnet:   \"tcp\",\n\t\tproto: proxy.ProtoTCP,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\taddr := s.dnsProxy.Addr(tc.proto)\n\t\t\tclient := dns.Client{Net: tc.net}\n\n\t\t\treply, _, err := client.Exchange(createGoogleATestMessage(), addr.String())\n\t\t\trequire.NoErrorf(t, err, \"couldn't talk to server %s: %s\", addr, err)\n\n\t\t\tassertGoogleAResponse(t, reply)\n\t\t})\n\t}\n}\n\nfunc TestServer_timeout(t *testing.T) {\n\tt.Run(\"custom\", func(t *testing.T) {\n\t\tsrvConf := &ServerConfig{\n\t\t\tUpstreamTimeout: testTimeout,\n\t\t\tTLSConf:         &TLSConfig{},\n\t\t\tConfig: Config{\n\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t},\n\t\t\tServePlainDNS: true,\n\t\t}\n\n\t\ts, err := NewServer(DNSCreateParams{\n\t\t\tDNSFilter: createTestDNSFilter(t),\n\t\t\tLogger:    testLogger,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\terr = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), srvConf)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, testTimeout, s.conf.UpstreamTimeout)\n\t})\n\n\tt.Run(\"default\", func(t *testing.T) {\n\t\ts, err := NewServer(DNSCreateParams{\n\t\t\tDNSFilter: createTestDNSFilter(t),\n\t\t\tLogger:    testLogger,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\ts.conf.TLSConf = &TLSConfig{}\n\t\ts.conf.Config.UpstreamMode = UpstreamModeLoadBalance\n\t\ts.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{\n\t\t\tEnabled: false,\n\t\t}\n\t\ts.conf.Config.ClientsContainer = EmptyClientsContainer{}\n\t\terr = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &s.conf)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, DefaultTimeout, s.conf.UpstreamTimeout)\n\t})\n}\n\nfunc TestServer_Prepare_fallbacks(t *testing.T) {\n\tsrvConf := &ServerConfig{\n\t\tTLSConf: &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tFallbackDNS: []string{\n\t\t\t\t\"#tls://1.1.1.1\",\n\t\t\t\t\"8.8.8.8\",\n\t\t\t},\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\n\ts, err := NewServer(DNSCreateParams{\n\t\tLogger: testLogger,\n\t})\n\trequire.NoError(t, err)\n\n\terr = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), srvConf)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, s.dnsProxy.Fallbacks)\n\n\tassert.Len(t, s.dnsProxy.Fallbacks.Upstreams, 1)\n}\n\nfunc TestServerWithProtectionDisabled(t *testing.T) {\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t})\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}\n\tstartDeferStop(t, s)\n\n\t// Message over UDP.\n\treq := createGoogleATestMessage()\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\tclient := &dns.Client{}\n\n\treply, _, err := client.Exchange(req, addr.String())\n\trequire.NoErrorf(t, err, \"couldn't talk to server %s: %s\", addr, err)\n\tassertGoogleAResponse(t, reply)\n}\n\nfunc TestDoTServer(t *testing.T) {\n\ts, certPem := createTestTLS(t, &TLSConfig{\n\t\tTLSListenAddrs: []*net.TCPAddr{{}},\n\t})\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}\n\tstartDeferStop(t, s)\n\n\t// Add our self-signed generated config to roots.\n\troots := x509.NewCertPool()\n\troots.AppendCertsFromPEM(certPem)\n\ttlsConfig := &tls.Config{\n\t\tServerName: tlsServerName,\n\t\tRootCAs:    roots,\n\t\tMinVersion: tls.VersionTLS12,\n\t}\n\n\t// Create a DNS-over-TLS client connection.\n\taddr := s.dnsProxy.Addr(proxy.ProtoTLS)\n\tconn, err := dns.DialWithTLS(\"tcp-tls\", addr.String(), tlsConfig)\n\trequire.NoErrorf(t, err, \"cannot connect to the proxy: %s\", err)\n\n\tsendTestMessages(t, conn)\n}\n\nfunc TestDoQServer(t *testing.T) {\n\ts, _ := createTestTLS(t, &TLSConfig{\n\t\tQUICListenAddrs: []*net.UDPAddr{{IP: net.IP{127, 0, 0, 1}}},\n\t})\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}\n\tstartDeferStop(t, s)\n\n\t// Create a DNS-over-QUIC upstream.\n\taddr := s.dnsProxy.Addr(proxy.ProtoQUIC)\n\topts := &upstream.Options{\n\t\tLogger:             testLogger,\n\t\tInsecureSkipVerify: true,\n\t}\n\tu, err := upstream.AddressToUpstream(fmt.Sprintf(\"%s://%s\", proxy.ProtoQUIC, addr), opts)\n\trequire.NoError(t, err)\n\n\t// Send the test message.\n\treq := createGoogleATestMessage()\n\tres, err := u.Exchange(req)\n\trequire.NoError(t, err)\n\n\tassertGoogleAResponse(t, res)\n}\n\nfunc TestServerRace(t *testing.T) {\n\tt.Skip(\"TODO(e.burkov): inspect the golibs/cache package for locks\")\n\n\tfilterConf := &filtering.Config{\n\t\tSafeBrowsingEnabled:   true,\n\t\tSafeBrowsingCacheSize: 1000,\n\t\tSafeSearchConf:        filtering.SafeSearchConfig{Enabled: true},\n\t\tSafeSearchCacheSize:   1000,\n\t\tParentalCacheSize:     1000,\n\t\tCacheTime:             30,\n\t}\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tUpstreamDNS:  []string{\"8.8.8.8:53\", \"8.8.4.4:53\"},\n\t\t},\n\t\tConfModifier:  agh.EmptyConfigModifier{},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, filterConf, forwardConf)\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}\n\tstartDeferStop(t, s)\n\n\t// Message over UDP.\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\tconn, err := dns.Dial(\"udp\", addr.String())\n\trequire.NoErrorf(t, err, \"cannot connect to the proxy: %s\", err)\n\n\tsendTestMessagesAsync(t, conn)\n}\n\nfunc TestSafeSearch(t *testing.T) {\n\tconst (\n\t\tgoogleSafeSearch = \"forcesafesearch.google.com.\"\n\t)\n\n\tsafeSearchConf := filtering.SafeSearchConfig{\n\t\tEnabled: true,\n\t\tGoogle:  true,\n\t\tYandex:  true,\n\t}\n\n\tfilterConf := &filtering.Config{\n\t\tLogger:              testLogger,\n\t\tBlockingMode:        filtering.BlockingModeDefault,\n\t\tProtectionEnabled:   true,\n\t\tSafeSearchConf:      safeSearchConf,\n\t\tSafeSearchCacheSize: 1000,\n\t\tCacheTime:           30,\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tsafeSearch, err := safesearch.NewDefault(ctx, &safesearch.DefaultConfig{\n\t\tLogger:         testLogger,\n\t\tServicesConfig: safeSearchConf,\n\t\tCacheSize:      filterConf.SafeSearchCacheSize,\n\t\tCacheTTL:       time.Minute * time.Duration(filterConf.CacheTime),\n\t})\n\trequire.NoError(t, err)\n\n\tfilterConf.SafeSearch = safeSearch\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, filterConf, forwardConf)\n\n\tups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {\n\t\tpt := testutil.PanicT{}\n\t\tassert.Equal(pt, googleSafeSearch, req.Question[0].Name)\n\n\t\treturn aghtest.MatchedResponse(req, dns.TypeA, googleSafeSearch, \"1.2.3.4\"), nil\n\t})\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups}\n\n\tstartDeferStop(t, s)\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP).String()\n\n\tyandexIP := netip.AddrFrom4([4]byte{213, 180, 193, 56})\n\n\ttestCases := []struct {\n\t\thost      string\n\t\twant      netip.Addr\n\t\twantCNAME string\n\t}{{\n\t\thost:      \"yandex.com.\",\n\t\twant:      yandexIP,\n\t\twantCNAME: \"\",\n\t}, {\n\t\thost:      \"yandex.by.\",\n\t\twant:      yandexIP,\n\t\twantCNAME: \"\",\n\t}, {\n\t\thost:      \"yandex.kz.\",\n\t\twant:      yandexIP,\n\t\twantCNAME: \"\",\n\t}, {\n\t\thost:      \"yandex.ru.\",\n\t\twant:      yandexIP,\n\t\twantCNAME: \"\",\n\t}, {\n\t\thost:      \"www.google.com.\",\n\t\twant:      netip.Addr{},\n\t\twantCNAME: \"forcesafesearch.google.com.\",\n\t}, {\n\t\thost:      \"www.google.com.af.\",\n\t\twant:      netip.Addr{},\n\t\twantCNAME: \"forcesafesearch.google.com.\",\n\t}, {\n\t\thost:      \"www.google.be.\",\n\t\twant:      netip.Addr{},\n\t\twantCNAME: \"forcesafesearch.google.com.\",\n\t}, {\n\t\thost:      \"www.google.by.\",\n\t\twant:      netip.Addr{},\n\t\twantCNAME: \"forcesafesearch.google.com.\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.host, func(t *testing.T) {\n\t\t\treq := createTestMessage(tc.host)\n\n\t\t\tvar reply *dns.Msg\n\t\t\treply, err = dns.Exchange(req, addr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.wantCNAME != \"\" {\n\t\t\t\trequire.Len(t, reply.Answer, 2)\n\n\t\t\t\tcname := testutil.RequireTypeAssert[*dns.CNAME](t, reply.Answer[0])\n\t\t\t\tassert.Equal(t, tc.wantCNAME, cname.Target)\n\n\t\t\t\ta := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[1])\n\t\t\t\tassert.NotEmpty(t, a.A)\n\t\t\t} else {\n\t\t\t\trequire.Len(t, reply.Answer, 1)\n\n\t\t\t\ta := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[0])\n\t\t\t\tassert.Equal(t, net.IP(tc.want.AsSlice()), a.A)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInvalidRequest(t *testing.T) {\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t})\n\tstartDeferStop(t, s)\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP).String()\n\treq := dns.Msg{\n\t\tMsgHdr: dns.MsgHdr{\n\t\t\tId:               dns.Id(),\n\t\t\tRecursionDesired: true,\n\t\t},\n\t}\n\n\t// Send a DNS request without question.\n\t_, _, err := (&dns.Client{\n\t\tTimeout: testTimeout,\n\t}).Exchange(&req, addr)\n\n\tassert.NoErrorf(t, err, \"got a response to an invalid query\")\n}\n\nfunc TestBlockedRequest(t *testing.T) {\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, &filtering.Config{\n\t\tProtectionEnabled: true,\n\t\tBlockingMode:      filtering.BlockingModeDefault,\n\t}, forwardConf)\n\tstartDeferStop(t, s)\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\n\t// Default blocking.\n\treq := createTestMessage(\"nxdomain.example.org.\")\n\n\treply, err := dns.Exchange(req, addr.String())\n\trequire.NoErrorf(t, err, \"couldn't talk to server %s: %s\", addr, err)\n\n\tassert.Equal(t, dns.RcodeSuccess, reply.Rcode)\n\n\trequire.Len(t, reply.Answer, 1)\n\tassert.True(t, reply.Answer[0].(*dns.A).A.IsUnspecified())\n}\n\nfunc TestServerCustomClientUpstream(t *testing.T) {\n\tconst defaultCacheSize = 1024 * 1024\n\n\tvar upsCalledCounter uint32\n\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tCacheSize:    defaultCacheSize,\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, forwardConf)\n\n\tups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {\n\t\tatomic.AddUint32(&upsCalledCounter, 1)\n\n\t\treturn cmp.Or(\n\t\t\taghtest.MatchedResponse(req, dns.TypeA, \"host\", \"192.168.0.1\"),\n\t\t\tnew(dns.Msg).SetRcode(req, dns.RcodeNameError),\n\t\t), nil\n\t})\n\n\tcustomUpsConf := proxy.NewCustomUpstreamConfig(\n\t\t&proxy.UpstreamConfig{\n\t\t\tUpstreams: []upstream.Upstream{ups},\n\t\t},\n\t\ttrue,\n\t\tdefaultCacheSize,\n\t\tforwardConf.EDNSClientSubnet.Enabled,\n\t)\n\n\ts.conf.ClientsContainer = &clientsContainer{\n\t\tOnCustomUpstreamConfig: func(\n\t\t\t_ string,\n\t\t\t_ netip.Addr,\n\t\t) (conf *proxy.CustomUpstreamConfig) {\n\t\t\treturn customUpsConf\n\t\t},\n\t}\n\n\tstartDeferStop(t, s)\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP).String()\n\n\t// Send test request.\n\treq := createTestMessage(\"host.\")\n\n\treply, err := dns.Exchange(req, addr)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, reply.Answer)\n\trequire.Len(t, reply.Answer, 1)\n\n\tassert.Equal(t, dns.RcodeSuccess, reply.Rcode)\n\tassert.Equal(t, net.IP{192, 168, 0, 1}, reply.Answer[0].(*dns.A).A)\n\tassert.Equal(t, uint32(1), atomic.LoadUint32(&upsCalledCounter))\n\n\t_, err = dns.Exchange(req, addr)\n\trequire.NoError(t, err)\n\tassert.Equal(t, uint32(1), atomic.LoadUint32(&upsCalledCounter))\n}\n\n// testCNAMEs is a map of names and CNAMEs necessary for the TestUpstream work.\nvar testCNAMEs = map[string][]string{\n\t\"badhost.\":               {\"NULL.example.org.\"},\n\t\"whitelist.example.org.\": {\"NULL.example.org.\"},\n}\n\n// testIPv4 is a map of names and IPv4s necessary for the TestUpstream work.\nvar testIPv4 = map[string][]net.IP{\n\t\"NULL.example.org.\": {{1, 2, 3, 4}},\n\t\"example.org.\":      {{127, 0, 0, 255}},\n}\n\nfunc TestBlockCNAMEProtectionEnabled(t *testing.T) {\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t})\n\ttestUpstm := &aghtest.Upstream{\n\t\tCName: testCNAMEs,\n\t\tIPv4:  testIPv4,\n\t}\n\n\ts.dnsProxy.UpstreamConfig = &proxy.UpstreamConfig{\n\t\tUpstreams: []upstream.Upstream{testUpstm},\n\t}\n\tstartDeferStop(t, s)\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\n\t// 'badhost' has a canonical name 'NULL.example.org' which should be\n\t// blocked by filters, but protection is disabled so it is not.\n\treq := createTestMessage(\"badhost.\")\n\n\treply, err := dns.Exchange(req, addr.String())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, dns.RcodeSuccess, reply.Rcode)\n}\n\nfunc TestBlockCNAME(t *testing.T) {\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, &filtering.Config{\n\t\tProtectionEnabled: true,\n\t\tBlockingMode:      filtering.BlockingModeDefault,\n\t}, forwardConf)\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{\n\t\t&aghtest.Upstream{\n\t\t\tCName: testCNAMEs,\n\t\t\tIPv4:  testIPv4,\n\t\t},\n\t}\n\tstartDeferStop(t, s)\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP).String()\n\n\ttestCases := []struct {\n\t\tname string\n\t\thost string\n\t\twant bool\n\t}{{\n\t\tname: \"block_request\",\n\t\thost: \"badhost.\",\n\t\t// 'badhost' has a canonical name 'NULL.example.org' which is\n\t\t// blocked by filters: response is blocked.\n\t\twant: true,\n\t}, {\n\t\tname: \"allowed\",\n\t\thost: \"whitelist.example.org.\",\n\t\t// 'whitelist.example.org' has a canonical name\n\t\t// 'NULL.example.org' which is blocked by filters\n\t\t// but 'whitelist.example.org' is in a whitelist:\n\t\t// response isn't blocked.\n\t\twant: false,\n\t}, {\n\t\tname: \"block_response\",\n\t\thost: \"example.org.\",\n\t\t// 'example.org' has a canonical name 'cname1' with IP\n\t\t// 127.0.0.255 which is blocked by filters: response is blocked.\n\t\twant: true,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\treq := createTestMessage(tc.host)\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treply, err := dns.Exchange(req, addr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, dns.RcodeSuccess, reply.Rcode)\n\t\t\tif tc.want {\n\t\t\t\trequire.Len(t, reply.Answer, 1)\n\n\t\t\t\tans := reply.Answer[0]\n\t\t\t\ta, ok := ans.(*dns.A)\n\t\t\t\trequire.True(t, ok)\n\n\t\t\t\tassert.True(t, a.A.IsUnspecified())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClientRulesForCNAMEMatching(t *testing.T) {\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, forwardConf)\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{\n\t\t&aghtest.Upstream{\n\t\t\tCName: testCNAMEs,\n\t\t\tIPv4:  testIPv4,\n\t\t},\n\t}\n\tstartDeferStop(t, s)\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\n\t// 'badhost' has a canonical name 'NULL.example.org' which is blocked by\n\t// filters: response is blocked.\n\treq := dns.Msg{\n\t\tMsgHdr: dns.MsgHdr{\n\t\t\tId: dns.Id(),\n\t\t},\n\t\tQuestion: []dns.Question{{\n\t\t\tName:   \"badhost.\",\n\t\t\tQtype:  dns.TypeA,\n\t\t\tQclass: dns.ClassINET,\n\t\t}},\n\t}\n\n\t// However, in our case it should not be blocked as filtering is\n\t// disabled on the client level.\n\treply, err := dns.Exchange(&req, addr.String())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, dns.RcodeSuccess, reply.Rcode)\n}\n\nfunc TestNullBlockedRequest(t *testing.T) {\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, &filtering.Config{\n\t\tProtectionEnabled: true,\n\t\tBlockingMode:      filtering.BlockingModeNullIP,\n\t}, forwardConf)\n\tstartDeferStop(t, s)\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\n\t// Nil filter blocking.\n\treq := dns.Msg{\n\t\tMsgHdr: dns.MsgHdr{\n\t\t\tId:               dns.Id(),\n\t\t\tRecursionDesired: true,\n\t\t},\n\t\tQuestion: []dns.Question{{\n\t\t\tName:   \"NULL.example.org.\",\n\t\t\tQtype:  dns.TypeA,\n\t\t\tQclass: dns.ClassINET,\n\t\t}},\n\t}\n\n\treply, err := dns.Exchange(&req, addr.String())\n\trequire.NoErrorf(t, err, \"couldn't talk to server %s: %s\", addr, err)\n\trequire.Lenf(\n\t\tt,\n\t\treply.Answer,\n\t\t1,\n\t\t\"dns server %s returned reply with wrong number of answers - %d\",\n\t\taddr,\n\t\tlen(reply.Answer),\n\t)\n\ta := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[0])\n\tassert.Truef(\n\t\tt,\n\t\ta.A.IsUnspecified(),\n\t\t\"dns server %s returned wrong answer instead of 0.0.0.0: %v\",\n\t\taddr,\n\t\ta.A,\n\t)\n}\n\nfunc TestBlockedCustomIP(t *testing.T) {\n\trules := \"||nxdomain.example.org^\\n||NULL.example.org^\\n127.0.0.1\thost.example.org\\n@@||whitelist.example.org^\\n||127.0.0.255\\n\"\n\tfilters := []filtering.Filter{{\n\t\tID:   0,\n\t\tData: []byte(rules),\n\t}}\n\n\tf, err := filtering.New(&filtering.Config{\n\t\tLogger:               testLogger,\n\t\tProtectionEnabled:    true,\n\t\tApplyClientFiltering: applyEmptyClientFiltering,\n\t\tBlockedServices:      emptyFilteringBlockedServices(),\n\t\tBlockingMode:         filtering.BlockingModeCustomIP,\n\t\tBlockingIPv4:         netip.Addr{},\n\t\tBlockingIPv6:         netip.Addr{},\n\t}, filters)\n\trequire.NoError(t, err)\n\n\tdhcp := &testDHCP{\n\t\tOnEnabled:  func() (ok bool) { return false },\n\t\tOnHostByIP: func(ip netip.Addr) (_ string) { panic(testutil.UnexpectedCall(ip)) },\n\t\tOnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },\n\t}\n\ts, err := NewServer(DNSCreateParams{\n\t\tDHCPServer:  dhcp,\n\t\tDNSFilter:   f,\n\t\tPrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\tLogger:      testLogger,\n\t})\n\trequire.NoError(t, err)\n\n\tconf := &ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamDNS:  []string{\"8.8.8.8:53\", \"8.8.4.4:53\"},\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\n\t// Invalid BlockingIPv4.\n\terr = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), conf)\n\tassert.Error(t, err)\n\n\ts.dnsFilter.SetBlockingMode(\n\t\tfiltering.BlockingModeCustomIP,\n\t\tnetip.AddrFrom4([4]byte{0, 0, 0, 1}),\n\t\tnetip.MustParseAddr(\"::1\"))\n\n\terr = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), conf)\n\trequire.NoError(t, err)\n\n\tf.SetEnabled(true)\n\tstartDeferStop(t, s)\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\n\treq := createTestMessageWithType(\"NULL.example.org.\", dns.TypeA)\n\treply, err := dns.Exchange(req, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Len(t, reply.Answer, 1)\n\n\ta, ok := reply.Answer[0].(*dns.A)\n\trequire.True(t, ok)\n\n\tassert.True(t, net.IP{0, 0, 0, 1}.Equal(a.A))\n\n\treq = createTestMessageWithType(\"NULL.example.org.\", dns.TypeAAAA)\n\treply, err = dns.Exchange(req, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Len(t, reply.Answer, 1)\n\n\ta6, ok := reply.Answer[0].(*dns.AAAA)\n\trequire.True(t, ok)\n\n\tassert.Equal(t, \"::1\", a6.AAAA.String())\n}\n\nfunc TestBlockedByHosts(t *testing.T) {\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\n\ts := createTestServer(t, &filtering.Config{\n\t\tProtectionEnabled: true,\n\t\tBlockingMode:      filtering.BlockingModeDefault,\n\t}, forwardConf)\n\tstartDeferStop(t, s)\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\n\t// Hosts blocking.\n\treq := createTestMessage(\"host.example.org.\")\n\n\treply, err := dns.Exchange(req, addr.String())\n\trequire.NoErrorf(t, err, \"couldn't talk to server %s: %s\", addr, err)\n\trequire.Lenf(\n\t\tt,\n\t\treply.Answer,\n\t\t1,\n\t\t\"dns server %s returned reply with wrong number of answers - %d\",\n\t\taddr,\n\t\tlen(reply.Answer),\n\t)\n\ta, ok := reply.Answer[0].(*dns.A)\n\trequire.Truef(\n\t\tt,\n\t\tok,\n\t\t\"dns server %s returned wrong answer type instead of A: %v\",\n\t\taddr,\n\t\treply.Answer[0],\n\t)\n\tassert.Equalf(\n\t\tt,\n\t\tnet.IP{127, 0, 0, 1},\n\t\ta.A,\n\t\t\"dns server %s returned wrong answer instead of 8.8.8.8: %v\",\n\t\taddr,\n\t\ta.A,\n\t)\n}\n\nfunc TestBlockedBySafeBrowsing(t *testing.T) {\n\tconst (\n\t\thostname  = \"wmconvirus.narod.ru\"\n\t\tcacheTime = 10 * time.Minute\n\t\tcacheSize = 10000\n\t)\n\n\tsbChecker := hashprefix.New(&hashprefix.Config{\n\t\tLogger:    testLogger,\n\t\tCacheTime: cacheTime,\n\t\tCacheSize: cacheSize,\n\t\tUpstream:  aghtest.NewBlockUpstream(hostname, true),\n\t})\n\n\tans4, _ := aghtest.HostToIPs(hostname)\n\n\tfilterConf := &filtering.Config{\n\t\tBlockingMode:          filtering.BlockingModeDefault,\n\t\tProtectionEnabled:     true,\n\t\tSafeBrowsingEnabled:   true,\n\t\tSafeBrowsingChecker:   sbChecker,\n\t\tSafeBrowsingBlockHost: ans4.String(),\n\t}\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, filterConf, forwardConf)\n\tstartDeferStop(t, s)\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\n\t// SafeBrowsing blocking.\n\treq := createTestMessage(hostname + \".\")\n\n\treply, err := dns.Exchange(req, addr.String())\n\trequire.NoErrorf(t, err, \"couldn't talk to server %s: %s\", addr, err)\n\trequire.Lenf(\n\t\tt,\n\t\treply.Answer,\n\t\t1,\n\t\t\"dns server %s returned reply with wrong number of answers - %d\",\n\t\taddr,\n\t\tlen(reply.Answer),\n\t)\n\n\tassertResponse(t, reply, ans4)\n}\n\nfunc TestRewrite(t *testing.T) {\n\tc := &filtering.Config{\n\t\tLogger:               testLogger,\n\t\tApplyClientFiltering: applyEmptyClientFiltering,\n\t\tBlockedServices:      emptyFilteringBlockedServices(),\n\t\tBlockingMode:         filtering.BlockingModeDefault,\n\t\tRewrites: []*filtering.LegacyRewrite{{\n\t\t\tDomain:  \"test.com\",\n\t\t\tAnswer:  \"1.2.3.4\",\n\t\t\tType:    dns.TypeA,\n\t\t\tEnabled: true,\n\t\t}, {\n\t\t\tDomain:  \"alias.test.com\",\n\t\t\tAnswer:  \"test.com\",\n\t\t\tType:    dns.TypeCNAME,\n\t\t\tEnabled: true,\n\t\t}, {\n\t\t\tDomain:  \"my.alias.example.org\",\n\t\t\tAnswer:  \"example.org\",\n\t\t\tType:    dns.TypeCNAME,\n\t\t\tEnabled: true,\n\t\t}},\n\t\tRewritesEnabled: true,\n\t}\n\tf, err := filtering.New(c, nil)\n\trequire.NoError(t, err)\n\n\tf.SetEnabled(true)\n\n\tdhcp := &testDHCP{\n\t\tOnEnabled:  func() (ok bool) { return false },\n\t\tOnHostByIP: func(ip netip.Addr) (_ string) { panic(testutil.UnexpectedCall(ip)) },\n\t\tOnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },\n\t}\n\ts, err := NewServer(DNSCreateParams{\n\t\tDHCPServer:  dhcp,\n\t\tDNSFilter:   f,\n\t\tPrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\tLogger:      testLogger,\n\t})\n\trequire.NoError(t, err)\n\n\tassert.NoError(t, s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamDNS:  []string{\"8.8.8.8:53\"},\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}))\n\n\tups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {\n\t\treturn cmp.Or(\n\t\t\taghtest.MatchedResponse(req, dns.TypeA, \"example.org\", \"4.3.2.1\"),\n\t\t\tnew(dns.Msg).SetRcode(req, dns.RcodeNameError),\n\t\t), nil\n\t})\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups}\n\tstartDeferStop(t, s)\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\n\tsubTestFunc := func(t *testing.T) {\n\t\treq := createTestMessageWithType(\"test.com.\", dns.TypeA)\n\t\treply, eerr := dns.Exchange(req, addr.String())\n\t\trequire.NoError(t, eerr)\n\n\t\trequire.Len(t, reply.Answer, 1)\n\n\t\ta, ok := reply.Answer[0].(*dns.A)\n\t\trequire.True(t, ok)\n\n\t\tassert.True(t, net.IP{1, 2, 3, 4}.Equal(a.A))\n\n\t\treq = createTestMessageWithType(\"test.com.\", dns.TypeAAAA)\n\t\treply, eerr = dns.Exchange(req, addr.String())\n\t\trequire.NoError(t, eerr)\n\n\t\tassert.Empty(t, reply.Answer)\n\n\t\treq = createTestMessageWithType(\"alias.test.com.\", dns.TypeA)\n\t\treply, eerr = dns.Exchange(req, addr.String())\n\t\trequire.NoError(t, eerr)\n\n\t\trequire.Len(t, reply.Answer, 2)\n\n\t\tassert.Equal(t, \"test.com.\", reply.Answer[0].(*dns.CNAME).Target)\n\t\tassert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A))\n\n\t\treq = createTestMessageWithType(\"my.alias.example.org.\", dns.TypeA)\n\t\treply, eerr = dns.Exchange(req, addr.String())\n\t\trequire.NoError(t, eerr)\n\n\t\t// The original question is restored.\n\t\trequire.Len(t, reply.Question, 1)\n\n\t\tassert.Equal(t, \"my.alias.example.org.\", reply.Question[0].Name)\n\n\t\trequire.Len(t, reply.Answer, 2)\n\n\t\tassert.Equal(t, \"example.org.\", reply.Answer[0].(*dns.CNAME).Target)\n\t\tassert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)\n\t}\n\n\tfor _, protect := range []bool{true, false} {\n\t\tval := protect\n\t\tconf := s.getDNSConfig(testutil.ContextWithTimeout(t, testTimeout))\n\t\tconf.ProtectionEnabled = &val\n\t\ts.setConfig(conf)\n\n\t\tt.Run(fmt.Sprintf(\"protection_is_%t\", val), subTestFunc)\n\t}\n}\n\nfunc publicKey(priv any) any {\n\tswitch k := priv.(type) {\n\tcase *rsa.PrivateKey:\n\t\treturn &k.PublicKey\n\n\tcase *ecdsa.PrivateKey:\n\t\treturn &k.PublicKey\n\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// testDHCP is a mock implementation of the [DHCP] interface.\ntype testDHCP struct {\n\tOnHostByIP func(ip netip.Addr) (host string)\n\tOnIPByHost func(host string) (ip netip.Addr)\n\tOnEnabled  func() (ok bool)\n}\n\n// type check\nvar _ DHCP = (*testDHCP)(nil)\n\n// HostByIP implements the [DHCP] interface for *testDHCP.\nfunc (d *testDHCP) HostByIP(ip netip.Addr) (host string) { return d.OnHostByIP(ip) }\n\n// IPByHost implements the [DHCP] interface for *testDHCP.\nfunc (d *testDHCP) IPByHost(host string) (ip netip.Addr) { return d.OnIPByHost(host) }\n\n// IsClientHost implements the [DHCP] interface for *testDHCP.\nfunc (d *testDHCP) Enabled() (ok bool) { return d.OnEnabled() }\n\nfunc TestPTRResponseFromDHCPLeases(t *testing.T) {\n\tconst localDomain = \"lan\"\n\n\tflt, err := filtering.New(&filtering.Config{\n\t\tLogger:               testLogger,\n\t\tApplyClientFiltering: applyEmptyClientFiltering,\n\t\tBlockedServices:      emptyFilteringBlockedServices(),\n\t\tBlockingMode:         filtering.BlockingModeDefault,\n\t}, nil)\n\trequire.NoError(t, err)\n\n\ts, err := NewServer(DNSCreateParams{\n\t\tDNSFilter: flt,\n\t\tDHCPServer: &testDHCP{\n\t\t\tOnEnabled:  func() (ok bool) { return true },\n\t\t\tOnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },\n\t\t\tOnHostByIP: func(ip netip.Addr) (host string) {\n\t\t\t\treturn \"myhost\"\n\t\t\t},\n\t\t},\n\t\tPrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\tLogger:      testLogger,\n\t\tLocalDomain: localDomain,\n\t})\n\trequire.NoError(t, err)\n\n\ts.conf.UDPListenAddrs = []*net.UDPAddr{{}}\n\ts.conf.TCPListenAddrs = []*net.TCPAddr{{}}\n\ts.conf.UpstreamDNS = []string{\"127.0.0.1:53\"}\n\ts.conf.TLSConf = &TLSConfig{}\n\ts.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}\n\ts.conf.Config.ClientsContainer = EmptyClientsContainer{}\n\ts.conf.Config.UpstreamMode = UpstreamModeLoadBalance\n\n\terr = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &s.conf)\n\trequire.NoError(t, err)\n\n\terr = s.Start(testutil.ContextWithTimeout(t, testTimeout))\n\trequire.NoError(t, err)\n\tt.Cleanup(func() { s.Close(testutil.ContextWithTimeout(t, testTimeout)) })\n\n\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\treq := createTestMessageWithType(\"34.12.168.192.in-addr.arpa.\", dns.TypePTR)\n\n\tresp, err := dns.Exchange(req, addr.String())\n\trequire.NoErrorf(t, err, \"%s\", addr)\n\n\trequire.Len(t, resp.Answer, 1)\n\n\tans := resp.Answer[0]\n\tassert.Equal(t, dns.TypePTR, ans.Header().Rrtype)\n\tassert.Equal(t, \"34.12.168.192.in-addr.arpa.\", ans.Header().Name)\n\n\tptr := testutil.RequireTypeAssert[*dns.PTR](t, ans)\n\n\tassert.Equal(t, dns.Fqdn(\"myhost.\"+localDomain), ptr.Ptr)\n}\n\nfunc TestPTRResponseFromHosts(t *testing.T) {\n\t// Prepare test hosts file.\n\n\tconst hostsFilename = \"hosts\"\n\n\ttestFS := fstest.MapFS{\n\t\thostsFilename: &fstest.MapFile{Data: []byte(`\n\t\t127.0.0.1   host # comment\n\t\t::1         localhost#comment\n\t`)},\n\t}\n\n\tdhcp := &testDHCP{\n\t\tOnEnabled:  func() (ok bool) { return false },\n\t\tOnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },\n\t\tOnHostByIP: func(ip netip.Addr) (host string) { return \"\" },\n\t}\n\n\tvar eventsCalledCounter uint32\n\twatcher := aghtest.NewFSWatcher()\n\twatcher.OnEvents = func() (e <-chan aghos.Event) {\n\t\tassert.Equal(t, uint32(1), atomic.AddUint32(&eventsCalledCounter, 1))\n\n\t\treturn nil\n\t}\n\twatcher.OnAdd = func(name string) (err error) {\n\t\tassert.Equal(t, filepath.Join(aghos.RootDir(), hostsFilename), name)\n\n\t\treturn nil\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\thc, err := aghnet.NewHostsContainer(ctx, testLogger, testFS, watcher, hostsFilename)\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\tassert.Equal(t, uint32(1), atomic.LoadUint32(&eventsCalledCounter))\n\t})\n\n\tflt, err := filtering.New(&filtering.Config{\n\t\tLogger:               testLogger,\n\t\tApplyClientFiltering: applyEmptyClientFiltering,\n\t\tBlockedServices:      emptyFilteringBlockedServices(),\n\t\tBlockingMode:         filtering.BlockingModeDefault,\n\t\tEtcHosts:             hc,\n\t}, nil)\n\trequire.NoError(t, err)\n\n\tflt.SetEnabled(true)\n\n\tvar s *Server\n\ts, err = NewServer(DNSCreateParams{\n\t\tDHCPServer:  dhcp,\n\t\tDNSFilter:   flt,\n\t\tPrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\tLogger:      testLogger,\n\t})\n\trequire.NoError(t, err)\n\n\ts.conf.UDPListenAddrs = []*net.UDPAddr{{}}\n\ts.conf.TCPListenAddrs = []*net.TCPAddr{{}}\n\ts.conf.UpstreamDNS = []string{\"127.0.0.1:53\"}\n\ts.conf.TLSConf = &TLSConfig{}\n\ts.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}\n\ts.conf.Config.ClientsContainer = EmptyClientsContainer{}\n\ts.conf.Config.UpstreamMode = UpstreamModeLoadBalance\n\n\terr = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &s.conf)\n\trequire.NoError(t, err)\n\n\terr = s.Start(testutil.ContextWithTimeout(t, testTimeout))\n\trequire.NoError(t, err)\n\tt.Cleanup(func() { s.Close(testutil.ContextWithTimeout(t, testTimeout)) })\n\n\tsubTestFunc := func(t *testing.T) {\n\t\taddr := s.dnsProxy.Addr(proxy.ProtoUDP)\n\t\treq := createTestMessageWithType(\"1.0.0.127.in-addr.arpa.\", dns.TypePTR)\n\n\t\tresp, eerr := dns.Exchange(req, addr.String())\n\t\trequire.NoError(t, eerr)\n\n\t\trequire.Len(t, resp.Answer, 1)\n\n\t\tassert.Equal(t, dns.TypePTR, resp.Answer[0].Header().Rrtype)\n\t\tassert.Equal(t, \"1.0.0.127.in-addr.arpa.\", resp.Answer[0].Header().Name)\n\n\t\tptr, ok := resp.Answer[0].(*dns.PTR)\n\t\trequire.True(t, ok)\n\t\tassert.Equal(t, \"host.\", ptr.Ptr)\n\t}\n\n\tfor _, protect := range []bool{true, false} {\n\t\tval := protect\n\t\tconf := s.getDNSConfig(testutil.ContextWithTimeout(t, testTimeout))\n\t\tconf.ProtectionEnabled = &val\n\t\ts.setConfig(conf)\n\n\t\tt.Run(fmt.Sprintf(\"protection_is_%t\", val), subTestFunc)\n\t}\n}\n\nfunc TestNewServer(t *testing.T) {\n\t// TODO(a.garipov): Consider moving away from the text-based error\n\t// checks and onto a more structured approach.\n\ttestCases := []struct {\n\t\tname       string\n\t\tin         DNSCreateParams\n\t\twantErrMsg string\n\t}{{\n\t\tname: \"success\",\n\t\tin: DNSCreateParams{\n\t\t\tLogger: testLogger,\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"success_local_tld\",\n\t\tin: DNSCreateParams{\n\t\t\tLogger:      testLogger,\n\t\t\tLocalDomain: \"mynet\",\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"success_local_domain\",\n\t\tin: DNSCreateParams{\n\t\t\tLogger:      testLogger,\n\t\t\tLocalDomain: \"my.local.net\",\n\t\t},\n\t\twantErrMsg: \"\",\n\t}, {\n\t\tname: \"bad_local_domain\",\n\t\tin: DNSCreateParams{\n\t\t\tLogger:      testLogger,\n\t\t\tLocalDomain: \"!!!\",\n\t\t},\n\t\twantErrMsg: `local domain: bad domain name \"!!!\": ` +\n\t\t\t`bad top-level domain name label \"!!!\": ` +\n\t\t\t`bad top-level domain name label rune '!'`,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t_, err := NewServer(tc.in)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\n// doubleTTL is a helper function that returns a clone of DNS PTR with appended\n// copy of first answer record with doubled TTL.\nfunc doubleTTL(msg *dns.Msg) (resp *dns.Msg) {\n\tif msg == nil {\n\t\treturn nil\n\t}\n\n\tif len(msg.Answer) == 0 {\n\t\treturn msg\n\t}\n\n\trec := msg.Answer[0]\n\tptr, ok := rec.(*dns.PTR)\n\tif !ok {\n\t\treturn msg\n\t}\n\n\tclone := *ptr\n\tclone.Hdr.Ttl *= 2\n\tmsg.Answer = append(msg.Answer, &clone)\n\n\treturn msg\n}\n\nfunc TestServer_Exchange(t *testing.T) {\n\tconst (\n\t\tonesHost        = \"one.one.one.one\"\n\t\ttwosHost        = \"two.two.two.two\"\n\t\tlocalDomainHost = \"local.domain\"\n\n\t\tdefaultTTL = time.Second * 60\n\t)\n\n\tvar (\n\t\tonesIP  = netip.MustParseAddr(\"1.1.1.1\")\n\t\ttwosIP  = netip.MustParseAddr(\"2.2.2.2\")\n\t\tlocalIP = netip.MustParseAddr(\"192.168.1.1\")\n\n\t\tpt = testutil.PanicT{}\n\t)\n\n\tonesRevExtIPv4, err := netutil.IPToReversedAddr(onesIP.AsSlice())\n\trequire.NoError(t, err)\n\n\ttwosRevExtIPv4, err := netutil.IPToReversedAddr(twosIP.AsSlice())\n\trequire.NoError(t, err)\n\n\textUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tresp := cmp.Or(\n\t\t\taghtest.MatchedResponse(req, dns.TypePTR, onesRevExtIPv4, dns.Fqdn(onesHost)),\n\t\t\tdoubleTTL(\n\t\t\t\taghtest.MatchedResponse(req, dns.TypePTR, twosRevExtIPv4, dns.Fqdn(twosHost)),\n\t\t\t),\n\t\t\tnew(dns.Msg).SetRcode(req, dns.RcodeNameError),\n\t\t)\n\n\t\trequire.NoError(pt, w.WriteMsg(resp))\n\t})\n\tupsAddr := aghtest.StartLocalhostUpstream(t, extUpsHdlr).String()\n\n\trevLocIPv4, err := netutil.IPToReversedAddr(localIP.AsSlice())\n\trequire.NoError(t, err)\n\n\tlocUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tresp := cmp.Or(\n\t\t\taghtest.MatchedResponse(req, dns.TypePTR, revLocIPv4, dns.Fqdn(localDomainHost)),\n\t\t\tnew(dns.Msg).SetRcode(req, dns.RcodeNameError),\n\t\t)\n\n\t\trequire.NoError(pt, w.WriteMsg(resp))\n\t})\n\n\terrUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\trequire.NoError(pt, w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)))\n\t})\n\n\tnonPtrHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\thash := sha256.Sum256([]byte(\"some-host\"))\n\t\tresp := (&dns.Msg{\n\t\t\tAnswer: []dns.RR{&dns.TXT{\n\t\t\t\tHdr: dns.RR_Header{\n\t\t\t\t\tName:   req.Question[0].Name,\n\t\t\t\t\tRrtype: dns.TypeTXT,\n\t\t\t\t\tClass:  dns.ClassINET,\n\t\t\t\t\tTtl:    60,\n\t\t\t\t},\n\t\t\t\tTxt: []string{hex.EncodeToString(hash[:])},\n\t\t\t}},\n\t\t}).SetReply(req)\n\n\t\trequire.NoError(pt, w.WriteMsg(resp))\n\t})\n\trefusingHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\trequire.NoError(pt, w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeRefused)))\n\t})\n\n\tzeroTTLHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tresp := (&dns.Msg{\n\t\t\tAnswer: []dns.RR{&dns.PTR{\n\t\t\t\tHdr: dns.RR_Header{\n\t\t\t\t\tName:   req.Question[0].Name,\n\t\t\t\t\tRrtype: dns.TypePTR,\n\t\t\t\t\tClass:  dns.ClassINET,\n\t\t\t\t\tTtl:    0,\n\t\t\t\t},\n\t\t\t\tPtr: dns.Fqdn(localDomainHost),\n\t\t\t}},\n\t\t}).SetReply(req)\n\n\t\trequire.NoError(pt, w.WriteMsg(resp))\n\t})\n\n\ttestCases := []struct {\n\t\treq         netip.Addr\n\t\twantErr     error\n\t\tlocUpstream dns.Handler\n\t\tname        string\n\t\twant        string\n\t\twantTTL     time.Duration\n\t}{{\n\t\tname:        \"external_good\",\n\t\twant:        onesHost,\n\t\twantErr:     nil,\n\t\tlocUpstream: nil,\n\t\treq:         onesIP,\n\t\twantTTL:     defaultTTL,\n\t}, {\n\t\tname:        \"local_good\",\n\t\twant:        localDomainHost,\n\t\twantErr:     nil,\n\t\tlocUpstream: locUpsHdlr,\n\t\treq:         localIP,\n\t\twantTTL:     defaultTTL,\n\t}, {\n\t\tname:        \"upstream_error\",\n\t\twant:        \"\",\n\t\twantErr:     ErrRDNSFailed,\n\t\tlocUpstream: errUpsHdlr,\n\t\treq:         localIP,\n\t\twantTTL:     0,\n\t}, {\n\t\tname:        \"empty_answer_error\",\n\t\twant:        \"\",\n\t\twantErr:     ErrRDNSNoData,\n\t\tlocUpstream: locUpsHdlr,\n\t\treq:         netip.MustParseAddr(\"192.168.1.2\"),\n\t\twantTTL:     0,\n\t}, {\n\t\tname:        \"invalid_answer\",\n\t\twant:        \"\",\n\t\twantErr:     ErrRDNSNoData,\n\t\tlocUpstream: nonPtrHdlr,\n\t\treq:         localIP,\n\t\twantTTL:     0,\n\t}, {\n\t\tname:        \"refused\",\n\t\twant:        \"\",\n\t\twantErr:     ErrRDNSFailed,\n\t\tlocUpstream: refusingHdlr,\n\t\treq:         localIP,\n\t\twantTTL:     0,\n\t}, {\n\t\tname:        \"longest_ttl\",\n\t\twant:        twosHost,\n\t\twantErr:     nil,\n\t\tlocUpstream: nil,\n\t\treq:         twosIP,\n\t\twantTTL:     defaultTTL * 2,\n\t}, {\n\t\tname:        \"zero_ttl\",\n\t\twant:        localDomainHost,\n\t\twantErr:     nil,\n\t\tlocUpstream: zeroTTLHdlr,\n\t\treq:         localIP,\n\t\twantTTL:     0,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tlocalUpsAddr := aghtest.StartLocalhostUpstream(t, tc.locUpstream).String()\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tsrv := createTestServer(t, &filtering.Config{\n\t\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t\t}, ServerConfig{\n\t\t\t\tTLSConf: &TLSConfig{},\n\t\t\t\tConfig: Config{\n\t\t\t\t\tUpstreamDNS:      []string{upsAddr},\n\t\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t\t},\n\t\t\t\tLocalPTRResolvers: []string{localUpsAddr},\n\t\t\t\tUsePrivateRDNS:    true,\n\t\t\t\tServePlainDNS:     true,\n\t\t\t})\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\thost, ttl, eerr := srv.Exchange(ctx, tc.req)\n\n\t\t\trequire.ErrorIs(t, eerr, tc.wantErr)\n\t\t\tassert.Equal(t, tc.want, host)\n\t\t\tassert.Equal(t, tc.wantTTL, ttl)\n\t\t})\n\t}\n\n\tt.Run(\"resolving_disabled\", func(t *testing.T) {\n\t\tsrv := createTestServer(t, &filtering.Config{\n\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t}, ServerConfig{\n\t\t\tTLSConf: &TLSConfig{},\n\t\t\tConfig: Config{\n\t\t\t\tUpstreamDNS:      []string{upsAddr},\n\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t},\n\t\t\tLocalPTRResolvers: []string{},\n\t\t\tServePlainDNS:     true,\n\t\t})\n\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\thost, _, eerr := srv.Exchange(ctx, localIP)\n\n\t\trequire.NoError(t, eerr)\n\t\tassert.Empty(t, host)\n\t})\n}\n"
  },
  {
    "path": "internal/dnsforward/dnsrewrite.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// filterDNSRewriteResponse handles a single DNS rewrite response entry.  It\n// returns the properly constructed answer resource record.\nfunc (s *Server) filterDNSRewriteResponse(\n\tctx context.Context,\n\treq *dns.Msg,\n\trr rules.RRType,\n\tv rules.RRValue,\n) (ans dns.RR, err error) {\n\tswitch rr {\n\tcase dns.TypeA, dns.TypeAAAA:\n\t\treturn s.ansFromDNSRewriteIP(v, rr, req)\n\tcase dns.TypePTR, dns.TypeTXT:\n\t\treturn s.ansFromDNSRewriteText(v, rr, req)\n\tcase dns.TypeMX:\n\t\treturn s.ansFromDNSRewriteMX(v, rr, req)\n\tcase dns.TypeHTTPS, dns.TypeSVCB:\n\t\treturn s.ansFromDNSRewriteSVCB(ctx, v, rr, req)\n\tcase dns.TypeSRV:\n\t\treturn s.ansFromDNSRewriteSRV(v, rr, req)\n\tdefault:\n\t\ts.logger.DebugContext(ctx, \"unsupported dns rr type, skipping\", \"res_record\", rr)\n\n\t\treturn nil, nil\n\t}\n}\n\n// ansFromDNSRewriteIP creates a new answer resource record from the A/AAAA\n// dnsrewrite rule data.\nfunc (s *Server) ansFromDNSRewriteIP(\n\tv rules.RRValue,\n\trr rules.RRType,\n\treq *dns.Msg,\n) (ans dns.RR, err error) {\n\tip, ok := v.(netip.Addr)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"value for rr type %s has type %T, not netip.Addr\", dns.Type(rr), v)\n\t}\n\n\tif rr == dns.TypeA {\n\t\treturn s.genAnswerA(req, ip), nil\n\t}\n\n\treturn s.genAnswerAAAA(req, ip), nil\n}\n\n// ansFromDNSRewriteText creates a new answer resource record from the TXT/PTR\n// dnsrewrite rule data.\nfunc (s *Server) ansFromDNSRewriteText(\n\tv rules.RRValue,\n\trr rules.RRType,\n\treq *dns.Msg,\n) (ans dns.RR, err error) {\n\tstr, ok := v.(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"value for rr type %s has type %T, not string\", dns.Type(rr), v)\n\t}\n\n\tif rr == dns.TypeTXT {\n\t\treturn s.genAnswerTXT(req, []string{str}), nil\n\t}\n\n\treturn s.genAnswerPTR(req, str), nil\n}\n\n// ansFromDNSRewriteMX creates a new answer resource record from the MX\n// dnsrewrite rule data.\nfunc (s *Server) ansFromDNSRewriteMX(\n\tv rules.RRValue,\n\trr rules.RRType,\n\treq *dns.Msg,\n) (ans dns.RR, err error) {\n\tmx, ok := v.(*rules.DNSMX)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"value for rr type %s has type %T, not *rules.DNSMX\",\n\t\t\tdns.Type(rr),\n\t\t\tv,\n\t\t)\n\t}\n\n\treturn s.genAnswerMX(req, mx), nil\n}\n\n// ansFromDNSRewriteSVCB creates a new answer resource record from the\n// SVCB/HTTPS dnsrewrite rule data.\nfunc (s *Server) ansFromDNSRewriteSVCB(\n\tctx context.Context,\n\tv rules.RRValue,\n\trr rules.RRType,\n\treq *dns.Msg,\n) (ans dns.RR, err error) {\n\tsvcb, ok := v.(*rules.DNSSVCB)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"value for rr type %s has type %T, not *rules.DNSSVCB\",\n\t\t\tdns.Type(rr),\n\t\t\tv,\n\t\t)\n\t}\n\n\tif rr == dns.TypeHTTPS {\n\t\treturn s.genAnswerHTTPS(ctx, req, svcb), nil\n\t}\n\n\treturn s.genAnswerSVCB(ctx, req, svcb), nil\n}\n\n// ansFromDNSRewriteSRV creates a new answer resource record from the SRV\n// dnsrewrite rule data.\nfunc (s *Server) ansFromDNSRewriteSRV(\n\tv rules.RRValue,\n\trr rules.RRType,\n\treq *dns.Msg,\n) (dns.RR, error) {\n\tsrv, ok := v.(*rules.DNSSRV)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"value for rr type %s has type %T, not *rules.DNSSRV\",\n\t\t\tdns.Type(rr),\n\t\t\tv,\n\t\t)\n\t}\n\n\treturn s.genAnswerSRV(req, srv), nil\n}\n\n// filterDNSRewrite handles dnsrewrite filters.  It constructs a DNS response\n// and sets it into pctx.Res.  All parameters must not be nil.\nfunc (s *Server) filterDNSRewrite(\n\tctx context.Context,\n\treq *dns.Msg,\n\tres *filtering.Result,\n\tpctx *proxy.DNSContext,\n) (err error) {\n\tresp := s.replyCompressed(req)\n\tdnsrr := res.DNSRewriteResult\n\tif dnsrr == nil {\n\t\treturn errors.Error(\"no dns rewrite rule content\")\n\t}\n\n\tresp.Rcode = dnsrr.RCode\n\tif resp.Rcode != dns.RcodeSuccess {\n\t\tpctx.Res = resp\n\n\t\treturn nil\n\t}\n\n\tif dnsrr.Response == nil {\n\t\treturn errors.Error(\"no dns rewrite rule responses\")\n\t}\n\n\tqtype := req.Question[0].Qtype\n\tvalues := dnsrr.Response[qtype]\n\tfor i, v := range values {\n\t\tvar ans dns.RR\n\t\tans, err = s.filterDNSRewriteResponse(ctx, req, qtype, v)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"dns rewrite response for %s[%d]: %w\", dns.Type(qtype), i, err)\n\t\t}\n\n\t\tresp.Answer = append(resp.Answer, ans)\n\t}\n\n\tpctx.Res = resp\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dnsforward/dnsrewrite_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestServer_FilterDNSRewrite(t *testing.T) {\n\t// Helper data.\n\tconst domain = \"example.com\"\n\tip4, ip6 := netutil.IPv4Localhost(), netutil.IPv6Localhost()\n\tmxVal := &rules.DNSMX{\n\t\tExchange:   \"mail.example.com\",\n\t\tPreference: 32,\n\t}\n\tsvcbVal := &rules.DNSSVCB{\n\t\tParams:   map[string]string{\"alpn\": \"h3\", \"dohpath\": \"/dns-query\"},\n\t\tTarget:   dns.Fqdn(domain),\n\t\tPriority: 32,\n\t}\n\tsrvVal := &rules.DNSSRV{\n\t\tPriority: 32,\n\t\tWeight:   60,\n\t\tPort:     8080,\n\t\tTarget:   dns.Fqdn(domain),\n\t}\n\n\t// Helper functions and entities.\n\tsrv := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tTLSConf: &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t})\n\n\tmakeQ := func(qtype rules.RRType) (req *dns.Msg) {\n\t\treturn &dns.Msg{\n\t\t\tQuestion: []dns.Question{{\n\t\t\t\tQtype: qtype,\n\t\t\t}},\n\t\t}\n\t}\n\tmakeRes := func(rcode rules.RCode, rr rules.RRType, v rules.RRValue) (res *filtering.Result) {\n\t\tresp := filtering.DNSRewriteResultResponse{\n\t\t\trr: []rules.RRValue{v},\n\t\t}\n\t\treturn &filtering.Result{\n\t\t\tDNSRewriteResult: &filtering.DNSRewriteResult{\n\t\t\t\tRCode:    rcode,\n\t\t\t\tResponse: resp,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Tests.\n\tt.Run(\"nxdomain\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeA)\n\t\tres := makeRes(dns.RcodeNameError, 0, nil)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeNameError, d.Res.Rcode)\n\t})\n\n\tt.Run(\"noerror_empty\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeA)\n\t\tres := makeRes(dns.RcodeSuccess, 0, nil)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\t\tassert.Empty(t, d.Res.Answer)\n\t})\n\n\tt.Run(\"noerror_a\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeA)\n\t\tres := makeRes(dns.RcodeSuccess, dns.TypeA, ip4)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\n\t\trequire.Len(t, d.Res.Answer, 1)\n\t\tassert.Equal(t, net.IP(ip4.AsSlice()), d.Res.Answer[0].(*dns.A).A)\n\t})\n\n\tt.Run(\"noerror_aaaa\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeAAAA)\n\t\tres := makeRes(dns.RcodeSuccess, dns.TypeAAAA, ip6)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\n\t\trequire.Len(t, d.Res.Answer, 1)\n\t\tassert.Equal(t, net.IP(ip6.AsSlice()), d.Res.Answer[0].(*dns.AAAA).AAAA)\n\t})\n\n\tt.Run(\"noerror_ptr\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypePTR)\n\t\tres := makeRes(dns.RcodeSuccess, dns.TypePTR, domain)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\n\t\trequire.Len(t, d.Res.Answer, 1)\n\t\tassert.Equal(t, dns.Fqdn(domain), d.Res.Answer[0].(*dns.PTR).Ptr)\n\t})\n\n\tt.Run(\"noerror_txt\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeTXT)\n\t\tres := makeRes(dns.RcodeSuccess, dns.TypeTXT, domain)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\n\t\trequire.Len(t, d.Res.Answer, 1)\n\t\tassert.Equal(t, []string{domain}, d.Res.Answer[0].(*dns.TXT).Txt)\n\t})\n\n\tt.Run(\"noerror_mx\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeMX)\n\t\tres := makeRes(dns.RcodeSuccess, dns.TypeMX, mxVal)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\n\t\trequire.Len(t, d.Res.Answer, 1)\n\t\tans, ok := d.Res.Answer[0].(*dns.MX)\n\n\t\trequire.True(t, ok)\n\t\tassert.Equal(t, dns.Fqdn(mxVal.Exchange), ans.Mx)\n\t\tassert.Equal(t, mxVal.Preference, ans.Preference)\n\t})\n\n\tt.Run(\"noerror_svcb\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeSVCB)\n\t\tres := makeRes(dns.RcodeSuccess, dns.TypeSVCB, svcbVal)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\n\t\trequire.Len(t, d.Res.Answer, 1)\n\t\tans, ok := d.Res.Answer[0].(*dns.SVCB)\n\n\t\trequire.True(t, ok)\n\t\trequire.Len(t, ans.Value, 2)\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]dns.SVCBKey{dns.SVCB_ALPN, dns.SVCB_DOHPATH},\n\t\t\t[]dns.SVCBKey{ans.Value[0].Key(), ans.Value[1].Key()},\n\t\t)\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{svcbVal.Params[\"alpn\"], svcbVal.Params[\"dohpath\"]},\n\t\t\t[]string{ans.Value[0].String(), ans.Value[1].String()},\n\t\t)\n\t\tassert.Equal(t, svcbVal.Target, ans.Target)\n\t\tassert.Equal(t, svcbVal.Priority, ans.Priority)\n\t})\n\n\tt.Run(\"noerror_https\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeHTTPS)\n\t\tres := makeRes(dns.RcodeSuccess, dns.TypeHTTPS, svcbVal)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\n\t\trequire.Len(t, d.Res.Answer, 1)\n\t\tans, ok := d.Res.Answer[0].(*dns.HTTPS)\n\n\t\trequire.True(t, ok)\n\t\trequire.Len(t, ans.Value, 2)\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]dns.SVCBKey{dns.SVCB_ALPN, dns.SVCB_DOHPATH},\n\t\t\t[]dns.SVCBKey{ans.Value[0].Key(), ans.Value[1].Key()},\n\t\t)\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{svcbVal.Params[\"alpn\"], svcbVal.Params[\"dohpath\"]},\n\t\t\t[]string{ans.Value[0].String(), ans.Value[1].String()},\n\t\t)\n\t\tassert.Equal(t, svcbVal.Target, ans.Target)\n\t\tassert.Equal(t, svcbVal.Priority, ans.Priority)\n\t})\n\n\tt.Run(\"noerror_srv\", func(t *testing.T) {\n\t\treq := makeQ(dns.TypeSRV)\n\t\tres := makeRes(dns.RcodeSuccess, dns.TypeSRV, srvVal)\n\t\td := &proxy.DNSContext{}\n\n\t\terr := srv.filterDNSRewrite(testutil.ContextWithTimeout(t, testTimeout), req, res, d)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)\n\n\t\trequire.Len(t, d.Res.Answer, 1)\n\t\tans, ok := d.Res.Answer[0].(*dns.SRV)\n\n\t\trequire.True(t, ok)\n\t\tassert.Equal(t, srvVal.Priority, ans.Priority)\n\t\tassert.Equal(t, srvVal.Weight, ans.Weight)\n\t\tassert.Equal(t, srvVal.Port, ans.Port)\n\t\tassert.Equal(t, srvVal.Target, ans.Target)\n\t})\n}\n"
  },
  {
    "path": "internal/dnsforward/filter.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// clientRequestFilteringSettings looks up client filtering settings using the\n// client's IP address and ID, if any, from dctx.\nfunc (s *Server) clientRequestFilteringSettings(dctx *dnsContext) (setts *filtering.Settings) {\n\tsetts = s.dnsFilter.Settings()\n\tsetts.ProtectionEnabled = dctx.protectionEnabled\n\ts.dnsFilter.ApplyAdditionalFiltering(dctx.proxyCtx.Addr.Addr(), dctx.clientID, setts)\n\n\treturn setts\n}\n\n// filterDNSRequest applies the dnsFilter and sets dctx.proxyCtx.Res if the\n// request was filtered.\nfunc (s *Server) filterDNSRequest(\n\tctx context.Context,\n\tdctx *dnsContext,\n) (res *filtering.Result, err error) {\n\tpctx := dctx.proxyCtx\n\treq := pctx.Req\n\tq := req.Question[0]\n\thost := strings.TrimSuffix(q.Name, \".\")\n\n\tresVal, err := s.dnsFilter.CheckHost(host, q.Qtype, dctx.setts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"checking host %q: %w\", host, err)\n\t}\n\n\t// TODO(a.garipov): Make CheckHost return a pointer.\n\tres = &resVal\n\tswitch {\n\tcase isRewrittenCNAME(res):\n\t\t// Resolve the new canonical name, not the original host name.  The\n\t\t// original question is readded in processFilteringAfterResponse.\n\t\tdctx.origQuestion = q\n\t\treq.Question[0].Name = dns.Fqdn(res.CanonName)\n\tcase res.IsFiltered:\n\t\ts.logger.DebugContext(ctx, \"host is filtered\", \"host\", host, \"reason\", res.Reason)\n\t\tpctx.Res = s.genDNSFilterMessage(ctx, pctx, res)\n\tcase res.Reason.In(filtering.Rewritten, filtering.FilteredSafeSearch):\n\t\tpctx.Res = s.getCNAMEWithIPs(ctx, req, res.IPList, res.CanonName)\n\tcase res.Reason.In(filtering.RewrittenRule, filtering.RewrittenAutoHosts):\n\t\tif err = s.filterDNSRewrite(ctx, req, res, pctx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn res, err\n}\n\n// isRewrittenCNAME returns true if the request considered to be rewritten with\n// CNAME and has no resolved IPs.\nfunc isRewrittenCNAME(res *filtering.Result) (ok bool) {\n\treturn res.Reason.In(\n\t\tfiltering.Rewritten,\n\t\tfiltering.RewrittenRule,\n\t\tfiltering.FilteredSafeSearch) &&\n\t\tres.CanonName != \"\" &&\n\t\tlen(res.IPList) == 0\n}\n\n// checkHostRules checks the host against filters.  It is safe for concurrent\n// use.\nfunc (s *Server) checkHostRules(\n\thost string,\n\trrtype rules.RRType,\n\tsetts *filtering.Settings,\n) (r *filtering.Result, err error) {\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\tres, err := s.dnsFilter.CheckHostRules(host, rrtype, setts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &res, err\n}\n\n// filterDNSResponse checks each resource record of answer section of\n// dctx.proxyCtx.Res.  It sets dctx.result and dctx.origResp if at least one of\n// canonical names, IP addresses, or HTTPS RR hints in it matches the filtering\n// rules, as well as sets dctx.proxyCtx.Res to the filtered response.\nfunc (s *Server) filterDNSResponse(ctx context.Context, dctx *dnsContext) (err error) {\n\tsetts := dctx.setts\n\tif !setts.FilteringEnabled {\n\t\treturn nil\n\t}\n\n\tvar res *filtering.Result\n\tpctx := dctx.proxyCtx\n\tfor i, a := range pctx.Res.Answer {\n\t\thost := \"\"\n\t\tvar rrtype rules.RRType\n\t\tswitch a := a.(type) {\n\t\tcase *dns.CNAME:\n\t\t\thost = strings.TrimSuffix(a.Target, \".\")\n\t\t\trrtype = dns.TypeCNAME\n\n\t\t\tres, err = s.checkHostRules(host, rrtype, setts)\n\t\tcase *dns.A:\n\t\t\thost = a.A.String()\n\t\t\trrtype = dns.TypeA\n\n\t\t\tres, err = s.checkHostRules(host, rrtype, setts)\n\t\tcase *dns.AAAA:\n\t\t\thost = a.AAAA.String()\n\t\t\trrtype = dns.TypeAAAA\n\n\t\t\tres, err = s.checkHostRules(host, rrtype, setts)\n\t\tcase *dns.HTTPS:\n\t\t\tres, err = s.filterHTTPSRecords(a, setts)\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"checked\",\n\t\t\t\"dns_type\", dns.Type(rrtype),\n\t\t\t\"host\", host,\n\t\t\t\"name\", a.Header().Name,\n\t\t)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"filtering answer at index %d: %w\", i, err)\n\t\t} else if res != nil && res.IsFiltered {\n\t\t\tdctx.result = res\n\t\t\tdctx.origResp = pctx.Res\n\t\t\tpctx.Res = s.genDNSFilterMessage(ctx, pctx, res)\n\n\t\t\ts.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"matched by response\",\n\t\t\t\t\"name\", pctx.Req.Question[0].Name,\n\t\t\t\t\"host\", host,\n\t\t\t)\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// removeIPv6Hints deletes IPv6 hints from RR values.\nfunc removeIPv6Hints(rr *dns.HTTPS) {\n\trr.Value = slices.DeleteFunc(rr.Value, func(kv dns.SVCBKeyValue) (del bool) {\n\t\t_, ok := kv.(*dns.SVCBIPv6Hint)\n\n\t\treturn ok\n\t})\n}\n\n// filterHTTPSRecords filters HTTPS answers information through all rule list\n// filters of the server filters.  Removes IPv6 hints if IPv6 resolving is\n// disabled.\nfunc (s *Server) filterHTTPSRecords(rr *dns.HTTPS, setts *filtering.Settings) (r *filtering.Result, err error) {\n\tif s.conf.AAAADisabled {\n\t\tremoveIPv6Hints(rr)\n\t}\n\n\tfor _, kv := range rr.Value {\n\t\tvar ips []net.IP\n\t\tswitch hint := kv.(type) {\n\t\tcase *dns.SVCBIPv4Hint:\n\t\t\tips = hint.Hint\n\t\tcase *dns.SVCBIPv6Hint:\n\t\t\tips = hint.Hint\n\t\tdefault:\n\t\t\t// Go on.\n\t\t}\n\n\t\tif len(ips) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tr, err = s.filterSVCBHint(ips, setts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"filtering svcb hints: %w\", err)\n\t\t}\n\n\t\tif r != nil {\n\t\t\treturn r, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\n// filterSVCBHint filters SVCB hint information.\nfunc (s *Server) filterSVCBHint(\n\thint []net.IP,\n\tsetts *filtering.Settings,\n) (res *filtering.Result, err error) {\n\tfor _, h := range hint {\n\t\tres, err = s.checkHostRules(h.String(), dns.TypeHTTPS, setts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"checking rules for %s: %w\", h, err)\n\t\t}\n\n\t\tif res != nil && res.IsFiltered {\n\t\t\treturn res, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n"
  },
  {
    "path": "internal/dnsforward/filter_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestServer_filterDNSResponse(t *testing.T) {\n\tconst (\n\t\tpassedIPv4Str  = \"1.1.1.1\"\n\t\tblockedIPv4Str = \"1.2.3.4\"\n\t\tblockedIPv6Str = \"1234::cdef\"\n\t\tblockRules     = blockedIPv4Str + \"\\n\" + blockedIPv6Str + \"\\n\"\n\t)\n\n\tvar (\n\t\tpassedIPv4  net.IP = netip.MustParseAddr(passedIPv4Str).AsSlice()\n\t\tblockedIPv4 net.IP = netip.MustParseAddr(blockedIPv4Str).AsSlice()\n\t\tblockedIPv6 net.IP = netip.MustParseAddr(blockedIPv6Str).AsSlice()\n\t)\n\n\tfilters := []filtering.Filter{{\n\t\tID: 0, Data: []byte(blockRules),\n\t}}\n\n\tf, err := filtering.New(&filtering.Config{\n\t\tLogger: testLogger,\n\t}, filters)\n\trequire.NoError(t, err)\n\n\tf.SetEnabled(true)\n\n\ts, err := NewServer(DNSCreateParams{\n\t\tDHCPServer:  &testDHCP{},\n\t\tDNSFilter:   f,\n\t\tPrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\tLogger:      testLogger,\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\treq      *dns.Msg\n\t\tname     string\n\t\twantRule string\n\t\trespAns  []dns.RR\n\t}{{\n\t\tname:     \"pass\",\n\t\treq:      createTestMessageWithType(aghtest.ReqFQDN, dns.TypeA),\n\t\twantRule: \"\",\n\t\trespAns: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   aghtest.ReqFQDN,\n\t\t\t\tRrtype: dns.TypeA,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tA: passedIPv4,\n\t\t}},\n\t}, {\n\t\tname:     \"ipv4\",\n\t\treq:      createTestMessageWithType(aghtest.ReqFQDN, dns.TypeA),\n\t\twantRule: blockedIPv4Str,\n\t\trespAns: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   aghtest.ReqFQDN,\n\t\t\t\tRrtype: dns.TypeA,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tA: blockedIPv4,\n\t\t}},\n\t}, {\n\t\tname:     \"ipv6\",\n\t\treq:      createTestMessageWithType(aghtest.ReqFQDN, dns.TypeAAAA),\n\t\twantRule: blockedIPv6Str,\n\t\trespAns: []dns.RR{&dns.AAAA{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   aghtest.ReqFQDN,\n\t\t\t\tRrtype: dns.TypeAAAA,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tAAAA: blockedIPv6,\n\t\t}},\n\t}, {\n\t\tname:     \"ipv4hint\",\n\t\treq:      createTestMessageWithType(aghtest.ReqFQDN, dns.TypeHTTPS),\n\t\twantRule: blockedIPv4Str,\n\t\trespAns: newSVCBHintsAnswer(\n\t\t\taghtest.ReqFQDN,\n\t\t\t[]dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBIPv4Hint{Hint: []net.IP{blockedIPv4}},\n\t\t\t\t&dns.SVCBIPv6Hint{Hint: []net.IP{}},\n\t\t\t},\n\t\t),\n\t}, {\n\t\tname:     \"ipv6hint\",\n\t\treq:      createTestMessageWithType(aghtest.ReqFQDN, dns.TypeHTTPS),\n\t\twantRule: blockedIPv6Str,\n\t\trespAns: newSVCBHintsAnswer(\n\t\t\taghtest.ReqFQDN,\n\t\t\t[]dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBIPv4Hint{Hint: []net.IP{}},\n\t\t\t\t&dns.SVCBIPv6Hint{Hint: []net.IP{blockedIPv6}},\n\t\t\t},\n\t\t),\n\t}, {\n\t\tname:     \"ipv4_ipv6_hints\",\n\t\treq:      createTestMessageWithType(aghtest.ReqFQDN, dns.TypeHTTPS),\n\t\twantRule: blockedIPv4Str,\n\t\trespAns: newSVCBHintsAnswer(\n\t\t\taghtest.ReqFQDN,\n\t\t\t[]dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBIPv4Hint{Hint: []net.IP{blockedIPv4}},\n\t\t\t\t&dns.SVCBIPv6Hint{Hint: []net.IP{blockedIPv6}},\n\t\t\t},\n\t\t),\n\t}, {\n\t\tname:     \"pass_hints\",\n\t\treq:      createTestMessageWithType(aghtest.ReqFQDN, dns.TypeHTTPS),\n\t\twantRule: \"\",\n\t\trespAns: newSVCBHintsAnswer(\n\t\t\taghtest.ReqFQDN,\n\t\t\t[]dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBIPv4Hint{Hint: []net.IP{passedIPv4}},\n\t\t\t\t&dns.SVCBIPv6Hint{Hint: []net.IP{}},\n\t\t\t},\n\t\t),\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresp := newResp(dns.RcodeSuccess, tc.req, tc.respAns)\n\n\t\t\tpctx := &proxy.DNSContext{\n\t\t\t\tProto: proxy.ProtoUDP,\n\t\t\t\tReq:   tc.req,\n\t\t\t\tRes:   resp,\n\t\t\t\tAddr:  testClientAddrPort,\n\t\t\t}\n\n\t\t\tdctx := &dnsContext{\n\t\t\t\tproxyCtx: pctx,\n\t\t\t\tsetts: &filtering.Settings{\n\t\t\t\t\tProtectionEnabled: true,\n\t\t\t\t\tFilteringEnabled:  true,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tfltErr := s.filterDNSResponse(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\t\t\trequire.NoError(t, fltErr)\n\n\t\t\tres := dctx.result\n\t\t\tif tc.wantRule == \"\" {\n\t\t\t\tassert.Nil(t, res)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\twantResult := &filtering.Result{\n\t\t\t\tIsFiltered: true,\n\t\t\t\tReason:     filtering.FilteredBlockList,\n\t\t\t\tRules: []*filtering.ResultRule{{\n\t\t\t\t\tText: tc.wantRule,\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tassert.Equal(t, wantResult, res)\n\t\t\tassert.Equal(t, resp, dctx.origResp)\n\t\t})\n\t}\n}\n\n// newSVCBHintsAnswer returns a test HTTPS answer RRs with SVCB hints.\nfunc newSVCBHintsAnswer(target string, hints []dns.SVCBKeyValue) (rrs []dns.RR) {\n\treturn []dns.RR{&dns.HTTPS{\n\t\tSVCB: dns.SVCB{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   target,\n\t\t\t\tRrtype: dns.TypeHTTPS,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tTarget: target,\n\t\t\tValue:  hints,\n\t\t},\n\t}}\n}\n"
  },
  {
    "path": "internal/dnsforward/http.go",
    "content": "package dnsforward\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghslog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n)\n\n// jsonDNSConfig is the JSON representation of the DNS server configuration.\n//\n// TODO(s.chzhen):  Split it into smaller pieces.  Use aghalg.NullBool instead\n// of *bool.\ntype jsonDNSConfig struct {\n\t// Upstreams is the list of upstream DNS servers.\n\tUpstreams *[]string `json:\"upstream_dns\"`\n\n\t// UpstreamsFile is the file containing upstream DNS servers.\n\tUpstreamsFile *string `json:\"upstream_dns_file\"`\n\n\t// Bootstraps is the list of DNS servers resolving IP addresses of the\n\t// upstream DoH/DoT resolvers.\n\tBootstraps *[]string `json:\"bootstrap_dns\"`\n\n\t// Fallbacks is the list of fallback DNS servers used when upstream DNS\n\t// servers are not responding.\n\tFallbacks *[]string `json:\"fallback_dns\"`\n\n\t// ProtectionEnabled defines if protection is enabled.\n\tProtectionEnabled *bool `json:\"protection_enabled\"`\n\n\t// Ratelimit is the number of requests per second allowed per client.\n\tRatelimit *uint32 `json:\"ratelimit\"`\n\n\t// RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for\n\t// rate limiting requests.\n\tRatelimitSubnetLenIPv4 *uint `json:\"ratelimit_subnet_len_ipv4\"`\n\n\t// RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for\n\t// rate limiting requests.\n\tRatelimitSubnetLenIPv6 *uint `json:\"ratelimit_subnet_len_ipv6\"`\n\n\t// UpstreamTimeout is an upstream timeout in seconds.\n\tUpstreamTimeout *int `json:\"upstream_timeout\"`\n\n\t// RatelimitWhitelist is a list of IP addresses excluded from rate limiting.\n\tRatelimitWhitelist *[]netip.Addr `json:\"ratelimit_whitelist\"`\n\n\t// BlockingMode defines the way blocked responses are constructed.\n\tBlockingMode *filtering.BlockingMode `json:\"blocking_mode\"`\n\n\t// EDNSCSEnabled defines if EDNS Client Subnet is enabled.\n\tEDNSCSEnabled *bool `json:\"edns_cs_enabled\"`\n\n\t// EDNSCSUseCustom defines if EDNSCSCustomIP should be used.\n\tEDNSCSUseCustom *bool `json:\"edns_cs_use_custom\"`\n\n\t// DNSSECEnabled defines if DNSSEC is enabled.\n\tDNSSECEnabled *bool `json:\"dnssec_enabled\"`\n\n\t// DisableIPv6 defines if IPv6 addresses should be dropped.\n\tDisableIPv6 *bool `json:\"disable_ipv6\"`\n\n\t// UpstreamMode defines the way DNS requests are constructed.\n\tUpstreamMode *jsonUpstreamMode `json:\"upstream_mode\"`\n\n\t// BlockedResponseTTL is the TTL for blocked responses.\n\tBlockedResponseTTL *uint32 `json:\"blocked_response_ttl\"`\n\n\t// CacheSize in bytes.\n\tCacheSize *uint32 `json:\"cache_size\"`\n\n\t// CacheMinTTL is custom minimum TTL for cached DNS responses.\n\tCacheMinTTL *uint32 `json:\"cache_ttl_min\"`\n\n\t// CacheMaxTTL is custom maximum TTL for cached DNS responses.\n\tCacheMaxTTL *uint32 `json:\"cache_ttl_max\"`\n\n\t// CacheEnabled defines if the DNS cache should be used.\n\tCacheEnabled *bool `json:\"cache_enabled\"`\n\n\t// CacheOptimistic defines if expired entries should be served.\n\tCacheOptimistic *bool `json:\"cache_optimistic\"`\n\n\t// ResolveClients defines if clients IPs should be resolved into hostnames.\n\tResolveClients *bool `json:\"resolve_clients\"`\n\n\t// UsePrivateRDNS defines if privates DNS resolvers should be used.\n\tUsePrivateRDNS *bool `json:\"use_private_ptr_resolvers\"`\n\n\t// LocalPTRUpstreams is the list of local private DNS resolvers.\n\tLocalPTRUpstreams *[]string `json:\"local_ptr_upstreams\"`\n\n\t// BlockingIPv4 is custom IPv4 address for blocked A requests.\n\tBlockingIPv4 netip.Addr `json:\"blocking_ipv4\"`\n\n\t// BlockingIPv6 is custom IPv6 address for blocked AAAA requests.\n\tBlockingIPv6 netip.Addr `json:\"blocking_ipv6\"`\n\n\t// DisabledUntil is a timestamp until when the protection is disabled.\n\tDisabledUntil *time.Time `json:\"protection_disabled_until\"`\n\n\t// EDNSCSCustomIP is custom IP for EDNS Client Subnet.\n\tEDNSCSCustomIP netip.Addr `json:\"edns_cs_custom_ip\"`\n\n\t// DefaultLocalPTRUpstreams is used to pass the addresses from\n\t// systemResolvers to the front-end.  It's not a pointer to the slice since\n\t// there is no need to omit it while decoding from JSON.\n\tDefaultLocalPTRUpstreams []string `json:\"default_local_ptr_upstreams,omitempty\"`\n}\n\n// jsonUpstreamMode is a enumeration of upstream modes.\ntype jsonUpstreamMode string\n\nconst (\n\t// jsonUpstreamModeEmpty is the default value on frontend, it is used as\n\t// jsonUpstreamModeLoadBalance mode.\n\t//\n\t// Deprecated: Use jsonUpstreamModeLoadBalance instead.\n\tjsonUpstreamModeEmpty jsonUpstreamMode = \"\"\n\n\tjsonUpstreamModeLoadBalance jsonUpstreamMode = \"load_balance\"\n\tjsonUpstreamModeParallel    jsonUpstreamMode = \"parallel\"\n\tjsonUpstreamModeFastestAddr jsonUpstreamMode = \"fastest_addr\"\n)\n\nfunc (s *Server) getDNSConfig(ctx context.Context) (c *jsonDNSConfig) {\n\tprotectionEnabled, protectionDisabledUntil := s.UpdatedProtectionStatus(ctx)\n\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\tupstreams := stringutil.CloneSliceOrEmpty(s.conf.UpstreamDNS)\n\tupstreamFile := s.conf.UpstreamDNSFileName\n\tbootstraps := stringutil.CloneSliceOrEmpty(s.conf.BootstrapDNS)\n\tfallbacks := stringutil.CloneSliceOrEmpty(s.conf.FallbackDNS)\n\tblockingMode, blockingIPv4, blockingIPv6 := s.dnsFilter.BlockingMode()\n\tblockedResponseTTL := s.dnsFilter.BlockedResponseTTL()\n\tratelimit := s.conf.Ratelimit\n\tratelimitSubnetLenIPv4 := s.conf.RatelimitSubnetLenIPv4\n\tratelimitSubnetLenIPv6 := s.conf.RatelimitSubnetLenIPv6\n\tratelimitWhitelist := append([]netip.Addr{}, s.conf.RatelimitWhitelist...)\n\tupstreamTimeout := int(s.conf.UpstreamTimeout.Seconds())\n\n\tcustomIP := s.conf.EDNSClientSubnet.CustomIP\n\tenableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled\n\tuseCustom := s.conf.EDNSClientSubnet.UseCustom\n\n\tenableDNSSEC := s.conf.EnableDNSSEC\n\taaaaDisabled := s.conf.AAAADisabled\n\tcacheEnabled := s.conf.CacheEnabled\n\tcacheSize := s.conf.CacheSize\n\tcacheMinTTL := s.conf.CacheMinTTL\n\tcacheMaxTTL := s.conf.CacheMaxTTL\n\tcacheOptimistic := s.conf.CacheOptimistic\n\tresolveClients := s.conf.AddrProcConf.UseRDNS\n\tusePrivateRDNS := s.conf.UsePrivateRDNS\n\tlocalPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)\n\n\tvar upstreamMode jsonUpstreamMode\n\tswitch s.conf.UpstreamMode {\n\tcase UpstreamModeLoadBalance:\n\t\t// TODO(d.kolyshev): Support jsonUpstreamModeLoadBalance on frontend instead\n\t\t// of jsonUpstreamModeEmpty.\n\t\tupstreamMode = jsonUpstreamModeEmpty\n\tcase UpstreamModeParallel:\n\t\tupstreamMode = jsonUpstreamModeParallel\n\tcase UpstreamModeFastestAddr:\n\t\tupstreamMode = jsonUpstreamModeFastestAddr\n\t}\n\n\tdefPTRUps, err := s.defaultLocalPTRUpstreams(ctx)\n\tif err != nil {\n\t\ts.logger.ErrorContext(ctx, \"getting local ptr upstreams\", slogutil.KeyError, err)\n\t}\n\n\treturn &jsonDNSConfig{\n\t\tUpstreams:                &upstreams,\n\t\tUpstreamsFile:            &upstreamFile,\n\t\tBootstraps:               &bootstraps,\n\t\tFallbacks:                &fallbacks,\n\t\tProtectionEnabled:        &protectionEnabled,\n\t\tBlockingMode:             &blockingMode,\n\t\tBlockingIPv4:             blockingIPv4,\n\t\tBlockingIPv6:             blockingIPv6,\n\t\tRatelimit:                &ratelimit,\n\t\tRatelimitSubnetLenIPv4:   &ratelimitSubnetLenIPv4,\n\t\tRatelimitSubnetLenIPv6:   &ratelimitSubnetLenIPv6,\n\t\tRatelimitWhitelist:       &ratelimitWhitelist,\n\t\tUpstreamTimeout:          &upstreamTimeout,\n\t\tEDNSCSCustomIP:           customIP,\n\t\tEDNSCSEnabled:            &enableEDNSClientSubnet,\n\t\tEDNSCSUseCustom:          &useCustom,\n\t\tDNSSECEnabled:            &enableDNSSEC,\n\t\tDisableIPv6:              &aaaaDisabled,\n\t\tBlockedResponseTTL:       &blockedResponseTTL,\n\t\tCacheEnabled:             &cacheEnabled,\n\t\tCacheSize:                &cacheSize,\n\t\tCacheMinTTL:              &cacheMinTTL,\n\t\tCacheMaxTTL:              &cacheMaxTTL,\n\t\tCacheOptimistic:          &cacheOptimistic,\n\t\tUpstreamMode:             &upstreamMode,\n\t\tResolveClients:           &resolveClients,\n\t\tUsePrivateRDNS:           &usePrivateRDNS,\n\t\tLocalPTRUpstreams:        &localPTRUpstreams,\n\t\tDefaultLocalPTRUpstreams: defPTRUps,\n\t\tDisabledUntil:            protectionDisabledUntil,\n\t}\n}\n\n// defaultLocalPTRUpstreams returns the list of default local PTR resolvers\n// filtered of AdGuard Home's own DNS server addresses.  It may appear empty.\nfunc (s *Server) defaultLocalPTRUpstreams(ctx context.Context) (ups []string, err error) {\n\tmatcher, err := s.conf.ourAddrsSet(ctx, s.logger)\n\tif err != nil {\n\t\t// Don't wrap the error because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tsysResolvers := slices.DeleteFunc(slices.Clone(s.sysResolvers.Addrs()), matcher.Has)\n\tups = make([]string, 0, len(sysResolvers))\n\tfor _, r := range sysResolvers {\n\t\tups = append(ups, r.String())\n\t}\n\n\treturn ups, nil\n}\n\n// handleGetConfig handles requests to the GET /control/dns_info endpoint.\nfunc (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tresp := s.getDNSConfig(ctx)\n\taghhttp.WriteJSONResponseOK(ctx, s.logger, w, r, resp)\n}\n\n// checkBlockingMode returns an error if blocking mode is invalid.\nfunc (req *jsonDNSConfig) checkBlockingMode() (err error) {\n\tif req.BlockingMode == nil {\n\t\treturn nil\n\t}\n\n\treturn validateBlockingMode(*req.BlockingMode, req.BlockingIPv4, req.BlockingIPv6)\n}\n\n// checkUpstreamMode returns an error if the upstream mode is invalid.\nfunc (req *jsonDNSConfig) checkUpstreamMode() (err error) {\n\tif req.UpstreamMode == nil {\n\t\treturn nil\n\t}\n\n\tswitch um := *req.UpstreamMode; um {\n\tcase\n\t\tjsonUpstreamModeEmpty,\n\t\tjsonUpstreamModeLoadBalance,\n\t\tjsonUpstreamModeParallel,\n\t\tjsonUpstreamModeFastestAddr:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"upstream_mode: incorrect value %q\", um)\n\t}\n}\n\n// validate returns an error if any field of req is invalid.  l, ownAddrs,\n// sysResolvers and privateNets must not be nil.\n//\n// TODO(s.chzhen):  Parse, don't validate.\nfunc (req *jsonDNSConfig) validate(\n\tctx context.Context,\n\tl *slog.Logger,\n\townAddrs addrPortSet,\n\tsysResolvers SystemResolvers,\n\tprivateNets netutil.SubnetSet,\n\tcurCacheSize uint32,\n) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"validating dns config: %w\") }()\n\n\terr = req.validateUpstreamDNSServers(ctx, l, ownAddrs, sysResolvers, privateNets)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = req.checkRatelimitSubnetMaskLen()\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = req.checkBlockingMode()\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = req.checkUpstreamMode()\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = req.validateCacheSettings(curCacheSize)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = req.checkUpstreamTimeout()\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// checkBootstrap returns an error if any bootstrap address is invalid.\nfunc (req *jsonDNSConfig) checkBootstrap() (err error) {\n\tif req.Bootstraps == nil {\n\t\treturn nil\n\t}\n\n\tvar b string\n\tdefer func() { err = errors.Annotate(err, \"checking bootstrap %s: %w\", b) }()\n\n\tfor _, b = range *req.Bootstraps {\n\t\tif b == \"\" {\n\t\t\treturn errors.Error(\"empty\")\n\t\t}\n\n\t\tvar resolver *upstream.UpstreamResolver\n\t\tif resolver, err = upstream.NewUpstreamResolver(b, nil); err != nil {\n\t\t\t// Don't wrap the error because it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\n\t\tif err = resolver.Close(); err != nil {\n\t\t\treturn fmt.Errorf(\"closing %s: %w\", b, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// containsPrivateRDNS returns true if req contains private RDNS settings and\n// should be validated.\nfunc (req *jsonDNSConfig) containsPrivateRDNS() (ok bool) {\n\treturn (req.UsePrivateRDNS != nil && *req.UsePrivateRDNS) ||\n\t\t(req.LocalPTRUpstreams != nil && len(*req.LocalPTRUpstreams) > 0)\n}\n\n// checkPrivateRDNS returns an error if the configuration of the private RDNS is\n// not valid.  l must not be nil.\nfunc (req *jsonDNSConfig) checkPrivateRDNS(\n\tctx context.Context,\n\tl *slog.Logger,\n\townAddrs addrPortSet,\n\tsysResolvers SystemResolvers,\n\tprivateNets netutil.SubnetSet,\n) (err error) {\n\tif !req.containsPrivateRDNS() {\n\t\treturn nil\n\t}\n\n\taddrs := cmp.Or(req.LocalPTRUpstreams, &[]string{})\n\n\tuc, err := newPrivateConfig(\n\t\tctx,\n\t\tl,\n\t\t*addrs,\n\t\townAddrs,\n\t\tsysResolvers,\n\t\tprivateNets,\n\t\t&upstream.Options{\n\t\t\tLogger: slogutil.NewDiscardLogger(),\n\t\t})\n\terr = errors.WithDeferred(err, uc.Close())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"private upstream servers: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// validateUpstreamDNSServers returns an error if any field of req is invalid.\n// l, ownAddrs, sysResolvers and privateNets must not be nil.\nfunc (req *jsonDNSConfig) validateUpstreamDNSServers(\n\tctx context.Context,\n\tl *slog.Logger,\n\townAddrs addrPortSet,\n\tsysResolvers SystemResolvers,\n\tprivateNets netutil.SubnetSet,\n) (err error) {\n\tvar uc *proxy.UpstreamConfig\n\topts := &upstream.Options{\n\t\tLogger: slogutil.NewDiscardLogger(),\n\t}\n\n\tif req.Upstreams != nil {\n\t\tuc, err = proxy.ParseUpstreamsConfig(*req.Upstreams, opts)\n\t\terr = errors.WithDeferred(err, uc.Close())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"upstream servers: %w\", err)\n\t\t}\n\t}\n\n\terr = req.checkPrivateRDNS(ctx, l, ownAddrs, sysResolvers, privateNets)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = req.checkBootstrap()\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif req.Fallbacks != nil {\n\t\tuc, err = proxy.ParseUpstreamsConfig(*req.Fallbacks, opts)\n\t\terr = errors.WithDeferred(err, uc.Close())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"fallback servers: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// validateCacheSettings returns an error if the cache configuration is invalid.\nfunc (req *jsonDNSConfig) validateCacheSettings(curCacheSize uint32) (err error) {\n\terr = req.validateCacheSize(curCacheSize)\n\tif err != nil {\n\t\t// Don't wrap the error because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif req.CacheMinTTL == nil && req.CacheMaxTTL == nil {\n\t\treturn nil\n\t}\n\n\tvar minTTL, maxTTL uint32\n\tif req.CacheMinTTL != nil {\n\t\tminTTL = *req.CacheMinTTL\n\t}\n\n\tif req.CacheMaxTTL != nil {\n\t\tmaxTTL = *req.CacheMaxTTL\n\t}\n\n\treturn validateCacheTTL(minTTL, maxTTL)\n}\n\n// validateCacheSize returns an error if the cache size configuration is\n// invalid.  It also explicitly sets CacheEnabled to support legacy behavior.\nfunc (req *jsonDNSConfig) validateCacheSize(curCacheSize uint32) (err error) {\n\tif req.CacheEnabled != nil && *req.CacheEnabled {\n\t\tsize := curCacheSize\n\t\tif req.CacheSize != nil {\n\t\t\tsize = *req.CacheSize\n\t\t}\n\n\t\tif size == 0 {\n\t\t\treturn errors.Error(\"cache_size must be greater than zero when cache_enabled is true\")\n\t\t}\n\t}\n\n\tif req.CacheEnabled == nil && req.CacheSize != nil {\n\t\tisEnabled := *req.CacheSize > 0\n\t\treq.CacheEnabled = &isEnabled\n\t}\n\n\treturn nil\n}\n\n// checkRatelimitSubnetMaskLen returns an error if the length of the subnet mask\n// for IPv4 or IPv6 addresses is invalid.\nfunc (req *jsonDNSConfig) checkRatelimitSubnetMaskLen() (err error) {\n\terr = checkInclusion(req.RatelimitSubnetLenIPv4, 0, netutil.IPv4BitLen)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ratelimit_subnet_len_ipv4 is invalid: %w\", err)\n\t}\n\n\terr = checkInclusion(req.RatelimitSubnetLenIPv6, 0, netutil.IPv6BitLen)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ratelimit_subnet_len_ipv6 is invalid: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// checkUpstreamTimeout returns an error if the configuration of the upstream\n// timeout is invalid.\nfunc (req *jsonDNSConfig) checkUpstreamTimeout() (err error) {\n\tif req.UpstreamTimeout == nil {\n\t\treturn nil\n\t}\n\n\treturn validate.NoLessThan(\"upstream_timeout\", *req.UpstreamTimeout, 1)\n}\n\n// checkInclusion returns an error if a ptr is not nil and points to value,\n// that not in the inclusive range between minN and maxN.\nfunc checkInclusion(ptr *uint, minN, maxN uint) (err error) {\n\tif ptr == nil {\n\t\treturn nil\n\t}\n\n\tn := *ptr\n\tswitch {\n\tcase n < minN:\n\t\treturn fmt.Errorf(\"value %d less than min %d\", n, minN)\n\tcase n > maxN:\n\t\treturn fmt.Errorf(\"value %d greater than max %d\", n, maxN)\n\t}\n\n\treturn nil\n}\n\n// handleSetConfig handles requests to the POST /control/dns_config endpoint.\nfunc (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.logger\n\n\treq := &jsonDNSConfig{}\n\terr := json.NewDecoder(r.Body).Decode(req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"decoding request: %s\", err)\n\n\t\treturn\n\t}\n\n\t// TODO(e.burkov):  Consider prebuilding this set on startup.\n\tourAddrs, err := s.conf.ourAddrsSet(ctx, s.logger)\n\tif err != nil {\n\t\t// TODO(e.burkov):  Put into openapi.\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"getting our addresses: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\terr = req.validate(ctx, s.logger, ourAddrs, s.sysResolvers, s.privateNets, s.conf.CacheSize)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\trestart := s.setConfig(req)\n\ts.conf.ConfModifier.Apply(ctx)\n\n\tif restart {\n\t\terr = s.Reconfigure(ctx, nil)\n\t\tif err != nil {\n\t\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"%s\", err)\n\t\t}\n\t}\n}\n\n// setConfig sets the server parameters.  shouldRestart is true if the server\n// should be restarted to apply changes.\nfunc (s *Server) setConfig(dc *jsonDNSConfig) (shouldRestart bool) {\n\ts.serverLock.Lock()\n\tdefer s.serverLock.Unlock()\n\n\tif dc.BlockingMode != nil {\n\t\ts.dnsFilter.SetBlockingMode(*dc.BlockingMode, dc.BlockingIPv4, dc.BlockingIPv6)\n\t}\n\n\tif dc.BlockedResponseTTL != nil {\n\t\ts.dnsFilter.SetBlockedResponseTTL(*dc.BlockedResponseTTL)\n\t}\n\n\tif dc.ProtectionEnabled != nil {\n\t\ts.dnsFilter.SetProtectionEnabled(*dc.ProtectionEnabled)\n\t}\n\n\tif dc.UpstreamMode != nil {\n\t\ts.conf.UpstreamMode = mustParseUpstreamMode(*dc.UpstreamMode)\n\t}\n\n\tif dc.EDNSCSUseCustom != nil && *dc.EDNSCSUseCustom {\n\t\ts.conf.EDNSClientSubnet.CustomIP = dc.EDNSCSCustomIP\n\t}\n\n\tsetIfNotNil(&s.conf.EnableDNSSEC, dc.DNSSECEnabled)\n\tsetIfNotNil(&s.conf.AAAADisabled, dc.DisableIPv6)\n\n\treturn s.setConfigRestartable(dc)\n}\n\n// mustParseUpstreamMode returns an upstream mode parsed from jsonUpstreamMode.\n// Panics in case of invalid value.\nfunc mustParseUpstreamMode(mode jsonUpstreamMode) (um UpstreamMode) {\n\tswitch mode {\n\tcase jsonUpstreamModeEmpty, jsonUpstreamModeLoadBalance:\n\t\treturn UpstreamModeLoadBalance\n\tcase jsonUpstreamModeParallel:\n\t\treturn UpstreamModeParallel\n\tcase jsonUpstreamModeFastestAddr:\n\t\treturn UpstreamModeFastestAddr\n\tdefault:\n\t\t// Should never happen, since the value should be validated.\n\t\tpanic(fmt.Errorf(\"unexpected upstream mode: %q\", mode))\n\t}\n}\n\n// setIfNotNil sets the value pointed at by currentPtr to the value pointed at\n// by newPtr if newPtr is not nil.  currentPtr must not be nil.\nfunc setIfNotNil[T any](currentPtr, newPtr *T) (hasSet bool) {\n\tif newPtr == nil {\n\t\treturn false\n\t}\n\n\t*currentPtr = *newPtr\n\n\treturn true\n}\n\n// setConfigRestartable sets the parameters which trigger a restart.\n// shouldRestart is true if the server should be restarted to apply changes.\n// s.serverLock is expected to be locked.\n//\n// TODO(a.garipov): Some of these could probably be updated without a restart.\n// Inspect and consider refactoring.\nfunc (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {\n\tfor _, hasSet := range []bool{\n\t\tsetIfNotNil(&s.conf.UpstreamDNS, dc.Upstreams),\n\t\tsetIfNotNil(&s.conf.LocalPTRResolvers, dc.LocalPTRUpstreams),\n\t\tsetIfNotNil(&s.conf.UpstreamDNSFileName, dc.UpstreamsFile),\n\t\tsetIfNotNil(&s.conf.BootstrapDNS, dc.Bootstraps),\n\t\tsetIfNotNil(&s.conf.FallbackDNS, dc.Fallbacks),\n\t\tsetIfNotNil(&s.conf.EDNSClientSubnet.Enabled, dc.EDNSCSEnabled),\n\t\tsetIfNotNil(&s.conf.EDNSClientSubnet.UseCustom, dc.EDNSCSUseCustom),\n\t\tsetIfNotNil(&s.conf.CacheEnabled, dc.CacheEnabled),\n\t\tsetIfNotNil(&s.conf.CacheSize, dc.CacheSize),\n\t\tsetIfNotNil(&s.conf.CacheMinTTL, dc.CacheMinTTL),\n\t\tsetIfNotNil(&s.conf.CacheMaxTTL, dc.CacheMaxTTL),\n\t\tsetIfNotNil(&s.conf.CacheOptimistic, dc.CacheOptimistic),\n\t\tsetIfNotNil(&s.conf.AddrProcConf.UseRDNS, dc.ResolveClients),\n\t\tsetIfNotNil(&s.conf.UsePrivateRDNS, dc.UsePrivateRDNS),\n\t\tsetIfNotNil(&s.conf.RatelimitSubnetLenIPv4, dc.RatelimitSubnetLenIPv4),\n\t\tsetIfNotNil(&s.conf.RatelimitSubnetLenIPv6, dc.RatelimitSubnetLenIPv6),\n\t\tsetIfNotNil(&s.conf.RatelimitWhitelist, dc.RatelimitWhitelist),\n\t} {\n\t\tshouldRestart = shouldRestart || hasSet\n\t\tif shouldRestart {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif dc.Ratelimit != nil && s.conf.Ratelimit != *dc.Ratelimit {\n\t\ts.conf.Ratelimit = *dc.Ratelimit\n\t\tshouldRestart = true\n\t}\n\n\tif dc.UpstreamTimeout != nil {\n\t\tut := time.Duration(*dc.UpstreamTimeout) * time.Second\n\t\tif s.conf.UpstreamTimeout != ut {\n\t\t\ts.conf.UpstreamTimeout = ut\n\t\t\tshouldRestart = true\n\t\t}\n\t}\n\n\treturn shouldRestart\n}\n\n// upstreamJSON is a request body for handleTestUpstreamDNS endpoint.\ntype upstreamJSON struct {\n\tUpstreams        []string `json:\"upstream_dns\"`\n\tBootstrapDNS     []string `json:\"bootstrap_dns\"`\n\tFallbackDNS      []string `json:\"fallback_dns\"`\n\tPrivateUpstreams []string `json:\"private_upstream\"`\n}\n\n// closeBoots closes all the provided bootstrap servers and logs errors if any.\n// l must not be nil.\nfunc closeBoots(ctx context.Context, l *slog.Logger, boots []*upstream.UpstreamResolver) {\n\tfor _, c := range boots {\n\t\tlogCloserErr(ctx, c, \"closing bootstrap\", l.With(\"address\", c.Address()))\n\t}\n}\n\n// handleTestUpstreamDNS handles requests to the POST /control/test_upstream_dns\n// endpoint.\nfunc (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.logger\n\n\treq := &upstreamJSON{}\n\terr := json.NewDecoder(r.Body).Decode(req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Failed to read request body: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\treq.BootstrapDNS = stringutil.FilterOut(req.BootstrapDNS, aghnet.IsCommentOrEmpty)\n\n\topts := &upstream.Options{\n\t\tLogger:     aghslog.NewForUpstream(s.baseLogger, aghslog.UpstreamTypeTest),\n\t\tTimeout:    s.conf.UpstreamTimeout,\n\t\tPreferIPv6: s.conf.BootstrapPreferIPv6,\n\t}\n\n\tvar boots []*upstream.UpstreamResolver\n\topts.Bootstrap, boots, err = newBootstrap(req.BootstrapDNS, s.etcHosts, opts)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Failed to parse bootstrap servers: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\tdefer closeBoots(ctx, s.logger, boots)\n\n\tcv := newUpstreamConfigValidator(ctx, req.Upstreams, req.FallbackDNS, req.PrivateUpstreams, opts)\n\tcv.check(ctx, s.logger)\n\tcv.close()\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, cv.status(ctx, l))\n}\n\n// handleCacheClear is the handler for the POST /control/cache_clear HTTP API.\nfunc (s *Server) handleCacheClear(w http.ResponseWriter, _ *http.Request) {\n\ts.dnsProxy.ClearCache()\n\ts.conf.ClientsContainer.ClearUpstreamCache()\n\n\t_, _ = io.WriteString(w, \"OK\")\n}\n\n// protectionJSON is an object for /control/protection endpoint.\ntype protectionJSON struct {\n\tEnabled  bool `json:\"enabled\"`\n\tDuration uint `json:\"duration\"`\n}\n\n// handleSetProtection is a handler for the POST /control/protection HTTP API.\nfunc (s *Server) handleSetProtection(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.logger\n\n\tprotectionReq := &protectionJSON{}\n\terr := json.NewDecoder(r.Body).Decode(protectionReq)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"reading req: %s\", err)\n\n\t\treturn\n\t}\n\n\tvar disabledUntil *time.Time\n\tif protectionReq.Duration > 0 {\n\t\tif protectionReq.Enabled {\n\t\t\taghhttp.ErrorAndLog(\n\t\t\t\tctx,\n\t\t\t\tl,\n\t\t\t\tr,\n\t\t\t\tw,\n\t\t\t\thttp.StatusBadRequest,\n\t\t\t\t\"Setting a duration is only allowed with protection disabling\",\n\t\t\t)\n\n\t\t\treturn\n\t\t}\n\n\t\tcalcTime := time.Now().Add(time.Duration(protectionReq.Duration) * time.Millisecond)\n\t\tdisabledUntil = &calcTime\n\t}\n\n\tfunc() {\n\t\ts.serverLock.Lock()\n\t\tdefer s.serverLock.Unlock()\n\n\t\ts.dnsFilter.SetProtectionStatus(protectionReq.Enabled, disabledUntil)\n\t}()\n\n\ts.conf.ConfModifier.Apply(ctx)\n\n\taghhttp.OK(ctx, l, w)\n}\n\n// handleDoH is the DNS-over-HTTPs handler.\n//\n// Control flow:\n//\n//\tHTTP server\n//\t-> dnsforward.handleDoH\n//\t-> dnsforward.ServeHTTP\n//\t-> proxy.ServeHTTP\n//\t-> proxy.handleDNSRequest\n//\t-> dnsforward.ServeDNS\nfunc (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := s.logger\n\n\tif !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusNotFound, \"Not Found\")\n\n\t\treturn\n\t}\n\n\tif !s.IsRunning() {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"dns server is not running\",\n\t\t)\n\n\t\treturn\n\t}\n\n\ts.ServeHTTP(w, r)\n}\n\nfunc (s *Server) registerHandlers() {\n\tif webRegistered || s.conf.HTTPReg == nil {\n\t\treturn\n\t}\n\n\ts.conf.HTTPReg.Register(http.MethodGet, \"/control/dns_info\", s.handleGetConfig)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/dns_config\", s.handleSetConfig)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/test_upstream_dns\", s.handleTestUpstreamDNS)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/protection\", s.handleSetProtection)\n\n\ts.conf.HTTPReg.Register(http.MethodGet, \"/control/access/list\", s.handleAccessList)\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/access/set\", s.handleAccessSet)\n\n\ts.conf.HTTPReg.Register(http.MethodPost, \"/control/cache_clear\", s.handleCacheClear)\n\n\t// Register both versions, with and without the trailing slash, to\n\t// prevent a 301 Moved Permanently redirect when clients request the\n\t// path without the trailing slash.  Those redirects break some clients.\n\t//\n\t// See go doc net/http.ServeMux.\n\t//\n\t// See also https://github.com/AdguardTeam/AdGuardHome/issues/2628.\n\ts.conf.HTTPReg.Register(\"\", \"/dns-query\", s.handleDoH)\n\ts.conf.HTTPReg.Register(\"\", \"/dns-query/\", s.handleDoH)\n\n\twebRegistered = true\n}\n"
  },
  {
    "path": "internal/dnsforward/http_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"testing/fstest\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TODO(e.burkov):  Use the better approach to testdata with a separate\n// directory for each test, and a separate file for each subtest.  See the\n// [configmigrate] package.\n\n// emptySysResolvers is an empty [SystemResolvers] implementation that always\n// returns nil.\ntype emptySysResolvers struct{}\n\n// Addrs implements the aghnet.SystemResolvers interface for emptySysResolvers.\nfunc (emptySysResolvers) Addrs() (addrs []netip.AddrPort) {\n\treturn nil\n}\n\n// loadTestData loads the test data from the file with the given name into\n// cases.\nfunc loadTestData(tb testing.TB, casesFileName string, cases any) {\n\ttb.Helper()\n\n\tvar f *os.File\n\tf, err := os.Open(filepath.Join(\"testdata\", casesFileName))\n\trequire.NoError(tb, err)\n\ttestutil.CleanupAndRequireSuccess(tb, f.Close)\n\n\terr = json.NewDecoder(f).Decode(cases)\n\trequire.NoError(tb, err)\n}\n\nconst (\n\tjsonExt = \".json\"\n\n\t// testBlockedRespTTL is the TTL for blocked responses to use in tests.\n\ttestBlockedRespTTL = 10\n)\n\nfunc TestDNSForwardHTTP_handleGetConfig(t *testing.T) {\n\tfilterConf := &filtering.Config{\n\t\tProtectionEnabled:     true,\n\t\tBlockingMode:          filtering.BlockingModeDefault,\n\t\tBlockedResponseTTL:    testBlockedRespTTL,\n\t\tSafeBrowsingEnabled:   true,\n\t\tSafeBrowsingCacheSize: 1000,\n\t\tSafeSearchConf:        filtering.SafeSearchConfig{Enabled: true},\n\t\tSafeSearchCacheSize:   1000,\n\t\tParentalCacheSize:     1000,\n\t\tCacheTime:             30,\n\t}\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{},\n\t\tTCPListenAddrs: []*net.TCPAddr{},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamDNS:            []string{\"8.8.8.8:53\", \"8.8.4.4:53\"},\n\t\t\tFallbackDNS:            []string{\"9.9.9.10\"},\n\t\t\tRatelimitSubnetLenIPv4: 24,\n\t\t\tRatelimitSubnetLenIPv6: 56,\n\t\t\tUpstreamMode:           UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet:       &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer:       EmptyClientsContainer{},\n\t\t},\n\t\tConfModifier:  agh.EmptyConfigModifier{},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, filterConf, forwardConf)\n\ts.sysResolvers = &emptySysResolvers{}\n\n\trequire.NoError(t, s.Start(testutil.ContextWithTimeout(t, testTimeout)))\n\ttestutil.CleanupAndRequireSuccess(t, func() (err error) {\n\t\treturn s.Stop(testutil.ContextWithTimeout(t, testTimeout))\n\t})\n\n\tdefaultConf := s.conf\n\n\tw := httptest.NewRecorder()\n\n\ttestCases := []struct {\n\t\tconf func() ServerConfig\n\t\tname string\n\t}{{\n\t\tconf: func() ServerConfig {\n\t\t\treturn defaultConf\n\t\t},\n\t\tname: \"all_right\",\n\t}, {\n\t\tconf: func() ServerConfig {\n\t\t\tconf := defaultConf\n\t\t\tconf.UpstreamMode = UpstreamModeFastestAddr\n\n\t\t\treturn conf\n\t\t},\n\t\tname: \"fastest_addr\",\n\t}, {\n\t\tconf: func() ServerConfig {\n\t\t\tconf := defaultConf\n\t\t\tconf.UpstreamMode = UpstreamModeParallel\n\n\t\t\treturn conf\n\t\t},\n\t\tname: \"parallel\",\n\t}}\n\n\tvar data map[string]json.RawMessage\n\tloadTestData(t, t.Name()+jsonExt, &data)\n\n\tfor _, tc := range testCases {\n\t\tcaseWant, ok := data[tc.name]\n\t\trequire.True(t, ok)\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Cleanup(w.Body.Reset)\n\n\t\t\ts.conf = tc.conf()\n\t\t\ts.handleGetConfig(w, httptest.NewRequest(http.MethodGet, \"/\", nil))\n\n\t\t\tcType := w.Header().Get(httphdr.ContentType)\n\t\t\tassert.Equal(t, aghhttp.HdrValApplicationJSON, cType)\n\t\t\tassert.JSONEq(t, string(caseWant), w.Body.String())\n\t\t})\n\t}\n}\n\nfunc TestDNSForwardHTTP_handleSetConfig(t *testing.T) {\n\tfilterConf := &filtering.Config{\n\t\tProtectionEnabled:     true,\n\t\tBlockingMode:          filtering.BlockingModeDefault,\n\t\tBlockedResponseTTL:    testBlockedRespTTL,\n\t\tSafeBrowsingEnabled:   true,\n\t\tSafeBrowsingCacheSize: 1000,\n\t\tSafeSearchConf:        filtering.SafeSearchConfig{Enabled: true},\n\t\tSafeSearchCacheSize:   1000,\n\t\tParentalCacheSize:     1000,\n\t\tCacheTime:             30,\n\t}\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{},\n\t\tTCPListenAddrs: []*net.TCPAddr{},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamDNS:            []string{\"8.8.8.8:53\", \"8.8.4.4:53\"},\n\t\t\tRatelimitSubnetLenIPv4: 24,\n\t\t\tRatelimitSubnetLenIPv6: 56,\n\t\t\tUpstreamMode:           UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet:       &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer:       EmptyClientsContainer{},\n\t\t},\n\t\tConfModifier:  agh.EmptyConfigModifier{},\n\t\tServePlainDNS: true,\n\t}\n\ts := createTestServer(t, filterConf, forwardConf)\n\ts.sysResolvers = &emptySysResolvers{}\n\n\tdefaultConf := s.conf\n\n\terr := s.Start(testutil.ContextWithTimeout(t, testTimeout))\n\tassert.NoError(t, err)\n\ttestutil.CleanupAndRequireSuccess(t, func() (err error) {\n\t\treturn s.Stop(testutil.ContextWithTimeout(t, testTimeout))\n\t})\n\n\tw := httptest.NewRecorder()\n\n\ttestCases := []struct {\n\t\tname    string\n\t\twantSet string\n\t}{{\n\t\tname:    \"upstream_dns\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"bootstraps\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"blocking_mode_good\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname: \"blocking_mode_bad\",\n\t\twantSet: \"validating dns config: \" +\n\t\t\t\"blocking_ipv4 must be valid ipv4 on custom_ip blocking_mode\",\n\t}, {\n\t\tname:    \"ratelimit\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"ratelimit_subnet_len\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"ratelimit_whitelist_not_ip\",\n\t\twantSet: `decoding request: ParseAddr(\"not.ip\"): unexpected character (at \"not.ip\")`,\n\t}, {\n\t\tname:    \"edns_cs_enabled\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"edns_cs_use_custom\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"edns_cs_use_custom_bad_ip\",\n\t\twantSet: \"decoding request: ParseAddr(\\\"bad.ip\\\"): unexpected character (at \\\"bad.ip\\\")\",\n\t}, {\n\t\tname:    \"dnssec_enabled\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"cache_size\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"cache_enabled\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"upstream_mode_parallel\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"upstream_mode_fastest_addr\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname: \"upstream_dns_bad\",\n\t\twantSet: `validating dns config: upstream servers: parsing error at index 0: ` +\n\t\t\t`cannot prepare the upstream: invalid address !!!: bad domain name \"!!!\": ` +\n\t\t\t`bad top-level domain name label \"!!!\": bad top-level domain name label rune '!'`,\n\t}, {\n\t\tname: \"bootstraps_bad\",\n\t\twantSet: `validating dns config: checking bootstrap a: not a bootstrap: ParseAddr(\"a\"): ` +\n\t\t\t`unable to parse IP`,\n\t}, {\n\t\tname:    \"cache_bad_ttl\",\n\t\twantSet: `validating dns config: cache_ttl_min must be less than or equal to cache_ttl_max`,\n\t}, {\n\t\tname:    \"upstream_mode_bad\",\n\t\twantSet: `validating dns config: upstream_mode: incorrect value \"somethingelse\"`,\n\t}, {\n\t\tname:    \"local_ptr_upstreams_good\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname: \"local_ptr_upstreams_bad\",\n\t\twantSet: `validating dns config: private upstream servers: ` +\n\t\t\t`bad arpa domain name \"non.arpa\": not a reversed ip network`,\n\t}, {\n\t\tname:    \"local_ptr_upstreams_null\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"fallbacks\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"blocked_response_ttl\",\n\t\twantSet: \"\",\n\t}, {\n\t\tname:    \"multiple_domain_specific_upstreams\",\n\t\twantSet: \"\",\n\t}}\n\n\tvar data map[string]struct {\n\t\tReq  json.RawMessage `json:\"req\"`\n\t\tWant json.RawMessage `json:\"want\"`\n\t}\n\n\ttestData := t.Name() + jsonExt\n\tloadTestData(t, testData, &data)\n\n\tfor _, tc := range testCases {\n\t\t// NOTE:  Do not use require.Contains, because the size of the data\n\t\t// prevents it from printing a meaningful error message.\n\t\tcaseData, ok := data[tc.name]\n\t\trequire.Truef(t, ok, \"%q does not contain test data for test case %s\", testData, tc.name)\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Cleanup(func() {\n\t\t\t\ts.dnsFilter.SetBlockingMode(\n\t\t\t\t\tfiltering.BlockingModeDefault,\n\t\t\t\t\tnetip.Addr{},\n\t\t\t\t\tnetip.Addr{},\n\t\t\t\t)\n\t\t\t\ts.conf = defaultConf\n\t\t\t\ts.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{}\n\t\t\t\ts.dnsFilter.SetBlockedResponseTTL(testBlockedRespTTL)\n\t\t\t})\n\n\t\t\trBody := io.NopCloser(bytes.NewReader(caseData.Req))\n\t\t\tvar r *http.Request\n\t\t\tr, err = http.NewRequest(http.MethodPost, \"http://example.com\", rBody)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.handleSetConfig(w, r)\n\t\t\tassert.Equal(t, tc.wantSet, strings.TrimSuffix(w.Body.String(), \"\\n\"))\n\t\t\tw.Body.Reset()\n\n\t\t\ts.handleGetConfig(w, httptest.NewRequest(http.MethodGet, \"/\", nil))\n\t\t\tassert.JSONEq(t, string(caseData.Want), w.Body.String())\n\t\t\tw.Body.Reset()\n\t\t})\n\t}\n}\n\n// newLocalUpstreamListener creates a local upstream listener and returns its\n// address.  The listener is started in a separate goroutine and stopped when\n// the tb's test is finished.\nfunc newLocalUpstreamListener(tb testing.TB, port uint16, h dns.Handler) (real netip.AddrPort) {\n\ttb.Helper()\n\n\tstartCh := make(chan struct{})\n\tupsSrv := &dns.Server{\n\t\tAddr:              netip.AddrPortFrom(netutil.IPv4Localhost(), port).String(),\n\t\tNet:               \"tcp\",\n\t\tHandler:           h,\n\t\tNotifyStartedFunc: func() { close(startCh) },\n\t}\n\tgo func() {\n\t\terr := upsSrv.ListenAndServe()\n\t\trequire.NoError(testutil.PanicT{}, err)\n\t}()\n\n\t<-startCh\n\ttestutil.CleanupAndRequireSuccess(tb, upsSrv.Shutdown)\n\n\treturn testutil.RequireTypeAssert[*net.TCPAddr](tb, upsSrv.Listener.Addr()).AddrPort()\n}\n\nfunc TestServer_HandleTestUpstreamDNS(t *testing.T) {\n\thdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {\n\t\terr := w.WriteMsg(new(dns.Msg).SetReply(m))\n\t\trequire.NoError(testutil.PanicT{}, err)\n\t})\n\n\tups := (&url.URL{\n\t\tScheme: \"tcp\",\n\t\tHost:   newLocalUpstreamListener(t, 0, hdlr).String(),\n\t}).String()\n\n\tconst (\n\t\tupsTimeout = 100 * time.Millisecond\n\n\t\thostsFileName = \"hosts\"\n\t\tupstreamHost  = \"custom.localhost\"\n\t)\n\n\thostsListener := newLocalUpstreamListener(t, 0, hdlr)\n\thostsUps := (&url.URL{\n\t\tScheme: \"tcp\",\n\t\tHost:   netutil.JoinHostPort(upstreamHost, hostsListener.Port()),\n\t}).String()\n\n\twatcher := aghtest.NewFSWatcher()\n\twatcher.OnEvents = func() (e <-chan struct{}) { return nil }\n\twatcher.OnAdd = func(_ string) (err error) { return nil }\n\twatcher.OnShutdown = func(_ context.Context) (err error) { return nil }\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\thc, err := aghnet.NewHostsContainer(\n\t\tctx,\n\t\ttestLogger,\n\t\tfstest.MapFS{\n\t\t\thostsFileName: &fstest.MapFile{\n\t\t\t\tData: []byte(hostsListener.Addr().String() + \" \" + upstreamHost),\n\t\t\t},\n\t\t},\n\t\twatcher,\n\t\thostsFileName,\n\t)\n\trequire.NoError(t, err)\n\n\tsrv := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\tEtcHosts:     hc,\n\t}, ServerConfig{\n\t\tUDPListenAddrs:  []*net.UDPAddr{{}},\n\t\tTCPListenAddrs:  []*net.TCPAddr{{}},\n\t\tUpstreamTimeout: upsTimeout,\n\t\tTLSConf:         &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t})\n\tsrv.etcHosts = upstream.NewHostsResolver(hc)\n\tstartDeferStop(t, srv)\n\n\ttestCases := []struct {\n\t\tbody     map[string]any\n\t\twantResp map[string]any\n\t\tname     string\n\t}{{\n\t\tbody: map[string]any{\n\t\t\t\"upstream_dns\": []string{hostsUps},\n\t\t},\n\t\twantResp: map[string]any{\n\t\t\thostsUps: \"OK\",\n\t\t},\n\t\tname: \"etc_hosts\",\n\t}, {\n\t\tbody: map[string]any{\n\t\t\t\"upstream_dns\": []string{ups, \"#this.is.comment\"},\n\t\t},\n\t\twantResp: map[string]any{\n\t\t\tups: \"OK\",\n\t\t},\n\t\tname: \"comment_mix\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar reqBody []byte\n\t\t\treqBody, err = json.Marshal(tc.body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tvar r *http.Request\n\t\t\tr, err = http.NewRequest(http.MethodPost, \"\", bytes.NewReader(reqBody))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.handleTestUpstreamDNS(w, r)\n\t\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\t\tresp := map[string]any{}\n\t\t\terr = json.NewDecoder(w.Body).Decode(&resp)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.wantResp, resp)\n\t\t})\n\t}\n\n\tt.Run(\"timeout\", func(t *testing.T) {\n\t\tslowHandler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {\n\t\t\ttime.Sleep(upsTimeout * 2)\n\t\t\twriteErr := w.WriteMsg(new(dns.Msg).SetReply(m))\n\t\t\trequire.NoError(testutil.PanicT{}, writeErr)\n\t\t})\n\t\tsleepyUps := (&url.URL{\n\t\t\tScheme: \"tcp\",\n\t\t\tHost:   newLocalUpstreamListener(t, 0, slowHandler).String(),\n\t\t}).String()\n\n\t\treq := map[string]any{\n\t\t\t\"upstream_dns\": []string{sleepyUps},\n\t\t}\n\n\t\tvar reqBody []byte\n\t\treqBody, err = json.Marshal(req)\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\n\t\tvar r *http.Request\n\t\tr, err = http.NewRequest(http.MethodPost, \"\", bytes.NewReader(reqBody))\n\t\trequire.NoError(t, err)\n\n\t\tsrv.handleTestUpstreamDNS(w, r)\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tresp := map[string]any{}\n\t\terr = json.NewDecoder(w.Body).Decode(&resp)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Contains(t, resp, sleepyUps)\n\t\tsleepyRes := testutil.RequireTypeAssert[string](t, resp[sleepyUps])\n\n\t\tassert.True(t, strings.HasSuffix(sleepyRes, \"i/o timeout\"))\n\t})\n}\n"
  },
  {
    "path": "internal/dnsforward/ipset.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/ipset\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// ipsetHandler is the ipset context.  ipsetMgr can be nil.\ntype ipsetHandler struct {\n\tipsetMgr ipset.Manager\n\tlogger   *slog.Logger\n}\n\n// newIpsetHandler returns a new initialized [ipsetHandler].  It is not safe for\n// concurrent use.\nfunc newIpsetHandler(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tipsetList []string,\n) (h *ipsetHandler, err error) {\n\th = &ipsetHandler{\n\t\tlogger: logger,\n\t}\n\tconf := &ipset.Config{\n\t\tLogger: logger,\n\t\tLines:  ipsetList,\n\t}\n\th.ipsetMgr, err = ipset.NewManager(ctx, conf)\n\tif errors.Is(err, os.ErrInvalid) ||\n\t\terrors.Is(err, os.ErrPermission) ||\n\t\terrors.Is(err, errors.ErrUnsupported) {\n\t\t// ipset cannot currently be initialized if the server was installed\n\t\t// from Snap or when the user or the binary doesn't have the required\n\t\t// permissions, or when the kernel doesn't support netfilter.\n\t\t//\n\t\t// Log and go on.\n\t\t//\n\t\t// TODO(a.garipov): The Snap problem can probably be solved if we add\n\t\t// the netlink-connector interface plug.\n\t\tlogger.WarnContext(ctx, \"cannot initialize\", slogutil.KeyError, err)\n\n\t\treturn h, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"initializing ipset: %w\", err)\n\t}\n\n\treturn h, nil\n}\n\n// close closes the Linux Netfilter connections.  close can be called on a nil\n// handler.\nfunc (h *ipsetHandler) close() (err error) {\n\tif h != nil && h.ipsetMgr != nil {\n\t\treturn h.ipsetMgr.Close()\n\t}\n\n\treturn nil\n}\n\n// dctxIsFilled returns true if dctx has enough information to process.\nfunc dctxIsFilled(dctx *dnsContext) (ok bool) {\n\treturn dctx != nil &&\n\t\tdctx.responseFromUpstream &&\n\t\tdctx.proxyCtx != nil &&\n\t\tdctx.proxyCtx.Res != nil &&\n\t\tdctx.proxyCtx.Req != nil &&\n\t\tlen(dctx.proxyCtx.Req.Question) > 0\n}\n\n// skipIpsetProcessing returns true when the ipset processing can be skipped for\n// this request.\nfunc (h *ipsetHandler) skipIpsetProcessing(dctx *dnsContext) (ok bool) {\n\tif h == nil || h.ipsetMgr == nil || !dctxIsFilled(dctx) {\n\t\treturn true\n\t}\n\n\tqtype := dctx.proxyCtx.Req.Question[0].Qtype\n\n\treturn qtype != dns.TypeA && qtype != dns.TypeAAAA && qtype != dns.TypeANY\n}\n\n// ipFromRR returns an IP address from a DNS resource record.\nfunc ipFromRR(rr dns.RR) (ip net.IP) {\n\tswitch a := rr.(type) {\n\tcase *dns.A:\n\t\treturn a.A\n\tcase *dns.AAAA:\n\t\treturn a.AAAA\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// ipsFromAnswer returns IPv4 and IPv6 addresses from a DNS answer.\nfunc ipsFromAnswer(ans []dns.RR) (ip4s, ip6s []net.IP) {\n\tfor _, rr := range ans {\n\t\tip := ipFromRR(rr)\n\t\tif ip == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif ip.To4() == nil {\n\t\t\tip6s = append(ip6s, ip)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tip4s = append(ip4s, ip)\n\t}\n\n\treturn ip4s, ip6s\n}\n\n// process adds the resolved IP addresses to the domain's ipsets, if any.\nfunc (h *ipsetHandler) process(ctx context.Context, dctx *dnsContext) (rc resultCode) {\n\th.logger.DebugContext(ctx, \"started processing\")\n\tdefer h.logger.DebugContext(ctx, \"finished processing\")\n\n\tif h.skipIpsetProcessing(dctx) {\n\t\treturn resultCodeSuccess\n\t}\n\n\treq := dctx.proxyCtx.Req\n\thost := req.Question[0].Name\n\thost = strings.TrimSuffix(host, \".\")\n\thost = strings.ToLower(host)\n\n\tip4s, ip6s := ipsFromAnswer(dctx.proxyCtx.Res.Answer)\n\tn, err := h.ipsetMgr.Add(ctx, host, ip4s, ip6s)\n\tif err != nil {\n\t\t// Consider ipset errors non-critical to the request.\n\t\th.logger.ErrorContext(ctx, \"adding host ips\", slogutil.KeyError, err)\n\n\t\treturn resultCodeSuccess\n\t}\n\n\th.logger.DebugContext(ctx, \"added new ipset entries\", \"num\", n)\n\n\treturn resultCodeSuccess\n}\n"
  },
  {
    "path": "internal/dnsforward/ipset_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// fakeIpsetMgr is a fake aghnet.IpsetManager for tests.\ntype fakeIpsetMgr struct {\n\tip4s []net.IP\n\tip6s []net.IP\n}\n\n// Add implements the aghnet.IpsetManager interface for *fakeIpsetMgr.\nfunc (m *fakeIpsetMgr) Add(_ context.Context, host string, ip4s, ip6s []net.IP) (n int, err error) {\n\tm.ip4s = append(m.ip4s, ip4s...)\n\tm.ip6s = append(m.ip6s, ip6s...)\n\n\treturn len(ip4s) + len(ip6s), nil\n}\n\n// Close implements the aghnet.IpsetManager interface for *fakeIpsetMgr.\nfunc (*fakeIpsetMgr) Close() (err error) {\n\treturn nil\n}\n\nfunc TestIpsetCtx_process(t *testing.T) {\n\tip4 := net.IP{1, 2, 3, 4}\n\tip6 := net.IP{\n\t\t0x12, 0x34, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x56, 0x78,\n\t}\n\n\treq4 := createTestMessageWithType(\"example.com\", dns.TypeA)\n\treq6 := createTestMessageWithType(\"example.com\", dns.TypeAAAA)\n\n\tresp4 := &dns.Msg{\n\t\tAnswer: []dns.RR{&dns.A{\n\t\t\tA: ip4,\n\t\t}},\n\t}\n\tresp6 := &dns.Msg{\n\t\tAnswer: []dns.RR{&dns.AAAA{\n\t\t\tAAAA: ip6,\n\t\t}},\n\t}\n\n\tt.Run(\"nil\", func(t *testing.T) {\n\t\tdctx := &dnsContext{\n\t\t\tproxyCtx: &proxy.DNSContext{},\n\n\t\t\tresponseFromUpstream: true,\n\t\t}\n\n\t\tictx := &ipsetHandler{\n\t\t\tlogger: testLogger,\n\t\t}\n\t\trc := ictx.process(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\t\tassert.Equal(t, resultCodeSuccess, rc)\n\n\t\terr := ictx.close()\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"ipv4\", func(t *testing.T) {\n\t\tdctx := &dnsContext{\n\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\tReq: req4,\n\t\t\t\tRes: resp4,\n\t\t\t},\n\n\t\t\tresponseFromUpstream: true,\n\t\t}\n\n\t\tm := &fakeIpsetMgr{}\n\t\tictx := &ipsetHandler{\n\t\t\tipsetMgr: m,\n\t\t\tlogger:   testLogger,\n\t\t}\n\n\t\trc := ictx.process(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\t\tassert.Equal(t, resultCodeSuccess, rc)\n\t\tassert.Equal(t, []net.IP{ip4}, m.ip4s)\n\t\tassert.Empty(t, m.ip6s)\n\n\t\terr := ictx.close()\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"ipv6\", func(t *testing.T) {\n\t\tdctx := &dnsContext{\n\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\tReq: req6,\n\t\t\t\tRes: resp6,\n\t\t\t},\n\n\t\t\tresponseFromUpstream: true,\n\t\t}\n\n\t\tm := &fakeIpsetMgr{}\n\t\tictx := &ipsetHandler{\n\t\t\tipsetMgr: m,\n\t\t\tlogger:   testLogger,\n\t\t}\n\n\t\trc := ictx.process(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\t\tassert.Equal(t, resultCodeSuccess, rc)\n\t\tassert.Empty(t, m.ip4s)\n\t\tassert.Equal(t, []net.IP{ip6}, m.ip6s)\n\n\t\terr := ictx.close()\n\t\tassert.NoError(t, err)\n\t})\n}\n\nfunc TestIpsetCtx_SkipIpsetProcessing(t *testing.T) {\n\treq4 := createTestMessage(\"example.com\")\n\tresp4 := &dns.Msg{\n\t\tAnswer: []dns.RR{&dns.A{\n\t\t\tA: net.IP{1, 2, 3, 4},\n\t\t}},\n\t}\n\n\tm := &fakeIpsetMgr{}\n\tictx := &ipsetHandler{\n\t\tipsetMgr: m,\n\t\tlogger:   testLogger,\n\t}\n\n\ttestCases := []struct {\n\t\tdctx *dnsContext\n\t\tname string\n\t\twant bool\n\t}{{\n\t\tname: \"basic\",\n\t\twant: false,\n\t\tdctx: &dnsContext{\n\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\tReq: req4,\n\t\t\t\tRes: resp4,\n\t\t\t},\n\n\t\t\tresponseFromUpstream: true,\n\t\t},\n\t}, {\n\t\tname: \"rewrite\",\n\t\twant: true,\n\t\tdctx: &dnsContext{\n\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\tReq: req4,\n\t\t\t\tRes: resp4,\n\t\t\t},\n\n\t\t\tresponseFromUpstream: false,\n\t\t},\n\t}, {\n\t\tname: \"empty_req\",\n\t\twant: true,\n\t\tdctx: &dnsContext{\n\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\tReq: nil,\n\t\t\t\tRes: resp4,\n\t\t\t},\n\n\t\t\tresponseFromUpstream: true,\n\t\t},\n\t}, {\n\t\tname: \"empty_res\",\n\t\twant: true,\n\t\tdctx: &dnsContext{\n\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\tReq: req4,\n\t\t\t\tRes: nil,\n\t\t\t},\n\n\t\t\tresponseFromUpstream: true,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := ictx.skipIpsetProcessing(tc.dctx)\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dnsforward/middleware.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/syncutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// type check\nvar _ proxy.Middleware = (*Server)(nil)\n\n// Wrap implements the [proxy.Middleware] interface for *Server.\n//\n// TODO(d.kolyshev):  Move to a dedicated package.\n// TODO(d.kolyshev):  Use logger from context.\nfunc (s *Server) Wrap(h proxy.Handler) (wrapped proxy.Handler) {\n\tf := func(ctx context.Context, p *proxy.Proxy, pctx *proxy.DNSContext) (err error) {\n\t\tclientID, err := s.clientIDFromDNSContext(ctx, pctx)\n\t\tif err != nil {\n\t\t\ts.logger.WarnContext(ctx, \"resolving client id\", slogutil.KeyError, err)\n\n\t\t\tpctx.Res = s.NewMsgSERVFAIL(pctx.Req)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tblocked, _ := s.IsBlockedClient(pctx.Addr.Addr(), clientID)\n\t\tif blocked {\n\t\t\treturn s.serveBlockedResponse(pctx)\n\t\t}\n\n\t\tblocked = s.isBlockedHost(ctx, pctx.Req.Question)\n\t\tif blocked {\n\t\t\treturn s.serveBlockedResponse(pctx)\n\t\t}\n\n\t\tif clientID != \"\" {\n\t\t\tctx = contextWithClientID(ctx, clientID)\n\t\t}\n\n\t\treturn h.ServeDNS(ctx, p, pctx)\n\t}\n\n\treturn proxy.HandlerFunc(f)\n}\n\n// serveBlockedResponse sets a protocol-appropriate response for a request that\n// was blocked by access settings.  pctx must be filled with the request.\nfunc (s *Server) serveBlockedResponse(pctx *proxy.DNSContext) (err error) {\n\tif pctx.Proto == proxy.ProtoUDP || pctx.Proto == proxy.ProtoDNSCrypt {\n\t\t// Return nil so that dnsproxy drops the connection and thus prevent DNS\n\t\t// amplification attacks.\n\t\treturn proxy.ErrDrop\n\t}\n\n\tpctx.Res = s.makeResponseREFUSED(pctx.Req)\n\n\treturn nil\n}\n\n// isBlockedHost checks if the request is in the access blocklist.\nfunc (s *Server) isBlockedHost(ctx context.Context, question []dns.Question) (blocked bool) {\n\tif len(question) != 1 {\n\t\treturn false\n\t}\n\n\tq := question[0]\n\tqt := q.Qtype\n\thost := aghnet.NormalizeDomain(q.Name)\n\n\tif s.access.isBlockedHost(host, qt) {\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"request is in access blocklist\",\n\t\t\t\"dns_type\", dns.Type(qt),\n\t\t\t\"host\", host,\n\t\t)\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// clientIDFromDNSContext extracts the client's ID from the server name of the\n// client's DoT or DoQ request or the path of the client's DoH.  If the protocol\n// is not one of these, clientID is an empty string and err is nil.\nfunc (s *Server) clientIDFromDNSContext(\n\tctx context.Context,\n\tpctx *proxy.DNSContext,\n) (clientID string, err error) {\n\tproto := pctx.Proto\n\tif proto == proxy.ProtoHTTPS {\n\t\tclientID, err = clientIDFromDNSContextHTTPS(pctx)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"checking url: %w\", err)\n\t\t} else if clientID != \"\" {\n\t\t\treturn clientID, nil\n\t\t}\n\n\t\t// Go on and check the domain name as well.\n\t} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {\n\t\treturn \"\", nil\n\t}\n\n\thostSrvName := s.conf.TLSConf.ServerName\n\tif hostSrvName == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\tcliSrvName, err := clientServerName(ctx, s.logger, pctx, proto)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"getting client server-name: %w\", err)\n\t}\n\n\tclientID, err = clientIDFromClientServerName(\n\t\thostSrvName,\n\t\tcliSrvName,\n\t\ts.conf.TLSConf.StrictSNICheck,\n\t)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"clientid check: %w\", err)\n\t}\n\n\treturn clientID, nil\n}\n\n// logMiddleware adds a logger using [slogutil.ContextWithLogger] and logs the\n// starts and ends of queries at a given level.\ntype logMiddleware struct {\n\tattrPool *syncutil.Pool[[]slog.Attr]\n\tlogger   *slog.Logger\n\tlvl      slog.Level\n}\n\n// logMwAttrNum is the number of attributes used by the logger set by\n// [logMiddleware].\nconst logMwAttrNum = 3\n\n// newLogMiddleware returns a new *logMiddleware with l as the base logger.\nfunc newLogMiddleware(l *slog.Logger, lvl slog.Level) (mw *logMiddleware) {\n\treturn &logMiddleware{\n\t\tattrPool: syncutil.NewSlicePool[slog.Attr](logMwAttrNum),\n\t\tlogger:   l,\n\t\tlvl:      lvl,\n\t}\n}\n\n// type check\nvar _ proxy.Middleware = (*logMiddleware)(nil)\n\n// Wrap implements the [proxy.Middleware] interface for *logMiddleware.  It adds\n// a logger to the context and logs the starts and ends of queries at a given\n// level.\nfunc (m *logMiddleware) Wrap(h proxy.Handler) (wrapped proxy.Handler) {\n\tf := func(ctx context.Context, p *proxy.Proxy, dctx *proxy.DNSContext) (err error) {\n\t\tstartTime := time.Now()\n\n\t\tattrsPtr := m.attrsSlicePtr(dctx.Req)\n\t\tdefer m.attrPool.Put(attrsPtr)\n\n\t\tlogHdlr := m.logger.Handler().WithAttrs(*attrsPtr)\n\t\tl := slog.New(logHdlr)\n\t\tctx = slogutil.ContextWithLogger(ctx, l)\n\n\t\tl.Log(ctx, m.lvl, \"started\")\n\t\tdefer m.logFinished(ctx, l, startTime)\n\n\t\treturn h.ServeDNS(ctx, p, dctx)\n\t}\n\n\treturn proxy.HandlerFunc(f)\n}\n\n// attrsSlicePtr returns a pointer to a slice with the attributes from the\n// request set.  Callers should defer returning attrsPtr back to the pool.\nfunc (m *logMiddleware) attrsSlicePtr(r *dns.Msg) (attrsPtr *[]slog.Attr) {\n\tattrsPtr = m.attrPool.Get()\n\n\tattrs := *attrsPtr\n\n\t// Optimize bounds checking.\n\t_ = attrs[logMwAttrNum-1]\n\n\tattrs[0] = slog.Uint64(\"id\", uint64(r.Id))\n\n\tif len(r.Question) > 0 {\n\t\tq := r.Question[0]\n\t\tattrs[1] = slog.String(\"qtype\", dns.Type(q.Qtype).String())\n\t\tattrs[2] = slog.String(\"target\", q.Name)\n\t} else {\n\t\tattrs[1] = slog.Attr{}\n\t\tattrs[2] = slog.Attr{}\n\t}\n\n\treturn attrsPtr\n}\n\n// logFinished is called at the end of handling of a query.\nfunc (m *logMiddleware) logFinished(ctx context.Context, l *slog.Logger, startTime time.Time) {\n\tif !l.Enabled(ctx, m.lvl) {\n\t\treturn\n\t}\n\n\tl.Log(ctx, m.lvl, \"finished\", \"elapsed\", timeutil.Duration(time.Since(startTime)))\n}\n"
  },
  {
    "path": "internal/dnsforward/middleware_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Common constants for tests.\nconst (\n\tblockedHost = \"blockedhost.org\"\n\ttestFQDN    = \"example.org.\"\n\n\tdnsClientTimeout = 200 * time.Millisecond\n)\n\nfunc TestServer_middlewareTLS(t *testing.T) {\n\tt.Parallel()\n\n\tconst clientID = \"client-1\"\n\n\ttestCases := []struct {\n\t\tclientSrvName     string\n\t\tname              string\n\t\thost              string\n\t\tallowedClients    []string\n\t\tdisallowedClients []string\n\t\tblockedHosts      []string\n\t\twantRCode         int\n\t}{{\n\t\tclientSrvName:     tlsServerName,\n\t\tname:              \"allow_all\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{},\n\t\twantRCode:         dns.RcodeSuccess,\n\t}, {\n\t\tclientSrvName:     \"%\" + \".\" + tlsServerName,\n\t\tname:              \"invalid_client_id\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{},\n\t\twantRCode:         dns.RcodeServerFailure,\n\t}, {\n\t\tclientSrvName:     clientID + \".\" + tlsServerName,\n\t\tname:              \"allowed_client_allowed\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{clientID},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{},\n\t\twantRCode:         dns.RcodeSuccess,\n\t}, {\n\t\tclientSrvName:     \"client-2.\" + tlsServerName,\n\t\tname:              \"allowed_client_rejected\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{clientID},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{},\n\t\twantRCode:         dns.RcodeRefused,\n\t}, {\n\t\tclientSrvName:     tlsServerName,\n\t\tname:              \"disallowed_client_allowed\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{clientID},\n\t\tblockedHosts:      []string{},\n\t\twantRCode:         dns.RcodeSuccess,\n\t}, {\n\t\tclientSrvName:     clientID + \".\" + tlsServerName,\n\t\tname:              \"disallowed_client_rejected\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{clientID},\n\t\tblockedHosts:      []string{},\n\t\twantRCode:         dns.RcodeRefused,\n\t}, {\n\t\tclientSrvName:     tlsServerName,\n\t\tname:              \"blocked_hosts_allowed\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{blockedHost},\n\t\twantRCode:         dns.RcodeSuccess,\n\t}, {\n\t\tclientSrvName:     tlsServerName,\n\t\tname:              \"blocked_hosts_rejected\",\n\t\thost:              dns.Fqdn(blockedHost),\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{blockedHost},\n\t\twantRCode:         dns.RcodeRefused,\n\t}}\n\n\tlocalAns := newTestDNSAnswer(testFQDN, net.IP{1, 2, 3, 4})\n\tlocalUpsAddr := newTestUpstream(t, localAns)\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ts, _ := createTestTLS(t, &TLSConfig{\n\t\t\t\tTLSListenAddrs: []*net.TCPAddr{{}},\n\t\t\t\tServerName:     tlsServerName,\n\t\t\t})\n\n\t\t\ts.conf.UpstreamDNS = []string{localUpsAddr}\n\t\t\ts.conf.AllowedClients = tc.allowedClients\n\t\t\ts.conf.DisallowedClients = tc.disallowedClients\n\t\t\ts.conf.BlockedHosts = tc.blockedHosts\n\n\t\t\terr := s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &s.conf)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstartDeferStop(t, s)\n\n\t\t\tclient := newTestTCPClient(tc.clientSrvName)\n\n\t\t\treq := createTestMessage(tc.host)\n\t\t\taddr := s.dnsProxy.Addr(proxy.ProtoTLS).String()\n\n\t\t\treply, _, err := client.Exchange(req, addr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.wantRCode == dns.RcodeSuccess {\n\t\t\t\tassertSuccessResponse(t, reply, localAns)\n\t\t\t} else {\n\t\t\t\tassertRejectedResponse(t, reply, tc.wantRCode)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestServer_middlewareUDP(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tclientIPv4 = \"127.0.0.1\"\n\t\tclientIPv6 = \"::1\"\n\t)\n\n\tclientIPs := []string{clientIPv4, clientIPv6}\n\n\ttestCases := []struct {\n\t\tname              string\n\t\thost              string\n\t\tallowedClients    []string\n\t\tdisallowedClients []string\n\t\tblockedHosts      []string\n\t\twantTimeout       bool\n\t}{{\n\t\tname:              \"allow_all\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{},\n\t\twantTimeout:       false,\n\t}, {\n\t\tname:              \"allowed_client_allowed\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    clientIPs,\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{},\n\t\twantTimeout:       false,\n\t}, {\n\t\tname:              \"allowed_client_rejected\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{\"1:2:3::4\"},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{},\n\t\twantTimeout:       true,\n\t}, {\n\t\tname:              \"disallowed_client_allowed\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{\"1:2:3::4\"},\n\t\tblockedHosts:      []string{},\n\t\twantTimeout:       false,\n\t}, {\n\t\tname:              \"disallowed_client_rejected\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: clientIPs,\n\t\tblockedHosts:      []string{},\n\t\twantTimeout:       true,\n\t}, {\n\t\tname:              \"blocked_hosts_allowed\",\n\t\thost:              testFQDN,\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{blockedHost},\n\t\twantTimeout:       false,\n\t}, {\n\t\tname:              \"blocked_hosts_rejected\",\n\t\thost:              dns.Fqdn(blockedHost),\n\t\tallowedClients:    []string{},\n\t\tdisallowedClients: []string{},\n\t\tblockedHosts:      []string{blockedHost},\n\t\twantTimeout:       true,\n\t}}\n\n\tlocalAns := newTestDNSAnswer(testFQDN, net.IP{1, 2, 3, 4})\n\tlocalUpsAddr := newTestUpstream(t, localAns)\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ts := createTestServer(t, &filtering.Config{\n\t\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t\t}, ServerConfig{\n\t\t\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\t\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\t\t\tTLSConf:        &TLSConfig{},\n\t\t\t\tConfig: Config{\n\t\t\t\t\tAllowedClients:    tc.allowedClients,\n\t\t\t\t\tDisallowedClients: tc.disallowedClients,\n\t\t\t\t\tBlockedHosts:      tc.blockedHosts,\n\t\t\t\t\tUpstreamDNS:       []string{localUpsAddr},\n\t\t\t\t\tUpstreamMode:      UpstreamModeLoadBalance,\n\t\t\t\t\tEDNSClientSubnet:  &EDNSClientSubnet{Enabled: false},\n\t\t\t\t\tClientsContainer:  EmptyClientsContainer{},\n\t\t\t\t},\n\t\t\t\tServePlainDNS: true,\n\t\t\t})\n\n\t\t\tstartDeferStop(t, s)\n\n\t\t\tclient := &dns.Client{\n\t\t\t\tNet:     \"udp\",\n\t\t\t\tTimeout: dnsClientTimeout,\n\t\t\t}\n\n\t\t\treq := createTestMessage(tc.host)\n\t\t\taddr := s.dnsProxy.Addr(proxy.ProtoUDP).String()\n\n\t\t\treply, _, err := client.Exchange(req, addr)\n\t\t\tif tc.wantTimeout {\n\t\t\t\tassertTimeoutError(t, err, reply)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassertSuccessResponse(t, reply, localAns)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// newTestDNSAnswer creates a standard A record answer for testing.\nfunc newTestDNSAnswer(fqdn string, ip net.IP) (ans []dns.RR) {\n\treturn []dns.RR{&dns.A{\n\t\tHdr: dns.RR_Header{\n\t\t\tName:     fqdn,\n\t\t\tRrtype:   dns.TypeA,\n\t\t\tClass:    dns.ClassINET,\n\t\t\tTtl:      3600,\n\t\t\tRdlength: 4,\n\t\t},\n\t\tA: ip,\n\t}}\n}\n\n// newTestUpstream creates a test upstream handler and returns its address.\nfunc newTestUpstream(tb testing.TB, answer []dns.RR) (addr string) {\n\ttb.Helper()\n\n\thandler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tresp := (&dns.Msg{}).SetReply(req)\n\t\tresp.Answer = answer\n\n\t\trequire.NoError(testutil.PanicT{}, w.WriteMsg(resp))\n\t})\n\n\treturn aghtest.StartLocalhostUpstream(tb, handler).String()\n}\n\n// newTestTCPClient creates a new TCP client for testing.\nfunc newTestTCPClient(clientSrvName string) (c *dns.Client) {\n\ttlsConfig := &tls.Config{\n\t\tInsecureSkipVerify: true,\n\t\tServerName:         clientSrvName,\n\t}\n\n\treturn &dns.Client{\n\t\tNet:       \"tcp-tls\",\n\t\tTLSConfig: tlsConfig,\n\t\tTimeout:   dnsClientTimeout,\n\t}\n}\n\n// assertSuccessResponse checks that the response is successful with expected\n// answer.\nfunc assertSuccessResponse(tb testing.TB, reply *dns.Msg, expectedAns []dns.RR) {\n\ttb.Helper()\n\n\trequire.NotNil(tb, reply)\n\n\tassert.Equal(tb, dns.RcodeSuccess, reply.Rcode)\n\tassert.Equal(tb, expectedAns, reply.Answer)\n}\n\n// assertRejectedResponse checks that the response has the expected error code\n// and no answer.\nfunc assertRejectedResponse(tb testing.TB, reply *dns.Msg, wantRCode int) {\n\ttb.Helper()\n\n\trequire.NotNil(tb, reply)\n\n\tassert.Equal(tb, wantRCode, reply.Rcode)\n\tassert.Empty(tb, reply.Answer)\n}\n\n// assertTimeoutError checks that the error is a timeout error and reply is nil.\nfunc assertTimeoutError(tb testing.TB, err error, reply *dns.Msg) {\n\ttb.Helper()\n\n\twantErr := &net.OpError{}\n\trequire.ErrorAs(tb, err, &wantErr)\n\n\tassert.True(tb, wantErr.Timeout())\n\tassert.Nil(tb, reply)\n}\n"
  },
  {
    "path": "internal/dnsforward/msg.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"net/netip\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// TODO(e.burkov):  Name all the methods by a [proxy.MessageConstructor]\n// template.  Also extract all the methods to a separate entity.\n\n// reply creates a DNS response for req.\nfunc (*Server) reply(req *dns.Msg, code int) (resp *dns.Msg) {\n\tresp = (&dns.Msg{}).SetRcode(req, code)\n\tresp.RecursionAvailable = true\n\n\treturn resp\n}\n\n// replyCompressed creates a DNS response for req and sets the compress flag.\nfunc (s *Server) replyCompressed(req *dns.Msg) (resp *dns.Msg) {\n\tresp = s.reply(req, dns.RcodeSuccess)\n\tresp.Compress = true\n\n\treturn resp\n}\n\n// ipsFromRules extracts unique non-IP addresses from the filtering result\n// rules.\nfunc ipsFromRules(resRules []*filtering.ResultRule) (ips []netip.Addr) {\n\tfor _, r := range resRules {\n\t\t// len(resRules) and len(ips) are actually small enough for O(n^2) to do\n\t\t// not raise performance questions.\n\t\tif ip := r.IP; ip != (netip.Addr{}) && !slices.Contains(ips, ip) {\n\t\t\tips = append(ips, ip)\n\t\t}\n\t}\n\n\treturn ips\n}\n\n// genDNSFilterMessage generates a filtered response to req for the filtering\n// result res.\nfunc (s *Server) genDNSFilterMessage(\n\tctx context.Context,\n\tdctx *proxy.DNSContext,\n\tres *filtering.Result,\n) (resp *dns.Msg) {\n\treq := dctx.Req\n\tqt := req.Question[0].Qtype\n\tif qt != dns.TypeA && qt != dns.TypeAAAA && qt != dns.TypeHTTPS {\n\t\tm, _, _ := s.dnsFilter.BlockingMode()\n\t\tif m == filtering.BlockingModeNullIP {\n\t\t\treturn s.replyCompressed(req)\n\t\t}\n\n\t\treturn s.NewMsgNODATA(req)\n\t}\n\n\tswitch res.Reason {\n\tcase filtering.FilteredSafeBrowsing:\n\t\treturn s.genBlockedHost(ctx, req, s.dnsFilter.SafeBrowsingBlockHost(), dctx)\n\tcase filtering.FilteredParental:\n\t\treturn s.genBlockedHost(ctx, req, s.dnsFilter.ParentalBlockHost(), dctx)\n\tcase filtering.FilteredSafeSearch:\n\t\t// If Safe Search generated the necessary IP addresses, use them.\n\t\t// Otherwise, if there were no errors, there are no addresses for the\n\t\t// requested IP version, so produce a NODATA response.\n\t\treturn s.getCNAMEWithIPs(ctx, req, ipsFromRules(res.Rules), res.CanonName)\n\tdefault:\n\t\treturn s.genForBlockingMode(ctx, req, ipsFromRules(res.Rules))\n\t}\n}\n\n// getCNAMEWithIPs generates a filtered response to req for with CNAME record\n// and provided ips.\nfunc (s *Server) getCNAMEWithIPs(\n\tctx context.Context,\n\treq *dns.Msg,\n\tips []netip.Addr,\n\tcname string,\n) (resp *dns.Msg) {\n\tresp = s.replyCompressed(req)\n\n\toriginalName := req.Question[0].Name\n\n\tvar ans []dns.RR\n\tif cname != \"\" {\n\t\tans = append(ans, s.genAnswerCNAME(req, cname))\n\n\t\t// The given IPs actually are resolved for this cname.\n\t\treq.Question[0].Name = dns.Fqdn(cname)\n\t\tdefer func() { req.Question[0].Name = originalName }()\n\t}\n\n\tswitch req.Question[0].Qtype {\n\tcase dns.TypeA:\n\t\tans = append(ans, s.genAnswersWithIPv4s(ctx, req, ips)...)\n\tcase dns.TypeAAAA:\n\t\tfor _, ip := range ips {\n\t\t\tif ip.Is6() {\n\t\t\t\tans = append(ans, s.genAnswerAAAA(req, ip))\n\t\t\t}\n\t\t}\n\tdefault:\n\t\t// Go on and return an empty response.\n\t}\n\n\tresp.Answer = ans\n\n\treturn resp\n}\n\n// genForBlockingMode generates a filtered response to req based on the server's\n// blocking mode.\nfunc (s *Server) genForBlockingMode(\n\tctx context.Context,\n\treq *dns.Msg,\n\tips []netip.Addr,\n) (resp *dns.Msg) {\n\tswitch mode, bIPv4, bIPv6 := s.dnsFilter.BlockingMode(); mode {\n\tcase filtering.BlockingModeCustomIP:\n\t\treturn s.makeResponseCustomIP(ctx, req, bIPv4, bIPv6)\n\tcase filtering.BlockingModeDefault:\n\t\tif len(ips) > 0 {\n\t\t\treturn s.genResponseWithIPs(ctx, req, ips)\n\t\t}\n\n\t\treturn s.makeResponseNullIP(ctx, req)\n\tcase filtering.BlockingModeNullIP:\n\t\treturn s.makeResponseNullIP(ctx, req)\n\tcase filtering.BlockingModeNXDOMAIN:\n\t\treturn s.NewMsgNXDOMAIN(req)\n\tcase filtering.BlockingModeREFUSED:\n\t\treturn s.makeResponseREFUSED(req)\n\tdefault:\n\t\ts.logger.ErrorContext(ctx, \"invalid blocking mode\", \"mode\", mode)\n\n\t\treturn s.replyCompressed(req)\n\t}\n}\n\n// makeResponseCustomIP generates a DNS response message for Custom IP blocking\n// mode with the provided IP addresses and an appropriate resource record type.\nfunc (s *Server) makeResponseCustomIP(\n\tctx context.Context,\n\treq *dns.Msg,\n\tbIPv4 netip.Addr,\n\tbIPv6 netip.Addr,\n) (resp *dns.Msg) {\n\tswitch qt := req.Question[0].Qtype; qt {\n\tcase dns.TypeA:\n\t\treturn s.genARecord(req, bIPv4)\n\tcase dns.TypeAAAA:\n\t\treturn s.genAAAARecord(req, bIPv6)\n\tdefault:\n\t\t// Generally shouldn't happen, since the types are checked in\n\t\t// genDNSFilterMessage.\n\t\ts.logger.ErrorContext(\n\t\t\tctx,\n\t\t\t\"invalid message type for custom IP blocking mode\",\n\t\t\t\"dns_type\", dns.Type(qt),\n\t\t)\n\n\t\treturn s.replyCompressed(req)\n\t}\n}\n\nfunc (s *Server) genARecord(request *dns.Msg, ip netip.Addr) *dns.Msg {\n\tresp := s.replyCompressed(request)\n\tresp.Answer = append(resp.Answer, s.genAnswerA(request, ip))\n\treturn resp\n}\n\nfunc (s *Server) genAAAARecord(request *dns.Msg, ip netip.Addr) *dns.Msg {\n\tresp := s.replyCompressed(request)\n\tresp.Answer = append(resp.Answer, s.genAnswerAAAA(request, ip))\n\treturn resp\n}\n\nfunc (s *Server) hdr(req *dns.Msg, rrType rules.RRType) (h dns.RR_Header) {\n\treturn dns.RR_Header{\n\t\tName:   req.Question[0].Name,\n\t\tRrtype: rrType,\n\t\tTtl:    s.dnsFilter.BlockedResponseTTL(),\n\t\tClass:  dns.ClassINET,\n\t}\n}\n\nfunc (s *Server) genAnswerA(req *dns.Msg, ip netip.Addr) (ans *dns.A) {\n\treturn &dns.A{\n\t\tHdr: s.hdr(req, dns.TypeA),\n\t\tA:   ip.AsSlice(),\n\t}\n}\n\nfunc (s *Server) genAnswerAAAA(req *dns.Msg, ip netip.Addr) (ans *dns.AAAA) {\n\treturn &dns.AAAA{\n\t\tHdr:  s.hdr(req, dns.TypeAAAA),\n\t\tAAAA: ip.AsSlice(),\n\t}\n}\n\nfunc (s *Server) genAnswerCNAME(req *dns.Msg, cname string) (ans *dns.CNAME) {\n\treturn &dns.CNAME{\n\t\tHdr:    s.hdr(req, dns.TypeCNAME),\n\t\tTarget: dns.Fqdn(cname),\n\t}\n}\n\nfunc (s *Server) genAnswerMX(req *dns.Msg, mx *rules.DNSMX) (ans *dns.MX) {\n\treturn &dns.MX{\n\t\tHdr:        s.hdr(req, dns.TypeMX),\n\t\tPreference: mx.Preference,\n\t\tMx:         dns.Fqdn(mx.Exchange),\n\t}\n}\n\nfunc (s *Server) genAnswerPTR(req *dns.Msg, ptr string) (ans *dns.PTR) {\n\treturn &dns.PTR{\n\t\tHdr: s.hdr(req, dns.TypePTR),\n\t\tPtr: dns.Fqdn(ptr),\n\t}\n}\n\nfunc (s *Server) genAnswerSRV(req *dns.Msg, srv *rules.DNSSRV) (ans *dns.SRV) {\n\treturn &dns.SRV{\n\t\tHdr:      s.hdr(req, dns.TypeSRV),\n\t\tPriority: srv.Priority,\n\t\tWeight:   srv.Weight,\n\t\tPort:     srv.Port,\n\t\tTarget:   dns.Fqdn(srv.Target),\n\t}\n}\n\nfunc (s *Server) genAnswerTXT(req *dns.Msg, strs []string) (ans *dns.TXT) {\n\treturn &dns.TXT{\n\t\tHdr: s.hdr(req, dns.TypeTXT),\n\t\tTxt: strs,\n\t}\n}\n\n// genResponseWithIPs generates a DNS response message with the provided IP\n// addresses and an appropriate resource record type.  If any of the IPs cannot\n// be converted to the correct protocol, genResponseWithIPs returns an empty\n// response.\nfunc (s *Server) genResponseWithIPs(\n\tctx context.Context,\n\treq *dns.Msg,\n\tips []netip.Addr,\n) (resp *dns.Msg) {\n\tvar ans []dns.RR\n\tswitch req.Question[0].Qtype {\n\tcase dns.TypeA:\n\t\tans = s.genAnswersWithIPv4s(ctx, req, ips)\n\tcase dns.TypeAAAA:\n\t\tfor _, ip := range ips {\n\t\t\tif ip.Is6() {\n\t\t\t\tans = append(ans, s.genAnswerAAAA(req, ip))\n\t\t\t}\n\t\t}\n\tdefault:\n\t\t// Go on and return an empty response.\n\t}\n\n\tresp = s.replyCompressed(req)\n\tresp.Answer = ans\n\n\treturn resp\n}\n\n// genAnswersWithIPv4s generates DNS A answers provided IPv4 addresses.  If any\n// of the IPs isn't an IPv4 address, genAnswersWithIPv4s logs a warning and\n// returns nil,\nfunc (s *Server) genAnswersWithIPv4s(\n\tctx context.Context,\n\treq *dns.Msg,\n\tips []netip.Addr,\n) (ans []dns.RR) {\n\tfor _, ip := range ips {\n\t\tif !ip.Is4() {\n\t\t\ts.logger.WarnContext(ctx, \"ip is not an ipv4 address\", \"ip\", ip)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tans = append(ans, s.genAnswerA(req, ip))\n\t}\n\n\treturn ans\n}\n\n// makeResponseNullIP creates a response with 0.0.0.0 for A requests, :: for\n// AAAA requests, and an empty response for other types.\nfunc (s *Server) makeResponseNullIP(ctx context.Context, req *dns.Msg) (resp *dns.Msg) {\n\t// Respond with the corresponding zero IP type as opposed to simply\n\t// using one or the other in both cases, because the IPv4 zero IP is\n\t// converted to a IPV6-mapped IPv4 address, while the IPv6 zero IP is\n\t// converted into an empty slice instead of the zero IPv4.\n\tswitch req.Question[0].Qtype {\n\tcase dns.TypeA:\n\t\tresp = s.genResponseWithIPs(ctx, req, []netip.Addr{netip.IPv4Unspecified()})\n\tcase dns.TypeAAAA:\n\t\tresp = s.genResponseWithIPs(ctx, req, []netip.Addr{netip.IPv6Unspecified()})\n\tdefault:\n\t\tresp = s.replyCompressed(req)\n\t}\n\n\treturn resp\n}\n\nfunc (s *Server) genBlockedHost(\n\tctx context.Context,\n\trequest *dns.Msg,\n\tnewAddr string,\n\td *proxy.DNSContext,\n) (msg *dns.Msg) {\n\tif newAddr == \"\" {\n\t\ts.logger.InfoContext(ctx, \"block host not specified\")\n\n\t\treturn s.NewMsgSERVFAIL(request)\n\t}\n\n\tip, err := netip.ParseAddr(newAddr)\n\tif err == nil {\n\t\treturn s.genResponseWithIPs(ctx, request, []netip.Addr{ip})\n\t}\n\n\t// look up the hostname, TODO: cache\n\treplReq := dns.Msg{}\n\treplReq.SetQuestion(dns.Fqdn(newAddr), request.Question[0].Qtype)\n\treplReq.RecursionDesired = true\n\n\tnewContext := &proxy.DNSContext{\n\t\tProto: d.Proto,\n\t\tAddr:  d.Addr,\n\t\tReq:   &replReq,\n\t}\n\n\tprx := s.proxy()\n\tif prx == nil {\n\t\ts.logger.DebugContext(ctx, \"getting current proxy\", slogutil.KeyError, srvClosedErr)\n\n\t\treturn s.NewMsgSERVFAIL(request)\n\t}\n\n\terr = prx.Resolve(ctx, newContext)\n\tif err != nil {\n\t\ts.logger.ErrorContext(\n\t\t\tctx,\n\t\t\t\"looking up replacement host\",\n\t\t\t\"host\", newAddr,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\n\t\treturn s.NewMsgSERVFAIL(request)\n\t}\n\n\tresp := s.replyCompressed(request)\n\tif newContext.Res != nil {\n\t\tfor _, answer := range newContext.Res.Answer {\n\t\t\tanswer.Header().Name = request.Question[0].Name\n\t\t\tresp.Answer = append(resp.Answer, answer)\n\t\t}\n\t}\n\n\treturn resp\n}\n\n// Create REFUSED DNS response\nfunc (s *Server) makeResponseREFUSED(req *dns.Msg) *dns.Msg {\n\treturn s.reply(req, dns.RcodeRefused)\n}\n\n// type check\nvar _ proxy.MessageConstructor = (*Server)(nil)\n\n// NewMsgNXDOMAIN implements the [proxy.MessageConstructor] interface for\n// *Server.\nfunc (s *Server) NewMsgNXDOMAIN(req *dns.Msg) (resp *dns.Msg) {\n\tresp = s.reply(req, dns.RcodeNameError)\n\tresp.Ns = s.genSOA(req)\n\n\treturn resp\n}\n\n// NewMsgSERVFAIL implements the [proxy.MessageConstructor] interface for\n// *Server.\nfunc (s *Server) NewMsgSERVFAIL(req *dns.Msg) (resp *dns.Msg) {\n\treturn s.reply(req, dns.RcodeServerFailure)\n}\n\n// NewMsgNOTIMPLEMENTED implements the [proxy.MessageConstructor] interface for\n// *Server.\nfunc (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) {\n\tresp = s.reply(req, dns.RcodeNotImplemented)\n\n\t// Most of the Internet and especially the inner core has an MTU of at least\n\t// 1500 octets.  Maximum DNS/UDP payload size for IPv6 on MTU 1500 ethernet\n\t// is 1452 (1500 minus 40 (IPv6 header size) minus 8 (UDP header size)).\n\t//\n\t// See appendix A of https://datatracker.ietf.org/doc/draft-ietf-dnsop-avoid-fragmentation/17.\n\tconst maxUDPPayload = 1452\n\n\t// NOTIMPLEMENTED without EDNS is treated as 'we don't support EDNS', so\n\t// explicitly set it.\n\tresp.SetEdns0(maxUDPPayload, false)\n\n\treturn resp\n}\n\n// NewMsgNODATA implements the [proxy.MessageConstructor] interface for *Server.\nfunc (s *Server) NewMsgNODATA(req *dns.Msg) (resp *dns.Msg) {\n\tresp = s.reply(req, dns.RcodeSuccess)\n\tresp.Ns = s.genSOA(req)\n\n\treturn resp\n}\n\nfunc (s *Server) genSOA(req *dns.Msg) []dns.RR {\n\tzone := \"\"\n\tif len(req.Question) > 0 {\n\t\tzone = req.Question[0].Name\n\t}\n\n\tconst defaultBlockedResponseTTL = 3600\n\n\tsoa := dns.SOA{\n\t\t// Values copied from verisign's nonexistent.com domain.\n\t\t//\n\t\t// Their exact values are not important in our use case because they are\n\t\t// used for domain transfers between primary/secondary DNS servers.\n\t\tRefresh: 1800,\n\t\tRetry:   900,\n\t\tExpire:  604800,\n\t\tMinttl:  86400,\n\t\t// copied from AdGuard DNS\n\t\tNs:     \"fake-for-negative-caching.adguard.com.\",\n\t\tSerial: 100500,\n\t\t// rest is request-specific\n\t\tHdr: dns.RR_Header{\n\t\t\tName:   zone,\n\t\t\tRrtype: dns.TypeSOA,\n\t\t\tTtl:    s.dnsFilter.BlockedResponseTTL(),\n\t\t\tClass:  dns.ClassINET,\n\t\t},\n\t\t// zone will be appended later if it's not \".\".\n\t\tMbox: \"hostmaster.\",\n\t}\n\tif soa.Hdr.Ttl == 0 {\n\t\tsoa.Hdr.Ttl = defaultBlockedResponseTTL\n\t}\n\n\tif zone != \".\" {\n\t\tsoa.Mbox += zone\n\t}\n\n\treturn []dns.RR{&soa}\n}\n"
  },
  {
    "path": "internal/dnsforward/process.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// To transfer information between modules\n//\n// TODO(s.chzhen):  Add lowercased, non-FQDN version of the hostname from the\n// question of the request.  Add persistent client.\ntype dnsContext struct {\n\tproxyCtx *proxy.DNSContext\n\n\t// setts are the filtering settings for the client.\n\tsetts *filtering.Settings\n\n\tresult *filtering.Result\n\n\t// origResp is the response received from upstream.  It is set when the\n\t// response is modified by filters.\n\torigResp *dns.Msg\n\n\t// err is the error returned from a processing function.\n\terr error\n\n\t// clientID is the ClientID from DoH, DoQ, or DoT, if provided.\n\tclientID string\n\n\t// startTime is the time at which the processing of the request has started.\n\tstartTime time.Time\n\n\t// origQuestion is the question received from the client.  It is set\n\t// when the request is modified by rewrites.\n\torigQuestion dns.Question\n\n\t// protectionEnabled shows if the filtering is enabled, and if the\n\t// server's DNS filter is ready.\n\tprotectionEnabled bool\n\n\t// responseFromUpstream shows if the response is received from the\n\t// upstream servers.\n\tresponseFromUpstream bool\n\n\t// responseAD shows if the response had the AD bit set.\n\tresponseAD bool\n\n\t// isDHCPHost is true if the request for a local domain name and the DHCP is\n\t// available for this request.\n\tisDHCPHost bool\n}\n\n// resultCode is the result of a request processing function.\ntype resultCode int\n\nconst (\n\t// resultCodeSuccess is returned when a handler performed successfully, and\n\t// the next handler must be called.\n\tresultCodeSuccess resultCode = iota\n\n\t// resultCodeFinish is returned when a handler performed successfully, and\n\t// the processing of the request must be stopped.\n\tresultCodeFinish\n\n\t// resultCodeError is returned when a handler failed, and the processing of\n\t// the request must be stopped.\n\tresultCodeError\n)\n\n// ddrHostFQDN is the FQDN used in Discovery of Designated Resolvers (DDR) requests.\n// See https://www.ietf.org/archive/id/draft-ietf-add-ddr-06.html.\nconst ddrHostFQDN = \"_dns.resolver.arpa.\"\n\n// mozillaFQDN is the domain used to signal the Firefox browser to not use its\n// own DoH server.\n//\n// See https://support.mozilla.org/en-US/kb/canary-domain-use-application-dnsnet.\nconst mozillaFQDN = \"use-application-dns.net.\"\n\n// healthcheckFQDN is a reserved domain-name used for healthchecking.\n//\n// [Section 6.2 of RFC 6761] states that DNS Registries/Registrars must not\n// grant requests to register test names in the normal way to any person or\n// entity, making domain names under the .test TLD free to use in internal\n// purposes.\n//\n// [Section 6.2 of RFC 6761]: https://www.rfc-editor.org/rfc/rfc6761.html#section-6.2\nconst healthcheckFQDN = \"healthcheck.adguardhome.test.\"\n\n// processInitial terminates the following processing for some requests if\n// needed and enriches dctx with some client-specific information.\n//\n// TODO(e.burkov):  Decompose into less general processors.\nfunc (s *Server) processInitial(ctx context.Context, dctx *dnsContext) (rc resultCode) {\n\ts.logger.DebugContext(ctx, \"started processing initial\")\n\tdefer s.logger.DebugContext(ctx, \"finished processing initial\")\n\n\tpctx := dctx.proxyCtx\n\ts.processClientIP(ctx, pctx.Addr.Addr())\n\n\tq := pctx.Req.Question[0]\n\tqt := q.Qtype\n\tif s.conf.AAAADisabled && qt == dns.TypeAAAA {\n\t\tpctx.Res = s.NewMsgNODATA(pctx.Req)\n\n\t\treturn resultCodeFinish\n\t}\n\n\tif (qt == dns.TypeA || qt == dns.TypeAAAA) && q.Name == mozillaFQDN {\n\t\tpctx.Res = s.NewMsgNXDOMAIN(pctx.Req)\n\n\t\treturn resultCodeFinish\n\t}\n\n\tif q.Name == healthcheckFQDN {\n\t\t// Generate a NODATA negative response to make nslookup exit with 0.\n\t\tpctx.Res = s.replyCompressed(pctx.Req)\n\n\t\treturn resultCodeFinish\n\t}\n\n\t// Get the ClientID, if any, before getting client-specific filtering\n\t// settings.\n\tclientID, ok := clientIDFromContext(ctx)\n\tif ok {\n\t\tdctx.clientID = clientID\n\t}\n\n\t// Get the client-specific filtering settings.\n\tdctx.protectionEnabled, _ = s.UpdatedProtectionStatus(ctx)\n\tdctx.setts = s.clientRequestFilteringSettings(dctx)\n\n\treturn resultCodeSuccess\n}\n\n// processClientIP sends the client IP address to s.addrProc, if needed.\nfunc (s *Server) processClientIP(ctx context.Context, addr netip.Addr) {\n\tif !addr.IsValid() {\n\t\ts.logger.WarnContext(ctx, \"bad client address\", \"addr\", addr)\n\n\t\treturn\n\t}\n\n\t// Do not assign s.addrProc to a local variable to then use, since this lock\n\t// also serializes the closure of s.addrProc.\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\ts.addrProc.Process(ctx, addr)\n}\n\n// processDDRQuery responds to Discovery of Designated Resolvers (DDR) SVCB\n// queries.  The response contains different types of encryption supported by\n// current user configuration.\n//\n// See https://www.ietf.org/archive/id/draft-ietf-add-ddr-10.html.\nfunc (s *Server) processDDRQuery(ctx context.Context, dctx *dnsContext) (rc resultCode) {\n\ts.logger.DebugContext(ctx, \"started processing ddr\")\n\tdefer s.logger.DebugContext(ctx, \"finished processing ddr\")\n\n\tif !s.conf.HandleDDR {\n\t\treturn resultCodeSuccess\n\t}\n\n\tpctx := dctx.proxyCtx\n\tq := pctx.Req.Question[0]\n\tif q.Name == ddrHostFQDN {\n\t\tpctx.Res = s.makeDDRResponse(pctx.Req)\n\n\t\treturn resultCodeFinish\n\t}\n\n\treturn resultCodeSuccess\n}\n\n// makeDDRResponse creates a DDR answer based on the server configuration.  The\n// constructed SVCB resource records have the priority of 1 for each entry,\n// similar to examples provided by the [draft standard].\n//\n// TODO(a.meshkov):  Consider setting the priority values based on the protocol.\n//\n// [draft standard]: https://www.ietf.org/archive/id/draft-ietf-add-ddr-10.html.\nfunc (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {\n\tresp = s.replyCompressed(req)\n\tif req.Question[0].Qtype != dns.TypeSVCB {\n\t\treturn resp\n\t}\n\n\t// TODO(e.burkov):  Think about storing the FQDN version of the server's\n\t// name somewhere.\n\tdomainName := dns.Fqdn(s.conf.TLSConf.ServerName)\n\n\tfor _, addr := range s.conf.TLSConf.HTTPSListenAddrs {\n\t\tvalues := []dns.SVCBKeyValue{\n\t\t\t&dns.SVCBAlpn{Alpn: []string{\"h2\"}},\n\t\t\t&dns.SVCBPort{Port: addr.Port()},\n\t\t\t&dns.SVCBDoHPath{Template: \"/dns-query{?dns}\"},\n\t\t}\n\n\t\tans := &dns.SVCB{\n\t\t\tHdr:      s.hdr(req, dns.TypeSVCB),\n\t\t\tPriority: 1,\n\t\t\tTarget:   domainName,\n\t\t\tValue:    values,\n\t\t}\n\n\t\tresp.Answer = append(resp.Answer, ans)\n\t}\n\n\tif s.hasIPAddrs {\n\t\t// Only add DNS-over-TLS resolvers in case the certificate contains IP\n\t\t// addresses.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/4927.\n\t\tfor _, addr := range s.dnsProxy.TLSListenAddr {\n\t\t\tvalues := []dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBAlpn{Alpn: []string{\"dot\"}},\n\t\t\t\t&dns.SVCBPort{Port: uint16(addr.Port)},\n\t\t\t}\n\n\t\t\tans := &dns.SVCB{\n\t\t\t\tHdr:      s.hdr(req, dns.TypeSVCB),\n\t\t\t\tPriority: 1,\n\t\t\t\tTarget:   domainName,\n\t\t\t\tValue:    values,\n\t\t\t}\n\n\t\t\tresp.Answer = append(resp.Answer, ans)\n\t\t}\n\t}\n\n\tfor _, addr := range s.dnsProxy.QUICListenAddr {\n\t\tvalues := []dns.SVCBKeyValue{\n\t\t\t&dns.SVCBAlpn{Alpn: []string{\"doq\"}},\n\t\t\t&dns.SVCBPort{Port: uint16(addr.Port)},\n\t\t}\n\n\t\tans := &dns.SVCB{\n\t\t\tHdr:      s.hdr(req, dns.TypeSVCB),\n\t\t\tPriority: 1,\n\t\t\tTarget:   domainName,\n\t\t\tValue:    values,\n\t\t}\n\n\t\tresp.Answer = append(resp.Answer, ans)\n\t}\n\n\treturn resp\n}\n\n// processDHCPHosts respond to A requests if the target hostname is known to\n// the server.  It responds with a mapped IP address if the DNS64 is enabled and\n// the request is for AAAA.\n//\n// TODO(a.garipov): Adapt to AAAA as well.\nfunc (s *Server) processDHCPHosts(ctx context.Context, dctx *dnsContext) (rc resultCode) {\n\ts.logger.DebugContext(ctx, \"started processing dhcp hosts\")\n\tdefer s.logger.DebugContext(ctx, \"finished processing dhcp hosts\")\n\n\tpctx := dctx.proxyCtx\n\treq := pctx.Req\n\n\tq := &req.Question[0]\n\tdhcpHost := s.dhcpHostFromRequest(q)\n\tif dctx.isDHCPHost = dhcpHost != \"\"; !dctx.isDHCPHost {\n\t\treturn resultCodeSuccess\n\t}\n\n\tif !pctx.IsPrivateClient {\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"requests for dhcp host\",\n\t\t\t\"addr\", pctx.Addr,\n\t\t\t\"dhcp_host\", dhcpHost,\n\t\t)\n\t\tpctx.Res = s.NewMsgNXDOMAIN(req)\n\n\t\t// Do not even put into query log.\n\t\treturn resultCodeFinish\n\t}\n\n\tip := s.dhcpServer.IPByHost(dhcpHost)\n\tif ip == (netip.Addr{}) {\n\t\t// Go on and process them with filters, including dnsrewrite ones, and\n\t\t// possibly route them to a domain-specific upstream.\n\t\ts.logger.DebugContext(ctx, \"no dhcp record\", \"dhcp_host\", dhcpHost)\n\n\t\treturn resultCodeSuccess\n\t}\n\n\ts.logger.DebugContext(ctx, \"dhcp record for\", \"dhcp_host\", dhcpHost, \"ip\", ip)\n\n\tresp := s.replyCompressed(req)\n\tswitch q.Qtype {\n\tcase dns.TypeA:\n\t\ta := &dns.A{\n\t\t\tHdr: s.hdr(req, dns.TypeA),\n\t\t\tA:   ip.AsSlice(),\n\t\t}\n\t\tresp.Answer = append(resp.Answer, a)\n\tcase dns.TypeAAAA:\n\t\tif s.dns64Pref != (netip.Prefix{}) {\n\t\t\t// Respond with DNS64-mapped address for IPv4 host if DNS64 is\n\t\t\t// enabled.\n\t\t\taaaa := &dns.AAAA{\n\t\t\t\tHdr:  s.hdr(req, dns.TypeAAAA),\n\t\t\t\tAAAA: s.mapDNS64(ip),\n\t\t\t}\n\t\t\tresp.Answer = append(resp.Answer, aaaa)\n\t\t}\n\tdefault:\n\t\t// Go on.\n\t}\n\n\tdctx.proxyCtx.Res = resp\n\n\treturn resultCodeSuccess\n}\n\n// processDHCPAddrs responds to PTR requests if the target IP is leased by the\n// DHCP server.\nfunc (s *Server) processDHCPAddrs(ctx context.Context, dctx *dnsContext) (rc resultCode) {\n\ts.logger.DebugContext(ctx, \"started processing dhcp addrs\")\n\tdefer s.logger.DebugContext(ctx, \"finished processing dhcp addrs\")\n\n\tpctx := dctx.proxyCtx\n\tif pctx.Res != nil {\n\t\treturn resultCodeSuccess\n\t}\n\n\treq := pctx.Req\n\tq := req.Question[0]\n\tpref := pctx.RequestedPrivateRDNS\n\t// TODO(e.burkov):  Consider answering authoritatively for SOA and NS\n\t// queries.\n\tif pref == (netip.Prefix{}) || q.Qtype != dns.TypePTR {\n\t\treturn resultCodeSuccess\n\t}\n\n\taddr := pref.Addr()\n\thost := s.dhcpServer.HostByIP(addr)\n\tif host == \"\" {\n\t\treturn resultCodeSuccess\n\t}\n\n\ts.logger.DebugContext(ctx, \"dhcp client\", \"addr\", addr, \"host\", host)\n\n\tresp := s.replyCompressed(req)\n\tptr := &dns.PTR{\n\t\tHdr: dns.RR_Header{\n\t\t\tName:   q.Name,\n\t\t\tRrtype: dns.TypePTR,\n\t\t\t// TODO(e.burkov):  Use [dhcpsvc.Lease.Expiry].  See\n\t\t\t// https://github.com/AdguardTeam/AdGuardHome/issues/3932.\n\t\t\tTtl:   s.dnsFilter.BlockedResponseTTL(),\n\t\t\tClass: dns.ClassINET,\n\t\t},\n\t\tPtr: dns.Fqdn(strings.Join([]string{host, s.localDomainSuffix}, \".\")),\n\t}\n\tresp.Answer = append(resp.Answer, ptr)\n\tpctx.Res = resp\n\n\treturn resultCodeSuccess\n}\n\n// Apply filtering logic\nfunc (s *Server) processFilteringBeforeRequest(\n\tctx context.Context,\n\tdctx *dnsContext,\n) (rc resultCode) {\n\ts.logger.DebugContext(ctx, \"started processing filtering before request\")\n\tdefer s.logger.DebugContext(ctx, \"finished processing filtering before request\")\n\n\tif dctx.proxyCtx.RequestedPrivateRDNS != (netip.Prefix{}) {\n\t\t// There is no need to filter request for locally served ARPA hostname\n\t\t// so disable redundant filters.\n\t\tdctx.setts.ParentalEnabled = false\n\t\tdctx.setts.SafeBrowsingEnabled = false\n\t\tdctx.setts.SafeSearchEnabled = false\n\t\tdctx.setts.ServicesRules = nil\n\t}\n\n\tif dctx.proxyCtx.Res != nil {\n\t\t// Go on since the response is already set.\n\t\treturn resultCodeSuccess\n\t}\n\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\tvar err error\n\tif dctx.result, err = s.filterDNSRequest(ctx, dctx); err != nil {\n\t\tdctx.err = err\n\n\t\treturn resultCodeError\n\t}\n\n\treturn resultCodeSuccess\n}\n\n// ipStringFromAddr extracts an IP address string from net.Addr.\nfunc ipStringFromAddr(addr net.Addr) (ipStr string) {\n\tif ip, _ := netutil.IPAndPortFromAddr(addr); ip != nil {\n\t\treturn ip.String()\n\t}\n\n\treturn \"\"\n}\n\n// processUpstream passes request to upstream servers and handles the response.\nfunc (s *Server) processUpstream(ctx context.Context, dctx *dnsContext) (rc resultCode) {\n\ts.logger.DebugContext(ctx, \"started processing upstream\")\n\tdefer s.logger.DebugContext(ctx, \"finished processing upstream\")\n\n\tpctx := dctx.proxyCtx\n\treq := pctx.Req\n\n\tif pctx.Res != nil {\n\t\t// The response has already been set.\n\t\treturn resultCodeSuccess\n\t} else if dctx.isDHCPHost {\n\t\t// A DHCP client hostname query that hasn't been handled or filtered.\n\t\t// Respond with an NXDOMAIN.\n\t\t//\n\t\t// TODO(a.garipov): Route such queries to a custom upstream for the\n\t\t// local domain name if there is one.\n\t\tname := req.Question[0].Name\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"dhcp client hostname was not filtered\",\n\t\t\t\"hostname\", name[:len(name)-1],\n\t\t)\n\t\tpctx.Res = s.NewMsgNXDOMAIN(req)\n\n\t\treturn resultCodeFinish\n\t}\n\n\ts.setCustomUpstream(ctx, pctx, dctx.clientID)\n\n\treqWantsDNSSEC := s.setReqAD(req)\n\n\t// Process the request further since it wasn't filtered.\n\tprx := s.proxy()\n\tif prx == nil {\n\t\tdctx.err = srvClosedErr\n\n\t\treturn resultCodeError\n\t}\n\n\tif dctx.err = prx.Resolve(ctx, pctx); dctx.err != nil {\n\t\treturn resultCodeError\n\t}\n\n\tdctx.responseFromUpstream = true\n\tdctx.responseAD = pctx.Res.AuthenticatedData\n\n\ts.setRespAD(pctx, reqWantsDNSSEC)\n\n\treturn resultCodeSuccess\n}\n\n// setReqAD changes the request based on the server settings.  wantsDNSSEC is\n// false if the response should be cleared of the AD bit.\n//\n// TODO(a.garipov, e.burkov): This should probably be done in module dnsproxy.\nfunc (s *Server) setReqAD(req *dns.Msg) (wantsDNSSEC bool) {\n\tif !s.conf.EnableDNSSEC {\n\t\treturn false\n\t}\n\n\torigReqAD := req.AuthenticatedData\n\treq.AuthenticatedData = true\n\n\t// Per [RFC 6840] says, validating resolvers should only set the AD bit when\n\t// the response has the AD bit set and the request contained either a set DO\n\t// bit or a set AD bit.  So, if neither of these is true, clear the AD bits\n\t// in [Server.setRespAD].\n\t//\n\t// [RFC 6840]: https://datatracker.ietf.org/doc/html/rfc6840#section-5.8\n\treturn origReqAD || hasDO(req)\n}\n\n// hasDO returns true if msg has EDNS(0) options and the DNSSEC OK flag is set\n// in there.\n//\n// TODO(a.garipov): Move to golibs/dnsmsg when it's there.\nfunc hasDO(msg *dns.Msg) (do bool) {\n\to := msg.IsEdns0()\n\tif o == nil {\n\t\treturn false\n\t}\n\n\treturn o.Do()\n}\n\n// setRespAD changes the request and response based on the server settings and\n// the original request data.\nfunc (s *Server) setRespAD(pctx *proxy.DNSContext, reqWantsDNSSEC bool) {\n\tif s.conf.EnableDNSSEC && !reqWantsDNSSEC {\n\t\tpctx.Req.AuthenticatedData = false\n\t\tpctx.Res.AuthenticatedData = false\n\t}\n}\n\n// dhcpHostFromRequest returns a hostname from question, if the request is for a\n// DHCP client's hostname when DHCP is enabled, and an empty string otherwise.\nfunc (s *Server) dhcpHostFromRequest(q *dns.Question) (reqHost string) {\n\tif !s.dhcpServer.Enabled() {\n\t\treturn \"\"\n\t}\n\n\t// Include AAAA here, because despite the fact that we don't support it yet,\n\t// the expected behavior here is to respond with an empty answer and not\n\t// NXDOMAIN.\n\tif qt := q.Qtype; qt != dns.TypeA && qt != dns.TypeAAAA {\n\t\treturn \"\"\n\t}\n\n\treqHost = strings.ToLower(q.Name[:len(q.Name)-1])\n\tif !netutil.IsSubdomain(reqHost, s.localDomainSuffix) {\n\t\treturn \"\"\n\t}\n\n\treturn reqHost[:len(reqHost)-len(s.localDomainSuffix)-1]\n}\n\n// setCustomUpstream sets custom upstream settings in pctx, if necessary.\nfunc (s *Server) setCustomUpstream(ctx context.Context, pctx *proxy.DNSContext, clientID string) {\n\tif !pctx.Addr.IsValid() || s.conf.ClientsContainer == nil {\n\t\treturn\n\t}\n\n\tcliAddr := pctx.Addr.Addr()\n\tupsConf := s.conf.ClientsContainer.CustomUpstreamConfig(clientID, cliAddr)\n\tif upsConf != nil {\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"using custom upstreams for client with\",\n\t\t\t\"ip\", cliAddr,\n\t\t\t\"client_id\", clientID,\n\t\t)\n\n\t\tpctx.CustomUpstreamConfig = upsConf\n\t}\n}\n\n// Apply filtering logic after we have received response from upstream servers\nfunc (s *Server) processFilteringAfterResponse(ctx context.Context, dctx *dnsContext) (rc resultCode) {\n\ts.logger.DebugContext(ctx, \"started processing filtering after response\")\n\tdefer s.logger.DebugContext(ctx, \"finished processing filtering after response\")\n\n\tswitch res := dctx.result; res.Reason {\n\tcase filtering.NotFilteredAllowList:\n\t\treturn resultCodeSuccess\n\tcase\n\t\tfiltering.Rewritten,\n\t\tfiltering.RewrittenRule,\n\t\tfiltering.FilteredSafeSearch:\n\n\t\tif dctx.origQuestion.Name == \"\" {\n\t\t\t// origQuestion is set in case we get only CNAME without IP from\n\t\t\t// rewrites table.\n\t\t\treturn resultCodeSuccess\n\t\t}\n\n\t\tpctx := dctx.proxyCtx\n\t\tpctx.Req.Question[0], pctx.Res.Question[0] = dctx.origQuestion, dctx.origQuestion\n\n\t\trr := s.genAnswerCNAME(pctx.Req, res.CanonName)\n\t\tanswer := append([]dns.RR{rr}, pctx.Res.Answer...)\n\t\tpctx.Res.Answer = answer\n\n\t\treturn resultCodeSuccess\n\tdefault:\n\t\treturn s.filterAfterResponse(ctx, dctx)\n\t}\n}\n\n// filterAfterResponse returns the result of filtering the response that wasn't\n// explicitly allowed or rewritten.\nfunc (s *Server) filterAfterResponse(ctx context.Context, dctx *dnsContext) (res resultCode) {\n\t// Check the response only if it's from an upstream.  Don't check the\n\t// response if the protection is disabled since dnsrewrite rules aren't\n\t// applied to it anyway.\n\tif !dctx.protectionEnabled || !dctx.responseFromUpstream {\n\t\treturn resultCodeSuccess\n\t}\n\n\terr := s.filterDNSResponse(ctx, dctx)\n\tif err != nil {\n\t\tdctx.err = err\n\n\t\treturn resultCodeError\n\t}\n\n\treturn resultCodeSuccess\n}\n"
  },
  {
    "path": "internal/dnsforward/process_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tddrTestDomainName = \"dns.example.net\"\n\tddrTestFQDN       = ddrTestDomainName + \".\"\n)\n\nfunc TestServer_ProcessInitial(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname         string\n\t\ttarget       string\n\t\twantRCode    rules.RCode\n\t\tqType        rules.RRType\n\t\taaaaDisabled bool\n\t\twantRC       resultCode\n\t}{{\n\t\tname:         \"success\",\n\t\ttarget:       testQuestionTarget,\n\t\twantRCode:    -1,\n\t\tqType:        dns.TypeA,\n\t\taaaaDisabled: false,\n\t\twantRC:       resultCodeSuccess,\n\t}, {\n\t\tname:         \"aaaa_disabled\",\n\t\ttarget:       testQuestionTarget,\n\t\twantRCode:    dns.RcodeSuccess,\n\t\tqType:        dns.TypeAAAA,\n\t\taaaaDisabled: true,\n\t\twantRC:       resultCodeFinish,\n\t}, {\n\t\tname:         \"aaaa_disabled_a\",\n\t\ttarget:       testQuestionTarget,\n\t\twantRCode:    -1,\n\t\tqType:        dns.TypeA,\n\t\taaaaDisabled: true,\n\t\twantRC:       resultCodeSuccess,\n\t}, {\n\t\tname:         \"mozilla_canary\",\n\t\ttarget:       mozillaFQDN,\n\t\twantRCode:    dns.RcodeNameError,\n\t\tqType:        dns.TypeA,\n\t\taaaaDisabled: false,\n\t\twantRC:       resultCodeFinish,\n\t}, {\n\t\tname:         \"adguardhome_healthcheck\",\n\t\ttarget:       healthcheckFQDN,\n\t\twantRCode:    dns.RcodeSuccess,\n\t\tqType:        dns.TypeA,\n\t\taaaaDisabled: false,\n\t\twantRC:       resultCodeFinish,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tc := ServerConfig{\n\t\t\t\tTLSConf: &TLSConfig{},\n\t\t\t\tConfig: Config{\n\t\t\t\t\tAAAADisabled:     tc.aaaaDisabled,\n\t\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t\t},\n\t\t\t\tServePlainDNS: true,\n\t\t\t}\n\n\t\t\ts := createTestServer(t, &filtering.Config{\n\t\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t\t}, c)\n\n\t\t\tvar gotAddr netip.Addr\n\t\t\ts.addrProc = &aghtest.AddressProcessor{\n\t\t\t\tOnProcess: func(ctx context.Context, ip netip.Addr) { gotAddr = ip },\n\t\t\t\tOnClose:   func() (_ error) { panic(testutil.UnexpectedCall()) },\n\t\t\t}\n\n\t\t\tdctx := &dnsContext{\n\t\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\t\tReq:       createTestMessageWithType(tc.target, tc.qType),\n\t\t\t\t\tAddr:      testClientAddrPort,\n\t\t\t\t\tRequestID: 1234,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tgotRC := s.processInitial(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\t\t\tassert.Equal(t, tc.wantRC, gotRC)\n\t\t\tassert.Equal(t, testClientAddrPort.Addr(), gotAddr)\n\n\t\t\tif tc.wantRCode > 0 {\n\t\t\t\tgotResp := dctx.proxyCtx.Res\n\t\t\t\trequire.NotNil(t, gotResp)\n\n\t\t\t\tassert.Equal(t, tc.wantRCode, gotResp.Rcode)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestServer_ProcessFilteringAfterResponse(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\ttestIPv4 net.IP = netip.MustParseAddr(\"1.1.1.1\").AsSlice()\n\t\ttestIPv6 net.IP = netip.MustParseAddr(\"1234::cdef\").AsSlice()\n\t)\n\n\ttestCases := []struct {\n\t\tname         string\n\t\treq          *dns.Msg\n\t\taaaaDisabled bool\n\t\trespAns      []dns.RR\n\t\twantRC       resultCode\n\t\twantRespAns  []dns.RR\n\t}{{\n\t\tname:         \"pass\",\n\t\treq:          createTestMessageWithType(aghtest.ReqFQDN, dns.TypeHTTPS),\n\t\taaaaDisabled: false,\n\t\trespAns: newSVCBHintsAnswer(\n\t\t\taghtest.ReqFQDN,\n\t\t\t[]dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBIPv4Hint{Hint: []net.IP{testIPv4}},\n\t\t\t\t&dns.SVCBIPv6Hint{Hint: []net.IP{testIPv6}},\n\t\t\t},\n\t\t),\n\t\twantRespAns: newSVCBHintsAnswer(\n\t\t\taghtest.ReqFQDN,\n\t\t\t[]dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBIPv4Hint{Hint: []net.IP{testIPv4}},\n\t\t\t\t&dns.SVCBIPv6Hint{Hint: []net.IP{testIPv6}},\n\t\t\t},\n\t\t),\n\t\twantRC: resultCodeSuccess,\n\t}, {\n\t\tname:         \"filter\",\n\t\treq:          createTestMessageWithType(aghtest.ReqFQDN, dns.TypeHTTPS),\n\t\taaaaDisabled: true,\n\t\trespAns: newSVCBHintsAnswer(\n\t\t\taghtest.ReqFQDN,\n\t\t\t[]dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBIPv4Hint{Hint: []net.IP{testIPv4}},\n\t\t\t\t&dns.SVCBIPv6Hint{Hint: []net.IP{testIPv6}},\n\t\t\t},\n\t\t),\n\t\twantRespAns: newSVCBHintsAnswer(\n\t\t\taghtest.ReqFQDN,\n\t\t\t[]dns.SVCBKeyValue{\n\t\t\t\t&dns.SVCBIPv4Hint{Hint: []net.IP{testIPv4}},\n\t\t\t},\n\t\t),\n\t\twantRC: resultCodeSuccess,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tc := ServerConfig{\n\t\t\t\tTLSConf: &TLSConfig{},\n\t\t\t\tConfig: Config{\n\t\t\t\t\tAAAADisabled:     tc.aaaaDisabled,\n\t\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t\t},\n\t\t\t\tServePlainDNS: true,\n\t\t\t}\n\n\t\t\ts := createTestServer(t, &filtering.Config{\n\t\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t\t}, c)\n\n\t\t\tresp := newResp(dns.RcodeSuccess, tc.req, tc.respAns)\n\t\t\tdctx := &dnsContext{\n\t\t\t\tsetts: &filtering.Settings{\n\t\t\t\t\tFilteringEnabled:  true,\n\t\t\t\t\tProtectionEnabled: true,\n\t\t\t\t},\n\t\t\t\tprotectionEnabled:    true,\n\t\t\t\tresponseFromUpstream: true,\n\t\t\t\tresult:               &filtering.Result{},\n\t\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\t\tProto: proxy.ProtoUDP,\n\t\t\t\t\tReq:   tc.req,\n\t\t\t\t\tRes:   resp,\n\t\t\t\t\tAddr:  testClientAddrPort,\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tgotRC := s.processFilteringAfterResponse(ctx, dctx)\n\t\t\tassert.Equal(t, tc.wantRC, gotRC)\n\t\t\tassert.Equal(t, newResp(dns.RcodeSuccess, tc.req, tc.wantRespAns), dctx.proxyCtx.Res)\n\t\t})\n\t}\n}\n\nfunc TestServer_ProcessDDRQuery(t *testing.T) {\n\tdohSVCB := &dns.SVCB{\n\t\tPriority: 1,\n\t\tTarget:   ddrTestFQDN,\n\t\tValue: []dns.SVCBKeyValue{\n\t\t\t&dns.SVCBAlpn{Alpn: []string{\"h2\"}},\n\t\t\t&dns.SVCBPort{Port: 8044},\n\t\t\t&dns.SVCBDoHPath{Template: \"/dns-query{?dns}\"},\n\t\t},\n\t}\n\n\tdotSVCB := &dns.SVCB{\n\t\tPriority: 1,\n\t\tTarget:   ddrTestFQDN,\n\t\tValue: []dns.SVCBKeyValue{\n\t\t\t&dns.SVCBAlpn{Alpn: []string{\"dot\"}},\n\t\t\t&dns.SVCBPort{Port: 8043},\n\t\t},\n\t}\n\n\tdoqSVCB := &dns.SVCB{\n\t\tPriority: 1,\n\t\tTarget:   ddrTestFQDN,\n\t\tValue: []dns.SVCBKeyValue{\n\t\t\t&dns.SVCBAlpn{Alpn: []string{\"doq\"}},\n\t\t\t&dns.SVCBPort{Port: 8042},\n\t\t},\n\t}\n\n\taddrsDoH := []netip.AddrPort{netip.AddrPortFrom(netutil.IPv4Localhost(), 8044)}\n\taddrsDoT := []*net.TCPAddr{{Port: 8043}}\n\taddrsDoQ := []*net.UDPAddr{{Port: 8042}}\n\n\ttestCases := []struct {\n\t\tname       string\n\t\thost       string\n\t\twant       []*dns.SVCB\n\t\twantRes    resultCode\n\t\taddrsDoH   []netip.AddrPort\n\t\taddrsDoT   []*net.TCPAddr\n\t\taddrsDoQ   []*net.UDPAddr\n\t\tqtype      uint16\n\t\tddrEnabled bool\n\t}{{\n\t\tname:       \"pass_host\",\n\t\twantRes:    resultCodeSuccess,\n\t\thost:       testQuestionTarget,\n\t\tqtype:      dns.TypeSVCB,\n\t\tddrEnabled: true,\n\t\taddrsDoH:   addrsDoH,\n\t}, {\n\t\tname:       \"pass_qtype\",\n\t\twantRes:    resultCodeFinish,\n\t\thost:       ddrHostFQDN,\n\t\tqtype:      dns.TypeA,\n\t\tddrEnabled: true,\n\t\taddrsDoH:   addrsDoH,\n\t}, {\n\t\tname:       \"pass_disabled_tls\",\n\t\twantRes:    resultCodeFinish,\n\t\thost:       ddrHostFQDN,\n\t\tqtype:      dns.TypeSVCB,\n\t\tddrEnabled: true,\n\t}, {\n\t\tname:       \"pass_disabled_ddr\",\n\t\twantRes:    resultCodeSuccess,\n\t\thost:       ddrHostFQDN,\n\t\tqtype:      dns.TypeSVCB,\n\t\tddrEnabled: false,\n\t\taddrsDoH:   addrsDoH,\n\t}, {\n\t\tname:       \"dot\",\n\t\twantRes:    resultCodeFinish,\n\t\twant:       []*dns.SVCB{dotSVCB},\n\t\thost:       ddrHostFQDN,\n\t\tqtype:      dns.TypeSVCB,\n\t\tddrEnabled: true,\n\t\taddrsDoT:   addrsDoT,\n\t}, {\n\t\tname:       \"doh\",\n\t\twantRes:    resultCodeFinish,\n\t\twant:       []*dns.SVCB{dohSVCB},\n\t\thost:       ddrHostFQDN,\n\t\tqtype:      dns.TypeSVCB,\n\t\tddrEnabled: true,\n\t\taddrsDoH:   addrsDoH,\n\t}, {\n\t\tname:       \"doq\",\n\t\twantRes:    resultCodeFinish,\n\t\twant:       []*dns.SVCB{doqSVCB},\n\t\thost:       ddrHostFQDN,\n\t\tqtype:      dns.TypeSVCB,\n\t\tddrEnabled: true,\n\t\taddrsDoQ:   addrsDoQ,\n\t}, {\n\t\tname:       \"dot_doh\",\n\t\twantRes:    resultCodeFinish,\n\t\twant:       []*dns.SVCB{dotSVCB, dohSVCB},\n\t\thost:       ddrHostFQDN,\n\t\tqtype:      dns.TypeSVCB,\n\t\tddrEnabled: true,\n\t\taddrsDoT:   addrsDoT,\n\t\taddrsDoH:   addrsDoH,\n\t}}\n\n\t_, certPem, keyPem := createServerTLSConfig(t)\n\tcert, err := tls.X509KeyPair(certPem, keyPem)\n\trequire.NoError(t, err)\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := createTestServer(t, &filtering.Config{\n\t\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t\t}, ServerConfig{\n\t\t\t\tConfig: Config{\n\t\t\t\t\tHandleDDR:        tc.ddrEnabled,\n\t\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t\t},\n\t\t\t\tTLSConf: &TLSConfig{\n\t\t\t\t\tServerName:       ddrTestDomainName,\n\t\t\t\t\tCert:             &cert,\n\t\t\t\t\tTLSListenAddrs:   tc.addrsDoT,\n\t\t\t\t\tHTTPSListenAddrs: tc.addrsDoH,\n\t\t\t\t\tQUICListenAddrs:  tc.addrsDoQ,\n\t\t\t\t},\n\t\t\t\tServePlainDNS: true,\n\t\t\t})\n\t\t\t// TODO(e.burkov):  Generate a certificate actually containing the\n\t\t\t// IP addresses.\n\t\t\ts.hasIPAddrs = true\n\n\t\t\treq := createTestMessageWithType(tc.host, tc.qtype)\n\n\t\t\tdctx := &dnsContext{\n\t\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\t\tReq: req,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tres := s.processDDRQuery(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\t\t\trequire.Equal(t, tc.wantRes, res)\n\n\t\t\tif tc.wantRes != resultCodeFinish {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmsg := dctx.proxyCtx.Res\n\t\t\trequire.NotNil(t, msg)\n\n\t\t\tfor _, v := range tc.want {\n\t\t\t\tv.Hdr = s.hdr(req, dns.TypeSVCB)\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, tc.want, msg.Answer)\n\t\t})\n\t}\n}\n\n// createTestDNSFilter returns the minimum valid DNSFilter.\nfunc createTestDNSFilter(tb testing.TB) (f *filtering.DNSFilter) {\n\ttb.Helper()\n\n\tf, err := filtering.New(&filtering.Config{\n\t\tLogger:       testLogger,\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, []filtering.Filter{})\n\trequire.NoError(tb, err)\n\n\treturn f\n}\n\nfunc TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {\n\tconst (\n\t\tlocalDomainSuffix = \"lan\"\n\t\tdhcpClient        = \"example\"\n\n\t\tknownHost   = dhcpClient + \".\" + localDomainSuffix\n\t\tunknownHost = \"wronghost.\" + localDomainSuffix\n\t)\n\n\tknownIP := netip.MustParseAddr(\"1.2.3.4\")\n\tdhcp := &testDHCP{\n\t\tOnEnabled: func() (_ bool) { return true },\n\t\tOnIPByHost: func(host string) (ip netip.Addr) {\n\t\t\tif host == dhcpClient {\n\t\t\t\tip = knownIP\n\t\t\t}\n\n\t\t\treturn ip\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\twantIP     netip.Addr\n\t\tname       string\n\t\thost       string\n\t\tisLocalCli bool\n\t}{{\n\t\twantIP:     knownIP,\n\t\tname:       \"local_client_success\",\n\t\thost:       knownHost,\n\t\tisLocalCli: true,\n\t}, {\n\t\twantIP:     netip.Addr{},\n\t\tname:       \"local_client_unknown_host\",\n\t\thost:       unknownHost,\n\t\tisLocalCli: true,\n\t}, {\n\t\twantIP:     netip.Addr{},\n\t\tname:       \"external_client_known_host\",\n\t\thost:       knownHost,\n\t\tisLocalCli: false,\n\t}, {\n\t\twantIP:     netip.Addr{},\n\t\tname:       \"external_client_unknown_host\",\n\t\thost:       unknownHost,\n\t\tisLocalCli: false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := &Server{\n\t\t\t\tdnsFilter:         createTestDNSFilter(t),\n\t\t\t\tdhcpServer:        dhcp,\n\t\t\t\tlocalDomainSuffix: localDomainSuffix,\n\t\t\t\tbaseLogger:        testLogger,\n\t\t\t\tlogger:            testLogger,\n\t\t\t}\n\n\t\t\treq := &dns.Msg{\n\t\t\t\tMsgHdr: dns.MsgHdr{\n\t\t\t\t\tId: dns.Id(),\n\t\t\t\t},\n\t\t\t\tQuestion: []dns.Question{{\n\t\t\t\t\tName:   dns.Fqdn(tc.host),\n\t\t\t\t\tQtype:  dns.TypeA,\n\t\t\t\t\tQclass: dns.ClassINET,\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tdctx := &dnsContext{\n\t\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\t\tReq:             req,\n\t\t\t\t\tIsPrivateClient: tc.isLocalCli,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tres := s.processDHCPHosts(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\n\t\t\tpctx := dctx.proxyCtx\n\t\t\tif !tc.isLocalCli {\n\t\t\t\trequire.Equal(t, resultCodeFinish, res)\n\t\t\t\trequire.NotNil(t, pctx.Res)\n\n\t\t\t\tassert.Equal(t, dns.RcodeNameError, pctx.Res.Rcode)\n\t\t\t\tassert.Empty(t, pctx.Res.Answer)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Equal(t, resultCodeSuccess, res)\n\n\t\t\tif tc.wantIP == (netip.Addr{}) {\n\t\t\t\tassert.Nil(t, pctx.Res)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NotNil(t, pctx.Res)\n\n\t\t\tans := pctx.Res.Answer\n\t\t\trequire.Len(t, ans, 1)\n\n\t\t\ta := testutil.RequireTypeAssert[*dns.A](t, ans[0])\n\n\t\t\tip, err := netutil.IPToAddr(a.A, netutil.AddrFamilyIPv4)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.wantIP, ip)\n\t\t})\n\t}\n}\n\nfunc TestServer_ProcessDHCPHosts(t *testing.T) {\n\tconst (\n\t\tlocalTLD  = \"lan\"\n\t\tcustomTLD = \"custom\"\n\n\t\toneLabelClient     = \"example\"\n\t\ttwoLabelClient     = \"sub.example\"\n\t\texternalHost       = oneLabelClient + \".com\"\n\t\toneLabelClientHost = oneLabelClient + \".\" + localTLD\n\t\ttwoLabelClientHost = twoLabelClient + \".\" + localTLD\n\t)\n\n\tknownClients := map[string]netip.Addr{\n\t\toneLabelClient: netip.MustParseAddr(\"1.2.3.4\"),\n\t\ttwoLabelClient: netip.MustParseAddr(\"1.2.3.5\"),\n\t}\n\n\ttestDHCP := &testDHCP{\n\t\tOnEnabled: func() (ok bool) { return true },\n\t\tOnIPByHost: func(host string) (ip netip.Addr) {\n\t\t\treturn knownClients[host]\n\t\t},\n\t\tOnHostByIP: func(ip netip.Addr) (_ string) { panic(testutil.UnexpectedCall(ip)) },\n\t}\n\n\ttestCases := []struct {\n\t\twantIP  netip.Addr\n\t\tname    string\n\t\thost    string\n\t\tsuffix  string\n\t\twantRes resultCode\n\t\tqtyp    uint16\n\t}{{\n\t\twantIP:  netip.Addr{},\n\t\tname:    \"external\",\n\t\thost:    externalHost,\n\t\tsuffix:  localTLD,\n\t\twantRes: resultCodeSuccess,\n\t\tqtyp:    dns.TypeA,\n\t}, {\n\t\twantIP:  netip.Addr{},\n\t\tname:    \"external_non_a\",\n\t\thost:    externalHost,\n\t\tsuffix:  localTLD,\n\t\twantRes: resultCodeSuccess,\n\t\tqtyp:    dns.TypeCNAME,\n\t}, {\n\t\twantIP:  knownClients[oneLabelClient],\n\t\tname:    \"internal\",\n\t\thost:    oneLabelClientHost,\n\t\tsuffix:  localTLD,\n\t\twantRes: resultCodeSuccess,\n\t\tqtyp:    dns.TypeA,\n\t}, {\n\t\twantIP:  knownClients[twoLabelClient],\n\t\tname:    \"internal_two_label\",\n\t\thost:    twoLabelClientHost,\n\t\tsuffix:  localTLD,\n\t\twantRes: resultCodeSuccess,\n\t\tqtyp:    dns.TypeA,\n\t}, {\n\t\twantIP:  netip.Addr{},\n\t\tname:    \"internal_unknown\",\n\t\thost:    \"example-new.lan\",\n\t\tsuffix:  localTLD,\n\t\twantRes: resultCodeSuccess,\n\t\tqtyp:    dns.TypeA,\n\t}, {\n\t\twantIP:  netip.Addr{},\n\t\tname:    \"internal_aaaa\",\n\t\thost:    oneLabelClientHost,\n\t\tsuffix:  localTLD,\n\t\twantRes: resultCodeSuccess,\n\t\tqtyp:    dns.TypeAAAA,\n\t}, {\n\t\twantIP:  knownClients[oneLabelClient],\n\t\tname:    \"custom_suffix\",\n\t\thost:    oneLabelClient + \".\" + customTLD,\n\t\tsuffix:  customTLD,\n\t\twantRes: resultCodeSuccess,\n\t\tqtyp:    dns.TypeA,\n\t}, {\n\t\twantIP:  knownClients[twoLabelClient],\n\t\tname:    \"custom_suffix_two_label\",\n\t\thost:    twoLabelClient + \".\" + customTLD,\n\t\tsuffix:  customTLD,\n\t\twantRes: resultCodeSuccess,\n\t\tqtyp:    dns.TypeA,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\ts := &Server{\n\t\t\tdnsFilter:         createTestDNSFilter(t),\n\t\t\tdhcpServer:        testDHCP,\n\t\t\tlocalDomainSuffix: tc.suffix,\n\t\t\tbaseLogger:        testLogger,\n\t\t\tlogger:            testLogger,\n\t\t}\n\n\t\treq := (&dns.Msg{}).SetQuestion(dns.Fqdn(tc.host), tc.qtyp)\n\n\t\tdctx := &dnsContext{\n\t\t\tproxyCtx: &proxy.DNSContext{\n\t\t\t\tReq:             req,\n\t\t\t\tIsPrivateClient: true,\n\t\t\t},\n\t\t}\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tres := s.processDHCPHosts(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\t\t\tpctx := dctx.proxyCtx\n\t\t\tassert.Equal(t, tc.wantRes, res)\n\t\t\trequire.NoError(t, dctx.err)\n\n\t\t\tif tc.qtyp == dns.TypeAAAA {\n\t\t\t\t// TODO(a.garipov): Remove this special handling when we fully\n\t\t\t\t// support AAAA.\n\t\t\t\trequire.NotNil(t, pctx.Res)\n\n\t\t\t\tans := pctx.Res.Answer\n\t\t\t\trequire.Len(t, ans, 0)\n\t\t\t} else if tc.wantIP == (netip.Addr{}) {\n\t\t\t\tassert.Nil(t, pctx.Res)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, pctx.Res)\n\n\t\t\t\tans := pctx.Res.Answer\n\t\t\t\trequire.Len(t, ans, 1)\n\n\t\t\t\ta := testutil.RequireTypeAssert[*dns.A](t, ans[0])\n\n\t\t\t\tip, err := netutil.IPToAddr(a.A, netutil.AddrFamilyIPv4)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, tc.wantIP, ip)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestServer_ProcessUpstream_localPTR(t *testing.T) {\n\tconst locDomain = \"some.local.\"\n\tconst reqAddr = \"1.1.168.192.in-addr.arpa.\"\n\n\tlocalUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tresp := cmp.Or(\n\t\t\taghtest.MatchedResponse(req, dns.TypePTR, reqAddr, locDomain),\n\t\t\t(&dns.Msg{}).SetRcode(req, dns.RcodeNameError),\n\t\t)\n\n\t\trequire.NoError(testutil.PanicT{}, w.WriteMsg(resp))\n\t})\n\tlocalUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()\n\n\tnewPrxCtx := func() (prxCtx *proxy.DNSContext) {\n\t\treturn &proxy.DNSContext{\n\t\t\tAddr:                 testClientAddrPort,\n\t\t\tReq:                  createTestMessageWithType(reqAddr, dns.TypePTR),\n\t\t\tIsPrivateClient:      true,\n\t\t\tRequestedPrivateRDNS: netip.MustParsePrefix(\"192.168.1.1/32\"),\n\t\t}\n\t}\n\n\tt.Run(\"enabled\", func(t *testing.T) {\n\t\ts := createTestServer(\n\t\t\tt,\n\t\t\t&filtering.Config{\n\t\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t\t},\n\t\t\tServerConfig{\n\t\t\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\t\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\t\t\tTLSConf:        &TLSConfig{},\n\t\t\t\tConfig: Config{\n\t\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t\t},\n\t\t\t\tUsePrivateRDNS:    true,\n\t\t\t\tLocalPTRResolvers: []string{localUpsAddr},\n\t\t\t\tServePlainDNS:     true,\n\t\t\t},\n\t\t)\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\tpctx := newPrxCtx()\n\t\trc := s.processUpstream(ctx, &dnsContext{proxyCtx: pctx})\n\t\trequire.Equal(t, resultCodeSuccess, rc)\n\t\trequire.NotEmpty(t, pctx.Res.Answer)\n\t\tptr := testutil.RequireTypeAssert[*dns.PTR](t, pctx.Res.Answer[0])\n\n\t\tassert.Equal(t, locDomain, ptr.Ptr)\n\t})\n\n\tt.Run(\"disabled\", func(t *testing.T) {\n\t\ts := createTestServer(\n\t\t\tt,\n\t\t\t&filtering.Config{\n\t\t\t\tBlockingMode: filtering.BlockingModeDefault,\n\t\t\t},\n\t\t\tServerConfig{\n\t\t\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\t\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\t\t\tTLSConf:        &TLSConfig{},\n\t\t\t\tConfig: Config{\n\t\t\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t\t\t},\n\t\t\t\tUsePrivateRDNS:    false,\n\t\t\t\tLocalPTRResolvers: []string{localUpsAddr},\n\t\t\t\tServePlainDNS:     true,\n\t\t\t},\n\t\t)\n\t\tpctx := newPrxCtx()\n\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\trc := s.processUpstream(ctx, &dnsContext{proxyCtx: pctx})\n\t\trequire.Equal(t, resultCodeError, rc)\n\t\trequire.Empty(t, pctx.Res.Answer)\n\t})\n}\n\nfunc TestIPStringFromAddr(t *testing.T) {\n\tt.Run(\"not_nil\", func(t *testing.T) {\n\t\taddr := net.UDPAddr{\n\t\t\tIP:   net.ParseIP(\"1:2:3::4\"),\n\t\t\tPort: 12345,\n\t\t\tZone: \"eth0\",\n\t\t}\n\t\tassert.Equal(t, ipStringFromAddr(&addr), addr.IP.String())\n\t})\n\n\tt.Run(\"nil\", func(t *testing.T) {\n\t\tassert.Empty(t, ipStringFromAddr(nil))\n\t})\n}\n"
  },
  {
    "path": "internal/dnsforward/requesthandler.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n)\n\n// type check\nvar _ proxy.Handler = (*Server)(nil)\n\n// ServeDNS implements the [proxy.Handler] interface for [*Server].\n//\n// TODO(d.kolyshev):  Use logger from context.\nfunc (s *Server) ServeDNS(ctx context.Context, _ *proxy.Proxy, pctx *proxy.DNSContext) (err error) {\n\tdctx := &dnsContext{\n\t\tproxyCtx:  pctx,\n\t\tresult:    &filtering.Result{},\n\t\tstartTime: time.Now(),\n\t}\n\n\ttype modProcessFunc func(ctx context.Context, dctx *dnsContext) (rc resultCode)\n\n\t// Since [*dnsforward.Server] is used as [proxy.Handler], there is no need\n\t// for additional index out of range checking in any of the following\n\t// functions, because the (*proxy.Proxy).handleDNSRequest method performs it\n\t// before calling the appropriate handler.\n\tmods := []modProcessFunc{\n\t\ts.processInitial,\n\t\ts.processDDRQuery,\n\t\ts.processDHCPHosts,\n\t\ts.processDHCPAddrs,\n\t\ts.processFilteringBeforeRequest,\n\t\ts.processUpstream,\n\t\ts.processFilteringAfterResponse,\n\t\ts.ipset.process,\n\t\ts.processQueryLogsAndStats,\n\t}\n\tfor _, process := range mods {\n\t\tr := process(ctx, dctx)\n\t\tswitch r {\n\t\tcase resultCodeSuccess:\n\t\t\t// continue: call the next filter\n\t\tcase resultCodeFinish:\n\t\t\treturn nil\n\t\tcase resultCodeError:\n\t\t\treturn dctx.err\n\t\t}\n\t}\n\n\tif pctx.Res != nil {\n\t\t// Some devices require DNS message compression.\n\t\tpctx.Res.Compress = true\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dnsforward/requesthandler_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"cmp\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestServer_ServeDNS(t *testing.T) {\n\trules := `\n||blocked.domain^\n@@||allowed.domain^\n||cname.specific^$dnstype=~CNAME\n||0.0.0.1^$dnstype=~A\n||::1^$dnstype=~AAAA\n0.0.0.0 duplicate.domain\n0.0.0.0 duplicate.domain\n0.0.0.0 blocked.by.hostrule\n`\n\n\tforwardConf := ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode: UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{\n\t\t\t\tEnabled: false,\n\t\t\t},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t}\n\tfilters := []filtering.Filter{{\n\t\tID: 0, Data: []byte(rules),\n\t}}\n\n\tf, err := filtering.New(&filtering.Config{\n\t\tLogger:               testLogger,\n\t\tProtectionEnabled:    true,\n\t\tApplyClientFiltering: applyEmptyClientFiltering,\n\t\tBlockedServices:      emptyFilteringBlockedServices(),\n\t\tBlockingMode:         filtering.BlockingModeDefault,\n\t}, filters)\n\trequire.NoError(t, err)\n\tf.SetEnabled(true)\n\n\ts, err := NewServer(DNSCreateParams{\n\t\tDHCPServer: &testDHCP{\n\t\t\tOnEnabled:  func() (ok bool) { return false },\n\t\t\tOnHostByIP: func(ip netip.Addr) (_ string) { panic(testutil.UnexpectedCall(ip)) },\n\t\t\tOnIPByHost: func(host string) (_ netip.Addr) { panic(testutil.UnexpectedCall(host)) },\n\t\t},\n\t\tDNSFilter:   f,\n\t\tPrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),\n\t\tLogger:      testLogger,\n\t})\n\trequire.NoError(t, err)\n\n\terr = s.Prepare(testutil.ContextWithTimeout(t, testTimeout), &forwardConf)\n\trequire.NoError(t, err)\n\n\ts.conf.UpstreamConfig.Upstreams = []upstream.Upstream{\n\t\t&aghtest.Upstream{\n\t\t\tCName: map[string][]string{\n\t\t\t\t\"cname.exception.\": {\"cname.specific.\"},\n\t\t\t\t\"should.block.\":    {\"blocked.domain.\"},\n\t\t\t\t\"allowed.first.\":   {\"allowed.domain.\", \"blocked.domain.\"},\n\t\t\t\t\"blocked.first.\":   {\"blocked.domain.\", \"allowed.domain.\"},\n\t\t\t},\n\t\t\tIPv4: map[string][]net.IP{\n\t\t\t\t\"a.exception.\": {{0, 0, 0, 1}},\n\t\t\t},\n\t\t\tIPv6: map[string][]net.IP{\n\t\t\t\t\"aaaa.exception.\": {net.ParseIP(\"::1\")},\n\t\t\t},\n\t\t},\n\t}\n\tstartDeferStop(t, s)\n\n\ttestCases := []struct {\n\t\treq       *dns.Msg\n\t\tname      string\n\t\twantRCode int\n\t\twantAns   []dns.RR\n\t}{{\n\t\treq:       createTestMessage(aghtest.ReqFQDN),\n\t\tname:      \"pass\",\n\t\twantRCode: dns.RcodeNameError,\n\t\twantAns:   nil,\n\t}, {\n\t\treq:       createTestMessage(\"cname.exception.\"),\n\t\tname:      \"cname_exception\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns: []dns.RR{&dns.CNAME{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   \"cname.exception.\",\n\t\t\t\tRrtype: dns.TypeCNAME,\n\t\t\t},\n\t\t\tTarget: \"cname.specific.\",\n\t\t}},\n\t}, {\n\t\treq:       createTestMessage(\"should.block.\"),\n\t\tname:      \"blocked_by_cname\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   \"should.block.\",\n\t\t\t\tRrtype: dns.TypeA,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tA: netutil.IPv4Zero(),\n\t\t}},\n\t}, {\n\t\treq:       createTestMessage(\"a.exception.\"),\n\t\tname:      \"a_exception\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   \"a.exception.\",\n\t\t\t\tRrtype: dns.TypeA,\n\t\t\t},\n\t\t\tA: net.IP{0, 0, 0, 1},\n\t\t}},\n\t}, {\n\t\treq:       createTestMessageWithType(\"aaaa.exception.\", dns.TypeAAAA),\n\t\tname:      \"aaaa_exception\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns: []dns.RR{&dns.AAAA{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   \"aaaa.exception.\",\n\t\t\t\tRrtype: dns.TypeAAAA,\n\t\t\t},\n\t\t\tAAAA: net.ParseIP(\"::1\"),\n\t\t}},\n\t}, {\n\t\treq:       createTestMessage(\"allowed.first.\"),\n\t\tname:      \"allowed_first\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   \"allowed.first.\",\n\t\t\t\tRrtype: dns.TypeA,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tA: netutil.IPv4Zero(),\n\t\t}},\n\t}, {\n\t\treq:       createTestMessage(\"blocked.first.\"),\n\t\tname:      \"blocked_first\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   \"blocked.first.\",\n\t\t\t\tRrtype: dns.TypeA,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tA: netutil.IPv4Zero(),\n\t\t}},\n\t}, {\n\t\treq:       createTestMessage(\"duplicate.domain.\"),\n\t\tname:      \"duplicate_domain\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   \"duplicate.domain.\",\n\t\t\t\tRrtype: dns.TypeA,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tA: netutil.IPv4Zero(),\n\t\t}},\n\t}, {\n\t\treq:       createTestMessageWithType(\"blocked.domain.\", dns.TypeHTTPS),\n\t\tname:      \"blocked_https_req\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns:   nil,\n\t}, {\n\t\treq:       createTestMessageWithType(\"blocked.by.hostrule.\", dns.TypeHTTPS),\n\t\tname:      \"blocked_host_rule_https_req\",\n\t\twantRCode: dns.RcodeSuccess,\n\t\twantAns:   nil,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tdctx := &proxy.DNSContext{\n\t\t\tProto: proxy.ProtoUDP,\n\t\t\tReq:   tc.req,\n\t\t\tAddr:  testClientAddrPort,\n\t\t}\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr = s.ServeDNS(testutil.ContextWithTimeout(t, testTimeout), nil, dctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, dctx.Res)\n\n\t\t\tassert.Equal(t, tc.wantRCode, dctx.Res.Rcode)\n\t\t\tassert.Equal(t, tc.wantAns, dctx.Res.Answer)\n\t\t})\n\t}\n}\n\n// TODO(e.burkov):  Rewrite this test to use the whole server instead of just\n// testing the [Handle] method.  See comment on \"from_external_for_local\" test\n// case.\nfunc TestServer_ServeDNS_restrictLocal(t *testing.T) {\n\tintAddr := netip.MustParseAddr(\"192.168.1.1\")\n\tintPTRQuestion, err := netutil.IPToReversedAddr(intAddr.AsSlice())\n\trequire.NoError(t, err)\n\n\textAddr := netip.MustParseAddr(\"254.253.252.1\")\n\textPTRQuestion, err := netutil.IPToReversedAddr(extAddr.AsSlice())\n\trequire.NoError(t, err)\n\n\tconst (\n\t\textPTRAnswer = \"host1.example.net.\"\n\t\tintPTRAnswer = \"some.local-client.\"\n\t)\n\n\tlocalUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tresp := cmp.Or(\n\t\t\taghtest.MatchedResponse(req, dns.TypePTR, extPTRQuestion, extPTRAnswer),\n\t\t\taghtest.MatchedResponse(req, dns.TypePTR, intPTRQuestion, intPTRAnswer),\n\t\t\t(&dns.Msg{}).SetRcode(req, dns.RcodeNameError),\n\t\t)\n\n\t\trequire.NoError(testutil.PanicT{}, w.WriteMsg(resp))\n\t})\n\tlocalUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()\n\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tUDPListenAddrs: []*net.UDPAddr{{}},\n\t\tTCPListenAddrs: []*net.TCPAddr{{}},\n\t\tTLSConf:        &TLSConfig{},\n\t\t// TODO(s.chzhen):  Add tests where EDNSClientSubnet.Enabled is true.\n\t\t// Improve Config declaration for tests.\n\t\tConfig: Config{\n\t\t\tUpstreamDNS:      []string{localUpsAddr},\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tUsePrivateRDNS:    true,\n\t\tLocalPTRResolvers: []string{localUpsAddr},\n\t\tServePlainDNS:     true,\n\t})\n\tstartDeferStop(t, s)\n\n\ttestCases := []struct {\n\t\tname      string\n\t\tquestion  string\n\t\twantErr   error\n\t\twantAns   []dns.RR\n\t\tisPrivate bool\n\t}{{\n\t\tname:     \"from_local_for_external\",\n\t\tquestion: extPTRQuestion,\n\t\twantErr:  nil,\n\t\twantAns: []dns.RR{&dns.PTR{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     dns.Fqdn(extPTRQuestion),\n\t\t\t\tRrtype:   dns.TypePTR,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      60,\n\t\t\t\tRdlength: uint16(len(extPTRAnswer) + 1),\n\t\t\t},\n\t\t\tPtr: dns.Fqdn(extPTRAnswer),\n\t\t}},\n\t\tisPrivate: true,\n\t}, {\n\t\t// In theory this case is not reproducible because [proxy.Proxy] should\n\t\t// respond to such queries with NXDOMAIN before they reach\n\t\t// [Server.Handle].\n\t\tname:      \"from_external_for_local\",\n\t\tquestion:  intPTRQuestion,\n\t\twantErr:   upstream.ErrNoUpstreams,\n\t\twantAns:   nil,\n\t\tisPrivate: false,\n\t}, {\n\t\tname:     \"from_local_for_local\",\n\t\tquestion: intPTRQuestion,\n\t\twantErr:  nil,\n\t\twantAns: []dns.RR{&dns.PTR{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     dns.Fqdn(intPTRQuestion),\n\t\t\t\tRrtype:   dns.TypePTR,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      60,\n\t\t\t\tRdlength: uint16(len(intPTRAnswer) + 1),\n\t\t\t},\n\t\t\tPtr: dns.Fqdn(intPTRAnswer),\n\t\t}},\n\t\tisPrivate: true,\n\t}, {\n\t\tname:     \"from_external_for_external\",\n\t\tquestion: extPTRQuestion,\n\t\twantErr:  nil,\n\t\twantAns: []dns.RR{&dns.PTR{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:     dns.Fqdn(extPTRQuestion),\n\t\t\t\tRrtype:   dns.TypePTR,\n\t\t\t\tClass:    dns.ClassINET,\n\t\t\t\tTtl:      60,\n\t\t\t\tRdlength: uint16(len(extPTRAnswer) + 1),\n\t\t\t},\n\t\t\tPtr: dns.Fqdn(extPTRAnswer),\n\t\t}},\n\t\tisPrivate: false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tpref, extErr := netutil.ExtractReversedAddr(tc.question)\n\t\trequire.NoError(t, extErr)\n\n\t\treq := createTestMessageWithType(dns.Fqdn(tc.question), dns.TypePTR)\n\t\tpctx := &proxy.DNSContext{\n\t\t\tReq:             req,\n\t\t\tIsPrivateClient: tc.isPrivate,\n\t\t}\n\t\t// TODO(e.burkov):  Configure the subnet set properly.\n\t\tif netutil.IsLocallyServed(pref.Addr()) {\n\t\t\tpctx.RequestedPrivateRDNS = pref\n\t\t}\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr = s.ServeDNS(testutil.ContextWithTimeout(t, testTimeout), s.dnsProxy, pctx)\n\t\t\trequire.ErrorIs(t, err, tc.wantErr)\n\n\t\t\trequire.NotNil(t, pctx.Res)\n\t\t\tassert.Equal(t, tc.wantAns, pctx.Res.Answer)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dnsforward/stats.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/querylog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/stats\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/miekg/dns\"\n)\n\n// Write Stats data and logs\nfunc (s *Server) processQueryLogsAndStats(ctx context.Context, dctx *dnsContext) (rc resultCode) {\n\ts.logger.DebugContext(ctx, \"started processing querylog and stats\")\n\tdefer s.logger.DebugContext(ctx, \"finished processing querylog and stats\")\n\n\tpctx := dctx.proxyCtx\n\tq := pctx.Req.Question[0]\n\thost := aghnet.NormalizeDomain(q.Name)\n\tprocessingTime := time.Since(dctx.startTime)\n\n\tip := pctx.Addr.Addr().AsSlice()\n\ts.anonymizer.Load()(ip)\n\tipStr := net.IP(ip).String()\n\n\ts.logger.DebugContext(ctx, \"client ip for stats and querylog\", \"ip\", ipStr)\n\n\tids := []string{ipStr}\n\tif dctx.clientID != \"\" {\n\t\t// Use the ClientID first because it has a higher priority.  Filters\n\t\t// have the same priority, see applyAdditionalFiltering.\n\t\tids = []string{dctx.clientID, ipStr}\n\t}\n\n\tqt, cl := q.Qtype, q.Qclass\n\n\t// Synchronize access to s.queryLog and s.stats so they won't be suddenly\n\t// uninitialized while in use.  This can happen after proxy server has been\n\t// stopped, but its workers haven't yet exited.\n\ts.serverLock.RLock()\n\tdefer s.serverLock.RUnlock()\n\n\tif s.shouldLog(host, qt, cl, ids) {\n\t\ts.logQuery(dctx, ip, processingTime)\n\t} else {\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"not adding to querylog\",\n\t\t\t\"dns_class\", dns.Class(cl),\n\t\t\t\"dns_type\", dns.Type(qt),\n\t\t\t\"host\", host,\n\t\t\t\"ip\", ipStr,\n\t\t)\n\t}\n\n\tif s.shouldCountStat(host, qt, cl, ids) {\n\t\ts.updateStats(dctx, ipStr, processingTime)\n\t} else {\n\t\ts.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"not counting in stats\",\n\t\t\t\"dns_class\", dns.Class(cl),\n\t\t\t\"dns_type\", dns.Type(qt),\n\t\t\t\"host\", host,\n\t\t\t\"ip\", ipStr,\n\t\t)\n\t}\n\n\treturn resultCodeSuccess\n}\n\n// shouldLog returns true if the query with the given data should be logged in\n// the query log.  s.serverLock is expected to be locked.\nfunc (s *Server) shouldLog(host string, qt, cl uint16, ids []string) (ok bool) {\n\tif qt == dns.TypeANY && s.conf.RefuseAny {\n\t\treturn false\n\t}\n\n\t// TODO(s.chzhen):  Use dnsforward.dnsContext when it will start containing\n\t// persistent client.\n\treturn s.queryLog != nil && s.queryLog.ShouldLog(host, qt, cl, ids)\n}\n\n// shouldCountStat returns true if the query with the given data should be\n// counted in the statistics.  s.serverLock is expected to be locked.\nfunc (s *Server) shouldCountStat(host string, qt, cl uint16, ids []string) (ok bool) {\n\t// TODO(s.chzhen):  Use dnsforward.dnsContext when it will start containing\n\t// persistent client.\n\treturn s.stats != nil && s.stats.ShouldCount(host, qt, cl, ids)\n}\n\n// logQuery pushes the request details into the query log.\nfunc (s *Server) logQuery(dctx *dnsContext, ip net.IP, processingTime time.Duration) {\n\tpctx := dctx.proxyCtx\n\n\tp := &querylog.AddParams{\n\t\tQuestion:          pctx.Req,\n\t\tReqECS:            pctx.ReqECS,\n\t\tAnswer:            pctx.Res,\n\t\tOrigAnswer:        dctx.origResp,\n\t\tResult:            dctx.result,\n\t\tClientID:          dctx.clientID,\n\t\tClientIP:          ip,\n\t\tElapsed:           processingTime,\n\t\tAuthenticatedData: dctx.responseAD,\n\t}\n\n\tswitch pctx.Proto {\n\tcase proxy.ProtoHTTPS:\n\t\tp.ClientProto = querylog.ClientProtoDoH\n\tcase proxy.ProtoQUIC:\n\t\tp.ClientProto = querylog.ClientProtoDoQ\n\tcase proxy.ProtoTLS:\n\t\tp.ClientProto = querylog.ClientProtoDoT\n\tcase proxy.ProtoDNSCrypt:\n\t\tp.ClientProto = querylog.ClientProtoDNSCrypt\n\tdefault:\n\t\t// Consider this a plain DNS-over-UDP or DNS-over-TCP request.\n\t}\n\n\tif pctx.Upstream != nil {\n\t\tp.Upstream = pctx.Upstream.Address()\n\t}\n\n\tif qs := pctx.QueryStatistics(); qs != nil {\n\t\tms := qs.Main()\n\t\tif len(ms) == 1 && ms[0].IsCached {\n\t\t\tp.Upstream = ms[0].Address\n\t\t\tp.Cached = true\n\t\t}\n\t}\n\n\ts.queryLog.Add(p)\n}\n\n// updateStats writes the request data into statistics.\nfunc (s *Server) updateStats(dctx *dnsContext, clientIP string, processingTime time.Duration) {\n\tpctx := dctx.proxyCtx\n\n\tvar upstreamStats []*proxy.UpstreamStatistics\n\tqs := pctx.QueryStatistics()\n\tif qs != nil {\n\t\tupstreamStats = append(upstreamStats, qs.Main()...)\n\t\tupstreamStats = append(upstreamStats, qs.Fallback()...)\n\t}\n\n\te := &stats.Entry{\n\t\tUpstreamStats:  upstreamStats,\n\t\tDomain:         aghnet.NormalizeDomain(pctx.Req.Question[0].Name),\n\t\tResult:         stats.RNotFiltered,\n\t\tProcessingTime: processingTime,\n\t}\n\n\tif clientID := dctx.clientID; clientID != \"\" {\n\t\te.Client = clientID\n\t} else {\n\t\te.Client = clientIP\n\t}\n\n\tswitch dctx.result.Reason {\n\tcase filtering.FilteredSafeBrowsing:\n\t\te.Result = stats.RSafeBrowsing\n\tcase filtering.FilteredParental:\n\t\te.Result = stats.RParental\n\tcase filtering.FilteredSafeSearch:\n\t\te.Result = stats.RSafeSearch\n\tcase\n\t\tfiltering.FilteredBlockList,\n\t\tfiltering.FilteredInvalid,\n\t\tfiltering.FilteredBlockedService:\n\t\te.Result = stats.RFiltered\n\t}\n\n\ts.stats.Update(e)\n}\n"
  },
  {
    "path": "internal/dnsforward/stats_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/querylog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/stats\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testQueryLog is a simple [querylog.QueryLog] implementation for tests.\ntype testQueryLog struct {\n\t// QueryLog is embedded here simply to make testQueryLog\n\t// a [querylog.QueryLog] without actually implementing all methods.\n\tquerylog.QueryLog\n\n\tlastParams *querylog.AddParams\n}\n\n// Add implements the [querylog.QueryLog] interface for *testQueryLog.\nfunc (l *testQueryLog) Add(p *querylog.AddParams) {\n\tl.lastParams = p\n}\n\n// ShouldLog implements the [querylog.QueryLog] interface for *testQueryLog.\nfunc (l *testQueryLog) ShouldLog(string, uint16, uint16, []string) bool {\n\treturn true\n}\n\n// testStats is a simple [stats.Interface] implementation for tests.\ntype testStats struct {\n\t// Stats is embedded here simply to make testStats a [stats.Interface]\n\t// without actually implementing all methods.\n\tstats.Interface\n\n\tlastEntry *stats.Entry\n}\n\n// Update implements the [stats.Interface] interface for *testStats.\nfunc (l *testStats) Update(e *stats.Entry) {\n\tif e.Domain == \"\" {\n\t\treturn\n\t}\n\n\tl.lastEntry = e\n}\n\n// ShouldCount implements the [stats.Interface] interface for *testStats.\nfunc (l *testStats) ShouldCount(string, uint16, uint16, []string) bool {\n\treturn true\n}\n\nfunc TestServer_ProcessQueryLogsAndStats(t *testing.T) {\n\tconst domain = \"example.com.\"\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tdomain         string\n\t\tproto          proxy.Proto\n\t\taddr           netip.AddrPort\n\t\tclientID       string\n\t\twantLogProto   querylog.ClientProto\n\t\twantStatClient string\n\t\twantCode       resultCode\n\t\treason         filtering.Reason\n\t\twantStatResult stats.Result\n\t}{{\n\t\tname:           \"success_udp\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoUDP,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   \"\",\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.NotFilteredNotFound,\n\t\twantStatResult: stats.RNotFiltered,\n\t}, {\n\t\tname:           \"success_tls_clientid\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoTLS,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"cli42\",\n\t\twantLogProto:   querylog.ClientProtoDoT,\n\t\twantStatClient: \"cli42\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.NotFilteredNotFound,\n\t\twantStatResult: stats.RNotFiltered,\n\t}, {\n\t\tname:           \"success_tls\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoTLS,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   querylog.ClientProtoDoT,\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.NotFilteredNotFound,\n\t\twantStatResult: stats.RNotFiltered,\n\t}, {\n\t\tname:           \"success_quic\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoQUIC,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   querylog.ClientProtoDoQ,\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.NotFilteredNotFound,\n\t\twantStatResult: stats.RNotFiltered,\n\t}, {\n\t\tname:           \"success_https\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoHTTPS,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   querylog.ClientProtoDoH,\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.NotFilteredNotFound,\n\t\twantStatResult: stats.RNotFiltered,\n\t}, {\n\t\tname:           \"success_dnscrypt\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoDNSCrypt,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   querylog.ClientProtoDNSCrypt,\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.NotFilteredNotFound,\n\t\twantStatResult: stats.RNotFiltered,\n\t}, {\n\t\tname:           \"success_udp_filtered\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoUDP,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   \"\",\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.FilteredBlockList,\n\t\twantStatResult: stats.RFiltered,\n\t}, {\n\t\tname:           \"success_udp_sb\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoUDP,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   \"\",\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.FilteredSafeBrowsing,\n\t\twantStatResult: stats.RSafeBrowsing,\n\t}, {\n\t\tname:           \"success_udp_ss\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoUDP,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   \"\",\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.FilteredSafeSearch,\n\t\twantStatResult: stats.RSafeSearch,\n\t}, {\n\t\tname:           \"success_udp_pc\",\n\t\tdomain:         domain,\n\t\tproto:          proxy.ProtoUDP,\n\t\taddr:           testClientAddrPort,\n\t\tclientID:       \"\",\n\t\twantLogProto:   \"\",\n\t\twantStatClient: \"1.2.3.4\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.FilteredParental,\n\t\twantStatResult: stats.RParental,\n\t}, {\n\t\tname:           \"success_udp_pc_empty_fqdn\",\n\t\tdomain:         \".\",\n\t\tproto:          proxy.ProtoUDP,\n\t\taddr:           netip.MustParseAddrPort(\"4.3.2.1:1234\"),\n\t\tclientID:       \"\",\n\t\twantLogProto:   \"\",\n\t\twantStatClient: \"4.3.2.1\",\n\t\twantCode:       resultCodeSuccess,\n\t\treason:         filtering.FilteredParental,\n\t\twantStatResult: stats.RParental,\n\t}}\n\n\tups, err := upstream.AddressToUpstream(\"1.1.1.1\", nil)\n\trequire.NoError(t, err)\n\n\tfor _, tc := range testCases {\n\t\tql := &testQueryLog{}\n\t\tst := &testStats{}\n\t\tsrv := &Server{\n\t\t\tbaseLogger: testLogger,\n\t\t\tlogger:     testLogger,\n\t\t\tqueryLog:   ql,\n\t\t\tstats:      st,\n\t\t\tanonymizer: aghnet.NewIPMut(nil),\n\t\t}\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := &dns.Msg{\n\t\t\t\tQuestion: []dns.Question{{\n\t\t\t\t\tName: tc.domain,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tpctx := &proxy.DNSContext{\n\t\t\t\tProto:    tc.proto,\n\t\t\t\tReq:      req,\n\t\t\t\tRes:      &dns.Msg{},\n\t\t\t\tAddr:     tc.addr,\n\t\t\t\tUpstream: ups,\n\t\t\t}\n\t\t\tdctx := &dnsContext{\n\t\t\t\tproxyCtx:  pctx,\n\t\t\t\tstartTime: time.Now(),\n\t\t\t\tresult: &filtering.Result{\n\t\t\t\t\tReason: tc.reason,\n\t\t\t\t},\n\t\t\t\tclientID: tc.clientID,\n\t\t\t}\n\n\t\t\tcode := srv.processQueryLogsAndStats(testutil.ContextWithTimeout(t, testTimeout), dctx)\n\t\t\tassert.Equal(t, tc.wantCode, code)\n\t\t\tassert.Equal(t, tc.wantLogProto, ql.lastParams.ClientProto)\n\t\t\tassert.Equal(t, tc.wantStatClient, st.lastEntry.Client)\n\t\t\tassert.Equal(t, tc.wantStatResult, st.lastEntry.Result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dnsforward/svcbmsg.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"log/slog\"\n\t\"net\"\n\t\"strconv\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// genAnswerHTTPS returns a properly initialized HTTPS resource record.\n//\n// See the comment on genAnswerSVCB for a list of current restrictions on\n// parameter values.\nfunc (s *Server) genAnswerHTTPS(ctx context.Context, req *dns.Msg, svcb *rules.DNSSVCB) (ans *dns.HTTPS) {\n\tans = &dns.HTTPS{\n\t\tSVCB: *s.genAnswerSVCB(ctx, req, svcb),\n\t}\n\n\tans.Hdr.Rrtype = dns.TypeHTTPS\n\n\treturn ans\n}\n\n// strToSVCBKey is the string-to-svcb-key mapping.\n//\n// See https://github.com/miekg/dns/blob/23c4faca9d32b0abbb6e179aa1aadc45ac53a916/svcb.go#L27.\n//\n// TODO(a.garipov): Propose exporting this API or something similar in the\n// github.com/miekg/dns module.\nvar strToSVCBKey = map[string]dns.SVCBKey{\n\t\"alpn\":            dns.SVCB_ALPN,\n\t\"ech\":             dns.SVCB_ECHCONFIG,\n\t\"ipv4hint\":        dns.SVCB_IPV4HINT,\n\t\"ipv6hint\":        dns.SVCB_IPV6HINT,\n\t\"mandatory\":       dns.SVCB_MANDATORY,\n\t\"no-default-alpn\": dns.SVCB_NO_DEFAULT_ALPN,\n\t\"port\":            dns.SVCB_PORT,\n\n\t// TODO(a.garipov): This is the previous name for the parameter that has\n\t// since been changed.  Remove this in v0.109.0.\n\t\"echconfig\": dns.SVCB_ECHCONFIG,\n}\n\n// svcbKeyHandler is a handler for one SVCB parameter key.  l must not be nil.\ntype svcbKeyHandler func(\n\tctx context.Context,\n\tl *slog.Logger,\n\tvalStr string,\n) (val dns.SVCBKeyValue)\n\n// svcbKeyHandlers are the supported SVCB parameters handlers.\nvar svcbKeyHandlers = map[string]svcbKeyHandler{\n\t\"alpn\": func(_ context.Context, _ *slog.Logger, valStr string) (val dns.SVCBKeyValue) {\n\t\treturn &dns.SVCBAlpn{\n\t\t\tAlpn: []string{valStr},\n\t\t}\n\t},\n\n\t\"ech\": func(ctx context.Context, l *slog.Logger, valStr string) (val dns.SVCBKeyValue) {\n\t\tech, err := base64.StdEncoding.DecodeString(valStr)\n\t\tif err != nil {\n\t\t\tl.DebugContext(ctx, \"can't parse svcb/https ech, ignoring\", slogutil.KeyError, err)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn &dns.SVCBECHConfig{\n\t\t\tECH: ech,\n\t\t}\n\t},\n\n\t\"ipv4hint\": func(ctx context.Context, l *slog.Logger, valStr string) (val dns.SVCBKeyValue) {\n\t\tip := net.ParseIP(valStr)\n\t\tif ip4 := ip.To4(); ip == nil || ip4 == nil {\n\t\t\tl.DebugContext(ctx, \"can't parse svcb/https ipv4 hint, ignoring\", \"value\", valStr)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn &dns.SVCBIPv4Hint{\n\t\t\tHint: []net.IP{ip},\n\t\t}\n\t},\n\n\t\"ipv6hint\": func(ctx context.Context, l *slog.Logger, valStr string) (val dns.SVCBKeyValue) {\n\t\tip := net.ParseIP(valStr)\n\t\tif ip == nil {\n\t\t\tl.DebugContext(ctx, \"can't parse svcb/https ipv6 hint, ignoring\", \"value\", valStr)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn &dns.SVCBIPv6Hint{\n\t\t\tHint: []net.IP{ip},\n\t\t}\n\t},\n\n\t\"mandatory\": func(ctx context.Context, l *slog.Logger, valStr string) (val dns.SVCBKeyValue) {\n\t\tcode, ok := strToSVCBKey[valStr]\n\t\tif !ok {\n\t\t\tl.DebugContext(ctx, \"unknown svcb/https mandatory key, ignoring\", \"value\", valStr)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn &dns.SVCBMandatory{\n\t\t\tCode: []dns.SVCBKey{code},\n\t\t}\n\t},\n\n\t\"no-default-alpn\": func(_ context.Context, _ *slog.Logger, _ string) (val dns.SVCBKeyValue) {\n\t\treturn &dns.SVCBNoDefaultAlpn{}\n\t},\n\n\t\"port\": func(ctx context.Context, l *slog.Logger, valStr string) (val dns.SVCBKeyValue) {\n\t\tport64, err := strconv.ParseUint(valStr, 10, 16)\n\t\tif err != nil {\n\t\t\tl.DebugContext(ctx, \"can't parse svcb/https port; ignoring\", slogutil.KeyError, err)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn &dns.SVCBPort{\n\t\t\tPort: uint16(port64),\n\t\t}\n\t},\n\n\t// TODO(a.garipov): This is the previous name for the parameter that has\n\t// since been changed.  Remove this in v0.109.0.\n\t\"echconfig\": func(ctx context.Context, l *slog.Logger, valStr string) (val dns.SVCBKeyValue) {\n\t\tl.WarnContext(\n\t\t\tctx,\n\t\t\t`svcb/https record parameter name \"echconfig\" is deprecated; `+\n\t\t\t\t`use \"ech\" instead`,\n\t\t)\n\n\t\tech, err := base64.StdEncoding.DecodeString(valStr)\n\t\tif err != nil {\n\t\t\tl.DebugContext(ctx, \"can't parse svcb/https ech; ignoring\", slogutil.KeyError, err)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn &dns.SVCBECHConfig{\n\t\t\tECH: ech,\n\t\t}\n\t},\n\n\t\"dohpath\": func(_ context.Context, _ *slog.Logger, valStr string) (val dns.SVCBKeyValue) {\n\t\treturn &dns.SVCBDoHPath{\n\t\t\tTemplate: valStr,\n\t\t}\n\t},\n}\n\n// genAnswerSVCB returns a properly initialized SVCB resource record.\n//\n// Currently, there are several restrictions on how the parameters are parsed.\n// Firstly, the parsing of non-contiguous values isn't supported.  Secondly, the\n// parsing of value-lists is not supported either.\n//\n//\tipv4hint=127.0.0.1             // Supported.\n//\tipv4hint=\"127.0.0.1\"           // Unsupported.\n//\tipv4hint=127.0.0.1,127.0.0.2   // Unsupported.\n//\tipv4hint=\"127.0.0.1,127.0.0.2\" // Unsupported.\n//\n// TODO(a.garipov): Support all of these.\nfunc (s *Server) genAnswerSVCB(\n\tctx context.Context,\n\treq *dns.Msg,\n\tsvcb *rules.DNSSVCB,\n) (ans *dns.SVCB) {\n\tans = &dns.SVCB{\n\t\tHdr:      s.hdr(req, dns.TypeSVCB),\n\t\tPriority: svcb.Priority,\n\t\tTarget:   dns.Fqdn(svcb.Target),\n\t}\n\tif len(svcb.Params) == 0 {\n\t\treturn ans\n\t}\n\n\tvalues := make([]dns.SVCBKeyValue, 0, len(svcb.Params))\n\tfor k, valStr := range svcb.Params {\n\t\thandler, ok := svcbKeyHandlers[k]\n\t\tif !ok {\n\t\t\ts.logger.DebugContext(ctx, \"unknown svcb/https key, ignoring\", \"key\", k)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tval := handler(ctx, s.logger, valStr)\n\t\tif val == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvalues = append(values, val)\n\t}\n\n\tif len(values) > 0 {\n\t\tans.Value = values\n\t}\n\n\treturn ans\n}\n"
  },
  {
    "path": "internal/dnsforward/svcbmsg_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGenAnswerHTTPS_andSVCB(t *testing.T) {\n\t// Preconditions.\n\n\ts := createTestServer(t, &filtering.Config{\n\t\tBlockingMode: filtering.BlockingModeDefault,\n\t}, ServerConfig{\n\t\tTLSConf: &TLSConfig{},\n\t\tConfig: Config{\n\t\t\tUpstreamMode:     UpstreamModeLoadBalance,\n\t\t\tEDNSClientSubnet: &EDNSClientSubnet{Enabled: false},\n\t\t\tClientsContainer: EmptyClientsContainer{},\n\t\t},\n\t\tServePlainDNS: true,\n\t})\n\n\treq := &dns.Msg{\n\t\tQuestion: []dns.Question{{\n\t\t\tName: \"abcd\",\n\t\t}},\n\t}\n\n\t// Constants and helper values.\n\n\tconst host = \"example.com\"\n\tconst prio = 32\n\n\tip4 := net.IPv4(127, 0, 0, 1)\n\tip6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}\n\n\t// Helper functions.\n\n\tdnssvcb := func(key, value string) (svcb *rules.DNSSVCB) {\n\t\tsvcb = &rules.DNSSVCB{\n\t\t\tTarget:   host,\n\t\t\tPriority: prio,\n\t\t}\n\n\t\tif key == \"\" {\n\t\t\treturn svcb\n\t\t}\n\n\t\tsvcb.Params = map[string]string{\n\t\t\tkey: value,\n\t\t}\n\n\t\treturn svcb\n\t}\n\n\twantsvcb := func(kv dns.SVCBKeyValue) (want *dns.SVCB) {\n\t\twant = &dns.SVCB{\n\t\t\tHdr:      s.hdr(req, dns.TypeSVCB),\n\t\t\tPriority: prio,\n\t\t\tTarget:   dns.Fqdn(host),\n\t\t}\n\n\t\tif kv == nil {\n\t\t\treturn want\n\t\t}\n\n\t\twant.Value = []dns.SVCBKeyValue{kv}\n\n\t\treturn want\n\t}\n\n\t// Tests.\n\n\ttestCases := []struct {\n\t\tsvcb *rules.DNSSVCB\n\t\twant *dns.SVCB\n\t\tname string\n\t}{{\n\t\tsvcb: dnssvcb(\"\", \"\"),\n\t\twant: wantsvcb(nil),\n\t\tname: \"no_params\",\n\t}, {\n\t\tsvcb: dnssvcb(\"foo\", \"bar\"),\n\t\twant: wantsvcb(nil),\n\t\tname: \"invalid\",\n\t}, {\n\t\tsvcb: dnssvcb(\"alpn\", \"h3\"),\n\t\twant: wantsvcb(&dns.SVCBAlpn{Alpn: []string{\"h3\"}}),\n\t\tname: \"alpn\",\n\t}, {\n\t\tsvcb: dnssvcb(\"ech\", \"AAAA\"),\n\t\twant: wantsvcb(&dns.SVCBECHConfig{ECH: []byte{0, 0, 0}}),\n\t\tname: \"ech\",\n\t}, {\n\t\tsvcb: dnssvcb(\"echconfig\", \"AAAA\"),\n\t\twant: wantsvcb(&dns.SVCBECHConfig{ECH: []byte{0, 0, 0}}),\n\t\tname: \"ech_deprecated\",\n\t}, {\n\t\tsvcb: dnssvcb(\"echconfig\", \"%BAD%\"),\n\t\twant: wantsvcb(nil),\n\t\tname: \"ech_invalid\",\n\t}, {\n\t\tsvcb: dnssvcb(\"ipv4hint\", \"127.0.0.1\"),\n\t\twant: wantsvcb(&dns.SVCBIPv4Hint{Hint: []net.IP{ip4}}),\n\t\tname: \"ipv4hint\",\n\t}, {\n\t\tsvcb: dnssvcb(\"ipv4hint\", \"127.0.01\"),\n\t\twant: wantsvcb(nil),\n\t\tname: \"ipv4hint_invalid\",\n\t}, {\n\t\tsvcb: dnssvcb(\"ipv6hint\", \"::1\"),\n\t\twant: wantsvcb(&dns.SVCBIPv6Hint{Hint: []net.IP{ip6}}),\n\t\tname: \"ipv6hint\",\n\t}, {\n\t\tsvcb: dnssvcb(\"ipv6hint\", \":::1\"),\n\t\twant: wantsvcb(nil),\n\t\tname: \"ipv6hint_invalid\",\n\t}, {\n\t\tsvcb: dnssvcb(\"mandatory\", \"alpn\"),\n\t\twant: wantsvcb(&dns.SVCBMandatory{Code: []dns.SVCBKey{dns.SVCB_ALPN}}),\n\t\tname: \"mandatory\",\n\t}, {\n\t\tsvcb: dnssvcb(\"mandatory\", \"alpnn\"),\n\t\twant: wantsvcb(nil),\n\t\tname: \"mandatory_invalid\",\n\t}, {\n\t\tsvcb: dnssvcb(\"no-default-alpn\", \"\"),\n\t\twant: wantsvcb(&dns.SVCBNoDefaultAlpn{}),\n\t\tname: \"no_default_alpn\",\n\t}, {\n\t\tsvcb: dnssvcb(\"dohpath\", \"/dns-query\"),\n\t\twant: wantsvcb(&dns.SVCBDoHPath{Template: \"/dns-query\"}),\n\t\tname: \"dohpath\",\n\t}, {\n\t\tsvcb: dnssvcb(\"port\", \"8080\"),\n\t\twant: wantsvcb(&dns.SVCBPort{Port: 8080}),\n\t\tname: \"port\",\n\t}, {\n\t\tsvcb: dnssvcb(\"port\", \"1005008080\"),\n\t\twant: wantsvcb(nil),\n\t\tname: \"bad_port\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(\"https\", func(t *testing.T) {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\twant := &dns.HTTPS{SVCB: *tc.want}\n\t\t\t\twant.Hdr.Rrtype = dns.TypeHTTPS\n\n\t\t\t\tgot := s.genAnswerHTTPS(testutil.ContextWithTimeout(t, testTimeout), req, tc.svcb)\n\t\t\t\tassert.Equal(t, want, got)\n\t\t\t})\n\t\t})\n\n\t\tt.Run(\"svcb\", func(t *testing.T) {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tgot := s.genAnswerSVCB(testutil.ContextWithTimeout(t, testTimeout), req, tc.svcb)\n\t\t\t\tassert.Equal(t, tc.want, got)\n\t\t\t})\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json",
    "content": "{\n  \"all_right\": {\n    \"upstream_dns\": [\n      \"8.8.8.8:53\",\n      \"8.8.4.4:53\"\n    ],\n    \"upstream_dns_file\": \"\",\n    \"bootstrap_dns\": [\n      \"9.9.9.10\",\n      \"149.112.112.10\",\n      \"2620:fe::10\",\n      \"2620:fe::fe:10\"\n    ],\n    \"fallback_dns\": [\n      \"9.9.9.10\"\n    ],\n    \"protection_enabled\": true,\n    \"protection_disabled_until\": null,\n    \"ratelimit\": 0,\n    \"ratelimit_subnet_len_ipv4\": 24,\n    \"ratelimit_subnet_len_ipv6\": 56,\n    \"ratelimit_whitelist\": [],\n    \"blocking_mode\": \"default\",\n    \"blocking_ipv4\": \"\",\n    \"blocking_ipv6\": \"\",\n    \"blocked_response_ttl\": 10,\n    \"upstream_timeout\": 10,\n    \"edns_cs_enabled\": false,\n    \"dnssec_enabled\": false,\n    \"disable_ipv6\": false,\n    \"upstream_mode\": \"\",\n    \"cache_size\": 0,\n    \"cache_ttl_min\": 0,\n    \"cache_ttl_max\": 0,\n    \"cache_enabled\": false,\n    \"cache_optimistic\": false,\n    \"resolve_clients\": false,\n    \"use_private_ptr_resolvers\": false,\n    \"local_ptr_upstreams\": [],\n    \"edns_cs_use_custom\": false,\n    \"edns_cs_custom_ip\": \"\"\n  },\n  \"fastest_addr\": {\n    \"upstream_dns\": [\n      \"8.8.8.8:53\",\n      \"8.8.4.4:53\"\n    ],\n    \"upstream_dns_file\": \"\",\n    \"bootstrap_dns\": [\n      \"9.9.9.10\",\n      \"149.112.112.10\",\n      \"2620:fe::10\",\n      \"2620:fe::fe:10\"\n    ],\n    \"fallback_dns\": [\n      \"9.9.9.10\"\n    ],\n    \"protection_enabled\": true,\n    \"protection_disabled_until\": null,\n    \"ratelimit\": 0,\n    \"ratelimit_subnet_len_ipv4\": 24,\n    \"ratelimit_subnet_len_ipv6\": 56,\n    \"ratelimit_whitelist\": [],\n    \"blocking_mode\": \"default\",\n    \"blocking_ipv4\": \"\",\n    \"blocking_ipv6\": \"\",\n    \"blocked_response_ttl\": 10,\n    \"upstream_timeout\": 10,\n    \"edns_cs_enabled\": false,\n    \"dnssec_enabled\": false,\n    \"disable_ipv6\": false,\n    \"upstream_mode\": \"fastest_addr\",\n    \"cache_size\": 0,\n    \"cache_ttl_min\": 0,\n    \"cache_ttl_max\": 0,\n    \"cache_enabled\": false,\n    \"cache_optimistic\": false,\n    \"resolve_clients\": false,\n    \"use_private_ptr_resolvers\": false,\n    \"local_ptr_upstreams\": [],\n    \"edns_cs_use_custom\": false,\n    \"edns_cs_custom_ip\": \"\"\n  },\n  \"parallel\": {\n    \"upstream_dns\": [\n      \"8.8.8.8:53\",\n      \"8.8.4.4:53\"\n    ],\n    \"upstream_dns_file\": \"\",\n    \"bootstrap_dns\": [\n      \"9.9.9.10\",\n      \"149.112.112.10\",\n      \"2620:fe::10\",\n      \"2620:fe::fe:10\"\n    ],\n    \"fallback_dns\": [\n      \"9.9.9.10\"\n    ],\n    \"protection_enabled\": true,\n    \"protection_disabled_until\": null,\n    \"ratelimit\": 0,\n    \"ratelimit_subnet_len_ipv4\": 24,\n    \"ratelimit_subnet_len_ipv6\": 56,\n    \"ratelimit_whitelist\": [],\n    \"blocking_mode\": \"default\",\n    \"blocking_ipv4\": \"\",\n    \"blocking_ipv6\": \"\",\n    \"blocked_response_ttl\": 10,\n    \"upstream_timeout\": 10,\n    \"edns_cs_enabled\": false,\n    \"dnssec_enabled\": false,\n    \"disable_ipv6\": false,\n    \"upstream_mode\": \"parallel\",\n    \"cache_size\": 0,\n    \"cache_ttl_min\": 0,\n    \"cache_ttl_max\": 0,\n    \"cache_enabled\": false,\n    \"cache_optimistic\": false,\n    \"resolve_clients\": false,\n    \"use_private_ptr_resolvers\": false,\n    \"local_ptr_upstreams\": [],\n    \"edns_cs_use_custom\": false,\n    \"edns_cs_custom_ip\": \"\"\n  }\n}\n"
  },
  {
    "path": "internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json",
    "content": "{\n  \"upstream_dns\": {\n    \"req\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:77\",\n        \"8.8.4.4:77\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:77\",\n        \"8.8.4.4:77\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"bootstraps\": {\n    \"req\": {\n      \"bootstrap_dns\": [\n        \"9.9.9.10\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"blocking_mode_good\": {\n    \"req\": {\n      \"blocking_mode\": \"refused\"\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"refused\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"blocking_mode_bad\": {\n    \"req\": {\n      \"blocking_mode\": \"custom_ip\"\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"ratelimit\": {\n    \"req\": {\n      \"ratelimit\": 6\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 6,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"ratelimit_subnet_len\": {\n    \"req\": {\n      \"ratelimit\": 12,\n      \"ratelimit_subnet_len_ipv4\": 32,\n      \"ratelimit_subnet_len_ipv6\": 128\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 12,\n      \"ratelimit_subnet_len_ipv4\": 32,\n      \"ratelimit_subnet_len_ipv6\": 128,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"ratelimit_whitelist_not_ip\": {\n    \"req\": {\n      \"ratelimit_whitelist\": [\n        \"1.2.3.4\",\n        \"not.ip\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"edns_cs_enabled\": {\n    \"req\": {\n      \"edns_cs_enabled\": true\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": true,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"edns_cs_use_custom\": {\n    \"req\": {\n      \"edns_cs_enabled\": true,\n      \"edns_cs_use_custom\": true,\n      \"edns_cs_custom_ip\": \"1.2.3.4\"\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": true,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": true,\n      \"edns_cs_custom_ip\": \"1.2.3.4\"\n    }\n  },\n  \"edns_cs_use_custom_bad_ip\": {\n    \"req\": {\n      \"edns_cs_enabled\": true,\n      \"edns_cs_use_custom\": true,\n      \"edns_cs_custom_ip\": \"bad.ip\"\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"dnssec_enabled\": {\n    \"req\": {\n      \"dnssec_enabled\": true\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": true,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"cache_size\": {\n    \"req\": {\n      \"cache_size\": 1024\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 1024,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": true,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"cache_enabled\": {\n    \"req\": {\n      \"cache_enabled\": true,\n      \"cache_size\": 1024\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 1024,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": true,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"upstream_mode_parallel\": {\n    \"req\": {\n      \"upstream_mode\": \"parallel\"\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"parallel\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"upstream_mode_fastest_addr\": {\n    \"req\": {\n      \"upstream_mode\": \"fastest_addr\"\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"fastest_addr\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"upstream_dns_bad\": {\n    \"req\": {\n      \"upstream_dns\": [\n        \"!!!\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"bootstraps_bad\": {\n    \"req\": {\n      \"bootstrap_dns\": [\n        \"a\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"cache_bad_ttl\": {\n    \"req\": {\n      \"cache_ttl_min\": 1024,\n      \"cache_ttl_max\": 512\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"upstream_mode_bad\": {\n    \"req\": {\n      \"upstream_mode\": \"somethingelse\"\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"local_ptr_upstreams_good\": {\n    \"req\": {\n      \"local_ptr_upstreams\": [\n        \"123.123.123.123\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [\n        \"123.123.123.123\"\n      ],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"local_ptr_upstreams_bad\": {\n    \"req\": {\n      \"local_ptr_upstreams\": [\n        \"123.123.123.123\",\n        \"[/non.arpa/]#\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"local_ptr_upstreams_null\": {\n    \"req\": {\n      \"local_ptr_upstreams\": null\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"fallbacks\": {\n    \"req\": {\n      \"fallback_dns\": [\n        \"9.9.9.10\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [\n        \"9.9.9.10\"\n      ],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"blocked_response_ttl\": {\n    \"req\": {\n      \"blocked_response_ttl\": 11\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 11,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"multiple_domain_specific_upstreams\": {\n    \"req\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:77\",\n        \"[/example.com/]8.8.4.4:77 9.9.9.10 https://1.1.1.1\"\n      ]\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:77\",\n        \"[/example.com/]8.8.4.4:77 9.9.9.10 https://1.1.1.1\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 10,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  },\n  \"upstream_timeout\": {\n    \"req\": {\n      \"upstream_timeout\": 11\n    },\n    \"want\": {\n      \"upstream_dns\": [\n        \"8.8.8.8:53\",\n        \"8.8.4.4:53\"\n      ],\n      \"upstream_dns_file\": \"\",\n      \"bootstrap_dns\": [\n        \"9.9.9.10\",\n        \"149.112.112.10\",\n        \"2620:fe::10\",\n        \"2620:fe::fe:10\"\n      ],\n      \"fallback_dns\": [],\n      \"protection_enabled\": true,\n      \"protection_disabled_until\": null,\n      \"ratelimit\": 0,\n      \"ratelimit_subnet_len_ipv4\": 24,\n      \"ratelimit_subnet_len_ipv6\": 56,\n      \"ratelimit_whitelist\": [],\n      \"blocking_mode\": \"default\",\n      \"blocking_ipv4\": \"\",\n      \"blocking_ipv6\": \"\",\n      \"blocked_response_ttl\": 10,\n      \"upstream_timeout\": 11,\n      \"edns_cs_enabled\": false,\n      \"dnssec_enabled\": false,\n      \"disable_ipv6\": false,\n      \"upstream_mode\": \"\",\n      \"cache_size\": 0,\n      \"cache_ttl_min\": 0,\n      \"cache_ttl_max\": 0,\n      \"cache_enabled\": false,\n      \"cache_optimistic\": false,\n      \"resolve_clients\": false,\n      \"use_private_ptr_resolvers\": false,\n      \"local_ptr_upstreams\": [],\n      \"edns_cs_use_custom\": false,\n      \"edns_cs_custom_ip\": \"\"\n    }\n  }\n}\n"
  },
  {
    "path": "internal/dnsforward/upstreams.go",
    "content": "package dnsforward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n)\n\n// newBootstrap returns a bootstrap resolver based on the configuration of s.\n// boots are the upstream resolvers that should be closed after use.  r is the\n// actual bootstrap resolver, which may include the system hosts.\n//\n// TODO(e.burkov):  This function currently returns a resolver and a slice of\n// the upstream resolvers, which are essentially the same.  boots are returned\n// for being able to close them afterwards, but it introduces an implicit\n// contract that r could only be used before that.  Anyway, this code should\n// improve when the [proxy.UpstreamConfig] will become an [upstream.Resolver]\n// and be used here.\nfunc newBootstrap(\n\taddrs []string,\n\tetcHosts upstream.Resolver,\n\topts *upstream.Options,\n) (r upstream.Resolver, boots []*upstream.UpstreamResolver, err error) {\n\tif len(addrs) == 0 {\n\t\taddrs = defaultBootstrap\n\t}\n\n\tboots, err = aghnet.ParseBootstraps(addrs, opts)\n\tif err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn nil, nil, err\n\t}\n\n\tvar parallel upstream.ParallelResolver\n\tfor _, b := range boots {\n\t\tparallel = append(parallel, upstream.NewCachingResolver(b))\n\t}\n\n\tif etcHosts != nil {\n\t\tr = upstream.ConsequentResolver{etcHosts, parallel}\n\t} else {\n\t\tr = parallel\n\t}\n\n\treturn r, boots, nil\n}\n\n// newUpstreamConfig returns the upstream configuration based on upstreams.  If\n// upstreams slice specifies no default upstreams, defaultUpstreams are used to\n// create upstreams with no domain specifications.  opts are used when creating\n// upstream configuration.  l must not be nil.\nfunc newUpstreamConfig(\n\tctx context.Context,\n\tl *slog.Logger,\n\tupstreams []string,\n\tdefaultUpstreams []string,\n\topts *upstream.Options,\n) (uc *proxy.UpstreamConfig, err error) {\n\tuc, err = proxy.ParseUpstreamsConfig(upstreams, opts)\n\tif err != nil {\n\t\treturn uc, fmt.Errorf(\"parsing upstreams: %w\", err)\n\t}\n\n\tif len(uc.Upstreams) == 0 && len(defaultUpstreams) > 0 {\n\t\tl.WarnContext(\n\t\t\tctx,\n\t\t\t\"no default upstreams specified\",\n\t\t\t\"default_upstreams\",\n\t\t\tdefaultUpstreams,\n\t\t)\n\n\t\tvar defaultUpstreamConfig *proxy.UpstreamConfig\n\t\tdefaultUpstreamConfig, err = proxy.ParseUpstreamsConfig(defaultUpstreams, opts)\n\t\tif err != nil {\n\t\t\treturn uc, fmt.Errorf(\"parsing default upstreams: %w\", err)\n\t\t}\n\n\t\tuc.Upstreams = defaultUpstreamConfig.Upstreams\n\t}\n\n\treturn uc, nil\n}\n\n// newPrivateConfig creates an upstream configuration for resolving PTR records\n// for local addresses.  The configuration is built either from the provided\n// addresses or from the system resolvers.  unwanted filters the resulting\n// upstream configuration.  l, unwanted, sysResolvers, privateNets and opts must\n// not be nil.\nfunc newPrivateConfig(\n\tctx context.Context,\n\tl *slog.Logger,\n\taddrs []string,\n\tunwanted addrPortSet,\n\tsysResolvers SystemResolvers,\n\tprivateNets netutil.SubnetSet,\n\topts *upstream.Options,\n) (uc *proxy.UpstreamConfig, err error) {\n\tconfNeedsFiltering := len(addrs) > 0\n\tif confNeedsFiltering {\n\t\taddrs = stringutil.FilterOut(addrs, aghnet.IsCommentOrEmpty)\n\t} else {\n\t\tsysResolvers := slices.DeleteFunc(slices.Clone(sysResolvers.Addrs()), unwanted.Has)\n\t\taddrs = make([]string, 0, len(sysResolvers))\n\t\tfor _, r := range sysResolvers {\n\t\t\taddrs = append(addrs, r.String())\n\t\t}\n\t}\n\n\tl.DebugContext(ctx, \"private-use upstreams\", \"addrs\", addrs)\n\n\tuc, err = proxy.ParseUpstreamsConfig(addrs, opts)\n\tif err != nil {\n\t\treturn uc, fmt.Errorf(\"preparing private upstreams: %w\", err)\n\t}\n\n\tif confNeedsFiltering {\n\t\terr = filterOutAddrs(uc, unwanted)\n\t\tif err != nil {\n\t\t\treturn uc, fmt.Errorf(\"filtering private upstreams: %w\", err)\n\t\t}\n\t}\n\n\t// Prevalidate the config to catch the exact error before creating proxy.\n\t// See TODO on [PrivateRDNSError].\n\terr = proxy.ValidatePrivateConfig(uc, privateNets)\n\tif err != nil {\n\t\treturn uc, &PrivateRDNSError{err: err}\n\t}\n\n\treturn uc, nil\n}\n\n// setProxyUpstreamMode sets the upstream mode and related settings in conf\n// based on provided parameters.\nfunc setProxyUpstreamMode(\n\tconf *proxy.Config,\n\tupstreamMode UpstreamMode,\n\tfastestTimeout time.Duration,\n) (err error) {\n\tswitch upstreamMode {\n\tcase UpstreamModeParallel:\n\t\tconf.UpstreamMode = proxy.UpstreamModeParallel\n\tcase UpstreamModeFastestAddr:\n\t\tconf.UpstreamMode = proxy.UpstreamModeFastestAddr\n\t\tconf.FastestPingTimeout = fastestTimeout\n\tcase UpstreamModeLoadBalance:\n\t\tconf.UpstreamMode = proxy.UpstreamModeLoadBalance\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected value %q\", upstreamMode)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dnsforward/upstreams_internal_test.go",
    "content": "package dnsforward\n\nimport (\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUpstreamConfigValidator(t *testing.T) {\n\tgoodHandler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {\n\t\terr := w.WriteMsg(new(dns.Msg).SetReply(m))\n\t\trequire.NoError(testutil.PanicT{}, err)\n\t})\n\tbadHandler := dns.HandlerFunc(func(w dns.ResponseWriter, _ *dns.Msg) {\n\t\terr := w.WriteMsg(new(dns.Msg))\n\t\trequire.NoError(testutil.PanicT{}, err)\n\t})\n\n\tgoodUps := (&url.URL{\n\t\tScheme: \"tcp\",\n\t\tHost:   newLocalUpstreamListener(t, 0, goodHandler).String(),\n\t}).String()\n\tbadUps := (&url.URL{\n\t\tScheme: \"tcp\",\n\t\tHost:   newLocalUpstreamListener(t, 0, badHandler).String(),\n\t}).String()\n\n\tgoodAndBadUps := strings.Join([]string{goodUps, badUps}, \" \")\n\n\t// upsTimeout restricts the checking process to prevent the test from\n\t// hanging.\n\tconst upsTimeout = 100 * time.Millisecond\n\n\ttestCases := []struct {\n\t\twant     map[string]string\n\t\tname     string\n\t\tgeneral  []string\n\t\tfallback []string\n\t\tprivate  []string\n\t}{{\n\t\tname:    \"success\",\n\t\tgeneral: []string{goodUps},\n\t\twant: map[string]string{\n\t\t\tgoodUps: \"OK\",\n\t\t},\n\t}, {\n\t\tname:    \"broken\",\n\t\tgeneral: []string{badUps},\n\t\twant: map[string]string{\n\t\t\tbadUps: `couldn't communicate with upstream: exchanging with ` +\n\t\t\t\tbadUps + ` over tcp: dns: id mismatch`,\n\t\t},\n\t}, {\n\t\tname:    \"both\",\n\t\tgeneral: []string{goodUps, badUps, goodUps},\n\t\twant: map[string]string{\n\t\t\tgoodUps: \"OK\",\n\t\t\tbadUps: `couldn't communicate with upstream: exchanging with ` +\n\t\t\t\tbadUps + ` over tcp: dns: id mismatch`,\n\t\t},\n\t}, {\n\t\tname:    \"domain_specific_error\",\n\t\tgeneral: []string{\"[/domain.example/]\" + badUps},\n\t\twant: map[string]string{\n\t\t\tbadUps: `WARNING: couldn't communicate ` +\n\t\t\t\t`with upstream: exchanging with ` + badUps + ` over tcp: ` +\n\t\t\t\t`dns: id mismatch`,\n\t\t},\n\t}, {\n\t\tname:     \"fallback_success\",\n\t\tfallback: []string{goodUps},\n\t\twant: map[string]string{\n\t\t\tgoodUps: \"OK\",\n\t\t},\n\t}, {\n\t\tname:     \"fallback_broken\",\n\t\tfallback: []string{badUps},\n\t\twant: map[string]string{\n\t\t\tbadUps: `couldn't communicate with upstream: exchanging with ` +\n\t\t\t\tbadUps + ` over tcp: dns: id mismatch`,\n\t\t},\n\t}, {\n\t\tname:    \"multiple_domain_specific_upstreams\",\n\t\tgeneral: []string{\"[/domain.example/]\" + goodAndBadUps},\n\t\twant: map[string]string{\n\t\t\tgoodUps: \"OK\",\n\t\t\tbadUps: `WARNING: couldn't communicate ` +\n\t\t\t\t`with upstream: exchanging with ` + badUps + ` over tcp: ` +\n\t\t\t\t`dns: id mismatch`,\n\t\t},\n\t}, {\n\t\tname:    \"bad_specification\",\n\t\tgeneral: []string{\"[/domain.example/]/]1.2.3.4\"},\n\t\twant: map[string]string{\n\t\t\t\"[/domain.example/]/]1.2.3.4\": generalTextLabel + \" 1: parsing error\",\n\t\t},\n\t}, {\n\t\tname:     \"all_different\",\n\t\tgeneral:  []string{\"[/domain.example/]\" + goodAndBadUps},\n\t\tfallback: []string{\"[/domain.example/]\" + goodAndBadUps},\n\t\tprivate:  []string{\"[/domain.example/]\" + goodAndBadUps},\n\t\twant: map[string]string{\n\t\t\tgoodUps: \"OK\",\n\t\t\tbadUps: `WARNING: couldn't communicate ` +\n\t\t\t\t`with upstream: exchanging with ` + badUps + ` over tcp: ` +\n\t\t\t\t`dns: id mismatch`,\n\t\t},\n\t}, {\n\t\tname:     \"bad_specific_domains\",\n\t\tgeneral:  []string{\"[/example/]/]\" + goodUps},\n\t\tfallback: []string{\"[/example/\" + goodUps},\n\t\tprivate:  []string{\"[/example//bad.123/]\" + goodUps},\n\t\twant: map[string]string{\n\t\t\t\"[/example/]/]\" + goodUps:        generalTextLabel + \" 1: parsing error\",\n\t\t\t\"[/example/\" + goodUps:           fallbackTextLabel + \" 1: parsing error\",\n\t\t\t\"[/example//bad.123/]\" + goodUps: privateTextLabel + \" 1: parsing error\",\n\t\t},\n\t}, {\n\t\tname: \"bad_proto\",\n\t\tgeneral: []string{\n\t\t\t\"bad://1.2.3.4\",\n\t\t},\n\t\twant: map[string]string{\n\t\t\t\"bad://1.2.3.4\": generalTextLabel + \" 1: parsing error\",\n\t\t},\n\t}, {\n\t\tname: \"truncated_line\",\n\t\tgeneral: []string{\n\t\t\t\"This is a very long line.  It will cause a parsing error and will be truncated here.\",\n\t\t},\n\t\twant: map[string]string{\n\t\t\t\"This is a very long line.  It will cause a parsing error and will be truncated …\": \"upstream_dns 1: parsing error\",\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\topts := &upstream.Options{\n\t\t\t\tLogger:    testLogger,\n\t\t\t\tTimeout:   upsTimeout,\n\t\t\t\tBootstrap: net.DefaultResolver,\n\t\t\t}\n\n\t\t\tcv := newUpstreamConfigValidator(ctx, tc.general, tc.fallback, tc.private, opts)\n\n\t\t\tcv.check(ctx, testLogger)\n\t\t\tcv.close()\n\n\t\t\tassert.Equal(t, tc.want, cv.status(ctx, testLogger))\n\t\t})\n\t}\n}\n\nfunc TestUpstreamConfigValidator_Check_once(t *testing.T) {\n\ttype signal = struct{}\n\n\treqCh := make(chan signal)\n\thdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {\n\t\tpt := testutil.PanicT{}\n\n\t\terr := w.WriteMsg(new(dns.Msg).SetReply(m))\n\t\trequire.NoError(pt, err)\n\n\t\ttestutil.RequireSend(pt, reqCh, signal{}, testTimeout)\n\t})\n\n\taddr := (&url.URL{\n\t\tScheme: \"tcp\",\n\t\tHost:   newLocalUpstreamListener(t, 0, hdlr).String(),\n\t}).String()\n\ttwoAddrs := strings.Join([]string{addr, addr}, \" \")\n\n\twantStatus := map[string]string{\n\t\taddr: \"OK\",\n\t}\n\n\ttestCases := []struct {\n\t\tname string\n\t\tups  []string\n\t}{{\n\t\tname: \"common\",\n\t\tups:  []string{addr, addr, addr},\n\t}, {\n\t\tname: \"domain-specific\",\n\t\tups:  []string{\"[/one.example/]\" + addr, \"[/two.example/]\" + twoAddrs},\n\t}, {\n\t\tname: \"both\",\n\t\tups:  []string{addr, \"[/one.example/]\" + addr, addr, \"[/two.example/]\" + twoAddrs},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tcv := newUpstreamConfigValidator(ctx, tc.ups, nil, nil, &upstream.Options{\n\t\t\t\tLogger:  testLogger,\n\t\t\t\tTimeout: testTimeout,\n\t\t\t})\n\n\t\t\tgo func() {\n\t\t\t\tcv.check(ctx, testLogger)\n\t\t\t\ttestutil.RequireSend(testutil.PanicT{}, reqCh, signal{}, testTimeout)\n\t\t\t}()\n\n\t\t\t// Wait for the only request to be sent.\n\t\t\ttestutil.RequireReceive(t, reqCh, testTimeout)\n\n\t\t\t// Wait for the check to finish.\n\t\t\ttestutil.RequireReceive(t, reqCh, testTimeout)\n\n\t\t\tcv.close()\n\t\t\trequire.Equal(t, wantStatus, cv.status(ctx, testLogger))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/blocked.go",
    "content": "package filtering\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/schedule\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n)\n\n// serviceRules maps a service ID to its filtering rules.\nvar serviceRules map[string][]*rules.NetworkRule\n\n// serviceIDs contains service IDs sorted alphabetically.\nvar serviceIDs []string\n\n// initBlockedServices initializes package-level blocked service data.  l must\n// not be nil.\nfunc initBlockedServices(ctx context.Context, l *slog.Logger) {\n\tsvcLen := len(blockedServices)\n\tserviceIDs = make([]string, svcLen)\n\tserviceRules = make(map[string][]*rules.NetworkRule, svcLen)\n\n\tfor i, s := range blockedServices {\n\t\tnetRules := make([]*rules.NetworkRule, 0, len(s.Rules))\n\t\tfor _, text := range s.Rules {\n\t\t\trule, err := rules.NewNetworkRule(text, rulelist.IDBlockedService)\n\t\t\tif err == nil {\n\t\t\t\tnetRules = append(netRules, rule)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tl.ErrorContext(\n\t\t\t\tctx,\n\t\t\t\t\"parsing blocked service rule\",\n\t\t\t\t\"svc\", s.ID,\n\t\t\t\t\"rule\", text,\n\t\t\t\tslogutil.KeyError, err,\n\t\t\t)\n\t\t}\n\n\t\tserviceIDs[i] = s.ID\n\t\tserviceRules[s.ID] = netRules\n\t}\n\n\tslices.Sort(serviceIDs)\n\n\tl.DebugContext(ctx, \"initialized services\", \"svc_len\", svcLen)\n}\n\n// BlockedServices is the configuration of blocked services.\n//\n// TODO(s.chzhen):  Move to a higher-level package to allow importing the client\n// package into the filtering package.\ntype BlockedServices struct {\n\t// Schedule is blocked services schedule for every day of the week.\n\tSchedule *schedule.Weekly `json:\"schedule\" yaml:\"schedule\"`\n\n\t// IDs is the names of blocked services.\n\tIDs []string `json:\"ids\" yaml:\"ids\"`\n}\n\n// Clone returns a deep copy of blocked services.\nfunc (s *BlockedServices) Clone() (c *BlockedServices) {\n\tif s == nil {\n\t\treturn nil\n\t}\n\n\treturn &BlockedServices{\n\t\tSchedule: s.Schedule.Clone(),\n\t\tIDs:      slices.Clone(s.IDs),\n\t}\n}\n\n// FilterUnknownIDs filters out unknown service IDs within s and logs them at\n// warning level.  It does nothing if s is nil.\nfunc (s *BlockedServices) FilterUnknownIDs(ctx context.Context, logger *slog.Logger) {\n\tif s == nil {\n\t\t// [BlockedServices.Validate] handles this case.\n\t\treturn\n\t}\n\n\ts.IDs = slices.DeleteFunc(s.IDs, func(id string) (ok bool) {\n\t\t_, isKnown := serviceRules[id]\n\t\tif !isKnown {\n\t\t\tlogger.WarnContext(ctx, \"filtered unknown service\", \"id\", id)\n\t\t}\n\n\t\treturn !isKnown\n\t})\n}\n\n// type check\nvar _ validate.Interface = (*BlockedServices)(nil)\n\n// Validate implements the [validate.Interface] interface for *BlockedServices.\nfunc (s *BlockedServices) Validate() (err error) {\n\tif s == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\tvar errs []error\n\tfor _, id := range s.IDs {\n\t\t_, ok := serviceRules[id]\n\t\tif !ok {\n\t\t\terrs = append(errs, fmt.Errorf(\"unknown blocked-service %q\", id))\n\t\t}\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// ApplyBlockedServices - set blocked services settings for this DNS request\nfunc (d *DNSFilter) ApplyBlockedServices(setts *Settings) {\n\td.confMu.RLock()\n\tdefer d.confMu.RUnlock()\n\n\tsetts.ServicesRules = []ServiceEntry{}\n\n\tbsvc := d.conf.BlockedServices\n\n\t// TODO(s.chzhen):  Use startTime from [dnsforward.dnsContext].\n\tif !bsvc.Schedule.Contains(time.Now()) {\n\t\td.ApplyBlockedServicesList(setts, bsvc.IDs)\n\t}\n}\n\n// ApplyBlockedServicesList appends filtering rules to the settings.\nfunc (d *DNSFilter) ApplyBlockedServicesList(setts *Settings, list []string) {\n\tfor _, name := range list {\n\t\trules, ok := serviceRules[name]\n\t\tif !ok {\n\t\t\td.logger.ErrorContext(context.TODO(), \"unknown service name\", \"name\", name)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tsetts.ServicesRules = append(setts.ServicesRules, ServiceEntry{\n\t\t\tName:  name,\n\t\t\tRules: rules,\n\t\t})\n\t}\n}\n\nfunc (d *DNSFilter) handleBlockedServicesIDs(w http.ResponseWriter, r *http.Request) {\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, serviceIDs)\n}\n\nfunc (d *DNSFilter) handleBlockedServicesAll(w http.ResponseWriter, r *http.Request) {\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, struct {\n\t\tBlockedServices []blockedService `json:\"blocked_services\"`\n\t\tServiceGroups   []serviceGroup   `json:\"groups\"`\n\t}{\n\t\tBlockedServices: blockedServices,\n\t\tServiceGroups:   serviceGroups,\n\t})\n}\n\n// handleBlockedServicesList is the handler for the GET\n// /control/blocked_services/list HTTP API.\n//\n// Deprecated:  Use handleBlockedServicesGet.\nfunc (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {\n\tvar list []string\n\tfunc() {\n\t\td.confMu.Lock()\n\t\tdefer d.confMu.Unlock()\n\n\t\tlist = d.conf.BlockedServices.IDs\n\t}()\n\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, list)\n}\n\n// handleBlockedServicesSet is the handler for the POST\n// /control/blocked_services/set HTTP API.\n//\n// Deprecated:  Use handleBlockedServicesUpdate.\nfunc (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tlist := []string{}\n\terr := json.NewDecoder(r.Body).Decode(&list)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, d.logger, r, w, http.StatusBadRequest, \"json.Decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tfunc() {\n\t\td.confMu.Lock()\n\t\tdefer d.confMu.Unlock()\n\n\t\td.conf.BlockedServices.IDs = list\n\t\td.logger.DebugContext(ctx, \"updated blocked services list\", \"len\", len(list))\n\t}()\n\n\td.conf.ConfModifier.Apply(ctx)\n}\n\n// handleBlockedServicesGet is the handler for the GET\n// /control/blocked_services/get HTTP API.\nfunc (d *DNSFilter) handleBlockedServicesGet(w http.ResponseWriter, r *http.Request) {\n\tvar bsvc *BlockedServices\n\tfunc() {\n\t\td.confMu.RLock()\n\t\tdefer d.confMu.RUnlock()\n\n\t\tbsvc = d.conf.BlockedServices.Clone()\n\t}()\n\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, bsvc)\n}\n\n// handleBlockedServicesUpdate is the handler for the PUT\n// /control/blocked_services/update HTTP API.\nfunc (d *DNSFilter) handleBlockedServicesUpdate(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\tbsvc := &BlockedServices{}\n\terr := json.NewDecoder(r.Body).Decode(bsvc)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"json.Decode: %s\", err)\n\n\t\treturn\n\t}\n\n\terr = bsvc.Validate()\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusUnprocessableEntity, \"validating: %s\", err)\n\n\t\treturn\n\t}\n\n\tif bsvc.Schedule == nil {\n\t\tbsvc.Schedule = schedule.EmptyWeekly()\n\t}\n\n\tfunc() {\n\t\td.confMu.Lock()\n\t\tdefer d.confMu.Unlock()\n\n\t\td.conf.BlockedServices = bsvc\n\t}()\n\n\tl.DebugContext(ctx, \"updated blocked services schedule\", \"len\", len(bsvc.IDs))\n\n\td.conf.ConfModifier.Apply(ctx)\n}\n"
  },
  {
    "path": "internal/filtering/dnsrewrite.go",
    "content": "package filtering\n\nimport (\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// DNSRewriteResult is the result of application of $dnsrewrite rules.\ntype DNSRewriteResult struct {\n\tResponse DNSRewriteResultResponse `json:\",omitempty\"`\n\tRCode    rules.RCode              `json:\",omitempty\"`\n}\n\n// DNSRewriteResultResponse is the collection of DNS response records\n// the server returns.\ntype DNSRewriteResultResponse map[rules.RRType][]rules.RRValue\n\n// processDNSRewrites processes DNS rewrite rules in dnsr.  It returns an empty\n// result if dnsr is empty.  Otherwise, the result will have either CanonName or\n// DNSRewriteResult set.  dnsr is expected to be non-empty.\nfunc (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {\n\tvar rules []*ResultRule\n\tdnsrr := &DNSRewriteResult{\n\t\tResponse: DNSRewriteResultResponse{},\n\t}\n\n\tfor _, nr := range dnsr {\n\t\tdr := nr.DNSRewrite\n\t\tif dr.NewCNAME != \"\" {\n\t\t\t// NewCNAME rules have a higher priority than other rules.\n\t\t\trules = []*ResultRule{\n\t\t\t\tNewResultRule(nr),\n\t\t\t}\n\n\t\t\treturn Result{\n\t\t\t\tRules:     rules,\n\t\t\t\tReason:    RewrittenRule,\n\t\t\t\tCanonName: dr.NewCNAME,\n\t\t\t}\n\t\t}\n\n\t\tswitch dr.RCode {\n\t\tcase dns.RcodeSuccess:\n\t\t\tdnsrr.RCode = dr.RCode\n\t\t\tdnsrr.Response[dr.RRType] = append(dnsrr.Response[dr.RRType], dr.Value)\n\t\t\trules = append(rules, NewResultRule(nr))\n\t\tdefault:\n\t\t\t// RcodeRefused and other such codes have higher priority.  Return\n\t\t\t// immediately.\n\t\t\trules = []*ResultRule{\n\t\t\t\tNewResultRule(nr),\n\t\t\t}\n\t\t\tdnsrr = &DNSRewriteResult{\n\t\t\t\tRCode: dr.RCode,\n\t\t\t}\n\n\t\t\treturn Result{\n\t\t\t\tDNSRewriteResult: dnsrr,\n\t\t\t\tRules:            rules,\n\t\t\t\tReason:           RewrittenRule,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn Result{\n\t\tDNSRewriteResult: dnsrr,\n\t\tRules:            rules,\n\t\tReason:           RewrittenRule,\n\t}\n}\n\n// processDNSResultRewrites returns an empty Result if there are no dnsrewrite\n// rules in dnsres.  Otherwise, it returns the processed Result.\nfunc (d *DNSFilter) processDNSResultRewrites(\n\tdnsres *urlfilter.DNSResult,\n\thost string,\n) (dnsRWRes Result) {\n\tdnsr := dnsres.DNSRewrites()\n\tif len(dnsr) == 0 {\n\t\treturn Result{}\n\t}\n\n\tres := d.processDNSRewrites(dnsr)\n\tif res.Reason == RewrittenRule && res.CanonName == host {\n\t\t// A rewrite of a host to itself.  Go on and try matching other things.\n\t\treturn Result{}\n\t}\n\n\treturn res\n}\n"
  },
  {
    "path": "internal/filtering/dnsrewrite_test.go",
    "content": "package filtering_test\n\nimport (\n\t\"net/netip\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) {\n\tconst text = `\n|cname^$dnsrewrite=new-cname\n\n|a-record^$dnsrewrite=127.0.0.1\n|aaaa-record^$dnsrewrite=::1\n\n|txt-record^$dnsrewrite=NOERROR;TXT;hello-world\n|refused^$dnsrewrite=REFUSED\n\n|mapped^$dnsrewrite=NOERROR;AAAA;::ffff:127.0.0.1\n\n|a-records^$dnsrewrite=127.0.0.1\n|a-records^$dnsrewrite=127.0.0.2\n\n|aaaa-records^$dnsrewrite=::1\n|aaaa-records^$dnsrewrite=::2\n\n|disable-one^$dnsrewrite=127.0.0.1\n|disable-one^$dnsrewrite=127.0.0.2\n@@||disable-one^$dnsrewrite=127.0.0.1\n\n|disable-cname^$dnsrewrite=127.0.0.1\n|disable-cname^$dnsrewrite=new-cname\n@@||disable-cname^$dnsrewrite=new-cname\n\n|disable-cname-many^$dnsrewrite=127.0.0.1\n|disable-cname-many^$dnsrewrite=new-cname-1\n|disable-cname-many^$dnsrewrite=new-cname-2\n@@||disable-cname-many^$dnsrewrite=new-cname-1\n\n|disable-all^$dnsrewrite=127.0.0.1\n|disable-all^$dnsrewrite=127.0.0.2\n@@||disable-all^$dnsrewrite\n\n|1.2.3.4.in-addr.arpa^$dnsrewrite=NOERROR;PTR;new-ptr\n|1.2.3.5.in-addr.arpa^$dnsrewrite=NOERROR;PTR;new-ptr-with-dot.\n`\n\n\tconf := &filtering.Config{\n\t\tLogger:                testLogger,\n\t\tSafeBrowsingCacheSize: 10000,\n\t\tParentalCacheSize:     10000,\n\t\tSafeSearchCacheSize:   1000,\n\t\tCacheTime:             30,\n\t}\n\n\tf, err := filtering.New(conf, []filtering.Filter{{ID: 0, Data: []byte(text)}})\n\trequire.NoError(t, err)\n\n\tsetts := &filtering.Settings{\n\t\tFilteringEnabled: true,\n\t}\n\n\tipv4p1 := netutil.IPv4Localhost()\n\tipv4p2 := ipv4p1.Next()\n\tipv6p1 := netutil.IPv6Localhost()\n\tipv6p2 := ipv6p1.Next()\n\tmapped := netip.AddrFrom16(ipv4p1.As16())\n\n\ttestCasesA := []struct {\n\t\tname  string\n\t\twant  []any\n\t\trcode int\n\t\tdtyp  uint16\n\t}{{\n\t\tname:  \"a-record\",\n\t\trcode: dns.RcodeSuccess,\n\t\twant:  []any{ipv4p1},\n\t\tdtyp:  dns.TypeA,\n\t}, {\n\t\tname:  \"aaaa-record\",\n\t\twant:  []any{ipv6p1},\n\t\trcode: dns.RcodeSuccess,\n\t\tdtyp:  dns.TypeAAAA,\n\t}, {\n\t\tname:  \"txt-record\",\n\t\twant:  []any{\"hello-world\"},\n\t\trcode: dns.RcodeSuccess,\n\t\tdtyp:  dns.TypeTXT,\n\t}, {\n\t\tname:  \"refused\",\n\t\twant:  nil,\n\t\trcode: dns.RcodeRefused,\n\t\tdtyp:  0,\n\t}, {\n\t\tname:  \"a-records\",\n\t\twant:  []any{ipv4p1, ipv4p2},\n\t\trcode: dns.RcodeSuccess,\n\t\tdtyp:  dns.TypeA,\n\t}, {\n\t\tname:  \"aaaa-records\",\n\t\twant:  []any{ipv6p1, ipv6p2},\n\t\trcode: dns.RcodeSuccess,\n\t\tdtyp:  dns.TypeAAAA,\n\t}, {\n\t\tname:  \"disable-one\",\n\t\twant:  []any{ipv4p2},\n\t\trcode: dns.RcodeSuccess,\n\t\tdtyp:  dns.TypeA,\n\t}, {\n\t\tname:  \"disable-cname\",\n\t\twant:  []any{ipv4p1},\n\t\trcode: dns.RcodeSuccess,\n\t\tdtyp:  dns.TypeA,\n\t}, {\n\t\tname:  \"mapped\",\n\t\twant:  []any{mapped},\n\t\trcode: dns.RcodeSuccess,\n\t\tdtyp:  dns.TypeAAAA,\n\t}}\n\n\tfor _, tc := range testCasesA {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\thost := path.Base(tc.name)\n\n\t\t\tvar res filtering.Result\n\t\t\tres, err = f.CheckHostRules(host, tc.dtyp, setts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdnsrr := res.DNSRewriteResult\n\t\t\trequire.NotNil(t, dnsrr)\n\n\t\t\tassert.Equal(t, tc.rcode, dnsrr.RCode)\n\t\t\tif tc.rcode == dns.RcodeRefused {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tipVals := dnsrr.Response[tc.dtyp]\n\t\t\trequire.Len(t, ipVals, len(tc.want))\n\n\t\t\tfor i, val := range tc.want {\n\t\t\t\trequire.Equal(t, val, ipVals[i])\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"cname\", func(t *testing.T) {\n\t\tdtyp := dns.TypeA\n\t\thost := path.Base(t.Name())\n\n\t\tvar res filtering.Result\n\t\tres, err = f.CheckHostRules(host, dtyp, setts)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, \"new-cname\", res.CanonName)\n\t})\n\n\tt.Run(\"disable-cname-many\", func(t *testing.T) {\n\t\tdtyp := dns.TypeA\n\t\thost := path.Base(t.Name())\n\n\t\tvar res filtering.Result\n\t\tres, err = f.CheckHostRules(host, dtyp, setts)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, \"new-cname-2\", res.CanonName)\n\t\tassert.Nil(t, res.DNSRewriteResult)\n\t})\n\n\tt.Run(\"disable-all\", func(t *testing.T) {\n\t\tdtyp := dns.TypeA\n\t\thost := path.Base(t.Name())\n\n\t\tvar res filtering.Result\n\t\tres, err = f.CheckHostRules(host, dtyp, setts)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, res.CanonName)\n\t\tassert.Empty(t, res.Rules)\n\t})\n\n\tt.Run(\"1.2.3.4.in-addr.arpa\", func(t *testing.T) {\n\t\tdtyp := dns.TypePTR\n\t\thost := path.Base(t.Name())\n\n\t\tvar res filtering.Result\n\t\tres, err = f.CheckHostRules(host, dtyp, setts)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, res.DNSRewriteResult)\n\n\t\trr := res.DNSRewriteResult\n\t\trequire.NotEmpty(t, rr.Response)\n\n\t\tresps := rr.Response[dtyp]\n\t\trequire.Len(t, resps, 1)\n\n\t\tptr, ok := resps[0].(string)\n\t\trequire.True(t, ok)\n\n\t\tassert.Equal(t, \"new-ptr.\", ptr)\n\t})\n\n\tt.Run(\"1.2.3.5.in-addr.arpa\", func(t *testing.T) {\n\t\tdtyp := dns.TypePTR\n\t\thost := path.Base(t.Name())\n\n\t\tvar res filtering.Result\n\t\tres, err = f.CheckHostRules(host, dtyp, setts)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, res.DNSRewriteResult)\n\n\t\trr := res.DNSRewriteResult\n\t\trequire.NotEmpty(t, rr.Response)\n\n\t\tresps := rr.Response[dtyp]\n\t\trequire.Len(t, resps, 1)\n\n\t\tptr, ok := resps[0].(string)\n\t\trequire.True(t, ok)\n\n\t\tassert.Equal(t, \"new-ptr-with-dot.\", ptr)\n\t})\n}\n"
  },
  {
    "path": "internal/filtering/filter.go",
    "content": "package filtering\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// filterDir is the subdirectory of a data directory to store downloaded\n// filters.\nconst filterDir = \"filters\"\n\n// FilterYAML represents a filter list in the configuration file.\n//\n// TODO(e.burkov):  Investigate if the field ordering is important.\ntype FilterYAML struct {\n\tEnabled     bool\n\tURL         string    // URL or a file path\n\tName        string    `yaml:\"name\"`\n\tRulesCount  int       `yaml:\"-\"`\n\tLastUpdated time.Time `yaml:\"-\"`\n\tchecksum    uint32    // checksum of the file data\n\twhite       bool\n\n\tFilter `yaml:\",inline\"`\n}\n\n// Clear filter rules\nfunc (filter *FilterYAML) unload() {\n\tfilter.RulesCount = 0\n\tfilter.checksum = 0\n}\n\n// Path to the filter contents\nfunc (filter *FilterYAML) Path(dataDir string) string {\n\treturn filepath.Join(\n\t\tdataDir,\n\t\tfilterDir,\n\t\tstrconv.FormatUint(uint64(filter.ID), 10)+\".txt\",\n\t)\n}\n\n// ensureName sets provided title or default name for the filter if it doesn't\n// have name already.\nfunc (filter *FilterYAML) ensureName(title string) {\n\tif filter.Name != \"\" {\n\t\treturn\n\t}\n\n\tif title != \"\" {\n\t\tfilter.Name = title\n\n\t\treturn\n\t}\n\n\tfilter.Name = fmt.Sprintf(\"List %d\", filter.ID)\n}\n\nconst (\n\t// errFilterNotExist is returned from [filterSetProperties] when there are\n\t// no lists with the desired URL to update.\n\t//\n\t// TODO(e.burkov):  Use wherever the same error is needed.\n\terrFilterNotExist errors.Error = \"url doesn't exist\"\n\n\t// errFilterExists is returned from [filterSetProperties] when there is\n\t// another filter having the same URL as the one updated.\n\t//\n\t// TODO(e.burkov):  Use wherever the same error is needed.\n\terrFilterExists errors.Error = \"url already exists\"\n)\n\n// filterSetProperties searches for the particular filter list by url and sets\n// the values of newList to it, updating afterwards if needed.  It returns true\n// if the update was performed and the filtering engine restart is required.\nfunc (d *DNSFilter) filterSetProperties(\n\tlistURL string,\n\tnewList FilterYAML,\n\tisAllowlist bool,\n) (shouldRestart bool, err error) {\n\td.conf.filtersMu.Lock()\n\tdefer d.conf.filtersMu.Unlock()\n\n\tfilters := d.conf.Filters\n\tif isAllowlist {\n\t\tfilters = d.conf.WhitelistFilters\n\t}\n\n\ti := slices.IndexFunc(filters, func(flt FilterYAML) bool { return flt.URL == listURL })\n\tif i == -1 {\n\t\treturn false, errFilterNotExist\n\t}\n\n\tflt := &filters[i]\n\td.logger.DebugContext(\n\t\tcontext.TODO(),\n\t\t\"updating filter\",\n\t\t\"name\", newList.Name,\n\t\t\"url\", newList.URL,\n\t\t\"enabled\", newList.Enabled,\n\t\t\"filter_url\", flt.URL,\n\t)\n\n\tdefer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time, oldRulesCount int) {\n\t\tif err != nil {\n\t\t\tflt.URL = oldURL\n\t\t\tflt.Name = oldName\n\t\t\tflt.Enabled = oldEnabled\n\t\t\tflt.LastUpdated = oldUpdated\n\t\t\tflt.RulesCount = oldRulesCount\n\t\t}\n\t}(flt.URL, flt.Name, flt.Enabled, flt.LastUpdated, flt.RulesCount)\n\n\tflt.Name = newList.Name\n\n\tif flt.URL != newList.URL {\n\t\tif d.filterExistsLocked(newList.URL) {\n\t\t\treturn false, errFilterExists\n\t\t}\n\n\t\tshouldRestart = true\n\n\t\tflt.URL = newList.URL\n\t\tflt.LastUpdated = time.Time{}\n\t\tflt.unload()\n\t}\n\n\tif flt.Enabled != newList.Enabled {\n\t\tflt.Enabled = newList.Enabled\n\t\tshouldRestart = true\n\t}\n\n\tif !flt.Enabled {\n\t\t// TODO(e.burkov):  The validation of the contents of the new URL is\n\t\t// currently skipped if the rule list is disabled.  This makes it\n\t\t// possible to set a bad rules source, but the validation should still\n\t\t// kick in when the filter is enabled.  Consider changing this behavior\n\t\t// to be stricter.\n\t\tflt.unload()\n\n\t\treturn shouldRestart, err\n\t}\n\n\tif !shouldRestart {\n\t\treturn false, nil\n\t}\n\n\treturn d.update(flt)\n}\n\n// filterExists returns true if a filter with the same url exists in d.  It's\n// safe for concurrent use.\nfunc (d *DNSFilter) filterExists(url string) (ok bool) {\n\td.conf.filtersMu.RLock()\n\tdefer d.conf.filtersMu.RUnlock()\n\n\tr := d.filterExistsLocked(url)\n\n\treturn r\n}\n\n// filterExistsLocked returns true if d contains the filter with the same url.\n// d.filtersMu is expected to be locked.\nfunc (d *DNSFilter) filterExistsLocked(url string) (ok bool) {\n\tfor _, f := range d.conf.Filters {\n\t\tif f.URL == url {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tfor _, f := range d.conf.WhitelistFilters {\n\t\tif f.URL == url {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Add a filter\n// Return FALSE if a filter with this URL exists\nfunc (d *DNSFilter) filterAdd(flt FilterYAML) (err error) {\n\t// Defer annotating to unlock sooner.\n\tdefer func() { err = errors.Annotate(err, \"adding filter: %w\") }()\n\n\td.conf.filtersMu.Lock()\n\tdefer d.conf.filtersMu.Unlock()\n\n\t// Check for duplicates.\n\tif d.filterExistsLocked(flt.URL) {\n\t\treturn errFilterExists\n\t}\n\n\tif flt.white {\n\t\td.conf.WhitelistFilters = append(d.conf.WhitelistFilters, flt)\n\t} else {\n\t\td.conf.Filters = append(d.conf.Filters, flt)\n\t}\n\n\treturn nil\n}\n\n// Load filters from the disk\n// And if any filter has zero ID, assign a new one\nfunc (d *DNSFilter) loadFilters(ctx context.Context, array []FilterYAML) {\n\tfor i := range array {\n\t\tfilter := &array[i] // otherwise we're operating on a copy\n\t\tif filter.ID == 0 {\n\t\t\tnewID := d.idGen.next()\n\t\t\td.logger.WarnContext(ctx, \"filter has no id\", \"idx\", i, \"new_id\", newID)\n\n\t\t\tfilter.ID = newID\n\t\t}\n\n\t\tif !filter.Enabled {\n\t\t\t// No need to load a filter that is not enabled\n\t\t\tcontinue\n\t\t}\n\n\t\terr := d.load(ctx, filter)\n\t\tif err != nil {\n\t\t\td.logger.ErrorContext(ctx, \"loading filter\", \"id\", filter.ID, slogutil.KeyError, err)\n\t\t}\n\t}\n}\n\nfunc deduplicateFilters(filters []FilterYAML) (deduplicated []FilterYAML) {\n\turls := container.NewMapSet[string]()\n\tlastIdx := 0\n\n\tfor _, filter := range filters {\n\t\tif !urls.Has(filter.URL) {\n\t\t\turls.Add(filter.URL)\n\t\t\tfilters[lastIdx] = filter\n\t\t\tlastIdx++\n\t\t}\n\t}\n\n\treturn filters[:lastIdx]\n}\n\n// tryRefreshFilters is like [refreshFilters], but backs down if the update is\n// already going on.\n//\n// TODO(e.burkov):  Get rid of the concurrency pattern which requires the\n// [sync.Mutex.TryLock].\nfunc (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {\n\tif ok = d.refreshLock.TryLock(); !ok {\n\t\treturn 0, false, false\n\t}\n\tdefer d.refreshLock.Unlock()\n\n\tupdated, isNetworkErr = d.refreshFiltersIntl(block, allow, force)\n\n\treturn updated, isNetworkErr, ok\n}\n\n// listsToUpdate returns the slice of filter lists that could be updated.\nfunc (d *DNSFilter) listsToUpdate(filters *[]FilterYAML, force bool) (toUpd []FilterYAML) {\n\tnow := time.Now()\n\n\td.conf.filtersMu.RLock()\n\tdefer d.conf.filtersMu.RUnlock()\n\n\tfor i := range *filters {\n\t\tflt := &(*filters)[i] // otherwise we will be operating on a copy\n\n\t\tif !flt.Enabled {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !force {\n\t\t\texp := flt.LastUpdated.Add(time.Duration(d.conf.FiltersUpdateIntervalHours) * time.Hour)\n\t\t\tif now.Before(exp) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\ttoUpd = append(toUpd, FilterYAML{\n\t\t\tFilter: Filter{\n\t\t\t\tID: flt.ID,\n\t\t\t},\n\t\t\tURL:      flt.URL,\n\t\t\tName:     flt.Name,\n\t\t\tchecksum: flt.checksum,\n\t\t})\n\t}\n\n\treturn toUpd\n}\n\n// refreshFiltersArray updates the filters array and returns the number of\n// filters that have been refreshed.  updateFlags is true if filter data has\n// changed.\nfunc (d *DNSFilter) refreshFiltersArray(\n\tctx context.Context,\n\tfilters *[]FilterYAML,\n\tforce bool,\n) (updateCount int, updateFilters []FilterYAML, updateFlags []bool, isNetErr bool) {\n\tupdateFilters = d.listsToUpdate(filters, force)\n\tif len(updateFilters) == 0 {\n\t\treturn 0, nil, nil, false\n\t}\n\n\tfailNum, updateFlags := d.updateFilterList(ctx, updateFilters)\n\tif failNum == len(updateFilters) {\n\t\treturn 0, nil, nil, true\n\t}\n\n\td.conf.filtersMu.Lock()\n\tdefer d.conf.filtersMu.Unlock()\n\n\tupdateCount = d.syncUpdatedFilters(ctx, filters, updateFilters, updateFlags)\n\n\treturn updateCount, updateFilters, updateFlags, false\n}\n\n// updateFilterList updates each filter in updateFilters and returns the number\n// of failures and the updateFlags slice aligned with updateFilters indicating\n// whether each filter's data changed.\nfunc (d *DNSFilter) updateFilterList(\n\tctx context.Context,\n\tupdateFilters []FilterYAML,\n) (failNum int, updateFlags []bool) {\n\tfor i := range updateFilters {\n\t\tuf := &updateFilters[i]\n\t\tupdated, err := d.update(uf)\n\t\tupdateFlags = append(updateFlags, updated)\n\t\tif err != nil {\n\t\t\tfailNum++\n\t\t\td.logger.ErrorContext(ctx, \"updating filter\", \"url\", uf.URL, slogutil.KeyError, err)\n\t\t}\n\t}\n\n\treturn failNum, updateFlags\n}\n\n// syncUpdatedFilters syncs updated filters back to the original filters slice\n// and returns the updateCount.  filters must not be nil.  updateFlags must\n// align with updateFilters.  d.conf.filtersMu must be locked.\nfunc (d *DNSFilter) syncUpdatedFilters(\n\tctx context.Context,\n\tfilters *[]FilterYAML,\n\tupdateFilters []FilterYAML,\n\tupdateFlags []bool,\n) (updateCount int) {\n\tfor i := range updateFilters {\n\t\tuf := &updateFilters[i]\n\t\tupdated := updateFlags[i]\n\n\t\tfor k := range *filters {\n\t\t\tf := &(*filters)[k]\n\t\t\tif f.ID != uf.ID || f.URL != uf.URL {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tf.LastUpdated = uf.LastUpdated\n\t\t\tif !updated {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\td.logger.InfoContext(\n\t\t\t\tctx,\n\t\t\t\t\"updated filter\",\n\t\t\t\t\"id\", f.ID,\n\t\t\t\t\"rules_count\", uf.RulesCount,\n\t\t\t\t\"prev_rules_count\", f.RulesCount,\n\t\t\t)\n\n\t\t\tf.Name = uf.Name\n\t\t\tf.RulesCount = uf.RulesCount\n\t\t\tf.checksum = uf.checksum\n\t\t\tupdateCount++\n\t\t}\n\t}\n\n\treturn updateCount\n}\n\n// refreshFiltersIntl checks filters and updates them if necessary.  If force is\n// true, it ignores the filter.LastUpdated field value.\n//\n// Algorithm:\n//\n//  1. Get the list of filters to be updated.  For each filter, run the download\n//     and checksum check operation.  Store downloaded data in a temporary file\n//     inside data/filters directory\n//\n//  2. For each filter, if filter data hasn't changed, just set new update time\n//     on file.  Otherwise, rename the temporary file (<temp> -> 1.txt).  Note\n//     that this method works only on Unix systems.  On Windows, don't pass\n//     files to filtering, pass the whole data.\n//\n// refreshFiltersIntl returns the number of updated filters.  It also returns\n// true if there was a network error and nothing could be updated.\n//\n// TODO(a.garipov, e.burkov): What the hell?\nfunc (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {\n\tctx := context.TODO()\n\n\tupdNum := 0\n\td.logger.DebugContext(ctx, \"starting update\")\n\tdefer func() {\n\t\td.logger.DebugContext(ctx, \"finished update\", \"updated\", updNum)\n\t}()\n\n\tvar lists []FilterYAML\n\tvar toUpd []bool\n\tisNetErr := false\n\n\tif block {\n\t\tupdNum, lists, toUpd, isNetErr = d.refreshFiltersArray(ctx, &d.conf.Filters, force)\n\t}\n\tif allow {\n\t\tupdNumAl, listsAl, toUpdAl, isNetErrAl := d.refreshFiltersArray(\n\t\t\tctx,\n\t\t\t&d.conf.WhitelistFilters,\n\t\t\tforce,\n\t\t)\n\n\t\tupdNum += updNumAl\n\t\tlists = append(lists, listsAl...)\n\t\ttoUpd = append(toUpd, toUpdAl...)\n\t\tisNetErr = isNetErr || isNetErrAl\n\t}\n\tif isNetErr {\n\t\treturn 0, true\n\t}\n\n\tif updNum == 0 {\n\t\treturn 0, false\n\t}\n\n\td.EnableFilters(false)\n\n\tfor i := range lists {\n\t\tif toUpd[i] {\n\t\t\tremoveOldFilterFile(ctx, d.logger, lists[i].Path(d.conf.DataDir))\n\t\t}\n\t}\n\n\treturn updNum, false\n}\n\n// removeOldFilterFile deletes the old filter file and logs any error at the\n// appropriate level.  l must not be nil.\nfunc removeOldFilterFile(ctx context.Context, l *slog.Logger, fltPath string) {\n\terr := os.Remove(fltPath + \".old\")\n\tif err == nil {\n\t\treturn\n\t}\n\n\tlvl := slog.LevelWarn\n\tif errors.Is(err, os.ErrNotExist) {\n\t\tlvl = slog.LevelDebug\n\t}\n\n\tl.Log(ctx, lvl, \"removing old filter\", \"path\", fltPath, slogutil.KeyError, err)\n}\n\n// update refreshes filter's content and a/mtimes of it's file.\nfunc (d *DNSFilter) update(filter *FilterYAML) (b bool, err error) {\n\tctx := context.TODO()\n\n\tb, err = d.updateIntl(ctx, filter)\n\tfilter.LastUpdated = time.Now()\n\tif !b {\n\t\tchErr := os.Chtimes(\n\t\t\tfilter.Path(d.conf.DataDir),\n\t\t\tfilter.LastUpdated,\n\t\t\tfilter.LastUpdated,\n\t\t)\n\t\tif chErr != nil {\n\t\t\td.logger.ErrorContext(ctx, \"changing last modified time\", slogutil.KeyError, chErr)\n\t\t}\n\t}\n\n\treturn b, err\n}\n\n// updateIntl updates the flt rewriting it's actual file.  It returns true if\n// the actual update has been performed.\nfunc (d *DNSFilter) updateIntl(ctx context.Context, flt *FilterYAML) (ok bool, err error) {\n\td.logger.DebugContext(ctx, \"downloading update for filter\", \"id\", flt.ID, \"url\", flt.URL)\n\n\tvar res *rulelist.ParseResult\n\n\ttmpFile, err := aghrenameio.NewPendingFile(flt.Path(d.conf.DataDir), aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer func() { err = d.finalizeUpdate(ctx, tmpFile, flt, res, err, ok) }()\n\n\tr, err := d.reader(flt.URL)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn false, err\n\t}\n\tdefer func() { err = errors.WithDeferred(err, r.Close()) }()\n\n\tbufPtr := d.bufPool.Get()\n\tdefer d.bufPool.Put(bufPtr)\n\n\tp := rulelist.NewParser()\n\tres, err = p.Parse(tmpFile, r, *bufPtr)\n\n\treturn res.Checksum != flt.checksum && err == nil, err\n}\n\n// finalizeUpdate closes and gets rid of temporary file f with filter's content\n// according to updated.  It also saves new values of flt's name, rules number\n// and checksum if succeeded.\nfunc (d *DNSFilter) finalizeUpdate(\n\tctx context.Context,\n\tfile aghrenameio.PendingFile,\n\tflt *FilterYAML,\n\tres *rulelist.ParseResult,\n\treturned error,\n\tupdated bool,\n) (err error) {\n\tid := flt.ID\n\tif !updated {\n\t\tif returned == nil {\n\t\t\td.logger.DebugContext(ctx, \"skipping filter with no changes\", \"id\", id, \"url\", flt.URL)\n\t\t}\n\n\t\treturn errors.WithDeferred(returned, file.Cleanup())\n\t}\n\n\td.logger.InfoContext(ctx, \"saving contents\", \"id\", id, \"path\", flt.Path(d.conf.DataDir))\n\n\terr = file.CloseReplace()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"finalizing update: %w\", err)\n\t}\n\n\trulesCount := res.RulesCount\n\td.logger.InfoContext(\n\t\tctx,\n\t\t\"filter updated\",\n\t\t\"id\", id,\n\t\t\"bytes_written\", res.BytesWritten,\n\t\t\"rules_count\", rulesCount,\n\t)\n\n\tflt.ensureName(res.Title)\n\tflt.checksum = res.Checksum\n\tflt.RulesCount = rulesCount\n\n\treturn nil\n}\n\n// reader returns an io.ReadCloser reading filtering-rule list data form either\n// a file on the filesystem or the filter's HTTP URL.\nfunc (d *DNSFilter) reader(fltURL string) (r io.ReadCloser, err error) {\n\tif !filepath.IsAbs(fltURL) {\n\t\tr, err = d.readerFromURL(fltURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"reading from url: %w\", err)\n\t\t}\n\n\t\treturn r, nil\n\t}\n\n\tfltURL = filepath.Clean(fltURL)\n\tif !pathMatchesAny(d.safeFSPatterns, fltURL) {\n\t\treturn nil, fmt.Errorf(\"path %q does not match safe patterns\", fltURL)\n\t}\n\n\tr, err = os.Open(fltURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"opening file: %w\", err)\n\t}\n\n\treturn r, nil\n}\n\n// readerFromURL returns an io.ReadCloser reading filtering-rule list data form\n// the filter's URL.\nfunc (d *DNSFilter) readerFromURL(fltURL string) (r io.ReadCloser, err error) {\n\tresp, err := d.conf.HTTPClient.Get(fltURL)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"got status code %d, want %d\", resp.StatusCode, http.StatusOK)\n\t}\n\n\treturn resp.Body, nil\n}\n\n// loads filter contents from the file in dataDir\nfunc (d *DNSFilter) load(ctx context.Context, flt *FilterYAML) (err error) {\n\tfileName := flt.Path(d.conf.DataDir)\n\n\td.logger.DebugContext(ctx, \"loading filter\", \"id\", flt.ID, \"path\", fileName)\n\n\t// #nosec G304 -- Assume that fileName is always within DataDir.\n\tfile, err := os.Open(fileName)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\t// Do nothing, file doesn't exist.\n\t\treturn nil\n\t} else if err != nil {\n\t\treturn fmt.Errorf(\"opening filter file: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, file.Close()) }()\n\n\tst, err := file.Stat()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting filter file stat: %w\", err)\n\t}\n\n\td.logger.DebugContext(ctx, \"filter file\", \"id\", flt.ID, \"path\", fileName, \"len\", st.Size())\n\n\tbufPtr := d.bufPool.Get()\n\tdefer d.bufPool.Put(bufPtr)\n\n\tp := rulelist.NewParser()\n\tres, err := p.Parse(io.Discard, file, *bufPtr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing filter file: %w\", err)\n\t}\n\n\tflt.ensureName(res.Title)\n\tflt.RulesCount, flt.checksum, flt.LastUpdated = res.RulesCount, res.Checksum, st.ModTime()\n\n\treturn nil\n}\n\n// EnableFilters enables filters.\nfunc (d *DNSFilter) EnableFilters(async bool) {\n\td.conf.filtersMu.RLock()\n\tdefer d.conf.filtersMu.RUnlock()\n\n\td.enableFiltersLocked(context.TODO(), async)\n}\n\n// enableFiltersLocked enables filters under the conf.filtersMu lock.\nfunc (d *DNSFilter) enableFiltersLocked(ctx context.Context, async bool) {\n\tfilters := make([]Filter, 1, len(d.conf.Filters)+len(d.conf.WhitelistFilters)+1)\n\tfilters[0] = Filter{\n\t\tID:   rulelist.IDCustom,\n\t\tData: []byte(strings.Join(d.conf.UserRules, \"\\n\")),\n\t}\n\n\tfor _, filter := range d.conf.Filters {\n\t\tif !filter.Enabled {\n\t\t\tcontinue\n\t\t}\n\n\t\tfilters = append(filters, Filter{\n\t\t\tID:       filter.ID,\n\t\t\tFilePath: filter.Path(d.conf.DataDir),\n\t\t})\n\t}\n\n\tvar allowFilters []Filter\n\tfor _, filter := range d.conf.WhitelistFilters {\n\t\tif !filter.Enabled {\n\t\t\tcontinue\n\t\t}\n\n\t\tallowFilters = append(allowFilters, Filter{\n\t\t\tID:       filter.ID,\n\t\t\tFilePath: filter.Path(d.conf.DataDir),\n\t\t})\n\t}\n\n\terr := d.setFilters(ctx, filters, allowFilters, async)\n\tif err != nil {\n\t\td.logger.ErrorContext(ctx, \"enabling filters\", slogutil.KeyError, err)\n\t}\n\n\td.SetEnabled(d.conf.FilteringEnabled)\n}\n\n// ApplyAdditionalFiltering enhances the provided filtering settings with\n// blocked services and client-specific configurations.\nfunc (d *DNSFilter) ApplyAdditionalFiltering(cliAddr netip.Addr, clientID string, setts *Settings) {\n\tsetts.ClientIP = cliAddr\n\n\td.ApplyBlockedServices(setts)\n\td.applyClientFiltering(clientID, cliAddr, setts)\n\tif setts.BlockedServices != nil {\n\t\t// TODO(e.burkov):  Get rid of this crutch.\n\t\tsetts.ServicesRules = nil\n\t\tsvcs := setts.BlockedServices.IDs\n\t\tif !setts.BlockedServices.Schedule.Contains(time.Now()) {\n\t\t\td.ApplyBlockedServicesList(setts, svcs)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/filter_internal_test.go",
    "content": "package filtering\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// serveHTTPLocally starts a new HTTP server, that handles its index with h.  It\n// also gracefully closes the listener when the test under t finishes.\nfunc serveHTTPLocally(tb testing.TB, h http.Handler) (urlStr string) {\n\ttb.Helper()\n\n\tl, err := net.Listen(\"tcp\", \":0\")\n\trequire.NoError(tb, err)\n\n\tgo func() { _ = http.Serve(l, h) }()\n\ttestutil.CleanupAndRequireSuccess(tb, l.Close)\n\n\taddr := testutil.RequireTypeAssert[*net.TCPAddr](tb, l.Addr())\n\n\treturn (&url.URL{\n\t\tScheme: urlutil.SchemeHTTP,\n\t\tHost:   addr.String(),\n\t}).String()\n}\n\n// serveFiltersLocally is a helper that concurrently listens on a free port to\n// respond with fltContent.\nfunc serveFiltersLocally(tb testing.TB, fltContent []byte) (urlStr string) {\n\ttb.Helper()\n\n\treturn serveHTTPLocally(tb, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tpt := testutil.PanicT{}\n\n\t\tn, werr := w.Write(fltContent)\n\t\trequire.NoError(pt, werr)\n\t\trequire.Equal(pt, len(fltContent), n)\n\t}))\n}\n\n// updateAndAssert loads filter content from its URL and then asserts rules\n// count.\nfunc updateAndAssert(\n\ttb testing.TB,\n\tctx context.Context,\n\tdnsFilter *DNSFilter,\n\tf *FilterYAML,\n\twantUpd require.BoolAssertionFunc,\n\twantRulesCount int,\n) {\n\ttb.Helper()\n\n\tok, err := dnsFilter.update(f)\n\trequire.NoError(tb, err)\n\twantUpd(tb, ok)\n\n\tassert.Equal(tb, wantRulesCount, f.RulesCount)\n\n\tdir, err := os.ReadDir(filepath.Join(dnsFilter.conf.DataDir, filterDir))\n\trequire.NoError(tb, err)\n\trequire.FileExists(tb, f.Path(dnsFilter.conf.DataDir))\n\n\tassert.Len(tb, dir, 1)\n\n\terr = dnsFilter.load(ctx, f)\n\trequire.NoError(tb, err)\n}\n\n// newDNSFilter returns a new properly initialized DNS filter instance.\nfunc newDNSFilter(tb testing.TB) (d *DNSFilter) {\n\ttb.Helper()\n\n\tdnsFilter, err := New(&Config{\n\t\tLogger:  testLogger,\n\t\tDataDir: tb.TempDir(),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: testTimeout,\n\t\t},\n\t}, nil)\n\trequire.NoError(tb, err)\n\n\treturn dnsFilter\n}\n\nfunc TestDNSFilter_Update(t *testing.T) {\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tconst content = `||example.org^$third-party\n\t# Inline comment example\n\t||example.com^$third-party\n\t0.0.0.0 example.com\n\t`\n\n\tfltContent := []byte(content)\n\taddr := serveFiltersLocally(t, fltContent)\n\tf := &FilterYAML{\n\t\tURL:  addr,\n\t\tName: \"test-filter\",\n\t}\n\n\tdnsFilter := newDNSFilter(t)\n\n\tt.Run(\"download\", func(t *testing.T) {\n\t\tupdateAndAssert(t, ctx, dnsFilter, f, require.True, 3)\n\t})\n\n\tt.Run(\"refresh_idle\", func(t *testing.T) {\n\t\tupdateAndAssert(t, ctx, dnsFilter, f, require.False, 3)\n\t})\n\n\tt.Run(\"refresh_actually\", func(t *testing.T) {\n\t\tanotherContent := []byte(`||example.com^`)\n\t\toldURL := f.URL\n\n\t\tf.URL = serveFiltersLocally(t, anotherContent)\n\t\tt.Cleanup(func() { f.URL = oldURL })\n\n\t\tupdateAndAssert(t, ctx, dnsFilter, f, require.True, 1)\n\t})\n\n\tt.Run(\"load_unload\", func(t *testing.T) {\n\t\terr := dnsFilter.load(ctx, f)\n\t\trequire.NoError(t, err)\n\n\t\tf.unload()\n\t})\n}\n\nfunc TestFilterYAML_EnsureName(t *testing.T) {\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tdnsFilter := newDNSFilter(t)\n\n\tt.Run(\"title_custom\", func(t *testing.T) {\n\t\tcontent := []byte(\"! Title: src-title\\n||example.com^\")\n\n\t\tf := &FilterYAML{\n\t\t\tURL:  serveFiltersLocally(t, content),\n\t\t\tName: \"user-custom\",\n\t\t}\n\n\t\tupdateAndAssert(t, ctx, dnsFilter, f, require.True, 1)\n\t\tassert.Equal(t, \"user-custom\", f.Name)\n\t})\n\n\tt.Run(\"title_from_src\", func(t *testing.T) {\n\t\tcontent := []byte(\"! Title: src-title\\n||example.com^\")\n\n\t\tf := &FilterYAML{\n\t\t\tURL: serveFiltersLocally(t, content),\n\t\t}\n\n\t\tupdateAndAssert(t, ctx, dnsFilter, f, require.True, 1)\n\t\tassert.Equal(t, \"src-title\", f.Name)\n\t})\n\n\tt.Run(\"title_default\", func(t *testing.T) {\n\t\tcontent := []byte(\"||example.com^\")\n\n\t\tf := &FilterYAML{\n\t\t\tURL: serveFiltersLocally(t, content),\n\t\t}\n\n\t\tupdateAndAssert(t, ctx, dnsFilter, f, require.True, 1)\n\t\tassert.Equal(t, \"List 0\", f.Name)\n\t})\n}\n"
  },
  {
    "path": "internal/filtering/filtering.go",
    "content": "// Package filtering implements a DNS request and response filter.\npackage filtering\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/hostsfile\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/mathutil\"\n\t\"github.com/AdguardTeam/golibs/syncutil\"\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/filterlist\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// ServiceEntry - blocked service array element\ntype ServiceEntry struct {\n\tName  string\n\tRules []*rules.NetworkRule\n}\n\n// Settings are custom filtering settings for a client.\n//\n// TODO(s.chzhen):  Move to the client package.\ntype Settings struct {\n\tClientName string\n\tClientIP   netip.Addr\n\tClientTags []string\n\n\tServicesRules []ServiceEntry\n\n\t// BlockedServices is the configuration of blocked services of a client.  It\n\t// is nil if the client does not have any blocked services.\n\tBlockedServices *BlockedServices\n\n\tProtectionEnabled   bool\n\tFilteringEnabled    bool\n\tSafeSearchEnabled   bool\n\tSafeBrowsingEnabled bool\n\tParentalEnabled     bool\n\n\t// ClientSafeSearch is a client configured safe search.\n\tClientSafeSearch SafeSearch\n}\n\n// Resolver is the interface for net.Resolver to simplify testing.\ntype Resolver interface {\n\tLookupIP(ctx context.Context, network, host string) (ips []net.IP, err error)\n}\n\n// Config allows you to configure DNS filtering with New() or just change variables directly.\ntype Config struct {\n\t// logger is used to log the operations of DNS filtering.  It must not be\n\t// nil.\n\tLogger *slog.Logger `yaml:\"-\"`\n\n\t// BlockingIPv4 is the IP address to be returned for a blocked A request.\n\tBlockingIPv4 netip.Addr `yaml:\"blocking_ipv4\"`\n\n\t// BlockingIPv6 is the IP address to be returned for a blocked AAAA request.\n\tBlockingIPv6 netip.Addr `yaml:\"blocking_ipv6\"`\n\n\t// SafeBrowsingChecker is the safe browsing hash-prefix checker.\n\tSafeBrowsingChecker Checker `yaml:\"-\"`\n\n\t// ParentControl is the parental control hash-prefix checker.\n\tParentalControlChecker Checker `yaml:\"-\"`\n\n\tSafeSearch SafeSearch `yaml:\"-\"`\n\n\t// ApplyClientFiltering retrieves persistent client information using the\n\t// ClientID or client IP address, and applies it to the filtering settings.\n\t// It must not be nil.\n\tApplyClientFiltering func(clientID string, cliAddr netip.Addr, setts *Settings) `yaml:\"-\"`\n\n\t// BlockedServices is the configuration of blocked services.\n\t// Per-client settings can override this configuration.\n\tBlockedServices *BlockedServices `yaml:\"blocked_services\"`\n\n\t// EtcHosts is a container of IP-hostname pairs taken from the operating\n\t// system configuration files (e.g. /etc/hosts).\n\t//\n\t// TODO(e.burkov):  Move it to dnsforward entirely.\n\tEtcHosts hostsfile.Storage `yaml:\"-\"`\n\n\t// ConfModifier is used to update the global configuration.  It must not be\n\t// nil.\n\tConfModifier agh.ConfigModifier `yaml:\"-\"`\n\n\t// HTTPReg registers HTTP handlers.  It must not be nil.\n\tHTTPReg aghhttp.Registrar `yaml:\"-\"`\n\n\t// HTTPClient is the client to use for updating the remote filters.\n\tHTTPClient *http.Client `yaml:\"-\"`\n\n\t// filtersMu protects filter lists.\n\tfiltersMu *sync.RWMutex\n\n\t// ProtectionDisabledUntil is the timestamp until when the protection is\n\t// disabled.\n\tProtectionDisabledUntil *time.Time `yaml:\"protection_disabled_until\"`\n\n\tSafeSearchConf SafeSearchConfig `yaml:\"safe_search\"`\n\n\t// DataDir is used to store filters' contents.\n\tDataDir string `yaml:\"-\"`\n\n\t// BlockingMode defines the way how blocked responses are constructed.\n\tBlockingMode BlockingMode `yaml:\"blocking_mode\"`\n\n\t// ParentalBlockHost is the IP (or domain name) which is used to respond to\n\t// DNS requests blocked by parental control.\n\tParentalBlockHost string `yaml:\"parental_block_host\"`\n\n\t// SafeBrowsingBlockHost is the IP (or domain name) which is used to respond\n\t// to DNS requests blocked by safe-browsing.\n\tSafeBrowsingBlockHost string `yaml:\"safebrowsing_block_host\"`\n\n\t// Rewrites is a list of legacy DNS rewrite records.\n\tRewrites []*LegacyRewrite `yaml:\"rewrites\"`\n\n\t// Filters are the blocking filter lists.\n\tFilters []FilterYAML `yaml:\"-\"`\n\n\t// WhitelistFilters are the allowing filter lists.\n\tWhitelistFilters []FilterYAML `yaml:\"-\"`\n\n\t// UserRules is the global list of custom rules.\n\tUserRules []string `yaml:\"-\"`\n\n\t// SafeFSPatterns are the patterns for matching which local filtering-rule\n\t// files can be added.\n\tSafeFSPatterns []string `yaml:\"safe_fs_patterns\"`\n\n\tSafeBrowsingCacheSize uint `yaml:\"safebrowsing_cache_size\"` // (in bytes)\n\tSafeSearchCacheSize   uint `yaml:\"safesearch_cache_size\"`   // (in bytes)\n\tParentalCacheSize     uint `yaml:\"parental_cache_size\"`     // (in bytes)\n\t// TODO(a.garipov): Use timeutil.Duration\n\tCacheTime uint `yaml:\"cache_time\"` // Element's TTL (in minutes)\n\n\t// enabled is used to be returned within Settings.\n\t//\n\t// It is of type uint32 to be accessed by atomic.\n\t//\n\t// TODO(e.burkov):  Use atomic.Bool in Go 1.19.\n\tenabled uint32\n\n\t// FiltersUpdateIntervalHours is the time period to update filters\n\t// (in hours).\n\tFiltersUpdateIntervalHours uint32 `yaml:\"filters_update_interval\"`\n\n\t// BlockedResponseTTL is the time-to-live value for blocked responses.  If\n\t// 0, then default value is used (3600).\n\tBlockedResponseTTL uint32 `yaml:\"blocked_response_ttl\"`\n\n\t// FilteringEnabled indicates whether or not use filter lists.\n\tFilteringEnabled bool `yaml:\"filtering_enabled\"`\n\n\t// RewritesEnabled indicates whether legacy rewrites are applied.\n\tRewritesEnabled bool `yaml:\"rewrites_enabled\"`\n\n\tParentalEnabled     bool `yaml:\"parental_enabled\"`\n\tSafeBrowsingEnabled bool `yaml:\"safebrowsing_enabled\"`\n\n\t// ProtectionEnabled defines whether or not use any of filtering features.\n\tProtectionEnabled bool `yaml:\"protection_enabled\"`\n}\n\n// BlockingMode is an enum of all allowed blocking modes.\ntype BlockingMode string\n\n// Allowed blocking modes.\nconst (\n\t// BlockingModeCustomIP means respond with a custom IP address.\n\tBlockingModeCustomIP BlockingMode = \"custom_ip\"\n\n\t// BlockingModeDefault is the same as BlockingModeNullIP for\n\t// Adblock-style rules, but responds with the IP address specified in\n\t// the rule when blocked by an `/etc/hosts`-style rule.\n\tBlockingModeDefault BlockingMode = \"default\"\n\n\t// BlockingModeNullIP means respond with a zero IP address: \"0.0.0.0\"\n\t// for A requests and \"::\" for AAAA ones.\n\tBlockingModeNullIP BlockingMode = \"null_ip\"\n\n\t// BlockingModeNXDOMAIN means respond with the NXDOMAIN code.\n\tBlockingModeNXDOMAIN BlockingMode = \"nxdomain\"\n\n\t// BlockingModeREFUSED means respond with the REFUSED code.\n\tBlockingModeREFUSED BlockingMode = \"refused\"\n)\n\n// LookupStats store stats collected during safebrowsing or parental checks\ntype LookupStats struct {\n\tRequests   uint64 // number of HTTP requests that were sent\n\tCacheHits  uint64 // number of lookups that didn't need HTTP requests\n\tPending    int64  // number of currently pending HTTP requests\n\tPendingMax int64  // maximum number of pending HTTP requests\n}\n\n// Stats store LookupStats for safebrowsing, parental and safesearch\ntype Stats struct {\n\tSafebrowsing LookupStats\n\tParental     LookupStats\n\tSafesearch   LookupStats\n}\n\n// Parameters to pass to filters-initializer goroutine\ntype filtersInitializerParams struct {\n\tallowFilters []Filter\n\tblockFilters []Filter\n}\n\ntype hostChecker struct {\n\tcheck func(host string, qtype uint16, setts *Settings) (res Result, err error)\n\tname  string\n}\n\n// Checker is used for safe browsing or parental control hash-prefix filtering.\ntype Checker interface {\n\t// Check returns true if request for the host should be blocked.\n\tCheck(host string) (block bool, err error)\n}\n\n// DNSFilter matches hostnames and DNS requests against filtering rules.\ntype DNSFilter struct {\n\t// logger is used for logging the filtering process.\n\tlogger *slog.Logger\n\n\t// idGen is used to generate IDs for package urlfilter.\n\tidGen *idGenerator\n\n\t// bufPool is a pool of buffers used for filtering-rule list parsing.\n\tbufPool *syncutil.Pool[[]byte]\n\n\trulesStorage    *filterlist.RuleStorage\n\tfilteringEngine *urlfilter.DNSEngine\n\n\trulesStorageAllow    *filterlist.RuleStorage\n\tfilteringEngineAllow *urlfilter.DNSEngine\n\n\tsafeSearch SafeSearch\n\n\t// safeBrowsingChecker is the safe browsing hash-prefix checker.\n\tsafeBrowsingChecker Checker\n\n\t// parentalControl is the parental control hash-prefix checker.\n\tparentalControlChecker Checker\n\n\t// applyClientFiltering retrieves persistent client information using the\n\t// ClientID or client IP address, and applies it to the filtering settings.\n\t//\n\t// TODO(s.chzhen):  Consider finding a better approach while taking an\n\t// import cycle into account.\n\tapplyClientFiltering func(clientID string, cliAddr netip.Addr, setts *Settings)\n\n\tengineLock sync.RWMutex\n\n\t// confMu protects conf.\n\tconfMu *sync.RWMutex\n\n\t// conf contains filtering parameters.\n\tconf *Config\n\n\t// done is the channel to signal to stop running filters updates loop.\n\tdone chan struct{}\n\n\t// Channel for passing data to filters-initializer goroutine\n\tfiltersInitializerChan chan filtersInitializerParams\n\tfiltersInitializerLock sync.Mutex\n\n\trefreshLock *sync.Mutex\n\n\thostCheckers []hostChecker\n\n\tsafeFSPatterns []string\n}\n\n// Filter represents a filter list\ntype Filter struct {\n\t// FilePath is the path to a filtering rules list file.\n\tFilePath string `yaml:\"-\"`\n\n\t// Data is the content of the file.\n\tData []byte `yaml:\"-\"`\n\n\t// ID is automatically assigned when filter is added.\n\tID rules.ListID `yaml:\"id\"`\n}\n\n// SetEnabled sets the status of the *DNSFilter.\nfunc (d *DNSFilter) SetEnabled(enabled bool) {\n\tatomic.StoreUint32(&d.conf.enabled, mathutil.BoolToNumber[uint32](enabled))\n}\n\n// Settings returns filtering settings.\nfunc (d *DNSFilter) Settings() (s *Settings) {\n\td.confMu.RLock()\n\tdefer d.confMu.RUnlock()\n\n\treturn &Settings{\n\t\tFilteringEnabled:    atomic.LoadUint32(&d.conf.enabled) != 0,\n\t\tSafeSearchEnabled:   d.conf.SafeSearchConf.Enabled,\n\t\tSafeBrowsingEnabled: d.conf.SafeBrowsingEnabled,\n\t\tParentalEnabled:     d.conf.ParentalEnabled,\n\t}\n}\n\n// WriteDiskConfig - write configuration\nfunc (d *DNSFilter) WriteDiskConfig(c *Config) {\n\tfunc() {\n\t\td.confMu.Lock()\n\t\tdefer d.confMu.Unlock()\n\n\t\t*c = *d.conf\n\t\tc.Rewrites = cloneRewrites(c.Rewrites)\n\t}()\n\n\td.conf.filtersMu.RLock()\n\tdefer d.conf.filtersMu.RUnlock()\n\n\tc.Filters = slices.Clone(d.conf.Filters)\n\tc.WhitelistFilters = slices.Clone(d.conf.WhitelistFilters)\n\tc.UserRules = slices.Clone(d.conf.UserRules)\n}\n\n// setFilters sets new filters, synchronously or asynchronously.  When filters\n// are set asynchronously, the old filters continue working until the new\n// filters are ready.\n//\n// In this case the caller must ensure that the old filter files are intact.\nfunc (d *DNSFilter) setFilters(\n\tctx context.Context,\n\tblockFilters []Filter,\n\tallowFilters []Filter,\n\tasync bool,\n) (err error) {\n\tif async {\n\t\tparams := filtersInitializerParams{\n\t\t\tallowFilters: allowFilters,\n\t\t\tblockFilters: blockFilters,\n\t\t}\n\n\t\td.filtersInitializerLock.Lock()\n\t\tdefer d.filtersInitializerLock.Unlock()\n\n\t\t// Remove all pending tasks.\n\tremoveLoop:\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-d.filtersInitializerChan:\n\t\t\t\t// Continue removing.\n\t\t\tdefault:\n\t\t\t\tbreak removeLoop\n\t\t\t}\n\t\t}\n\n\t\td.filtersInitializerChan <- params\n\n\t\treturn nil\n\t}\n\n\treturn d.initFiltering(ctx, allowFilters, blockFilters)\n}\n\n// Close - close the object\nfunc (d *DNSFilter) Close() {\n\td.engineLock.Lock()\n\tdefer d.engineLock.Unlock()\n\n\tif d.done != nil {\n\t\td.done <- struct{}{}\n\t}\n\n\td.reset(context.TODO())\n}\n\nfunc (d *DNSFilter) reset(ctx context.Context) {\n\tif d.rulesStorage != nil {\n\t\tif err := d.rulesStorage.Close(); err != nil {\n\t\t\td.logger.ErrorContext(ctx, \"closing rules storage\", slogutil.KeyError, err)\n\t\t}\n\t}\n\n\tif d.rulesStorageAllow != nil {\n\t\tif err := d.rulesStorageAllow.Close(); err != nil {\n\t\t\td.logger.ErrorContext(ctx, \"closing allow rules storage\", slogutil.KeyError, err)\n\t\t}\n\t}\n}\n\n// ProtectionStatus returns the status of protection and time until it's\n// disabled if so.\nfunc (d *DNSFilter) ProtectionStatus() (status bool, disabledUntil *time.Time) {\n\td.confMu.RLock()\n\tdefer d.confMu.RUnlock()\n\n\treturn d.conf.ProtectionEnabled, d.conf.ProtectionDisabledUntil\n}\n\n// SetProtectionStatus updates the status of protection and time until it's\n// disabled.\nfunc (d *DNSFilter) SetProtectionStatus(status bool, disabledUntil *time.Time) {\n\td.confMu.Lock()\n\tdefer d.confMu.Unlock()\n\n\td.conf.ProtectionEnabled = status\n\td.conf.ProtectionDisabledUntil = disabledUntil\n}\n\n// SetProtectionEnabled updates the status of protection.\nfunc (d *DNSFilter) SetProtectionEnabled(status bool) {\n\td.confMu.Lock()\n\tdefer d.confMu.Unlock()\n\n\td.conf.ProtectionEnabled = status\n}\n\n// SetBlockingMode sets blocking mode properties.\nfunc (d *DNSFilter) SetBlockingMode(mode BlockingMode, bIPv4, bIPv6 netip.Addr) {\n\td.confMu.Lock()\n\tdefer d.confMu.Unlock()\n\n\td.conf.BlockingMode = mode\n\tif mode == BlockingModeCustomIP {\n\t\td.conf.BlockingIPv4 = bIPv4\n\t\td.conf.BlockingIPv6 = bIPv6\n\t}\n}\n\n// BlockingMode returns blocking mode properties.\nfunc (d *DNSFilter) BlockingMode() (mode BlockingMode, bIPv4, bIPv6 netip.Addr) {\n\td.confMu.RLock()\n\tdefer d.confMu.RUnlock()\n\n\treturn d.conf.BlockingMode, d.conf.BlockingIPv4, d.conf.BlockingIPv6\n}\n\n// SetBlockedResponseTTL sets TTL for blocked responses.\nfunc (d *DNSFilter) SetBlockedResponseTTL(ttl uint32) {\n\td.confMu.Lock()\n\tdefer d.confMu.Unlock()\n\n\td.conf.BlockedResponseTTL = ttl\n}\n\n// BlockedResponseTTL returns TTL for blocked responses.\nfunc (d *DNSFilter) BlockedResponseTTL() (ttl uint32) {\n\td.confMu.Lock()\n\tdefer d.confMu.Unlock()\n\n\treturn d.conf.BlockedResponseTTL\n}\n\n// SafeBrowsingBlockHost returns a host for safe browsing blocked responses.\nfunc (d *DNSFilter) SafeBrowsingBlockHost() (host string) {\n\treturn d.conf.SafeBrowsingBlockHost\n}\n\n// ParentalBlockHost returns a host for parental protection blocked responses.\nfunc (d *DNSFilter) ParentalBlockHost() (host string) {\n\treturn d.conf.ParentalBlockHost\n}\n\n// Matched returns true if any match at all was found regardless of\n// whether it was filtered or not.\nfunc (r Reason) Matched() bool {\n\treturn r != NotFilteredNotFound\n}\n\n// CheckHostRules tries to match the host against filtering rules only.\nfunc (d *DNSFilter) CheckHostRules(host string, rrtype uint16, setts *Settings) (Result, error) {\n\treturn d.matchHost(strings.ToLower(host), rrtype, setts)\n}\n\n// CheckHost tries to match the host against filtering rules, then safebrowsing\n// and parental control rules, if they are enabled.\nfunc (d *DNSFilter) CheckHost(\n\thost string,\n\tqtype uint16,\n\tsetts *Settings,\n) (res Result, err error) {\n\t// Sometimes clients try to resolve \".\", which is a request to get root\n\t// servers.\n\tif host == \"\" {\n\t\treturn Result{}, nil\n\t}\n\n\thost = strings.ToLower(host)\n\n\tif setts.FilteringEnabled {\n\t\tres = d.processRewrites(host, qtype)\n\t\tif res.Reason == Rewritten {\n\t\t\treturn res, nil\n\t\t}\n\t}\n\n\tfor _, hc := range d.hostCheckers {\n\t\tres, err = hc.check(host, qtype, setts)\n\t\tif err != nil {\n\t\t\treturn Result{}, fmt.Errorf(\"%s: %w\", hc.name, err)\n\t\t}\n\n\t\tif res.Reason.Matched() {\n\t\t\treturn res, nil\n\t\t}\n\t}\n\n\treturn Result{}, nil\n}\n\n// processRewrites performs filtering based on the legacy rewrite records.\n//\n// Firstly, it finds CNAME rewrites for host.  If the CNAME is the same as host,\n// this query isn't filtered.  If it's different, repeat the process for the new\n// CNAME, breaking loops in the process.\n//\n// Secondly, it finds A or AAAA rewrites for host and, if found, sets res.IPList\n// accordingly.  If the found rewrite has a special value of \"A\" or \"AAAA\", the\n// result is an exception.\nfunc (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {\n\td.confMu.RLock()\n\tdefer d.confMu.RUnlock()\n\n\tctx := context.TODO()\n\n\tif !d.conf.RewritesEnabled {\n\t\treturn Result{}\n\t}\n\n\trewrites, matched := findRewrites(d.conf.Rewrites, host, qtype)\n\tif !matched {\n\t\treturn Result{}\n\t}\n\n\tres.Reason = Rewritten\n\n\treturn d.handleRewriteLoop(ctx, host, qtype, rewrites, matched, &res)\n}\n\n// handleRewriteLoop performs filtering rewrite processing based on the legacy\n// rewrite records.  res must not be nil.\nfunc (d *DNSFilter) handleRewriteLoop(\n\tctx context.Context,\n\thost string,\n\tqtype uint16,\n\trewrites []*LegacyRewrite,\n\tmatched bool,\n\tres *Result,\n) (resResult Result) {\n\tcnames := container.NewMapSet[string]()\n\torigHost := host\n\n\tfor matched && len(rewrites) > 0 && rewrites[0].Type == dns.TypeCNAME {\n\t\trw := rewrites[0]\n\t\trwPat := rw.Domain\n\t\trwAns := rw.Answer\n\n\t\td.logger.DebugContext(ctx, \"found rewrite\", \"host\", host, \"cname\", rwAns)\n\n\t\tif origHost == rwAns || rwPat == rwAns {\n\t\t\t// Either a request for the hostname itself or a rewrite of\n\t\t\t// a pattern onto itself, both of which are an exception rules.\n\t\t\t// Return a not filtered result.\n\t\t\treturn Result{}\n\t\t} else if host == rwAns && isWildcard(rwPat) {\n\t\t\t// An \"*.example.com → sub.example.com\" rewrite matching in a loop.\n\t\t\t//\n\t\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/4016.\n\n\t\t\tres.CanonName = host\n\n\t\t\tbreak\n\t\t}\n\n\t\thost = rwAns\n\t\tif cnames.Has(host) {\n\t\t\td.logger.InfoContext(ctx, \"cname loop\", \"host\", host, \"original\", origHost)\n\n\t\t\treturn *res\n\t\t}\n\n\t\tcnames.Add(host)\n\t\tres.CanonName = host\n\t\trewrites, matched = findRewrites(d.conf.Rewrites, host, qtype)\n\t}\n\n\td.setRewriteResult(ctx, res, host, rewrites, qtype)\n\n\treturn *res\n}\n\n// matchBlockedServicesRules checks the host against the blocked services rules\n// in settings, if any.  err is always nil, it is only there to make this a\n// valid hostChecker function.\nfunc (d *DNSFilter) matchBlockedServicesRules(\n\thost string,\n\t_ uint16,\n\tsetts *Settings,\n) (res Result, err error) {\n\tif !setts.ProtectionEnabled {\n\t\treturn Result{}, nil\n\t}\n\n\tsvcs := setts.ServicesRules\n\tif len(svcs) == 0 {\n\t\treturn Result{}, nil\n\t}\n\n\treq := rules.NewRequestForHostname(host)\n\tfor _, s := range svcs {\n\t\tfor _, rule := range s.Rules {\n\t\t\tif rule.Match(req) {\n\t\t\t\tres.Reason = FilteredBlockedService\n\t\t\t\tres.IsFiltered = true\n\t\t\t\tres.ServiceName = s.Name\n\n\t\t\t\truleText := rule.Text()\n\t\t\t\tres.Rules = []*ResultRule{{\n\t\t\t\t\t// #nosec G115 -- The overflow is required for backwards\n\t\t\t\t\t// compatibility.\n\t\t\t\t\tFilterListID: rulelist.APIID(rule.GetFilterListID()),\n\t\t\t\t\tText:         ruleText,\n\t\t\t\t}}\n\n\t\t\t\td.logger.DebugContext(\n\t\t\t\t\tcontext.TODO(),\n\t\t\t\t\t\"blocked services matched rule\",\n\t\t\t\t\t\"rule\", ruleText,\n\t\t\t\t\t\"host\", host,\n\t\t\t\t\t\"service\", s.Name,\n\t\t\t\t)\n\n\t\t\t\treturn res, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\n//\n// Adding rule and matching against the rules\n//\n\nfunc newRuleStorage(filters []Filter) (rs *filterlist.RuleStorage, err error) {\n\tlists := make([]filterlist.Interface, 0, len(filters))\n\tfor _, f := range filters {\n\t\tvar rl filterlist.Interface\n\t\tvar skip bool\n\t\trl, skip, err = ruleListFromFilter(f)\n\t\tif skip {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err != nil {\n\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\treturn nil, err\n\t\t}\n\n\t\tlists = append(lists, rl)\n\t}\n\n\trs, err = filterlist.NewRuleStorage(lists)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating rule storage: %w\", err)\n\t}\n\n\treturn rs, nil\n}\n\n// ruleListFromFilter returns a rule list from a Filter.\nfunc ruleListFromFilter(f Filter) (rl filterlist.Interface, skip bool, err error) {\n\tif len(f.Data) != 0 {\n\t\treturn filterlist.NewBytes(&filterlist.BytesConfig{\n\t\t\tID:             f.ID,\n\t\t\tRulesText:      f.Data,\n\t\t\tIgnoreCosmetic: true,\n\t\t}), false, nil\n\t}\n\n\tif f.FilePath == \"\" {\n\t\treturn nil, true, nil\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\t// On Windows we don't pass a file to urlfilter because it's\n\t\t// difficult to update this file while it's being used.\n\t\tvar data []byte\n\t\tdata, err = os.ReadFile(f.FilePath)\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, true, nil\n\t\t} else if err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"reading filter content: %w\", err)\n\t\t}\n\n\t\treturn filterlist.NewBytes(&filterlist.BytesConfig{\n\t\t\tID:             f.ID,\n\t\t\tRulesText:      data,\n\t\t\tIgnoreCosmetic: true,\n\t\t}), false, nil\n\t}\n\n\trl, err = filterlist.NewFile(&filterlist.FileConfig{\n\t\tID:             f.ID,\n\t\tPath:           f.FilePath,\n\t\tIgnoreCosmetic: true,\n\t})\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil, true, nil\n\t} else if err != nil {\n\t\treturn nil, false, fmt.Errorf(\"creating file rule list with %q: %w\", f.FilePath, err)\n\t}\n\n\treturn rl, false, nil\n}\n\n// Initialize urlfilter objects.\nfunc (d *DNSFilter) initFiltering(ctx context.Context, allowFilters, blockFilters []Filter) (err error) {\n\trulesStorage, err := newRuleStorage(blockFilters)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trulesStorageAllow, err := newRuleStorage(allowFilters)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfilteringEngine := urlfilter.NewDNSEngine(rulesStorage)\n\tfilteringEngineAllow := urlfilter.NewDNSEngine(rulesStorageAllow)\n\n\tfunc() {\n\t\td.engineLock.Lock()\n\t\tdefer d.engineLock.Unlock()\n\n\t\td.reset(ctx)\n\t\td.rulesStorage = rulesStorage\n\t\td.filteringEngine = filteringEngine\n\t\td.rulesStorageAllow = rulesStorageAllow\n\t\td.filteringEngineAllow = filteringEngineAllow\n\t}()\n\n\t// Make sure that the OS reclaims memory as soon as possible.\n\tdebug.FreeOSMemory()\n\n\td.logger.DebugContext(ctx, \"initialized filtering engine\")\n\n\treturn nil\n}\n\n// hostRules is a helper that converts a slice of host rules into a slice of the\n// rules.Rule interface values.\nfunc hostRulesToRules(netRules []*rules.HostRule) (res []rules.Rule) {\n\tif netRules == nil {\n\t\treturn nil\n\t}\n\n\tres = make([]rules.Rule, len(netRules))\n\tfor i, nr := range netRules {\n\t\tres[i] = nr\n\t}\n\n\treturn res\n}\n\n// matchHostProcessAllowList processes the allowlist logic of host matching.\nfunc (d *DNSFilter) matchHostProcessAllowList(\n\tctx context.Context,\n\thost string,\n\tdnsres *urlfilter.DNSResult,\n) (res Result, err error) {\n\tvar matchedRules []rules.Rule\n\tif dnsres.NetworkRule != nil {\n\t\tmatchedRules = []rules.Rule{dnsres.NetworkRule}\n\t} else if len(dnsres.HostRulesV4) > 0 {\n\t\tmatchedRules = hostRulesToRules(dnsres.HostRulesV4)\n\t} else if len(dnsres.HostRulesV6) > 0 {\n\t\tmatchedRules = hostRulesToRules(dnsres.HostRulesV6)\n\t}\n\n\tif len(matchedRules) == 0 {\n\t\treturn Result{}, fmt.Errorf(\"invalid dns result: rules are empty\")\n\t}\n\n\td.logger.DebugContext(\n\t\tctx,\n\t\t\"allowlist rules for host\",\n\t\t\"host\", host,\n\t\t\"rules\", matchedRules,\n\t)\n\n\treturn makeResult(matchedRules, NotFilteredAllowList), nil\n}\n\n// matchHostProcessDNSResult processes the matched DNS filtering result.\nfunc (d *DNSFilter) matchHostProcessDNSResult(\n\tqtype uint16,\n\tdnsres *urlfilter.DNSResult,\n) (res Result) {\n\tif dnsres.NetworkRule != nil {\n\t\treason := FilteredBlockList\n\t\tif dnsres.NetworkRule.Whitelist {\n\t\t\treason = NotFilteredAllowList\n\t\t}\n\n\t\treturn makeResult([]rules.Rule{dnsres.NetworkRule}, reason)\n\t}\n\n\tif result, ok := resultFromHostRules(qtype, dnsres); ok {\n\t\treturn result\n\t}\n\n\treturn hostResultForOtherQType(dnsres)\n}\n\n// resultFromHostRules handles the HostRulesV4/HostRulesV6 case for\n// [matchHostProcessDNSResult].  dnsres must not be nil.\nfunc resultFromHostRules(qtype uint16, dnsres *urlfilter.DNSResult) (res Result, ok bool) {\n\tif qtype == dns.TypeA && dnsres.HostRulesV4 != nil {\n\t\tres = makeResult(hostRulesToRules(dnsres.HostRulesV4), FilteredBlockList)\n\t\tfor i, hr := range dnsres.HostRulesV4 {\n\t\t\tres.Rules[i].IP = hr.IP\n\t\t}\n\n\t\treturn res, true\n\t}\n\n\tif qtype == dns.TypeAAAA && dnsres.HostRulesV6 != nil {\n\t\tres = makeResult(hostRulesToRules(dnsres.HostRulesV6), FilteredBlockList)\n\t\tfor i, hr := range dnsres.HostRulesV6 {\n\t\t\tres.Rules[i].IP = hr.IP\n\t\t}\n\n\t\treturn res, true\n\t}\n\n\treturn Result{}, false\n}\n\n// hostResultForOtherQType returns a result based on the host rules in dnsres,\n// if any.  dnsres.HostRulesV4 take precedence over dnsres.HostRulesV6.\nfunc hostResultForOtherQType(dnsres *urlfilter.DNSResult) (res Result) {\n\tif len(dnsres.HostRulesV4) != 0 {\n\t\treturn makeResult([]rules.Rule{dnsres.HostRulesV4[0]}, FilteredBlockList)\n\t}\n\n\tif len(dnsres.HostRulesV6) != 0 {\n\t\treturn makeResult([]rules.Rule{dnsres.HostRulesV6[0]}, FilteredBlockList)\n\t}\n\n\treturn Result{}\n}\n\n// matchHost is a low-level way to check only if host is filtered by rules,\n// skipping expensive safebrowsing and parental lookups.\nfunc (d *DNSFilter) matchHost(\n\thost string,\n\trrtype uint16,\n\tsetts *Settings,\n) (res Result, err error) {\n\tif !setts.FilteringEnabled {\n\t\treturn Result{}, nil\n\t}\n\n\tctx := context.TODO()\n\n\t// TODO(f.setrakov): Reuse client tags and identifiers.\n\tufReq := &urlfilter.DNSRequest{\n\t\tHostname:          host,\n\t\tClientTags:        container.NewSortedSliceSet(setts.ClientTags...),\n\t\tClientIP:          setts.ClientIP,\n\t\tClientIdentifiers: container.NewSortedSliceSet(setts.ClientName),\n\t\tDNSType:           rrtype,\n\t}\n\n\td.engineLock.RLock()\n\t// Keep in mind that this lock must be held no just when calling Match() but\n\t// also while using the rules returned by it.\n\t//\n\t// TODO(e.burkov):  Inspect if the above is true.\n\tdefer d.engineLock.RUnlock()\n\n\tif setts.ProtectionEnabled && d.filteringEngineAllow != nil {\n\t\tdnsres, ok := d.filteringEngineAllow.MatchRequest(ufReq)\n\t\tif ok {\n\t\t\treturn d.matchHostProcessAllowList(ctx, host, dnsres)\n\t\t}\n\t}\n\n\tif d.filteringEngine == nil {\n\t\treturn Result{}, nil\n\t}\n\n\tdnsres, matchedEngine := d.filteringEngine.MatchRequest(ufReq)\n\n\t// Check DNS rewrites first, because the API there is a bit awkward.\n\tdnsRWRes := d.processDNSResultRewrites(dnsres, host)\n\tif dnsRWRes.Reason != NotFilteredNotFound {\n\t\treturn dnsRWRes, nil\n\t} else if !matchedEngine {\n\t\treturn Result{}, nil\n\t}\n\n\tif !setts.ProtectionEnabled {\n\t\t// Don't check non-dnsrewrite filtering results.\n\t\treturn Result{}, nil\n\t}\n\n\tres = d.matchHostProcessDNSResult(rrtype, dnsres)\n\tfor _, r := range res.Rules {\n\t\td.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"found rule for host\",\n\t\t\t\"host\", host,\n\t\t\t\"rule\", r.Text,\n\t\t\t\"filter_list_id\", r.FilterListID,\n\t\t)\n\t}\n\n\treturn res, nil\n}\n\n// makeResult returns a properly constructed Result.\nfunc makeResult(matchedRules []rules.Rule, reason Reason) (res Result) {\n\tresRules := make([]*ResultRule, len(matchedRules))\n\tfor i, mr := range matchedRules {\n\t\tresRules[i] = NewResultRule(mr)\n\t}\n\n\treturn Result{\n\t\tRules:      resRules,\n\t\tReason:     reason,\n\t\tIsFiltered: reason == FilteredBlockList,\n\t}\n}\n\n// InitModule manually initializes blocked services map.  l must not be nil.\nfunc InitModule(ctx context.Context, l *slog.Logger) {\n\tinitBlockedServices(ctx, l)\n}\n\n// New creates properly initialized DNS Filter that is ready to be used.  c must\n// be non-nil.\nfunc New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {\n\tctx := context.TODO()\n\n\td = &DNSFilter{\n\t\tlogger: c.Logger,\n\t\t// #nosec G115 -- The Unix epoch time is highly unlikely to be negative.\n\t\tidGen:                  newIDGenerator(uint64(time.Now().Unix()), c.Logger),\n\t\tbufPool:                syncutil.NewSlicePool[byte](rulelist.DefaultRuleBufSize),\n\t\tsafeSearch:             c.SafeSearch,\n\t\trefreshLock:            &sync.Mutex{},\n\t\tsafeBrowsingChecker:    c.SafeBrowsingChecker,\n\t\tparentalControlChecker: c.ParentalControlChecker,\n\t\tapplyClientFiltering:   c.ApplyClientFiltering,\n\t\tconfMu:                 &sync.RWMutex{},\n\t}\n\n\terr = d.validateSafeFSPatterns(c.SafeFSPatterns)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\td.hostCheckers = []hostChecker{{\n\t\tcheck: d.matchSysHosts,\n\t\tname:  \"hosts container\",\n\t}, {\n\t\tcheck: d.matchHost,\n\t\tname:  \"filtering\",\n\t}, {\n\t\tcheck: d.matchBlockedServicesRules,\n\t\tname:  \"blocked services\",\n\t}, {\n\t\tcheck: d.checkSafeBrowsing,\n\t\tname:  \"safe browsing\",\n\t}, {\n\t\tcheck: d.checkParental,\n\t\tname:  \"parental\",\n\t}, {\n\t\tcheck: d.checkSafeSearch,\n\t\tname:  \"safe search\",\n\t}}\n\n\tdefer func() { err = errors.Annotate(err, \"filtering: %w\") }()\n\n\td.conf = c\n\td.conf.filtersMu = &sync.RWMutex{}\n\n\terr = d.prepareRewrites(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rewrites: preparing: %w\", err)\n\t}\n\n\tif d.conf.BlockedServices != nil {\n\t\td.conf.BlockedServices.FilterUnknownIDs(ctx, d.logger)\n\t\terr = d.conf.BlockedServices.Validate()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"initializing blocked services: %w\", err)\n\t\t}\n\t}\n\n\tif blockFilters != nil {\n\t\terr = d.initFiltering(ctx, nil, blockFilters)\n\t\tif err != nil {\n\t\t\td.Close()\n\n\t\t\treturn nil, fmt.Errorf(\"initializing filtering subsystem: %w\", err)\n\t\t}\n\t}\n\n\terr = os.MkdirAll(filepath.Join(d.conf.DataDir, filterDir), aghos.DefaultPermDir)\n\tif err != nil {\n\t\td.Close()\n\n\t\treturn nil, fmt.Errorf(\"making filtering directory: %w\", err)\n\t}\n\n\td.loadFilters(ctx, d.conf.Filters)\n\td.loadFilters(ctx, d.conf.WhitelistFilters)\n\n\td.conf.Filters = deduplicateFilters(d.conf.Filters)\n\td.conf.WhitelistFilters = deduplicateFilters(d.conf.WhitelistFilters)\n\n\td.idGen.fix(d.conf.Filters)\n\td.idGen.fix(d.conf.WhitelistFilters)\n\n\treturn d, nil\n}\n\n// validateSafeFSPatterns validates and stores patterns for local filtering‑rule\n// files.\nfunc (d *DNSFilter) validateSafeFSPatterns(patterns []string) (err error) {\n\tfor i, p := range patterns {\n\t\t// Use Match to validate the patterns here.\n\t\t_, err = filepath.Match(p, \"test\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"safe_fs_patterns: at index %d: %w\", i, err)\n\t\t}\n\n\t\td.safeFSPatterns = append(d.safeFSPatterns, p)\n\t}\n\n\treturn nil\n}\n\n// Start registers web handlers and starts filters updates loop.\nfunc (d *DNSFilter) Start() {\n\td.filtersInitializerChan = make(chan filtersInitializerParams, 1)\n\td.done = make(chan struct{}, 1)\n\n\td.RegisterFilteringHandlers()\n\n\tgo d.updatesLoop(context.TODO())\n}\n\n// updatesLoop initializes new filters and checks for filters updates in a loop.\nfunc (d *DNSFilter) updatesLoop(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, d.logger)\n\n\tivl := time.Second * 5\n\tt := time.NewTimer(ivl)\n\n\tfor {\n\t\tselect {\n\t\tcase params := <-d.filtersInitializerChan:\n\t\t\terr := d.initFiltering(ctx, params.allowFilters, params.blockFilters)\n\t\t\tif err != nil {\n\t\t\t\td.logger.ErrorContext(ctx, \"initializing\", slogutil.KeyError, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase <-t.C:\n\t\t\tivl = d.periodicallyRefreshFilters(ivl)\n\t\t\tt.Reset(ivl)\n\t\tcase <-d.done:\n\t\t\tt.Stop()\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// periodicallyRefreshFilters checks for filters updates and returns time\n// interval for the next update.\nfunc (d *DNSFilter) periodicallyRefreshFilters(ivl time.Duration) (nextIvl time.Duration) {\n\tconst maxInterval = time.Hour\n\n\tif d.conf.FiltersUpdateIntervalHours == 0 {\n\t\treturn ivl\n\t}\n\n\tisNetErr, ok := false, false\n\t_, isNetErr, ok = d.tryRefreshFilters(true, true, false)\n\n\tif ok && !isNetErr {\n\t\tivl = maxInterval\n\t} else if isNetErr {\n\t\tivl *= 2\n\t\tivl = max(ivl, maxInterval)\n\t}\n\n\treturn ivl\n}\n\n// Safe browsing and parental control methods.\n\n// TODO(a.garipov): Unify with checkParental.\nfunc (d *DNSFilter) checkSafeBrowsing(\n\thost string,\n\t_ uint16,\n\tsetts *Settings,\n) (res Result, err error) {\n\tif !setts.ProtectionEnabled || !setts.SafeBrowsingEnabled {\n\t\treturn Result{}, nil\n\t}\n\n\tctx := context.TODO()\n\tif d.logger.Enabled(ctx, slogutil.LevelDebug) {\n\t\tstartTime := time.Now()\n\t\tdefer func() {\n\t\t\telapsed := time.Since(startTime)\n\t\t\td.logger.DebugContext(ctx, \"safebrowsing lookup\", \"host\", host, \"elapsed\", elapsed)\n\t\t}()\n\t}\n\n\tres = Result{\n\t\tRules: []*ResultRule{{\n\t\t\tText:         \"adguard-malware-shavar\",\n\t\t\tFilterListID: rulelist.APIIDSafeBrowsing,\n\t\t}},\n\t\tReason:     FilteredSafeBrowsing,\n\t\tIsFiltered: true,\n\t}\n\n\tblock, err := d.safeBrowsingChecker.Check(host)\n\tif !block || err != nil {\n\t\treturn Result{}, err\n\t}\n\n\treturn res, nil\n}\n\n// TODO(a.garipov): Unify with checkSafeBrowsing.\nfunc (d *DNSFilter) checkParental(\n\thost string,\n\t_ uint16,\n\tsetts *Settings,\n) (res Result, err error) {\n\tif !setts.ProtectionEnabled || !setts.ParentalEnabled {\n\t\treturn Result{}, nil\n\t}\n\n\tctx := context.TODO()\n\tif d.logger.Enabled(ctx, slogutil.LevelDebug) {\n\t\tstartTime := time.Now()\n\t\tdefer func() {\n\t\t\telapsed := time.Since(startTime)\n\t\t\td.logger.DebugContext(ctx, \"parental lookup\", \"host\", host, \"elapsed\", elapsed)\n\t\t}()\n\t}\n\n\tres = Result{\n\t\tRules: []*ResultRule{{\n\t\t\tText:         \"parental CATEGORY_BLACKLISTED\",\n\t\t\tFilterListID: rulelist.APIIDParentalControl,\n\t\t}},\n\t\tReason:     FilteredParental,\n\t\tIsFiltered: true,\n\t}\n\n\tblock, err := d.parentalControlChecker.Check(host)\n\tif !block || err != nil {\n\t\treturn Result{}, err\n\t}\n\n\treturn res, nil\n}\n"
  },
  {
    "path": "internal/filtering/filtering_internal_test.go",
    "content": "package filtering\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is a common timeout for tests.\nconst testTimeout = 1 * time.Second\n\nconst (\n\tsbBlocked = \"wmconvirus.narod.ru\"\n\tpcBlocked = \"pornhub.com\"\n)\n\n// testLogger is the common logger for tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\n// Helpers.\n\nfunc newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts *Settings) {\n\tsetts = &Settings{\n\t\tProtectionEnabled: true,\n\t\tFilteringEnabled:  true,\n\t}\n\tif c != nil {\n\t\tc.Logger = cmp.Or(c.Logger, testLogger)\n\t\tc.SafeBrowsingCacheSize = 10000\n\t\tc.ParentalCacheSize = 10000\n\t\tc.SafeSearchCacheSize = 1000\n\t\tc.CacheTime = 30\n\t\tsetts.SafeSearchEnabled = c.SafeSearchConf.Enabled\n\t\tsetts.SafeBrowsingEnabled = c.SafeBrowsingEnabled\n\t\tsetts.ParentalEnabled = c.ParentalEnabled\n\t} else {\n\t\t// It must not be nil.\n\t\tc = &Config{\n\t\t\tLogger:          testLogger,\n\t\t\tRewritesEnabled: true,\n\t\t}\n\t}\n\tf, err := New(c, filters)\n\trequire.NoError(t, err)\n\n\treturn f, setts\n}\n\nfunc newChecker(host string) Checker {\n\treturn hashprefix.New(&hashprefix.Config{\n\t\tLogger:    testLogger,\n\t\tCacheTime: 10,\n\t\tCacheSize: 100000,\n\t\tUpstream:  aghtest.NewBlockUpstream(host, true),\n\t})\n}\n\nfunc (d *DNSFilter) checkMatch(tb testing.TB, hostname string, setts *Settings) {\n\ttb.Helper()\n\n\tres, err := d.CheckHost(hostname, dns.TypeA, setts)\n\trequire.NoErrorf(tb, err, \"host %q\", hostname)\n\n\tassert.Truef(tb, res.IsFiltered, \"host %q\", hostname)\n}\n\nfunc (d *DNSFilter) checkMatchIP(tb testing.TB, hostname, ip string, qtype uint16, setts *Settings) {\n\ttb.Helper()\n\n\tres, err := d.CheckHost(hostname, qtype, setts)\n\trequire.NoErrorf(tb, err, \"host %q\", hostname, err)\n\trequire.NotEmpty(tb, res.Rules, \"host %q\", hostname)\n\n\tassert.Truef(tb, res.IsFiltered, \"host %q\", hostname)\n\n\tr := res.Rules[0]\n\trequire.NotNilf(tb, r.IP, \"Expected ip %s to match, actual: %v\", ip, r.IP)\n\n\tassert.Equalf(tb, ip, r.IP.String(), \"host %q\", hostname)\n}\n\nfunc (d *DNSFilter) checkMatchEmpty(tb testing.TB, hostname string, setts *Settings) {\n\ttb.Helper()\n\n\tres, err := d.CheckHost(hostname, dns.TypeA, setts)\n\trequire.NoErrorf(tb, err, \"host %q\", hostname)\n\n\tassert.Falsef(tb, res.IsFiltered, \"host %q\", hostname)\n}\n\nfunc TestDNSFilter_CheckHost_hostRules(t *testing.T) {\n\taddr := \"216.239.38.120\"\n\taddr6 := \"::1\"\n\ttext := fmt.Sprintf(`  %s  google.com www.google.com   # enforce google's safesearch\n%s  ipv6.com\n0.0.0.0 block.com\n0.0.0.1 host2\n0.0.0.2 host2\n::1 host2\n`,\n\t\taddr, addr6)\n\tfilters := []Filter{{\n\t\tID: 0, Data: []byte(text),\n\t}}\n\td, setts := newForTest(t, nil, filters)\n\tt.Cleanup(d.Close)\n\n\td.checkMatchIP(t, \"google.com\", addr, dns.TypeA, setts)\n\td.checkMatchIP(t, \"www.google.com\", addr, dns.TypeA, setts)\n\td.checkMatchEmpty(t, \"subdomain.google.com\", setts)\n\td.checkMatchEmpty(t, \"example.org\", setts)\n\n\t// IPv4 match.\n\td.checkMatchIP(t, \"block.com\", \"0.0.0.0\", dns.TypeA, setts)\n\n\t// Empty IPv6.\n\tres, err := d.CheckHost(\"block.com\", dns.TypeAAAA, setts)\n\trequire.NoError(t, err)\n\n\tassert.True(t, res.IsFiltered)\n\n\trequire.Len(t, res.Rules, 1)\n\n\tassert.Equal(t, \"0.0.0.0 block.com\", res.Rules[0].Text)\n\tassert.Empty(t, res.Rules[0].IP)\n\n\t// IPv6 match.\n\td.checkMatchIP(t, \"ipv6.com\", addr6, dns.TypeAAAA, setts)\n\n\t// Empty IPv4.\n\tres, err = d.CheckHost(\"ipv6.com\", dns.TypeA, setts)\n\trequire.NoError(t, err)\n\n\tassert.True(t, res.IsFiltered)\n\n\trequire.Len(t, res.Rules, 1)\n\n\tassert.Equal(t, \"::1  ipv6.com\", res.Rules[0].Text)\n\tassert.Empty(t, res.Rules[0].IP)\n\n\t// Two IPv4, both must be returned.\n\tres, err = d.CheckHost(\"host2\", dns.TypeA, setts)\n\trequire.NoError(t, err)\n\n\tassert.True(t, res.IsFiltered)\n\n\trequire.Len(t, res.Rules, 2)\n\n\tassert.Equal(t, res.Rules[0].IP, netip.AddrFrom4([4]byte{0, 0, 0, 1}))\n\tassert.Equal(t, res.Rules[1].IP, netip.AddrFrom4([4]byte{0, 0, 0, 2}))\n\n\t// One IPv6 address.\n\tres, err = d.CheckHost(\"host2\", dns.TypeAAAA, setts)\n\trequire.NoError(t, err)\n\n\tassert.True(t, res.IsFiltered)\n\n\trequire.Len(t, res.Rules, 1)\n\n\tassert.Equal(t, res.Rules[0].IP, netutil.IPv6Localhost())\n}\n\n// Safe Browsing.\n\nfunc TestSafeBrowsing(t *testing.T) {\n\tlogOutput := &bytes.Buffer{}\n\tsbChecker := newChecker(sbBlocked)\n\n\td, setts := newForTest(t, &Config{\n\t\tLogger: slogutil.New(&slogutil.Config{\n\t\t\tLevel:        slogutil.LevelDebug,\n\t\t\tOutput:       logOutput,\n\t\t\tFormat:       slogutil.FormatDefault,\n\t\t\tAddTimestamp: false,\n\t\t}),\n\t\tSafeBrowsingEnabled: true,\n\t\tSafeBrowsingChecker: sbChecker,\n\t}, nil)\n\tt.Cleanup(d.Close)\n\n\td.checkMatch(t, sbBlocked, setts)\n\n\trequire.Contains(t, logOutput.String(), fmt.Sprintf(\"safebrowsing lookup host=%s\", sbBlocked))\n\n\td.checkMatch(t, \"test.\"+sbBlocked, setts)\n\td.checkMatchEmpty(t, \"yandex.ru\", setts)\n\td.checkMatchEmpty(t, pcBlocked, setts)\n\n\t// Cached result.\n\td.checkMatch(t, sbBlocked, setts)\n\td.checkMatchEmpty(t, pcBlocked, setts)\n}\n\nfunc TestParallelSB(t *testing.T) {\n\td, setts := newForTest(t, &Config{\n\t\tSafeBrowsingEnabled: true,\n\t\tSafeBrowsingChecker: newChecker(sbBlocked),\n\t}, nil)\n\tt.Cleanup(d.Close)\n\n\tt.Run(\"group\", func(t *testing.T) {\n\t\tfor i := range 100 {\n\t\t\tt.Run(fmt.Sprintf(\"aaa%d\", i), func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\td.checkMatch(t, sbBlocked, setts)\n\t\t\t\td.checkMatch(t, \"test.\"+sbBlocked, setts)\n\t\t\t\td.checkMatchEmpty(t, \"yandex.ru\", setts)\n\t\t\t\td.checkMatchEmpty(t, pcBlocked, setts)\n\t\t\t})\n\t\t}\n\t})\n}\n\n// Parental.\n\nfunc TestParentalControl(t *testing.T) {\n\tlogOutput := &bytes.Buffer{}\n\n\td, setts := newForTest(t, &Config{\n\t\tLogger: slogutil.New(&slogutil.Config{\n\t\t\tLevel:        slogutil.LevelDebug,\n\t\t\tOutput:       logOutput,\n\t\t\tFormat:       slogutil.FormatDefault,\n\t\t\tAddTimestamp: false,\n\t\t}),\n\t\tParentalEnabled:        true,\n\t\tParentalControlChecker: newChecker(pcBlocked),\n\t}, nil)\n\tt.Cleanup(d.Close)\n\n\td.checkMatch(t, pcBlocked, setts)\n\trequire.Contains(t, logOutput.String(), fmt.Sprintf(\"parental lookup host=%s\", pcBlocked))\n\n\td.checkMatch(t, \"www.\"+pcBlocked, setts)\n\td.checkMatchEmpty(t, \"www.yandex.ru\", setts)\n\td.checkMatchEmpty(t, \"yandex.ru\", setts)\n\td.checkMatchEmpty(t, \"api.jquery.com\", setts)\n\n\t// Test cached result.\n\td.checkMatch(t, pcBlocked, setts)\n\td.checkMatchEmpty(t, \"yandex.ru\", setts)\n}\n\n// Filtering.\n\nfunc TestMatching(t *testing.T) {\n\tconst nl = \"\\n\"\n\tconst (\n\t\tblockingRules  = `||example.org^` + nl\n\t\tallowlistRules = `||example.org^` + nl + `@@||test.example.org` + nl\n\t\timportantRules = `@@||example.org^` + nl + `||test.example.org^$important` + nl\n\t\tregexRules     = `/example\\.org/` + nl + `@@||test.example.org^` + nl\n\t\tmaskRules      = `test*.example.org^` + nl + `exam*.com` + nl\n\t\tdnstypeRules   = `||example.org^$dnstype=AAAA` + nl + `@@||test.example.org^` + nl\n\t)\n\ttestCases := []struct {\n\t\tname           string\n\t\trules          string\n\t\thost           string\n\t\twantReason     Reason\n\t\twantIsFiltered bool\n\t\twantDNSType    uint16\n\t}{{\n\t\tname:           \"sanity\",\n\t\trules:          \"||doubleclick.net^\",\n\t\thost:           \"www.doubleclick.net\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"sanity\",\n\t\trules:          \"||doubleclick.net^\",\n\t\thost:           \"nodoubleclick.net\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"sanity\",\n\t\trules:          \"||doubleclick.net^\",\n\t\thost:           \"doubleclick.net.ru\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"sanity\",\n\t\trules:          \"||doubleclick.net^\",\n\t\thost:           sbBlocked,\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"blocking\",\n\t\trules:          blockingRules,\n\t\thost:           \"example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"blocking\",\n\t\trules:          blockingRules,\n\t\thost:           \"test.example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"blocking\",\n\t\trules:          blockingRules,\n\t\thost:           \"test.test.example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"blocking\",\n\t\trules:          blockingRules,\n\t\thost:           \"testexample.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"blocking\",\n\t\trules:          blockingRules,\n\t\thost:           \"onemoreexample.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"allowlist\",\n\t\trules:          allowlistRules,\n\t\thost:           \"example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"allowlist\",\n\t\trules:          allowlistRules,\n\t\thost:           \"test.example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredAllowList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"allowlist\",\n\t\trules:          allowlistRules,\n\t\thost:           \"test.test.example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredAllowList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"allowlist\",\n\t\trules:          allowlistRules,\n\t\thost:           \"testexample.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"allowlist\",\n\t\trules:          allowlistRules,\n\t\thost:           \"onemoreexample.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"important\",\n\t\trules:          importantRules,\n\t\thost:           \"example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredAllowList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"important\",\n\t\trules:          importantRules,\n\t\thost:           \"test.example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"important\",\n\t\trules:          importantRules,\n\t\thost:           \"test.test.example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"important\",\n\t\trules:          importantRules,\n\t\thost:           \"testexample.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"important\",\n\t\trules:          importantRules,\n\t\thost:           \"onemoreexample.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"regex\",\n\t\trules:          regexRules,\n\t\thost:           \"example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"regex\",\n\t\trules:          regexRules,\n\t\thost:           \"test.example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredAllowList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"regex\",\n\t\trules:          regexRules,\n\t\thost:           \"test.test.example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredAllowList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"regex\",\n\t\trules:          regexRules,\n\t\thost:           \"testexample.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"regex\",\n\t\trules:          regexRules,\n\t\thost:           \"onemoreexample.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"mask\",\n\t\trules:          maskRules,\n\t\thost:           \"test.example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"mask\",\n\t\trules:          maskRules,\n\t\thost:           \"test2.example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"mask\",\n\t\trules:          maskRules,\n\t\thost:           \"example.com\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"mask\",\n\t\trules:          maskRules,\n\t\thost:           \"exampleeee.com\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"mask\",\n\t\trules:          maskRules,\n\t\thost:           \"onemoreexamsite.com\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"mask\",\n\t\trules:          maskRules,\n\t\thost:           \"example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"mask\",\n\t\trules:          maskRules,\n\t\thost:           \"testexample.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"mask\",\n\t\trules:          maskRules,\n\t\thost:           \"example.co.uk\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"dnstype\",\n\t\trules:          dnstypeRules,\n\t\thost:           \"onemoreexample.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"dnstype\",\n\t\trules:          dnstypeRules,\n\t\thost:           \"example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredNotFound,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"dnstype\",\n\t\trules:          dnstypeRules,\n\t\thost:           \"example.org\",\n\t\twantIsFiltered: true,\n\t\twantReason:     FilteredBlockList,\n\t\twantDNSType:    dns.TypeAAAA,\n\t}, {\n\t\tname:           \"dnstype\",\n\t\trules:          dnstypeRules,\n\t\thost:           \"test.example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredAllowList,\n\t\twantDNSType:    dns.TypeA,\n\t}, {\n\t\tname:           \"dnstype\",\n\t\trules:          dnstypeRules,\n\t\thost:           \"test.example.org\",\n\t\twantIsFiltered: false,\n\t\twantReason:     NotFilteredAllowList,\n\t\twantDNSType:    dns.TypeAAAA,\n\t}}\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%s-%s\", tc.name, tc.host), func(t *testing.T) {\n\t\t\tfilters := []Filter{{ID: 0, Data: []byte(tc.rules)}}\n\t\t\td, setts := newForTest(t, nil, filters)\n\t\t\tt.Cleanup(d.Close)\n\n\t\t\tres, err := d.CheckHost(tc.host, tc.wantDNSType, setts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equalf(t, tc.wantIsFiltered, res.IsFiltered, \"Hostname %s has wrong result (%v must be %v)\", tc.host, res.IsFiltered, tc.wantIsFiltered)\n\t\t\tassert.Equalf(t, tc.wantReason, res.Reason, \"Hostname %s has wrong reason (%v must be %v)\", tc.host, res.Reason, tc.wantReason)\n\t\t})\n\t}\n}\n\nfunc TestWhitelist(t *testing.T) {\n\trules := `||host1^\n||host2^\n`\n\tfilters := []Filter{{\n\t\tID: 0, Data: []byte(rules),\n\t}}\n\n\twhiteRules := `||host1^\n||host3^\n`\n\twhiteFilters := []Filter{{\n\t\tID: 0, Data: []byte(whiteRules),\n\t}}\n\td, setts := newForTest(t, nil, filters)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\terr := d.setFilters(ctx, filters, whiteFilters, false)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(d.Close)\n\n\t// Matched by white filter.\n\tres, err := d.CheckHost(\"host1\", dns.TypeA, setts)\n\trequire.NoError(t, err)\n\n\tassert.False(t, res.IsFiltered)\n\tassert.Equal(t, res.Reason, NotFilteredAllowList)\n\n\trequire.Len(t, res.Rules, 1)\n\n\tassert.Equal(t, \"||host1^\", res.Rules[0].Text)\n\n\t// Not matched by white filter, but matched by block filter.\n\tres, err = d.CheckHost(\"host2\", dns.TypeA, setts)\n\trequire.NoError(t, err)\n\n\tassert.True(t, res.IsFiltered)\n\tassert.Equal(t, res.Reason, FilteredBlockList)\n\n\trequire.Len(t, res.Rules, 1)\n\n\tassert.Equal(t, \"||host2^\", res.Rules[0].Text)\n}\n\n// Client Settings.\n\nfunc applyClientSettings(setts *Settings) {\n\tsetts.FilteringEnabled = false\n\tsetts.ParentalEnabled = false\n\tsetts.SafeBrowsingEnabled = true\n\n\trule, _ := rules.NewNetworkRule(\"||facebook.com^\", 0)\n\ts := ServiceEntry{}\n\ts.Name = \"facebook\"\n\ts.Rules = []*rules.NetworkRule{rule}\n\tsetts.ServicesRules = append(setts.ServicesRules, s)\n}\n\nfunc TestClientSettings(t *testing.T) {\n\td, setts := newForTest(t,\n\t\t&Config{\n\t\t\tParentalEnabled:        true,\n\t\t\tSafeBrowsingEnabled:    false,\n\t\t\tSafeBrowsingChecker:    newChecker(sbBlocked),\n\t\t\tParentalControlChecker: newChecker(pcBlocked),\n\t\t},\n\t\t[]Filter{{\n\t\t\tID: 0, Data: []byte(\"||example.org^\\n\"),\n\t\t}},\n\t)\n\tt.Cleanup(d.Close)\n\n\ttype testCase struct {\n\t\tname       string\n\t\thost       string\n\t\tbefore     bool\n\t\twantReason Reason\n\t}\n\ttestCases := []testCase{{\n\t\tname:       \"filters\",\n\t\thost:       \"example.org\",\n\t\tbefore:     true,\n\t\twantReason: FilteredBlockList,\n\t}, {\n\t\tname:       \"parental\",\n\t\thost:       pcBlocked,\n\t\tbefore:     true,\n\t\twantReason: FilteredParental,\n\t}, {\n\t\tname:       \"safebrowsing\",\n\t\thost:       sbBlocked,\n\t\tbefore:     false,\n\t\twantReason: FilteredSafeBrowsing,\n\t}, {\n\t\tname:       \"additional_rules\",\n\t\thost:       \"facebook.com\",\n\t\tbefore:     false,\n\t\twantReason: FilteredBlockedService,\n\t}}\n\n\tmakeTester := func(tc testCase, before bool) func(t *testing.T) {\n\t\treturn func(t *testing.T) {\n\t\t\tt.Helper()\n\n\t\t\tr, err := d.CheckHost(tc.host, dns.TypeA, setts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif before {\n\t\t\t\tassert.True(t, r.IsFiltered)\n\t\t\t\tassert.Equal(t, tc.wantReason, r.Reason)\n\t\t\t} else {\n\t\t\t\tassert.False(t, r.IsFiltered)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check behaviour without any per-client settings, then apply per-client\n\t// settings and check behavior once again.\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, makeTester(tc, tc.before))\n\t}\n\n\tapplyClientSettings(setts)\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, makeTester(tc, !tc.before))\n\t}\n}\n\nfunc BenchmarkSafeBrowsing(b *testing.B) {\n\td, setts := newForTest(b, &Config{\n\t\tLogger:              testLogger,\n\t\tSafeBrowsingEnabled: true,\n\t\tSafeBrowsingChecker: newChecker(sbBlocked),\n\t}, nil)\n\tb.Cleanup(d.Close)\n\n\tvar res Result\n\tvar err error\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres, err = d.CheckHost(sbBlocked, dns.TypeA, setts)\n\t}\n\n\trequire.NoError(b, err)\n\tassert.Truef(b, res.IsFiltered, \"expected hostname %q to match\", sbBlocked)\n\n\t// Most recent results:\n\t//\n\t//\tgoos: darwin\n\t//\tgoarch: arm64\n\t//\tpkg: github.com/AdguardTeam/AdGuardHome/internal/filtering\n\t//\tcpu: Apple M3\n\t//\tBenchmarkSafeBrowsing-8   \t  846363\t      1280 ns/op\t    1424 B/op\t      41 allocs/op\n}\n\nfunc BenchmarkSafeBrowsing_parallel(b *testing.B) {\n\td, setts := newForTest(b, &Config{\n\t\tLogger:              testLogger,\n\t\tSafeBrowsingEnabled: true,\n\t\tSafeBrowsingChecker: newChecker(sbBlocked),\n\t}, nil)\n\tb.Cleanup(d.Close)\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tres, err := d.CheckHost(sbBlocked, dns.TypeA, setts)\n\t\t\trequire.NoError(b, err)\n\n\t\t\tassert.Truef(b, res.IsFiltered, \"expected hostname %q to match\", sbBlocked)\n\t\t}\n\t})\n\n\t// Most recent results:\n\t//\n\t//\tgoos: darwin\n\t//\tgoarch: arm64\n\t//\tpkg: github.com/AdguardTeam/AdGuardHome/internal/filtering\n\t//\tcpu: Apple M3\n\t//\tBenchmarkSafeBrowsing_parallel-8   \t 1040792\t      1076 ns/op\t    1472 B/op\t      43 allocs/op\n}\n"
  },
  {
    "path": "internal/filtering/filtering_test.go",
    "content": "package filtering_test\n\nimport (\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// testTimeout is a common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n"
  },
  {
    "path": "internal/filtering/hashprefix/cache.go",
    "content": "package hashprefix\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"time\"\n)\n\n// expirySize is the size of expiry in cacheItem.\nconst expirySize = 8\n\n// cacheItem represents an item that we will store in the cache.\ntype cacheItem struct {\n\t// expiry is the time when cacheItem will expire.\n\texpiry time.Time\n\n\t// hashes is the hashed hostnames.\n\thashes []hostnameHash\n}\n\n// toCacheItem decodes cacheItem from data.  data must be at least equal to\n// expiry size.\nfunc toCacheItem(data []byte) *cacheItem {\n\t// #nosec G115 -- Assume that the values are as the ones that have been\n\t// encoded.\n\tt := time.Unix(int64(binary.BigEndian.Uint64(data)), 0)\n\n\tdata = data[expirySize:]\n\thashes := make([]hostnameHash, 0, len(data)/hashSize)\n\n\tfor i := 0; i < len(data); i += hashSize {\n\t\tvar hash hostnameHash\n\t\tcopy(hash[:], data[i:i+hashSize])\n\t\thashes = append(hashes, hash)\n\t}\n\n\treturn &cacheItem{\n\t\texpiry: t,\n\t\thashes: hashes,\n\t}\n}\n\n// fromCacheItem encodes cacheItem into data.\nfunc fromCacheItem(item *cacheItem) (data []byte) {\n\tdata = make([]byte, 0, len(item.hashes)*hashSize+expirySize)\n\n\texpiry := item.expiry.Unix()\n\t// #nosec G115 -- The Unix epoch time is highly unlikely to be negative.\n\tdata = binary.BigEndian.AppendUint64(data, uint64(expiry))\n\n\tfor _, v := range item.hashes {\n\t\tdata = append(data, v[:]...)\n\t}\n\n\treturn data\n}\n\n// findInCache finds hashes in the cache.  If nothing found returns list of\n// hashes, prefixes of which will be sent to upstream.\nfunc (c *Checker) findInCache(\n\thashes []hostnameHash,\n) (found, blocked bool, hashesToRequest []hostnameHash) {\n\tnow := time.Now()\n\n\ti := 0\n\tfor _, hash := range hashes {\n\t\tdata := c.cache.Get(hash[:prefixLen])\n\t\tif data == nil {\n\t\t\thashes[i] = hash\n\t\t\ti++\n\n\t\t\tcontinue\n\t\t}\n\n\t\titem := toCacheItem(data)\n\t\tif now.After(item.expiry) {\n\t\t\thashes[i] = hash\n\t\t\ti++\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif ok := findMatch(hashes, item.hashes); ok {\n\t\t\treturn true, true, nil\n\t\t}\n\t}\n\n\tif i == 0 {\n\t\treturn true, false, nil\n\t}\n\n\treturn false, false, hashes[:i]\n}\n\n// storeInCache caches hashes.\nfunc (c *Checker) storeInCache(ctx context.Context, hashesToRequest, respHashes []hostnameHash) {\n\thashToStore := make(map[prefix][]hostnameHash)\n\n\tfor _, hash := range respHashes {\n\t\tvar pref prefix\n\t\tcopy(pref[:], hash[:])\n\n\t\thashToStore[pref] = append(hashToStore[pref], hash)\n\t}\n\n\tfor pref, hash := range hashToStore {\n\t\tc.setCache(ctx, pref, hash)\n\t}\n\n\tfor _, hash := range hashesToRequest {\n\t\tval := c.cache.Get(hash[:prefixLen])\n\t\tif val == nil {\n\t\t\tvar pref prefix\n\t\t\tcopy(pref[:], hash[:])\n\n\t\t\tc.setCache(ctx, pref, nil)\n\t\t}\n\t}\n}\n\n// setCache stores hash in cache.\nfunc (c *Checker) setCache(ctx context.Context, pref prefix, hashes []hostnameHash) {\n\titem := &cacheItem{\n\t\texpiry: time.Now().Add(c.cacheTime),\n\t\thashes: hashes,\n\t}\n\n\tc.cache.Set(pref[:], fromCacheItem(item))\n\tc.logger.DebugContext(ctx, \"stored in cache\", \"pref\", pref)\n}\n"
  },
  {
    "path": "internal/filtering/hashprefix/cache_internal_test.go",
    "content": "package hashprefix\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCacheItem(t *testing.T) {\n\titem := &cacheItem{\n\t\texpiry: time.Unix(0x01_23_45_67_89_AB_CD_EF, 0),\n\t\thashes: []hostnameHash{{\n\t\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t}, {\n\t\t\t0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,\n\t\t\t0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,\n\t\t\t0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,\n\t\t\t0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,\n\t\t}},\n\t}\n\n\twantData := []byte{\n\t\t0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,\n\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,\n\t\t0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,\n\t\t0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,\n\t\t0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,\n\t}\n\n\tgotData := fromCacheItem(item)\n\tassert.Equal(t, wantData, gotData)\n\n\tnewItem := toCacheItem(gotData)\n\tgotData = fromCacheItem(newItem)\n\tassert.Equal(t, wantData, gotData)\n}\n"
  },
  {
    "path": "internal/filtering/hashprefix/hashprefix.go",
    "content": "// Package hashprefix used for safe browsing and parent control.\npackage hashprefix\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/cache\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n\t\"github.com/miekg/dns\"\n\t\"golang.org/x/net/publicsuffix\"\n)\n\nconst (\n\t// prefixLen is the length of the hash prefix of the filtered hostname.\n\tprefixLen = 2\n\n\t// hashSize is the size of hashed hostname.\n\thashSize = sha256.Size\n\n\t// hexSize is the size of hexadecimal representation of hashed hostname.\n\thexSize = hashSize * 2\n)\n\n// prefix is the type of the SHA256 hash prefix used to match against the\n// domain-name database.\ntype prefix [prefixLen]byte\n\n// hostnameHash is the hashed hostname.\n//\n// TODO(s.chzhen):  Split into prefix and suffix.\ntype hostnameHash [hashSize]byte\n\n// findMatch returns true if one of the a hostnames matches one of the b.\nfunc findMatch(a, b []hostnameHash) (matched bool) {\n\tfor _, hash := range a {\n\t\tif slices.Contains(b, hash) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Config is the configuration structure for safe browsing and parental\n// control.\ntype Config struct {\n\t// Logger is used for logging the check process.  It must not be nil.\n\tLogger *slog.Logger\n\n\t// Upstream is the upstream DNS server.\n\tUpstream upstream.Upstream\n\n\t// TXTSuffix is the TXT suffix for DNS request.\n\tTXTSuffix string\n\n\t// CacheTime is the time period to store hash.\n\tCacheTime time.Duration\n\n\t// CacheSize is the maximum size of the cache.  If it's zero, cache size is\n\t// unlimited.\n\tCacheSize uint\n}\n\ntype Checker struct {\n\t// logger is used for logging the check process.\n\tlogger *slog.Logger\n\n\t// upstream is the upstream DNS server.\n\tupstream upstream.Upstream\n\n\t// cache stores hostname hashes.\n\tcache cache.Cache\n\n\t// txtSuffix is the TXT suffix for DNS request.\n\ttxtSuffix string\n\n\t// cacheTime is the time period to store hash.\n\tcacheTime time.Duration\n}\n\n// New returns Checker.\nfunc New(conf *Config) (c *Checker) {\n\treturn &Checker{\n\t\tlogger:   conf.Logger,\n\t\tupstream: conf.Upstream,\n\t\tcache: cache.New(cache.Config{\n\t\t\tEnableLRU: true,\n\t\t\tMaxSize:   conf.CacheSize,\n\t\t}),\n\t\ttxtSuffix: conf.TXTSuffix,\n\t\tcacheTime: conf.CacheTime,\n\t}\n}\n\n// Check returns true if request for the host should be blocked.\nfunc (c *Checker) Check(host string) (ok bool, err error) {\n\tctx := context.TODO()\n\n\thashes := hostnameToHashes(host)\n\n\tl := c.logger.With(\"host\", host)\n\n\tfound, blocked, hashesToRequest := c.findInCache(hashes)\n\tif found {\n\t\tl.DebugContext(ctx, \"found in cache\", \"blocked\", blocked)\n\n\t\treturn blocked, nil\n\t}\n\n\tquestion := c.getQuestion(hashesToRequest)\n\n\tl.DebugContext(ctx, \"checking\", \"question\", question)\n\treq := (&dns.Msg{}).SetQuestion(question, dns.TypeTXT)\n\n\tresp, err := c.upstream.Exchange(req)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"getting hashes: %w\", err)\n\t}\n\n\tmatched, receivedHashes := c.processAnswer(ctx, l, hashesToRequest, resp)\n\n\tc.storeInCache(ctx, hashesToRequest, receivedHashes)\n\n\treturn matched, nil\n}\n\n// hostnameToHashes returns hashes that should be checked by the hash prefix\n// filter.\nfunc hostnameToHashes(host string) (hashes []hostnameHash) {\n\t// subDomainNum defines how many labels should be hashed to match against a\n\t// hash prefix filter.\n\tconst subDomainNum = 4\n\n\tpubSuf, icann := publicsuffix.PublicSuffix(host)\n\tif !icann {\n\t\t// Check the full private domain space.\n\t\tpubSuf = \"\"\n\t}\n\n\tnDots := 0\n\ti := strings.LastIndexFunc(host, func(r rune) (ok bool) {\n\t\tif r == '.' {\n\t\t\tnDots++\n\t\t}\n\n\t\treturn nDots == subDomainNum\n\t})\n\tif i != -1 {\n\t\thost = host[i+1:]\n\t}\n\n\t// TODO(e.burkov):  Use pools and [netutil.AppendSubdomains].\n\tsub := netutil.Subdomains(host)\n\n\tfor _, s := range sub {\n\t\tif s == pubSuf {\n\t\t\tbreak\n\t\t}\n\n\t\tsum := sha256.Sum256([]byte(s))\n\t\thashes = append(hashes, sum)\n\t}\n\n\treturn hashes\n}\n\n// getQuestion combines hexadecimal encoded prefixes of hashed hostnames into\n// string.\nfunc (c *Checker) getQuestion(hashes []hostnameHash) (q string) {\n\tb := &strings.Builder{}\n\n\tfor _, hash := range hashes {\n\t\tstringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), \".\")\n\t}\n\n\tstringutil.WriteToBuilder(b, c.txtSuffix)\n\n\treturn b.String()\n}\n\n// processAnswer returns true if DNS response matches the hash, and received\n// hashed hostnames from the upstream.  l must not be nil.\nfunc (c *Checker) processAnswer(\n\tctx context.Context,\n\tl *slog.Logger,\n\thashesToRequest []hostnameHash,\n\tresp *dns.Msg,\n) (matched bool, receivedHashes []hostnameHash) {\n\ttxtCount := 0\n\n\tfor _, a := range resp.Answer {\n\t\ttxt, ok := a.(*dns.TXT)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\ttxtCount++\n\n\t\treceivedHashes = c.appendHashesFromTXT(ctx, l, receivedHashes, txt)\n\t}\n\n\tl.DebugContext(ctx, \"processing answer with TXT\", \"txt_count\", txtCount)\n\n\tmatched = findMatch(hashesToRequest, receivedHashes)\n\tif matched {\n\t\tl.DebugContext(ctx, \"matched\")\n\n\t\treturn true, receivedHashes\n\t}\n\n\treturn false, receivedHashes\n}\n\n// appendHashesFromTXT appends received hashed hostnames.  l must not be nil.\nfunc (c *Checker) appendHashesFromTXT(\n\tctx context.Context,\n\tl *slog.Logger,\n\thashes []hostnameHash,\n\ttxt *dns.TXT,\n) (receivedHashes []hostnameHash) {\n\tl.DebugContext(ctx, \"received hashes\", \"txt\", txt.Txt)\n\n\tfor _, t := range txt.Txt {\n\t\tif len(t) != hexSize {\n\t\t\tl.DebugContext(ctx, \"wrong hex size\", \"len\", len(t), \"txt\", t)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tbuf, err := hex.DecodeString(t)\n\t\tif err != nil {\n\t\t\tl.DebugContext(ctx, \"decoding hex string\", \"txt\", t, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tvar hash hostnameHash\n\t\tcopy(hash[:], buf)\n\t\thashes = append(hashes, hash)\n\t}\n\n\treturn hashes\n}\n"
  },
  {
    "path": "internal/filtering/hashprefix/hashprefix_internal_test.go",
    "content": "package hashprefix\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/golibs/cache\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tcacheTime = 10 * time.Minute\n\tcacheSize = 10000\n)\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\nfunc TestChcker_getQuestion(t *testing.T) {\n\tconst suf = \"sb.dns.adguard.com.\"\n\n\t// test hostnameToHashes()\n\thashes := hostnameToHashes(\"1.2.3.sub.host.com\")\n\tassert.Len(t, hashes, 3)\n\n\thash := hostnameHash(sha256.Sum256([]byte(\"3.sub.host.com\")))\n\thexPref1 := hex.EncodeToString(hash[:prefixLen])\n\tassert.True(t, slices.Contains(hashes, hash))\n\n\thash = sha256.Sum256([]byte(\"sub.host.com\"))\n\thexPref2 := hex.EncodeToString(hash[:prefixLen])\n\tassert.True(t, slices.Contains(hashes, hash))\n\n\thash = sha256.Sum256([]byte(\"host.com\"))\n\thexPref3 := hex.EncodeToString(hash[:prefixLen])\n\tassert.True(t, slices.Contains(hashes, hash))\n\n\thash = sha256.Sum256([]byte(\"com\"))\n\tassert.False(t, slices.Contains(hashes, hash))\n\n\tc := New(&Config{\n\t\tLogger:    testLogger,\n\t\tTXTSuffix: suf,\n\t})\n\n\tq := c.getQuestion(hashes)\n\n\tassert.Contains(t, q, hexPref1)\n\tassert.Contains(t, q, hexPref2)\n\tassert.Contains(t, q, hexPref3)\n\tassert.True(t, strings.HasSuffix(q, suf))\n}\n\nfunc TestHostnameToHashes(t *testing.T) {\n\ttestCases := []struct {\n\t\tname    string\n\t\thost    string\n\t\twantLen int\n\t}{{\n\t\tname:    \"basic\",\n\t\thost:    \"example.com\",\n\t\twantLen: 1,\n\t}, {\n\t\tname:    \"sub_basic\",\n\t\thost:    \"www.example.com\",\n\t\twantLen: 2,\n\t}, {\n\t\tname:    \"private_domain\",\n\t\thost:    \"foo.co.uk\",\n\t\twantLen: 1,\n\t}, {\n\t\tname:    \"sub_private_domain\",\n\t\thost:    \"bar.foo.co.uk\",\n\t\twantLen: 2,\n\t}, {\n\t\tname:    \"private_domain_v2\",\n\t\thost:    \"foo.dyndns.org\",\n\t\twantLen: 3,\n\t}, {\n\t\tname:    \"sub_private_domain_v2\",\n\t\thost:    \"bar.foo.dyndns.org\",\n\t\twantLen: 4,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\thashes := hostnameToHashes(tc.host)\n\t\t\tassert.Len(t, hashes, tc.wantLen)\n\t\t})\n\t}\n}\n\nfunc TestChecker_storeInCache(t *testing.T) {\n\tconst testTimeout = 1 * time.Second\n\n\tc := New(&Config{\n\t\tLogger:    testLogger,\n\t\tCacheTime: cacheTime,\n\t})\n\n\tconf := cache.Config{}\n\tc.cache = cache.New(conf)\n\n\t// store in cache hashes for \"3.sub.host.com\" and \"host.com\"\n\t//  and empty data for hash-prefix for \"sub.host.com\"\n\thashes := []hostnameHash{}\n\thash := hostnameHash(sha256.Sum256([]byte(\"sub.host.com\")))\n\thashes = append(hashes, hash)\n\tvar hashesArray []hostnameHash\n\thash4 := sha256.Sum256([]byte(\"3.sub.host.com\"))\n\thashesArray = append(hashesArray, hash4)\n\thash2 := sha256.Sum256([]byte(\"host.com\"))\n\thashesArray = append(hashesArray, hash2)\n\tc.storeInCache(testutil.ContextWithTimeout(t, testTimeout), hashes, hashesArray)\n\n\t// match \"3.sub.host.com\" or \"host.com\" from cache\n\thashes = []hostnameHash{}\n\thash = sha256.Sum256([]byte(\"3.sub.host.com\"))\n\thashes = append(hashes, hash)\n\thash = sha256.Sum256([]byte(\"sub.host.com\"))\n\thashes = append(hashes, hash)\n\thash = sha256.Sum256([]byte(\"host.com\"))\n\thashes = append(hashes, hash)\n\tfound, blocked, _ := c.findInCache(hashes)\n\tassert.True(t, found)\n\tassert.True(t, blocked)\n\n\t// match \"sub.host.com\" from cache\n\thashes = []hostnameHash{}\n\thash = sha256.Sum256([]byte(\"sub.host.com\"))\n\thashes = append(hashes, hash)\n\tfound, blocked, _ = c.findInCache(hashes)\n\tassert.True(t, found)\n\tassert.False(t, blocked)\n\n\t// Match \"sub.host.com\" from cache.  Another hash for \"host.example\" is not\n\t// in the cache, so get data for it from the server.\n\thashes = []hostnameHash{}\n\thash = sha256.Sum256([]byte(\"sub.host.com\"))\n\thashes = append(hashes, hash)\n\thash = sha256.Sum256([]byte(\"host.example\"))\n\thashes = append(hashes, hash)\n\tfound, _, hashesToRequest := c.findInCache(hashes)\n\tassert.False(t, found)\n\n\thash = sha256.Sum256([]byte(\"sub.host.com\"))\n\tok := slices.Contains(hashesToRequest, hash)\n\tassert.False(t, ok)\n\n\thash = sha256.Sum256([]byte(\"host.example\"))\n\tok = slices.Contains(hashesToRequest, hash)\n\tassert.True(t, ok)\n\n\tc = New(&Config{\n\t\tLogger:    testLogger,\n\t\tCacheTime: cacheTime,\n\t})\n\n\tc.cache = cache.New(cache.Config{})\n\n\thashes = []hostnameHash{}\n\thash = sha256.Sum256([]byte(\"sub.host.com\"))\n\thashes = append(hashes, hash)\n\n\tc.cache.Set(hash[:prefixLen], make([]byte, expirySize+hashSize))\n\tfound, _, _ = c.findInCache(hashes)\n\tassert.False(t, found)\n}\n\nfunc TestChecker_Check(t *testing.T) {\n\tconst hostname = \"example.org\"\n\n\ttestCases := []struct {\n\t\tname      string\n\t\twantBlock bool\n\t}{{\n\t\tname:      \"sb_no_block\",\n\t\twantBlock: false,\n\t}, {\n\t\tname:      \"sb_block\",\n\t\twantBlock: true,\n\t}, {\n\t\tname:      \"pc_no_block\",\n\t\twantBlock: false,\n\t}, {\n\t\tname:      \"pc_block\",\n\t\twantBlock: true,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tc := New(&Config{\n\t\t\tLogger:    testLogger,\n\t\t\tCacheTime: cacheTime,\n\t\t\tCacheSize: cacheSize,\n\t\t})\n\n\t\t// Prepare the upstream.\n\t\tups := aghtest.NewBlockUpstream(hostname, tc.wantBlock)\n\n\t\tvar numReq int\n\t\tonExchange := ups.OnExchange\n\t\tups.OnExchange = func(req *dns.Msg) (resp *dns.Msg, err error) {\n\t\t\tnumReq++\n\n\t\t\treturn onExchange(req)\n\t\t}\n\n\t\tc.upstream = ups\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Firstly, check the request blocking.\n\t\t\thits := 0\n\t\t\tres := false\n\t\t\tres, err := c.Check(hostname)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.wantBlock {\n\t\t\t\tassert.True(t, res)\n\t\t\t\thits++\n\t\t\t} else {\n\t\t\t\trequire.False(t, res)\n\t\t\t}\n\n\t\t\t// Check the cache state, check the response is now cached.\n\t\t\tassert.Equal(t, 1, c.cache.Stats().Count)\n\t\t\tassert.Equal(t, hits, c.cache.Stats().Hit)\n\n\t\t\t// There was one request to an upstream.\n\t\t\tassert.Equal(t, 1, numReq)\n\n\t\t\t// Now make the same request to check the cache was used.\n\t\t\tres, err = c.Check(hostname)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.wantBlock {\n\t\t\t\tassert.True(t, res)\n\t\t\t} else {\n\t\t\t\trequire.False(t, res)\n\t\t\t}\n\n\t\t\t// Check the cache state, it should've been used.\n\t\t\tassert.Equal(t, 1, c.cache.Stats().Count)\n\t\t\tassert.Equal(t, hits+1, c.cache.Stats().Hit)\n\n\t\t\t// Check that there were no additional requests.\n\t\t\tassert.Equal(t, 1, numReq)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/hosts.go",
    "content": "package filtering\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/hostsfile\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// matchSysHosts tries to match the host against the operating system's hosts\n// database.  err is always nil.\nfunc (d *DNSFilter) matchSysHosts(\n\thost string,\n\tqtype uint16,\n\tsetts *Settings,\n) (res Result, err error) {\n\t// TODO(e.burkov):  Where else is this checked?\n\tif !setts.FilteringEnabled || d.conf.EtcHosts == nil {\n\t\treturn Result{}, nil\n\t}\n\n\tvals, rs, matched := d.hostsRewrites(qtype, host, d.conf.EtcHosts)\n\tif !matched {\n\t\treturn Result{}, nil\n\t}\n\n\treturn Result{\n\t\tDNSRewriteResult: &DNSRewriteResult{\n\t\t\tResponse: DNSRewriteResultResponse{\n\t\t\t\tqtype: vals,\n\t\t\t},\n\t\t\tRCode: dns.RcodeSuccess,\n\t\t},\n\t\tRules:  rs,\n\t\tReason: RewrittenAutoHosts,\n\t}, nil\n}\n\n// hostsRewrites returns values and rules matched by qt and host within hs.\nfunc (d *DNSFilter) hostsRewrites(\n\tqtype uint16,\n\thost string,\n\ths hostsfile.Storage,\n) (vals []rules.RRValue, rls []*ResultRule, matched bool) {\n\tctx := context.TODO()\n\n\tvar isValidProto func(netip.Addr) (ok bool)\n\tswitch qtype {\n\tcase dns.TypeA:\n\t\tisValidProto = netip.Addr.Is4\n\tcase dns.TypeAAAA:\n\t\tisValidProto = netip.Addr.Is6\n\tcase dns.TypePTR:\n\t\taddr, err := netutil.IPFromReversedAddr(host)\n\t\tif err != nil {\n\t\t\td.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"failed to parse PTR record\",\n\t\t\t\t\"host\", host,\n\t\t\t\tslogutil.KeyError, err,\n\t\t\t)\n\n\t\t\treturn nil, nil, false\n\t\t}\n\n\t\tnames := hs.ByAddr(addr)\n\n\t\tfor _, name := range names {\n\t\t\tvals = append(vals, name)\n\t\t\trls = append(rls, &ResultRule{\n\t\t\t\tText:         fmt.Sprintf(\"%s %s\", addr, name),\n\t\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t\t})\n\t\t}\n\n\t\treturn vals, rls, len(names) > 0\n\tdefault:\n\t\td.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"unsupported qtype\",\n\t\t\t\"qtype\", qtype,\n\t\t)\n\n\t\treturn nil, nil, false\n\t}\n\n\taddrs := hs.ByName(host)\n\tfor _, addr := range addrs {\n\t\tif isValidProto(addr) {\n\t\t\tvals = append(vals, addr)\n\t\t}\n\t\trls = append(rls, &ResultRule{\n\t\t\tText:         fmt.Sprintf(\"%s %s\", addr, host),\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t})\n\t}\n\n\treturn vals, rls, len(addrs) > 0\n}\n"
  },
  {
    "path": "internal/filtering/hosts_test.go",
    "content": "package filtering_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {\n\taddrv4 := netip.MustParseAddr(\"1.2.3.4\")\n\taddrv6 := netip.MustParseAddr(\"::1\")\n\taddrMapped := netip.MustParseAddr(\"::ffff:1.2.3.4\")\n\taddrv4Dup := netip.MustParseAddr(\"4.3.2.1\")\n\n\tdata := fmt.Sprintf(\n\t\t\"\"+\n\t\t\t\"%[1]s v4.host.example\\n\"+\n\t\t\t\"%[2]s v6.host.example\\n\"+\n\t\t\t\"%[3]s mapped.host.example\\n\"+\n\t\t\t\"%[4]s v4.host.with-dup\\n\"+\n\t\t\t\"%[4]s v4.host.with-dup\\n\",\n\t\taddrv4,\n\t\taddrv6,\n\t\taddrMapped,\n\t\taddrv4Dup,\n\t)\n\n\tfiles := fstest.MapFS{\n\t\t\"hosts\": &fstest.MapFile{\n\t\t\tData: []byte(data),\n\t\t},\n\t}\n\twatcher := aghtest.NewFSWatcher()\n\twatcher.OnEvents = func() (e <-chan struct{}) { return nil }\n\twatcher.OnAdd = func(name string) (err error) { return nil }\n\twatcher.OnShutdown = func(_ context.Context) (err error) { return nil }\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\thc, err := aghnet.NewHostsContainer(ctx, testLogger, files, watcher, \"hosts\")\n\trequire.NoError(t, err)\n\ttestutil.CleanupAndRequireSuccess(t, hc.Close)\n\n\tconf := &filtering.Config{\n\t\tLogger:   testLogger,\n\t\tEtcHosts: hc,\n\t}\n\tf, err := filtering.New(conf, nil)\n\trequire.NoError(t, err)\n\n\tsetts := &filtering.Settings{\n\t\tFilteringEnabled: true,\n\t}\n\n\ttestCases := []struct {\n\t\tname      string\n\t\thost      string\n\t\twantRules []*filtering.ResultRule\n\t\twantResps []rules.RRValue\n\t\tdtyp      uint16\n\t}{{\n\t\tname: \"v4\",\n\t\thost: \"v4.host.example\",\n\t\tdtyp: dns.TypeA,\n\t\twantRules: []*filtering.ResultRule{{\n\t\t\tText:         \"1.2.3.4 v4.host.example\",\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t}},\n\t\twantResps: []rules.RRValue{addrv4},\n\t}, {\n\t\tname: \"v6\",\n\t\thost: \"v6.host.example\",\n\t\tdtyp: dns.TypeAAAA,\n\t\twantRules: []*filtering.ResultRule{{\n\t\t\tText:         \"::1 v6.host.example\",\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t}},\n\t\twantResps: []rules.RRValue{addrv6},\n\t}, {\n\t\tname: \"mapped\",\n\t\thost: \"mapped.host.example\",\n\t\tdtyp: dns.TypeAAAA,\n\t\twantRules: []*filtering.ResultRule{{\n\t\t\tText:         \"::ffff:1.2.3.4 mapped.host.example\",\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t}},\n\t\twantResps: []rules.RRValue{addrMapped},\n\t}, {\n\t\tname: \"ptr\",\n\t\thost: \"4.3.2.1.in-addr.arpa\",\n\t\tdtyp: dns.TypePTR,\n\t\twantRules: []*filtering.ResultRule{{\n\t\t\tText:         \"1.2.3.4 v4.host.example\",\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t}},\n\t\twantResps: []rules.RRValue{\"v4.host.example\"},\n\t}, {\n\t\tname: \"ptr-mapped\",\n\t\thost: \"4.0.3.0.2.0.1.0.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa\",\n\t\tdtyp: dns.TypePTR,\n\t\twantRules: []*filtering.ResultRule{{\n\t\t\tText:         \"::ffff:1.2.3.4 mapped.host.example\",\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t}},\n\t\twantResps: []rules.RRValue{\"mapped.host.example\"},\n\t}, {\n\t\tname:      \"not_found_v4\",\n\t\thost:      \"non.existent.example\",\n\t\tdtyp:      dns.TypeA,\n\t\twantRules: nil,\n\t\twantResps: nil,\n\t}, {\n\t\tname:      \"not_found_v6\",\n\t\thost:      \"non.existent.example\",\n\t\tdtyp:      dns.TypeAAAA,\n\t\twantRules: nil,\n\t\twantResps: nil,\n\t}, {\n\t\tname:      \"not_found_ptr\",\n\t\thost:      \"4.3.2.2.in-addr.arpa\",\n\t\tdtyp:      dns.TypePTR,\n\t\twantRules: nil,\n\t\twantResps: nil,\n\t}, {\n\t\tname: \"v4_mismatch\",\n\t\thost: \"v4.host.example\",\n\t\tdtyp: dns.TypeAAAA,\n\t\twantRules: []*filtering.ResultRule{{\n\t\t\tText:         fmt.Sprintf(\"%s v4.host.example\", addrv4),\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t}},\n\t\twantResps: nil,\n\t}, {\n\t\tname: \"v6_mismatch\",\n\t\thost: \"v6.host.example\",\n\t\tdtyp: dns.TypeA,\n\t\twantRules: []*filtering.ResultRule{{\n\t\t\tText:         fmt.Sprintf(\"%s v6.host.example\", addrv6),\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t}},\n\t\twantResps: nil,\n\t}, {\n\t\tname:      \"wrong_ptr\",\n\t\thost:      \"4.3.2.1.ip6.arpa\",\n\t\tdtyp:      dns.TypePTR,\n\t\twantRules: nil,\n\t\twantResps: nil,\n\t}, {\n\t\tname:      \"unsupported_type\",\n\t\thost:      \"v4.host.example\",\n\t\tdtyp:      dns.TypeCNAME,\n\t\twantRules: nil,\n\t\twantResps: nil,\n\t}, {\n\t\tname: \"v4_dup\",\n\t\thost: \"v4.host.with-dup\",\n\t\tdtyp: dns.TypeA,\n\t\twantRules: []*filtering.ResultRule{{\n\t\t\tText:         \"4.3.2.1 v4.host.with-dup\",\n\t\t\tFilterListID: rulelist.APIIDEtcHosts,\n\t\t}},\n\t\twantResps: []rules.RRValue{addrv4Dup},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar res filtering.Result\n\t\t\tres, err = f.CheckHost(tc.host, tc.dtyp, setts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif len(tc.wantRules) == 0 {\n\t\t\t\tassert.Empty(t, res.Rules)\n\t\t\t\tassert.Nil(t, res.DNSRewriteResult)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NotNil(t, res.DNSRewriteResult)\n\t\t\trequire.Contains(t, res.DNSRewriteResult.Response, tc.dtyp)\n\n\t\t\tassert.Equal(t, tc.wantResps, res.DNSRewriteResult.Response[tc.dtyp])\n\t\t\tassert.Equal(t, tc.wantRules, res.Rules)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/http.go",
    "content": "package filtering\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// validateFilterURL validates the filter list URL or file name.\nfunc (d *DNSFilter) validateFilterURL(urlStr string) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"checking filter: %w\") }()\n\n\tif filepath.IsAbs(urlStr) {\n\t\turlStr = filepath.Clean(urlStr)\n\t\t_, err = os.Stat(urlStr)\n\t\tif err != nil {\n\t\t\t// Don't wrap the error since it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\n\t\tif !pathMatchesAny(d.safeFSPatterns, urlStr) {\n\t\t\treturn fmt.Errorf(\"path %q does not match safe patterns\", urlStr)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tu, err := url.ParseRequestURI(urlStr)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = urlutil.ValidateHTTPURL(u)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype filterAddJSON struct {\n\tName      string `json:\"name\"`\n\tURL       string `json:\"url\"`\n\tWhitelist bool   `json:\"whitelist\"`\n}\n\nfunc (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\tfj := filterAddJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&fj)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Failed to parse request body json: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\terr = d.validateFilterURL(fj.URL)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\t// Check for duplicates\n\tif d.filterExists(fj.URL) {\n\t\terr = errFilterExists\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Filter with URL %q: %s\",\n\t\t\tfj.URL,\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\t// Set necessary properties\n\tfilt := FilterYAML{\n\t\tEnabled: true,\n\t\tURL:     fj.URL,\n\t\tName:    fj.Name,\n\t\twhite:   fj.Whitelist,\n\t\tFilter: Filter{\n\t\t\tID: d.idGen.next(),\n\t\t},\n\t}\n\n\t// Download the filter contents\n\tok, err := d.update(&filt)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Couldn't fetch filter from URL %q: %s\",\n\t\t\tfilt.URL,\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tif !ok {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Filter with URL %q is invalid (maybe it points to blank page?)\",\n\t\t\tfilt.URL,\n\t\t)\n\n\t\treturn\n\t}\n\n\t// URL is assumed valid so append it to filters, update config, write new\n\t// file and reload it to engines.\n\terr = d.filterAdd(filt)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Filter with URL %q: %s\",\n\t\t\tfilt.URL,\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\td.conf.ConfModifier.Apply(ctx)\n\td.EnableFilters(true)\n\n\t_, err = fmt.Fprintf(w, \"OK %d rules\\n\", filt.RulesCount)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"Couldn't write body: %s\",\n\t\t\terr,\n\t\t)\n\t}\n}\n\nfunc (d *DNSFilter) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {\n\ttype request struct {\n\t\tURL       string `json:\"url\"`\n\t\tWhitelist bool   `json:\"whitelist\"`\n\t}\n\n\tctx := r.Context()\n\n\treq := request{}\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\td.logger,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"failed to parse request body json: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tvar deleted FilterYAML\n\tfunc() {\n\t\td.conf.filtersMu.Lock()\n\t\tdefer d.conf.filtersMu.Unlock()\n\n\t\tfilters := &d.conf.Filters\n\t\tif req.Whitelist {\n\t\t\tfilters = &d.conf.WhitelistFilters\n\t\t}\n\n\t\tdelIdx := slices.IndexFunc(*filters, func(flt FilterYAML) bool {\n\t\t\treturn flt.URL == req.URL\n\t\t})\n\t\tif delIdx == -1 {\n\t\t\td.logger.ErrorContext(\n\t\t\t\tctx,\n\t\t\t\t\"deleting filter\",\n\t\t\t\t\"url\", req.URL,\n\t\t\t\tslogutil.KeyError, errFilterNotExist,\n\t\t\t)\n\n\t\t\treturn\n\t\t}\n\n\t\tdeleted = (*filters)[delIdx]\n\t\tp := deleted.Path(d.conf.DataDir)\n\t\terr = os.Rename(p, p+\".old\")\n\t\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\t\td.logger.ErrorContext(\n\t\t\t\tctx,\n\t\t\t\t\"renaming filter file\",\n\t\t\t\t\"id\", deleted.ID,\n\t\t\t\t\"path\", p,\n\t\t\t\tslogutil.KeyError, err,\n\t\t\t)\n\n\t\t\treturn\n\t\t}\n\n\t\t*filters = slices.Delete(*filters, delIdx, delIdx+1)\n\n\t\td.logger.InfoContext(ctx, \"deleted filter\", \"id\", deleted.ID)\n\t}()\n\n\td.conf.ConfModifier.Apply(ctx)\n\td.EnableFilters(true)\n\n\t// NOTE: The old files \"filter.txt.old\" aren't deleted.  It's not really\n\t// necessary, but will require the additional complicated code to run\n\t// after enableFilters is done.\n\t//\n\t// TODO(a.garipov): Make sure the above comment is true.\n\n\t_, err = fmt.Fprintf(w, \"OK %d rules\\n\", deleted.RulesCount)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\td.logger,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"couldn't write body: %s\",\n\t\t\terr,\n\t\t)\n\t}\n}\n\ntype filterURLReqData struct {\n\tName    string `json:\"name\"`\n\tURL     string `json:\"url\"`\n\tEnabled bool   `json:\"enabled\"`\n}\n\ntype filterURLReq struct {\n\tData      *filterURLReqData `json:\"data\"`\n\tURL       string            `json:\"url\"`\n\tWhitelist bool              `json:\"whitelist\"`\n}\n\nfunc (d *DNSFilter) handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\tfj := filterURLReq{}\n\terr := json.NewDecoder(r.Body).Decode(&fj)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"decoding request: %s\", err)\n\n\t\treturn\n\t}\n\n\tif fj.Data == nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"%s\",\n\t\t\terrors.Error(\"data is absent\"),\n\t\t)\n\n\t\treturn\n\t}\n\n\terr = d.validateFilterURL(fj.Data.URL)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"invalid url: %s\", err)\n\n\t\treturn\n\t}\n\n\tfilt := FilterYAML{\n\t\tEnabled: fj.Data.Enabled,\n\t\tName:    fj.Data.Name,\n\t\tURL:     fj.Data.URL,\n\t}\n\n\trestart, err := d.filterSetProperties(fj.URL, filt, fj.Whitelist)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\td.conf.ConfModifier.Apply(ctx)\n\tif restart {\n\t\td.EnableFilters(true)\n\t}\n}\n\n// filteringRulesReq is the JSON structure for settings custom filtering rules.\ntype filteringRulesReq struct {\n\tRules []string `json:\"rules\"`\n}\n\nfunc (d *DNSFilter) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tif aghhttp.WriteTextPlainDeprecated(ctx, d.logger, w, r) {\n\t\treturn\n\t}\n\n\treq := &filteringRulesReq{}\n\terr := json.NewDecoder(r.Body).Decode(req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, d.logger, r, w, http.StatusBadRequest, \"reading req: %s\", err)\n\n\t\treturn\n\t}\n\n\td.conf.UserRules = req.Rules\n\td.conf.ConfModifier.Apply(ctx)\n\td.EnableFilters(true)\n}\n\nfunc (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {\n\ttype Req struct {\n\t\tWhite bool `json:\"whitelist\"`\n\t}\n\tvar err error\n\n\tctx := r.Context()\n\tl := d.logger\n\n\treq := Req{}\n\terr = json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"json decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tvar ok bool\n\tresp := struct {\n\t\tUpdated int `json:\"updated\"`\n\t}{}\n\tresp.Updated, _, ok = d.tryRefreshFilters(!req.White, req.White, true)\n\tif !ok {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"filters update procedure is already running\",\n\t\t)\n\n\t\treturn\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, resp)\n}\n\ntype filterJSON struct {\n\tURL         string `json:\"url\"`\n\tName        string `json:\"name\"`\n\tLastUpdated string `json:\"last_updated,omitempty\"`\n\n\tID rulelist.APIID `json:\"id\"`\n\n\tRulesCount uint64 `json:\"rules_count\"`\n\tEnabled    bool   `json:\"enabled\"`\n}\n\ntype filteringConfig struct {\n\tFilters          []filterJSON `json:\"filters\"`\n\tWhitelistFilters []filterJSON `json:\"whitelist_filters\"`\n\tUserRules        []string     `json:\"user_rules\"`\n\tInterval         uint32       `json:\"interval\"` // in hours\n\tEnabled          bool         `json:\"enabled\"`\n}\n\nfunc filterToJSON(f FilterYAML) filterJSON {\n\tfj := filterJSON{\n\t\t// #nosec G115 -- The overflow is required for backwards compatibility.\n\t\tID:      rulelist.APIID(f.ID),\n\t\tEnabled: f.Enabled,\n\t\tURL:     f.URL,\n\t\tName:    f.Name,\n\t\t// #nosec G115 -- The number of rules must not be negative.\n\t\tRulesCount: uint64(f.RulesCount),\n\t}\n\n\tif !f.LastUpdated.IsZero() {\n\t\tfj.LastUpdated = f.LastUpdated.Format(time.RFC3339)\n\t}\n\n\treturn fj\n}\n\n// Get filtering configuration\nfunc (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {\n\tresp := filteringConfig{}\n\td.conf.filtersMu.RLock()\n\tresp.Enabled = d.conf.FilteringEnabled\n\tresp.Interval = d.conf.FiltersUpdateIntervalHours\n\tfor _, f := range d.conf.Filters {\n\t\tfj := filterToJSON(f)\n\t\tresp.Filters = append(resp.Filters, fj)\n\t}\n\tfor _, f := range d.conf.WhitelistFilters {\n\t\tfj := filterToJSON(f)\n\t\tresp.WhitelistFilters = append(resp.WhitelistFilters, fj)\n\t}\n\tresp.UserRules = d.conf.UserRules\n\td.conf.filtersMu.RUnlock()\n\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, resp)\n}\n\n// Set filtering configuration\nfunc (d *DNSFilter) handleFilteringConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\treq := filteringConfig{}\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"json decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tif !ValidateUpdateIvl(req.Interval) {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"Unsupported interval\")\n\n\t\treturn\n\t}\n\n\tfunc() {\n\t\td.conf.filtersMu.Lock()\n\t\tdefer d.conf.filtersMu.Unlock()\n\n\t\td.conf.FilteringEnabled = req.Enabled\n\t\td.conf.FiltersUpdateIntervalHours = req.Interval\n\t}()\n\n\td.conf.ConfModifier.Apply(ctx)\n\td.EnableFilters(true)\n}\n\ntype checkHostRespRule struct {\n\tText string `json:\"text\"`\n\n\tFilterListID rulelist.APIID `json:\"filter_list_id\"`\n}\n\ntype checkHostResp struct {\n\tReason string `json:\"reason\"`\n\n\t// Rule is the text of the matched rule.\n\t//\n\t// Deprecated: Use Rules[*].Text.\n\tRule string `json:\"rule\"`\n\n\tRules []*checkHostRespRule `json:\"rules\"`\n\n\t// for FilteredBlockedService:\n\tSvcName string `json:\"service_name\"`\n\n\t// for Rewrite:\n\tCanonName string       `json:\"cname\"`    // CNAME value\n\tIPList    []netip.Addr `json:\"ip_addrs\"` // list of IP addresses\n\n\t// FilterID is the ID of the rule's filter list.\n\t//\n\t// Deprecated: Use Rules[*].FilterListID.\n\tFilterID rulelist.APIID `json:\"filter_id\"`\n}\n\n// handleCheckHost is the handler for the GET /control/filtering/check_host HTTP\n// API.\nfunc (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\tquery := r.URL.Query()\n\thost := query.Get(\"name\")\n\tif host == \"\" {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t`query parameter \"name\" is required`,\n\t\t)\n\n\t\treturn\n\t}\n\n\tcli := query.Get(\"client\")\n\tqTypeStr := query.Get(\"qtype\")\n\tqType, err := stringToDNSType(qTypeStr)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusUnprocessableEntity,\n\t\t\t\"bad qtype query parameter: %q\",\n\t\t\tqTypeStr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tsetts := d.Settings()\n\tsetts.FilteringEnabled = true\n\tsetts.ProtectionEnabled = true\n\n\taddr, err := netip.ParseAddr(cli)\n\tif err == nil {\n\t\td.ApplyAdditionalFiltering(addr, \"\", setts)\n\t} else if cli != \"\" {\n\t\t// TODO(s.chzhen):  Set [Settings.ClientName] once urlfilter supports\n\t\t// multiple client names.  This will handle the case when a rule exists\n\t\t// but the persistent client does not.\n\t\td.ApplyAdditionalFiltering(netip.Addr{}, cli, setts)\n\t}\n\n\tresult, err := d.CheckHost(host, qType, setts)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"couldn't apply filtering: %s: %s\",\n\t\t\thost,\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\trulesLen := len(result.Rules)\n\tresp := checkHostResp{\n\t\tReason:    result.Reason.String(),\n\t\tSvcName:   result.ServiceName,\n\t\tCanonName: result.CanonName,\n\t\tIPList:    result.IPList,\n\t\tRules:     make([]*checkHostRespRule, len(result.Rules)),\n\t}\n\n\tif rulesLen > 0 {\n\t\tresp.FilterID = result.Rules[0].FilterListID\n\t\tresp.Rule = result.Rules[0].Text\n\t}\n\n\tfor i, r := range result.Rules {\n\t\tresp.Rules[i] = &checkHostRespRule{\n\t\t\tFilterListID: r.FilterListID,\n\t\t\tText:         r.Text,\n\t\t}\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, resp)\n}\n\n// stringToDNSType is a helper function that converts a string to DNS type.  If\n// the string is empty, it returns the default value [dns.TypeA].\nfunc stringToDNSType(str string) (qtype uint16, err error) {\n\tif str == \"\" {\n\t\treturn dns.TypeA, nil\n\t}\n\n\tqtype, ok := dns.StringToType[str]\n\tif ok {\n\t\treturn qtype, nil\n\t}\n\n\t// typePref is a prefix for DNS types from experimental RFCs.\n\tconst typePref = \"TYPE\"\n\n\tif !strings.HasPrefix(str, typePref) {\n\t\treturn 0, errors.ErrBadEnumValue\n\t}\n\n\tval, err := strconv.ParseUint(str[len(typePref):], 10, 16)\n\tif err != nil {\n\t\treturn 0, errors.ErrBadEnumValue\n\t}\n\n\treturn uint16(val), nil\n}\n\n// setProtectedBool sets the value of a boolean pointer under a lock.  l must\n// protect the value under ptr.\n//\n// TODO(e.burkov):  Make it generic?\nfunc setProtectedBool(mu *sync.RWMutex, ptr *bool, val bool) {\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\t*ptr = val\n}\n\n// protectedBool gets the value of a boolean pointer under a read lock.  l must\n// protect the value under ptr.\n//\n// TODO(e.burkov):  Make it generic?\nfunc protectedBool(mu *sync.RWMutex, ptr *bool) (val bool) {\n\tmu.RLock()\n\tdefer mu.RUnlock()\n\n\treturn *ptr\n}\n\n// handleSafeBrowsingEnable is the handler for the POST\n// /control/safebrowsing/enable HTTP API.\nfunc (d *DNSFilter) handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) {\n\tsetProtectedBool(d.confMu, &d.conf.SafeBrowsingEnabled, true)\n\td.conf.ConfModifier.Apply(r.Context())\n}\n\n// handleSafeBrowsingDisable is the handler for the POST\n// /control/safebrowsing/disable HTTP API.\nfunc (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) {\n\tsetProtectedBool(d.confMu, &d.conf.SafeBrowsingEnabled, false)\n\td.conf.ConfModifier.Apply(r.Context())\n}\n\n// handleSafeBrowsingStatus is the handler for the GET\n// /control/safebrowsing/status HTTP API.\nfunc (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {\n\tresp := &struct {\n\t\tEnabled bool `json:\"enabled\"`\n\t}{\n\t\tEnabled: protectedBool(d.confMu, &d.conf.SafeBrowsingEnabled),\n\t}\n\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, resp)\n}\n\n// handleParentalEnable is the handler for the POST /control/parental/enable\n// HTTP API.\nfunc (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {\n\tsetProtectedBool(d.confMu, &d.conf.ParentalEnabled, true)\n\td.conf.ConfModifier.Apply(r.Context())\n}\n\n// handleParentalDisable is the handler for the POST /control/parental/disable\n// HTTP API.\nfunc (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request) {\n\tsetProtectedBool(d.confMu, &d.conf.ParentalEnabled, false)\n\td.conf.ConfModifier.Apply(r.Context())\n}\n\n// handleParentalStatus is the handler for the GET /control/parental/status\n// HTTP API.\nfunc (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {\n\tresp := &struct {\n\t\tEnabled bool `json:\"enabled\"`\n\t}{\n\t\tEnabled: protectedBool(d.confMu, &d.conf.ParentalEnabled),\n\t}\n\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, resp)\n}\n\n// RegisterFilteringHandlers - register handlers\nfunc (d *DNSFilter) RegisterFilteringHandlers() {\n\tregisterHTTP := d.conf.HTTPReg.Register\n\n\tregisterHTTP(http.MethodPost, \"/control/safebrowsing/enable\", d.handleSafeBrowsingEnable)\n\tregisterHTTP(http.MethodPost, \"/control/safebrowsing/disable\", d.handleSafeBrowsingDisable)\n\tregisterHTTP(http.MethodGet, \"/control/safebrowsing/status\", d.handleSafeBrowsingStatus)\n\n\tregisterHTTP(http.MethodPost, \"/control/parental/enable\", d.handleParentalEnable)\n\tregisterHTTP(http.MethodPost, \"/control/parental/disable\", d.handleParentalDisable)\n\tregisterHTTP(http.MethodGet, \"/control/parental/status\", d.handleParentalStatus)\n\n\tregisterHTTP(http.MethodPost, \"/control/safesearch/enable\", d.handleSafeSearchEnable)\n\tregisterHTTP(http.MethodPost, \"/control/safesearch/disable\", d.handleSafeSearchDisable)\n\tregisterHTTP(http.MethodGet, \"/control/safesearch/status\", d.handleSafeSearchStatus)\n\tregisterHTTP(http.MethodPut, \"/control/safesearch/settings\", d.handleSafeSearchSettings)\n\n\tregisterHTTP(http.MethodGet, \"/control/rewrite/list\", d.handleRewriteList)\n\tregisterHTTP(http.MethodGet, \"/control/rewrite/settings\", d.handleRewriteSettings)\n\tregisterHTTP(http.MethodPost, \"/control/rewrite/add\", d.handleRewriteAdd)\n\tregisterHTTP(http.MethodPost, \"/control/rewrite/delete\", d.handleRewriteDelete)\n\tregisterHTTP(http.MethodPut, \"/control/rewrite/settings/update\", d.handleRewriteSettingsUpdate)\n\tregisterHTTP(http.MethodPut, \"/control/rewrite/update\", d.handleRewriteUpdate)\n\n\tregisterHTTP(http.MethodGet, \"/control/blocked_services/services\", d.handleBlockedServicesIDs)\n\tregisterHTTP(http.MethodGet, \"/control/blocked_services/all\", d.handleBlockedServicesAll)\n\n\t// Deprecated handlers.\n\tregisterHTTP(http.MethodGet, \"/control/blocked_services/list\", d.handleBlockedServicesList)\n\tregisterHTTP(http.MethodPost, \"/control/blocked_services/set\", d.handleBlockedServicesSet)\n\n\tregisterHTTP(http.MethodGet, \"/control/blocked_services/get\", d.handleBlockedServicesGet)\n\tregisterHTTP(http.MethodPut, \"/control/blocked_services/update\", d.handleBlockedServicesUpdate)\n\n\tregisterHTTP(http.MethodGet, \"/control/filtering/status\", d.handleFilteringStatus)\n\tregisterHTTP(http.MethodPost, \"/control/filtering/config\", d.handleFilteringConfig)\n\tregisterHTTP(http.MethodPost, \"/control/filtering/add_url\", d.handleFilteringAddURL)\n\tregisterHTTP(http.MethodPost, \"/control/filtering/remove_url\", d.handleFilteringRemoveURL)\n\tregisterHTTP(http.MethodPost, \"/control/filtering/set_url\", d.handleFilteringSetURL)\n\tregisterHTTP(http.MethodPost, \"/control/filtering/refresh\", d.handleFilteringRefresh)\n\tregisterHTTP(http.MethodPost, \"/control/filtering/set_rules\", d.handleFilteringSetRules)\n\tregisterHTTP(http.MethodGet, \"/control/filtering/check_host\", d.handleCheckHost)\n}\n\n// ValidateUpdateIvl returns false if i is not a valid filters update interval.\nfunc ValidateUpdateIvl(i uint32) bool {\n\treturn i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24\n}\n"
  },
  {
    "path": "internal/filtering/http_internal_test.go",
    "content": "package filtering\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/schedule\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDNSFilter_handleFilteringSetURL(t *testing.T) {\n\tfiltersDir := t.TempDir()\n\n\tvar goodRulesEndpoint, anotherGoodRulesEndpoint, badRulesEndpoint string\n\tfor _, rulesSource := range []struct {\n\t\tendpoint *string\n\t\tcontent  []byte\n\t}{{\n\t\tendpoint: &goodRulesEndpoint,\n\t\tcontent:  []byte(`||example.org^`),\n\t}, {\n\t\tendpoint: &anotherGoodRulesEndpoint,\n\t\tcontent:  []byte(`||example.com^`),\n\t}, {\n\t\tendpoint: &badRulesEndpoint,\n\t\tcontent:  []byte(`<html></html>`),\n\t}} {\n\t\t*rulesSource.endpoint = serveFiltersLocally(t, rulesSource.content)\n\t}\n\n\ttestCases := []struct {\n\t\tname     string\n\t\twantBody string\n\t\toldURL   string\n\t\tnewName  string\n\t\tnewURL   string\n\t\tinitial  []FilterYAML\n\t}{{\n\t\tname:     \"success\",\n\t\twantBody: \"\",\n\t\toldURL:   goodRulesEndpoint,\n\t\tnewName:  \"default_one\",\n\t\tnewURL:   anotherGoodRulesEndpoint,\n\t\tinitial: []FilterYAML{{\n\t\t\tEnabled: true,\n\t\t\tURL:     goodRulesEndpoint,\n\t\t\tName:    \"default_one\",\n\t\t\twhite:   false,\n\t\t}},\n\t}, {\n\t\tname:     \"non-existing\",\n\t\twantBody: \"url doesn't exist\\n\",\n\t\toldURL:   anotherGoodRulesEndpoint,\n\t\tnewName:  \"default_one\",\n\t\tnewURL:   goodRulesEndpoint,\n\t\tinitial: []FilterYAML{{\n\t\t\tEnabled: true,\n\t\t\tURL:     goodRulesEndpoint,\n\t\t\tName:    \"default_one\",\n\t\t\twhite:   false,\n\t\t}},\n\t}, {\n\t\tname:     \"existing\",\n\t\twantBody: \"url already exists\\n\",\n\t\toldURL:   goodRulesEndpoint,\n\t\tnewName:  \"default_one\",\n\t\tnewURL:   anotherGoodRulesEndpoint,\n\t\tinitial: []FilterYAML{{\n\t\t\tEnabled: true,\n\t\t\tURL:     goodRulesEndpoint,\n\t\t\tName:    \"default_one\",\n\t\t\twhite:   false,\n\t\t}, {\n\t\t\tEnabled: true,\n\t\t\tURL:     anotherGoodRulesEndpoint,\n\t\t\tName:    \"another_default_one\",\n\t\t\twhite:   false,\n\t\t}},\n\t}, {\n\t\tname:     \"bad_rules\",\n\t\twantBody: \"data is HTML, not plain text\\n\",\n\t\toldURL:   goodRulesEndpoint,\n\t\tnewName:  \"default_one\",\n\t\tnewURL:   badRulesEndpoint,\n\t\tinitial: []FilterYAML{{\n\t\t\tEnabled: true,\n\t\t\tURL:     goodRulesEndpoint,\n\t\t\tName:    \"default_one\",\n\t\t\twhite:   false,\n\t\t}},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tconfModifiedCalled := false\n\t\t\tconfModifier := &aghtest.ConfigModifier{}\n\t\t\tconfModifier.OnApply = func(_ context.Context) {\n\t\t\t\tconfModifiedCalled = true\n\t\t\t}\n\t\t\td, err := New(&Config{\n\t\t\t\tLogger:           testLogger,\n\t\t\t\tFilteringEnabled: true,\n\t\t\t\tFilters:          tc.initial,\n\t\t\t\tHTTPClient: &http.Client{\n\t\t\t\t\tTimeout: 5 * time.Second,\n\t\t\t\t},\n\t\t\t\tConfModifier: confModifier,\n\t\t\t\tHTTPReg:      aghhttp.EmptyRegistrar{},\n\t\t\t\tDataDir:      filtersDir,\n\t\t\t}, nil)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(d.Close)\n\n\t\t\td.Start()\n\n\t\t\treqData := &filterURLReq{\n\t\t\t\tData: &filterURLReqData{\n\t\t\t\t\t// Leave the name of an existing list.\n\t\t\t\t\tName:    tc.newName,\n\t\t\t\t\tURL:     tc.newURL,\n\t\t\t\t\tEnabled: true,\n\t\t\t\t},\n\t\t\t\tURL:       tc.oldURL,\n\t\t\t\tWhitelist: false,\n\t\t\t}\n\t\t\tdata, err := json.Marshal(reqData)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tr := httptest.NewRequest(http.MethodPost, \"http://example.org\", bytes.NewReader(data))\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\td.handleFilteringSetURL(w, r)\n\t\t\tassert.Equal(t, tc.wantBody, w.Body.String())\n\n\t\t\t// For the moment the non-empty response body only contains occurred\n\t\t\t// error, so the configuration shouldn't be written.\n\t\t\tassert.Equal(t, tc.wantBody == \"\", confModifiedCalled)\n\t\t})\n\t}\n}\n\nfunc TestDNSFilter_handleSafeBrowsingStatus(t *testing.T) {\n\tconst (\n\t\ttestTimeout = time.Second\n\t\tstatusURL   = \"/control/safebrowsing/status\"\n\t)\n\n\tconfModCh := make(chan struct{})\n\tfiltersDir := t.TempDir()\n\n\ttestCases := []struct {\n\t\tname       string\n\t\turl        string\n\t\tenabled    bool\n\t\twantStatus assert.BoolAssertionFunc\n\t}{{\n\t\tname:       \"enable_off\",\n\t\turl:        \"/control/safebrowsing/enable\",\n\t\tenabled:    false,\n\t\twantStatus: assert.True,\n\t}, {\n\t\tname:       \"enable_on\",\n\t\turl:        \"/control/safebrowsing/enable\",\n\t\tenabled:    true,\n\t\twantStatus: assert.True,\n\t}, {\n\t\tname:       \"disable_on\",\n\t\turl:        \"/control/safebrowsing/disable\",\n\t\tenabled:    true,\n\t\twantStatus: assert.False,\n\t}, {\n\t\tname:       \"disable_off\",\n\t\turl:        \"/control/safebrowsing/disable\",\n\t\tenabled:    false,\n\t\twantStatus: assert.False,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\thandlers := make(map[string]http.Handler)\n\t\t\tconfModifier := &aghtest.ConfigModifier{}\n\t\t\tconfModifier.OnApply = func(_ context.Context) {\n\t\t\t\ttestutil.RequireSend(testutil.PanicT{}, confModCh, struct{}{}, testTimeout)\n\t\t\t}\n\n\t\t\td, err := New(&Config{\n\t\t\t\tLogger:       testLogger,\n\t\t\t\tConfModifier: confModifier,\n\t\t\t\tDataDir:      filtersDir,\n\t\t\t\tHTTPReg: &aghtest.Registrar{\n\t\t\t\t\tOnRegister: func(_, url string, handler http.HandlerFunc) {\n\t\t\t\t\t\thandlers[url] = handler\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSafeBrowsingEnabled: tc.enabled,\n\t\t\t}, nil)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(d.Close)\n\n\t\t\td.RegisterFilteringHandlers()\n\t\t\trequire.NotEmpty(t, handlers)\n\t\t\trequire.Contains(t, handlers, statusURL)\n\n\t\t\tr := httptest.NewRequest(http.MethodPost, tc.url, nil)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tgo handlers[tc.url].ServeHTTP(w, r)\n\n\t\t\ttestutil.RequireReceive(t, confModCh, testTimeout)\n\n\t\t\tr = httptest.NewRequest(http.MethodGet, statusURL, nil)\n\t\t\tw = httptest.NewRecorder()\n\n\t\t\thandlers[statusURL].ServeHTTP(w, r)\n\t\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\t\tstatus := struct {\n\t\t\t\tEnabled bool `json:\"enabled\"`\n\t\t\t}{\n\t\t\t\tEnabled: false,\n\t\t\t}\n\n\t\t\terr = json.NewDecoder(w.Body).Decode(&status)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.wantStatus(t, status.Enabled)\n\t\t})\n\t}\n}\n\nfunc TestDNSFilter_handleParentalStatus(t *testing.T) {\n\tconst (\n\t\ttestTimeout = time.Second\n\t\tstatusURL   = \"/control/parental/status\"\n\t)\n\n\tconfModCh := make(chan struct{})\n\tfiltersDir := t.TempDir()\n\n\ttestCases := []struct {\n\t\tname       string\n\t\turl        string\n\t\tenabled    bool\n\t\twantStatus assert.BoolAssertionFunc\n\t}{{\n\t\tname:       \"enable_off\",\n\t\turl:        \"/control/parental/enable\",\n\t\tenabled:    false,\n\t\twantStatus: assert.True,\n\t}, {\n\t\tname:       \"enable_on\",\n\t\turl:        \"/control/parental/enable\",\n\t\tenabled:    true,\n\t\twantStatus: assert.True,\n\t}, {\n\t\tname:       \"disable_on\",\n\t\turl:        \"/control/parental/disable\",\n\t\tenabled:    true,\n\t\twantStatus: assert.False,\n\t}, {\n\t\tname:       \"disable_off\",\n\t\turl:        \"/control/parental/disable\",\n\t\tenabled:    false,\n\t\twantStatus: assert.False,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\thandlers := make(map[string]http.Handler)\n\t\t\tconfModifier := &aghtest.ConfigModifier{}\n\t\t\tconfModifier.OnApply = func(_ context.Context) {\n\t\t\t\ttestutil.RequireSend(testutil.PanicT{}, confModCh, struct{}{}, testTimeout)\n\t\t\t}\n\n\t\t\td, err := New(&Config{\n\t\t\t\tLogger:       testLogger,\n\t\t\t\tConfModifier: confModifier,\n\t\t\t\tDataDir:      filtersDir,\n\t\t\t\tHTTPReg: &aghtest.Registrar{\n\t\t\t\t\tOnRegister: func(_, url string, handler http.HandlerFunc) {\n\t\t\t\t\t\thandlers[url] = handler\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tParentalEnabled: tc.enabled,\n\t\t\t}, nil)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(d.Close)\n\n\t\t\td.RegisterFilteringHandlers()\n\t\t\trequire.NotEmpty(t, handlers)\n\t\t\trequire.Contains(t, handlers, statusURL)\n\n\t\t\tr := httptest.NewRequest(http.MethodPost, tc.url, nil)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tgo handlers[tc.url].ServeHTTP(w, r)\n\n\t\t\ttestutil.RequireReceive(t, confModCh, testTimeout)\n\n\t\t\tr = httptest.NewRequest(http.MethodGet, statusURL, nil)\n\t\t\tw = httptest.NewRecorder()\n\n\t\t\thandlers[statusURL].ServeHTTP(w, r)\n\t\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\t\tstatus := struct {\n\t\t\t\tEnabled bool `json:\"enabled\"`\n\t\t\t}{\n\t\t\t\tEnabled: false,\n\t\t\t}\n\n\t\t\terr = json.NewDecoder(w.Body).Decode(&status)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.wantStatus(t, status.Enabled)\n\t\t})\n\t}\n}\n\nfunc TestDNSFilter_HandleCheckHost(t *testing.T) {\n\tconst (\n\t\tcliName = \"client_name\"\n\t\tcliID   = \"client_id\"\n\n\t\tnotFilteredHost = \"not.filterd.example\"\n\t\tallowedHost     = \"allowed.example\"\n\t\tblockedHost     = \"blocked.example\"\n\t\tcliHost         = \"client.example\"\n\t\tqTypeHost       = \"qtype.example\"\n\t\tcliQTypeHost    = \"cli.qtype.example\"\n\n\t\ttarget          = \"/control/check_host\"\n\t\thostFmt         = target + \"?name=%s\"\n\t\thostCliFmt      = hostFmt + \"&client=%s\"\n\t\thostQTypeFmt    = hostFmt + \"&qtype=%s\"\n\t\thostCliQTypeFmt = hostCliFmt + \"&qtype=%s\"\n\n\t\tallowedRuleFmt         = \"@@||%s^\"\n\t\tblockedRuleFmt         = \"||%s^\"\n\t\tblockedRuleCliFmt      = blockedRuleFmt + \"$client=%s\"\n\t\tblockedRuleQTypeFmt    = blockedRuleFmt + \"$dnstype=%s\"\n\t\tblockedRuleCliQTypeFmt = blockedRuleCliFmt + \",dnstype=%s\"\n\t)\n\n\tvar (\n\t\tallowedRule            = fmt.Sprintf(allowedRuleFmt, allowedHost)\n\t\tblockedRule            = fmt.Sprintf(blockedRuleFmt, blockedHost)\n\t\tblockedClientRule      = fmt.Sprintf(blockedRuleCliFmt, cliHost, cliName)\n\t\tblockedQTypeRule       = fmt.Sprintf(blockedRuleQTypeFmt, qTypeHost, \"CNAME\")\n\t\tblockedClientQTypeRule = fmt.Sprintf(blockedRuleCliQTypeFmt, cliQTypeHost, cliName, \"CNAME\")\n\n\t\tnotFilteredURL        = fmt.Sprintf(hostFmt, notFilteredHost)\n\t\tallowedURL            = fmt.Sprintf(hostFmt, allowedHost)\n\t\tblockedURL            = fmt.Sprintf(hostFmt, blockedHost)\n\t\tblockedClientURL      = fmt.Sprintf(hostCliFmt, cliHost, cliID)\n\t\tallowedQTypeURL       = fmt.Sprintf(hostQTypeFmt, qTypeHost, \"AAAA\")\n\t\tblockedQTypeURL       = fmt.Sprintf(hostQTypeFmt, qTypeHost, \"CNAME\")\n\t\tallowedClientQTypeURL = fmt.Sprintf(hostCliQTypeFmt, cliQTypeHost, cliID, \"AAAA\")\n\t\tblockedClientQTypeURL = fmt.Sprintf(hostCliQTypeFmt, cliQTypeHost, cliID, \"CNAME\")\n\t)\n\n\trules := []string{\n\t\tallowedRule,\n\t\tblockedRule,\n\t\tblockedClientRule,\n\t\tblockedQTypeRule,\n\t\tblockedClientQTypeRule,\n\t}\n\trulesData := strings.Join(rules, \"\\n\")\n\n\tfilters := []Filter{{\n\t\tID: 0, Data: []byte(rulesData),\n\t}}\n\n\tclientNames := map[string]string{\n\t\tcliID: cliName,\n\t}\n\n\tdnsFilter, err := New(&Config{\n\t\tLogger: testLogger,\n\t\tBlockedServices: &BlockedServices{\n\t\t\tSchedule: schedule.EmptyWeekly(),\n\t\t},\n\t\tApplyClientFiltering: func(clientID string, cliAddr netip.Addr, setts *Settings) {\n\t\t\tsetts.ClientName = clientNames[clientID]\n\t\t},\n\t}, filters)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname string\n\t\turl  string\n\t\twant *checkHostResp\n\t}{{\n\t\tname: \"not_filtered\",\n\t\turl:  notFilteredURL,\n\t\twant: &checkHostResp{\n\t\t\tReason: reasonNames[NotFilteredNotFound],\n\t\t\tRule:   \"\",\n\t\t\tRules:  []*checkHostRespRule{},\n\t\t},\n\t}, {\n\t\tname: \"allowed\",\n\t\turl:  allowedURL,\n\t\twant: &checkHostResp{\n\t\t\tReason: reasonNames[NotFilteredAllowList],\n\t\t\tRule:   allowedRule,\n\t\t\tRules: []*checkHostRespRule{{\n\t\t\t\tText: allowedRule,\n\t\t\t}},\n\t\t},\n\t}, {\n\t\tname: \"blocked\",\n\t\turl:  blockedURL,\n\t\twant: &checkHostResp{\n\t\t\tReason: reasonNames[FilteredBlockList],\n\t\t\tRule:   blockedRule,\n\t\t\tRules: []*checkHostRespRule{{\n\t\t\t\tText: blockedRule,\n\t\t\t}},\n\t\t},\n\t}, {\n\t\tname: \"blocked_client\",\n\t\turl:  blockedClientURL,\n\t\twant: &checkHostResp{\n\t\t\tReason: reasonNames[FilteredBlockList],\n\t\t\tRule:   blockedClientRule,\n\t\t\tRules: []*checkHostRespRule{{\n\t\t\t\tText: blockedClientRule,\n\t\t\t}},\n\t\t},\n\t}, {\n\t\tname: \"allowed_qtype\",\n\t\turl:  allowedQTypeURL,\n\t\twant: &checkHostResp{\n\t\t\tReason: reasonNames[NotFilteredNotFound],\n\t\t\tRule:   \"\",\n\t\t\tRules:  []*checkHostRespRule{},\n\t\t},\n\t}, {\n\t\tname: \"blocked_qtype\",\n\t\turl:  blockedQTypeURL,\n\t\twant: &checkHostResp{\n\t\t\tReason: reasonNames[FilteredBlockList],\n\t\t\tRule:   blockedQTypeRule,\n\t\t\tRules: []*checkHostRespRule{{\n\t\t\t\tText: blockedQTypeRule,\n\t\t\t}},\n\t\t},\n\t}, {\n\t\tname: \"blocked_client_qtype\",\n\t\turl:  blockedClientQTypeURL,\n\t\twant: &checkHostResp{\n\t\t\tReason: reasonNames[FilteredBlockList],\n\t\t\tRule:   blockedClientQTypeRule,\n\t\t\tRules: []*checkHostRespRule{{\n\t\t\t\tText: blockedClientQTypeRule,\n\t\t\t}},\n\t\t},\n\t}, {\n\t\tname: \"allowed_client_qtype\",\n\t\turl:  allowedClientQTypeURL,\n\t\twant: &checkHostResp{\n\t\t\tReason: reasonNames[NotFilteredNotFound],\n\t\t\tRule:   \"\",\n\t\t\tRules:  []*checkHostRespRule{},\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := httptest.NewRequest(http.MethodGet, tc.url, nil)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tdnsFilter.handleCheckHost(w, r)\n\n\t\t\tres := &checkHostResp{}\n\t\t\terr = json.NewDecoder(w.Body).Decode(res)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.want, res)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/idgenerator.go",
    "content": "package filtering\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync/atomic\"\n\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n)\n\n// idGenerator generates filtering-list IDs in a way broadly compatible with the\n// legacy approach of AdGuard Home.\n//\n// TODO(a.garipov): Get rid of this once we switch completely to the new\n// rule-list architecture.\ntype idGenerator struct {\n\tcurrent *atomic.Uint64\n\tlogger  *slog.Logger\n}\n\n// newIDGenerator returns a new ID generator initialized with the given seed\n// value.  l must not be nil.\nfunc newIDGenerator(seed uint64, l *slog.Logger) (g *idGenerator) {\n\tg = &idGenerator{\n\t\tcurrent: &atomic.Uint64{},\n\t\tlogger:  l,\n\t}\n\n\tg.current.Store(seed)\n\n\treturn g\n}\n\n// next returns the next ID from the generator.  It is safe for concurrent use.\nfunc (g *idGenerator) next() (id rules.ListID) {\n\tid64 := g.current.Add(1)\n\tif id64 == 0 {\n\t\tpanic(fmt.Errorf(\"invalid current id value %d\", id64))\n\t}\n\n\treturn rules.ListID(id64)\n}\n\n// fix ensures that flts all have unique IDs.\nfunc (g *idGenerator) fix(flts []FilterYAML) {\n\tset := container.NewMapSet[rules.ListID]()\n\tfor i, f := range flts {\n\t\tid := f.ID\n\t\tif id == 0 {\n\t\t\tid = g.next()\n\t\t\tflts[i].ID = id\n\t\t}\n\n\t\tif !set.Has(id) {\n\t\t\tset.Add(id)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tnewID := g.next()\n\t\tfor set.Has(newID) {\n\t\t\tnewID = g.next()\n\t\t}\n\n\t\tg.logger.WarnContext(\n\t\t\tcontext.TODO(),\n\t\t\t\"filter has duplicate id; reassigning\",\n\t\t\t\"idx\", i,\n\t\t\t\"id\", id,\n\t\t\t\"new_id\", newID,\n\t\t)\n\n\t\tflts[i].ID = newID\n\t\tset.Add(newID)\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/idgenerator_internal_test.go",
    "content": "package filtering\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIDGenerator_Fix(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   []FilterYAML\n\t}{{\n\t\tname: \"nil\",\n\t\tin:   nil,\n\t}, {\n\t\tname: \"empty\",\n\t\tin:   []FilterYAML{},\n\t}, {\n\t\tname: \"one_zero\",\n\t\tin:   []FilterYAML{{}},\n\t}, {\n\t\tname: \"two_zeros\",\n\t\tin:   []FilterYAML{{}, {}},\n\t}, {\n\t\tname: \"many_good\",\n\t\tin: []FilterYAML{{\n\t\t\tFilter: Filter{\n\t\t\t\tID: 1,\n\t\t\t},\n\t\t}, {\n\t\t\tFilter: Filter{\n\t\t\t\tID: 2,\n\t\t\t},\n\t\t}, {\n\t\t\tFilter: Filter{\n\t\t\t\tID: 3,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname: \"two_dups\",\n\t\tin: []FilterYAML{{\n\t\t\tFilter: Filter{\n\t\t\t\tID: 1,\n\t\t\t},\n\t\t}, {\n\t\t\tFilter: Filter{\n\t\t\t\tID: 3,\n\t\t\t},\n\t\t}, {\n\t\t\tFilter: Filter{\n\t\t\t\tID: 1,\n\t\t\t},\n\t\t}, {\n\t\t\tFilter: Filter{\n\t\t\t\tID: 2,\n\t\t\t},\n\t\t}},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tg := newIDGenerator(1, testLogger)\n\t\t\tg.fix(tc.in)\n\n\t\t\tassertUniqueIDs(t, tc.in)\n\t\t})\n\t}\n}\n\n// assertUniqueIDs is a test helper that asserts that the IDs of filters are\n// unique.\nfunc assertUniqueIDs(tb testing.TB, flts []FilterYAML) {\n\ttb.Helper()\n\n\tuc := aghalg.UniqChecker[rules.ListID]{}\n\tfor _, f := range flts {\n\t\tuc.Add(f.ID)\n\t}\n\n\tassert.NoError(tb, uc.Validate())\n}\n"
  },
  {
    "path": "internal/filtering/path.go",
    "content": "package filtering\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n)\n\n// pathMatchesAny returns true if filePath matches one of globs.  globs must be\n// valid.  filePath must be absolute and clean.  If globs are empty,\n// pathMatchesAny returns false.\n//\n// TODO(a.garipov): Move to golibs?\nfunc pathMatchesAny(globs []string, filePath string) (ok bool) {\n\tif len(globs) == 0 {\n\t\treturn false\n\t}\n\n\tclean, err := filepath.Abs(filePath)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"pathMatchesAny: %w\", err))\n\t} else if clean != filePath {\n\t\tpanic(fmt.Errorf(\"pathMatchesAny: filepath %q is not absolute\", filePath))\n\t}\n\n\tfor _, g := range globs {\n\t\tok, err = filepath.Match(g, filePath)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"pathMatchesAny: bad pattern: %w\", err))\n\t\t}\n\n\t\tif ok {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/filtering/path_unix_internal_test.go",
    "content": "//go:build unix\n\npackage filtering\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPathInAnyDir(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tfilePath      = \"/path/to/file.txt\"\n\t\tfilePathGlob  = \"/path/to/*\"\n\t\totherFilePath = \"/otherpath/to/file.txt\"\n\t)\n\n\ttestCases := []struct {\n\t\twant     assert.BoolAssertionFunc\n\t\tfilePath string\n\t\tname     string\n\t\tglobs    []string\n\t}{{\n\t\twant:     assert.False,\n\t\tfilePath: filePath,\n\t\tname:     \"nil_pats\",\n\t\tglobs:    nil,\n\t}, {\n\t\twant:     assert.True,\n\t\tfilePath: filePath,\n\t\tname:     \"match\",\n\t\tglobs: []string{\n\t\t\tfilePath,\n\t\t\totherFilePath,\n\t\t},\n\t}, {\n\t\twant:     assert.False,\n\t\tfilePath: filePath,\n\t\tname:     \"no_match\",\n\t\tglobs: []string{\n\t\t\totherFilePath,\n\t\t},\n\t}, {\n\t\twant:     assert.True,\n\t\tfilePath: filePath,\n\t\tname:     \"match_star\",\n\t\tglobs: []string{\n\t\t\tfilePathGlob,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttc.want(t, pathMatchesAny(tc.globs, tc.filePath))\n\t\t})\n\t}\n\n\trequire.True(t, t.Run(\"panic_on_unabs_file_path\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tassert.Panics(t, func() {\n\t\t\t_ = pathMatchesAny([]string{\"/home/user\"}, \"../../etc/passwd\")\n\t\t})\n\t}))\n\n\trequire.True(t, t.Run(\"panic_on_bad_pat\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tassert.Panics(t, func() {\n\t\t\t_ = pathMatchesAny([]string{`\\`}, filePath)\n\t\t})\n\t}))\n}\n"
  },
  {
    "path": "internal/filtering/path_windows_internal_test.go",
    "content": "//go:build windows\n\npackage filtering\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPathInAnyDir(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tfilePath      = `C:\\path\\to\\file.txt`\n\t\tfilePathGlob  = `C:\\path\\to\\*`\n\t\totherFilePath = `C:\\otherpath\\to\\file.txt`\n\t)\n\n\ttestCases := []struct {\n\t\twant     assert.BoolAssertionFunc\n\t\tfilePath string\n\t\tname     string\n\t\tglobs    []string\n\t}{{\n\t\twant:     assert.False,\n\t\tfilePath: filePath,\n\t\tname:     \"nil_pats\",\n\t\tglobs:    nil,\n\t}, {\n\t\twant:     assert.True,\n\t\tfilePath: filePath,\n\t\tname:     \"match\",\n\t\tglobs: []string{\n\t\t\tfilePath,\n\t\t\totherFilePath,\n\t\t},\n\t}, {\n\t\twant:     assert.False,\n\t\tfilePath: filePath,\n\t\tname:     \"no_match\",\n\t\tglobs: []string{\n\t\t\totherFilePath,\n\t\t},\n\t}, {\n\t\twant:     assert.True,\n\t\tfilePath: filePath,\n\t\tname:     \"match_star\",\n\t\tglobs: []string{\n\t\t\tfilePathGlob,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttc.want(t, pathMatchesAny(tc.globs, tc.filePath))\n\t\t})\n\t}\n\n\trequire.True(t, t.Run(\"panic_on_unabs_file_path\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tassert.Panics(t, func() {\n\t\t\t_ = pathMatchesAny([]string{`C:\\home\\user`}, `..\\..\\etc\\passwd`)\n\t\t})\n\t}))\n\n\t// TODO(a.garipov): See if there is anything for which filepath.Match\n\t// returns ErrBadPattern on Windows.\n}\n"
  },
  {
    "path": "internal/filtering/result.go",
    "content": "package filtering\n\nimport (\n\t\"fmt\"\n\t\"net/netip\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n)\n\n// Result contains the result of a request check.  All fields transitively have\n// omitempty tags so that the query log doesn't become too large.\n//\n// TODO(a.garipov): Clarify relationships between fields.  Perhaps replace with\n// a sum type or an interface?\ntype Result struct {\n\t// DNSRewriteResult is the $dnsrewrite filter rule result.\n\tDNSRewriteResult *DNSRewriteResult `json:\",omitempty\"`\n\n\t// CanonName is the CNAME value from the lookup rewrite result.  It is empty\n\t// unless Reason is set to Rewritten or RewrittenRule.\n\tCanonName string `json:\",omitempty\"`\n\n\t// ServiceName is the name of the blocked service.  It is empty unless\n\t// Reason is set to FilteredBlockedService.\n\tServiceName string `json:\",omitempty\"`\n\n\t// IPList is the lookup rewrite result.  It is empty unless Reason is set to\n\t// Rewritten.\n\tIPList []netip.Addr `json:\",omitempty\"`\n\n\t// Rules are applied rules.  If Rules are not empty, each rule is not nil.\n\tRules []*ResultRule `json:\",omitempty\"`\n\n\t// Reason is the reason for blocking or unblocking the request.\n\tReason Reason `json:\",omitempty\"`\n\n\t// IsFiltered is true if the request is filtered.\n\t//\n\t// TODO(d.kolyshev): Get rid of this flag.\n\tIsFiltered bool `json:\",omitempty\"`\n}\n\n// ResultRule contains information about applied rules.\ntype ResultRule struct {\n\t// Text is the text of the rule.\n\tText string `json:\",omitempty\"`\n\n\t// IP is the host IP.  It is nil unless the rule uses the /etc/hosts syntax\n\t// or the reason is [FilteredSafeSearch].\n\tIP netip.Addr `json:\",omitzero\"`\n\n\t// FilterListID is the ID of the rule's filter list.\n\tFilterListID rulelist.APIID `json:\",omitempty\"`\n}\n\n// NewResultRule converts an URLFilter rule into a *ResultRule.  nr must not be\n// nil.\nfunc NewResultRule(r rules.Rule) (rr *ResultRule) {\n\treturn &ResultRule{\n\t\t// #nosec G115 -- The overflow is required for backwards\n\t\t// compatibility.\n\t\tFilterListID: rulelist.APIID(r.GetFilterListID()),\n\t\tText:         r.Text(),\n\t}\n}\n\n// Reason holds an enum detailing why it was filtered or not filtered\ntype Reason int\n\nconst (\n\t// NotFilteredNotFound: the host was not find in any checks, default value\n\t// for results.\n\tNotFilteredNotFound Reason = iota\n\n\t// NotFilteredAllowList: the host is explicitly allowed.\n\tNotFilteredAllowList\n\n\t// NotFilteredError is returned when there was an error during checking.\n\t// Reserved, currently unused.\n\tNotFilteredError\n\n\t// FilteredBlockList: the host was matched to be advertising host.\n\tFilteredBlockList\n\n\t// FilteredSafeBrowsing: the host was matched to be malicious/phishing.\n\tFilteredSafeBrowsing\n\n\t// FilteredParental: the host was matched to be outside of parental control\n\t// settings.\n\tFilteredParental\n\n\t// FilteredInvalid: the request was invalid and was not processed.\n\tFilteredInvalid\n\n\t// FilteredSafeSearch: the host was replaced with safesearch variant.\n\tFilteredSafeSearch\n\n\t// FilteredBlockedService: the host is blocked by the blocked services\n\t// feature.\n\tFilteredBlockedService\n\n\t// Rewritten is returned when there was a rewrite by a legacy DNS rewrite\n\t// rule.\n\tRewritten\n\n\t// RewrittenAutoHosts is returned when there was a rewrite by /etc/hosts.\n\tRewrittenAutoHosts\n\n\t// RewrittenRule is returned when a $dnsrewrite filter rule was applied.\n\t//\n\t// TODO(a.garipov): Remove [Rewritten] and [RewrittenAutoHosts] by merging\n\t// their functionality into RewrittenRule.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2499.\n\tRewrittenRule\n)\n\n// TODO(a.garipov): Resync with actual code names or replace completely in HTTP\n// API v1.\nvar reasonNames = []string{\n\tNotFilteredNotFound:  \"NotFilteredNotFound\",\n\tNotFilteredAllowList: \"NotFilteredWhiteList\",\n\tNotFilteredError:     \"NotFilteredError\",\n\n\tFilteredBlockList:      \"FilteredBlackList\",\n\tFilteredSafeBrowsing:   \"FilteredSafeBrowsing\",\n\tFilteredParental:       \"FilteredParental\",\n\tFilteredInvalid:        \"FilteredInvalid\",\n\tFilteredSafeSearch:     \"FilteredSafeSearch\",\n\tFilteredBlockedService: \"FilteredBlockedService\",\n\n\tRewritten:          \"Rewrite\",\n\tRewrittenAutoHosts: \"RewriteEtcHosts\",\n\tRewrittenRule:      \"RewriteRule\",\n}\n\n// type check\nvar _ fmt.Stringer = NotFilteredNotFound\n\n// String implements the [fmt.Stringer] interface for Reason.\nfunc (r Reason) String() (s string) {\n\tif r < 0 || int(r) >= len(reasonNames) {\n\t\treturn \"\"\n\t}\n\n\treturn reasonNames[r]\n}\n\n// In returns true if reasons include r.\nfunc (r Reason) In(reasons ...Reason) (ok bool) { return slices.Contains(reasons, r) }\n"
  },
  {
    "path": "internal/filtering/rewrite/item.go",
    "content": "package rewrite\n\nimport (\n\t\"fmt\"\n\t\"net/netip\"\n\t\"strings\"\n\n\t\"github.com/miekg/dns\"\n)\n\n// Item is a single DNS rewrite record.\n//\n// TODO(s.chzhen):  Add \"Enabled\" property.\ntype Item struct {\n\t// Domain is the domain pattern for which this rewrite should work.\n\tDomain string `yaml:\"domain\"`\n\n\t// Answer is the IP address, canonical name, or one of the special\n\t// values: \"A\" or \"AAAA\".\n\tAnswer string `yaml:\"answer\"`\n}\n\n// equal returns true if rw is equal to other.\nfunc (rw *Item) equal(other *Item) (ok bool) {\n\tif rw == nil {\n\t\treturn other == nil\n\t} else if other == nil {\n\t\treturn false\n\t}\n\n\treturn *rw == *other\n}\n\n// toRule converts rw to a filter rule.\nfunc (rw *Item) toRule() (res string) {\n\tif rw == nil {\n\t\treturn \"\"\n\t}\n\n\tdomain := strings.ToLower(rw.Domain)\n\n\tdType, exception := rw.rewriteParams()\n\tdTypeKey := dns.TypeToString[dType]\n\tif exception {\n\t\treturn fmt.Sprintf(\"@@||%s^$dnstype=%s,dnsrewrite\", domain, dTypeKey)\n\t}\n\n\treturn fmt.Sprintf(\"|%s^$dnsrewrite=NOERROR;%s;%s\", domain, dTypeKey, rw.Answer)\n}\n\n// rewriteParams returns dns request type and exception flag for rw.\nfunc (rw *Item) rewriteParams() (dType uint16, exception bool) {\n\tswitch rw.Answer {\n\tcase \"AAAA\":\n\t\treturn dns.TypeAAAA, true\n\tcase \"A\":\n\t\treturn dns.TypeA, true\n\tdefault:\n\t\t// Go on.\n\t}\n\n\taddr, err := netip.ParseAddr(rw.Answer)\n\tif err != nil {\n\t\t// TODO(d.kolyshev): Validate rw.Answer as a domain name.\n\t\treturn dns.TypeCNAME, false\n\t}\n\n\tif addr.Is4() {\n\t\tdType = dns.TypeA\n\t} else {\n\t\tdType = dns.TypeAAAA\n\t}\n\n\treturn dType, false\n}\n"
  },
  {
    "path": "internal/filtering/rewrite/item_internal_test.go",
    "content": "package rewrite\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestItem_equal(t *testing.T) {\n\tconst (\n\t\ttestDomain = \"example.org\"\n\t\ttestAnswer = \"1.1.1.1\"\n\t)\n\n\ttestItem := &Item{\n\t\tDomain: testDomain,\n\t\tAnswer: testAnswer,\n\t}\n\n\ttestCases := []struct {\n\t\tleft  *Item\n\t\tright *Item\n\t\tname  string\n\t\twant  bool\n\t}{{\n\t\tname:  \"nil_left\",\n\t\tleft:  nil,\n\t\tright: testItem,\n\t\twant:  false,\n\t}, {\n\t\tname:  \"nil_right\",\n\t\tleft:  testItem,\n\t\tright: nil,\n\t\twant:  false,\n\t}, {\n\t\tname:  \"nils\",\n\t\tleft:  nil,\n\t\tright: nil,\n\t\twant:  true,\n\t}, {\n\t\tname:  \"equal\",\n\t\tleft:  testItem,\n\t\tright: testItem,\n\t\twant:  true,\n\t}, {\n\t\tname: \"distinct\",\n\t\tleft: testItem,\n\t\tright: &Item{\n\t\t\tDomain: \"other\",\n\t\t\tAnswer: \"other\",\n\t\t},\n\t\twant: false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tres := tc.left.equal(tc.right)\n\t\t\tassert.Equal(t, tc.want, res)\n\t\t})\n\t}\n}\n\nfunc TestItem_toRule(t *testing.T) {\n\tconst testDomain = \"example.org\"\n\n\ttestCases := []struct {\n\t\tname string\n\t\titem *Item\n\t\twant string\n\t}{{\n\t\tname: \"nil\",\n\t\titem: nil,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"a_rule\",\n\t\titem: &Item{\n\t\t\tDomain: testDomain,\n\t\t\tAnswer: \"1.1.1.1\",\n\t\t},\n\t\twant: \"|example.org^$dnsrewrite=NOERROR;A;1.1.1.1\",\n\t}, {\n\t\tname: \"aaaa_rule\",\n\t\titem: &Item{\n\t\t\tDomain: testDomain,\n\t\t\tAnswer: \"1:2:3::4\",\n\t\t},\n\t\twant: \"|example.org^$dnsrewrite=NOERROR;AAAA;1:2:3::4\",\n\t}, {\n\t\tname: \"cname_rule\",\n\t\titem: &Item{\n\t\t\tDomain: testDomain,\n\t\t\tAnswer: \"other.org\",\n\t\t},\n\t\twant: \"|example.org^$dnsrewrite=NOERROR;CNAME;other.org\",\n\t}, {\n\t\tname: \"wildcard_rule\",\n\t\titem: &Item{\n\t\t\tDomain: \"*.example.org\",\n\t\t\tAnswer: \"other.org\",\n\t\t},\n\t\twant: \"|*.example.org^$dnsrewrite=NOERROR;CNAME;other.org\",\n\t}, {\n\t\tname: \"aaaa_exception\",\n\t\titem: &Item{\n\t\t\tDomain: testDomain,\n\t\t\tAnswer: \"A\",\n\t\t},\n\t\twant: \"@@||example.org^$dnstype=A,dnsrewrite\",\n\t}, {\n\t\tname: \"aaaa_exception\",\n\t\titem: &Item{\n\t\t\tDomain: testDomain,\n\t\t\tAnswer: \"AAAA\",\n\t\t},\n\t\twant: \"@@||example.org^$dnstype=AAAA,dnsrewrite\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tres := tc.item.toRule()\n\t\t\tassert.Equal(t, tc.want, res)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/rewrite/storage.go",
    "content": "// Package rewrite implements DNS Rewrites storage and request matching.\npackage rewrite\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/filterlist\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// Storage is a storage for rewrite rules.\ntype Storage interface {\n\t// MatchRequest returns matching dnsrewrites for the specified request.\n\tMatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite)\n\n\t// Add adds item to the storage.\n\tAdd(item *Item) (err error)\n\n\t// Remove deletes item from the storage.\n\tRemove(item *Item) (err error)\n\n\t// List returns all items from the storage.\n\tList() (items []*Item)\n}\n\n// Config is the configuration for DefaultStorage.\ntype Config struct {\n\t// logger is used for logging storage processes.  It must not be nil.\n\tLogger *slog.Logger\n\n\t// Rewrites stores the rewrite entries.  It must not be nil.\n\tRewrites []*Item\n\n\t// ListID is used as an identifier of the underlying rules list.\n\tListID rules.ListID\n}\n\n// DefaultStorage is the default storage for rewrite rules.\ntype DefaultStorage struct {\n\t// logger is used for logging storage processes.  It must not be nil.\n\tlogger *slog.Logger\n\n\t// mu protects items.\n\tmu *sync.RWMutex\n\n\t// engine is the DNS filtering engine.\n\tengine *urlfilter.DNSEngine\n\n\t// ruleList is the filtering rule ruleList used by the engine.\n\truleList filterlist.Interface\n\n\t// rewrites stores the rewrite entries from configuration.\n\trewrites []*Item\n\n\t// urlFilterID is the synthetic integer identifier for the urlfilter engine.\n\turlFilterID rules.ListID\n}\n\n// NewDefaultStorage returns new rewrites storage.  conf must not be nil.\nfunc NewDefaultStorage(conf *Config) (s *DefaultStorage, err error) {\n\ts = &DefaultStorage{\n\t\tlogger:      conf.Logger,\n\t\tmu:          &sync.RWMutex{},\n\t\turlFilterID: conf.ListID,\n\t\trewrites:    conf.Rewrites,\n\t}\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\terr = s.resetRules()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn s, nil\n}\n\n// type check\nvar _ Storage = (*DefaultStorage)(nil)\n\n// MatchRequest implements the [Storage] interface for *DefaultStorage.\nfunc (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite) {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\n\tctx := context.TODO()\n\n\trewriteRules := s.rewriteRulesForReq(dReq)\n\tif len(rewriteRules) == 0 {\n\t\treturn nil\n\t}\n\n\tresolvedRules, wildcardRewrite := s.resolveCNAMEChain(ctx, dReq, rewriteRules)\n\tif wildcardRewrite != nil {\n\t\treturn []*rules.DNSRewrite{wildcardRewrite}\n\t}\n\n\tif resolvedRules == nil {\n\t\treturn nil\n\t}\n\n\treturn s.collectDNSRewrites(resolvedRules, dReq.DNSType)\n}\n\n// resolveCNAMEChain follows the CNAME chain for a DNS request, handling loops\n// and special cases.  dReq must not be nil, and rewriteRules must not contain\n// nil elements.\nfunc (s *DefaultStorage) resolveCNAMEChain(\n\tctx context.Context,\n\tdReq *urlfilter.DNSRequest,\n\trewriteRules []*rules.NetworkRule,\n) (resolvedRules []*rules.NetworkRule, wildcardRewrite *rules.DNSRewrite) {\n\t// TODO(a.garipov): Check cnames for cycles on initialization.\n\tcnames := container.NewMapSet[string]()\n\thost := dReq.Hostname\n\tfor len(rewriteRules) > 0 &&\n\t\trewriteRules[0].DNSRewrite != nil &&\n\t\trewriteRules[0].DNSRewrite.NewCNAME != \"\" {\n\t\trule := rewriteRules[0]\n\t\trwAns := rule.DNSRewrite.NewCNAME\n\n\t\ts.logger.DebugContext(ctx, \"cname found\", \"host\", host, \"cname\", rwAns)\n\n\t\tif dReq.Hostname == rwAns {\n\t\t\t// A request for the hostname itself is an exception rule.\n\t\t\t// TODO(d.kolyshev): Check rewrite of a pattern onto itself.\n\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tif isSelfMatchingWildcard(host, rwAns, rule.Text()) {\n\t\t\treturn nil, rule.DNSRewrite\n\t\t}\n\n\t\tif cnames.Has(rwAns) {\n\t\t\ts.logger.WarnContext(ctx, \"rewrite cname loop\", \"host\", dReq.Hostname, \"rewrite\", rwAns)\n\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tcnames.Add(rwAns)\n\n\t\trewriteRulesForReq := s.rewriteRulesForReq(&urlfilter.DNSRequest{\n\t\t\tHostname: rwAns,\n\t\t\tDNSType:  dReq.DNSType,\n\t\t})\n\t\tif rewriteRulesForReq != nil {\n\t\t\trewriteRules = rewriteRulesForReq\n\t\t}\n\n\t\thost = rwAns\n\t}\n\n\treturn rewriteRules, nil\n}\n\n// isSelfMatchingWildcard returns true when a wildcard rewrite matches its own\n// result.\n//\n// For example, an \"*.example.com → sub.example.com\" rewrite matching in a loop.\n//\n// See https://github.com/AdguardTeam/AdGuardHome/issues/4016.\nfunc isSelfMatchingWildcard(host, rwAns, ruleText string) (ok bool) {\n\treturn host == rwAns && isWildcard(ruleText)\n}\n\n// collectDNSRewrites filters DNSRewrite by question type.\nfunc (s *DefaultStorage) collectDNSRewrites(\n\trewrites []*rules.NetworkRule,\n\tqtyp uint16,\n) (rws []*rules.DNSRewrite) {\n\tfor _, rewrite := range rewrites {\n\t\tdnsRewrite := rewrite.DNSRewrite\n\t\tif matchesQType(dnsRewrite, qtyp) {\n\t\t\trws = append(rws, dnsRewrite)\n\t\t}\n\t}\n\n\treturn rws\n}\n\n// rewriteRulesForReq returns matching dnsrewrite rules.\nfunc (s *DefaultStorage) rewriteRulesForReq(dReq *urlfilter.DNSRequest) (rules []*rules.NetworkRule) {\n\tres, _ := s.engine.MatchRequest(dReq)\n\n\treturn res.DNSRewrites()\n}\n\n// Add implements the [Storage] interface for *DefaultStorage.\nfunc (s *DefaultStorage) Add(item *Item) (err error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\t// TODO(d.kolyshev): Handle duplicate items.\n\ts.rewrites = append(s.rewrites, item)\n\n\treturn s.resetRules()\n}\n\n// Remove implements the [Storage] interface for *DefaultStorage.\nfunc (s *DefaultStorage) Remove(item *Item) (err error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tctx := context.TODO()\n\n\tarr := []*Item{}\n\n\t// TODO(d.kolyshev): Use slices.IndexFunc + slices.Delete?\n\tfor _, ent := range s.rewrites {\n\t\tif ent.equal(item) {\n\t\t\ts.logger.DebugContext(ctx, \"removed element\", \"domain\", ent.Domain, \"ans\", ent.Answer)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tarr = append(arr, ent)\n\t}\n\ts.rewrites = arr\n\n\treturn s.resetRules()\n}\n\n// List implements the [Storage] interface for *DefaultStorage.\nfunc (s *DefaultStorage) List() (items []*Item) {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\n\treturn slices.Clone(s.rewrites)\n}\n\n// resetRules resets the filtering rules.\nfunc (s *DefaultStorage) resetRules() (err error) {\n\t// TODO(a.garipov): Use strings.Builder.\n\tvar rulesText []string\n\tfor _, rewrite := range s.rewrites {\n\t\trulesText = append(rulesText, rewrite.toRule())\n\t}\n\n\tstrList := filterlist.NewString(&filterlist.StringConfig{\n\t\tID:             s.urlFilterID,\n\t\tRulesText:      strings.Join(rulesText, \"\\n\"),\n\t\tIgnoreCosmetic: true,\n\t})\n\n\trs, err := filterlist.NewRuleStorage([]filterlist.Interface{strList})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating list storage: %w\", err)\n\t}\n\n\ts.ruleList = strList\n\ts.engine = urlfilter.NewDNSEngine(rs)\n\n\ts.logger.InfoContext(\n\t\tcontext.TODO(),\n\t\t\"reset rules\",\n\t\t\"filter\", s.urlFilterID,\n\t\t\"count\", s.engine.RulesCount,\n\t)\n\n\treturn nil\n}\n\n// matchesQType returns true if dnsrewrite matches the question type qt.\nfunc matchesQType(dnsrr *rules.DNSRewrite, qt uint16) (ok bool) {\n\t// Add CNAMEs, since they match for all types requests.\n\tif dnsrr.RRType == dns.TypeCNAME {\n\t\treturn true\n\t}\n\n\t// Reject types other than A and AAAA.\n\tif qt != dns.TypeA && qt != dns.TypeAAAA {\n\t\treturn false\n\t}\n\n\treturn dnsrr.RRType == qt\n}\n\n// isWildcard returns true if pat is a wildcard domain pattern.\nfunc isWildcard(pat string) (res bool) {\n\treturn strings.HasPrefix(pat, \"|*.\")\n}\n"
  },
  {
    "path": "internal/filtering/rewrite/storage_internal_test.go",
    "content": "package rewrite\n\nimport (\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testListID is the common rule-list ID for tests.\nconst testListID rules.ListID = 1\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\nfunc TestNewDefaultStorage(t *testing.T) {\n\titems := []*Item{{\n\t\tDomain: \"example.com\",\n\t\tAnswer: \"answer.com\",\n\t}}\n\n\ts, err := NewDefaultStorage(&Config{\n\t\tLogger:   testLogger,\n\t\tRewrites: items,\n\t\tListID:   testListID,\n\t})\n\trequire.NoError(t, err)\n\n\trequire.Len(t, s.List(), 1)\n}\n\nfunc TestDefaultStorage_CRUD(t *testing.T) {\n\tvar items []*Item\n\n\ts, err := NewDefaultStorage(&Config{\n\t\tLogger:   testLogger,\n\t\tRewrites: items,\n\t\tListID:   testListID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, s.List(), 0)\n\n\titem := &Item{Domain: \"example.com\", Answer: \"answer.com\"}\n\n\terr = s.Add(item)\n\trequire.NoError(t, err)\n\n\tlist := s.List()\n\trequire.Len(t, list, 1)\n\trequire.True(t, item.equal(list[0]))\n\n\terr = s.Remove(item)\n\trequire.NoError(t, err)\n\trequire.Len(t, s.List(), 0)\n}\n\nfunc TestDefaultStorage_MatchRequest(t *testing.T) {\n\tvar (\n\t\taddr1v4 = netip.AddrFrom4([4]byte{1, 2, 3, 4})\n\t\taddr2v4 = netip.AddrFrom4([4]byte{1, 2, 3, 5})\n\t\taddr3v4 = netip.AddrFrom4([4]byte{1, 2, 3, 6})\n\t\taddr4v4 = netip.AddrFrom4([4]byte{1, 2, 3, 7})\n\n\t\taddr1v6 = netip.MustParseAddr(\"1:2:3::4\")\n\t\taddr2v6 = netip.MustParseAddr(\"1234::5678\")\n\t)\n\n\titems := []*Item{{\n\t\t// This one and below are about CNAME, A and AAAA.\n\t\tDomain: \"somecname\",\n\t\tAnswer: \"somehost.com\",\n\t}, {\n\t\tDomain: \"somehost.com\",\n\t\tAnswer: netip.IPv4Unspecified().String(),\n\t}, {\n\t\tDomain: \"host.com\",\n\t\tAnswer: addr1v4.String(),\n\t}, {\n\t\tDomain: \"host.com\",\n\t\tAnswer: addr2v4.String(),\n\t}, {\n\t\tDomain: \"host.com\",\n\t\tAnswer: addr1v6.String(),\n\t}, {\n\t\tDomain: \"www.host.com\",\n\t\tAnswer: \"host.com\",\n\t}, {\n\t\t// This one is a wildcard.\n\t\tDomain: \"*.host.com\",\n\t\tAnswer: addr2v4.String(),\n\t}, {\n\t\t// This one and below are about wildcard overriding.\n\t\tDomain: \"a.host.com\",\n\t\tAnswer: addr1v4.String(),\n\t}, {\n\t\t// This one is about CNAME and wildcard interacting.\n\t\tDomain: \"*.host2.com\",\n\t\tAnswer: \"host.com\",\n\t}, {\n\t\t// This one and below are about 2 level CNAME.\n\t\tDomain: \"b.host.com\",\n\t\tAnswer: \"somecname\",\n\t}, {\n\t\t// This one and below are about 2 level CNAME and wildcard.\n\t\tDomain: \"b.host3.com\",\n\t\tAnswer: \"a.host3.com\",\n\t}, {\n\t\tDomain: \"a.host3.com\",\n\t\tAnswer: \"x.host.com\",\n\t}, {\n\t\tDomain: \"*.hostboth.com\",\n\t\tAnswer: addr3v4.String(),\n\t}, {\n\t\tDomain: \"*.hostboth.com\",\n\t\tAnswer: addr2v6.String(),\n\t}, {\n\t\tDomain: \"BIGHOST.COM\",\n\t\tAnswer: addr4v4.String(),\n\t}, {\n\t\tDomain: \"*.issue4016.com\",\n\t\tAnswer: \"sub.issue4016.com\",\n\t}}\n\n\ts, err := NewDefaultStorage(&Config{\n\t\tLogger:   testLogger,\n\t\tRewrites: items,\n\t\tListID:   testListID,\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname            string\n\t\thost            string\n\t\twantDNSRewrites []*rules.DNSRewrite\n\t\tdtyp            uint16\n\t}{{\n\t\tname:            \"not_filtered_not_found\",\n\t\thost:            \"hoost.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeA,\n\t}, {\n\t\tname:            \"not_filtered_qtype\",\n\t\thost:            \"www.host.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeMX,\n\t}, {\n\t\tname: \"rewritten_a\",\n\t\thost: \"www.host.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr1v4,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}, {\n\t\t\tValue:    addr2v4,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname: \"rewritten_aaaa\",\n\t\thost: \"www.host.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr1v6,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeAAAA,\n\t\t}},\n\t\tdtyp: dns.TypeAAAA,\n\t}, {\n\t\tname: \"wildcard_match\",\n\t\thost: \"abc.host.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr2v4,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t\t//}, {\n\t\t// TODO(d.kolyshev): This is about matching in urlfilter.\n\t\t//\tname: \"wildcard_override\",\n\t\t//\thost: \"a.host.com\",\n\t\t//\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t//\t\tValue:    net.IP{1, 2, 3, 4}.To16(),\n\t\t//\t\tNewCNAME: \"\",\n\t\t//\t\tRCode:    dns.RcodeSuccess,\n\t\t//\t\tRRType:   dns.TypeA,\n\t\t//\t}},\n\t\t//\tdtyp: dns.TypeA,\n\t}, {\n\t\tname: \"wildcard_cname_interaction\",\n\t\thost: \"www.host2.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr1v4,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}, {\n\t\t\tValue:    addr2v4,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname: \"two_cnames\",\n\t\thost: \"b.host.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    netip.IPv4Unspecified(),\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname: \"two_cnames_and_wildcard\",\n\t\thost: \"b.host3.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr2v4,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname: \"issue3343\",\n\t\thost: \"www.hostboth.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr2v6,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeAAAA,\n\t\t}},\n\t\tdtyp: dns.TypeAAAA,\n\t}, {\n\t\tname: \"issue3351\",\n\t\thost: \"bighost.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr4v4,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname:            \"issue4008\",\n\t\thost:            \"somehost.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeHTTPS,\n\t}, {\n\t\tname: \"issue4016\",\n\t\thost: \"www.issue4016.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    nil,\n\t\t\tNewCNAME: \"sub.issue4016.com\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeNone,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname:            \"issue4016_self\",\n\t\thost:            \"sub.issue4016.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeA,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdnsRewrites := s.MatchRequest(&urlfilter.DNSRequest{\n\t\t\t\tHostname: tc.host,\n\t\t\t\tDNSType:  tc.dtyp,\n\t\t\t})\n\n\t\t\tassert.Equal(t, tc.wantDNSRewrites, dnsRewrites)\n\t\t})\n\t}\n}\n\nfunc TestDefaultStorage_MatchRequest_Levels(t *testing.T) {\n\tvar (\n\t\taddr1 = netip.AddrFrom4([4]byte{1, 1, 1, 1})\n\t\taddr2 = netip.AddrFrom4([4]byte{2, 2, 2, 2})\n\t\taddr3 = netip.AddrFrom4([4]byte{3, 3, 3, 3})\n\t)\n\n\t// Exact host, wildcard L2, wildcard L3.\n\titems := []*Item{{\n\t\tDomain: \"host.com\",\n\t\tAnswer: addr1.String(),\n\t}, {\n\t\tDomain: \"*.host.com\",\n\t\tAnswer: addr2.String(),\n\t}, {\n\t\tDomain: \"*.sub.host.com\",\n\t\tAnswer: addr3.String(),\n\t}}\n\n\ts, err := NewDefaultStorage(&Config{\n\t\tLogger:   testLogger,\n\t\tRewrites: items,\n\t\tListID:   testListID,\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname            string\n\t\thost            string\n\t\twantDNSRewrites []*rules.DNSRewrite\n\t\tdtyp            uint16\n\t}{{\n\t\tname: \"exact_match\",\n\t\thost: \"host.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr1,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname: \"l2_match\",\n\t\thost: \"sub.host.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr2,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t\t//}, {\n\t\t// TODO(d.kolyshev): This is about matching in urlfilter.\n\t\t//\tname: \"l3_match\",\n\t\t//\thost: \"my.sub.host.com\",\n\t\t//\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t//\t\tValue:    addr3,\n\t\t//\t\tNewCNAME: \"\",\n\t\t//\t\tRCode:    dns.RcodeSuccess,\n\t\t//\t\tRRType:   dns.TypeA,\n\t\t//\t}},\n\t\t//\tdtyp: dns.TypeA,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdnsRewrites := s.MatchRequest(&urlfilter.DNSRequest{\n\t\t\t\tHostname: tc.host,\n\t\t\t\tDNSType:  tc.dtyp,\n\t\t\t})\n\n\t\t\tassert.Equal(t, tc.wantDNSRewrites, dnsRewrites)\n\t\t})\n\t}\n}\n\nfunc TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {\n\taddr := netip.AddrFrom4([4]byte{2, 2, 2, 2})\n\n\t// Wildcard and exception for a sub-domain.\n\titems := []*Item{{\n\t\tDomain: \"*.host.com\",\n\t\tAnswer: addr.String(),\n\t}, {\n\t\tDomain: \"sub.host.com\",\n\t\tAnswer: \"sub.host.com\",\n\t}, {\n\t\tDomain: \"*.sub.host.com\",\n\t\tAnswer: \"*.sub.host.com\",\n\t}}\n\n\ts, err := NewDefaultStorage(&Config{\n\t\tLogger:   testLogger,\n\t\tRewrites: items,\n\t\tListID:   testListID,\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname            string\n\t\thost            string\n\t\twantDNSRewrites []*rules.DNSRewrite\n\t\tdtyp            uint16\n\t}{{\n\t\tname: \"match_subdomain\",\n\t\thost: \"my.host.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname:            \"exception_cname\",\n\t\thost:            \"sub.host.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeA,\n\t\t//}, {\n\t\t// TODO(d.kolyshev): This is about matching in urlfilter.\n\t\t//\tname:            \"exception_wildcard\",\n\t\t//\thost:            \"my.sub.host.com\",\n\t\t//\twantDNSRewrites: nil,\n\t\t//\tdtyp:            dns.TypeA,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdnsRewrites := s.MatchRequest(&urlfilter.DNSRequest{\n\t\t\t\tHostname: tc.host,\n\t\t\t\tDNSType:  tc.dtyp,\n\t\t\t})\n\n\t\t\tassert.Equal(t, tc.wantDNSRewrites, dnsRewrites)\n\t\t})\n\t}\n}\n\nfunc TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {\n\taddr := netip.AddrFrom4([4]byte{1, 2, 3, 4})\n\n\t// Exception for AAAA record.\n\titems := []*Item{{\n\t\tDomain: \"host.com\",\n\t\tAnswer: addr.String(),\n\t}, {\n\t\tDomain: \"host.com\",\n\t\tAnswer: \"AAAA\",\n\t}, {\n\t\tDomain: \"host2.com\",\n\t\tAnswer: netutil.IPv6Localhost().String(),\n\t}, {\n\t\tDomain: \"host2.com\",\n\t\tAnswer: \"A\",\n\t}, {\n\t\tDomain: \"host3.com\",\n\t\tAnswer: \"A\",\n\t}}\n\n\ts, err := NewDefaultStorage(&Config{\n\t\tLogger:   testLogger,\n\t\tRewrites: items,\n\t\tListID:   testListID,\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname            string\n\t\thost            string\n\t\twantDNSRewrites []*rules.DNSRewrite\n\t\tdtyp            uint16\n\t}{{\n\t\tname: \"match_A\",\n\t\thost: \"host.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    addr,\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeA,\n\t\t}},\n\t\tdtyp: dns.TypeA,\n\t}, {\n\t\tname:            \"exception_AAAA_host.com\",\n\t\thost:            \"host.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeAAAA,\n\t}, {\n\t\tname:            \"exception_A_host2.com\",\n\t\thost:            \"host2.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeA,\n\t}, {\n\t\tname: \"match_AAAA_host2.com\",\n\t\thost: \"host2.com\",\n\t\twantDNSRewrites: []*rules.DNSRewrite{{\n\t\t\tValue:    netutil.IPv6Localhost(),\n\t\t\tNewCNAME: \"\",\n\t\t\tRCode:    dns.RcodeSuccess,\n\t\t\tRRType:   dns.TypeAAAA,\n\t\t}},\n\t\tdtyp: dns.TypeAAAA,\n\t}, {\n\t\tname:            \"exception_A_host3.com\",\n\t\thost:            \"host3.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeA,\n\t}, {\n\t\tname:            \"match_AAAA_host3.com\",\n\t\thost:            \"host3.com\",\n\t\twantDNSRewrites: nil,\n\t\tdtyp:            dns.TypeAAAA,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdnsRewrites := s.MatchRequest(&urlfilter.DNSRequest{\n\t\t\t\tHostname: tc.host,\n\t\t\t\tDNSType:  tc.dtyp,\n\t\t\t})\n\n\t\t\tassert.Equal(t, tc.wantDNSRewrites, dnsRewrites)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/rewritehttp.go",
    "content": "package filtering\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n)\n\n// rewriteEntryJSON is a single entry of the DNS rewrite.\n//\n// TODO(d.kolyshev): Use [rewrite.Item] instead.\ntype rewriteEntryJSON struct {\n\tDomain  string          `json:\"domain\"`\n\tAnswer  string          `json:\"answer\"`\n\tEnabled aghalg.NullBool `json:\"enabled\"`\n}\n\n// rewriteSettings contains DNS rewrite settings.\ntype rewriteSettings struct {\n\t// Enabled indicates whether legacy rewrites are applied.\n\t//\n\t// TODO(s.chzhen):  Consider using [aghalg.NullBool] so \"{}\" won't\n\t// accidentally disable rewrites on decode.\n\tEnabled bool `json:\"enabled\"`\n}\n\n// handleRewriteList is the handler for the GET /control/rewrite/list HTTP API.\nfunc (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {\n\tarr := []*rewriteEntryJSON{}\n\n\tfunc() {\n\t\td.confMu.RLock()\n\t\tdefer d.confMu.RUnlock()\n\n\t\tfor _, ent := range d.conf.Rewrites {\n\t\t\tjsonEnt := rewriteEntryJSON{\n\t\t\t\tDomain:  ent.Domain,\n\t\t\t\tAnswer:  ent.Answer,\n\t\t\t\tEnabled: aghalg.BoolToNullBool(ent.Enabled),\n\t\t\t}\n\t\t\tarr = append(arr, &jsonEnt)\n\t\t}\n\t}()\n\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, arr)\n}\n\n// handleRewriteAdd is the handler for the POST /control/rewrite/add HTTP API.\nfunc (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\trwJSON := rewriteEntryJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&rwJSON)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"json.Decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tenabled := true\n\tif rwJSON.Enabled != aghalg.NBNull {\n\t\tenabled = rwJSON.Enabled == aghalg.NBTrue\n\t}\n\n\trw := &LegacyRewrite{\n\t\tDomain:  rwJSON.Domain,\n\t\tAnswer:  rwJSON.Answer,\n\t\tEnabled: enabled,\n\t}\n\n\terr = rw.normalize(ctx, l)\n\tif err != nil {\n\t\t// Shouldn't happen currently, since normalize only returns a non-nil\n\t\t// error when a rewrite is nil, but be change-proof.\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"normalizing: %s\", err)\n\n\t\treturn\n\t}\n\n\tfunc() {\n\t\td.confMu.Lock()\n\t\tdefer d.confMu.Unlock()\n\n\t\td.conf.Rewrites = append(d.conf.Rewrites, rw)\n\t\tl.DebugContext(\n\t\t\tctx,\n\t\t\t\"added rewrite element\",\n\t\t\t\"domain\", rw.Domain,\n\t\t\t\"answer\", rw.Answer,\n\t\t\t\"rewrites_len\", len(d.conf.Rewrites),\n\t\t)\n\t}()\n\n\td.conf.ConfModifier.Apply(ctx)\n}\n\n// handleRewriteDelete is the handler for the POST /control/rewrite/delete HTTP\n// API.\nfunc (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\tjsent := rewriteEntryJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&jsent)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"json.Decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tentDel := &LegacyRewrite{\n\t\tDomain: jsent.Domain,\n\t\tAnswer: jsent.Answer,\n\t}\n\tarr := []*LegacyRewrite{}\n\n\tdefer d.conf.ConfModifier.Apply(ctx)\n\n\td.confMu.Lock()\n\tdefer d.confMu.Unlock()\n\n\tfor _, ent := range d.conf.Rewrites {\n\t\tif !ent.equal(entDel) {\n\t\t\tarr = append(arr, ent)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tl.DebugContext(\n\t\t\tctx,\n\t\t\t\"removed rewrite element\",\n\t\t\t\"domain\", ent.Domain,\n\t\t\t\"answer\", ent.Answer,\n\t\t)\n\t}\n\n\td.conf.Rewrites = arr\n}\n\n// rewriteUpdateJSON is a struct for JSON object with rewrite rule update info.\ntype rewriteUpdateJSON struct {\n\tTarget rewriteEntryJSON `json:\"target\"`\n\tUpdate rewriteEntryJSON `json:\"update\"`\n}\n\n// handleRewriteUpdate is the handler for the PUT /control/rewrite/update HTTP\n// API.\nfunc (d *DNSFilter) handleRewriteUpdate(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\tupdateJSON := rewriteUpdateJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&updateJSON)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"json.Decode: %s\", err)\n\n\t\treturn\n\t}\n\n\trwDel := &LegacyRewrite{\n\t\tDomain: updateJSON.Target.Domain,\n\t\tAnswer: updateJSON.Target.Answer,\n\t}\n\n\trwAdd := &LegacyRewrite{\n\t\tDomain: updateJSON.Update.Domain,\n\t\tAnswer: updateJSON.Update.Answer,\n\t}\n\n\terr = rwAdd.normalize(ctx, l)\n\tif err != nil {\n\t\t// Shouldn't happen currently, since normalize only returns a non-nil\n\t\t// error when a rewrite is nil, but be change-proof.\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"normalizing: %s\", err)\n\n\t\treturn\n\t}\n\n\tindex := -1\n\tdefer func() {\n\t\tif index >= 0 {\n\t\t\td.conf.ConfModifier.Apply(ctx)\n\t\t}\n\t}()\n\n\td.confMu.Lock()\n\tdefer d.confMu.Unlock()\n\n\tindex = slices.IndexFunc(d.conf.Rewrites, rwDel.equal)\n\tif index == -1 {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"target rule not found\")\n\n\t\treturn\n\t}\n\n\trwDel.Enabled = d.conf.Rewrites[index].Enabled\n\tif updateJSON.Update.Enabled == aghalg.NBNull {\n\t\trwAdd.Enabled = rwDel.Enabled\n\t} else {\n\t\trwAdd.Enabled = updateJSON.Update.Enabled == aghalg.NBTrue\n\t}\n\n\td.conf.Rewrites = slices.Replace(d.conf.Rewrites, index, index+1, rwAdd)\n\n\tl.DebugContext(\n\t\tctx,\n\t\t\"removed rewrite element\",\n\t\t\"domain\", rwDel.Domain,\n\t\t\"answer\", rwDel.Answer,\n\t\t\"enabled\", rwDel.Enabled,\n\t)\n\tl.DebugContext(\n\t\tctx,\n\t\t\"added rewrite element\",\n\t\t\"domain\", rwAdd.Domain,\n\t\t\"answer\", rwAdd.Answer,\n\t\t\"enabled\", rwAdd.Enabled,\n\t)\n}\n\n// handleRewriteSettings is the handler for the GET /control/rewrite/settings\n// HTTP API.\nfunc (d *DNSFilter) handleRewriteSettings(w http.ResponseWriter, r *http.Request) {\n\tresp := &rewriteSettings{\n\t\tEnabled: protectedBool(d.confMu, &d.conf.RewritesEnabled),\n\t}\n\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, resp)\n}\n\n// handleRewriteSettingsUpdate is the handler for the PUT\n// /control/rewrite/settings/update HTTP API.\nfunc (d *DNSFilter) handleRewriteSettingsUpdate(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\treq := &rewriteSettings{}\n\terr := json.NewDecoder(r.Body).Decode(req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, d.logger, r, w, http.StatusBadRequest, \"json.Decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tsetProtectedBool(d.confMu, &d.conf.RewritesEnabled, req.Enabled)\n\td.conf.ConfModifier.Apply(ctx)\n}\n"
  },
  {
    "path": "internal/filtering/rewritehttp_test.go",
    "content": "package filtering_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TODO(d.kolyshev): Use [rewrite.Item] instead.\ntype rewriteJSON struct {\n\tDomain  string          `json:\"domain\"`\n\tAnswer  string          `json:\"answer\"`\n\tEnabled aghalg.NullBool `json:\"enabled\"`\n}\n\n// newRewriteJSON returns a freshly initialized *rewriteJSON.\nfunc newRewriteJSON(domain, answer string, enabled aghalg.NullBool) (rw *rewriteJSON) {\n\treturn &rewriteJSON{\n\t\tDomain:  domain,\n\t\tAnswer:  answer,\n\t\tEnabled: enabled,\n\t}\n}\n\ntype rewriteUpdateJSON struct {\n\tTarget rewriteJSON `json:\"target\"`\n\tUpdate rewriteJSON `json:\"update\"`\n}\n\nconst (\n\tlistURL   = \"/control/rewrite/list\"\n\taddURL    = \"/control/rewrite/add\"\n\tdeleteURL = \"/control/rewrite/delete\"\n\tupdateURL = \"/control/rewrite/update\"\n\n\tdecodeMsg            = \"json.Decode: json: cannot unmarshal string into Go value of type\"\n\tdecodeErrorMsg       = decodeMsg + \" filtering.rewriteEntryJSON\\n\"\n\tdecodeUpdateErrorMsg = decodeMsg + \" filtering.rewriteUpdateJSON\\n\"\n)\n\nfunc TestDNSFilter_HandleRewriteHTTP(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\texampleDomain  = \"example.local\"\n\t\texampleAnswer  = \"example.rewrite\"\n\t\toneDomain      = \"one.local\"\n\t\toneAnswer      = \"one.rewrite\"\n\t\tdisabledDomain = \"disabled.local\"\n\t\tdisabledAnswer = \"disabled.rewrite\"\n\t\taddDomain      = \"add.local\"\n\t\taddAnswer      = \"add.rewrite\"\n\t\tupdDomain      = \"upd.local\"\n\t\tupdAnswer      = \"upd.rewrite\"\n\t\tinvDomain      = \"inv.local\"\n\t\tinvAnswer      = \"inv.rewrite\"\n\t)\n\n\ttestRewrites := []*rewriteJSON{\n\t\tnewRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),\n\t\tnewRewriteJSON(oneDomain, oneAnswer, aghalg.NBTrue),\n\t\tnewRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),\n\t}\n\n\ttestRewritesJSON, mErr := json.Marshal(testRewrites)\n\trequire.NoError(t, mErr)\n\n\ttestCases := []struct {\n\t\treqData     any\n\t\tname        string\n\t\turl         string\n\t\tmethod      string\n\t\twantList    []*rewriteJSON\n\t\twantBody    string\n\t\twantConfMod bool\n\t\twantStatus  int\n\t}{{\n\t\tname:        \"list\",\n\t\turl:         listURL,\n\t\tmethod:      http.MethodGet,\n\t\treqData:     nil,\n\t\twantConfMod: false,\n\t\twantStatus:  http.StatusOK,\n\t\twantBody:    string(testRewritesJSON) + \"\\n\",\n\t\twantList:    testRewrites,\n\t}, {\n\t\tname:        \"add_enabled_null\",\n\t\turl:         addURL,\n\t\tmethod:      http.MethodPost,\n\t\treqData:     rewriteJSON{Domain: addDomain, Answer: addAnswer},\n\t\twantConfMod: true,\n\t\twantStatus:  http.StatusOK,\n\t\twantBody:    \"\",\n\t\twantList: append(\n\t\t\ttestRewrites,\n\t\t\tnewRewriteJSON(addDomain, addAnswer, aghalg.NBTrue),\n\t\t),\n\t}, {\n\t\tname:   \"add_enabled_false\",\n\t\turl:    addURL,\n\t\tmethod: http.MethodPost,\n\t\treqData: rewriteJSON{\n\t\t\tDomain:  addDomain,\n\t\t\tAnswer:  addAnswer,\n\t\t\tEnabled: aghalg.NBFalse,\n\t\t},\n\t\twantConfMod: true,\n\t\twantStatus:  http.StatusOK,\n\t\twantBody:    \"\",\n\t\twantList: append(\n\t\t\ttestRewrites,\n\t\t\tnewRewriteJSON(addDomain, addAnswer, aghalg.NBFalse),\n\t\t),\n\t}, {\n\t\tname:   \"add_enabled_true\",\n\t\turl:    addURL,\n\t\tmethod: http.MethodPost,\n\t\treqData: rewriteJSON{\n\t\t\tDomain:  addDomain,\n\t\t\tAnswer:  addAnswer,\n\t\t\tEnabled: aghalg.NBTrue,\n\t\t},\n\t\twantConfMod: true,\n\t\twantStatus:  http.StatusOK,\n\t\twantBody:    \"\",\n\t\twantList: append(\n\t\t\ttestRewrites,\n\t\t\tnewRewriteJSON(addDomain, addAnswer, aghalg.NBTrue),\n\t\t),\n\t}, {\n\t\tname:        \"add_error\",\n\t\turl:         addURL,\n\t\tmethod:      http.MethodPost,\n\t\treqData:     \"invalid_json\",\n\t\twantConfMod: false,\n\t\twantStatus:  http.StatusBadRequest,\n\t\twantBody:    decodeErrorMsg,\n\t\twantList:    testRewrites,\n\t}, {\n\t\tname:        \"delete\",\n\t\turl:         deleteURL,\n\t\tmethod:      http.MethodPost,\n\t\treqData:     rewriteJSON{Domain: oneDomain, Answer: oneAnswer},\n\t\twantConfMod: true,\n\t\twantStatus:  http.StatusOK,\n\t\twantBody:    \"\",\n\t\twantList: []*rewriteJSON{\n\t\t\tnewRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),\n\t\t\tnewRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),\n\t\t},\n\t}, {\n\t\tname:        \"delete_error\",\n\t\turl:         deleteURL,\n\t\tmethod:      http.MethodPost,\n\t\treqData:     \"invalid_json\",\n\t\twantConfMod: false,\n\t\twantStatus:  http.StatusBadRequest,\n\t\twantBody:    decodeErrorMsg,\n\t\twantList:    testRewrites,\n\t}, {\n\t\tname:   \"update_enabled_null\",\n\t\turl:    updateURL,\n\t\tmethod: http.MethodPut,\n\t\treqData: rewriteUpdateJSON{\n\t\t\tTarget: rewriteJSON{Domain: oneDomain, Answer: oneAnswer},\n\t\t\tUpdate: rewriteJSON{Domain: updDomain, Answer: updAnswer},\n\t\t},\n\t\twantConfMod: true,\n\t\twantStatus:  http.StatusOK,\n\t\twantBody:    \"\",\n\t\twantList: []*rewriteJSON{\n\t\t\tnewRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),\n\t\t\tnewRewriteJSON(updDomain, updAnswer, aghalg.NBTrue),\n\t\t\tnewRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),\n\t\t},\n\t}, {\n\t\tname:   \"update_enabled_false\",\n\t\turl:    updateURL,\n\t\tmethod: http.MethodPut,\n\t\treqData: rewriteUpdateJSON{\n\t\t\tTarget: rewriteJSON{Domain: oneDomain, Answer: oneAnswer},\n\t\t\tUpdate: rewriteJSON{\n\t\t\t\tDomain:  updDomain,\n\t\t\t\tAnswer:  updAnswer,\n\t\t\t\tEnabled: aghalg.NBFalse,\n\t\t\t},\n\t\t},\n\t\twantConfMod: true,\n\t\twantStatus:  http.StatusOK,\n\t\twantBody:    \"\",\n\t\twantList: []*rewriteJSON{\n\t\t\tnewRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),\n\t\t\tnewRewriteJSON(updDomain, updAnswer, aghalg.NBFalse),\n\t\t\tnewRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),\n\t\t},\n\t}, {\n\t\tname:   \"update_enabled_true\",\n\t\turl:    updateURL,\n\t\tmethod: http.MethodPut,\n\t\treqData: rewriteUpdateJSON{\n\t\t\tTarget: rewriteJSON{Domain: oneDomain, Answer: oneAnswer},\n\t\t\tUpdate: rewriteJSON{Domain: updDomain, Answer: updAnswer, Enabled: aghalg.NBTrue},\n\t\t},\n\t\twantConfMod: true,\n\t\twantStatus:  http.StatusOK,\n\t\twantBody:    \"\",\n\t\twantList: []*rewriteJSON{\n\t\t\tnewRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),\n\t\t\tnewRewriteJSON(updDomain, updAnswer, aghalg.NBTrue),\n\t\t\tnewRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),\n\t\t},\n\t}, {\n\t\tname:        \"update_error\",\n\t\turl:         updateURL,\n\t\tmethod:      http.MethodPut,\n\t\treqData:     \"invalid_json\",\n\t\twantConfMod: false,\n\t\twantStatus:  http.StatusBadRequest,\n\t\twantBody:    decodeUpdateErrorMsg,\n\t\twantList:    testRewrites,\n\t}, {\n\t\tname:   \"update_error_target\",\n\t\turl:    updateURL,\n\t\tmethod: http.MethodPut,\n\t\treqData: rewriteUpdateJSON{\n\t\t\tTarget: rewriteJSON{Domain: invDomain, Answer: invAnswer},\n\t\t\tUpdate: rewriteJSON{Domain: updDomain, Answer: updAnswer},\n\t\t},\n\t\twantConfMod: false,\n\t\twantStatus:  http.StatusBadRequest,\n\t\twantBody:    \"target rule not found\\n\",\n\t\twantList:    testRewrites,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tconfModCh := make(chan struct{})\n\t\t\treqCh := make(chan struct{})\n\n\t\t\thandlers := make(map[string]http.Handler)\n\t\t\tconfModifier := &aghtest.ConfigModifier{}\n\t\t\tconfModifier.OnApply = func(_ context.Context) {\n\t\t\t\trequire.Truef(t, tc.wantConfMod, \"config modified has been fired\")\n\t\t\t\ttestutil.RequireSend(testutil.PanicT{}, confModCh, struct{}{}, testTimeout)\n\t\t\t}\n\n\t\t\td, err := filtering.New(&filtering.Config{\n\t\t\t\tLogger:       testLogger,\n\t\t\t\tConfModifier: confModifier,\n\t\t\t\tHTTPReg: &aghtest.Registrar{\n\t\t\t\t\tOnRegister: func(_, url string, handler http.HandlerFunc) {\n\t\t\t\t\t\thandlers[url] = handler\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRewrites: rewriteEntriesToLegacyRewrites(testRewrites),\n\t\t\t}, nil)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(d.Close)\n\n\t\t\td.RegisterFilteringHandlers()\n\t\t\trequire.NotEmpty(t, handlers)\n\t\t\trequire.Contains(t, handlers, listURL)\n\t\t\trequire.Contains(t, handlers, tc.url)\n\n\t\t\tvar body io.Reader\n\t\t\tif tc.reqData != nil {\n\t\t\t\tdata, rErr := json.Marshal(tc.reqData)\n\t\t\t\trequire.NoError(t, rErr)\n\n\t\t\t\tbody = bytes.NewReader(data)\n\t\t\t}\n\n\t\t\tr := httptest.NewRequest(tc.method, tc.url, body)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tgo func() {\n\t\t\t\thandlers[tc.url].ServeHTTP(w, r)\n\n\t\t\t\ttestutil.RequireSend(testutil.PanicT{}, reqCh, struct{}{}, testTimeout)\n\t\t\t}()\n\n\t\t\tif tc.wantConfMod {\n\t\t\t\ttestutil.RequireReceive(t, confModCh, testTimeout)\n\t\t\t}\n\n\t\t\ttestutil.RequireReceive(t, reqCh, testTimeout)\n\t\t\tassert.Equal(t, tc.wantStatus, w.Code)\n\n\t\t\trespBody, err := io.ReadAll(w.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, []byte(tc.wantBody), respBody)\n\n\t\t\tassertRewritesList(t, handlers[listURL], tc.wantList)\n\t\t})\n\t}\n}\n\n// assertRewritesList checks if rewrites list equals the list received from the\n// handler by listURL.\nfunc assertRewritesList(tb testing.TB, handler http.Handler, wantList []*rewriteJSON) {\n\ttb.Helper()\n\n\tr := httptest.NewRequest(http.MethodGet, listURL, nil)\n\tw := httptest.NewRecorder()\n\n\thandler.ServeHTTP(w, r)\n\trequire.Equal(tb, http.StatusOK, w.Code)\n\n\tvar actual []*rewriteJSON\n\terr := json.NewDecoder(w.Body).Decode(&actual)\n\trequire.NoError(tb, err)\n\n\tassert.Equal(tb, wantList, actual)\n}\n\n// rewriteEntriesToLegacyRewrites gets legacy rewrites from json entries.\nfunc rewriteEntriesToLegacyRewrites(entries []*rewriteJSON) (rw []*filtering.LegacyRewrite) {\n\tfor _, entry := range entries {\n\t\trw = append(rw, &filtering.LegacyRewrite{\n\t\t\tDomain:  entry.Domain,\n\t\t\tAnswer:  entry.Answer,\n\t\t\tEnabled: entry.Enabled == aghalg.NBTrue,\n\t\t})\n\t}\n\n\treturn rw\n}\n\nfunc TestDNSFilter_HandleRewriteSettings(t *testing.T) {\n\tconst (\n\t\tenabled = \"enabled\"\n\n\t\tpath       = \"/control/rewrite/settings\"\n\t\tpathUpdate = path + \"/update\"\n\t)\n\n\tvar (\n\t\twantEnabled  = fmt.Sprintf(\"{%q:%s}\", enabled, \"true\")\n\t\twantDisabled = fmt.Sprintf(\"{%q:%s}\", enabled, \"false\")\n\t)\n\n\tconfUpdated := false\n\tconfModifier := &aghtest.ConfigModifier{\n\t\tOnApply: func(_ context.Context) {\n\t\t\tconfUpdated = true\n\t\t},\n\t}\n\thandlers := make(map[string]http.Handler)\n\n\td, err := filtering.New(&filtering.Config{\n\t\tLogger:       testLogger,\n\t\tConfModifier: confModifier,\n\t\tHTTPReg: &aghtest.Registrar{\n\t\t\tOnRegister: func(_, url string, handler http.HandlerFunc) {\n\t\t\t\thandlers[url] = handler\n\t\t\t},\n\t\t},\n\t\tRewritesEnabled: false,\n\t}, nil)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(d.Close)\n\n\trequire.True(t, t.Run(\"register\", func(t *testing.T) {\n\t\td.RegisterFilteringHandlers()\n\t\trequire.NotEmpty(t, handlers)\n\t\trequire.Contains(t, handlers, path)\n\t\trequire.Contains(t, handlers, pathUpdate)\n\n\t\tr := httptest.NewRequest(http.MethodGet, path, nil)\n\t\tw := httptest.NewRecorder()\n\t\thandlers[path].ServeHTTP(w, r)\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tassert.JSONEq(t, wantDisabled, w.Body.String())\n\t}))\n\n\trequire.True(t, t.Run(\"update\", func(t *testing.T) {\n\t\tr := httptest.NewRequest(http.MethodPut, path, bytes.NewReader([]byte(wantEnabled)))\n\t\tw := httptest.NewRecorder()\n\t\thandlers[pathUpdate].ServeHTTP(w, r)\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tr = httptest.NewRequest(http.MethodGet, path, nil)\n\t\tw = httptest.NewRecorder()\n\t\thandlers[path].ServeHTTP(w, r)\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tassert.True(t, confUpdated)\n\t\tassert.JSONEq(t, wantEnabled, w.Body.String())\n\t}))\n}\n"
  },
  {
    "path": "internal/filtering/rewrites.go",
    "content": "package filtering\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// Legacy DNS rewrites\n\n// LegacyRewrite is a single legacy DNS rewrite record.\n//\n// Instances of *LegacyRewrite must not be nil.\n//\n// NOTE:  Keep fields in sync with [cloneRewrites].\ntype LegacyRewrite struct {\n\t// Domain is the pattern to which this rewrite applies.\n\tDomain string `yaml:\"domain\"`\n\n\t// Answer is the IP address, canonical name, or one of the special\n\t// values: \"A\" or \"AAAA\".\n\tAnswer string `yaml:\"answer\"`\n\n\t// IP is the IP address that should be used in the response if Type is\n\t// dns.TypeA or dns.TypeAAAA.\n\tIP netip.Addr `yaml:\"-\"`\n\n\t// Type is the DNS record type: A, AAAA, or CNAME.\n\tType uint16 `yaml:\"-\"`\n\n\t// Enabled indicates whether this rewrite is active.\n\tEnabled bool `yaml:\"enabled\"`\n}\n\n// equal returns true if the rw is equal to the other.\nfunc (rw *LegacyRewrite) equal(other *LegacyRewrite) (ok bool) {\n\treturn rw.Domain == other.Domain && rw.Answer == other.Answer\n}\n\n// matchesQType returns true if the entry matches the question type qt.\nfunc (rw *LegacyRewrite) matchesQType(qt uint16) (ok bool) {\n\t// Add CNAMEs, since they match for all types requests.\n\tif rw.Type == dns.TypeCNAME {\n\t\treturn true\n\t}\n\n\t// Reject types other than A and AAAA.\n\tif qt != dns.TypeA && qt != dns.TypeAAAA {\n\t\treturn false\n\t}\n\n\t// If the types match or the entry is set to allow only the other type,\n\t// include them.\n\treturn rw.Type == qt || rw.IP == netip.Addr{}\n}\n\n// normalize makes sure that the new or decoded entry is normalized with regards\n// to domain name case, IP length, and so on.\n//\n// If rw is nil, it returns an errors.\nfunc (rw *LegacyRewrite) normalize(ctx context.Context, l *slog.Logger) (err error) {\n\tif rw == nil {\n\t\treturn errors.Error(\"nil rewrite entry\")\n\t}\n\n\t// TODO(a.garipov): Write a case-agnostic version of strings.HasSuffix and\n\t// use it in matchDomainWildcard instead of using strings.ToLower\n\t// everywhere.\n\trw.Domain = strings.ToLower(rw.Domain)\n\n\tswitch rw.Answer {\n\tcase \"AAAA\":\n\t\trw.IP = netip.Addr{}\n\t\trw.Type = dns.TypeAAAA\n\n\t\treturn nil\n\tcase \"A\":\n\t\trw.IP = netip.Addr{}\n\t\trw.Type = dns.TypeA\n\n\t\treturn nil\n\tdefault:\n\t\t// Go on.\n\t}\n\n\tip, err := netip.ParseAddr(rw.Answer)\n\tif err != nil {\n\t\tl.DebugContext(ctx, \"normalizing legacy rewrite\", slogutil.KeyError, err)\n\t\trw.Type = dns.TypeCNAME\n\n\t\treturn nil\n\t}\n\n\trw.IP = ip\n\tif ip.Is4() {\n\t\trw.Type = dns.TypeA\n\t} else {\n\t\trw.Type = dns.TypeAAAA\n\t}\n\n\treturn nil\n}\n\n// isWildcard returns true if pat is a wildcard domain pattern.\nfunc isWildcard(pat string) bool {\n\treturn len(pat) > 1 && pat[0] == '*' && pat[1] == '.'\n}\n\n// matchDomainWildcard returns true if host matches the wildcard pattern.\nfunc matchDomainWildcard(host, wildcard string) (ok bool) {\n\treturn isWildcard(wildcard) && strings.HasSuffix(host, wildcard[1:])\n}\n\n// Compare is used to sort rewrites according to the following priority:\n//\n//  1. A and AAAA > CNAME;\n//  2. wildcard > exact;\n//  3. lower level wildcard > higher level wildcard;\nfunc (rw *LegacyRewrite) Compare(b *LegacyRewrite) (res int) {\n\tif rw.Type == dns.TypeCNAME {\n\t\tif b.Type != dns.TypeCNAME {\n\t\t\treturn -1\n\t\t}\n\t} else if b.Type == dns.TypeCNAME {\n\t\treturn 1\n\t}\n\n\tif aIsWld, bIsWld := isWildcard(rw.Domain), isWildcard(b.Domain); aIsWld == bIsWld {\n\t\t// Both are either wildcards or both aren't.\n\t\treturn len(b.Domain) - len(rw.Domain)\n\t} else if aIsWld {\n\t\treturn 1\n\t} else {\n\t\treturn -1\n\t}\n}\n\n// prepareRewrites normalizes and validates all legacy DNS rewrites.\nfunc (d *DNSFilter) prepareRewrites(ctx context.Context) (err error) {\n\tfor i, r := range d.conf.Rewrites {\n\t\terr = r.normalize(ctx, d.logger)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"at index %d: %w\", i, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// findRewrites returns the list of matched rewrite entries.  If rewrites are\n// empty, but matched is true, the domain is found among the rewrite rules but\n// not for this question type.\n//\n// The result priority is: CNAME, then A and AAAA; exact, then wildcard.  If the\n// host is matched exactly, wildcard entries aren't returned.  If the host\n// matched by wildcards, return the most specific for the question type.\nfunc findRewrites(\n\tentries []*LegacyRewrite,\n\thost string,\n\tqtype uint16,\n) (rewrites []*LegacyRewrite, matched bool) {\n\tfor _, e := range entries {\n\t\tif !e.Enabled {\n\t\t\tcontinue\n\t\t}\n\n\t\tif e.Domain != host && !matchDomainWildcard(host, e.Domain) {\n\t\t\tcontinue\n\t\t}\n\n\t\tmatched = true\n\t\tif e.matchesQType(qtype) {\n\t\t\trewrites = append(rewrites, e)\n\t\t}\n\t}\n\n\tif len(rewrites) == 0 {\n\t\treturn nil, matched\n\t}\n\n\treturn finalizeRewrites(rewrites), matched\n}\n\n// finalizeRewrites sorts rewrites and truncates wildcard ones.\nfunc finalizeRewrites(rewrites []*LegacyRewrite) (resRewrites []*LegacyRewrite) {\n\tslices.SortFunc(rewrites, (*LegacyRewrite).Compare)\n\n\tfor i, r := range rewrites {\n\t\tif isWildcard(r.Domain) {\n\t\t\t// Don't use rewrites[:0], because we need to return at least one\n\t\t\t// item here.\n\t\t\trewrites = rewrites[:max(1, i)]\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn rewrites\n}\n\n// setRewriteResult sets the Reason or IPList of res if necessary.  res must not\n// be nil.\nfunc (d *DNSFilter) setRewriteResult(\n\tctx context.Context,\n\tres *Result,\n\thost string,\n\trewrites []*LegacyRewrite,\n\tqtype uint16,\n) {\n\tfor _, rw := range rewrites {\n\t\tif rw.Type == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {\n\t\t\tif rw.IP == (netip.Addr{}) {\n\t\t\t\t// \"A\"/\"AAAA\" exception: allow getting from upstream.\n\t\t\t\tres.Reason = NotFilteredNotFound\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tres.IPList = append(res.IPList, rw.IP)\n\n\t\t\td.logger.DebugContext(ctx, \"set a/aaaa rewrite\", \"host\", host, \"ans\", rw.IP)\n\t\t}\n\t}\n}\n\n// cloneRewrites returns a deep copy of entries.\nfunc cloneRewrites(entries []*LegacyRewrite) (clone []*LegacyRewrite) {\n\tclone = make([]*LegacyRewrite, len(entries))\n\tfor i, rw := range entries {\n\t\tclone[i] = &LegacyRewrite{\n\t\t\tDomain:  rw.Domain,\n\t\t\tAnswer:  rw.Answer,\n\t\t\tIP:      rw.IP,\n\t\t\tType:    rw.Type,\n\t\t\tEnabled: rw.Enabled,\n\t\t}\n\t}\n\n\treturn clone\n}\n"
  },
  {
    "path": "internal/filtering/rewrites_internal_test.go",
    "content": "package filtering\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TODO(e.burkov): All the tests in this file may and should me merged together.\n\nfunc TestRewrites(t *testing.T) {\n\td, _ := newForTest(t, nil, nil)\n\tt.Cleanup(d.Close)\n\n\tvar (\n\t\taddr1v4 = netip.AddrFrom4([4]byte{1, 2, 3, 4})\n\t\taddr2v4 = netip.AddrFrom4([4]byte{1, 2, 3, 5})\n\t\taddr3v4 = netip.AddrFrom4([4]byte{1, 2, 3, 6})\n\t\taddr4v4 = netip.AddrFrom4([4]byte{1, 2, 3, 7})\n\n\t\taddr1v6 = netip.MustParseAddr(\"1:2:3::4\")\n\t\taddr2v6 = netip.MustParseAddr(\"1234::5678\")\n\t)\n\n\td.conf.Rewrites = []*LegacyRewrite{{\n\t\t// This one and below are about CNAME, A and AAAA.\n\t\tDomain:  \"somecname\",\n\t\tAnswer:  \"somehost.com\",\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"somehost.com\",\n\t\tAnswer:  netip.IPv4Unspecified().String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"host.com\",\n\t\tAnswer:  addr1v4.String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"host.com\",\n\t\tAnswer:  addr2v4.String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"host.com\",\n\t\tAnswer:  addr1v6.String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"www.host.com\",\n\t\tAnswer:  \"host.com\",\n\t\tEnabled: true,\n\t}, {\n\t\t// This one is a wildcard.\n\t\tDomain:  \"*.host.com\",\n\t\tAnswer:  addr2v4.String(),\n\t\tEnabled: true,\n\t}, {\n\t\t// This one and below are about wildcard overriding.\n\t\tDomain:  \"a.host.com\",\n\t\tAnswer:  addr1v4.String(),\n\t\tEnabled: true,\n\t}, {\n\t\t// This one is about CNAME and wildcard interacting.\n\t\tDomain:  \"*.host2.com\",\n\t\tAnswer:  \"host.com\",\n\t\tEnabled: true,\n\t}, {\n\t\t// This one and below are about 2 level CNAME.\n\t\tDomain:  \"b.host.com\",\n\t\tAnswer:  \"somecname\",\n\t\tEnabled: true,\n\t}, {\n\t\t// This one and below are about 2 level CNAME and wildcard.\n\t\tDomain:  \"b.host3.com\",\n\t\tAnswer:  \"a.host3.com\",\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"a.host3.com\",\n\t\tAnswer:  \"x.host.com\",\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"*.hostboth.com\",\n\t\tAnswer:  addr3v4.String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"*.hostboth.com\",\n\t\tAnswer:  addr2v6.String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"BIGHOST.COM\",\n\t\tAnswer:  addr4v4.String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"*.issue4016.com\",\n\t\tAnswer:  \"sub.issue4016.com\",\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"*.sub.issue6226.com\",\n\t\tAnswer:  addr2v4.String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"*.issue6226.com\",\n\t\tAnswer:  addr1v4.String(),\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"disabled.rewrite.test\",\n\t\tAnswer:  addr1v4.String(),\n\t\tEnabled: false,\n\t}}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\trequire.NoError(t, d.prepareRewrites(ctx))\n\n\ttestCases := []struct {\n\t\tname       string\n\t\thost       string\n\t\twantCName  string\n\t\twantIPs    []netip.Addr\n\t\twantReason Reason\n\t\tdtyp       uint16\n\t}{{\n\t\tname:       \"not_filtered_not_found\",\n\t\thost:       \"hoost.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    nil,\n\t\twantReason: NotFilteredNotFound,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"rewritten_a\",\n\t\thost:       \"www.host.com\",\n\t\twantCName:  \"host.com\",\n\t\twantIPs:    []netip.Addr{addr1v4, addr2v4},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"rewritten_aaaa\",\n\t\thost:       \"www.host.com\",\n\t\twantCName:  \"host.com\",\n\t\twantIPs:    []netip.Addr{addr1v6},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeAAAA,\n\t}, {\n\t\tname:       \"wildcard_match\",\n\t\thost:       \"abc.host.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    []netip.Addr{addr2v4},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"wildcard_override\",\n\t\thost:       \"a.host.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    []netip.Addr{addr1v4},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"wildcard_cname_interaction\",\n\t\thost:       \"www.host2.com\",\n\t\twantCName:  \"host.com\",\n\t\twantIPs:    []netip.Addr{addr1v4, addr2v4},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"two_cnames\",\n\t\thost:       \"b.host.com\",\n\t\twantCName:  \"somehost.com\",\n\t\twantIPs:    []netip.Addr{netip.IPv4Unspecified()},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"two_cnames_and_wildcard\",\n\t\thost:       \"b.host3.com\",\n\t\twantCName:  \"x.host.com\",\n\t\twantIPs:    []netip.Addr{addr2v4},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"issue3343\",\n\t\thost:       \"www.hostboth.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    []netip.Addr{addr2v6},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeAAAA,\n\t}, {\n\t\tname:       \"issue3351\",\n\t\thost:       \"bighost.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    []netip.Addr{addr4v4},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"issue4008\",\n\t\thost:       \"somehost.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    nil,\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeHTTPS,\n\t}, {\n\t\tname:       \"issue4016\",\n\t\thost:       \"www.issue4016.com\",\n\t\twantCName:  \"sub.issue4016.com\",\n\t\twantIPs:    nil,\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"issue4016_self\",\n\t\thost:       \"sub.issue4016.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    nil,\n\t\twantReason: NotFilteredNotFound,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"issue6226\",\n\t\thost:       \"www.issue6226.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    []netip.Addr{addr1v4},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"issue6226_sub\",\n\t\thost:       \"www.sub.issue6226.com\",\n\t\twantCName:  \"\",\n\t\twantIPs:    []netip.Addr{addr2v4},\n\t\twantReason: Rewritten,\n\t\tdtyp:       dns.TypeA,\n\t}, {\n\t\tname:       \"not_filtered_disabled_rewrite\",\n\t\thost:       \"disabled.rewrite.test\",\n\t\twantCName:  \"\",\n\t\twantIPs:    nil,\n\t\twantReason: NotFilteredNotFound,\n\t\tdtyp:       dns.TypeA,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := d.processRewrites(tc.host, tc.dtyp)\n\t\t\trequire.Equalf(t, tc.wantReason, r.Reason, \"got %s\", r.Reason)\n\n\t\t\tif tc.wantCName != \"\" {\n\t\t\t\tassert.Equal(t, tc.wantCName, r.CanonName)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.wantIPs, r.IPList)\n\t\t})\n\t}\n}\n\nfunc TestRewritesLevels(t *testing.T) {\n\td, _ := newForTest(t, nil, nil)\n\tt.Cleanup(d.Close)\n\t// Exact host, wildcard L2, wildcard L3.\n\td.conf.Rewrites = []*LegacyRewrite{{\n\t\tDomain:  \"host.com\",\n\t\tAnswer:  \"1.1.1.1\",\n\t\tType:    dns.TypeA,\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"*.host.com\",\n\t\tAnswer:  \"2.2.2.2\",\n\t\tType:    dns.TypeA,\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"*.sub.host.com\",\n\t\tAnswer:  \"3.3.3.3\",\n\t\tType:    dns.TypeA,\n\t\tEnabled: true,\n\t}}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\trequire.NoError(t, d.prepareRewrites(ctx))\n\n\ttestCases := []struct {\n\t\tname string\n\t\thost string\n\t\twant net.IP\n\t}{{\n\t\tname: \"exact_match\",\n\t\thost: \"host.com\",\n\t\twant: net.IP{1, 1, 1, 1},\n\t}, {\n\t\tname: \"l2_match\",\n\t\thost: \"sub.host.com\",\n\t\twant: net.IP{2, 2, 2, 2},\n\t}, {\n\t\tname: \"l3_match\",\n\t\thost: \"my.sub.host.com\",\n\t\twant: net.IP{3, 3, 3, 3},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := d.processRewrites(tc.host, dns.TypeA)\n\t\t\tassert.Equal(t, Rewritten, r.Reason)\n\t\t\trequire.Len(t, r.IPList, 1)\n\t\t})\n\t}\n}\n\nfunc TestRewritesExceptionCNAME(t *testing.T) {\n\td, _ := newForTest(t, nil, nil)\n\tt.Cleanup(d.Close)\n\t// Wildcard and exception for a sub-domain.\n\td.conf.Rewrites = []*LegacyRewrite{{\n\t\tDomain:  \"*.host.com\",\n\t\tAnswer:  \"2.2.2.2\",\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"sub.host.com\",\n\t\tAnswer:  \"sub.host.com\",\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"*.sub.host.com\",\n\t\tAnswer:  \"*.sub.host.com\",\n\t\tEnabled: true,\n\t}}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\trequire.NoError(t, d.prepareRewrites(ctx))\n\n\ttestCases := []struct {\n\t\tname string\n\t\thost string\n\t\twant netip.Addr\n\t}{{\n\t\tname: \"match_subdomain\",\n\t\thost: \"my.host.com\",\n\t\twant: netip.AddrFrom4([4]byte{2, 2, 2, 2}),\n\t}, {\n\t\tname: \"exception_cname\",\n\t\thost: \"sub.host.com\",\n\t\twant: netip.Addr{},\n\t}, {\n\t\tname: \"exception_wildcard\",\n\t\thost: \"my.sub.host.com\",\n\t\twant: netip.Addr{},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := d.processRewrites(tc.host, dns.TypeA)\n\t\t\tif tc.want == (netip.Addr{}) {\n\t\t\t\tassert.Equal(t, NotFilteredNotFound, r.Reason, \"got %s\", r.Reason)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, Rewritten, r.Reason)\n\t\t\trequire.Len(t, r.IPList, 1)\n\t\t\tassert.Equal(t, tc.want, r.IPList[0])\n\t\t})\n\t}\n}\n\nfunc TestRewritesExceptionIP(t *testing.T) {\n\td, _ := newForTest(t, nil, nil)\n\tt.Cleanup(d.Close)\n\t// Exception for AAAA record.\n\td.conf.Rewrites = []*LegacyRewrite{{\n\t\tDomain:  \"host.com\",\n\t\tAnswer:  \"1.2.3.4\",\n\t\tType:    dns.TypeA,\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"host.com\",\n\t\tAnswer:  \"AAAA\",\n\t\tType:    dns.TypeAAAA,\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"host2.com\",\n\t\tAnswer:  \"::1\",\n\t\tType:    dns.TypeAAAA,\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"host2.com\",\n\t\tAnswer:  \"A\",\n\t\tType:    dns.TypeA,\n\t\tEnabled: true,\n\t}, {\n\t\tDomain:  \"host3.com\",\n\t\tAnswer:  \"A\",\n\t\tType:    dns.TypeA,\n\t\tEnabled: true,\n\t}}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\trequire.NoError(t, d.prepareRewrites(ctx))\n\n\ttestCases := []struct {\n\t\tname       string\n\t\thost       string\n\t\twant       []netip.Addr\n\t\tdtyp       uint16\n\t\twantReason Reason\n\t}{{\n\t\tname:       \"match_A\",\n\t\thost:       \"host.com\",\n\t\twant:       []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},\n\t\tdtyp:       dns.TypeA,\n\t\twantReason: Rewritten,\n\t}, {\n\t\tname:       \"exception_AAAA_host.com\",\n\t\thost:       \"host.com\",\n\t\twant:       nil,\n\t\tdtyp:       dns.TypeAAAA,\n\t\twantReason: NotFilteredNotFound,\n\t}, {\n\t\tname:       \"exception_A_host2.com\",\n\t\thost:       \"host2.com\",\n\t\twant:       nil,\n\t\tdtyp:       dns.TypeA,\n\t\twantReason: NotFilteredNotFound,\n\t}, {\n\t\tname:       \"match_AAAA_host2.com\",\n\t\thost:       \"host2.com\",\n\t\twant:       []netip.Addr{netip.MustParseAddr(\"::1\")},\n\t\tdtyp:       dns.TypeAAAA,\n\t\twantReason: Rewritten,\n\t}, {\n\t\tname:       \"exception_A_host3.com\",\n\t\thost:       \"host3.com\",\n\t\twant:       nil,\n\t\tdtyp:       dns.TypeA,\n\t\twantReason: NotFilteredNotFound,\n\t}, {\n\t\tname:       \"match_AAAA_host3.com\",\n\t\thost:       \"host3.com\",\n\t\twant:       nil,\n\t\tdtyp:       dns.TypeAAAA,\n\t\twantReason: Rewritten,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.name != \"match_AAAA_host3.com\" {\n\t\t\t\tt.SkipNow()\n\t\t\t}\n\n\t\t\tr := d.processRewrites(tc.host, tc.dtyp)\n\t\t\tassert.Equal(t, tc.want, r.IPList)\n\t\t\tassert.Equal(t, tc.wantReason, r.Reason)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/engine.go",
    "content": "package rulelist\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/filterlist\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\n// Engine is a single DNS filter based on one or more rule lists.  This\n// structure contains the filtering engine combining several rule lists.\n//\n// TODO(a.garipov): Merge with [TextEngine] in some way?\ntype Engine struct {\n\t// logger is used to log the operation of the engine and its refreshes.\n\tlogger *slog.Logger\n\n\t// mu protects engine and storage.\n\t//\n\t// TODO(a.garipov): See if anything else should be protected.\n\tmu *sync.RWMutex\n\n\t// engine is the filtering engine.\n\tengine *urlfilter.DNSEngine\n\n\t// storage is the filtering-rule storage.  It is saved here to close it.\n\tstorage *filterlist.RuleStorage\n\n\t// name is the human-readable name of the engine.\n\tname string\n\n\t// filters is the data about rule filters in this engine.\n\tfilters []*Filter\n}\n\n// EngineConfig is the configuration for rule-list filtering engines created by\n// combining refreshable filters.\ntype EngineConfig struct {\n\t// Logger is used to log the operation of the engine.  It must not be nil.\n\tLogger *slog.Logger\n\n\t// name is the human-readable name of the engine; see [EngineNameAllow] and\n\t// similar constants.\n\tName string\n\n\t// Filters is the data about rule lists in this engine.  There must be no\n\t// other references to the items of this slice.  Each item must not be nil.\n\tFilters []*Filter\n}\n\n// NewEngine returns a new rule-list filtering engine.  The engine is not\n// refreshed, so a refresh should be performed before use.\nfunc NewEngine(c *EngineConfig) (e *Engine) {\n\treturn &Engine{\n\t\tlogger:  c.Logger,\n\t\tmu:      &sync.RWMutex{},\n\t\tname:    c.Name,\n\t\tfilters: c.Filters,\n\t}\n}\n\n// Close closes the underlying rule-list engine as well as the rule lists.\nfunc (e *Engine) Close() (err error) {\n\te.mu.Lock()\n\tdefer e.mu.Unlock()\n\n\tif e.storage == nil {\n\t\treturn nil\n\t}\n\n\terr = e.storage.Close()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"closing engine %q: %w\", e.name, err)\n\t}\n\n\treturn nil\n}\n\n// FilterRequest returns the result of filtering req using the DNS filtering\n// engine.\nfunc (e *Engine) FilterRequest(\n\treq *urlfilter.DNSRequest,\n) (res *urlfilter.DNSResult, hasMatched bool) {\n\treturn e.currentEngine().MatchRequest(req)\n}\n\n// currentEngine returns the current filtering engine.\nfunc (e *Engine) currentEngine() (engine *urlfilter.DNSEngine) {\n\te.mu.RLock()\n\tdefer e.mu.RUnlock()\n\n\treturn e.engine\n}\n\n// Refresh updates all rule lists in e.  ctx is used for cancellation.\n// parseBuf, cli, cacheDir, and maxSize are used for updates of rule-list\n// filters; see [Filter.Refresh].\n//\n// TODO(a.garipov): Unexport and test in an internal test or through engine\n// tests.\nfunc (e *Engine) Refresh(\n\tctx context.Context,\n\tparseBuf []byte,\n\tcli *http.Client,\n\tcacheDir string,\n\tmaxSize datasize.ByteSize,\n) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"updating engine %q: %w\", e.name) }()\n\n\tvar filtersToRefresh []*Filter\n\tfor _, f := range e.filters {\n\t\tif f.enabled {\n\t\t\tfiltersToRefresh = append(filtersToRefresh, f)\n\t\t}\n\t}\n\n\tif len(filtersToRefresh) == 0 {\n\t\te.logger.InfoContext(ctx, \"updating: no rule-list filters\")\n\n\t\treturn nil\n\t}\n\n\tengRefr := &engineRefresh{\n\t\tlogger:   e.logger,\n\t\thttpCli:  cli,\n\t\tcacheDir: cacheDir,\n\t\tparseBuf: parseBuf,\n\t\tmaxSize:  maxSize,\n\t}\n\n\truleLists, errs := engRefr.process(ctx, filtersToRefresh)\n\tif isOneTimeoutError(errs) {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tstorage, err := filterlist.NewRuleStorage(ruleLists)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"creating rule storage: %w\", err))\n\n\t\treturn errors.Join(errs...)\n\t}\n\n\te.resetStorage(ctx, storage)\n\n\treturn errors.Join(errs...)\n}\n\n// resetStorage sets e.storage and e.engine and closes the previous storage.\n// Errors from closing the previous storage are logged.\nfunc (e *Engine) resetStorage(ctx context.Context, storage *filterlist.RuleStorage) {\n\te.mu.Lock()\n\tdefer e.mu.Unlock()\n\n\tprevStorage := e.storage\n\te.storage, e.engine = storage, urlfilter.NewDNSEngine(storage)\n\n\tif prevStorage == nil {\n\t\treturn\n\t}\n\n\terr := prevStorage.Close()\n\tif err != nil {\n\t\te.logger.WarnContext(ctx, \"closing old storage\", slogutil.KeyError, err)\n\t}\n}\n\n// isOneTimeoutError returns true if the sole error in errs is either\n// [context.Canceled] or [context.DeadlineExceeded].\nfunc isOneTimeoutError(errs []error) (ok bool) {\n\tif len(errs) != 1 {\n\t\treturn false\n\t}\n\n\terr := errs[0]\n\n\treturn errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)\n}\n\n// engineRefresh represents a single ongoing engine refresh.\ntype engineRefresh struct {\n\tlogger   *slog.Logger\n\thttpCli  *http.Client\n\tcacheDir string\n\tparseBuf []byte\n\tmaxSize  datasize.ByteSize\n}\n\n// process runs updates of all given rule-list filters.  All errors are logged\n// as they appear, since the update can take a significant amount of time.\n// errs contains all errors that happened during the update, unless the context\n// is canceled or its deadline is reached, in which case errs will only contain\n// a single timeout error.\n//\n// TODO(a.garipov): Think of a better way to communicate the timeout condition?\nfunc (r *engineRefresh) process(\n\tctx context.Context,\n\tfilters []*Filter,\n) (ruleLists []filterlist.Interface, errs []error) {\n\truleLists = make([]filterlist.Interface, 0, len(filters))\n\tfor i, f := range filters {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, []error{fmt.Errorf(\"timeout after updating %d filters: %w\", i, ctx.Err())}\n\t\tdefault:\n\t\t\t// Go on.\n\t\t}\n\n\t\terr := r.processFilter(ctx, f)\n\t\tif err == nil {\n\t\t\truleLists = append(ruleLists, f.ruleList)\n\n\t\t\tcontinue\n\t\t}\n\n\t\terrs = append(errs, err)\n\n\t\t// Also log immediately, since the update can take a lot of time.\n\t\tr.logger.ErrorContext(\n\t\t\tctx,\n\t\t\t\"updating rule list\",\n\t\t\t\"uid\", f.uid,\n\t\t\t\"url\", f.url,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\t}\n\n\treturn ruleLists, errs\n}\n\n// processFilter runs an update of a single rule-list filter.\nfunc (r *engineRefresh) processFilter(ctx context.Context, f *Filter) (err error) {\n\tprevChecksum := f.checksum\n\tparseRes, err := f.Refresh(ctx, r.parseBuf, r.httpCli, r.cacheDir, r.maxSize)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"updating %s: %w\", f.uid, err)\n\t}\n\n\tif prevChecksum == parseRes.Checksum {\n\t\tr.logger.InfoContext(ctx, \"no change in filter\", \"uid\", f.uid)\n\n\t\treturn nil\n\t}\n\n\tr.logger.InfoContext(\n\t\tctx,\n\t\t\"filter updated\",\n\t\t\"uid\", f.uid,\n\t\t\"bytes\", parseRes.BytesWritten,\n\t\t\"rules\", parseRes.RulesCount,\n\t)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/engine_test.go",
    "content": "package rulelist_test\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEngine_Refresh(t *testing.T) {\n\tt.Parallel()\n\n\tcacheDir := t.TempDir()\n\n\tfileURL, srvURL := newFilterLocations(t, cacheDir, testRuleTextBlocked, testRuleTextBlocked2)\n\n\tfileFlt := newFilter(t, fileURL, \"File Filter\")\n\thttpFlt := newFilter(t, srvURL, \"HTTP Filter\")\n\n\teng := rulelist.NewEngine(&rulelist.EngineConfig{\n\t\tLogger:  slogutil.NewDiscardLogger(),\n\t\tName:    \"Engine\",\n\t\tFilters: []*rulelist.Filter{fileFlt, httpFlt},\n\t})\n\trequire.NotNil(t, eng)\n\ttestutil.CleanupAndRequireSuccess(t, eng.Close)\n\n\tbuf := make([]byte, rulelist.DefaultRuleBufSize)\n\tcli := &http.Client{\n\t\tTimeout: testTimeout,\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\terr := eng.Refresh(ctx, buf, cli, cacheDir, rulelist.DefaultMaxRuleListSize)\n\trequire.NoError(t, err)\n\n\tfltReq := &urlfilter.DNSRequest{\n\t\tHostname: \"blocked.example\",\n\t\tAnswer:   false,\n\t\tDNSType:  dns.TypeA,\n\t}\n\n\tfltRes, hasMatched := eng.FilterRequest(fltReq)\n\tassert.True(t, hasMatched)\n\n\trequire.NotNil(t, fltRes)\n\n\tfltReq = &urlfilter.DNSRequest{\n\t\tHostname: \"blocked-2.example\",\n\t\tAnswer:   false,\n\t\tDNSType:  dns.TypeA,\n\t}\n\n\tfltRes, hasMatched = eng.FilterRequest(fltReq)\n\tassert.True(t, hasMatched)\n\n\trequire.NotNil(t, fltRes)\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/error.go",
    "content": "package rulelist\n\nimport \"github.com/AdguardTeam/golibs/errors\"\n\n// ErrHTML is returned by [Parser.Parse] if the data is likely to be HTML.\n//\n// TODO(a.garipov): This error is currently returned to the UI.  Stop that and\n// make it all-lowercase.\nconst ErrHTML errors.Error = \"data is HTML, not plain text\"\n"
  },
  {
    "path": "internal/filtering/rulelist/filter.go",
    "content": "package rulelist\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/AdguardTeam/urlfilter/filterlist\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\n// Filter contains information about a single rule-list filter.\n//\n// TODO(a.garipov): Use.\ntype Filter struct {\n\t// url is the URL of this rule list.  Supported schemes are:\n\t//   - http\n\t//   - https\n\t//   - file\n\turl *url.URL\n\n\t// ruleList is the last successfully compiled [filterlist.RuleList].\n\truleList filterlist.Interface\n\n\t// updated is the time of the last successful update.\n\tupdated time.Time\n\n\t// name is the human-readable name of this rule-list filter.\n\tname string\n\n\t// uid is the unique ID of this rule-list filter.\n\tuid UID\n\n\t// urlFilterID is used for working with package urlfilter.\n\turlFilterID rules.ListID\n\n\t// rulesCount contains the number of rules in this rule-list filter.\n\trulesCount int\n\n\t// checksum is a CRC32 hash used to quickly check if the rules within a list\n\t// file have changed.\n\tchecksum uint32\n\n\t// enabled, if true, means that this rule-list filter is used for filtering.\n\tenabled bool\n}\n\n// FilterConfig contains the configuration for a [Filter].\ntype FilterConfig struct {\n\t// URL is the URL of this rule-list filter.  Supported schemes are:\n\t//   - http\n\t//   - https\n\t//   - file\n\tURL *url.URL\n\n\t// Name is the human-readable name of this rule-list filter.  If not set, it\n\t// is either taken from the rule-list data or generated synthetically from\n\t// the UID.\n\tName string\n\n\t// UID is the unique ID of this rule-list filter.\n\tUID UID\n\n\t// URLFilterID is used for working with package urlfilter.\n\tURLFilterID rules.ListID\n\n\t// Enabled, if true, means that this rule-list filter is used for filtering.\n\tEnabled bool\n}\n\n// NewFilter creates a new rule-list filter.  The filter is not refreshed, so a\n// refresh should be performed before use.\nfunc NewFilter(c *FilterConfig) (f *Filter, err error) {\n\tif c.URL == nil {\n\t\treturn nil, errors.Error(\"no url\")\n\t}\n\n\tswitch s := c.URL.Scheme; s {\n\tcase \"http\", \"https\", \"file\":\n\t\t// Go on.\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"bad url scheme: %q\", s)\n\t}\n\n\treturn &Filter{\n\t\turl:         c.URL,\n\t\tname:        c.Name,\n\t\tuid:         c.UID,\n\t\turlFilterID: c.URLFilterID,\n\t\tenabled:     c.Enabled,\n\t}, nil\n}\n\n// Refresh updates the data in the rule-list filter.  parseBuf is the initial\n// buffer used to parse information from the data.  cli and maxSize are only\n// used when f is a URL-based list.\n//\n// TODO(a.garipov): Unexport and test in an internal test or through engine\n// tests.\n//\n// TODO(a.garipov): Consider not returning parseRes.\nfunc (f *Filter) Refresh(\n\tctx context.Context,\n\tparseBuf []byte,\n\tcli *http.Client,\n\tcacheDir string,\n\tmaxSize datasize.ByteSize,\n) (parseRes *ParseResult, err error) {\n\tcachePath := filepath.Join(cacheDir, f.uid.String()+\".txt\")\n\n\tswitch s := f.url.Scheme; s {\n\tcase \"http\", \"https\":\n\t\tparseRes, err = f.setFromHTTP(ctx, parseBuf, cli, cachePath, maxSize.Bytes())\n\tcase \"file\":\n\t\tparseRes, err = f.setFromFile(parseBuf, f.url.Path, cachePath)\n\tdefault:\n\t\t// Since the URL has been prevalidated in New, consider this a\n\t\t// programmer error.\n\t\tpanic(fmt.Errorf(\"bad url scheme: %q\", s))\n\t}\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tif f.checksum != parseRes.Checksum {\n\t\tf.checksum = parseRes.Checksum\n\t\tf.rulesCount = parseRes.RulesCount\n\t\tf.setName(parseRes.Title)\n\t\tf.updated = time.Now()\n\t}\n\n\treturn parseRes, nil\n}\n\n// setFromHTTP sets the rule-list filter's data from its URL.  It also caches\n// the data into a file.\nfunc (f *Filter) setFromHTTP(\n\tctx context.Context,\n\tparseBuf []byte,\n\tcli *http.Client,\n\tcachePath string,\n\tmaxSize uint64,\n) (parseRes *ParseResult, err error) {\n\tdefer func() { err = errors.Annotate(err, \"setting from http: %w\") }()\n\n\tdata, parseRes, err := f.readFromHTTP(ctx, parseBuf, cli, cachePath, maxSize)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tf.ruleList = filterlist.NewBytes(&filterlist.BytesConfig{\n\t\tID:             f.urlFilterID,\n\t\tRulesText:      data,\n\t\tIgnoreCosmetic: true,\n\t})\n\n\treturn parseRes, nil\n}\n\n// readFromHTTP reads the data from the rule-list filter's URL into the cache\n// file as well as returns it as a string.  The data is filtered through a\n// parser and so is free from comments, unnecessary whitespace, etc.\nfunc (f *Filter) readFromHTTP(\n\tctx context.Context,\n\tparseBuf []byte,\n\tcli *http.Client,\n\tcachePath string,\n\tmaxSize uint64,\n) (data []byte, parseRes *ParseResult, err error) {\n\turlStr := f.url.String()\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"making request for http url %q: %w\", urlStr, err)\n\t}\n\n\t// #nosec G704 -- Trust the URL explicitly given by the user.\n\tresp, err := cli.Do(req)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"requesting from http url: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()\n\n\t// TODO(a.garipov): Use [agdhttp.CheckStatus] when it's moved to golibs.\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, nil, fmt.Errorf(\"got status code %d, want %d\", resp.StatusCode, http.StatusOK)\n\t}\n\n\tfltFile, err := aghrenameio.NewPendingFile(cachePath, aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"creating temp file: %w\", err)\n\t}\n\tdefer func() { err = aghrenameio.WithDeferredCleanup(err, fltFile) }()\n\n\tbuf := &bytes.Buffer{}\n\tmw := io.MultiWriter(buf, fltFile)\n\n\tparser := NewParser()\n\thttpBody := ioutil.LimitReader(resp.Body, maxSize)\n\tparseRes, err = parser.Parse(mw, httpBody, parseBuf)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"parsing response from http url %q: %w\", urlStr, err)\n\t}\n\n\treturn buf.Bytes(), parseRes, nil\n}\n\n// setName sets the title using either the already-present name, the given title\n// from the rule-list data, or a synthetic name.\nfunc (f *Filter) setName(title string) {\n\tif f.name != \"\" {\n\t\treturn\n\t}\n\n\tif title != \"\" {\n\t\tf.name = title\n\n\t\treturn\n\t}\n\n\tf.name = fmt.Sprintf(\"List %s\", f.uid)\n}\n\n// setFromFile sets the rule-list filter's data from a file path.  It also\n// caches the data into a file.\n//\n// TODO(a.garipov): Retest on Windows once rule-list updater is committed.  See\n// if calling Close is necessary here.\nfunc (f *Filter) setFromFile(\n\tparseBuf []byte,\n\tfilePath string,\n\tcachePath string,\n) (parseRes *ParseResult, err error) {\n\tdefer func() { err = errors.Annotate(err, \"setting from file: %w\") }()\n\n\tparseRes, err = parseIntoCache(parseBuf, filePath, cachePath)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\terr = f.Close()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"closing old rule list: %w\", err)\n\t}\n\n\trl, err := filterlist.NewFile(&filterlist.FileConfig{\n\t\tID:             f.urlFilterID,\n\t\tPath:           cachePath,\n\t\tIgnoreCosmetic: true,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"opening new rule list: %w\", err)\n\t}\n\n\tf.ruleList = rl\n\n\treturn parseRes, nil\n}\n\n// parseIntoCache copies the relevant the data from filePath into cachePath\n// while also parsing it.\nfunc parseIntoCache(\n\tparseBuf []byte,\n\tfilePath string,\n\tcachePath string,\n) (parseRes *ParseResult, err error) {\n\ttmpFile, err := aghrenameio.NewPendingFile(cachePath, aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating temp file: %w\", err)\n\t}\n\tdefer func() { err = aghrenameio.WithDeferredCleanup(err, tmpFile) }()\n\n\t// #nosec G304 -- Assume that cachePath is always cacheDir joined with a\n\t// uid using [filepath.Join].\n\tf, err := os.Open(filePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"opening src file: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, f.Close()) }()\n\n\tparser := NewParser()\n\tparseRes, err = parser.Parse(tmpFile, f, parseBuf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"copying src file: %w\", err)\n\t}\n\n\treturn parseRes, nil\n}\n\n// Close closes the underlying rule list.\nfunc (f *Filter) Close() (err error) {\n\tif f.ruleList == nil {\n\t\treturn nil\n\t}\n\n\treturn f.ruleList.Close()\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/filter_test.go",
    "content": "package rulelist_test\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFilter_Refresh(t *testing.T) {\n\tt.Parallel()\n\n\tcacheDir := t.TempDir()\n\n\tconst fltData = testRuleTextTitle + testRuleTextBlocked\n\tfileURL, srvURL := newFilterLocations(t, cacheDir, fltData, fltData)\n\n\ttestCases := []struct {\n\t\turl           *url.URL\n\t\tname          string\n\t\twantNewErrMsg string\n\t}{{\n\t\turl:           nil,\n\t\tname:          \"nil_url\",\n\t\twantNewErrMsg: \"no url\",\n\t}, {\n\t\turl: &url.URL{\n\t\t\tScheme: \"ftp\",\n\t\t},\n\t\tname:          \"bad_scheme\",\n\t\twantNewErrMsg: `bad url scheme: \"ftp\"`,\n\t}, {\n\t\tname: \"file\",\n\t\turl: &url.URL{\n\t\t\tScheme: urlutil.SchemeFile,\n\t\t\tPath:   fileURL.Path,\n\t\t},\n\t\twantNewErrMsg: \"\",\n\t}, {\n\t\tname:          \"http\",\n\t\turl:           srvURL,\n\t\twantNewErrMsg: \"\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tuid := rulelist.MustNewUID()\n\t\t\tf, err := rulelist.NewFilter(&rulelist.FilterConfig{\n\t\t\t\tURL:         tc.url,\n\t\t\t\tName:        tc.name,\n\t\t\t\tUID:         uid,\n\t\t\t\tURLFilterID: testURLFilterID,\n\t\t\t\tEnabled:     true,\n\t\t\t})\n\t\t\tif tc.wantNewErrMsg != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.wantNewErrMsg)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttestutil.CleanupAndRequireSuccess(t, f.Close)\n\n\t\t\trequire.NotNil(t, f)\n\n\t\t\tbuf := make([]byte, rulelist.DefaultRuleBufSize)\n\t\t\tcli := &http.Client{\n\t\t\t\tTimeout: testTimeout,\n\t\t\t}\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tres, err := f.Refresh(ctx, buf, cli, cacheDir, rulelist.DefaultMaxRuleListSize)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, testTitle, res.Title)\n\t\t\tassert.Equal(t, len(testRuleTextBlocked), res.BytesWritten)\n\t\t\tassert.Equal(t, 1, res.RulesCount)\n\n\t\t\t// Check that the cached file exists.\n\t\t\t_, err = os.Stat(filepath.Join(cacheDir, uid.String()+\".txt\"))\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/parser.go",
    "content": "package rulelist\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"io\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// Parser is a filtering-rule parser that collects data, such as the checksum\n// and the title, as well as counts rules and removes comments.\ntype Parser struct {\n\ttitle      string\n\trulesCount int\n\twritten    int\n\tchecksum   uint32\n\ttitleFound bool\n}\n\n// NewParser returns a new filtering-rule parser.\nfunc NewParser() (p *Parser) {\n\treturn &Parser{}\n}\n\n// ParseResult contains information about the results of parsing a\n// filtering-rule list by [Parser.Parse].\ntype ParseResult struct {\n\t// Title is the title contained within the filtering-rule list, if any.\n\tTitle string\n\n\t// RulesCount is the number of rules in the list.  It excludes empty lines\n\t// and comments.\n\tRulesCount int\n\n\t// BytesWritten is the number of bytes written to dst.\n\tBytesWritten int\n\n\t// Checksum is the CRC-32 checksum of the rules content.  That is, excluding\n\t// empty lines and comments.\n\tChecksum uint32\n}\n\n// Parse parses data from src into dst using buf during parsing.  r is never\n// nil.\nfunc (p *Parser) Parse(dst io.Writer, src io.Reader, buf []byte) (r *ParseResult, err error) {\n\ts := bufio.NewScanner(src)\n\n\t// Don't use [DefaultRuleBufSize] as the maximum size, since some\n\t// filtering-rule lists compressed by e.g. HostlistsCompiler can have very\n\t// large lines.  The buffer optimization still works for the more common\n\t// case of reasonably-sized lines.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/6003.\n\ts.Buffer(buf, bufio.MaxScanTokenSize)\n\n\t// Use a one-based index for lines and columns, since these errors end up in\n\t// the frontend, and users are more familiar with one-based line and column\n\t// indexes.\n\tlineNum := 1\n\tfor s.Scan() {\n\t\tvar n int\n\t\tn, err = p.processLine(dst, s.Bytes(), lineNum)\n\t\tp.written += n\n\t\tif err != nil {\n\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\treturn p.result(), err\n\t\t}\n\n\t\tlineNum++\n\t}\n\n\tr = p.result()\n\terr = s.Err()\n\n\treturn r, errors.Annotate(err, \"scanning filter contents: %w\")\n}\n\n// result returns the current parsing result.\nfunc (p *Parser) result() (r *ParseResult) {\n\treturn &ParseResult{\n\t\tTitle:        p.title,\n\t\tRulesCount:   p.rulesCount,\n\t\tBytesWritten: p.written,\n\t\tChecksum:     p.checksum,\n\t}\n}\n\n// processLine processes a single line.  It may write to dst, and if it does, n\n// is the number of bytes written.\nfunc (p *Parser) processLine(dst io.Writer, line []byte, lineNum int) (n int, err error) {\n\ttrimmed := bytes.TrimSpace(line)\n\tif p.written == 0 && isHTMLLine(trimmed) {\n\t\treturn 0, ErrHTML\n\t}\n\n\tbadIdx, isRule := 0, false\n\tif p.titleFound {\n\t\tbadIdx, isRule = parseLine(trimmed)\n\t} else {\n\t\tbadIdx, isRule = p.parseLineTitle(trimmed)\n\t}\n\tif badIdx != -1 {\n\t\treturn 0, fmt.Errorf(\n\t\t\t\"line %d: character %d: likely binary character %q\",\n\t\t\tlineNum,\n\t\t\tbadIdx+bytes.Index(line, trimmed)+1,\n\t\t\ttrimmed[badIdx],\n\t\t)\n\t}\n\n\tif !isRule {\n\t\treturn 0, nil\n\t}\n\n\tp.rulesCount++\n\tp.checksum = crc32.Update(p.checksum, crc32.IEEETable, trimmed)\n\n\t// Assume that there is generally enough space in the buffer to add a\n\t// newline.\n\tn, err = dst.Write(append(trimmed, '\\n'))\n\n\treturn n, errors.Annotate(err, \"writing rule line: %w\")\n}\n\n// isHTMLLine returns true if line is likely an HTML line.  line is assumed to\n// be trimmed of whitespace characters.\nfunc isHTMLLine(line []byte) (isHTML bool) {\n\treturn hasPrefixFold(line, []byte(\"<html\")) || hasPrefixFold(line, []byte(\"<!doctype\"))\n}\n\n// hasPrefixFold is a simple, best-effort prefix matcher.  It may return\n// incorrect results for some non-ASCII characters.\nfunc hasPrefixFold(b, prefix []byte) (ok bool) {\n\tl := len(prefix)\n\n\treturn len(b) >= l && bytes.EqualFold(b[:l], prefix)\n}\n\n// parseLine returns true if the parsed line is a filtering rule.  line is\n// assumed to be trimmed of whitespace characters.  badIdx is the index of the\n// first character that may indicate that this is a binary file, or -1 if none.\n//\n// A line is considered a rule if it's not empty, not a comment, and contains\n// only printable characters.\nfunc parseLine(line []byte) (badIdx int, isRule bool) {\n\tif len(line) == 0 || line[0] == '#' || line[0] == '!' {\n\t\treturn -1, false\n\t}\n\n\tbadIdx = slices.IndexFunc(line, likelyBinary)\n\n\treturn badIdx, badIdx == -1\n}\n\n// likelyBinary returns true if b is likely to be a byte from a binary file.\nfunc likelyBinary(b byte) (ok bool) {\n\treturn (b < ' ' || b == 0x7f) && b != '\\n' && b != '\\r' && b != '\\t'\n}\n\n// parseLineTitle is like [parseLine] but additionally looks for a title.  line\n// is assumed to be trimmed of whitespace characters.\nfunc (p *Parser) parseLineTitle(line []byte) (badIdx int, isRule bool) {\n\tif len(line) == 0 || line[0] == '#' {\n\t\treturn -1, false\n\t}\n\n\tif line[0] != '!' {\n\t\tbadIdx = slices.IndexFunc(line, likelyBinary)\n\n\t\treturn badIdx, badIdx == -1\n\t}\n\n\tconst titlePattern = \"! Title: \"\n\tif !bytes.HasPrefix(line, []byte(titlePattern)) {\n\t\treturn -1, false\n\t}\n\n\ttitle := bytes.TrimSpace(line[len(titlePattern):])\n\tif title != nil {\n\t\t// Note that title can be a non-nil empty slice.  Consider that normal\n\t\t// and just stop looking for other titles.\n\t\tp.title = string(title)\n\t\tp.titleFound = true\n\t}\n\n\treturn -1, false\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/parser_test.go",
    "content": "package rulelist_test\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/fakeio\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParser_Parse(t *testing.T) {\n\tt.Parallel()\n\n\tlongRule := strings.Repeat(\"a\", rulelist.DefaultRuleBufSize+1) + \"\\n\"\n\ttooLongRule := strings.Repeat(\"a\", bufio.MaxScanTokenSize+1) + \"\\n\"\n\n\ttestCases := []struct {\n\t\tname         string\n\t\tin           string\n\t\twantDst      string\n\t\twantErrMsg   string\n\t\twantTitle    string\n\t\twantRulesNum int\n\t\twantWritten  int\n\t}{{\n\t\tname:         \"empty\",\n\t\tin:           \"\",\n\t\twantDst:      \"\",\n\t\twantErrMsg:   \"\",\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 0,\n\t\twantWritten:  0,\n\t}, {\n\t\tname:         \"html\",\n\t\tin:           testRuleTextHTML,\n\t\twantErrMsg:   rulelist.ErrHTML.Error(),\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 0,\n\t\twantWritten:  0,\n\t}, {\n\t\tname: \"comments\",\n\t\tin: \"# Comment 1\\n\" +\n\t\t\t\"! Comment 2\\n\",\n\t\twantErrMsg:   \"\",\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 0,\n\t\twantWritten:  0,\n\t}, {}, {\n\t\tname:         \"rule\",\n\t\tin:           testRuleTextBlocked,\n\t\twantDst:      testRuleTextBlocked,\n\t\twantErrMsg:   \"\",\n\t\twantRulesNum: 1,\n\t\twantTitle:    \"\",\n\t\twantWritten:  len(testRuleTextBlocked),\n\t}, {\n\t\tname:         \"html_in_rule\",\n\t\tin:           testRuleTextBlocked + testRuleTextHTML,\n\t\twantDst:      testRuleTextBlocked + testRuleTextHTML,\n\t\twantErrMsg:   \"\",\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 2,\n\t\twantWritten:  len(testRuleTextBlocked) + len(testRuleTextHTML),\n\t}, {\n\t\tname: \"title\",\n\t\tin: testRuleTextTitle +\n\t\t\t\"! Title: Bad, Ignored Title\\n\" +\n\t\t\ttestRuleTextBlocked,\n\t\twantDst:      testRuleTextBlocked,\n\t\twantErrMsg:   \"\",\n\t\twantTitle:    testTitle,\n\t\twantRulesNum: 1,\n\t\twantWritten:  len(testRuleTextBlocked),\n\t}, {\n\t\tname:         \"cosmetic_with_zwnj\",\n\t\tin:           testRuleTextCosmetic,\n\t\twantDst:      testRuleTextCosmetic,\n\t\twantErrMsg:   \"\",\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 1,\n\t\twantWritten:  len(testRuleTextCosmetic),\n\t}, {\n\t\tname: \"bad_char\",\n\t\tin: testRuleTextTitle +\n\t\t\ttestRuleTextBlocked +\n\t\t\t\">>>\\x7F<<<\",\n\t\twantDst: testRuleTextBlocked,\n\t\twantErrMsg: \"line 3: \" +\n\t\t\t\"character 4: \" +\n\t\t\t\"likely binary character '\\\\x7f'\",\n\t\twantTitle:    testTitle,\n\t\twantRulesNum: 1,\n\t\twantWritten:  len(testRuleTextBlocked),\n\t}, {\n\t\tname:         \"too_long\",\n\t\tin:           tooLongRule,\n\t\twantDst:      \"\",\n\t\twantErrMsg:   \"scanning filter contents: bufio.Scanner: token too long\",\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 0,\n\t\twantWritten:  0,\n\t}, {\n\t\tname:         \"longer_than_default\",\n\t\tin:           longRule,\n\t\twantDst:      longRule,\n\t\twantErrMsg:   \"\",\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 1,\n\t\twantWritten:  len(longRule),\n\t}, {\n\t\tname:         \"bad_tab_and_comment\",\n\t\tin:           testRuleTextBadTab,\n\t\twantDst:      testRuleTextBadTab,\n\t\twantErrMsg:   \"\",\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 1,\n\t\twantWritten:  len(testRuleTextBadTab),\n\t}, {\n\t\tname:         \"etc_hosts_tab_and_comment\",\n\t\tin:           testRuleTextEtcHostsTab,\n\t\twantDst:      testRuleTextEtcHostsTab,\n\t\twantErrMsg:   \"\",\n\t\twantTitle:    \"\",\n\t\twantRulesNum: 1,\n\t\twantWritten:  len(testRuleTextEtcHostsTab),\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdst := &bytes.Buffer{}\n\t\t\tbuf := make([]byte, rulelist.DefaultRuleBufSize)\n\n\t\t\tp := rulelist.NewParser()\n\t\t\tr, err := p.Parse(dst, strings.NewReader(tc.in), buf)\n\t\t\trequire.NotNil(t, r)\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t\tassert.Equal(t, tc.wantDst, dst.String())\n\t\t\tassert.Equal(t, tc.wantTitle, r.Title)\n\t\t\tassert.Equal(t, tc.wantRulesNum, r.RulesCount)\n\t\t\tassert.Equal(t, tc.wantWritten, r.BytesWritten)\n\n\t\t\tif tc.wantWritten > 0 {\n\t\t\t\tassert.NotZero(t, r.Checksum)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParser_Parse_writeError(t *testing.T) {\n\tt.Parallel()\n\n\tdst := &fakeio.Writer{\n\t\tOnWrite: func(b []byte) (n int, err error) {\n\t\t\treturn 1, errors.Error(\"test error\")\n\t\t},\n\t}\n\tbuf := make([]byte, rulelist.DefaultRuleBufSize)\n\n\tp := rulelist.NewParser()\n\tr, err := p.Parse(dst, strings.NewReader(testRuleTextBlocked), buf)\n\trequire.NotNil(t, r)\n\n\ttestutil.AssertErrorMsg(t, \"writing rule line: test error\", err)\n\tassert.Equal(t, 1, r.BytesWritten)\n}\n\nfunc TestParser_Parse_checksums(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\twithoutComments = testRuleTextBlocked\n\t\twithComments    = \"! Some comment.\\n\" +\n\t\t\t\"  \" + testRuleTextBlocked +\n\t\t\t\"# Another comment.\\n\"\n\t)\n\n\tbuf := make([]byte, rulelist.DefaultRuleBufSize)\n\n\tp := rulelist.NewParser()\n\tr, err := p.Parse(&bytes.Buffer{}, strings.NewReader(withoutComments), buf)\n\trequire.NotNil(t, r)\n\trequire.NoError(t, err)\n\n\tgotWithoutComments := r.Checksum\n\n\tp = rulelist.NewParser()\n\n\tr, err = p.Parse(&bytes.Buffer{}, strings.NewReader(withComments), buf)\n\trequire.NotNil(t, r)\n\trequire.NoError(t, err)\n\n\tgotWithComments := r.Checksum\n\tassert.Equal(t, gotWithoutComments, gotWithComments)\n}\n\nfunc BenchmarkParser_Parse(b *testing.B) {\n\tdst := &bytes.Buffer{}\n\tsrc := strings.NewReader(strings.Repeat(testRuleTextBlocked, 1000))\n\tbuf := make([]byte, rulelist.DefaultRuleBufSize)\n\tp := rulelist.NewParser()\n\n\tvar res *rulelist.ParseResult\n\tvar err error\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres, err = p.Parse(dst, src, buf)\n\t\tdst.Reset()\n\t}\n\n\trequire.NoError(b, err)\n\trequire.NotNil(b, res)\n\n\t// Most recent results:\n\t//\n\t//\tgoos: darwin\n\t//\tgoarch: amd64\n\t//\tpkg: github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\n\t//\tcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz\n\t//\tBenchmarkParser_Parse-12    \t19635926\t        53.70 ns/op\t      48 B/op\t       1 allocs/op\n}\n\nfunc FuzzParser_Parse(f *testing.F) {\n\tconst n = 64\n\n\ttestCases := []string{\n\t\t\"\",\n\t\t\"# Comment\",\n\t\t\"! Comment\",\n\t\t\"! Title \",\n\t\t\"! Title XXX\",\n\t\ttestRuleTextBadTab,\n\t\ttestRuleTextBlocked,\n\t\ttestRuleTextCosmetic,\n\t\ttestRuleTextEtcHostsTab,\n\t\ttestRuleTextHTML,\n\t\t\"1.2.3.4\",\n\t\t\"1.2.3.4 etc-hosts.example\",\n\t\t\">>>\\x00<<<\",\n\t\t\">>>\\x7F<<<\",\n\t\tstrings.Repeat(\"a\", rulelist.DefaultRuleBufSize+1),\n\t\tstrings.Repeat(\"a\", bufio.MaxScanTokenSize+1),\n\t}\n\n\tfor _, tc := range testCases {\n\t\tf.Add(tc)\n\t}\n\n\tbuf := make([]byte, n)\n\n\tf.Fuzz(func(t *testing.T, input string) {\n\t\trequire.Eventually(t, func() (ok bool) {\n\t\t\tdst := &bytes.Buffer{}\n\t\t\tsrc := strings.NewReader(input)\n\n\t\t\tp := rulelist.NewParser()\n\t\t\tr, _ := p.Parse(dst, src, buf)\n\t\t\trequire.NotNil(t, r)\n\n\t\t\treturn true\n\t\t}, testTimeout, testTimeout/100)\n\t})\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/rulelist.go",
    "content": "// Package rulelist contains the implementation of the standard rule-list\n// filter that wraps an urlfilter filtering-engine.\n//\n// TODO(a.garipov): Add a new update worker.\npackage rulelist\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/c2h5oh/datasize\"\n\t\"github.com/google/uuid\"\n)\n\n// DefaultRuleBufSize is the default length of a buffer used to read a line with\n// a filtering rule, in bytes.\n//\n// TODO(a.garipov): Consider using [datasize.ByteSize].  It is currently only\n// used as an int.\nconst DefaultRuleBufSize = 1024\n\n// DefaultMaxRuleListSize is the default maximum filtering-rule list size.\nconst DefaultMaxRuleListSize = 64 * datasize.MB\n\n// APIID is the type for the rule-list IDs used in the HTTP API.\ntype APIID int64\n\n// The IDs of built-in filter lists for the HTTP API.\n//\n// NOTE:  Do not change without the need for it and keep in sync with\n// client/src/helpers/constants.ts.\nconst (\n\tAPIIDCustom          APIID = 0\n\tAPIIDEtcHosts        APIID = -1\n\tAPIIDBlockedService  APIID = -2\n\tAPIIDParentalControl APIID = -3\n\tAPIIDSafeBrowsing    APIID = -4\n\tAPIIDSafeSearch      APIID = -5\n)\n\n// The IDs of built-in filter lists.  The IDs for the blocked-service and the\n// safe-search filters are chosen so that they equal to their [APIID]\n// counterparts when converted to it.\n//\n// NOTE:  Keep in sync with [APIIDCustom] etc.\n//\n// TODO(d.kolyshev): Add URLFilterIDLegacyRewrite here and to the UI.\nconst (\n\tIDCustom         rules.ListID = rules.ListID(APIIDCustom)\n\tIDBlockedService rules.ListID = math.MaxUint64 - rules.ListID(-APIIDBlockedService) + 1\n\tIDSafeSearch     rules.ListID = math.MaxUint64 - rules.ListID(-APIIDSafeSearch) + 1\n)\n\n// UID is the type for the unique IDs of filtering-rule lists.\ntype UID uuid.UUID\n\n// NewUID returns a new filtering-rule list UID.  Any error returned is an error\n// from the cryptographic randomness reader.\nfunc NewUID() (uid UID, err error) {\n\tuuidv7, err := uuid.NewV7()\n\n\treturn UID(uuidv7), err\n}\n\n// MustNewUID is a wrapper around [NewUID] that panics if there is an error.\nfunc MustNewUID() (uid UID) {\n\tuid, err := NewUID()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unexpected uuidv7 error: %w\", err))\n\t}\n\n\treturn uid\n}\n\n// type check\nvar _ fmt.Stringer = UID{}\n\n// String implements the [fmt.Stringer] interface for UID.\nfunc (id UID) String() (s string) {\n\treturn uuid.UUID(id).String()\n}\n\n// Common engine names.\nconst (\n\tEngineNameAllow  = \"allow\"\n\tEngineNameBlock  = \"block\"\n\tEngineNameCustom = \"custom\"\n)\n"
  },
  {
    "path": "internal/filtering/rulelist/rulelist_test.go",
    "content": "package rulelist_test\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testURLFilterID is the common rules.ListID for tests.\nconst testURLFilterID rules.ListID = 1\n\n// testTitle is the common title for tests.\nconst testTitle = \"Test Title\"\n\n// Common rule texts for tests.\nconst (\n\ttestRuleTextAllowed     = \"||allowed.example^\\n\"\n\ttestRuleTextBadTab      = \"||bad-tab-and-comment.example^\\t# A comment.\\n\"\n\ttestRuleTextBlocked     = \"||blocked.example^\\n\"\n\ttestRuleTextBlocked2    = \"||blocked-2.example^\\n\"\n\ttestRuleTextEtcHostsTab = \"0.0.0.0 tab..example^\\t# A comment.\\n\"\n\ttestRuleTextHTML        = \"<!DOCTYPE html>\\n\"\n\ttestRuleTextTitle       = \"! Title:  \" + testTitle + \" \\n\"\n\n\t// testRuleTextCosmetic is a cosmetic rule with a zero-width non-joiner.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/6003.\n\ttestRuleTextCosmetic = \"||cosmetic.example## :has-text(/\\u200c/i)\\n\"\n)\n\n// urlFilterIDCounter is the atomic integer used to create unique filter IDs.\nvar urlFilterIDCounter = &atomic.Uint64{}\n\n// newURLFilterID returns a new unique URLFilterID.\nfunc newURLFilterID() (id rules.ListID) {\n\treturn rules.ListID(urlFilterIDCounter.Add(1))\n}\n\n// newFilter is a helper for creating new filters in tests.  It does not\n// register the closing of the filter using t.Cleanup; callers must do that\n// either directly or by using the filter in an engine.\nfunc newFilter(tb testing.TB, u *url.URL, name string) (f *rulelist.Filter) {\n\ttb.Helper()\n\n\tf, err := rulelist.NewFilter(&rulelist.FilterConfig{\n\t\tURL:         u,\n\t\tName:        name,\n\t\tUID:         rulelist.MustNewUID(),\n\t\tURLFilterID: newURLFilterID(),\n\t\tEnabled:     true,\n\t})\n\trequire.NoError(tb, err)\n\n\treturn f\n}\n\n// newFilterLocations is a test helper that sets up both the filtering-rule list\n// file and the HTTP-server.  It also registers file removal and server stopping\n// using t.Cleanup.\nfunc newFilterLocations(\n\ttb testing.TB,\n\tcacheDir string,\n\tfileData string,\n\thttpData string,\n) (fileURL, srvURL *url.URL) {\n\ttb.Helper()\n\n\tf, err := os.CreateTemp(cacheDir, \"\")\n\trequire.NoError(tb, err)\n\n\terr = f.Close()\n\trequire.NoError(tb, err)\n\n\tfilePath := f.Name()\n\terr = os.WriteFile(filePath, []byte(fileData), 0o644)\n\trequire.NoError(tb, err)\n\n\ttestutil.CleanupAndRequireSuccess(tb, func() (err error) {\n\t\treturn os.Remove(filePath)\n\t})\n\n\tfileURL = &url.URL{\n\t\tScheme: urlutil.SchemeFile,\n\t\tPath:   filePath,\n\t}\n\n\tsrv := newStringHTTPServer(httpData)\n\ttb.Cleanup(srv.Close)\n\n\tsrvURL, err = url.Parse(srv.URL)\n\trequire.NoError(tb, err)\n\n\treturn fileURL, srvURL\n}\n\n// newStringHTTPServer returns a new HTTP server that serves s.\nfunc newStringHTTPServer(s string) (srv *httptest.Server) {\n\treturn httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tpt := testutil.PanicT{}\n\n\t\t_, err := io.WriteString(w, s)\n\t\trequire.NoError(pt, err)\n\t}))\n}\n\nfunc TestIDs(t *testing.T) {\n\t// Use a variable to prevent compilation errors.\n\tid := rulelist.IDCustom\n\tassert.Equal(t, rulelist.APIIDCustom, rulelist.APIID(id))\n\n\tid = rulelist.IDBlockedService\n\tassert.Equal(t, rulelist.APIIDBlockedService, rulelist.APIID(id))\n\n\tid = rulelist.IDSafeSearch\n\tassert.Equal(t, rulelist.APIIDSafeSearch, rulelist.APIID(id))\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/storage.go",
    "content": "package rulelist\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\n// Storage contains the main filtering engines, including the allowlist, the\n// blocklist, and the user's custom filtering rules.\ntype Storage struct {\n\t// refreshMu makes sure that only one update takes place at a time.\n\trefreshMu *sync.Mutex\n\n\tallow    *Engine\n\tblock    *Engine\n\tcustom   *TextEngine\n\thttpCli  *http.Client\n\tcacheDir string\n\tparseBuf []byte\n\tmaxSize  datasize.ByteSize\n}\n\n// StorageConfig is the configuration for the filtering-engine storage.\ntype StorageConfig struct {\n\t// Logger is used to log the operation of the storage.  It must not be nil.\n\tLogger *slog.Logger\n\n\t// HTTPClient is the HTTP client used to perform updates of rule lists.\n\t// It must not be nil.\n\tHTTPClient *http.Client\n\n\t// CacheDir is the path to the directory used to cache rule-list files.\n\t// It must be set.\n\tCacheDir string\n\n\t// AllowFilters are the filtering-rule lists used to exclude domain names\n\t// from the filtering.  Each item must not be nil and must have unique IDs\n\t// between AllowFilters and BlockFilters.\n\tAllowFilters []*Filter\n\n\t// BlockFilters are the filtering-rule lists used to block domain names.\n\t// Each item must not be nil and must have unique IDs between AllowFilters\n\t// and BlockFilters.\n\tBlockFilters []*Filter\n\n\t// CustomRules contains custom rules of the user.  They have priority over\n\t// both allow- and blacklist rules.\n\tCustomRules []string\n\n\t// MaxRuleListTextSize is the maximum size of a rule-list file.  It must be\n\t// greater than zero.\n\tMaxRuleListTextSize datasize.ByteSize\n}\n\n// NewStorage creates a new filtering-engine storage.  The engines are not\n// refreshed, so a refresh should be performed before use.\nfunc NewStorage(c *StorageConfig) (s *Storage, err error) {\n\tcustom, err := NewTextEngine(&TextEngineConfig{\n\t\tName:  EngineNameCustom,\n\t\tRules: c.CustomRules,\n\t\tID:    IDCustom,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating custom engine: %w\", err)\n\t}\n\n\treturn &Storage{\n\t\trefreshMu: &sync.Mutex{},\n\t\tallow: NewEngine(&EngineConfig{\n\t\t\tLogger:  c.Logger.With(\"engine\", EngineNameAllow),\n\t\t\tName:    EngineNameAllow,\n\t\t\tFilters: c.AllowFilters,\n\t\t}),\n\t\tblock: NewEngine(&EngineConfig{\n\t\t\tLogger:  c.Logger.With(\"engine\", EngineNameBlock),\n\t\t\tName:    EngineNameBlock,\n\t\t\tFilters: c.BlockFilters,\n\t\t}),\n\t\tcustom:   custom,\n\t\thttpCli:  c.HTTPClient,\n\t\tcacheDir: c.CacheDir,\n\t\tparseBuf: make([]byte, DefaultRuleBufSize),\n\t\tmaxSize:  c.MaxRuleListTextSize,\n\t}, nil\n}\n\n// Close closes the underlying rule-list engines.\nfunc (s *Storage) Close() (err error) {\n\t// Don't wrap the errors since they are informative enough as is.\n\treturn errors.Join(\n\t\ts.allow.Close(),\n\t\ts.block.Close(),\n\t)\n}\n\n// Refresh updates all engines in s.\n//\n// TODO(a.garipov): Refresh allow and block separately?\nfunc (s *Storage) Refresh(ctx context.Context) (err error) {\n\ts.refreshMu.Lock()\n\tdefer s.refreshMu.Unlock()\n\n\t// Don't wrap the errors since they are informative enough as is.\n\treturn errors.Join(\n\t\ts.allow.Refresh(ctx, s.parseBuf, s.httpCli, s.cacheDir, s.maxSize),\n\t\ts.block.Refresh(ctx, s.parseBuf, s.httpCli, s.cacheDir, s.maxSize),\n\t)\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/storage_test.go",
    "content": "package rulelist_test\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/c2h5oh/datasize\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStorage_Refresh(t *testing.T) {\n\tt.Parallel()\n\n\tcacheDir := t.TempDir()\n\n\tallowedFileURL, _ := newFilterLocations(t, cacheDir, testRuleTextAllowed, \"\")\n\tallowedFlt := newFilter(t, allowedFileURL, \"Allowed 1\")\n\n\tblockedFileURL, _ := newFilterLocations(t, cacheDir, testRuleTextBlocked, \"\")\n\tblockedFlt := newFilter(t, blockedFileURL, \"Blocked 1\")\n\n\tstrg, err := rulelist.NewStorage(&rulelist.StorageConfig{\n\t\tLogger: slogutil.NewDiscardLogger(),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: testTimeout,\n\t\t},\n\t\tCacheDir: cacheDir,\n\t\tAllowFilters: []*rulelist.Filter{\n\t\t\tallowedFlt,\n\t\t},\n\t\tBlockFilters: []*rulelist.Filter{\n\t\t\tblockedFlt,\n\t\t},\n\t\tCustomRules: []string{\n\t\t\ttestRuleTextBlocked2,\n\t\t},\n\t\tMaxRuleListTextSize: 1 * datasize.KB,\n\t})\n\trequire.NoError(t, err)\n\ttestutil.CleanupAndRequireSuccess(t, strg.Close)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\terr = strg.Refresh(ctx)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/textengine.go",
    "content": "package rulelist\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/filterlist\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n)\n\n// TextEngine is a single DNS filter based on a list of rules in text form.\ntype TextEngine struct {\n\t// mu protects engine and storage.\n\tmu *sync.RWMutex\n\n\t// engine is the filtering engine.\n\tengine *urlfilter.DNSEngine\n\n\t// storage is the filtering-rule storage.  It is saved here to close it.\n\tstorage *filterlist.RuleStorage\n\n\t// name is the human-readable name of the engine.\n\tname string\n}\n\n// TextEngineConfig is the configuration for a rule-list filtering engine\n// created from a filtering rule text.\ntype TextEngineConfig struct {\n\t// name is the human-readable name of the engine; see [EngineNameAllow] and\n\t// similar constants.\n\tName string\n\n\t// Rules is the text of the filtering rules for this engine.\n\tRules []string\n\n\t// ID is the ID to use inside a URL-filter engine.\n\tID rules.ListID\n}\n\n// NewTextEngine returns a new rule-list filtering engine that uses rules\n// directly.  The engine is ready to use and should not be refreshed.\nfunc NewTextEngine(c *TextEngineConfig) (e *TextEngine, err error) {\n\ttext := strings.Join(c.Rules, \"\\n\")\n\tstorage, err := filterlist.NewRuleStorage([]filterlist.Interface{\n\t\tfilterlist.NewString(&filterlist.StringConfig{\n\t\t\tRulesText:      text,\n\t\t\tID:             c.ID,\n\t\t\tIgnoreCosmetic: true,\n\t\t}),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating rule storage: %w\", err)\n\t}\n\n\tengine := urlfilter.NewDNSEngine(storage)\n\n\treturn &TextEngine{\n\t\tmu:      &sync.RWMutex{},\n\t\tengine:  engine,\n\t\tstorage: storage,\n\t\tname:    c.Name,\n\t}, nil\n}\n\n// FilterRequest returns the result of filtering req using the DNS filtering\n// engine.\nfunc (e *TextEngine) FilterRequest(\n\treq *urlfilter.DNSRequest,\n) (res *urlfilter.DNSResult, hasMatched bool) {\n\tvar engine *urlfilter.DNSEngine\n\n\tfunc() {\n\t\te.mu.RLock()\n\t\tdefer e.mu.RUnlock()\n\n\t\tengine = e.engine\n\t}()\n\n\treturn engine.MatchRequest(req)\n}\n\n// Close closes the underlying rule list engine as well as the rule lists.\nfunc (e *TextEngine) Close() (err error) {\n\te.mu.Lock()\n\tdefer e.mu.Unlock()\n\n\tif e.storage == nil {\n\t\treturn nil\n\t}\n\n\terr = e.storage.Close()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"closing text engine %q: %w\", e.name, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/filtering/rulelist/textengine_test.go",
    "content": "package rulelist_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewTextEngine(t *testing.T) {\n\tt.Parallel()\n\n\teng, err := rulelist.NewTextEngine(&rulelist.TextEngineConfig{\n\t\tName: \"RulesEngine\",\n\t\tRules: []string{\n\t\t\ttestRuleTextTitle,\n\t\t\ttestRuleTextBlocked,\n\t\t},\n\t\tID: testURLFilterID,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, eng)\n\ttestutil.CleanupAndRequireSuccess(t, eng.Close)\n\n\tfltReq := &urlfilter.DNSRequest{\n\t\tHostname: \"blocked.example\",\n\t\tAnswer:   false,\n\t\tDNSType:  dns.TypeA,\n\t}\n\n\tfltRes, hasMatched := eng.FilterRequest(fltReq)\n\tassert.True(t, hasMatched)\n\n\trequire.NotNil(t, fltRes)\n\trequire.NotNil(t, fltRes.NetworkRule)\n\n\tassert.Equal(t, fltRes.NetworkRule.GetFilterListID(), testURLFilterID)\n}\n"
  },
  {
    "path": "internal/filtering/safesearch/rules/bing.txt",
    "content": "|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com\n|edgeservices.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com\n"
  },
  {
    "path": "internal/filtering/safesearch/rules/duckduckgo.txt",
    "content": "|duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com\n|start.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com\n|www.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com\n"
  },
  {
    "path": "internal/filtering/safesearch/rules/ecosia.txt",
    "content": "|www.ecosia.org^$dnsrewrite=NOERROR;CNAME;strict-safe-search.ecosia.org\n"
  },
  {
    "path": "internal/filtering/safesearch/rules/google.txt",
    "content": "|www.google.ad^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ae^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.al^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.am^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.as^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.at^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.az^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ba^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.be^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.bf^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.bg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.bi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.bj^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.bs^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.bt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.by^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ca^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cat^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cd^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cf^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ch^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ci^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.ao^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.bw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.ck^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.cr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.id^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.il^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.in^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.jp^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.ke^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.kr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.ls^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.ma^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.mz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.nz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.th^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.tz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.ug^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.uk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.uz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.ve^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.vi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.za^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.zm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.co.zw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.af^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ag^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ai^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ar^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.au^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.bd^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.bh^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.bn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.bo^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.br^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.bz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.co^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.cu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.cy^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.do^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ec^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.eg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.et^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.fj^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.gh^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.gi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.gt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.hk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.jm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.kh^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.kw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.lb^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ly^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.mm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.mt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.mx^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.my^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.na^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.nf^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ng^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ni^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.np^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.om^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.pa^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.pe^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.pg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ph^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.pk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.pr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.py^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.qa^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.sa^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.sb^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.sg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.sl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.sv^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.tj^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.tr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.tw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.ua^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.uy^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.vc^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com.vn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.com^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cv^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.cz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.de^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.dj^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.dk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.dm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.dz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ee^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.es^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.fi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.fm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.fr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ga^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ge^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.gg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.gl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.gm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.gp^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.gr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.gy^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.hn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.hr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ht^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.hu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ie^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.im^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.iq^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.is^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.it^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.je^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.jo^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.kg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ki^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.kz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.la^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.li^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.lk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.lt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.lu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.lv^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.md^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.me^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.mg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.mk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ml^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.mn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ms^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.mu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.mv^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.mw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ne^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.nl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.no^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.nr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.nu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.pl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.pn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ps^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.pt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ro^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.rs^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ru^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.rw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.sc^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.se^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.sh^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.si^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.sk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.sm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.sn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.so^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.sr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.st^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.td^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.tg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.tk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.tl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.tm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.tn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.to^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.tt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.vg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.vu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n|www.google.ws^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com\n"
  },
  {
    "path": "internal/filtering/safesearch/rules/pixabay.txt",
    "content": "|pixabay.com^$dnsrewrite=NOERROR;CNAME;safesearch.pixabay.com\n"
  },
  {
    "path": "internal/filtering/safesearch/rules/yandex.txt",
    "content": "|www.xn--d1acpjx3f.xn--p1ai^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.ya.ru^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.az^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.by^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.co.il^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.com.am^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.com.ge^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.com.ru^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.com.tr^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.com^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.de^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.ee^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.eu^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.fi^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.fr^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.kz^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.lt^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.lv^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.md^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.net^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.org^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.pl^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56\n|www.yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56\n|xn--d1acpjx3f.xn--p1ai^$dnsrewrite=NOERROR;A;213.180.193.56\n|ya.ru^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.az^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.by^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.co.il^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.com.am^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.com.ge^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.com.ru^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.com.tr^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.com^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.de^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.ee^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.eu^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.fi^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.fr^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.kz^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.lt^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.lv^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.md^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.net^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.org^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.pl^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56\n|yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56\n"
  },
  {
    "path": "internal/filtering/safesearch/rules/youtube.txt",
    "content": "|www.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com\n|m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com\n|youtubei.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com\n|youtube.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com\n|www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com\n"
  },
  {
    "path": "internal/filtering/safesearch/rules.go",
    "content": "package safesearch\n\nimport _ \"embed\"\n\n//go:embed rules/bing.txt\nvar bing string\n\n//go:embed rules/google.txt\nvar google string\n\n//go:embed rules/pixabay.txt\nvar pixabay string\n\n//go:embed rules/duckduckgo.txt\nvar duckduckgo string\n\n//go:embed rules/ecosia.txt\nvar ecosia string\n\n//go:embed rules/yandex.txt\nvar yandex string\n\n//go:embed rules/youtube.txt\nvar youtube string\n\n// safeSearchRules is a map with rules texts grouped by search providers.\n// Source rules downloaded from:\n// https://adguardteam.github.io/HostlistsRegistry/assets/engines_safe_search.txt,\n// https://adguardteam.github.io/HostlistsRegistry/assets/youtube_safe_search.txt.\nvar safeSearchRules = map[Service]string{\n\tBing:       bing,\n\tDuckDuckGo: duckduckgo,\n\tEcosia:     ecosia,\n\tGoogle:     google,\n\tPixabay:    pixabay,\n\tYandex:     yandex,\n\tYouTube:    youtube,\n}\n"
  },
  {
    "path": "internal/filtering/safesearch/safesearch.go",
    "content": "// Package safesearch implements safesearch host matching.\npackage safesearch\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"encoding/gob\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/cache\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/urlfilter\"\n\t\"github.com/AdguardTeam/urlfilter/filterlist\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/c2h5oh/datasize\"\n\t\"github.com/miekg/dns\"\n)\n\n// Attribute keys and values for logging.\nconst (\n\tLogPrefix    = \"safesearch\"\n\tLogKeyClient = \"client\"\n)\n\n// Service is a enum with service names used as search providers.\ntype Service string\n\n// Service enum members.\nconst (\n\tBing       Service = \"bing\"\n\tDuckDuckGo Service = \"duckduckgo\"\n\tEcosia     Service = \"ecosia\"\n\tGoogle     Service = \"google\"\n\tPixabay    Service = \"pixabay\"\n\tYandex     Service = \"yandex\"\n\tYouTube    Service = \"youtube\"\n)\n\n// isServiceProtected returns true if the service safe search is active.\nfunc isServiceProtected(s filtering.SafeSearchConfig, service Service) (ok bool) {\n\tswitch service {\n\tcase Bing:\n\t\treturn s.Bing\n\tcase DuckDuckGo:\n\t\treturn s.DuckDuckGo\n\tcase Ecosia:\n\t\treturn s.Ecosia\n\tcase Google:\n\t\treturn s.Google\n\tcase Pixabay:\n\t\treturn s.Pixabay\n\tcase Yandex:\n\t\treturn s.Yandex\n\tcase YouTube:\n\t\treturn s.YouTube\n\tdefault:\n\t\tpanic(fmt.Errorf(\"safesearch: invalid sources: not found service %q\", service))\n\t}\n}\n\n// DefaultConfig is the configuration structure for [Default].\ntype DefaultConfig struct {\n\t// Logger is used for logging the operation of the safe search filter.\n\tLogger *slog.Logger\n\n\t// ClientName is the name of the persistent client associated with the safe\n\t// search filter, if there is one.\n\tClientName string\n\n\t// CacheSize is the size of the filter results cache.\n\tCacheSize uint\n\n\t// CacheTTL is the Time to Live duration for cached items.\n\tCacheTTL time.Duration\n\n\t// ServicesConfig contains safe search settings for services.  It must not\n\t// be nil.\n\tServicesConfig filtering.SafeSearchConfig\n}\n\n// Default is the default safe search filter that uses filtering rules with the\n// dnsrewrite modifier.\ntype Default struct {\n\t// logger is used for logging the operation of the safe search filter.\n\tlogger *slog.Logger\n\n\t// mu protects engine.\n\tmu *sync.RWMutex\n\n\t// engine is the filtering engine that contains the DNS rewrite rules.\n\t// engine may be nil, which means that this safe search filter is disabled.\n\tengine *urlfilter.DNSEngine\n\n\t// cache stores safe search filtering results.\n\tcache cache.Cache\n\n\t// cacheTTL is the Time to Live duration for cached items.\n\tcacheTTL time.Duration\n}\n\n// NewDefault returns an initialized default safe search filter.  ctx is used\n// to log the initial refresh.\nfunc NewDefault(ctx context.Context, conf *DefaultConfig) (ss *Default, err error) {\n\tss = &Default{\n\t\tlogger: conf.Logger,\n\t\tmu:     &sync.RWMutex{},\n\t\tcache: cache.New(cache.Config{\n\t\t\tEnableLRU: true,\n\t\t\tMaxSize:   conf.CacheSize,\n\t\t}),\n\t\tcacheTTL: conf.CacheTTL,\n\t}\n\n\t// TODO(s.chzhen):  Move to [Default.InitialRefresh].\n\terr = ss.resetEngine(ctx, rulelist.IDSafeSearch, conf.ServicesConfig)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\treturn ss, nil\n}\n\n// resetEngine creates new engine for provided safe search configuration and\n// sets it in ss.\nfunc (ss *Default) resetEngine(\n\tctx context.Context,\n\tid rules.ListID,\n\tconf filtering.SafeSearchConfig,\n) (err error) {\n\tif !conf.Enabled {\n\t\tss.logger.DebugContext(ctx, \"disabled\")\n\n\t\treturn nil\n\t}\n\n\tvar sb strings.Builder\n\tfor service, serviceRules := range safeSearchRules {\n\t\tif isServiceProtected(conf, service) {\n\t\t\tsb.WriteString(serviceRules)\n\t\t}\n\t}\n\n\tstrList := []filterlist.Interface{\n\t\tfilterlist.NewString(&filterlist.StringConfig{\n\t\t\tID:             id,\n\t\t\tRulesText:      sb.String(),\n\t\t\tIgnoreCosmetic: true,\n\t\t}),\n\t}\n\n\trs, err := filterlist.NewRuleStorage(strList)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating rule storage: %w\", err)\n\t}\n\n\tss.engine = urlfilter.NewDNSEngine(rs)\n\n\tss.logger.InfoContext(ctx, \"reset rules\", \"count\", ss.engine.RulesCount)\n\n\treturn nil\n}\n\n// type check\nvar _ filtering.SafeSearch = (*Default)(nil)\n\n// CheckHost implements the [filtering.SafeSearch] interface for *Default.\nfunc (ss *Default) CheckHost(\n\tctx context.Context,\n\thost string,\n\tqtype rules.RRType,\n) (res filtering.Result, err error) {\n\tstart := time.Now()\n\tdefer func() {\n\t\tss.logger.DebugContext(ctx, \"lookup finished\", \"host\", host, \"elapsed\", time.Since(start))\n\t}()\n\n\tswitch qtype {\n\tcase dns.TypeA, dns.TypeAAAA, dns.TypeHTTPS:\n\t\t// Go on.\n\tdefault:\n\t\treturn filtering.Result{}, nil\n\t}\n\n\t// Check cache. Return cached result if it was found\n\tcachedValue, isFound := ss.getCachedResult(ctx, host, qtype)\n\tif isFound {\n\t\tss.logger.DebugContext(ctx, \"found in cache\", \"host\", host)\n\n\t\treturn cachedValue, nil\n\t}\n\n\trewrite := ss.searchHost(host, qtype)\n\tif rewrite == nil {\n\t\treturn filtering.Result{}, nil\n\t}\n\n\tfltRes, err := ss.newResult(rewrite, qtype)\n\tif err != nil {\n\t\tss.logger.ErrorContext(ctx, \"looking up addresses\", \"host\", host, slogutil.KeyError, err)\n\n\t\treturn filtering.Result{}, err\n\t}\n\n\tres = *fltRes\n\n\t// TODO(a.garipov): Consider switch back to resolving CNAME records IPs and\n\t// saving results to cache.\n\tss.setCacheResult(ctx, host, qtype, res)\n\n\treturn res, nil\n}\n\n// searchHost looks up DNS rewrites in the internal DNS filtering engine.\nfunc (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRewrite) {\n\tss.mu.RLock()\n\tdefer ss.mu.RUnlock()\n\n\tif ss.engine == nil {\n\t\treturn nil\n\t}\n\n\tr, _ := ss.engine.MatchRequest(&urlfilter.DNSRequest{\n\t\tHostname: strings.ToLower(host),\n\t\tDNSType:  qtype,\n\t})\n\n\trewritesRules := r.DNSRewrites()\n\tif len(rewritesRules) > 0 {\n\t\treturn rewritesRules[0].DNSRewrite\n\t}\n\n\treturn nil\n}\n\n// newResult creates Result object from rewrite rule.  qtype must be either\n// [dns.TypeA] or [dns.TypeAAAA], or [dns.TypeHTTPS].  If err is nil, res is\n// never nil, so that the empty result is converted into a NODATA response.\nfunc (ss *Default) newResult(\n\trewrite *rules.DNSRewrite,\n\tqtype rules.RRType,\n) (res *filtering.Result, err error) {\n\tres = &filtering.Result{\n\t\tReason:     filtering.FilteredSafeSearch,\n\t\tIsFiltered: true,\n\t}\n\n\tif rewrite.RRType == qtype {\n\t\tip, ok := rewrite.Value.(netip.Addr)\n\t\tif !ok || ip == (netip.Addr{}) {\n\t\t\treturn nil, fmt.Errorf(\"expected ip rewrite value, got %T(%[1]v)\", rewrite.Value)\n\t\t}\n\n\t\tres.Rules = []*filtering.ResultRule{{\n\t\t\tFilterListID: rulelist.APIIDSafeSearch,\n\t\t\tIP:           ip,\n\t\t}}\n\n\t\treturn res, nil\n\t}\n\n\tres.CanonName = rewrite.NewCNAME\n\n\treturn res, nil\n}\n\n// setCacheResult stores data in cache for host.  qtype is expected to be either\n// [dns.TypeA] or [dns.TypeAAAA].\n//\n// TODO(a.garipov):  Remove gob and use uint64.\nfunc (ss *Default) setCacheResult(\n\tctx context.Context,\n\thost string,\n\tqtype rules.RRType,\n\tres filtering.Result,\n) {\n\t// #nosec G115 -- The Unix epoch time is highly unlikely to be negative, and\n\t// also see the TODO.\n\texpire := uint32(time.Now().Add(ss.cacheTTL).Unix())\n\texp := make([]byte, 4)\n\tbinary.BigEndian.PutUint32(exp, expire)\n\tbuf := bytes.NewBuffer(exp)\n\n\terr := gob.NewEncoder(buf).Encode(res)\n\tif err != nil {\n\t\tss.logger.ErrorContext(ctx, \"cache encoding\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tval := buf.Bytes()\n\t_ = ss.cache.Set([]byte(dns.Type(qtype).String()+\" \"+host), val)\n\n\tss.logger.DebugContext(\n\t\tctx,\n\t\t\"stored in cache\",\n\t\t\"host\", host,\n\t\t\"entry_size\", datasize.ByteSize(len(val)),\n\t)\n}\n\n// getCachedResult returns stored data from cache for host.  qtype is expected\n// to be either [dns.TypeA] or [dns.TypeAAAA].\n//\n// TODO(a.garipov):  Remove gob and use uint64.\nfunc (ss *Default) getCachedResult(\n\tctx context.Context,\n\thost string,\n\tqtype rules.RRType,\n) (res filtering.Result, ok bool) {\n\tres = filtering.Result{}\n\n\tdata := ss.cache.Get([]byte(dns.Type(qtype).String() + \" \" + host))\n\tif data == nil {\n\t\treturn res, false\n\t}\n\n\texp := binary.BigEndian.Uint32(data[:4])\n\t// #nosec G115 -- The Unix epoch time is highly unlikely to be negative, and\n\t// also see the TODO.\n\tif exp <= uint32(time.Now().Unix()) {\n\t\tss.cache.Del([]byte(host))\n\n\t\treturn res, false\n\t}\n\n\tbuf := bytes.NewBuffer(data[4:])\n\n\terr := gob.NewDecoder(buf).Decode(&res)\n\tif err != nil {\n\t\tss.logger.ErrorContext(ctx, \"cache decoding\", slogutil.KeyError, err)\n\n\t\treturn filtering.Result{}, false\n\t}\n\n\treturn res, true\n}\n\n// Update implements the [filtering.SafeSearch] interface for *Default.  Update\n// ignores the CustomResolver and Enabled fields.\nfunc (ss *Default) Update(ctx context.Context, conf filtering.SafeSearchConfig) (err error) {\n\tss.mu.Lock()\n\tdefer ss.mu.Unlock()\n\n\terr = ss.resetEngine(ctx, rulelist.IDSafeSearch, conf)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tss.cache.Clear()\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/filtering/safesearch/safesearch_internal_test.go",
    "content": "package safesearch\n\nimport (\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TODO(a.garipov): Move as much of this as possible into proper external tests.\n\nconst (\n\t// TODO(a.garipov): Add IPv6 tests.\n\ttestQType     = dns.TypeA\n\ttestCacheSize = 5000\n\ttestCacheTTL  = 30 * time.Minute\n)\n\n// testTimeout is the common timeout for tests and contexts.\nconst testTimeout = 1 * time.Second\n\nvar defaultSafeSearchConf = filtering.SafeSearchConfig{\n\tEnabled:    true,\n\tBing:       true,\n\tDuckDuckGo: true,\n\tEcosia:     true,\n\tGoogle:     true,\n\tPixabay:    true,\n\tYandex:     true,\n\tYouTube:    true,\n}\n\nvar yandexIP = netip.AddrFrom4([4]byte{213, 180, 193, 56})\n\nfunc newForTest(t testing.TB, ssConf filtering.SafeSearchConfig) (ss *Default) {\n\tss, err := NewDefault(testutil.ContextWithTimeout(t, testTimeout), &DefaultConfig{\n\t\tLogger:         slogutil.NewDiscardLogger(),\n\t\tServicesConfig: ssConf,\n\t\tCacheSize:      testCacheSize,\n\t\tCacheTTL:       testCacheTTL,\n\t})\n\trequire.NoError(t, err)\n\n\treturn ss\n}\n\nfunc TestSafeSearch(t *testing.T) {\n\tss := newForTest(t, defaultSafeSearchConf)\n\tval := ss.searchHost(\"www.google.com\", testQType)\n\n\tassert.Equal(t, &rules.DNSRewrite{NewCNAME: \"forcesafesearch.google.com\"}, val)\n}\n\nfunc TestSafeSearchCacheYandex(t *testing.T) {\n\tconst domain = \"yandex.ru\"\n\n\tss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\t// Check host with disabled safesearch.\n\tres, err := ss.CheckHost(ctx, domain, testQType)\n\trequire.NoError(t, err)\n\n\tassert.False(t, res.IsFiltered)\n\tassert.Empty(t, res.Rules)\n\n\tss = newForTest(t, defaultSafeSearchConf)\n\tres, err = ss.CheckHost(ctx, domain, testQType)\n\trequire.NoError(t, err)\n\n\t// For yandex we already know valid IP.\n\trequire.Len(t, res.Rules, 1)\n\n\tassert.Equal(t, res.Rules[0].IP, yandexIP)\n\n\t// Check cache.\n\tcachedValue, isFound := ss.getCachedResult(ctx, domain, testQType)\n\trequire.True(t, isFound)\n\trequire.Len(t, cachedValue.Rules, 1)\n\n\tassert.Equal(t, cachedValue.Rules[0].IP, yandexIP)\n}\n\nfunc BenchmarkDefault_SearchHost(b *testing.B) {\n\tconst googleHost = \"www.google.com\"\n\n\tss := newForTest(b, defaultSafeSearchConf)\n\n\tvar rewrite *rules.DNSRewrite\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\trewrite = ss.searchHost(googleHost, testQType)\n\t}\n\n\trequire.NotNil(b, rewrite)\n\tassert.Equal(b, \"forcesafesearch.google.com\", rewrite.NewCNAME)\n\n\t// Most recent results:\n\t//\n\t//\tgoos: darwin\n\t//\tgoarch: amd64\n\t//\tpkg: github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch\n\t//\tcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz\n\t//\tBenchmarkDefault_SearchHost-12    \t  751882\t      1604 ns/op\t     129 B/op\t       5 allocs/op\n}\n"
  },
  {
    "path": "internal/filtering/safesearch/safesearch_test.go",
    "content": "package safesearch_test\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is the common timeout for tests and contexts.\nconst testTimeout = 1 * time.Second\n\n// Common test constants.\nconst (\n\t// TODO(a.garipov): Add IPv6 tests.\n\ttestQType     = dns.TypeA\n\ttestCacheSize = 5000\n\ttestCacheTTL  = 30 * time.Minute\n)\n\n// testConf is the default safe search configuration for tests.\nvar testConf = filtering.SafeSearchConfig{\n\tEnabled: true,\n\n\tBing:       true,\n\tDuckDuckGo: true,\n\tEcosia:     true,\n\tGoogle:     true,\n\tPixabay:    true,\n\tYandex:     true,\n\tYouTube:    true,\n}\n\n// testLogger is a logger used in tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\n// yandexIP is the expected IP address of Yandex safe search results.  Keep in\n// sync with the rules data.\nvar yandexIP = netip.AddrFrom4([4]byte{213, 180, 193, 56})\n\nfunc TestDefault_CheckHost_yandex(t *testing.T) {\n\tconf := testConf\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tss, err := safesearch.NewDefault(ctx, &safesearch.DefaultConfig{\n\t\tLogger:         testLogger,\n\t\tServicesConfig: conf,\n\t\tCacheSize:      testCacheSize,\n\t\tCacheTTL:       testCacheTTL,\n\t})\n\trequire.NoError(t, err)\n\n\thosts := []string{\n\t\t\"yandex.ru\",\n\t\t\"yAndeX.ru\",\n\t\t\"YANdex.COM\",\n\t\t\"yandex.by\",\n\t\t\"yandex.kz\",\n\t\t\"www.yandex.com\",\n\t}\n\n\ttestCases := []struct {\n\t\twant netip.Addr\n\t\tname string\n\t\tqt   uint16\n\t}{{\n\t\twant: yandexIP,\n\t\tname: \"a\",\n\t\tqt:   dns.TypeA,\n\t}, {\n\t\twant: netip.Addr{},\n\t\tname: \"aaaa\",\n\t\tqt:   dns.TypeAAAA,\n\t}, {\n\t\twant: netip.Addr{},\n\t\tname: \"https\",\n\t\tqt:   dns.TypeHTTPS,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tfor _, host := range hosts {\n\t\t\t\t// Check host for each domain.\n\t\t\t\tvar res filtering.Result\n\t\t\t\tres, err = ss.CheckHost(ctx, host, tc.qt)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.True(t, res.IsFiltered)\n\t\t\t\tassert.Equal(t, filtering.FilteredSafeSearch, res.Reason)\n\n\t\t\t\tif tc.want == (netip.Addr{}) {\n\t\t\t\t\tassert.Empty(t, res.Rules)\n\t\t\t\t} else {\n\t\t\t\t\trequire.Len(t, res.Rules, 1)\n\n\t\t\t\t\trule := res.Rules[0]\n\t\t\t\t\tassert.Equal(t, tc.want, rule.IP)\n\t\t\t\t\tassert.Equal(t, rulelist.APIIDSafeSearch, rule.FilterListID)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefault_CheckHost_google(t *testing.T) {\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tss, err := safesearch.NewDefault(ctx, &safesearch.DefaultConfig{\n\t\tLogger:         testLogger,\n\t\tServicesConfig: testConf,\n\t\tCacheSize:      testCacheSize,\n\t\tCacheTTL:       testCacheTTL,\n\t})\n\trequire.NoError(t, err)\n\n\t// Check host for each domain.\n\tfor _, host := range []string{\n\t\t\"www.google.com\",\n\t\t\"www.google.im\",\n\t\t\"www.google.co.in\",\n\t\t\"www.google.iq\",\n\t\t\"www.google.is\",\n\t\t\"www.google.it\",\n\t\t\"www.google.je\",\n\t} {\n\t\tt.Run(host, func(t *testing.T) {\n\t\t\tvar res filtering.Result\n\t\t\tres, err = ss.CheckHost(ctx, host, testQType)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.True(t, res.IsFiltered)\n\t\t\tassert.Equal(t, filtering.FilteredSafeSearch, res.Reason)\n\t\t\tassert.Equal(t, \"forcesafesearch.google.com\", res.CanonName)\n\t\t\tassert.Empty(t, res.Rules)\n\t\t})\n\t}\n}\n\n// testResolver is a [filtering.Resolver] for tests.\n//\n// TODO(a.garipov): Move to aghtest and use everywhere.\ntype testResolver struct {\n\tOnLookupIP func(ctx context.Context, network, host string) (ips []net.IP, err error)\n}\n\n// type check\nvar _ filtering.Resolver = (*testResolver)(nil)\n\n// LookupIP implements the [filtering.Resolver] interface for *testResolver.\nfunc (r *testResolver) LookupIP(\n\tctx context.Context,\n\tnetwork string,\n\thost string,\n) (ips []net.IP, err error) {\n\treturn r.OnLookupIP(ctx, network, host)\n}\n\nfunc TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tss, err := safesearch.NewDefault(ctx, &safesearch.DefaultConfig{\n\t\tLogger:         testLogger,\n\t\tServicesConfig: testConf,\n\t\tCacheSize:      testCacheSize,\n\t\tCacheTTL:       testCacheTTL,\n\t})\n\trequire.NoError(t, err)\n\n\t// The DuckDuckGo safe-search addresses are resolved through CNAMEs, but\n\t// DuckDuckGo doesn't have a safe-search IPv6 address.  The result should be\n\t// the same as the one for Yandex IPv6.  That is, a NODATA response.\n\tres, err := ss.CheckHost(ctx, \"www.duckduckgo.com\", dns.TypeAAAA)\n\trequire.NoError(t, err)\n\n\tassert.True(t, res.IsFiltered)\n\tassert.Equal(t, filtering.FilteredSafeSearch, res.Reason)\n\tassert.Equal(t, \"safe.duckduckgo.com\", res.CanonName)\n\tassert.Empty(t, res.Rules)\n}\n\nfunc TestDefault_Update(t *testing.T) {\n\tconf := testConf\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tss, err := safesearch.NewDefault(ctx, &safesearch.DefaultConfig{\n\t\tLogger:         testLogger,\n\t\tServicesConfig: conf,\n\t\tCacheSize:      testCacheSize,\n\t\tCacheTTL:       testCacheTTL,\n\t})\n\trequire.NoError(t, err)\n\n\tres, err := ss.CheckHost(ctx, \"www.yandex.com\", testQType)\n\trequire.NoError(t, err)\n\n\tassert.True(t, res.IsFiltered)\n\n\terr = ss.Update(ctx, filtering.SafeSearchConfig{\n\t\tEnabled: true,\n\t\tGoogle:  false,\n\t})\n\trequire.NoError(t, err)\n\n\tres, err = ss.CheckHost(ctx, \"www.yandex.com\", testQType)\n\trequire.NoError(t, err)\n\n\tassert.False(t, res.IsFiltered)\n\n\terr = ss.Update(ctx, filtering.SafeSearchConfig{\n\t\tEnabled: false,\n\t\tGoogle:  true,\n\t})\n\trequire.NoError(t, err)\n\n\tres, err = ss.CheckHost(ctx, \"www.yandex.com\", testQType)\n\trequire.NoError(t, err)\n\n\tassert.False(t, res.IsFiltered)\n}\n"
  },
  {
    "path": "internal/filtering/safesearch.go",
    "content": "package filtering\n\nimport \"context\"\n\n// SafeSearch interface describes a service for search engines hosts rewrites.\n//\n// TODO(s.chzhen):  Move to a higher-level package to allow importing the client\n// package into the filtering package.\ntype SafeSearch interface {\n\t// CheckHost checks host with safe search filter.  CheckHost must be safe\n\t// for concurrent use.  qtype must be either [dns.TypeA] or [dns.TypeAAAA].\n\tCheckHost(ctx context.Context, host string, qtype uint16) (res Result, err error)\n\n\t// Update updates the configuration of the safe search filter.  Update must\n\t// be safe for concurrent use.  An implementation of Update may ignore some\n\t// fields, but it must document which.\n\tUpdate(ctx context.Context, conf SafeSearchConfig) (err error)\n}\n\n// SafeSearchConfig is a struct with safe search related settings.\ntype SafeSearchConfig struct {\n\t// Enabled indicates if safe search is enabled entirely.\n\tEnabled bool `yaml:\"enabled\" json:\"enabled\"`\n\n\t// Services flags.  Each flag indicates if the corresponding service is\n\t// enabled or disabled.\n\n\tBing       bool `yaml:\"bing\" json:\"bing\"`\n\tDuckDuckGo bool `yaml:\"duckduckgo\" json:\"duckduckgo\"`\n\tEcosia     bool `yaml:\"ecosia\" json:\"ecosia\"`\n\tGoogle     bool `yaml:\"google\" json:\"google\"`\n\tPixabay    bool `yaml:\"pixabay\" json:\"pixabay\"`\n\tYandex     bool `yaml:\"yandex\" json:\"yandex\"`\n\tYouTube    bool `yaml:\"youtube\" json:\"youtube\"`\n}\n\n// checkSafeSearch checks host with safe search engine.  Matches\n// [hostChecker.check].\nfunc (d *DNSFilter) checkSafeSearch(\n\thost string,\n\tqtype uint16,\n\tsetts *Settings,\n) (res Result, err error) {\n\tif d.safeSearch == nil || !setts.ProtectionEnabled || !setts.SafeSearchEnabled {\n\t\treturn Result{}, nil\n\t}\n\n\t// TODO(s.chzhen):  Pass context.\n\tctx := context.TODO()\n\n\tclientSafeSearch := setts.ClientSafeSearch\n\tif clientSafeSearch != nil {\n\t\treturn clientSafeSearch.CheckHost(ctx, host, qtype)\n\t}\n\n\treturn d.safeSearch.CheckHost(ctx, host, qtype)\n}\n"
  },
  {
    "path": "internal/filtering/safesearchhttp.go",
    "content": "package filtering\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n)\n\n// handleSafeSearchEnable is the handler for POST /control/safesearch/enable\n// HTTP API.\n//\n// Deprecated: Use handleSafeSearchSettings.\nfunc (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {\n\tsetProtectedBool(d.confMu, &d.conf.SafeSearchConf.Enabled, true)\n\td.conf.ConfModifier.Apply(r.Context())\n}\n\n// handleSafeSearchDisable is the handler for POST /control/safesearch/disable\n// HTTP API.\n//\n// Deprecated: Use handleSafeSearchSettings.\nfunc (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {\n\tsetProtectedBool(d.confMu, &d.conf.SafeSearchConf.Enabled, false)\n\td.conf.ConfModifier.Apply(r.Context())\n}\n\n// handleSafeSearchStatus is the handler for GET /control/safesearch/status\n// HTTP API.\nfunc (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {\n\tvar resp SafeSearchConfig\n\tfunc() {\n\t\td.confMu.RLock()\n\t\tdefer d.confMu.RUnlock()\n\n\t\tresp = d.conf.SafeSearchConf\n\t}()\n\n\taghhttp.WriteJSONResponseOK(r.Context(), d.logger, w, r, resp)\n}\n\n// handleSafeSearchSettings is the handler for PUT /control/safesearch/settings\n// HTTP API.\nfunc (d *DNSFilter) handleSafeSearchSettings(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := d.logger\n\n\treq := &SafeSearchConfig{}\n\terr := json.NewDecoder(r.Body).Decode(req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"reading req: %s\", err)\n\n\t\treturn\n\t}\n\n\tconf := *req\n\terr = d.safeSearch.Update(ctx, conf)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"updating: %s\", err)\n\n\t\treturn\n\t}\n\n\tfunc() {\n\t\td.confMu.Lock()\n\t\tdefer d.confMu.Unlock()\n\n\t\td.conf.SafeSearchConf = conf\n\t}()\n\n\td.conf.ConfModifier.Apply(ctx)\n\n\taghhttp.OK(ctx, l, w)\n}\n"
  },
  {
    "path": "internal/filtering/servicelist.go",
    "content": "// Code generated by go run ./scripts/blocked-services/main.go; DO NOT EDIT.\n\npackage filtering\n\n// blockedService represents a single blocked service.\ntype blockedService struct {\n\tID      string   `json:\"id\"`\n\tName    string   `json:\"name\"`\n\tIconSVG []byte   `json:\"icon_svg\"`\n\tRules   []string `json:\"rules\"`\n\tGroupID string   `json:\"group_id\"`\n}\n\n// serviceGroup represents single group of services.\ntype serviceGroup struct {\n\tID string `json:\"id\"`\n}\n\n// blockedServices contains raw blocked service data.\nvar blockedServices = []blockedService{{\n\tID:      \"4chan\",\n\tName:    \"4chan\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M33.25 2C31.311 2 29 3.724 29 11.928c0 4.614 1.237 8.92 1.29 9.101l.265.909.922-.215a32.132 32.132 0 0 0 5.472-1.907C39.37 18.711 45 15.614 45 10.75 45 8.098 43.789 5 40.375 5c-.996 0-1.807.356-2.387.736C36.868 4.038 35.13 2 33.25 2zM11.852 4C8.297 4 6 5.956 6 8.984c0 .975.355 1.752.746 2.325C5.044 12.389 3 14.129 3 16.25c0 1.428.97 4.75 9.943 4.75 5.104 0 8.958-1.248 9.12-1.3l.837-.276-.17-.865c-.042-.215-1.054-5.3-3.75-9.905C18.038 7.044 15.335 4 11.852 4zm8.22 24.057-.836.228c-.273.075-2.742.774-5.336 2.336-3.184 1.916-6.9 5.436-6.9 9.047C7 43.987 9.755 46 11.617 46a4.894 4.894 0 0 0 2.791-.877C15.383 46.262 17.144 48 19.316 48 22.957 48 23 41.027 23 40.957c0-6.061-2.477-11.86-2.582-12.103l-.346-.797zm16.67.943c-5.242 0-8.636 1.001-8.777 1.043l-.83.248.127.855c.02.138.518 3.416 1.88 6.627C31.395 43.078 34.483 46 37.845 46 42.704 46 44 43.161 44 41.484a4.94 4.94 0 0 0-.768-2.611C44.666 37.885 47 35.971 47 33.984 47 29.916 39.403 29 36.742 29z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||4cdn.org^\",\n\t\t\"||4chan.org^\",\n\t\t\"||4channel.org^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"500px\",\n\tName:    \"500px\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 5 14 L 2.5 26 L 6.800781 26 C 6.800781 26 7.699219 24.300781 10.199219 24.300781 C 12.699219 24.300781 14 26.199219 14 28.300781 C 14 30.402344 12.5 32.800781 10.199219 32.800781 C 7.898438 32.800781 6.5 30.398438 6.5 29 L 2 29 C 2 30.199219 3 36 10.199219 36 C 15.15625 36 17.417969 33.121094 18.015625 31.898438 C 19.386719 34.34375 21.992188 36 24.984375 36 C 27.253906 36 29.777344 34.808594 32.5 32.453125 C 35.222656 34.808594 37.746094 36 40.015625 36 C 44.417969 36 48 32.410156 48 28 C 48 23.589844 44.417969 20 40.015625 20 C 37.746094 20 35.222656 21.191406 32.5 23.546875 C 29.777344 21.191406 27.253906 20 24.984375 20 C 21.832031 20 19.105469 21.847656 17.8125 24.511719 C 17.113281 23.382813 15.414063 21 11.902344 21 C 8.101563 21 7.300781 22.597656 7.300781 22.597656 C 7.300781 22.597656 7.699219 21.300781 8.300781 18 L 17 18 L 17 14 Z M 24.984375 25 C 25.453125 25 26.800781 25.226563 29.230469 27.328125 L 30.011719 28 L 29.230469 28.671875 C 26.800781 30.773438 25.453125 31 24.984375 31 C 23.339844 31 22 29.652344 22 28 C 22 26.347656 23.339844 25 24.984375 25 Z M 40.015625 25 C 41.660156 25 43 26.347656 43 28 C 43 29.652344 41.660156 31 40.015625 31 C 39.546875 31 38.199219 30.773438 35.769531 28.671875 L 34.988281 28 L 35.769531 27.328125 C 38.199219 25.226563 39.546875 25 40.015625 25 Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||500px.com^\",\n\t\t\"||500px.org^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"9gag\",\n\tName:    \"9GAG\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 44 14 C 44 13.644531 43.8125 13.316406 43.507813 13.136719 C 40.453125 11.347656 28.46875 4.847656 25.535156 3.136719 C 25.222656 2.957031 24.839844 2.957031 24.527344 3.136719 C 21.128906 5.117188 10.089844 11.621094 7.496094 13.136719 C 7.1875 13.316406 7 13.644531 7 14 L 7 20 C 7 20.378906 7.214844 20.722656 7.550781 20.894531 C 7.660156 20.949219 18.597656 26.453125 24.5 29.867188 C 24.8125 30.046875 25.195313 30.046875 25.507813 29.863281 C 27.269531 28.828125 29.117188 27.859375 30.902344 26.921875 C 32.253906 26.214844 33.636719 25.488281 35.003906 24.722656 C 35.007813 26.820313 35.003906 29.296875 35 30.40625 L 25 35.859375 L 14.480469 30.121094 C 14.144531 29.9375 13.730469 29.964844 13.417969 30.1875 L 6.417969 35.1875 C 6.140625 35.386719 5.980469 35.714844 6.003906 36.054688 C 6.023438 36.398438 6.214844 36.707031 6.515625 36.871094 L 24.542969 46.871094 C 24.695313 46.957031 24.859375 47 25.027344 47 C 25.195313 47 25.363281 46.957031 25.515625 46.875 L 43.484375 36.875 C 43.804688 36.695313 44 36.363281 44 36 C 44 36 43.992188 21.011719 44 14 Z M 25 20 L 18 16 L 25 12 L 32 16 Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||9cache.com^\",\n\t\t\"||9gag.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"activision_blizzard\",\n\tName:    \"Activision Blizzard\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-237 0 1572 1572\\\"><path d=\\\"m549.1.2 548.4 1571.4H798l-74.2-200H374.5l-74.3 200H.7zM626 1085.1l-83-274.3-82.9 274.3z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||activision.com^\",\n\t\t\"||activisionblizzard.com^\",\n\t\t\"||callofduty.com^\",\n\t\t\"||callofdutyleague.com^\",\n\t\t\"||codmwest.com^\",\n\t\t\"||demonware.net^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"aliexpress\",\n\tName:    \"AliExpress\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\"  viewBox=\\\"0 0 50 50\\\"><path d=\\\"M9 4C6.25 4 4 6.25 4 9v32c0 2.75 2.25 5 5 5h32c2.75 0 5-2.25 5-5V9c0-2.75-2.25-5-5-5H9zm0 2h32c1.668 0 3 1.332 3 3v3.38A3.973 3.973 0 0 0 41 11H9a3.973 3.973 0 0 0-3 1.38V9c0-1.668 1.332-3 3-3zm6 11a1 1 0 0 1 1 1c0 4.962 4.037 9 9 9s9-4.038 9-9a1 1 0 1 1 2 0c0 6.065-4.935 11-11 11s-11-4.935-11-11a1 1 0 0 1 1-1z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||ae-rus.net^\",\n\t\t\"||ae-rus.ru^\",\n\t\t\"||aliexpress.com^\",\n\t\t\"||aliexpress.ru^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"amazon\",\n\tName:    \"Amazon\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 32 32\\\"><path d=\\\"M16.2,4c-3.3,0-6.9,1.2-7.7,5.3C8.4,9.7,8.7,10,9,10l3.3,0.3c0.3,0,0.6-0.3,0.6-0.6c0.3-1.4,1.5-2.1,2.8-2.1c0.7,0,1.5,0.3,1.9,0.9c0.5,0.7,0.4,1.7,0.4,2.5v0.5c-2,0.2-4.6,0.4-6.5,1.2c-2.2,0.9-3.7,2.8-3.7,5.7c0,3.6,2.3,5.4,5.2,5.4c2.5,0,3.8-0.6,5.7-2.5c0.6,0.9,0.9,1.4,2,2.3c0.3,0.1,0.6,0.1,0.8-0.1v0c0.7-0.6,2-1.7,2.7-2.3c0.3-0.2,0.2-0.6,0-0.9c-0.6-0.9-1.3-1.6-1.3-3.2v-5.4c0-2.3,0.2-4.4-1.5-6C20.1,4.4,17.9,4,16.2,4z M17.1,14.3c0.3,0,0.6,0,0.9,0v0.8c0,1.3,0.1,2.5-0.6,3.7c-0.5,1-1.4,1.6-2.4,1.6c-1.3,0-2.1-1-2.1-2.5C12.9,15.2,14.9,14.5,17.1,14.3z M26.7,22.4c-0.9,0-1.9,0.2-2.7,0.8c-0.2,0.2-0.2,0.4,0.1,0.4c0.9-0.1,2.8-0.4,3.2,0.1s-0.4,2.3-0.7,3.1c-0.1,0.2,0.1,0.3,0.3,0.2c1.5-1.2,1.9-3.8,1.6-4.2C28.3,22.5,27.6,22.4,26.7,22.4z M3.7,22.8c-0.2,0-0.3,0.3-0.1,0.4c3.3,3,7.6,4.7,12.4,4.7c3.4,0,7.4-1.1,10.2-3.1c0.5-0.3,0.1-0.9-0.4-0.7c-3.1,1.3-6.4,1.9-9.5,1.9c-4.5,0-8.8-1.2-12.4-3.3C3.8,22.9,3.7,22.8,3.7,22.8z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||a2z.com^\",\n\t\t\"||a2z.org.cn^\",\n\t\t\"||aboutamazon.cn^\",\n\t\t\"||aboutamazon.co.uk^\",\n\t\t\"||aboutamazon.com.au^\",\n\t\t\"||aboutamazon.com^\",\n\t\t\"||aboutamazon.de^\",\n\t\t\"||aboutamazon.es^\",\n\t\t\"||aboutamazon.eu^\",\n\t\t\"||aboutamazon.fr^\",\n\t\t\"||aboutamazon.in^\",\n\t\t\"||aboutamazon.it^\",\n\t\t\"||aboutamazon.jp^\",\n\t\t\"||aboutamazon.pl^\",\n\t\t\"||acmvalidations.com^\",\n\t\t\"||acmvalidationsaws.com^\",\n\t\t\"||aesworkshops.com^\",\n\t\t\"||aiv-cdn.net^\",\n\t\t\"||alexa.com^\",\n\t\t\"||alexafund.cn^\",\n\t\t\"||alexafund.com.cn^\",\n\t\t\"||amaaozn.com^\",\n\t\t\"||amazon-adsystem.com^\",\n\t\t\"||amazon-fashions.com^\",\n\t\t\"||amazon-jp-recruiting.com^\",\n\t\t\"||amazon-lantern.com^\",\n\t\t\"||amazon-launchpad.com^\",\n\t\t\"||amazon.ae^\",\n\t\t\"||amazon.ca^\",\n\t\t\"||amazon.cn^\",\n\t\t\"||amazon.co.jp^\",\n\t\t\"||amazon.co.uk^\",\n\t\t\"||amazon.com.au^\",\n\t\t\"||amazon.com.be^\",\n\t\t\"||amazon.com.br^\",\n\t\t\"||amazon.com.mx^\",\n\t\t\"||amazon.com.tr^\",\n\t\t\"||amazon.com^\",\n\t\t\"||amazon.de^\",\n\t\t\"||amazon.es^\",\n\t\t\"||amazon.fr^\",\n\t\t\"||amazon.in^\",\n\t\t\"||amazon.it^\",\n\t\t\"||amazon.jobs^\",\n\t\t\"||amazon.jp^\",\n\t\t\"||amazon.nl^\",\n\t\t\"||amazon.red^\",\n\t\t\"||amazon.se^\",\n\t\t\"||amazon.sg^\",\n\t\t\"||amazon^\",\n\t\t\"||amazonalexavoxcon.com^\",\n\t\t\"||amazonauthorinsights.com^\",\n\t\t\"||amazonaws-china.com^\",\n\t\t\"||amazonaws.cn^\",\n\t\t\"||amazonaws.co.uk^\",\n\t\t\"||amazonaws.com.cn^\",\n\t\t\"||amazonaws.com^$dnstype=~CNAME\",\n\t\t\"||amazonaws.tv^\",\n\t\t\"||amazonbusiness.cn^\",\n\t\t\"||amazonbusiness.com.cn^\",\n\t\t\"||amazonbusiness.org^\",\n\t\t\"||amazonbusinessblog.com^\",\n\t\t\"||amazonchoice.cn^\",\n\t\t\"||amazonchoice.com.cn^\",\n\t\t\"||amazonchoices.cn^\",\n\t\t\"||amazonchoices.com.cn^\",\n\t\t\"||amazondevicesupport.com^\",\n\t\t\"||amazonfctours.com^\",\n\t\t\"||amazonianblog.com^\",\n\t\t\"||amazonimages.com^\",\n\t\t\"||amazoninspire.cn^\",\n\t\t\"||amazoninspire.com.cn^\",\n\t\t\"||amazonlaunchpad.cn^\",\n\t\t\"||amazonlaunchpad.com.cn^\",\n\t\t\"||amazonlaunchpad.com^\",\n\t\t\"||amazonlending.com.cn^\",\n\t\t\"||amazonliterarypartnership.com^\",\n\t\t\"||amazonlumberyard.wang^\",\n\t\t\"||amazonnow.cn^\",\n\t\t\"||amazonnow.com.cn^\",\n\t\t\"||amazonpay.com^\",\n\t\t\"||amazonpay.in^\",\n\t\t\"||amazonprimevideo.cn^\",\n\t\t\"||amazonprimevideo.com.cn^\",\n\t\t\"||amazonprimevideos.com^\",\n\t\t\"||amazonsdi.com^\",\n\t\t\"||amazonses.com^\",\n\t\t\"||amazonstudiosguilds.com^\",\n\t\t\"||amazontrust.com^\",\n\t\t\"||amazonvideo.cc^\",\n\t\t\"||amazonvideo.com^\",\n\t\t\"||amazonvideodirect.com^\",\n\t\t\"||amazonwebservices.com.cn^\",\n\t\t\"||amazonworkdocs.cn^\",\n\t\t\"||amazonworkdocs.com.cn^\",\n\t\t\"||amazonworkdocs.com^\",\n\t\t\"||amplifyapp.com^\",\n\t\t\"||amplifyframework.com^\",\n\t\t\"||amzn.asia^\",\n\t\t\"||amzn.com^\",\n\t\t\"||amzn.to^\",\n\t\t\"||amznl.com^\",\n\t\t\"||asfiovnxocqpcry.com.cn^\",\n\t\t\"||assoc-amazon.cn^\",\n\t\t\"||associates-amazon.com^\",\n\t\t\"||audible.com^\",\n\t\t\"||aws-border.cn^\",\n\t\t\"||aws-icp-domain-manager.cn^\",\n\t\t\"||aws-iot-hackathon.com^\",\n\t\t\"||aws^\",\n\t\t\"||awsapps.cn^\",\n\t\t\"||awsapps.com.cn^\",\n\t\t\"||awsautopilot.com^\",\n\t\t\"||awsautoscaling.com^\",\n\t\t\"||awsbraket.com^\",\n\t\t\"||awscommandlineinterface.com^\",\n\t\t\"||awsdns-*.co.uk^\",\n\t\t\"||awsdns-*.com^\",\n\t\t\"||awsdns-*.net^\",\n\t\t\"||awsdns-*.org^\",\n\t\t\"||awsdns-cn-*.biz^\",\n\t\t\"||awsdns-cn-*.cn^\",\n\t\t\"||awsdns-cn-*.top^\",\n\t\t\"||awsedstart.com^\",\n\t\t\"||awseducate.com^\",\n\t\t\"||awseducate.net^\",\n\t\t\"||awseducate.org^\",\n\t\t\"||awsglobalaccelerator.com^\",\n\t\t\"||awsloft-johannesburg.com^\",\n\t\t\"||awsloft-stockholm.com^\",\n\t\t\"||awssecworkshops.com^\",\n\t\t\"||awsstatic.cn^\",\n\t\t\"||awsstatic.com^\",\n\t\t\"||awsthinkbox.com^\",\n\t\t\"||awstrack.me^\",\n\t\t\"||awstrust.com^\",\n\t\t\"||boxofficemojo.com^\",\n\t\t\"||cdkworkshop.com^\",\n\t\t\"||cloudfront-cn.net^\",\n\t\t\"||cloudfront-test.cn^\",\n\t\t\"||cloudfront.cn^\",\n\t\t\"||cloudfront.net^\",\n\t\t\"||containersonaws.com^\",\n\t\t\"||createspace.com^\",\n\t\t\"||elasticbeanstalk.com^\",\n\t\t\"||gameon-masters.com^\",\n\t\t\"||gdansk-amazon.com^\",\n\t\t\"||images-amazon.com^\",\n\t\t\"||imdb.com^\",\n\t\t\"||imdb.to^\",\n\t\t\"||imdb^\",\n\t\t\"||kindle.cn^\",\n\t\t\"||kindle.co.jp^\",\n\t\t\"||kindle.co.uk^\",\n\t\t\"||kindle.com^\",\n\t\t\"||kindle.de^\",\n\t\t\"||kindle.es^\",\n\t\t\"||kindle.fr^\",\n\t\t\"||kindle.in^\",\n\t\t\"||kindle.it^\",\n\t\t\"||kindle.jp^\",\n\t\t\"||kindle^\",\n\t\t\"||kindleoasis.cn^\",\n\t\t\"||kindleoasis.com.cn^\",\n\t\t\"||kindleoasis.com^\",\n\t\t\"||kindleoasis.info^\",\n\t\t\"||kindleoasis.jp^\",\n\t\t\"||kindleoasis.org^\",\n\t\t\"||kindleoasis.us^\",\n\t\t\"||kindleoasisnews.com^\",\n\t\t\"||kindleproject.com^\",\n\t\t\"||media-amazon.com^\",\n\t\t\"||media-imdb.com^\",\n\t\t\"||nwcdcloud.cn^\",\n\t\t\"||nwcdcloud.com.cn^\",\n\t\t\"||nwcddns.cn^\",\n\t\t\"||nwcdinfosec.cn^\",\n\t\t\"||prime-video.com^\",\n\t\t\"||primeday.cn^\",\n\t\t\"||primeday.com.cn^\",\n\t\t\"||primeday.info^\",\n\t\t\"||primevideo.cc^\",\n\t\t\"||primevideo.com^\",\n\t\t\"||primevideo.info^\",\n\t\t\"||primevideo.org^\",\n\t\t\"||primevideo.tv^\",\n\t\t\"||route53.cn^\",\n\t\t\"||sagemaker.com.cn^\",\n\t\t\"||serving-sys.com^\",\n\t\t\"||siege-amazon.com^\",\n\t\t\"||ss2.us^\",\n\t\t\"||ssl-images-amazon.com^\",\n\t\t\"||thinkboxsoftware.com^\",\n\t\t\"||ueberamazon.de^\",\n\t\t\"||xn--cckwcxetd^\",\n\t\t\"||xn--jlq480n2rg^\",\n\t\t\"||yamaxun.cn^\",\n\t\t\"||yamaxun.com^\",\n\t\t\"||yamaxun^\",\n\t\t\"||z.cn^\",\n\t\t\"||zappos^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"amazon_streaming\",\n\tName:    \"Amazon Streaming\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 32 32\\\"><path d=\\\"M16.2,4c-3.3,0-6.9,1.2-7.7,5.3C8.4,9.7,8.7,10,9,10l3.3,0.3c0.3,0,0.6-0.3,0.6-0.6c0.3-1.4,1.5-2.1,2.8-2.1c0.7,0,1.5,0.3,1.9,0.9c0.5,0.7,0.4,1.7,0.4,2.5v0.5c-2,0.2-4.6,0.4-6.5,1.2c-2.2,0.9-3.7,2.8-3.7,5.7c0,3.6,2.3,5.4,5.2,5.4c2.5,0,3.8-0.6,5.7-2.5c0.6,0.9,0.9,1.4,2,2.3c0.3,0.1,0.6,0.1,0.8-0.1v0c0.7-0.6,2-1.7,2.7-2.3c0.3-0.2,0.2-0.6,0-0.9c-0.6-0.9-1.3-1.6-1.3-3.2v-5.4c0-2.3,0.2-4.4-1.5-6C20.1,4.4,17.9,4,16.2,4z M17.1,14.3c0.3,0,0.6,0,0.9,0v0.8c0,1.3,0.1,2.5-0.6,3.7c-0.5,1-1.4,1.6-2.4,1.6c-1.3,0-2.1-1-2.1-2.5C12.9,15.2,14.9,14.5,17.1,14.3z M26.7,22.4c-0.9,0-1.9,0.2-2.7,0.8c-0.2,0.2-0.2,0.4,0.1,0.4c0.9-0.1,2.8-0.4,3.2,0.1s-0.4,2.3-0.7,3.1c-0.1,0.2,0.1,0.3,0.3,0.2c1.5-1.2,1.9-3.8,1.6-4.2C28.3,22.5,27.6,22.4,26.7,22.4z M3.7,22.8c-0.2,0-0.3,0.3-0.1,0.4c3.3,3,7.6,4.7,12.4,4.7c3.4,0,7.4-1.1,10.2-3.1c0.5-0.3,0.1-0.9-0.4-0.7c-3.1,1.3-6.4,1.9-9.5,1.9c-4.5,0-8.8-1.2-12.4-3.3C3.8,22.9,3.7,22.8,3.7,22.8z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||aiv-delivery.net^\",\n\t\t\"||amazonmusic.com^\",\n\t\t\"||amazonprimevideo.cn^\",\n\t\t\"||amazonprimevideo.com.cn^\",\n\t\t\"||amazonprimevideos.com^\",\n\t\t\"||amazonvideo.cc^\",\n\t\t\"||amazonvideo.com^\",\n\t\t\"||amazonvideodirect.com^\",\n\t\t\"||atv-ext-eu.amazon.com^\",\n\t\t\"||atv-ext-fe.amazon.com^\",\n\t\t\"||atv-ext.amazon.com^\",\n\t\t\"||atv-ps-eu.amazon.co.uk^\",\n\t\t\"||atv-ps-eu.amazon.com^\",\n\t\t\"||atv-ps-fe.amazon.co.jp^\",\n\t\t\"||atv-ps-fe.amazon.com^\",\n\t\t\"||atv-ps.amazon.com^\",\n\t\t\"||av-eu.amazon.com^\",\n\t\t\"||av-na.amazon.com^\",\n\t\t\"||music.a2z.com^\",\n\t\t\"||music.amazon.co.uk^\",\n\t\t\"||music.amazon.com^\",\n\t\t\"||music.amazon.in^\",\n\t\t\"||prime-video.com^\",\n\t\t\"||primevideo.cc^\",\n\t\t\"||primevideo.com^\",\n\t\t\"||primevideo.info^\",\n\t\t\"||primevideo.org^\",\n\t\t\"||primevideo.tv^\",\n\t\t\"||video.a2z.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"amino\",\n\tName:    \"Amino\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M9 4C6.24 4 4 6.24 4 9v32c0 2.76 2.24 5 5 5h32c2.76 0 5-2.24 5-5V9c0-2.76-2.24-5-5-5H9zm8.17 3.78a1.001 1.001 0 0 1 1.01 1c0 .37-.21.73-.57.9-.14.07-.29.1-.43.1a.987.987 0 0 1-.9-.56 1 1 0 0 1 .89-1.44zm15.66 0c.14 0 .29.03.43.1.36.17.56.53.56.9a.987.987 0 0 1-1 1c-.14 0-.29-.03-.43-.1a.998.998 0 0 1-.57-.9c0-.14.03-.29.1-.43a1 1 0 0 1 .91-.57zM13.77 9.93c.29 0 .58.13.78.38.15.18.22.4.22.62 0 .3-.13.59-.38.78a.963.963 0 0 1-.62.22c-.29 0-.58-.13-.78-.37a1.001 1.001 0 0 1 .78-1.63zm22.46 0c.22 0 .44.07.63.22a1.01 1.01 0 0 1 .15 1.41c-.2.24-.49.37-.78.37a.963.963 0 0 1-.62-.22.972.972 0 0 1-.38-.78c0-.22.07-.44.22-.62.2-.25.49-.38.78-.38zM25 10c8.27 0 15 6.73 15 15s-6.73 15-15 15-15-6.73-15-15 6.73-15 15-15zm-14.08 2.78a1.01 1.01 0 0 1 1.01 1 1.005 1.005 0 0 1-1.629.781 1 1 0 0 1 .619-1.782zm28.16 0c.29 0 .58.13.78.38.14.18.22.4.22.62 0 .29-.13.58-.38.78a1.005 1.005 0 0 1-1.629-.781 1.01 1.01 0 0 1 1.01-1zm-14.434 3.222a2.185 2.185 0 0 0-2.175 1.398l-5.09 13.59a.75.75 0 0 0 .7 1.01h1.458a.973.973 0 0 0 .75-.35c1.75-2 4.35-3.58 6.64-4.7l1.821 4.43a1.003 1.003 0 0 0 .92.62h1.25a.75.75 0 0 0 .7-1.01l-1.97-5.24c1.3-.52 2.19-.79 2.22-.79.8-.21 1.29-1.02 1.08-1.83a1.502 1.502 0 0 0-1.81-1.09c-.12.03-1.13.3-2.57.82l-2.01-5.35c-.25-.69-.79-1.25-1.5-1.44a2.223 2.223 0 0 0-.414-.068zm-15.867.187a1 1 0 0 1 1 1.01c0 .14-.03.292-.1.432a1.004 1.004 0 0 1-1.34.459.991.991 0 0 1-.458-1.33c.17-.36.528-.57.898-.57zm32.442 0c.37 0 .728.21.898.57a.985.985 0 0 1-.459 1.33 1.004 1.004 0 0 1-1.34-.459.971.971 0 0 1-.1-.43 1 1 0 0 1 1-1.01zM24.5 22.04c.24 0 .48.13.59.39l.66 1.622a27.9 27.9 0 0 0-1.61.83c-.543.304-1.09.633-1.63.988l1.4-3.44c.11-.26.35-.39.59-.39zM8.78 31.811c.37 0 .73.208.9.558a1 1 0 1 1-1.9.432c0-.36.2-.72.56-.89.14-.07.29-.1.44-.1zm32.44 0a.991.991 0 0 1 .898 1.43.995.995 0 0 1-1.329.47 1 1 0 0 1-.568-.91c0-.14.03-.292.1-.432.17-.35.53-.558.9-.558zm-30.3 3.41a1.005 1.005 0 0 1 1.01 1 1.01 1.01 0 0 1-1.01 1 1 1 0 0 1-.78-.381c-.14-.18-.22-.4-.22-.62 0-.29.13-.58.38-.78.18-.15.4-.22.62-.22zm28.16 0a1 1 0 1 1-.63 1.78.996.996 0 0 1-.38-.78 1.005 1.005 0 0 1 1.01-1zm-25.31 2.85c.22 0 .44.068.62.218.25.19.38.481.38.781 0 .22-.07.44-.22.62a1.002 1.002 0 0 1-1.41.16 1.01 1.01 0 0 1-.15-1.41c.2-.24.49-.37.78-.37zm22.46 0c.29 0 .58.128.78.368a1.001 1.001 0 0 1-.78 1.631c-.29 0-.58-.13-.78-.38a.958.958 0 0 1-.22-.62c0-.3.13-.59.38-.78a.963.963 0 0 1 .62-.22zm-19.05 2.15c.14 0 .29.03.43.1.36.17.57.53.57.9 0 .14-.03.29-.1.43a1.001 1.001 0 0 1-1.34.468.986.986 0 0 1-.56-.898.987.987 0 0 1 1-1zm15.64 0c.37 0 .73.198.9.558a1 1 0 1 1-1.9.442c0-.37.21-.73.57-.9.14-.07.29-.1.43-.1z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||aminoapps.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"apple_streaming\",\n\tName:    \"Apple Streaming\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M33.375 0c-2.836.191-5.871 1.879-7.75 4.156-1.645 2.004-3.023 4.946-2.5 8-.469-.144-.895-.16-1.406-.344-1.395-.496-2.989-1.03-4.969-1.03-3.934 0-7.96 2.34-10.5 6.25C2.555 22.71 3.297 32.706 8.906 41.25c.989 1.5 2.14 3.137 3.563 4.438 1.422 1.3 3.14 2.292 5.156 2.312 1.723.02 2.922-.555 4-1.031 1.078-.477 2.082-.899 3.969-.907h.031c1.879-.015 2.852.399 3.906.876 1.055.476 2.242 1.078 3.969 1.062 2.055-.016 3.8-1.14 5.25-2.531 1.45-1.39 2.64-3.098 3.625-4.594 1.41-2.148 1.977-3.32 3.063-5.719a1.001 1.001 0 0 0-.563-1.344C41.32 32.47 39.293 29.325 39 26c-.293-3.324 1.113-6.746 4.656-8.688a1 1 0 0 0 .508-.675 1.007 1.007 0 0 0-.195-.825c-2.543-3.16-6.121-5.03-9.625-5.03-2.235 0-3.875.527-5.219 1.03-.223.086-.387.079-.594.157 1.364-.719 2.567-1.715 3.469-2.875 1.64-2.106 2.906-5.102 2.438-8.25A.999.999 0 0 0 33.374 0Zm-1.063 2.375c-.066 2.02-.757 3.996-1.906 5.469-1.203 1.547-3.226 2.617-5.187 2.937.035-1.941.8-3.953 1.968-5.375 1.227-1.484 3.258-2.554 5.125-3.031ZM16.75 12.781c1.613 0 2.906.418 4.281.906 1.375.489 2.824 1.063 4.532 1.063 1.667 0 2.988-.578 4.28-1.063 1.294-.484 2.583-.906 4.5-.906 2.505 0 5.212 1.301 7.344 3.563-3.414 2.41-5.011 6.168-4.687 9.812.324 3.684 2.543 7.18 6.188 9-.79 1.719-1.31 2.856-2.47 4.625-.956 1.457-2.093 3.051-3.343 4.25-1.25 1.2-2.574 1.957-3.906 1.969-1.285.012-2.016-.371-3.125-.875-1.11-.504-2.543-1.082-4.75-1.063-2.203.012-3.657.567-4.782 1.063s-1.863.887-3.156.875c-1.367-.012-2.636-.676-3.843-1.781-1.208-1.106-2.297-2.614-3.25-4.063-5.25-8-5.672-17.398-2.657-22.031 2.211-3.402 5.723-5.344 8.844-5.344Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||applemusic.apple^\",\n\t\t\"||hls-svod-aoc-ve.itunes.g.aaplimg.com^\",\n\t\t\"||itun.es^\",\n\t\t\"||itunes.apple.com^\",\n\t\t\"||itunes.ca^\",\n\t\t\"||itunes.co.th^\",\n\t\t\"||itunes.co^\",\n\t\t\"||itunes.com^\",\n\t\t\"||itunes.es^\",\n\t\t\"||itunes.g.aaplimg.com^\",\n\t\t\"||itunes.hk^\",\n\t\t\"||itunes.mx^\",\n\t\t\"||itunes.org^\",\n\t\t\"||itunes.us^\",\n\t\t\"||music.apple.com^\",\n\t\t\"||tv.apple.com^\",\n\t\t\"||tv.g.apple.com^\",\n\t\t\"||tv.v.aaplimg.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"battle_net\",\n\tName:    \"Battle.net\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M43.11 22.15s3.95.2 3.95-2.12c0-3.03-5.26-5.77-5.26-5.77s.83-1.74 1.34-2.72a37.3 37.3 0 0 0 2.09-5.65c.16-1.1-.09-1.44-.09-1.44-.35 2.34-4.17 9.09-4.47 9.32-3.72-1.75-8.83-2.23-8.83-2.23S26.84 1 22.13 1c-4.67 0-4.65 9.02-4.65 9.02s-1.32-2.56-2.97-2.56c-2.42 0-3.22 3.67-3.22 7.64a37.8 37.8 0 0 0-9.16 1.17c-.36.1-1.49.92-.97.82 1.04-.34 5.95-1.1 10.25-.72.24 3.77 2.44 8.68 2.44 8.68S9.13 31.9 9.13 36.78c0 1.29.56 3.64 3.95 3.64 2.84 0 6.03-1.7 6.63-2.06a6.33 6.33 0 0 0-.91 2.83c0 .54.31 2.06 2.5 2.06 2.82 0 5.96-2.16 5.96-2.16s2.96 4.93 5.5 7.2c.69.6 1.34.71 1.34.71s-2.52-2.43-5.84-8.68c3.08-1.9 6.3-6.4 6.3-6.4l3.3.01c4.6 0 11.11-.96 11.11-4.61 0-3.77-5.86-7.17-5.86-7.17Zm.52-2.26c0 1.33-1.27 1.3-1.27 1.3l-.97.08s-1.82-.97-2.93-1.41c0 0 1.72-2.65 2.12-3.4.3.18 3.05 1.9 3.05 3.43ZM24.43 6.3c2.15 0 5.23 5.1 5.23 5.1s-4.8-.44-8.76 1.89c.1-3.67 1.34-7 3.52-7Zm-8.56 4.13c.69 0 1.36.83 1.64 1.54 0 .47.24 3.2.24 3.2l-3.96-.16c0-3.57 1.4-4.58 2.08-4.58Zm-.4 24.8c-2.17 0-2.62-1.2-2.62-2.29 0-2.45 1.96-5.9 1.96-5.9s2.2 4.63 6.04 6.59a10.02 10.02 0 0 1-5.39 1.6Zm7.02 4.85c-1.52 0-1.7-.98-1.7-1.21 0-.7.55-1.54.55-1.54s2.55-1.73 2.71-1.91l1.89 3.52s-1.93 1.14-3.45 1.14Zm4.74-1.92c-.93-1.62-1.6-3.3-1.6-3.3s3.78.24 5.82-1.86a11.2 11.2 0 0 1-5.65 1.07c4.93-4.34 7.8-7.48 10.23-10.74a9.46 9.46 0 0 0-1.6-1.15c-1.46 1.76-7.16 7.86-12.45 10.88-6.69-3.64-8.09-14.38-8.23-16.6l3.65.34s-1.37 2.44-1.37 4.23c0 1.79.21 1.89.21 1.89s-.04-3.13 1.89-5.54c1.46 7.82 3 11.83 4.19 14.22.6-.25 1.74-.76 1.74-.76s-3.38-9.73-3.19-16.31a13.8 13.8 0 0 1 6.36-1.66c6.73 0 12.14 2.9 12.14 2.9l-2.12 2.95s-1.89-3.42-4.55-4.03c1.4 1.05 2.98 2.44 3.8 4.43a68.4 68.4 0 0 0-14.47-3.59c-.19.8-.17 1.94-.17 1.94s9.03 1.66 15.6 5.43c-.05 8.21-9 14.53-10.23 15.26Zm8.55-6.14s2.8-3.68 2.76-8.55c0 0 4.52 2.8 4.52 5.54 0 3.05-7.28 3-7.28 3Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||battle.net^\",\n\t\t\"||battlenet.com.cn^\",\n\t\t\"||bnet.163.com^\",\n\t\t\"||bnet.cn^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"betano\",\n\tName:    \"Betano\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M9 3a1.0001 1.0001 0 0 0-1 1v42a1.0001 1.0001 0 0 0 1 1h17.7383c4.6 0 8.412-1.1874 11.0957-3.4395 2.6837-2.252 4.166-5.5679 4.166-9.494 0-5.0374-3.1967-8.9574-7.8125-10.422 3.3907-1.6449 5.7715-5.1833 5.7715-9.166 0-3.4752-1.3196-6.4288-3.7168-8.4297C33.845 4.048 30.4469 3 26.3379 3H9zm1 2h16.3379c3.769 0 6.6787.9611 8.623 2.584 1.9443 1.6228 2.998 3.9088 2.998 6.8945 0 3.8013-2.7237 7.2947-6.1718 8.045A1.0001 1.0001 0 0 0 31 23.5v.5a1.0001 1.0001 0 0 0 .871.9922c4.9067.6337 8.129 4.204 8.129 9.0742 0 3.4368-1.2203 6.0892-3.4531 7.9629C34.314 43.903 30.9943 45 26.7383 45H10V5zm7 4a1.0001 1.0001 0 0 0-1 1v12a1.0001 1.0001 0 0 0 1 1h6.0605c2.7307 0 4.9369-.546 6.5196-1.748S32 18.1768 32 16c0-2.1224-.7643-3.9577-2.1934-5.1816C28.3776 9.5944 26.3741 9 23.9961 9H17zm1 2h5.996c2.0451 0 3.5427.5096 4.5099 1.3379C29.473 13.1662 30 14.3314 30 16c0 1.7042-.5389 2.8304-1.629 3.6582C27.281 20.486 25.519 21 23.0606 21H18V11zm-1 15a1.0001 1.0001 0 0 0-1 1v13a1.0001 1.0001 0 0 0 1 1h7.629c2.8718 0 5.1969-.5884 6.8534-1.8887C33.139 37.811 34 35.8007 34 33.4277c0-2.3405-.902-4.3298-2.6074-5.5957C29.687 26.5662 27.3054 26 24.3418 26H17zm1 2h6.3418c2.7024 0 4.6504.5436 5.8574 1.4395C31.4062 30.3353 32 31.5613 32 33.4277c0 1.922-.5915 3.197-1.754 4.1094C29.0838 38.4496 27.223 39 24.629 39H18V28z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||betano.bg^\",\n\t\t\"||betano.ca^\",\n\t\t\"||betano.com^\",\n\t\t\"||betano.cz^\",\n\t\t\"||betano.de^\",\n\t\t\"||betano.ng^\",\n\t\t\"||betano.pt^\",\n\t},\n\tGroupID: \"gambling\",\n}, {\n\tID:      \"betfair\",\n\tName:    \"Betfair\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"10 240 160 160\\\"><path d=\\\"M137.95 269.613H96.725V290.3H74.688l42.65 49.825L160 290.3h-22.05v-20.688M20.337 351.587h22.05v20.7h41.226v-20.712h22.05l-42.638-49.788-42.688 49.8\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||betfair.com.au^\",\n\t\t\"||betfair.com^\",\n\t\t\"||betfair.es^\",\n\t\t\"||betfair.it^\",\n\t\t\"||betfair.ro^\",\n\t\t\"||betfair.se^\",\n\t},\n\tGroupID: \"gambling\",\n}, {\n\tID:      \"betway\",\n\tName:    \"Betway\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -150 418 418\\\"><path d=\\\"M23.73 24.54a42.662 42.662 0 0 1 5.76-1.28 45.4 45.4 0 0 1 7-.48 30.89 30.89 0 0 1 12.88 2.58 27.47 27.47 0 0 1 9.56 7A31.091 31.091 0 0 1 64.82 43a41 41 0 0 1 2 13 42.17 42.17 0 0 1-2.78 16 29.33 29.33 0 0 1-7.8 11.18 32.71 32.71 0 0 1-11.86 6.58 49.611 49.611 0 0 1-15 2.17 76.328 76.328 0 0 1-16.14-1.56A93.775 93.775 0 0 1 0 86.51V0h23.73v24.54zm0 49.09a22 22 0 0 0 6.24.82 12.66 12.66 0 0 0 9.9-4.27c2.533-2.853 3.797-7.217 3.79-13.09 0-5.7-1.243-9.903-3.73-12.61a12.22 12.22 0 0 0-9.42-4.07 17.06 17.06 0 0 0-3.53.34 28 28 0 0 0-3.25.88v32zm69.02-11.8c.18 4 1.47 7.097 3.87 9.29a12.91 12.91 0 0 0 9.15 3.33 16.75 16.75 0 0 0 7.66-1.63 18.48 18.48 0 0 0 5.9-5l14.51 11.39a32.174 32.174 0 0 1-5 5 33.45 33.45 0 0 1-14.65 6.72 54.08 54.08 0 0 1-10.51.94 39.47 39.47 0 0 1-13.22-2.17 29.73 29.73 0 0 1-18.1-17.15 38.33 38.33 0 0 1-2.71-15 39.27 39.27 0 0 1 2.58-14.71 31.76 31.76 0 0 1 7-10.92A29.2 29.2 0 0 1 90 25.09a37.8 37.8 0 0 1 13.36-2.31c9.853 0 17.693 3.187 23.52 9.56s8.787 16.203 8.88 29.49H92.75zm20.75-12.61a13.52 13.52 0 0 0-3.19-8.33 8.74 8.74 0 0 0-6.71-3.06A9.65 9.65 0 0 0 96.48 41a13.52 13.52 0 0 0-3.6 8.27l20.62-.05zm21.69-8.4V24.28h9.36V7.59h23.73v16.69h10.06l5.86 16.54h-15.92v26.57a15.07 15.07 0 0 0 .74 5.29c.5 1.27 1.61 1.9 3.33 1.9A10.62 10.62 0 0 0 177 73.5a23.121 23.121 0 0 0 4-2.44l6.1 14.37a35.208 35.208 0 0 1-9.69 4.75 40.998 40.998 0 0 1-12.14 1.62 27.2 27.2 0 0 1-9.83-1.56 16 16 0 0 1-6.44-4.4 16.68 16.68 0 0 1-3.46-6.92 37.889 37.889 0 0 1-1-9.08v-29l-9.35-.02zm70.3 49.63L176.17 7.56h22.94l17 51.84 10.17-35.12h15.42l10.17 35.12 9.76-35.12h24.27l-23.46 66.17h-18.17l-10.44-34.72-10.3 34.72zm117.63 0-1.9-5.16a22.659 22.659 0 0 1-7.32 4.41 28.91 28.91 0 0 1-10.71 1.83 26.2 26.2 0 0 1-8.55-1.35 19.5 19.5 0 0 1-6.84-4 18.58 18.58 0 0 1-4.55-6.3 20.58 20.58 0 0 1-1.62-8.41 18.07 18.07 0 0 1 6.84-14.78 27.46 27.46 0 0 1 7.4-4.2 40.168 40.168 0 0 1 8.94-2.31c2.9-.45 5.58-.79 8.07-1s4.77-.39 6.85-.48v-1.64c0-2.72-.86-4.7-2.58-6a10 10 0 0 0-6.1-1.9 18.55 18.55 0 0 0-7.39 1.49 30.17 30.17 0 0 0-6.66 4.1L286.37 33a42.588 42.588 0 0 1 12.14-7.46 43.002 43.002 0 0 1 16.07-2.71c8.667 0 15.58 2.103 20.74 6.31s7.737 10.737 7.73 19.59v41.72h-19.93zm-3.39-30c-1.36.09-2.71.2-4.07.34s-2.76.34-4.2.61c-2.8.45-4.8 1.42-6 2.91a7.79 7.79 0 0 0-1.76 5 6.08 6.08 0 0 0 2 4.68 7.73 7.73 0 0 0 5.49 1.83 12.9 12.9 0 0 0 4.67-.81 15.638 15.638 0 0 0 3.87-2.17V60.45z\\\"/><path d=\\\"M349.12 97.77a23.76 23.76 0 0 0 4.48 1.9 15.88 15.88 0 0 0 4.74.81 7.93 7.93 0 0 0 4.14-1 7.19 7.19 0 0 0 2.64-3.39l3.8-8.54-28.07-63.27h25.76L380 62.92l13.29-38.64h24.4l-33.08 78.37c-2.347 5.507-5.533 9.393-9.56 11.66a28.49 28.49 0 0 1-14.17 3.39 39.2 39.2 0 0 1-9.49-1.08 50.372 50.372 0 0 1-8.27-2.85l6-16z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||betway.be^\",\n\t\t\"||betway.bet.ar^\",\n\t\t\"||betway.co.za^\",\n\t\t\"||betway.com.gh^\",\n\t\t\"||betway.com.ng^\",\n\t\t\"||betway.com^\",\n\t\t\"||betway.de^\",\n\t\t\"||betway.es^\",\n\t\t\"||betway.fr^\",\n\t\t\"||betway.it^\",\n\t\t\"||betway.mx^\",\n\t\t\"||betway.pl^\",\n\t\t\"||betway.se^\",\n\t\t\"||betwaygroup.com^\",\n\t\t\"||betwaysatta.com^\",\n\t\t\"||vietnambetway88.com^\",\n\t},\n\tGroupID: \"gambling\",\n}, {\n\tID:      \"bigo_live\",\n\tName:    \"Bigo Live\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"3 3.5 41 41\\\" fill=\\\"currentColor\\\"><g fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\"><path d=\\\"M29.18 26.13c5.84-2.13 10.03-4.81 5.7-10.34.3-11.15-18.48-13.27-23.18-.61-.54 2.44-.36 7.7 1.48 11.92.68 1.6.15 8.1-2.23 5.16-1.12-1.52-2.16-1.58-2.6-1.42-2.14 3.95 3.39 7.32 4.6 7.34 3.02 3.5 5.59 3.07 7.77 3.23\\\"/><path d=\\\"M32.03 24.95c5 7.69 2.45 12.6-3.85 14.2M18.16 25.72c.97.92 2.4.74 3.38.06.19-1.83 1.67-1.76 1.92.11 1.2-.02 2.44-.16 2.67.7.14.9-.63 1.44-1.8 1.28.06 1.84-.86 1.78-2.93 1.6-.68.3-1.3.42-1.93.55m3.33-8.6c5.01 1.97 9.5 1.55 12.19-2.67\\\"/><ellipse cx=\\\"25.08\\\" cy=\\\"18.77\\\" rx=\\\"1.12\\\" ry=\\\"1.28\\\"/><ellipse cx=\\\"32.83\\\" cy=\\\"16.88\\\" rx=\\\".93\\\" ry=\\\"1.22\\\"/><path d=\\\"M20.52 40c6.87 13.72 14.2-18.17.53-6.2m13.52-4.57c11.28-7.01 2.68 13.07-1.86 8.09M25.83 6.57c-4.11-3.58-6.75-2.2-8.56 1.68-7.73-1.63-7.5 4.7-5.57 6.93-3.37 2.43-4.1 4.87.12 7.3-1.62.75-3.3 3.43 1.36 5.35-1.18.95-2.77 1.83-.12 3.4m24.07-15.36h2.63m-1.31-1.5v2.92\\\"/></g></svg>\"),\n\tRules: []string{\n\t\t\"||bigo.sg^\",\n\t\t\"||bigo.tv^\",\n\t\t\"||bigolive.tv^\",\n\t\t\"||bigovideo.tv^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"bilibili\",\n\tName:    \"Bilibili\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 48 48\\\" fill=\\\"currentColor\\\"><path d=\\\"M36.5 12h-7.09l3.8-3.8a1 1 0 1 0-1.42-1.4L26.6 12H21.4l-5.2-5.2a1 1 0 1 0-1.42 1.4l3.8 3.8H12.5A5.5 5.5 0 0 0 7 17.5v15a5.5 5.5 0 0 0 5.5 5.5h2a1.5 1.5 0 1 0 3 0h14a1.5 1.5 0 1 0 3 0h2a5.5 5.5 0 0 0 5.5-5.5v-15a5.5 5.5 0 0 0-5.5-5.5ZM39 32.5a2.5 2.5 0 0 1-2.5 2.5h-24a2.5 2.5 0 0 1-2.5-2.5v-15a2.5 2.5 0 0 1 2.5-2.5h24a2.5 2.5 0 0 1 2.5 2.5v15Z\\\"/><path d=\\\"m29.08 19.58-.87 2.6 6.71 2.24.87-2.6-6.71-2.24Zm-8.16 0-6.7 2.23.86 2.61 6.71-2.23-.87-2.61Zm7.11 7.95c-.19.59-.64.9-1.32.9l-.24-.03c-.02 0-.05-.02-.07 0a1.99 1.99 0 0 1-1.06-.47 2.37 2.37 0 0 1-.94-1.48s-.3 1.18-.9 1.5l-.25.16-.04.02a2.47 2.47 0 0 1-1.09.28c-.56-.05-.94-.33-1.13-.82l-.07-.17-1.42.66.05.14a2.82 2.82 0 0 0 2.74 1.77c.92 0 1.66-.3 2.2-.91.56.6 1.3.9 2.22.9a2.82 2.82 0 0 0 2.76-1.84l.05-.12-1.43-.66-.06.17Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"|upos-hz-mirrorakam.akamaized.net^\",\n\t\t\"||acg.tv^\",\n\t\t\"||acgvideo.com^\",\n\t\t\"||animetamashi.cn^\",\n\t\t\"||animetamashi.com^\",\n\t\t\"||anitama.cn^\",\n\t\t\"||anitama.net^\",\n\t\t\"||b23.tv^\",\n\t\t\"||bigfun.cn^\",\n\t\t\"||bili22.cn^\",\n\t\t\"||bili2233.cn^\",\n\t\t\"||bili23.cn^\",\n\t\t\"||bili33.cn^\",\n\t\t\"||biliapi.com^\",\n\t\t\"||biliapi.net^\",\n\t\t\"||bilibili.cc^\",\n\t\t\"||bilibili.cn^\",\n\t\t\"||bilibili.com^\",\n\t\t\"||bilibili.net^\",\n\t\t\"||bilibili.tv^\",\n\t\t\"||bilibiligame.cn^\",\n\t\t\"||bilibiligame.co^\",\n\t\t\"||bilibiligame.net^\",\n\t\t\"||bilibilipay.cn^\",\n\t\t\"||bilibilipay.com^\",\n\t\t\"||bilicdn1.com^\",\n\t\t\"||bilicdn2.com^\",\n\t\t\"||bilicdn3.com^\",\n\t\t\"||bilicdn4.com^\",\n\t\t\"||bilicdn5.com^\",\n\t\t\"||biligame.co^\",\n\t\t\"||biligame.com^\",\n\t\t\"||biligame.net^\",\n\t\t\"||biligo.com^\",\n\t\t\"||biliimg.com^\",\n\t\t\"||biliintl.com^\",\n\t\t\"||bilivideo.cn^\",\n\t\t\"||bilivideo.com^\",\n\t\t\"||bilivideo.net^\",\n\t\t\"||dreamcast.hk^\",\n\t\t\"||hdslb.com^\",\n\t\t\"||hdslb.org^\",\n\t\t\"||im9.com^\",\n\t\t\"||maoercdn.com^\",\n\t\t\"||mincdn.com^\",\n\t\t\"||yo9.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"blaze\",\n\tName:    \"Blaze\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-5 0 40 40\\\"><path d=\\\"m25.087 27.122-7.63 7.63a3.606 3.606 0 0 1-2.947 1.037 3.6 3.6 0 0 1-2.163-1.036l-7.63-7.631a3.603 3.603 0 0 1-1.058-2.553 3.6 3.6 0 0 1 1.058-2.556l7.63-7.63a3.605 3.605 0 0 1 2.86-1.046 3.61 3.61 0 0 1 2.25 1.045l7.63 7.631a3.601 3.601 0 0 1 1.058 2.553 3.6 3.6 0 0 1-1.058 2.556Zm4.225-5.226.007-.003-.037-.101c-.029-.074-.052-.149-.083-.222L24.571 9.112l-2.785 3.99L14.905 0S4.055 16.137 1.304 20.252h.005c-2.022 3.021-1.703 7.144.965 9.812l7.131 7.131a7.773 7.773 0 0 0 10.993 0l7.132-7.131c2.21-2.21 2.79-5.414 1.782-8.168Z\\\"/><path d=\\\"M14.902 17.182a2.244 2.244 0 1 0 0 4.488 2.244 2.244 0 0 0 0-4.488Zm0 10.283a2.244 2.244 0 1 0 0 4.488 2.244 2.244 0 0 0 0-4.488Zm5.141-5.142a2.244 2.244 0 1 0 0 4.489 2.244 2.244 0 0 0 0-4.489ZM7.516 24.567a2.245 2.245 0 1 0 4.489 0 2.245 2.245 0 0 0-4.49 0Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||blaze.bet^\",\n\t\t\"||blaze.com.br^\",\n\t\t\"||blaze.com^\",\n\t\t\"||blazecareers.com^\",\n\t},\n\tGroupID: \"gambling\",\n}, {\n\tID:      \"blizzard_entertainment\",\n\tName:    \"Blizzard Entertainment\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -32 128 128\\\"><path fill-rule=\\\"evenodd\\\" d=\\\"M105 2h3v1h2l2 1 1 1h3l1 1h4l1 1 2 2v1l1 3v4l1 2v6l-1 2v2l-1 3v2l-1 2v14l-1 2v1l-1 3-1 1h-3l-1 1h-6a5 5 0 0 0 1-6l2-1h-1l-1-3v-3a350 350 0 0 1 0-8l-1-3v-1l-1-1V9h1V6l-1-1-4-3Zm9 13v10h1v25a8 8 0 0 0 2-4l1-1 1-3V30l1-1v-2l1-1v-5l-1-2-2-3-1-1h-3Z\\\" clip-rule=\\\"evenodd\\\"/><path fill-rule=\\\"evenodd\\\" d=\\\"M101 24v1l2 1h1v2h1l1 2v5l1 2s0-1 0 0l1 7 1 2v7l-1 5h-2l-2-2-4-1 1-3 1-2a22 22 0 0 0-1-10l-1-4h-1l1-4-1-1-2-3v2l-1 1v3l1 6v4l1 1-1 3v4l1 1-1 2v4l-1-1a13 13 0 0 0-4-5l-2-2 2-5V27l-1-1v-4l-1-1v-5h-1a33 33 0 0 1 0-4l4-4h-2l-4-4h-1V3h10l2 1 2 1h1c2 0 2 1 3 2l2 3 1 3v1l-1 2v1a11 11 0 0 1-1 4l-4 3ZM96 9v13l1 1a3 3 0 0 0 1-1c1 0 2-1 2-3v-1l1-1v-3l-2-3-2-2h-1ZM26 3l1 1h1l2 3v5l1 1v2l-1 1v9l1 1 1 1-1 7v9l-1 1 1 1-1 1v8h3l1-1h7v-1h16v6l1 2h-6l-1-1h-2l-1-1H31a4 4 0 0 0-3-1l-1 1h-1l-1 1h-5l1-1a10 10 0 0 0 3-2v-9l1-1-1-1V35l1-1V21l-1-1v-4l1-1v-3l1-2-1-3h-1l-2-2-1-1 1-1h4Z\\\" clip-rule=\\\"evenodd\\\"/><path fill-rule=\\\"evenodd\\\" d=\\\"M84 60v-3l-1-2v-4l-3-2v-1l1-2a11 11 0 0 0 2-6l-1-1-3-2h-2v3l1 1h1l-1 2h-4l-2 1-2 1-1-2v-1l1-1 1-1 1-2v-5l1-1v-6l1-1v-3l1-1v-3l1-2 1-1-1-1 1-1 1-3 1-1V7l1-1c1-1 0-4 2-3l1 3 1 1 1 2v1l1 5 1 3v2l1 1v2l1 1v8l1 3v9l-1 1-2 5v3l-1 2v4l-1 1h-1Zm-4-36-1 1v2l-1 2v4l4 1h2v-7l-1-3-2-1-1-1v2Z\\\" clip-rule=\\\"evenodd\\\"/><path fill-rule=\\\"evenodd\\\" d=\\\"M77 4v1l-2 3v2l-1 2v1l-1 1-1 4v7h-1v2a5 5 0 0 1-1 2v2l-2 2v7l-1 2v2l-2 4v3-1h3v-1l3-1 1-1 3-2h3l1 1-1 1a3 3 0 0 0 0 1l1 1v5l-1 1h-7v-1h-2l-2 1h-4l-2 1-1-2v-2l1-1v-1l1-1-1-1 1-1v-2l1-2-1-2v-8l1-2 2-5-1-1 1-2v-1l1-1v-4l1-1 2-4v-2l1-1h-3V8h-1l-1 1-2 3-1 4h-1l-1-1v-2l1-1V4h16ZM32 4h9l1 2-3 2 1 2-1 1v13l1 2-1 2v6l-1 1v2l1 1v5l-1 1 1 2 1 1 2 1v2h-7l-2 1-1-1 3-2v-8a4 4 0 0 1 0-2l1-1v-3l-1-14v-2l1-1h-1V7l-2-1h-1l-1-1 1-1Zm12 0h14v15c-2 1-2 4-3 6v2c-1 0-3 1-2 4h-1l-2 3-1 2v3l-1 2-2 5h2l1-1h2l1-1c1-1 1-3 3-3l1-2 2-2h1l1 3h-1v1l-1 1v7h-8l-1 1-2-1h-3l-1-1 1-1v-3l-1-2 1-1-1-1 1-3v-2l1-2 1-3a7 7 0 0 1 2-4l1-4 2-2 2-3v-3h1l2-3V8l-3-1h-2l-1 1a3 3 0 0 0-2 3l-1 1v4l-1 1-1 1v-1l-1-1V4ZM17 22l1 1h1v3s0-1 0 0l2 1v5l1 2-1 8v3a6 6 0 0 1 0 2l-1 2-1 2-1 3-3 2-2 2-3 1-1-1-1 1H1l-1-1 2-1 1-4V26l1-1-1-3V11l1-1-1-1H2V8L1 7 0 6V5l1-1h15l1 1c2 0 3 1 3 2l1 3v6l-4 6Zm-6-11v9h1l1-2 2-1v-6h-1l-1-1h-2v1Zm0 19-1 1 1 2-1 3v9a2 2 0 0 0 0 1v6l-1 1 3-1 1-2h1v-4l1-4v-5l-1-1 1-3-1-1v-2s0 1 0 0v-1l-1-1a20 20 0 0 1-2-2v4Z\\\" clip-rule=\\\"evenodd\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||battle.net^\",\n\t\t\"||battlenet.com.cn^\",\n\t\t\"||blizzard.cn^\",\n\t\t\"||blizzard.com^\",\n\t\t\"||blizzardgames.cn^\",\n\t\t\"||blz-contentstack.com^\",\n\t\t\"||blzstatic.cn^\",\n\t\t\"||bnet.163.com^\",\n\t\t\"||bnet.cn^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"bluesky\",\n\tName:    \"Bluesky\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -33 568 568\\\"><path d=\\\"M123.12 33.66C188.24 82.56 258.28 181.68 284 234.87c25.72-53.19 95.76-152.32 160.88-201.2C491.87-1.62 568-28.92 568 57.94c0 17.34-9.95 145.71-15.78 166.55-20.27 72.46-94.15 90.94-159.87 79.75 114.87 19.55 144.1 84.31 80.98 149.07-119.86 123-172.27-30.86-185.7-70.28-2.46-7.23-3.61-10.6-3.63-7.73-.02-2.88-1.17.5-3.63 7.73-13.43 39.42-65.84 193.27-185.7 70.28-63.11-64.76-33.9-129.52 80.98-149.07-65.72 11.18-139.6-7.3-159.87-79.75C9.95 203.66 0 75.3 0 57.95 0-28.91 76.14-1.61 123.12 33.66Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||bsky.app^\",\n\t\t\"||bsky.social^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"box\",\n\tName:    \"Box\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -9 40 40\\\"><path d=\\\"M39.7 19.2c.5.7.4 1.6-.2 2.1-.7.5-1.7.4-2.2-.2l-3.5-4.5-3.4 4.4c-.5.7-1.5.7-2.2.2-.7-.5-.8-1.4-.3-2.1l4-5.2-4-5.2c-.5-.7-.3-1.7.3-2.2.7-.5 1.7-.3 2.2.3l3.4 4.5L37.3 7c.5-.7 1.4-.8 2.2-.3.7.5.7 1.5.2 2.2L35.8 14l3.9 5.2zm-18.2-.6c-2.6 0-4.7-2-4.7-4.6 0-2.5 2.1-4.6 4.7-4.6s4.7 2.1 4.7 4.6c-.1 2.6-2.2 4.6-4.7 4.6zm-13.8 0c-2.6 0-4.7-2-4.7-4.6 0-2.5 2.1-4.6 4.7-4.6s4.7 2.1 4.7 4.6c0 2.6-2.1 4.6-4.7 4.6zM21.5 6.4c-2.9 0-5.5 1.6-6.8 4-1.3-2.4-3.9-4-6.9-4-1.8 0-3.4.6-4.7 1.5V1.5C3.1.7 2.4 0 1.6 0 .7 0 0 .7 0 1.5v12.6c.1 4.2 3.5 7.5 7.7 7.5 3 0 5.6-1.7 6.9-4.1 1.3 2.4 3.9 4.1 6.8 4.1 4.3 0 7.8-3.4 7.8-7.7.1-4.1-3.4-7.5-7.7-7.5z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||box.com^\",\n\t\t\"||box.net^\",\n\t\t\"||boxcdn.net^\",\n\t\t\"||boxcloud.com^\",\n\t},\n\tGroupID: \"hosting\",\n}, {\n\tID:      \"canais_globo\",\n\tName:    \"Canais Globo\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 980 980\\\"><path d=\\\"M455.5 1.1a484.3 484.3 0 0 0-258 95.4 501.4 501.4 0 0 0-101.1 101A483.8 483.8 0 0 0 4 426.5 491.7 491.7 0 0 0 54.7 716a481.2 481.2 0 0 0 89.7 121.5C252.7 945.3 400 995.1 554 975.9c92.4-11.4 178-49.3 253.5-112 15-12.4 47.5-45.5 60.6-61.7A483.7 483.7 0 0 0 976 553.5a488.4 488.4 0 0 0-135.7-406.6A494.8 494.8 0 0 0 640.8 23.2 506.9 506.9 0 0 0 455.5 1.1zm-76.4 245.4c6.4 2.3 359.1 210.1 364.3 214.7 2.8 2.4 5.8 6.5 7.8 10.6 3.2 6.4 3.3 7.2 3.3 18.2s-.1 11.8-3.3 18.2c-2 4.1-5 8.2-7.8 10.6-6.7 5.9-358.7 212.7-365.3 214.6a42 42 0 0 1-29.1-2.6 46 46 0 0 1-18.6-19l-2.9-6.3v-431l2.9-6.2c2.7-6 9.5-13.6 15.7-17.6a44.3 44.3 0 0 1 33-4.2z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||canaisglobo.globo.com^\",\n\t\t\"||globosat.globo.com^\",\n\t\t\"||gsatmulti.globo.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"chatgpt\",\n\tName:    \"ChatGPT\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M45.403 25.562c-.506-1.89-1.518-3.553-2.906-4.862 1.134-2.665.963-5.724-.487-8.237-1.391-2.408-3.636-4.131-6.322-4.851-1.891-.506-3.839-.462-5.669.088C28.276 5.382 25.562 4 22.647 4c-4.906 0-9.021 3.416-10.116 7.991-.01.001-.019-.003-.029-.002-2.902.36-5.404 2.019-6.865 4.549-1.391 2.408-1.76 5.214-1.04 7.9.507 1.891 1.519 3.556 2.909 4.865-1.134 2.666-.97 5.714.484 8.234 1.391 2.408 3.636 4.131 6.322 4.851.896.24 1.807.359 2.711.359 1.003 0 1.995-.161 2.957-.45C21.722 44.619 24.425 46 27.353 46c4.911 0 9.028-3.422 10.12-8.003 2.88-.35 5.431-2.006 6.891-4.535 1.39-2.408 1.759-5.214 1.039-7.9zM35.17 9.543c2.171.581 3.984 1.974 5.107 3.919 1.049 1.817 1.243 4 .569 5.967-.099-.062-.193-.131-.294-.19l-9.169-5.294a1.0072 1.0072 0 0 0-1.01.006l-10.198 6.041-.052-4.607 8.663-5.001C30.733 9.26 33 8.963 35.17 9.543zm-5.433 12.652.062 5.504-4.736 2.805-4.799-2.699-.062-5.504 4.736-2.805 4.799 2.699zm-15.502-7.783C14.235 9.773 18.009 6 22.647 6c2.109 0 4.092.916 5.458 2.488-.105.056-.214.103-.318.163l-9.17 5.294c-.312.181-.504.517-.5.877l.133 11.851-4.015-2.258V14.412zm-7.707 9.509c-.581-2.17-.282-4.438.841-6.383 1.06-1.836 2.823-3.074 4.884-3.474-.004.116-.018.23-.018.348V25c0 .361.195.694.51.872l10.329 5.81-3.964 2.348-8.662-5.002c-1.946-1.123-3.338-2.936-3.92-5.107zm8.302 16.536c-2.171-.581-3.984-1.974-5.107-3.919-1.053-1.824-1.249-4.001-.573-5.97.101.063.196.133.299.193l9.169 5.294a.9998.9998 0 0 0 1.01-.006l10.198-6.041.052 4.607-8.663 5.001c-1.946 1.125-4.214 1.424-6.385.841zm20.935-4.869c0 4.639-3.773 8.412-8.412 8.412-2.119 0-4.094-.919-5.459-2.494.105-.056.216-.098.32-.158l9.17-5.294c.312-.181.504-.517.5-.877l-.134-11.85 4.015 2.258v10.003zm6.866-3.126c-1.056 1.83-2.84 3.086-4.884 3.483.004-.12.018-.237.018-.357V25c0-.361-.195-.694-.51-.872l-10.329-5.81 3.964-2.348 8.662 5.002c1.946 1.123 3.338 2.937 3.92 5.107.581 2.17.282 4.438-.841 6.383z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||chatgpt.com^\",\n\t\t\"||oaistatic.com^\",\n\t\t\"||oaiusercontent.com^\",\n\t\t\"||openai.com^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"claro\",\n\tName:    \"Claro\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -21 67 67\\\"><path d=\\\"M49.004 0c.933.01 1.866.002 2.8.003.003 2.842.001 5.684 0 8.525-.934.001-1.867.002-2.8.001 0-2.842-.002-5.686 0-8.529ZM55.2 9.622c2.564-2.63 5.1-5.292 7.662-7.926.657.69 1.334 1.36 1.978 2.064-2.535 2.654-5.096 5.282-7.632 7.933-.68-.679-1.339-1.38-2.008-2.07ZM6.091 8.06a7.942 7.942 0 0 1 2.155-.233c2.405-.058 4.742 1.202 6.232 3.131a8.516 8.516 0 0 1 1.514 3.12c-1.102.004-2.204 0-3.306 0-.486-1.001-1.23-1.893-2.2-2.413a4.756 4.756 0 0 0-1.728-.58c-.565-.012-1.142-.062-1.695.086a4.798 4.798 0 0 0-2.452 1.427c-.859.836-1.434 2.013-1.485 3.243-.11 1.171.105 2.399.749 3.384.619.944 1.494 1.73 2.53 2.135 1.739.666 3.843.265 5.174-1.095a6.18 6.18 0 0 0 1.118-1.604c1.098-.006 2.195-.006 3.292 0-.271 1.202-.863 2.316-1.611 3.27-.513.556-1.016 1.138-1.648 1.552-2.835 2.024-6.953 1.91-9.618-.379-.829-.73-1.586-1.572-2.107-2.57-.96-1.765-1.199-3.886-.859-5.863.286-1.676 1.135-3.22 2.305-4.4.987-1.065 2.25-1.868 3.64-2.21Zm11.58-.234h3.142c0 5.723.003 11.446-.002 17.169-1.047.002-2.093-.002-3.14-.001V7.826Zm9.493 3.417c.596-.125 1.205-.054 1.807-.07.698.062 1.398.166 2.062.41.665.24 1.35.54 1.817 1.111.548.676.742 1.574.785 2.435-.002 3.288.002 6.577-.002 9.866-1.062-.006-2.126.023-3.187-.015-.01-.447.009-.895-.007-1.341-.924.826-2.147 1.207-3.346 1.303-.756.135-1.54.013-2.261-.238a3.151 3.151 0 0 1-1.968-2.1c-.297-1.042-.235-2.183.112-3.204.377-1.04 1.285-1.78 2.284-2.117 1.28-.469 2.647-.541 3.97-.812.458 0 .91-.294 1.08-.74.123-.486.017-1.096-.397-1.405-.455-.311-1.011-.376-1.543-.392-.473.015-.973.02-1.392.28-.544.32-.788.956-.895 1.564-1.052-.023-2.105.001-3.157-.018.13-1.072.347-2.217 1.09-3.031.777-.943 1.982-1.392 3.148-1.486Zm2.316 7.423c-.622.149-1.234.34-1.866.44-.502.103-1.031.271-1.389.674-.497.608-.533 1.547-.148 2.224.168.31.5.463.809.574.997.2 2.091-.122 2.819-.864.746-.967.614-2.278.612-3.437-.294.097-.53.33-.837.389Zm11.644-7.032c.648-.164 1.284-.44 1.965-.375.007 1.111.065 2.224.043 3.337-.58-.083-1.184-.15-1.752.03-1.225.351-2.25 1.471-2.394 2.801-.008.293-.084.58-.087.873-.002 2.233.003 4.464 0 6.696-1.044-.002-2.09.008-3.134-.006-.012-4.395-.003-8.791.006-13.187 1.012-.01 2.023.03 3.035.068.001.543-.013 1.086.006 1.63.592-.819 1.369-1.527 2.312-1.867Zm7.824-.34c.466-.049.94-.095 1.409-.055 2.817.037 5.389 2.29 6.06 5.1.58 2.296.017 4.946-1.66 6.612-.97 1.086-2.302 1.823-3.714 2.044-.681.006-1.362.002-2.042.001-2.033-.296-3.8-1.735-4.802-3.557-1.042-2-1.046-4.535-.024-6.545.97-1.849 2.736-3.299 4.773-3.6Zm.253 3.255c-.938.22-1.737.902-2.215 1.757-.714 1.244-.6 2.94.29 4.06.907 1.354 2.77 1.811 4.194 1.117.828-.386 1.46-1.144 1.811-2.002.39-.985.32-2.141-.148-3.084-.492-.954-1.395-1.703-2.436-1.875-.497-.042-1.003-.057-1.496.027Zm9.407.496c2.796 0 5.594-.002 8.392.002-.002.963.002 1.927-.001 2.892-2.797-.004-5.593.006-8.39.001-.004-.965.003-1.93-.002-2.895Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||claro.com.ar^\",\n\t\t\"||claro.com.br^\",\n\t\t\"||claro.com.co^\",\n\t\t\"||claro.com.do^\",\n\t\t\"||claro.com.ec^\",\n\t\t\"||claro.com.gt^\",\n\t\t\"||claro.com.hn^\",\n\t\t\"||claro.com.ni^\",\n\t\t\"||claro.com.pa^\",\n\t\t\"||claro.com.pe^\",\n\t\t\"||claro.com.py^\",\n\t\t\"||claro.com.sv^\",\n\t\t\"||claro.com.uy^\",\n\t\t\"||claro.com^\",\n\t\t\"||claro.cr^\",\n\t\t\"||claro.net.br^\",\n\t\t\"||claro.net.co^\",\n\t\t\"||clarochile.cl^\",\n\t\t\"||claromusica.com^\",\n\t\t\"||claropr.com^\",\n\t\t\"||clarovideo.com^\",\n\t\t\"||usclaro.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"claude\",\n\tName:    \"Claude\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 40 40\\\"><path d=\\\"m7.8 26.3 7.7-4.4.1-.4v-.2h-1.8L9.4 21H5.5l-3.7-.2-1-.2-.8-1.2v-.6l.9-.5H2l2.5.3 3.8.2 2.7.2 4 .4h.7v-.3l-.2-.1-.1-.2-4-2.6-4.1-2.8L5 11.8 3.9 11l-.6-.8L3 8.6l1.1-1.2h1.4l.4.2 1.5 1.1 3.1 2.4 4.1 3 .6.6.3-.2v-.1l-.3-.5-2.2-4-2.4-4.1-1-1.7-.3-1A4.9 4.9 0 0 1 9 1.9L10.3.2 11 0l1.7.2.7.6 1 2.3L16 6.8l2.6 5 .7 1.5.4 1.4.2.4h.2v-.3l.2-2.8.4-3.4.4-4.5.1-1.2.7-1.5L23 .6l1 .4.8 1.2-.2.7-.4 3-1 4.8-.5 3.2h.3l.4-.4 1.6-2.1L27.8 8 29 6.6l1.4-1.5 1-.7H33l1.3 1.9-.6 1.9-1.7 2.2-1.5 1.9-2 2.8-1.4 2.2.2.2h.3l4.7-1 2.5-.5 3-.5 1.4.6.2.7-.6 1.3-3.2.8-3.8.8L26 21v.2l2.6.2h3.7l5 .4 1.3 1 .8 1-.1.8-2 1-2.7-.7-6.3-1.5-2.2-.5H26v.2l1.8 1.7 3.3 3 4.1 3.9.2 1-.5.7-.5-.1-3.7-2.7-1.4-1.3-3.1-2.7h-.3v.3l.8 1.1 3.8 5.8.2 1.8-.2.6-1 .3-1.1-.2-2.3-3.2-2.3-3.5-2-3.2-.1.1-1.1 12-.6.6-1.2.4-1-.7-.5-1.3.5-2.4.7-3.2.5-2.5.5-3.1.2-1v-.1h-.2L17 28.4l-3.6 4.9-2.8 3-.7.3-1.2-.6.2-1.1.6-1 4-5 2.3-3 1.5-1.9v-.2L6.7 30.6l-1.9.2L4 30l.1-1.2.4-.4 3.2-2.1Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||anthropic.com^\",\n\t\t\"||claude.ai^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"cloudflare\",\n\tName:    \"Cloudflare\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M38 10.813l-.906 3.78-1.907-3.405v1.718c2.899 2.301 4.926 5.79 5.126 9.688.699-.2 1.3-.188 2-.188 1.374 0 2.667.297 3.812.875l-1.031-.593 3.812-.875-3.812-.907L48.5 19h-3.813l2.813-2.688-3.688 1.094 2-3.312-3.312 2 1.094-3.688-2.688 2.781.094-3.874-2 3.28zM27 11c-5 0-9.414 2.992-11.313 7.594-.699-.399-1.687-.688-2.687-.688-3.2 0-5.906 2.606-5.906 5.907v.5c-3.899.3-7.094 3.68-7.094 7.78 0 .802.113 1.52.313 2.22.101.398.5.687 1 .687h47c.398 0 .675-.195.874-.594.5-1.101.813-2.207.813-3.406 0-4.2-3.488-7.594-7.688-7.594-.8 0-1.511.082-2.312.282l4.906 6.625-5.5-4.5L22 29.593l15.094-4.905L28.5 21.5l10.688 1.813v-.125C39.188 16.488 33.699 11 27 11zm19.781 12.656c.434.274.844.586 1.219.938h.5z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||argotunnel.com^\",\n\t\t\"||cf-ipfs.com^\",\n\t\t\"||cloudflare-dns.com^\",\n\t\t\"||cloudflare-ipfs.com^\",\n\t\t\"||cloudflare-quic.com^\",\n\t\t\"||cloudflare.com^\",\n\t\t\"||cloudflare.net^\",\n\t\t\"||cloudflare.tv^\",\n\t\t\"||cloudflareaccess.com^\",\n\t\t\"||cloudflareapps.com^\",\n\t\t\"||cloudflarebolt.com^\",\n\t\t\"||cloudflareclient.com^\",\n\t\t\"||cloudflareinsights.com^\",\n\t\t\"||cloudflareok.com^\",\n\t\t\"||cloudflarepreview.com^\",\n\t\t\"||cloudflareresolve.com^\",\n\t\t\"||cloudflaressl.com^\",\n\t\t\"||cloudflarestatus.com^\",\n\t\t\"||cloudflarestorage.com^\",\n\t\t\"||cloudflarestream.com^\",\n\t\t\"||cloudflaretest.com^\",\n\t\t\"||cloudflarewarp.com^\",\n\t\t\"||every1dns.net^\",\n\t\t\"||one.one.one^\",\n\t\t\"||pacloudflare.com^\",\n\t\t\"||pages.dev^\",\n\t\t\"||trycloudflare.com^\",\n\t\t\"||videodelivery.net^\",\n\t\t\"||warp.plus^\",\n\t\t\"||workers.dev^\",\n\t},\n\tGroupID: \"cdn\",\n}, {\n\tID:      \"clubhouse\",\n\tName:    \"Clubhouse\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M29.8 4a1 1 0 0 0-.92.7 1 1 0 0 0 .36 1.1 31.2 31.2 0 0 1 6 6.02 1 1 0 1 0 1.6-1.2 33.2 33.2 0 0 0-6.4-6.4A1 1 0 0 0 29.8 4Zm-7.16 1.06c-.46 0-.87.3-.99.74a1 1 0 0 0 .5 1.15 31.13 31.13 0 0 1 11.13 10.6 1 1 0 1 0 1.7-1.07A33.12 33.12 0 0 0 23.11 5.2a.96.96 0 0 0-.48-.14ZM14.5 7.01a3.42 3.42 0 0 0-3.27 2.28l-.26-.27A3.49 3.49 0 0 0 8.5 8.01c-.9 0-1.8.34-2.48 1.01a3.51 3.51 0 0 0-.57 4.17c-.52.15-1.01.42-1.43.84a3.52 3.52 0 0 0 0 4.94l.27.27c-.46.16-.9.41-1.27.79a3.52 3.52 0 0 0 0 4.94l.88.88 16.47 16.47a9.01 9.01 0 0 0 12.72 0l4.23-4.22a9.94 9.94 0 0 0 2.3-3.59l2.63-7.08a8.03 8.03 0 0 1 1.84-2.87l1.74-1.73 1-1a4.02 4.02 0 0 0 0-5.66 4.02 4.02 0 0 0-5.66 0l-1 1-.7.71-4.2 4.2a2.98 2.98 0 0 1-4.24 0L17.9 8.96l-.94-.94a3.49 3.49 0 0 0-2.47-1.01Zm0 1.98c.38 0 .76.15 1.06.45l.94.94 13.1 13.1a5.02 5.02 0 0 0 7.08 0l4.2-4.18.7-.71 1-1c.8-.8 2.05-.8 2.83 0 .8.79.8 2.04 0 2.83l-2.73 2.73a10.03 10.03 0 0 0-2.3 3.58l-2.63 7.08a8.02 8.02 0 0 1-1.84 2.87l-4.23 4.23a6.99 6.99 0 0 1-9.9 0L4.44 23.56a1.5 1.5 0 0 1 0-2.12c.59-.59 1.45-.55 2.08.08l.1.09 8.2 8.37a1 1 0 0 0 .97.29 1 1 0 0 0 .46-1.68l-9.52-9.73-.01-.01-1.28-1.29a1.5 1.5 0 0 1 0-2.12c.6-.6 1.47-.58 2.08.03l9.18 9.17a1 1 0 0 0 1.69-.43 1 1 0 0 0-.28-.98L9 14.13l-.06-.07-1.5-1.5c-.6-.6-.6-1.53 0-2.12a1.5 1.5 0 0 1 2.12 0L20.8 21.67a1 1 0 0 0 1.68-.44 1 1 0 0 0-.27-.97l-8.7-8.7-.06-.06a1.4 1.4 0 0 1-.01-2.06c.3-.3.68-.45 1.06-.45ZM4.23 32a1 1 0 0 0-.82 1.51c3 5.18 7.36 9.46 12.59 12.37a1 1 0 0 0 1.51-.89 1 1 0 0 0-.54-.86A31.16 31.16 0 0 1 5.15 32.5a1.01 1.01 0 0 0-.92-.51Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||clubhouse.com^\",\n\t\t\"||clubhouseapi.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"coolapk\",\n\tName:    \"CoolApk\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 384 384\\\"><path fill-rule=\\\"evenodd\\\" d=\\\"M105.62 104.96c30-1.4 57 6.8 81 24.5a717.7 717.7 0 0 0 34.5 27.5l29-48c12-7.4 21-4.8 27 8l79 142c3 15-3.3 22-18.5 20.5a3007.8 3007.8 0 0 1-103-76 166.46 166.46 0 0 1 25.5-17.5 574.67 574.67 0 0 1 33 24l.5-1a1227.62 1227.62 0 0 0-33-58 2174.49 2174.49 0 0 0-33.5 54c-15 25-35 45.6-59.5 61.5a104.39 104.39 0 0 1-90 6c-41.3-23.1-57.1-58-47.5-104.5a89.1 89.1 0 0 1 75.5-63zm1 31c23-1.6 43.7 4.6 62 18.5a777.4 777.4 0 0 1 26 20.5 668.04 668.04 0 0 0 25.5-17 318.5 318.5 0 0 1-38 57 95.75 95.75 0 0 1-49.5 32.5c-32.5 7-56.4-4.2-71.5-33.5-9.8-30.7-1-54.5 26.5-71.5 6.2-2.9 12.5-5 19-6.5z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||coolapk.com^\",\n\t\t\"||coolapkmarket.com^\",\n\t\t\"||coolapkmarket.net^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"copilot\",\n\tName:    \"Copilot\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"m26 32.1-1.7 5.6-1.9 6.4a4 4 0 0 1-3.8 2.9h-.1a5 5 0 0 1-4.8-3.6l-1.4-4.8A5 5 0 0 0 7.6 35h13.9a5 5 0 0 0 4.5-2.9m19.6 2.3C43.6 40.7 40.8 47 36 47H23a6 6 0 0 0 1.3-2.3c3-10.1 4.4-14.6 7.1-24.2.6-2 2.5-3.5 4.6-3.5h6.2c8.6 0 5.6 10 3.4 17.4M42.4 15H28.5a5 5 0 0 0-4.5 2.9l1.7-5.6 1.9-6.4A4 4 0 0 1 31.4 3h.1a5 5 0 0 1 4.8 3.6l1.4 4.8a5 5 0 0 0 4.7 3.6M27 3a6 6 0 0 0-1.3 2.3l-7.1 24.2C18 31.5 16 33 14 33H7.8c-8.6 0-5.6-10-3.4-17.4C6.4 9.3 9.2 3 14 3z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||copilot.ai^\",\n\t\t\"||copilot.cloud.microsoft^\",\n\t\t\"||copilot.com^\",\n\t\t\"||copilot.microsoft.com^\",\n\t\t\"||copilotstudio.microsoft.com^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"crunchyroll\",\n\tName:    \"Crunchyroll\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 25 3 C 12.85 3 3 12.85 3 25 C 3 40.188 13.387672 44.538609 20.388672 45.974609 C 20.427672 45.982609 20.465953 45.986328 20.501953 45.986328 C 21.006953 45.986328 21.206312 45.25525 20.695312 45.03125 C 13.285312 41.79025 8.0301562 34.327141 9.1601562 25.494141 C 10.256156 16.920141 17.244938 10.069141 25.835938 9.1191406 C 26.564937 9.0381406 27.287 9 28 9 C 35.541 9 42.044422 13.395672 45.107422 19.763672 C 45.206422 19.968672 45.382594 20.058594 45.558594 20.058594 C 45.853594 20.058594 46.144828 19.8075 46.048828 19.4375 C 44.302828 12.7105 39 3 25 3 z M 29 14 C 20.481 14 13.619625 21.101031 14.015625 29.707031 C 14.366625 37.346031 20.653016 43.631422 28.291016 43.982422 C 28.528016 43.994422 28.766 44 29 44 C 37.285 44 44 37.285 44 29 C 44 27.819 43.860563 26.670359 43.601562 25.568359 C 43.542563 25.319359 43.332234 25.183594 43.115234 25.183594 C 42.961234 25.183594 42.806266 25.251484 42.697266 25.396484 C 41.512266 26.976484 39.627 28 37.5 28 C 37.397 28 37.293453 27.997188 37.189453 27.992188 C 34.031453 27.845188 31.348203 25.317875 31.033203 22.171875 C 30.763203 19.477875 32.142297 17.082328 34.279297 15.861328 C 34.656297 15.646328 34.62475 15.100266 34.21875 14.947266 C 32.59375 14.340266 30.838 14 29 14 z M 44.296875 26.595703 L 44.300781 26.595703 L 44.296875 26.595703 z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||crunchyroll.com^\",\n\t\t\"||gccrunchyroll.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"dailymotion\",\n\tName:    \"Dailymotion\",\n\tIconSVG: []byte(\"<svg viewBox=\\\"0 0 24 24\\\" fill=\\\"currentColor\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\"><path d=\\\"M13.551 11.485a2.327 2.327 0 0 0-2.328 2.332c0 1.314 1.013 2.313 2.441 2.313l-.012.002c1.192 0 2.193-.983 2.193-2.28.001-1.349-1.001-2.367-2.294-2.367z\\\"/><path d=\\\"M3 3v18h18V3H3zm15.52 15.605h-2.682v-1.058c-.825.81-1.667 1.103-2.786 1.103-1.142 0-2.124-.371-2.947-1.114-1.086-.956-1.648-2.227-1.648-3.701 0-1.351.524-2.561 1.507-3.506.878-.859 1.946-1.298 3.139-1.298 1.14 0 2.018.385 2.647 1.192V6.118l2.77-.574v-.002l.002.003h-.002v13.06z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||dailymotion.com^\",\n\t\t\"||dm-event.net^\",\n\t\t\"||dmcdn.net^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"deepseek\",\n\tName:    \"DeepSeek\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 30 30\\\"><path d=\\\"M29.7 5.8c-.3-.1-.5.2-.7.3l-.1.2c-.5.5-1 .8-1.7.8a3 3 0 0 0-2.7 1c-.2-1-.8-1.5-1.6-2-.4-.1-.9-.3-1.2-.7l-.4-1c0-.2-.1-.4-.3-.4-.3 0-.4.1-.5.3-.4.7-.5 1.5-.5 2.3a5 5 0 0 0 2.3 4.3c.1 0 .2.2.1.4l-.3 1c0 .2-.2.3-.4.2-.8-.4-1.5-.9-2.2-1.5-1-1-2-2.2-3.2-3a13.8 13.8 0 0 0-.9-.7c-1.2-1.2.2-2.1.5-2.3.3 0 .1-.5-1-.5-1 0-2 .4-3.3.9a3.8 3.8 0 0 1-.6.1 12 12 0 0 0-3.6 0 7.8 7.8 0 0 0-5.6 3.2 9.6 9.6 0 0 0-1.6 7.6c.5 2.8 2 5.2 4.2 7 2.3 2 5 2.9 8 2.7 1.9 0 4-.3 6.3-2.3a7.3 7.3 0 0 0 4.4.3c.9-.2.8-1 .5-1.2-2.4-.8-2.7-1.1-2.7-1.1 1.4-1.7 3.4-3.3 4.3-8.8v-1c0-.3 0-.3.2-.4a5.2 5.2 0 0 0 2-.6c1.7-1 2.4-2.5 2.6-4.3 0-.3 0-.6-.3-.8zm-15.2 17C11.9 20.6 10.6 20 10 20c-.5 0-.4.6-.3 1l.4.9c.2.2.3.5 0 .7-1 .6-2.4-.1-2.5-.2a12.2 12.2 0 0 1-5.7-9.7c0-.5.1-.7.6-.7a5.9 5.9 0 0 1 1.9 0c2.7.3 5 1.5 6.8 3.4 1.1 1 2 2.3 2.8 3.6a17.3 17.3 0 0 0 4.2 4.5c-1 0-2.7.1-3.8-.8zm1.2-8.1a.4.4 0 0 1 .5-.4.4.4 0 0 1 .3.4.4.4 0 0 1-.4.3.4.4 0 0 1-.4-.3zm4 2-.8.2c-.4 0-.8-.2-1-.4-.4-.2-.6-.4-.7-1V15c.1-.5 0-.7-.3-1l-.8-.2a.7.7 0 0 1-.4-.1c-.1 0-.2-.2-.1-.4l.2-.3c.5-.3 1-.2 1.5 0 .4.2.7.5 1.2 1l.9 1.1.5 1c.1.3 0 .5-.3.7z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||deepseek.com^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"deezer\",\n\tName:    \"Deezer\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M18.81 4.16v3.03H24V4.16h-5.19zM6.27 8.38v3.027h5.189V8.38h-5.19zm12.54 0v3.027H24V8.38h-5.19zM6.27 12.594v3.027h5.189v-3.027h-5.19zm6.271 0v3.027h5.19v-3.027h-5.19zm6.27 0v3.027H24v-3.027h-5.19zM0 16.81v3.029h5.19v-3.03H0zm6.27 0v3.029h5.189v-3.03h-5.19zm6.271 0v3.029h5.19v-3.03h-5.19zm6.27 0v3.029H24v-3.03h-5.19Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||deezer.com^\",\n\t\t\"||dzcdn.net^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"directvgo\",\n\tName:    \"DirecTV Go\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-2 0 20 20\\\"><path d=\\\"M17 9.97A10.12 10.12 0 0 1 6.82 20H.46a.44.44 0 0 1-.34-.16.42.42 0 0 1-.11-.36A5.03 5.03 0 0 1 5 15.32h1.82a5.16 5.16 0 0 0 5.17-5.35v-.36a5.2 5.2 0 0 0-5.25-4.93H5a5.1 5.1 0 0 1-3.57-1.46A4.98 4.98 0 0 1 0 .54.45.45 0 0 1 .1.16.45.45 0 0 1 .47 0h6.26C12.36 0 16.95 4.44 17 9.97z\\\"/><path d=\\\"M12 9.97a9.95 9.95 0 0 1-2.9 7.09A9.85 9.85 0 0 1 2.04 20H.45a.43.43 0 0 1-.34-.16.43.43 0 0 1-.1-.36 4.94 4.94 0 0 1 4.86-4.16h1.77a5.36 5.36 0 0 0 5.27-4.89 4.32 4.32 0 0 0 0-.49v-.3a5.36 5.36 0 0 0-5.34-4.96h-1.7A4.92 4.92 0 0 1 1.4 3.22 5.02 5.02 0 0 1 .01.54.46.46 0 0 1 .45 0h1.5c5.51-.02 10 4.44 10.05 9.97z\\\"/><path d=\\\"M4 10a3 3 0 1 0 6 0 3 3 0 0 0-6 0z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||directvgo.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"discord\",\n\tName:    \"Discord\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M20.098 5.559C18.156 4 15.09 3.734 14.96 3.723a.493.493 0 0 0-.484.285c-.004.008-.172.504-.34.984 2.254.395 3.785 1.27 3.867 1.317a.8.8 0 1 1-.805 1.382C17.176 7.68 14.93 6.398 12 6.398c-2.93 0-5.176 1.282-5.2 1.293a.8.8 0 0 1-.805-1.383c.083-.046 1.622-.925 3.88-1.32-.172-.484-.348-.972-.352-.98a.487.487 0 0 0-.484-.285c-.129.011-3.195.273-5.16 1.855C2.852 6.528.8 12.074.8 16.871c0 .082.02.164.062.238 1.418 2.489 5.282 3.141 6.16 3.168h.016c.156 0 .3-.074.395-.199l.949-1.289c-2.086-.504-3.192-1.293-3.258-1.344a.799.799 0 0 1-.168-1.117.794.794 0 0 1 1.113-.172c.032.016 2.067 1.446 5.93 1.446 3.879 0 5.91-1.434 5.93-1.45a.8.8 0 0 1 .945 1.293c-.066.047-1.164.836-3.246 1.34l.937 1.293c.094.125.239.2.395.2h.016c.882-.028 4.742-.68 6.16-3.169a.477.477 0 0 0 .062-.242c0-4.793-2.05-10.34-3.101-11.308zM8.8 15.199c-.887 0-1.602-.894-1.602-2 0-1.105.715-2 1.602-2 .883 0 1.597.895 1.597 2 0 1.106-.714 2-1.597 2zm6.398 0c-.883 0-1.597-.894-1.597-2 0-1.105.714-2 1.597-2 .887 0 1.602.895 1.602 2 0 1.106-.715 2-1.602 2zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"|hammerandchisel.ssl.zendesk.com^\",\n\t\t\"||airhorn.solutions^\",\n\t\t\"||airhornbot.com^\",\n\t\t\"||bigbeans.solutions^\",\n\t\t\"||dis.gd^\",\n\t\t\"||discord-activities.com^\",\n\t\t\"||discord.co^\",\n\t\t\"||discord.com^\",\n\t\t\"||discord.design^\",\n\t\t\"||discord.dev^\",\n\t\t\"||discord.gg^\",\n\t\t\"||discord.gift^\",\n\t\t\"||discord.gifts^\",\n\t\t\"||discord.media^\",\n\t\t\"||discord.new^\",\n\t\t\"||discord.store^\",\n\t\t\"||discord.tools^\",\n\t\t\"||discordactivities.com^\",\n\t\t\"||discordapp.com^\",\n\t\t\"||discordapp.io^\",\n\t\t\"||discordapp.net^\",\n\t\t\"||discordcdn.com^\",\n\t\t\"||discordmerch.com^\",\n\t\t\"||discordpartygames.com^\",\n\t\t\"||discordsays.com^\",\n\t\t\"||discordstatus.com^\",\n\t\t\"||watchanimeattheoffice.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"discoveryplus\",\n\tName:    \"Discovery+\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"166 -24 320 320\\\"><path d=\\\"M346.4.1h-98.2v71.5a101 101 0 0 0-81.7 98.9c0 58.6 48.2 101 101.2 101h78.7c78.5 0 138.5-59.9 138.5-135.7C485 62.2 424.9.1 346.4.1Zm0 263.5h-78.7a93.3 93.3 0 0 1-11.5-185.7V8.1h90.2c68.5 0 130.5 52.9 130.5 127.7.1 77.2-61.9 127.8-130.5 127.8Z\\\"/><path d=\\\"M345.8 22h-77.3v56c45.5 0 92 37.7 92 93.2S315.3 251 315.3 251h30.5c61.5 0 117.7-45.1 117.7-114.4C463.5 66.1 403.1 22 345.8 22Z\\\"/><path d=\\\"M347.5 170a80 80 0 0 1-80 80 80 80 0 0 1-80-80 80 80 0 0 1 80-80 80 80 0 0 1 80 80z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||disco-api.com^\",\n\t\t\"||discoveryplus.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"disneyplus\",\n\tName:    \"Disney+\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M19.986,25.212c0,0,0.562-0.878,1.545-1.264s0.667,0.562,0.141,0.948C21.144,25.282,20.617,25.388,19.986,25.212z M20.512,23.631c-0.913,0.176-1.194,0.667-1.089,1.194C19.564,24.404,20.512,23.632,20.512,23.631z M34.771,27.882 c-0.387,0.351-1.159,1.616-1.159,1.616s1.194-0.141,1.546-1.089C35.508,27.46,35.157,27.531,34.771,27.882z M10.398,28.83 c0.667,0.597,1.861,1.089,1.861,1.089l0.07-2.247c-0.526,0-1.755,0.316-2.072,0.386C9.941,28.127,9.73,28.233,10.398,28.83z M46,13 v24c0,4.963-4.038,9-9,9H13c-4.962,0-9-4.037-9-9V13c0-4.963,4.038-9,9-9h24C41.962,4,46,8.037,46,13z M13.541,21.934l0.572,0.223 c0.073,0.006,0.143-0.029,0.182-0.091c2.349-3.742,6.583-6.256,11.456-6.256c5.798,0,10.708,3.552,12.578,8.508 c0.029,0.076,0.1,0.127,0.182,0.127h0.596c0.07,0,0.12-0.071,0.097-0.137c-1.904-5.398-7.217-9.294-13.46-9.294 c-5.214,0-9.786,2.724-12.282,6.764C13.421,21.842,13.466,21.927,13.541,21.934z M15.911,23.843c-2.81-1.37-3.793-1.546-5.971-1.405 c-2.177,0.141-2.177,1.229-1.651,1.265c0.311,0.021,0.487,0.044,0.58,0.055c0.078,0.009,0.089-0.011,0.115-0.046 c0.022-0.03,0.032-0.075,0.043-0.102c0.019-0.049-0.003-0.099-0.047-0.118c-0.045-0.02-0.55-0.245-0.55-0.245 s2.74-0.702,6.146,0.948c3.406,1.65,4.285,3.793,3.337,4.847c-0.948,1.054-2.599,1.581-4.636,1.124 c-0.035-1.44,0.035-2.494,0.035-2.494s1.897-0.07,2.389,0.281c0.42,0.3,0.174,0.497-0.103,0.614 c-0.047,0.02-0.052,0.107-0.009,0.132c0.393,0.218,0.706,0.269,0.99-0.078c0.316-0.386,0.667-2.037-3.196-1.932 c-0.07-0.667,0.141-1.124-0.421-1.791C12.4,24.228,12.4,26.722,12.4,26.722s-1.44,0.21-2.634,1.054 c-1.194,0.843,2.458,2.88,2.458,2.88s0.07,0.913,0.597,1.019c0.527,0.105,0.492-0.667,0.492-0.667s3.16,0.913,5.269-0.948 C20.688,28.197,18.721,25.212,15.911,23.843z M19.74,25.844c0.562,0.247,1.65,0.071,2.353-0.632c0.702-0.702,1.019-2.002-0.21-1.932 c0,0-0.457-0.597-1.44-0.316c-0.983,0.281-2.212,1.581-1.334,2.423C19.108,25.773,19.214,26.511,19.74,25.844z M20.723,30.305 c0-0.527-0.035-2.634-0.106-3.056c-0.07-0.421-0.456-0.773-0.667-0.351c0,0-0.176,2.037-0.106,3.301 C19.88,30.727,20.723,30.831,20.723,30.305z M22.654,27.53c0.737-0.07,1.265-0.106,1.897-0.21c0.632-0.105,0.667-0.562,0.175-0.667 c0,0-1.932-0.421-3.231,0.421c-0.632,0.421-0.457,1.194-0.106,1.229c0.351,0.035,2.142,0.106,2.564,0.386 c0.421,0.281,0.527,0.492,0.07,0.773c-0.457,0.281-1.475,0.421-1.897,0.281c-0.421-0.141-0.562-0.878,0.21-0.492 c0.773,0.386,2.177-0.07,1.229-0.386c-0.948-0.316-1.72-0.245-2.212,0.106c-0.492,0.351,0.176,1.089,0.737,1.334 c0.562,0.245,1.616,0.316,2.248-0.07c0.632-0.386,0.948-1.861-0.07-2.107c-1.019-0.245-1.651-0.21-2.107-0.281 C21.706,27.776,21.917,27.601,22.654,27.53z M27.783,26.338c-1.089-1.265-0.528,0.525-0.457,0.841 c0.07,0.316,0.351,1.51,0.351,2.002c-0.351-0.527-0.913-1.581-1.405-2.072c-0.492-0.492-0.632-0.07-0.632-0.07 s-0.492,1.791-0.351,2.95c0.141,1.159,0.737,0.351,0.773,0c0.035-0.351,0.106-1.51,0.106-1.826c0.316,0.702,0.808,1.51,1.194,1.897 c0.386,0.386,0.773,0.141,0.913-0.21S28.872,27.602,27.783,26.338z M31.54,29.917c0.386-0.316-0.071-0.631-0.457-0.595 c-0.386,0.035-1.51,0.21-1.51,0.21l0.245-0.737c0,0,0.737,0.035,1.334-0.035s0.176-0.737-0.035-0.737c-0.21,0-0.983,0-0.983,0 l0.176-0.492c0,0,1.299-0.035,1.72-0.106c0.421-0.07-0.245-0.773-0.737-0.808c-0.492-0.035-1.546,0.106-2.248,0.245 c-0.702,0.141,0.457,0.667,0.457,0.667s-0.106,0.351-0.245,0.597c-0.421,0.07-0.386,0.316-0.245,0.632 c-0.281,0.737-0.421,1.651,0.386,1.932C30.206,30.97,31.154,30.232,31.54,29.917z M35.826,27.882 c0.105-1.476-1.23-1.159-1.757-0.632c-0.527,0.527-1.44,1.897-1.44,1.897c0.106-0.913,0.351-1.159,0.913-2.072 c0.48-0.78-0.281-0.492-0.281-0.492s-1.791,1.51-1.019,3.231c-0.527,1.299-1.37,3.16-0.141,4.144c0.316,0.21,0.21-0.421,0.316-0.773 c0.106-0.351,0.457-2.142,0.808-2.634C34.07,30.481,35.58,30.165,35.826,27.882z M42.004,27.786c0.003-0.063-0.05-0.115-0.115-0.115 h-2.114c-0.044-0.699-0.128-1.445-0.282-2.146c-0.009-0.039-0.041-0.07-0.081-0.073c-0.287,0-0.331,0.002-0.601,0.002 c-0.078,0.002-0.131,0.075-0.111,0.149c0.088,0.323,0.216,1.147,0.258,2.067H36.95c-0.062,0-0.114,0.051-0.117,0.112l-0.053,0.532 c-0.004,0.085,0.069,0.155,0.154,0.146l2.048,0.007c0.012,0.754,0.018,1.207-0.046,2.004c-0.008,0.053,0.033,0.098,0.087,0.101 c0.322,0.02,0.526,0.045,0.647,0.047c0.049,0.001,0.089-0.036,0.093-0.085c0.019-0.256,0.068-1.06,0.044-2.064l2.08,0.007 c0.065,0,0.117-0.053,0.117-0.118V27.786z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||disney-plus.net^\",\n\t\t\"||disney.playback.edge.bamgrid.com^\",\n\t\t\"||disneynow.com^\",\n\t\t\"||disneyplus.com^\",\n\t\t\"||hotstar.com^\",\n\t\t\"||media.dssott.com^\",\n\t\t\"||star.playback.edge.bamgrid.com^\",\n\t\t\"||starplus.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"douban\",\n\tName:    \"Douban\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M36.54 18.51H13.315v8.012H36.54zM8.58 14.32h32.643v16.39H8.581zM5.146 5.754h39.516v4.193H5.145zm11.051 25.652c1.73 2.416 3.28 5.336 4.647 8.748h8.263c1.637-2.632 3.076-5.552 4.31-8.748l4.75 1.631c-1.241 2.694-2.581 5.07-4.003 7.117h11.615v4.147H4.028v-4.147h12.168c-1.117-2.203-2.568-4.574-4.371-7.117z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||douban.com^\",\n\t\t\"||douban.fm^\",\n\t\t\"||doubanio.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"dropbox\",\n\tName:    \"Dropbox\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -2.5 30 30\\\"><path d=\\\"M7.7.32.48 4.92 7.7 9.5l7.22-4.6 7.23 4.6 7.22-4.6L22.15.32l-7.23 4.6L7.7.31Zm0 18.38L.48 14.1 7.7 9.5l7.22 4.6-7.22 4.6Z\\\"/><path d=\\\"m14.92 14.1 7.23-4.6 7.22 4.6-7.22 4.6-7.23-4.6Zm0 10.72-7.22-4.6 7.22-4.59 7.23 4.6-7.23 4.59Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||addtodropbox.com^\",\n\t\t\"||app.hellosign.com^\",\n\t\t\"||dash.ai^\",\n\t\t\"||db.tt^\",\n\t\t\"||docsend.com^\",\n\t\t\"||dropbox-dns.com^\",\n\t\t\"||dropbox.com^\",\n\t\t\"||dropbox.tech^\",\n\t\t\"||dropbox.zendesk.com^\",\n\t\t\"||dropboxapi.com^\",\n\t\t\"||dropboxbusiness.com^\",\n\t\t\"||dropboxcaptcha.com^\",\n\t\t\"||dropboxforum.com^\",\n\t\t\"||dropboxforums.com^\",\n\t\t\"||dropboxinsiders.com^\",\n\t\t\"||dropboxlegal.com^\",\n\t\t\"||dropboxmail.com^\",\n\t\t\"||dropboxpartners.com^\",\n\t\t\"||dropboxstatic.com^\",\n\t\t\"||dropboxteam.com^\",\n\t\t\"||dropboxusercontent.com^\",\n\t\t\"||getdropbox.com^\",\n\t},\n\tGroupID: \"hosting\",\n}, {\n\tID:      \"ebay\",\n\tName:    \"eBay\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 12.601563 13.671875 L 12.636719 24.058594 C 12.632813 22.457031 12.128906 18.101563 6.464844 18.097656 C 0.210938 18.097656 -0.03125 22.964844 0.00390625 24.230469 C 0.00390625 24.230469 -0.304688 29.917969 6.3125 29.917969 C 11.996094 29.917969 12.277344 26.347656 12.277344 26.347656 L 9.664063 26.355469 C 9.664063 26.355469 9.152344 28.320313 6.320313 28.265625 C 2.683594 28.199219 2.546875 24.675781 2.546875 24.675781 L 12.621094 24.675781 C 12.621094 24.675781 12.628906 24.566406 12.636719 24.425781 L 12.644531 26.960938 C 12.644531 26.960938 12.628906 28.507813 12.535156 29.53125 L 14.984375 29.53125 L 15.089844 28.039063 C 15.089844 28.039063 16.230469 29.917969 19.566406 29.917969 C 22.902344 29.917969 25.535156 27.863281 25.609375 24.050781 C 25.675781 20.242188 22.761719 18.117188 19.617188 18.097656 C 16.472656 18.082031 15.121094 19.960938 15.121094 19.960938 L 15.121094 13.671875 Z M 31.054688 18.046875 C 29.566406 18.097656 26.539063 18.558594 26.132813 21.460938 L 28.796875 21.460938 C 28.796875 21.460938 29 19.6875 31.703125 19.738281 C 34.257813 19.785156 34.722656 21.039063 34.707031 22.578125 C 34.707031 22.578125 32.519531 22.585938 31.785156 22.59375 C 30.46875 22.597656 25.863281 22.742188 25.433594 25.550781 C 24.917969 28.890625 27.898438 29.933594 30.230469 29.917969 C 32.5625 29.90625 33.890625 29.207031 34.878906 27.953125 L 34.984375 29.511719 L 37.300781 29.496094 C 37.300781 29.496094 37.242188 28.628906 37.25 26.90625 C 37.257813 25.1875 37.308594 23.65625 37.25 22.574219 C 37.183594 21.316406 37.304688 18.285156 31.875 18.0625 C 31.875 18.0625 31.550781 18.03125 31.054688 18.046875 Z M 35.871094 18.519531 L 41.675781 29.496094 L 39.4375 33.71875 L 42.265625 33.71875 L 50 18.519531 L 47.359375 18.519531 L 43.074219 27.046875 L 38.796875 18.519531 Z M 6.402344 19.765625 C 9.984375 19.761719 9.984375 22.949219 9.984375 22.949219 L 2.628906 22.949219 C 2.628906 22.949219 2.804688 19.765625 6.402344 19.765625 Z M 19.035156 19.800781 C 23.078125 19.699219 22.949219 24.097656 22.949219 24.097656 C 22.949219 24.097656 23.011719 28.167969 19.042969 28.21875 C 15.070313 28.269531 15.136719 24.011719 15.136719 24.011719 C 15.136719 24.011719 14.992188 19.90625 19.035156 19.800781 Z M 34.734375 24.265625 C 34.734375 24.269531 35.195313 28.371094 30.664063 28.3125 C 30.664063 28.3125 28.136719 28.3125 27.988281 26.296875 C 27.832031 24.140625 31.875 24.269531 31.875 24.269531 Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"|ebay-*.s3-us-west-1.amazonaws.com^\",\n\t\t\"||21centuryaccess.com^\",\n\t\t\"||4ebaytraders.com^\",\n\t\t\"||adcommerce.cn^\",\n\t\t\"||adcommerce.tv^\",\n\t\t\"||appforebay.cn^\",\n\t\t\"||appsonebay.net^\",\n\t\t\"||asebay.com^\",\n\t\t\"||baazee.com^\",\n\t\t\"||bidbay.com^\",\n\t\t\"||bidorbuyindia.com^\",\n\t\t\"||billpoint.com^\",\n\t\t\"||billpoint.info^\",\n\t\t\"||billpoint.tv^\",\n\t\t\"||billpoint.us^\",\n\t\t\"||billpointnewzealand.com^\",\n\t\t\"||blogebay.com^\",\n\t\t\"||bookclubcorner.com^\",\n\t\t\"||builtfromebay.com^\",\n\t\t\"||buyitnow.com^\",\n\t\t\"||buyitnow.net^\",\n\t\t\"||buyitnow.org^\",\n\t\t\"||buyitnow.tv^\",\n\t\t\"||buyitnowshop.net^\",\n\t\t\"||cafr.ca^\",\n\t\t\"||carebay.com^\",\n\t\t\"||cargigileads.com^\",\n\t\t\"||cebay.com^\",\n\t\t\"||collective99.com^\",\n\t\t\"||commerceos.com^\",\n\t\t\"||connectcommerce.cn^\",\n\t\t\"||connectcommerce.com.cn^\",\n\t\t\"||connectcommerce.hk^\",\n\t\t\"||connectcommerce.info^\",\n\t\t\"||connectcommerce.tv^\",\n\t\t\"||connectedcommerce.cn^\",\n\t\t\"||connectedcommerce.com^\",\n\t\t\"||connectedcommerce.tv^\",\n\t\t\"||crececonebay.com^\",\n\t\t\"||creditcardsbay.com^\",\n\t\t\"||cyber-bay.cn^\",\n\t\t\"||cyber-bay.com.cn^\",\n\t\t\"||cyber-bay.info^\",\n\t\t\"||cyber-bay.org^\",\n\t\t\"||dba.dk^\",\n\t\t\"||dealbay.com^\",\n\t\t\"||dealtime.com^\",\n\t\t\"||didce.com^\",\n\t\t\"||douya.org^\",\n\t\t\"||dreamtoplay.com^\",\n\t\t\"||e-bay.com^\",\n\t\t\"||e-bay.it^\",\n\t\t\"||e-bay.net^\",\n\t\t\"||eachpay.com^\",\n\t\t\"||eachpay.net^\",\n\t\t\"||ebahy.com^\",\n\t\t\"||ebay-authenticate.net^\",\n\t\t\"||ebay-confirm.com^\",\n\t\t\"||ebay-course.com^\",\n\t\t\"||ebay-cz.com^\",\n\t\t\"||ebay-delivery.com^\",\n\t\t\"||ebay-discoveries.com^\",\n\t\t\"||ebay-fashion.com^\",\n\t\t\"||ebay-inc.com^\",\n\t\t\"||ebay-inc.net^\",\n\t\t\"||ebay-inc.org^\",\n\t\t\"||ebay-online.com^\",\n\t\t\"||ebay-sales.com^\",\n\t\t\"||ebay-stories.com^\",\n\t\t\"||ebay-us.com^\",\n\t\t\"||ebay-vacation.com^\",\n\t\t\"||ebay.at^\",\n\t\t\"||ebay.be^\",\n\t\t\"||ebay.ca^\",\n\t\t\"||ebay.ch^\",\n\t\t\"||ebay.cn^\",\n\t\t\"||ebay.co.nz^\",\n\t\t\"||ebay.co.uk^\",\n\t\t\"||ebay.co.ve^\",\n\t\t\"||ebay.co.za^\",\n\t\t\"||ebay.com.ar^\",\n\t\t\"||ebay.com.au^\",\n\t\t\"||ebay.com.cn^\",\n\t\t\"||ebay.com.ec^\",\n\t\t\"||ebay.com.hk^\",\n\t\t\"||ebay.com.mt^\",\n\t\t\"||ebay.com.my^\",\n\t\t\"||ebay.com.ph^\",\n\t\t\"||ebay.com.sg^\",\n\t\t\"||ebay.com^\",\n\t\t\"||ebay.de^\",\n\t\t\"||ebay.es^\",\n\t\t\"||ebay.fr^\",\n\t\t\"||ebay.ie^\",\n\t\t\"||ebay.in^\",\n\t\t\"||ebay.it^\",\n\t\t\"||ebay.jp^\",\n\t\t\"||ebay.lt^\",\n\t\t\"||ebay.mn^\",\n\t\t\"||ebay.net.cn^\",\n\t\t\"||ebay.nl^\",\n\t\t\"||ebay.org.cn^\",\n\t\t\"||ebay.org^\",\n\t\t\"||ebay.ph^\",\n\t\t\"||ebay.pk^\",\n\t\t\"||ebay.pl^\",\n\t\t\"||ebay.sg^\",\n\t\t\"||ebay.us^\",\n\t\t\"||ebay.vn^\",\n\t\t\"||ebay.yn.cn^\",\n\t\t\"||ebay.zj.cn^\",\n\t\t\"||ebay25.com^\",\n\t\t\"||ebay68.com^\",\n\t\t\"||ebaya.com^\",\n\t\t\"||ebayads.com^\",\n\t\t\"||ebayads.net^\",\n\t\t\"||ebayadvertising.cn^\",\n\t\t\"||ebayadvertising.com^\",\n\t\t\"||ebayanunsios.net^\",\n\t\t\"||ebayauction.com^\",\n\t\t\"||ebayaustralia.com^\",\n\t\t\"||ebayauthenticate.com.cn^\",\n\t\t\"||ebaybags.com^\",\n\t\t\"||ebaybank.com^\",\n\t\t\"||ebaybenefits.com^\",\n\t\t\"||ebayboutique.com^\",\n\t\t\"||ebayca.com^\",\n\t\t\"||ebayca.org^\",\n\t\t\"||ebaycafe.com^\",\n\t\t\"||ebaycar.com^\",\n\t\t\"||ebaycareers.com^\",\n\t\t\"||ebaycbt.co.kr^\",\n\t\t\"||ebaycdn.net^\",\n\t\t\"||ebaychina.net^\",\n\t\t\"||ebayclassifieds.cn^\",\n\t\t\"||ebayclassifieds.com.cn^\",\n\t\t\"||ebayclassifieds.com^\",\n\t\t\"||ebayclassifieds.info^\",\n\t\t\"||ebayclassifieds.org^\",\n\t\t\"||ebayclassifieds.tv^\",\n\t\t\"||ebayclassifiedsgroup.com^\",\n\t\t\"||ebayclassifiedsgroup.com^\",\n\t\t\"||ebayclassifiedsgroup.info^\",\n\t\t\"||ebayclassifiedsgroup.org^\",\n\t\t\"||ebayclassifies.com^\",\n\t\t\"||ebayclub.com^\",\n\t\t\"||ebaycoins.com^\",\n\t\t\"||ebaycom.com^\",\n\t\t\"||ebaycommercenetwork.com^\",\n\t\t\"||ebaycourse.com^\",\n\t\t\"||ebayd.com^\",\n\t\t\"||ebayde.com^\",\n\t\t\"||ebaydesc.cn^\",\n\t\t\"||ebaydesc.com.cn^\",\n\t\t\"||ebaydlassifieds.com^\",\n\t\t\"||ebaydns.cn^\",\n\t\t\"||ebaydts.com^\",\n\t\t\"||ebayedu.com^\",\n\t\t\"||ebayeletro.com^\",\n\t\t\"||ebayenterprise.cn^\",\n\t\t\"||ebayenterprise.com.cn^\",\n\t\t\"||ebayenterprise.com^\",\n\t\t\"||ebayenterprise.info^\",\n\t\t\"||ebayenterprise.net^\",\n\t\t\"||ebayenterprise.tv^\",\n\t\t\"||ebayetc.com^\",\n\t\t\"||ebayexpress.sg^\",\n\t\t\"||ebayfashion.com^\",\n\t\t\"||ebayfashion.net^\",\n\t\t\"||ebayforcharity.org^\",\n\t\t\"||ebayforeclosure.org^\",\n\t\t\"||ebayfrance.com^\",\n\t\t\"||ebayglobalshipping.com^\",\n\t\t\"||ebaygroup.com^\",\n\t\t\"||ebayhabit.com^\",\n\t\t\"||ebayheels.com^\",\n\t\t\"||ebayhots.com^\",\n\t\t\"||ebayimg.com^\",\n\t\t\"||ebayinc.com^\",\n\t\t\"||ebayinc.net^\",\n\t\t\"||ebayinc.org^\",\n\t\t\"||ebayincconnectedcommerce.net^\",\n\t\t\"||ebayinkblog.com^\",\n\t\t\"||ebayinternetsalestax.com^\",\n\t\t\"||ebayit.com^\",\n\t\t\"||ebayjewelry.com^\",\n\t\t\"||ebayjob.com^\",\n\t\t\"||ebayla.org^\",\n\t\t\"||ebaylisting.com^\",\n\t\t\"||ebaylocal.net^\",\n\t\t\"||ebaylocationsdevacances.com^\",\n\t\t\"||ebaymag.com^\",\n\t\t\"||ebaymainstreet.com^\",\n\t\t\"||ebaymall.com^\",\n\t\t\"||ebaymarketplace.net^\",\n\t\t\"||ebaymotors.ca^\",\n\t\t\"||ebaymotors.cn^\",\n\t\t\"||ebaymotors.com.cn^\",\n\t\t\"||ebaymotors.com^\",\n\t\t\"||ebaymotors.org^\",\n\t\t\"||ebaymotorsblog.com^\",\n\t\t\"||ebaynow.com^\",\n\t\t\"||ebaynyc.com^\",\n\t\t\"||ebayon.com^\",\n\t\t\"||ebayon.net^\",\n\t\t\"||ebayoncampus.com^\",\n\t\t\"||ebayopen.com^\",\n\t\t\"||ebayopensource.com^\",\n\t\t\"||ebayopensource.net^\",\n\t\t\"||ebaypakistan.net^\",\n\t\t\"||ebaypark.com^\",\n\t\t\"||ebayparts.com^\",\n\t\t\"||ebaypedia.cn^\",\n\t\t\"||ebaypedia.com.cn^\",\n\t\t\"||ebayprivacycenter.com^\",\n\t\t\"||ebayqq.com^\",\n\t\t\"||ebayradio.com^\",\n\t\t\"||ebayrtm.com^\",\n\t\t\"||ebayseller.com^\",\n\t\t\"||ebayshoesstore.com^\",\n\t\t\"||ebayshop.com^\",\n\t\t\"||ebayshop111.com^\",\n\t\t\"||ebayshopping.cn^\",\n\t\t\"||ebayshopping.com.cn^\",\n\t\t\"||ebayshopping.org^\",\n\t\t\"||ebaysocial.com^\",\n\t\t\"||ebaysocial.ru^\",\n\t\t\"||ebaysoho.com^\",\n\t\t\"||ebaysohos.com^\",\n\t\t\"||ebaystatic.cn^\",\n\t\t\"||ebaystatic.com^\",\n\t\t\"||ebaystore.com^\",\n\t\t\"||ebaystore77.com^\",\n\t\t\"||ebaystores.cn^\",\n\t\t\"||ebaystyle.com^\",\n\t\t\"||ebaysweden.com^\",\n\t\t\"||ebayt.com^\",\n\t\t\"||ebaytechblog.com^\",\n\t\t\"||ebaytopratedseller.net^\",\n\t\t\"||ebaytrading.com^\",\n\t\t\"||ebaytradingassistant.com^\",\n\t\t\"||ebaytv.org^\",\n\t\t\"||ebayuae.net^\",\n\t\t\"||ebayvakantiehuizen.com^\",\n\t\t\"||ebayvalet.com^\",\n\t\t\"||ebayvietnam.net^\",\n\t\t\"||ebayworlds.com^\",\n\t\t\"||ebayy.com^\",\n\t\t\"||edisebay.com^\",\n\t\t\"||eebay.com^\",\n\t\t\"||epinions.com^\",\n\t\t\"||eu-consumer-empowerment.com^\",\n\t\t\"||expertmaker.com^\",\n\t\t\"||fairmarket.com^\",\n\t\t\"||fragrancebay.com^\",\n\t\t\"||francemail.com^\",\n\t\t\"||half.com.cn^\",\n\t\t\"||half.com^\",\n\t\t\"||half.tv^\",\n\t\t\"||halfcanada.com^\",\n\t\t\"||halfjapan.com^\",\n\t\t\"||handbagsoutletebay.com^\",\n\t\t\"||iebay.com^\",\n\t\t\"||irribay.com^\",\n\t\t\"||itsbetterwhenyouwinit.com^\",\n\t\t\"||liketwice.com^\",\n\t\t\"||liveauction.com^\",\n\t\t\"||milofetch.com^\",\n\t\t\"||musicbay.net^\",\n\t\t\"||myconstructionworld.net^\",\n\t\t\"||myebay.com^\",\n\t\t\"||nebay.net^\",\n\t\t\"||paisapay.cc^\",\n\t\t\"||paisapay.info^\",\n\t\t\"||paisapay.tv^\",\n\t\t\"||premobay.com^\",\n\t\t\"||privatemarketplaces.net^\",\n\t\t\"||privatemarketplaces.us^\",\n\t\t\"||prostores.cn^\",\n\t\t\"||prostores.com.cn^\",\n\t\t\"||prostores.com^\",\n\t\t\"||rethink.net^\",\n\t\t\"||shopibay.net^\",\n\t\t\"||shoping.com^\",\n\t\t\"||sourcingforebay.com.cn^\",\n\t\t\"||sourcingforebay.net^\",\n\t\t\"||sourcingforebay.tv^\",\n\t\t\"||speybay.com^\",\n\t\t\"||storesense.com^\",\n\t\t\"||svpply.com^\",\n\t\t\"||telebay.com^\",\n\t\t\"||telesell.com^\",\n\t\t\"||texttobuy.org^\",\n\t\t\"||theebayshop.com^\",\n\t\t\"||theopportunityproject.org^\",\n\t\t\"||towerauction.com^\",\n\t\t\"||vendu.com^\",\n\t\t\"||watch-ebay.org^\",\n\t\t\"||weareebay.com^\",\n\t\t\"||wwwdecide.com^\",\n\t\t\"||wwwebay.com^\",\n\t\t\"||wwwebay.net^\",\n\t\t\"||wwwwebay.com^\",\n\t\t\"||xindelu.com^\",\n\t\t\"||xn--3et96bj49ahpq.com^\",\n\t\t\"||xn--4vq475g.com^\",\n\t\t\"||xn--4vq477m.com^\",\n\t\t\"||xn--7hv594h.com^\",\n\t\t\"||xn--7hvy28f.cn^\",\n\t\t\"||xn--hb4aw0g.com^\",\n\t\t\"||xn--q41am8x.com^\",\n\t\t\"||xn--qoq462m.com^\",\n\t\t\"||xn--tkry91n.com^\",\n\t\t\"||xn--ubt498knmf.com^\",\n\t\t\"||xn--xsq421m.com^\",\n\t\t\"||xn--xsq605n.com^\",\n\t\t\"||xn--xsq959n.com^\",\n\t\t\"||xn--yf1at58a.com^\",\n\t\t\"||xxbay.com^\",\n\t\t\"||yibei.org^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"electronic_arts\",\n\tName:    \"Electronic Arts\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 1000 1000\\\"><path d=\\\"M500 1000C224.3 1000 0 775.7 0 500S224.3 0 500 0s500 224.3 500 500-224.3 500-500 500zm84.63-693.4H302.05l-42.87 68.9h282.25zm57.75.66L469.63 582.33H278.02l44.2-68.96h114.85l43.87-68.93h-265.5l-43.86 68.93h62.9L147.2 651.05h364.2L645.9 438.9l49.05 74.46h-44.23l-41.88 68.96H739.8l45.48 68.72h83.54z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||ea.com^\",\n\t\t\"||eamobile.com^\",\n\t\t\"||easports.com^\",\n\t\t\"||nearpolar.com^\",\n\t\t\"||swtor.com^\",\n\t\t\"||tnt-ea.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"epic_games\",\n\tName:    \"Epic Games\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 10 3 C 6.69 3 4 5.69 4 9 L 4 41.240234 L 25 47.539062 L 46 41.240234 L 46 9 C 46 5.69 43.31 3 40 3 L 10 3 z M 11 8 L 15 8 L 15 11 L 11 11 L 11 18 L 14 18 L 14 21 L 11 21 L 11 28 L 15 28 L 15 31 L 11 31 C 9.34 31 8 29.66 8 28 L 8 11 C 8 9.34 9.34 8 11 8 z M 17 8 L 23 8 C 24.66 8 26 9.34 26 11 L 26 18 C 26 19.66 24.66 21 23 21 L 20 21 L 20 31 L 17 31 L 17 8 z M 28 8 L 31 8 L 31 31 L 28 31 L 28 8 z M 36 8 L 39 8 C 40.66 8 42 9.34 42 11 L 42 15 L 39 15 L 39 11 L 36 11 L 36 28 L 39 28 L 39 24 L 42 24 L 42 28 C 42 29.66 40.66 31 39 31 L 36 31 C 34.34 31 33 29.66 33 28 L 33 11 C 33 9.34 34.34 8 36 8 z M 20 11 L 20 18 L 23 18 L 23 11 L 20 11 z M 9 34 L 13 34 C 13.55 34 14 34.45 14 35 L 14 36 L 13 36 L 13 35.25 C 13 35.11 12.89 35 12.75 35 L 9.25 35 C 9.11 35 9 35.11 9 35.25 L 9 38.75 C 9 38.89 9.11 39 9.25 39 L 12.75 39 C 12.89 39 13 38.89 13 38.75 L 13 38 L 12 38 L 12 37 L 14 37 L 14 39 C 14 39.55 13.55 40 13 40 L 9 40 C 8.45 40 8 39.55 8 39 L 8 35 C 8 34.45 8.45 34 9 34 z M 18 34 L 19 34 L 22 40 L 21 40 L 20.5 39 L 16.5 39 L 16 40 L 15 40 L 18 34 z M 23 34 L 24 34 L 26 38 L 28 34 L 29 34 L 29 40 L 28 40 L 28 36 L 26.5 39 L 25.5 39 L 24 36 L 24 40 L 23 40 L 23 34 z M 30 34 L 35 34 L 35 35 L 31 35 L 31 36.5 L 33 36.5 L 33 37.5 L 31 37.5 L 31 39 L 35 39 L 35 40 L 30 40 L 30 34 z M 37 34 L 41 34 C 41.55 34 42 34.45 42 35 L 42 35.5 L 41 35.5 L 41 35.25 C 41 35.11 40.89 35 40.75 35 L 37.25 35 C 37.11 35 37 35.11 37 35.25 L 37 36.25 C 37 36.39 37.11 36.5 37.25 36.5 L 41 36.5 C 41.55 36.5 42 36.95 42 37.5 L 42 39 C 42 39.55 41.55 40 41 40 L 37 40 C 36.45 40 36 39.55 36 39 L 36 38.5 L 37 38.5 L 37 38.75 C 37 38.89 37.11 39 37.25 39 L 40.75 39 C 40.89 39 41 38.89 41 38.75 L 41 37.75 C 41 37.61 40.89 37.5 40.75 37.5 L 37 37.5 C 36.45 37.5 36 37.05 36 36.5 L 36 35 C 36 34.45 36.45 34 37 34 z M 18.5 35 L 17 38 L 20 38 L 18.5 35 z\\\"></path></svg>\"),\n\tRules: []string{\n\t\t\"|cdn*-epicgames-*.file.myqcloud.com^\",\n\t\t\"|epicgames-download*-*.file.myqcloud.com^\",\n\t\t\"|epicgames-download*.akamaized.net^\",\n\t\t\"||eac-cdn.com^\",\n\t\t\"||easy.ac^\",\n\t\t\"||easyanticheat.net^\",\n\t\t\"||epicgames.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"espn\",\n\tName:    \"ESPN\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M9.9 6a1 1 0 0 0-.98.89L7.9 15.86a1 1 0 0 0 1 1.11h34.66a1 1 0 0 0 .99-.88l1.02-8.98a1 1 0 0 0-1-1.11H9.91zM8.48 21a1 1 0 0 0-1 .89L5.02 43.92a1 1 0 0 0 .99 1.1h34.67a1 1 0 0 0 .99-.87l1.02-8.02a1 1 0 0 0-1-1.13H21.16l.48-4h20.49a1 1 0 0 0 .99-.87l1.02-8a1 1 0 0 0-1-1.13H8.48z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||es.pn^\",\n\t\t\"||espn.cl^\",\n\t\t\"||espn.co.uk^\",\n\t\t\"||espn.com.ar^\",\n\t\t\"||espn.com.au^\",\n\t\t\"||espn.com.co^\",\n\t\t\"||espn.com.ec^\",\n\t\t\"||espn.com.mx^\",\n\t\t\"||espn.com.pa^\",\n\t\t\"||espn.com.pe^\",\n\t\t\"||espn.com.uy^\",\n\t\t\"||espn.com.ve^\",\n\t\t\"||espn.com^\",\n\t\t\"||espn.in\",\n\t\t\"||espn.net^\",\n\t\t\"||espncdn.com^\",\n\t\t\"||espncricinfo.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"facebook\",\n\tName:    \"Facebook\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 27 27\\\"><path d=\\\"M12 0C5.371 0 0 5.371 0 12c0 6.016 4.434 10.984 10.207 11.852V15.18H7.238v-3.153h2.969V9.926c0-3.473 1.691-5 4.578-5 1.387 0 2.117.105 2.461.148v2.754h-1.969c-1.226 0-1.652 1.164-1.652 2.473v1.726h3.594l-.489 3.153h-3.105v8.699C19.48 23.082 24 18.074 24 12c0-6.629-5.371-12-12-12zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"|fbcdn-a.akamaihd.net^\",\n\t\t\"||aboutfacebook.com^\",\n\t\t\"||accessfacebookfromschool.com^\",\n\t\t\"||accountkit.com^\",\n\t\t\"||accountkit.com^\",\n\t\t\"||acebooik.com^\",\n\t\t\"||acebook.com^\",\n\t\t\"||advancediddetection.com^\",\n\t\t\"||askfacebook.net^\",\n\t\t\"||askfacebook.org^\",\n\t\t\"||atdmt2.com^\",\n\t\t\"||atlasdmt.com^\",\n\t\t\"||atlasonepoint.com^\",\n\t\t\"||atscaleconference.com^\",\n\t\t\"||botorch.org^\",\n\t\t\"||buck.build^\",\n\t\t\"||buckbuild.com^\",\n\t\t\"||buyingfacebooklikes.com^\",\n\t\t\"||careersatfb.com^\",\n\t\t\"||celebgramme.com^\",\n\t\t\"||china-facebook.com^\",\n\t\t\"||click-url.com^\",\n\t\t\"||como-hackearfacebook.com^\",\n\t\t\"||componentkit.org^\",\n\t\t\"||crowdtangle.com^\",\n\t\t\"||dacebook.com^\",\n\t\t\"||dlfacebook.com^\",\n\t\t\"||dotfacebook.com^\",\n\t\t\"||dotfacebook.net^\",\n\t\t\"||draftjs.org^\",\n\t\t\"||expresswifi.com^\",\n\t\t\"||f8.com^\",\n\t\t\"||faacebok.com^\",\n\t\t\"||faacebook.com^\",\n\t\t\"||faasbook.com^\",\n\t\t\"||facbebook.com^\",\n\t\t\"||facbeok.com^\",\n\t\t\"||facboo.com^\",\n\t\t\"||facbook.com^\",\n\t\t\"||facbool.com^\",\n\t\t\"||facboox.com^\",\n\t\t\"||faccebook.com^\",\n\t\t\"||faccebookk.com^\",\n\t\t\"||facdbook.com^\",\n\t\t\"||facdebook.com^\",\n\t\t\"||face-book.com^\",\n\t\t\"||faceabook.com^\",\n\t\t\"||facebboc.com^\",\n\t\t\"||facebbook.com^\",\n\t\t\"||facebboook.com^\",\n\t\t\"||facebcook.com^\",\n\t\t\"||facebdok.com^\",\n\t\t\"||facebgook.com^\",\n\t\t\"||facebhook.com^\",\n\t\t\"||facebkkk.com^\",\n\t\t\"||facebo-ok.com^\",\n\t\t\"||faceboak.com^\",\n\t\t\"||facebock.com^\",\n\t\t\"||facebocke.com^\",\n\t\t\"||facebof.com^\",\n\t\t\"||faceboik.com^\",\n\t\t\"||facebok.com^\",\n\t\t\"||facebokbook.com^\",\n\t\t\"||facebokc.com^\",\n\t\t\"||facebokk.com^\",\n\t\t\"||facebokok.com^\",\n\t\t\"||faceboks.com^\",\n\t\t\"||facebol.com^\",\n\t\t\"||facebolk.com^\",\n\t\t\"||facebomok.com^\",\n\t\t\"||faceboo.com^\",\n\t\t\"||facebooa.com^\",\n\t\t\"||faceboob.com^\",\n\t\t\"||faceboobok.com^\",\n\t\t\"||facebooc.com^\",\n\t\t\"||faceboock.com^\",\n\t\t\"||facebood.com^\",\n\t\t\"||facebooe.com^\",\n\t\t\"||faceboof.com^\",\n\t\t\"||facebooi.com^\",\n\t\t\"||facebooik.com^\",\n\t\t\"||facebooik.org^\",\n\t\t\"||facebooj.com^\",\n\t\t\"||facebook-corp.com^\",\n\t\t\"||facebook-covid-19.com^\",\n\t\t\"||facebook-ebook.com^\",\n\t\t\"||facebook-forum.com^\",\n\t\t\"||facebook-hardware.com^\",\n\t\t\"||facebook-inc.com^\",\n\t\t\"||facebook-login.com^\",\n\t\t\"||facebook-newsroom.com^\",\n\t\t\"||facebook-newsroom.org^\",\n\t\t\"||facebook-pmdcenter.com^\",\n\t\t\"||facebook-pmdcenter.net^\",\n\t\t\"||facebook-pmdcenter.org^\",\n\t\t\"||facebook-privacy.com^\",\n\t\t\"||facebook-program.com^\",\n\t\t\"||facebook-studio.com^\",\n\t\t\"||facebook-support.org^\",\n\t\t\"||facebook-texas-holdem.com^\",\n\t\t\"||facebook-texas-holdem.net^\",\n\t\t\"||facebook.br^\",\n\t\t\"||facebook.ca^\",\n\t\t\"||facebook.cc^\",\n\t\t\"||facebook.com^\",\n\t\t\"||facebook.design^\",\n\t\t\"||facebook.hu^\",\n\t\t\"||facebook.in^\",\n\t\t\"||facebook.net^\",\n\t\t\"||facebook.nl^\",\n\t\t\"||facebook.org^\",\n\t\t\"||facebook.se^\",\n\t\t\"||facebook.shop^\",\n\t\t\"||facebook.tv^\",\n\t\t\"||facebook.us^\",\n\t\t\"||facebook.wang^\",\n\t\t\"||facebook123.org^\",\n\t\t\"||facebook30.com^\",\n\t\t\"||facebook30.net^\",\n\t\t\"||facebook30.org^\",\n\t\t\"||facebook4business.com^\",\n\t\t\"||facebookads.com^\",\n\t\t\"||facebookadvertisingsecrets.com^\",\n\t\t\"||facebookappcenter.info^\",\n\t\t\"||facebookappcenter.net^\",\n\t\t\"||facebookappcenter.org^\",\n\t\t\"||facebookatschool.com^\",\n\t\t\"||facebookawards.com^\",\n\t\t\"||facebookblueprint.net^\",\n\t\t\"||facebookbrand.com^\",\n\t\t\"||facebookbrand.net^\",\n\t\t\"||facebookcanadianelectionintegrityinitiative.com^\",\n\t\t\"||facebookcareer.com^\",\n\t\t\"||facebookcheats.com^\",\n\t\t\"||facebookck.com^\",\n\t\t\"||facebookclub.com^\",\n\t\t\"||facebookcom.com^\",\n\t\t\"||facebookconnect.com^\",\n\t\t\"||facebookconsultant.org^\",\n\t\t\"||facebookcoronavirus.com^\",\n\t\t\"||facebookcovers.org^\",\n\t\t\"||facebookcredits.info^\",\n\t\t\"||facebookdating.net^\",\n\t\t\"||facebookdevelopergarage.com^\",\n\t\t\"||facebookdusexe.org^\",\n\t\t\"||facebookemail.com^\",\n\t\t\"||facebookenespanol.com^\",\n\t\t\"||facebookexchange.com^\",\n\t\t\"||facebookexchange.net^\",\n\t\t\"||facebookfacebook.com^\",\n\t\t\"||facebookflow.com^\",\n\t\t\"||facebookgames.com^\",\n\t\t\"||facebookgraphsearch.com^\",\n\t\t\"||facebookgraphsearch.info^\",\n\t\t\"||facebookgroups.com^\",\n\t\t\"||facebookhome.cc^\",\n\t\t\"||facebookhome.com^\",\n\t\t\"||facebookhome.info^\",\n\t\t\"||facebookhub.com^\",\n\t\t\"||facebooki.com^\",\n\t\t\"||facebookinc.com^\",\n\t\t\"||facebookland.com^\",\n\t\t\"||facebooklikeexchange.com^\",\n\t\t\"||facebooklive.com^\",\n\t\t\"||facebooklivestaging.net^\",\n\t\t\"||facebooklivestaging.org^\",\n\t\t\"||facebooklogin.com^\",\n\t\t\"||facebooklogin.info^\",\n\t\t\"||facebookloginhelp.net^\",\n\t\t\"||facebooklogs.com^\",\n\t\t\"||facebookmail.com^\",\n\t\t\"||facebookmail.tv^\",\n\t\t\"||facebookmanager.info^\",\n\t\t\"||facebookmarketing.info^\",\n\t\t\"||facebookmarketingpartner.com^\",\n\t\t\"||facebookmarketingpartners.com^\",\n\t\t\"||facebookmobile.com^\",\n\t\t\"||facebookmsn.com^\",\n\t\t\"||facebooknews.com^\",\n\t\t\"||facebooknfl.com^\",\n\t\t\"||facebooknude.com^\",\n\t\t\"||facebookofsex.com^\",\n\t\t\"||facebookook.com^\",\n\t\t\"||facebookpaper.com^\",\n\t\t\"||facebookpay.com^\",\n\t\t\"||facebookphonenumber.net^\",\n\t\t\"||facebookphoto.com^\",\n\t\t\"||facebookphotos.com^\",\n\t\t\"||facebookpmdcenter.com^\",\n\t\t\"||facebookpoke.net^\",\n\t\t\"||facebookpoke.org^\",\n\t\t\"||facebookpoker.info^\",\n\t\t\"||facebookpokerchips.info^\",\n\t\t\"||facebookporn.net^\",\n\t\t\"||facebookporn.org^\",\n\t\t\"||facebookporno.net^\",\n\t\t\"||facebookportal.com^\",\n\t\t\"||facebooks.com^\",\n\t\t\"||facebooksafety.com^\",\n\t\t\"||facebooksecurity.net^\",\n\t\t\"||facebookshop.com^\",\n\t\t\"||facebooksignup.net^\",\n\t\t\"||facebooksite.net^\",\n\t\t\"||facebookstories.com^\",\n\t\t\"||facebookstudios.net^\",\n\t\t\"||facebookstudios.org^\",\n\t\t\"||facebooksupplier.com^\",\n\t\t\"||facebooksuppliers.com^\",\n\t\t\"||facebookswagemea.com^\",\n\t\t\"||facebookswagstore.com^\",\n\t\t\"||facebooksz.com^\",\n\t\t\"||facebookthreads.net^\",\n\t\t\"||facebooktv.net^\",\n\t\t\"||facebooktv.org^\",\n\t\t\"||facebookvacation.com^\",\n\t\t\"||facebookw.com^\",\n\t\t\"||facebookwork.com^\",\n\t\t\"||facebookworld.com^\",\n\t\t\"||facebool.com^\",\n\t\t\"||facebool.info^\",\n\t\t\"||facebooll.com^\",\n\t\t\"||faceboom.com^\",\n\t\t\"||faceboon.com^\",\n\t\t\"||faceboonk.com^\",\n\t\t\"||faceboooik.com^\",\n\t\t\"||faceboook.com^\",\n\t\t\"||faceboop.com^\",\n\t\t\"||faceboot.com^\",\n\t\t\"||faceboox.com^\",\n\t\t\"||facebopk.com^\",\n\t\t\"||facebpook.com^\",\n\t\t\"||facebuk.com^\",\n\t\t\"||facebuok.com^\",\n\t\t\"||facebvook.com^\",\n\t\t\"||facebyook.com^\",\n\t\t\"||facebzook.com^\",\n\t\t\"||facecbgook.com^\",\n\t\t\"||facecbook.com^\",\n\t\t\"||facecbook.org^\",\n\t\t\"||facecook.com^\",\n\t\t\"||facecook.org^\",\n\t\t\"||facedbook.com^\",\n\t\t\"||faceebok.com^\",\n\t\t\"||faceebook.com^\",\n\t\t\"||faceebot.com^\",\n\t\t\"||facegbok.com^\",\n\t\t\"||facegbook.com^\",\n\t\t\"||faceobk.com^\",\n\t\t\"||faceobok.com^\",\n\t\t\"||faceobook.com^\",\n\t\t\"||faceook.com^\",\n\t\t\"||facerbooik.com^\",\n\t\t\"||facerbook.com^\",\n\t\t\"||facesbooc.com^\",\n\t\t\"||facesounds.com^\",\n\t\t\"||facetook.com^\",\n\t\t\"||facevbook.com^\",\n\t\t\"||facewbook.co^\",\n\t\t\"||facewook.com^\",\n\t\t\"||facfacebook.com^\",\n\t\t\"||facfebook.com^\",\n\t\t\"||faciometrics.com^\",\n\t\t\"||fackebook.com^\",\n\t\t\"||facnbook.com^\",\n\t\t\"||facrbook.com^\",\n\t\t\"||facvebook.com^\",\n\t\t\"||facwebook.com^\",\n\t\t\"||facxebook.com^\",\n\t\t\"||fadebook.com^\",\n\t\t\"||faebok.com^\",\n\t\t\"||faebook.com^\",\n\t\t\"||faebookc.com^\",\n\t\t\"||faeboook.com^\",\n\t\t\"||faecebok.com^\",\n\t\t\"||faesebook.com^\",\n\t\t\"||fafacebook.com^\",\n\t\t\"||faicbooc.com^\",\n\t\t\"||fasebokk.com^\",\n\t\t\"||fasebook.com^\",\n\t\t\"||faseboox.com^\",\n\t\t\"||fasttext.cc^\",\n\t\t\"||favebook.com^\",\n\t\t\"||faycbok.com^\",\n\t\t\"||fb.careers^\",\n\t\t\"||fb.com^\",\n\t\t\"||fb.gg^\",\n\t\t\"||fb.me^\",\n\t\t\"||fb.watch^\",\n\t\t\"||fbacebook.com^\",\n\t\t\"||fbbmarket.com^\",\n\t\t\"||fbboostyourbusiness.com^\",\n\t\t\"||fbcdn.com^\",\n\t\t\"||fbcdn.net^\",\n\t\t\"||fbf8.com^\",\n\t\t\"||fbfeedback.com^\",\n\t\t\"||fbhome.com^\",\n\t\t\"||fbidb.io^\",\n\t\t\"||fbinc.com^\",\n\t\t\"||fbinfer.com^\",\n\t\t\"||fbinnovation.com^\",\n\t\t\"||fblitho.com^\",\n\t\t\"||fbmarketing.com^\",\n\t\t\"||fbmessenger.com^\",\n\t\t\"||fbredex.com^\",\n\t\t\"||fbreg.com^\",\n\t\t\"||fbrell.com^\",\n\t\t\"||fbrpms.com^\",\n\t\t\"||fbsbx.com^\",\n\t\t\"||fbsbx.net^\",\n\t\t\"||fbsupport-covid.net^\",\n\t\t\"||fbthirdpartypixel.com^\",\n\t\t\"||fbthirdpartypixel.net^\",\n\t\t\"||fbthirdpartypixel.org^\",\n\t\t\"||fburl.com^\",\n\t\t\"||fbwat.ch^\",\n\t\t\"||fbworkmail.com^\",\n\t\t\"||fcacebook.com^\",\n\t\t\"||fcaebook.com^\",\n\t\t\"||fcebook.com^\",\n\t\t\"||fcebookk.com^\",\n\t\t\"||fcfacebook.com^\",\n\t\t\"||fdacebook.info^\",\n\t\t\"||feacboo.com^\",\n\t\t\"||feacbook.com^\",\n\t\t\"||feacbooke.com^\",\n\t\t\"||feacebook.com^\",\n\t\t\"||fecbbok.com^\",\n\t\t\"||fecbooc.com^\",\n\t\t\"||fecbook.com^\",\n\t\t\"||feceboock.com^\",\n\t\t\"||fecebook.net^\",\n\t\t\"||feceboox.com^\",\n\t\t\"||fececbook.com^\",\n\t\t\"||feook.com^\",\n\t\t\"||ferabook.com^\",\n\t\t\"||fescebook.com^\",\n\t\t\"||fesebook.com^\",\n\t\t\"||ffacebook.com^\",\n\t\t\"||fgacebook.com^\",\n\t\t\"||ficeboock.com^\",\n\t\t\"||flow.dev^\",\n\t\t\"||flow.org^\",\n\t\t\"||flowtype.org^\",\n\t\t\"||fmcebook.com^\",\n\t\t\"||fnacebook.com^\",\n\t\t\"||fosebook.com^\",\n\t\t\"||fpacebook.com^\",\n\t\t\"||fqcebook.com^\",\n\t\t\"||fracebook.com^\",\n\t\t\"||freeb.com^\",\n\t\t\"||freebasics.com^\",\n\t\t\"||freebasics.net^\",\n\t\t\"||freebs.com^\",\n\t\t\"||freefacebook.com^\",\n\t\t\"||freefacebook.net^\",\n\t\t\"||freefacebookads.net^\",\n\t\t\"||freefblikes.com^\",\n\t\t\"||freindfeed.com^\",\n\t\t\"||frescolib.org^\",\n\t\t\"||friendbook.info^\",\n\t\t\"||friendfed.com^\",\n\t\t\"||friendfeed-api.com^\",\n\t\t\"||friendfeed-media.com^\",\n\t\t\"||friendfeed.com^\",\n\t\t\"||friendfeedmedia.com^\",\n\t\t\"||fsacebok.com^\",\n\t\t\"||fscebook.com^\",\n\t\t\"||fundraisingwithfacebook.com^\",\n\t\t\"||funnyfacebook.org^\",\n\t\t\"||futureofbusinesssurvey.org^\",\n\t\t\"||gacebook.com^\",\n\t\t\"||gameroom.com^\",\n\t\t\"||gfacecbook.com^\",\n\t\t\"||groups.com^\",\n\t\t\"||hackerfacebook.com^\",\n\t\t\"||hackfacebook.com^\",\n\t\t\"||hackfacebookid.com^\",\n\t\t\"||hacklang.org^\",\n\t\t\"||hhvm.com^\",\n\t\t\"||hifacebook.info^\",\n\t\t\"||howtohackfacebook-account.com^\",\n\t\t\"||hsfacebook.com^\",\n\t\t\"||httpfacebook.com^\",\n\t\t\"||httpsfacebook.com^\",\n\t\t\"||httpwwwfacebook.com^\",\n\t\t\"||i.org^\",\n\t\t\"||internet.org^\",\n\t\t\"||klik.me^\",\n\t\t\"||liverail.com^\",\n\t\t\"||liverail.tv^\",\n\t\t\"||login-account.net^\",\n\t\t\"||m.me^\",\n\t\t\"||makeitopen.com^\",\n\t\t\"||markzuckerberg.com^\",\n\t\t\"||mcrouter.net^\",\n\t\t\"||mcrouter.org^\",\n\t\t\"||messenger.com^\",\n\t\t\"||messengerdevelopers.com^\",\n\t\t\"||midentsolutions.com^\",\n\t\t\"||mobilefacebook.com^\",\n\t\t\"||moneywithfacebook.com^\",\n\t\t\"||myfbfans.com^\",\n\t\t\"||nbabot.net^\",\n\t\t\"||newsfeed.com^\",\n\t\t\"||nextstop.com^\",\n\t\t\"||ogp.me^\",\n\t\t\"||online-deals.net^\",\n\t\t\"||opencreate.org^\",\n\t\t\"||opengraphprotocol.com^\",\n\t\t\"||opengraphprotocol.org^\",\n\t\t\"||parse.com^\",\n\t\t\"||pyrobot.org^\",\n\t\t\"||reachtheworldonfacebook.com^\",\n\t\t\"||react.com^\",\n\t\t\"||reactjs.com^\",\n\t\t\"||reactjs.org^\",\n\t\t\"||recoiljs.org^\",\n\t\t\"||redkix.com^\",\n\t\t\"||rocksdb.com^\",\n\t\t\"||rocksdb.net^\",\n\t\t\"||rocksdb.org^\",\n\t\t\"||rocksdb.org^\",\n\t\t\"||shopfacebook.com^\",\n\t\t\"||sportsfacebook.com^\",\n\t\t\"||sportstream.com^\",\n\t\t\"||supportfacebook.com^\",\n\t\t\"||terragraph.com^\",\n\t\t\"||thefacebook.com^\",\n\t\t\"||thefacebook.net^\",\n\t\t\"||thefind.com^\",\n\t\t\"||toplayerserver.com^\",\n\t\t\"||viewpointsfromfacebook.com^\",\n\t\t\"||whyfacebook.com^\",\n\t\t\"||workplace.com^\",\n\t\t\"||workplaceusecases.com^\",\n\t\t\"||worldhack.com^\",\n\t\t\"||www-facebook.com^\",\n\t\t\"||wwwfacebok.com^\",\n\t\t\"||wwwfacebook.com^\",\n\t\t\"||wwwmfacebook.com^\",\n\t\t\"||yogalayout.com^\",\n\t\t\"||zuckerberg.com^\",\n\t\t\"||zuckerberg.net^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"fifa\",\n\tName:    \"FIFA\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M0 8.06v7.88h2.49v-2.85H4.2l.68-1.72h-2.4v-1.6H5.4l.63-1.7zm6.8 0v7.88h2.46V8.06zm4.15 0v7.88h2.49v-2.85h1.72l.68-1.72h-2.4v-1.6h2.92l.64-1.7zm7.66 0-2.83 7.88h2.38l.3-1.06h2.77l.32 1.06H24l-2.84-7.88zm1.24 2.03.98 3.27H18.9z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||fifa.com^\",\n\t\t\"||fifaplus.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"flickr\",\n\tName:    \"Flickr\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 9 4 C 6.2504839 4 4 6.2504839 4 9 L 4 41 C 4 43.749516 6.2504839 46 9 46 L 41 46 C 43.749516 46 46 43.749516 46 41 L 46 9 C 46 6.2504839 43.749516 4 41 4 L 9 4 z M 9 6 L 41 6 C 42.668484 6 44 7.3315161 44 9 L 44 41 C 44 42.668484 42.668484 44 41 44 L 9 44 C 7.3315161 44 6 42.668484 6 41 L 6 9 C 6 7.3315161 7.3315161 6 9 6 z M 16 17 C 11.59 17 8 20.59 8 25 C 8 29.41 11.59 33 16 33 C 20.41 33 24 29.41 24 25 C 24 20.59 20.41 17 16 17 z M 34 17 C 29.59 17 26 20.59 26 25 C 26 29.41 29.59 33 34 33 C 38.41 33 42 29.41 42 25 C 42 20.59 38.41 17 34 17 z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||flic.kr^\",\n\t\t\"||flickr.com^\",\n\t\t\"||flickr.net^\",\n\t\t\"||flickrprints.com^\",\n\t\t\"||flickrpro.com^\",\n\t\t\"||staticflickr.com^\",\n\t},\n\tGroupID: \"hosting\",\n}, {\n\tID:      \"gemini\",\n\tName:    \"Gemini\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M49 24h-1A23 23 0 0 1 26 2V1a1 1 0 0 0-2 0v1A23 23 0 0 1 2 24H1a1 1 0 0 0 0 2h1a23 23 0 0 1 22 22v1a1 1 0 0 0 2 0v-1a23 23 0 0 1 22-22h1a1 1 0 0 0 0-2\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||gemini.com^\",\n\t\t\"||gemini.google.com^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"globoplay\",\n\tName:    \"Globoplay\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"30 -5 90 90\\\"><path d=\\\"M36.45 18.035a8.498 8.498 0 0 0-6.298 7.33 8.23 8.23 0 0 0 3.298 8.27 8.685 8.685 0 0 0 4.952 1.635 5.696 5.696 0 0 0 4.497-1.714c.06 2.704-1.139 4.685-4.319 4.784a10.012 10.012 0 0 1-5.556-1.535c-.901-.535-1.337-.376-1.793.535-.257.505-.505 1.04-.782 1.555-.397.723-.258 1.178.465 1.773a11.886 11.886 0 0 0 7.23 2.218c8.063 0 9.559-5.962 9.559-10.707V19.67c.02-1.753.079-1.654-2.457-1.654a3.16 3.16 0 0 0-1.584.238c-.327.258-.387.753-.387 1.535a6.601 6.601 0 0 0-6.825-1.753m2.469 13.153c-2.486-.02-4.319-1.654-4.319-4.556a4.15 4.15 0 0 1 4.338-4.467 4.19 4.19 0 0 1 4.16 4.507h-.02a4.16 4.16 0 0 1-4.144 4.516h-.015m76.025 14.606c-.644 1.872-1.337 3.705-1.981 5.557-.476 1.347-1.05 2.843-1.555 4.319 0 0-2.645-6.795-3.863-9.905a1.11 1.11 0 0 0-1.229-.852h-2.753c-1.436 0-1.496.079-.911 1.416 2.07 4.755 6.606 15.095 6.606 15.095-.387.923-.851 1.81-1.386 2.655a2.042 2.042 0 0 1-2.477.99c-1.674-.396-1.674-.376-2.486 1.199a8.951 8.951 0 0 0-.336.594c-.288.505-.466 1.178.138 1.496a5.717 5.717 0 0 0 3.387 1.137h.071c.088-.001.178-.004.266-.008 3.19 0 5.131-2.892 5.943-4.873 1.288-3.23 2.952-7.191 4.259-10.44 1.06-2.665 2.16-5.3 3.15-7.924.436-1.08.238-1.397-.931-1.397-.862 0-1.714.06-2.555 0a1.168 1.168 0 0 0-1.357.941M76.237 12.072V34.1c-.016.288.046.575.178.832.337.515 3.675.554 3.962 0 .238-.308.119-1.397.238-1.298a7.696 7.696 0 0 0 11.153-.892c2.703-2.931 2.684-8.775.237-11.746a8.19 8.19 0 0 0-6.517-3.25 6.754 6.754 0 0 0-4.507 1.813c-.02-.08-.06-5.28-.079-7.408 0-.882-.267-1.298-1.258-1.298-.733.06-1.317.02-2.17 0h-.065c-.806 0-1.172.36-1.172 1.219m8.486 19.155a4.25 4.25 0 0 1-3.92-4.555c.06-2.734 1.872-4.487 4.24-4.487a4.22 4.22 0 0 1 4.24 4.546 4.249 4.249 0 0 1-4.237 4.508 4.33 4.33 0 0 1-.323-.012m-23.521 15.39c0-1.297.079-1.693-1.278-1.693h-1.872c-.773-.11-1.05.267-1.05.99v22.098c0 .713.218 1.07.99 1.07h1.773c1.714 0 1.714 0 1.714-1.655v-6.933c.475.297.723.495 1.05.674a6.815 6.815 0 0 0 7.101.386 9.033 9.033 0 0 0 4.576-10.281c-.853-3.919-4.351-6.738-7.994-6.737-1.726 0-3.485.633-5.01 2.081m4.445 11.341a4.328 4.328 0 0 1-4.249-4.596 4.22 4.22 0 0 1 4.447-4.467c2.684.1 4.081 1.952 4.081 4.507a4.23 4.23 0 0 1-4.215 4.557h-.064M87.29 45.776a9.231 9.231 0 0 0-.753 14.738 7.084 7.084 0 0 0 5.2 1.832 6.24 6.24 0 0 0 4.557-2.06c0 1.734.317 1.734 1.892 1.734h1.377c.901.079 1.258-.377 1.258-1.209V46.033c0-.762-.278-1.258-1.16-1.159h-1.574c-1.753-.02-1.535 0-1.753 1.595l-.773-.495a6.925 6.925 0 0 0-4.269-1.47c-1.399 0-2.8.423-4.001 1.272m.564 7.626v-.04a4.279 4.279 0 0 1 4.16-4.487l.154.005a4.348 4.348 0 0 1 4.145 4.542 4.279 4.279 0 0 1-4.26 4.596l-.105.001c-2.372 0-4.094-1.972-4.094-4.617m-31.291-26.91a8.855 8.855 0 0 0 9.053 9.217 8.855 8.855 0 0 0 8.647-9.058c.001-.067.002-.133.001-.2a8.736 8.736 0 0 0-8.787-8.684h-.071a8.846 8.846 0 0 0-8.843 8.726m8.63 4.806a4.487 4.487 0 0 1-4.143-4.807c.002-.069.004-.138.01-.207a4.299 4.299 0 0 1 8.579.564v.06a4.358 4.358 0 0 1-4.32 4.397 4.08 4.08 0 0 1-.125-.008m30.527-4.665a8.816 8.816 0 1 0 17.63.258 8.816 8.816 0 0 0-8.686-8.945 8.817 8.817 0 0 0-8.944 8.686m4.478.377a4.378 4.378 0 0 1 4.17-4.863 4.259 4.259 0 0 1 4.437 4.487v.02a4.338 4.338 0 0 1-4.16 4.655h-.021a4.428 4.428 0 0 1-4.426-4.299M77.93 37.762c-1.228.02-1.397.198-1.397 1.436v21.514c0 .89.307 1.346 1.298 1.287.722-.04 1.446-.04 2.169 0 .822.04 1.139-.297 1.139-1.149V49.895c0-3.644-.02-7.31 0-10.974 0-.822-.297-1.159-1.139-1.159h-2.07ZM50.846 10.505c-.669.004-.913.505-.913 1.408v11.252c-.02 1.09-.02 2.199 0 3.21v7.923c-.02 1.317.812 1.694 1.713 1.317.456-.168 1.07-.485 1.377-.623 1.506-.674 1.446-1.21 1.446-2.427.06-6.25.06-13.045.06-19.126 0-1.595-.496-1.753-2.407-2.586-.528-.233-.942-.346-1.26-.348h-.016Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||cloud-jarvis.globo.com^\",\n\t\t\"||globoplay.com.br^\",\n\t\t\"||globoplay.com^\",\n\t\t\"||globoplay.globo.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"gog\",\n\tName:    \"GOG\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 34 34\\\"><path d=\\\"M31 31H3a3 3 0 0 1-3-3V3A3 3 0 0 1 3 0H31a3 3 0 0 1 3 3V28A3 3 0 0 1 31 31ZM4 24.5A1.5 1.5 0 0 0 5.5 26H11V24H6.5a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 .5-.5H11V18H5.5A1.5 1.5 0 0 0 4 19.5Zm8-18A1.5 1.5 0 0 0 10.5 5h-5A1.5 1.5 0 0 0 4 6.5v5A1.5 1.5 0 0 0 5.5 13H9V11H6.5a.5.5 0 0 1-.5-.5v-3A.5.5 0 0 1 6.5 7h3a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-.5.5H4v2h6.5A1.5 1.5 0 0 0 12 14.5Zm0 13v5A1.5 1.5 0 0 0 13.5 26h5A1.5 1.5 0 0 0 20 24.5v-5A1.5 1.5 0 0 0 18.5 18h-5A1.5 1.5 0 0 0 12 19.5Zm9-13A1.5 1.5 0 0 0 19.5 5h-5A1.5 1.5 0 0 0 13 6.5v5A1.5 1.5 0 0 0 14.5 13h5A1.5 1.5 0 0 0 21 11.5Zm9 0A1.5 1.5 0 0 0 28.5 5h-5A1.5 1.5 0 0 0 22 6.5v5A1.5 1.5 0 0 0 23.5 13H27V11H24.5a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-.5.5H22v2h6.5A1.5 1.5 0 0 0 30 14.5ZM30 18H22.5A1.5 1.5 0 0 0 21 19.5V26h2V20.5a.5.5 0 0 1 .5-.5h1v6h2V20H28v6h2ZM18.5 11h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v3A.5.5 0 0 1 18.5 11Zm-4 9h3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-3A.5.5 0 0 1 14.5 20Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||gog-cdn-lumen.secure2.footprint.net^\",\n\t\t\"||gog-statics.com^\",\n\t\t\"||gog.com^\",\n\t\t\"||gogalaxy.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"grok\",\n\tName:    \"Grok\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"m47.7 2-.7.3-6.1 6.2h-.2L19.1 30.4a1 1 0 0 0 1.3 1.5l15.1-11.1q.3-.2.5-.1l.2.2c1.7 4.3.8 9.1-2.5 12.4-2.9 3-6.9 4-10.9 3h-.7L16.8 39a1 1 0 0 0 0 1.7q4.4 2.4 9 2.4 7 0 12.5-5.2A18 18 0 0 0 43.1 21c-1.7-7.5.4-10.5 5-16.9l.4-.5a1 1 0 0 0-.8-1.5M26 7.2A18 18 0 0 0 9 32c1.7 4-1 7-4.2 10.3L1.5 46A1 1 0 0 0 3 47.3l14.3-12.8q.3 0 .4-.4a1 1 0 0 0-.3-1.1 10 10 0 0 1-3.3-7.5 11.7 11.7 0 0 1 13.3-11.6l.6-.1 5.7-3a1 1 0 0 0 0-1.8l-1.2-.5Q29.3 7.2 26 7\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||grok.com^\",\n\t\t\"||x.ai^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"hbomax\",\n\tName:    \"HBO Max\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M0 14v22h5v-9h3v9h5V14H8v8H5v-8H0zm15 0v22h8.4c3.1 0 5.7-2.3 6.2-5.4a11 11 0 1 0 0-11.2c-.5-3-3-5.4-6.2-5.4H15zm5 5h3a2 2 0 1 1 0 4h-3v-4zm19 0a6 6 0 1 1 0 12 6 6 0 0 1 0-12zm0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4zm-11 2.8v2.4c-.4-.5-1-1-2-1.2 1-.3 1.7-.8 2-1.3zm-8 4h3a2 2 0 1 1 0 4h-3v-4z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||hbo.com^\",\n\t\t\"||hbogo.co.th^\",\n\t\t\"||hbogo.com^\",\n\t\t\"||hbogo.eu^\",\n\t\t\"||hbogoasia.com^\",\n\t\t\"||hbogoasia.id^\",\n\t\t\"||hbogoasia.ph^\",\n\t\t\"||hbomax-images.warnermediacdn.com^\",\n\t\t\"||hbomax.com^\",\n\t\t\"||hbomaxcdn.com^\",\n\t\t\"||hbonow.com^\",\n\t\t\"||max.com^\",\n\t\t\"||maxgo.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"hulu\",\n\tName:    \"Hulu\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 0 15 L 0 34 L 4 34 L 4 25 C 4 24.5 4.398438 24 5 24 L 8 24 C 8.5 24 9 24.398438 9 25 L 9 34 L 13 34 L 13 24 C 13 21.800781 11.199219 20 9 20 L 4 20 L 4 15 Z M 30 15 L 30 34 L 34 34 L 34 15 Z M 15 20 L 15 30 C 15 32.199219 16.800781 34 19 34 L 24 34 C 26.199219 34 27.992188 32.199219 28.09375 30 L 28.09375 20 L 24.09375 20 L 24.09375 28.90625 C 24.09375 29.507813 23.601563 30 23 30 L 20.09375 30 C 19.492188 30 19 29.507813 19 28.90625 L 19 20 Z M 36 20 L 36 30 C 36 32.199219 37.800781 34 40 34 L 45 34 C 47.199219 34 49 32.199219 49 30 L 49 20 L 45 20 L 45 29 C 45 29.5 44.601563 30 44 30 L 41 30 C 40.5 30 40 29.601563 40 29 L 40 20 Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||hulu.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"icloud_private_relay\",\n\tName:    \"iCloud Private Relay\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 512 512\\\"><path d=\\\"M395.748 272.046c-.646-64.841 52.88-95.938 55.271-97.483-30.075-44.01-76.925-50.039-93.62-50.736-39.871-4.037-77.798 23.474-98.033 23.474-20.184 0-51.409-22.877-84.476-22.276-43.458.646-83.529 25.269-105.906 64.19-45.152 78.35-11.563 194.42 32.445 257.963 21.504 31.104 47.146 66.038 80.813 64.79 32.421-1.294 44.681-20.979 83.878-20.979 39.196 0 50.215 20.979 84.524 20.335 34.888-.648 56.991-31.699 78.347-62.898 24.694-36.084 34.862-71.019 35.462-72.812-.775-.354-68.031-26.119-68.705-103.568zM331.28 81.761C349.149 60.082 361.21 30.005 357.92 0c-25.739 1.048-56.938 17.145-75.405 38.775-16.57 19.188-31.075 49.813-27.188 79.218 28.734 2.242 58.065-14.602 75.953-36.232z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||mask-canary.icloud.com^$dnsrewrite=NXDOMAIN;;\",\n\t\t\"||mask-h2.icloud.com^$dnsrewrite=NXDOMAIN;;\",\n\t\t\"||mask.icloud.com^$dnsrewrite=NXDOMAIN;;\",\n\t},\n\tGroupID: \"privacy\",\n}, {\n\tID:      \"iheartradio\",\n\tName:    \"iHeartRadio\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -4 33 33\\\"><path d=\\\"M23.997.25c-3.053 0-5.79 1.613-7.402 4.003C15.01 1.892 12.275.25 9.193.25A8.826 8.826 0 0 0 .35 9.092c0 3.11 1.958 5.328 4.003 7.402l8.007 7.575c.432.403 1.123.086 1.123-.49v-5.904c0-1.7 1.383-3.082 3.082-3.082 1.7 0 3.082 1.383 3.082 3.082v5.904c0 .576.69.864 1.123.49l8.007-7.575c2.045-2.074 4.003-4.291 4.003-7.402C32.84 4.196 28.893.25 23.997.25ZM8.587 15.918a.573.573 0 0 1-.46.202.688.688 0 0 1-.403-.144c-2.65-2.333-3.975-4.781-3.975-7.258v-.03c0-2.13 1.296-4.55 3.024-5.615a.607.607 0 1 1 .634 1.036c-1.383.864-2.448 2.88-2.448 4.58v.029c0 2.102 1.21 4.233 3.571 6.336.26.202.288.605.058.864Zm3.947-2.275a.618.618 0 0 1-.548.317.487.487 0 0 1-.288-.087c-1.785-1.008-2.995-2.966-2.995-4.896v-.029a4.925 4.925 0 0 1 2.65-4.378c.288-.144.662-.029.835.26.144.287.029.662-.26.835a3.664 3.664 0 0 0-1.987 3.283c0 1.498.95 3.053 2.362 3.83.288.173.375.548.23.865Zm4.06-1.584a2.643 2.643 0 0 1-2.65-2.65 2.644 2.644 0 0 1 2.65-2.65c1.47 0 2.65 1.181 2.65 2.65.03 1.469-1.18 2.65-2.65 2.65Zm4.926 1.814a.556.556 0 0 1-.288.087.652.652 0 0 1-.547-.317c-.173-.288-.058-.663.23-.836 1.411-.806 2.362-2.332 2.362-3.83 0-1.383-.778-2.65-1.988-3.283-.288-.144-.403-.519-.259-.836.144-.288.518-.403.835-.259a4.925 4.925 0 0 1 2.65 4.378v.029c0 1.9-1.21 3.86-2.995 4.867Zm7.949-5.184c0 2.477-1.325 4.925-3.975 7.258-.115.115-.259.144-.403.144a.573.573 0 0 1-.46-.202.602.602 0 0 1 .057-.864c2.362-2.102 3.571-4.234 3.571-6.336V8.66c0-1.728-1.065-3.744-2.448-4.58a.607.607 0 1 1 .634-1.036c1.728 1.094 3.024 3.514 3.024 5.616v.029Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||937theriver.com^\",\n\t\t\"||iheart.com^\",\n\t\t\"||iheart.mx^\",\n\t\t\"||iheartmedia.com^\",\n\t\t\"||iheartradio.ca^\",\n\t\t\"||iheartradio.co.nz^\",\n\t\t\"||iheartradio.com^\",\n\t\t\"||ihrdev.com^\",\n\t\t\"||ihrhls.com^\",\n\t\t\"||ihrint.com^\",\n\t\t\"||ihrstage.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"imgur\",\n\tName:    \"Imgur\",\n\tIconSVG: []byte(\"<svg fill=\\\"currentColor\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\"  viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 13.890625 4 C 8.440625 4 4 8.440625 4 13.890625 L 4 14 L 4 25.150391 L 4 36 C 4 41.511334 8.4886661 46 14 46 L 25.240234 46 L 36 46 L 36.109375 46 C 41.559375 46 46 41.559375 46 36.109375 L 46 36 L 46 14 L 46 13.890625 C 46 8.440625 41.559375 4 36.109375 4 L 36 4 L 14 4 L 13.890625 4 z M 30.650391 12.841797 C 32.240547 12.849766 33.879609 12.879687 35.599609 12.929688 C 36.589609 12.949687 37.410469 13.759531 37.480469 14.769531 C 37.880469 20.889531 37.589062 26.699063 37.039062 33.539062 C 36.969062 34.359063 36.559688 35.090547 35.929688 35.560547 C 35.919688 35.560547 35.910156 35.570312 35.910156 35.570312 L 27.287109 44 L 14 44 C 9.5693339 44 6 40.430666 6 36 L 6 23.175781 L 15.140625 14.150391 L 15.150391 14.150391 C 15.600391 13.700391 16.199141 13.419141 16.869141 13.369141 C 21.556641 12.986641 25.879922 12.817891 30.650391 12.841797 z M 30.029297 14.839844 C 25.569297 14.839844 21.459297 14.999375 17.029297 15.359375 C 16.569297 15.399375 16.399609 15.740859 16.349609 15.880859 C 16.299609 16.020859 16.209297 16.390937 16.529297 16.710938 L 21.490234 21.669922 L 12.789062 30.369141 C 13.169063 31.119141 14.099922 32.660234 15.919922 34.490234 C 17.749922 36.310234 19.289062 37.229141 20.039062 37.619141 L 28.730469 28.910156 L 33.699219 33.880859 C 34.019219 34.200859 34.379297 34.110547 34.529297 34.060547 C 34.669297 34.010547 35.009062 33.830859 35.039062 33.380859 C 35.589062 26.630859 35.870469 20.910156 35.480469 14.910156 C 33.580469 14.860156 31.779297 14.839844 30.029297 14.839844 z M 14.501953 20 C 14.450328 20 14.399406 20.028938 14.378906 20.085938 C 14.163906 20.686938 13.684938 21.159047 13.085938 21.373047 C 12.971938 21.413047 12.971937 21.580094 13.085938 21.621094 C 13.684937 21.837094 14.162906 22.315062 14.378906 22.914062 C 14.419906 23.028062 14.585953 23.028063 14.626953 22.914062 C 14.840953 22.316062 15.313063 21.837094 15.914062 21.621094 C 16.028062 21.580094 16.027109 21.415047 15.912109 21.373047 C 15.312109 21.160047 14.839 20.685938 14.625 20.085938 C 14.605 20.028938 14.553578 20 14.501953 20 z M 10.503906 24 C 10.417781 23.999875 10.332828 24.047578 10.298828 24.142578 C 9.9398281 25.143578 9.1425312 25.935016 8.1445312 26.291016 C 7.9535313 26.359016 7.9525781 26.635125 8.1425781 26.703125 C 9.1405781 27.063125 9.9398281 27.859422 10.298828 28.857422 C 10.366828 29.047422 10.642938 29.047422 10.710938 28.857422 C 11.066937 27.859422 11.856422 27.062125 12.857422 26.703125 C 13.047422 26.635125 13.047422 26.358016 12.857422 26.291016 C 11.857422 25.936016 11.066937 25.145531 10.710938 24.144531 C 10.677437 24.049031 10.590031 24.000125 10.503906 24 z M 10.501953 33 C 10.450328 33 10.399406 33.028938 10.378906 33.085938 C 10.163906 33.686938 9.6849375 34.159047 9.0859375 34.373047 C 8.9719375 34.413047 8.9719375 34.580094 9.0859375 34.621094 C 9.6849375 34.837094 10.162906 35.315063 10.378906 35.914062 C 10.419906 36.028062 10.585953 36.028062 10.626953 35.914062 C 10.840953 35.316063 11.313063 34.837094 11.914062 34.621094 C 12.028062 34.580094 12.027109 34.415047 11.912109 34.373047 C 11.312109 34.160047 10.839 33.685938 10.625 33.085938 C 10.605 33.028937 10.553578 33 10.501953 33 z M 29.001953 33 C 28.967453 33 28.933422 33.018641 28.919922 33.056641 C 28.775922 33.456641 28.455641 33.774016 28.056641 33.916016 C 27.980641 33.943016 27.980641 34.053078 28.056641 34.080078 C 28.455641 34.224078 28.775922 34.544359 28.919922 34.943359 C 28.947922 35.019359 29.056984 35.019359 29.083984 34.943359 C 29.225984 34.544359 29.543359 34.224078 29.943359 34.080078 C 30.019359 34.053078 30.019359 33.943016 29.943359 33.916016 C 29.543359 33.774016 29.225984 33.456641 29.083984 33.056641 C 29.070484 33.018641 29.036453 33 29.001953 33 z M 26.001953 36 C 25.933078 35.999875 25.865391 36.037281 25.837891 36.113281 C 25.550891 36.914281 24.911281 37.547031 24.113281 37.832031 C 23.960281 37.887031 23.961281 38.107109 24.113281 38.162109 C 24.912281 38.450109 25.549891 39.088719 25.837891 39.886719 C 25.892891 40.038719 26.113969 40.036766 26.167969 39.884766 C 26.452969 39.086766 27.085719 38.450109 27.886719 38.162109 C 28.038719 38.108109 28.037766 37.887031 27.884766 37.832031 C 27.083766 37.547031 26.452969 36.914281 26.167969 36.113281 C 26.140969 36.036781 26.070828 36.000125 26.001953 36 z M 9.0019531 38 C 8.9674531 38 8.9334219 38.018641 8.9199219 38.056641 C 8.7759219 38.456641 8.4556406 38.774016 8.0566406 38.916016 C 7.9806406 38.943016 7.9806406 39.053078 8.0566406 39.080078 C 8.4556406 39.224078 8.7759219 39.544359 8.9199219 39.943359 C 8.9479219 40.019359 9.0569844 40.019359 9.0839844 39.943359 C 9.2259844 39.544359 9.5433594 39.224078 9.9433594 39.080078 C 10.019359 39.053078 10.019359 38.943016 9.9433594 38.916016 C 9.5433594 38.774016 9.2259844 38.456641 9.0839844 38.056641 C 9.0704844 38.018641 9.0364531 38 9.0019531 38 z M 13.501953 39 C 13.450328 39 13.399406 39.028937 13.378906 39.085938 C 13.163906 39.686938 12.684938 40.159047 12.085938 40.373047 C 11.971938 40.413047 11.971937 40.580094 12.085938 40.621094 C 12.684937 40.837094 13.162906 41.315063 13.378906 41.914062 C 13.419906 42.028062 13.585953 42.028062 13.626953 41.914062 C 13.840953 41.316063 14.313063 40.837094 14.914062 40.621094 C 15.028062 40.580094 15.027109 40.415047 14.912109 40.373047 C 14.312109 40.160047 13.839 39.685938 13.625 39.085938 C 13.605 39.028937 13.553578 39 13.501953 39 z M 20.501953 40 C 20.450328 40 20.399406 40.028938 20.378906 40.085938 C 20.163906 40.686938 19.684938 41.159047 19.085938 41.373047 C 18.971937 41.413047 18.971937 41.580094 19.085938 41.621094 C 19.684938 41.837094 20.162906 42.315063 20.378906 42.914062 C 20.419906 43.028062 20.585953 43.028062 20.626953 42.914062 C 20.840953 42.316062 21.313063 41.837094 21.914062 41.621094 C 22.028062 41.580094 22.027109 41.415047 21.912109 41.373047 C 21.312109 41.160047 20.839 40.685938 20.625 40.085938 C 20.605 40.028937 20.553578 40 20.501953 40 z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||imgur.com^\",\n\t},\n\tGroupID: \"hosting\",\n}, {\n\tID:      \"instagram\",\n\tName:    \"Instagram\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M12 8.8A3.2 3.2 0 0 0 8.8 12a3.2 3.2 0 0 0 3.2 3.2 3.2 3.2 0 0 0 3.2-3.2A3.2 3.2 0 0 0 12 8.8zm0 0\\\" /><path d=\\\"M16 2.398H8A5.609 5.609 0 0 0 2.398 8v8A5.609 5.609 0 0 0 8 21.602h8A5.609 5.609 0 0 0 21.602 16V8A5.609 5.609 0 0 0 16 2.398zm-4 14.403A4.805 4.805 0 0 1 7.2 12c0-2.648 2.152-4.8 4.8-4.8 2.648 0 4.8 2.152 4.8 4.8 0 2.648-2.152 4.8-4.8 4.8zm5.602-9.602a.799.799 0 1 1 0 0zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||achat-followers-instagram.com^\",\n\t\t\"||acheter-followers-instagram.com^\",\n\t\t\"||acheterdesfollowersinstagram.com^\",\n\t\t\"||acheterfollowersinstagram.com^\",\n\t\t\"||bookstagram.com^\",\n\t\t\"||carstagram.com^\",\n\t\t\"||cdninstagram.com^\",\n\t\t\"||chickstagram.com^\",\n\t\t\"||ig.me^\",\n\t\t\"||igcdn.com^\",\n\t\t\"||igsonar.com^\",\n\t\t\"||igtv.com^\",\n\t\t\"||imstagram.com^\",\n\t\t\"||imtagram.com^\",\n\t\t\"||instaadder.com^\",\n\t\t\"||instachecker.com^\",\n\t\t\"||instafallow.com^\",\n\t\t\"||instafollower.com^\",\n\t\t\"||instagainer.com^\",\n\t\t\"||instagda.com^\",\n\t\t\"||instagify.com^\",\n\t\t\"||instagmania.com^\",\n\t\t\"||instagor.com^\",\n\t\t\"||instagram-brand.com^\",\n\t\t\"||instagram-engineering.com^\",\n\t\t\"||instagram-help.com^\",\n\t\t\"||instagram-press.com^\",\n\t\t\"||instagram-press.net^\",\n\t\t\"||instagram.com^\",\n\t\t\"||instagramci.com^\",\n\t\t\"||instagramcn.com^\",\n\t\t\"||instagramdi.com^\",\n\t\t\"||instagramhashtags.net^\",\n\t\t\"||instagramhilecim.com^\",\n\t\t\"||instagramhilesi.org^\",\n\t\t\"||instagramium.com^\",\n\t\t\"||instagramizlenme.com^\",\n\t\t\"||instagramkusu.com^\",\n\t\t\"||instagramlogin.com^\",\n\t\t\"||instagramm.com^\",\n\t\t\"||instagramn.com^\",\n\t\t\"||instagrampartners.com^\",\n\t\t\"||instagramphoto.com^\",\n\t\t\"||instagramq.com^\",\n\t\t\"||instagramsepeti.com^\",\n\t\t\"||instagramtakipcisatinal.net^\",\n\t\t\"||instagramtakiphilesi.com^\",\n\t\t\"||instagramtips.com^\",\n\t\t\"||instagramtr.com^\",\n\t\t\"||instagran.com^\",\n\t\t\"||instagranm.com^\",\n\t\t\"||instagrem.com^\",\n\t\t\"||instagrm.com^\",\n\t\t\"||instagtram.com^\",\n\t\t\"||instagy.com^\",\n\t\t\"||instamgram.com^\",\n\t\t\"||instangram.com^\",\n\t\t\"||instanttelegram.com^\",\n\t\t\"||instaplayer.net^\",\n\t\t\"||instastyle.tv^\",\n\t\t\"||instgram.com^\",\n\t\t\"||intagram.com^\",\n\t\t\"||intagrm.com^\",\n\t\t\"||intgram.com^\",\n\t\t\"||kingstagram.com^\",\n\t\t\"||lnstagram-help.com^\",\n\t\t\"||oninstagram.com^\",\n\t\t\"||online-instagram.com^\",\n\t\t\"||onlineinstagram.com^\",\n\t\t\"||theinstagramhack.com^\",\n\t\t\"||web-instagram.net^\",\n\t\t\"||wwwinstagram.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"io_interactive\",\n\tName:    \"IO Interactive\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -8.5 37 37\\\"><path d=\\\"M2.992 20V1.908L0 4.905V20z\\\"/><path d=\\\"M0 .008h2.992v1.9H0z\\\"/><path  d=\\\"m26.2 10.023-8.116 8.105-8.116-8.114 8.116-8.105zM20.383 20l8.384-8.345V8.372L20.383 0h-4.59L7.41 8.372v3.283L15.766 20zm15.718 0V1.908l-2.992 2.997V20z\\\"/><path d=\\\"M33.11.008h2.99v1.9h-2.992z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||hitman.com^\",\n\t\t\"||hitman.io^\",\n\t\t\"||ioi.dk^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"iqiyi\",\n\tName:    \"iQIYI\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M12.09 31.5a8 8 0 0 0 4.27.7c.19 0 .33 0 .38.21a1.41 1.41 0 0 0 1.4 1.25 16.86 16.86 0 0 0 2 0c.27 0 .4-.13.41-.41s0-.64 0-.95-.13-.45-.47-.46-.77 0-1.15 0a.56.56 0 0 1-.39-.08c.06-.1.15-.11.22-.14a2.25 2.25 0 0 0 1.45-2.07V18.29a2.13 2.13 0 0 0-.88-1.75 4 4 0 0 0-1.47-.65 10.45 10.45 0 0 0-3.82-.17 4.68 4.68 0 0 0-2 .61 2.29 2.29 0 0 0-1.25 2.09v11a2.06 2.06 0 0 0 1.3 2.08Zm2.3-12.84a2.55 2.55 0 0 1 0-.39.68.68 0 0 1 .47-.55 2.23 2.23 0 0 1 1.28 0 .58.58 0 0 1 .48.61v11.01c0 .68-.17.88-.86 1a1.52 1.52 0 0 1-1-.17.69.69 0 0 1-.34-.56v-.32ZM5.54 15h3c.21 0 .29-.08.32-.29a2.19 2.19 0 0 1 1.47-1.95 5.13 5.13 0 0 1 .88-.21 77.76 77.76 0 0 1 9.15-.83h7.55c1.8 0 3.59.18 5.38.36 1.24.13 2.48.28 3.72.49a2.26 2.26 0 0 1 2.05 2.13c0 .21.13.27.32.27h3c.24 0 .31-.09.29-.31a13.43 13.43 0 0 0-.26-1.66 3.92 3.92 0 0 0-3.12-3.12 42.34 42.34 0 0 0-4.86-.77C33.23 9 32 8.88 30.84 8.8c-1.41-.1-2.83-.18-4.24-.22h-2.65c-1.48 0-3 0-4.44.09-1 .07-1.92.09-2.88.16-1.48.11-2.95.24-4.42.44a28.28 28.28 0 0 0-4 .73 3.62 3.62 0 0 0-2.46 2.22 8 8 0 0 0-.52 2.51c.02.2.14.27.31.27Zm36.85 18h-3c-.24 0-.3.07-.34.29a2.14 2.14 0 0 1-1 1.72 3.55 3.55 0 0 1-1.19.39c-2.27.36-4.57.6-6.87.74-1.25.07-2.51.13-3.77.14h-4.54c-2 0-4-.12-6-.3-1.53-.14-3.06-.32-4.57-.56a3 3 0 0 1-1.44-.59 2.41 2.41 0 0 1-.77-1.6c0-.14-.06-.22-.22-.21-.7 0-1.39-.06-2 0H5.53c-.17 0-.24.08-.23.24a8.37 8.37 0 0 0 .46 2.38A3.65 3.65 0 0 0 8.27 38a26.19 26.19 0 0 0 3.49.67c2.32.32 4.65.49 7 .64 1.63.1 3.27.12 4.9.13s3.39 0 5.08-.12c.93-.06 1.87-.1 2.81-.17 1.43-.11 2.87-.24 4.29-.42a29.25 29.25 0 0 0 4-.74 3.63 3.63 0 0 0 2.4-2.18 7.86 7.86 0 0 0 .53-2.49c-.05-.24-.16-.32-.38-.32Zm-7.34-9.2c.86-2.38 1.71-4.76 2.57-7.13.14-.4 0-.58-.41-.59h-2.63c-.33 0-.4.06-.51.38-.32 1-.64 2.05-1 3.07-.22.7-.42 1.41-.65 2.17a2.14 2.14 0 0 1-.07-.25c-.47-1.67-1-3.32-1.52-5-.1-.32-.18-.38-.5-.39H27.7c-.38 0-.52.2-.39.56s.25.67.37 1c.83 2.3 1.64 4.61 2.49 6.9a7.16 7.16 0 0 1 .53 2.86c-.06 1.29 0 2.58 0 3.87 0 .45.11.55.54.56h2.48c.45 0 .52-.08.52-.52v-4.91a1.51 1.51 0 0 1 .06-.48l.75-2.1Zm-9.72-7.72h-2.41c-.49 0-.58.1-.58.59v14.65c0 .42.11.52.52.52h2.48c.48 0 .58-.1.58-.58V16.67c0-.49-.1-.59-.59-.59Zm16.89 0h-2.48c-.44 0-.53.09-.53.53v14.75c0 .4.09.48.49.48h2.53c.42 0 .52-.1.52-.52V16.6c0-.41-.11-.52-.53-.52ZM5.64 31.84h2.77a.33.33 0 0 0 .37-.34 2.71 2.71 0 0 0 0-.29v-9.89c0-.4-.09-.48-.49-.49H5.75c-.43 0-.5.08-.5.51V31.4c0 .32.1.44.39.44Zm-.39-12.58a.35.35 0 0 0 .39.4h2.72c.3 0 .42-.13.43-.42v-2.73c0-.29-.13-.42-.42-.42h-2.7c-.3 0-.42.12-.42.42v2.75Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||iq.com^\",\n\t\t\"||iqiyi.com^\",\n\t\t\"||iqiyipic.com^\",\n\t\t\"||pps.tv^\",\n\t\t\"||ppsimg.com^\",\n\t\t\"||qiyi.com^\",\n\t\t\"||qiyipic.com^\",\n\t\t\"||qy.net^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"kakaotalk\",\n\tName:    \"KakaoTalk\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\" ><path d=\\\"M22.125 0H1.875C.839 0 0 .84 0 1.875v20.25C0 23.161.84 24 1.875 24h20.25C23.161 24 24 23.16 24 22.125V1.875C24 .839 23.16 0 22.125 0zM12 18.75c-.591 0-1.17-.041-1.732-.12-.562.396-3.813 2.679-4.12 2.722 0 0-.125.049-.232-.014s-.088-.229-.088-.229c.032-.22.843-3.018.992-3.533-2.745-1.36-4.57-3.769-4.57-6.513 0-4.246 4.365-7.688 9.75-7.688s9.75 3.442 9.75 7.688c0 4.245-4.365 7.687-9.75 7.687zM8.05 9.867h-.878v3.342c0 .296-.252.537-.563.537s-.562-.24-.562-.537V9.867h-.878a.552.552 0 0 1 0-1.101h2.88a.552.552 0 0 1 0 1.101zm10.987 2.957a.558.558 0 0 1 .109.417.559.559 0 0 1-.219.37.557.557 0 0 1-.338.114.558.558 0 0 1-.45-.224l-1.319-1.747-.195.195v1.227a.564.564 0 0 1-.562.563.563.563 0 0 1-.563-.563V9.328a.563.563 0 0 1 1.125 0v1.21l1.57-1.57a.437.437 0 0 1 .311-.126c.14 0 .282.061.388.167a.555.555 0 0 1 .165.356.438.438 0 0 1-.124.343l-1.282 1.281 1.385 1.835zm-8.35-3.502c-.095-.27-.383-.548-.75-.556-.366.008-.654.286-.749.555l-1.345 3.541c-.171.53-.022.728.133.8a.857.857 0 0 0 .357.077c.235 0 .414-.095.468-.248l.279-.73h1.715l.279.73c.054.153.233.248.468.248a.86.86 0 0 0 .357-.078c.155-.071.304-.268.133-.8l-1.345-3.54zm-1.311 2.443.562-1.596.561 1.596H9.376zm5.905 1.383a.528.528 0 0 1-.539.516h-1.804a.528.528 0 0 1-.54-.516v-3.82c0-.31.258-.562.575-.562s.574.252.574.562v3.305h1.195c.297 0 .54.231.54.515z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||kakao.com^\",\n\t\t\"||kgslb.com^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"kik\",\n\tName:    \"Kik\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 3.5039062 12 C 1.9347705 11.994817 0.87857579 12.97636 0.4453125 13.849609 C 0.01204921 14.722858 0 15.564453 0 15.564453 A 1.0001 1.0001 0 0 0 0 15.59375 L 0 35 A 1.0001 1.0001 0 0 0 0.00390625 35.078125 C 0.00390625 35.078125 0.05696144 35.828363 0.5390625 36.554688 C 1.0211636 37.281011 2.0459252 38.004441 3.5019531 38.001953 C 4.8916439 38.000053 5.8837351 37.273604 6.3769531 36.578125 C 6.8701712 35.882646 6.9863281 35.166016 6.9863281 35.166016 A 1.0001 1.0001 0 0 0 7 35 L 7 31.802734 L 10.167969 36.554688 L 10.130859 36.494141 C 10.511831 37.164615 11.143097 37.525465 11.742188 37.730469 C 12.341278 37.935473 12.950104 38.001953 13.5 38.001953 C 15.411725 38.001953 17 36.431487 17 34.5 C 17 34.056649 16.90825 34.03442 16.851562 33.912109 C 16.794882 33.789799 16.730864 33.671331 16.654297 33.537109 C 16.501163 33.268666 16.298339 32.944015 16.058594 32.572266 C 15.579103 31.828767 14.950355 30.90254 14.322266 29.992188 C 13.310206 28.525308 12.655222 27.610988 12.300781 27.113281 L 14.707031 24.707031 A 1.0001 1.0001 0 0 0 14.738281 24.673828 C 14.738281 24.673828 15.354706 24.012223 15.748047 23.042969 C 16.141388 22.073714 16.298687 20.56089 15.259766 19.349609 C 14.281705 18.208994 12.842689 18.141009 11.925781 18.416016 C 11.008874 18.691022 10.371094 19.222656 10.371094 19.222656 A 1.0001 1.0001 0 0 0 10.292969 19.292969 L 6.9980469 22.587891 L 6.9921875 15.646484 A 1.0001 1.0001 0 0 0 6.9902344 15.580078 C 6.9902344 15.580078 6.9441634 14.743069 6.5058594 13.875 C 6.0675579 13.006938 5.0412971 12.005313 3.5039062 12 z M 30.503906 12 C 28.93477 11.9948 27.878577 12.97636 27.445312 13.849609 C 27.012049 14.722858 27 15.564453 27 15.564453 A 1.0001 1.0001 0 0 0 27 15.59375 L 27 35 A 1.0001 1.0001 0 0 0 27.003906 35.078125 C 27.003906 35.078125 27.056966 35.828363 27.539062 36.554688 C 28.021165 37.281011 29.045925 38.004441 30.501953 38.001953 C 31.891644 38.000053 32.883735 37.273604 33.376953 36.578125 C 33.870171 35.882646 33.986328 35.166016 33.986328 35.166016 A 1.0001 1.0001 0 0 0 34 35 L 34 31.802734 L 37.167969 36.554688 L 37.130859 36.494141 C 37.511831 37.164615 38.143096 37.525465 38.742188 37.730469 C 39.341277 37.935473 39.950104 38.001953 40.5 38.001953 C 42.411725 38.001953 44 36.431487 44 34.5 C 44 34.056649 43.908251 34.03442 43.851562 33.912109 C 43.794882 33.789799 43.730864 33.671331 43.654297 33.537109 C 43.501163 33.268666 43.298339 32.944015 43.058594 32.572266 C 42.579103 31.828767 41.950355 30.90254 41.322266 29.992188 C 40.310206 28.525308 39.655222 27.610988 39.300781 27.113281 L 41.707031 24.707031 A 1.0001 1.0001 0 0 0 41.738281 24.673828 C 41.738281 24.673828 42.354706 24.012223 42.748047 23.042969 C 43.141388 22.073714 43.298687 20.56089 42.259766 19.349609 C 41.281705 18.208994 39.842689 18.141009 38.925781 18.416016 C 38.008874 18.691022 37.371094 19.222656 37.371094 19.222656 A 1.0001 1.0001 0 0 0 37.292969 19.292969 L 33.998047 22.587891 L 33.992188 15.646484 A 1.0001 1.0001 0 0 0 33.990234 15.580078 C 33.990234 15.580078 33.944164 14.743069 33.505859 13.875 C 33.067647 13.006938 32.041297 12.005313 30.503906 12 z M 21.507812 18 C 19.85324 17.98686 18.785557 19.124468 18.382812 20.09375 C 18.181441 20.578391 18.090615 21.031738 18.044922 21.375 C 18.022072 21.546631 18.011459 21.69063 18.005859 21.796875 C 18.000252 21.90312 18 22.065333 18 21.984375 L 17.982422 34.998047 A 1.0001 1.0001 0 0 0 17.990234 35.134766 C 17.990234 35.134766 18.085674 35.862804 18.576172 36.568359 C 19.06667 37.273915 20.071581 37.997467 21.486328 38 C 22.885358 38.0026 23.885897 37.278643 24.380859 36.580078 C 24.875822 35.881513 24.986328 35.160156 24.986328 35.160156 A 1.0001 1.0001 0 0 0 25 35 L 25 21.996094 C 25 21.996094 25.02572 21.084043 24.625 20.117188 C 24.224283 19.150332 23.164841 18.013078 21.507812 18 z M 46.5 24 C 44.578848 24 43 25.578848 43 27.5 C 43 29.421152 44.578848 31 46.5 31 C 48.421152 31 50 29.421152 50 27.5 C 50 25.578848 48.421152 24 46.5 24 z M 46.5 26 C 47.340272 26 48 26.659728 48 27.5 C 48 28.340272 47.340272 29 46.5 29 C 45.659728 29 45 28.340272 45 27.5 C 45 26.659728 45.659728 26 46.5 26 z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||kik.com^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"kook\",\n\tName:    \"KOOK\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"18 18 220 220\\\"><path d=\\\"M32 87c25.86-.19 51.72-.33 77.58-.41 12-.05 24.01-.1 36.02-.2 10.46-.07 20.93-.12 31.4-.14 5.53-.01 11.07-.04 16.62-.1 5.21-.05 10.43-.07 15.64-.06a387.2 387.2 0 0 0 5.75-.04c14.35-.22 14.35-.22 18.22 2.55A17.33 17.33 0 0 1 236 95l.88 3.6c.12 3.5-.2 6.37-.94 9.78l-.78 3.69-.87 3.92-.87 4.07a1749.43 1749.43 0 0 1-1.84 8.47c-.8 3.63-1.58 7.26-2.36 10.89l-2.24 10.38-.44 2.01c-.4 1.86-.8 3.73-1.22 5.59l-.7 3.21C224 163 224 163 223 164a233.75 233.75 0 0 1-8.63.1h-2.62l-8.31-.04A6730.23 6730.23 0 0 1 184 164l-3 16h-11l-8-16H25c-6.84-12.54-6.84-12.54-5-20l1.22-6.25 1.5-7.1.8-3.8 2.11-9.9 2.15-10.13L32 87Zm7 8c-1.35 3.37-2.33 6-3.05 9.44l-.55 2.55-.56 2.73-.6 2.83-1.24 5.92c-.62 3.02-1.26 6.03-1.9 9.05l-1.2 5.76-.58 2.73-.53 2.55-.46 2.24c-.45 2.65-.45 2.65-.33 7.2h17l3-12 .97 2.62c1.7 3.74 3.14 6.44 6.03 9.38 5.17 1.54 10.74.68 16 0 1.18-4.5 1.38-6.94-.94-11.04a187.05 187.05 0 0 0-2.62-4.15c-.9-1.38-1.77-2.77-2.63-4.17l-1.2-1.83c-.61-1.81-.61-1.81.16-3.88a71.73 71.73 0 0 1 5.42-7.18l2.42-2.94a157.62 157.62 0 0 1 8.08-8.76c1.85-2.9 1.99-5.68 2.31-9.05-10.17-1.5-10.17-1.5-19.72 1.08-3.04 2.88-5.15 6.35-7.28 9.92h-1l2-11H39Zm44 7c-1.15 2.9-1.8 5.8-2.37 8.86l-.52 2.5c-.36 1.72-.7 3.45-1.04 5.18a558.08 558.08 0 0 1-1.6 7.94l-1.03 5.06-.49 2.4c-1.1 5.76-.68 8.96 2.05 14.06 5.18.15 10.35.26 15.53.33l5.29.12c2.53.08 5.06.1 7.6.14l2.37.09c4.28 0 5.75-.28 9.1-3.19 2.58-3.7 3.5-7.6 4.35-11.96l.51-2.42c.36-1.68.7-3.36 1.04-5.04a665.71 665.71 0 0 1 1.61-7.68l1.03-4.92.48-2.3c1.57-7.33 1.57-7.33.09-14.5-3.04-2.53-6.18-2.3-9.96-2.3l-2.36-.04a398.83 398.83 0 0 0-4.96-.02c-2.52 0-5.03-.06-7.54-.12l-4.83-.01-2.26-.08c-6.16.11-9.06 2.62-12.09 7.9Zm48.96-2.85c-1.75 3.37-2.51 6.83-3.23 10.54l-.53 2.48-1.05 5.19a606 606 0 0 1-1.64 7.9l-1.03 5.05-.5 2.37c-1.13 5.82-1.4 10.19 2.02 15.32a61 61 0 0 0 7.86.5l2.37.03c1.66.02 3.31.03 4.97.03 2.53 0 5.05.06 7.57.11l4.84.02 2.27.07c3.99-.06 6.47-.57 9.5-3.26 3.18-3.67 4.1-7.5 5-12.19l.51-2.49c.36-1.72.7-3.45 1.04-5.18.5-2.64 1.05-5.27 1.6-7.9l1.03-5.05.49-2.37c1.1-5.81 1.36-10.2-2.05-15.32a66.9 66.9 0 0 0-8.11-.5l-2.45-.03a558.66 558.66 0 0 0-5.12-.03c-2.61 0-5.22-.06-7.83-.11l-4.98-.02-2.35-.07c-4.75.06-7.42.93-10.2 4.91ZM181 95a1692.08 1692.08 0 0 0-8.88 39.44l-.65 3.08-.59 2.85-.52 2.5c-.44 2.26-.44 2.26-.36 5.13h17l3-12c5.87 9.75 5.87 9.75 7 12l7.94.06 2.28.03c1.93 0 3.85-.04 5.78-.09 1.34-1.14 1.34-1.14 1.32-4.12-.37-4.5-1.96-7.32-4.38-11-3.93-5.99-3.93-5.99-3.98-9.47 1.39-3.22 3.3-5.26 5.73-7.79l2.72-2.91a409.78 409.78 0 0 1 4.55-4.7l1.6-1.7 1.38-1.38c1.72-3.14 1.73-6.39 2.06-9.93-9.68-1.42-9.68-1.42-18.79 1.08-3.29 2.87-5.8 6.28-8.21 9.92l1-11h-17Z\\\"/><path d=\\\"M100 112h7c-1.88 16.88-1.88 16.88-4 19-2 .04-4 .04-6 0 .46-6.55 1.21-12.68 3-19Zm48 0h7c-1 6.46-2.02 12.77-4 19h-7c.47-2.8.95-5.58 1.44-8.38l.4-2.4c1.05-6 1.05-6 2.16-8.22Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||kaiheila.cn^\",\n\t\t\"||kookapp.cn^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"lazada\",\n\tName:    \"Lazada\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 100 100\\\"><path d=\\\"M26 17a2 2 0 0 0-1.1.3L8.5 27.4A3 3 0 0 0 7 30v29.1c0 1 .6 2 1.5 2.6l40 24.8c.2.3.6.4 1 .5h1a3 3 0 0 0 1-.5l40-24.8a3 3 0 0 0 1.5-2.6v-29c0-1.1-.6-2.1-1.5-2.7l-16.4-10a2.1 2.1 0 0 0-2.2 0l-15.4 9.4a14.3 14.3 0 0 1-15 0L27 17.3c-.3-.2-.7-.3-1.1-.3zm0 2 15.4 9.5a16.3 16.3 0 0 0 17.2 0L74 19l16.5 10.1v.1L50 51.4 9.4 29.2h.1L26 19zm48 4c-.4 0-.9 0-1.3.3l-5.5 3.4a.5.5 0 1 0 .6.9l5.4-3.4c.5-.3 1.1-.3 1.6 0l9.4 5.7a.5.5 0 1 0 .5-.8l-9.4-5.8c-.4-.2-.8-.4-1.3-.4zm-8.7 5a.5.5 0 0 0-.3 0l-1.6 1a.5.5 0 0 0 .6 1l1.6-1a.5.5 0 0 0-.3-1zM9 30.1l40.5 22.2v32.6L9.5 60a1 1 0 0 1-.5-.9v-29zm82 0v29c0 .4-.2.7-.5 1l-40 24.7V52.4L91 30.1zM12.5 35a.5.5 0 0 0-.5.5v21.2c0 .8.4 1.6 1.2 2l16 10a.5.5 0 1 0 .6-.8l-16-10c-.5-.2-.8-.7-.8-1.2V35.5a.5.5 0 0 0-.5-.5zm24 37.2a.5.5 0 0 0-.3.9l4 2.5a.5.5 0 1 0 .6-.9l-4-2.4a.5.5 0 0 0-.3-.1zm7 4.3a.5.5 0 0 0-.3 1l1 .6a.5.5 0 1 0 .6-.9l-1-.6a.5.5 0 0 0-.3 0z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||k1-lazadasg-oversea.gslb.ksyuncdn.com^\",\n\t\t\"||lazada.co.id^\",\n\t\t\"||lazada.co.th^\",\n\t\t\"||lazada.com.my^\",\n\t\t\"||lazada.com.ph^\",\n\t\t\"||lazada.com^\",\n\t\t\"||lazada.sg^\",\n\t\t\"||lazada.vn^\",\n\t\t\"||slatic.net^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"leagueoflegends\",\n\tName:    \"League of Legends\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 30 30\\\"><path d=\\\"M 7 4 L 9 7.25 L 9 22.75 L 6.875 26 L 21.957031 26 L 25 22 L 14 22 L 14 4 L 7 4 z M 16 4.0507812 L 16 6.0585938 C 20.493 6.5575937 24 10.375 24 15 C 24 16.849 23.438516 18.569 22.478516 20 L 24.785156 20 C 25.556156 18.498 26 16.801 26 15 C 26 9.272 21.598 4.5577812 16 4.0507812 z M 6.8730469 7.6113281 C 5.0940469 9.5663281 4 12.155 4 15 C 4 17.837 5.0884219 20.418094 6.8574219 22.371094 L 7 22.154297 L 7 19.105469 C 6.365 17.872469 6 16.479 6 15 C 6 13.521 6.365 12.127531 7 10.894531 L 7 7.8164062 L 6.8730469 7.6113281 z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||leagueoflegends.co.kr^\",\n\t\t\"||leagueoflegends.com^\",\n\t\t\"||lol.riotgames.com^\",\n\t\t\"||lolstatic.com^\",\n\t\t\"||lolusercontent.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"line\",\n\tName:    \"LINE\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 9 4 C 6.24 4 4 6.24 4 9 L 4 41 C 4 43.76 6.24 46 9 46 L 41 46 C 43.76 46 46 43.76 46 41 L 46 9 C 46 6.24 43.76 4 41 4 L 9 4 z M 25 11 C 33.27 11 40 16.359219 40 22.949219 C 40 25.579219 38.959297 27.960781 36.779297 30.300781 C 35.209297 32.080781 32.660547 34.040156 30.310547 35.660156 C 27.960547 37.260156 25.8 38.519609 25 38.849609 C 24.68 38.979609 24.44 39.039062 24.25 39.039062 C 23.59 39.039062 23.649219 38.340781 23.699219 38.050781 C 23.739219 37.830781 23.919922 36.789063 23.919922 36.789062 C 23.969922 36.419063 24.019141 35.830937 23.869141 35.460938 C 23.699141 35.050938 23.029062 34.840234 22.539062 34.740234 C 15.339063 33.800234 10 28.849219 10 22.949219 C 10 16.359219 16.73 11 25 11 z M 23.992188 18.998047 C 23.488379 19.007393 23 19.391875 23 20 L 23 26 C 23 26.552 23.448 27 24 27 C 24.552 27 25 26.552 25 26 L 25 23.121094 L 27.185547 26.580078 C 27.751547 27.372078 29 26.973 29 26 L 29 20 C 29 19.448 28.552 19 28 19 C 27.448 19 27 19.448 27 20 L 27 23 L 24.814453 19.419922 C 24.602203 19.122922 24.294473 18.992439 23.992188 18.998047 z M 15 19 C 14.448 19 14 19.448 14 20 L 14 26 C 14 26.552 14.448 27 15 27 L 18 27 C 18.552 27 19 26.552 19 26 C 19 25.448 18.552 25 18 25 L 16 25 L 16 20 C 16 19.448 15.552 19 15 19 z M 21 19 C 20.448 19 20 19.448 20 20 L 20 26 C 20 26.552 20.448 27 21 27 C 21.552 27 22 26.552 22 26 L 22 20 C 22 19.448 21.552 19 21 19 z M 31 19 C 30.448 19 30 19.448 30 20 L 30 26 C 30 26.552 30.448 27 31 27 L 34 27 C 34.552 27 35 26.552 35 26 C 35 25.448 34.552 25 34 25 L 32 25 L 32 24 L 34 24 C 34.553 24 35 23.552 35 23 C 35 22.448 34.553 22 34 22 L 32 22 L 32 21 L 34 21 C 34.552 21 35 20.552 35 20 C 35 19.448 34.552 19 34 19 L 31 19 z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||gcld-line.com^\",\n\t\t\"||lin.ee^\",\n\t\t\"||line-apps-beta.com^\",\n\t\t\"||line-apps-rc.com^\",\n\t\t\"||line-apps.com^\",\n\t\t\"||line-cdn.net^\",\n\t\t\"||line-scdn.net^\",\n\t\t\"||line.biz^\",\n\t\t\"||line.me^\",\n\t\t\"||line.naver.jp^\",\n\t\t\"||linecorp.com^\",\n\t\t\"||linefriends.com.tw^\",\n\t\t\"||linefriends.com^\",\n\t\t\"||linegame.jp^\",\n\t\t\"||linemobile.com^\",\n\t\t\"||linemyshop.com^\",\n\t\t\"||lineshoppingseller.com^\",\n\t\t\"||linetv.tw^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"linkedin\",\n\tName:    \"LinkedIn\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M41,4H9C6.24,4,4,6.24,4,9v32c0,2.76,2.24,5,5,5h32c2.76,0,5-2.24,5-5V9C46,6.24,43.76,4,41,4z M17,20v19h-6V20H17z M11,14.47c0-1.4,1.2-2.47,3-2.47s2.93,1.07,3,2.47c0,1.4-1.12,2.53-3,2.53C12.2,17,11,15.87,11,14.47z M39,39h-6c0,0,0-9.26,0-10 c0-2-1-4-3.5-4.04h-0.08C27,24.96,26,27.02,26,29c0,0.91,0,10,0,10h-6V20h6v2.56c0,0,1.93-2.56,5.81-2.56 c3.97,0,7.19,2.73,7.19,8.26V39z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||bizographics.com^\",\n\t\t\"||cs1404.wpc.epsiloncdn.net^\",\n\t\t\"||cs767.wpc.epsiloncdn.net^\",\n\t\t\"||l-0005.dc-msedge.net^\",\n\t\t\"||l-0005.l-dc-msedge.net^\",\n\t\t\"||l-0005.l-msedge.net^\",\n\t\t\"||l-0015.l-msedge.net^\",\n\t\t\"||licdn.cn^\",\n\t\t\"||licdn.com^\",\n\t\t\"||linkedin.at^\",\n\t\t\"||linkedin.be^\",\n\t\t\"||linkedin.cn^\",\n\t\t\"||linkedin.com^\",\n\t\t\"||linkedin.nl^\",\n\t\t\"||linkedin.qtlcdn.com^\",\n\t\t\"||lnkd.in^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"lionsgateplus\",\n\tName:    \"Lionsgate+\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-21 2 120 120\\\"><path d=\\\"M35 3.7v84.8h43.9v31.8H0V3.7Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||lionsgateplus.com^\",\n\t\t\"||starz.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"looke\",\n\tName:    \"Looke\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -28 100 100\\\"><path d=\\\"m16.1.1-2-.1C7 0 0 5.2 0 11.2 0 15.4 2.4 17 6.2 17 5 16 4 14.8 4 11.4 4 7 6.7 4 11 2.8v33.5h1c6.1 0 10.5 7.5 15.5 7.5 3.3 0 5.3-2.3 5.3-5 0-.4 0-.8-.2-1.2a9 9 0 0 1-3.6 1c-5.4 0-8.8-3.4-12.9-4.3V.3z\\\"/><path d=\\\"M31.6 11.2c-7.1 0-9.3 7.7-9.3 13.2 0 8.2 4.7 11.1 9 11.1 5 0 8-4.7 8-12.5v-2c3-.4 6-1.8 7.3-3.9L46 16a9.2 9.2 0 0 1-6.5 2.8H39c-.6-4.2-2.5-7.5-7.5-7.5zm.5 20.7c-2.1 0-4.6-1.5-4.6-7.7 0-4.6 1.4-10.4 5.4-10.4 1.4 0 2.6.8 3.4 3-1.2 0-2 .5-2 2 0 1.6.9 2.3 2.7 2.4v1.1c0 6-1.6 9.6-4.9 9.6z\\\"/><path d=\\\"M51.6 11.2c-7.1 0-9.3 7.7-9.3 13.2 0 8.2 4.7 11.1 9 11.1 5 0 8-4.7 8-12.5v-2c3-.4 6-1.8 7.3-3.9L66 16a9.2 9.2 0 0 1-6.5 2.8H59c-.6-4.2-2.5-7.5-7.5-7.5zm.5 20.7c-2.1 0-4.6-1.5-4.6-7.7 0-4.6 1.4-10.4 5.4-10.4 1.4 0 2.6.8 3.4 3-1.2 0-2 .5-2 2 0 1.6.9 2.3 2.7 2.4v1.1c0 6-1.6 9.6-4.9 9.6z\\\"/><path d=\\\"M63 2.6v32.6h4.7v-10c1-2.8 2.2-3.7 3.9-3.7 1.6 0 3 .8 3 4.2V30c0 3 1.5 5.4 5.3 5.4 2 0 5.2-.6 6.6-8.8h-1.7c-.8 3.6-2 5.2-3.6 5.2-1.7 0-1.9-1.6-1.9-2.5l.2-4.1c0-3.1-.8-7.4-6.5-7.4h-.5l8-6.5h-4.6l-8.2 7.8V1.9Z\\\"/><path d=\\\"M99.6 17.4c0-5-3.2-6.2-6.4-6.2-6.8 0-9 8-9 13.3 0 8 4.8 11 9 11 3.6 0 6.8-1.9 6.8-4.7l-.1-1.2c-1.2 1.7-3.1 2.3-5.2 2.3-2.7 0-5-1.1-5.5-6.2 6.5-.7 10.4-3.9 10.4-8.3zm-10.4 6.5c0-4.7 1.6-10.4 5-10.4 1.5 0 2.3 1 2.3 3.4 0 3.6-2.8 6.2-7.3 7z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||looke.com.br^\",\n\t\t\"||ottvs.com.br^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"mail_ru\",\n\tName:    \"Mail.ru\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 512 512\\\"><path d=\\\"M256 141.176c-63.306 0-114.809 51.503-114.809 114.809S192.694 370.795 256 370.795s114.809-51.503 114.809-114.809S319.306 141.176 256 141.176zm0 188.254c-40.498 0-73.445-32.947-73.445-73.445 0-40.498 32.947-73.445 73.445-73.445 40.499 0 73.445 32.947 73.445 73.445 0 40.498-32.946 73.445-73.445 73.445z\\\" /><path d=\\\"M437.008 74.97C388.656 26.623 324.375 0 256 0h-.017C187.603.004 123.318 26.637 74.97 74.992 26.62 123.347-.005 187.637 0 256.017c.004 68.379 26.637 132.666 74.992 181.014C123.344 485.377 187.625 512.001 256 512h.017c55.945-.004 111.216-18.738 155.631-52.752 9.07-6.945 10.792-19.927 3.846-28.995-6.945-9.069-19.926-10.794-28.995-3.846-37.24 28.518-83.58 44.224-130.486 44.228h-.014c-57.324 0-111.224-22.324-151.761-62.856-40.542-40.536-62.871-94.435-62.875-151.766-.006-118.35 96.273-214.641 214.623-214.649H256c118.34 0 214.628 96.279 214.636 214.622v23.532c0 27.523-22.39 49.913-49.913 49.913-27.523 0-49.913-22.391-49.913-49.913v-23.532c0-11.422-9.259-20.682-20.682-20.682s-20.682 9.26-20.682 20.682v23.532c0 50.33 40.947 91.278 91.278 91.278S512 329.848 512 279.518v-23.534c-.005-68.38-26.638-132.666-74.992-181.014z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||imgsmail.ru^\",\n\t\t\"||mail.ru^\",\n\t\t\"||mycdn.me^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"manus\",\n\tName:    \"Manus\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><g clip-path=\\\"url(#a)\\\"><g clip-path=\\\"url(#b)\\\"><path fill=\\\"var(--logo-color)\\\" d=\\\"M17.6 22.6c1.7-1 2.8-6.7 2.6-8.4 0 0-.3-1-1-1s-.8.8-.8.8-1.1 4-2.1 4.5-2 0-2 0c.8 2.7 1.7 5.1 3.3 4\\\"/><path fill=\\\"var(--logo-color)\\\" d=\\\"m14.9 9.4-1.7-.3V9zm.2-1 1.7.3v.1zm-.2-.8.4-1.6h.2l.2.1zm-1.3.4-1.4-1.1v-.1h.2zm-1.2 1.6 1.6.8-.2.3-.2.2zm-.6.9 1.7-.3.7 3.3-3.1-1.5zm-4 3.6-1.8.4zm.8.7V13q.8 0 1.4.7.5.8.2 1.6zM8.4 16H10zm7.2-3.9 1.5-.9zm-2.8 6-.5 1.6zm3 .7.4-1.7zm2-3.4.6-1.6zm.4.2L19 14l1 .5v1.1zm-.9 2.1-1.4-1zM5 9l-1.5-.8zm3.2 2.1.7-1.5zM8 8.8l-.5 1.6zm5.3.2.3-1 3.3.8-.2 1zm2-.6L13.3 8v.3q0 .3.7.8l1.6-3q1 1 1.2 1.7v.9zm-.3-.8-.5 1.7h.4l-2.4-2.6a3 3 0 0 1 2.9-.7zm0 1.5-1 1.3L11 9l1.3-2zm-1.3 1.8-.3.3.1-.2q.1-.1 0-.8l-3.3.6.1-1.2.4-.5.7-.7zm-4.2 2.9-.2-.4-.3-.3h-.3v3.4h-.4l-.6-.2q-1-.4-1.6-1.8zm-.8 1 1.6.6-.1.6-3.5.1a5 5 0 0 1 .3-2zm6 .5q-.4-.8-.3-1.2v-.6l-.1-.4 2.9-1.8q.8 1.5.6 2.3v.5l-.1-.3zm-.4-2.2-.4-.6q-.3-.3-.5-.9-.4-1.2-.1-2.5l3.4.7v.9q.2 0 .2.3l.3.3zm-.8 3.5 2.7.6-.2 1.7-.4 1.6-3.2-.7zm4.2-2.8-.3-.3h.2l.4.2h.3l.2.2-1.1 3.2-.4-.2-1.4-.6q-.5-.3-1-1zm.8 0 .4.2.5.4.4.5-.8-.8-1.4 3.1-.5-.3-.5-.6.8.8zm1.5 2-.4 1.7-.8 1.4-2.8-2 .6-1.2q0-.2 0 0zm-1.2 3q-.4.9-1.2 1.4a2 2 0 0 1-2 .3l.8-3.3h-.4l-.4.1.4-.5zM10.1 16q-.2-.5-.2-.5l.7.3 1.4.4q.7 0 1.4.4l-1.2 3.2-1-.3-1.5-.4q-.9-.2-1.8-.8a3 3 0 0 1-1.3-2.2zM6.5 9.8V9l-.2-.4.4.2 1 .4.6.1.5.2-1.3 3.1h-.1l-.6-.2q-.6-.1-1.4-.5-.6-.3-1.6-1-.4-.4-.6-1.2-.1-.9.2-1.6zm2.3-.2 1 .7q.6.8.6 1.4-.1 1.2-.5 1.7l-.3.4-.1.4q-.1.3 0-.4l-3.5.7v-1.2l.4-.7q0-.3.3-.5l.2-.4v-.2.3l.3.7.3.2zM11 12l-3.7-1.6 1-3.2c1.3.4 3.2 1.4 4 1.8zm-3.7-1.6L6 10h.1l.3-.3-3.1-1.6a3 3 0 0 1 2.5-1.5q1.4 0 2.6.5z\\\" mask=\\\"url(#c)\\\"/><path stroke=\\\"var(--logo-color)\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" stroke-width=\\\"1.7\\\" d=\\\"M7.7 12c.6-.6 2.3-1 4-.2 1.9.8 2.2 3.4 1.4 4.3-.7.8-2.4 0-2.4 0\\\"/><path stroke=\\\"var(--logo-color)\\\" stroke-linecap=\\\"round\\\" stroke-miterlimit=\\\"10\\\" stroke-width=\\\"1.7\\\" d=\\\"M6.4 5.1q-1-1-1.9-1.3M9.1 4l-.5-2.3M12 4.5q.2-1.4.8-2.4\\\"/><mask id=\\\"c\\\" width=\\\"20.7\\\" height=\\\"21.9\\\" x=\\\"2.1\\\" y=\\\"2.4\\\" fill=\\\"#000\\\" maskUnits=\\\"userSpaceOnUse\\\"><path fill=\\\"#fff\\\" d=\\\"M2.1 2.4h20.7v21.9H2.1z\\\"/><path d=\\\"m15.1 8.4-.2 1c-.3 1.5.1 1.8.7 2.8s.2 1.9.5 2.4 1.3.7 1.8.9c.4.1 0 0 .3.2 0 .6-.6 1.7-.9 2.1q-1 1.4-1.4 1l-3-.6c-1.3-.5-4.5-.7-4.5-2.1q.1-1.2.2-1.3s-.7 0-.9-.7c-.2-.9 1.9-2.4.4-3C7.6 11 4.3 10.3 5 9q.7-1 3-.2l3.9 1.7q0-.3.6-.9.4-.7 1.2-1.6c.6-.6 1.3-.4 1.3-.4.3.2.2.8.2.8\\\"/></mask></g></g><defs><clipPath id=\\\"a\\\"><path fill=\\\"#fff\\\" d=\\\"M0 24h24V0H0z\\\"/></clipPath><clipPath id=\\\"b\\\"><path fill=\\\"#fff\\\" d=\\\"M0 24h24V0H0z\\\"/></clipPath></defs></svg>\"),\n\tRules: []string{\n\t\t\"||manus.im^\",\n\t\t\"||manuscdn.com^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"mastodon\",\n\tName:    \"Mastodon\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 512 512\\\"><path d=\\\"M433 179.11c0-97.2-63.71-125.7-63.71-125.7-62.52-28.7-228.56-28.4-290.48 0 0 0-63.72 28.5-63.72 125.7 0 115.7-6.6 259.4 105.63 289.1 40.51 10.7 75.32 13 103.33 11.4 50.81-2.8 79.32-18.1 79.32-18.1l-1.7-36.9s-36.31 11.4-77.12 10.1c-40.41-1.4-83-4.4-89.63-54a102.54 102.54 0 0 1-.9-13.9c85.63 20.9 158.65 9.1 178.75 6.7 56.12-6.7 105-41.3 111.23-72.9 9.8-49.8 9-121.5 9-121.5zm-75.12 125.2h-46.63v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.33V197c0-58.5-64-56.6-64-6.9v114.2H90.19c0-122.1-5.2-147.9 18.41-175 25.9-28.9 79.82-30.8 103.83 6.1l11.6 19.5 11.6-19.5c24.11-37.1 78.12-34.8 103.83-6.1 23.71 27.3 18.4 53 18.4 175z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||aus.social^\",\n\t\t\"||awscommunity.social^\",\n\t\t\"||climatejustice.social^\",\n\t\t\"||cupoftea.social^\",\n\t\t\"||cyberplace.social^\",\n\t\t\"||defcon.social^\",\n\t\t\"||det.social^\",\n\t\t\"||glasgow.social^\",\n\t\t\"||h4.io^\",\n\t\t\"||hachyderm.io^\",\n\t\t\"||hessen.social^\",\n\t\t\"||hostux.social^\",\n\t\t\"||ieji.de^\",\n\t\t\"||indieweb.social^\",\n\t\t\"||infosec.exchange^\",\n\t\t\"||ioc.exchange^\",\n\t\t\"||kolektiva.social^\",\n\t\t\"||livellosegreto.it^\",\n\t\t\"||lor.sh^\",\n\t\t\"||lou.lt^\",\n\t\t\"||m.cmx.im^\",\n\t\t\"||mas.to^\",\n\t\t\"||masto.ai^\",\n\t\t\"||masto.es^\",\n\t\t\"||masto.nu^\",\n\t\t\"||masto.pt^\",\n\t\t\"||mastodon.au^\",\n\t\t\"||mastodon.bida.im^\",\n\t\t\"||mastodon.com.tr^\",\n\t\t\"||mastodon.eus^\",\n\t\t\"||mastodon.green^\",\n\t\t\"||mastodon.ie^\",\n\t\t\"||mastodon.iriseden.eu^\",\n\t\t\"||mastodon.nl^\",\n\t\t\"||mastodon.nu^\",\n\t\t\"||mastodon.nz^\",\n\t\t\"||mastodon.online^\",\n\t\t\"||mastodon.online^\",\n\t\t\"||mastodon.scot^\",\n\t\t\"||mastodon.sdf.org^\",\n\t\t\"||mastodon.social^\",\n\t\t\"||mastodon.social^\",\n\t\t\"||mastodon.top^\",\n\t\t\"||mastodon.uno^\",\n\t\t\"||mastodon.world^\",\n\t\t\"||mastodon.zaclys.com^\",\n\t\t\"||mastodonapp.uk^\",\n\t\t\"||mastodont.cat^\",\n\t\t\"||mastodontech.de^\",\n\t\t\"||mastodontti.fi^\",\n\t\t\"||mastouille.fr^\",\n\t\t\"||mathstodon.xyz^\",\n\t\t\"||metalhead.club^\",\n\t\t\"||mindly.social^\",\n\t\t\"||mstdn.ca^\",\n\t\t\"||mstdn.jp^\",\n\t\t\"||mstdn.party^\",\n\t\t\"||mstdn.plus^\",\n\t\t\"||mstdn.social^\",\n\t\t\"||muenchen.social^\",\n\t\t\"||muenster.im^\",\n\t\t\"||nerdculture.de^\",\n\t\t\"||noc.social^\",\n\t\t\"||norden.social^\",\n\t\t\"||nrw.social^\",\n\t\t\"||o3o.ca^\",\n\t\t\"||ohai.social^\",\n\t\t\"||piaille.fr^\",\n\t\t\"||pol.social^\",\n\t\t\"||ravenation.club^\",\n\t\t\"||rollenspiel.social^\",\n\t\t\"||ruby.social^\",\n\t\t\"||ruhr.social^\",\n\t\t\"||sfba.social^\",\n\t\t\"||socel.net^\",\n\t\t\"||social.anoxinon.de^\",\n\t\t\"||social.cologne^\",\n\t\t\"||social.dev-wiki.de^\",\n\t\t\"||social.linux.pizza^\",\n\t\t\"||social.politicaconciencia.org^\",\n\t\t\"||social.vivaldi.net^\",\n\t\t\"||stranger.social^\",\n\t\t\"||sueden.social^\",\n\t\t\"||tech.lgbt^\",\n\t\t\"||techhub.social^\",\n\t\t\"||theblower.au^\",\n\t\t\"||tkz.one^\",\n\t\t\"||todon.eu^\",\n\t\t\"||toot.aquilenet.fr^\",\n\t\t\"||toot.community^\",\n\t\t\"||toot.funami.tech^\",\n\t\t\"||toot.io^\",\n\t\t\"||toot.wales^\",\n\t\t\"||troet.cafe^\",\n\t\t\"||union.place^\",\n\t\t\"||universeodon.com^\",\n\t\t\"||urbanists.social^\",\n\t\t\"||wien.rocks^\",\n\t\t\"||wxw.moe^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"max\",\n\tName:    \"MAX\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 42 42\\\"><path d=\\\"M21.5 41.9c-4.1 0-6-.6-9.4-3-2 2.7-8.7 4.8-9 1.2q-.1-3.9-1.3-7.5C1 29.5.1 26 .1 21A21 21 0 0 1 21.4.3C32.9.3 42 9.7 42 21.3a20.6 20.6 0 0 1-20.5 20.6m.1-31.3c-5.6-.3-10 3.6-11 9.7-.7 5 .7 11.2 1.9 11.5.6.1 2-1 3-2a10 10 0 0 0 5 1.8 10.7 10.7 0 0 0 11.2-10 10.7 10.7 0 0 0-10-11\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||max.ru^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"mercado_libre\",\n\tName:    \"Mercado Libre\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 256 256\\\"><path d=\\\"M114 56.6c-10.8 1.3-23.7 4-32.6 6.9C31.3 79.9 8.6 119.2 29 154.7c9.3 16.2 31 31.5 55 38.7 16 4.7 25.2 6 44 6 12.6 0 20-.5 26.5-1.7 48.6-9.5 79.9-36.8 79.9-69.7 0-14.3-5.3-26.4-16.6-38.6-13.8-14.7-35-25.4-60.7-30.5-9.3-1.8-35.3-3.2-43.1-2.3zm39 7.1c16.2 2.6 38.4 10.9 47.5 17.9 2.4 1.8 1.6 2.5-6.2 4.9-9.5 3-21.9 1.7-39.2-3.9-22.2-7.2-29-7.2-41.2.3-5 3.2-13.2 5.4-23.5 6.6-6.6.7-17.8-1.4-28.8-5.6-6.9-2.5-6.9-2.6 4.9-8.6C90.9 62.9 122 58.7 153 63.7zm-13.7 19.9c3.9.9 10.1 2.8 13.6 4.1 9.3 3.5 21.8 6.3 28.1 6.3 6.4 0 15.6-2.2 21.7-5.1 5.3-2.5 6.5-1.9 13.4 6.4 5.1 6.1 11.9 18.6 11.9 21.8 0 1.2-1.3 1.9-4.7 2.4-7.3 1.1-20.9 5.3-29.6 9.1l-7.8 3.4-11.7-10.9C153.4 101.6 141 93 133.6 93c-4.8 0-13.2 3.9-22.7 10.5-5.8 4.1-8.7 5.5-11.3 5.5-4.9 0-5.2-1.4-1.3-6 6.8-8 21.5-18.5 28-20 4.7-1.2 5.2-1.1 13 .6zm-65.6 9.5c6.7 1.6 10.8 2 17.1 1.6l8.3-.5-4.6 4.9c-7 7.5-6.2 12.8 2.3 15.4 4.1 1.2 8.6-.4 15.7-5.6 13.7-10 20.2-12.2 26.8-9 6.1 3 19.9 14.4 34.5 28.6l12.7 12.3-2.1 2.9c-1.2 1.5-3 2.8-4.1 2.8-1.2 0-6.5-4-12.2-9.2-10-9-13.1-10.5-13.1-6.3 0 1 3.8 5.3 8.4 9.5 4.7 4.2 8.7 8.3 9 9 .7 2-3.2 6.5-5.6 6.5-1.2 0-5.3-3.2-9.9-7.6-5.2-5.1-8.3-7.5-9.4-7.1-2.9 1.2-1.5 4.2 5.4 11.2 6 6 6.8 7.3 6 9.3-.4 1.2-2 2.7-3.4 3.4-2.4 1.1-3 .8-8.8-5-6.3-6.2-9.2-7.5-10.4-4.6-.4 1 .9 3.1 3.7 5.9 2.3 2.4 4 4.8 3.6 5.3-.9 1.5-5.7 2.1-8.3 1.1-1.9-.7-2.3-1.6-2.3-5 0-7.5-4.2-12.9-10-12.9-1.5 0-2-.7-2-2.8 0-6.4-5.9-11.1-11.9-9.5-1.6.4-3-.3-5.2-2.5-3.5-3.5-8.1-4.8-12.1-3.3-2 .8-3.4.6-6.4-1-4.7-2.3-6.8-2.4-10.3-.3-2.6 1.6-3 1.5-6.7-.8-9-5.7-29.7-12.8-37-12.8-1.2 0-2.5-.4-2.8-.9-.3-.5 1.2-4.6 3.5-9 2.8-5.7 6.1-10.2 10.9-15.1l6.8-6.9 7.6 3c4.2 1.6 11.5 3.9 16.3 5zm-31.2 31.6c3.9 1 11.5 4 17 6.8l10 5-.3 4.3c-.3 3.7.1 4.7 3.1 7.7 2.1 2.1 4.5 3.5 6 3.5 1.8 0 2.8.8 3.7 3 1.7 4.2 4.7 6 9.7 6 3.7 0 4.5.4 5.7 2.9 1.7 3.2 3.4 4.1 7.9 4.1 2 0 4.1.9 5.8 2.6 4 3.8 10.2 4.9 14.9 2.6 3.1-1.5 4.1-1.6 6.6-.5 4.2 1.8 9.4 1.6 12.9-.5 2-1.2 4.3-1.7 7.1-1.4 3.5.3 4.7-.1 7.8-2.8 2-1.8 3.6-3.8 3.6-4.5 0-.7 1.7-1.5 3.9-1.9 3.6-.5 9.1-5.1 9.1-7.6 0-.4 1.4-1 3.1-1.4 6.8-1.3 11.7-7.6 10.6-13.5-.6-2.7-.3-3 5.1-5.4 6.7-3 20.1-7.2 25.7-8.2 2.2-.4 4.8-.9 5.9-1.2 1.7-.5 1.8 0 1.4 7.5-.8 14-9.6 28.6-23.5 39.1-20.3 15.3-47 23.3-77.3 23.3-23.5 0-44.1-4.6-62-13.8-17.3-9-28.2-19.3-34.9-33.3-3-6.4-3.6-8.7-3.9-16.4l-.5-9 4.4.6c2.4.4 7.6 1.5 11.4 2.4zm42 13.3c1.8 3.5 3.5 3.8 6 1 1.4-1.5 2.9-2 5.4-1.8 3 .2 3.9.9 5 3.5 1.6 3.7 3.5 4.7 6.1 3.3 3.1-1.6 6.8-1.2 8 1 .6 1.1.7 3.7.4 6l-.7 4h4.6c3.7 0 4.9.5 6.6 2.6 2.7 3.5 2.6 5.9-.4 8.9-4.3 4.4-10.6 2.4-12.9-3.9-.6-1.8-2.3-1.8-6.9 0-1.8.7-4.7-2.1-4.7-4.5 0-3.2-2.1-4.1-6.8-2.8-5.1 1.4-7.3-.2-7.7-5.4-.2-1.9-.3-3.8-.4-4.2-.1-.4-2.3-.5-5.1-.1-4.7.6-5 .5-6-2-2.6-7 6.1-12.2 9.5-5.6z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||mercadolibre.cl^\",\n\t\t\"||mercadolibre.co.cr^\",\n\t\t\"||mercadolibre.com.ar^\",\n\t\t\"||mercadolibre.com.bo^\",\n\t\t\"||mercadolibre.com.co^\",\n\t\t\"||mercadolibre.com.do^\",\n\t\t\"||mercadolibre.com.ec^\",\n\t\t\"||mercadolibre.com.gt^\",\n\t\t\"||mercadolibre.com.hn^\",\n\t\t\"||mercadolibre.com.mx^\",\n\t\t\"||mercadolibre.com.ni^\",\n\t\t\"||mercadolibre.com.pa^\",\n\t\t\"||mercadolibre.com.pe^\",\n\t\t\"||mercadolibre.com.py^\",\n\t\t\"||mercadolibre.com.sv^\",\n\t\t\"||mercadolibre.com.uy^\",\n\t\t\"||mercadolibre.com.ve^\",\n\t\t\"||mercadolibre.com^\",\n\t\t\"||mercadolivre.com.br^\",\n\t\t\"||mlstatic.com^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"meta_ai\",\n\tName:    \"Meta AI\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M12 0a12 12 0 1 1 0 24 12 12 0 0 1 0-24m0 3.6a8.4 8.4 0 1 0 0 16.8 8.4 8.4 0 0 0 0-16.8\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||meta.ai^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"microsoft_teams\",\n\tName:    \"Microsoft Teams\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M20 2a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13m16.5 4a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11M15.7 18a7 7 0 0 0-6.5 5h9.3c3 0 5.5 2.5 5.5 5.5v11c0 3-2.5 5.5-5.5 5.5h-4.7q2.8 2 6.4 2h13.5a8 8 0 0 1-2.7-6V24.7c0-3.7-3-6.7-6.7-6.7zm15.9 2q1.4 2.1 1.4 4.7V41a6 6 0 0 0 6 6 6 6 0 0 0 5.9-6V26.8c0-3.8-3-6.8-6.8-6.8zM7.5 25c-2 0-3.5 1.6-3.5 3.5v11c0 2 1.6 3.5 3.5 3.5h11c2 0 3.5-1.6 3.5-3.5v-11c0-2-1.6-3.5-3.5-3.5zm1.7 4h7.6v1.8H14V39h-2v-8.2H9.2z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||teams.cdn.office.net^\",\n\t\t\"||teams.live.com^\",\n\t\t\"||teams.microsoft.com^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"minecraft\",\n\tName:    \"Minecraft\",\n\tIconSVG: []byte(\"<svg fill=\\\"currentColor\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\"  viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 10.5 4 C 10.066406 4 9.695313 4.273438 9.5625 4.6875 L 0.0625 34.28125 C -0.0546875 34.648438 0.0273438 35.078125 0.3125 35.34375 L 10.40625 44.71875 C 10.589844 44.890625 10.839844 45 11.09375 45 L 22 45 C 22.449219 45 22.851563 44.683594 22.96875 44.25 L 24.3125 39.34375 C 24.40625 39.371094 24.492188 39.40625 24.59375 39.40625 L 27.03125 39.40625 L 31.65625 44.65625 C 31.847656 44.875 32.117188 45 32.40625 45 L 43.6875 45 C 44.167969 45 44.597656 44.660156 44.6875 44.1875 L 49.96875 16.1875 C 50.011719 15.96875 49.976563 15.730469 49.875 15.53125 L 44.1875 4.53125 C 44.015625 4.199219 43.6875 4 43.3125 4 L 31.90625 4 C 31.449219 4 31.050781 4.308594 30.9375 4.75 L 29.8125 9.125 L 27.46875 9.1875 C 27.03125 9.203125 26.648438 9.515625 26.53125 9.9375 L 26.46875 10.125 L 22.3125 4.40625 C 22.125 4.148438 21.820313 4 21.5 4 Z M 11.21875 6 L 20.09375 6 L 18.71875 10.59375 L 9.75 10.59375 Z M 32.6875 6 L 41.96875 6 L 39.6875 15.59375 L 35 15.59375 C 35.046875 15.449219 35.074219 15.3125 35.09375 15.15625 L 35.59375 11.71875 C 35.703125 10.734375 35.359375 10.113281 35.0625 9.78125 C 34.78125 9.46875 34.277344 9.09375 33.40625 9.09375 L 31.875 9.09375 Z M 30.625 11.09375 L 33.40625 11.09375 C 33.542969 11.09375 33.589844 11.125 33.59375 11.125 C 33.609375 11.152344 33.644531 11.277344 33.625 11.46875 L 33.09375 14.875 C 33.050781 15.273438 32.679688 15.5 32.3125 15.5 C 32.15625 15.5 32.007813 15.53125 31.875 15.59375 L 27.125 15.59375 L 28.0625 12.15625 C 28.089844 12.054688 28.097656 11.945313 28.09375 11.84375 L 28.28125 11.1875 Z M 9.125 12.59375 L 20.71875 12.59375 L 19.71875 16.34375 C 19.640625 16.644531 19.714844 16.972656 19.90625 17.21875 C 20.09375 17.464844 20.378906 17.59375 20.6875 17.59375 L 39.25 17.59375 L 35.78125 33.6875 L 27 33.6875 L 29.96875 22.65625 C 30.050781 22.355469 29.96875 22.027344 29.78125 21.78125 C 29.59375 21.535156 29.3125 21.40625 29 21.40625 L 25.5 21.40625 C 25.046875 21.40625 24.648438 21.71875 24.53125 22.15625 L 23.21875 27 L 19.125 27 L 20.375 22.5625 C 20.460938 22.261719 20.375 21.9375 20.1875 21.6875 C 20 21.4375 19.71875 21.3125 19.40625 21.3125 L 15.90625 21.3125 C 15.460938 21.3125 15.0625 21.605469 14.9375 22.03125 L 13.8125 25.90625 C 13.742188 26 13.660156 26.101563 13.625 26.21875 L 13.59375 26.40625 L 10.15625 26.40625 L 10.5625 25.1875 C 10.75 24.53125 10.628906 23.84375 10.25 23.34375 C 9.890625 22.867188 9.320313 22.59375 8.6875 22.59375 L 5.90625 22.59375 Z M 5.25 24.59375 L 8.65625 24.59375 L 8.28125 25.71875 C 8.089844 26.167969 8.042969 26.613281 8.15625 27.03125 C 7.808594 27.316406 7.5625 27.6875 7.4375 28.125 L 7.0625 29.46875 C 6.773438 30.195313 6.832031 30.976563 7.21875 31.5625 C 7.53125 32.035156 8.183594 32.59375 9.59375 32.59375 L 11.8125 32.59375 L 11.53125 33.5 L 2.375 33.59375 Z M 10.09375 28.40625 L 13 28.40625 L 12.375 30.59375 L 9.59375 30.59375 C 9.015625 30.59375 8.875 30.46875 8.875 30.46875 C 8.863281 30.441406 8.851563 30.316406 8.90625 30.1875 C 8.921875 30.148438 8.957031 30.101563 8.96875 30.0625 L 9.375 28.6875 C 9.410156 28.558594 9.714844 28.40625 10.09375 28.40625 Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||minecraft.net^\",\n\t\t\"||minecraftservices.com^\",\n\t\t\"||mojang.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"nebula\",\n\tName:    \"Nebula\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50.8 50.8\\\"><path d=\\\"m20.404 31.36 4.847 14.195 4.6-14.295 14.988-.05-11.97-9.002 4.748-14.344-12.316 9.002-12.416-9.15 4.65 14.492-11.773 9.003Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||nebula.app^\",\n\t\t\"||nebula.tv^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"netflix\",\n\tName:    \"Netflix\",\n\tIconSVG: []byte(\"<svg fill=\\\"currentColor\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\"  viewBox=\\\"0 0 30 30\\\"><path d=\\\"M24,4H6C4.895,4,4,4.895,4,6v18c0,1.105,0.895,2,2,2h18c1.105,0,2-0.895,2-2V6C26,4.895,25.105,4,24,4z M19,22c0,0-1.5-0.232-3-0.232l-2-5.507v5.507c-1.5,0-3,0.232-3,0.232V8h3l2,5.6V8h3V22z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"|netflix.com.edgesuite.net^\",\n\t\t\"||dualstack.apiproxy-*.amazonaws.com^\",\n\t\t\"||dualstack.ichnaea-web-*.amazonaws.com^\",\n\t\t\"||fast.com^\",\n\t\t\"||netflix.ca^\",\n\t\t\"||netflix.com^\",\n\t\t\"||netflix.net^\",\n\t\t\"||netflixdnstest1.com^\",\n\t\t\"||netflixdnstest10.com^\",\n\t\t\"||netflixdnstest2.com^\",\n\t\t\"||netflixdnstest3.com^\",\n\t\t\"||netflixdnstest4.com^\",\n\t\t\"||netflixdnstest5.com^\",\n\t\t\"||netflixdnstest6.com^\",\n\t\t\"||netflixdnstest7.com^\",\n\t\t\"||netflixdnstest8.com^\",\n\t\t\"||netflixdnstest9.com^\",\n\t\t\"||netflixinvestor.com^\",\n\t\t\"||netflixtechblog.com^\",\n\t\t\"||nflxext.com^\",\n\t\t\"||nflximg.com^\",\n\t\t\"||nflximg.net^\",\n\t\t\"||nflxsearch.net^\",\n\t\t\"||nflxso.net^\",\n\t\t\"||nflxvideo.net^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"nintendo\",\n\tName:    \"Nintendo\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M6 7v36h12.6V21.75l13 20.78.27.47H44V7H31.4v1l.04 20.22L18.5 7.47 18.22 7Zm2 2h9.1l14.5 23.22 1.84 3v-3.5L33.4 9H42v32h-9L18.44 17.75l-1.85-2.94V41H8Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||nintendo-europe.com^\",\n\t\t\"||nintendo.be^\",\n\t\t\"||nintendo.co.jp^\",\n\t\t\"||nintendo.co.uk^\",\n\t\t\"||nintendo.com.au^\",\n\t\t\"||nintendo.com^\",\n\t\t\"||nintendo.de^\",\n\t\t\"||nintendo.es^\",\n\t\t\"||nintendo.eu^\",\n\t\t\"||nintendo.fr^\",\n\t\t\"||nintendo.it^\",\n\t\t\"||nintendo.jp^\",\n\t\t\"||nintendo.net^\",\n\t\t\"||nintendo.nl^\",\n\t\t\"||nintendo.pt^\",\n\t\t\"||nintendoswitch.cn^\",\n\t\t\"||nintendowifi.net^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"nvidia\",\n\tName:    \"Nvidia\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 48 48\\\"><path d=\\\"M20 8a2 2 0 0 0-2 2v2.55l.84-.05c10.76-.37 17.78 8.82 17.78 8.82s-8.05 9.8-16.44 9.8c-.73 0-1.47-.07-2.18-.19v-2.2c.73.23 1.52.35 2.3.35 5.88 0 11.35-7.6 11.35-7.6s-5.07-6.91-12.81-6.66l-.82.03v-2.3c-9.49.77-17.68 8.8-17.68 8.8S4.97 34.76 18 35.98v-2.44c.59.07 1.22.12 1.81.12 7.82 0 13.47-3.99 18.94-8.7.91.73 4.62 2.49 5.4 3.26-5.2 4.36-17.33 7.86-24.2 7.86-.66 0-1.32-.03-1.95-.1V38c0 1.1.9 2 2 2h25a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H20zm-2 6.86v2.82a11.8 11.8 0 0 1 1.57-.07c4.95 0 7.9 3.85 7.9 3.85l-4.03 3.39c-1.8-3.02-2.43-4.35-5.44-4.7v8.57c-4.06-1.38-5.4-6.14-5.4-6.14s2.37-2.83 5.38-2.46H18v-2.44a15.66 15.66 0 0 0-9.22 4.46s2 7.52 9.22 8.8v2.6c-9.56-1.17-12.82-11.7-12.82-11.7s4.27-6.3 12.82-6.97z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||geforce.com^\",\n\t\t\"||geforcenow.com^\",\n\t\t\"||nvidia.cn^\",\n\t\t\"||nvidia.com.global.ogslb.com^\",\n\t\t\"||nvidia.com^\",\n\t\t\"||nvidia.eu^\",\n\t\t\"||nvidia.partners^\",\n\t\t\"||nvidiagrid.net^\",\n\t\t\"||nvidianews.com^\",\n\t\t\"||tegrazone.com^\",\n\t},\n\tGroupID: \"software\",\n}, {\n\tID:      \"odysee\",\n\tName:    \"Odysee\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-11 2 178 178\\\"><path d=\\\"M82 57c-14 5-20-2-21-14-1-13 12-17 12-17 14-5 18 2 21 12s1 14-12 19m65 85-9-28a67 67 0 0 0-21-23 5 5 0 0 1 0-8c7-6 18-18 22-25 3-4 7-13 8-20 0-6-1-12-8-15s-11 1-11 1c-5 3-6 12-9 21-4 10-10 11-13 11s-1-3-9-24c-7-21-26-17-40-9-19 11-11 35-6 50-3 2-12 4-21 9l-15 8c-6 6-9 11-7 19a12 12 0 0 0 6 7c5 2 13-1 24-10 9-6 19-9 19-9l13 24c7 13-7 17-8 17-2 0-23-2-18 16s31 12 44 3c14-9 10-38 10-38 13-2 17 12 19 19 1 7-2 19 11 20a21 21 0 0 0 6-1c7-2 11-5 13-9a9 9 0 0 0 0-6M88 33a9 9 0 0 0-2-3h-2v2l1 2 1 1h1l1-3m0 7-1 2a7 7 0 0 1 1 5l1 2h1l1-1c1-3 0-5-1-7l-2-1\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||odycdn.com^\",\n\t\t\"||odysee.com^\",\n\t\t\"||odysee.live^\",\n\t\t\"||odysee.tv^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"ok\",\n\tName:    \"OK.ru\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 96 96\\\"><path d=\\\"M50 28c-3.313 0-6 2.688-6 6 0 3.313 2.688 6 6 6 3.313 0 6-2.688 6-6 0-3.313-2.688-6-6-6zm0 0\\\" /><path d=\\\"M50 4C24.637 4 4 24.637 4 50s20.637 46 46 46 46-20.637 46-46S75.363 4 50 4zm0 16c7.73 0 14 6.27 14 14s-6.27 14-14 14-14-6.27-14-14 6.27-14 14-14zm14.828 49.172A3.999 3.999 0 0 1 62 76a3.987 3.987 0 0 1-2.828-1.172L50 65.656l-9.172 9.172a3.999 3.999 0 0 1-5.656 0 3.999 3.999 0 0 1 0-5.656l6.43-6.43c-1.836-.539-3.618-1.207-5.29-2.066A4.302 4.302 0 0 1 34 56.859c0-2.98 3.172-4.761 5.809-3.375A21.767 21.767 0 0 0 50 56c3.684 0 7.148-.91 10.191-2.516C62.828 52.098 66 53.88 66 56.86c0 1.602-.89 3.078-2.313 3.813-1.671.863-3.453 1.531-5.289 2.07zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||insideok.ru^\",\n\t\t\"||ok.games^\",\n\t\t\"||ok.ru^\",\n\t\t\"||okcdn.ru^\",\n\t\t\"||oktech.ru^\",\n\t\t\"||st.mycdn.me^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"olvid\",\n\tName:    \"Olvid\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 250 250\\\"><path d=\\\"M51 2.5c-18.4 4-35 17.3-43.3 34.6C.3 52.4.5 50.3.5 126v67.5l2.3 8c3.4 11.7 8.3 19.8 17.6 29 5.9 5.9 10.1 9 15.6 11.7 14.2 7 12.9 6.9 91.3 6.6 69.5-.3 70.3-.3 76-2.5 17.3-6.6 30.3-17.7 37.4-32 7.5-14.9 7.4-13.9 7.1-92.3-.3-68.8-.3-69.6-2.5-76-3.3-9.6-9-18.6-16.3-26-7.6-7.5-14.8-12-25.2-15.8l-7.3-2.7-69.5-.2c-56.7-.1-70.7.1-76 1.2zm95 39.9c39.6 9.5 66 42.4 66 82.1 0 32.6-17.1 60.1-46.5 74.6-14.9 7.4-23 9.2-40.5 9.2-17.3 0-25.5-1.8-39.7-8.8A85.66 85.66 0 0 1 40.4 145c-2.5-9.1-2.5-31.9 0-41 9.3-33.7 34.8-56.6 70.4-63 6.9-1.3 27.7-.5 35.2 1.4z\\\"/><path d=\\\"M113.5 78.4a43.05 43.05 0 0 0-29 23.9c-10.1 21.4-4.2 47.7 13.8 61.4 1.7 1.4 2.5 2.7 2.1 3.7-.9 2.3-9.1 8.1-16.2 11.4l-6.2 2.9 3.8.7c10.4 1.7 31.4-.2 44.2-4 17.5-5.2 32.8-17.4 39.5-31.5 5-10.5 6.4-20.9 4.3-31.6-2.9-14.6-10-25.4-20.9-32-9.9-5.9-23.7-7.8-35.4-4.9z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||olvid-attachment-chunks.s3.eu-west-3.amazonaws.com^\",\n\t\t\"||olvid.io^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"onlyfans\",\n\tName:    \"OnlyFans\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 512 512\\\"><path d=\\\"M157 97c-23.9 3.1-43.2 9.7-63.5 21.8-24.7 14.7-47.6 39.5-60.8 65.8-5.3 10.5-12 30-13.7 39.4-.6 3.6-1.4 7.6-1.7 9-1 4.8-1.2 35.6-.2 43 1.2 9.1 5.8 29.1 7 30.4.5.6.9 1.6.9 2.4 0 2.5 8.2 20.3 12.9 28.2 8.8 14.6 12.9 19.8 25.7 32.6 21.9 22 49.5 36.9 79.4 43.1 17.9 3.7 42.5 4.2 60 1.2 30.9-5.2 60.2-19.9 83.1-41.7 14.3-13.6 25.2-28.6 35.4-48.7 1.8-3.6 2.4-4 6.5-4.3 7.4-.5 23.9-4.1 33.2-7.2 20-6.7 39.1-19.1 52.3-34.1 13.5-15.3 21.7-30.3 28.1-50.9 1.8-5.8 3.1-10.6 3-10.8-.2-.1-2.7.3-5.7.8-13.7 2.7-19.5 3.2-38.9 3.5-16.5.3-27.8-.7-39-3.4-4.1-1-3.5-1.2 7-3.6 13.7-3.2 31.3-9.5 44.5-16 16.4-8.1 17.5-8.8 28.2-16.7 19.7-14.7 33.7-31.5 44.4-53.3 4.1-8.5 10.9-27.6 10.9-30.7 0-1-129.5-.6-133.5.4-1.1.3-4.7.9-8 1.2-31 3.6-52.9 13.4-69.6 31.3l-4.6 4.9-3.9-3.3c-16.8-14.3-43.9-27.1-67.4-32-14.2-2.9-38.6-4-52-2.3zm33.6 113.4c6.8 2.1 16 8.2 21.3 14.2 4.7 5.2 9.6 14.7 10.7 20.4 3.3 17.8-1.3 33.1-13.6 45.1-3.8 3.8-7.5 6.9-8.2 6.9-.6 0-1.9.6-2.7 1.3-2.8 2.4-13.1 5-20.8 5.2-13.9.5-25-4-35-14.1-4.1-4-7.4-8.5-9.3-12.6-10.5-22.7-2.6-49 18.6-61.8 11-6.6 26.5-8.4 39-4.6z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||onlyfans.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"origin\",\n\tName:    \"Origin\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M 12 4 C 11.539063 4 11.09375 4.046875 10.65625 4.121094 C 11.082031 3.183594 11.550781 2.445313 12 2 C 12.195313 1.804688 12.011719 1.484375 11.738281 1.539063 C 7.808594 2.359375 4 7.0625 4 12 C 4 16.417969 7.582031 20 12 20 C 12.460938 20 12.90625 19.953125 13.34375 19.878906 C 12.917969 20.816406 12.449219 21.554688 12 22 C 11.804688 22.195313 11.988281 22.515625 12.261719 22.460938 C 16.191406 21.640625 20 16.9375 20 12 C 20 7.582031 16.417969 4 12 4 Z M 12 15 C 10.34375 15 9 13.65625 9 12 C 9 10.34375 10.34375 9 12 9 C 13.65625 9 15 10.34375 15 12 C 15 13.65625 13.65625 15 12 15 Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"|cloudsync-prod.s3.amazonaws.com^\",\n\t\t\"|origin-a.akamaihd.net^\",\n\t\t\"|rtm.tnt-ea.com^\",\n\t\t\"|ssl-lvlt.cdn.ea.com^\",\n\t\t\"||accounts.ea.com^\",\n\t\t\"||dawngate.com^\",\n\t\t\"||eastore.com^\",\n\t\t\"||lordofultima.com^\",\n\t\t\"||origin.com^\",\n\t\t\"||origin.tv^\",\n\t\t\"||signin.ea.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"paramountplus\",\n\tName:    \"Paramount Plus\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"180 60 200 200\\\"><path d=\\\"M260.2 203.3c-.9-.4-2.4-2 .1-6.6l5.6-11.8c.2-.4-.2-.8-.5-.5l-4.9 5c-2.3 2.4-6.3 9.3-7.1 10.6l-6 9.9c.6 0 1 .7.7 1.2l-5.5 9.2c-1.3 2.3 1.1 3.9 1.4 3.4 8.6-13.9 13.6-12.8 13.6-12.8l2.9-6.7c.3-.4.1-.8-.3-.9\\\"/><path d=\\\"M279.8 87.2c-49.3 0-89.3 40-89.3 89.3 0 19.9 6.5 38.2 17.4 53 3.7-1.6 5.8-4 7.3-5.9l16.6-21.3c.4-.4.8-.8 1.3-1l2.5-1.1 27.3-34.7 4-3.1 8.1-11.3c.2-.3.5-.6.8-.8l3.6-2.6c.9-.6 2.1-.7 3 0l4.3 3a17 17 0 0 1 5.4 6.2l17.3 30.3c.4.7.7 1 1.4 1.3 3.4 1.7 5.4 2 9.9 6.8 2.1 2.3 11.1 12.4 23.8 28.1 1.8 2.4 4 4.4 7.2 5.8a88.95 88.95 0 0 0 17.4-52.9c0-49.1-40-89.1-89.3-89.1m-65.2 94.1-5.8-1.9-3.6 4.9v-6.1l-5.8-1.9 5.8-1.9v-6.1l3.6 4.9 5.8-1.9-3.6 4.9 3.6 5.1zm-1.3 20-1.9 5.8-1.9-5.8h-6.1l4.9-3.6-1.9-5.8 4.9 3.6 4.9-3.6-1.9 5.8 4.9 3.6h-5.9zm1.1-46.1 1.9 5.8-4.9-3.6-4.9 3.6 1.9-5.8-4.9-3.6h6.1l1.9-5.8 1.9 5.8h6.1l-5.1 3.6zm9.8-13.1-3.6-4.9-5.8 1.9 3.6-4.9-3.6-4.9 5.8 1.9 3.6-4.9v6.1l5.8 1.9-5.8 1.9v5.9zm15.2-21.3-1.9 5.8-1.9-5.8h-6.1l4.9-3.6-1.9-5.8 4.9 3.6 4.9-3.6-1.9 5.8 4.9 3.6h-5.9zm19.2-9.7L255 116v-6.1l-5.8-1.9 5.8-1.9v-6l3.6 4.9 5.8-1.9-3.6 4.9 3.6 4.9-5.8-1.8zm24.3-5.7 1.9 5.8-4.9-3.6-4.9 3.6 1.9-5.8-4.9-3.6h6.1L280 96l1.9 5.8h6.1l-5.1 3.6zm21.8 4.5v6.1l-3.6-4.9-5.8 1.9 3.6-4.9-3.6-4.9 5.8 1.9 3.6-4.9v6.1l5.8 1.9-5.8 1.7zm40.3 61.5 5.8 1.9 3.6-4.9v6.1l5.8 1.9-5.8 1.9v6.1l-3.6-4.9-5.8 1.9 3.6-4.9-3.6-5.1zm-22.9-44.8-1.9-5.8h-6.1l4.9-3.6-1.9-5.8 4.9 3.6 4.9-3.6-1.9 5.8 4.9 3.6H324l-1.9 5.8zm13.4 15.5v-6l-5.8-1.9 5.8-1.9v-6.1l3.6 4.9 5.8-1.9-3.6 4.9 3.6 4.9-5.8-1.9-3.6 5zm10.9 9.5 1.9-5.8 1.9 5.8h6.1l-4.9 3.6 1.9 5.8-4.9-3.6-4.9 3.6 1.9-5.8-4.9-3.6h5.9zm3.8 49.7-1.9 5.8-1.9-5.8h-6.1l4.9-3.6-1.9-5.8 4.9 3.6 4.9-3.6-1.9 5.8 4.9 3.6h-5.9z\\\"/><path d=\\\"M312.2 228.1c.4-.6 1.1-2.3-.2-5.5l-3.9-10.6c-.5-1.4.6-2.2 1.4-1.3 0 0 7.5 8.6 9.4 12.2l3.7 6.1c3.2.2 11.9.5 20.3.5-.8-.8-1.6-1.7-2.4-2.7C326.1 209 317 199.1 317 199c-2.9-3.2-4.3-3.8-6.5-4.8-.3-.1-.7-.3-1-.5v2.7c0 .4-.4.5-.6.2l-21.1-37.1-.1-.1c-.9-1.7-2.2-3.2-3.8-4.4l-2.1-1.4-10.1 23.2c1.6 0 2.6 1.6 2 3l-9.3 21.5h8.5c3.3 0 6.6.6 9.6 1.9l2.2.9s-6.8 14-6.8 21.3c0 1.3.2 2.7.6 4h15.6l-.7-4.4c.2-.1 9.6 2.2 18.8 3.1\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||paramountplus.com^\",\n\t\t\"||pplusstatic.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"peacock_tv\",\n\tName:    \"Peacock TV\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-233 0 1964 1964\\\"><path d=\\\"M519.7 101.8h6.2c20.7 0 41.1.6 61.7 3.3l3.7.5a322.8 322.8 0 0 1 43.2 8c47.3 10.3 105.3 25.3 144 55.5v2l2.5.7a62 62 0 0 1 12.4 6.7l2.5 1.6c12.8 8.3 25 17.1 36.8 26.8l11.4 9.2c12.3 9.6 12.3 9.6 15.4 13.5a37 37 0 0 0 5.6 5.6c5.3 4.5 10.2 9.5 15 14.5a611.3 611.3 0 0 0 4 4c9 9 17.3 18.7 25.5 28.4l2.3 2.8c50.7 61 50.7 61 50.7 76.2l2 1c5.2 7.3 9 15.7 13 23.7a1540.5 1540.5 0 0 0 3 6.2c11.7 23.3 11.7 23.3 13 31.1h2c28 71.8 46.2 144.9 46.2 222.3v3.6c0 45.4-5 91.7-19.2 135.1l-1.1 3.3c-5 15.4-10.5 30.6-16.7 45.5l-.8 1.9a346.3 346.3 0 0 1-11.4 24.3h-2l-.5 1.6a84 84 0 0 1-8 17.5l-1.7 2.8a339.7 339.7 0 0 1-14.8 23.1l-1.5 2-9 13a600 600 0 0 0-4.2 5.8 161 161 0 0 1-15 18.6 176 176 0 0 0-8.5 10.3 217 217 0 0 1-8.8 10.3l-1.4 1.5c-10.4 11.5-10.4 11.5-16 16.1a47.3 47.3 0 0 0-5 5.2 96.5 96.5 0 0 1-13.6 13.2h-2v2c-1.6 1.4-1.6 1.4-3.8 3-3.8 2.9-7.3 6-10.9 9.3-6 5.6-12.5 10.5-19.3 15.2l-8.8 6.4-7 5a398 398 0 0 0-7.5 5.6c-11 8.1-22.8 14.8-34.6 21.7a673.5 673.5 0 0 0-8.4 5 330.7 330.7 0 0 1-62.2 28.5l-15 5.6a850 850 0 0 1-27.5 10.1 2134 2134 0 0 0-48 17.6 1789 1789 0 0 1-61 22.2c-5.8 2-11.4 4.1-17 6.4-8 3.2-16 6.2-24.2 9.2l-2.4.9a1593.7 1593.7 0 0 1-37 13 353 353 0 0 0-20.6 7.8 849 849 0 0 1-37.6 14l-2.1.8c-2.9 1-5 1.7-8.1 1.7l-1 2c-1.7.7-1.7.7-4 1.3-5.9 1.6-11.5 3.6-17.2 5.7a24 24 0 0 1-8.8 2l-1 2-5 2-2.3.9-2.4.8-2.7 1a4858.8 4858.8 0 0 1-12 4.4 1727.8 1727.8 0 0 0-6.4 2.3 1774.4 1774.4 0 0 1-9.2 3.3l-2.8 1-2.7 1-2.4.8c-3.2.7-5.2 0-8.1-1.5v-2h-2v-2h-2c-1.7-1.7-1.7-1.7-3.5-4-2.4-3-2.4-3-5.5-5v-2l-3-1v-2h-2v-2h-2v-2l-1.8-.8a21.7 21.7 0 0 1-5.7-4.5l-1.8-1.9-1.7-1.8a129.3 129.3 0 0 0-8.9-8.3 63 63 0 0 1-7.3-7.4 176.4 176.4 0 0 0-20.8-19.7 88.8 88.8 0 0 1-8-7.6v-2h-2l-5-6-1 664c-5.7 2.3-5.7 2.3-9.3 2.3h-62a32105.1 32105.1 0 0 1-69.7-.1H10c-6.2 0-6.2 0-7.3-1.2a58 58 0 0 1-.2-4v-27.6a20952.2 20952.2 0 0 1-.2-43 87531.5 87531.5 0 0 1 0-47.4l-.2-42.6a422579 422579 0 0 1-.5-200.8L1 1136.5v-2.1a812326.8 812326.8 0 0 1-.2-129.5v-6L.4 879.6a91455.8 91455.8 0 0 1-.1-97.9v-16a23348.6 23348.6 0 0 0-.2-41v-40a4694 4694 0 0 0 0-20.9 430.2 430.2 0 0 1 8.8-101l1-4.5c9.6-48.4 9.6-48.4 17.1-68 1.5-4 2.8-7.9 4-11.9a678.2 678.2 0 0 1 40.2-99.8 536.4 536.4 0 0 1 53-86l7.8-10.5c9.9-13.7 9.9-13.7 14-16.8 2.8-2.4 4.7-5 7-8 2.7-3.7 5.9-7 9-10.3l2-2a905.3 905.3 0 0 1 5.6-5.9l1.7-1.8 4.3-4.2h2v-2c1.4-1.6 1.4-1.6 3-3h2v-2c1.3-1.4 1.3-1.4 3.2-3l2.2-1.9 2.6-2 2.8-2.5 9.2-7.6 2.9-2.4 2.7-2.2 2.4-2c2-1.4 2-1.4 4-1.4l.7-1.8c1.5-2.6 2.8-3.6 5.3-5.2l2.7-1.8 3.3-2.2 4.1-2.8 13.9-9.2 1.9-1.3a379.8 379.8 0 0 1 44-24.7l2.5-1.2a1139.5 1139.5 0 0 1 18.6-8.8l2.4-1.1c32.4-15.1 69-24.4 104.2-30.1a81.6 81.6 0 0 0 8-1.8c5.6-1.4 11.4-2 17.1-2.7l5.8-.8c23.5-3 47-3.9 70.6-3.8zm-35.1 245.3-6.4.3a198 198 0 0 0-20.5 2.6l-3 .5c-5 .9-10 2-14.8 3.8-2.5.9-5 1.6-7.6 2.4a216 216 0 0 0-71.7 34.4 157.7 157.7 0 0 0-24.5 20.6l-9 8c-21.2 20.2-36.4 46.7-49.5 72.4l-1.5 3a312.7 312.7 0 0 0-27.5 84l-.5 2.7c-.6 3.7-.7 7.3-.6 11v15.6a11121.7 11121.7 0 0 0 0 60.3 94668.8 94668.8 0 0 1 0 173.3v191.2a8930 8930 0 0 0 9.6-3.4l2.7-1c7-2.4 13.8-5 20.6-7.8 22-9 44-17.6 66.2-26.2l3-1c4.8-2 9.7-3.8 14.5-5.7 11.2-4.3 11.2-4.3 22.3-9 9-3.9 18.4-7.3 27.7-10.8 16-6 32-12 47.8-18.7 12.4-5.2 25-10 37.6-14.8a876.7 876.7 0 0 0 42.5-16.9 411 411 0 0 1 20.9-8.2 5160 5160 0 0 0 24-9.2 4214 4214 0 0 1 5.2-2 652 652 0 0 0 27.4-11 422 422 0 0 1 8-3.4 200.7 200.7 0 0 0 52-29l8.6-6.3a229 229 0 0 0 24.6-21.6 779.4 779.4 0 0 1 4.4-4.4 217 217 0 0 0 17.5-19.7l1.6-2a250.7 250.7 0 0 0 39.6-73.8c.8-2.4 1.8-4.7 2.8-7a106 106 0 0 0 5-17.5l.8-3.5c12.3-56 9.8-117.8-11.8-171.2l-1.6-3.9a782.4 782.4 0 0 0-3-7.6 381.4 381.4 0 0 1-2.5-6.4 272.8 272.8 0 0 0-39-65l-1.8-2.6a219.7 219.7 0 0 0-16.6-18.2l-2.4-2.4-2.3-2.3-2-2.1c-1.7-1.7-1.7-1.7-3.8-1.5l-.7-1.7c-1.8-3.1-4.4-5.1-7.1-7.4l-1.6-1.4c-3-2.5-5.9-4.2-9.6-5.5v-2h-3v-2c-3.8-3-8-5.4-12-8l-2.1-1.3c-10-6.4-20-12-31-16.7l-2.1-1a404 404 0 0 0-43.8-15l-2.8-.8c-7.1-2-14.3-3.3-21.7-4.3l-3.4-.5c-14-2-27.8-2.5-42-2.5h-18c-4.2 0-8 .1-12.1 1.1zm934 3 1.9.9c9.2 4.3 17.8 9.1 26 15.1l2.6 1.8a131 131 0 0 1 48.8 86.5c3.9 32-2.7 67.8-22.8 93.6a583.5 583.5 0 0 1-10 12.3 168.2 168.2 0 0 0-2.8 3.4c-3 3.9-6.5 6.9-10.3 9.9l-2 1.4c-7.9 6-16.3 11.1-25 15.9l-2.1 1.1a92.3 92.3 0 0 1-24.4 8.2l-2.3.6a124 124 0 0 1-83.6-11.7l-2-1c-9-4.4-16.5-9.3-24-16l-2.4-2a94.3 94.3 0 0 1-25.6-32v-3h-2c-4.5-8-7.8-15.8-10.3-24.5l-.8-2.6a136 136 0 0 1-4.3-36v-2a123.5 123.5 0 0 1 8.2-45.5 123.3 123.3 0 0 1 66.4-71.6 248.6 248.6 0 0 0 4.8-2.2 118.6 118.6 0 0 1 98-.6zm27-325 2.6 2a515.6 515.6 0 0 1 7.4 6l1.9 1.5a68 68 0 0 1 13 14.5v2h2l2 5h2a142.2 142.2 0 0 1 19 106c-1.3 5.5-3 10.7-5 16l-.6 2a128 128 0 0 1-163.1 75.3c-8-3.2-15.4-6.8-22.8-11a145.2 145.2 0 0 0-6.4-3.3v-2l-5-2v-2l-1.8-.9c-2.2-1-2.2-1-5.2-3v-2l-1.8-.3c-2.8-1-4-2.3-5.8-4.6l-1.8-2a31 31 0 0 1-3.6-6.2h-2l-2.6-3.6-1.4-2.2-1.5-2.4-1.5-2.4c-3.7-5.8-3.7-5.8-5-8.5-1-2-1-2-3-3.9-5-9.8-8.7-20-11.2-30.7l-.5-2.1c-4.5-19.5-4.4-42.7.7-62.2l.5-2.2c5-18.8 15.2-35.2 27.3-50.3l2.9-4 1.3-1.5h2v-2c3.4-3.2 7.2-6 11-9l3-2.4c43.5-33.3 109.2-37.3 153-1.6zM1370.3 678l3.1-.1c6.4 0 12 .6 18.2 2.2l3.9.8a129.8 129.8 0 0 1 83 57.2c19.8 30.1 24 67.2 17 102a124.2 124.2 0 0 1-19 44h-2l-.6 2.1c-1.6 3.6-4 6.3-6.5 9.3l-1.4 1.6a100 100 0 0 1-19.7 18l-2.3 1.6a1114.3 1114.3 0 0 1-4.4 3 211.7 211.7 0 0 0-4.8 3.5c-24.9 17.3-59.8 20.7-89 15.8-25.3-5.9-46-17.7-65.2-34.9l-2-1.7a119.7 119.7 0 0 1-21-28.3v-2l-2-1a119.2 119.2 0 0 1-3.4-7.2l-1-2.2c-2.8-6.7-5-13.5-6.6-20.6l-1-3.6c-4.3-19.2-4-41.3 1-60.4l.6-2.4a151.3 151.3 0 0 1 21.4-44.6h2l.6-2.1a34 34 0 0 1 6.8-9.1l1.6-1.8a128.2 128.2 0 0 1 74.6-37.6c2.3-.3 2.3-.3 4.2-.9 4.5-1 9.2-.7 13.9-.7zm80.3 715.1a319.8 319.8 0 0 1 5 5l2.3 1.7 1.7 1.3v2h2c2.8 3 5.2 6.2 7.7 9.4a345.5 345.5 0 0 0 3.6 4.5c14 17.8 21 39 24.7 61.1l.6 3.5c3.2 27.1-3.3 55.3-15.6 79.5h-2l-1 5h-2v3h-2v3h-2l-.8 1.7c-8.3 15.6-23.4 26.7-38.2 35.3l-2.2 1.3a129.4 129.4 0 0 1-98.8 11.7 131.6 131.6 0 0 1-54-33l-1.8-1.6a123 123 0 0 1-36.3-82c-.8-28.2 3.2-57 20-80.4l2-3c19.6-29.5 48-47.1 82.2-55.8a96 96 0 0 1 7.9-1.2l3-.4c33.9-3.1 67.5 7.2 94 28.4zm4.6-333.9c4 3.3 4 3.3 5.4 5v2h2a107 107 0 0 1 4.6 5.7l1.4 1.8 2.7 3.7a254.2 254.2 0 0 0 3.4 4.5c19.4 25.2 27 59.8 23.4 91-3.9 29-14.8 53.7-34.5 75.2l-1.5 1.6a131 131 0 0 1-55.5 34.4l-3 1c-21.8 7-51.5 7.7-72.6-1.9-2.8-1.3-5.7-2.3-8.6-3.3-13-5-24-12-34.8-20.8l-2.9-2.2-2.1-1.8v-2h-2v-2l-3-1a123.1 123.1 0 0 1-33-61l-.6-2.3c-4.2-19.5-4-40.3.6-59.7l.5-2.1c5.9-24.4 20.1-49 39.5-64.9l1.8-1.6c48.3-45 120-38.4 168.8.7zm-4.6 671.9 2.5 2.1a130.3 130.3 0 0 1 43.5 123l-.4 2a147.8 147.8 0 0 1-14.6 42h-2l-1 5h-2v3h-2v3a98.9 98.9 0 0 1-3.5 3.4 107.5 107.5 0 0 1-33.8 30.5 246.8 246.8 0 0 0-4.4 2.8c-24.6 15.7-59 20.3-87.4 14.7-15-3.4-31.2-8.8-44-17.5v-2l-2-.7a34.4 34.4 0 0 1-9-6.2l-1.6-1.4a677 677 0 0 1-5.3-4.7l-1.9-1.6a736.5 736.5 0 0 1-5.3-4.8l-1.7-1.5a23 23 0 0 1-6.1-8 587.9 587.9 0 0 0-6-9l-4-6-1.5-2.4c-3.9-6-6.5-12.2-8.7-19l-1-3.2a142 142 0 0 1-2.8-72.5l.4-2c3.4-15.5 11-31.4 20.6-44h2l.9-2.3c1-2.7 1-2.7 3-5.7h2l1-1.8c1-2.2 1-2.2 3-5.2h2v-2h2v-2l4.3-3.4 7.7-6c2-1.6 2-1.6 4-1.6v-2h3v-2a160.3 160.3 0 0 1 56-19l3.2-.4c32-3.1 66.5 7.2 90.9 28.4z\\\"/><path d=\\\"M246.6 586.1h1l1 446 5-1c-1 2-1 2-4 3.1l-3 1c-1.7-1.8-1.1-3.8-1.2-6v-17.6a11055.4 11055.4 0 0 1 0-63.8V841.6a146142.6 146142.6 0 0 0 0-133.3v-37.4a42143 42143 0 0 1 0-75.6v-2.1c0-4.9 0-4.9 1.2-7zm-54-369 2 1-37 37-2-1 6.6-7.1 1.9-2a994.8 994.8 0 0 1 5.5-5.9l1.7-1.8 4.3-4.2h2v-2c1.4-1.6 1.4-1.6 3-3h2v-2l3.9-3.5a98 98 0 0 0 6-5.5z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||peacock.com^\",\n\t\t\"||peacocktv.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"perplexity\",\n\tName:    \"Perplexity\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 400 400\\\"><path fill-rule=\\\"evenodd\\\" d=\\\"m101 42 90 83V42.1h17.5v83L299 42v94.5h37V273h-37v84l-90.5-79.5V358H191v-79l-90 79v-85.1H64V136.5h37zm76.8 111.8H81.5v101.8h19.6v-32.1zm-59.2 77.3v88.3l72.4-63.7v-90.3zm90.4 23.7v-89.5l72.4 65.8v87.4zm90 .8h19.5V153.8h-95.6l76.1 69zm-17.6-119V81.7L222 136.5zm-103.5 0h-59.4V81.7z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||perplexity.ai^\",\n\t},\n\tGroupID: \"ai\",\n}, {\n\tID:      \"pinterest\",\n\tName:    \"Pinterest\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M25,2C12.318,2,2,12.317,2,25s10.318,23,23,23s23-10.317,23-23S37.682,2,25,2z M27.542,32.719 c-3.297,0-4.516-2.138-4.516-2.138s-0.588,2.309-1.021,3.95s-0.507,1.665-0.927,2.591c-0.471,1.039-1.626,2.674-1.966,3.177 c-0.271,0.401-0.607,0.735-0.804,0.696c-0.197-0.038-0.197-0.245-0.245-0.678c-0.066-0.595-0.258-2.594-0.166-3.946 c0.06-0.88,0.367-2.371,0.367-2.371l2.225-9.108c-1.368-2.807-0.246-7.192,2.871-7.192c2.211,0,2.79,2.001,2.113,4.406 c-0.301,1.073-1.246,4.082-1.275,4.224c-0.029,0.142-0.099,0.442-0.083,0.738c0,0.878,0.671,2.672,2.995,2.672 c3.744,0,5.517-5.535,5.517-9.237c0-2.977-1.892-6.573-7.416-6.573c-5.628,0-8.732,4.283-8.732,8.214 c0,2.205,0.87,3.091,1.273,3.577c0.328,0.395,0.162,0.774,0.162,0.774l-0.355,1.425c-0.131,0.471-0.552,0.713-1.143,0.368 C15.824,27.948,13,26.752,13,21.649C13,16.42,17.926,11,25.571,11C31.64,11,37,14.817,37,21.001 C37,28.635,32.232,32.719,27.542,32.719z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||pin.it^\",\n\t\t\"||pinimg.com^\",\n\t\t\"||pinterest.at^\",\n\t\t\"||pinterest.be^\",\n\t\t\"||pinterest.ca^\",\n\t\t\"||pinterest.ch^\",\n\t\t\"||pinterest.cl^\",\n\t\t\"||pinterest.co.at^\",\n\t\t\"||pinterest.co.in^\",\n\t\t\"||pinterest.co.kr^\",\n\t\t\"||pinterest.co.nz^\",\n\t\t\"||pinterest.co.uk^\",\n\t\t\"||pinterest.co^\",\n\t\t\"||pinterest.com.au^\",\n\t\t\"||pinterest.com.bo^\",\n\t\t\"||pinterest.com.ec^\",\n\t\t\"||pinterest.com.mx^\",\n\t\t\"||pinterest.com.pe^\",\n\t\t\"||pinterest.com.py^\",\n\t\t\"||pinterest.com.uy^\",\n\t\t\"||pinterest.com.vn^\",\n\t\t\"||pinterest.com^\",\n\t\t\"||pinterest.de^\",\n\t\t\"||pinterest.dk^\",\n\t\t\"||pinterest.ec^\",\n\t\t\"||pinterest.engineering^\",\n\t\t\"||pinterest.es^\",\n\t\t\"||pinterest.fr^\",\n\t\t\"||pinterest.hu^\",\n\t\t\"||pinterest.id^\",\n\t\t\"||pinterest.ie^\",\n\t\t\"||pinterest.in^\",\n\t\t\"||pinterest.info^\",\n\t\t\"||pinterest.it^\",\n\t\t\"||pinterest.jp^\",\n\t\t\"||pinterest.kr^\",\n\t\t\"||pinterest.mx^\",\n\t\t\"||pinterest.nl^\",\n\t\t\"||pinterest.nz^\",\n\t\t\"||pinterest.pe^\",\n\t\t\"||pinterest.ph^\",\n\t\t\"||pinterest.pt^\",\n\t\t\"||pinterest.ru^\",\n\t\t\"||pinterest.se^\",\n\t\t\"||pinterest.th^\",\n\t\t\"||pinterest.tw^\",\n\t\t\"||pinterest.uk^\",\n\t\t\"||pinterest.vn^\",\n\t\t\"||pinterestmail.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"playstation\",\n\tName:    \"PlayStation\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 30 30\\\"><path d=\\\"M11.18 3.74v21.12l4.58 1.4V8.58c0-.51 0-.77.26-1.02.12-.26.38-.26.63-.13.64.26 1.02.76 1.02 1.78v7c1.53.76 2.8.76 3.81 0 1.02-.77 1.53-1.9 1.53-3.82 0-2.03-.38-3.3-1.27-4.32-.76-1.02-2.16-1.91-4.2-2.55-2.54-.76-4.7-1.4-6.36-1.78zM9.91 16.97l-5.85 2.04-.89.38c-1.4.63-2.16 1.27-2.16 1.9.12.77.38 1.79 2.29 2.42 1.78.64 3.18.9 6.74-.12v-2.3c-3.44 1.15-3.95 1.02-4.45.77-.51-.25-.51-.5-.39-.64.39-.25 1.78-.76 1.78-.76l2.93-1.02v-2.67zm12.94 1c-.41-.02-.82-.01-1.24.02-1.4 0-2.67.25-4.2.64v2.67l2.8-1.02 1.53-.51s.64-.13 1.02-.25c.63-.13 1.4.12 1.4.12.38 0 .63.13.63.38.13.26-.12.39-.76.64l-1.4.51-5.09 1.9v2.68l2.3-.77 6.35-2.28.77-.39c1.52-.5 2.16-1.14 2.03-1.9 0-.77-.89-1.28-2.42-1.79a14.28 14.28 0 0 0-3.72-.66z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||gaikai.com\",\n\t\t\"||playstation-cloud.com\",\n\t\t\"||playstation-cloud.net\",\n\t\t\"||playstation.com\",\n\t\t\"||playstation.net\",\n\t\t\"||scea.com\",\n\t\t\"||sonyentertainmentnetwork.com\",\n\t\t\"||station.sony.com\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"playstore\",\n\tName:    \"Google Play Store\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-3 -2 24 24\\\"><path d=\\\"M17.6 9.3 14 7.2 11 10l2.7 2.6 4-2.2.3-.6zm-4.2-2.5-4-2.3L.6 0l10 9.6zM.8 20l8.6-4.8 3.7-2.1-2.6-2.6zM0 .4v19.3l10-9.6z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||play-fe.googleapis.com^\",\n\t\t\"||play-lh.googleusercontent.com^\",\n\t\t\"||prod-lt-playstoregatewayadapter-pa.googleapis.com^\",\n\t},\n\tGroupID: \"software\",\n}, {\n\tID:      \"plenty_of_fish\",\n\tName:    \"Plenty of Fish\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-8 -3 30 30\\\"><path d=\\\"M13.96 4.88C11.3.68 7.04 1.25.22 1.25v.09c.52.2.95.59 1.21 1.08.26.47.38 1.04.38 1.72v16.44c.01.61-.11 1.21-.38 1.76-.25.5-.68.9-1.2 1.1v.09h7.13v-.09c-.5-.22-.92-.6-1.17-1.1a3.78 3.78 0 0 1-.4-1.76V16c.54.13 1.08.2 1.62.19a7.66 7.66 0 0 0 6.84-4.3c.47-.97.7-2.1.7-3.41a6.6 6.6 0 0 0-1-3.6ZM9.45 13.6a3.33 3.33 0 0 1-2.96 1.65c-.24 0-.49-.02-.73-.05V7.56l.01-4.63a3 3 0 0 0-.1-.88c1.62 0 2.43.5 3.16 1.19.74.68 1.86 2.68 1.86 5.52a9.4 9.4 0 0 1-1.24 4.85Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||pof.com^\",\n\t},\n\tGroupID: \"dating\",\n}, {\n\tID:      \"plex\",\n\tName:    \"Plex\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 48 48\\\"><path d=\\\"M11.5 6A5.5 5.5 0 0 0 6 11.5v25a5.5 5.5 0 0 0 5.5 5.5h25a5.5 5.5 0 0 0 5.5-5.5v-25A5.5 5.5 0 0 0 36.5 6h-25zm6.67 7.08h6.47L31.75 24l-7.1 10.92h-6.48L25.2 24l-7.03-10.92z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||plex.bz^\",\n\t\t\"||plex.direct^\",\n\t\t\"||plex.tv^\",\n\t\t\"||plexapp.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"pluto_tv\",\n\tName:    \"Pluto TV\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -80 560 560\\\"><path d=\\\"M357.3 164.3c-19.7 0-35.7 16-35.7 35.7s16 35.7 35.7 35.7 35.7-16 35.7-35.7-16-35.7-35.7-35.7m0 52.1a16.41 16.41 0 0 1 0-32.8c9 0 16.4 7.4 16.4 16.4 0 9.1-7.4 16.4-16.4 16.4m-40.6-32.3v-18.4h-16.6v-18l-20 8.1v9.9h-12.3v18.4H280v23.7c0 8.2 2.4 15 7 19.6 4.6 4.6 11.4 7 19.7 7h9.9V216h-6.8c-6.8 0-9.8-3-9.8-9.8v-22.1h16.7zM240 173.8v30.1c0 6.9-5.6 12.6-12.6 12.6-6.9 0-12.6-5.7-12.6-12.6v-37.4l-19.8 8v28.8c0 17.9 14.5 32.5 32.4 32.5s32.5-14.5 32.5-32.5v-37.6l-19.9 8.1zm-73.6-18v78.5h20v-86.5zm-41.1 8.5c-8.3 0-15.4 2.8-20.7 8.2l-2.8-6.7-16.2 6.5v80h20v-23.6c5.2 4.7 12 7 19.7 7 9.2 0 17.6-3.7 23.8-10.5 6.1-6.6 9.4-15.6 9.4-25.2 0-19.9-14.6-35.7-33.2-35.7m-3.1 52.1c-9.2 0-16.7-7.4-16.7-16.4 0-9.1 7.5-16.4 16.7-16.4s16.7 7.4 16.7 16.4-7.5 16.4-16.7 16.4m316.5-52.1c-19.7 0-35.7 16-35.7 35.7s16 35.7 35.7 35.7 35.7-16 35.7-35.7c0-19.7-16-35.7-35.7-35.7m14.1 53.1h-6.7l-9.5-23H427v11.9c0 2.6 1.2 4.1 4.1 4.1h4.2v7h-5.2c-7.1 0-10.8-4-10.8-10.6v-12.4H414v-6.7h5.3V183l7.6-3.1v7.8h15l7.4 19.3 7.5-19.4h8.2l-12.2 29.8z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||pluto.tv^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"privacy\",\n\tName:    \"Privacy\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-2 0 42 42\\\"><path fill-rule=\\\"evenodd\\\" d=\\\"m28.516 30.648-.857-1.386-1.935-3.136a9.853 9.853 0 0 0 2.523-3.66 9.76 9.76 0 0 0 .31-6.26 9.853 9.853 0 0 0-3.562-5.185 9.955 9.955 0 0 0-5.985-2 9.94 9.94 0 0 0-5.986 2 9.848 9.848 0 0 0-3.564 5.185 9.76 9.76 0 0 0 .31 6.26 9.853 9.853 0 0 0 2.523 3.66L5.031 37.892a.654.654 0 0 1-.312.267.665.665 0 0 1-.42.013.65.65 0 0 1-.343-.225.65.65 0 0 1-.123-.397V18.875h-.007c0-3.331 1.107-6.468 3.022-9.016a15.152 15.152 0 0 1 7.842-5.436 15.292 15.292 0 0 1 9.572.306c3 1.096 5.65 3.126 7.481 5.92a14.998 14.998 0 0 1 2.427 9.197 15.002 15.002 0 0 1-3.587 8.808 15.267 15.267 0 0 1-2.065 1.992m-9.505 3.26c1.129 0 2.284-.079 3.388-.328.979-.222 1.936-.54 2.856-.951l-.854-1.383-2.838-4.6a1.888 1.888 0 0 1 .636-2.608 6.058 6.058 0 0 0 2.487-2.953 6.02 6.02 0 0 0-1.995-7.03 6.115 6.115 0 0 0-3.68-1.225 6.12 6.12 0 0 0-3.682 1.225 6.03 6.03 0 0 0-2.185 3.178 6.008 6.008 0 0 0 .19 3.852 6.056 6.056 0 0 0 2.487 2.953 1.889 1.889 0 0 1 .637 2.607l-1.845 2.99-1.562 2.532-.278.45a14.152 14.152 0 0 0 6.24 1.292h-.002ZM8.772 39.098l-.476.772a4.468 4.468 0 0 1-2.184 1.829 4.48 4.48 0 0 1-2.846.13 4.468 4.468 0 0 1-2.364-1.592A4.404 4.404 0 0 1 0 37.552V18.875h.007c0-4.183 1.382-8.113 3.771-11.29A18.992 18.992 0 0 1 13.611.78a19.102 19.102 0 0 1 11.967.38 18.97 18.97 0 0 1 9.368 7.422 18.758 18.758 0 0 1 3.04 11.501 18.767 18.767 0 0 1-4.5 11.024 19.006 19.006 0 0 1-10.247 6.17 19.07 19.07 0 0 1-3.613.463c-.089-.003-.534.007-.613.007a19.111 19.111 0 0 1-8.257-1.867l-1.984 3.215v.001Z\\\" clip-rule=\\\"evenodd\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||privacy.com.br^\",\n\t},\n\tGroupID: \"privacy\",\n}, {\n\tID:      \"proton\",\n\tName:    \"Proton\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 30 30\\\"><path d=\\\"M4.7 21.2V28h4.8v-6.5a2.4 2.4 0 0 1 2.3-2.4h5a8.5 8.5 0 0 0 8.5-8.5A8.6 8.6 0 0 0 16.7 2h-12v8.5h4.8v-4h6.9a4 4 0 0 1 2.9 6.9 4 4 0 0 1-3 1.2h-5a6.6 6.6 0 0 0-6.6 6.6\\\"/><path d=\\\"M11.8 19.1a7 7 0 0 0-7 7.2V28h4.7v-6.5a2.4 2.4 0 0 1 2.3-2.4\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||pm.me^\",\n\t\t\"||proton.me^\",\n\t\t\"||protonmail.ch^\",\n\t\t\"||protonmail.com^\",\n\t\t\"||protonvpn.com^\",\n\t},\n\tGroupID: \"privacy\",\n}, {\n\tID:      \"qq\",\n\tName:    \"QQ\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 32 32\\\" fill=\\\"currentColor\\\"><path d=\\\"M11.25 32C8.35 32 6 30.74 6 29.24c0-1.5 2.34-2.75 5.25-2.75s5.25 1.26 5.25 2.75S14.16 32 11.25 32ZM27 29.24c0-1.5-2.34-2.75-5.25-2.75s-5.25 1.26-5.25 2.75S18.84 32 21.75 32 27 30.74 27 29.24ZM14.88 7.18c0 .63-.32 1.18-.8 1.18-.49 0-.81-.55-.81-1.18 0-.63.32-1.18.8-1.18.5 0 .81.55.81 1.18ZM18.93 6c-.48 0-.8.55-.8 1.18 0 .63.32-.4.8-.4.49 0 .81 1.03.81.4S19.41 6 18.93 6Z\\\"/><path d=\\\"M6.65 12.64s4.69 2.46 9.93 2.46c5.24 0 9.93-2.46 9.93-2.46.1-.1.21-.16.31-.21 0-1.1-.08-2.03-.08-2.81C26.74 4.29 22.14 0 16.5 0S6.18 4.3 6.18 9.62v2.78c.14.04.3.11.47.24Zm12.63-8.67c1.11 0 1.98 1.28 1.98 2.79s-.87 2.78-1.98 2.78c-1.11 0-1.99-1.27-1.99-2.78 0-1.51.88-2.79 1.99-2.79Zm-5.56 0c1.11 0 1.99 1.28 1.99 2.79s-.88 2.78-1.99 2.78c-1.11 0-1.99-1.27-1.99-2.78 0-1.51.88-2.79 2-2.79Zm2.78 6.63c2.91 0 5.3.46 5.3 1s-2.39 1.65-5.3 1.65c-2.91 0-5.3-1.13-5.3-1.66s2.39-1 5.3-1Zm11.37 5.18-.17.12c-.16.08-5.24 3.18-11.04 3.18-1.43 0-2.7-.24-3.97-.48-.24 1.67-.24 3.26-.24 3.97 0 1.28-1.03 1.2-2.3 1.28-1.27 0-2.23.16-2.3-1.04 0-.16-.09-2.78.4-5.56a23.87 23.87 0 0 1-2.7-1.35 3.3 3.3 0 0 1-.34-.22C4 17.55 3 19.6 3 21.22c0 3.82 1.11 3.42 1.11 3.42.48 0 1.27-.8 1.99-1.67C7.77 27.67 11.73 31 16.5 31c4.76 0 8.73-3.34 10.4-8.03.72.88 1.51 1.67 1.99 1.67 0 0 1.11.4 1.11-3.42 0-1.58-.97-3.63-2.13-5.44Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||qq-video.cdn-go.cn^\",\n\t\t\"||qq.com^$denyallow=wx.qq.com|weixin.qq.com\",\n\t\t\"||url.cn^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"rakuten_viki\",\n\tName:    \"Rakuten Viki\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 5 5 L 5 45 L 45 45 L 45 5 L 5 5 z M 19.435547 14 L 19.4375 14 L 20.587891 14 L 20.587891 17.21875 L 21.929688 15.570312 L 23.496094 15.570312 L 21.578125 17.789062 L 23.802734 20.302734 L 22.236328 20.302734 L 20.587891 18.357422 L 20.587891 20.302734 L 19.435547 20.302734 L 19.435547 14 z M 10.001953 14.390625 L 11.933594 14.390625 C 13.105594 14.390625 14.056641 15.304687 14.056641 16.429688 C 14.056641 17.114687 13.701109 17.719797 13.162109 18.091797 L 14.892578 20.302734 L 13.427734 20.302734 L 11.996094 18.470703 L 11.169922 18.470703 L 11.169922 20.302734 L 10.001953 20.302734 L 10.001953 14.390625 z M 28.746094 14.390625 L 29.865234 14.390625 L 29.865234 15.568359 L 30.820312 15.568359 L 30.820312 16.691406 L 29.865234 16.691406 L 29.865234 18.716797 C 29.865234 19.163797 30.210578 19.298828 30.392578 19.298828 C 30.538578 19.298828 30.665625 19.246641 30.765625 19.181641 L 31.501953 20.123047 C 31.150953 20.323047 30.719203 20.421875 30.408203 20.421875 C 29.574203 20.421875 28.746094 19.791375 28.746094 18.734375 L 28.746094 16.691406 L 28.154297 16.691406 L 28.154297 15.570312 L 28.15625 15.570312 L 28.746094 15.570312 L 28.746094 14.390625 z M 33.347656 15.449219 C 34.620656 15.449219 35.762281 16.566578 35.488281 18.267578 L 32.269531 18.267578 C 32.416531 19.349578 33.688172 19.898406 34.451172 18.816406 L 35.439453 19.363281 C 34.804453 20.229281 34.048984 20.423828 33.458984 20.423828 C 32.290984 20.423828 31.126953 19.401547 31.126953 17.935547 C 31.126953 16.541547 32.053656 15.449219 33.347656 15.449219 z M 16.628906 15.453125 C 17.041906 15.453125 17.344641 15.573297 17.681641 15.779297 L 17.681641 15.570312 L 18.800781 15.570312 L 18.800781 20.302734 L 17.681641 20.302734 L 17.681641 20.09375 C 17.344641 20.29975 17.042906 20.419922 16.628906 20.419922 C 15.357906 20.419922 14.392578 19.3055 14.392578 17.9375 C 14.391578 16.5695 15.356906 15.453125 16.628906 15.453125 z M 38 15.453125 C 39.187 15.453125 40 16.379484 40 17.521484 L 40 20.302734 L 38.882812 20.302734 L 38.882812 17.521484 C 38.882812 17.000484 38.509797 16.560547 37.966797 16.560547 C 37.423797 16.560547 37.050781 17.000484 37.050781 17.521484 L 37.050781 20.302734 C 37.050781 20.302734 35.931641 20.303734 35.931641 20.302734 L 35.931641 15.570312 L 37.050781 15.570312 L 37.050781 15.744141 C 37.050781 15.744141 37.45 15.453125 38 15.453125 z M 11.169922 15.513672 L 11.169922 17.345703 L 11.931641 17.345703 C 12.456641 17.346703 12.886719 16.936688 12.886719 16.429688 C 12.886719 15.924687 12.456641 15.513672 11.931641 15.513672 L 11.169922 15.513672 z M 23.685547 15.570312 L 24.802734 15.570312 L 24.802734 18.351562 C 24.802734 18.872563 25.17575 19.314453 25.71875 19.314453 C 26.26175 19.314453 26.634766 18.872563 26.634766 18.351562 L 26.634766 15.570312 L 27.753906 15.570312 L 27.753906 20.304688 L 26.634766 20.304688 L 26.634766 20.128906 C 26.634766 20.128906 26.235547 20.421875 25.685547 20.421875 C 24.498547 20.421875 23.685547 19.493562 23.685547 18.351562 L 23.685547 15.570312 z M 33.324219 16.470703 C 32.877344 16.478703 32.428047 16.772172 32.310547 17.326172 L 34.324219 17.326172 C 34.215219 16.740172 33.771094 16.462703 33.324219 16.470703 z M 16.628906 16.59375 C 16.001906 16.59375 15.542969 17.1885 15.542969 17.9375 C 15.542969 18.6875 16.001906 19.279297 16.628906 19.279297 C 17.255906 19.279297 17.698219 18.6865 17.699219 17.9375 C 17.699219 17.1875 17.255906 16.59375 16.628906 16.59375 z M 16.240234 21.587891 L 34.472656 21.587891 L 17.693359 23.046875 L 16.240234 21.587891 z M 10 25.466797 L 13.179688 25.466797 L 15.326172 32.365234 L 17.441406 25.466797 L 20.607422 25.466797 L 16.578125 36 L 14.074219 36 L 10 25.466797 z M 21.617188 25.466797 L 24.628906 25.466797 L 24.628906 36 L 21.617188 36 L 21.617188 25.466797 z M 26.335938 25.466797 L 29.347656 25.466797 L 29.347656 29.470703 L 32.681641 25.466797 L 36.078125 25.466797 L 32.095703 30.183594 L 36.355469 36 L 32.896484 36 L 30.179688 32.126953 L 29.347656 32.957031 L 29.347656 36 L 26.335938 36 L 26.335938 25.466797 z M 36.988281 25.466797 L 40 25.466797 L 40 36 L 36.988281 36 L 36.988281 25.466797 z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||m-content-viki.s.llnwi.net^\",\n\t\t\"||viki.com^\",\n\t\t\"||viki.io^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"reddit\",\n\tName:    \"Reddit\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M14.238 15.348c.085.084.085.221 0 .306-.465.462-1.194.687-2.231.687l-.008-.002-.008.002c-1.036 0-1.766-.225-2.231-.688-.085-.084-.085-.221 0-.305.084-.084.222-.084.307 0 .379.377 1.008.561 1.924.561l.008.002.008-.002c.915 0 1.544-.184 1.924-.561.085-.084.223-.084.307 0zm-3.44-2.418c0-.507-.414-.919-.922-.919-.509 0-.923.412-.923.919 0 .506.414.918.923.918.508.001.922-.411.922-.918zm13.202-.93c0 6.627-5.373 12-12 12s-12-5.373-12-12 5.373-12 12-12 12 5.373 12 12zm-5-.129c0-.851-.695-1.543-1.55-1.543-.417 0-.795.167-1.074.435-1.056-.695-2.485-1.137-4.066-1.194l.865-2.724 2.343.549-.003.034c0 .696.569 1.262 1.268 1.262.699 0 1.267-.566 1.267-1.262s-.568-1.262-1.267-1.262c-.537 0-.994.335-1.179.804l-2.525-.592c-.11-.027-.223.037-.257.145l-.965 3.038c-1.656.02-3.155.466-4.258 1.181-.277-.255-.644-.415-1.05-.415-.854.001-1.549.693-1.549 1.544 0 .566.311 1.056.768 1.325-.03.164-.05.331-.05.5 0 2.281 2.805 4.137 6.253 4.137s6.253-1.856 6.253-4.137c0-.16-.017-.317-.044-.472.486-.261.82-.766.82-1.353zm-4.872.141c-.509 0-.922.412-.922.919 0 .506.414.918.922.918s.922-.412.922-.918c0-.507-.413-.919-.922-.919z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||redd.it^\",\n\t\t\"||reddit.com^\",\n\t\t\"||redditmail.com^\",\n\t\t\"||redditmedia.com^\",\n\t\t\"||redditstatic.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"riot_games\",\n\tName:    \"Riot Games\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 64 64\\\"><path d=\\\"M31.3 2.2a1 1 0 0 0-.8 1.4l.8 1.8a1 1 0 1 0 1.8-.8l-.8-1.8a1 1 0 0 0-1-.6zm-4.3 2a1 1 0 0 0-.9 1.5l1 1.8a1 1 0 1 0 1.7-.9L28 4.8a1 1 0 0 0-1-.6zm12 1a1 1 0 0 0-.4.1l-34 16.2a1 1 0 0 0-.6 1.1L9.3 47a1 1 0 0 0 1 .8h7a1 1 0 0 0 1-1l-1-12.2 3 12.4a1 1 0 0 0 .9.8h7.3a1 1 0 0 0 1-1l-.2-15.8L32 47a1 1 0 0 0 1 .8h7.6a1 1 0 0 0 1-1l1.3-19.4 1.4 19.5a1 1 0 0 0 1 1h10.2a1 1 0 0 0 1-1L60 11.2a1 1 0 0 0-.8-1l-20-5a1 1 0 0 0-.3 0zm-16.3 1a1 1 0 0 0-.9 1.5l.9 1.8a1 1 0 1 0 1.8-.8l-.9-1.8a1 1 0 0 0-1-.6zm16.4 1L57.9 12l-3.3 33.8h-8.3l-1.9-25a1 1 0 0 0-1.2-.8l-1.2.3a1 1 0 0 0-.7.9l-1.7 24.6h-5.8L30.3 25a1 1 0 0 0-1.3-.8l-1 .4a1 1 0 0 0-.8 1l.3 20.2H22l-4-17a1 1 0 0 0-1.2-.7l-1.1.3a1 1 0 0 0-.7 1l1.2 16.4H11L6.1 23l33-15.7zM18.5 8.4a1 1 0 0 0-1 1.5l.9 1.8a1 1 0 1 0 1.8-.9L19.3 9a1 1 0 0 0-.8-.6zm-4.3 2.1a1 1 0 0 0-.1 0 1 1 0 0 0-.9 1.4l.9 1.8a1 1 0 1 0 1.8-.8L15 11a1 1 0 0 0-.8-.6zm-4.4 2a1 1 0 0 0-.9 1.5l.9 1.8a1 1 0 1 0 1.8-.9l-.9-1.8a1 1 0 0 0-1-.5zm-4.3 2.1a1 1 0 0 0-.9 1.4l.9 1.9a1 1 0 1 0 1.8-1l-.9-1.7a1 1 0 0 0-.9-.6zM30.7 49a1 1 0 0 0-.9 1.4l2.5 6.5a1 1 0 0 0 .7.6l20.7 5.3a1 1 0 0 0 1.2-.9L56 51.4a1 1 0 0 0-1-1L30.9 49a1 1 0 0 0-.1 0zm1.5 2.1L54 52.3l-.9 8.2-19-4.8-1.8-4.6z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||dradis-prod.rdatasrv.net^\",\n\t\t\"||pvp.net^\",\n\t\t\"||rgpub.io^\",\n\t\t\"||riotcdn.com^\",\n\t\t\"||riotcdn.net^\",\n\t\t\"||riotgames.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"roblox\",\n\tName:    \"Roblox\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"m13.383 14.341-3.726-.958.959-3.726 3.726.959-.96 3.726zM4.913 0 0 19.088 19.088 24 24 4.912 4.912 0z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||blox.com^\",\n\t\t\"||rbx.cn^\",\n\t\t\"||rbx.com^\",\n\t\t\"||rbxadder.com^\",\n\t\t\"||rbxcdn.com^\",\n\t\t\"||rbxcdn.net^\",\n\t\t\"||rbxinfra.com^\",\n\t\t\"||rbxinfra.net^\",\n\t\t\"||roblox.cn^\",\n\t\t\"||roblox.com^\",\n\t\t\"||roblox.qq.com^\",\n\t\t\"||robloxcdn.com^\",\n\t\t\"||robloxdev.cn^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"rockstar_games\",\n\tName:    \"Rockstar Games\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M12 3c-4.96 0-9 4.04-9 9v26c0 4.96 4.04 9 9 9h26c4.96 0 9-4.04 9-9V12c0-4.96-4.04-9-9-9H12zm0 2h26c3.88 0 7 3.12 7 7v26c0 3.88-3.12 7-7 7H12c-3.88 0-7-3.12-7-7V12c0-3.88 3.12-7 7-7zm3.72 5a1 1 0 0 0-.97.79l-3.87 18a1 1 0 0 0 .98 1.21h4.27a1 1 0 0 0 .97-.79L18.47 23h2.07c.94 0 1.12.15 1.36.73.24.57.3 1.76.1 3.4-.08.68-.05 1.22.02 1.6v.03a1 1 0 0 0 .3.97l3.37 3.12-2.6 5.74a1 1 0 0 0 1.43 1.26l5.58-3.39 4.29 3.33a1 1 0 0 0 1.6-.98l-1.09-5.56 4.7-3.47a1 1 0 0 0-.6-1.8h-4.86l-.82-5.14a1 1 0 0 0-.98-.84 1 1 0 0 0-.88.51l-2.77 5a14.3 14.3 0 0 1 .06-2.83c.15-1.48.01-2.64-.18-3.45-.06-.28-.08-.25-.15-.45.3-.17.4-.13.77-.5.8-.8 1.6-2.18 1.75-4.26.17-2.26-.55-3.98-1.92-4.9C27.65 10.17 25.91 10 24 10h-8.28zm.81 2H24c1.75 0 3.13.25 3.9.77.76.52 1.18 1.27 1.05 3.1-.13 1.67-.69 2.51-1.17 3a2 2 0 0 1-.82.56 1 1 0 0 0-.6 1.44s.12.21.27.82c.14.6.26 1.53.13 2.79a14.24 14.24 0 0 0-.01 3.52h-2.76c-.01-.19-.04-.32 0-.62.22-1.78.25-3.21-.24-4.42A3.38 3.38 0 0 0 20.54 21h-2.87a1 1 0 0 0-.98.78L15.32 28H13.1l3.44-16zm2.76 1.03a1 1 0 0 0-.98.8l-.98 4.94a1 1 0 0 0 .98 1.2h4.47c.79 0 1.65-.12 2.44-.58a3.6 3.6 0 0 0 1.68-2.41 3.3 3.3 0 0 0-.72-2.92 3.35 3.35 0 0 0-2.47-1.03h-4.42zm.82 2h3.6c.41 0 .79.16 1 .4.22.22.36.52.23 1.15-.13.62-.36.88-.72 1.08a3 3 0 0 1-1.44.3h-3.25l.58-2.93zm11.7 10.99.49 3.11a1 1 0 0 0 .98.84h2.69l-2.76 2.05a1 1 0 0 0-.4 1l.7 3.56-2.73-2.12a1 1 0 0 0-1.13-.07l-3.4 2.07 1.56-3.44a1 1 0 0 0-.23-1.15L25.55 30H29a1 1 0 0 0 .88-.51l1.92-3.47z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||rockstargames.com^\",\n\t\t\"||rsg.sc^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"samsung_tv_plus\",\n\tName:    \"Samsung TV Plus\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"121 -91 672 672\\\"><path d=\\\"m710.504 89.711.004 38.892c0 .073.021.14.021.214V352.97c0 21.351-17.05 38.526-38.326 38.835l-.05.052h-.473c-.015 0-.028.004-.042.004h-96.769v31.034a24.11 24.11 0 0 1-24.162 24.163H368.046a24.11 24.11 0 0 1-24.162-24.163v-31.034H208.832v35.86c0 34.394 27.687 62.083 62.081 62.083h457.22c34.395 0 62.085-27.69 62.085-62.083V151.795c0-34.394-27.69-62.084-62.084-62.084zM185.028 0c-34.394 0-62.084 27.69-62.084 62.084V329.78c0 34.393 27.69 62.081 62.083 62.081h23.804V128.817c0-21.546 17.346-38.89 38.892-38.89h38.6l.176-.216h424.005V62.084C710.504 27.69 682.814 0 648.421 0Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||internetat.tv^\",\n\t\t\"||samsung.wurl.tv^\",\n\t\t\"||samsungcloud.tv^\",\n\t\t\"||samsungtvplus.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"shein\",\n\tName:    \"Shein\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M14 4C8.49 4 4 8.49 4 14v22c0 5.51 4.49 10 10 10h22c5.51 0 10-4.49 10-10V14c0-5.51-4.49-10-10-10H14zm0 2h22c4.43 0 8 3.57 8 8v22c0 4.43-3.57 8-8 8H14c-4.43 0-8-3.57-8-8V14c0-4.43 3.57-8 8-8zm11 7c-4 0-8.5 2-8.5 6.5 0 7.75 13 6.25 13 11 0 3.25-3.5 3.5-4.5 3.5-2 0-5-1-6.5-2L16 34.5c2.5 1.5 5.5 2.75 9 2.75 3 0 8.5-1.5 8.5-6.75 0-7-13-6.75-13-11 0-3 3.5-3.5 4.5-3.5 1.25 0 3.75.5 5 1.5l2.5-2.25C31 14 29 13 25 13z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||shein.co.uk^\",\n\t\t\"||shein.com^\",\n\t\t\"||shein.se^\",\n\t\t\"||sheinsz.ltwebstatic.com^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"shopee\",\n\tName:    \"Shopee\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M25 1c-5.3 0-9.4 5-9.8 11H5a2 2 0 0 0-2 2.1l1.7 30.2a5 5 0 0 0 5 4.7h30.4a5 5 0 0 0 5-4.7L47 14a2 2 0 0 0-2-2.1H35C34.3 6 30.2 1 25 1zm0 2c4 0 7.4 3.9 7.8 9H17.2c.4-5.1 3.8-9 7.8-9zM5 14h10.8a1 1 0 0 0 .4 0h17.6a1 1 0 0 0 .4 0h10.7l-1.7 30.2a3 3 0 0 1-3 2.8H9.8a3 3 0 0 1-3-2.8L5 14zm20 4c-4.2 0-7.5 2.7-7.5 6.3 0 4 3.8 5.4 7 6.6 4 1.4 6.5 2.5 6.5 5.7 0 2.4-2.7 4.4-6 4.4-3.8 0-7-2.7-7-2.7l-1.2 1.6c.8.7 4.1 3.1 8.1 3.1 4.5 0 8-2.8 8-6.4 0-4.8-4-6.3-7.7-7.6-3.5-1.3-5.7-2.3-5.7-4.7 0-2.5 2.3-4.3 5.6-4.3a11 11 0 0 1 6 1.9l1-1.7c-.3-.1-3.2-2.2-7-2.2z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||shopee.cl^\",\n\t\t\"||shopee.cn^\",\n\t\t\"||shopee.co.id^\",\n\t\t\"||shopee.co.th^\",\n\t\t\"||shopee.com.br^\",\n\t\t\"||shopee.com.co^\",\n\t\t\"||shopee.com.mx^\",\n\t\t\"||shopee.com.my^\",\n\t\t\"||shopee.com^\",\n\t\t\"||shopee.es^\",\n\t\t\"||shopee.fr^\",\n\t\t\"||shopee.id^\",\n\t\t\"||shopee.in^\",\n\t\t\"||shopee.io^\",\n\t\t\"||shopee.ph^\",\n\t\t\"||shopee.sg^\",\n\t\t\"||shopee.tw^\",\n\t\t\"||shopee.vn^\",\n\t\t\"||shopeemobile.com^\",\n\t\t\"||shp.ee^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"signal\",\n\tName:    \"Signal\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 32 32\\\"><path d=\\\"M26.6 16c-.2 4-1.9 7.2-5.5 9.2a10.6 10.6 0 0 1-10.5-.2h-.5l-3.8.9c-.2.1-.3 0-.2-.2l.9-3.8-.1-.5a10.6 10.6 0 1 1 19.5-7.2l.2 1.8z\\\"/><path d=\\\"M4.6 28.6c-.8 0-1.4-.7-1.2-1.4l.6-2.5.2-.1.9.2c.2 0 .1.1.1.2l-.5 2c-.1.3-.1.3.3.2l2-.5c.2 0 .2 0 .3.2l.2.8-.1.2-2.8.7c.1 0 .1 0 0 0zm5.1-1.2-1 .2c-.2.1-.3 0-.3-.2l-.2-.8.1-.2 1.5-.3h.2c.9.5 1.9.9 2.9 1.2.1 0 .2.1.1.2l-.2.7c0 .2-.1.2-.3.2-.9-.2-1.8-.6-2.6-1 0 .1 0 0-.2 0zm-3.8-5.3-.3 1.3c-.1.5 0 .4-.5.3l-.6-.1c-.1 0-.2-.1-.1-.2l.2-.9v-.4c-.4-.8-.8-1.7-1-2.6-.1-.3-.1-.3.2-.3l.7-.2.2.1c.3 1 .7 2 1.2 2.9-.1 0 0 0 0 .1zM26.4 8.3l-.1.1-.7.5c-.1.1-.2.1-.2 0-.7-.9-1.4-1.6-2.3-2.3-.1-.1-.1-.1 0-.2l.5-.7c.1-.1.1-.1.2 0 1 .7 1.8 1.5 2.6 2.6 0-.1 0-.1 0 0zm-7.2-4.9h.1c1.1.3 2.2.7 3.2 1.3.2.1.2.2.1.3l-.4.7-.2.1a10 10 0 0 0-3-1.2c-.1 0-.2-.1-.1-.2l.2-.9.1-.1zM6.5 9s-.1-.1 0 0l-.8-.6v-.3l1-1.3 1.4-1.2h.3l.5.7v.3C8 7.3 7.3 8 6.6 8.9l-.1.1zm21.1 9.9.9.2.1.2-.8 2.2-.5 1-.3.1-.7-.4-.1-.2c.5-.9 1-1.9 1.2-3 0-.1.1-.2.2-.1zM4.4 13.1l-.9-.2-.1-.2c.2-.9.6-1.8 1-2.6l.3-.6c.2-.2.2-.2.3-.1l.8.5c.1.1.1.1 0 .2-.5.9-1 1.9-1.2 3 0 0 0 .1-.2 0zM3 16l.1-1.7c0-.2.1-.2.3-.2l.8.1.2.2c-.1 1.1-.1 2.1 0 3.2l-.1.2-.9.1c-.2 0-.2-.1-.2-.2C3 17.2 3 16.6 3 16zm25.6-3.2-.1.1-.9.2c-.2 0-.2-.1-.2-.2-.2-.9-.6-1.7-1-2.5l-.2-.4c-.1-.1 0-.1 0-.2l.8-.5h.2a15 15 0 0 1 1.4 3.5zm-2.2 10.9-.1.1c-.7.9-1.6 1.8-2.5 2.5-.1.1-.2.1-.2 0l-.5-.7v-.3c.8-.6 1.6-1.4 2.2-2.2h.3l.7.5.1.1zM16 3l1.7.1c.2 0 .2.1.2.3l-.1.7-.2.2a16 16 0 0 0-3.1 0c-.2 0-.2 0-.2-.2l-.1-.8c0-.1 0-.2.2-.2L16 3zm0 26-1.8-.1c-.2 0-.2-.1-.2-.3l.1-.8.2-.2c1.1.1 2.1.1 3.2 0 .1 0 .2 0 .2.2l.1.8c0 .2-.1.2-.2.2-.4.2-1 .2-1.6.2zM12.8 3.4l.1.1.2.9c0 .2-.1.2-.2.2-.9.2-1.8.6-2.6 1-.3.2-.5.2-.7-.1l-.2-.4c-.1-.2-.1-.2.1-.3.8-.5 1.7-.9 2.6-1.2l.7-.2zM29 16l-.1 1.7c0 .2-.1.2-.3.2l-.7-.1c-.3 0-.3 0-.2-.3v-3.1c0-.1 0-.2.2-.2l.8-.1c.2 0 .2 0 .2.2L29 16zm-9.8 12.6-.1-.1-.2-.9.2-.2c.9-.2 1.7-.6 2.5-1l.4-.2c.1-.1.1 0 .2 0l.5.8v.2c-1.1.6-2.2 1.1-3.5 1.4z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||signal.org^\",\n\t\t\"||whispersystems.org^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"skype\",\n\tName:    \"Skype\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 26 26\\\"><path d=\\\"M23.363 14.387c.153-.739.23-1.5.23-2.266C23.594 5.883 18.45.805 12.122.805c-.594 0-1.191.047-1.781.136A6.891 6.891 0 0 0 6.852 0C3.074 0 0 3.035 0 6.762c0 1.144.293 2.27.852 3.265-.133.688-.2 1.391-.2 2.094 0 6.238 5.149 11.316 11.47 11.316.648 0 1.3-.054 1.94-.164.95.477 2.012.727 3.086.727C20.926 24 24 20.969 24 17.238c0-1.004-.215-1.96-.637-2.851zM17.758 17.3c-.508.707-1.258 1.27-2.23 1.668-.966.394-2.122.593-3.434.593-1.578 0-2.903-.273-3.934-.812a5.074 5.074 0 0 1-1.808-1.582c-.47-.664-.707-1.324-.707-1.961 0-.395.156-.738.457-1.023.304-.278.687-.418 1.148-.418.379 0 .703.109.969.332.254.21.469.523.644.93.192.437.407.808.633 1.1.211.282.524.52.918.704.399.188.938.281 1.598.281.91 0 1.652-.191 2.215-.57.546-.367.812-.813.812-1.352 0-.43-.14-.765-.422-1.027-.3-.277-.699-.492-1.176-.637-.5-.152-1.18-.32-2.015-.496-1.14-.238-2.11-.523-2.88-.847-.788-.332-1.425-.79-1.89-1.364-.472-.582-.71-1.312-.71-2.172 0-.816.253-1.554.75-2.191.488-.633 1.206-1.125 2.132-1.46.91-.333 1.996-.5 3.223-.5.98 0 1.844.108 2.566.331.723.223 1.336.524 1.813.89.484.376.843.774 1.07 1.188.227.418.344.832.344 1.235 0 .386-.153.738-.453 1.046-.297.31-.68.465-1.125.465-.41 0-.727-.097-.95-.289-.207-.18-.418-.46-.656-.863-.273-.516-.605-.918-.984-1.203-.371-.277-.989-.418-1.836-.418-.79 0-1.43.156-1.902.465-.461.293-.684.633-.684 1.039 0 .246.07.449.219.629.156.187.379.351.656.488.289.145.586.258.883.34.308.082.82.207 1.523.367.887.191 1.707.398 2.43.625.73.234 1.363.516 1.879.848.527.34.941.773 1.238 1.293.297.52.445 1.16.445 1.91a4.07 4.07 0 0 1-.77 2.418zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||edge-skype-com.s-0001.s-msedge.net^\",\n\t\t\"||skype-edf.akadns.net^\",\n\t\t\"||skype.com^\",\n\t\t\"||skype.net^\",\n\t\t\"||skype^\",\n\t\t\"||skypeassets.com^\",\n\t\t\"||skypeassets.net^\",\n\t\t\"||skypedata.akadns.net^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"slack\",\n\tName:    \"Slack\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 54 54\\\"><path fill-rule=\\\"evenodd\\\" d=\\\"M19.71.13a5.38 5.38 0 0 0-5.37 5.39 5.38 5.38 0 0 0 5.37 5.39h5.38V5.52A5.38 5.38 0 0 0 19.7.13m0 14.37H5.38A5.38 5.38 0 0 0 0 19.88a5.38 5.38 0 0 0 5.38 5.4H19.7a5.38 5.38 0 0 0 5.38-5.4 5.38 5.38 0 0 0-5.38-5.38m34.06 5.38a5.38 5.38 0 0 0-5.38-5.38 5.38 5.38 0 0 0-5.37 5.38v5.4h5.37a5.38 5.38 0 0 0 5.38-5.4m-14.34 0V5.52A5.38 5.38 0 0 0 34.05.13a5.38 5.38 0 0 0-5.38 5.39v14.36a5.38 5.38 0 0 0 5.38 5.4 5.38 5.38 0 0 0 5.37-5.4M34.05 54a5.38 5.38 0 0 0 5.37-5.39 5.38 5.38 0 0 0-5.37-5.38h-5.38v5.38A5.38 5.38 0 0 0 34.05 54m0-14.37h14.33a5.38 5.38 0 0 0 5.38-5.38 5.38 5.38 0 0 0-5.38-5.39H34.05a5.38 5.38 0 0 0-5.38 5.39 5.38 5.38 0 0 0 5.38 5.38M0 34.25a5.38 5.38 0 0 0 5.38 5.39 5.38 5.38 0 0 0 5.37-5.4v-5.38H5.38A5.38 5.38 0 0 0 0 34.25m14.34 0V48.6A5.38 5.38 0 0 0 19.7 54a5.38 5.38 0 0 0 5.38-5.39V34.25a5.38 5.38 0 0 0-5.38-5.39 5.38 5.38 0 0 0-5.37 5.39\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||slack-edge.com^\",\n\t\t\"||slack-files.com ^\",\n\t\t\"||slack-imgs.com^\",\n\t\t\"||slack.com^\",\n\t\t\"||slackb.com^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"snapchat\",\n\tName:    \"Snapchat\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M12.176 4c.715 0 3.136.191 4.277 2.668.383.828.285 2.273.211 3.437l-.004.051c-.008.164-.02.32-.027.469.015.02.164.156.492.168.25-.012.54-.086.855-.23a.784.784 0 0 1 .57.008h.005c.254.09.422.261.425.44.004.173-.128.43-.789.68a2.694 2.694 0 0 1-.25.082c-.375.118-.945.293-1.117.692-.097.215-.066.48.09.785 0 .004.004.008.004.012.047.105 1.187 2.62 3.73 3.027.094.016.16.094.153.188a.24.24 0 0 1-.024.101c-.105.238-.578.574-2.234.824-.133.02-.188.188-.266.547-.03.13-.058.258-.101.39-.035.118-.11.173-.235.173h-.02a2.34 2.34 0 0 1-.37-.043 4.986 4.986 0 0 0-.996-.102c-.23 0-.473.02-.715.059-.496.078-.918.367-1.363.672-.653.445-1.32.902-2.364.902-.047 0-.09 0-.136-.004-.028.004-.055.004-.086.004-1.043 0-1.711-.457-2.36-.902-.445-.305-.867-.594-1.363-.672a4.533 4.533 0 0 0-.719-.059c-.418 0-.75.063-.992.106a2.02 2.02 0 0 1-.371.054c-.102 0-.211-.023-.258-.18-.039-.136-.07-.269-.101-.394-.075-.328-.125-.531-.266-.55-1.656-.247-2.129-.587-2.234-.825-.012-.035-.024-.066-.024-.101a.182.182 0 0 1 .156-.188c2.54-.406 3.68-2.922 3.727-3.031.004 0 .004-.004.004-.008.156-.305.187-.57.094-.79-.176-.398-.747-.57-1.122-.687a3.147 3.147 0 0 1-.25-.082c-.75-.289-.812-.582-.785-.734.051-.254.407-.434.692-.434a.49.49 0 0 1 .207.04c.336.152.64.23.906.23.363 0 .52-.148.54-.168-.009-.164-.02-.34-.032-.52-.074-1.164-.168-2.609.21-3.433 1.138-2.477 3.555-2.668 4.27-2.668L12.133 4h.043m0-1.602h-.043l-.313.008v-.004c-.953 0-4.187.262-5.722 3.598-.387.844-.45 1.887-.422 2.922-.922.02-2 .625-2.215 1.726-.082.407-.184 1.786 1.781 2.54.012.003.02.007.031.011-.39.559-1.113 1.34-2.168 1.508-.902.14-1.55.941-1.5 1.86.016.226.067.44.153.64.41.938 1.406 1.363 2.543 1.613a1.83 1.83 0 0 0 1.785 1.305c.246 0 .465-.043.66-.078a3.44 3.44 0 0 1 .703-.082c.149 0 .305.012.465.039.14.023.457.238.711.41.73.5 1.727 1.184 3.266 1.184h.101c.04 0 .078.004.121.004 1.532 0 2.528-.68 3.258-1.176.281-.192.582-.399.723-.422.156-.024.312-.04.46-.04.259 0 .458.032.696.075.266.05.477.074.668.074.852 0 1.543-.508 1.785-1.293 1.137-.25 2.129-.672 2.535-1.593.094-.22.149-.43.16-.649a1.783 1.783 0 0 0-1.496-1.871c-1.054-.168-1.78-.95-2.172-1.508l.036-.011c1.601-.618 1.824-1.645 1.816-2.204-.02-.855-.594-1.601-1.477-1.918a2.37 2.37 0 0 0-.777-.156c.027-1.015-.039-2.078-.422-2.914-1.539-3.336-4.773-3.598-5.73-3.598zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||impala-media-production.s3.amazonaws.com^\",\n\t\t\"||sc-cdn.net^\",\n\t\t\"||snap-dev.net^\",\n\t\t\"||snapads.com^\",\n\t\t\"||snapchat.com^\",\n\t\t\"||snapkit.co\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"soundcloud\",\n\tName:    \"SoundCloud\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\" fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\"><path d=\\\"M19 17.75c2.07 0 3.75-1.68 3.75-3.75 0-2.07-1.68-3.75-3.75-3.75-.173 0-.344.012-.511.035-.73-2.337-2.913-4.035-5.489-4.035-.818 0-1.596.171-2.301.48-.273.119-.449.389-.449.687l0 9.583c0 .414.336.75.75.75l8 0zM7.25 8l0 9c0 .414.336.75.75.75.414 0 .75-.336.75-.75l0-9c0-.414-.336-.75-.75-.75-.414 0-.75.336-.75.75zM4.25 10l0 7c0 .414.336.75.75.75.414 0 .75-.336.75-.75l0-7c0-.414-.336-.75-.75-.75-.414 0-.75.336-.75.75zM1.25 12l0 5c0 .414.336.75.75.75.414 0 .75-.336.75-.75l0-5c0-.414-.336-.75-.75-.75-.414 0-.75.336-.75.75z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||sndcdn.com^\",\n\t\t\"||soundcloud.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"spotify\",\n\tName:    \"Spotify\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M25.009,1.982C12.322,1.982,2,12.304,2,24.991S12.322,48,25.009,48s23.009-10.321,23.009-23.009S37.696,1.982,25.009,1.982z M34.748,35.333c-0.289,0.434-0.765,0.668-1.25,0.668c-0.286,0-0.575-0.081-0.831-0.252C30.194,34.1,26,33,22.5,33.001 c-3.714,0.002-6.498,0.914-6.526,0.923c-0.784,0.266-1.635-0.162-1.897-0.948s0.163-1.636,0.949-1.897 c0.132-0.044,3.279-1.075,7.474-1.077C26,30,30.868,30.944,34.332,33.253C35.022,33.713,35.208,34.644,34.748,35.333z M37.74,29.193 c-0.325,0.522-0.886,0.809-1.459,0.809c-0.31,0-0.624-0.083-0.906-0.26c-4.484-2.794-9.092-3.385-13.062-3.35 c-4.482,0.04-8.066,0.895-8.127,0.913c-0.907,0.258-1.861-0.272-2.12-1.183c-0.259-0.913,0.272-1.862,1.184-2.12 c0.277-0.079,3.854-0.959,8.751-1c4.465-0.037,10.029,0.61,15.191,3.826C37.995,27.328,38.242,28.388,37.74,29.193z M40.725,22.013 C40.352,22.647,39.684,23,38.998,23c-0.344,0-0.692-0.089-1.011-0.275c-5.226-3.068-11.58-3.719-15.99-3.725 c-0.021,0-0.042,0-0.063,0c-5.333,0-9.44,0.938-9.481,0.948c-1.078,0.247-2.151-0.419-2.401-1.495 c-0.25-1.075,0.417-2.149,1.492-2.4C11.729,16.01,16.117,15,21.934,15c0.023,0,0.046,0,0.069,0 c4.905,0.007,12.011,0.753,18.01,4.275C40.965,19.835,41.284,21.061,40.725,22.013z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"/_spotify-connect._tcp.local/\",\n\t\t\"|audio-ak-spotify-com.akamaized.net^\",\n\t\t\"|audio4-ak-spotify-com.akamaized.net^\",\n\t\t\"|heads-ak-spotify-com.akamaized.net^\",\n\t\t\"|heads4-ak-spotify-com.akamaized.net^\",\n\t\t\"|spotify.com.edgesuite.net^\",\n\t\t\"|spotify.map.fastly.net^\",\n\t\t\"|spotify.map.fastlylb.net^\",\n\t\t\"||byspotify.com^\",\n\t\t\"||pscdn.co^\",\n\t\t\"||scdn.co^\",\n\t\t\"||spoti.fi^\",\n\t\t\"||spotify-everywhere.com^\",\n\t\t\"||spotify.com^\",\n\t\t\"||spotify.design^\",\n\t\t\"||spotifycdn.com^\",\n\t\t\"||spotifycdn.net^\",\n\t\t\"||spotifycharts.com^\",\n\t\t\"||spotifycodes.com^\",\n\t\t\"||spotifyforbrands.com^\",\n\t\t\"||spotifyjobs.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"spotify_video\",\n\tName:    \"Spotify Video\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M25.009,1.982C12.322,1.982,2,12.304,2,24.991S12.322,48,25.009,48s23.009-10.321,23.009-23.009S37.696,1.982,25.009,1.982z M34.748,35.333c-0.289,0.434-0.765,0.668-1.25,0.668c-0.286,0-0.575-0.081-0.831-0.252C30.194,34.1,26,33,22.5,33.001 c-3.714,0.002-6.498,0.914-6.526,0.923c-0.784,0.266-1.635-0.162-1.897-0.948s0.163-1.636,0.949-1.897 c0.132-0.044,3.279-1.075,7.474-1.077C26,30,30.868,30.944,34.332,33.253C35.022,33.713,35.208,34.644,34.748,35.333z M37.74,29.193 c-0.325,0.522-0.886,0.809-1.459,0.809c-0.31,0-0.624-0.083-0.906-0.26c-4.484-2.794-9.092-3.385-13.062-3.35 c-4.482,0.04-8.066,0.895-8.127,0.913c-0.907,0.258-1.861-0.272-2.12-1.183c-0.259-0.913,0.272-1.862,1.184-2.12 c0.277-0.079,3.854-0.959,8.751-1c4.465-0.037,10.029,0.61,15.191,3.826C37.995,27.328,38.242,28.388,37.74,29.193z M40.725,22.013 C40.352,22.647,39.684,23,38.998,23c-0.344,0-0.692-0.089-1.011-0.275c-5.226-3.068-11.58-3.719-15.99-3.725 c-0.021,0-0.042,0-0.063,0c-5.333,0-9.44,0.938-9.481,0.948c-1.078,0.247-2.151-0.419-2.401-1.495 c-0.25-1.075,0.417-2.149,1.492-2.4C11.729,16.01,16.117,15,21.934,15c0.023,0,0.046,0,0.069,0 c4.905,0.007,12.011,0.753,18.01,4.275C40.965,19.835,41.284,21.061,40.725,22.013z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||eip-ntt.video-ak.cdn.spotify.com.akahost.net^\",\n\t\t\"||video-ak.cdn.spotify.com^\",\n\t\t\"||video-akpcw-cdn-spotify-com.akamaized.net^\",\n\t\t\"||video-akpcw.spotifycdn.com.edgesuite.net^\",\n\t\t\"||video-akpcw.spotifycdn.com^\",\n\t\t\"||video-fa.scdn.co^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"steam\",\n\tName:    \"Steam\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 22 22\\\"><path d=\\\"M14.398 7.2a2.4 2.4 0 1 0 .003 4.799 2.4 2.4 0 0 0-.003-4.8zm0 0\\\" fill=\\\"none\\\" strokeWidth=\\\"1.6\\\" stroke=\\\"currentColor\\\" strokeMiterlimit=\\\"10\\\" /><path d=\\\"M8 14c-.629 0-1.18.297-1.547.75l1.758.48c.426.114.68.555.562.98a.804.804 0 0 1-.984.563l-1.762-.48A1.998 1.998 0 0 0 10 16c0-1.105-.895-2-2-2zm0 0\\\" /><path d=\\\"M19.2 3.2H4.8c-.886 0-1.6.714-1.6 1.6v9.063l2.027.551a3.213 3.213 0 0 1 2.289-1.566l2.136-2.567a4.799 4.799 0 1 1 4.066 4.066l-2.566 2.137A3.195 3.195 0 0 1 8 19.2 3.2 3.2 0 0 1 4.8 16c0-.016.005-.027.005-.043l-1.606-.437v3.68c0 .886.715 1.6 1.602 1.6h14.398c.887 0 1.602-.714 1.602-1.6V4.8c0-.886-.715-1.6-1.602-1.6zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"|steambroadcast.akamaized.net^\",\n\t\t\"|steamcdn-a.akamaihd.net^\",\n\t\t\"|steamcommunity-a.akamaihd.net^\",\n\t\t\"|steamstore-a.akamaihd.net^\",\n\t\t\"|steamusercontent-a.akamaihd.net^\",\n\t\t\"|steamuserimages-a.akamaihd.net^\",\n\t\t\"|steamvideo-a.akamaihd.net^\",\n\t\t\"|xz.pphimalayanrt.com^\",\n\t\t\"||csgo.wmsj.cn^\",\n\t\t\"||dl.steam.clngaa.com^\",\n\t\t\"||dl.steam.ksyna.com^\",\n\t\t\"||dota2.wmsj.cn^\",\n\t\t\"||playartifact.com^\",\n\t\t\"||s.team^\",\n\t\t\"||st.dl.bscstorage.net^\",\n\t\t\"||st.dl.eccdnx.com^\",\n\t\t\"||st.dl.pinyuncloud.com^\",\n\t\t\"||steam-api.com^\",\n\t\t\"||steam-chat.com^\",\n\t\t\"||steamchina.com^\",\n\t\t\"||steamcommunity.com^\",\n\t\t\"||steamcontent.com^\",\n\t\t\"||steamdeck.com^\",\n\t\t\"||steamgames.com^\",\n\t\t\"||steampipe.steamcontent.tnkjmec.com^\",\n\t\t\"||steampowered.com.8686c.com^\",\n\t\t\"||steampowered.com^\",\n\t\t\"||steamserver.net^\",\n\t\t\"||steamstatic.com.8686c.com^\",\n\t\t\"||steamstatic.com^\",\n\t\t\"||steamusercontent.com^\",\n\t\t\"||underlords.com^\",\n\t\t\"||valvesoftware.com^\",\n\t\t\"||wmsjsteam.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"telegram\",\n\tName:    \"Telegram (Web)\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M46.137,6.552c-0.75-0.636-1.928-0.727-3.146-0.238l-0.002,0C41.708,6.828,6.728,21.832,5.304,22.445 c-0.259,0.09-2.521,0.934-2.288,2.814c0.208,1.695,2.026,2.397,2.248,2.478l8.893,3.045c0.59,1.964,2.765,9.21,3.246,10.758 c0.3,0.965,0.789,2.233,1.646,2.494c0.752,0.29,1.5,0.025,1.984-0.355l5.437-5.043l8.777,6.845l0.209,0.125 c0.596,0.264,1.167,0.396,1.712,0.396c0.421,0,0.825-0.079,1.211-0.237c1.315-0.54,1.841-1.793,1.896-1.935l6.556-34.077 C47.231,7.933,46.675,7.007,46.137,6.552z M22,32l-3,8l-3-10l23-17L22,32z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||comments.app^\",\n\t\t\"||contest.com^\",\n\t\t\"||graph.org^\",\n\t\t\"||quiz.directory^\",\n\t\t\"||t.me^\",\n\t\t\"||tdesktop.com^\",\n\t\t\"||telega.one^\",\n\t\t\"||telegra.ph^\",\n\t\t\"||telegram-cdn.org^\",\n\t\t\"||telegram.dog^\",\n\t\t\"||telegram.me^\",\n\t\t\"||telegram.org^\",\n\t\t\"||telegram.space^\",\n\t\t\"||telesco.pe^\",\n\t\t\"||tg.dev^\",\n\t\t\"||tx.me^\",\n\t\t\"||usercontent.dev^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"temu\",\n\tName:    \"Temu\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 256 256\\\"><path d=\\\"M199.1 0C230.525 0 256 25.475 256 56.9v142.2c0 31.425-25.475 56.9-56.9 56.9H56.9C25.475 256 0 230.525 0 199.1V56.9C0 25.475 25.475 0 56.9 0zm-64 132.98h-3.4c-3.025 0-5.5 2.45-5.475 5.475v37.625c0 3.025 2.45 5.5 5.475 5.5s5.5-2.45 5.5-5.5v-24.7l9.25 13.05c1.925 2.7 5.925 2.7 7.875 0l9.25-13.05v24.7a5.5 5.5 0 0 0 5.5 5.5c3.025 0 5.5-2.45 5.475-5.5v-37.625c0-3.025-2.45-5.5-5.475-5.475h-3.4c-1.3 0-2.55.625-3.3 1.7l-11.975 18-12-18a3.997 3.997 0 0 0-3.3-1.7zm85.05 0c-3.025 0-5.5 2.45-5.5 5.475v22.975c0 7.225-4.075 10.925-10.775 10.9-6.7 0-10.775-3.825-10.75-11.225v-22.65c0-3.025-2.45-5.5-5.5-5.475-3.025 0-5.5 2.45-5.475 5.475v22.9c0 13.4 8.2 20.225 21.6 20.225s21.9-6.75 21.875-20.55v-22.575c0-3.025-2.45-5.5-5.475-5.475zm-154.22 0H33.855c-3.025 0-5.5 2.45-5.5 5.475s2.45 5.5 5.5 5.5h10.55v32.075c0 3.025 2.45 5.5 5.475 5.5s5.5-2.45 5.5-5.5v-32.075h10.55a5.5 5.5 0 0 0 5.5-5.5c0-3.025-2.45-5.5-5.5-5.475zm47.475 0H83.68c-3.025 0-5.5 2.45-5.5 5.475v37.575a5.5 5.5 0 0 0 5.5 5.5h29.725c3.025 0 5.5-2.45 5.475-5.5 0-3.025-2.45-5.5-5.475-5.5h-24.25v-7.8h21.1c3.025 0 5.5-2.45 5.5-5.475s-2.45-5.5-5.5-5.5h-21.1v-7.8h24.25c3.025 0 5.5-2.45 5.475-5.5 0-3.025-2.45-5.5-5.475-5.475zM59.78 75.63l-1.025.025c-4.275.275-7.2 2.125-8.85 4.625-1.925-2.875-5.525-4.9-10.95-4.6l-.125.175c-.625 1-2.975 5.475.825 10.35.775.825 2.675 3.15 1.9 6.125L30.53 110.155c-.9 1.45-.5 3.325.875 4.3 2.85 2 8.575 4.75 18.5 4.75 9.9 0 15.625-2.75 18.475-4.75l.375-.325a3.179 3.179 0 0 0 .5-3.975l-11-17.825.075.325-.125-.5c-.6-2.675.9-4.8 1.725-5.75l.2-.2c3.825-4.875 1.45-9.325.825-10.35l-.1-.175zm35.7 8.35c-3.775-7.5-8.675-8.775-11.125-6.825-1.875 1.5-6.2 7.425-6.5 7.825-4.775 6.775-4.5 8.425 1.625 12.275 3.45 2.175 6.225-.625 7.425-1.45-.575 3.575-2.325 9.2-4.95 13.15-1.425-1.075-2.475-1.9-3.125-2.5-.825-.75-2.075-.7-2.875.075a1.865 1.865 0 0 0-.55 1.425c.025.525.25 1.025.625 1.375 6.375 5.825 14.75 9.125 23.675 9.15 8.95 0 17.375-3.3 23.75-9.15.825-.75.85-2 .1-2.8a2.07 2.07 0 0 0-2.875-.075c-.5.45-1 .875-1.525 1.3l-2.8-6.25c-.45-1.075-.95-2.425-1.5-4.05.275-.675.85-1.325 1.675-2.175.6-.6 1.1-1.2 1.475-1.775 1.85-2.925.8-4.65.225-5.8-1.325-2.7-3.4-1.825-4.9-.225-1.85 1.95-3.65 2.8-6.55 3.45-2.425.55-4.3.275-5.85-.7-2.15-1.325-5.45-6.25-5.45-6.25zm69.325-7.625c-8 7.6-.325 24.125-14.875 31.15-1.6.775-2.925-1.775-5.075-1.775-6.075.05-17.675 5.4-18.125 8.1-.375 2.225 4.575 4 19.175 4.025 12.7 0 16.8-19.325 21.25-19.35 4.45 0 2.375 17.525 1.9 19.35h4.65c-.4-1.825-.7-7.325-.675-15.1 0-7.775 1.4-9.5 2.525-15.375.975-5.1-6.575-9.525-10.75-11.025zm45.6.625H197.38c-8.425 0-15.425 6.525-16 14.925l-.95 13.475c-.45 6.4 4.625 11.825 11.025 11.85h24.85c6.425 0 11.475-5.425 11.05-11.85l-.95-13.475c-.6-8.4-7.575-14.925-16-14.925zm-110.65 31c3.925 0 6.925 1.925 8.025 5.5-2.675.7-5.35 1.05-8.075 1.025-4.1 0-5.55-.375-8.175-1.075 1.05-3.15 4.525-5.45 8.225-5.45zm98.225-19.825v.375c0 3.25 2.65 5.925 5.9 5.925s5.925-2.65 5.925-5.925v-.375c0-1.45 5.25-1.45 5.25 0v.375c0 6.15-5 11.15-11.175 11.15-6.15 0-11.15-5-11.15-11.15v-.375c0-1.45 5.225-1.45 5.25 0z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||kwcdn.com^\",\n\t\t\"||temu.com^\",\n\t\t\"||temucdn.com^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"tidal\",\n\tName:    \"Tidal\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 9 12 C 8.7615 12 8.5237969 12.091437 8.3417969 12.273438 L 1.2734375 19.341797 C 0.9094375 19.705797 0.9094375 20.294203 1.2734375 20.658203 L 8.3417969 27.726562 C 8.7057969 28.090563 9.2942031 28.090563 9.6582031 27.726562 L 16.726562 20.658203 C 16.908563 20.476203 17 20.2385 17 20 C 17 19.7615 16.908563 19.523797 16.726562 19.341797 L 9.6582031 12.273438 C 9.4762031 12.091437 9.2385 12 9 12 z M 17 20 C 17 20.2385 17.091438 20.476203 17.273438 20.658203 L 24.341797 27.726562 C 24.523797 27.908563 24.7615 28 25 28 C 25.2385 28 25.476203 27.908563 25.658203 27.726562 L 32.726562 20.658203 C 32.908563 20.476203 33 20.2385 33 20 C 33 19.7615 32.908563 19.523797 32.726562 19.341797 L 25.658203 12.273438 C 25.294203 11.909437 24.705797 11.909437 24.341797 12.273438 L 17.273438 19.341797 C 17.091437 19.523797 17 19.7615 17 20 z M 33 20 C 33 20.2385 33.091437 20.476203 33.273438 20.658203 L 40.341797 27.726562 C 40.705797 28.090563 41.294203 28.090563 41.658203 27.726562 L 48.726562 20.658203 C 49.090563 20.294203 49.090563 19.705797 48.726562 19.341797 L 41.658203 12.273438 C 41.294203 11.909437 40.705797 11.909437 40.341797 12.273438 L 33.273438 19.341797 C 33.091437 19.523797 33 19.7615 33 20 z M 25 28 C 24.7615 28 24.523797 28.091437 24.341797 28.273438 L 17.273438 35.341797 C 16.909437 35.705797 16.909437 36.294203 17.273438 36.658203 L 24.341797 43.726562 C 24.705797 44.090562 25.294203 44.090562 25.658203 43.726562 L 32.726562 36.658203 C 33.090563 36.294203 33.090563 35.705797 32.726562 35.341797 L 25.658203 28.273438 C 25.476203 28.091437 25.2385 28 25 28 z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||tidal.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"tiktok\",\n\tName:    \"TikTok\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M41 4H9C6.243 4 4 6.243 4 9v32c0 2.757 2.243 5 5 5h32c2.757 0 5-2.243 5-5V9c0-2.757-2.243-5-5-5zm-3.994 18.323a7.482 7.482 0 0 1-.69.035 7.492 7.492 0 0 1-6.269-3.388v11.537a8.527 8.527 0 1 1-8.527-8.527c.178 0 .352.016.527.027v4.202c-.175-.021-.347-.053-.527-.053a4.351 4.351 0 1 0 0 8.704c2.404 0 4.527-1.894 4.527-4.298l.042-19.594h4.016a7.488 7.488 0 0 0 6.901 6.685v4.67z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||amemv.com^\",\n\t\t\"||bdurl.com^\",\n\t\t\"||bytecdn.cn^\",\n\t\t\"||bytedance.map.fastly.net^\",\n\t\t\"||bytedapm.com^\",\n\t\t\"||bytegoofy.com^\",\n\t\t\"||byteimg.com^\",\n\t\t\"||byteoversea.com^\",\n\t\t\"||bytescm.com^\",\n\t\t\"||douyin.com^\",\n\t\t\"||douyincdn.com^\",\n\t\t\"||douyinliving.com^\",\n\t\t\"||douyinpic.com^\",\n\t\t\"||douyinstatic.com^\",\n\t\t\"||douyinvod.com^\",\n\t\t\"||huoshan.com^\",\n\t\t\"||huoshanstatic.com^\",\n\t\t\"||huoshanzhibo.com^\",\n\t\t\"||muscdn.com^\",\n\t\t\"||musical.ly^\",\n\t\t\"||p16-tiktok-*.ibyteimg.com^\",\n\t\t\"||p16-tiktokcdn-com.akamaized.net^\",\n\t\t\"||pstatp.com^\",\n\t\t\"||snssdk.com^\",\n\t\t\"||tiktok.com^\",\n\t\t\"||tiktokcdn-us.com^\",\n\t\t\"||tiktokcdn.com^\",\n\t\t\"||tiktokrow-cdn.com^\",\n\t\t\"||tiktokv.com^\",\n\t\t\"||ttlivecdn.com.c.bytefcdn-oversea.com^\",\n\t\t\"||ttlivecdn.com^\",\n\t\t\"||v*.tiktokcdn-eu.com^\",\n\t\t\"||zijieapi.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"tinder\",\n\tName:    \"Tinder\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M25,48C13.225,48,5,39.888,5,28.271c0-6.065,3.922-12.709,9.325-15.797c0.151-0.086,0.322-0.132,0.496-0.132 c0.803,0,1.407,0.547,1.407,1.271c0,1.18,0.456,3.923,1.541,5.738c4.455-1.65,9.074-5.839,7.464-16.308 c-0.008-0.051-0.012-0.102-0.012-0.152c0-0.484,0.217-0.907,0.579-1.132c0.34-0.208,0.764-0.221,1.14-0.034 C31.173,3.808,45,11.892,45,28.407C45,39.394,36.215,48,25,48z M26.052,3.519c0.003,0.001,0.005,0.002,0.008,0.004 C26.057,3.521,26.055,3.52,26.052,3.519z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||gotinder.com^\",\n\t\t\"||tinder.com^\",\n\t\t\"||tindersparks.com^\",\n\t},\n\tGroupID: \"dating\",\n}, {\n\tID:      \"tumblr\",\n\tName:    \"Tumblr\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M40 0H10A10 10 0 0 0 0 10v30a10 10 0 0 0 10 10h30a10 10 0 0 0 10-10V10A10 10 0 0 0 40 0Zm-6 40.24c0 .12-.05.24-.14.32-.12.1-2.85 2.44-9.12 2.44-7.51 0-7.74-8.38-7.74-9.34V23.01L13.43 23a.42.42 0 0 1-.43-.42V18.8c0-.18.1-.34.27-.4.07-.03 6.79-2.64 6.79-8.98 0-.24.2-.43.43-.43h4.09c.24 0 .43.2.43.43L25 17h6.56c.24 0 .43.2.43.45v5.1c0 .24-.19.45-.43.45H25v10.5c0 .25.23 3.27 3.43 3.27a10.3 10.3 0 0 0 4.91-1.39c.14-.08.3-.09.44 0 .13.07.22.21.22.37Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||tumblr.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"twitch\",\n\tName:    \"Twitch\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M4.8 3.2L3.2 6.397V19.2h4v2.403h3.198l2.403-2.403H16l4.8-4.8v-11.2zm14.4 10.402L16.8 16H12l-2.398 2.398V16H6.398V4.8H19.2zm0 0\\\" /><path d=\\\"M15.2 12.8h-1.598V7.2h1.597zm-3.2 0h-1.602V7.2H12zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||ext-twitch.tv^\",\n\t\t\"||jtvnw.net^\",\n\t\t\"||ttvnw.net^\",\n\t\t\"||twitch.tv^\",\n\t\t\"||twitchcdn.net^\",\n\t\t\"||twitchsvc.net^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"twitter\",\n\tName:    \"X (formerly Twitter)\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||ads-twitter.com^\",\n\t\t\"||cms-twdigitalassets.com^\",\n\t\t\"||periscope.tv^\",\n\t\t\"||pscp.tv^\",\n\t\t\"||t.co^\",\n\t\t\"||tellapart.com^\",\n\t\t\"||tweetdeck.com^\",\n\t\t\"||twimg.com^\",\n\t\t\"||twitpic.com^\",\n\t\t\"||twitter.biz^\",\n\t\t\"||twitter.com^\",\n\t\t\"||twitter.jp^\",\n\t\t\"||twittercommunity.com^\",\n\t\t\"||twitterflightschool.com^\",\n\t\t\"||twitterinc.com^\",\n\t\t\"||twitteroauth.com^\",\n\t\t\"||twitterstat.us^\",\n\t\t\"||twtrdns.net^\",\n\t\t\"||twttr.com^\",\n\t\t\"||twttr.net^\",\n\t\t\"||twvid.com^\",\n\t\t\"||vine.co^\",\n\t\t\"||x.com^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"ubisoft\",\n\tName:    \"Ubisoft\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 32 32\\\"><path d=\\\"M15.22 3C7.14 3 3.66 10.18 3.66 10.18l1.03.74s-1.3 2.45-1.26 5.6A12.5 12.5 0 0 0 16.08 29a12.5 12.5 0 0 0 12.49-12.46c0-9-6.98-13.54-13.35-13.54zm.07 2.2c6.3 0 11.2 5.07 11.2 10.98 0 6.27-4.71 10.62-10.2 10.62-4.04 0-7.69-3.08-7.69-7.3a5.8 5.8 0 0 1 2.75-5.03l.21.23a6.37 6.37 0 0 0-1.53 3.91c0 3.32 2.6 5.62 5.88 5.62 4.18 0 6.97-3.56 6.97-7.7 0-4.81-4.25-8.9-9.36-8.9a11.1 11.1 0 0 0-6.61 2.3l-.21-.2a10.07 10.07 0 0 1 8.59-4.54zM13.4 9.8c3.26 0 6.44 2.15 7.24 5.22l-.3.1a8.35 8.35 0 0 0-6.52-3.44c-5.08 0-7.75 4.62-7.36 8.47l-.3.12s-.56-1.24-.56-2.71a7.8 7.8 0 0 1 7.8-7.76zm2.15 5.33a2.77 2.77 0 0 1 2.78 2.74c0 1.23-.79 1.96-.79 1.96l.94.65s-.93 1.46-2.82 1.46a3.4 3.4 0 0 1-.1-6.8z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||ubi.com^\",\n\t\t\"||ubisoft.com^\",\n\t\t\"||ubisoft.org^\",\n\t\t\"||ubisoftconnect.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"valorant\",\n\tName:    \"Valorant\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M4 6a1 1 0 0 0-1 1v18a1 1 0 0 0 .2.6l14 17a1 1 0 0 0 .8.4h14a1 1 0 0 0 .8-1.6l-28-35A1 1 0 0 0 4 6zm42 1a1 1 0 0 0-.8.4l-18 22A1 1 0 0 0 28 31h14a1 1 0 0 0 .8-.4l4-5a1 1 0 0 0 .2-.6V8a1 1 0 0 0-1-1zM5 9.9 30 41H18.4L5 24.6V10zm40 .9v13.8L41.5 29H30.1L45 10.8z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||playvalorant.com\",\n\t\t\"||valorant.scd.riotcdn.net\",\n\t\t\"||valorant.secure.dyn.riotcdn.net\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"viber\",\n\tName:    \"Viber\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 44.78125 13.15625 C 44 10.367188 42.453125 8.164063 40.1875 6.605469 C 37.328125 4.632813 34.039063 3.9375 31.199219 3.511719 C 27.269531 2.925781 23.710938 2.84375 20.316406 3.257813 C 17.136719 3.648438 14.742188 4.269531 12.558594 5.273438 C 8.277344 7.242188 5.707031 10.425781 4.921875 14.734375 C 4.539063 16.828125 4.28125 18.71875 4.132813 20.523438 C 3.789063 24.695313 4.101563 28.386719 5.085938 31.808594 C 6.046875 35.144531 7.722656 37.527344 10.210938 39.09375 C 10.84375 39.492188 11.65625 39.78125 12.441406 40.058594 C 12.886719 40.214844 13.320313 40.367188 13.675781 40.535156 C 14.003906 40.6875 14.003906 40.714844 14 40.988281 C 13.972656 43.359375 14 48.007813 14 48.007813 L 14.007813 49 L 15.789063 49 L 16.078125 48.71875 C 16.269531 48.539063 20.683594 44.273438 22.257813 42.554688 L 22.472656 42.316406 C 22.742188 42.003906 22.742188 42.003906 23.019531 42 C 25.144531 41.957031 27.316406 41.875 29.472656 41.757813 C 32.085938 41.617188 35.113281 41.363281 37.964844 40.175781 C 40.574219 39.085938 42.480469 37.355469 43.625 35.035156 C 44.820313 32.613281 45.527344 29.992188 45.792969 27.019531 C 46.261719 21.792969 45.929688 17.257813 44.78125 13.15625 Z M 35.382813 33.480469 C 34.726563 34.546875 33.75 35.289063 32.597656 35.769531 C 31.753906 36.121094 30.894531 36.046875 30.0625 35.695313 C 23.097656 32.746094 17.632813 28.101563 14.023438 21.421875 C 13.277344 20.046875 12.761719 18.546875 12.167969 17.09375 C 12.046875 16.796875 12.054688 16.445313 12 16.117188 C 12.050781 13.769531 13.851563 12.445313 15.671875 12.046875 C 16.367188 11.890625 16.984375 12.136719 17.5 12.632813 C 18.929688 13.992188 20.058594 15.574219 20.910156 17.347656 C 21.28125 18.125 21.113281 18.8125 20.480469 19.390625 C 20.347656 19.511719 20.210938 19.621094 20.066406 19.730469 C 18.621094 20.816406 18.410156 21.640625 19.179688 23.277344 C 20.492188 26.0625 22.671875 27.933594 25.488281 29.09375 C 26.230469 29.398438 26.929688 29.246094 27.496094 28.644531 C 27.574219 28.566406 27.660156 28.488281 27.714844 28.394531 C 28.824219 26.542969 30.4375 26.726563 31.925781 27.78125 C 32.902344 28.476563 33.851563 29.210938 34.816406 29.917969 C 36.289063 31 36.277344 32.015625 35.382813 33.480469 Z M 26.144531 15 C 25.816406 15 25.488281 15.027344 25.164063 15.082031 C 24.617188 15.171875 24.105469 14.804688 24.011719 14.257813 C 23.921875 13.714844 24.289063 13.199219 24.835938 13.109375 C 25.265625 13.035156 25.707031 13 26.144531 13 C 30.476563 13 34 16.523438 34 20.855469 C 34 21.296875 33.964844 21.738281 33.890625 22.164063 C 33.808594 22.652344 33.386719 23 32.90625 23 C 32.851563 23 32.796875 22.996094 32.738281 22.984375 C 32.195313 22.894531 31.828125 22.378906 31.917969 21.835938 C 31.972656 21.515625 32 21.1875 32 20.855469 C 32 17.628906 29.371094 15 26.144531 15 Z M 31 21 C 31 21.550781 30.550781 22 30 22 C 29.449219 22 29 21.550781 29 21 C 29 19.347656 27.652344 18 26 18 C 25.449219 18 25 17.550781 25 17 C 25 16.449219 25.449219 16 26 16 C 28.757813 16 31 18.242188 31 21 Z M 36.710938 23.222656 C 36.605469 23.6875 36.191406 24 35.734375 24 C 35.660156 24 35.585938 23.992188 35.511719 23.976563 C 34.972656 23.851563 34.636719 23.316406 34.757813 22.777344 C 34.902344 22.140625 34.976563 21.480469 34.976563 20.816406 C 34.976563 15.957031 31.019531 12 26.160156 12 C 25.496094 12 24.835938 12.074219 24.199219 12.21875 C 23.660156 12.34375 23.125 12.003906 23.003906 11.464844 C 22.878906 10.925781 23.21875 10.390625 23.757813 10.269531 C 24.539063 10.089844 25.347656 10 26.160156 10 C 32.125 10 36.976563 14.851563 36.976563 20.816406 C 36.976563 21.628906 36.886719 22.4375 36.710938 23.222656 Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||viber.com^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"vimeo\",\n\tName:    \"Vimeo\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 41 5 C 34.210938 4.992188 30.46875 8.796875 28.167969 16.210938 C 29.371094 15.765625 30.578125 15.214844 31.671875 15.214844 C 33.972656 15.214844 34.738281 16.070313 34.410156 18.726563 C 34.300781 20.386719 33.644531 23.066406 31.671875 26.164063 C 29.699219 29.152344 27.984375 30 27 30 C 25.796875 30 24.882813 28.269531 23.898438 23.621094 C 23.570313 22.292969 22.804688 19.304688 21.925781 13.664063 C 21.160156 8.464844 18.613281 5.667969 15 6 C 13.46875 6.109375 11.636719 7.535156 8.570313 10.191406 C 6.378906 12.183594 4.300781 13.621094 2 15.613281 L 4.191406 18.421875 C 6.269531 16.984375 7.476563 16.429688 7.804688 16.429688 C 9.335938 16.429688 10.757813 18.863281 12.183594 23.84375 C 13.386719 28.378906 14.699219 32.914063 15.90625 37.449219 C 17.765625 42.429688 20.066406 44.863281 22.695313 44.863281 C 27.074219 44.863281 32.328125 40.882813 38.570313 32.695313 C 44.699219 24.949219 47.78125 18.535156 48 14 C 48.21875 8.027344 45.816406 5.109375 41 5 Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"*vod-adaptive.akamaized.net^\",\n\t\t\"||livestream.com^\",\n\t\t\"||vhx.tv^\",\n\t\t\"||vhxqa1.com^\",\n\t\t\"||vhxqa2.com^\",\n\t\t\"||vhxqa3.com^\",\n\t\t\"||vhxqa4.com^\",\n\t\t\"||vhxqa6.com^\",\n\t\t\"||vimeo-staging.com^\",\n\t\t\"||vimeo-staging2.com^\",\n\t\t\"||vimeo.com^\",\n\t\t\"||vimeo.fr^\",\n\t\t\"||vimeobusiness.com^\",\n\t\t\"||vimeocdn.com^\",\n\t\t\"||vimeogoods.com^\",\n\t\t\"||vimeoondemand.com^\",\n\t\t\"||vimeostatus.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"vivo_play\",\n\tName:    \"Vivo Play\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"-36 0 263 263\\\"><path d=\\\"M86 1Q72 4 60 15a48 48 0 0 0-10 52q7 15 22 25 8 6 1 12c-2 1-5 1-27-4-22-6-25-6-29-5-9 2-12 6-15 20-2 8-2 10-1 14q3 7 8 11l24 6 25 7q6 4 6 11c0 4-1 6-25 36l-22 28q-5 9 0 18c3 5 15 15 20 16q8 2 14-2l21-23 19-24c6-4 9-1 32 28 16 19 19 21 27 21 6 0 8-1 17-8q14-11 9-24l-23-30c-24-30-26-32-26-37q1-6 6-10l24-7 26-7q6-4 8-12c0-4-3-21-6-25q-6-7-14-8l-24 5c-28 7-27 6-29 5q-3-2-3-6t5-6q13-9 18-19c13-25 4-56-21-68q-15-7-31-4\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||vivoplay.com.br^\",\n\t\t\"||vivoplay.net^\",\n\t\t\"||vivotv.com.br^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"vk\",\n\tName:    \"VK.com\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M12 .96C5.914.96.96 5.915.96 12c0 6.086 4.954 11.04 11.04 11.04 6.086 0 11.04-4.954 11.04-11.04C23.04 5.914 18.085.96 12 .96zm4.785 13.216c1.074.953 1.3 1.293 1.336 1.351.445.707-.492.793-.492.793h-1.98s-.481.004-.891-.27c-.672-.437-1.375-1.288-1.867-1.14-.414.125-.41.684-.41 1.16 0 .172-.149.25-.481.25h-.617c-1.086 0-2.262-.363-3.434-1.59-1.656-1.734-3.113-5.222-3.113-5.222s-.086-.176.008-.281c.105-.122.394-.106.394-.106h1.918s.18.031.309.125c.11.074.168.219.168.219s.32 1.062.734 1.742c.801 1.32 1.172 1.355 1.445 1.215.399-.207.266-1.617.266-1.617s.02-.602-.187-.871c-.16-.211-.465-.32-.598-.336-.11-.016.07-.203.3-.313.31-.137.727-.172 1.446-.164.563.004.723.04.941.09.665.152.5.555.5 1.969 0 .453-.062 1.09.278 1.3.148.09.652.204 1.55-1.257.43-.692.77-1.84.77-1.84s.067-.125.176-.188c.113-.066.11-.062.262-.062.152 0 1.683-.012 2.02-.012.335 0 .651-.004.702.191.078.282-.246 1.25-1.07 2.305-1.355 1.723-1.504 1.563-.383 2.559zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||mvk.com^\",\n\t\t\"||userapi.com^\",\n\t\t\"||vk-cdn.me^\",\n\t\t\"||vk-cdn.net^\",\n\t\t\"||vk-portal.net^\",\n\t\t\"||vk.cc^\",\n\t\t\"||vk.com^\",\n\t\t\"||vk.design^\",\n\t\t\"||vk.link^\",\n\t\t\"||vk.me^\",\n\t\t\"||vkcache.com^\",\n\t\t\"||vkgo.app^\",\n\t\t\"||vklive.app^\",\n\t\t\"||vkmessenger.app^\",\n\t\t\"||vkmessenger.com^\",\n\t\t\"||vkontakte.ru^\",\n\t\t\"||vkuseraudio.com^\",\n\t\t\"||vkuserlive.net^\",\n\t\t\"||vkuservideo.com^\",\n\t\t\"||vkuservideo.net^\",\n\t},\n\tGroupID: \"social_network\",\n}, {\n\tID:      \"voot\",\n\tName:    \"Voot\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 512 512\\\"><path d=\\\"M96 340c-1 4-4 6-7 6H48c-3 0-6-2-8-6L0 213c-1-2 0-5 2-5l2-1h30c4 0 7 3 8 6l25 87c1 3 2 3 3 0l25-87c1-3 4-6 7-6h31c2 0 4 2 4 4v2L96 340zm46-50v-32c0-29 14-56 63-56s63 27 63 56v32c0 28-14 56-63 56s-63-28-63-56zm85 1v-35c0-13-7-20-22-20s-22 7-22 20v35c0 13 7 20 22 20s22-7 22-20zm54-1v-32c0-29 14-56 63-56s63 27 63 56v32c0 28-14 56-63 56s-63-28-63-56zm85 1v-35c0-13-7-20-22-20s-21 7-21 20v35c0 13 6 20 21 20s22-7 22-20zm144 44-2-17-1-2c-1-3-3-5-6-5h-2l-10 1c-2 1-4 0-6-2l-2-11v-56c0-3 2-6 6-6h17c4 0 6-2 7-5l1-22c0-3-2-5-5-6h-21c-3 0-5-2-5-5v-28c0-3-2-5-5-5h-1l-30 4c-3 1-5 4-5 7v22c0 3-3 5-6 5h-7c-4 0-6 3-6 6v22c0 3 2 5 6 5h7c3 0 6 3 6 6v67c0 26 15 36 42 36 8 0 16-1 23-4h1c2 0 5-3 5-6l-1-1z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||voot.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"wargaming\",\n\tName:    \"Wargaming\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M12 1.998c-5.52 0-10 4.481-10 9.988 0 5.52 4.48 9.996 10 9.996s10-4.476 10-9.996c0-5.507-4.48-9.988-10-9.988zm0 2c4.413 0 8 3.588 8 7.988 0 3.246-1.944 6.04-4.727 7.293.54-1.861.831-3.988.807-6.226l1.414.414a23.648 23.648 0 0 0-2-4.041c-.627 1.347-1.48 2.56-2.52 3.68l1.68-.133c-1.507 2.92-3.134 3.906-5.547 4.013-.386-4.213.12-7.014 2.827-9.04l.386 1.493c.653-.974 1.36-2.12 2.373-2.947-1.506-.6-2.999-.627-4.492-.334.386.16.76.588 1.014.828-3.485 1.662-5.643 4.202-6.744 7.68A7.95 7.95 0 0 1 4 11.986c0-4.4 3.587-7.988 8-7.988z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||wargaming.com^\",\n\t\t\"||wargaming.net^\",\n\t\t\"||wgcdn.co^\",\n\t\t\"||wgcrowd.io^\",\n\t\t\"||worldoftanks.com^\",\n\t\t\"||worldofwarplanes.com^\",\n\t\t\"||worldofwarships.eu^\",\n\t\t\"||wotblitz.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"warnerbrosgames\",\n\tName:    \"Warner Bros. Games\",\n\tIconSVG: []byte(\"<svg fill=\\\"currentColor\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 40 40\\\"><path d=\\\"M36.9 10V8l-2.6-1.4v-2L33 4Q27.3 1.3 20.3.4h-.6Q12.5 1.3 7 4l-1.3.6v2L3 8v2L.3 11.5.5 13q1.2 8 5.9 14.4Q11 33.8 19 37.8l1 .5 1-.5q8-3.9 12.6-10.3 4.8-6.3 5.9-14.4l.2-1.6zm.3 2.7c-.3 1.8-1.2 7.7-5.4 13.4-4.4 5.9-10 8.8-11.8 9.6-1.7-.8-7.4-3.7-11.8-9.6-4.3-5.7-5-11.6-5.4-13.4l2.6-1.3v-2L8 8V6.1q5.3-2.5 12-3.4 6.7.9 12 3.4v2l2.6 1.3v2z\\\"/><path d=\\\"M16.2 7.3V23a49 49 0 0 1-3-14.6l-2.6 1.4q.5 7 2.7 12.9l-.9.5q-3.2-4.7-4.6-11.3l-2 2a28 28 0 0 0 6.3 12.7l2.3-1.4a26 26 0 0 0 4.4 6.5V7zm14.7 8.1q1.2-1.4 1-2.7-.1-2-2.5-3.4-1.4-1-4.5-1.9L21.2 7v24.8c5.2-2.7 11.4-10 11.5-13.8q0-1.6-1.8-2.5m-7.1-5.6c1.3 0 5.4 1.3 5.4 3q.2 1-2.1 2.3L23.8 17zM27 23.6q-1.2 1.5-3.2 3.2v-7.2l4.9-2.7c1.4.3 1.3 1.4 1.3 1.5 0 1.3-1.3 3.3-3 5.2\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||warnerbrosgames.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"wechat\",\n\tName:    \"WeChat\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 19 6 C 9.625 6 2 12.503906 2 20.5 C 2 24.769531 4.058594 28.609375 7.816406 31.390625 L 5.179688 39.304688 L 13.425781 34.199219 C 15.714844 34.917969 18.507813 35.171875 21.203125 34.875 C 23.390625 39.109375 28.332031 42 34 42 C 35.722656 42 37.316406 41.675781 38.796875 41.234375 L 45.644531 45.066406 L 43.734375 38.515625 C 46.3125 36.375 48 33.394531 48 30 C 48 23.789063 42.597656 18.835938 35.75 18.105469 C 34.40625 11.152344 27.367188 6 19 6 Z M 13 14 C 14.101563 14 15 14.898438 15 16 C 15 17.101563 14.101563 18 13 18 C 11.898438 18 11 17.101563 11 16 C 11 14.898438 11.898438 14 13 14 Z M 25 14 C 26.101563 14 27 14.898438 27 16 C 27 17.101563 26.101563 18 25 18 C 23.898438 18 23 17.101563 23 16 C 23 14.898438 23.898438 14 25 14 Z M 34 20 C 40.746094 20 46 24.535156 46 30 C 46 32.957031 44.492188 35.550781 42.003906 37.394531 L 41.445313 37.8125 L 42.355469 40.933594 L 39.105469 39.109375 L 38.683594 39.25 C 37.285156 39.71875 35.6875 40 34 40 C 27.253906 40 22 35.464844 22 30 C 22 24.535156 27.253906 20 34 20 Z M 29.5 26 C 28.699219 26 28 26.699219 28 27.5 C 28 28.300781 28.699219 29 29.5 29 C 30.300781 29 31 28.300781 31 27.5 C 31 26.699219 30.300781 26 29.5 26 Z M 38.5 26 C 37.699219 26 37 26.699219 37 27.5 C 37 28.300781 37.699219 29 38.5 29 C 39.300781 29 40 28.300781 40 27.5 C 40 26.699219 39.300781 26 38.5 26 Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||wechat.com^\",\n\t\t\"||weixin.qq.com.cn^\",\n\t\t\"||weixin.qq.com^\",\n\t\t\"||weixinbridge.com^\",\n\t\t\"||wx.qq.com^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"weibo\",\n\tName:    \"Weibo\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M 35 6 C 34.222656 6 33.472656 6.078125 32.75 6.207031 C 32.207031 6.300781 31.84375 6.820313 31.9375 7.363281 C 32.03125 7.910156 32.550781 8.273438 33.09375 8.179688 C 33.726563 8.066406 34.359375 8 35 8 C 41.085938 8 46 12.914063 46 19 C 46 20.316406 45.757813 21.574219 45.328125 22.753906 C 45.195313 23.09375 45.253906 23.476563 45.484375 23.757813 C 45.71875 24.039063 46.082031 24.171875 46.441406 24.105469 C 46.800781 24.039063 47.09375 23.78125 47.207031 23.4375 C 47.710938 22.054688 48 20.566406 48 19 C 48 11.832031 42.167969 6 35 6 Z M 35 12 C 34.574219 12 34.171875 12.042969 33.789063 12.109375 C 33.246094 12.207031 32.878906 12.722656 32.976563 13.269531 C 33.070313 13.8125 33.589844 14.175781 34.132813 14.082031 C 34.425781 14.03125 34.714844 14 35 14 C 37.773438 14 40 16.226563 40 19 C 40 19.597656 39.890625 20.167969 39.691406 20.707031 C 39.503906 21.226563 39.773438 21.800781 40.292969 21.988281 C 40.8125 22.175781 41.386719 21.910156 41.574219 21.390625 C 41.84375 20.648438 42 19.84375 42 19 C 42 15.144531 38.855469 12 35 12 Z M 21.175781 12.40625 C 17.964844 12.34375 13.121094 14.878906 8.804688 19.113281 C 4.511719 23.40625 2 27.90625 2 31.78125 C 2 39.3125 11.628906 43.8125 21.152344 43.8125 C 33.5 43.8125 41.765625 36.699219 41.765625 31.046875 C 41.765625 27.59375 38.835938 25.707031 36.21875 24.871094 C 35.59375 24.660156 35.175781 24.558594 35.488281 23.71875 C 35.695313 23.21875 36 22.265625 36 21 C 36 19.5625 35 18.316406 33 18.09375 C 32.007813 17.984375 28 18 25.339844 19.113281 C 25.339844 19.113281 23.871094 19.746094 24.289063 18.59375 C 25.023438 16.292969 24.917969 14.40625 23.765625 13.359375 C 23.140625 12.730469 22.25 12.425781 21.175781 12.40625 Z M 20.3125 23.933594 C 28.117188 23.933594 34.441406 27.914063 34.441406 32.828125 C 34.441406 37.738281 28.117188 41.71875 20.3125 41.71875 C 12.511719 41.71875 6.1875 37.738281 6.1875 32.828125 C 6.1875 27.914063 12.511719 23.933594 20.3125 23.933594 Z M 19.265625 26.023438 C 16.246094 26.046875 13.3125 27.699219 12.039063 30.246094 C 10.46875 33.484375 11.933594 37.042969 15.699219 38.191406 C 19.464844 39.445313 23.960938 37.5625 25.53125 34.113281 C 27.097656 30.769531 25.113281 27.214844 21.347656 26.277344 C 20.660156 26.097656 19.960938 26.019531 19.265625 26.023438 Z M 20.824219 30.25 C 21.402344 30.25 21.871094 30.714844 21.871094 31.292969 C 21.871094 31.871094 21.402344 32.339844 20.824219 32.339844 C 20.246094 32.339844 19.777344 31.871094 19.777344 31.292969 C 19.777344 30.714844 20.246094 30.25 20.824219 30.25 Z M 16.417969 31.292969 C 16.746094 31.296875 17.074219 31.347656 17.382813 31.453125 C 18.722656 31.878906 19.132813 33.148438 18.308594 34.207031 C 17.589844 35.265625 15.945313 35.792969 14.707031 35.265625 C 13.476563 34.738281 13.167969 33.464844 13.886719 32.515625 C 14.425781 31.71875 15.429688 31.28125 16.417969 31.292969 Z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||wbimg.cn^\",\n\t\t\"||wbimg.com^\",\n\t\t\"||wcdn.cn^\",\n\t\t\"||weibo.cn^\",\n\t\t\"||weibo.com.cn^\",\n\t\t\"||weibo.com^\",\n\t\t\"||weibocdn.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"whatsapp\",\n\tName:    \"WhatsApp\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M3.836 16.668l-1.352 4.934 5.047-1.329zm0 0\\\" /><path d=\\\"M12 2.398C6.7 2.398 2.398 6.7 2.398 12c0 5.3 4.301 9.602 9.602 9.602 5.3 0 9.602-4.301 9.602-9.602 0-5.3-4.301-9.602-9.602-9.602zm4.738 12.915c-.195.554-1.168 1.093-1.601 1.128-.442.043-.852.2-2.856-.59-2.418-.953-3.945-3.433-4.062-3.593-.121-.156-.969-1.285-.969-2.453 0-1.172.613-1.746.828-1.985a.875.875 0 0 1 .637-.297c.156 0 .316 0 .453.004.172.004.36.016.535.41.215.47.676 1.645.735 1.766.058.117.101.262.019.418-.078.156-.121.254-.234.399-.121.136-.25.308-.36.41-.117.12-.242.25-.101.488.136.238.613 1.016 1.32 1.645.906.812 1.672 1.062 1.91 1.18.238.12.38.1.516-.06.14-.156.594-.69.754-.93.156-.237.316-.198.531-.12.219.078 1.39.656 1.629.773.238.121.394.18.453.278.063.097.063.574-.137 1.129zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||wa.me^\",\n\t\t\"||whatsapp-plus.info^\",\n\t\t\"||whatsapp-plus.me^\",\n\t\t\"||whatsapp-plus.net^\",\n\t\t\"||whatsapp.cc^\",\n\t\t\"||whatsapp.com^\",\n\t\t\"||whatsapp.info^\",\n\t\t\"||whatsapp.net^\",\n\t\t\"||whatsapp.org^\",\n\t\t\"||whatsapp.tv^\",\n\t\t\"||whatsappbrand.com^\",\n\t},\n\tGroupID: \"messenger\",\n}, {\n\tID:      \"wizz\",\n\tName:    \"Wizz\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 -297 867 867\\\"><path d=\\\"M94.66 31.7c0-11.49-7.72-21.78-17.23-23.05Q47.26 4.74 17.18.16C7.7-1.34 0 7.77 0 20.57v207.77c0 25.69 15.38 43.88 34.38 40.93q44.73-6.52 89.6-11.57c17.66-2.09 32.49-18.81 34.34-38.88q4.86-52.95 9.73-105.04c.54-5.82 8.04-5.63 8.59.16q4.86 51.9 9.73 103.01c1.85 19.55 16.69 33.01 34.4 31.58q44.97-3.47 90.02-5.46a36.76 36.76 0 0 0 34.66-36.57V47.86a18.27 18.27 0 0 0-17.33-18.23q-30.32-1.15-60.63-2.97c-9.56-.59-17.31 7.13-17.31 17.32v112.25c0 5.52-7.5 6.48-8.57 1.06q-10.13-51.32-20.26-104c-3.38-17.49-17.5-31.07-33.85-32.59q-15.16-1.35-30.32-2.87c-16.34-1.7-30.42 10.04-33.79 28.63q-10.1 55.6-20.2 113.34c-1.06 6.1-8.52 5.4-8.52-.87Zm297.01-.38c-12.77-.2-23.11 7.4-23.12 17.07V223.6c0 9.69 10.35 17.28 23.12 17.08q41.4-.63 82.8 0c12.77.2 23.11-7.4 23.11-17.08V48.4c0-9.69-10.34-17.28-23.11-17.08q-41.4.62-82.8 0ZM520.7 47.87a18.28 18.28 0 0 1 17.33-18.23q64.98-2.47 129.85-8c9.55-.84 17.3 7.09 17.3 17.78v65.23c0 18.33-11.46 34.12-27.47 37.67q-27.56 6.23-55.16 12.04c-5.08 1.07-4.31 8.95.9 9.04q32.23.5 64.44 1.2c9.55.17 17.29 9 17.29 19.7v48.28c0 10.7-7.74 18.62-17.3 17.78q-64.84-5.53-129.84-8a18.27 18.27 0 0 1-17.33-18.23V164.9a35.26 35.26 0 0 1 27.86-34.62q29.6-6.15 59.18-12.68c5.12-1.14 4.38-9.15-.85-9.06q-34.42.55-68.86.87a17.3 17.3 0 0 1-17.33-17.48Zm181.75-9.81c0-10.8 7.74-20.32 17.28-21.34Q784.49 9.97 848.97.16c9.5-1.49 17.18 7.62 17.18 20.41v77.96c0 21.94-11.38 40.83-27.3 44.95q-27.4 7.28-54.87 13.76c-5.06 1.2-4.29 10.3.9 10.47q32.08 1.03 64.1 2.25c9.49.31 17.18 10.96 17.18 23.75v57.72c0 12.8-7.7 21.9-17.19 20.41q-64.46-9.8-129.24-16.56c-9.54-1.03-17.27-10.55-17.27-21.34v-65.82c0-18.61 11.63-34.66 27.77-38.57q29.49-7.02 58.92-14.88c5.1-1.37 4.36-10.67-.85-10.49q-34.25 1.11-68.57 2c-9.54.22-17.27-8.36-17.27-19.15Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||getwizz.io^\",\n\t\t\"||wizz.chat^\",\n\t\t\"||wizzapp.com^\",\n\t},\n\tGroupID: \"dating\",\n}, {\n\tID:      \"xboxlive\",\n\tName:    \"Xbox Live\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 84 84\\\" fill=\\\"currentColor\\\"><path d=\\\"M42.44 34.08c12.8 9.73 34.5 33.65 27.94 40.42a42.24 42.24 0 0 1-27.94 10.48A42.03 42.03 0 0 1 14.5 74.5c-6.67-6.77 15.13-30.69 27.83-40.32 0-.1.1-.1.1-.1ZM64.56 6.24A41.32 41.32 0 0 0 42.43 0a41.32 41.32 0 0 0-22.11 6.24c-.1 0-.1.1-.1.21s.1.11.2.11c8.26-1.8 20.75 5.3 21.91 6.03h.21c1.17-.74 13.65-7.83 21.9-6.03.12 0 .22 0 .22-.1s0-.22-.1-.22ZM12.7 12.17c-.1 0-.1.1-.21.1a42.56 42.56 0 0 0-3.81 55.88c0 .11.1.11.2.11s.11-.1 0-.21C5.62 57.99 22.23 33.75 30.8 23.6l.11-.1c0-.11 0-.11-.1-.11-13.02-12.91-17.36-11.54-18.1-11.22Zm41.38 11.11-.1.1s0 .11.1.11c8.57 10.16 25.08 34.4 21.9 44.45v.22c.11 0 .22 0 .22-.11a42.53 42.53 0 0 0 8.67-25.72 42.21 42.21 0 0 0-12.59-30.16c-.1-.1-.1-.1-.21-.1-.64-.22-4.97-1.6-18 11.21Z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||gamepass.com^\",\n\t\t\"||xbox-global.ifs.windows.com^\",\n\t\t\"||xbox-guide-public.rec.mp.microsoft.com^\",\n\t\t\"||xbox.ipv6.microsoft.com^\",\n\t\t\"||xboxab.com^\",\n\t\t\"||xboxab.net^\",\n\t\t\"||xboxlive.com^\",\n\t\t\"||xboxservices.com^\",\n\t},\n\tGroupID: \"gaming\",\n}, {\n\tID:      \"xiaohongshu\",\n\tName:    \"Xiaohongshu\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 50 50\\\"><path d=\\\"M35 22v2h1v-2h-1zm0 0v2h1v-2h-1zm9-18H6c-1.09 0-2 .91-2 2v38c0 1.09.91 2 2 2h38c1.09 0 2-.91 2-2V6c0-1.09-.91-2-2-2zM12 24c0 1.38-.19 5.89-2.61 6.24l-.28-1.98c.39-.19.89-2.14.89-4.26v-2h2v2zm3 6h-2V19h2v11zm2.29-.29c-1.2-1.2-1.29-4.73-1.29-5.78V22h2v1.93c0 1.91.34 3.99.71 4.36l-1.42 1.42zM22 31h-3l1-2h3l-1 2zm9 0h-7l1-2h2v-7h-2l-2.1 4.38h1.72l-1 2H21a1 1 0 0 1-.82-1.57L22 24h-2a1 1 0 0 1-.86-1.51l3-5 1.72 1.02L21.77 22H25v-2h6v2h-2v7h2v2zm9-2.5a2.5 2.5 0 0 1-2.5 2.5c-1.21 0-1.22-.86-1.45-2H38v-3h-3v5h-2v-5h-2v-2h2v-2h-1v-2h1v-1h2v1h1a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2.5zm0-6.5h-1v-1c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1zm-5 2h1v-2h-1v2zm0-2v2h1v-2h-1z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||xhscdn.com^\",\n\t\t\"||xhscdn.net^\",\n\t\t\"||xiaohongshu.com.my^\",\n\t\t\"||xiaohongshu.com^\",\n\t\t\"||xiaohongshu.net^\",\n\t},\n\tGroupID: \"shopping\",\n}, {\n\tID:      \"youtube\",\n\tName:    \"YouTube\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M19.695 4.04S15.348 3.2 12 3.2s-7.695.84-7.695.84L1.602 7.2v9.6l2.703 3.16s4.347.84 7.695.84 7.695-.84 7.695-.84l2.703-3.16V12 7.2zM9.602 15.68V8.32L16 12zm0 0\\\" /><path d=\\\"M19.2 4a3.198 3.198 0 1 0 0 6.398c1.769 0 3.198-1.43 3.198-3.199C22.398 5.434 20.968 4 19.2 4zm0 9.602a3.198 3.198 0 1 0 0 6.398c1.769 0 3.198-1.434 3.198-3.2 0-1.769-1.43-3.198-3.199-3.198zM1.601 7.199c0 1.77 1.43 3.2 3.199 3.2 1.765 0 2.398-1.43 2.398-3.2C7.2 5.434 6.566 4 4.801 4 3.03 4 1.6 5.434 1.6 7.2zM4.8 13.602c-1.77 0-3.2 1.43-3.2 3.199A3.198 3.198 0 1 0 8 16.8c0-1.77-1.434-3.2-3.2-3.2zm0 0\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||ggpht.cn^\",\n\t\t\"||ggpht.com^\",\n\t\t\"||googlevideo.com^\",\n\t\t\"||wide-youtube.l.google.com^\",\n\t\t\"||withyoutube.com^\",\n\t\t\"||youtu.be^\",\n\t\t\"||youtube-nocookie.com^\",\n\t\t\"||youtube-ui.l.google.com^\",\n\t\t\"||youtube.ae^\",\n\t\t\"||youtube.al^\",\n\t\t\"||youtube.am^\",\n\t\t\"||youtube.at^\",\n\t\t\"||youtube.az^\",\n\t\t\"||youtube.ba^\",\n\t\t\"||youtube.be^\",\n\t\t\"||youtube.bg^\",\n\t\t\"||youtube.bh^\",\n\t\t\"||youtube.bo^\",\n\t\t\"||youtube.by^\",\n\t\t\"||youtube.ca^\",\n\t\t\"||youtube.cat^\",\n\t\t\"||youtube.ch^\",\n\t\t\"||youtube.cl^\",\n\t\t\"||youtube.co.ae^\",\n\t\t\"||youtube.co.at^\",\n\t\t\"||youtube.co.cr^\",\n\t\t\"||youtube.co.hu^\",\n\t\t\"||youtube.co.id^\",\n\t\t\"||youtube.co.il^\",\n\t\t\"||youtube.co.in^\",\n\t\t\"||youtube.co.jp^\",\n\t\t\"||youtube.co.ke^\",\n\t\t\"||youtube.co.kr^\",\n\t\t\"||youtube.co.ma^\",\n\t\t\"||youtube.co.nz^\",\n\t\t\"||youtube.co.th^\",\n\t\t\"||youtube.co.tz^\",\n\t\t\"||youtube.co.ug^\",\n\t\t\"||youtube.co.uk^\",\n\t\t\"||youtube.co.ve^\",\n\t\t\"||youtube.co.za^\",\n\t\t\"||youtube.co.zw^\",\n\t\t\"||youtube.co^\",\n\t\t\"||youtube.com.ar^\",\n\t\t\"||youtube.com.au^\",\n\t\t\"||youtube.com.az^\",\n\t\t\"||youtube.com.bd^\",\n\t\t\"||youtube.com.bh^\",\n\t\t\"||youtube.com.bo^\",\n\t\t\"||youtube.com.br^\",\n\t\t\"||youtube.com.by^\",\n\t\t\"||youtube.com.co^\",\n\t\t\"||youtube.com.do^\",\n\t\t\"||youtube.com.ec^\",\n\t\t\"||youtube.com.ee^\",\n\t\t\"||youtube.com.eg^\",\n\t\t\"||youtube.com.es^\",\n\t\t\"||youtube.com.gh^\",\n\t\t\"||youtube.com.gr^\",\n\t\t\"||youtube.com.gt^\",\n\t\t\"||youtube.com.hk^\",\n\t\t\"||youtube.com.hn^\",\n\t\t\"||youtube.com.hr^\",\n\t\t\"||youtube.com.jm^\",\n\t\t\"||youtube.com.jo^\",\n\t\t\"||youtube.com.kw^\",\n\t\t\"||youtube.com.lb^\",\n\t\t\"||youtube.com.lv^\",\n\t\t\"||youtube.com.ly^\",\n\t\t\"||youtube.com.mk^\",\n\t\t\"||youtube.com.mt^\",\n\t\t\"||youtube.com.mx^\",\n\t\t\"||youtube.com.my^\",\n\t\t\"||youtube.com.ng^\",\n\t\t\"||youtube.com.ni^\",\n\t\t\"||youtube.com.om^\",\n\t\t\"||youtube.com.pa^\",\n\t\t\"||youtube.com.pe^\",\n\t\t\"||youtube.com.ph^\",\n\t\t\"||youtube.com.pk^\",\n\t\t\"||youtube.com.pt^\",\n\t\t\"||youtube.com.py^\",\n\t\t\"||youtube.com.qa^\",\n\t\t\"||youtube.com.ro^\",\n\t\t\"||youtube.com.sa^\",\n\t\t\"||youtube.com.sg^\",\n\t\t\"||youtube.com.sv^\",\n\t\t\"||youtube.com.tn^\",\n\t\t\"||youtube.com.tr^\",\n\t\t\"||youtube.com.tw^\",\n\t\t\"||youtube.com.ua^\",\n\t\t\"||youtube.com.uy^\",\n\t\t\"||youtube.com.ve^\",\n\t\t\"||youtube.com^\",\n\t\t\"||youtube.cr^\",\n\t\t\"||youtube.cz^\",\n\t\t\"||youtube.de^\",\n\t\t\"||youtube.dk^\",\n\t\t\"||youtube.ee^\",\n\t\t\"||youtube.es^\",\n\t\t\"||youtube.fi^\",\n\t\t\"||youtube.fr^\",\n\t\t\"||youtube.ge^\",\n\t\t\"||youtube.googleapis.com^\",\n\t\t\"||youtube.gr^\",\n\t\t\"||youtube.gt^\",\n\t\t\"||youtube.hk^\",\n\t\t\"||youtube.hr^\",\n\t\t\"||youtube.hu^\",\n\t\t\"||youtube.ie^\",\n\t\t\"||youtube.in^\",\n\t\t\"||youtube.iq^\",\n\t\t\"||youtube.is^\",\n\t\t\"||youtube.it^\",\n\t\t\"||youtube.jo^\",\n\t\t\"||youtube.jp^\",\n\t\t\"||youtube.kr^\",\n\t\t\"||youtube.kz^\",\n\t\t\"||youtube.la^\",\n\t\t\"||youtube.lk^\",\n\t\t\"||youtube.lt^\",\n\t\t\"||youtube.lu^\",\n\t\t\"||youtube.lv^\",\n\t\t\"||youtube.ly^\",\n\t\t\"||youtube.ma^\",\n\t\t\"||youtube.md^\",\n\t\t\"||youtube.me^\",\n\t\t\"||youtube.mk^\",\n\t\t\"||youtube.mn^\",\n\t\t\"||youtube.mx^\",\n\t\t\"||youtube.my^\",\n\t\t\"||youtube.ng^\",\n\t\t\"||youtube.ni^\",\n\t\t\"||youtube.nl^\",\n\t\t\"||youtube.no^\",\n\t\t\"||youtube.pa^\",\n\t\t\"||youtube.pe^\",\n\t\t\"||youtube.ph^\",\n\t\t\"||youtube.pk^\",\n\t\t\"||youtube.pl^\",\n\t\t\"||youtube.pr^\",\n\t\t\"||youtube.pt^\",\n\t\t\"||youtube.qa^\",\n\t\t\"||youtube.ro^\",\n\t\t\"||youtube.rs^\",\n\t\t\"||youtube.ru^\",\n\t\t\"||youtube.sa^\",\n\t\t\"||youtube.se^\",\n\t\t\"||youtube.sg^\",\n\t\t\"||youtube.si^\",\n\t\t\"||youtube.sk^\",\n\t\t\"||youtube.sn^\",\n\t\t\"||youtube.soy^\",\n\t\t\"||youtube.sv^\",\n\t\t\"||youtube.tn^\",\n\t\t\"||youtube.tv^\",\n\t\t\"||youtube.ua^\",\n\t\t\"||youtube.ug^\",\n\t\t\"||youtube.uy^\",\n\t\t\"||youtube.vn^\",\n\t\t\"||youtube^\",\n\t\t\"||youtubeeducation.com^\",\n\t\t\"||youtubeembeddedplayer.googleapis.com^\",\n\t\t\"||youtubefanfest.com^\",\n\t\t\"||youtubegaming.com^\",\n\t\t\"||youtubego.co.id^\",\n\t\t\"||youtubego.co.in^\",\n\t\t\"||youtubego.com.br^\",\n\t\t\"||youtubego.com^\",\n\t\t\"||youtubego.id^\",\n\t\t\"||youtubego.in^\",\n\t\t\"||youtubei.googleapis.com^\",\n\t\t\"||youtubekids.com^\",\n\t\t\"||youtubemobilesupport.com^\",\n\t\t\"||yt.be^\",\n\t\t\"||ytimg.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"yy\",\n\tName:    \"YY\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"33 41 194 194\\\"><path d=\\\"M36.7 100.1c-2.3 1.3-3.9 5.6-3.2 8.1.4 1 8.2 10.9 17.5 22l16.9 20.1-2.2 2.7c-12.1 14.7-12.6 16.2-8.2 20.5 2.6 2.7 7.1 3.3 9.7 1.2 2.3-1.8 53.3-62.9 54.7-65.4 2.5-4.9-1.4-10.3-7.6-10.3-2.9 0-4.5 1.6-19.4 19.2-8.9 10.6-16.5 19.3-16.9 19.3-.5 0-7.2-7.7-15-17-7.8-9.4-15.2-18-16.6-19.3-2.7-2.3-6.7-2.8-9.7-1.1zm6.2 3.9c1 .5 8.5 8.8 16.6 18.4 8 9.6 15.5 17.8 16.6 18.1 1 .4 2.8.4 3.8 0 1.1-.3 8.6-8.5 16.7-18.1 8.1-9.6 15.6-17.9 16.6-18.5 2.3-1.2 4.8.4 4.8 3.1 0 .9-3.6 5.9-7.9 11.1-4.4 5.2-15.6 18.6-25.1 29.9-18.1 21.7-20.8 24.4-23.4 23.4-2.9-1.1-1.7-5 3.4-11.4 2.8-3.5 5.3-7.3 5.6-8.6.7-2.9-.1-4.1-18.2-25.6C44.5 116.4 38 107.9 38 107c0-1.4 1.8-4 2.8-4 .2 0 1.1.4 2.1 1zm96.3-2.7c-1.2 1.3-2.2 3.5-2.2 4.9 0 3 .1 3.2 19.2 25.9 8.2 9.7 14.8 18 14.8 18.4 0 .4-2.7 3.8-6 7.7-4.8 5.5-6 7.6-6 10.3 0 5.6 5.1 8.9 10.4 6.9 1.3-.5 46.3-53.3 54.4-63.8 1.2-1.6 2.2-4.1 2.2-5.6 0-3.5-3.8-7-7.6-7-3.6 0-6.1 2.4-23.8 23.7-7 8.4-12.8 15.3-13 15.3-.2 0-7.3-8.2-15.7-18.3-8.3-10-15.9-18.8-16.7-19.5-2.6-2-7.6-1.5-10 1.1zm11.4 6.9c2.8 2.9 9.6 10.7 15.1 17.3 12.1 14.4 13.5 15.8 16.4 15.3 2.3-.3 9.1-7.5 25.4-27.1 8.6-10.4 11-12 13.2-9.1.7 1 1.3 2.1 1.3 2.6 0 .7-20 24.9-41.8 50.6-11.2 13.2-12.8 14.6-15.5 13.1-3-1.7-2.7-2.4 6.1-13.9 1.7-2.4 3.2-5.5 3.2-7 0-2-4.1-7.5-15.9-21.6-8.7-10.4-16.1-19.9-16.5-21.1-.5-1.5-.2-2.5 1.1-3.2 2.6-1.5 2.4-1.6 7.9 4.1z\\\"/></svg>\"),\n\tRules: []string{\n\t\t\"||yy.com^\",\n\t},\n\tGroupID: \"streaming\",\n}, {\n\tID:      \"zhihu\",\n\tName:    \"Zhihu\",\n\tIconSVG: []byte(\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 30 30\\\"><path d=\\\"M14.46 14.191H9.982c0-.471.033-.954.039-1.458v-5.5h5.106V5.935a1.352 1.352 0 00-.404-.957 1.378 1.378 0 00-.968-.396H5.783c.028-.088.056-.177.084-.255.274-.82 1.153-3.326 1.153-3.326a4.262 4.262 0 00-2.413.698c-.57.4-.912.682-1.371 1.946-.532 1.453-.997 2.856-1.31 3.693C1.444 8.674.28 11.025.28 11.025a5.85 5.85 0 002.52-.61c1.119-.593 1.679-1.502 2.054-2.883l.09-.3h2.334v5.5c0 .5-.045.982-.073 1.46h-4.12c-.71 0-1.39.278-1.893.775a2.638 2.638 0 00-.783 1.874h6.527a17.717 17.717 0 01-.778 3.649 16.796 16.796 0 01-3.012 5.273A33.104 33.104 0 010 28.74s3.13 1.175 5.425-.954c1.388-1.292 2.631-3.814 3.23-5.727a28.09 28.09 0 001.12-5.229h5.967v-1.37a1.254 1.254 0 00-.373-.899 1.279 1.279 0 00-.909-.37zm-3.19 5.484l-2.312 1.491 5.038 7.458a6.905 6.905 0 00.672-2.218 3.15 3.15 0 00-.28-2.168l-3.118-4.563zM29.05 4.582H16.733V25.94h3.018l.403 2.572 4.081-2.572h4.815V4.582zm-5.207 18.69l-2.396 1.509-.235-1.508h-1.724V7.233h6.78v16.04h-2.425z\\\" /></svg>\"),\n\tRules: []string{\n\t\t\"||zhihu.com^\",\n\t\t\"||zhimg.com^\",\n\t},\n\tGroupID: \"social_network\",\n}}\n\n// serviceGroups contains raw service group data.\nvar serviceGroups = []serviceGroup{{\n\tID: \"ai\",\n}, {\n\tID: \"cdn\",\n}, {\n\tID: \"dating\",\n}, {\n\tID: \"gambling\",\n}, {\n\tID: \"gaming\",\n}, {\n\tID: \"hosting\",\n}, {\n\tID: \"messenger\",\n}, {\n\tID: \"privacy\",\n}, {\n\tID: \"shopping\",\n}, {\n\tID: \"social_network\",\n}, {\n\tID: \"software\",\n}, {\n\tID: \"streaming\",\n}}\n"
  },
  {
    "path": "internal/filtering/tests/dns.txt",
    "content": "! Title: Adguard DNS filter\n!\n!\n! Title: Simplified domain names filter\n! Homepage: https://github.com/AdguardTeam/AdguardDNS\n! License: https://github.com/AdguardTeam/AdguardDNS/blob/master/LICENSE\n! Last modified: 2017-01-27 12:00:10\n! Decsription: Filter composed from several other filters (English filter, Social media filter, Spyware filter, Mobile ads filter, EasyList and EasyPrivacy) and simplified specifically to be better compatible with DNS-level ad blocking.\n!\n! Adservice\n!\n! EasyList\n||007-gateway.com^\n||04dn8g4f.space^\n||0emn.com^\n||0fmm.com^\n||0icep80f.com^\n||0pixl.com^\n||0xwxmj21r75kka.com^\n||101m3.com^\n||103092804.com^\n||104.154.237.93^\n||10fbb07a4b0.se^\n||10pipsaffiliates.com^\n||1100i.com^\n||123date.me^\n||12place.com^\n||152media.com^\n||15f3c01a.info^\n||15f3c01c.info^\n||174.142.194.177^\n||17a898b9.info^\n||17a898bb.info^\n||188.138.1.45^\n||188server.com^\n||18clicks.com^\n||194.71.107.25^\n||199.102.225.178^\n||1ccbt.com^\n||1clickdownloads.com^\n||1e0y.xyz^\n||1empiredirect.com^\n||1nimo.com^\n||1phads.com^\n||1rx.io^\n||1rxntv.io^\n||1sadx.net^\n||1web.me^\n||1yk851od.com^\n||204.93.181.78^\n||206ads.com^\n||209.222.8.217^\n||20dollars2surf.com^\n||213.163.70.183^\n||221.141.213.254^\n||247realmedia.com^\n||254a.com^\n||2al.pw^\n||2beon.co.kr^\n||2d4c3870.info^\n||2d4c3872.info^\n||2dpt.com^\n||2h045kx8.review^\n||2mdn.info^\n||2xbpub.com^\n||32b4oilo.com^\n||3393.com^\n||33across.com^\n||350media.com^\n||360ads.com^\n||360adstrack.com^\n||360installer.com^\n||360popads.com^\n||360yield.com^\n||365sbaffiliates.com^\n||3cnce854.com^\n||3jsbf5.xyz^\n||3lift.com^\n||3lr67y45.com^\n||3omb.com^\n||3rdads.com^\n||3redlightfix.com^\n||3t7euflv.com^\n||3wr110.net^\n||3wr110.xyz^\n||43plc.com^\n||46.165.197.153^\n||46.165.197.231^\n||46.246.120.230^\n||4affiliate.net^\n||4dsply.com^\n||4e43ac9c.info^\n||4uvjosuc.com^\n||4wnet.com^\n||50.7.243.123^\n||5362367e.info^\n||56fh8x.xyz^\n||5advertise.com^\n||5clickcashsoftware.com^\n||5gl1x9qc.com^\n||600z.com^\n||62.27.51.163^\n||63.225.61.4^\n||64.20.60.123^\n||67s6gxv28kin.com^\n||69wnz64h.xyz^\n||74.117.182.77^\n||777seo.com^\n||778669.com^\n||78.138.126.253^\n||78.140.131.214^\n||7insight.com^\n||7pud.com^\n||7search.com^\n||7u8a8i88.com^\n||82d914.se^\n||87.230.102.24^\n||888media.net^\n||888medianetwork.com^\n||888promos.com^\n||8yxupue8.com^\n||97d73lsi.com^\n||9d63c80da.pw^\n||9ts3tpia.com^\n||a-ads.com^\n||a-ssl.ligatus.com^\n||a-static.com^\n||a.adroll.com^\n||a.ligatus.com^\n||a.raasnet.com^\n||a2dfp.net^\n||a2pub.com^\n||a3pub.com^\n||a433.com^\n||a4dtrk.com^\n||a4to4.pw^\n||a5a5a.com^\n||a5pub.com^\n||aa.voice2page.com^\n||aaa.at4.info^\n||aaa.dv0.info^\n||abasourdir.tech^\n||abletomeet.com^\n||abnad.net^\n||aboutads.quantcast.com^\n||abtracker.us^\n||accelacomm.com^\n||access-mc.com^\n||accmgr.com^\n||accounts.pkr.com^\n||accumulatork.com^\n||accuserveadsystem.com^\n||acf-webmaster.net^\n||acronym.com^\n||actiondesk.com^\n||activedancer.com^\n||ad-back.net^\n||ad-balancer.net^\n||ad-bay.com^\n||ad-clicks.com^\n||ad-delivery.net^\n||ad-flow.com^\n||ad-gbn.com^\n||ad-goi.com^\n||ad-indicator.com^\n||ad-m.asia^\n||ad-maven.com^\n||ad-media.org^\n||ad-server.co.za^\n||ad-serverparc.nl^\n||ad-sponsor.com^\n||ad-srv.net^\n||ad-stir.com^\n||ad-vice.biz^\n||ad.doubleclick.net^\n||ad.linksynergy.com^\n||ad.pxlad.io^\n||ad.yieldpartners.com^\n||ad120m.com^\n||ad121m.com^\n||ad122m.com^\n||ad123m.com^\n||ad125m.com^\n||ad127m.com^\n||ad128m.com^\n||ad129m.com^\n||ad131m.com^\n||ad132m.com^\n||ad134m.com^\n||ad20.net^\n||ad2387.com^\n||ad2adnetwork.biz^\n||ad2games.com^\n||ad2up.com^\n||ad4980.kr^\n||ad4989.co.kr^\n||ad4game.com^\n||ad6media.fr^\n||adacado.com^\n||adaction.se^\n||adacts.com^\n||adadvisor.net^\n||adagora.com^\n||adaos-ads.net^\n||adap.tv^\n||adapd.com^\n||adbard.net^\n||adbasket.net^\n||adbit.co^\n||adbma.com^\n||adboost.com^\n||adbooth.com^\n||adbooth.net^\n||adbrau.com^\n||adbrite.com^\n||adbroo.com^\n||adbrook.com^\n||adbuff.com^\n||adbull.com^\n||adbureau.net^\n||adbutler.com^\n||adbuyer.com^\n||adcade.com^\n||adcarem.co^\n||adcash.com^\n||adcastplus.net^\n||adcde.com^\n||adcdnx.com^\n||adcentriconline.com^\n||adcfrthyo.tk^\n||adchap.com^\n||adchemical.com^\n||adchoice.co.za^\n||adclick.lv^\n||adclick.pk^\n||adclickafrica.com^\n||adclickmedia.com^\n||adclickservice.com^\n||adcloud.net^\n||adcmps.com^\n||adcolo.com^\n||adconjure.com^\n||adconscious.com^\n||adcount.in^\n||adcrax.com^\n||adcron.com^\n||adcru.com^\n||addaim.com^\n||addelive.com^\n||addiply.com^\n||addoer.com^\n||addroid.com^\n||addynamics.eu^\n||addynamix.com^\n||addynamo.net^\n||adecn.com^\n||adedy.com^\n||adelement.com^\n||ademails.com^\n||adengage.com^\n||adespresso.com^\n||adexc.net^\n||adexchange.io^\n||adexchangeprediction.com^\n||adexcite.com^\n||adexprt.com^\n||adexprts.com^\n||adextent.com^\n||adf01.net^\n||adfactory88.com^\n||adfeedstrk.com^\n||adfootprints.com^\n||adforgames.com^\n||adforgeinc.com^\n||adform.net^\n||adframesrc.com^\n||adfrika.com^\n||adfrog.info^\n||adfrontiers.com^\n||adfunkyserver.com^\n||adfusion.com^\n||adgalax.com^\n||adgardener.com^\n||adgatemedia.com^\n||adgear.com^\n||adgebra.co.in^\n||adgent007.com^\n||adgila.com^\n||adgine.net^\n||adgitize.com^\n||adglamour.net^\n||adglare.net^\n||adgoi.com^\n||adgoi.mobi^\n||adgorithms.com^\n||adgoto.com^\n||adgroups.com^\n||adgrx.com^\n||adhese.be^\n||adhese.com^\n||adhese.net^\n||adhigh.net^\n||adhitzads.com^\n||adhostingsolutions.com^\n||adhub.co.nz^\n||adicate.com^\n||adigniter.org^\n||adikteev.com^\n||adimise.com^\n||adimpact.com^\n||adimperia.com^\n||adimpression.net^\n||adinch.com^\n||adincon.com^\n||adindigo.com^\n||adinfinity.com.au^\n||adintend.com^\n||adinterax.com^\n||adinvigorate.com^\n||adip.ly^\n||adiqglobal.com^\n||adireland.com^\n||adisfy.com^\n||adisn.com^\n||adit-media.com^\n||adition.com^\n||aditize.com^\n||adjal.com^\n||adjector.com^\n||adjourne.com^\n||adjug.com^\n||adjuggler.com^\n||adjuggler.net^\n||adjungle.com^\n||adk2.co^\n||adk2.com^\n||adk2x.com^\n||adkengage.com^\n||adkick.net^\n||adklip.com^\n||adknowledge.com^\n||adkonekt.com^\n||adkova.com^\n||adlatch.com^\n||adlayer.net^\n||adlegend.com^\n||adlink.net^\n||adlinx.info^\n||adlisher.com^\n||adloaded.com^\n||adlooxtracking.com^\n||adlpartner.com^\n||adlure.biz^\n||adlux.com^\n||admagnet.net^\n||admailtiser.com^\n||admamba.com^\n||adman.gr^\n||admanage.com^\n||admanmedia.com^\n||admarketplace.net^\n||admaster.net^\n||admaxim.com^\n||admaya.in^\n||admedia.com^\n||admedias.net^\n||admedit.net^\n||admedo.com^\n||admeld.com^\n||admeta.com^\n||admission.net^\n||admixer.net^\n||admngronline.com^\n||admpads.com^\n||admtpmp127.com^\n||admulti.com^\n||admzn.com^\n||adne.tv^\n||adnectar.com^\n||adnemo.com^\n||adnet-media.net^\n||adnet.biz^\n||adnet.com^\n||adnet.de^\n||adnet.lt^\n||adnet.ru^\n||adnet.vn^\n||adnetworkme.com^\n||adnetworkperformance.com^\n||adnext.fr^\n||adngin.com^\n||adnigma.com^\n||adnimation.com^\n||adnium.com^\n||adnmore.co.kr^\n||adnoble.com^\n||adnow.com^\n||adnxs.com^\n||adnxs.net^\n||adnxs1.com^\n||adocean.pl^\n||adohana.com^\n||adomik.com^\n||adonion.com^\n||adonly.com^\n||adonnews.com^\n||adonweb.ru^\n||adoperator.com^\n||adoptim.com^\n||adorika.com^\n||adorika.net^\n||adotic.com^\n||adotomy.com^\n||adotube.com^\n||adovida.com^\n||adowner.net^\n||adpacks.com^\n||adparlor.com^\n||adpath.mobi^\n||adpay.com^\n||adpays.net^\n||adpdx.com^\n||adperfect.com^\n||adperium.com^\n||adphreak.com^\n||adpinion.com^\n||adpionier.de^\n||adplans.info^\n||adplex.media^\n||adplugg.com^\n||adplxmd.com^\n||adpoper.com^\n||adppv.com^\n||adpredictive.com^\n||adpremo.com^\n||adprofit2share.com^\n||adproper.info^\n||adprotected.com^\n||adprovi.de^\n||adprs.net^\n||adpushup.com^\n||adquantix.com^\n||adquest3d.com^\n||adrcdn.com^\n||adreactor.com^\n||adready.com^\n||adreadytractions.com^\n||adrecover.com^\n||adresellers.com^\n||adrevivify.com^\n||adrevolver.com^\n||adrich.cash^\n||adrife.net^\n||adrise.de^\n||adrocket.com^\n||adrsp.net^\n||adrunnr.com^\n||ads-4u.com^\n||ads-elsevier.net^\n||ads-stats.com^\n||ads-twitter.com^\n||ads.cc^\n||ads.rd.linksynergy.com^\n||ads01.com^\n||ads2ads.net^\n||ads2srv.com^\n||ads4cheap.com^\n||adsafeprotected.com^\n||adsafety.net^\n||adsalvo.com^\n||adsame.com^\n||adsbookie.com^\n||adsbrook.com^\n||adscale.de^\n||adscampaign.net^\n||adscendmedia.com^\n||adsclickingnetwork.com^\n||adscope.co.kr^\n||adscpm.net^\n||adsdk.com^\n||adsdot.ph^\n||adsearcher.ru^\n||adsensecamp.com^\n||adserv8.com^\n||adserve.com^\n||adserve.ph^\n||adserver-fx.com^\n||adserverplus.com^\n||adserverpub.com^\n||adservhere.com^\n||adservingfactory.com^\n||adservinginternational.com^\n||adservpi.com^\n||adservr.de^\n||adsfac.eu^\n||adsfac.net^\n||adsfac.us^\n||adsfactor.net^\n||adsfan.net^\n||adsfast.com^\n||adsforindians.com^\n||adsfundi.com^\n||adsfundi.net^\n||adsfuse.com^\n||adshack.com^\n||adshexa.com^\n||adshopping.com^\n||adshost1.com^\n||adshost2.com^\n||adshot.de^\n||adshuffle.com^\n||adsiduous.com^\n||adsignals.com^\n||adsimilis.com^\n||adsinimages.com^\n||adsjudo.com.^\n||adsjudo.com^\n||adskeeper.co.uk^\n||adslidango.com^\n||adslingers.com^\n||adslot.com^\n||adslvr.com^\n||adsmarket.com^\n||adsmarket.es^\n||adsmedia.cc^\n||adsmile.biz^\n||adsmoon.com^\n||adsmws.cloudapp.net^\n||adsnative.com^\n||adsnetworkserver.com^\n||adsnext.net^\n||adsniper.ru^\n||adsomi.com^\n||adsonar.com^\n||adsoptimal.com^\n||adsopx.com^\n||adsovo.com^\n||adsp.com^\n||adspaper.org^\n||adsparc.net^\n||adspdbl.com^\n||adspeed.com^\n||adspirit.de^\n||adspring.to^\n||adspruce.com^\n||adspynet.com^\n||adsrevenue.net^\n||adsring.com^\n||adsrv.us^\n||adsrvmedia.com^\n||adsrvmedia.net^\n||adsrvr.org^\n||adssend.net^\n||adssites.net^\n||adstargeting.com^\n||adstatic.com^\n||adsterra.com^\n||adsummos.net^\n||adsupermarket.com^\n||adsupply.com^\n||adsupplyssl.com^\n||adsurve.com^\n||adsvcs.com^\n||adsvert.com^\n||adsvids.com^\n||adsxgm.com^\n||adszom.com^\n||adtaily.com^\n||adtaily.eu^\n||adtaily.pl^\n||adtdp.com^\n||adtecc.com^\n||adtech.de^\n||adtechus.com^\n||adtegrity.net^\n||adteractive.com^\n||adtgs.com^\n||adthrive.com^\n||adtoadd.com^\n||adtoll.com^\n||adtology1.com^\n||adtology2.com^\n||adtology3.com^\n||adtoma.com^\n||adtomafusion.com^\n||adtoox.com^\n||adtotal.pl^\n||adtpix.com^\n||adtrace.org^\n||adtransfer.net^\n||adtrgt.com^\n||adtrieval.com^\n||adtrix.com^\n||adtrovert.com^\n||adtruism.com^\n||adtwirl.com^\n||aduacni.com^\n||adult-adv.com^\n||adultadworld.com^\n||adultimate.net^\n||adulttds.com^\n||adurr.com^\n||adv-adserver.com^\n||adv9.net^\n||advanseads.com^\n||advantageglobalmarketing.com^\n||advard.com^\n||advarkads.com^\n||advatar.to^\n||adventori.com^\n||adverpub.com^\n||adversal.com^\n||adversaldisplay.com^\n||adversalservers.com^\n||adverserve.net^\n||advertarium.com.ua^\n||advertbox.us^\n||adverteerdirect.nl^\n||adverticum.net^\n||advertise.com^\n||advertiseforfree.co.za^\n||advertisegame.com^\n||advertisespace.com^\n||advertiseworld.com^\n||advertiseyourgame.com^\n||advertising-department.com^\n||advertising.com^\n||advertising365.com^\n||advertisingiq.com^\n||advertisingpath.net^\n||advertisingvalue.info^\n||advertjunction.com^\n||advertlane.com^\n||advertlead.net^\n||advertlets.com^\n||advertmarketing.com^\n||advertmedias.com^\n||advertnetworks.com^\n||advertone.ru^\n||advertpay.net^\n||advertrev.com^\n||advertserve.com^\n||advertstatic.com^\n||advertstream.com^\n||advertur.ru^\n||advertxi.com^\n||advg.jp^\n||advgoogle.com^\n||advideum.com^\n||advisorded.com^\n||adviva.net^\n||advmd.com^\n||advmedialtd.com^\n||advombat.ru^\n||advpoints.com^\n||advrtice.com^\n||advserver.xyz^\n||advsnx.net^\n||adwires.com^\n||adwordsservicapi.com^\n||adworkmedia.com^\n||adworldmedia.com^\n||adworldmedia.net^\n||adxchg.com^\n||adxcore.com^\n||adxion.com^\n||adxpose.com^\n||adxpower.com^\n||adyoulike.com^\n||adyoz.com^\n||adz.co.zw^\n||adzbazar.com^\n||adzerk.net^\n||adzhub.com^\n||adziff.com^\n||adzmedia.com^\n||adzonk.com^\n||adzouk.com^\n||adzpower.com^\n||adzs.nl^\n||afcyhf.com^\n||afdads.com^\n||aff-online.com^\n||aff.biz^\n||affbot1.com^\n||affbot3.com^\n||affbot7.com^\n||affbot8.com^\n||affbuzzads.com^\n||affec.tv^\n||affiliate-b.com^\n||affiliate-gate.com^\n||affiliate-robot.com^\n||affiliate.com^\n||affiliate.cx^\n||affiliatebannerfarm.com^\n||affiliateedge.com^\n||affiliateer.com^\n||affiliatefuel.com^\n||affiliatefuture.com^\n||affiliategateways.co^\n||affiliategroove.com^\n||affiliatelounge.com^\n||affiliatemembership.com^\n||affiliatesensor.com^\n||affiliation-france.com^\n||affiliationcash.com^\n||affiliationworld.com^\n||affiliationzone.com^\n||affilijack.de^\n||affiliproducts.com^\n||affiliserve.com^\n||affimo.de^\n||affinitad.com^\n||affinity.com^\n||affiz.net^\n||affplanet.com^\n||afftrack.com^\n||aflrm.com^\n||africawin.com^\n||afterdownload.com^\n||afterdownloads.com^\n||afy11.net^\n||agcdn.com^\n||agentcenters.com^\n||aggregateknowledge.com^\n||aggregatorgetb.com^\n||aglocobanners.com^\n||agmtrk.com^\n||agomwefq.com^\n||agvzvwof.com^\n||aim4media.com^\n||aimatch.com^\n||aio.media^\n||ajansreklam.net^\n||ajillionmax.com^\n||akamhd.com^\n||albopa.work^\n||alchemysocial.com^\n||alfynetwork.com^\n||algovid.com^\n||alimama.com^\n||allabc.com^\n||alleliteads.com^\n||allmt.com^\n||allopenclose.click^\n||alloydigital.com^\n||allyes.com^\n||alphabird.com^\n||alphabirdnetwork.com^\n||alphagodaddy.com^\n||alternads.info^\n||alternativeadverts.com^\n||altitude-arena.com^\n||am-display.com^\n||am10.ru^\n||am11.ru^\n||am15.net^\n||amazon-adsystem.com^\n||amazon-cornerstone.com^\n||amazonily.com^\n||ambaab.com^\n||amd2016.com^\n||amertazy.com^\n||amgdgt.com^\n||amp.rd.linksynergy.com^\n||ampxchange.com^\n||anastasiasaffiliate.com^\n||andbeyond.media^\n||andohs.net^\n||andomedia.com^\n||andomediagroup.com^\n||anet*.tradedoubler.com^\n||angege.com^\n||anonymousads.com^\n||anwufkjjja.com^\n||anymedia.lv^\n||anyxp.com^\n||aoqneyvmaz.com^\n||aorms.com^\n||aorpum.com^\n||apex-ad.com^\n||apmebf.com^\n||appendad.com^\n||applebarq.com^\n||appnext.com^\n||apportium.com^\n||apptap.com^\n||appwebview.com^\n||april29-disp-download.com^\n||apsmediaagency.com^\n||apugod.work^\n||apxlv.com^\n||arab4eg.com^\n||arabweb.biz^\n||arcadebannerexchange.net^\n||arcadebannerexchange.org^\n||arcadebanners.com^\n||arcadebe.com^\n||arcadechain.com^\n||areasins.com^\n||areasnap.com^\n||arecio.work^\n||arti-mediagroup.com^\n||as-farm.com^\n||as5000.com^\n||asafesite.com^\n||aseadnet.com^\n||asermtawlfs.xyz^\n||asklots.com^\n||asooda.com^\n||asrety.com^\n||assetize.com^\n||assoc-amazon.ca^\n||assoc-amazon.co.uk^\n||assoc-amazon.com^\n||assoc-amazon.de^\n||assoc-amazon.es^\n||assoc-amazon.fr^\n||assoc-amazon.it^\n||asterpix.com^\n||astree.be^\n||atadserver.com^\n||atas.io^\n||atemda.com^\n||atmalinks.com^\n||ato.mx^\n||atomex.net^\n||atomicblast.lol^\n||atrinsic.com^\n||atwola.com^\n||au2m8.com^\n||auctionnudge.com^\n||audience2media.com^\n||audiencefuel.com^\n||audienceprofiler.com^\n||auditoire.ph^\n||auditude.com^\n||audu0yi.bid^\n||augmentad.net^\n||august15download.com^\n||aunmdhxrco.com^\n||auspipe.com^\n||auto-im.com^\n||auto-insurance-quotes-compare.com^\n||automatedtraffic.com^\n||automateyourlist.com^\n||avads.co.uk^\n||avalanchers.com^\n||avalopaly.com^\n||avazu.net^\n||avazutracking.net^\n||avercarto.com^\n||awakebottlestudy.com^\n||awaps.net^\n||awempire.com^\n||awltovhc.com^\n||awsmer.com^\n||awstaticdn.net^\n||awsurveys.com^\n||axill.com^\n||ayabreya.xyz^\n||ayboll.com^\n||azads.com^\n||azjmp.com^\n||azoogleads.com^\n||azorbe.com^\n||b117f8da23446a91387efea0e428392a.pl^\n||b2s1uqa6.download^\n||b4banner.in^\n||b6508157d.website^\n||babbnrs.com^\n||backbeatmedia.com^\n||backlinks.com^\n||badjocks.com^\n||bakkels.com^\n||baldiro.de^\n||bam-bam-slam.com^\n||bambergerkennanchitinous.com^\n||bamboocast.com^\n||bamj630h.tech^\n||bananaflippy.com^\n||bandelcot.com^\n||banner-clix.com^\n||banner-rotation.com^\n||bannerbank.ru^\n||bannerblasters.com^\n||bannerbridge.net^\n||bannercde.com^\n||bannerconnect.com^\n||bannerconnect.net^\n||bannerdealer.com^\n||bannerexchange.com.au^\n||bannerflow.com^\n||bannerflux.com^\n||bannerignition.co.za^\n||bannerjammers.com^\n||bannerlot.com^\n||bannerperformance.net^\n||bannerrage.com^\n||bannersmania.com^\n||bannersnack.com^\n||bannersnack.net^\n||bannersurvey.biz^\n||bannertgt.com^\n||bannertracker-script.com^\n||bannerweb.com^\n||banniere.reussissonsensemble.fr^\n||bargainpricedude.com^\n||baronsoffers.com^\n||basebanner.com^\n||bbelements.com^\n||bbuni.com^\n||beaconads.com^\n||beatchucknorris.com^\n||bebi.com^\n||become.successfultogether.co.uk^\n||bedorm.com^\n||beead.co.uk^\n||beead.net^\n||beerforthepipl.com^\n||befade.com^\n||beforescence.com^\n||begun.ru^\n||belointeractive.com^\n||belvertising.be^\n||benchmarkingstuff.com^\n||benisoncanorous.org^\n||bentdownload.com^\n||bepolite.eu^\n||beringmedia.com^\n||bestarmour4u.work^\n||bestcasinopartner.com^\n||bestdeals.ws^\n||bestfindsite.com^\n||bestforexpartners.com^\n||bestgameads.com^\n||besthitsnow.com^\n||bestofferdirect.com^\n||bestonlinecoupons.com^\n||bestpricewala.com^\n||bet3000partners.com^\n||bet365affiliates.com^\n||betaffs.com^\n||betoga.com^\n||betpartners.it^\n||betrad.com^\n||bettingpartners.com^\n||bezoya.work^\n||bf-ad.net^\n||bfast.com^\n||bh3.net^\n||bidgewatr.com^\n||bidsystem.com^\n||bidverdrd.com^\n||bidvertiser.com^\n||biemedia.com^\n||bigadpoint.net^\n||bigfineads.com^\n||bigpulpit.com^\n||bijscode.com^\n||billypub.com^\n||bimlocal.com^\n||bin-layer.de^\n||bin-layer.ru^\n||binaryoptionssystems.org^\n||bingo4affiliates.com^\n||binlayer.com^\n||binlayer.de^\n||biskerando.com^\n||bitads.net^\n||bitcoinadvertisers.com^\n||bitfalcon.tv^\n||bittads.com^\n||bitx.tv^\n||bizographics.com^\n||bizrotator.com^\n||bizzclick.com^\n||bjjingda.com^\n||blamads.com^\n||blamcity.com^\n||blardenso.com^\n||blinkadr.com^\n||blogads.com^\n||blogbannerexchange.com^\n||blogclans.com^\n||bloggerex.com^\n||blogherads.com^\n||blogohertz.com^\n||blueadvertise.com^\n||bluesli.de^\n||bluestreak.com^\n||bluetoad.com^\n||blumi.to^\n||bmanpn.com^\n||bnetworx.com^\n||bnhtml.com^\n||bnmla.com^\n||bnr.sys.lv^\n||bnrs.it^\n||bnserving.com^\n||bogads.com^\n||bokroet.com^\n||bonusfapturbo.com^\n||bonzai.ad^\n||boo-box.com^\n||bookelement.biz^\n||booklandonline.info^\n||boom-boom-vroom.com^\n||boostable.com^\n||boostads.net^\n||boostclic.com^\n||boostshow.com^\n||bop-bop-bam.com^\n||bormoni.ru^\n||bororas.com^\n||bostonparadise.com^\n||bostonwall.com^\n||bounce.bar^\n||boydadvertising.co.uk^\n||boylesportsreklame.com^\n||bpasyspro.com^\n||bptracking.com^\n||br.rk.com^\n||brainient.com^\n||brakefluid.website^\n||branchr.com^\n||brand-display.com^\n||brand.net^\n||brandads.net^\n||brandaffinity.net^\n||brandclik.com^\n||brandreachsys.com^\n||braside.ru^\n||brassyobedientcotangent.com^\n||bravenetmedianetwork.com^\n||breadpro.com^\n||brealtime.com^\n||brethrengenotypeteledyne.com^\n||bridgetrack.com^\n||brighteroption.com^\n||brightshare.com^\n||broadstreetads.com^\n||brokeloy.com^\n||browsersfeedback.com^\n||brucelead.com^\n||bstrtb.com^\n||btnibbler.com^\n||btrll.com^\n||bttbgroup.com^\n||bttrack.com^\n||bu520.com^\n||bubblesmedia.ru^\n||bucketsofbanners.com^\n||budgetedbauer.com^\n||budurl.com^\n||buildtrafficx.com^\n||buletproofserving.com^\n||bulgarine.com^\n||bulletproofserving.com^\n||bunchofads.com^\n||bunny-net.com^\n||burbanked.info^\n||burjam.com^\n||burnsoftware.info^\n||burstnet.com^\n||businesscare.com^\n||businessclick.com^\n||busterzaster.de^\n||buxept.com^\n||buxflow.com^\n||buxp.org^\n||buyflood.com^\n||buyorselltnhomes.com^\n||buysellads.com^\n||buyt.in^\n||buzzcity.net^\n||buzzparadise.com^\n||bw94.xyz^\n||bwinpartypartners.com^\n||bwknu1lo.top^\n||byspot.com^\n||byzoo.org^\n||bznclicks.com^\n||c-on-text.com^\n||c-planet.net^\n||c03jij5q.website^\n||c8.net.ua^\n||c9snorwj.website^\n||callmd5map.com^\n||camleyads.info^\n||campanja.com^\n||canaanita.com^\n||canadasungam.net^\n||canoeklix.com^\n||capacitygrid.com^\n||capitatmarket.com^\n||captainad.com^\n||captifymedia.com^\n||carbonads.com^\n||cardincraping.net^\n||carrier.bz^\n||cartorkins.com^\n||cartstick.com^\n||casalemedia.com^\n||cash-duck.com^\n||cash4members.com^\n||cashatgsc.com^\n||cashmylinks.com^\n||cashonvisit.com^\n||cashtrafic.com^\n||cashtrafic.info^\n||cashworld.biz^\n||casino-zilla.com^\n||caspion.com^\n||casterpretic.com^\n||castplatform.com^\n||caygh.com^\n||cb-content.com^\n||cbaazars.com^\n||cbclickbank.com^\n||cbclicks.com^\n||cbcx8t95.space^\n||cbleads.com^\n||cbn.tbn.ru^\n||cc-dt.com^\n||cd828.com^\n||cdn-image.com^\n||cdn.mobicow.com^\n||cdna.tremormedia.com^\n||cdnads.com^\n||cdnapi.net^\n||cdnload.top^\n||cdnmedia.xyz^\n||cdnrl.com^\n||cdnservr.com^\n||cdntrip.com^\n||centralnervous.net^\n||cerotop.com^\n||cgecwm.org^\n||chango.com^\n||chanished.net^\n||chanitet.ru^\n||chargeplatform.com^\n||charltonmedia.com^\n||checkapi.xyz^\n||checkm8.com^\n||checkmystats.com.au^\n||checkoutfree.com^\n||cherytso.com^\n||chicbuy.info^\n||chiliadv.com^\n||china-netwave.com^\n||chinagrad.ru^\n||chipleader.com^\n||chitika.com^\n||chitika.net^\n||chronicads.com^\n||cibleclick.com^\n||city-ads.de^\n||cityadspix.com^\n||citysite.net^\n||cjt1.net^\n||clarityray.com^\n||clash-media.com^\n||claxonmedia.com^\n||clayaim.com^\n||cldlr.com^\n||cleafs.com^\n||clear-request.com^\n||clente.com^\n||clevv.com^\n||click.scour.com^\n||click2jump.com^\n||click4free.info^\n||clickable.com^\n||clickad.pl^\n||clickagy.com^\n||clickbet88.com^\n||clickbooth.com^\n||clickboothlnk.com^\n||clickbubbles.net^\n||clickcash.com^\n||clickcertain.com^\n||clickequations.net^\n||clickexa.com^\n||clickexperts.net^\n||clickfuse.com^\n||clickinc.com^\n||clickintext.com^\n||clickintext.net^\n||clickiocdn.com^\n||clickkingdom.net^\n||clickly.co^\n||clickmngr.com^\n||clickmon.co.kr^\n||clickmyads.info^\n||clicknano.com^\n||clickosmedia.com^\n||clicks2count.com^\n||clicks4ads.com^\n||clicksor.com^\n||clicksor.net^\n||clicksurvey.mobi^\n||clickterra.net^\n||clickthrucash.com^\n||clicktripz.co^\n||clicktripz.com^\n||clickupto.com^\n||clickwinks.com^\n||clickxchange.com^\n||clickzxc.com^\n||clipurl.club^\n||clixgalore.com^\n||clixsense.com^\n||clixtrac.com^\n||clkdown.info^\n||clkrev.com^\n||clmbtech.com^\n||clnk.me^\n||cloudioo.net^\n||cloudset.xyz^\n||cltomedia.info^\n||clz3.net^\n||cmfads.com^\n||cmllk1.info^\n||cnt.my^\n||cntdy.mobi^\n||coadvertise.com^\n||codeonclick.com^\n||codezap.com^\n||codigobarras.net^\n||coedmediagroup.com^\n||cogocast.net^\n||cogsdigital.com^\n||coguan.com^\n||coinad.com^\n||coinadvert.net^\n||coinmedia.co^\n||coinsicmp.com^\n||cointraffic.in^\n||cointraffic.io^\n||collection-day.com^\n||collective-media.net^\n||colliersads.com^\n||combotag.com^\n||comclick.com^\n||comeadvertisewithus.com^\n||commission-junction.com^\n||commission.bz^\n||commissionfactory.com.au^\n||commissionlounge.com^\n||commissionmonster.com^\n||completecarrd.com^\n||complive.link^\n||comscore.com^\n||conduit-banners.com^\n||conduit-services.com^\n||connatix.com^\n||connectedads.net^\n||connectignite.com^\n||connectionads.com^\n||connexity.net^\n||connexplace.com^\n||connextra.com^\n||construment.com^\n||consumergenepool.com^\n||contadd.com^\n||contaxe.com^\n||content-ad.net^\n||content-cooperation.com^\n||contentclick.co.uk^\n||contentdigital.info^\n||contentjs.com^\n||contenture.com^\n||contentwidgets.net^\n||contexlink.se^\n||contextads.net^\n||contextuads.com^\n||contextweb.com^\n||conyak.com^\n||coolerads.com^\n||coolmirage.com^\n||coolyeti.info^\n||copacet.com^\n||cor-natty.com^\n||coretarget.co.uk^\n||cornflip.com^\n||corruptcy.com^\n||corwrite.com^\n||cosmjs.com^\n||coull.com^\n||coupon2buy.com^\n||covertarget.com^*_*.php\n||cpabeyond.com^\n||cpaclicks.com^\n||cpaclickz.com^\n||cpagrip.com^\n||cpalead.com^\n||cpalock.com^\n||cpanuk.com^\n||cpaway.com^\n||cpays.com^\n||cpcadnet.com^\n||cpfclassifieds.com^\n||cpm.biz^\n||cpmadvisors.com^\n||cpmaffiliation.com^\n||cpmleader.com^\n||cpmmedia.net^\n||cpmrocket.com^\n||cpmstar.com^\n||cpmtree.com^\n||cpuim.com^\n||cpulaptop.com^\n||cpvads.com^\n||cpvadvertise.com^\n||cpvmarketplace.info^\n||cpvtgt.com^\n||cpx24.com^\n||cpxadroit.com^\n||cpxinteractive.com^\n||crakmedia.com^\n||crazylead.com^\n||crazyvideosempire.com^\n||creative-serving.com^\n||creditcards15x.tk^\n||crispads.com^\n||crocspaceoptimizer.com^\n||crossrider.com^\n||crowdgatheradnetwork.com^\n||crowdgravity.com^\n||cruftexcision.xyz^\n||cruiseworldinc.com^\n||csklde.space^\n||ctasnet.com^\n||ctenetwork.com^\n||ctm-media.com^\n||ctrhub.com^\n||cubics.com^\n||cuelinks.com^\n||curancience.com^\n||currentlyobsessed.me^\n||curtaecompartilha.com^\n||curtisfrierson.com^\n||cwtrackit.com^\n||cybmas.com^\n||cygnus.com^\n||czasnaherbate.info^\n||d.adroll.com^\n||d.ligatus.com^\n||d.m3.net^\n||d03x2011.com^\n||d1110e4.se^\n||d2.ligatus.com^\n||d2ship.com^\n||d5zob5vm0r8li6khce5he5.com^\n||da-ads.com^\n||dadegid.ru^\n||dai0eej.bid^\n||danitabedtick.net^\n||danmeneldur.com^\n||dapper.net^\n||darwarvid.com^\n||das5ku9q.com^\n||dascasdw.xyz^\n||dashad.io^\n||dashbida.com^\n||dashboardad.net^\n||dashgreen.online^\n||data.adroll.com^\n||datacratic-px.com^\n||datawrkz.com^\n||dating-banners.com^\n||datinggold.com^\n||datumreact.com^\n||dazhantai.com^\n||dbbsrv.com^\n||dbclix.com^\n||dbtaclpoahri.com^\n||dc121677.com^\n||dealcurrent.com^\n||decisionmark.com^\n||decisionnews.com^\n||decknetwork.net^\n||dedicatedmedia.com^\n||dedicatednetworks.com^\n||deepintent.com^\n||deepmetrix.com^\n||defaultimg.com^\n||deguiste.com^\n||dehtale.ru^\n||deletemer.online^\n||deliberatelyvirtuallyshared.xyz^\n||delivery45.com^\n||delivery47.com^\n||delivery49.com^\n||delivery51.com^\n||delnapb.com^\n||deplayer.net^\n||deployads.com^\n||depresis.com^\n||deriversal.com^\n||derlatas.com^\n||descapita.com^\n||descz.ovh^\n||destinationurl.com^\n||detailtoothteam.com^\n||dethao.com^\n||detroposal.com^\n||developermedia.com^\n||deximedia.com^\n||dexplatform.com^\n||dfskgmrepts.com^\n||dgmatix.com^\n||dgmaustralia.com^\n||dgmaxinteractive.com^\n||dhundora.com^\n||diamondtraff.com^\n||dianomi.com^\n||dianomioffers.co.uk^\n||digipathmedia.com^\n||digitrevenue.com^\n||dinclinx.com^\n||dipads.net^\n||directaclick.com^\n||directclicksonly.com^\n||directile.info^\n||directile.net^\n||directleads.com^\n||directoral.info^\n||directorym.com^\n||directrev.com^\n||directtrack.com^\n||directtrk.com^\n||dispop.com^\n||disqusads.com^\n||distilled.ie^\n||districtm.ca^\n||dit-dit-dot.com^\n||ditwrite.com^\n||dj-updates.com^\n||dl-rms.com^\n||dltags.com^\n||dmu20vut.com^\n||dnbizcdn.com^\n||dntrck.com^\n||document4u.info^\n||dollarade.com^\n||dollarsponsor.com^\n||domainadvertising.com^\n||domainbuyingservices.com^\n||domainsponsor.com^\n||dombeya.info^\n||domdex.com^\n||dominoad.com^\n||dooc.info^\n||doogleonduty.com^\n||doomail.org^\n||dorenga.com^\n||dotandad.com^\n||dotandads.com^\n||double.net^\n||doubleclick.com^\n||doubleclick.net^\n||doubleclickbygoogle.com^\n||doubleclicks.me^\n||doublemax.net^\n||doublepimp.com^\n||doublerads.com^\n||doublerecall.com^\n||doubleverify.com^\n||down1oads.com^\n||downloadboutique.com^\n||downloatransfer.com^\n||downsonglyrics.com^\n||dp25.kr^\n||dpmsrv.com^\n||dpsrexor.com^\n||dpstack.com^\n||dreamaquarium.com^\n||dreamsearch.or.kr^\n||drnxs.com^\n||dromorama.xyz^\n||dropzenad.com^\n||drowle.com^\n||dsero.net^\n||dsnextgen.com^\n||dsnr-affiliates.com^\n||dsultra.com^\n||dt00.net^\n||dt07.net^\n||dtmpub.com^\n||dtzads.com^\n||dualmarket.info^\n||dubshub.com^\n||dudelsa.com^\n||duetads.com^\n||duggiads.com^\n||dumedia.ru^\n||durnowar.com^\n||durokuro.com^\n||durtz.com^\n||dvaminusodin.net^\n||dveribo.ru^\n||dyino.com^\n||dynad.net^\n||dynamicdn.com^\n||dynamicoxygen.com^\n||dynamitedata.com^\n||e-find.co^\n||e-generator.com^\n||e-planning.net^\n||e-viral.com^\n||e2yth.tv^\n||e9mlrvy1.com^\n||eads-adserving.com^\n||eads.to^\n||earnify.com^\n||easy-adserver.com^\n||easyad.com^\n||easydownload4you.com^\n||easyflirt-partners.biz^\n||easyhits4u.com^\n||easyinline.com^\n||ebannertraffic.com^\n||ebayobjects.com.au^\n||ebayobjects.com^\n||ebdr3.com^\n||eblastengine.com^\n||ebuzzing.com^\n||ebz.io^\n||ecpmrocks.com^\n||ecto-ecto-uno.com^\n||edgeads.org^\n||edgevertise.com^\n||edomz.net^\n||eedr.org^\n||effectivemeasure.net^\n||egamingonline.com^\n||ekmas.com^\n||ektezis.ru^\n||elasticad.net^\n||electnext.com^\n||electosake.com^\n||elefantsearch.com^\n||elvate.net^\n||emberads.com^\n||embraceablemidpointcinnabar.com^\n||emediate.ch^\n||emediate.dk^\n||emediate.eu^\n||emediate.se^\n||empiremoney.com^\n||employers-freshly.org^\n||emptyspaceads.com^\n||engineseeker.com^\n||enlnks.com^\n||enterads.com^\n||entrecard.com^\n||entrecard.s3.amazonaws.com^\n||eosads.com^\n||ep7kpqn8.online^\n||epicgameads.com^\n||epnredirect.ru^\n||eptord.com^\n||eptum.com^\n||eqads.com^\n||erado.org^\n||erendri.com^\n||ergerww.net^\n||ergodob.ru^\n||ergoledo.com^\n||ero-advertising.com^\n||erovation.com^\n||erovinmo.com^\n||escalatenetwork.com^\n||escale.to^\n||escokuro.com^\n||especifican.com^\n||essayads.com^\n||essaycoupons.com^\n||et-code.ru^\n||etargetnet.com^\n||etgdta.com^\n||etmanly.ru^\n||etology.com^\n||etrevro.com^\n||eurew.com^\n||euroclick.com^\n||europacash.com^\n||euros4click.de^\n||euym8eel.club^\n||euz.net^\n||evewrite.net^\n||evolvemediallc.com^\n||evolvenation.com^\n||exactdrive.com^\n||excellenceads.com^\n||exchange4media.com^\n||exdynsrv.com^\n||exitexplosion.com^\n||exitjunction.com^\n||exoclick.com^\n||exoneratedresignation.info^\n||explainidentifycoding.info^\n||expocrack.com^\n||expogrim.com^\n||exponential.com^\n||expresswebtraffic.com^\n||extend.tv^\n||extra33.com^\n||eyere.com^\n||eyereturn.com^\n||eyeviewads.com^\n||eyewond.hs.llnwd.net^\n||eyewonder.com^\n||ezadserver.net^\n||ezmob.com^\n||ezoic.net^\n||facebooker.top^\n||faggrim.com^\n||fairadsnetwork.com^\n||falkag.net^\n||fandelcot.com^\n||far-far-star.com^\n||fast2earn.com^\n||fastapi.net^\n||fastates.net^\n||fastclick.net^\n||fasttracktech.biz^\n||fb-plus.com^\n||fbgdc.com^\n||fbsvu.com^\n||fearfulflag.com^\n||featence.com^\n||featuredusers.com^\n||featurelink.com^\n||feed-ads.com^\n||feljack.com^\n||fenixm.com^\n||fiberpairjo.link^\n||ficusoid.xyz^\n||fidel.to^\n||filetarget.com^\n||filtermomosearch.com^\n||fimserve.com^\n||finalanypar.link^\n||fincastavancessetti.info^\n||find-abc.com^\n||find-cheap-hotels.org^\n||findbestsolution.net^\n||findsthat.com^\n||firaxtech.com^\n||firefeeder.com^\n||firegetbook.com^\n||firegetbook4u.biz^\n||firegob.com^\n||firmharborlinked.com^\n||first-rate.com^\n||firstadsolution.com^\n||firstimpression.io^\n||firstlightera.com^\n||fisari.com^\n||fixionmedia.com^\n||fl-ads.com^\n||flagads.net^\n||flappybadger.net^\n||flappyhamster.net^\n||flappysquid.net^\n||flashclicks.com^\n||flashtalking.com^\n||flexlinks.com^\n||fliionos.co.uk^\n||flite.com^\n||fllwert.net^\n||flodonas.com^\n||flomigo.com^\n||fluidads.co^\n||flurryconakrychamfer.info^\n||fluxads.com^\n||flyertown.ca^\n||flymyads.com^\n||flytomars.online^\n||fmpub.net^\n||fmsads.com^\n||fnro4yu0.loan^\n||focalex.com^\n||focre.info^\n||fogzyads.com^\n||foodieblogroll.com^\n||foonad.com^\n||footar.com^\n||footerslideupad.com^\n||footnote.com^\n||forced-lose.de^\n||forcepprofile.com^\n||forex-affiliate.com^\n||forex-affiliate.net^\n||forexyard.com^\n||forifiha.com^\n||forpyke.com^\n||forrestersurveys.com^\n||fphnwvkp.info^\n||frameptp.com^\n||free-domain.net^\n||freebannerswap.co.uk^\n||freebiesurveys.com^\n||freecouponbiz.com^\n||freedownloadsoft.net^\n||freepaidsurveyz.com^\n||freerotator.com^\n||freeskreen.com^\n||freesoftwarelive.com^\n||fresh8.co^\n||friendlyduck.com^\n||fromfriendswithlove.com^\n||fruitkings.com^\n||ftjcfx.com^\n||ftv-publicite.fr^\n||fulltraffic.net^\n||fungoiddempseyimpasse.info^\n||fungus.online^\n||funklicks.com^\n||furginator.pw^\n||fusionads.net^\n||futureresiduals.com^\n||futureus.com^\n||fuurqgbfhvqx.com^\n||fxdepo.com^\n||fxyc0dwa.com^\n||g-cash.biz^\n||g4whisperermedia.com^\n||gagacon.com^\n||gagenez.com^\n||gainmoneyfast.com^\n||galleyn.com^\n||gambling-affiliation.com^\n||game-advertising-online.com^\n||game-clicks.com^\n||gameads.com^\n||gamecetera.com^\n||gamehotus.com^\n||gamersad.com^\n||gamersbanner.com^\n||gamesbannerexchange.com^\n||gamesrevenue.com^\n||gan.doubleclick.net^\n||gandrad.org^\n||gannett.gcion.com^\n||garristo.com^\n||garvmedia.com^\n||gate-ru.com^\n||gatikus.com^\n||gayadnetwork.com^\n||gbkfkofgks.com^\n||gbkfkofgmks.com^\n||gctwh9xc.site^\n||gdmdigital.com^\n||geede.info^\n||geek2us.net^\n||gefhasio.com^\n||geld-internet-verdienen.net^\n||gemineering.com^\n||genericlink.com^\n||genericsteps.com^\n||genesismedia.com^\n||geniad.net^\n||genieessp.com^\n||genotba.online^\n||genovesetacet.com^\n||genusaceracousticophobia.com^\n||geo-idm.fr^\n||geoipads.com^\n||geopromos.com^\n||geovisite.com^\n||gestionpub.com^\n||getfuneta.info^\n||getgamers.eu^\n||getgscfree.com^\n||getpopunder.com^\n||gets-web.space^\n||getscorecash.com^\n||getthislistbuildingvideo.biz^\n||gettipsz.info^\n||ggncpm.com^\n||giantaffiliates.com^\n||gigamega.su^\n||gimiclub.com^\n||gitcdn.pw^\n||gitcdn.site^\n||gitload.site^\n||gk25qeyc.xyz^\n||gklmedia.com^\n||glaswall.online^\n||glical.com^\n||global-success-club.net^\n||globaladsales.com^\n||globaladv.net^\n||globalinteractive.com^\n||globalsuccessclub.com^\n||globaltakeoff.net^\n||glowdot.com^\n||gmads.net^\n||go2jump.org^\n||go2media.org^\n||go2speed.org^\n||goclickon.us^\n||godspeaks.net^\n||goember.com^\n||gogoplexer.com^\n||gogvo.com^\n||gojoingscnow.com^\n||gold-file.com^\n||gold-good4u.com^\n||goodadvert.ru^\n||goodadvertising.info^\n||goodluckblockingthis.com^\n||googleadservicepixel.com^\n||googlesyndicatiion.com^\n||gorgonkil.com^\n||gortags.com^\n||gotagy.com^\n||gotjs.xyz^\n||gourmetads.com^\n||governmenttrainingexchange.com^\n||goviral-content.com^\n||goviral.hs.llnwd.net^\n||gpacalculatorhighschoolfree.com^\n||grabmyads.com^\n||grabo.bg^\n||grafpedia.com^\n||granodiorite.com^\n||grapeshot.co.uk^\n||gratisnetwork.com^\n||green-red.com^\n||greenads.org^\n||greenlabelppc.com^\n||grenstia.com^\n||gretzalz.com^\n||gripdownload.co^\n||grllopa.com^\n||grmtas.com^\n||groovinads.com^\n||groupcommerce.com^\n||grt02.com^\n||grt03.com^\n||grumpyadzen.com^\n||gscontxt.net^\n||gscsystemwithdarren.com^\n||guardiandigitalcomparison.co.uk^\n||guitaralliance.com^\n||gumgum.com^\n||gunpartners.com^\n||gururevenue.com^\n||gwallet.com^\n||gx101.com^\n||h-images.net^\n||h12-media.com^\n||halfpriceozarks.com^\n||hallucius.com^\n||halogennetwork.com^\n||halpeperglagedokkei.info^\n||hanaprop.com^\n||happilyswitching.net^\n||harrenmedianetwork.com^\n||havamedia.net^\n||havetohave.com^\n||havinates.com^\n||havingo.xyz^\n||hb-247.com^\n||hd-plugin.com^\n||hdplayer-download.com^\n||hdplayer.li^\n||hdvid-codecs-dl.net^\n||hdvidcodecs.com^\n||header.tech^\n||headup.com^\n||healthaffiliatesnetwork.com^\n||healthcarestars.com^\n||hebiichigo.com^\n||helloreverb.com^\n||helotero.com^\n||heracgjcuqmk.com^\n||heravda.com^\n||herocpm.com^\n||hexagram.com^\n||hgdat.com^\n||hiadone.com^\n||hijacksystem.com^\n||hilltopads.net^\n||himediads.com^\n||himediadx.com^\n||hipersushiads.com^\n||hiplair.com^\n||histians.com^\n||hit-now.com^\n||hits.sys.lv^\n||hitwastedgarden.com^\n||hlads.com^\n||hlu9tseh.men^\n||hmongcash.com^\n||hokaybo.com^\n||hola-shopping.com^\n||holdingprice.net^\n||holidaytravelguide.org^\n||honestlypopularvary.xyz^\n||hoomezip.biz^\n||hopfeed.com^\n||horse-racing-affiliate-program.co.uk^\n||horsered.com^\n||hortestoz.com^\n||horyzon-media.com^\n||hostgit.net^\n||hosticanaffiliate.com^\n||hot-hits.us^\n||hotelscombined.com.au^\n||hotfeed.net^\n||hotkeys.com^\n||hotptp.com^\n||hotwords.com.br^\n||hotwords.com.mx^\n||hotwords.com^\n||houstion.com^\n||hover.in^\n||hoverr.co^\n||hoverr.media^\n||howtodoblog.com^\n||hplose.de^\n||hsslx.com^\n||hstpnetwork.com^\n||htl.bid^\n||htmlhubing.xyz^\n||httpool.com^\n||httpsecurity.org^\n||hulahooprect.com^\n||huzonico.com^\n||hype-ads.com^\n||hypeads.org^\n||hypemakers.net^\n||hyperbanner.net^\n||hyperlinksecure.com^\n||hyperpromote.com^\n||hypertrackeraff.com^\n||hypervre.com^\n||hyperwebads.com^\n||i-media.co.nz^\n||i.skimresources.com^\n||iamediaserve.com^\n||iasbetaffiliates.com^\n||iasrv.com^\n||ibannerexchange.com^\n||ibatom.com^\n||ibryte.com^\n||icdirect.com^\n||icqadvnew.com^\n||idealmedia.com^\n||identads.com^\n||idownloadgalore.com^\n||idreammedia.com^\n||ieh1ook.bid^\n||ifmnwi.club^\n||iframe.mediaplazza.com^\n||igameunion.com^\n||igloohq.com^\n||ignitioninstaller.com^\n||iicheewi.com^\n||ikzikistheking.com^\n||imageadnet.com^\n||imedia.co.il^\n||imediaaudiences.com^\n||imediarevenue.com^\n||img-giganto.net^\n||imgfeedget.com^\n||imglt.com^\n||imgsniper.com^\n||imgtty.com^\n||imgwebfeed.com^\n||imho.ru^\n||imiclk.com^\n||imonomy.com^\n||imp*.tradedoubler.com^\n||impact-ad.jp^\n||impactradius.com^\n||implix.com^\n||impore.com^\n||impresionesweb.com^\n||impressionaffiliate.com^\n||impressionaffiliate.mobi^\n||impressioncontent.info^\n||impressiondesk.com^\n||impressionperformance.biz^\n||impressionvalue.mobi^\n||in-appadvertising.com^\n||incentaclick.com^\n||incloak.com^\n||incomeliberation.com^\n||increas.eu^\n||increase-marketing.com^\n||indeterman.com^\n||indexww.com^\n||indiabanner.com^\n||indiads.com^\n||indianbannerexchange.com^\n||indianlinkexchange.com^\n||indicate.to^\n||indieclick.com^\n||indisancal.com^\n||indofad.com^\n||industrybrains.com^\n||inentasky.com^\n||inetinteractive.com^\n||infectiousmedia.com^\n||infinite-ads.com^\n||infinityads.com^\n||influads.com^\n||info4.a7.org^\n||infolinks.com^\n||information-sale.com^\n||infra-ad.com^\n||ingame.ad^\n||inktad.com^\n||innity.com^\n||innity.net^\n||innovid.com^\n||insightexpress.com^\n||insightexpressai.com^\n||insitepromotion.com^\n||insitesystems.com^\n||inskinad.com^\n||inskinmedia.com^\n||inspiringsweater.xyz^\n||insta-cash.net^\n||instancetour.info^\n||instantbannercreator.com^\n||instantclk.com^\n||instantdollarz.com^\n||insticator.com^\n||instinctiveads.com^\n||instivate.com^\n||instraffic.com^\n||instreamvideo.ru^\n||integral-marketing.com^\n||intellibanners.com^\n||intellitxt.com^\n||intenthq.com^\n||intentmedia.net^\n||interactivespot.net^\n||interclick.com^\n||interestably.com^\n||interesting.cc^\n||intergi.com^\n||intermarkets.net^\n||internetadbrokers.com^\n||interpolls.com^\n||interworksmedia.co.kr^\n||intextad.net^\n||intextdirect.com^\n||intextscript.com^\n||intextual.net^\n||intgr.net^\n||intopicmedia.com^\n||inttrax.com^\n||intuneads.com^\n||inuvo.com^\n||inuxu.biz^\n||inuxu.co.in^\n||invernetter.info^\n||investingchannel.com^\n||inviziads.com^\n||iocawy99.science^\n||ip-adress.com^\n||ipowercdn.com^\n||ipredictive.com^\n||ipromote.com^\n||ipsowrite.com^\n||isapi.solutions^\n||isohits.com^\n||isparkmedia.com^\n||isubdom.com^\n||isubdomains.com^\n||it4oop7.bid^\n||itempana.site^\n||itrengia.com^\n||iu16wmye.com^\n||iu1xoe7o.com^\n||iv.doubleclick.net^\n||iwantmoar.net^\n||ixnp.com^\n||iz319xlstbsqs34623cb.com^\n||izeads.com^\n||izoyshe6.review^\n||j2ef76da3.website^\n||j4y01i3o.win^\n||jacquarter.com^\n||jadcenter.com^\n||jango.com^\n||jangonetwork.com^\n||jarvinzo.com^\n||javacript.cf^\n||javacript.ga^\n||javacript.gq^\n||javacript.ml^\n||jbrlsr.com^\n||jdproject.net^\n||jeetyetmedia.com^\n||jemmgroup.com^\n||jf2mn2ms.club^\n||jfx61qca.site^\n||jiawen88.com^\n||jivox.com^\n||jiwire.com^\n||jizzontoy.com^\n||jmp9.com^\n||jmvnolvmspponhnyd6b.com^\n||jo7cofh3.com^\n||jobsyndicate.com^\n||jobtarget.com^\n||joytocash.com^\n||jque.net^\n||js.cdn.ac^\n||jscloud.org^\n||jscount.com^\n||jsfeedadsget.com^\n||jsretra.com^\n||jssearch.net^\n||jtrakk.com^\n||judicated.com^\n||juiceadv.com^\n||juiceadv.net^\n||juicyads.com^\n||jujuads.com^\n||jujzh9va.com^\n||jumboaffiliates.com^\n||jumbolt.ru^\n||jumpelead.com^\n||jumptap.com^\n||jursp.com^\n||justrelevant.com^\n||jwaavsze.com^\n||jyvtidkx.com^\n||jzeu6qlk.accountant^\n||k0z09okc.com^\n||k9anf8bc.webcam^\n||kanoodle.com^\n||kantarmedia.com^\n||kavanga.ru^\n||keewurd.com^\n||kehalim.com^\n||kenduktur.com^\n||kerg.net^\n||ketads.com^\n||ketoo.com^\n||keyrunmodel.com^\n||keywordblocks.com^\n||keywordlink.co.kr^\n||keywordpop.com^\n||keywordsconnect.com^\n||kgidpryrz8u2v0rz37.com^\n||kikuzip.com^\n||kinley.com^\n||kintokup.com^\n||kiosked.com^\n||kitnmedia.com^\n||kjgh5o.com^\n||klikadvertising.com^\n||kliksaya.com^\n||klikvip.com^\n||klipmart.com^\n||klixfeed.com^\n||klnrew.site^\n||kloapers.com^\n||klonedaset.org^\n||knorex.asia^\n||knowd.com^\n||kolition.com^\n||komoona.com^\n||kontextua.com^\n||koocash.com^\n||korrelate.net^\n||kovla.com^\n||kr3vinsx.com^\n||kromeleta.ru^\n||kumpulblogger.com^\n||kzkjewg7.stream^\n||l3op.info^\n||ladbrokesaffiliates.com.au^\n||laim.tv^\n||lakequincy.com^\n||lakidar.net^\n||lamiflor.xyz^\n||landelcut.com^\n||lanistaconcepts.com^\n||larentisol.com^\n||large-format.net^\n||largestable.com^\n||laserhairremovalstore.com^\n||launchbit.com^\n||layer-ad.org^\n||layerloop.com^\n||layerwelt.com^\n||lazynerd.info^\n||lbm1.com^\n||lcl2adserver.com^\n||ldgateway.com^\n||lduhtrp.net^\n||leadacceptor.com^\n||leadad.mobi^\n||leadadvert.info^\n||leadbolt.net^\n||leadcola.com^\n||leaderpub.fr^\n||leadmediapartners.com^\n||leaptrade.com^\n||leetmedia.com^\n||legisland.net^\n||leohd59.ru^\n||lepinsar.com^\n||lepintor.com^\n||letadnew.com^\n||letilyadothejob.com^\n||letsadvertisetogether.com^\n||letsgoshopping.tk^\n||letysheeps.ru^\n||lfstmedia.com^\n||lgse.com^\n||liftdna.com^\n||ligadx.com^\n||ligational.com^\n||lightad.co.kr^\n||lightningcast.net^\n||linicom.co.il^\n||linkbuddies.com^\n||linkclicks.com^\n||linkelevator.com^\n||linkexchange.com^\n||linkexchangers.net^\n||linkgrand.com^\n||linkmads.com^\n||linkoffers.net^\n||linkreferral.com^\n||links.io^\n||linkshowoff.com^\n||linksmart.com^\n||linkstorm.net^\n||linkwash.de^\n||linkworth.com^\n||linkybank.com^\n||linkz.net^\n||linoleictanzaniatitanic.com^\n||lionsads.com^\n||liqwid.net^\n||listingcafe.com^\n||liveadexchanger.com^\n||liveadoptimizer.com^\n||liveadserver.net^\n||liverail.com^\n||liveuniversenetwork.com^\n||lkqd.net^\n||lndjj.com^\n||loading-resource.com^\n||localadbuy.com^\n||localedgemedia.com^\n||localsearch24.co.uk^\n||lockerdome.com^\n||lockhosts.com^\n||lockscalecompare.com^\n||logo-net.co.uk^\n||loodyas.com^\n||lookit-quick.com^\n||looksmart.com^\n||looneyads.com^\n||looneynetwork.com^\n||loopmaze.com^\n||lose-ads.de^\n||loseads.eu^\n||losomy.com^\n||lostelephants.xyz^\n||lotteryaffiliates.com^\n||love-banner.com^\n||loxtk.com^\n||lqcdn.com^\n||lqw.me^\n||ltassrv.com.s3.amazonaws.com^\n||lucidmedia.com^\n||lushcrush.com^\n||luxadv.com^\n||luxbetaffiliates.com.au^\n||luxup.ru^\n||lx2rv.com^\n||lzjl.com^\n||m10s8.com^\n||m2.ai^\n||m2pub.com^\n||m4pub.com^\n||m57ku6sm.com^\n||m5prod.net^\n||mabirol.com^\n||machings.com^\n||madadsmedia.com^\n||madserving.com^\n||madsone.com^\n||magicalled.info^\n||magnetisemedia.com^\n||mailmarketingmachine.com^\n||mainadv.com^\n||mainroll.com^\n||makecashtakingsurveys.biz^\n||makemoneymakemoney.net^\n||mallsponsor.com^\n||mangoforex.com^\n||mansiontheologysoon.xyz^\n||marbil24.co.za^\n||marginalwoodfernrounddance.com^\n||marimedia.com^\n||markergot.com^\n||marketbanker.com^\n||marketfly.net^\n||marketgid.com^\n||markethealth.com^\n||marketingenhanced.com^\n||marketleverage.com^\n||marketnetwork.com^\n||marketoring.com^\n||marsads.com^\n||martiniadnetwork.com^\n||masterads.org^\n||masternal.com^\n||mastertraffic.cn^\n||mathads.com^\n||matiro.com^\n||maudau.com^\n||maxserving.com^\n||mb01.com^\n||mb102.com^\n||mb104.com^\n||mb38.com^\n||mb57.com^\n||mb8e17f12.website^\n||mbn.com.ua^\n||mcdomainalot.com^\n||mcdstorage.com^\n||mdadvertising.net^\n||mdadx.com^\n||mdialog.com^\n||mdn2015x1.com^\n||mdn2015x2.com^\n||mdn2015x3.com^\n||mdn2015x4.com^\n||mdn2015x5.com^\n||meadigital.com^\n||measurelyapp.com^\n||media-general.com^\n||media-ks.net^\n||media-networks.ru^\n||media-servers.net^\n||media.net^\n||media303.com^\n||media6degrees.com^\n||media970.com^\n||mediaadserver.org^\n||mediaclick.com^\n||mediacpm.com^\n||mediaessence.net^\n||mediaffiliation.com^\n||mediafilesdownload.com^\n||mediaflire.com^\n||mediaforce.com^\n||mediaforge.com^\n||mediag4.com^\n||mediagridwork.com^\n||mediakeywords.com^\n||medialand.ru^\n||medialation.net^\n||mediaonenetwork.net^\n||mediaonpro.com^\n||mediapeo.com^\n||mediaraily.com^\n||mediatarget.com^\n||mediative.ca^\n||mediative.com^\n||mediatraffic.com^\n||mediatraks.com^\n||mediaver.com^\n||medleyads.com^\n||medrx.sensis.com.au^\n||medyanet.net^\n||medyanetads.com^\n||meendocash.com^\n||meetic-partners.com^\n||megaad.nz^\n||megacpm.com^\n||megapopads.com^\n||megatronmailer.com^\n||megbase.com^\n||meinlist.com^\n||mellowads.com^\n||mengheng.net^\n||mentad.com^\n||mentalks.ru^\n||merchenta.com^\n||mercuras.com^\n||messagespaceads.com^\n||metaffiliation.com^\n||metaffiliation.com^*^maff=\n||metaffiliation.com^*^taff=\n||metavertising.com^\n||metavertizer.com^\n||metogo.work^\n||metrics.io^\n||meviodisplayads.com^\n||meya41w7.com^\n||mezaa.com^\n||mezimedia.com^\n||mftracking.com^\n||mgcash.com^\n||mgcashgate.com^\n||mgid.com^\n||mgplatform.com^\n||mi-mi-fa.com^\n||mibebu.com^\n||microad.jp^\n||microadinc.com^\n||microsoftaffiliates.net^\n||milabra.com^\n||mindlytix.com^\n||minimumpay.info^\n||minodazi.com^\n||mintake.com^\n||mirago.com^\n||mirrorpersonalinjury.co.uk^\n||mistands.com^\n||mixmarket.biz^\n||mixpo.com^\n||mktseek.com^\n||mlnadvertising.com^\n||mmadsgadget.com^\n||mmgads.com^\n||mmismm.com^\n||mmngte.net^\n||mmo123.co^\n||mmondi.com^\n||mmoptional.com^\n||mmotraffic.com^\n||mnetads.com^\n||moatads.com^\n||mobatori.com^\n||mobatory.com^\n||mobday.com^\n||mobfox.com^\n||mobicont.com^\n||mobidevdom.com^\n||mobifobi.com^\n||mobikano.com^\n||mobile-10.com^\n||mobiright.com^\n||mobisla.com^\n||mobitracker.info^\n||mobiyield.com^\n||moborobot.com^\n||mobsterbird.info^\n||mobstitialtag.com^\n||mobstrks.com^\n||mobtrks.com^\n||mobytrks.com^\n||modelegating.com^\n||moffsets.com^\n||mogointeractive.com^\n||mojoaffiliates.com^\n||mokonocdn.com^\n||monetizer101.com^\n||money-cpm.fr^\n||money4ads.com^\n||moneycosmos.com^\n||moneywhisper.com^\n||monkeybroker.net^\n||monsoonads.com^\n||mookie1.com^\n||mootermedia.com^\n||mooxar.com^\n||moregamers.com^\n||moreplayerz.com^\n||morgdm.ru^\n||mosaicolor.website^\n||moselats.com^\n||mottnow.com^\n||movad.net^\n||mozcloud.net^\n||mp3toavi.xyz^\n||mpnrs.com^\n||mpression.net^\n||mprezchc.com^\n||mrperfect.in^\n||msads.net^\n||msypr.com^\n||mtagmonetizationa.com^\n||mtagmonetizationb.com^\n||mtagmonetizationc.com^\n||mtrcss.com^\n||mujap.com^\n||multiadserv.com^\n||multiview.com^\n||munically.com^\n||music-desktop.com^\n||musicnote.info^\n||mutary.com^\n||mxf.dfp.host^\n||mxtads.com^\n||my-layer.net^\n||myaffiliates.com^\n||mycasinoaccounts.com^\n||myclickbankads.com^\n||mycooliframe.net^\n||mydreamads.com^\n||myemailbox.info^\n||myinfotopia.com^\n||mylinkbox.com^\n||mynativeads.com^\n||mynewcarquote.us^\n||myplayerhd.net^\n||mysafeurl.com^\n||mystaticfiles.com^\n||mythings.com^\n||myuniques.ru^\n||myvads.com^\n||mywidget.mobi^\n||mz28ismn.com^\n||n130adserv.com^\n||n161adserv.com^\n||n388hkxg.com^\n||n4403ad.doubleclick.net^\n||nabbr.com^\n||naganaga.lol^\n||nagrande.com^\n||nanigans.com^\n||nasdak.in^\n||native-adserver.com^\n||nativead.co^\n||nativead.tech^\n||nativeads.com^\n||nativeadsfeed.com^\n||nativeleads.net^\n||nbjmp.com^\n||nbstatic.com^\n||ncrjsserver.com^\n||neblotech.com^\n||negolist.com^\n||neo-neo-xeo.com^\n||neobux.com^\n||neodatagroup.com^\n||neoebiz.co.kr^\n||neoffic.com^\n||net-ad-vantage.com^\n||net3media.com^\n||netaffiliation.com^\n||netavenir.com^\n||netflixalternative.net^\n||netliker.com^\n||netloader.cc^\n||netpondads.com^\n||netrosol.net^\n||netseer.com^\n||netshelter.net^\n||netsolads.com^\n||networkplay.in^\n||networkxi.com^\n||networld.hk^\n||networldmedia.net^\n||neudesicmediagroup.com^\n||new-new-years.com^\n||newdosug.eu^\n||newgentraffic.com^\n||newideasdaily.com^\n||newsadstream.com^\n||newsmaxfeednetwork.com^\n||newsnet.in.ua^\n||newstogram.com^\n||newtention.net^\n||newyorkwhil.com^\n||nexac.com^\n||nexage.com^\n||nextlandingads.com^\n||nextmobilecash.com^\n||ngecity.com^\n||nglmedia.com^\n||nicheadgenerator.com^\n||nicheads.com^\n||nighter.club^\n||njkiho.info^\n||nkredir.com^\n||nm7xq628.click^\n||nmcdn.us^\n||nmwrdr.net^\n||nobleppc.com^\n||nobsetfinvestor.com^\n||nonstoppartner.de^\n||norentisol.com^\n||noretia.com^\n||normkela.com^\n||northmay.com^\n||nothering.com^\n||novarevenue.com^\n||nowlooking.net^\n||nowspots.com^\n||nplexmedia.com^\n||npvos.com^\n||nquchhfyex.com^\n||nrnma.com^\n||nscontext.com^\n||nsdsvc.com^\n||nsmartad.com^\n||nspmotion.com^\n||nsstatic.net^\n||nster.net^\n||ntent.com^\n||ntv.io^\n||nuclersoncanthinger.info^\n||nui.media^\n||nullenabler.com^\n||numberium.com^\n||numbers.md^\n||numberthreebear.com^\n||nuseek.com^\n||nvadn.com^\n||nvero.net^\n||nwfhalifax.com^\n||nxtck.com^\n||nyadmcncserve-05y06a.com^\n||nzads.net.nz^\n||nzphoenix.com^\n||o.gweini.com^\n||oads.co^\n||oainternetservices.com^\n||obeisantcloddishprocrustes.com^\n||obesw.com^\n||obeus.com^\n||obibanners.com^\n||objects.tremormedia.com^\n||objectservers.com^\n||oceanwebcraft.com^\n||oclaserver.com^\n||oclasrv.com^\n||oclsasrv.com^\n||oclus.com^\n||oehposan.com^\n||offeradvertising.biz^\n||offerforge.com^\n||offerpalads.com^\n||offerserve.com^\n||offersquared.com^\n||officerrecordscale.info^\n||ofino.ru^\n||ogercron.com^\n||oggifinogi.com^\n||ohmcasting.com^\n||ohmwrite.com^\n||oileddaintiessunset.info^\n||oldership.com^\n||oldtiger.net^\n||omclick.com^\n||omg2.com^\n||omni-ads.com^\n||omnitagjs.com^\n||onad.eu^\n||onads.com^\n||onclasrv.com^\n||onclickads.net^\n||onedmp.com^\n||onenetworkdirect.com^\n||onenetworkdirect.net^\n||oneopenclose.click^\n||onespot.com^\n||online-adnetwork.com^\n||online-media24.de^\n||onlineadtracker.co.uk^\n||onlinedl.info^\n||onlyalad.net^\n||onrampadvertising.com^\n||onscroll.com^\n||onsitemarketplace.net^\n||onvertise.com^\n||onwsys.net^\n||oodode.com^\n||ooecyaauiz.com^\n||oofte.com^\n||oos4l.com^\n||opap.co.kr^\n||openbook.net^\n||openclose.click^\n||openetray.com^\n||opensourceadvertisementnetwork.info^\n||openx.net^\n||openxadexchange.com^\n||openxenterprise.com^\n||openxmarket.asia^\n||operatical.com^\n||opt-intelligence.com^\n||opt-n.net^\n||opteama.com^\n||optiad.net^\n||optimalroi.info^\n||optimatic.com^\n||optimizeadvert.biz^\n||optimizesocial.com^\n||optinemailpro.com^\n||optinmonster.com^\n||orangeads.fr^\n||orarala.com^\n||oratosaeron.com^\n||orbengine.com^\n||ordingly.com^\n||oriel.io^\n||osiaffiliate.com^\n||oskale.ru^\n||ospreymedialp.com^\n||othersonline.com^\n||ourunlimitedleads.com^\n||ov8pc.tv^\n||oveld.com^\n||overhaps.com^\n||oversailor.com^\n||overture.com^\n||overturs.com^\n||ovtopli.ru^\n||owlads.io^\n||oxado.com^\n||oxsng.com^\n||oxtracking.com^\n||oxybe.com^\n||ozertesa.com^\n||ozonemedia.com^\n||p-advg.com^\n||p-comme-performance.com^\n||p-digital-server.com^\n||p2ads.com^\n||p7hwvdb4p.com^\n||paads.dk^\n||padsdelivery.com^\n||padstm.com^\n||pagefair.net^\n||pagesinxt.com^\n||paid4ad.de^\n||paidonresults.net^\n||paidsearchexperts.com^\n||pakbanners.com^\n||panachetech.com^\n||panatran.xyz^\n||pantherads.com^\n||paperg.com^\n||paradocs.ru^\n||parkingcrew.net^\n||partner-ads.com^\n||partner.googleadservices.com^\n||partner.video.syndication.msn.com^\n||partnerearning.com^\n||partnermax.de^\n||partycasino.com^\n||partypartners.com^\n||partypoker.com^\n||parwrite.com^\n||pas-rahav.com^\n||passionfruitads.com^\n||passive-earner.com^\n||pautaspr.com^\n||pay-click.ru^\n||paydotcom.com^\n||payperpost.com^\n||pc-ads.com^\n||pdn-2.com^\n||pe2k2dty.com^\n||peakclick.com^\n||pebblemedia.be^\n||peelawaymaker.com^\n||peemee.com^\n||peer39.com^\n||peer39.net^\n||penuma.com^\n||pepperjamnetwork.com^\n||percularity.com^\n||peremoga.xyz^\n||perfb.com^\n||perfcreatives.com^\n||perfectmarket.com^\n||perfoormapp.info^\n||performance-based.com^\n||performanceadvertising.mobi^\n||performancetrack.info^\n||performancingads.com^\n||persevered.com^\n||pezrphjl.com^\n||pgmediaserve.com^\n||pgpartner.com^\n||pgssl.com^\n||pharmcash.com^\n||pheedo.com^\n||philbardre.com^\n||philipstreehouse.info^\n||philosophere.com^\n||phonespybubble.com^\n||pianobuyerdeals.com^\n||picadmedia.com^\n||picbucks.com^\n||picsti.com^\n||pictela.net^\n||piercial.com^\n||pinballpublishernetwork.com^\n||pioneeringad.com^\n||pip-pip-pop.com^\n||pipeaota.com^\n||pipsol.net^\n||piticlik.com^\n||pivotalmedialabs.com^\n||pivotrunner.com^\n||pixazza.com^\n||pixeltrack66.com^\n||pixfuture.net^\n||pixiv.org^\n||pixxur.com^\n||plannto.com^\n||platinumadvertisement.com^\n||play24.us^\n||playertraffic.com^\n||playukinternet.com^\n||pleasesavemyimages.com^\n||pleeko.com^\n||plenomedia.com^\n||plexop.net^\n||pllddc.com^\n||plocap.com^\n||plugerr.com^\n||plugs.co^\n||plusfind.net^\n||plushlikegarnier.com^\n||plxserve.com^\n||pmsrvr.com^\n||pnoss.com^\n||pointclicktrack.com^\n||pointroll.com^\n||points2shop.com^\n||polanders.com^\n||polluxnetwork.com^\n||polmontventures.com^\n||polyad.net^\n||polydarth.com^\n||poolnoodle.tech^\n||popads.net^\n||popadscdn.net^\n||popcash.net^\n||popcpm.com^\n||popcpv.com^\n||popearn.com^\n||popmajor.com^\n||popmarker.com^\n||popmyad.com^\n||popmyads.com^\n||poponclick.com^\n||poppysol.com^\n||poprev.net^\n||poprevenue.net^\n||popsads.com^\n||popshow.info^\n||poptarts.me^\n||poptm.com^\n||popularitish.com^\n||popularmedia.net^\n||populis.com^\n||populisengage.com^\n||popunder.ru^\n||popundertotal.com^\n||popunderz.com^\n||popuptraffic.com^\n||popupvia.com^\n||popwin.net^\n||pornv.org^\n||porojo.net^\n||portkingric.net^\n||posternel.com^\n||postrelease.com^\n||potcityzip.com^\n||poundaccordexecute.info^\n||poweradvertising.co.uk^\n||powerfulbusiness.net^\n||powerlinks.com^\n||powermarketing.com^\n||ppcindo.com^\n||ppclinking.com^\n||ppctrck.com^\n||ppcwebspy.com^\n||ppsearcher.ru^\n||prebid.org^\n||precisionclick.com^\n||predictad.com^\n||predictivadnetwork.com^\n||prestadsng.com^\n||prexista.com^\n||prf.hn^\n||prickac.com^\n||primaryads.com^\n||pritesol.com^\n||privilegebedroomlate.xyz^\n||prizel.com^\n||prm-native.com^\n||pro-advert.de^\n||pro-advertising.com^\n||pro-market.net^\n||pro-pro-go.com^\n||proadsdirect.com^\n||probannerswap.com^\n||prod.untd.com^\n||proffigurufast.com^\n||profitpeelers.com^\n||programresolver.net^\n||projectwonderful.com^\n||promenadd.ru^\n||promo-reklama.ru^\n||promobenef.com^\n||promoted.com^\n||promotionoffer.mobi^\n||promotiontrack.mobi^\n||propellerads.com^\n||propellerpops.com^\n||propelplus.com^\n||proper.io^\n||prorentisol.com^\n||prosperent.com^\n||protally.net^\n||provider-direct.com^\n||proximic.com^\n||prre.ru^\n||prxio.github.io^\n||prxio.pw^\n||prxio.site^\n||psclicks.com^\n||pseqcs05.com^\n||psma02.com^\n||psnmail.su^\n||ptmopenclose.click^\n||ptmzr.com^\n||ptp.lolco.net^\n||ptp22.com^\n||ptp24.com^\n||pub-fit.com^\n||pubdirecte.com^\n||pubgears.com^\n||publicidad.net^\n||publicityclerks.com^\n||publicsunrise.link^\n||publir.com^\n||publisher.to^\n||publisheradnetwork.com^\n||pubmatic.com^\n||pubmine.com^\n||pubnation.com^\n||pubrain.com^\n||pubserve.net^\n||pubted.com^\n||puhtml.com^\n||pullapi.site^\n||pullcdn.top^\n||pulpyads.com^\n||pulse360.com^\n||pulsemgr.com^\n||purpleflag.net^\n||puserving.com^\n||push2check.com^\n||pwrads.net^\n||pxl2015x1.com^\n||pxstda.com^\n||pzaasocba.com^\n||pzuwqncdai.com^\n||q1media.com^\n||q1mediahydraplatform.com^\n||q1xyxm89.com^\n||q45nsj9d.accountant^\n||qadserve.com^\n||qadservice.com^\n||qdmil.com^\n||qertewrt.com^\n||qksrv.net^\n||qksz.net^\n||qnrzmapdcc.com^\n||qnsr.com^\n||qrlsx.com^\n||qservz.com^\n||qualitypageviews.com^\n||quantumads.com^\n||queenmult.link^\n||quensillo.com^\n||queryly.com^\n||questionmarket.com^\n||questus.com^\n||quickcash500.com^\n||quideo.men^\n||quinstreet.com^\n||qwobl.net^\n||qwun46bs.review^\n||qwzmje9w.com^\n||qyh7u6wo0c8vz0szdhnvbn.com^\n||r66net.com^\n||r66net.net^\n||r91c6tvs.science^\n||rabilitan.com^\n||radeant.com^\n||radiatorial.online^\n||radicalwealthformula.com^\n||radiusmarketing.com^\n||ragapa.com^\n||raiggy.com^\n||rainbowtgx.com^\n||rainwealth.com^\n||rampanel.com^\n||rapt.com^\n||rateaccept.net^\n||rawasy.com^\n||rbnt.org^\n||rcads.net^\n||rcurn.com^\n||rddywd.com^\n||reachjunction.com^\n||reachlocal.com^\n||reachmode.com^\n||reactx.com^\n||readserver.net^\n||realclick.co.kr^\n||realmatch.com^\n||realmedia.com^\n||realsecuredredir.com^\n||realsecuredredirect.com^\n||realssp.co.kr^\n||realvu.net^\n||reate.info^\n||recentres.com^\n||recomendedsite.com^\n||redcourtside.com^\n||redintelligence.net^\n||redirectpopads.com^\n||rediskina.com^\n||redpeepers.com^\n||redstick.online^\n||reduxmediagroup.com^\n||reelcentric.com^\n||refban.com^\n||referback.com^\n||regdfh.info^\n||registry.cw.cm^\n||regurgical.com^\n||reklamz.com^\n||relatedweboffers.com^\n||relestar.com^\n||relevanti.com^\n||relytec.com^\n||remintrex.com^\n||remiroyal.ro^\n||repaynik.com^\n||replacescript.in^\n||replase.cf^\n||requiredcollectfilm.info^\n||resideral.com^\n||resonance.pk^\n||respecific.net^\n||respond-adserver.cloudapp.net^\n||respondhq.com^\n||resultlinks.com^\n||resultsz.com^\n||retargeter.com^\n||retono42.us^\n||retrayan.com^\n||rev2pub.com^\n||revcontent.com^\n||revdepo.com^\n||revenue.com^\n||revenuegiants.com^\n||revenuehits.com^\n||revenuemantra.com^\n||revenuemax.de^\n||revfusion.net^\n||revmob.com^\n||revnuehub.com^\n||revokinets.com^\n||revresda.com^\n||revresponse.com^\n||revsci.net^\n||rewardisement.com^\n||rewardsaffiliates.com^\n||rfgsi.com^\n||rfihub.net^\n||rhown.com^\n||rhythmcontent.com^\n||rhythmxchange.com^\n||ric-ric-rum.com^\n||ricead.com^\n||richmedia247.com^\n||richwebmedia.com^\n||ringtonematcher.com^\n||ringtonepartner.com^\n||riowrite.com^\n||ripplead.com^\n||riverbanksand.com^\n||rixaka.com^\n||rkgnmwre.site^\n||rmxads.com^\n||rnmd.net^\n||robocat.me^\n||rocketier.net^\n||rogueaffiliatesystem.com^\n||roicharger.com^\n||roirocket.com^\n||rolinda.work^\n||romance-net.com^\n||rometroit.com^\n||rotaban.ru^\n||rotatingad.com^\n||rotorads.com^\n||rovion.com^\n||roxyaffiliates.com^\n||rpts.org^\n||rtbidder.net^\n||rtbmedia.org^\n||rtbpop.com^\n||rtbpops.com^\n||rtk.io^\n||rubiconproject.com^\n||ruckusschroederraspberry.com^\n||rue1mi4.bid^\n||rummyaffiliates.com^\n||runadtag.com^\n||runreproducerow.com^\n||rvtlife.com^\n||rvttrack.com^\n||rwpads.com^\n||rxlex.faith^\n||ryminos.com^\n||s.adroll.com^\n||s2d6.com^\n||sa.entireweb.com^\n||sa2eoqu.bid^\n||safeadnetworkdata.net^\n||safecllc.com^\n||safelistextreme.com^\n||sakura-traffic.com^\n||salesnleads.com^\n||saltamendors.com^\n||salvador24.com^\n||samvaulter.com^\n||samvinva.info^\n||saoboo.com^\n||sape.ru^\n||saple.net^\n||satgreera.com^\n||saveads.net^\n||saveads.org^\n||sayadcoltd.com^\n||saymedia.com^\n||sba.about.co.kr^\n||sbaffiliates.com^\n||sbcpower.com^\n||sc-f6eade8.js^\n||scanmedios.com^\n||scanscout.com^\n||sceno.ru^\n||scootloor.com^\n||scrap.me^\n||scratchaffs.com^\n||scriptall.cf^\n||scriptall.ga^\n||scriptall.gq^\n||scriptall.tk^\n||search123.uk.com^\n||seccoads.com^\n||secondstreetmedia.com^\n||secure-softwaremanager.com^\n||securesoft.info^\n||securewebsiteaccess.com^\n||securitain.com^\n||sedoparking.com^\n||seductionprofits.com^\n||seekads.net^\n||sekindo.com^\n||selectablemedia.com^\n||sellhealth.com^\n||selsin.net^\n||semanticrep.com^\n||sendptp.com^\n||senzapudore.net^\n||serialbay.com^\n||seriousfiles.com^\n||servali.net^\n||serve-sys.com^\n||servebom.com^\n||servedby-buysellads.com^\n||servedbyadbutler.com^\n||servedbyopenx.com^\n||servemeads.com^\n||servicegetbook.net^\n||serving-system.com^\n||sethads.info^\n||sev4ifmxa.com^\n||sevenads.net^\n||sevendaystart.com^\n||sexmoney.com^\n||shakamech.com^\n||shallowschool.com^\n||share-server.com^\n||sharecash.org^\n||sharegods.com^\n||shareresults.com^\n||sharethrough.com^\n||shipthankrecognizing.info^\n||shokala.com^\n||shoogloonetwork.com^\n||shopalyst.com^\n||shoppingads.com^\n||shopzyapp.com^\n||showyoursite.com^\n||siamzone.com^\n||silence-ads.com^\n||silstavo.com^\n||silverads.net^\n||simpio.com^\n||simply.com^\n||simplyhired.com^\n||simvinvo.com^\n||sirfad.com^\n||sitebrand.com^\n||siteencore.com^\n||sitescout.com^\n||sitescoutadserver.com^\n||sitesense-oo.com^\n||sitethree.com^\n||sittiad.com^\n||skimlinks.com^\n||skinected.com^\n||skoovyads.com^\n||skyactivate.com^\n||skyscrpr.com^\n||skytemjo.link^\n||skywarts.ru^\n||slfpu.com^\n||slikslik.com^\n||slimspots.com^\n||slimtrade.com^\n||slinse.com^\n||slopeaota.com^\n||smaclick.com^\n||smart-feed-online.com^\n||smart.allocine.fr^\n||smart2.allocine.fr^\n||smartad.ee^\n||smartadserver.com^\n||smartdevicemedia.com^\n||smarterdownloads.net^\n||smartredirect.de^\n||smarttargetting.co.uk^\n||smarttargetting.com^\n||smarttargetting.net^\n||smarttds.ru^\n||smartyads.com^\n||smileycentral.com^\n||smilyes4u.com^\n||smowtion.com^\n||smpgfx.com^\n||sms-mmm.com^\n||sn00.net^\n||snap.com^\n||sndkorea.co.kr^\n||so-excited.com^\n||sochr.com^\n||socialbirth.com^\n||socialelective.com^\n||sociallypublish.com^\n||socialmedia.com^\n||socialreach.com^\n||socialspark.com^\n||society6.com^\n||sociocast.com^\n||sociomantic.com^\n||sodud.com^\n||soft4dle.com^\n||softonicads.com^\n||softpopads.com^\n||softwares2015.com^\n||sokitosa.com^\n||solapoka.com^\n||solarmosa.com^\n||solocpm.com^\n||solutionzip.info^\n||sonnerie.net^\n||sonobi.com^\n||soosooka.com^\n||sophiasearch.com^\n||sotuktraffic.com^\n||sparkstudios.com^\n||specificclick.net^\n||specificmedia.com^\n||spectato.com^\n||speeb.com^\n||speednetwork14.com^\n||speedserver.top^\n||speedshiftmedia.com^\n||speedsuccess.net^\n||spider.ad^\n||spiderhood.net^\n||spinbox.freedom.com^\n||spinbox.net^\n||splinky.com^\n||splut.com^\n||spmxs.com^\n||spongecell.com^\n||sponsoredby.me^\n||sponsoredtweets.com^\n||sponsormob.com^\n||sponsorpalace.com^\n||sponsorpay.com^\n||sponsorselect.com^\n||sportslovin.com^\n||sportsyndicator.com^\n||spotrails.com^\n||spotscenered.info^\n||spottt.com^\n||spottysense.com^\n||spotxcdn.com^\n||spotxchange.com^\n||spoutable.com^\n||sprawley.com^\n||springserve.com^\n||sprintrade.com^\n||sproose.com^\n||sq2trk2.com^\n||srtk.net^\n||srv.yavli.com^\n||srx.com.sg^\n||sslboost.com^\n||sslcheckerapi.com^\n||sta-ads.com^\n||stabilityappointdaily.xyz^\n||stabletrappeddevote.info^\n||stackadapt.com^\n||stackattacka.com^\n||stalesplit.com^\n||standartads.com^\n||star-advertising.com^\n||stargamesaffiliate.com^\n||starlayer.com^\n||startpagea.com^\n||startraint.com^\n||statcamp.net^\n||statecannoticed.com^\n||statelead.com^\n||statesol.net^\n||staticswind.club^\n||statsmobi.com^\n||stealthlockers.com^\n||steepto.com^\n||step-step-go.com^\n||stickcoinad.com^\n||stickyadstv.com^\n||stirshakead.com^\n||stocker.bonnint.net^\n||streamate.com^\n||streamdownloadonline.com^\n||strikead.com^\n||struq.com^\n||sturdynotwithstandingpersuasive.info^\n||style-eyes.eu^\n||subemania.com^\n||sublimemedia.net^\n||submitexpress.co.uk^\n||suffusefacultytsunami.info^\n||sufzmohljbgw.com^\n||sugarlistsuggest.info^\n||suggesttool.com^\n||suite6ixty6ix.com^\n||suitesmart.com^\n||sulvo.co^\n||sumarketing.co.uk^\n||sunmedia.net^\n||sunrisewebjo.link^\n||suparewards.com^\n||super-links.net^\n||superadexchange.com^\n||superinterstitial.com^\n||superloofy.com^\n||supersitetime.com^\n||supplyframe.com^\n||supprent.com^\n||supremeadsonline.com^\n||surf-bar-traffic.com^\n||surfboarddigital.com.au^\n||surgeprice.com^\n||survey-poll.com^\n||surveyvalue.mobi^\n||surveyvalue.net^\n||surveywidget.biz^\n||suthome.com^\n||svlu.net^\n||swadvertising.org^\n||swan-swan-goose.com^\n||swbdds.com^\n||swelen.com^\n||switchadhub.com^\n||swoop.com^\n||symbiosting.com^\n||syndicatedsearchresults.com^\n||synerpattern.com^\n||synhandler.net^\n||t3q7af0z.com^\n||tacastas.com^\n||tacoda.net^\n||tacrater.com^\n||tacticalrepublic.com^\n||tafmaster.com^\n||taggify.net^\n||tagjunction.com^\n||tagshost.com^\n||tailsweep.com^\n||takensparks.com^\n||talaropa.com^\n||tangozebra.com^\n||tapad.com^\n||taqyljgaqsaz.com^\n||tardangro.com^\n||targetadverts.com^\n||targetnet.com^\n||targetpoint.com^\n||targetspot.com^\n||tataget.ru^\n||tattomedia.com^\n||tbaffiliate.com^\n||tcadops.ca^\n||td553.com^\n||td563.com^\n||teads.tv^\n||teambetaffiliates.com^\n||teasernet.com^\n||tec-tec-boom.com^\n||techclicks.net^\n||technoratimedia.com^\n||tek-tek-trek.com^\n||telemetryverification.net^\n||telwrite.com^\n||tennerlist.com^\n||teosredic.com^\n||teracent.net^\n||teracreative.com^\n||teraxhif.com^third-party\n||terraclicks.com^\n||teschenite.com^\n||testfilter.com^\n||testnet.nl^\n||texasboston.com^\n||text-link-ads.com^\n||textonlyads.com^\n||textsrv.com^\n||tfag.de^\n||tgtmedia.com^\n||thaez4sh.com^\n||thangasoline.com^\n||thankyouforadvertising.com^\n||theadgateway.com^\n||theads.me^\n||thebannerexchange.com^\n||thebflix.info^\n||theequalground.info^\n||thefoxes.ru^\n||thelistassassin.com^\n||theloungenet.com^\n||themidnightmatulas.com^\n||theodosium.com^\n||thepiratereactor.net^\n||thewebgemnetwork.com^\n||thewheelof.com^\n||thoseads.com^\n||thoughtleadr.com^\n||thoughtsondance.info^\n||tic-tic-bam.com^\n||tic-tic-toc.com^\n||ticrite.com^\n||tidaltv.com^\n||tightexact.net^\n||tiller.co^\n||tin-tin-win.com^\n||tinbuadserv.com^\n||tisadama.com^\n||tiser.com^\n||tissage-extension.com^\n||tldadserv.com^\n||tlvmedia.com^\n||tmdn2015x9.com^\n||tmpopenclose.click^\n||tnyzin.ru^\n||toboads.com^\n||todich.ru^\n||tokenads.com^\n||tollfreeforwarding.com^\n||tomekas.com^\n||tonefuse.com^\n||tool-site.com^\n||top26.net^\n||topad.mobi^\n||topauto10.com^\n||topbananaad.com^\n||topcasino10.com^\n||topeuro.biz^\n||topfox.co.uk^\n||tophotoffers.com^\n||torads.me^\n||torads.xyz^\n||torconpro.com^\n||torerolumiere.net^\n||toroadvertising.com^\n||toroadvertisingmedia.com^\n||torpsol.com^\n||torrida.net^\n||torrpedoads.net^\n||torvind.com^\n||tostickad.com^\n||total-media.net^\n||totalprofitplan.com^\n||totemcash.com^\n||towardstelephone.com^\n||tower-colocation.de^\n||tower-colocation.info^\n||tpnads.com^\n||tqlkg.com^\n||tqlkg.net^\n||traceadmanager.com^\n||trackadvertising.net^\n||trackaffpix.com^\n||trackcorner.com^\n||tracking.to^\n||tracking101.com^\n||tracking11.com^\n||trackingoffer.info^\n||trackingoffer.net^\n||trackpath.biz^\n||trackpromotion.net^\n||trackstarsengland.net^\n||trackthatad.com^\n||tracktor.co.uk^\n||trackword.net^\n||trackyourlinks.com^\n||tradeadexchange.com^\n||tradeexpert.net^\n||tradepopups.com^\n||traff-advertazer.com^\n||traffads.su^\n||traffboost.net^\n||traffic-supremacy.com^\n||trafficbarads.com^\n||trafficbee.com^\n||trafficbroker.com^\n||trafficfactory.biz^\n||trafficforce.com^\n||trafficformoney.com^\n||traffichaus.com^\n||trafficjunky.net^\n||trafficmasterz.net^\n||trafficmp.com^\n||trafficposse.com^\n||trafficrevenue.net^\n||trafficspaces.net^\n||trafficswarm.com^\n||trafficsway.com^\n||trafficsynergy.com^\n||traffictrader.net^\n||trafficular.com^\n||trafficvance.com^\n||trafficwave.net^\n||trafficz.com^\n||trafficzap.com^\n||traffirms.com^\n||trafmag.com^\n||trahic.ru^\n||trapasol.com^\n||traveladvertising.com^\n||travelscream.com^\n||travidia.com^\n||tredirect.com^\n||treksol.net^\n||trenpyle.com^\n||triadmedianetwork.com^\n||tribalfusion.com^\n||trigami.com^\n||trimpur.com^\n||trk4.com^\n||trkalot.com^\n||trkclk.net^\n||trker.com^\n||trklnks.com^\n||trks.us^\n||trmit.com^\n||trombocrack.com^\n||trtrccl.com^\n||truesecurejump.com^\n||truex.com^\n||trygen.co.uk^\n||trzi30ic.com^\n||ttzmedia.com^\n||tubberlo.com^\n||tubemogul.com^\n||tubereplay.com^\n||tumri.net^\n||tur-tur-key.com^\n||turboadv.com^\n||turbotraff.net^\n||turn.com^\n||tusno.com^\n||tutvp.com^\n||tvas-a.pw^\n||tvas-c.pw^\n||tvprocessing.com^\n||twalm.com^\n||tweard.com^\n||twinpinenetwork.com^\n||twistads.com^\n||twittad.com^\n||twtad.com^\n||tyroo.com^\n||u-ad.info^\n||u1hw38x0.com^\n||ubercpm.com^\n||ubudigital.com^\n||ucaluco.com^\n||ucoxa.work^\n||udmserve.net^\n||ueuerea.com^\n||ufraton.com^\n||ufyvdps3.webcam^\n||ugaral.com^\n||ughus.com^\n||uglyst.com^\n||uiadserver.com^\n||uiqatnpooq.com^\n||ukbanners.com^\n||ukulelead.com^\n||ultimategracelessness.info^\n||umamdmo.com^\n||unanimis.co.uk^\n||underclick.ru^\n||undertone.com^\n||unicast.com^\n||unitethecows.com^\n||universityofinternetscience.com^\n||unlockr.com^\n||unoblotto.net^\n||unrulymedia.com^\n||unterary.com^\n||uonj2o6i.loan^\n||upads.info^\n||upliftsearch.com^\n||urbation.net^\n||ureace.com^\n||urlads.net^\n||urlcash.net^\n||usbanners.com^\n||usemax.de^\n||usenetjunction.com^\n||usenetpassport.com^\n||usercash.com^\n||usswrite.com^\n||usurv.com^\n||utarget.co.uk^\n||utarget.ru^\n||utokapa.com^\n||utubeconverter.com^\n||v.movad.de^\n||v11media.com^\n||v2cigs.com^\n||v2mlblack.biz^\n||v3g4s.com^\n||vacwrite.com^\n||vadpay.com^\n||validclick.com^\n||valuead.com^\n||valueaffiliate.net^\n||valueclick.com^\n||valueclick.net^\n||valueclickmedia.com^\n||valuecommerce.com^\n||valuecontent.net^\n||vapedia.com^\n||vashoot.com^\n||vastopped.com^\n||vaultwrite.com^\n||vcmedia.com^\n||vcommission.com^\n||vcxzv.website^\n||vdopia.com^\n||vellde.com^\n||velmedia.net^\n||velti.com^\n||vemba.com^\n||vendexo.com^\n||venusbux.com^\n||veoxa.com^\n||verata.xyz^\n||versahq.com^\n||versetime.com^\n||verymuchad.com^\n||vhmnetwork.com^\n||vianadserver.com^\n||vibrant.co^\n||vibrantmedia.com^\n||video-loader.com^\n||video1404.info^\n||videoadex.com^\n||videoclick.ru^\n||videodeals.com^\n||videoegg.com^\n||videohub.com^\n||videohube.eu^\n||videolansoftware.com^\n||videoliver.com^\n||videologygroup.com^\n||videoplaza.com^\n||videoplaza.tv^\n||videoroll.net^\n||videovfr.com^\n||vidpay.com^\n||viedeo2k.tv^\n||view-ads.de^\n||viewablemedia.net^\n||viewclc.com^\n||viewex.co.uk^\n||viewivo.com^\n||vindicosuite.com^\n||vipquesting.com^\n||viralmediatech.com^\n||visiads.com^\n||visiblegains.com^\n||visiblemeasures.com^\n||visitdetails.com^\n||visitweb.com^\n||visualsteel.net^\n||vitalads.net^\n||vivamob.net^\n||vixnixxer.com^\n||vkoad.com^\n||vntsm.com^\n||vogosita.com^\n||vogozaw.ru^\n||vpico.com^\n||vs20060817.com^\n||vs4entertainment.com^\n||vs4family.com^\n||vsservers.net^\n||vth05dse.com^\n||vuiads.de^\n||vuiads.info^\n||vuiads.net^\n||vxqhchlyijwu.com^\n||w00tads.com^\n||w00tmedia.net^\n||w3exit.com^\n||w4.com^\n||w5statistics.info^\n||w9statistics.info^\n||wafmedia3.com^\n||wafmedia5.com^\n||wafmedia6.com^\n||waframedia3.com^\n||waframedia5.com^\n||waframedia7.com^\n||waframedia8.com^\n||wagershare.com^\n||wahoha.com^\n||wallacemaloneymindanao.info^\n||wamnetwork.com^\n||wangfenxi.com^\n||waploft.cc^\n||waploft.com^\n||warezlayer.to^\n||warfacco.com^\n||warpwrite.com^\n||wat.freesubdom.com^\n||wat.ipowerapps.com^\n||watchfree.flv.in^\n||watchnowlive.eu^\n||wateristian.com^\n||waymp.com^\n||wbptqzmv.com^\n||wcmcs.net^\n||wcpanalytics.com^\n||weadrevenue.com^\n||web-adservice.com^\n||web-bird.jp^\n||webads.co.nz^\n||webads.nl^\n||webadvertise123.com^\n||webeatyouradblocker.com^\n||webmedia.co.il^\n||webonlinnew.com^\n||weborama.fr^\n||webseeds.com^\n||webtraffic.ttinet.com^\n||webusersurvey.com^\n||wegetpaid.net^\n||wegotmedia.com^\n||wellturnedpenne.info^\n||werbe-sponsor.de^\n||wfnetwork.com^\n||wgreatdream.com^\n||wh5kb0u4.com^\n||where.com^\n||whiteboardnez.com^\n||whoads.net^\n||whtsrv9.com^\n||why-outsource.net^\n||widget.yavli.com^\n||widgetadvertising.biz^\n||widgetbanner.mobi^\n||widgetbucks.com^\n||widgetlead.net^\n||widgets.fccinteractive.com^\n||widgetsurvey.biz^\n||widgetvalue.net^\n||widgetwidget.mobi^\n||wigetmedia.com^\n||wigetstudios.com^\n||winbuyer.com^\n||windgetbook.info^\n||wingads.com^\n||winsspeeder.info^\n||wlmarketing.com^\n||wmmediacorp.com^\n||wonclick.com^\n||wootmedia.net^\n||wordbankads.com^\n||wordego.com^\n||wordgetboo.com^\n||worlddatinghere.com^\n||worldsearchpro.com^\n||worldwidemailer.com^\n||worthathousandwords.com^\n||worthyadvertising.com^\n||wpzka4t6.site^\n||ws-gateway.com^\n||wsp.mgid.com^\n||wulium.com^\n||wurea.com^\n||wwbn.com^\n||wwv4ez0n.com^\n||wwwadcntr.com^\n||wwwpromoter.com^\n||x.mochiads.com^\n||x4300tiz.com^\n||x8bhr.com^\n||xad.com^\n||xadcentral.com^\n||xaxoro.com^\n||xbfk51p7.review^\n||xcelltech.com^\n||xcelsiusadserver.com^\n||xchangebanners.com^\n||xdev.info^\n||xdirectx.com^\n||xeontopa.com^\n||xfileload.com^\n||xfs5yhr1.com^\n||xgraph.net^\n||xjfjx8hw.com^\n||xmas-xmas-wow.com^\n||xmasdom.com^\n||xmaswrite.com^\n||xmlconfig.ltassrv.com^\n||xs.mochiads.com^\n||xtcie.com^\n||xtendadvert.com^\n||xtendmedia.com^\n||xubob.com^\n||xvika.com^\n||xwwmhfbikx.net^\n||xx00.info^\n||xxlink.net^\n||ya88s1yk.com^\n||yabuka.com^\n||yadomedia.com^\n||yambotan.ru^\n||yashi.com^\n||yathmoth.com^\n||yawnedgtuis.org^\n||yb0t.com^\n||ycasmd.info^\n||yceml.net^\n||yeabble.com^\n||yellads.com^\n||yellowmango.eu^\n||yepoints.net^\n||yes-messenger.com^\n||yesadsrv.com^\n||yesnexus.com^\n||yieldads.com^\n||yieldadvert.com^\n||yieldbuild.com^\n||yieldkit.com^\n||yieldlab.net^\n||yieldmanager.com^\n||yieldmanager.net^\n||yieldoptimizer.com^\n||yieldselect.com^\n||yieldx.com^\n||yiq6p.com^\n||yjxuda0oi.com^\n||yldbt.com^\n||yldmgrimg.net^\n||yllix.com^\n||ymads.com^\n||yoc-adserver.com^\n||yottacash.com^\n||youcandoitwithroi.com^\n||youlamedia.com^\n||youlouk.com^\n||your-tornado-file.com^\n||your-tornado-file.org^\n||youradexchange.com^\n||yourfastpaydayloans.com^\n||yourlegacy.club^\n||youroffers.win^\n||yourquickads.com^\n||youwatchtools.com^\n||ytsa.net^\n||yuarth.com^\n||yucce.com^\n||yumenetworks.com^\n||yunshipei.com^\n||yupfiles.club^\n||yupfiles.net^\n||yupfiles.org^\n||yvoria.com^\n||yz56lywd.com^\n||yzrnur.com^\n||yzus09by.com^\n||z-defense.com^\n||z5x.net^\n||zangocash.com^\n||zaparena.com^\n||zappy.co.za^\n||zapunited.com^\n||zde-engage.com^\n||zeads.com^\n||zedo.com^\n||zeesiti.com^\n||zenoviaexchange.com^\n||zenoviagroup.com^\n||zercstas.com^\n||zerezas.com^\n||zeropark.com^\n||zerozo.work^\n||zferral.com^\n||zidae.com^\n||ziffdavis.com^\n||zim-zim-zam.com^\n||zipropyl.com^\n||znaptag.com^\n||zoglafi.info^\n||zompmedia.com^\n||zonealta.com^\n||zonplug.com^\n||zoomdirect.com.au^\n||zorwrite.com^\n||zugo.com^\n||zwaar.org^\n||zxxds.net^\n||zyiis.net^\n||zypenetwork.com^\n! Mobile\n||adbuddiz.com^\n||adcolony.com^\n||adiquity.com^\n||admob.com^\n||adwhirl.com^\n||adwired.mobi^\n||adzmob.com^\n||airpush.com^\n||amobee.com^\n||appads.com^\n||buxx.mobi^\n||dmg-mobile.com^\n||greystripe.com^\n||inmobi.com^\n||kuad.kusogi.com^\n||mad-adz.com^\n||millennialmedia.com^\n||mkhoj.com^\n||mobgold.com^\n||mobizme.net^\n||mobpartner.mobi^\n||mocean.mobi^\n||mojiva.com^\n||mysearch-online.com^\n||smaato.net^\n||startappexchange.com^\n||stepkeydo.com^\n||tapjoyads.com^\n||vungle.com^\n||wapdollar.in^\n||waptrick.com^\n||yieldmo.com^\n! Admiral\n||brassrule.com^\n||breezybath.com^\n||fanaticalfly.com^\n||metapelite.com^\n||roastedvoice.com^\n! Non-English (instead of whitelisting ads)\n||adhood.com^\n||atresadvertising.com^\n! youwatch.org adservers\n||acamar.xyz^\n||achird.xyz^\n||acubens.xyz^\n||adhafera.xyz^\n||aladfar.xyz^\n||alamak.xyz^\n||alaraph.xyz^\n||albaldah.xyz^\n||albali.xyz^\n||albireo.xyz^\n! admeasures.com domains\n||ads-codes.net^\n||aeghae5y.com^\n||aeghie6dien.info^\n||aew9eigieng.info^\n||ahn2phee3oh.info^\n||booj7tho.com^\n||chohye2t.com^\n||ci3ixee8.com^\n||dah0ooy4doe.info^\n||ef5ahgoo.com^\n||faeph6ax.com^\n||lie8oong.com^\n||meinooriut3.info^\n||nepalhtml.com^\n||nich1eox.com^\n||no1chie7poh.info^\n||ohs1upuwi8b.info^\n||ohv1tie2.com^\n||qued9yae1ai.info^\n||sahraex7vah.info^\n||terraadstools.com^\n||urahor9u.com^\n||vipcpms.com^\n||viuboin4.com^\n||yie4zooseif.info^\n! popads.net\n||aadbobwqgmzi.com^\n||aanvxbvkdxph.com^\n||aaqpajztftqw.com^\n||aasopqgmzywa.com^\n||aatmytrykqhi.com^\n||acjmkenepeyn.com^\n||acnsavlosahs.com^\n||adtbomthnsyz.com^\n||adudzlhdjgof.com^\n||afedispdljgb.com^\n||afqwfxkjmgwv.com^\n||agpnzrmptmos.com^\n||ahkpdnrtjwat.com^\n||ahzybvwdwrhi.com^\n||aiiaqehoqgrj.com^\n||aiypulgy.com^\n||ajaeihzlcwvn.com^\n||ajgffcat.com^\n||ajmggjgrardn.com^\n||ajxftwwmlinv.com^\n||akrzgxzjynpi.com^\n||alasdzdnfvtj.com^\n||alvivigqrogq.com^\n||ambqphwf.com^\n||amnpmitevuxx.com^\n||amqtbshegbqg.com^\n||anasjdzutdmv.com^\n||anluecyopslm.com^\n||antrtrtyzkhw.com^\n||anypbbervqig.com^\n||anyuwksovtwv.com^\n||aominpzhzhwj.com^\n||aomvdhxvblfp.com^\n||aoqviogrwckf.com^\n||apgjczhgjrka.com^\n||aqdrzqsuxxvd.com^\n||aqlvpnfxrkyf.com^\n||aqornnfwxmua.com^\n||aragvjeosjdx.com^\n||aryufuxbmwnb.com^\n||asqamasz.com^\n||atcyboopajyp.com^\n||avrdpbiwvwyt.com^\n||avzkjvbaxgqk.com^\n||awfjqdhcuftd.com^\n||awsatstb.com^\n||awvrvqxq.com^\n||axfkfstrbacx.com^\n||azbdbtsmdocl.com^\n||azditojzcdkc.com^\n||azeozrjk.com^\n||azgyzdjexcxg.com^\n||azzvkcavtgwp.com^\n||bajofdblygev.com^\n||bayvlsmaahou.com^\n||bbheuxcancwj.com^\n||bbjlsdqhpbuqaspgjyxaobmpmzunjnvqmahejnwwvaqbzzqodu.com^\n||bboemhlddgju.com^\n||bbopkapcgonb.com^\n||beghfkrygvxp.com^\n||bfhavmgufvhn.com^\n||bgarilrzlgez.com^\n||bgcsojmtgdrv.com^\n||bgitczbd.com^\n||bguaeoakgmrw.com^\n||bhejerqgrtlq.com^\n||bhjhijisulwl.com^\n||bhyqllgtzjee.com^\n||bircgizd.com^\n||bjpktmjdxqpl.com^\n||bjzcyqezwksznxxhscsfcogugkyiupgjhikadadgoiruasxpxo.com^\n||bkmmlcbertdbselmdxpzcuyuilaolxqfhtyukmjkklxphbwsae.com^\n||blprkaomvazv.com^\n||bmjccqfxlabturkmpzzokhsahleqqrysudwpuzqjbxbqeakgnf.com^\n||bmqnguru.com^\n||bmubqabepbcb.com^\n||bnkgacehxxmx.com^\n||bocksnabswdq.com^\n||bqptlqmtroto.com^\n||bqqjowpigdnx.com^\n||brqrtgjklary.com^\n||bsaixnxcpaai.com^\n||bspjagxietut.com^\n||bsupflnjmuzn.com^\n||btbapoifsphl.com^\n||btcwkbqojiyg.com^\n||bujntrmh.com^\n||bvezznurwekr.com^\n||bvobtmbziccr.com^\n||bwyckpmsolzk.com^\n||bxoixzbtllwx.com^\n||byqmzodcdhhu.com^\n||bzjtjfjteazqzmukjwhyzsaqdtouiopcmtmgdiytfdzboxdann.com^\n||bzyrhqbdldds.com^\n||cbwrwcjdctrj.com^\n||cbxqceuuwnaz.com^\n||cbxtnudkklwh.com^\n||ccbaobjyprxh.com^\n||ccdkyvyw.com^\n||ccwinenmbnso.com^\n||cdbkxcnfmehf.com^\n||cdicyazp.com^\n||cdqmeyhqrwinofutpcepbahedusocxqyfokvehqlqpusttfwve.com^\n||cdrjblrhsuxljwesjholugzxwukkerpobmonocjygnautvzjjm.com^\n||cdveeechegws.com^\n||ceseyitsikzs.com^\n||cewdbisyrzdv.com^\n||cfdmkifknsjt.com^\n||cfsdtzggpcmr.com^\n||cgmkpdqjnedb.com^\n||chvjfriqlvnt.com^\n||chxfeymgmwbo.com^\n||cihnrhqwbcsq.com^\n||cixjmaxkemzknxxuyvkbzlhvvgeqmzgopppvefpfkqdraonoez.com^\n||cjnoeafncyzb.com^\n||cjvgnswapbqo.com^\n||cjxkzkzmdomd.com^\n||ckwpsghi.com^\n||cmdjujqlfbts.com^\n||cmpsuzvr.com^\n||cmqyhtqkhduy.com^\n||cnfiukuediuy.com^\n||cnntsmnymvnp.com^\n||cogxsnvqesph.com^\n||comgnnyx.com^\n||comwgi.com^\n||cortxphssdvc.com^\n||cpamnizzierk.com^\n||cphxwpicozlatvnsospudjhswfxwmykgbihjzvckxvtxzfsgtx.com^\n||crkgtnad.com^\n||csbsyukodmga.com^\n||cscactmkbfvn.com^\n||csmqorveetie.com^\n||ctimfrfrmqip.com^\n||cuguwxkasghy.com^\n||cwliihvsjckn.com^\n||cwtekghutpaq.com^\n||cxgwwsapihlo.com^\n||cxnxognwkuxm.com^\n||cxoxruotepqgcvgqxdlwwucgyazmbkhdojqzihljdwwfeylovh.com^\n||cxrmgoybhyrk.com^\n||cymuxbcnhinm.com^\n||cywegkfcrhup.com^\n||czcbkaptwfmv.com^\n||czgeitdowtlv.com^\n||czoivochvduv.com^\n||dacqmkmsjajm.com^\n||daxzupqivdoj.com^\n||dbjcbnlwchgu.com^\n||dbwawnzkjniz.com^\n||dcdalkgtbmip.com^\n||dcgbswcvywyl.com^\n||dcneohtx.com^\n||ddprxzxnhzbq.com^\n||dgmlubjidcxc.com^\n||dgwrxyucxpizivncznkpmdhtrdzyyylpoeitiannqfxmdzpmwx.com^\n||dhlnlwxspczc.com^\n||disbkzufvqhk.com^\n||ditouyldfqgt.com^\n||diysqcbfyuru.com^\n||djbnmqdawodm.com^\n||djntmaplqzbi.com^\n||djzmpsingsrtfsnbnkphyagxdemeagsiabguuqbiqvpupamgej.com^\n||dkrhsftochvzqryurlptloayhlpftkogvzptcmjlwjgymcfrmv.com^\n||dmatquyckwtu.com^\n||dmbjbgiifpfo.com^\n||dmdcpvgu.com^\n||dmojscqlwewu.com^\n||dmwubqhtuvls.com^\n||dmyypseympjf.com^\n||dnxpseduuehm.com^\n||dobgfkflsnmpaeetycphmcloiijxbvxeyfxgjdlczcuuaxmdzz.com^\n||dobjgpqzygow.com^\n||dohhehsgnxfl.com^\n||dovltuzibsfs.com^\n||dpallyihgtgu.com^\n||dppcevxbshdl.com^\n||drbwugautcgh.com^\n||drtqfejznjnl.com^\n||dsevjzklcjjb.com^\n||dsmysdzjhxot.com^\n||dswwghrlwwcm.com^\n||dubzmzpdkddi.com^\n||duchmcmpmqqu.com^\n||dulcetcgvcxr.com^\n||dulpsxaznlwr.com^\n||duvyjbofwfqh.com^\n||duxyrxhfwilv.com^\n||dwentymgplvrizqhieugzkozmqjxrxcyxeqdjvcbjmrhnkguwk.com^\n||dxcqavshmvst.com^\n||dxfsbkmaydtt.com^\n||dxigubtmyllj.com^\n||dyazeqpeoykf.com^\n||dyunhvev.com^\n||easnviytengk.com^\n||ebfjbrlcvjlv.com^\n||ecmeqhxevxgmtoxubrjstrrlyfgrrtqhvafyagettmwnwkwltn.com^\n||ectbduztanog.com^\n||edvbyybaviln.com^\n||edwywpsufuda.com^\n||eejcqlenlsko.com^\n||eeqabqioietkquydwxfgvtvpxpzkuilfcpzkplhcckoghwgacb.com^\n||eerdckbwujcx.com^\n||ehnjtmqchrub.com^\n||eidzaqzygtvq.com^\n||eifbewnmtgpi.com^\n||eiibdnjlautz.com^\n||ejgxyfzciwyi.com^\n||ejjrckrhigez.com^\n||ejwmxjttljbe.com^\n||ekhgvpsfrwqm.com^\n||elbeobjhnsvh.com^\n||elkpxsfzrubq.com^\n||elxxkpaeudxu.com^\n||elzlogcphhka.com^\n||emdbszgmxggo.com^\n||emirdzzvhviv.com^\n||emrumkgmdmdq.com^\n||enfhddbnariw.com^\n||eovkzcueutgf.com^\n||epesogtigole.com^\n||epgooipixbbo.com^\n||erbsqnmglmnv.com^\n||erkwkjfompvt.com^\n||erszwzaidmlc.com^\n||ervpgpxr.com^\n||eslgydoqbedo.com^\n||eslydbnukkme.com^\n||esnirgskobfj.com^\n||etggiddfdaqd.com^\n||evhvoeqfrlsb.com^\n||exnyzdboihvi.com^\n||ezbtpdjeimlv.com^\n||ezuosstmbcle.com^\n||farkkbndawtxczozilrrrunxflspkyowishacdueiqzeddsnuu.com^\n||fbbjlubvwmwd.com^\n||fcjnqpkrdglw.com^\n||fdepobamndfn.com^\n||fdogfuqpgeub.com^\n||fegyacmbobil.com^\n||ffanszicnoqs.com^\n||ffwbpadvkcyi.com^\n||fgkvpyrmkbap.com^\n||fgmucsiirrsq.com^\n||fgwsjwiaqtjc.com^\n||fhawywadfjlo.com^\n||fhylnqzxwsbo.com^\n||firugsivsqot.com^\n||fjfxpykp.com^\n||fjvolzrojowa.com^\n||fkdqrjnoxhch.com^\n||fkekipafwlqd.com^\n||fkjyzxnoxusg.com^\n||fkrrvhoierty.com^\n||fluohbiy.com^\n||flzelfqolfnf.com^\n||fmuxugcqucuu.com^\n||fmzxzkgmpmrx.com^\n||fneheruhxqtv.com^\n||fnjcriccyuna.com^\n||fokisduu.com^\n||fpbmjwoebzby.com^\n||fppupmqbydpk.com^\n||fqkcdhptlqma.com^\n||fqmxwckinopg.com^\n||fqovfxpsytxf.com^\n||fqpteozo.com^\n||frddujheozns.com^\n||frdhsmerubfg.com^\n||frlvfzybstsa.com^\n||frlzxwxictmg.com^\n||fsddidfmmzvw.com^\n||ftjrekbpjkwe.com^\n||ftytssqazcqx.com^\n||fvbeyduylvgy.com^\n||fwcrhzvfxoyi.com^\n||fxjgprpozntk.com^\n||fxjyultd.com^\n||fxrgikipxnlq.com^\n||fxtgrttlarkl.com^\n||fxvxgwqcddvm.com^\n||fzsiwzxnqadb.com^\n||fzzudxglrnrr.com^\n||gaxmdcfkxygs.com^\n||gazogsjsoxty.com^\n||gbiwxmjw.com^\n||gbltotkythfh.com^\n||gdixpvfqbhun.com^\n||gdpuknsngvps.com^\n||geazikjazoid.com^\n||gefaqjwdgzbo.com^\n||geqcqduubhll.com^\n||gerpkshe.com^\n||gggemaop.com^\n||ggnabmvnwphu.com^\n||ggzuksudqktn.com^\n||ghtroafchzrt.com^\n||giojhiimnvwr.com^\n||gjxdibyzvczd.com^\n||gkgdqahkcbmykurmngzrrolrecfqvsjgqdyujvgdrgoezkcobq.com^\n||gkiryieltcbg.com^\n||glnqvqbedbmvtcdzcokrfczopbddhopygrvrnlgmalgvhnsfsc.com^\n||glslciwwvtxn.com^\n||gmpdixdh.com^\n||gmpmuqniggyz.com^\n||gnadhzstittd.com^\n||gnipadiiodpa.com^\n||gofgfsvnfnfw.com^\n||gojwyansqmcl.com^\n||gpbznagpormpyusuxbvlpbuejqzwvspcyqjcxbqtbdtlixcgzp.com^\n||gpgsxlmjnfid.com^\n||gphfgyrkpumn.com^\n||gpnduywxhgme.com^\n||gqthfroeirol.com^\n||gqulrzprheth.com^\n||grceweaxhbpvclyxhwuozrbtvqzjgbnzklvxdezzficwjnmfil.com^\n||grfqrhqlzvjl.com^\n||gtaouarrwypu.com^\n||gtevyaeeiged.com^\n||gtmonytxxglu.com^\n||gtqfsxrrerzu.com^\n||gubdadtxwqow.com^\n||gvgakxvukmrm.com^\n||gvoszbzfzmtl.com^\n||gvrqquiotcyr.com^\n||gvxobjcxcbkb.com^\n||gwaatiev.com^\n||gwcujaprdsen.com^\n||gwsomeiyywaz.com^\n||gxdyluyqciac.com^\n||gxgnvickedxpuiavkgpisnlsphrcyyvkgtordatszlrspkgppe.com^\n||gxvbogvbcivs.com^\n||gyinmxpztbgf.com^\n||gypxbcrmxsmikqbmnlwtezmjotrrdxpqtafumympsdtsfvkkza.com^\n||gzkoehgbpozz.com^\n||gzmofmqddajr.com^\n||hafbezbemwwd.com^\n||haqlmmii.com^\n||hbbwlhxfnbpq.com^\n||hbedvoyluzmq.com^\n||hbrbtmjyvdsy.com^\n||hcyxksgsxnzb.com^\n||heefwozhlxgz.com^\n||hevdxhsfbwud.com^\n||hffmxndinqyo.com^\n||hffmzplu.com^\n||hfgevdzcoocs.com^\n||hfjuehls.com^\n||hfmtqgiqscvg.com^\n||hgbmwkklwittcdkjapnpeikxojivfhgszbxmrjfrvajzhzhuks.com^\n||hgztvnjbsrki.com^\n||hhwqfmqyqoks.com^\n||higygtvnzxad.com^\n||hilkfxdqxzac.com^\n||hjukmfdbryln.com^\n||hkacgxlpfurb.com^\n||hkdjrnkjwtqo.com^\n||hkoxlirf.com^\n||hlekbinpgsuk.com^\n||hlotiwnz.com^\n||hndesrzcgjmprqbbropdulvkfroonnrlbpqxhvprsavhwrfxtv.com^\n||hnoajsaivjsg.com^\n||hntpbpeiuajc.com^\n||hpdmnmehzcor.com^\n||hplgpoicsnea.com^\n||hpmgdwvvqulp.com^\n||hpxxzfzdocinivvulcujuhypyrniicjfauortalmjerubjgaja.com^\n||hqnyahlpmehp.com^\n||hsvqfvjidloc.com^\n||htllanmhrnjrbestmyabzhyweaccazvuslvadtvutfiqnjyavg.com^\n||hueenmivecmx.com^\n||huejizictcgd.com^\n||huzmweoxlwanzvstlgygbrnfrmodaodqaczzibeplcezmyjnlv.com^\n||hvfzacisynoq.com^\n||hvukouhckryjudrawwylpboxdsonxhacpodmxvbonqipalsprb.com^\n||hwvwuoxsosfp.com^\n||hxbvbmxv.com^\n||hxuvwqsecumg.com^\n||hytkatubjuln.com^\n||hyubowucvkch.com^\n||hyvsquazvafrmmmcfpqkabocwpjuabojycniphsmwyhizxgebu.com^\n||hyzncftkveum.com^\n||hzskbnafzwsu.com^\n||hztkbjdkaiwt.com^\n||ibqmccuuhjqc.com^\n||icafyriewzzrwxlxhtoeakmwroueywnwhmqmaxsqdntasgfvhc.com^\n||icjeqbqdzhyx.com^\n||iczhhiiowapd.com^\n||idkyfrsbzesx.com^\n||idpukwmp.com^\n||idvuakamkzmx.com^\n||iectshrhpgsl.com^\n||ieqprskfariw.com^\n||ifaklabnhplb.com^\n||ifvetqzfiawg.com^\n||igdfzixkdzxe.com^\n||iglwibwbjxuoflrczfvpibhihwuqneyvmhzeqbmdmujmirdkae.com^\n||igupodzh.com^\n||iibcejrrfhxh.com^\n||iknctklddhoh.com^\n||ilsivrexvpyv.com^\n||imbbjywwahev.com^\n||imgoatxhxior.com^\n||imqkdsdgfygm.com^\n||imrwxmau.com^\n||imtdtaloqwcz.com^\n||imyqdbxq.com^\n||insbrvwfrcgb.com^\n||ioatyggwaypq.com^\n||iohaqrkjddeq.com^\n||ioighavxylne.com^\n||ionbpysfukdh.com^\n||iqmjedevvojm.com^\n||iqrqmhrfkyuu.com^\n||irjaeupzarkvwmxonaeslgicvjvgdruvdywmdvuaoyfsjgdzhk.com^\n||irrttzthsxot.com^\n||irxpndjg.com^\n||irzdishtggyo.com^\n||isbzjaedbdjr.com^\n||isdlyvhegxxz.com^\n||isggimkjabpa.com^\n||isqgobsgtqsh.com^\n||itevcsjvtcmb.com^\n||iuymaolvzery.com^\n||iwrjczthkkla.com^\n||iydghotpzofn.com^\n||izhvnderudte.com^\n||iziwhlafxitn.com^\n||iztsbnkxphnj.com^\n||izwsvyqv.com^\n||jahsrhlp.com^\n||jakzxxzrymhz.com^\n||jamkkydyiyhx.com^\n||jauftivogtho.com^\n||jbgehhqvfppf.com^\n||jboovenoenkh.com^\n||jbvisobwrlcv.com^\n||jbyksmjmbmku.com^\n||jcctggmdccmt.com^\n||jcnoeyqsdfrc.com^\n||jdlnquri.com^\n||jeyoxmhhnofdhaalzlfbrsfmezfxqxgwqjkxthzptjdizuyojh.com^\n||jffwwuyychxw.com^\n||jgqkrvjtuapt.com^\n||jhupypvmcsqfqpbxbvumiaatlilzjrzbembarnhyoochsedzvi.com^\n||jiyairvjgfqk.com^\n||jjdrwkistgfh.com^\n||jjipgxjf.com^\n||jjpoxurorlsb.com^\n||jkjoxlhkwnxd.com^\n||jkkernvkrwdr.com^\n||jlarmqbypyku.com^\n||jlflzjdt.com^\n||jlymmwnkxhph.com^\n||jmzaqwcmcbui.com^\n||jncjzdohkgic.com^\n||jnercechoqjb.com^\n||jnxqlltlnezn.com^\n||jnylpjlnjfsp.com^\n||jogpsoiyngua.com^\n||jorndvyzchaq.com^\n||jpuiucicqwan.com^\n||jpwvdpvsmhow.com^\n||jqibqqxghcfk.com^\n||jqmcbepfjgks.com^\n||jqqrcwwd.com^\n||jrmyhchnfawh.com^\n||jshjrozmwmyj.com^\n||jtzlsdmbmfms.com^\n||juqmlmoclnhe.com^\n||jusrlkubhjnr.com^\n||jvodizomnxtg.com^\n||jwfdyujffrzt.com^\n||jyauuwrrigim.com^\n||jydbctzvbqrh.com^\n||jzekquhmaxrk.com^\n||jzqharwtwqei.com^\n||kadjwdpzxdxd.com^\n||karcvrpwayal.com^\n||karownxatpbd.com^\n||kayfdraimewk.com^\n||kayophjgzqdq.com^\n||kbrnfzgglehh.com^\n||kceikbfhsnet.com^\n||kdvcvkwwtbwn.com^\n||kecldktirqzk.com^\n||keqnebfovnhl.com^\n||kgkjlivo.com^\n||kgvgtudoridc.com^\n||kihhgldtpuho.com^\n||kjjlucebvxtu.com^\n||kjmddlhlejeh.com^\n||kjplmlvtdoaf.com^\n||kjqyvgvvazii.com^\n||klakcdiqmgxq.com^\n||kldwitfrqwal.com^\n||klmvharqoxdq.com^\n||klrdsagmuepg.com^\n||kmtubsbmwdep.com^\n||kmveerigfvyy.com^\n||kmvupiadkzdn.com^\n||knslxwqgatnd.com^\n||kplzvizvsqrh.com^\n||kpnuqvpevotn.com^\n||kqcflzvunhew.com^\n||kqmjmrzjhmdn.com^\n||kqsipdhvcejx.com^\n||krovrhmqgupd.com^\n||krziyrrnvjai.com^\n||ksbklucaxgbf.com^\n||ktcltsgjcbjdcyrcdaspmwqwscxgbqhscmkpsxarejfsfpohkk.com^\n||kthdreplfmil.com^\n||ktjqfqadgmxh.com^\n||kurtgcwrdakv.com^\n||kvpofpkxmlpb.com^\n||kvsyksorguja.com^\n||kwgpddeduvje.com^\n||kwipnlppnybc.com^\n||kwystoaqjvml.com^\n||kxdprqrrfhhn.com^\n||kxtepdregiuo.com^\n||kyhkyreweusn.com^\n||kylqpeevrkgh.com^\n||kyowarob.com^\n||kzwddxlpcqww.com^\n||lazkslkkmtpy.com^\n||lbfryfttoihl.com^\n||lbpndcvhuqlm.com^\n||lbypppwfvagq.com^\n||lckpubqq.com^\n||lctpaemybjkv.com^\n||lcxrhcqouqtw.com^\n||lcyxmuhxroyo.com^\n||ldaiuhkayqtu.com^\n||leuojmgbkpcl.com^\n||lgnjcntegeqf.com^\n||lgthvsytzwtc.com^\n||lhuqalcxjmtq.com^\n||liosawitskzd.com^\n||lixzmpxjilqp.com^\n||ljhuvzutnpza.com^\n||ljngencgbdbn.com^\n||lkbvfdgqvvpk.com^\n||lkjmcevfgoxfbyhhmzambtzydolhmeelgkotdllwtfshrkhrev.com^\n||lkktkgcpqzwd.com^\n||lkrcapch.com^\n||lmejuamdbtwc.com^\n||lmuxaeyapbqxszavtsljaqvmlsuuvifznvttuuqfcxcbgqdnn.com^\n||lnzcmgguxlac.com^\n||lplqyocxmify.com^\n||lppoblhorbrf.com^\n||lpwvdgfo.com^\n||lroywnhohfrj.com^\n||lsegvhvzrpqc.com^\n||lstkfdmmxbmv.com^\n||lvlvpdztdnro.com^\n||lwenrqtarmdx.com^\n||lwqwsptepdxy.com^\n||lwysswaxnutn.com^\n||lxkqybzanzug.com^\n||lyifwfhdizcc.com^\n||lytpdzqyiygthvxlmgblonknzrctcwsjycmlcczifxbkquknsr.com^\n||lyzskjigkxwy.com^\n||lzmovatu.com^\n||lzvnaaozpqyb.com^\n||maboflgkaxqn.com^\n||maxgirlgames.com^\n||mbfvfdkawpoi.com^\n||mbvmecdlwlts.com^\n||mdeaoowvqxma.com^\n||medyagundem.com^\n||mepchnbjsrik.com^\n||mflkgrgxadij.com^\n||mfmikwfdopmiusbveskwmouxvafvzurvklwyfamxlddexgrtci.com^\n||mftbfgcusnzl.com^\n||mgrxsztbcfeg.com^\n||mhrfhwlqsnzf.com^\n||mhwxckevqdkx.com^\n||miadbbnreara.com^\n||mizmhwicqhprznhflygfnymqbmvwokewzlmymmvjodqlizwlrf.com^\n||mjujcjfrgslf.com^\n||mkmxovjaijti.com^\n||mkzynqxqlcxk.com^\n||mlbzafthbtsl.com^\n||mlgrrqymdsyk.com^\n||mmdcibihoimt.com^\n||mmdifgneivng.com^\n||mmeddgjhplqy.com^\n||mmesheltljyi.com^\n||mmnridsrreyh.com^\n||mmojdtejhgeg.com^\n||mmvcmovwegkz.com^\n||mnjgoxmx.com^\n||mnusvlgl.com^\n||mnyavixcddgx.com^\n||mnzimonbovqs.com^\n||moadlbgojatn.com^\n||mohcafpwpldi.com^\n||molqvpnnlmnb.com^\n||mopvkjodhcwscyudzfqtjuwvpzpgzuwndtofzftbtpdfszeido.com^\n||mosdqxsgjhes.com^\n||mpoboqvqhjqv.com^\n||mpytdykvcdsg.com^\n||mpzuzvqyuvbh.com^\n||mqphkzwlartq.com^\n||mrfveznetjtp.com^\n||mszfmpseoqbu.com^\n||mueqzsdabscd.com^\n||munpprwlhric.com^\n||mvjuhdjuwqtk.com^\n||mvqinxgp.com^\n||mwqkpxsrlrus.com^\n||mzbetmhucxih.com^\n||mzguykhxnuap.com^\n||mzkhhjueazkn.com^\n||nbbljlzbbpck.com^\n||nbkwnsonadrb.com^\n||nbrwtboukesx.com^\n||ndemlviibdyc.com^\n||ndkvzncsuxgx.com^\n||ndxidnvvyvwx.com^\n||nefczemmdcqi.com^\n||nefxtwxk.com^\n||negdrvgo.com^\n||nfdntqlqrgwc.com^\n||nfniziqm.com^\n||nfxusyviqsnh.com^\n||ngmckvucrjbnyybvgesxozxcwpgnaljhpedttelavqmpgvfsxg.com^\n||nguooqblyjrz.com^\n||nheanvabodkw.com^\n||nifyalnngdhb.com^\n||njjybqyiuotl.com^\n||nkkreqvurtoh.com^\n||nkyngrtleloc.com^\n||nlfqbfwbfovt.com^\n||nlljrfvbnisi.com^\n||nmaafswoiecv.com^\n||nmayxdwzhaus.com^\n||nmhhnyqmxgku.com^\n||nnigsvoorscmgnyobwuhrgnbcgtiicyflrtpwxsekldubasizg.com^\n||nnjiluslnwli.com^\n||nnvjigagpwsh.com^\n||nokswnfvghee.com^\n||noolablkcuyu.com^\n||npeanaixbjptsemxrcivetuusaagofdeahtrxofqpxoshduhri.com^\n||npgdqwtrprfq.com^\n||nqlkwyyzzgtn.com^\n||nrectoqhwdhi.com^\n||nrgpugas.com^\n||nryvxfosuiju.com^\n||nsazelqlavtc.com^\n||ntnlawgchgds.com^\n||nuayfpthqlkq.com^\n||nubtjnopbjup.com^\n||nucqkjkvppgs.com^\n||nunsbvlzuhyi.com^\n||nvajxoahenwe.com^\n||nwfdrxktftep.com^\n||nybpurpgexoe.com^\n||nyqogyaflmln.com^\n||oalicqudnfhf.com^\n||oawleebf.com^\n||oazojnwqtsaj.com^\n||obthqxbm.com^\n||obuuyneuhfwf.com^\n||obvbubmzdvom.com^\n||obxwnnheaixf.com^\n||ocyhpouojiss.com^\n||odsljzffiixm.com^\n||oehjxqhiasrk.com^\n||oewscpwrvoca.com^\n||ofgapiydisrw.com^\n||ofghrodsrqkg.com^\n||ofjampfenbwv.com^\n||oguorftbvegb.com^\n||oiffrtkdgoef.com^\n||okasfshomqmg.com^\n||okgfvcourjeb.com^\n||okmuxdbq.com^\n||oknmanswftcd.com^\n||olctpejrnnfh.com^\n||ompzowzfwwfc.com^\n||ongkidcasarv.com^\n||onkcjpgmshqx.com^\n||oosdjdhqayjm.com^\n||ooyhetoodapmrjvffzpmjdqubnpevefsofghrfsvixxcbwtmrj.com^\n||ophpbseelohv.com^\n||opyisszzoyhc.com^\n||oqmjxcqgdghq.com^\n||ormnduxoewtl.com^\n||orszajhynaqr.com^\n||osbblnlmwzcr.com^\n||oslzqjnh.com^\n||ossdqciz.com^\n||otpyldlrygga.com^\n||otrfmbluvrde.com^\n||ougfkbyllars.com^\n||ovgzbnjj.com^\n||ovoczhahelca.com^\n||ovzmelkxgtgf.com^\n||owihjchxgydd.com^\n||owlmjcogunzx.com^\n||owodfrquhqui.com^\n||owqobhxvaack.com^\n||owwewfaxvpch.com^\n||oytrrdlrovcn.com^\n||ozhwenyohtpb.com^\n||pbnnsras.com^\n||pcebrrqydcox.com^\n||pdbaewqjyvux.com^\n||pdzqwzrxlltz.com^\n||peewuranpdwo.com^\n||peewuvgdcian.com^\n||peqdwnztlzjp.com^\n||piwwplvxvqqi.com^\n||pixjqfvlsqvu.com^\n||pjffrqroudcp.com^\n||pjnrwznmzguc.com^\n||pkmzxzfazpst.com^\n||pkougirndckw.com^\n||pkqbgjuinhgpizxifssrtqsyxnzjxwozacnxsrxnvkrokysnhb.com^\n||plcsedkinoul.com^\n||plgdhrvzsvxp.com^\n||plmuxaeyapbqxszavtsljaqvmlsuuvifznvttuuqfcxcbgqdnn.com^\n||plwvwvhudkuv.com^\n||pmlcuxqbngrl.com^\n||pnmkuqkonlzj.com^\n||pnunijdm.com^\n||popzkvfimbox.com^\n||ppqfteducvts.com^\n||ppuuwencqopa.com^\n||ppxrlfhsouac.com^\n||ppzfvypsurty.com^\n||pqwaaocbzrob.com^\n||prenvifxzjuo.com^\n||prggimadscvm.com^\n||prqivgpcjxpp.com^\n||prwlzpyschwi.com^\n||pserhnmbbwexmbjderezswultfqlamugbqzsmyxwumgqwxuerl.com^\n||pshcqtizgdlm.com^\n||psmlgjalddqu.com^\n||psrbrytujuxv.com^\n||ptiqsfrnkmmtvtpucwzsaqonmvaprjafeerwlyhabobuvuazun.com^\n||ptoflpqqqkdk.com^\n||pugklldkhrfg.com^\n||punlkhusprgw.com^\n||puogotzrsvtg.com^\n||pusbamejpkxq.com^\n||pvoplkodbxra.com^\n||pvtcntdlcdsb.com^\n||pwynoympqwgg.com^\n||pxarwmerpavfmomfyjwuuinxaipktnanwlkvbmuldgimposwzm.com^\n||pxgkuwybzuqz.com^\n||pxktkwmrribg.com^\n||pzgchrjikhfyueumavkqiccvsdqhdjpljgwhbcobsnjrjfidpq.com^\n||pzkpyzgqvofi.com^\n||qajaohrcbpkd.com^\n||qarqyhfwient.com^\n||qazzzxwynmot.com^\n||qbfvwovkuewm.com^\n||qclxheddcepf.com^\n||qdlhprdtwhvgxuzklovisrdbkhptpfarrbcmtrxbzlvhygqisv.com^\n||qerlbvqwsqtb.com^\n||qevivcixnngf.com^\n||qfhjthejwvgm.com^\n||qfmbgvgvauvt.com^\n||qfrpehkvqtyj.com^\n||qgraprebabxo.com^\n||qijffgqsbkii.com^\n||qiktwikahncl.com^\n||qinsmmxvacuh.com^\n||qiremmtynkae.com^\n||qixlpaaeaspr.com^\n||qjskosdsxanp.com^\n||qkpwdakgxynv.com^\n||qkuprxbmkeqp.com^\n||qljczwei.com^\n||qndqwtrwguhv.com^\n||qnpolbme.com^\n||qnqrmqwehcpa.com^\n||qoiowocphgjm.com^\n||qolnnepubuyz.com^\n||qotwtnckqrke.com^\n||qpcyafunjtir.com^\n||qpiyjprptazz.com^\n||qqapezviufsh.com^\n||qqylzyrqnewl.com^\n||qrozsnmc.com^\n||qtjafpcpmcri.com^\n||qtsmzrnccnwz.com^\n||quaizzywzluk.com^\n||qudpdpkxffzt.com^\n||qvsbroqoaggw.com^\n||qwbnzilogwdc.com^\n||qwhkndqqxxbq.com^\n||qxnniyuuaxhv.com^\n||qxxyzmukttyp.com^\n||qzxtbsnaebfw.com^\n||rbdmtydtobai.com^\n||rbfxurlfctsz.com^\n||rbppnzuxoatx.com^\n||rbrbvedkazkr.com^\n||rbsfglbipyfs.com^\n||rbvfibdsouqz.com^\n||rbyjirwjbibz.com^\n||rcjthosmxldl.com^\n||rdikvendxamg.com^\n||rdzxpvbveezdkcyustcomuhczsbvteccejkdkfepouuhxpxtmy.com^\n||reebinbxhlva.com^\n||rffqzbqqmuhaomjpwatukocrykmesssfdhpjuoptovsthbsswd.com^\n||rfvicvayyfsp.com^\n||rgztepyoefvm.com^\n||rhfntvnbxfxu.com^\n||rhfvzboqkjfmabakkxggqdmulrsxmisvuzqijzvysbcgyycwfk.com^\n||riaetcuycxjz.com^\n||rihzsedipaqq.com^\n||rjnkpqax.com^\n||rjyihkorkewq.com^\n||rkelvtnnhofl.com^\n||rklluqchluxg.com^\n||rkrpvzgzdwqaynyzxkuviotbvibnpqaktcioaaukckhbvkognu.com^\n||rkvpcjiuumbk.com^\n||rlypbeouoxxw.com^\n||rmetgarrpiouttmwqtuajcnzgesgozrihrzwmjlpxvcnmdqath.com^\n||rmjxcosbfgyl.com^\n||rnrbvhaoqzcksxbhgqtrucinodprlsmuvwmaxqhxngkqlsiwwp.com^\n||rpczohkv.com^\n||rpspeqqiddjm.com^\n||rpulxcwmnuxi.com^\n||rqtdnrhjktzr.com^\n||rscgfvsximqdpowcmruwitolouncrmnribnfobxzfhrpdmahqe.com^\n||ruovcruc.com^\n||rvzudtgpvwxz.com^\n||rwtvvdspsbll.com^\n||rxicrihobtkf.com^\n||rxisfwvggzot.com^\n||rxsazdeoypma.com^\n||rxuqpktyqixa.com^\n||rzcmcqljwxyy.com^\n||sagulzuyvybu.com^\n||saipuciruuja.com^\n||sajhiqlcsugy.com^\n||sapvummffiay.com^\n||sauispjbeisl.com^\n||sbftffngpzwt.com^\n||sbhnftwdlpbo.com^\n||scbnvzfscfmn.com^\n||scbywuiojqvh.com^\n||sceuexzmiwrf.com^\n||scgyndrujhzf.com^\n||scmffjmashzc.com^\n||scuwbelujeeu.com^\n||scxxbyqjslyp.com^\n||sdemctwaiazt.com^\n||sdqspuyipbof.com^\n||seiqobwpbofg.com^\n||sgfcsnwegazn.com^\n||sgzsviqlvcxc.com^\n||shnmhrlcredd.com^\n||siwtuvvgraum.com^\n||sjgklyyyraghhrgimsepycygdqvezppyfjkqddhlzbimoabjae.com^\n||sjpexaylsfjnopulpgkbqtkzieizcdtslnofpkafsqweztufpa.com^\n||skknyxzaixws.com^\n||skzhfyqozkic.com^\n||smrqvdpgkbvz.com^\n||sncpizczabhhafkzeifklgonzzkpqgogmnhyeggikzloelmfmd.com^\n||snetddbbbgbp.com^\n||snjhhcnr.com^\n||snpevihwaepwxapnevcpiqxrsewuuonzuslrzrcxqwltupzbwu.com^\n||sokanffuyinr.com^\n||spfrlpjmvkmq.com^\n||sqnezuqjdbhe.com^\n||sqtsuzrfefwy.com^\n||sriaqmzx.com^\n||srizwhcdjruf.com^\n||srksyzqzcetq.com^\n||srppykbedhqp.com^\n||ssdphmfduwcl.com^\n||ssjhkvwjoovf.com^\n||ssvolkkihcyp.com^\n||stnvgvtwzzrh.com^\n||sualzmze.com^\n||suwadesdshrg.com^\n||svjloaomrher.com^\n||svrsqqtj.com^\n||swckuwtoyrklhtccjuuvcstyesxpbmycjogrqkivmmcqqdezld.com^\n||swgvpkwmojcv.com^\n||sxprcyzcpqil.com^\n||sxtzhwvbuflt.com^\n||sydnkqqscbxc.com^\n||syorlvhuzgmdqbuxgiulsrusnkgkpvbwmxeqqcboeamyqmyexv.com^\n||sznxdqqvjgam.com^\n||szyejlnlvnmy.com^\n||tabeduhsdhlkalelecelxbcwvsfyspwictbszchbbratpojhlb.com^\n||tailpdulprkp.com^\n||tamqqjgbvbps.com^\n||taodggarfrmd.com^\n||tapihmxemcksuvleuzpodsdfubceomxfqayamnsoswxzkijjmw.com^\n||tawgiuioeaovaozwassucoydtrsellartytpikvcjpuwpagwfv.com^\n||tazvowjqekha.com^\n||tcgojxmwkkgm.com^\n||tedlrouwixqq.com^\n||tevrhhgzzutw.com^\n||teyuzyrjmrdi.com^\n||tfbzzigqzbax.com^\n||tfqzkesrzttj.com^\n||tftsbqbeuthh.com^\n||tgrmzphjmvem.com^\n||thnqemehtyfe.com^\n||thvdzghlvfoh.com^\n||thxdbyracswy.com^\n||tigzuaivmtgo.com^\n||tijosnqojfmv.com^\n||tikwglketskr.com^\n||tiouqzubepuy.com^\n||tivlvdeuokwy.com^\n||tjbgiyek.com^\n||tkarkbzkirlw.com^\n||tkeeebdseixv.com^\n||tkfsmiyiozuo.com^\n||tkoatkkdwyky.com^\n||tksljtdqkqxh.com^\n||tlzhxxfeteeimoonsegagetpulbygiqyfvulvemqnfqnoazccg.com^\n||tmdcfkxcckvqbqbixszbdyfjgusfzyguvtvvisojtswwvoduhi.com^\n||tmexywfvjoei.com^\n||tmkbpnkruped.com^\n||tmwhazsjnhip.com^\n||tnpbbdrvwwip.com^\n||toyhxqjgqcjo.com^\n||tpvprtdclnym.com^\n||trcbxjusetvc.com^\n||trqbzsxnzxmf.com^\n||tskctmvpwjdb.com^\n||tsuitufixxlf.com^\n||tswhwnkcjvxf.com^\n||tujbidamlfrn.com^\n||tumfvfvyxusz.com^\n||turyvfzreolc.com^\n||twnrkedqefhv.com^\n||txbvzcyfyyoy.com^\n||tyzfzrjaxxcg.com^\n||tzjngascinro.com^\n||uavqdzorwish.com^\n||uaxdkesuxtvu.com^\n||ubazpxeafwjr.com^\n||ubhzahnzujqlvecihiyukradtnbmjyjsktsoeagcrbbsfzzrfi.com^\n||uccgdtmmxota.com^\n||uckxjsiy.com^\n||udbwpgvnalth.com^\n||udvbtgkxwnap.com^\n||uebcqdgigsid.com^\n||uecjpplzfjur.com^\n||uerhhgezdrdi.com^\n||uerladwdpkge.com^\n||ufmnicckqyru.com^\n||ugxyemavfvlolypdqcksmqzorlphjycckszifyknwlfcvxxihx.com^\n||uhfqrxwlnszw.com^\n||uilknldyynwm.com^\n||ujdctbsbbimb.com^\n||ujocmihdknwj.com^\n||ujqbxbcqtbqt.com^\n||ujtyosgemtnx.com^\n||ujyyciaedxqr.com^\n||ukjzdydnveuc.com^\n||ulpxnhiugynh.com^\n||umboffikfkoc.com^\n||umqsrvdg.com^\n||umxzhxfrrkmt.com^\n||uqgloylf.com^\n||uqpotqld.com^\n||usoqghurirvz.com^\n||usymycvrilyt.com^\n||uszpxpcoflkl.com^\n||utzpjbrtyjuj.com^\n||uupqrsjbxrstncicwcdlzrcgoycrgurvfbuiraklyimzzyimrq.com^\n||uuproxhcbcsl.com^\n||uvakjjlbjrmx.com^\n||uvmsfffedzzw.com^\n||uvxaafcozjgh.com^\n||uwrzafoopcyr.com^\n||uxyofgcf.com^\n||uyfsqkwhpihm.com^\n||uyqzlnmdtfpnqskyyvidmllmzauitvaijcgqjldwcwvewjgwfj.com^\n||uzbboiydfzog.com^\n||uzesptwcwwmt.com^\n||uzqtaxiorsev.com^\n||uzreuvnlizlz.com^\n||vamuglchdpte.com^\n||vaoajrwmjzxp.com^\n||vbupfouyymse.com^\n||vbuqjdyrsrvi.com^\n||vdlvaqsbaiok.com^\n||vdpyueivvsuc.com^\n||vdqarbfqauec.com^\n||vdvylfkwjpvw.com^\n||vdyqcdxqvebl.com^\n||veeqneifeblh.com^\n||vejlbuixnknc.com^\n||vfasewomnmco.com^\n||vfkfctmtgrtq.com^\n||vfnvsvxlgxbvndhgqqohfgdcfprvxqisiqhclfhdpnjzloctny.com^\n||vgfeahkrzixa.com^\n||vgmrqurgxlimcawbweuzbvbzxabsfuuxseldfapjmxoboaplmg.com^\n||vhctcywajcwv.com^\n||vhiaxerjzbqi.com^\n||vhwuphctrfil.com^\n||vivcdctagoij.com^\n||vizsvhgfkcli.com^\n||vjrpdagpjwyt.com^\n||vjzttumdetao.com^\n||vkarvfrrlhmv.com^\n||vkdbvgcawubn.com^\n||vlnveqkifcpxdosizybusvjqkfmowoawoshlmcbittpoywblpe.com^\n||vlvowhlxxibn.com^\n||vmcpydzlqfcg.com^\n||vmvhmwppcsvd.com^\n||vnadjbcsxfyt.com^\n||vnyginzinvmq.com^\n||volleqgoafcb.com^\n||vpfiiojohjch.com^\n||vpsotshujdguwijdiyzyacgwuxgnlucgsrhhhglezlkrpmdfiy.com^\n||vpwwtzprrkcn.com^\n||vqaprwkiwset.com^\n||vqfksrwnxodc.com^\n||vrqajyuu.com^\n||vtcquvxsaosz.com^\n||vtoygnkflehv.com^\n||vtqdavdjsymt.com^\n||vucanmoywief.com^\n||vulexmouotod.com^\n||vunwzlxfsogj.com^\n||vwugfpktabed.com^\n||vxbtrsqjnjpq.com^\n||vxlpefsjnmws.com^\n||vxuhavco.com^\n||vxvxsgut.com^\n||vydlqaxchmij.com^\n||vyozgtrtyoms.com^\n||vywycfxgxqlv.com^\n||vzhbfwpo.com^\n||vzmnvqiqgxqk.com^\n||waentchjzuwq.com^\n||watxeoifxbjo.com^\n||wbqliddtojkf.com^\n||wbtgtphzivet.com^\n||wbvsgqtwyvjb.com^\n||wcgquaaknuha.com^\n||wcoloqvrhhcf.com^\n||wdbddckjoguz.com^\n||wdcxuezpxivqgmecukeirnsyhjpjoqdqfdtchquwyqatlwxtgq.com^\n||wddtrsuqmqhw.com^\n||wephuklsjobdxqllpeklcrvquyyifgkictuepzxxhzpjbclmcq.com^\n||wepmmzpypfwq.com^\n||wepzfylndtwu.com^\n||wfiejyjdlbsrkklvxxwkferadhbcwtxrotehopgqppsqwluboc.com^\n||wgefjuno.com^\n||wggnmbmedlmo.com^\n||whsjufifuwkw.com^\n||whzbmdeypkrb.com^\n||wicxfvlozsqz.com^\n||wijczxvihjyu.com^\n||wjdjovjrxsqx.com^\n||wkexsfmw.com^\n||wkggjmkrkvot.com^\n||wkhychiklhdglppaeynvntkublzecyyymosjkiofraxechigon.com^\n||wklyhvfc.com^\n||wljuxryvolwc.com^\n||wmhksxycucxb.com^\n||wmjdnluokizo.com^\n||wmwkwubufart.com^\n||wnzxwgatxjuf.com^\n||wpktjtwsidcz.com^\n||wqbvqmremvgp.com^\n||wqnxcthitqpf.com^\n||wqpcxujvkvhr.com^\n||wqzaloayckal.com^\n||wrmcfyzl.com^\n||wrmwikcnynbk.com^\n||wrqjwrrpsnnm.com^\n||wrtnetixxrmg.com^\n||wsaijhlcnsqu.com^\n||wscrsmuagezg.com^\n||wscvmnvhanbr.com^\n||wsfqmxdljrknkalwskqmefnonnyoqjmeapkmzqwghehedukmuj.com^\n||wsscyuyclild.com^\n||wtvyenir.com^\n||wvljugmqpfyd.com^\n||wvqqugicfuac.com^\n||wwgdpbvbrublvjfbeunqvkrnvggoeubcfxzdjrgcgbnvgcolbf.com^\n||wwgjtcge.com^\n||wwnlyzbedeum.com^\n||wxdtvssnezam.com^\n||wxjqyqvagefw.com^\n||wxxfcyoaymug.com^\n||wydwkpjomckb.com^\n||wylnauxhkerp.com^\n||wzjbvbxldfrn.com^\n||wzueqhwf.com^\n||xbdlsolradeh.com^\n||xbynkkqi.com^\n||xcakezoqgkmj.com^\n||xcjoqraqjwmk.com^\n||xconeeitqrrq.com^\n||xcrruqesggzc.com^\n||xdqlnidntqmz.com^\n||xdwqixeyhvqd.com^\n||xegavyzkxowj.com^\n||xfgqvqoyzeiu.com^\n||xhdzcofomosh.com^\n||xhojlvfznietogsusdiflwvxpkfhixbgdxcnsdshxwdlnhtlih.com^\n||xhqilhfrfkoecllmthusrpycaogrfivehyymyqkpmxbtomexwl.com^\n||xhwtilplkmvbxumaxwmpaqexnwxypcyndhjokwqkxcwbbsclqh.com^\n||ximeldnjuusl.com^\n||xirtesuryeqk.com^\n||xjompsubsozc.com^\n||xjsqhlfscjxo.com^\n||xkygmtrrjalx.com^\n||xmmnwyxkfcavuqhsoxfrjplodnhzaafbpsojnqjeoofyqallmf.com^\n||xoqwirroygxv.com^\n||xpkhmrdqhiux.com^\n||xpnttdct.com^\n||xqhgisklvxrh.com^\n||xqopbyfjdqfs.com^\n||xrivpngzagpy.com^\n||xseczkcysdvc.com^\n||xswnrjbzmdof.com^\n||xswutjmmznesinsltpkefkjifvchyqiinnorwikatwbqzjelnp.com^\n||xteabvgwersq.com^\n||xtqfguvsmroo.com^\n||xttrofww.com^\n||xuwptpzdwyaw.com^\n||xwmbaxufcdxb.com^\n||xwwkuacmqblu.com^\n||xwwsojvluzsb.com^\n||xxwpminhccoq.com^\n||xxyafiswqcqz.com^\n||xxzkqbdibdgq.com^\n||yaqysxlohdyg.com^\n||yasltdlichfd.com^\n||yaxdboxgsbgh.com^\n||ybhaoglgbgdk.com^\n||ycmejutxukkz.com^\n||ycojhxdobkrd.com^\n||yctquwjbbkfa.com^\n||yepiafsrxffl.com^\n||yesucplcylxg.com^\n||yfkwqoswbghk.com^\n||yflpucjkuwvh.com^\n||ygrtbssc.com^\n||yhzobwqqecaa.com^\n||yiyycuqozjwc.com^\n||yjjglyoytiew.com^\n||yjjtxuhfglxa.com^\n||ykbcogkoiqdw.com^\n||yktkodofnikf.com^\n||ykwdfjergthe.com^\n||ylhjsrwqtqqb.com^\n||ylksuifuyryt.com^\n||ylqezcnlzfsj.com^\n||ymlbuooxppzt.com^\n||ynlrfiwj.com^\n||ynrbxyxmvihoydoduefogolpzgdlpnejalxldwjlnsolmismqd.com^\n||ynxrrzgfkuih.com^\n||yoywgmzjgtfl.com^\n||ypbfrhlgquaj.com^\n||ypyarwgh.com^\n||yqtzhigbiame.com^\n||ytapgckhhvou.com^\n||ytaujxmxxxmm.com^\n||ytiyuqfxjbke.com^\n||ytwtqabrkfmu.com^\n||yupwqyocvvnw.com^\n||ywbfhuofnvuk.com^\n||yxbtyzqcczra.com^\n||yyajvvjrcigf.com^\n||yyuztnlcpiym.com^\n||yzsiwyvmgftjuqfoejhypwkmdawtwlpvawzewtrrrdfykqhccq.com^\n||yzygkqjhedpw.com^\n||zacbwfgqvxan.com^\n||zamjzpwgekeo.com^\n||zbfncjtaiwngdsrxvykupflpibvbrewhemghxlwsdoluaztwyi.com^\n||zbrkywjutuxu.com^\n||zbtqpkimkjcr.com^\n||zfkkmayphqrw.com^\n||zfqpjxuycxdl.com^\n||zfrzdepuaqebzlenihciadhdjzujnexvnksksqtazbaywgmzwl.com^\n||zgalejbegahc.com^\n||zgdejlhmzjrd.com^\n||zhdmplptugiu.com^\n||zhkziiaajuad.com^\n||ziuxkdcgsjhq.com^\n||zizmvnytmdto.com^\n||zjgygpdfudfu.com^\n||zkennongwozs.com^\n||zlbdtqoayesloeazgxkueqhfzadqjqqduwrufqemhpbrjvwaar.com^\n||zlvbqseyjdna.com^\n||zmbrweqglexv.com^\n||zmnqoymznwng.com^\n||zmxcefuntbgf.com^\n||zmytwgfd.com^\n||znmrgzozlohe.com^\n||znvctmolksaj.com^\n||zoileyozfexv.com^\n||zoowknbw.com^\n||zpkobplsfnxf.com^\n||zpmbsivi.com^\n||zpnbzxbiqann.com^\n||zptncsir.com^\n||zpxbdukjmcft.com^\n||zpznbracwdai.com^\n||zqaxaqqqutrx.com^\n||zqjfpxcgivkv.com^\n||zrxgdnxneslb.com^\n||zsancthhfvqm.com^\n||zsihqvjfwwlk.com^\n||ztfrlktqtcnl.com^\n||ztioesdyffrr.com^\n||ztyrgxdelngf.com^\n||zualhpolssus.com^\n||zupeaoohmntp.com^\n||zuuwfrphdgxk.com^\n||zvqjjurhikku.com^\n||zvuespzsdgdq.com^\n||zwqfnizwcvbx.com^\n||zxbjgrxbcgrp.com^\n||zyaorkkdvcbl.com^\n||zycvyudt.com^\n||zylokfmgrtzv.com^\n||zyqlfplqdgxu.com^\n! Yavli.com\n||247view.net^\n||absilf.com^\n||absquint.com^\n||acceletor.net^\n||accltr.com^\n||accmndtion.org^\n||addo-mnton.com^\n||advuatianf.com^\n||aistilierf.com^\n||allianrd.net^\n||ambushar.net^\n||anomiely.com^\n||antuandi.net^\n||anumiltrk.com^\n||appr8.net^\n||artbr.net^\n||azwergz.net^\n||baordrid.com^\n||batarsur.com^\n||baungarnr.com^\n||biankord.net^\n||biastoful.net^\n||blaundorz.com^\n||blazwuatr.com^\n||bliankerd.net^\n||blindury.com^\n||blipi.net^\n||blowwor.com^\n||bluandi.com^\n||bluazard.net^\n||bluposr.com^\n||boafernd.com^\n||bridlonz.com^\n||bridlonz.link^\n||briduend.com^\n||bualtwif.com^\n||buamingh.com^\n||buandirs.net^\n||buangkoj.com^\n||buatongz.net^\n||buhafr.net^\n||buoalait.com^\n||c8factor.com^\n||casiours.com^\n||changosity.com^\n||chansiar.net^\n||charctr.com^\n||chiuawa.net^\n||chualangry.com^\n||clicksifter.com^\n||coaterhand.net^\n||compoter.net^\n||conexitry.com^\n||content-4-u.com^\n||contentolyze.net^\n||contentr.net^\n||cotnr.com^\n||crhikay.me^\n||cuantroy.com^\n||cuasparian.com^\n||d3lens.com^\n||derkopd.com^\n||deuskex.link^\n||diabolicaf.com^\n||dilpy.org^\n||discvr.net^\n||domri.net^\n||doumantr.com^\n||draugonda.net^\n||drfflt.info^\n||drndi.net^\n||duactinor.net^\n||duading.link^\n||duaing.net^\n||duamews.com^\n||duavindr.com^\n||dutolats.net^\n||ectensian.net^\n||edabl.net^\n||edgualf.com^\n||elepheny.com^\n||entru.co^\n||ergers.net^\n||ershgrst.com^\n||esults.net^\n||exactly0r.com^\n||exciliburn.com^\n||excolobar.com^\n||exernala.com^\n||exlpor.com^\n||extonsuan.com^\n||faunsts.me^\n||flaudnrs.me^\n||flaurse.net^\n||fleawier.com^\n||foulsomty.com^\n||fowar.net^\n||frevi.net^\n||frhgxd.com^\n||frizergt.net^\n||frlssw.me^\n||fruamens.com^\n||frxle.com^\n||frxrydv.com^\n||frzdrn.info^\n||fuandarst.com^\n||genegd.com^\n||gghfncd.net^\n||gruadhc.com^\n||gualdoniye.com^\n||guaperty.net^\n||gusufrs.me^\n||hapnr.net^\n||havnr.com^\n||heizuanubr.net^\n||hobri.net^\n||holmgard.link^\n||hoppr.co^\n||huamfriys.net^\n||iambibiler.net^\n||ignup.com^\n||incotand.com^\n||induanajo.com^\n||inomoang.com^\n||insiruand.com^\n||invetpl.com^\n||iunbrudy.net^\n||ivism.org^\n||jaspensar.com^\n||jdrm4.com^\n||jellr.net^\n||jerwing.net^\n||jianscoat.com^\n||juarinet.com^\n||juruasikr.net^\n||jusukrs.com^\n||kidasfid.com^\n||kilomonj.net^\n||kioshow.com^\n||knoandr.com^\n||kowodan.net^\n||kuangard.net^\n||leanoisgo.com^\n||lesuard.com^\n||lia-ndr.com^\n||liksuad.com^\n||lirte.org^\n||liveclik.co^\n||loopr.co^\n||luadcik.com^\n||lunio.net^\n||maningrs.com^\n||marvilias.com^\n||meniald.com^\n||monova.site^\n||moucitons.com^\n||muatrasec.com^\n||muriarw.com^\n||nrfort.com^\n||nuafguy.com^\n||nuaknamg.net^\n||nualoghy.com^\n||nuzilung.net^\n||oplo.org^\n||opner.co^\n||opter.co^\n||osrto.com^\n||p7vortex.com^\n||pianoldor.com^\n||pikkr.net^\n||poaurtor.com^\n||polawrg.com^\n||polephen.com^\n||prfffc.info^\n||prndi.net^\n||puoplord.net^\n||q3sift.com^\n||qewa33a.com^\n||qulifiad.com^\n||qzsccm.com^\n||r3seek.com^\n||rdige.com^\n||reaspans.com^\n||regersd.net^\n||reitb.com^\n||rfgre.info^\n||rheneyer.net^\n||rhgersf.com^\n||rigistrar.net^\n||rlex.org^\n||rterdf.me^\n||ruamupr.com^\n||ruandorg.com^\n||ruandr.com^\n||ruap-oldr.net^\n||rugistoto.net^\n||rugistratuan.com^\n||selectr.net^\n||sfesd.net^\n||sightr.net^\n||simusangr.com^\n||spamualfr.com^\n||speandorf.net^\n||spereminf.com^\n||splazards.com^\n||spoa-soard.com^\n||suadimons.net^\n||suarbiard.com^\n||suartings.com^\n||suavalds.com^\n||swualyer.com^\n||sxrrxa.net^\n||t3sort.com^\n||t7row.com^\n||tersur.net^\n||th4wwe.net^\n||thiscdn.com^\n||thrilamd.net^\n||thrutime.net^\n||tolosgrey.net^\n||topdi.net^\n||trinusuras.net^\n||trllxv.co^\n||trndi.net^\n||trualaid.com^\n||tualipoly.net^\n||turanasi.com^\n||uanbalible.com^\n||unuarvse.net^\n||uppo.co^\n||username1.link^\n||v8bridge.link^\n||vieway.co^\n||viewscout.com^\n||virsualr.com^\n||vopdi.com^\n||vuadiolgy.net^\n||waddr.com^\n||wensdteuy.com^\n||wolopiar.com^\n||wopdi.com^\n||wqqqpe.com^\n||wuakula.net^\n||wuarnurf.net^\n||wuatriser.net^\n||wudr.net^\n||xcrsqg.com^\n||xplrer.co^\n||xylopologyn.com^\n||yardr.net^\n||yerstrd.net^\n||yobr.net^\n||yodr.net^\n||yomri.net^\n||yopdi.com^\n||ypppdc.com^\n||ypprr.com^\n||yrrrbn.me^\n||yualongf.net^\n||yuasaghn.com^\n||z4pick.com^\n||ziccardia.com^\n||zomri.net^\n||zrfrornn.net^\n! temporary workaround for Adblock Plus for Chrome bug #4599 (https://issues.adblockplus.org/ticket/4599)\n||voodoo.com^\n!\n! Adguard Filter\n!\n! ???????? ???????? ?????, ?????????????? ?????????? ??????.\n!\n||mykinotochka.ru^\n||xxuhter.ru:8040\n||ughwashis.ru^\n||dgulden.ru^\n||lvodomi.info^\n||appsflybeta.biz^\n||statredpic.ru^\n||scelebnow.xyz^\n||et-code.ru:8443\n||littel.biz\n||level1cdn.com^\n||0483bm4mlow8.xyz^\n||vifog.com^\n||videoframe.blue^\n||uglactons.com^\n||promoengine.biz^\n||yplan.ru^\n||technogies.ru^\n||keyti.ru^\n||news2you.ru\n||adafazerub.com^\n||adforce.team^\n||adkeeper.ru^\n||adsentiz.ru^\n||bc05.ru^\n||bestadlinks.ru^\n||brtmout.pro^\n||cdn1now.com^\n||ettalhap.com^\n||getfon.ru^\n||inhadhen.com^\n||iqkbi.top^\n||iqsns.top^\n||livedecnow.com^\n||luxads.net^\n||magic-traff.com^\n||mediavenus.com^\n||nest.ru.net^\n||oblivki.biz^\n||octobird.com^\n||oftatsit.com^\n||otinekocin.com^\n||rushkolnik.ru^\n||s1venus.com^\n||seavideo22.com^\n||teaserpro.ru^\n||tizerlady.ru^\n||tizgo.ru^\n||tn05.ru^\n||tqqjk.top^\n||video1002.com^\n||wycji.top^\n||zoomclick.ru^\n||vengovision.ru^\n||unolis.ru^\n||shpultiki.ru^\n||vpath.net^\n||mybce.top^\n||refbanners.website^\n||themoneyes.ru^\n||mpay1.info^\n||tizinfo.ru^\n||bingp.ru^\n||octoclick.net^\n||parabit.ru^\n||api.ztgm17.ru^\n||irtula.ru^\n||cajkov.ru^\n||mt-data.ru\n||amigo-biz.ru^\n||dnovaku.ru^\n||izdagda.ru^\n||rypamigbr.ru^\n||hottod.info^\n||pecaning.com^\n||fperefo.ru^\n||ksdifdd.com^\n||luxupadva.com^\n||sjsmartcontent.org^\n||stablemoney.ru^\n||adsbc.pp.ua^\n||dreti.ru^\n||exoticads.com^\n||dlski.space^\n||rutrk.org^\n||ts-ads.info^\n||azsin.ru^\n||ibishic.ru^\n||media.kahoxa.ru^\n||clhko.top^\n||justonsrep.com^\n||tlr1.biz^\n||apytrc.com^\n||xxuhter.ru^\n||utarget.pro^\n||witgatons.com^\n||winvideo.xyz^\n||qutreinr.pw^\n||adsmmgp.com^\n||kinomagnitamana.ru^\n||th700.com^\n||trst-st.com^\n||kinotochkaz.com^\n||xpicw.top^\n||thefoxads.ru^\n||hesrepsa.com^\n||efpark.ru^\n||ozdau.top^\n||noletdint.com^\n||toormpc.com^\n||p.biasdo.com^\n://gj7.ru^\n||now73.ru^\n||bingq.ru^\n||fseed.ru^\n||vidalak.com^\n||flipdigital.ru^\n||clickads.name^\n||ksjdkjh.ru^\n||cdn-my1.ru^\n||os340.com^\n||advertise.ru^\n||adreaction.ru^\n||keitush.ru^\n||61861486484.ru^\n||purchaseklik.ru^\n||tizka.ru^\n||tedpasit.com^\n||skidl.ru^\n||pajigkcel.work^\n||ledhatbet.com^\n||cohedttoft.com^\n||glumifo.ru^\n||whatcl.ru^\n||tedverspar.com^\n||cmsmodnews.com^\n||mp-https.info^\n||bodykaa.ru^\n||eudcqm.uihdlx.xyz^\n||fovs.qkvipgloy.xyz^\n||aztu.ynfolstw.xyz^\n||gddrio.com^\n||wofri.uihdlx.xyz^\n||neyrvru.wmbgc.xyz^\n||tpjz.tmuurnthtf.xyz^\n||dmoid.top^\n||nnoxzo.rfskbylbsf.xyz^\n||tcoxndk.hfgjdcbrv.xyz^\n||izfbxg.joyreactor.cc^\n||eatp.evztib.xyz^\n||kinak.top^\n||dyrxq.rfskbylbsf.xyz^\n||pelckw.qjudpxkisv.xyz^\n||ual.ocmcbyxm.xyz^\n||nxstx.top^\n||mqmh.uihdlx.xyz^\n||ogbwqq.gkwtk.xyz^\n||vv.tmska.com^\n||parhadat.com^\n||best-cargo.ru^\n||moevideo.biz^\n||delivery.adrecover.com^\n||wheretogo.bid^\n||fxbpro.com^\n||wwgate.ru^\n||fytboti.ru^\n||tramate.ru^\n||ewtofu.ru^\n||ajpxs.xyz^\n||admachina.com^\n||s1venus.biz^\n||bedowntoft.com^\n||lerester.com^\n||scelnow.xyz^&third-party\n||ettalhap.com^&third-party\n||aoodw.top^&third-party\n||itdzv.top^&third-party\n||pidorg.ru^\n||irleti.com^\n||belosne6zhka.ru^\n||airlead.ru^\n||kxqvnfcg.xyz^\n||aqcmri.xyz^\n||khilane.ru^\n||adgrowmedia.com^\n||v2mlamber.com^\n||dflfnrmi.xyz^\n||uplvcx.xyz^\n||imtowoz.ru^\n||offshp.ru^\n||a.thairesort.ru^\n||mcbag.top^\n||retail-server.ru^\n||10root25.website^\n||img1461.r.worldssl.net^\n||owkkdsfg.com^\n||adveasy.ru^\n||idaschop.ru^\n||huminfakt.ru^\n||vidyp.com^\n||lagrobe.ru^\n||vidgain.com^\n||2xclick.ru^\n||code.vh45130.eurodir.ru^\n||trenews.ru^\n||golayazv.com^\n||seobloger.ru^\n||veretiggoo.com^\n||refpa.top^\n||textun.ru^\n||klivz.com^\n||advertop.ru^\n||jxxgg.top^\n||traffictoadv.com^\n||littel.biz^\n||telvanil.ru^\n||puklisi.ru^\n||navaxudoru.com^\n||robaduvulo.com^\n||vogozara.ru^\n||mobiile-service.ru^\n||uytichas.ru^\n||01n2e3pac2.com^\n||istcs.top^\n||eioxy.top^\n||hzfcx.top^\n||retagapp.com^\n||advcache.ru^\n||my-img.ru^\n||giraff.io^\n||chajv.top^\n||cobrand.ria.com^\n||yourfoxes1.ru^\n||0916video.ru^\n||uldmare.com^\n||sandsoftors.com^\n||teasermoney.com^\n||bin40.com^\n||protovid.com^\n||orionsp.xyz^\n||3lx.ru^\n||add.ladycleo.ru^\n||teasermoney.ru^\n||teaserwin.ru^\n||ulosmuynstes.ml^\n||tizergo.net^\n||gottimuch.com^\n||to330.com^\n||vogozapa.ru^\n||yourfoxes2.ru^\n||yourfoxes3.ru^\n||yourfoxes4.ru^\n||yourfoxes5.ru^\n||tizernet.biz^\n||gynax.com^\n||nativeroll.tv^\n||mastervesti.ru^\n||purige.ru^\n||rootmedia.ws\n||peptido.ru^\n.adonweb.ru^\n.adv-modulation.info^\n.adv-multithreading.info^\n.allalla.com^\n.cpa1.ru^\n.ecommtools.com^\n.endplace.info^\n.gameleads.ru^\n.good-teaser.info^\n.goprofessionalback.info^\n.is.traff-numerical.info^\n.jjjo.ru^\n.marketgid.com^\n.methiskormvs.info^\n.opposingban.info^\n.piccash.net^\n.script-root.info^\n.sexplaycam.com^\n.soft11.ru^\n.tovarro.com^\n.tpm.pw^\n.webhop.net^\n.work-proxy.info^\n.yourbesttraf.info^\n//oyy.ru^\n//pp1.ru^\n//www.oyy.ru^\n/js/loader.js|\n/rotator/jqld_cu.php\n/vk_code.php\n://epn.bz^\n://jhf.ru^\n://kma.biz^\nhttp://7ads.ru^\nhttp://cpa1.ru^\nhttp://cpa6.ru^\nhttp://okeo.ru^\nhttp://or.ru^\nhttp://p-up.ru^\nhttp://red.by^\nhttp://ttcl.ru^\nhttp://w7.ru/\nhttp://wq3.ru/\npolezno-2012.info^\nthr.ru##.top_branding\n||00bloggers.ru^\n||029qz3xam2qq.xyz^\n||0427d7.se^\n||0664ic555p.com^\n||0816bvh.ru^\n||1000zakazov.ru^\n||1001noch.net^\n||100proc.com^\n||100projects.ru^\n||109.203.98.37^\n||10cd.ru/\n||123fvd.com^\n||176.58.105.153^\n||195.234.99.231^\n||1biznes.net\n||1invite.ru^\n||1pop.ru/\n||1sputnik.ru^\n||1the-message.in^\n||1tizer.ru^\n||1traf.com^\n||1traf.ru^\n||1traff.ru^\n||1txt.ru/\n||1under.ru^\n||2.kaktakkk.ru^\n||2011-ru.com^\n||2011-rus.com^\n||2012.novaclub.net\n||20d625b48e.se^\n||212.7.200.164^\n||213.133.110.4^\n||213.133.111.49^\n||213.5.66.161^\n||24new.ru^\n||24newz.ru^\n||29info.ru^\n||2da2.ru/\n||2firefox.ru^\n||2lycosu.com^\n||2manygirlzhere.org^\n||2under.ru^\n||2vulkan.com^\n||34bogatirya.ru^\n||37.220.26.135^\n||37.220.26.136^\n||37.220.26.137^\n||3file.info^\n||3lucosy.com^\n||3under.ru^\n||46.182.85.201^\n||468.vologdainfo.ru^\n||4allclick.ru^\n||4click.ru^\n||4smi.ru/\n||5-kg.ru^\n||5.61.32.163^\n||56nnn.net^\n||5cc5.ru/\n||5licosy.com^\n||62.212.73.194^\n||6likosy.com^\n||7-link.ru^\n||77.247.178.41^\n||777-888.ru^\n||78.140.134.198^\n||78.140.149.216^\n||7dvd.ru/\n||7gomedia.ru^\n||7metodik.ru^\n||7out.ru^\n||7rtv.com^\n||84.16.230.172^\n||85.17.254.150^\n||85.25.243.206^\n||87.242.75.165^\n||88.85.77.92^\n||89.108.124.78^\n||8coins.net^\n||8test.ru^\n||8testov.ru^\n||91.207.192.31^\n||91.210.7.41^\n||92.241.163.207^\n||95.211.125.226^\n||95.211.82.233^\n||9fine.ru^\n||a.ava.com.ua^\n||a.fobos.tv^\n||a.jurnalu.ru^\n||a.kat.ph^\n||a.kubik3.ru^\n||a.mayakinfo.ru^\n||a.mediapapa.org^\n||a.sdska.ru^\n||aa-ds.ru^\n||aastob.com^\n||abcfilm.org^\n||abdmi.ru^\n||abrolog.ru^\n||abs-cdn.org^\n||abs.firstvds.ru^\n||absawm.com\n||abteaser.com^\n||abusieux.com^\n||academand.com^\n||aceadsys.net^\n||aceventik.com^\n||actgo.info^\n||actionads.ru^\n||actionpay.ru^\n||actionrtb.com^\n||actionteaser.ru^\n||ad-click.ru^\n||ad-context.com^\n||ad-emea.doubleclick.net/\n||ad-tizer.net^\n||ad-tool.com^\n||ad.doubleclick.*;sz=\n||ad.dumedia.ru^\n||ad.hutor.ru^\n||ad.istokiku.ru^\n||ad.oyy.ru^\n||ad.tbn.ru^\n||ad.yadro.ru^\n||ad1game.ru^\n||ad2.rambler.ru^\n||ad3.rambler.ru^\n||ad4sell.com^\n||adavz.xyz^\n||adbart.ru^\n||adbean.ru^\n||adbid.pl^\n||adbmi.com^\n||adbn.ru^\n||adbomb.ru^\n||adcamp.ru^\n||adcast.ru^\n||add.in.ua^\n||addflow.ru^\n||addmob.info^\n||addtraf.ru^\n||addweb.ru^\n||addynamo.com^\n||adeclc.com^\n||adforce.ru^\n||adfox.ru^\n||adfun.ru^\n||adgamble.net^\n||adhands.ru^\n||adhub.ru^\n||adinfo.ru^\n||adjs.ru^\n||adlabs.ru^\n||adlabsnetworks.com^\n||adland.ru^\n||adlift.ru^\n||adliner.ru^\n||adlog.com^\n||adlook.net^\n||admelon.ru^\n||admigo.ru^\n||admilk.ru^\n||admitlead.ru^\n||admixercreatives.blob.core.windows.net^\n||admulti.\n||admxr.com^\n||adn.100litca.ru^\n||adname.devsector.ru^\n||adname.ru^\n||adne.info^\n||adnova.ru^\n||adone.ru^\n||adoneast.ru^\n||adonweb.com/\n||adonweb.ru/\n||adpartner.pro^\n||adpod.in^\n||adpos.ru^\n||adpremium.ru^\n||adpro.com.ua^\n||adprofy.com^\n||adregain.ru^\n||adrichmedia.info^\n||adriverm.narod2.ru^\n||adrock.ru^\n||adrock.ua^\n||adroll.com^\n||adrots.ru^\n||adru.net^\n||ads-market.ru^\n||ads.people-group.net^\n||ads.viptrophy.com^\n||adselector.ru^\n||adsellers.net^\n||adserv01.ru^\n||adserver-live.yoc.mobi^\n||adserver1.\n||adservone.com^\n||adshell.ru^\n||adsliv.ru^\n||adsmo.ru^\n||adsn24.ru^\n||adssyscom.com^\n||adst.biz^\n||adstock.ru^\n||adswm.com^\n||adsyst.biz/\n||adsyst.net^\n||adsyst.ru/\n||adtimaserver.vn^\n||adtraff.ru^\n||adult-click.ru^\n||adultpay.net^\n||adultsharks.ru^\n||adulttiz.com\n||adulttraffic.ru^\n||adunit.chango.ca^\n||adv-first.ru^\n||adv-lancer.com^\n||adv-target.ru^\n||adv.vz.ru^\n||adv01st.com^\n||adv225489.ru^\n||adv457895.ru^\n||adv578125.ru^\n||adv679854.ru^\n||adv758968.ru^\n||adv859672.ru^\n||advaction.ru^\n||advagava.ru^\n||advagava.su^\n||advancets.org^\n||advant.ml^\n||advanter.ru^\n||advbroker.ru^\n||advclicks.net^\n||advcoder.ru^\n||advelogy.ru^\n||adverclicks.net^\n||advert.polonsil.ru^\n||advertbox.us\n||advertclick.ru^\n||adverti.me^\n||advertlink.ru^\n||advertoly.com^\n||advertpay.^\n||advertpro.^\n||advertshot.ru^\n||advertstar.ru^\n||advertte.com^\n||adverttraf.com^\n||adverweb.ru^\n||advgame.org^\n||advideo.ru^\n||advkino.ru^\n||advmaker.net^\n||advon.net^\n||advredirect.net^\n||advrush.com^\n||advsupra.com^\n||advvideo.com^\n||adwank.com^\n||adward.ru^\n||adwile.net^\n||adwolf.ru^\n||adxxx.com^\n||adylalahb.ru^\n||aerontre.com^\n||afdads.com\n||afficent.com^\n||affiliate.astraweb.com^\n||affiliates.a2hosting.com^\n||affiliates.bravenet.com^\n||affiliates.generatorsoftware.com^\n||affiliates.hotelclub.com^\n||affiliates.supergreenhosting.com^\n||afili.ru^\n||afterview.ru^\n||afuiw.com^\n||agitazio.com^\n||agitmedia.com^\n||agranis.ru^\n||agro.net.ru^\n||ahial.top^\n||aiadvi.com^\n||ainterme.com^\n||ajaxbig.ru^\n||aka-banner.com^\n||akabo.ru^\n||aksisma.ru^\n||alemobile.ru\n||alfainternet.su^\n||alfatarget.ru^\n||alferac.ru^\n||alienradar.ru\n||aliexpress-internet.ru^\n||alimama.cn^\n||allbn.net^\n||allmt.com\n||allowac.com^\n||allpopular.org^\n||allsiemens.com^\n||alltereg0.ru^\n||alltheladyz.xyz^\n||alltizer.ru^\n||almosto.com\n||alpari.ru^\n||alphacash.biz^\n||alphasyst.ru^\n||alslz.top^\n||altpubli.com^\n||am-investor.ru^\n||am12.ru^\n||amazingcl.ru^\n||analyticsncc.net^\n||andase.com^\n||anevod.ru^\n||anews.cc^\n||angestr.ru^\n||animeyes.ru^\n||aniruyt.ru^\n||anyfiles.ru^\n||aorta-net.com^\n||api-keks.com^\n||api.bonusberry.ru^\n||api.smartadv.ru^\n||api9.net^\n||appadvert.org^\n||aprelite.com^\n||arbadgika.ru^\n||arcade-advertisement.com^\n||ardiver.ru^\n||as.stat.su^\n||asdhit.com^\n||asiantraffic.net^\n||asketo.ru^\n||askmarket.net^\n||associazio.com^\n||associeta.com^\n||astob.com^\n||asuler.ru^\n||atamjanebyl.biz^\n||atlanhe.ru^\n||atlassolutions.com^\n||attivertura.com^\n||audtd.com^\n||autodengi.com^\n||autoteaser.ru^\n||autoua.com^\n||avacd.us^\n||avazone.ru^\n||avcyr.com^\n||avdego.net^\n||avmul.space^\n||avxcore.com^\n||awesomeredirector.com^\n||awmengine.net\n||axlpf.xyz^\n||azartaffiliates.com^\n||azartcash.com^\n||azartdaddy.com^\n||azartplay.com^\n||azbns.com^\n||azmdy.com^\n||azvozac.ru^\n||b-one.com.ua^\n||b.ddestiny.ru^\n||b.sweet-hd.com^\n||b2bcontext.ru^\n||b2bvideo.ru^\n||b81x63nc.ws.md^\n||ba2b687.se^\n||babki-online.ru^\n||bablogon.net^\n||backromy.com^\n||ban.mirorgazma.ru^\n||ban.tipsport.com^\n||ban.xxxvid.ru^\n||banamertur.com^\n||banerator.net^\n||banerator.silvercdn.com^\n||banner-in.net\n||banner-media.ru^\n||banner-mix.ru^\n||banner.kiev.ua\n||banner.lbs.km.ru^\n||banner.tomline.ru^\n||banner.ua\n||banner7.ru\n||bannerbook.ru^\n||bannerbro.ru^\n||bannerd.ru^\n||bannerka.ua^\n||bannerprog.tk^\n||banners.friendfinder.com^\n||banners.getiton.com^\n||banners.newshost.net^\n||banners.newsru.com^\n||banners.penthouse.com^\n||bannersold.eu^\n||bannet.fryazino.net^\n||bannuncio.com^\n||banreklama.ru^\n||bantex.ru^\n||bantiz.ru^\n||bara-banner.com^\n||barbys.ru^\n||barpe.ru^\n||barraien.com^\n||basetts.com^\n||bashnya.ru^\n||basketfan.my1.ru^\n||bb.stream24.ru^\n||bb.tidopro.biz^\n||bcclear.ru^\n||bcggo.ru^\n||bcoolandjustrelax.org^\n||bdwbxmzmpu.ru^\n||bdzhhjnml.pw^\n||be-mine.ru^\n||beboo.ru^\n||beerboms.ru^\n||beetraf.ru^\n||belole.ru^\n||belos2nez5hka.ru^\n||bepartner.ru^\n||berryfico.com^\n||best-solution-for-you.ru^\n||best.imgbum.net^\n||bestbestgirlz.in.net^\n||bestdevchenki.pw^\n||bestdoska.ru^\n||bestnews.biz^\n||bestnewsforbest.com^\n||bestofgeorgia.ge^\n||besttochka.ru^\n||bet-at-home.com^\n||beta.mediafort.ru^\n||betimogolef.com^\n||bgrndi.com\n||bidtraffic.ru^\n||bigbord.net^\n||bigleads.ru^\n||bigteaser.ru^\n||binmedia.su^\n||binteaser.su^\n||binweb.ru^\n||birgatrafa.ru^\n||bistr4.ru^\n||bistroduy.ru^\n||bizcom.com.ru^\n||bizua.com^\n||bjpwv.com^\n||bk.goodline.info^\n||bkrkv.com^\n||blackads.ru^\n||blafo.ru^\n||blessdi.com^\n||blinkogold.ru^\n||block.s2block.com^\n||blogan.ru^\n||blogun.ru^\n||bmax*.slavakirkudu.ru^\n||bmwcash.biz^\n||bn.ohah.ru^\n||bn.orthodoxy.ru^\n||bobrilla.com^\n||bodaybo.net^\n||bodrer.kinoromb.ru^\n||bodyclick.net^\n||bogopo.biz^\n||bold-in.ru^\n||bolshoykush.ru^\n||bombeers.ru^\n||bongacash.com^\n||bongobono.com^\n||boobzi.com^\n||boolff.com^\n||boom.1ccbt.com^\n||boroto.ru^\n||bossmoney.ru^\n||bposterss.net^\n||brandarium.net^\n||brandarium.ru^\n||brandomatic.ru^\n||braviration.ru^\n||breedac.com^\n||brndrm.com^\n||broklam.com^\n||browser-onlytv.ru^\n||brtom.ru^\n||bseen2.biz^\n||bugaga.tut.by^\n||bumaikr.com^\n||bussters.com^\n||busyprice.ru^\n||buytraf.ru^\n||buzzoola.com^\n||bw95vpjda.ru^\n||bx.neolabs.kz^\n||byrgin.ru^\n||bytde.com^\n||bzlwe.com^\n||c.cllvw.in^\n||c.cpa5.ru^\n||c.cpa6.ru^\n||c.cpl1.ru^\n||c.cpl2.ru^\n||c.ile.ru^\n||cacheserve.eurogrand.com^\n||camadmin.ru^\n||campeut.com^\n||caragots.com/\n||cartponi.tk^\n||carveac.com^\n||cash.loadsex.ru^\n||cashandfavor.ru^\n||cashimtrap.com^\n||cashprom.ru^\n||catwhatsup.org^\n||cbanners.virtuagirlhd.com^\n||cda.0-shiny.com^\n||cdcwn.xyz^\n||cdn-rtb.sape.ru^\n||cdn7.network^\n||cdn7.rocks^\n||cdn7.space^\n||cdsuper.ru^\n||cellbux.com^\n||cenovik.net^\n||cepereh.ru^\n||ceregete.com^\n||cettente.com^\n||cettenu.com^\n||cf-track.info^\n||chabg.top^\n||chbn.ru^\n||cheaptop.ru^\n||chevlaga.ru^\n||chickenkiller.com^\n||chistochisto.com^\n||cholaga.ru^\n||choose-a-lady.biz^\n||choozalady.pw^\n||chtoumenja.biz^\n||chursida.ru^\n||cimics.ru^\n||cityads.com^\n||cityads.ru^\n||cityads1.com^\n||cityteaser.ru^\n||ckqby.com^\n||clarm.ru^\n||clclcl.ru^\n||clearac.com^\n||cleen.ru^\n||click-da-click.com^\n||click.ad1.ru^\n||click2sell.eu^\n||click4girlz.pw^\n||clickandjoinyourgirl.com^\n||clickbux.ru^\n||clickcashmoney.com^\n||clickcoin.com^\n||clickhere.ru^\n||clickhereforfun.org^\n||clickov.com^\n||clickprofi.com^\n||clicks.moy.su^\n||clickscapture.com^\n||clicktizer.ru^\n||clicktrafic.ru\n||clickunder.net^\n||clickunderad.com^\n||clickvip.ru\n||cliennes.com^\n||clievise.com^\n||clme.biz^\n||cloudkillers.com/?\n||clvw.net^\n||cmix.org^\n||cmoneba.ru^\n||cms-skin.com^\n||cnyharo.ru^\n||code-click.info^\n||code.barrior.ru^\n||code.curs.net.ua^\n||code.xidx.org^\n||codeenter.ru^\n||coinsup.com^\n||collanetti.com^\n||comp-ex.ru^\n||computer-study.wq3.ru^\n||connection36.com^\n||contact-direct.ru^\n||contact-under.ru^\n||contactreserve.com^\n||contactsin.com^\n||contactsin.ru^\n||contema.ru^\n||content.printdirect.ru^\n||context.hotline.ua^\n||context.meta.ua^\n||contextbar.ru^\n||contextrtb.com^\n||conusmedia.com^\n||convergator.net^\n||coolsor.ru^\n||copylon.space^\n||corkery.biz^\n||corpore.ru^\n||counter.megaindex.ru^\n||cpaevent.ru^\n||cpam.pro^\n||cpateaser.ru^\n||cpatext.ru^\n||cpatrck.net^\n||cpazilla.ru^\n||cpl1.ru^\n||cpm.wargaming.net^\n||cpm.worldoftanks.com^\n||cpmsolution.ru\n||cppgf.com^\n||crackac.com^\n||crateac.com^\n||creara-media.com^\n||creara-media.ru^\n||creatives.livejasmin.com^\n||creofive.com^\n||creofun.com^\n||crpoy.com^\n||crutop.nu^\n||cshi.ru^\n||csht.ru^\n||ctr-media.info^\n||ctr-media.net^\n||ctyzd.com^\n||cufcw.com^\n||cunderdr.net^\n||curker.ru^\n||cxmolk.com^\n||cyberstyle.ru^\n||cyhtr.com^\n||d-agency.net^\n||d1jl096lp4cce0.cloudfront.net^\n||d1mib12jcgwmnv.cloudfront.net^\n||d3td6g0k30g56f.cloudfront.net^\n||daccrois.com^\n||dafferes.com^\n||dafficha.com^\n||danilidi.ru^\n||daopz.top^\n||dariku.ru^\n||dartimyl.com^\n||dasterx.ru^\n||data.neosmi.ru^\n||datamind.ru^\n||dataur.ru^\n||dating-cart.com^\n||dating-exchange.com^\n||davarello.com^\n||dddevki4u.com^\n||ddkdr.ru^\n||dedenye.ru^\n||deenomo.com^\n||defiques.com^\n||delightcash.com^\n||delo-teh.ru^\n||delta.mediafort.ru^\n||denezhnyie-rucheyki.ru^\n||deobp.com^\n||derploime.com^\n||deskmony.info^\n||dev4enki.com^\n||devahi.devki.xyz^\n||devaxi.org^\n||devkiforyou.org^\n||dewis.h1.ru^\n||df.mmo001.info^\n||dicwa.ru^\n||dietologexpert.com\n||digisets.com^\n||digital-forest.info^\n||dikm.ru^\n||dimprive.com^\n||dimpy.narod.ru^\n||dioperu.com^\n||dircash-promost.com\n||directadvert.net^\n||directnow.me^\n||directtogo.ru^\n||dirkino-traff.ru^\n||disklaimer.ru^\n||disredi.ru^\n||dizzyac.com^\n||dl.qcash.ws\n||dndd.ru^\n||doichering.ru^\n||dom2-xx.ru\n||domahainfo.domah.ru^\n||domahru.domah.ru^\n||domaingid.ru^\n||domentino.ru^\n||domertb.com^\n||domvkq.net^\n||doortrade.ru^\n||doskki.com^\n||dostavka.ru^\n||dosugcz.info^\n||dosugcz.net\n||dosugobzor.ru\n||dosugvip.ru^\n||download.mediaplay.ru^\n||dreamlog.ru^\n||dreampartners.ru^\n||dreimer.ru^\n||drivenetwork.ru^\n||drozhdeni.ru^\n||drtraff.ru^\n||drulelet.ru^\n||dskrt.net^\n||dsvload.dsvtracker.com^\n||dunta.ru^\n||dvanaro.ru^\n||dverser.ru^\n||dyndns-blog.com^\n||dyndns-free.com\n||dyndns-home.com^\n||dyndns.info^\n||dyzha.com^\n||e-partner.ru^\n||eaqci.com^\n||ebaytoday.ru/?\n||ebmzp.top^\n||ebuvpopku.ru^\n||ecgawdakfa.biz^\n||echo.12cpm.com^\n||echo.andumb.com^\n||echo.scund.com^\n||echo.teasernet.com/\n||econrus.ru^\n||ecsebo.ru^\n||edge-dl.andomedia.com^\n||edirect.efind.ru^\n||ee.tmska.com\n||effad.ru^\n||effectfree.net^\n||efmeni.ru^\n||efresa.ru^\n||eijwpxc.net^\n||ejevika.com^\n||ejmovec.ru^\n||ekod.info^\n||elephant.fotostrana.ru^\n||eliterudating.com\n||elle.alljournal.ru^\n||elnpe.com^\n||elparmo.ru^\n||elvidro.ru^\n||emu.ilovemp3.top^\n||enbadar.ru^\n||endata.cx^\n||enfreine.com^\n||engine.*.medialand.ru/\n||engine.adct.ru^\n||engine.turboroller.ru^\n||enot.k-yroky.ru^\n||entimee.com^\n||enzezin.ru^\n||epartner.ru^\n||eradek.ru^\n||ero-spinula.ru^\n||ero-tizzer.info\n||ero2you.com^\n||eropays2.com^\n||erotikdeal.com^\n||erotizzer.info\n||erotraf.com^\n||eroxdating.com\n||err.cloudbit.rocks^\n||errtmotmw.pw^\n||esc-team.com^\n||esliga.ru^\n||estiques.com^\n||et-code.ru\n||etarg.ru^\n||eurostyle-express.com/\n||evlega.ru^\n||evsembu.com^\n||exaccess.ru^\n||excalatom.com^\n||exchangenews.ru^\n||excluzive.net^\n||exo.kiev.ua^\n||expert.ruab.ru^\n||exta-z.ru^\n||extad.org^\n||externalmedia.ru^\n||extrabucks.ru\n||ezaktak.ru^\n||ezaste.ru^\n||ezotizer.ru^\n||f.battlespace.ru^\n||faceblum.ru^\n||fairfax.com.au^\n||fairlink.ru^\n||fairypays.com^\n||faktino.ru^\n||faktozhe.ru^\n||falcoware.com^\n||fanat.kz^\n||fas.catholicgreatestinterpret.xyz^\n||fast2load.ru^\n||fastnewsis.net^\n||fastsex.ru^\n||fasttrening.ru^\n||fbcjk.com^\n||femcasi.ru^\n||fextor.ru^\n||fialet.com^\n||file-online.ru^\n||filecontrol.ru^\n||filepost.ru^\n||filmplus.ru^\n||firelove.ru^\n||fitfas.ru^\n||flambo777.ru\n||flapoint.ru^\n||fleshlight-russia.com^\n||flipflapflo.\n||flipflapflo.biz\n||float-l.ru^\n||flygo.ru^\n||fmates.ru^\n||fmusive.ru^\n||fogtrack.net^\n||fooder.ru^\n||footbolka.ru^*partner=\n||forcetraf.com^\n||fortuka.com^\n||forum.dwnld.net.ua^\n||forum.showsteps.ru^\n||fotkidepo.ru^\n||fotkostrana.ru^\n||fotocash.ru^\n||fotos.ua^\n||fotostrna.ru^\n||fotostrtana.ru^\n||fotrento.com^\n||foundtr.com^\n||fpmef.com^\n||freddyman.com^\n||freeavalanche.ru^\n||freeexchange.ru^\n||freehd.com.ua^\n||fresh-video.com^\n||freshnews.su^\n||frfgn.xyz^\n||frrtrr.banggirls.ru^\n||fsdwd.xyz^\n||fstredirr.com^\n||fueox.us^\n||fullteaser.lsokwiub.ru^\n||funtest-ru.com^\n||further-enhanced.info^\n||fx-trend.com^\n||fxcast.com^\n||fxtrade.org.ru^\n||fzwoa.com^\n||g.doubleclick.net^\n||gadanieporuke.ru^\n||gagnifie.com^\n||gain-click.info^\n||gainclick.biz^\n||galaxymeet.ru^\n||galia.s-maligin.ru^\n||gamblingtraffic.ru^\n||game-tester.ru^\n||game.gwsk.ru^\n||game.vulcan-casino.com^\n||gamedl.ru^\n||gamepitstop.ru^*pid=\n||gameteaser.ru^\n||gamingpartners.org^\n||gateway-02.com^\n||gateway-03.com^\n||gatewey.net^\n||gawxf.com^\n||gbaseby.ru^\n||gbs.gamingmedia.ru\n||gdeslon.ru^\n||gdp.game-rust.ru^\n||geracbv.leohd59.ru^\n||get-ads.ru^\n||get-click.net^\n||get-click.ru^\n||get-hit.ru^\n||get.promofor.me^\n||getall.tv^\n||getb.7ya.ru^\n||getcd.ru^\n||getelem.ru^\n||getfl.net^\n||gethit.ru^\n||getraf.info^\n||gettopup.com^\n||getuplinks.ru^\n||getvisits.ru^\n||gfstrck.com^\n||ggffggffplus.com\n||ggsaffiliates.com^\n||ghxadv.com^\n||gigamega.ru^\n||ginads.com^\n||gingardo.com^\n||giosany.com^\n||giotyo.com^\n||girl-ceases.info^\n||girlstalks.ru^\n||gj7.ru^\n||gjslm.com^\n||glamurka.net^\n||glavrich.com^\n||globalstars.ru^\n||globalteaser.com^\n||globalteaser.ru\n||globaltizer.ru^\n||glojune.biz^\n||glordd.com^\n||glouposek.ru^\n||gnezdo.ru^\n||go.jetswap.com^\n||go.media-x.ru^\n||go.wsockd.com^\n||go2vulcan.com^\n||go7me.ru^\n||go7media.ru^\n||go8me.ru^\n||goallurl.ru^\n||gobf.ru^\n||gobzonet.ru^\n||gocdn.ru^\n||gogetlinks.net^\n||gogo4girlz.com^\n||gogousenet.com^\n||gold-wm.ru^\n||golden-fishka.com^\n||gomakemerich.com^\n||gomakemerich2.com^\n||gonews*.net^\n||good-traf.ru^\n||good-traff.ru^\n||goodadvert.\n||goodkind.ru^\n||goodlooknews.net^\n||goodsblock.dt00.net^\n||goodsbrowser.com^\n||google-analistyc.\n||googlsyndication.com^\n||googlyandex.ru^\n||goon.ru^\n||goossb.com^\n||gotarget.su^\n||goto.astdn.ru^\n||gotoredr.com^\n||gotostat.ru^\n||gotraff.ru^\n||gowoman.ru^\n||gqer.ru^\n||grainac.com^\n||grand-casino.com/?\n||grand-masters.ru^\n||gratifymecorrect.com^\n||grebibablo.com^\n||gredinatib.org^\n||greencofelive.ru^\n||greens.apishops.ru^\n||grey-eyes.info^\n||grouteg.com^\n||grovel.ru^\n||grt01.com^\n||grt02.org^\n||grteab.com^\n||gryzuche.ru^\n||gsmonitor.ru^\n||gueur.com^\n||guktuti.ru^\n||gvahh.top^\n||gvapp.ru^\n||hahemla.com^\n||hakiloporet.com^\n||haln.info^\n||hamphlete.com^\n||hannist.com^\n||hay-borsa.ru^\n||hd3pi8cv.com^\n||hdat.xyz^\n||hdtracker.ru^\n||hedvid.com^\n||hello.satclubbing.info\n||hellywood.ru^\n||helpls.ru^\n||hermes-studio.net^\n||heroeswm.ru^\n||hertxs.no-ip.org^\n||hewoman.info^\n||hgbn.space^\n||hhit.xyz^\n||hi.xn--d1accz.su^\n||hid24.com^\n||hiopdi.com^\n||hit-star.ru^\n||hitlist.ru^\n||hitter.ru^\n||hjiss.com^\n||hnixr.com^\n||hochu-deneg.ru^\n||holder.com.ua^\n||holm.ru^\n||holysts.com^\n||hommunit.com^\n||homrus.net^\n||hopedac.com^\n||host4media.com^\n||hotels24.ua^\n||hotinga.ru^\n||hotrusvids11.com^\n||hotsexmeet.ru^\n||hotvideoruszzz2011.com\n||hotworldnews.ru^\n||how2web.com^\n||hq-films.ws^\n||http.showsteps.ru^\n||hulkeileen556.uk.to^\n||hyndir.com^\n||hypersell.ru^\n||i-adv.biz^\n||i.j2j.ru^\n||i.mabila.ua/\n||iadv.biz^\n||iadvert.net^\n||iberacon.com^\n||ibizne.ru^\n||ice-media.ru^\n||iclckk.com^\n||icmil.ru^\n||icq-window.\n||icqadvert.com^\n||icqwindow.\n||ictowaz.ru\n||idpojar.ru^\n||idwrx.com^\n||ifnime.ru^\n||iframepay.com^\n||ifxprofits.com*aid=\n||ignorelist.com^\n||igrafum.ru^\n||igraplus.com^\n||igrayvmeste.ru^\n||igrun.com/?\n||ihqht.com^\n||ihsibe.ru^\n||iiutq.xyz^\n||ijdenha.ru^\n||ijrah.top^\n||ijtefix.ru^\n||ikiif.com^\n||ikritis.ru^\n||ili.pp.ua^\n||iluzur.com^\n||ilysa.ru^\n||im6-tub.com^\n||imagesatlantic.com^\n||imamby.ru^\n||imarker.ru^\n||img.fitbut.ru^\n||img1458.r.worldssl.net^\n||imgg.dt00.net\n||imperia-casino.com^\n||imrk.net^\n||imzhv.xyz^\n||indcoest.com^\n||indemandads.com^\n||inetlog.ru^\n||inextlink.com^\n||inf.wqa.ru^\n||inferalton.com^\n||info-dvd.ru^\n||info.tm/\n||informers.openmedia.com.ua^\n||informers.ukr.net^\n||infotor.me^\n||instantcash.ru^\n||intelligencehost.net^\n||intelliworker.kupivip.ru^\n||intency.com^\n||intencysrv.com^\n||intimcity.org^\n||intimmag.ru^\n||intimsexmagazin.ru^\n||intimshop.ru*&partner=\n||intimznaki.tomsk.ru^\n||invest-pool.ru^\n||invest-system.net^\n||ipgettraff.info^\n||ipgold.ru^\n||iphone-caviar.ru*entranceId\n||iphonetopsite.ru^\n||iptraff.com^\n||ipurl.ru^\n||iqok.ru^\n||iqsoh.ru^\n||iqtewa.ru^\n||irdanen.ru^\n||iredirr.com^\n||islamclick.ru^\n||iso100.ru^\n||isonlynews.net^\n||ispeakvideo.ru^\n||itageli.ru^\n||itcgin.net^\n||itrajy.ru^\n||ivanie.com^\n||ivetki.ru^\n||ivoxua.socratos.net^\n||ivsiveg.ru^\n||ixtyted.ru\n||iygke.com^\n||izdatra.ru^\n||izitizi.ru^\n||izvozic.ru^\n||jadedi.com^\n||jaymedianetwork.com^\n||jazg97clb.ru^\n||jbugk.com^\n||jcnqc.us^\n||jcomusic.com^\n||je.revolvermaps.com/\n||jekson44.ru^\n||jerkngo.com^\n||jhrwekjrowegm.com^\n||jiayuofficial.com\n||job-info2015.ru^\n||jofbu.com^\n||joycasino.com^\n||js.einfo.by/?\n||js.gdsln.ru^\n||js.goods.redtram.com\n||js.novatizer.com\n||js.textshift.net^\n||jsc.sexy-torrents.com^\n||jsn.24smi.org/\n||juppser.ru^\n||justclicks.biz^\n||justmi4.co.in^\n||jutulep.com^\n||jvs.price.ru^\n||jvvqm.us^\n||kadam.net^\n||kadavara.com^\n||kagortus.ru^\n||kaktakkk.ru^\n||kaliba.alivesex.ru^\n||kalooga.com^\n||kan3.info^\n||karaokepesni.ru^\n||kartinka.com.ua/\n||kartinuly.ru^\n||katzewazup.org^\n||kazmedia.su^\n||kbllskkwvp.pw^\n||keqrd.top^\n||key4test.in^\n||khermesi.ru^\n||khesino.ru^\n||khoteris.ru^\n||khotokra.ru^\n||kilkiva.ru^\n||kilonefast.net^\n||kilopog.com^\n||kimus.ru^\n||kinderfinder.ru^\n||kingtraff.ru^\n||kino.kox.su^\n||kino.planetkino.ru^\n||kinoclub.org^\n||kinopromobase.ru^\n||kinorama.tv/\n||kinotraff.ru^\n||kinott.com^\n||kinott.ru^\n||kisakuku.org^\n||kissmyads.biz^\n||kistured.com^\n||kit5hgver.com^\n||kitchenfilm.ru^\n||kitopr.com^\n||kkkiski4u.com^\n||kkpur.xyz^\n||kladoffka.dyndns.biz^\n||klikaem.ru/\n||kliklink.ru\n||klikmoney.net\n||klyuchev.in.ua^\n||klyunker.ru^\n||kmndj.top^\n||knewy.com^\n||koelpin.biz^\n||koiper.com^\n||kolendrin.ru^\n||kolezeynews.ru^\n||komok.com^\n||kontenka.ru^\n||kontera.com^\n||konverta.ru^\n||korenizsemi.net^\n||koreniztreh.net^\n||korenizvosmi.net^\n||koromi.ru^\n||korovkasms.ru^\n||kosibablo.ucoz.ua^\n||kotengens.net^\n||koviovius.com^\n||kpdn.ru/\n||kpup.floomba.ru\n||kqtyz.com^\n||krolikplatit.ru^\n||krutilka.net^\n||krytilka.ru^\n||ksdnf.com^\n||kudrafa.ru^\n||kuhni.kuhnibrother.ru\n||kuhni.kuhnisiblings.ru^\n||kuhnibrother.ru^\n||kuhnisister.ru^\n||kurilo.pro^\n||kuveres.com^\n||l2got.ru^\n||labadon.com^\n||lacemme.com^\n||ladiesnadosuge.com^\n||ladyads.ru^\n||ladycash.ru^\n||ladyclicks.ru/\n||ladypay.ru^\n||ladyshopping.ru^\n||ladytizer.org^\n||ladyzxxyz.com^\n||lalabla.biz^\n||lamma.24ex.net^\n||langosh.biz^\n||lantocha.ru^\n||lapeduzis.org^\n||lapumo.com^\n||lareson.com^\n||larotret.ru^\n||lbbanners.com^\n||lcads.ru^\n||ldnxy.xyz^\n||leadgid.go2cloud.org^\n||leads.su^\n||leadsleader.ru^\n||leadtopays.com^\n||leadzu.com^\n||leafleech.com^\n||league-of-legends.ru^\n||leashac.com^\n||ledinika.ru^\n||ledonov.uz.ua^\n||leechac.com^\n||leforma.com^\n||legandruk.com^\n||leleorix.ru^\n||lemfama.ru^\n||lenta-novostei.com^\n||lenty.ru^\n||leplena.ru^\n||lerova.ru^\n||lerrex.ru^\n||letitsoft.com^\n||letmelook.net^\n||letorrent.org^\n||letsseks.\n||letssex.\n||levelpay.ru^\n||levitatsia.ru^\n||li2p.com^\n||lichyela.ru^\n||lidicando.com^\n||liex.ru/\n||lifemeet.biz^\n||likemagazine.ru^\n||likondok.com^\n||lim50.ru^\n||limo20.ru^\n||limo21.ru^\n||limonads.net^\n||limonadsources.ru^\n||limoncash.com^\n||lineoflife.ru^\n||lingospot.com^\n||link.ru/\n||linkbuy.biz^\n||linkexchange.\n||linkfeed.ru^\n||linkmyc.com^\n||linkpeoples.com^\n||links-wm.ru^\n||linkslot.ru^\n||linktraff.ru^\n||linkunder.ru^\n||linkwall.ru^\n||linkwithin.com^\n||linkwmr.ru^\n||linkword.ru^\n||linodo.ru^\n||linono.ru^\n||litenicholraamos.uk.to^\n||litiumo.com^\n||littel.net^\n||litthegre.com^\n||liveclix.net^\n||liventernet.ml^\n||livesmi.com^\n||lizhodo.ru^\n||ljteas.com^\n||llladiez.click^\n||loader.adrelayer.com^\n||loadmoney.ru^\n||login.tracking101.com^\n||loginbox.ru^\n||logsoc.ru^\n||lokomusic.com^\n||lol.bash.org.ru^\n||lolper.ru^\n||loltrk.com^\n||lolxm.xyz^\n||loneday.com\n||lookszone.ru^\n||lopiner.ru^\n||lopireto.com^\n||lopitus.com^\n||loponop.com^\n||lopuer.biz^\n||lopuut.ru^\n||lopytol.com^\n||lotkano.ru^\n||love-our.ru^\n||loveadvert.ru^\n||loveraz.com^\n||lovn.ru/\n||lpmde.xyz^\n||lqela.ru^\n||lrcs.info^\n||ltcraft.ru^\n||luck4.me^\n||luckiestclick.com^\n||luisardo.com^\n||luke-forelegs0.asia^\n||lux-bn.com.ua^\n||luxcash.ru^\n||luxurycash.net^\n||lvkwz.com^\n||lvuic.com^\n||lycodz.com^\n||lycosu.com^\n||lycosy.com^\n||lydiz.com^\n||m-youtube.ru\n||m3gadeth.ru^\n||ma-static.ru^\n||mabila.ua^\n||macamba.ru^\n||madcash.biz^\n||madnet.ru^\n||magicanfy.com^\n||magicintim.ru^\n||magna.ru^\n||mail.banklife.ru/\n||makesomemoney.ru\n||mambo.kiev.ua^\n||mamypos.com^\n||manfys.com^\n||manuelu.com^\n||marketgid.info^\n||marketingsolutions.yahoo.com^\n||marketnews.pw^\n||mastertarget.ru^\n||masudel.com^\n||mateast.com^\n||maus77.ru^\n||maxato.com^\n||maxforta.com^\n||maxpark.com^\n||maxplan.ru^\n||maxtrust.ru^\n||maxwino.ru^\n||mayvbm.com^\n||mazetin.ru^\n||mazuma.ru^\n||mcljm.top^\n||md5s.ru^\n||mdeih.com^\n||mdesign.planet.ee^\n||mecash.ru^\n||medads.ru^\n||media-active.ru^\n||media-vip.com^\n||media.fairlink.ru\n||media.goldline.pro^\n||mediabaf.ru^\n||mediabanner.net^\n||mediacartel.ru^\n||mediacontext.\n||mediacot.com^\n||mediadar.ru^\n||medialand.\n||medianaft.com^\n||medianaft.ru^\n||medianetworks.ru^\n||medianovosti.ru^\n||mediaplex.com^\n||mediaportal.ru^\n||mediarich.cc\n||mediarich.us^\n||mediarotate.com^\n||mediatoday.ru^\n||mediatraffic.com.ua^\n||mediaunder.info^\n||mediaunder.org^\n||mediaunder.us^\n||mediayoutube.com^\n||medicinetizer.ru^\n||medigaly.com^\n||medrol.ru^\n||meelba.com^\n||meendo.ru^\n||megaban.com.ua^\n||megabestnews.net^\n||megafingroup.com^\n||meganewss.ru^\n||megartb.com^\n||megastock.ru^\n||megatizer.com^\n||megatizer.ru^\n||megogo.1ru.tv^\n||mekadr.com^\n||menflirt.ru/?\n||menu.narodbit.net^\n||meofur.ru^\n||mercatos.ru^\n||mestkom.ru^\n||metaliners.ru^\n||metaprofit.net^\n||metroclick.ru^\n||mezima.com^\n||mfatallp.com^\n||mg.yadro.ru^\n||mglsk.com^\n||micro-win.com^\n||migratecookiesacrossdomains.asp\n||mimizet.ru^\n||miokoo.com^\n||mipagerank.com^\n||mirilow-tds.ru^\n||mirvoinov.biz^\n||misqb.xyz^\n||missrich.net^\n||mix1traff.ru^\n||mixadvert.com^\n||mixclick.ru^\n||mixmarket.biz/\n||mixtraff.com^\n||mjavagames.ru\n||mmassa.com^\n||mmcispartners.com^\n||mmm--2012.com\n||mmm-investor.ru^\n||mnetads.net^\n||mobalert.net^\n||mobatori.com\n||mobi-mobi.info^\n||mobiads.ru^\n||mobidump.com^\n||mobile-click.biz^\n||mobioffers.ru^\n||mobitema.ru\n||mobn.net^\n||modastro.ee^\n||modelatos.com^\n||moe.video^\n||moijs.com^\n||money-domen.com^\n||moneyhere.ru^\n||moneymaiker.ru\n||moneysyst.biz\n||moneyteacher.ru^\n||moneytrap.ru^\n||monstra.smallmovies.ru\n||moogle.ru^\n||morar.biz^\n||morenews1.net^\n||morenews2.net^\n||morenews3.net^\n||morenews4.net^\n||morkovo4ki.org^\n||mostopana.ru^\n||motoadvert.ru^\n||moviecash.ru^\n||mpay69.biz^\n||mpay69.pw^\n||mpays.pw^\n||mptri.net\n||mrbasic.com^\n||msrv.su^\n||mstds1.ru^\n||mtbox.ru^\n||muhtoni.ru^\n||mukipol.com^\n||multimedia-boom.com^\n||multirek.ru^\n||multonly.ru^\n||muneni.ru^\n||muposa.com^\n||mutrik.com^\n||muttr.ru^\n||muzokit.ru^\n||mvuhy.space^\n||mxttrf.com^\n||my-adv.ru^\n||my.golosplus.ru^\n||myads.ru^\n||mycpm.ru^\n||myfishsoup.com^\n||myiphone.be^\n||mykaren.ru^\n||mynagor.com^\n||mynewdomen.ru^\n||myonionsoup.com^\n||myredirecter.info\n||mytizer.com^\n||mytizer.ru^\n||mytomatosoup.com^\n||mytraf.info^\n||mytraf.ru^\n||n.adonweb.ru/\n||n.vestey.ru^\n||n.vn-chk777.com^\n||nachat-s-nula.net^\n||nadavi.net^\n||nali4ka.info^\n||namezones.ru^\n||nanofantiki.edigest.ru^\n||naplela.ru^\n||narod-vrach.ru^\n||nasimke.ru^\n||naxerposlan.com^\n||nbolame.ru^\n||ndkes.com^\n||nebanmenia.kinozov.ru^\n||neme.electric-pdatu.ru\n||nenrk.us^\n||neosap.ru^\n||nepalon.com^\n||neqky.com^\n||neroom.ru^\n||network-t.net^\n||nevidanno.ru^\n||new-lead.ru^\n||new.traffic.ru^\n||new.zalizo.pp.ua^\n||newhdfilms.ru^\n||newisnews.info^\n||newlostrek.ru^\n||news.1ru.tv^\n||newsadvert.net^\n||newsblocks.ru^\n||newsfeed.net.ua^\n||newsmarket.pixarea.ru^\n||newsonlynews.com^\n||newsteaser.ru^\n||newstizer.ru^\n||newsvidnews.info^\n||newtizz.net^\n||next-few.info^\n||nextclick.com.ru^\n||nextclick.com.ua^\n||nextgame.ru^\n||ngads.com^\n||nhqqv.space^\n||nickhel.com^\n||nightdate.ru\n||nigvbyd.net^\n||nihewfi.net^\n||ninavyg.ru^\n||ningme.ru^\n||nlserver.xyz^\n||nnn.ru^\n||nnu.re^\n||noion.ru^\n||nomer1.ru.ru^\n||nostalgia.onego.ru^\n||nostushi.ru^\n||not-only.info^\n||noteframe4o.tk^\n||nova-click.ru^\n||novem.pl^\n||novosti.dn.ua/\n||novosti1.com^\n||novoteka.ru\n||nrzcj.top^\n||nsoft-s.com^\n||num-link.ru^\n||nutkaekwcm.ru^\n||nvjqm.com^\n||nwmum.com^\n||nya.tessko.ru^\n||nyyed.com^\n||obhodsb.com^\n||obi.load-games.com^\n||obigre.ru^\n||obislame.ru\n||obkatra.ru^\n||oblako42.com^\n||oblivochki.biz^\n||obman-casino.com\n||obmen.starstudio.org.ua^\n||obmnt.com^\n||obsudam.ru^\n||obuvnogo.net^\n||ochze.com^\n||oconner.biz^\n||odinkod.ru^\n||ofapes.com^\n||ofenop.ru^\n||office.ad1.ru^\n||office.profi66.ru^\n||ohnooo.ru^\n||oilefy.com^\n||oiokewfkjnvq.com^\n||oirplane.com^\n||oiya.ru^\n||ojooo.com^\n||okoshechka.net^\n||okp.paypic.ru^\n||olivka.biz^\n||olizyr.com^\n||oltonve.ru^\n||olymptrade.com^\n||omotorax.ru^\n||omynews.net^\n||onadvert.ru^\n||one.lg.ua^\n||onelead.ru^\n||onenima.com^\n||oneund.com^\n||oneund.ru^\n||online-path.com^\n||online.koko-ko.com^\n||online.mik123.com^\n||onlineacutions.com^\n||onlinim.ru^\n||only4men.ru^\n||onlysexygirls.com^\n||onoff.in^\n||ooredrr.com^\n||opekom.ru^\n||openad.ru^\n||openads.ab-daily.by\n||openlinks.ru^\n||openunder.net^\n||openurls.co.cc^\n||openx.com^\n||openx.org^\n||opinionbar.com^\n||opresat.ru^\n||opyavar.ru^\n||orbit.lun.ua^\n||order-seo.net^\n||oreshki-news.net^\n||ortetse.ru^\n||osqbk.com^\n||otclick-adv.ru^\n||otmolod.ru^\n||otomsu.ru^\n||oupox.club^\n||outwitch.com^\n||ovap.in^\n||ovintic.ru^\n||owebmoney.ru^\n||ownadne.ru^\n||oxn.gerkon.eu^\n||oxredex.ru^\n||oztumte.ru^\n||p2p.bz^\n||pabhaco.ru^\n||pages.tn24.ru\n||palandan.com^\n||pammru.net^\n||pandre10.ru^\n||panoll.com^\n||parabolla.net^\n||parafiliya.ru^\n||paramedjo.com^\n||partner.24smile.org^\n||partner.eviton.ru^\n||partner.gde.ru^\n||partner.join.com.ua^\n||partner.mediametrics.ru^\n||partnerearn.net^\n||partnergateway.liga-stavok.com^\n||partners.agoda.com^\n||partners.vsemayki.ru^\n||partniorka.com^\n||pasvvord-icq.ru^\n||pay-hit.com^\n||paybrides.org^\n||payfery.ru^\n||paymonster.org^\n||paymonsters.biz^\n||paywoman.ru^\n||pbid.iforex.com^\n||pbnet.ru^\n||pc-software.ru/\n||pcads.ru^\n||pelicanprogram.com^\n||people-group.su^\n||peoplemobile.ru^\n||perezzz.ru^\n||perfecttds.net^\n||perinstallcash.com^\n||personalki.net^\n||pestoro.ru^\n||peyzamo.ru^\n||pfrve.xyz^\n||phinker.ru^\n||phoenix-widget.com^\n||phpteaser.ru^\n||piar-m.ru^\n||pic*.teasernet.com/\n||pic.picxxx.ru^\n||picbay.ru^\n||pichyefu.ru^\n||pindotrafforg.54.com1.ru^\n||pinkintim.com^\n||pir.zspb.ru^\n||piroji.com^\n||pixxxocl.ml^\n||placeoff.ru^\n||pladform.ru^\n||planetapozitiva.ru^\n||platinumcode.net\n||platnic.ru^\n||play2.biz^\n||pleasedirect.me^\n||pleasemeright.com^\n||plomihy.com^\n||plsdrct1.me^\n||pluginsjquery.com^\n||pluralismus.ru^\n||plus10sm.ru^\n||pmicama.ru^\n||po-cloud.net^\n||pofqm.xyz^\n||pointclc.com^\n||poiskovik.tv^\n||poiskraboty2011.com\n||pokajem.tele-dom2.com/\n||pokazuha.pp.ua^\n||pokerstrategy.com/#\n||pokitom.com^\n||pokrutim.ru^\n||pokupaem.info^\n||polipol.pw^\n||pomolation.ru^\n||ponyvod.ru^\n||pop-parad.\n||pop-under.\n||pop.mrstiff.com^\n||popander.com/\n||popander.mobi^\n||popinads.com^\n||popsoft.us\n||poptraf.\n||popunder.\n||popupclick.\n||popups.ru^\n||popuptraf.\n||pornorunet.ru^\n||pornoscanner.com^\n||portak.net^\n||portech315.net^\n||poshalim.\n||post.rmbn.net^\n||post.rmbn.ru^\n||powertraf.com^\n||poyva.com^\n||poza-69.ru^\n||pozzitiv.ru^\n||pp.pornoveka.ru\n||ppvlj.com^\n||pr28.com^\n||prbn.ru/\n||privatteaser.ru^\n||privetadb4.ru^\n||privetadblock.ru^\n||produman.ru^\n||profi-link.org^\n||profit-casino.com^\n||profit-partner.ru^\n||profitraf.ru^\n||profitwizard.net^\n||project-apartment.ru^\n||proligtb.com^\n||prolinker.ru^\n||promo-banner.ru^\n||promo.karosgame.ru^\n||promo.md^\n||promo.partner.alawar.ru^\n||promo.rzonline.ru/\n||promo.va-bank.com\n||promobit.pw^\n||promoblock.\n||promoblocks.ru\n||promoboom.org^\n||promobuster.net\n||promobuster.org^\n||promocenter.biz^\n||promocns.com^\n||promofox.org^\n||promoheads.com^\n||promoloader.com^\n||promorise.org^\n||promorocket.org^\n||promoskiki.ru^\n||promovips.ru^\n||promoworld.pw^\n||prospero.ru^\n||prostolos.ru^\n||prostopartnerka.ru^\n||protectorfirewall.com/?\n||protizer.net\n||protrafv2.com^\n||proxy.insorg.org^\n||ps.infiplay.ru^\n||psma01.com\n||psma02.com\n||psma03.com\n||psmardr.com^\n||psnets.com^\n||ptrck.ru^\n||pu.zipsites.ru^\n||publicitysmart.com^\n||puls.lv/\n||purecash.ru^\n||pusk.ua^\n||putana-cz.org^\n||putana.cz^\n||putanki.org^\n||puttyac.com^\n||pv.bookarchive.ru^\n||pwnz.org^\n||pyksf.com^\n||qanmw.space^\n||qhiip.com^\n||qload.ru^\n||qrstes.com^\n||qtymi.com^\n||quber.ru^\n||quisma.com^\n||qvi0.ru/\n||qwe.qrrgv.space^\n||qwertypay.com^\n||qwex.ru/\n||qxplus.ru^\n||r.ad1.ru/\n||r.cpa6.ru^\n||r.partner.badoo.ru^\n||r.qip.ru^\n||r.toplaygame.ru^\n||rajniko.ru^\n||ramosetlex.ru^\n||rap4me.com^\n||rarenok.biz^\n||rasskaju.ru^\n||ratioboom.ru^\n||ratke.biz^\n||ratyakhu.ru^\n||rb*.design.ru^\n||rb.ondu.ru^\n||rb.rfn.ru^\n||rb.sport-express.ru^\n||rb.tv-best.net^\n||rbc.magna.ru^\n||rbcdn.com^\n||rdiul.com^\n||readme.ru^\n||readnewstoday.ru^\n||ready4win.com^\n||real*traf.ru^\n||realer.info^\n||realtraf.net^\n||reasulty.com^\n||rebevengwas.com^\n||rebill.me\n||reborko.com^\n||rec2000.at.ua^\n||rec3re23.com^\n||recreativ.com.ua^\n||recreativ.com^\n||recreativ.ru^\n||redclick.ru^\n||redexchange.net^\n||redflu.ru^\n||redsurf.ru^\n||redtram.com^\n||redvase.bravenet.com^\n||refer.ru^\n||referers.net^\n||referral.game-insight.com^\n||regpole.com^\n||rekhatov.ru^\n||rekl.sexicelebs.net^\n||reklama-top.ru^\n||reklama8.ru^\n||reklamaizer.ru^\n||reklamaster.com^\n||reklammingggg.ru^\n||reklamoman.ru^\n||reklamper.com^\n||release-me.ru^\n||renewnewss.net^\n||republer.com^\n||retargetads.ru^\n||reterafu.torplanet.ru^\n||rettik.ru^\n||returso.com^\n||rewdinghes.com^\n||rg-be.ru^\n||rgitc.xyz^\n||richbanner.ru^\n||richthof.com^\n||riibl.com^\n||ripemember.com^\n||risale.ru^\n||risle.ru^\n||riverlead.ru^\n||rmbn.ru/\n||rnd.yxo.ru^\n||roadshortway.ru^\n||roagz.us^\n||robotext.info^\n||rocli.ru^\n||rofhathe.com^\n||roklerok.com^\n||rokno.com^\n||rollad.ru^\n||romilit.com^\n||roninex.ru^\n||rontar.com^\n||rooton.in^\n||ropedri.com^\n||ropsinde.com^\n||rorer.ru^\n||rot.rusoul.ru^\n||rot.spotsniper.ru^\n||rotaban.\n||rotabanner.\n||rotation-context.ru^\n||rotation-media.net^\n||rotator.\n||roukel.com^\n||rs-context.ru^\n||rss20.ru^\n||rsuuc.com^\n||rt-image.ru^\n||rt-ns.ru^\n||rtb-media.ru^\n||rtbads.info^\n||rtbclick.net^\n||rtbget.com^\n||rtbinternet.com^\n||rtbstream.com^\n||rtbtraf.com^\n||rtbweb.com^\n||rteneme.ru^\n||rtl1.biz^\n||rtl1.net^\n||ru4.com/\n||ruad.net^\n||rublevodom.ru^\n||ruigra.com^\n||ruklik.com/\n||runetki.tv^\n||rus-ero.net^\n||rus-novocti.com^\n||ruscams.com\n||ruschopi.ru^\n||ruscontext.com^\n||rusdating.org^\n||rusmedserv.com^\n||rusrestbest.\n||russervisbest.ru^\n||russian-cuties.info^\n||rustiz.ru^\n||rustizer.com^\n||rustrackers.ru^\n||rutarget.ru^\n||rutizer.org^\n||rutorads.com^\n||rutraff.cc^\n||ruttwind.com^\n||rutube.com^\n||rutubexx.com\n||rutvind.com^\n||rwhxz.space^\n||rwqckakqfq.ru^\n||rxtbb.top^\n||rybkono.ru^\n||rybnyati.ru^\n||ryuosy.com^\n||ryushare.com^\n||rzhujam.com^\n||s-manager.com^\n||s.ex.region70.ru^\n||s.holder.com.ua^\n||s.kma1.biz^\n||s.sdx.ru^\n||s1.intimshop.ru^\n||s4block.com^\n||sadreno.com^\n||saferedd.com^\n||saferedirrect.com^\n||salesdoubler.com.ua^\n||samiana.com^\n||sapmedia.ru^\n||sasisa.ru^\n||savclick.ru^\n||sawayn.link^\n||sb-money.ru^\n||scafer.ru^\n||scarpbooking.ru^\n||scr-tz.com^\n||scrool.ru^\n||sdziy.us^\n||seafox26.com^\n||searchlinker.ru^\n||searchmeta.webhost.ru^\n||searchtds.ru^\n||seclick.ru^\n||secur.ws^\n||securewebboard.com^\n||seedr.com^\n||seedr.ru^\n||seedrug.ru^\n||seedthree.ru^\n||seels.ru^\n||seevenbilcoo.uk.to^\n||seexycams*.da.ru^\n||sektorial.com^\n||selectornews.com^\n||selhost.ru^\n||selosin.com^\n||senkevich-sl.net^\n||senkevich-vk.net^\n||seo-sport.ru^\n||seo.arxua.com^\n||seopult.ru^\n||seorate.ru^\n||seosape.com^\n||seosprint.net^\n||serenky.ru^\n||sergey-mavrodi-mmm.net^\n||serial-smotret-online.ru^\n||serialinfo.ru^\n||serialo.net^\n||seriouspartner.biz^\n||seriouspartner.ru^\n||serl.mooo.com^\n||serolan.com^\n||serve.5visions.com^\n||serve.adhance.com^\n||servebbs.net^\n||servemoney.ru^\n||server.adeasy.ru^\n||server.roolim.ru^\n||server2034.ru^\n||serverbest.xyz^\n||service.rorer.ru^\n||sex-finger.com^\n||sex-mamba.biz\n||sex-shkola.ru^\n||sex4u.lg.ua^\n||sexcashv2.com^\n||sexfilms.ru^\n||sexnarod.tv^\n||sexnimfa.ru^\n||sexpalace.gs^\n||sexprice.ru^\n||sextizer.net^\n||sexvrusko.org^\n||sexyfriend.ru^\n||sfac.ctaxc.ru^\n||sg.freeimg.ru^\n||sgood.ru^\n||shakeson.ru^\n||sherlockseries.ru^\n||shigopo.ru^\n||shinkado.ru^\n||shinobi.jp^\n||shotyfy.com^\n||shoveac.com^\n||show.everytell.com^\n||showad.ru^\n||shuffele.com^\n||site-rank.com^\n||skimresources.com^\n||skyad5.ru^\n||skyadvert.su^\n||skyadvideo.ru^\n||skycdnhost.com^\n||slava.soloway.su^\n||slejv.space^\n||slickjump.net^\n||slimgipnoz.ru^\n||slivz.com^\n||slopeac.com^\n||slot-888.ru^\n||slowpoker.ru^\n||smart.allocine.fr\n||smartinfomarketing.ru^\n||smeshen.ru^\n||smigid.ru^\n||smilered.com^\n||smimarket.com^\n||smonstr.ru^\n||sn00.net\n||socadvnet.com^\n||sochetat.net^\n||softbn.ru^\n||soknm.com^\n||solanog.com^\n||solipen.pw^\n||solopon.com^\n||sontere.com/\n||sopital.com^\n||sovietit.com^\n||spacewalk.info^\n||spalitemu.ru^\n||sparelli.com^\n||sparical.com^\n||spechee.com^\n||speedmen.ru^\n||spinyla.ru^\n||spitter.pauk.ru^\n||spoki-noki.net^\n||sponsorads.de^\n||sport.metabar.ru^\n||sputnik1.ru^\n||spytrack.tic.ru^\n||st.cv46.ru^\n||st.pay-click.ru/\n||st02.net^\n||stableprofit.ru^\n||standadv.com^\n||start.fotostrana.ru/\n||startscript.ru^\n||static.addflow.ru/\n||static.adshow^\n||static.prototypes.ru^\n||static.terrhq.ru^\n||statistik.duplanet.tk^\n||statscreen.info^\n||stb.msn.com/i\n||steamac.com^\n||step1landing_rus.html\n||steptocash.ru^\n||stf779.ru^\n||still-innocent.info^\n||stimulmodules.com^\n||stisl.vse-rukami.ru\n||stopto.da.ru^\n||storm01.ru^\n||stream-home.ru^\n||street-on-which.info^\n||stripvidz.com^\n||studsss.da.ru^\n||stylesheet-js.ru^\n||sub.intoback.tk^\n||subnewss*.net^\n||summerlove4u.org^\n||sunior.loftlm.ru^\n||super-sxema.ru^\n||superfastcomputer.ru^\n||superlady.org^\n||superpcexpert.ru\n||superseksi.pw^\n||supersovet.info^\n||superstyle.ru^\n||supertop.ru^\n||supertura.com^\n||superzarabotok.info^\n||surbis.ru^\n||surfearner.com^\n||sutgof.ru^\n||svc.kartinka.com.ua/\n||svk100hp.ru^\n||svotu.top^\n||sweetest.ru^\n||swxhp.pw^\n||sxtut.org^\n||sypleni.ru^\n||sysdiag.ru^\n||syznate.ru^\n||t.4623.ru^\n||t.proext.com^\n||t.pusk.ru^\n||talkfusion.com\n||tankionline.com#friend=\n||targetan.com^\n||targetix.net^\n||tastishi.ru^\n||tbe.tom.ru/?\n||tch10.com^\n||tch30.com^\n||td-everest.biz^\n||tds.astdn.ru^\n||tds.org.ua/\n||tdssss.com^\n||teachac.com^\n||teamearn.ru^\n||teamrtb.net^\n||teaser-goods.ru^\n||teaser-mobile.com^\n||teaser.cc^\n||teaser.meta.ua^\n||teaser.pupers.net^\n||teaser.strocher.ru^\n||teaser1m.com^\n||teasera.ru^\n||teasercentr.ru^\n||teasereach.com^\n||teasergate.com^\n||teasergold.ru^\n||teasergood.net^\n||teasergroup.ru^\n||teaserka.ru^\n||teaserleader.ru^\n||teaserleads.com^\n||teasermall.com^\n||teasermedia.net^\n||teasernet.\n||teaserplay.ru^\n||teaserrotator.com^\n||teasers.pro^\n||teasers.ru^\n||teasers.ucoz.ru^\n||teasersystem.com^\n||teasertop.net^\n||teasertraf.net^\n||teazeramo.net^\n||teazzer.ru^\n||tech1515983.ru^\n||tech2648159.ru^\n||tech4215978.ru^\n||tech517283.ru^\n||tech547789.ru^\n||tech5877413.ru^\n||tech9638514.ru^\n||techbeat.com^\n||technical-rtl.ru^\n||tecontx.com^\n||tedobe.com^\n||teenseks.da.ru^\n||tehnoklad.ru^\n||telderi.ru^\n||terapou.com^\n||teromil.com^\n||test-studio.ru^\n||test.p.12cpm.com^\n||testi-bonus.com^\n||testiada.ru^\n||testsbox.ru^\n||text-ali.ru^\n||tg.mybb.ru^\n||tg2016.net^\n||the-people-group.com^\n||thor-media.ru^\n||thrilling.ru^\n||tibrashadow.ru^\n||tibu.ru/p\n||timedirect.ru\n||tisagama.com^\n||tisakama.com^\n||tisalama.com^\n||tisapama.com^\n||tisarama.com^\n||tisref.com^\n||titanpoker.com^\n||tiutietur.com^\n||tivisla.ru^\n||tiz-et.ru^\n||tiz.neosmi.ru\n||tiz.ukrr.ru^\n||tizer-bazar.com^\n||tizer-click.biz^\n||tizer-net.ru^\n||tizer.in^\n||tizer.passion.ru^\n||tizer.ukraine-ru.net^\n||tizer.ws^\n||tizer6.net^\n||tizer7.net^\n||tizer7.ru^\n||tizer8.net^\n||tizerads.ru^\n||tizerbank.com^\n||tizerbox.ru^\n||tizerclik.com^\n||tizerda.net^\n||tizerelite.net^\n||tizerfly.net^\n||tizerget.net^\n||tizergid.com^\n||tizergun.net^\n||tizerka.info^\n||tizerka.net^\n||tizerlink.com^\n||tizerman.info^\n||tizermine.net^\n||tizermy.net^\n||tizernaya-reklama.ru^\n||tizero.ru\n||tizeroff.ru^\n||tizerpro.ru^\n||tizers.net^\n||tizerset.net^\n||tizershop.ru\n||tizersmaster.ru^\n||tizerstock.com^\n||tizertraf.com^\n||tizerweb.ru^\n||tizoclick.ru^\n||tizru.com^\n||tizru.ru^\n||tizsistems.ru^\n||tizy.ru/\n||tizzer.ru^\n||tlafu.space^\n||tldredenter.com^\n||tm-core.net^\n||tmserver-2.net\n||tmstrack.com^\n||tnx.net/\n||to2012ok.ru^\n||todaymix.ru^\n||toftforcal.com^\n||toftori.ru^\n||tojinr.com^\n||tolicando.com^\n||tomingo.biz^\n||tomiti.ru^\n||tonopole.com^\n||tonsunjo.com^\n||top-shop.ru^\n||top100.ezar.ru^\n||top100ya.org\n||top4.mail.ru\n||topadvert.ru^\n||topcpa.ru^\n||topcto.ru^\n||tophot-news.com^\n||topkino.tv^\n||topoffer.biz^\n||toptizer.ru/\n||toptrackers.ru^\n||toraccept.ru^\n||torgdom.biz^\n||torgnn.ru^\n||torrent-trackers.ru^\n||torrent.torrbit.ru\n||tostega.ru^\n||totrena.ru^\n||toutiles.com^\n||tptrk.ru^\n||tpxur.top^\n||tr.samoresim.ru^\n||trade7.ru^\n||tradedoubler.com^\n||tradeleads.su^\n||tradeone.com.ua^\n||tradepub.com^\n||traefllab.ru^\n||traf-3rs.com^\n||traf-zona.ru^\n||traf.spb.ru^\n||trafex.net^\n||traff.ru^\n||traffic-delivery.com^\n||traffic-media.co^\n||trafficcost.ru^\n||traffim.com^\n||traffmaster.ru^\n||traffpay.ru^\n||traffshop.com^\n||traffstock.ru^\n||traffworld.ru^\n||trafjiz.com^\n||trafka.ru^\n||trafmake.ru^\n||trafmaster.com^\n||traforet.biz^\n||traforet.ru^\n||trafovod.com^\n||trafovod.ru^\n||trafpyat.ru^\n||trafsiz.com^\n||trafstore.com^\n||trafvip.com\n||trafvip.in\n||transfto.com^\n||transiz.ru^\n||translate.pronpic.org^\n||traru.vsezaibis.ru^\n||tratouler.com^\n||traxload.ru\n||tredman.com^\n||trekluck.ru^\n||tres8.info^\n||trigub.ru^\n||tromen.ru^\n||tropicalos.com^\n||trrraflab.ru^\n||trunderka.ru^\n||trunex.info^\n||truthinside.ru^\n||trxxh.com^\n||tsinadol.ru^\n||tsiso.ru^\n||tsitelur.ru^\n||tsr.zlatoff.ru^\n||ttarget.ru^\n||tteasr.com^\n||tubealliance.com^\n||tubecontext.com^\n||tubedot.ru^\n||tulula.biz^\n||tunituttybanner.com^\n||turgolde.ru^\n||turnefo.ru^\n||tutula.biz^\n||tv-best.net^\n||tvoi-dosug.com^\n||tvoyarodoslovnaya.com/\n||twilightparadox.com^\n||twinplan.com^\n||twinzo.ru^\n||tx2.ru^\n||txtrek.net^\n||txtrk.com^\n||typiol.com^\n||typyky.com^\n||u*.takru.com^\n||ua-baner.com^\n||uaadi.com^\n||uamobile.net^\n||uchmuk.com^\n||ueo.load-games.com^\n||ugnetush.ru^\n||uhi_click.cgi?\n||uitrens.ru^\n||ujav.net^\n||ukrbanner.net^\n||ulenr.top^\n||ulogix.ru^\n||ultrapay.net^\n||umekana.ru^\n||umrefebaot.biz^\n||umyetor.ru^\n||unads.ru^\n||under-click.\n||underclick.\n||underdone.ru^\n||undere.com^\n||uniontraff.com^\n||unitedtraders.com^\n||universalsrc.com^\n||unscrewing.ru^\n||untily.com^\n||uocux.com^\n||upcash.ru^\n||upclick.ru^\n||upxip.xyz^\n||urlrtb.com^\n||urltraf.com^\n||urwb.ru/\n||us.a1.yimg.com^\n||us.i1.yimg.com^\n||useorthe.com^\n||userclick.su^\n||usr.trava.io^\n||utilient.com^\n||utrehter.com^\n||uuaoy.com^\n||uvcwj.com^\n||uwuyn.us^\n||v.ckpvz.space^\n||v.cuioj.com^\n||v.mir-18.ru^\n||v2mlemerald.com^\n||v2mljs.org^\n||v777.com/\n||vareza.net^\n||varinitconfique.ru^\n||varsloqt.name^\n||vatizon.com^\n||vayyly.com^\n||vbmay16.com^\n||vbmer.com^\n||vclicks.net^\n||vcslotoplay.com^\n||vedety.ru^\n||velegi.ru^\n||velopoc.ru^\n||venetsbezbrachiya.narod.ru\n||ventite.com^\n||venturead.com^\n||veroui.com^\n||vertelka.ru^\n||vertom.ru^\n||vessport.info^\n||vezetmne.ru^\n||vezuha.me^\n||viboom.com^\n||viboom.ru^\n||vicepiter.ru^\n||vidasys.ru^\n||vidaugust.ru^\n||viddirect.ru^\n||videc10.com^\n||video-invest.net^\n||video-link.ru^\n||video.begun.ru^\n||video.razvratu.net^\n||video.videonow.ru^\n||video001.com^\n||video103.ru^\n||video1132.com^\n||videoburner2015.com^\n||videoclick.\n||videoclik.\n||videofan.ru^\n||videojune.ru^\n||videoklass.ru^\n||videoleks.com^\n||videopotok.pro^\n||videorot.com^\n||videosmor.com^\n||videostrip.com^\n||videovikt.com^\n||videovint.com^\n||videsjs.com^\n||vidigital.ru^\n||vidkoz.com^\n||vidozex.ru^\n||vids63.com^\n||vidseed.ru^\n||vidustal.com^\n||vietalle.com^\n||viewrtb.com^\n||vijune.com^\n||vimart16.com^\n||vimgs.ru^\n||vimvio.ru^\n||vinov24.com^\n||vinregle.com^\n||viokan.xvhod.ru^\n||vip-traffic4.com\n||vip-traffic5.com^\n||vipadvert.net^\n||viperotika.net^\n||viplinck.com^\n||vipmest.info^\n||viprelax.com.ua^\n||vipromo.biz^\n||viptizer.com^\n||viptizerka.ru^\n||viptraff.ru^\n||viral-cdn.ru^\n||viraladnetwork.net^\n||virtl.xyz^\n||viruntek.ru^\n||visummer.com^\n||vitrine.sup.com^\n||viva-vanna.ru^\n||vivapays.com^\n||vizh.avetirus.ru^\n||vizh.zavetiss.ru^\n||vizp.dotrakivara.ru^\n||vjcbm.com^\n||vkkods.net^\n||vklike.com^\n||vkmessage01.net^\n||vkmoll.ru^\n||vkmonster.com^\n||vmblock.net^\n||vmet.ro^\n||vodnet.info^\n||vogo-vogo.ru^\n||vogopita.com^\n||vogorita.com^\n||vogotita.com^\n||vogozae.ru^\n||vogozaq.ru^\n||vongomedia.ru^\n||voob.ru/\n||voploti.com\n||vorinteon.ru^\n||vospate.ru^\n||vpbyl.com^\n||vpostele.net\n||vpsite.ru^\n||vpvsy.com^\n||vqfqo.us^\n||vrzgn.com^\n||vse.srazu.org^\n||vsekiski.org^\n||vsesumki.com^\n||vtdoska.ru^\n||vtizer.com^\n||vtizr.com^\n||vtochku.net^\n||vtrtl.de^\n||vulcan-bit.com^\n||vumek.club^\n||vvmblock.ru^\n||vwcsl.com^\n||vxqpomwum.pw^\n||vyalkata.ru^\n||vzarabotke.ru/\n||vzyat-kredit.info^\n||w*statistics.info^\n||wafum.ml^\n||waogi.com^\n||wapbanner.net^\n||wapstart.ru^\n||waptraff.mobi^\n||waveview.info^\n||web-centr.com^\n||web-rotation.net\n||web-under.net^\n||web-visor.\n||web.imgbum.net^\n||web.xlap.ru^\n||webangel.ru^\n||webgringo.ru^\n||webpamella90.uk.to^\n||webproficlub.ru^\n||webqs.ru^\n||websex24.ru^\n||websharks.ru^\n||websurf.ru/?\n||webturn.ru^\n||webunder.ru^\n||wecai.us^\n||wedgeac.com^\n||wedtor.com^\n||weropiy.com^\n||wewonurgold.ws^\n||wexrt.ru^\n||whaleserver.com^\n||whisla.com^\n||widefox.ru^\n||widetunel.ru^\n||widget.socialmart.ru^\n||widget.utinet.ru^\n||widgets.planeta.ru^\n||wifly.net^\n||wiflyad.net^\n||winvideo.org^\n||withnewswearebest.com^\n||wizard-teasers.com^\n||wizard-traffic.com^\n||wiztiz.pixarea.ru^\n||wjzvx.com^\n||wlbann.com^\n||wm-panel.com^\n||wm.rehdd.ru^\n||wmcasher.ru^\n||wmclickz.ru^\n||wmip.ru/\n||wmirk.ru^\n||wmlink.ru^\n||wmrlinks.ru^\n||wmrok.com^\n||wmrok.net^\n||wmwyq.xyz^\n||wmzona.com^\n||wolist.ru^\n||womanclick.ru^\n||women-and-handsome.info^\n||womenchop.com^\n||womenclick.ru^\n||wonderhit.com^\n||work-easy.info^\n||work-saite.ru\n||workon.ru^\n||world-2012.info^\n||worldoffer.ru^\n||worldofrest.com.ua^\n||wowmoscow.ru^\n||writeln.ru^\n||wshosting.ru^\n||wsp.marketgid.com^\n||wssdoo.com^\n||wtoredir.com^\n||wvzhj.com^\n||ww.showsteps.ru^\n||www0.xyz^\n||www2.bubblesmedia.ru^\n||www2.domah.ru^\n||wwwomen.ru^\n||wydpt.com^\n||wyuxy.com^\n||wzor.web-serf.info^\n||x-busty.org^\n||x-ip-adv.com^\n||x-ip-adwpc.com^\n||x-nomer.com^\n||x-tds-all.com^\n||x-tds-wiz.com^\n||x4x4x.org^\n||xanax.cbn.ru^\n||xaogi.com^\n||xarisma.ru^\n||xawab.com^\n||xbmhm.xyz^\n||xeemcol.com^\n||xexyc.com^\n||xiepl.com^\n||xiuuh.com^\n||xjmrl.xyz^\n||xjqha.com^\n||xkort.biz^\n||xmemory.ru^\n||xmqju.xyz^\n||xn-----7kcnh2ac3afebiffxijf2d6a5e.xn--p1ai^\n||xn----7sbbnc8bele.xn--p1ai^\n||xn----8sbcooencx8c3bm.xn--p1ai^\n||xn----etbhjdhwegjlz.xn--p1ai^\n||xn--80affy2c3b.xn--p1ai^\n||xn--u1aaw.xn--p1ai^\n||xoliter.com^\n||xptvk.com^\n||xsell.6waves.com^\n||xts-pay.ru^\n||xtvoh.com^\n||xuculit.ru^\n||xwell.ru/\n||xxslu.space^\n||xxx-babes.org^\n||xzwdo.top^\n||yandepit.com^\n||yangot.com^\n||yateaser.ru^\n||yatvyanos.ru^\n||yellowmedia.biz^\n||yellowsearch.ru^\n||yfkha.xyz^\n||yhake.xyz^\n||yidop.com^\n||yjvfuzdr.pw^\n||yluvo.com^\n||ynwia.com^\n||yoga-terapia.ru^\n||yohioo.com^\n||yopik.club^\n||yotaplay.ru*promo=true\n||yottos.com\n||yourclick.pw^\n||yourfoxes.ru^\n||yourlustmedia.com^\n||yrsfs.com^\n||ysmile.ru^\n||yulinata.ru^\n||yuznay.ru^\n||ywadk.top^\n||yyuin.com^\n||zabedi.ru^\n||zaebaladblock.ru^\n||zakladka.org.ua^\n||zamiko.ru^\n||zamok911.com^\n||zapbox.ru^\n||zarabotay.1gif.ru^\n||zarabotki.ru^\n||zatexta.com^\n||zboac.com^\n||zdenochary.com^\n||zdesoftina.ru^\n||zgvvx.com^\n||zhaykani.ru^\n||zherimo.ru^\n||zheton.com*ref=\n||zhopka.vsezaibis.ru^\n||zipmonster.biz^\n||zlat24.ru.com^\n||znews.su^\n||zoda.ru/\n||zoo-porno.biz^\n||zoomclick.info^\n||zorkabiz.ru^\n||zowlu.xyz^\n||zoy.org^\n||zozocash.biz^\n||zozoter.ru^\n||zqizn.com^\n||zqvix.xyz^\n||ztmol.club^\n||zto.h16.ru^\n||zurage.ru^\n||zxrtn.com^\n! VisitWeb ads\n! Example: http://svetmonet.ru/2098.html\n!\n! Section contains the list of advertising networks, which are hosted on non advertising sites as subdomains\n!\n://*.pika-img.ru^\n://*.imagespics.ru^\n://*.freerutor.com^\n://*.imgspic.ru^\n||pr.rusmed.ru^\n://*.imgpics.ru^\n://*.pix-images.ru^\n||adf.kino-go.co^\n||bn.kino-go.co^\n||theblackdeath.ru^\n://*.picstraff.ru^\n://*.anime-free.net^\n://*.img-picas.ru^\n://*.pica-img.ru^\n://*.picmir.nu^\n://*.images-pix.ru^\n://*.pikas-img.ru^\n://*.images-pic.ru^\n||r.dimkriju.bget.ru^\n||adl.kinogo.by^\n||m.kinolot.com^\n||cloud.seedoff.tv^\n||tizer.rupornophoto.com^\n||t.sexycontent.net^\n||n.cashheaven.ru^\n||da.rosrabota.ru^\n||moya.furma.ru^\n||ad.cbonds.info^\n||mixtraff.silvercdn.com^\n||partners.parimatch.net^\n||c.grimuar.ru^\n||r.topdent.ru^\n.forpicfree.ru^\nws*://video.docfilms.info^\n||987654321.kiev.ua^\n||action.evrikak.ru^\n||ad.gameagregator.com^\n||add.alltorrents.net^\n||ads.211.ru^\n||ads.dfiles.ru^\n||adshow.sc2tv.ru^\n||af.1gdz.ru^\n||d899.webazilla.com^\n||fastpicc.ru^\n||ff.astv.ru^\n||js.pornolab.eu^\n||mega.megatorrents.org.ua^\n||promo.adult-torrent.com^\n||r6.galya.ru^\n||real.attico-design.ru^\n||rs.mail.ru^\n||rv.mastercity.ru^\n||server.zombie-tv.org^\n||showbiz.mail.ru^\n||statistic.imgpay.ru^\n||stats.imgpay.ru^\n||ts2.hockey-talks.com^\n!\n! Section contains list of advertising networks\n!\n||2performant.com^\n||simudotheh.com^\n||giu9aab.bid^\n||tubeadvertising.eu^\n||brand.ad^\n||v1n7c.com^\n||referrer.website^\n||filadmir.site^\n||syndication.exosrv.com^\n||borotango.com^\n||sextadate.net^\n||adsrvgateway.com^\n||api.reon.club^\n||parserwords.info^\n||bravo-lea.com^\n||cloudsdns.net^\n||tracking.cirrusinsight.com^\n||130.211.230.53^\n||thankswrite.com^\n||networkmanag.com^\n||dk7rftbivnkgr.cloudfront.net^\n||voluumtrk2.com^\n||bundasnovinhas.com^\n||newviralmobistore.com^\n||block.s*block.com^\n||adsrvmedia.adk2.co^\n||onclicktop.com^\n||cah9ooy.bid^\n||wsp.adskeeper.co.uk^\n||ov2ochu.bid^\n||phox2ey.bid^\n||555fff555f.net^\n||wsp.steepto.com^\n||beglorena.com^\n||awesome-revenue.com^\n||track.bcvc.mobi^\n||go.verymuchad.com^\n||afftrack001.com^\n||syndication.globaltraffico.com^\n||adrecreate.com^\n||asdorka.com^\n||ppndr.xyz^\n||ujieva.com^\n||code.poptm.com^\n||adzos.com^\n||mxtads.com:8040\n||go.cartstick.com^\n||gernewt.info^\n||onshowit.com^\n||ads.tremorhub.com^\n||bee7.com^\n||advertiserurl.com^\n||reeviveglobal.com^\n||r7mediar.com^\n||prxii.cf^\n||alxsite.com^\n.bbelements.com^\n.doublepimp.com^\n.lockscalecompare.com^\n||0aac4e6a54c170b0.se^\n||1100ad.com^\n||178.17.164.58^\n||1app.blob.core.windows.net^\n||360popunder.com^\n||3al.pw^\n||5b15a826.xyz^\n||60ads.com^\n||78.140.130.91^\n||863c4c0c521.se^\n||abckj123.com^\n||activepop.net^\n||ad2goal.com^\n||adamoads.com^\n||adbetnet.advertserve.com^\n||adglare.org^\n||adk2.net^\n||admax.quisma.com^\n||admost.com^\n||adosia.com^\n||adpeepshosted.com^\n||adplus.co.id^\n||adproxy2.com^\n||adrewards.com^\n||adrotate.se^\n||adrotator.se^\n||ads.eorezo.com^\n||ads.heias.com^\n||ads2-adnow.com^\n||adsglow.net^\n||adshell.net^\n||adsmeda.com^\n||adspeed.net^\n||adstizer.com^\n||adtech.com^\n||adternal.com^\n||adtomafusion.net^\n||adtraffic.pl^\n||adultadspy.com^\n||adv.mediaharbor.co.kr^\n||adversolutions.com^\n||advertclickme.com^\n||adviator.com^\n||adworkmedia.com\n||adxpansion.com^\n||aff.camplace.com^\n||affsnetwork.com^\n||alephd.com^\n||alexrid.com^\n||allbestnews.net^\n||appintop.com^\n||applicationgrabb.net^\n||asto-exo.com^\n||asv.whatismyip.win^\n||axecash.com^\n||az708531.vo.msecnd.net^\n||b.rmgserving.com^\n||badtopwitch.work^\n||banner.dabi.ir^\n||bannery.hledejceny.cz^\n||bbcdn.go.evolutionmedia.bbelements.co\n||bedebadum.net^\n||best.infoiswhatwedo.com^\n||bestevernews.com^\n||bestevernews.net^\n||bestnewsworld.net^\n||bestwebdeal.net^\n||bitmedia.io^\n||bitmedianetwork.com^\n||bkrtx.com^\n||black6adv.com^\n||blkget8.com^\n||boomads.com^\n||breeffnet.com^\n||bs.serving-sys.com^\n||btdnav.com^\n||buy-banner.com^\n||buy-targeted-traffic.com^\n||buzzabc.com^\n||c.mfstatic.cz^\n||cdn.adk2.com^\n||cdn.pulpix.com^\n||checkchick4u.org^\n||clickadu.com^\n||clickforward.net^\n||clkoffers.com^\n||cntrafficpro.com^\n||contactsin.mobi^\n||contextual.media.net^\n||corrosif.science^\n||costaction.com^\n||cpmterra.com^\n||ctmconnect.com^\n||dasistnews.net^\n||data.permittingnorthlandseamen.info^\n||data.shipboardserviceberrysiltstone.info^\n||ddomb.com^\n||detour.click^\n||diamond88bet.com^\n||dotomi.com^\n||dpjtch.vestabalin.com^\n||dsp-eu.exe.bid^\n||e-n-t-e-r-n-e-x.com^\n||e.lndjj.com^\n||eclkmpsa.com^\n||eclkspbn.com^\n||emediate.apmmedia.net^\n||engine.adtidy.net^\n||epom.com^\n||eropayper.com^\n||evolutionmedia.bbelements.com\n||exitintel.com^\n||exogripper.com^\n||ext.movixhub.com^\n||f.asdfzxcv1312.com^\n||f.iaftjs.info^\n||fabolele.com^\n||fdgeen.com^\n||filetarget.net^\n||firstfirst.net^\n||flashteaser.com^\n||fls.doubleclick.net^\n||fsoft4down.com^\n||fungetbag.info^\n||fxhoog.com^\n||game.gamingnonstop.net^\n||girlzgirlzgirlz4u.com^\n||go.onclasrv.com^\n||go.trafficshop.com^\n||gomakemerich1.com^\n||grandslammedia.com^\n||growtaller4adults.com^\n||growtaller4men.com^\n||growtaller4women.com^\n||growth-flexvprosystem.com^\n||heightboost.com^\n||herezera.com^\n||hgads.com^\n||hubrus.com^\n||id.kbmg.cz^\n||imaginaxs.com^\n||india-zed.com^\n||inter1ads.com^\n||j.77power.com^\n||jfduv7.com^\n||ji.ihualun.com^\n||jkolp.com^\n||join.xlgirls.com^\n||js.mengheng.net^\n||js.srcsmrtgs.com^\n||jump.fhoa365.com^\n||junbi-tracker.com^\n||klickthru.com^\n||kmylvwo5.com^\n||landing.meendo.com^\n||lepubs.com^\n||lifestyle24h.com^\n||liflandaffiliates.com^\n||ligatus.com^\n||linkbucks.com^\n||liversely.com^\n||liversely.net^\n||llgerege.com^\n||llladiez.org^\n||lookfornews.net^\n||madyna.ru^\n||magicplayer-api.torrentstream.org^\n||magicplayer-s.acestream.net^\n||magicplayer-s.torrentstream.org^\n||magifirst.com^\n||mainclc.com^\n||maneta.info^\n||masteroids.com^\n||measure.mf.cz^\n||media.adtrack1.pl^\n||mediastinct.com^\n||metodikadeneg.info^\n||mf*.advantage.as^\n||mirs.com^\n||moguldom.com^\n||moneyplatform.biz^\n||mtburn.com^\n||myadshub.com^\n||mybinarysystem.com^\n||myvpn.pro^\n||n156adserv.com^\n||nefroto.net^\n||newsforbest.net^\n||njmaq.com^\n||nrged.com^\n||nsdfsfi1q8asdasdzz.com^\n||offergo.net^\n||offpageads.com^\n||onlinesupaads.com^\n||onlinewebfind.com^\n||opt.ximad.com^\n||paradoxtraffic.com^\n||physoi.eu^\n||pketred.com^\n||plsdrct2.me^\n||polimadv.com^\n||pomtiy.com^\n||pop-road47.info^\n||popped.biz^\n||poprush.net^\n||popup-traffic.com^\n||popvertising.com^\n||popzila.com^\n||poweradcash.net^\n||print3.info^\n||prm.sushis.kim^\n||profit-pay.net^\n||profitboosterapp.com^\n||profitsdeluxe.com^\n||proteaser.net^\n||protectaffiliates.org^\n||prxii.tk^\n||purgrobi.com^\n||qaadv.com^\n||qadabra.com^\n||qractv.com^\n||quicksense.net^\n||qwtm.purecertainengine.com^\n||rdsa2012.com^\n||redichat.com^\n||regisg.com^\n||retainguaninefluorite.info^\n||retarget.ssl-services.com^\n||rhtag.com^\n||roulettebot-plus.com^\n||rtbpopd.com^\n||s1.adguard.com^\n||sc.rvtlife.com^\n||scr.flashcast.org.uk^\n||secure-uk.imrworldwide.com^\n||serve.williamhill.com^\n||shorlakmedia.com^\n||soadvr.com^\n||softexcellence.com^\n||sourcengo.com^\n||spidtest.org^\n||srvpub.com^\n||static.adtidy.net^\n||static.hoptopboy.com^\n||strands.com^\n||super-promo.cyme.info^\n||syndication.exoclick.com^\n||syndication.jsadapi.com^\n||syngrestic.com^\n||synhandler.net\n||t.booksuper.info^\n||tapfiliate.com^\n||teen-porn-portal.com\n||temphilltop.net^\n||thetraffic-translate.com^\n||tizermedias.com^\n||tr553.com^\n||trafficholder.com^\n||trafficmagnates.com^\n||traforet.com^\n||transpera.com^\n||u034024.778669.com^\n||vartoken.com^\n||videogenetic.com^\n||waframedia16.com^\n||watrz.com^\n||weberotic.net^\n||winneradsmedia.com^\n||www.fuze-hill*.xyz^\n||www.fuze-sea*.xyz^\n||xpostx.com^\n||yesadvertising.com^\n||ylx-4.com^\n||yuiop.trade^\n||zanox.com^\n||zaperplop.info^\n||zemanta.com^\n||zeti.com^\n!\n! Section contains the list of advertising networks, which are hosted on non advertising sites as subdomains\n!\n||d19182vyfoustz.cloudfront.net^\n||d1dli2tyorled9.cloudfront.net^\n||d2i54aseqwhx68.cloudfront.net^\n||d3laygk9zni6hc.cloudfront.net^\n||d1v6js7bjzmhoa.cloudfront.net^\n||da5w2k479hyx2.cloudfront.net^\n||d1vwpe7grtcv9d.cloudfront.net^\n||beap-bc.yahoo.com^\n||d32r49xyei4vz6.cloudfront.net^\n||d1ewpr7kbabyrj.cloudfront.net^\n||d240937yockcdo.cloudfront.net^\n||d3id4jppiyyek8.cloudfront.net^\n||dohs95d6tfj19.cloudfront.net^\n||d2nn3xyicdpsrf.cloudfront.net^\n||dviixeyykyqjv.cloudfront.net^\n||d3noqwmgo39at7.cloudfront.net^\n||d2l8bbn629wykr.cloudfront.net^\n||d2ymkpxi1rgldj.cloudfront.net^\n||beap1.cb.g01.yahoodns.net^\n||script.hqpass.com^\n||d17rlarvg2khuc.cloudfront.net^\n||d1gp9nlx229wzz.cloudfront.net^\n||d1k29rhvz38kg5.cloudfront.net^\n||d2ue9k1rhsumed.cloudfront.net^\n||d2xsy1lxezptdm.cloudfront.net^\n||d3ax6xygyb5hn9.cloudfront.net^\n||d3m79ugzs2d8im.cloudfront.net^\n||d9rj2sdxjer5v.cloudfront.net^\n||dr3k6qonw2kee.cloudfront.net^\n||js.bulkhentai.com^\n!\n! Section contains list of advertising networks\n!\n||appwall.tv2phone.cn^\n||ads.pdbarea.com^\n||sdk.starbolt.io^\n||mobads4app.com^\n||redirector.gvt1.com^\n||adsmoloco.com^\n||yhdichan.com^\n||partnerad.l.doubleclick.net^\n||pagead46.l.doubleclick.net^\n||ad.kixer.com^\n||mobon.com^\n||pixel.admobclick.com^\n||bayctrk.com^\n||clk.taptica.com^\n||api.leadbolt.net^\n||exp.glispa.com^\n||adserver.unityads.unity3d.com^\n||cdn.unityads.unity3d.com^\n||show.niqiu99.org^\n||pl.263gmail.org^\n||slb.gedawang.com^\n||ctl.buyt.in^\n||adv.mxmcdn.net^\n||api.adflake.com^\n||dante2007.com^\n||iad.appboy.com^\n||banners.etermax.com^\n||mob.huimee.net^\n||mediate-ios-*.hyprmx.com^\n||marketplace-ios-*.hyprmx.com^\n||live.hyprmx.com^\n||cdn.ads.fotoable.net^\n||bannerwall.herewetest.com^\n||bannerwall.s3.appcnt.com^\n||bttn.io^\n||api.kiip.me^\n||config.tremorhub.com^\n||l0-secure.videohub.tv^\n||perf-events.cloud.unity3d.com^\n||e.apsalar.com^\n||ha-api.pushwoosh.com^\n||cp.pushwoosh.com^\n||houseads-prod.elasticbeanstalk.com^\n||ads.aerserv.com^\n||production-adserver-*.amazonaws.com^\n||ads.pinger.com^\n||cf-ads.pinger.com^\n||tracking.applift.com^\n||ads.yahoo.com^\n||west.bidtellect.com^\n||appshelf.ttpsdk.info^\n||houseads.ttpsdk.info^\n||ultraadserver.com^\n||abtest.swrve.com^\n||clkfeed.com^\n||campaigns.apps-connects.com^\n||d2nrdy2pg3k168.cloudfront.net^\n||buzzonclick.com^\n||hormebets.info^\n||googleadservices.com^\n||admarketing.yahoo.net^\n||adback.tango.me^\n||adinfo.tango.me^\n||ads.admarvel.com^\n||base-cdn.admarvel.com^\n||usekahuna.com^\n||api.usebutton.com^\n||adserver.pandora.com^\n||tap-nexus.appspot.com^\n||dev.appboy.com^\n||shared.iad.appboy.com^\n||iadsdk.apple.com^\n||manage.com^\n||intowow.com^\n||adtilt.com^\n||ads.glispa.com^\n||trk.glispa.com^\n||ad.apsalar.com^\n||syndication.trafficreps.com^\n||reporo.net^\n||apxadtracking.net^\n||admarvel.s3.amazonaws.com^\n||leanplum.com^\n||vidout.net^\n||ad.jorte.com^\n://o0e.ru^\n||1mp.mobi^\n||31.14.252.148^\n||ad*.nexage.com^\n||ad-brix.com^\n||ad-creatives-public.commondatastorage.googleapis.com^\n||ad-x.co.uk^\n||ad.apps.fm^\n||ad.leadbolt.net^\n||ad.leadboltapps.net^\n||ad.madvertise.de^\n||ad.mail.ru^\n||ad.ohmyad.co^\n||ad.smaad.jp^\n||ad.vrvm.com^\n||adblade.com^\n||adcel.vrvm.com^\n||addapptr.com^\n||adform.com^\n||adinfuse.com^\n||aditic.net^\n||adkmob.com^\n||adleads.com^\n||adlibr.com^\n||admail.am^\n||admicro1.vcmedia.vn^\n||admicro2.vcmedia.vn^\n||admin.appnext.com^\n||admixer.co.kr^\n||adotsolution.com^\n||adplay.vm5apis.com^\n||adpublisher.s3.amazonaws.com^\n||adquota.com^\n||ads.admoda.com^\n||ads.fotoable.com^\n||ads.marvel.com^\n||ads.matomymobile.com^\n||ads.mdotm.com^\n||ads.mobilefuse.net^\n||ads.mobilityware.com^\n||ads.mobvertising.net^\n||ads.mopub.com^\n||mpx.mopub.com^\n||ads.n-ws.org^\n||ads.ookla.com^\n||ads.reward.rakuten.jp^\n||ads.taptapnetworks.com^\n||ads.xlxtra.com^\n||ads.youtube.com^\n||adsee.jp^\n||adserver.goforandroid.com^\n||adserver.kimia.es^\n||adserver.mobillex.com^\n||adserver.ubiyoo.com^\n||adsx.greystripe.com^\n||adsymptotic.com^\n||adtrack.king.com^\n||adups.com^\n||advertiser.fyber.com^\n||adwods.com^\n||adz.mobi^\n||adzmobi.com^\n||adzworld.in^\n||amoad.com^\n||api.appfireworks.com^\n||api.fusepowered.com^\n||app-measurement.com^\n||app-trackings.com^\n||appclick.co^\n||appclick.net^\n||applifier.com^\n||applift.com^empty\n||applovin.com^\n||appnext-a.akamaihd.net^\n||appodeal.com^\n||appsdt.com^\n||appserver-ap.com^\n||appserver-cp.com^\n||appsflyer.com^\n||as.adfonic.net^\n||astrsk.net^\n||atdmt.com^\n||atti.com^\n||axonix.com^\n||b.cpiera.com^\n||bcfads.com^\n||bigmobileads.com^\n||brokerbabe.com^\n||cdn.cpiera.com^\n||cell.zhybzp.cn^\n||celtra.com^\n||ceppartner.com^\n||chartboost.com^\n||cloudads-a.akamaihd.net^\n||cntcash.ru^\n||content.ad^\n||crispadvertising.com^\n||crossboardmobile.com^\n||csi.gstatic.com^\n||d60y8cj1tje2a.cloudfront.net^\n||d830x8j3o1b2k.cloudfront.net^\n||debojuagug1sf.cloudfront.net^\n||decide.mixpanel.com^\n||dispatcher.mng-ads.com^\n||e.qq.com^\n||eltrafiko.com^\n||fiksu.com^\n||fizcell.mobi^\n||fluentmobile.com^\n||flurry.cachefly.net^\n||flurry.com^\n||focas.jp^\n||fyber.com^\n||go.mobstitial.com^\n||go.mobstitialtag.com^\n||go.mobtrks.com^\n||go.pushnative.com^\n||gsmtop.net^\n||gts-ads.twistbox.com^\n||hastrk1.com^\n||heyzap.com^\n||huntmad.com^\n||i-mobile.co.jp^\n||i-vengo.com^\n||i.tapit.com^\n||inmobi.net^\n||inmobicdn.net^\n||inmobisdk-a.akamaihd.net^\n||inner-active.com^\n||inner-active.mobi^\n||jsc.marketgid.com^\n||justad.mobi^\n||keydot.net^\n||kochava.com^\n||lb.usemaxserver.de^\n||leadboltads.net^\n||loopme.me^\n||ma-code.ru^\n||mad.mobisky.pl^\n||mads.bz^\n||mc.yandex.ru^\n||mdotm.com^\n||media.motrixi.com^\n||metamx.com^\n||mgage.com^\n||mi.gdt.qq.com^\n||microad.net^\n||mixpanel.com^\n||mmnetwork.mobi^\n||moba8.net^\n||mobad.ijinshan.com^\n||mobads.baidu.com^\n||mobclix.com^\n||mobicow.com^\n||mobile.mng-ads.com^\n||mobile333.com^\n||mobileads.msn.com^\n||mobileapptracking.com^\n||mobilecore.com^\n||mobiledl.adboe.com^\n||moboland.net^\n||mobred.net^\n||mobzver.ru^\n||mofox.com^\n||mologiq.net^\n||mopub.web107-east.manage.com^\n||msntest.serving-sys.com^\n||mtburn.jp^\n||mydas.mobi^\n||nativex.com^\n||nearbyad.com^\n||nend.net^\n||njs.manhuahome.com^\n||nomalleadzuaff.com^\n||nonmoves.date^\n||novostimira.biz^\n||os.scmpacdn.com^\n||owap.su^\n||pflexads.com^\n||phluant.com^\n||playhaven.com^\n||plugin.mediavoice.com^\n||pontiflex.com^\n||pos.baidu.com^\n||pub1.co^\n||pubnative.net^\n||rek.mobi^\n||relap.io^\n||richpays.com^\n||rtb.nexage.com^\n||rtbimp-loadbalancer-*.amazonaws.com^\n||s.innovid.com^\n||sas816.ujikdd041o.cn^\n||sdk.native123.com^\n||serving-sys.com^\n||sessionm.com^\n||shareasale.com^\n||silvermob.com^\n||singular.net^\n||splash.appsgeyser.com^\n||sscefsol.com^\n||stat.kika-backend.com^\n||static.doubleclick.net^\n||supersonicads-a.akamaihd.net^\n||supersonicads.com^\n||tapcreatives.net^\n||tapit.com^\n||tapone.jp^\n||tappx.com^\n||tapsense.com^\n||tapstat.ru^\n||thetrafficstat.net^\n||theylike.org^\n||tm1.hoiplay.com^\n||tpc.googlesyndication.com^\n||track.cpatool.net^\n||trackimpression.com^\n||tracking.acekoala.com^\n||traff10wap.com^\n||traktum.com^\n||trax-ad.jp^\n||trc.taboola.com^\n||umbel.com^\n||unicume.com^\n||update.sdk.batmobi.net^\n||upsight.com^\n||vserv.mobi^\n||waiads.com^\n||wap-click.com^\n||wbn.su^\n||websexy.mobi^\n||widespace.com^\n||wsback*.presage.io^\n||wtraff.com^\n||ximad.com^\n||yandexadexchange.net^\n||yc-ads.s3.amazonaws.com^\n||yemonisoni.com^\n||youmi.net^\n||yuhuads.com^\n! HTTPS\n! Facebook Ad Choices\n! ||graph.facebook.com/network_ads_common/\n!----- For HTTPS websites when HTTPS filtering is disabled ------\n||rg.yottos.com^\n!----------------------------------------------------------------\n!\n! Tracking service\n!\n||statlife.ru^\n||bia.brightinfo.com^\n||track.tenjin.io^\n||extremetracking.com^\n||tags.bluekai.com^\n||p.smartertravel.com^\n||ta.queit.in^\n||ninja.onap.io^\n||ape-tagit.timeinc.net^\n||cointent.com^\n||tag.aticdn.net^\n||lciapi.ninthdecimal.com^\n||tap.idg.de^\n||trackingapi.cloudapp.net^\n||mastertag.kpcustomer.de^\n||p.d.rpts.org^\n||ctl.mobitrack.co.kr^\n||m.addthisedge.com^\n||ct.buzzfeed.com^\n||collector.schibsted.io^\n||analytics.archive.org^\n||apps.poln.co^\n||hindsight.significanceapps.com^\n||track.spots.im^\n||pixelg.adswizz.com^\n||geoservice.curse.com^\n||in.cuebiq.com^\n||target.smi2.net^\n||d.impactradius-event.com^\n||wa.ui-portal.de^\n||whos.amung.us^\n||app.crossengage.io^\n||tracking.musixmatch.com^\n||beacon.errorception.com^\n||vmweb.net^\n||geoip.herewetest.com^\n||crm.zenguard.biz^\n||yieldmo-builds.s3.amazonaws.com^\n||apm.crittercism.com^\n||kinesis.us-east-1.amazonaws.com^\n||pippio.com^\n||imrworldwide.net^\n||imrworldwide.com^\n||pageview.goroost.com^\n||tracking.bdi-services.de^\n||ingest.crittercism.com^\n||projeanaliz.com^\n||tracking.vitringez.com^\n||t.channeladvisor.com^\n||decenthat.com^\n||analytics.nextopia.net^\n||sv.ltzvs.ru^\n||count.channeladvisor.com^\n||metrics.pacsun.com^\n||tracking2.channeladvisor.com^\n||track.custora.com^\n||track.securedvisit.com^\n||t.custora.com^\n||fullstory.com^\n||te.supportfreecontent.com^\n||slingshot.io^\n||ftrack.ru^\n||extstat.info^\n||collect-elb-*.amazonaws.com^\n||stats.jpush.cn^\n||log.iyunmai.com^\n||instabug.com^\n||datametrical.com^\n||apptimize.com^\n||apptentive.com^\n||c00.adobe.com^\n||device-analytics.rollout.io^\n||data.mob.com^\n||stats.pandora.com^\n||log.musical.ly^\n||stats.paypal.com^\n||metrics.mzstatic.com^\n||fc.webmasterpro.de^\n||trck.spoteffects.net^\n||visilabs.com^\n||cmt.setrowid.com^\n||hit.dogannet.tv^\n||fitanalytics.com^\n||analytic.piri.net^\n//t100.ru^\n://tru.am^\n||0emm.com^\n||0stats.com^\n||0tracker.com^\n||1-cl0ud.com^\n||100im.info^\n||103bees.com^\n||105app.com^\n||11nux.com^\n||123-counter.de^\n||123compteur.com^\n||123count.com^\n||12mnkys.com^\n||149.13.65.144^\n||180.76.2.18^\n||193.197.158.209^\n||195.10.245.55^\n||1freecounter.com^\n||1pel.com^\n||200stran.ru^\n||200summit.com^\n||204st.us^\n||206solutions.com^\n||208.88.226.75^\n||212.227.100.108^\n||212.95.32.75^\n||216.18.184.18^\n||247ilabs.com^\n||24businessnews.com^\n||24counter.com^\n||24log.com^\n||24log.de^\n||24log.ru^\n||2cnt.net^\n||2o7.net^\n||360i.com^\n||360tag.com^\n||360tag.net^\n||3dlivestats.com^\n||3dstats.com^\n||3gl.net/\n||3gl.net^\n||40nuggets.com^\n||4oney.com^\n||4stats.de^\n||50bang.org^\n||51yes.com^\n||55labs.com^\n||62.160.52.73^\n||66.228.52.30^\n||67.228.151.70^\n||6sc.co^\n||72.172.88.25^\n||74.55.82.102^\n||77tracking.com^\n||79.125.117.123^\n||7bpeople.com^\n||7eer.net^\n||8020solutions.net^\n||99click.com^\n||99counters.com^\n||99stats.com^\n||9nl.eu^\n||a-cast.jp^\n||a-counter.com.ua^\n||a-counter.kiev.ua^\n||a-counters.com^\n||a-pagerank.net^\n||a.hstrck.com^\n||a.oix.net^\n||a013.com^\n||a3.ogt.jp^\n||a8.net^\n||a8ww.net^\n||aaddzz.com^\n||aamsitecertifier.com^\n||abcompteur.com^\n||abcounter.de^\n||abcstats.com^\n||abmr.net^\n||absolstats.co.za^\n||acc-hd.de^\n||access-analyze.org^\n||access-traffic.com^\n||accessi.it^\n||accessintel.com^\n||accumulatorg.com^\n||acecounter.com^\n||acestats.net^\n||acetrk.com^\n||acexedge.com^\n||acint.net^\n||acq.io^\n||acs86.com^\n||actionallocator.com^\n||active-tracking.de^\n||active-trk7.com^\n||active24stats.nl^\n||activeconversion.com^\n||activemeter.com^\n||activeprospects.com^\n||actnx.com^\n||actonsoftware.com^\n||acxiom-online.com^\n||ad-score.com^\n||adbutler.us^\n||adc-serv.net^\n||adchemix.com^\n||adchemy-content.com^\n||adchemy.com^\n||adclear.net^\n||adclickstats.net^\n||addcontrol.net^\n||addfreestats.com^\n||addlvr.com^\n||adelixir.com^\n||adg.bzgint.com^\n||adgreed.com^\n||adinsight.com^\n||adinsight.eu^\n||adku.co^\n||adku.com^\n||admantx.com^\n||admdspc.com^\n||admeo.ru^\n||adobetag.com^\n||adoftheyear.com^\n||adprotraffic.com^\n||adrank24.de^\n||adrta.com^\n||ads-trk.vidible.tv^\n||adsensedetective.com^\n||adsettings.com^\n||adspsp.com^\n||adstat.4u.pl^\n||adtarget.me^\n||adtelligence.de^\n||adtraction.com^\n||adtraxx.de^\n||adultblogtoplist.com^\n||advanced-web-analytics.com^\n||advconversion.com^\n||adyapper.com^\n||adzoe.de^\n||afairweb.com^\n||affilae.com^\n||affiliateedge.eu^\n||affiliates-pro.com^\n||affiliatetrackingsetup.com^\n||affiliatly.com^\n||affinesystems.com^\n||affinitymatrix.com^\n||affistats.com^\n||afsanalytics.com^\n||agencytradingdesk.net^\n||agentinteractive.com^\n||agillic.eu^\n||agilone.com^\n||agkn.com^\n||aidata.io^\n||aimediagroup.com^\n||airbrake.io^\n||airpr.com^\n||akanoo.com^\n||akavita.com^\n||akstat.com^\n||alcvid.com^\n||alenty.com^\n||alexacdn.com^\n||alexametrics.com^\n||alltagcloud.info^\n||alltracked.com^\n||alnera.eu^\n||altabold1.com^\n||altastat.com^\n||alvenda.com^\n||alzexa.com^\n||amadesa.com^\n||amavalet.com^\n||amazingcounters.com^\n||amazy.ru^\n||ambercrow.com^\n||amctp.net^\n||amikay.com^\n||amplifypixel.outbrain.com^\n||amung.us^\n||amxdt.com^\n||analoganalytics.com^\n||analysistools.net^\n||analytics-egain.com^\n||analytics-engine.net^\n||analytics.anvato.net^\n||analytics.artirix.com^\n||analytics.aweber.com^\n||analytics.edgekey.net^\n||analytics.edgesuite.net^\n||analytics.electro-com.ru^\n||analytics.fairfax.com.au^\n||analytics.localytics.com^\n||analytics.newsinc.com^\n||analytics.query.yahoo.com^\n||analytics.rechtslupe.org^\n||analytics.shareaholic.com^\n||analytics.twitter.com^\n||analytics.yahoo.com^\n||analyticswizard.com^\n||analytk.com^\n||anametrix.com^\n||anametrix.net^\n||anatid3.com^\n||andersenit.dk^\n||andyhoppe.com^\n||anexia-it.com^\n||angelfishstats.com^\n||angsrvr.com^\n||announcement.ru^\n||anonymousdmp.com^\n||anormal-tracker.de^\n||anrdoezrs.net^\n||anti-cheat.info^\n||apexstats.com^\n||apextwo.com^\n||apicit.net^\n||apkonline.ru^\n||apollofind.com^\n||appboycdn.com^\n||aprtx.com^\n||aqtracker.com^\n||arcadeweb.com^\n||arch-nicto.com^\n||arena-quantum.co.uk^\n||arkayne.com^\n||arlime.com^\n||arpuonline.com^\n||arpxs.com^\n||arrowpushengine.com^\n||artefact.is^\n||arturtrack.com^\n||assoctrac.com^\n||astro-way.com^\n||atatus.com^\n||athenainstitute.biz^\n||atm.youku.com^\n||atoshonetwork.com^\n||atraxio.com^\n||atsfi.de^\n||atticwicket.com^\n||attracta.com^\n||audience.visiblemeasures.com^\n||audienceamplify.com^\n||audienceiq.com^\n||audiencerate.com^\n||audrte.com^\n||authorinsights.com^\n||auto-ping.com^\n||autoaffiliatenetwork.com^\n||autoaudience.com^\n||autoid.com^\n||avantlink.com^\n||avastats.com^\n||avazudsp.net^\n||avencio.de^\n||avenseo.com^\n||avmws.com^\n||avstat.it^\n||awasete.com^\n||awesomelytics.com^\n||awin1.com^\n||awmcounter.de^\n||axf8.net^\n||az7t8.com^\n||azalead.com^\n||azera-s014.com^\n||b.aol.com^\n||b.grvcdn.com^\n||b.oix.net^\n||b1img.com^\n||b2c.com^\n||babator.com^\n||backlink-test.de^\n||backlink-umsonst.de^\n||backlinkdino.de^\n||backlinkprofi.info^\n||backlinks.li^\n||backlinktausch.biz^\n||bam-x.com^\n||baptisttop1000.com^\n||barilliance.net^\n||barlive.link^\n||basicstat.com^\n||basilic.io^\n||bats.video.yahoo.com^\n||baynote.net^\n||beacon.guim.co.uk^\n||beacon.kmi-us.com^\n||beanscattering.jp^\n||beanstalkdata.com^\n||beanstock.com^\n||bebj.com^\n||beemrdwn.com^\n||beencounter.com^\n||behavioralengine.com^\n||bekannt-im-web.de^\n||beliebtestewebseite.de^\n||belstat.at^\n||belstat.be^\n||belstat.ch^\n||belstat.com^\n||belstat.de^\n||belstat.fr^\n||belstat.nl^\n||benchit.com^\n||berg-6-82.com^\n||best-top.de^\n||best-top.ro^\n||bestcontactform.com^\n||bestweb2013stat.lk^\n||besucherstats.de^\n||besucherzaehler-counter.de^\n||besucherzaehler-homepage.de^\n||besucherzaehler-zugriffszaehler.de^\n||besucherzaehler.org^\n||besucherzahlen.com^\n||betarget.com^\n||betarget.de^\n||betarget.net^\n||bfoleyinteractive.com^\n||bhs4.com^\n||bid.run^\n||bidswitch.net^\n||bigcattracks.com^\n||bigmir.net^\n||bigsauron.ru^\n||bigstats.net^\n||bigtracker.com^\n||bionicclick.com^\n||bitrix.info^\n||bizible.com^\n||bizo.com^\n||bizspring.net^\n||bkvtrack.com^\n||blizzardcheck.com^\n||blockbreaker.io^\n||blockmetrics.com^\n||blog-o-rama.de^\n||blog-stat.com^\n||blog-webkatalog.de^\n||blog104.com^\n||blogcounter.com^\n||blogcounter.de^\n||bloggeramt.de^\n||bloggerei.de^\n||blogmeetsbrand.com^\n||blogpatrol.com^\n||blogrankers.com^\n||blogranking.net^\n||blogreaderproject.com^\n||blogscounter.com^\n||blogsontop.com^\n||blogtoplist.com^\n||blogtraffic.de^\n||blogtraffic.sg^\n||blogtw.net^\n||blogverzeichnis.eu^\n||bluecava.com^\n||blueconic.net^\n||bluecounter.de^\n||bluekai.com^\n||blvdstatus.com^\n||bm23.com^\n||bm324.com^\n||bmlmedia.com^\n||bmmetrix.com^\n||bonitrust.de^\n||bonuscounter.de^\n||bookforest.biz^\n||boomerang.com.au^\n||boomtrain.com^\n||botscanner.com^\n||botsvisit.com^\n||bounceexchange.com^\n||bouncex.com^\n||br.phorm.com^\n||brat-online.ro^\n||brcdn.com^\n||bridgevine.com^\n||brightedge.com^\n||brilig.com^\n||bronto.com^\n||browser-statistik.de^\n||browser-update.org^\n||brsrvr.com^\n||bstk.co^\n||bstn-14-ma.com^\n||bt.phorm.com^\n||btbuckets.com^\n||btstatic.com^\n||btttag.com^\n||bubblestat.com^\n||bugherd.com^\n||bugsnag.com^\n||burstbeacon.com^\n||burt.io^\n||button.blogs.yandex.\n||bux1le001.com^\n||buzzdeck.com^\n||bytemgdd.com^\n||c-o-u-n-t.com^\n||c-webstats.de^\n||c.adroll.com^\n||c.bigmir.net\n||c.hit.ua^\n||c.imrk.net^\n||c.msn.com^\n||c.newsinc.com^\n||c.opinion.com.ua^\n||c.tbex.ru^\n||c1exchange.com^\n||c3metrics.com^\n||c3tag.com^\n||c4tracking01.com^\n||c4tw.net^\n||cache.am^\n||cache.fm^\n||cadreon.com^\n||call-tracking.co.uk^\n||callingjustified.com^\n||callisto.fm^\n||callmeasurement.com^\n||callrail.com^\n||calltrackingmetrics.com^\n||calltracks.com^\n||campaigncog.com^\n||canalstat.com^\n||canddi.com^\n||canopylabs.com^\n||caphyon-analytics.com^\n||captify.co.uk^\n||captivate.ai^\n||capturly.com^\n||cashburners.com^\n||cashcount.com^\n||castelein.nu^\n||cc.swiftype.com^\n||cc.zeit.de^\n||cccpmo.com^\n||cdnstats-a.akamaihd.net^\n||cedexis.com^\n||cedexis.net^\n||celebros-analytics.com^\n||centraltag.com^\n||certifica.com^\n||cetrk.com^\n||cftrack.com^\n||chart.dk^\n||chartaca.com^\n||chartbeat.com^\n||chartbeat.net^\n||checkeffect.at^\n||checkmypr.net^\n||checkstat.nl^\n||cheezburger-analytics.com^\n||christiantop1000.com^\n||christmalicious.com^\n||chrumedia.com^\n||circle.am^\n||circular-counters.com^\n||cityua.net^\n||clarifyingquack.com^\n||claritytag.com^\n||cleananalytics.com^\n||clearviewstats.com^\n||cleveritics.com^\n||clevi.com^\n||click-linking.com^\n||click-url.com^\n||click2meter.com^\n||click4assistance.co.uk^\n||clickable.net^\n||clickaider.com^\n||clickalyzer.com^\n||clickclick.net^\n||clickcloud.info^\n||clickconversion.net^\n||clickdensity.com^\n||clickdimensions.com^\n||clickening.com^\n||clickforensics.com^\n||clicki.cn^\n||clickigniter.io^\n||clickmanage.com^\n||clickmap.ch^\n||clickmeter.com^\n||clickpathmedia.com^\n||clickprotector.com^\n||clickreport.com^\n||clicks.hurriyet.com.tr^\n||clicksagent.com^\n||clicksen.se^\n||clickshift.com^\n||clickstream.co.za^\n||clicktale.net^\n||clicktrack1.com^\n||clicktracks.com^\n||clickzs.com^\n||clickzzs.nl^\n||clixcount.com^\n||clixpy.com^\n||cloud-exploration.com^\n||cloudtracer101.com^\n||clubcollector.com^\n||clustrmaps.com^\n||cmcintra.net^\n||cmcore.com^\n||cmmeglobal.com^\n||cmptch.com^\n||cms.lv^\n||cnetcontentsolutions.com^\n||cnstats.ru^\n||cnt.cerber.rambler.ru^\n||cnt.inforotor.net^\n||cnt.logoslovo.ru^\n||cnt.nov.ru^\n||cnt.rambler.ru^\n||cnt.ramlife.ru^\n||cnt1.net^\n||cntcerber.rambler.ru^\n||cnxweb.com^\n||cnzz.com^\n||cnzz.net^\n||codata.ru^\n||cogmatch.net^\n||cognitivematch.com^\n||collarity.com^\n||collect.igodigital.com^\n||collserve.com^\n||comagic.ru^\n||commander1.com^\n||company-target.com^\n||compteur-gratuit.org^\n||compteur.cc^\n||compteur.com^\n||comradepony.com^\n||confirmational.com^\n||confirmit.com^\n||contactmonkey.com^\n||contadordevisitas.es^\n||contadorgratis.com^\n||contadorgratis.es^\n||contadorweb.com^\n||contatoreaccessi.com^\n||contemporaryceremonies.ca^\n||contentexchange.me^\n||contentinsights.com^\n||contentspread.net^\n||contextly.com^\n||continue.com^\n||convergetrack.com^\n||conversionlogic.net^\n||conversionly.com^\n||conversionruler.com^\n||convertexperiments.com^\n||convertglobal.com^\n||convertmarketing.net^\n||convertro.com^\n||cookie.fuel451.com^\n||cooladata.com^\n||copacast.net^\n||copperegg.com^\n||core-cen-54.com^\n||coremetrics.com^\n||coremotives.com^\n||cormce.com^\n||cosmi.io^\n||cost1action.com^\n||count-cnt.ru^\n||count.fr^\n||count.rbc.ru^\n||count24.de^\n||countar.de^\n||countby.com^\n||counted.at^\n||counter-city.de^\n||counter-go.de^\n||counter-gratis.com^\n||counter-kostenlos.info^\n||counter-kostenlos.net^\n||counter-pagerank.de^\n||counter-treff.de^\n||counter.all.biz^\n||counter.de^\n||counter.gd^\n||counter.nn.ru^\n||counter.ok.ee^\n||counter.opinion.com.ua^\n||counter.rambler.ru^\n||counter.scribblelive.com^\n||counter.scribblelive.net^\n||counter.top.ge^\n||counter.top.kg^\n||counter.ukr.net^\n||counter160.com^\n||counter27.ch^\n||counter4all.de^\n||counterbot.com^\n||countercentral.com^\n||countercity.de^\n||countercity.net^\n||countergeo.com^\n||counterland.com^\n||counterlevel.de^\n||counteronline.de^\n||counters4u.com^\n||counterseite.de^\n||counterserver.de^\n||counterservis.com^\n||countersforlife.com^\n||counterstation.de^\n||counterstatistik.de^\n||countertracker.com^\n||counterviews.net^\n||counthis.com^\n||counti.de^\n||countimo.de^\n||counting4free.com^\n||countingbiz.info^\n||countino.de^\n||countit.ch^\n||countnow.de^\n||counto.de^\n||countok.de^\n||countomat.com^\n||countus.fr^\n||countyou.de^\n||countz.com^\n||cpcmanager.com^\n||cpx.golem.de^\n||cpx.to^\n||cqcounter.com^\n||cquotient.com^\n||craftkeys.com^\n||craktraffic.com^\n||crashfootwork.com^\n||crazyegg.com^\n||creativecdn.com^\n||criteo.com^\n||criteo.net^\n||crmmetrix.fr^\n||crmmetrixwris.com^\n||crosspixel.net^\n||crosswalkmail.com^\n||crowdscience.com^\n||crowdskout.com^\n||crowdtwist.com^\n||crsspxl.com^\n||crwdcntrl.net^\n||csdata1.com^\n||csi-tracking.com^\n||ctnsnet.com^\n||cttracking02.com^\n||cuntador.com^\n||curalate.com^\n||customer.io^\n||customerconversio.com^\n||customerdiscoverytrack.com^\n||cxense.com^\n||cxt.ms/\n||cxt.ms^\n||cy-pr.com/\n||cya1t.net^\n||cya2.net^\n||cybermonitor.com^\n||cypr.com/\n||cytoclause.com^\n||d169bbxks24g2u.cloudfront.net^\n||d1cdnlzf6usiff.cloudfront.net^\n||d1cerpgff739r9.cloudfront.net^\n||d1clfvuu2240eh.cloudfront.net^\n||d1clufhfw8sswh.cloudfront.net^\n||d1fc8wv8zag5ca.cloudfront.net^\n||d1gp8joe0evc8s.cloudfront.net^\n||d1l6p2sc9645hc.cloudfront.net^\n||d1lm7kd3bd3yo9.cloudfront.net^\n||d1nh2vjpqpfnin.cloudfront.net^\n||d1qpxk1wfeh8v1.cloudfront.net^\n||d1r27qvpjiaqj3.cloudfront.net^\n||d1rgnfh960lz2b.cloudfront.net^\n||d1ros97qkrwjf5.cloudfront.net^\n||d1wscoizcbxzhp.cloudfront.net^\n||d1yu5hbtu8mng9.cloudfront.net^\n||d1z2jf7jlzjs58.cloudfront.net^\n||d21o24qxwf7uku.cloudfront.net^\n||d23p9gffjvre9v.cloudfront.net^\n||d28g9g3vb08y70.cloudfront.net^\n||d2gfdmu30u15x7.cloudfront.net^\n||d2gfi8ctn6kki7.cloudfront.net^\n||d2kmrmwhq7wkvs.cloudfront.net^\n||d2lv4zbk7v5f93.cloudfront.net^\n||d2nxi61n77zqpl.cloudfront.net^\n||d2oh4tlt9mrke9.cloudfront.net^\n||d2pxb4n3f9klsc.cloudfront.net^\n||d2ry9vue95px0b.cloudfront.net^\n||d2so4705rl485y.cloudfront.net^\n||d2tgfbvjf3q6hn.cloudfront.net^\n||d2xgf76oeu9pbh.cloudfront.net^\n||d303e3cdddb4ded4b6ff495a7b496ed5.s3.amazonaws.com^\n||d3135glefggiep.cloudfront.net^\n||d31qbv1cthcecs.cloudfront.net^\n||d33im0067v833a.cloudfront.net^\n||d34ko97cxuv4p7.cloudfront.net^\n||d36lvucg9kzous.cloudfront.net^\n||d36wtdrdo22bqa.cloudfront.net^\n||d396ihyrqc81w.cloudfront.net^\n||d3a2okcloueqyx.cloudfront.net^\n||d3avqv6zaxegeu.cloudfront.net^\n||d3c3cq33003psk.cloudfront.net^\n||d3cxv97fi8q177.cloudfront.net^\n||d3ezl4ajpp2zy8.cloudfront.net^\n||d3h1v5cflrhzi4.cloudfront.net^\n||d3hr5gm0wlxm5h.cloudfront.net^\n||d3l3lkinz3f56t.cloudfront.net^\n||d3ojzyhbolvoi5.cloudfront.net^\n||d3qxwzhswv93jk.cloudfront.net^\n||d3s7ggfq1s6jlj.cloudfront.net^\n||d4ax0r5detcsu.cloudfront.net^\n||d5i9o0tpq9sa1.cloudfront.net^\n||d6jkenny8w8yo.cloudfront.net^\n||d81mfvml8p5ml.cloudfront.net^\n||d8rk54i4mohrb.cloudfront.net^\n||d9lq0o81skkdj.cloudfront.net^\n||dacounter.com^\n||dadi.technology^\n||dailycaller-alerts.com^\n||dailymotion-ams.gravityrd-services.com^\n||dapxl.com^\n||daq0d0aotgq0f.cloudfront.net^\n||dashboard.io^\n||data-analytics.jp^\n||databrain.com^\n||datacaciques.com^\n||datacollect*.abtasty.com^\n||datafeedfile.com^\n||datam.com^\n||datamaster.com.cn^\n||dataperforma.com^\n||datarating.com^\n||datvantage.com^\n||daylife-analytics.com^\n||daylogs.com^\n||dc-storm.com^\n||dc.tremormedia.com^\n||dc8xl0ndzn2cb.cloudfront.net^\n||de17a.com^\n||decdna.net^\n||decibelinsight.net^\n||decideinteractive.com^\n||declaredthoughtfulness.co^\n||deepattention.com^\n||dejavu.mlapps.com^\n||demandbase.com^\n||demdex.net^\n||deqwas.net^\n||desert.ru^\n||device-metrics-us*.amazon.com^\n||device9.com^\n||dgmsearchlab.com^\n||dhmtracking.co.za^\n||dialogtech.com^\n||did-it.com^\n||didit.com^\n||die-rankliste.com^\n||diffusion-tracker.com^\n||digdeepdigital.com.au^\n||digidip.net^\n||digitaloptout.com^\n||digitaltarget.ru^\n||dignow.org^\n||dimestore.com^\n||dimml.io^\n||dinkstat.com^\n||directcounter.de^\n||directrdr.com^\n||directrix.ru^\n||discover-path.com^\n||discovertrail.net^\n||displaymarketplace.com^\n||distralytics.com^\n||divolution.com^\n||djers.com^\n||djibeacon.dowjoneson.com^\n||dk-statistik.de^\n||dlrowehtfodne.com^\n||dmanalytics1.com^\n||dmclick.cn^\n||dmd53.com^\n||dmpxs.com^\n||dmtracker.com^\n||dmtracking2.alibaba.com^\n||dmtry.com^\n||dnn506yrbagrg.cloudfront.net^\n||doclix.com^\n||dominocounter.net^\n||domodomain.com^\n||dotmetrics.net^\n||downture.in^\n||dpbolvw.net^\n||dps-reach.com^\n||dreamcounter.de^\n||ds-aksb-a.akamaihd.net^\n||dsmmadvantage.com^\n||dsparking.com^\n||dsply.com^\n||dstrack2.info^\n||dtc-v6t.com^\n||dti-ranker.com^\n||dtscout.com^\n||dtxtngytz5im1.cloudfront.net^\n||dummy-domain-do-not-change.com^\n||durocount.com^\n||dv0.info^\n||dw.com.com^\n||dwin1.com^\n||dwin2.com^\n||dyntrk.com^\n||e-kaiseki.com^\n||e-pagerank.net^\n||e-referrer.com^\n||e-webtrack.net^\n||e-zeeinternet.com^\n||eanalyzer.de^\n||earnitup.com^\n||easy-hit-counters.com^\n||easy.lv/\n||easycounter.com^\n||easyhitcounters.com^\n||easyresearch.se^\n||easytarget.ru^\n||easytracking.de^\n||ebtrk1.com^\n||ec-track.com^\n||eclampsialemontree.net^\n||ecn5.com^\n||eco-tag.jp^\n||ecommstats.com^\n||econda-monitor.de^\n||ecustomeropinions.com^\n||edge.quantserve.com^\n||edgeadx.net^\n||edococounter.de^\n||edt02.net^\n||edtp.de^\n||ekmpinpoint.co.uk^\n||ekmpinpoint.com^\n||ela-3-tnk.com^\n||electusdigital.com^\n||elite-s001.com^\n||elitics.com^\n||eloqua.com^\n||email-match.com^\n||emailretargeting.com^\n||embeddedanalytics.com^\n||emediatrack.com^\n||emetriq.de^\n||emjcd.com^\n||emltrk.com^\n||emsecure.net^\n||en25.com^\n||enecto.com^\n||enectoanalytics.com^\n||engageya.com^\n||engine212.com^\n||engine64.com^\n||enhance.com^\n||enquisite.com^\n||ensighten.com^\n||enter-system.com^\n||enticelabs.com^\n||eperfectdata.com^\n||epilot.com^\n||epiodata.com^\n||epitrack.com^\n||eproof.com^\n||eps-analyzer.de^\n||er.mmi.bemobile.ua^\n||ereportz.com^\n||eresmas.net^\n||erkaseriilan.com.tr^\n||erotikcounter.org^\n||erotop.lv^\n||err.rambler.ru^\n||esearchvision.com^\n||esm1.net^\n||esomniture.com^\n||estadisticasgratis.com^\n||estadisticasgratis.es^\n||estara.com^\n||estat.com^\n||estrack.net^\n||etahub.com^\n||etherealhakai.com^\n||ethnio.com^\n||etracker.com^\n||etracker.de^\n||etracking24.de^\n||etrafficcounter.com^\n||etrafficstats.com^\n||etrigue.com^\n||etyper.com^\n||eu-survey.com^\n||eulerian.net^\n||euleriancdn.net^\n||eum-appdynamics.com^\n||euro-pr.eu^\n||euroads.dk^\n||eurocounter.com^\n||europagerank.com^\n||europuls.eu^\n||europuls.net^\n||evanetpro.com^\n||event.condenastdigital.com^\n||event.trove.com^\n||events.artirix.com^\n||everestjs.net^\n||everesttech.net^\n||evergage.com^\n||evisitanalyst.com^\n||evisitcs.com^\n||evisitcs2.com^\n||evolvemediametrics.com^\n||evyy.net^\n||ewebanalytics.com^\n||ewebcounter.com^\n||exactag.com^\n||exacttarget.com^\n||exapxl.de^\n||exclusiveclicks.com^\n||exelator.com^\n||exitmonitor.com^\n||exmarkt.de^\n||exovueplatform.com^\n||explore-123.com^\n||exposebox.com^\n||extole.com^\n||extreme-dm.com^\n||eyein.com^\n||eyeota.net^\n||ezakus.net^\n||ezec.co.uk^\n||ezytrack.com^\n||fabricww.com^\n||factortg.com^\n||faibl.org^\n||fairfaxmedia.sharedcount.com^\n||fallingfalcon.com^\n||fanplayr.com^\n||farmer.wego.com^\n||fast-thinking.co.uk^\n||fastanalytic.com^\n||fastcounter.de^\n||fastly-analytics.com^\n||fastonlineusers.com^\n||faststart.ru^\n||fastwebcounter.com^\n||fathomseo.com^\n||fcs.ovh^\n||feedcat.net^\n||feedjit.com^\n||feedperfect.com^\n||ferank.fr^\n||fetchback.com^\n||filitrac.com^\n||finalid.com^\n||find-ip-address.org^\n||finderlocator.com^\n||fishhoo.com^\n||fixcounter.com^\n||fixwap.net/\n||flagcounter.com^\n||flash-counter.com^\n||flash-stat.com^\n||flashadengine.com^\n||flashgamestats.com^\n||flcounter.com^\n||flix360.com^\n||flixcar.com^\n||flixfacts.co.uk^\n||flixfacts.com^\n||flixsyndication.net^\n||flowstats.net^\n||fluctuo.com^\n||fluencymedia.com^\n||fluidsurveys.com^\n||flx1.com^\n||flxpxl.com^\n||flyingpt.com^\n||followercounter.com^\n||footprintdns.com^\n||footprintlive.com^\n||force24.co.uk^\n||forensics1000.com^\n||foreseeresults.com^\n||forkcdn.com^\n||formalyzer.com^\n||formisimo.com^\n||forter.com^\n||foundry42.com^\n||fout.jp^\n||fpctraffic2.com^\n||fprnt.com^\n||fqtag.com^\n||free-counter.co.uk^\n||free-counter.com^\n||free-counters.co.uk^\n||free-counters.net^\n||free-website-hit-counters.com^\n||free-website-statistics.com^\n||freebloghitcounter.com^\n||freecounter.it^\n||freecountercode.com^\n||freecounterstat.com^\n||freegeoip.net^\n||freehitscounter.org^\n||freelogs.com^\n||freeonlineusers.com^\n||freesitemapgenerator.com^\n||freestat.ws^\n||freestats.biz^\n||freestats.com^\n||freestats.me^\n||freestats.net^\n||freestats.org^\n||freestats.tk^\n||freestats.tv^\n||freestats.ws^\n||freetracker.biz^\n||freetrafficsystem.com^\n||freeusersonline.com^\n||freeweblogger.com^\n||freihit.de^\n||fremaks.net^\n||frescoerspica.com^\n||freshcounter.com^\n||freshplum.com^\n||friendbuy.com^\n||frog.wix.com^\n||frosmo.com^\n||fruitflan.com^\n||fshka.com^\n||ftbpro.com^\n||fueldeck.com^\n||fugetech.com^\n||fun-hits.com^\n||funn.graphiq.com^\n||funneld.com^\n||funstage.com^\n||fuse-data.com^\n||fusestats.com^\n||fuziontech.net^\n||fyreball.com^\n||gacela.eu^\n||gallupnet.fi^\n||gaug.es/\n||gaug.es^\n||gbotvisit.com^\n||geistm.com^\n||gemius.pl^\n||gemtrackers.com^\n||generaltracking.de^\n||genieesspv.jp^\n||geo.query.yahoo.com^\n||geo.xcel.io^\n||geo.yahoo.com^\n||geobytes.com^\n||geocompteur.com^\n||geocontatore.com^\n||geolocation.performgroup.com^\n||geoplugin.net^\n||getbackstory.com^\n||getclicky.com^\n||getconversion.net^\n||getcounter.de^\n||getetafun.info^\n||getfreebacklinks.com^\n||getfreebl.com^\n||getsidecar.com^\n||getsmartcontent.com^\n||getstatistics.se^\n||gez.io^\n||gezaehlt.de^\n||gezinti.com^\n||gigcount.com^\n||gixmo.dk^\n||glanceguide.com^\n||glbtracker.com^\n||globalviptraffic.com^\n||globase.com^\n||globel.co.uk^\n||globetrackr.com^\n||gmodmp.jp^\n||go-mpulse.net^\n||goaltraffic.com^\n||godhat.com^\n||goingup.com^\n||goldstats.com^\n||goneviral.com^\n||goodcounter.org^\n||google-pagerank.net^\n||google-pr7.de^\n||google-rank.org^\n||googlerank.info^\n||gooo.al/\n||gooo.al^\n||gopjn.com^\n||gosquared.com^\n||gostats.com^\n||gostats.de^\n||gostats.pl^\n||gostats.ro^\n||gostats.ru^\n||gostats.vn^\n||govmetric.com^\n||grapheffect.com^\n||graphinsider.com^\n||gratis-besucherzaehler.de^\n||gratis-counter-gratis.de^\n||gratisbacklink.de^\n||gravity4.com^\n||greatviews.de^\n||grepdata.com^\n||grfz.de^\n||gridsum.com^\n||grmtech.net^\n||groundspeak.com^\n||gscounters.*.gigya.com^\n||gscounters.gigya.com^\n||gsecondscreen.com^\n||gsimedia.net^\n||gsspat.jp^\n||gssprt.jp^\n||gstats.cn^\n||gtcslt-di2.com^\n||gtop.ro^\n||gtopstats.com^\n||guanoo.net^\n||guardwork.info^\n||guruquicks.net^\n||gvisit.com^\n||h4k5.com^\n||halldata.com^\n||halstats.com^\n||haraju.co^\n||haveamint.com^\n||haymarket.com^\n||hc.uralweb.ru^\n||heapanalytics.com^\n||heatmap.it^\n||hellosherpa.com^\n||hentaicounter.com^\n||hetchi.com^\n||heystaks.com^\n||hiconversion.com^\n||hiddencounter.de^\n||higherengine.com^\n||highmetrics.com^\n||hiperstat.com^\n||hirmatrix.hu^\n||histats.com^\n||hit-counter-download.com^\n||hit-counter.info^\n||hit-counters.net^\n||hit-counts.com^\n||hit-parade.com^\n||hit.copesa.cl^\n||hit100.ro^\n||hit2map.com^\n||hitbox.com^\n||hitcount.dk^\n||hitcounter.ru\n||hitcountersonline.com^\n||hitcounterstats.com^\n||hitfarm.com^\n||hitgelsin.com^\n||hitgraph.jp^\n||hitmaster.de^\n||hitmatic.com^\n||hitmaze-counters.net^\n||hitmeter.ru^\n||hitmir.ru^\n||hits.e.cl^\n||hits.io^\n||hits.theguardian.com^\n||hits.top.lv^\n||hits2u.com^\n||hitslink.com^\n||hitslog.com^\n||hitsniffer.com^\n||hitsprocessor.com^\n||hitstatus.com^\n||hittail.com^\n||hittracker.com^\n||hitwake.com^\n||hitwebcounter.com^\n||hlserve.com^\n||hm.baidu.com^\n||homechader.com^\n||hopurl.org^\n||host-tracker.com^\n||hostip.info^\n||hoststats.info^\n||hot-count.com^\n||hotcounter.de^\n||hotjar.com^\n||hotlog.ru^\n||hotrank.com.tw^\n||hotstats.gr^\n||hqhrt.com^\n||hs-analytics.net^\n||hsdn.org^\n||humanclick.com^\n||hung.ch^\n||hunt-leads.com^\n||hurra.com^\n||hurterkranach.net^\n||hwpub.com^\n||hxtrack.com^\n||hyfntrak.com^\n||hyperactivate.com^\n||hypestat.com^\n||i-stats.com^\n||iadvize.com^\n||ib-ibi.com^\n||ibillboard.com^\n||ibpxl.com^\n||ibpxl.net^\n||ic-live.com^\n||ichnaea.netflix.com^\n||iclive.com^\n||ics0.com^\n||icstats.nl^\n||id-visitors.com^\n||ideoclick.com^\n||idio.co^\n||idot.cz/\n||idtargeting.com^\n||iesnare.com^\n||ifactz.com^\n||ig.fp.oix.net^\n||igaming.biz^\n||iivt.com^\n||ijncw.tv^\n||iljmp.com^\n||ilk10.az^\n||illumenix.com^\n||ilogbox.com^\n||imanginatium.com^\n||imcht.net^\n||imetrix.it^\n||img.opentracker.net\n||immanalytics.com^\n||impcounter.com^\n||imrtrack.com^\n||inaudium.com^\n||inboxtag.com^\n||incentivesnetwork.net^\n||index.ru^\n||indexstats.com^\n||indextools.com^\n||indicia.com^\n||individuad.net^\n||ineedhits.com^\n||inet-tracker.de^\n||inferclick.com^\n||infinigraph.com^\n||infinity-tracking.net^\n||inflectionpointmedia.com^\n||influid.co^\n||infocollect.dk^\n||informer.yandex.ru^\n||infostroy.nnov.ru^\n||inimbus.com.au^\n||innovateads.com^\n||inphonic.com^\n||inpref.com^\n||inpwrd.com^\n||inside-graph.com^\n||insightera.com^\n||insightgrit.com^\n||insigit.com^\n||insitemetrics.com^\n||inspectlet.com^\n||inspsearchapi.com^\n||instadia.net^\n||instore.biz^\n||intarget.ru^\n||intelevance.com^\n||intelimet.com^\n||intelli-direct.com^\n||intelli-tracker.com^\n||intelliad-tracking.com^\n||intelliad.de^\n||intelligencefocus.com^\n||interaktiv-net.de^\n||interceptum.com^\n||intergid.ru^\n||intergient.com^\n||interhits.de^\n||intermundomedia.com^\n||internetmap.info^\n||interstateanalytics.com^\n||intervigil.com^\n||intrastats.com^\n||invitemedia.com^\n||invoc.us^\n||invoca.net^\n||invoca.solutions^\n||ioam.de/?\n||mqs.ioam.de^\n||iol.io^\n||ip-api.com^\n||ip-label.net^\n||ip2location.com^\n||ip2map.com^\n||ip2phrase.com^\n||ipaddresslabs.com^\n||ipcatch.com^\n||ipcount.net^\n||ipcounter.de^\n||ipcounter.net^\n||iperceptions.com^\n||ipfingerprint.com^\n||ipfrom.com^\n||ipify.org^\n||ipinfo.info^\n||ipinfodb.com^\n||ipinyou.com.cn^\n||iplocationtools.com^\n||ipro.com^\n||ipstat.com^\n||iptrack.biz^\n||ipv6monitoring.eu^\n||iqfp1.com^\n||iraiser.eu^\n||irelandmetrix.ie^\n||irs09.com^\n||iryazan.ru^\n||is.sabah.com.tr^\n||ist-track.com^\n||istat24.com^\n||istats.nl^\n||istrack.com^\n||italianadirectory.com^\n||itop.cz/\n||itrackerpro.com^\n||ivcbrasil.org.br^\n||ivwbox.de^\n||iwebtrack.com^\n||iwstats.com^\n||ixiaa.com^\n||iyi.net^\n||izea.com^\n||izearanks.com^\n||japanmetrix.jp^\n||jdoqocy.com^\n||jetcounter.ru^\n||jiankongbao.com^\n||jimdo-stats.com^\n||jirafe.com^\n||js.medi-8.net^\n||jscounter.com^\n||jsid.info^\n||jsonip.com^\n||jstracker.com^\n||jump-time.net^\n||jumptime.com^\n||justuno.com^\n||jwmstats.com^\n||jwpltx.com^\n||kameleoon.com^\n||kampyle.com^\n||kavijaseuranta.fi^\n||keen.io^\n||keyade.com^\n||keymetric.net^\n||keytrack.de^\n||keywee.co^\n||keywordmax.com^\n||keywordstrategy.org^\n||kieden.com^\n||killerwebstats.com^\n||kissmetrics.com^\n||kisstesting.com^\n||kitbit.net^\n||kitcode.net^\n||kka.agitos.de^\n||klamm-counter.de^\n||klert.com^\n||klldabck.com^\n||kmindex.ru^\n||knowledgevine.net^\n||komtrack.com^\n||kono-research.de^\n||kontagent.net^\n||kopsil.com^\n||kostenlose-counter.com^\n||kqzyfj.com^\n||kr.phorm.com^\n||krxd.net^\n||ksyrium0014.com^\n||kupona.de^\n||l2.visiblemeasures.com^\n||landingpg.com^\n||laserstat.com^\n||lb.secureweb24.net^\n||lct.salesforce.com^\n||lddt.de^\n||lead-123.com^\n||lead-converter.com^\n||lead-or-call.ru^\n||lead-tracking.biz^\n||leadforce1.com^\n||leadforensics.com^\n||leadformix.com^\n||leadid.com^\n||leadin.com^\n||leadintelligence.co.uk^\n||leadium.com^\n||leadlife.com^\n||leadmanagerfx.com^\n||leadsius.com^\n||legenhit.com^\n||legolas-media.com^\n||les-experts.com^\n||leserservice-tracking.de^\n||letterboxtrail.com^\n||levexis.com^\n||lexity.com^\n||lfov.net^\n||liadm.com^\n||libstat.com^\n||lightboxcdn.com^\n||lijit.com^\n||linezing.com^\n||link-empfehlen24.de^\n||link-smart.com^\n||linkconnector.com^\n||linkpulse.com^\n||linktausch-pagerank.de^\n||linktausch.li^\n||linkxchanger.com^\n||listrakbi.com^\n||listtop.ru^\n||live.ec2.cxo.name^\n||livecount.fr^\n||livecounter.dk^\n||livehit.net^\n||liverank.org^\n||livestat.com^\n||livestats.fr^\n||livewebstats.dk^\n||lloogg.com^\n||load.sumome.com^\n||localytics.com^\n||lockview.cn^\n||log.go.com^\n||log.invodo.com^\n||log.krs-ix.ru^\n||log.outbrain.com^\n||log.pinterest.com^\n||logaholic.com^\n||logcounter.com^\n||logdy.com^\n||logentries.com^\n||loger.ru^\n||logger.co.kr^\n||logger.su^\n||logger.virgul.com^\n||loggingservices.tribune.com^\n||loggly.com^\n||lognormal.net^\n||logua.com^\n||logxp.ru^\n||logz.ru/\n||lookery.com^\n||lookit.cz^\n||lookmy.info^\n||loopfuse.net^\n||lopley.com^\n||losecounter.de^\n||losstrack.com^\n||loxodo-analytics.ext.nile.works^\n||loxodo-ct.ext.nile.works^\n||lp4.io^\n||lpbeta.com^\n||lporirxe.com^\n||lptracker.ru^\n||lsfinteractive.com^\n||lstats.qip.ru^\n||lucidel.com^\n||luckyorange.com^\n||lugansk-info.ru^\n||lumatag.co.uk^\n||luminate.com^\n||lxtrack.com^\n||lymantriacypresdoctrine.biz^\n||lynn.zgsemi.cn^\n||lypn.com^\n||lypn.net^\n||lytics.io^\n||lytiks.com^\n||m-brain.fi^\n||m-pathy.com^\n||m.trb.com^\n||m1ll1c4n0.com^\n||m6r.eu^\n||magiq.com^\n||magnetmail1.net^\n||magnify360.com^\n||mailstat.us^\n||maploco.com^\n||mapmyuser.com^\n||marinsm.com^\n||market015.com^\n||market2lead.com^\n||marketing-page.de^\n||marketingautomation.services^\n||marketizator.com^\n||marketo.net^\n||marktest.pt^\n||martianstats.com^\n||masterstats.com^\n||matheranalytics.com^\n||mathtag.com^\n||maxtracker.net^\n||maxymiser.com^\n||maxymiser.net^\n||mb4a.com^\n||mbid.io^\n||mbotvisit.com^\n||mbsy.co^\n||mbww.com^\n||mcount.ru^\n||md-ia.info^\n||mdotlabs.com^\n||measure.ly^\n||measuremap.com^\n||measurementapi.com^\n||mediaarmor.com^\n||mediaforgews.com^\n||mediagauge.com^\n||mediaindex.ee^\n||mediametrics.ru^\n||mediaplan.ru^\n||mediaseeding.com^\n||mediego.com^\n||meetrics.net^\n||mega-stats.com^\n||megastat.net^\n||megatopsites.com^\n||melatstat.com^\n||memecounter.com^\n||mengis-linden.org^\n||mercadoclics.com^\n||mercent.com^\n||met.vgwort.de^\n||metakeyproducer.com^\n||metalyzer.com^\n||meteorsolutions.com^\n||meter-svc.nytimes.com^\n||metrics-api.librato.com^\n||metrics.brightcove.com^\n||metrics.cnn.com^\n||metrics.foxnews.com^\n||metrics.washingtonpost.com^\n||metricsdirect.com^\n||metrigo.com^\n||metriweb.be^\n||mezzobit.com^\n||mialbj6.com^\n||micodigo.com^\n||microcounter.de^\n||midas-i.com^\n||midkotatraffic.net^\n||millioncounter.com^\n||minewhat.com^\n||mitmeisseln.de^\n||mkt3261.com^\n||mkt51.net^\n||mkt941.com^\n||mktoresp.com^\n||ml314.com^\n||mlclick.com^\n||mletracker.com^\n||mlno6.com^\n||mlstat.com^\n||mm7.net^\n||mmccint.com^\n||mmetrix.mobi^\n||mmi-agency.com^\n||mmstat.com^\n||mmtro.com^\n||mobalyzer.net^\n||mobify.com^\n||mobtop.ru^\n||mobylog.jp^\n||mochibot.com^\n||modernus.is^\n||mokuz.ru^\n||monetate.net^\n||mongoosemetrics.com^\n||monitis.com^\n||monitor.phorm.com^\n||monitus.net^\n||moreusers.info^\n||morevisits.info^\n||motorpresse-statistik.de^\n||motrixi.com^\n||mouse3k.com^\n||mouseflow.com^\n||mousestats.com^\n||mousetrace.com^\n||movable-ink-6710.com^\n||mplxtms.com^\n||mpstat.us^\n||mpwe.net^\n||mr-rank.de^\n||msg.71.am^\n||msgapp.com^\n||msgtag.com^\n||mstat.acestream.net^\n||mstop.ru/\n||mstracker.net^\n||mtp.spaces.ru^\n||mtrack.nl^\n||mtracking.com^\n||mtrics.cdc.gov^\n||mucocutaneousmyrmecophaga.com^\n||musiccounter.ru^\n||mvilivestats.com^\n||mvtracker.com^\n||mx03.phorm.com^\n||mxcdn.net^\n||mxpnl.com^\n||mxptint.net^\n||my-ranking.de^\n||my-stats.info^\n||myaffiliateprogram.com^\n||myaudience.de^\n||mybloglog.com^\n||mycounter.com.ua^\n||mycounter.ua^\n||myfastcounter.com^\n||mynewcounter.com^\n||myntelligence.com^\n||myomnistar.com^\n||myonlineanalytics.com^\n||mypagerank.net^\n||myreferer.com^\n||myroitracking.com^\n||myseostats.com^\n||mysitetraffic.net^\n||mysocialpixel.com^\n||mystat-in.net^\n||mystat.hu^\n||mystat.it^\n||mystats.nl^\n||mysumo.de^\n||mytictac.com^\n||mytop.az^\n||myusersonline.com^\n||myvisualiq.net^\n||mywebstats.com.au^\n||mywebstats.org^\n||naayna.com^\n||naj.sk^\n||nakanohito.jp^\n||nalook.com^\n||nanovisor.io^\n||native.ai^\n||natpal.com^\n||naturaltracking.com^\n||navdmp.com^\n||navegg.com^\n||navilytics.com^\n||navrcholu.cz^\n||naytev.com^\n||ncom.dk/\n||ndg.io^\n||neatstats.com^\n||nedstat.com^\n||nedstat.net^\n||nedstatbasic.net^\n||nedstatpro.net^\n||neki.org^\n||nestedmedia.com^\n||net-filter.com^\n||netagent.cz^\n||netapplications.com^\n||netbiscuits.net^\n||netclickstats.com^\n||netcounter.de^\n||netcustos.com^\n||netdebit-counter.de^\n||netflame.cc^\n||netgraviton.net^\n||netminers.dk^\n||netmining.com^\n||netmng.com^\n||netmonitor.fi^\n||netratings.com^\n||netstats.dk^\n||netupdater.info^\n||netzaehler.de^\n||netzstat.ch^\n||newpoints.info^\n||js-agent.newrelic.com^\n||beacon-1.newrelic.com^\n||newsanalytics.com.au^\n||newscurve.com^\n||newstatscounter.info^\n||newtrackmedia.com^\n||nexeps.com^\n||nextstat.com^\n||ng.virgul.com^\n||ngacm.com^\n||ngastatic.com^\n||ngmco.net^\n||nicewii.com^\n||niftymaps.com^\n||nik.io^\n||ninestats.com^\n||noowho.com^\n||nordicresearch.com^\n||northclick-statistiken.de^\n||notifyvisitors.com^\n||nowinteract.com^\n||npario-inc.net^\n||nprove.com^\n||nr-data.net^\n||nr7.us^\n||ns1.phorm.com^\n||ns1p.net^\n||ns2.phorm.com^\n||nstracking.com^\n||ntlab.org^\n||ntvk1.ru^\n||nuconomy.com^\n||nudatasecurity.com^\n||nuggad.net^\n||numerino.cz^\n||nyctrl32.com^\n||nytlog.com^\n||oadz.com^\n||observare.de^\n||observerapp.com^\n||octavius.rocks^\n||od.visiblemeasures.com^\n||odb.outbrain.com^\n||odesaconflate.com^\n||odoscope.com^\n||oewabox.at^\n||offermatica.com^\n||offerpoint.net^\n||offerstrategy.com^\n||ogt.jp^\n||ohmystats.com^\n||oidah.com^\n||oimg.nbcuni.com^\n||oix-stage.net^\n||oix.com^\n||oix.phorm.com^\n||oixcrv-lab.net^\n||oixcrv-stage.net^\n||oixcrv.net^\n||oixpre-stage.net^\n||oixpre.net^\n||oixssp-lab.net^\n||oixssp.net^\n||ojrq.net^\n||okt.to^\n||oktopost.com^\n||om.cbsi.com^\n||om.dowjoneson.com^\n||omarsys.com^\n||omeda.com^\n||ometria.com^\n||omgpm.com^\n||omguk.com^\n||omkt.co^\n||on-line.lv^\n||onefeed.co.uk^\n||onelink-translations.com^\n||onestat.com^\n||onetag-sys.com^\n||ongsono.com^\n||online-media-stats.com^\n||online-metrix.net^\n||online-right-now.net^\n||onlinewebstat.com^\n||onlinewebstats.com^\n||onlysix.co.uk^\n||onthe.io^\n||opbandit.com^\n||openclick.com^\n||openhit.com^\n||openinternetexchange.com^\n||openinternetexchange.net^\n||openstat.net^\n||opentracker.net^\n||openvenue.com^\n||openxtracker.com^\n||oproi.com^\n||optify.net^\n||optimahub.com^\n||optimierung-der-website.de^\n||optimix.asia^\n||optimize-stats.voxmedia.com^\n||optimost.com^\n||optin-machine.com^\n||optorb.com^\n||optreadetrus.info^\n||oranges88.com^\n||orcapia.com^\n||org-dot-com.com^\n||os-data.com^\n||ositracker.com^\n||osxau.de^\n||otoshiana.com^\n||otracking.com^\n||ournet-analytics.com^\n||ourstats.de^\n||outboundlink.me^\n||overstat.com^\n||ovk.xceler8.io^\n||owldata.com^\n||owneriq.net^\n||oxidy.com^\n||p-log.ykimg.com^\n||p-td.com^\n||p.l1v.ly^\n||p.raasnet.com^\n||p.typekit.net^\n||p.yotpo.com^\n||p0.raasnet.com^\n||p2trc.emv2.com^\n||pa-oa.com^\n||page-hit.de^\n||pagefair.com^\n||pagerank-backlink.eu^\n||pagerank-hamburg.de^\n||pagerank-linkverzeichnis.de^\n||pagerank-online.eu^\n||pagerank-suchmaschine.de^\n||pagerank.fr^\n||pagerank4you.eu^\n||pagerankfree.com^\n||pageranking-counter.de^\n||pageranking.li^\n||pages05.net^\n||pagesense.com^\n||paidstats.com^\n||parameter.dk^\n||parklogic.com^\n||parrable.com^\n||partner.shareaholic.com^\n||pass-1234.com^\n||pathful.com^\n||pc-agency24.de^\n||pc1.io^\n||pclicks.com^\n||pcspeedup.com^\n||peakcounter.dk^\n||pebed.dm.gg^\n||peerius.com^\n||percentmobile.com^\n||perfdrive.com^\n||perfectaudience.com^\n||perfiliate.com^\n||performancerevenues.com^\n||performax.cz^\n||performtracking.com^\n||perion.com^\n||permutive.com^\n||persianstat.com^\n||persianstat.ir^\n||personage.name^\n||personyze.com^\n||petametrics.com^\n||phonalytics.com^\n||phone-analytics.com^\n||phorm.com.tr^\n||phorm.kr^\n||phormlabs.com^\n||photorank.me^\n||phpstat.com^\n||pi-stats.com^\n||pi.a42.ru\n||piano-media.com^\n||pickzor.com^\n||pikzor.com^\n||pimpmypr.de^\n||ping-fast.com^\n||pingagenow.com^\n||pingdom.net^\n||pingil.com^\n||pingomatic.com^\n||pings.conviva.com^\n||piwik.matrix.ua^\n||piwik.org^\n||pix.sniperlog.ru^\n||pixanalytics.com^\n||pixel.ad^\n||pixel.bild.de^\n||pixel.condenastdigital.com^\n||pixel.facebook.com^\n||pixel.parsely.com^\n||pixel.quantserve.com\n||pixel.watch^\n||pixel.wp.com^\n||pixeleze.com^\n||pixelinteractivemedia.com^\n||pixelrevenue.com^\n||pixelsnippet.com^\n||piximedia.com^\n||pjatr.com^\n||pjtra.com^\n||placemypixel.com^\n||platformpanda.com^\n||plecki.com^\n||pleisty.com^\n||plexop.com^\n||plexworks.de^\n||plugin.ws^\n||plwosvr.net^\n||pm0.net^\n||pm14.com/\n||pm14.com^\n||pmbox.biz^\n||pntra.com^\n||pntrac.com^\n||pntrs.com^\n||pocitadlo.cz^\n||pocitadlo.sk^\n||pointomatic.com^\n||polarmobile.com^\n||popsample.com^\n||popstats.com.br^\n||populr.me^\n||porngraph.com^\n||portfold.com^\n||posst.co^\n||postaffiliatepro.com^\n||postclickmarketing.com^\n||powerbar-pagerank.de^\n||powercount.com^\n||pp-serve.newsinc.com^\n||ppclocation.biz^\n||ppctracking.net^\n||pr-chart.com^\n||pr-chart.de^\n||pr-cy.ru^\n||pr-link.eu^\n||pr-linktausch.de^\n||pr-rang.de^\n||pr-sunshine.de^\n||pr-textlink.de^\n||pr-update.biz^\n||prchecker.info^\n||precisioncounter.com^\n||predicta.net^\n||predictivedna.com^\n||predictiveresponse.net^\n||prfct.co^\n||prm-ext.phorm.com^\n||prnetwork.de^\n||prnx.net^\n||proclivitysystems.com^\n||production-eqbc.lvp.llnw.net^\n||production-mcs.lvp.llnw.net^\n||productsup.com^\n||proext.com^\n||profilertracking3.com^\n||profilesnitch.com^\n||projecthaile.com^\n||projectsunblock.com^\n||promotionengine.com^\n||proofpositivemedia.com^\n||propagerank.de^\n||prospecteye.com^\n||prostats.it^\n||provenpixel.com^\n||providence.voxmedia.com^\n||proxad.net^\n||prtracker.com^\n||pstats.com^\n||psyma-statistics.com^\n||ptengine.com^\n||pto-slb-09.com^\n||ptp123.com^\n||ptrk-wn.com^\n||publicidees.com^\n||publishflow.com^\n||pulleymarketing.com^\n||puls.lv^\n||pulselog.com^\n||pulsemaps.com^\n||pureairhits.com^\n||purevideo.com^\n||putags.com^\n||px.dynamicyield.com^\n||pxi.pub^\n||pzkysq.pink^\n||q-counter.com^\n||q-stats.nl^\n||qbaka.net^\n||qbop.com^\n||qdtracking.com^\n||qoijertneio.com^\n||qos.video.yimg.com^\n||qsstats.com^\n||qualtrics.com^\n||quantserve.com^\n||quantserve.com^*.swf?\n||quantserve.com^*^a=\n||qubitproducts.com^\n||questionpro.com^\n||questradeaffiliates.com^\n||quick-counter.net^\n||quillion.com^\n||quintelligence.com^\n||qzlog.com^\n||r.bbci.co.uk^\n||r.movad.de^\n||r7ls.net^\n||radarstats.com^\n||radarurl.com^\n||radiomanlibya.com^\n||rampmetrics.com^\n||rang.com.ua^\n||rank-hits.com^\n||rank-power.com^\n||rank4all.eu^\n||rankchamp.de^\n||ranking-charts.de^\n||ranking-counter.de^\n||ranking-hits.de^\n||ranking-it.de^\n||ranking-links.de^\n||rankingpartner.com^\n||rankings24.de^\n||rankinteractive.com^\n||ranklink.de^\n||rapidcounter.com^\n||rapidstats.net^\n||rapidtrk.net^\n||rating.in^\n||rcounter.rambler.ru^\n||reachforce.com^\n||reachsocket.com^\n||reactful.com^\n||readertracking.com^\n||readnotify.com^\n||real5traf.ru^\n||realcounter.eu^\n||realcounters.com^\n||reali.st^\n||realist.gen.tr^\n||realtimeplease.com^\n||realtimewebstats.net^\n||realtracker.com^\n||realtracking.ninja^\n||realytics.io^\n||realzeit.io^\n||recoset.com^\n||redcounter.net^\n||redistats.com^\n||redstatcounter.com^\n||reedbusiness.net^\n||reedge.com^\n||referer.org^\n||referforex.com^\n||referlytics.com^\n||referrer.disqus.com^\n||referrer.org^\n||refersion.com^\n||refinedads.com^\n||reinvigorate.net^\n||reitingas.lt^\n||reitingi.lv^\n||rejestr.org^\n||relead.com^\n||reliablecounter.com^\n||relmaxtop.com^\n||remarketstats.com^\n||rep0pkgr.com^\n||repdata.usatoday.com^\n||res-x.com^\n||research-artisan.com^\n||research-int.se^\n||research-tool.com^\n||research.de.com^\n||researchnow.co.uk^\n||reseau-pub.com^\n||reson8.com^\n||responsetap.com^\n||retags.us^\n||retailrocket.ru^\n||retargetly.com^\n||reussissonsensemble.fr^\n||revdn.net^\n||revenuepilot.com^\n||revenuescience.com^\n||revenuewire.net^\n||revolvermaps.com^\n||revsw.net^\n||rewardtv.com^\n||reztrack.com^\n||rfihub.com^\n||rhinoseo.com^\n||riastats.com^\n||richard-group.com^\n||richmetrics.com^\n||rightstats.com^\n||ritecounter.com^\n||rkdms.com^\n||rktu.com^\n||rlcdn.com^\n||rmtag.com^\n||rnengage.com^\n||rng-snp-003.com^\n||rnlabs.com^\n||roi-pro.com^\n||roi-rocket.net^\n||roia.biz^\n||roiservice.com^\n||roispy.com^\n||roitesting.com^\n||roitracking.net^\n||roivista.com^\n||rollingcounters.com^\n||rover.ebay.com.au^*&cguid=\n||royalcount.de^\n||rpdtrk.com^\n||rqtrk.eu^\n||rrimpl.com^\n||rs0.co.uk^\n||rs6.net^\n||rsvpgenius.com^\n||rta.dailymail.co.uk^\n||rtax.criteo.com^\n||rtbauction.com^\n||rtfn.net^\n||rtoaster.jp^\n||rtrk.co.nz^\n||rtrk.com^\n||ru4.com^\n||rucounter.ru^\n||rumanalytics.com^\n||rva.outbrain.com^\n||rztrkr.com^\n||s.conyak.com^\n||s3s-main.net^\n||s6w.de^\n||sa.aol.com^\n||sageanalyst.net^\n||sail-horizon.com^\n||sajari.com^\n||salesgenius.com^\n||saletrack.co.uk^\n||sana.newsinc.com.s3.amazonaws.com^\n||sapha.com^\n||sarevtop.com^\n||sas15k01.com^\n||sayutracking.co.uk^\n||sayyac.com^\n||sayyac.net^\n||sbdtds.com^\n||scaledb.com^\n||scarabresearch.com^\n||scastnet.com^\n||schoolyeargo.com^\n||sciencerevenue.com^\n||scounter.rambler.ru^\n||scoutanalytics.net^\n||scrippscontroller.com^\n||script.ag^\n||script.opentracker.net\n||scriptil.com^\n||scripts21.com^\n||scriptshead.com^\n||sddan.com^\n||sea-nov-1.com^\n||searchfeed.com^\n||searchignite.com^\n||searchplow.com^\n||searchstats.usa.gov^\n||secure-pixel.com^\n||secure-wa-na.unileversolutions.com^\n||securepaths.com^\n||securestudies.com^\n||sedotracker.com^\n||sedotracker.de^\n||seehits.com^\n||seewhy.com^\n||segment-analytics.com^\n||segment.com^\n||segment.io^\n||segmentify.com^\n||seitwert.de^\n||selaris.com^\n||selipuquoe.com^\n||sellpoints.com^\n||semanticverses.com^\n||semasio.net^\n||sematext.com^\n||semtracker.de^\n||sendtraffic.com^\n||sensic.net^\n||sensor.org.ua^\n||sentry01.zerg.rambler.ru^\n||seo-master.net^\n||seogift.ru^\n||seomonitor.ro^\n||seomoz.org^\n||seoparts.net^\n||seoradar.ro^\n||sepyra.com^\n||serating.ru^\n||serious-partners.com^\n||servestats.com^\n||servinator.pw^\n||servingtrkid.com^\n||servustats.com^\n||sessioncam.com^\n||sexcounter.com^\n||sexystat.com^\n||sf14g.com^\n||sharp.ondu.ru^\n||sharpspring.com^\n||shinystat.com^\n||shinystat.it^\n||shippinginsights.com^\n||showroomlogic.com^\n||shrinktheweb.com^\n||siftscience.com^\n||signup-way.com^\n||silverpop.com^\n||silverpush.co^\n||sim-technik.de^*&uniqueTrackId=\n||simplehitcounter.com^\n||simplereach.com^\n||simpletop.net^\n||simpli.fi^\n||simplycast.us^\n||simplymeasured.com^\n||singlefeed.com^\n||singleice.link^\n||site-submit.com.ua^\n||site24x7rum.com^\n||siteapps.com^\n||sitebot.cn^\n||sitebro.com^\n||sitebro.de^\n||sitebro.net^\n||sitebro.tw^\n||sitechart.dk^\n||sitecompass.com^\n||siteimprove.com^\n||siteimproveanalytics.com^\n||siteintercept*.qualtrics.com^\n||sitelinktrack.com^\n||sitemeter.com^\n||sitereport.org^\n||sitestat.com^\n||sitetag.us^\n||sitetagger.co.uk^\n||sitetistik.com^\n||sitetracker.com^\n||sitetraq.nl^\n||skyglue.com^\n||skylog.kz^\n||sl-ct5.com^\n||slingpic.com^\n||slogantrend.de^\n||smallseotools.com^\n||smart-digital-solutions.com^\n||smart-ip.net^\n||smartctr.com^\n||smarterhq.io^\n||smarterremarketer.net^\n||smartpixel.auditorius.ru^\n||smartracker.net^\n||smartzonessva.com^\n||smfsvc.com^\n||smileyhost.net^\n||smrtlnks.com^\n||sniperlog.ru^\n||sniphub.com^\n||snoobi.com^\n||snowplow-collector.sugarops.com^\n||snowsignal.com^\n||socialprofitmachine.com^\n||socialtrack.co^\n||socialtrack.net^\n||sociaplus.com^\n||socketanalytics.com^\n||sodoit.com^\n||softonic-analytics.net^\n||sokrati.com^\n||sometrics.com^\n||sophus3.com^\n||sovetnik.yandex.net^\n||space-link.de^\n||spacehits.net^\n||specialstat.com^\n||spectate.com^\n||speed-trap.com^\n||speedcount.de^\n||speedcounter.net^\n||speedtracker.de^\n||spelar.org^\n||spider-mich.com^\n||splittag.com^\n||splurgi.com^\n||splyt.com^\n||spn-twr-14.com^\n||spn.ee^\n||sponsorcounter.de^\n||sponsored.com^\n||spoods.rce.veeseo.com^\n||spotmx.com^\n||spring-tns.net^\n||spring.de^\n||springmetrics.com^\n||sptag.com^\n||sptag1.com^\n||sptag2.com^\n||sptag3.com^\n||spycounter.net^\n||spylog.com^\n||spylog.ru^\n||spywords.com^\n||squidanalytics.com^\n||srpx.net^\n||ssl4stats.de^\n||st.dynamicyield.com^\n||st.top100.ru^\n||start.ru^\n||startstat.ru^\n||stat.4u.pl^\n||stat.alibaba.com^\n||stat.clickfrog.ru^\n||stat.eagleplatform.com^\n||stat.php-d.com^\n||stat.pl/\n||stat.radar.imgsmail.ru^\n||stat.rare.ru^\n||stat.sputnik.ru^\n||stat.www.fi^\n||stat.youku.com^\n||stat08.com^\n||stat24.com^\n||stat24.meta.ua^\n||stat24.ru^\n||statcount.com^\n||statcounter.com^\n||statcounterfree.com^\n||statcounters.info^\n||statdb.pressflex.com^\n||state.sml2.ru^\n||stathat.com^\n||stathound.com^\n||static.parsely.com^\n||statisfy.net^\n||statistiche-free.com^\n||statistiche-web.com^\n||statistiche.it^\n||statistiche.ws^\n||statistichegratis.net^\n||statistics.ro^\n||statistik-gallup.net^\n||statistika.lv^\n||statistiq.com^\n||statistx.com^\n||statok.net^\n||statowl.com^\n||stats-analytics.info^\n||stats.bbc.co.uk^\n||stats.cnevids.com^\n||stats.cz^\n||stats.de^\n||stats.ebay.com^\n||stats.fr^\n||stats.ft.com^\n||stats.g.doubleclick.net^\n||stats.kaltura.com^\n||stats.lt^\n||stats.rutracker.ga^\n||stats.shopify.com^\n||stats.tudou.com^\n||stats.wired.com^\n||stats.wordpress.com^\n||stats.wp.com^\n||stats2.com^\n||stats21.com^\n||stats2513.com^\n||stats4all.com^\n||stats4free.de^\n||stats4u.lv^\n||stats4you.com^\n||statsadvance-01.net^\n||statsale.com^\n||statsbox.nl^\n||statsevent.com^\n||statsforever.com^\n||statsimg.com^\n||statsinsight.com^\n||statsit.com^\n||statsmachine.com^\n||statsrely.com^\n||statssheet.com^\n||statsview.it^\n||statsw.com^\n||statswave.com^\n||statsy.net^\n||stattds.club^\n||stattooz.com^\n||stattrax.com^\n||statun.com^\n||statuncore.com^\n||statuscake.com^\n||stcllctrs.com^\n||stcounter.com^\n||stealth.nl^\n||steelhousemedia.com^\n||stellaservice.com^\n||stippleit.com^\n||stopphoulplay.com^\n||stopphoulplay.net^\n||stormcontainertag.com^\n||stormiq.com^\n||storystack.com^\n||stroeerdigitalmedia.de^\n||strs.jp/\n||strs.jp^\n||sub2tech.com^\n||submitnet.net^\n||subtraxion.com^\n||successfultogether.co.uk^\n||suchmaschinen-ranking-hits.de^\n||summitemarketinganalytics.com^\n||sumologic.com^\n||sundaysky.com^\n||sunios.de^\n||supercounters.com^\n||superstat.info^\n||superstats.com^\n||supert.ag^\n||surfcounters.com^\n||surfertracker.com^\n||surveyscout.com^\n||surveywriter.com^\n||survicate.com^\n||svr-prc-01.com^\n||svtrd.com^\n||swcs.jp^\n||swfstats.com^\n||swiss-counter.com^\n||swoopgrid.com^\n||sxtracking.com^\n||sync.morgdm.ru^\n||synergy-e.com^\n||synergy-sync.com^\n||synthasite.net^\n||sysomos.com^\n||t-analytics.com^\n||t.dailymail.co.uk^\n||t.kck.st^\n||t4ft.de^\n||taboola.com^\n||tag.datariver.ru^\n||tag4arm.com^\n||tagcommander.com^\n||tagifydiageo.com^\n||tags.news.com.au^\n||tagsrvcs.com^\n||tagtray.com^\n||tailtarget.com^\n||tamedia.ch^\n||tanx.com^\n||taps.io/\n||taps.io^\n||tapstream.com^\n||target.ukr.net^\n||targetfuel.com^\n||targeting.wpdigital.net^\n||tcactivity.net^\n||tcimg.com^\n||tctm.co^\n||td573.com^\n||tds348bf.us^\n||tdstats.com^\n||tealiumiq.com^\n||ted.dailymail.co.uk^\n||telemetrytaxonomy.net^\n||telize.com^\n||teljari.is^\n||tellapart.com^\n||tellaparts.com^\n||temnos.com^\n||tendatta.com^\n||tentaculos.net^\n||terabytemedia.com^\n||teriotracker.de^\n||terra.fp.oix.net^\n||tetigi.com^\n||tetoolbox.com^\n||theadex.com^\n||thebestlinks.com^\n||thebrighttag.com^\n||thecounter.com^\n||thefreehitcounter.com^\n||thermstats.com^\n||thesearchagency.net^\n||thespecialsearch.com^\n||thestat.net^\n||tinycounter.com^\n||tinystat.ir^\n||tiser.com.au^\n||tisoomi-services.com^\n||titag.com^\n||tkqlhce.com^\n||tl813.com^\n||tm.zedo.com^\n||tm1-001.com^\n||tmpjmp.com^\n||tmvtp.com^\n||tns-counter.ru^\n||tns-cs.net^\n||tns-gallup.dk^\n||tnsinternet.be^\n||top-bloggers.com^\n||top-ro.ro^\n||top-staging.mail.ru^\n||top.eomy.net^\n||top.gamesby.net^\n||top.gigmir.net^\n||top.list.ru^\n||top.myfilms.su^\n||top.nydus.org^\n||top.startua.com/?\n||top.t-sk.ru^\n||top.topua.net^\n||top100-images.rambler.ru^\n||top100.rambler.ru^\n||top100.vkirove.ru^\n||top100bloggers.com^\n||top100webshops.com^\n||top4wap.ru/\n||topblogarea.com^\n||topblogging.com^\n||topdepo.com^\n||tophits4u.de^\n||topli.ru^\n||toplist.cz^\n||toplist.eu^\n||toplist.raidrush.ws^\n||toplist.sk^\n||toplog.az^\n||topmalaysia.com^\n||topofblogs.com^\n||topsite.lv^\n||topstat.com^\n||toptracker.ru^\n||torbit.com^\n||toro-tags.com^\n||touchclarity.com^\n||tovery.net^\n||tracc.it^\n||trace-2000.com^\n||trace.events^\n||tracelog.www.alibaba.com^\n||tracelytics.com^\n||tracemyip.org^\n||tracer.jp^\n||tracetracking.net^\n||traceworks.com^\n||track-web.net^\n||track.atom-data.io^\n||track.audtd.com^\n||track.effiliation.com^\n||track.ft.com^\n||track.recreativ.ru^\n||track2.me^\n||trackalyzer.com^\n||trackbar.info^\n||trackcdn.com^\n||trackcmp.net^\n||trackconsole.com^\n||trackdiscovery.net^\n||trackeame.com^\n||trackedlink.net^\n||trackedweb.net^\n||tracker.cartprotector.com^\n||tracker.neon-images.com^\n||tracker.prom.ua^\n||tracker.stats.in.th^\n||trackfeed.com^\n||trackfreundlich.de^\n||tracking*.euroads.fi^\n||tracking-rce.veeseo.com^\n||tracking.adweb.co.kr^\n||tracking.olx-st.com^\n||tracking.performgroup.com^\n||tracking.pixelfederation.com^\n||tracking.rce.veeseo.com^\n||tracking.sim-technik.de^\n||tracking.tomshardware.co.uk^\n||tracking100.com^\n||tracking202.com^\n||trackinglabs.com^\n||trackkas.com^\n||trackmyweb.net^\n||trackset.com^\n||trackset.it^\n||tracksy.com^\n||tracktrk.net^\n||trackuity.com^\n||trackvoluum.com^\n||trackword.biz^\n||trackyourstats.com^\n||tradelab.fr^\n||tradescape.biz^\n||traffic4u.nl^\n||trafficby.net^\n||trafficengine.net^\n||trafficfacts.com^\n||trafficjoint.com^\n||trafficmaxx.de^\n||trafficregenerator.com^\n||traffikcntr.com^\n||trafic.ro^\n||trafikkfondet.no^\n||trafinfo.info^\n||trafit.com^\n||trafix.ro^\n||trafiz.net^\n||trail-web.com^\n||trailheadapp.com^\n||trakken.de^\n||traktr.news.com.au^\n||trakzor.com^\n||treasuredata.com^\n||treehousei.com^\n||trekmedia.net^\n||trendcounter.com^\n||trendcounter.de^\n||trendemon.com^\n||triggeredmessaging.com^\n||triggertag.gorillanation.com^\n||triggit.com^\n||trk*.vidible.tv^\n||trkjmp.com^\n||trkme.net^\n||trovus.co.uk^\n||tru.am^\n||truehits.in.th^\n||truehits.net^\n||truehits1.gits.net.th^\n||truehits3.gits.net.th^\n||try.abtasty.com^\n||tscapeplay.com^\n||tscounter.com^\n||tsk4.com^\n||tsk5.com^\n||tst14netreal.com^\n||tstlabs.co.uk^\n||tsw0.com^\n||tubetrafficcash.com^\n||twcount.com^\n||twopointo.io^\n||tylere.net^\n||tynt.com^\n||tyxo.bg/\n||tyxo.com^\n||u5e.com^\n||uadx.com^\n||uapoisk.net^\n||uarating.com^\n||ubertags.com^\n||ubertracking.info^\n||udc.msn.com^\n||ugdturner.com^\n||ui.oix.net^\n||ukrre-tea.info^\n||ultrastats.it^\n||unicaondemand.com^\n||universaltrackingcontainer.com^\n||unknowntray.com^\n||up-rank.com^\n||upalytics.com^\n||upstats.ru^\n||uptime.monitorus.ru^\n||uptimeviewer.com^\n||uptracs.com^\n||uralweb.ru^\n||uriuridfg.com^\n||urlbrief.com^\n||urlself.com^\n||urstats.de^\n||usabilitytools.com^\n||usabilla.com^\n||userchecker.info^\n||usercycle.com^\n||userdmp.com^\n||userlook.com^\n||userneeds.dk^\n||useronlinecounter.com^\n||userreport.com^\n||users.51.la^\n||userzoom.com^\n||usuarios-online.com^\n||uzrating.com^\n||v.shopify.com^\n||valaffiliates.com^\n||valuedopinions.co.uk^\n||vantage-media.net^\n||vbanalytics.com^\n||vdna-assets.com^\n||vdoing.com^\n||veduy.com^\n||veinteractive.com^\n||velaro.com^\n||vendri.io^\n||ventivmedia.com^\n||vepxl1.net^\n||vertical-leap.co.uk^\n||vertical-leap.net^\n||verticalscope.com^\n||verticalsearchworks.com^\n||vertster.com^\n||verypopularwebsite.com^\n||video-ad-stats.googlesyndication.com^\n||video.oms.eu^\n||videos.oms.eu^\n||videostat.com^\n||viewar.org^\n||vigo.ru^\n||vihtori-analytics.fi^\n||vinlens.com^\n||vinsight.de^\n||vinub.com^\n||viralninjas.com^\n||virool.com^\n||virtualnet.co.uk^\n||visadd.com^\n||visibility-stats.com^\n||visibli.com^\n||visioncriticalpanels.com^\n||visistat.com^\n||visit.webhosting.yahoo.com^\n||visitlog.net^\n||visitor-analytics.net^\n||visitor-stats.de^\n||visitor-track.com^\n||visitorglobe.com^\n||visitorinspector.com^\n||visitorjs.com^\n||visitorpath.com^\n||visitorprofiler.com^\n||visitortracklog.com^\n||visitorville.com^\n||visits.lt^\n||visitstreamer.com^\n||visualdna-stats.com^\n||visualdna.com^\n||visualrevenue.com^\n||visualwebsiteoptimizer.com^\n||vivistats.com^\n||vivocha.com^\n||vizisense.net^\n||vizu.com^\n||vizury.com^\n||vmm-satellite1.com^\n||vmm-satellite2.com^\n||vmmpxl.com^\n||vmtrk.com^\n||voicefive.com^\n||voodooalerts.com^\n||votistics.com^\n||vra.outbrain.com^\n||vrt.outbrain.com^\n||vstats.co^\n||vtracker.net^\n||vunetotbe.com^\n||w.oix.net^\n||w3counter.com^\n||w55c.net^\n||w88.go.com^\n||wa.and.co.uk^\n||wa4y.com^\n||waplog.mobi^\n||waplog.net^\n||warlog.ru^\n||waudit.cz^\n||way2traffic.com^\n||web-analytics.engagio.com^\n||web-boosting.net^\n||web-controlling.org^\n||web-counter.net^\n||web-stat.com^\n||web-stat.net^\n||web-visor.com^\n||web100kz.com^\n||webalytics.pw^\n||webclicks24.com^\n||webclicktracker.com^\n||webcompteur.com^\n||webcounter.co.za^\n||webcounter.ws^\n||webed.dm.gg^\n||webengage.com^\n||webest.info^\n||webflowmetrics.com^\n||webforensics.co.uk^\n||webgains.com^\n||webglstats.com^\n||webgozar.com^\n||webgozar.ir^\n||webhits.de^\n||webiqonline.com^\n||webkatalog.li^\n||webleads-tracker.com^\n||weblist.de^\n||weblog.com.ua^\n||weblytics.io^\n||webmasterplan.com^\n||webmeter.ws^\n||webmobile.ws^\n||webprospector.de^\n||webseoanalytics.co.za^\n||webserviceaward.com^\n||webservis.gen.tr^\n||website-hit-counters.com^\n||websiteceo.com^\n||websiteonlinecounter.com^\n||websiteperform.com^\n||websitesampling.com^\n||websitewelcome.com^\n||webspectator.com^\n||webstat.com^\n||webstat.net^\n||webstat.no^\n||webstat.se^\n||webstatistic.ml^\n||webstatistika.lt^\n||webstatistika.lv^\n||webstats.com^\n||webstats4u.com^\n||webtemsilcisi.com^\n||webtrack.biz^\n||webtraffic.se^\n||webtrafficagents.com^\n||webtraffiq.com^\n||webtraffstats.net^\n||webtraxs.com^\n||webtrekk-asia.net^\n||webtrekk-us.net^\n||webtrekk.de^\n||webtrekk.net^\n||webtrends.com^\n||webtrendslive.com^\n||webttracking.de^\n||webtuna.com^\n||webvisor.ru^\n||webwise.com^\n||webwise.net^\n||webwise.org^\n||wecount4u.com^\n||weesh.co.uk^\n||welt-der-links.de^\n||wemfbox.ch^\n||whackedmedia.com^\n||whatismyip.win^\n||whisbi.com^\n||whitepixel.com^\n||whoaremyfriends.com^\n||whoaremyfriends.net^\n||whoisonline.net^\n||whoisvisiting.com^\n||whosclickingwho.com^\n||whoseesyou.com^\n||whoson.com^\n||widget.perfectmarket.com^\n||wikia-beacon.com^\n||wikiodeliv.com^\n||wildxtraffic.com^\n||wipe.de^\n||wiredminds.de^\n||wisetrack.net^\n||wishloop.com^\n||woopra-ns.com^\n||woopra.com^\n||wordkeyhelper.com^\n||worldgravity.com^\n||worldlogger.com^\n||wowanalytics.co.uk^\n||wp-stats.com^\n||wpdstat.com^\n||wrating.com^\n||wredint.com^\n||wstatslive.com^\n||wt-eu02.net^\n||wt-safetag.com^\n||wtp101.com^\n||wtstats.com^\n||wundercounter.com^\n||wunderdaten.com^\n||wunderloop.net^\n||www-path.com^\n||www.hey.lt^\n||www.net.kg^\n||www.rt-ns.ru^\n||wwwstats.info^\n||wysistat.com^\n||wywy.com^\n||wywyuserservice.com^\n||wzrk.co^\n||wzrkt.com^\n||x-stat.de^\n||x-traceur.com^\n||x.ligatus.com^\n||xa-counter.com^\n||xclaimwords.net^\n||xclk-integracion.com^\n||xcounter.ch^\n||xg4ken.com^\n||xhit.com^\n||xiti.com^\n||xl-counti.com^\n||xpanama.net^\n||xplosion.de^\n||xref.io^\n||xslt.alexa.com^\n||xtractor.no^\n||xtremline.com^\n||xxxcounter.com^\n||xyztraffic.com^\n||y-track.com^\n||yadro.ru^\n||yamanoha.com^\n||yaudience.com^\n||ybotvisit.com^\n||ycctrk.co.uk^\n||yellowbrix.com^\n||ygsm.com^\n||yieldbot.com^\n||yieldify.com^\n||yieldsoftware.com^\n||yjtag.jp^\n||yoochoose.net^\n||youcanoptin.com^\n||youcanoptin.org^\n||youcanoptout.com^\n||youcanoptout.net^\n||youcanoptout.org^\n||youmetrix.co.uk^\n||your-counter.be^\n||youramigo.com^\n||zaehler.tv^\n||zanox-affiliate.de^\n||zdbb.net^\n||zdtag.com^\n||zenlivestats.com^\n||zero.kz/\n||zesep.com^\n||zipstat.dk^\n||zirve100.com^\n||ziyu.net^\n||zoomanalytics.co^\n||zoomflow.com^\n||zoomino.com^\n||zoosnet.net^\n||zoossoft.net^\n||zowary.com^\n||zqtk.net^\n||zroitracker.com^\n!\n! Section contains the list of tracking servers, which are hosted on useful sites as subdomains\n!\n||cnt.alawar.com^\n||ascstats.iobit.com^\n||analytics.livestream.com^\n||ut.o2.pl^\n||o.addthis.com^\n||su.addthis.com^\n||beacon.riskified.com^\n||d1m6l9dfulcyw7.cloudfront.net^\n||stats.mako.co.il^\n||analytics.xyscdn.com^\n||perr.h-cdn.com^\n||analytics.similarweb.com^\n||stats.mirror.co.uk^\n||eclick.baidu.com^\n||xtag.kbb.com^\n||analytics.thevideo.me^\n||s.frida.vse42.ru^\n||metric.gstatic.com^\n||startrekk.flaconi.de^\n||tracking.rtl.de^\n||log.adap.tv^\n||qlog.adap.tv^\n||segments.adap.tv^\n||sync.adap.tv^\n||hitweb2.chosun.com^\n||d31bfnnwekbny6.cloudfront.net^\n||epiv.cardlytics.com^\n||track.tiara.daum.net^\n||put.qostore.daumkakao.io^\n||bat.bing.com^\n||stats.videoseyredin.net^\n||geoip.newsdev.nytimes.com^\n||comet.sputniknews.com^\n||stat.novostimira.com^\n||log.rutube.ru^\n||analytics.mobile.walmart.com^\n||xtag.autotrader.com^\n||hit.mynet.com^\n||gekko.spiceworks.com^\n||bzclk.baidu.com^\n||d2q7mvwub8tmwf.cloudfront.net^\n||event.scimo.io^\n||vstats.digitaltrends.com^\n||statistics.mailerlite.com^\n||analytics.sgnapps.com^\n||bi-eventtracker-*.amazonaws.com^\n||counter.yadro.ru^\n||w88.espn.com^\n||stags.bluekai.com^\n||vzw.sp1.convertro.com^\n||app.adjust.io^\n||exacttarget.api.mashery.com^\n||lt*.tritondigital.com^\n||stats2.arstechnica.com^\n||timeslog.indiatimes.com^\n||ibeat.indiatimes.com^\n||ucounter.ucoz.net^\n||clientlogger-prod.elasticbeanstalk.com^\n||analytics.vadio.com^\n||ls.srvcs.tumblr.com^\n||saber.srvcs.tumblr.com^\n||clc.stackoverflow.com^\n||counter.1gb.ru^\n||tr.datanyze.com^\n||stats.olark.com^\n||assets.olark.com^\n||nrpc.olark.com^\n||iptracker-lb-*.amazonaws.com^\n||d.haberler.com^\n||adanalytics.openload.co^\n||tracking.adactioninteractive.com^\n||r.remarketingpixel.com^\n||onclickpredictiv.com^\n||pixelhere.com^\n||analytics.ma.tune.com^\n||log-live.direct.ly^\n||lively-collect-elb-*.amazonaws.com^\n||lgr.visilabs.net^\n||rt.visilabs.net^\n||analytics-rollout-*.amazonaws.com^\n||ce-global-track-*.amazonaws.com^\n||metric.timewarnercable.com^\n||data-collector.wefi.com^\n||mevents.trusteer.com^\n||counter.mediastealer.com^\n||counter.tovarro.com^\n||dcs.maxthon.com^\n||log.olark.com^\n|http://r.i.ua^\n||analyticapi.piri.net^\n||analytics-static.ugc.bazaarvoice.com^\n||analytics.go.com^\n||analytics.tout.com^\n||analytics.vesti-ukr.com^\n||b-aws.aol.com^\n||b-aws.huffingtonpost.com^\n||beacon.livefyre.com^\n||bi-pipeline-collector.stylight.net^\n||collector.apester.com^\n||collector.githubapp.com^\n||ct.pinterest.com^\n||d1xfq2052q7thw.cloudfront.net^\n||d3r7h55ola878c.cloudfront.net^\n||events-endpoint-*.amazonaws.com^\n||events.apester.com^\n||geobeacon.ign.com^\n||hi.hellobar.com^\n||hrumpoc.hotels.com^\n||images.dmca.com^\n||l.ooyala.com^\n||l.player.ooyala.com^\n||l.sharethis.com^\n||logdev.openload.co^\n||logs.thebloggernetwork.com^\n||metrics.toysrus.com^\n||pi.pardot.com^\n||ping.hellobar.com^\n||qnajplxtvz-a.akamaihd.net^\n||recs.atgsvcs.com^\n||smetrics.uhc.com^\n||stats-newyork1.bloxcms.com^\n||stats.openload.co^\n||tagx.nytimes.com^\n||top-fwz1.mail.ru^\n||track.dictionary.com^\n||track.tooplay.com^\n||tracking.tchibo.de^\n||wtsdc.uhc.com^\n!\n! Section contains rules for mobile analytics and spyware\n!\n||bidder.mdspinc.com^\n||report.appmetrica.yandex.net^\n||clktrk.display.io^\n||events.lbesecapi.com^\n||imp.apprevolve.com^\n||tracking.lenzmx.com^\n||tknet.rayjump.com^\n||analyzer.omniata.com^\n||metrics.bestbuy.com^\n||analytics-server.gimbal.com^\n||beacon.carfax.com^\n||invenio_tracking_*.sgnapps.com^\n||metrics.macys.net^\n||sypi.gpshopper.com^\n||collector.brandify.com^\n||lhzbdvm.com^\n||prugskh.net^\n||prugskh.com^\n||rlog.9gag.com^\n||rlog-api.under9.co^\n||track.pingstart.com^\n||geoip.fotoable.net^\n||andmlb.kshwtj.com^\n||cb.ksmobile.com^\n||mlb.did.ijinshan.com^\n||mobile-collector.newrelic.com^\n||api.appsee.com^\n||metrics.adflake.com^\n||nlog.droid4x.cn^\n||mtlog.droid4x.cn^\n||log.droid4x.cn^\n||api.gimbal.com^\n||onelink.me^\n||tunein.adswizz.com^\n||nativesdks.mparticle.com^\n||config2.mparticle.com^\n||iam-agof-app.irquest.com^\niapp-cp.nuggad.net^\n||ulink.adjust.com^\n||adj.st^\n||clientmetrics.kik.com^\n||clientmetrics-augmentum.kik.com^\n||api.gameanalytics.com^\n||lepodownload.mediatek.com^\n||amp-error-reporting.appspot.com^\n||stats.popcap.com^\n||alog.umeng.com^\n||oc.umeng.com^\n||ar.umeng.com^\n||alog.umengcloud.com^\n||crasheye.cn^\n||beacon.shazam.com^\n||etl.tindersparks.com^\n||trk.pinterest.com^\n||log.umsns.com^\n||pingma.qq.com^\n||mobilelog.upqzfile.com^\n||beacon.qq.com^\n||adsunflower.com^\n||adfuture.cn^\n||advmob.cn^\n||abtest.mistat.xiaomi.com^\n||acompli.helpshift.com^\n||ad-analytics-bootstrap.metaps.com^\n||xlmc.sec.miui.com^\n||beha.ksmobile.com^\n||adserver.snapads.com^\n||adv.sec.miui.com^\n||analytics.ad.daum.net^\n||analytics.liftoff.io^\n||analytics.mobile.yandex.net^\n||analytics.mopub.com^\n||api-analytics-bootstrap.metaps.com^\n||api-analytics.metaps.com^\n||api.amplitude.com^\n||api.analytics.omgpop.com^\n||api.apptentive.com^\n||api.branch.io^\n||api.crittercism.com^\n||api.sec.miui.com^\n||api.segment.io^\n||api.taplytics.com^\n||api.vigo.ru^\n||app.adjust.com^\n||appmetrica.yandex.com^\n||brahe.apptimize.com^\n||c.bigmir.net^\n||cedexis-radar.net^\n||counter.kingsoft.com^\n||crashlytics.com^\n||data.mistat.xiaomi.com^\n||data.sec.miui.com^\n||ep.xone.com^\n||etl.xlmc.sandai.net^\n||fcanr.tracking.miui.com^\n||google-analytics.com^\n||googleadapis.l.google.com^\n||installtracker.com^\n||logupdate.avlyun.sec.miui.com^\n||metrics.sdkbox.com^\n||mobileanalytics.*.amazonaws.com^\n||ms.cmcm.com^\n||nrc.tapas.net^\n||ping.taplytics.com^\n||ping.xlmc.sandai.net^\n||quantcount.com^\n||r.browser.miui.com^\n||rc.dxsvr.com^\n||sc-analytics.appspot.com^\n||sdkconfig.ad.xiaomi.com^\n||ssl-google-analytics.l.google.com^\n||stat.duokanbox.com^\n||statistics.videofarm.daum.net^\n||stats.unity3d.com^\n||tapjoy.net^\n||tapjoy.com^\n||4seeresults.com^\n||taurus.iad.appboy.com^\n||tns.simba.taobao.com^\n||tracker-api.my.com^\n||tracking.miui.com^\n||tu.dxcnd.cn^\n||ws.ksmobile.net^\n||www-google-analytics.l.google.com^\n! UC Browser\n||upoll.umengcloud.com^\n||utop.umengcloud.com^\n! KingRoot\n||analy.qq.com^\n!\n! Adguard DNS rules\n||hghit.com^\n||hgbnr.com^\n||hgbn1.com^\n||hg-bn.com^\n||ad.adriver.ru^\n||content.adriver.ru^\n||ad.3dnews.ru^\n||an.yandex.ru^\n||t.insigit.com^\n||traffichunt.com^\n||sportbets.su^\n||ad.onliner.by^\n||directadvert.ru^\n||betweendigital.com^\n||tools.runetki.co^\n||a4.overclockers.ua^\n||novunu.football-plyus.net^\n! https://github.com/AdguardTeam/AdguardDNS/issues/52\n!\n! Adguard DNS exceptions\n! https://github.com/AdguardTeam/AdguardForiOS/issues/242\n@@||t.appsflyer.com^\n@@||stats.appsflyer.com^\n! https://github.com/AdguardTeam/AdguardDNS/issues/63\n@@||googleadapis.l.google.com^\n! linkedin.com\n@@||cedexis.net^\n@@||licdn.com^\n! Fixing Walmart\n@@||omniture.walmart.com^\n"
  },
  {
    "path": "internal/home/auth.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghuser\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/httputil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// sessionsDBName is the name of the file where session data is stored.\nconst sessionsDBName = \"sessions.db\"\n\n// webUser represents a user of the Web UI.\n//\n// TODO(s.chzhen):  Improve naming.\ntype webUser struct {\n\t// Name represents the login name of the web user.\n\tName string `yaml:\"name\"`\n\n\t// PasswordHash is the hashed representation of the web user password.\n\tPasswordHash string `yaml:\"password\"`\n\n\t// UserID is the unique identifier of the web user.\n\tUserID aghuser.UserID `yaml:\"-\"`\n}\n\n// toUser returns the new properly initialized *aghuser.User using stored\n// properties.  It panics if there is an error generating the user ID.\nfunc (wu *webUser) toUser() (u *aghuser.User) {\n\tuid := wu.UserID\n\tif uid == (aghuser.UserID{}) {\n\t\tuid = aghuser.MustNewUserID()\n\t}\n\n\treturn &aghuser.User{\n\t\tPassword: aghuser.NewDefaultPassword(wu.PasswordHash),\n\t\tLogin:    aghuser.Login(wu.Name),\n\t\tID:       uid,\n\t}\n}\n\n// authConfig is the configuration structure for [auth].\ntype authConfig struct {\n\t// baseLogger is used for creating other loggers.  It must not be nil.\n\tbaseLogger *slog.Logger\n\n\t// rateLimiter manages the rate limiting for login attempts.  It must not be\n\t// nil.\n\trateLimiter loginRateLimiter\n\n\t// trustedProxies is a set of subnets considered as trusted.\n\ttrustedProxies netutil.SubnetSet\n\n\t// dbFilename is the name of the file where session data is stored.  It must\n\t// not be empty.\n\tdbFilename string\n\n\t// users contains web user information from the configuration file.\n\tusers []webUser\n\n\t// sessionTTL is the TTL (Time To Live) for web user sessions.\n\tsessionTTL time.Duration\n\n\t// isGLiNet indicates whether GLiNet mode is enabled.\n\tisGLiNet bool\n}\n\n// auth stores web user information and handles authentication.\ntype auth struct {\n\t// logger is used to log the operation of the auth module.\n\tlogger *slog.Logger\n\n\t// rateLimiter manages rate limiting for login attempts.\n\trateLimiter loginRateLimiter\n\n\t// trustedProxies is a set of subnets considered trusted.\n\ttrustedProxies netutil.SubnetSet\n\n\t// sessions stores web users' sessions.\n\tsessions aghuser.SessionStorage\n\n\t// users stores user credentials.\n\tusers aghuser.DB\n\n\t// isGLiNet indicates whether GLiNet mode is enabled.\n\tisGLiNet bool\n\n\t// isUserless indicates that there are no users defined in the configuration\n\t// file.\n\tisUserless bool\n}\n\n// newAuth returns the new properly initialized *auth.\nfunc newAuth(ctx context.Context, conf *authConfig) (a *auth, err error) {\n\tuserDB := aghuser.NewDefaultDB()\n\tfor i, u := range conf.users {\n\t\terr = userDB.Create(ctx, u.toUser())\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"users: at index %d: %w\", i, err)\n\t\t}\n\t}\n\n\ts, err := aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{\n\t\tLogger:     conf.baseLogger.With(slogutil.KeyPrefix, \"session_storage\"),\n\t\tClock:      timeutil.SystemClock{},\n\t\tUserDB:     userDB,\n\t\tDBPath:     conf.dbFilename,\n\t\tSessionTTL: conf.sessionTTL,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating session storage: %w\", err)\n\t}\n\n\treturn &auth{\n\t\tlogger:         conf.baseLogger.With(slogutil.KeyPrefix, \"auth\"),\n\t\trateLimiter:    conf.rateLimiter,\n\t\ttrustedProxies: conf.trustedProxies,\n\t\tsessions:       s,\n\t\tusers:          userDB,\n\t\tisGLiNet:       conf.isGLiNet,\n\t\tisUserless:     len(conf.users) == 0,\n\t}, nil\n}\n\n// middleware returns authentication middleware.\nfunc (a *auth) middleware() (mw httputil.Middleware) {\n\tif a.isGLiNet {\n\t\treturn newAuthMiddlewareGLiNet(&authMiddlewareGLiNetConfig{\n\t\t\tlogger:          a.logger,\n\t\t\tclock:           timeutil.SystemClock{},\n\t\t\ttokenFilePrefix: glFilePrefix,\n\t\t\tttl:             glTokenTimeout,\n\t\t\tmaxTokenSize:    MaxFileSize,\n\t\t})\n\t}\n\n\treturn newAuthMiddlewareDefault(&authMiddlewareDefaultConfig{\n\t\tlogger:         a.logger,\n\t\trateLimiter:    a.rateLimiter,\n\t\ttrustedProxies: a.trustedProxies,\n\t\tsessions:       a.sessions,\n\t\tusers:          a.users,\n\t})\n}\n\n// usersList returns a copy of a users list.\nfunc (a *auth) usersList(ctx context.Context) (webUsers []webUser) {\n\tusers, err := a.users.All(ctx)\n\tif err != nil {\n\t\t// Should not happen.\n\t\tpanic(err)\n\t}\n\n\twebUsers = make([]webUser, 0, len(users))\n\tfor _, u := range users {\n\t\twebUsers = append(webUsers, webUser{\n\t\t\tName:         string(u.Login),\n\t\t\tPasswordHash: string(u.Password.Hash()),\n\t\t\tUserID:       u.ID,\n\t\t})\n\t}\n\n\treturn webUsers\n}\n\n// addUser adds a new user with the given password.  u must not be nil.\nfunc (a *auth) addUser(ctx context.Context, u *webUser, password string) (err error) {\n\tif len(password) == 0 {\n\t\treturn errors.Error(\"empty password\")\n\t}\n\n\thash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"generating hash: %w\", err)\n\t}\n\n\tu.PasswordHash = string(hash)\n\n\terr = a.users.Create(ctx, u.toUser())\n\tif err != nil {\n\t\t// Should not happen.\n\t\tpanic(err)\n\t}\n\n\ta.isUserless = false\n\n\ta.logger.DebugContext(ctx, \"added user\", \"login\", u.Name)\n\n\treturn nil\n}\n\n// close closes the authentication database.\nfunc (a *auth) close(ctx context.Context) {\n\terr := a.sessions.Close()\n\tif err != nil {\n\t\ta.logger.ErrorContext(ctx, \"closing session storage\", slogutil.KeyError, err)\n\t}\n}\n"
  },
  {
    "path": "internal/home/auth_internal_test.go",
    "content": "package home\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghuser\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nfunc TestAuth_UsersList(t *testing.T) {\n\tconst (\n\t\tuserName     = \"name\"\n\t\tuserPassword = \"password\"\n\t)\n\n\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)\n\trequire.NoError(t, err)\n\n\tsessionsDB := filepath.Join(t.TempDir(), \"sessions.db\")\n\n\tuser := webUser{\n\t\tName:         userName,\n\t\tPasswordHash: string(passwordHash),\n\t\tUserID:       aghuser.MustNewUserID(),\n\t}\n\n\tauth, err := newAuth(testutil.ContextWithTimeout(t, testTimeout), &authConfig{\n\t\tbaseLogger:     testLogger,\n\t\trateLimiter:    emptyRateLimiter{},\n\t\ttrustedProxies: testTrustedProxies,\n\t\tdbFilename:     sessionsDB,\n\t\tusers:          nil,\n\t\tsessionTTL:     testTimeout,\n\t\tisGLiNet:       false,\n\t})\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() { auth.close(testutil.ContextWithTimeout(t, testTimeout)) })\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tassert.Empty(t, auth.usersList(ctx))\n\n\terr = auth.addUser(ctx, &user, userPassword)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, []webUser{user}, auth.usersList(ctx))\n}\n"
  },
  {
    "path": "internal/home/authglinet.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/httputil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// glFilePrefix is the prefix of the filepath where the authentication token is\n// stored.  Note that it is variable so it can be edited in tests.\n//\n// TODO(s.chzhen):  Make it a constant.\nvar glFilePrefix = \"/tmp/gl_token_\"\n\nconst (\n\t// glTokenTimeout is the TTL (Time To Live) of the authentication token.\n\tglTokenTimeout = 3600 * time.Second\n\n\t// glCookieName is the name of the cookie that stores the authentication\n\t// token.\n\tglCookieName = \"Admin-Token\"\n)\n\n// MaxFileSize is a maximum file length in bytes.\nconst MaxFileSize = 1024 * 1024\n\n// authMiddlewareGLiNetConfig is the configuration structure for the GLiNet\n// authentication middleware.\ntype authMiddlewareGLiNetConfig struct {\n\t// logger is used for logging the operation of the middleware.  It must not\n\t// be nil.\n\t//\n\t// TODO(s.chzhen):  Use logger from the context.\n\tlogger *slog.Logger\n\n\t// clock is used to get the current time.  It must not be nil.\n\tclock timeutil.Clock\n\n\t// tokenFilePrefix is the prefix of the filepath where the authentication\n\t// token is stored.  It must not be empty.\n\ttokenFilePrefix string\n\n\t// ttl is the TTL (Time To Live) of the authentication token.  It must be\n\t// greater than zero.\n\tttl time.Duration\n\n\t// maxTokenSize is the maximum size of the file containing the\n\t// authentication token.  It must be greater than zero.\n\tmaxTokenSize uint\n}\n\n// authMiddlewareGLiNet is the GLiNet authentication middleware.  It checks if\n// the request is authenticated using a cookie.\ntype authMiddlewareGLiNet struct {\n\tlogger          *slog.Logger\n\tclock           timeutil.Clock\n\ttokenFilePrefix string\n\tttl             time.Duration\n\tmaxTokenSize    uint\n}\n\n// newAuthMiddlewareGLiNet returns the new properly initialized\n// *authMiddlewareGLiNet.\nfunc newAuthMiddlewareGLiNet(c *authMiddlewareGLiNetConfig) (mw *authMiddlewareGLiNet) {\n\treturn &authMiddlewareGLiNet{\n\t\tlogger:          c.logger,\n\t\tclock:           c.clock,\n\t\ttokenFilePrefix: c.tokenFilePrefix,\n\t\tttl:             c.ttl,\n\t\tmaxTokenSize:    c.maxTokenSize,\n\t}\n}\n\n// type check\nvar _ httputil.Middleware = (*authMiddlewareGLiNet)(nil)\n\n// Wrap implements the [httputil.Middleware] interface for\n// *authMiddlewareGLiNet.\nfunc (mw *authMiddlewareGLiNet) Wrap(h http.Handler) (wrapped http.Handler) {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\n\t\tpath := r.URL.Path\n\t\tif isPublicResource(path) {\n\t\t\th.ServeHTTP(w, r)\n\n\t\t\treturn\n\t\t}\n\n\t\tif mw.isAuthenticated(ctx, r) {\n\t\t\th.ServeHTTP(w, r)\n\n\t\t\treturn\n\t\t}\n\n\t\tif path == \"/\" || path == \"/index.html\" {\n\t\t\thost := r.Host\n\n\t\t\tif h, _, err := net.SplitHostPort(r.Host); err == nil {\n\t\t\t\thost = h\n\t\t\t}\n\n\t\t\tu := &url.URL{\n\t\t\t\tScheme: urlutil.SchemeHTTP,\n\t\t\t\tHost:   host,\n\t\t\t}\n\n\t\t\thttp.Redirect(w, r, u.String(), http.StatusFound)\n\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\t})\n}\n\n// isAuthenticated returns true if the request is authenticated using a cookie.\n//\n// TODO(s.chzhen):  Use the request's path.\nfunc (mw *authMiddlewareGLiNet) isAuthenticated(ctx context.Context, r *http.Request) (ok bool) {\n\tc, err := r.Cookie(glCookieName)\n\tif err == http.ErrNoCookie {\n\t\tmw.logger.ErrorContext(ctx, \"no authentication cookie\", slogutil.KeyError, err)\n\n\t\treturn false\n\t}\n\n\treturn mw.checkToken(ctx, c.Value)\n}\n\n// checkToken verifies the validity of an authentication token.  It retrieves\n// the time stored in a file named after the token and checks if the token has\n// expired based on that time.\nfunc (mw *authMiddlewareGLiNet) checkToken(ctx context.Context, token string) (ok bool) {\n\ttokenFile := mw.tokenFilePrefix + token\n\ttokenDate := mw.tokenDate(ctx, tokenFile)\n\tnow := mw.clock.Now()\n\tif now.Before(tokenDate.Add(mw.ttl)) {\n\t\treturn true\n\t}\n\n\tmw.logger.DebugContext(ctx, \"authentication token has expired\")\n\n\treturn false\n}\n\n// tokenDate returns the time stored in the authentication token file.  If there\n// is an error, it logs the error and returns the zero time.\nfunc (mw *authMiddlewareGLiNet) tokenDate(ctx context.Context, tokenFile string) (t time.Time) {\n\tf, err := os.Open(tokenFile)\n\tif err != nil {\n\t\tmw.logger.ErrorContext(ctx, \"opening token file\", slogutil.KeyError, err)\n\n\t\treturn time.Time{}\n\t}\n\n\tdefer slogutil.CloseAndLog(ctx, mw.logger, f, slog.LevelError)\n\n\t// Create a 4-byte long buffer to store Unix time as a uint32, since GL.iNet\n\t// routers use it as part of an authentication mechanism.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/1853.\n\tdata := make([]byte, 4)\n\t_, err = io.ReadFull(f, data)\n\tif err != nil {\n\t\tmw.logger.ErrorContext(ctx, \"reading token file\", slogutil.KeyError, err)\n\n\t\treturn time.Time{}\n\t}\n\n\treturn time.Unix(int64(binary.NativeEndian.Uint32(data)), 0)\n}\n"
  },
  {
    "path": "internal/home/authglinet_internal_test.go",
    "content": "package home\n\nimport (\n\t\"encoding/binary\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAuthMiddlewareGLiNet(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\ttestTTL = 60 * time.Second\n\n\t\tglTokenFileSuffix = \"test\"\n\t)\n\n\ttempDir := t.TempDir()\n\tglFilePrefix = tempDir + \"/gl_token_\"\n\tglTokenFile := glFilePrefix + glTokenFileSuffix\n\n\tglFileData := make([]byte, 4)\n\tbinary.NativeEndian.PutUint32(glFileData, uint32(time.Now().Add(testTTL).Unix()))\n\n\terr := os.WriteFile(glTokenFile, glFileData, 0o644)\n\trequire.NoError(t, err)\n\n\tmw := newAuthMiddlewareGLiNet(&authMiddlewareGLiNetConfig{\n\t\tlogger:          testLogger,\n\t\tclock:           timeutil.SystemClock{},\n\t\ttokenFilePrefix: glFilePrefix,\n\t\tmaxTokenSize:    MaxFileSize,\n\t\tttl:             testTTL,\n\t})\n\n\th := &testAuthHandler{}\n\twrapped := mw.Wrap(h)\n\n\treqValidCookie := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treqValidCookie.AddCookie(&http.Cookie{Name: glCookieName, Value: glTokenFileSuffix})\n\n\treqInvalidCookie := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treqInvalidCookie.AddCookie(&http.Cookie{Name: glCookieName, Value: \"invalid_cookie\"})\n\n\ttestCases := []struct {\n\t\treq      *http.Request\n\t\tname     string\n\t\twantCode int\n\t}{{\n\t\treq:      httptest.NewRequest(http.MethodGet, \"/\", nil),\n\t\tname:     \"no_cookie\",\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\treq:      reqValidCookie,\n\t\tname:     \"valid_cookie\",\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\treq:      reqInvalidCookie,\n\t\tname:     \"invalid_cookie\",\n\t\twantCode: http.StatusFound,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\twrapped.ServeHTTP(w, tc.req)\n\n\t\t\tassert.Equal(t, tc.wantCode, w.Code)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/home/authhttp.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"path\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghuser\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/httputil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n)\n\n// cookieTTL is the time-to-live of the session cookie.\nconst cookieTTL = 365 * timeutil.Day\n\n// sessionCookieName is the name of the session cookie.\nconst sessionCookieName = \"agh_session\"\n\n// loginJSON is the JSON structure for authentication.\ntype loginJSON struct {\n\tName     string `json:\"name\"`\n\tPassword string `json:\"password\"`\n}\n\n// realIP extracts the real IP address of the client from an HTTP request using\n// the known HTTP headers.\n//\n// TODO(a.garipov): Currently, this is basically a copy of a similar function in\n// module dnsproxy.  This should really become a part of module golibs and be\n// replaced both here and there.  Or be replaced in both places by\n// a well-maintained third-party module.\n//\n// TODO(a.garipov): Support header Forwarded from RFC 7329.\nfunc realIP(r *http.Request) (ip netip.Addr, err error) {\n\tproxyHeaders := []string{\n\t\thttphdr.CFConnectingIP,\n\t\thttphdr.TrueClientIP,\n\t\thttphdr.XRealIP,\n\t}\n\n\tfor _, h := range proxyHeaders {\n\t\tv := r.Header.Get(h)\n\t\tip, err = netip.ParseAddr(v)\n\t\tif err == nil {\n\t\t\treturn ip, nil\n\t\t}\n\t}\n\n\t// If none of the above yielded any results, get the leftmost IP address\n\t// from the X-Forwarded-For header.\n\ts := r.Header.Get(httphdr.XForwardedFor)\n\tipStr, _, _ := strings.Cut(s, \",\")\n\tip, err = netip.ParseAddr(ipStr)\n\tif err == nil {\n\t\treturn ip, nil\n\t}\n\n\t// When everything else fails, just return the remote address as understood\n\t// by the stdlib.\n\tipStr, err = netutil.SplitHost(r.RemoteAddr)\n\tif err != nil {\n\t\treturn netip.Addr{}, fmt.Errorf(\"getting ip from client addr: %w\", err)\n\t}\n\n\treturn netip.ParseAddr(ipStr)\n}\n\n// writeErrorWithIP is like [aghhttp.Error], but includes the remote IP address\n// when it writes to the log.\nfunc writeErrorWithIP(\n\tr *http.Request,\n\tw http.ResponseWriter,\n\tcode int,\n\tremoteIP string,\n\tformat string,\n\targs ...any,\n) {\n\ttext := fmt.Sprintf(format, args...)\n\tlog.Error(\"%s %s %s: from ip %s: %s\", r.Method, r.Host, r.URL, remoteIP, text)\n\thttp.Error(w, text, code)\n}\n\n// handleLogin is the handler for the POST /control/login HTTP API.\nfunc (web *webAPI) handleLogin(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\treq := loginJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, web.logger, r, w, http.StatusBadRequest, \"json decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tvar remoteIPStr string\n\t// The real IP address of the client [realIP] cannot be used here without\n\t// taking trusted proxies into account due to security issues:\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.\n\tif remoteIPStr, err = netutil.SplitHost(r.RemoteAddr); err != nil {\n\t\twriteErrorWithIP(\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\tr.RemoteAddr,\n\t\t\t\"auth: getting remote address: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tif rateLimiter := web.auth.rateLimiter; rateLimiter != nil {\n\t\tif left := rateLimiter.check(remoteIPStr); left > 0 {\n\t\t\tw.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))\n\t\t\twriteErrorWithIP(\n\t\t\t\tr,\n\t\t\t\tw,\n\t\t\t\thttp.StatusTooManyRequests,\n\t\t\t\tremoteIPStr,\n\t\t\t\t\"auth: blocked for %s\",\n\t\t\t\tleft,\n\t\t\t)\n\n\t\t\treturn\n\t\t}\n\t}\n\n\tip, err := realIP(r)\n\tif err != nil {\n\t\tweb.logger.ErrorContext(\n\t\t\tctx,\n\t\t\t\"getting real ip\",\n\t\t\t\"remote_ip\", remoteIPStr,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\t}\n\n\tremoteIP, err := netip.ParseAddr(remoteIPStr)\n\tif err != nil {\n\t\twriteErrorWithIP(\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\tr.RemoteAddr,\n\t\t\t\"auth: parsing remote address: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tlogIP := remoteIPStr\n\tif web.auth.trustedProxies.Contains(remoteIP.Unmap()) {\n\t\tlogIP = ip.String()\n\t}\n\n\tcookie, err := newCookie(ctx, web.auth, req, remoteIPStr)\n\tif err != nil {\n\t\twriteErrorWithIP(r, w, http.StatusForbidden, logIP, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tweb.logger.InfoContext(ctx, \"successful login\", \"user\", req.Name, \"ip\", logIP)\n\n\thttp.SetCookie(w, cookie)\n\n\th := w.Header()\n\th.Set(httphdr.CacheControl, \"no-store, no-cache, must-revalidate, proxy-revalidate\")\n\th.Set(httphdr.Pragma, \"no-cache\")\n\th.Set(httphdr.Expires, \"0\")\n\n\taghhttp.OK(ctx, web.logger, w)\n}\n\n// newCookie creates a new authentication cookie.  rateLimiter must not be nil.\nfunc newCookie(\n\tctx context.Context,\n\tauth *auth,\n\treq loginJSON,\n\taddr string,\n) (c *http.Cookie, err error) {\n\tuser, err := auth.users.ByLogin(ctx, aghuser.Login(req.Name))\n\tif err != nil {\n\t\t// Should not happen.\n\t\tpanic(err)\n\t}\n\n\trateLimiter := auth.rateLimiter\n\tif user == nil {\n\t\trateLimiter.inc(addr)\n\n\t\treturn nil, errInvalidLogin\n\t}\n\n\tok := user.Password.Authenticate(ctx, req.Password)\n\tif !ok {\n\t\trateLimiter.inc(addr)\n\n\t\treturn nil, errInvalidLogin\n\t}\n\n\trateLimiter.remove(addr)\n\n\tsess, err := auth.sessions.New(ctx, user)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &http.Cookie{\n\t\tName:     sessionCookieName,\n\t\tValue:    hex.EncodeToString(sess.Token[:]),\n\t\tPath:     \"/\",\n\t\tExpires:  time.Now().Add(cookieTTL),\n\t\tHttpOnly: true,\n\t\tSameSite: http.SameSiteLaxMode,\n\t}, nil\n}\n\n// handleLogout is the handler for the GET /control/logout HTTP API.\nfunc (web *webAPI) handleLogout(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\trespHdr := w.Header()\n\tc, err := r.Cookie(sessionCookieName)\n\tif err != nil {\n\t\t// The only error that is returned from r.Cookie is [http.ErrNoCookie].\n\t\t// The user is already logged out.\n\t\trespHdr.Set(httphdr.Location, \"/login.html\")\n\t\tw.WriteHeader(http.StatusFound)\n\n\t\treturn\n\t}\n\n\tt, err := sessionTokenFromHex(c.Value)\n\tif err != nil {\n\t\tweb.logger.ErrorContext(ctx, \"getting token\", slogutil.KeyError, err)\n\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\n\t\treturn\n\t}\n\n\terr = web.auth.sessions.DeleteByToken(ctx, t)\n\tif err != nil {\n\t\tweb.logger.ErrorContext(ctx, \"removing session by token\", slogutil.KeyError, err)\n\t}\n\n\tc = &http.Cookie{\n\t\tName:    sessionCookieName,\n\t\tValue:   \"\",\n\t\tPath:    \"/\",\n\t\tExpires: time.Unix(0, 0),\n\n\t\tHttpOnly: true,\n\t\tSameSite: http.SameSiteLaxMode,\n\t}\n\n\trespHdr.Set(httphdr.Location, \"/login.html\")\n\trespHdr.Set(httphdr.SetCookie, c.String())\n\tw.WriteHeader(http.StatusFound)\n}\n\n// registerAuthHandlers registers authentication handlers.\nfunc (web *webAPI) registerAuthHandlers() {\n\tweb.conf.mux.Handle(\n\t\thttp.MethodPost+\" \"+\"/control/login\",\n\t\tweb.postInstallHandler(http.HandlerFunc(web.handleLogin)),\n\t)\n\tweb.httpReg.Register(http.MethodGet, \"/control/logout\", web.handleLogout)\n}\n\n// isPublicResource returns true if p is a path to a public resource.\nfunc isPublicResource(p string) (ok bool) {\n\tisAsset, err := path.Match(\"/assets/*\", p)\n\tif err != nil {\n\t\t// The only error that is returned from path.Match is\n\t\t// [path.ErrBadPattern].  This is a programmer error.\n\t\tpanic(fmt.Errorf(\"bad asset pattern: %w\", err))\n\t}\n\n\tisLogin, err := path.Match(\"/login.*\", p)\n\tif err != nil {\n\t\t// Same as above.\n\t\tpanic(fmt.Errorf(\"bad login pattern: %w\", err))\n\t}\n\n\t// TODO(s.chzhen):  Implement a more strict version.\n\tif strings.HasPrefix(p, \"/dns-query/\") {\n\t\treturn true\n\t}\n\n\tpaths := []string{\n\t\t\"/dns-query\",\n\t\t\"/control/login\",\n\t\t\"/apple/doh.mobileconfig\",\n\t\t\"/apple/dot.mobileconfig\",\n\t\t\"/control/install/get_addresses\",\n\t\t\"/control/install/check_config\",\n\t\t\"/control/install/configure\",\n\t\t\"/install.html\",\n\t}\n\n\treturn isAsset || isLogin || slices.Contains(paths, p)\n}\n\nconst (\n\t// errInvalidLogin is returned when there is an invalid login attempt.\n\terrInvalidLogin errors.Error = \"invalid username or password\"\n)\n\n// authMiddlewareDefaultConfig is the configuration structure for the default\n// authentication middleware.\ntype authMiddlewareDefaultConfig struct {\n\t// logger is used for logging the operation of the middleware.  It must not\n\t// be nil.\n\t//\n\t// TODO(e.burkov):  Require a logger in request's context instead.\n\tlogger *slog.Logger\n\n\t// rateLimiter manages the rate limiting for login attempts.\n\trateLimiter loginRateLimiter\n\n\t// trustedProxies is a set of subnets considered as trusted.\n\t//\n\t// TODO(s.chzhen):  Use it not only to pass it to the middleware but also to\n\t// log the work of the rate limiter.\n\ttrustedProxies netutil.SubnetSet\n\n\t// sessions contains web user sessions.  It must not be nil.\n\tsessions aghuser.SessionStorage\n\n\t// users contains web user information.  It must not be nil.\n\tusers aghuser.DB\n}\n\n// authMiddlewareDefault is the default authentication middleware.  It searches\n// for a web client using an authentication cookie or basic auth credentials and\n// passes it with the context.\ntype authMiddlewareDefault struct {\n\tlogger         *slog.Logger\n\trateLimiter    loginRateLimiter\n\ttrustedProxies netutil.SubnetSet\n\tsessions       aghuser.SessionStorage\n\tusers          aghuser.DB\n}\n\n// newAuthMiddlewareDefault returns the new properly initialized\n// *authMiddlewareDefault.\nfunc newAuthMiddlewareDefault(c *authMiddlewareDefaultConfig) (mw *authMiddlewareDefault) {\n\treturn &authMiddlewareDefault{\n\t\tlogger:         c.logger,\n\t\trateLimiter:    c.rateLimiter,\n\t\ttrustedProxies: c.trustedProxies,\n\t\tsessions:       c.sessions,\n\t\tusers:          c.users,\n\t}\n}\n\n// type check\nvar _ httputil.Middleware = (*authMiddlewareDefault)(nil)\n\n// Wrap implements the [httputil.Middleware] interface for\n// *authMiddlewareDefault.\nfunc (mw *authMiddlewareDefault) Wrap(h http.Handler) (wrapped http.Handler) {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\n\t\tif !mw.needsAuthentication(ctx) {\n\t\t\th.ServeHTTP(w, r)\n\n\t\t\treturn\n\t\t}\n\n\t\tpath := r.URL.Path\n\t\tif mw.handleAuthenticatedUser(ctx, w, r, h, path) {\n\t\t\treturn\n\t\t}\n\n\t\tif mw.handlePublicAccess(w, r, h, path) {\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\t})\n}\n\n// handleAuthenticatedUser tries to get user from request and processes request\n// if user was successfully authenticated.  Returns true if request was handled.\nfunc (mw *authMiddlewareDefault) handleAuthenticatedUser(\n\tctx context.Context,\n\tw http.ResponseWriter,\n\tr *http.Request,\n\th http.Handler,\n\tpath string,\n) (ok bool) {\n\tu, err := mw.userFromRequest(ctx, r)\n\tif err != nil {\n\t\tmw.logger.ErrorContext(ctx, \"retrieving user from request\", slogutil.KeyError, err)\n\t}\n\n\tif u == nil {\n\t\tmw.logger.DebugContext(ctx, \"no user found in request\")\n\n\t\treturn false\n\t}\n\n\tif path == \"/login.html\" {\n\t\thttp.Redirect(w, r, \"/\", http.StatusFound)\n\n\t\treturn true\n\t}\n\n\th.ServeHTTP(w, r.WithContext(withWebUser(ctx, u)))\n\n\treturn true\n}\n\n// handlePublicAccess handles request if user is trying to access public or root\n// pages.\nfunc (mw *authMiddlewareDefault) handlePublicAccess(\n\tw http.ResponseWriter,\n\tr *http.Request,\n\th http.Handler,\n\tpath string,\n) (ok bool) {\n\tif isPublicResource(path) {\n\t\th.ServeHTTP(w, r)\n\n\t\treturn true\n\t}\n\n\tif path == \"/\" || path == \"/index.html\" {\n\t\thttp.Redirect(w, r, \"login.html\", http.StatusFound)\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// needsAuthentication returns true if there are stored web users and requests\n// should be authenticated first.\nfunc (mw *authMiddlewareDefault) needsAuthentication(ctx context.Context) (ok bool) {\n\tusers, err := mw.users.All(ctx)\n\tif err != nil {\n\t\t// Should not happen.\n\t\tpanic(err)\n\t}\n\n\treturn len(users) != 0\n}\n\n// userFromRequest tries to retrieve a user based on the request.  r must not be\n// nil.\nfunc (mw *authMiddlewareDefault) userFromRequest(\n\tctx context.Context,\n\tr *http.Request,\n) (u *aghuser.User, err error) {\n\tdefer func() { err = errors.Annotate(err, \"getting user from request: %w\") }()\n\n\tcookie, err := r.Cookie(sessionCookieName)\n\tif err == nil {\n\t\treturn mw.userFromCookie(ctx, cookie.Value)\n\t}\n\n\treturn mw.userFromRequestBasicAuth(ctx, r)\n}\n\n// userFromCookie tries to retrieve a user based on the provided cookie value.\nfunc (mw *authMiddlewareDefault) userFromCookie(\n\tctx context.Context,\n\tval string,\n) (u *aghuser.User, err error) {\n\tt, err := sessionTokenFromHex(val)\n\tif err != nil {\n\t\t// Don't wrap the error because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\ts, err := mw.sessions.FindByToken(ctx, t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"searching session by token: %w\", err)\n\t}\n\n\tif s == nil {\n\t\treturn nil, nil\n\t}\n\n\tu, err = mw.users.ByLogin(ctx, s.UserLogin)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"searching user by login %q: %w\", s.UserLogin, err)\n\t}\n\n\treturn u, nil\n}\n\n// sessionTokenFromHex converts a hexadecimal string into a session token.\nfunc sessionTokenFromHex(val string) (token aghuser.SessionToken, err error) {\n\tsess, err := hex.DecodeString(val)\n\tif err != nil {\n\t\treturn token, fmt.Errorf(\"decoding value: %w\", err)\n\t}\n\n\tl := aghuser.SessionTokenLength\n\n\terr = validate.Equal(\"token length\", l, len(sess))\n\tif err != nil {\n\t\t// Don't wrap the error because it's informative enough as is.\n\t\treturn token, err\n\t}\n\n\treturn aghuser.SessionToken(sess), nil\n}\n\n// userFromRequestBasicAuth searches for a user using Basic Auth credentials.  r\n// must not be nil.\nfunc (mw *authMiddlewareDefault) userFromRequestBasicAuth(\n\tctx context.Context,\n\tr *http.Request,\n) (user *aghuser.User, err error) {\n\tlogin, pass, ok := r.BasicAuth()\n\tif !ok {\n\t\treturn nil, nil\n\t}\n\n\tvar remoteIP string\n\t// The real IP address of the client [realIP] cannot be used here without\n\t// taking trusted proxies into account due to security issues:\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.\n\tif remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {\n\t\treturn nil, fmt.Errorf(\"getting remote address: %w\", err)\n\t}\n\n\trateLimiter := mw.rateLimiter\n\tif left := rateLimiter.check(remoteIP); left > 0 {\n\t\treturn nil, fmt.Errorf(\"login attempt blocked for %s\", left)\n\t}\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\trateLimiter.inc(remoteIP)\n\n\t\t\treturn\n\t\t}\n\n\t\trateLimiter.remove(remoteIP)\n\t}()\n\n\tuser, _ = mw.users.ByLogin(ctx, aghuser.Login(login))\n\tif user == nil {\n\t\treturn nil, errInvalidLogin\n\t}\n\n\tok = user.Password.Authenticate(ctx, pass)\n\tif !ok {\n\t\treturn nil, errInvalidLogin\n\t}\n\n\treturn user, nil\n}\n"
  },
  {
    "path": "internal/home/authhttp_internal_test.go",
    "content": "package home\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"maps\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/netip\"\n\t\"net/textproto\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghuser\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// testSessionStorage is the mock implementation of the [aghuser.SessionStorage]\n// interface.\ntype testSessionStorage struct {\n\tonNew         func(ctx context.Context, u *aghuser.User) (s *aghuser.Session, err error)\n\tonFindByToken func(\n\t\tctx context.Context,\n\t\tt aghuser.SessionToken,\n\t) (s *aghuser.Session, err error)\n\tonDeleteByToken func(ctx context.Context, t aghuser.SessionToken) (err error)\n\tonClose         func() (err error)\n}\n\n// type check\nvar _ aghuser.SessionStorage = (*testSessionStorage)(nil)\n\n// newTestSessionStorage returns a new *testSessionStorage all methods of which\n// panic.\nfunc newTestSessionStorage() (ts *testSessionStorage) {\n\treturn &testSessionStorage{\n\t\tonNew: func(ctx context.Context, u *aghuser.User) (_ *aghuser.Session, _ error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx, u))\n\t\t},\n\t\tonFindByToken: func(\n\t\t\tctx context.Context,\n\t\t\tt aghuser.SessionToken,\n\t\t) (_ *aghuser.Session, err error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx, t))\n\t\t},\n\t\tonDeleteByToken: func(ctx context.Context, t aghuser.SessionToken) (_ error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx, t))\n\t\t},\n\t\tonClose: func() (_ error) {\n\t\t\tpanic(testutil.UnexpectedCall())\n\t\t},\n\t}\n}\n\n// New implements the [aghuser.SessionStorage] interface for\n// *testSessionStorage.\nfunc (ts *testSessionStorage) New(\n\tctx context.Context,\n\tu *aghuser.User,\n) (s *aghuser.Session, err error) {\n\treturn ts.onNew(ctx, u)\n}\n\n// FindByToken implements the [aghuser.SessionStorage] interface for\n// *testSessionStorage.\nfunc (ts *testSessionStorage) FindByToken(\n\tctx context.Context,\n\tt aghuser.SessionToken,\n) (s *aghuser.Session, err error) {\n\treturn ts.onFindByToken(ctx, t)\n}\n\n// DeleteByToken implements the [aghuser.SessionStorage] interface for\n// *testSessionStorage.\nfunc (ts *testSessionStorage) DeleteByToken(\n\tctx context.Context,\n\tt aghuser.SessionToken,\n) (err error) {\n\treturn ts.onDeleteByToken(ctx, t)\n}\n\n// Close implements the [aghuser.SessionStorage] interface for\n// *testSessionStorage.\nfunc (ts *testSessionStorage) Close() (err error) {\n\treturn ts.onClose()\n}\n\n// testUsersDB is the mock implementation of the [aghuser.DB] interface.\ntype testUsersDB struct {\n\tonAll     func(ctx context.Context) (users []*aghuser.User, err error)\n\tonByLogin func(ctx context.Context, login aghuser.Login) (u *aghuser.User, err error)\n\tonByUUID  func(ctx context.Context, id aghuser.UserID) (u *aghuser.User, err error)\n\tonCreate  func(ctx context.Context, u *aghuser.User) (err error)\n}\n\n// newTestUsersDB returns a new *testUsersDB all methods of which panic.\nfunc newTestUsersDB() (ts *testUsersDB) {\n\treturn &testUsersDB{\n\t\tonAll: func(ctx context.Context) (_ []*aghuser.User, _ error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx))\n\t\t},\n\t\tonByLogin: func(ctx context.Context, l aghuser.Login) (_ *aghuser.User, _ error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx, l))\n\t\t},\n\t\tonByUUID: func(ctx context.Context, id aghuser.UserID) (_ *aghuser.User, _ error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx, id))\n\t\t},\n\t\tonCreate: func(ctx context.Context, u *aghuser.User) (_ error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx, u))\n\t\t},\n\t}\n}\n\n// type check\nvar _ aghuser.DB = (*testUsersDB)(nil)\n\n// All implements the [aghuser.DB] interface for *testUsersDB.\nfunc (db *testUsersDB) All(ctx context.Context) (users []*aghuser.User, err error) {\n\treturn db.onAll(ctx)\n}\n\n// ByLogin implements the [aghuser.DB] interface for *testUsersDB.\nfunc (db *testUsersDB) ByLogin(\n\tctx context.Context,\n\tlogin aghuser.Login,\n) (u *aghuser.User, err error) {\n\treturn db.onByLogin(ctx, login)\n}\n\n// ByUUID implements the [aghuser.DB] interface for *testUsersDB.\nfunc (db *testUsersDB) ByUUID(ctx context.Context, id aghuser.UserID) (u *aghuser.User, err error) {\n\treturn db.onByUUID(ctx, id)\n}\n\n// Create implements the [aghuser.DB] interface for *testUsersDB.\nfunc (db *testUsersDB) Create(ctx context.Context, u *aghuser.User) (err error) {\n\treturn db.onCreate(ctx, u)\n}\n\n// testAuthHandler is a helper handler used for testing HTTP middleware.\ntype testAuthHandler struct {\n\tuser   *aghuser.User\n\tcalled bool\n}\n\n// type check\nvar _ http.Handler = (*testAuthHandler)(nil)\n\n// ServeHTTP implements the [http.Handler] interface for *testAuthHandler.\nfunc (h *testAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\th.called = true\n\th.user, _ = webUserFromContext(r.Context())\n}\n\nfunc TestAuthMiddlewareDefault(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tloginStr    = \"user_login\"\n\t\tpasswordStr = \"user_password\"\n\n\t\tlogin = aghuser.Login(loginStr)\n\t)\n\n\tpasswordHash, err := bcrypt.GenerateFromPassword(\n\t\t[]byte(passwordStr),\n\t\tbcrypt.DefaultCost,\n\t)\n\trequire.NoError(t, err)\n\n\tuser := &aghuser.User{\n\t\tLogin:    login,\n\t\tPassword: aghuser.NewDefaultPassword(string(passwordHash)),\n\t}\n\n\tvar token aghuser.SessionToken\n\t_, _ = rand.Read(token[:])\n\n\ttokenHex := hex.EncodeToString(token[:])\n\n\tusers := map[aghuser.Login]*aghuser.User{login: user}\n\tusersDB := newTestUsersDB()\n\tusersDB.onAll = func(_ context.Context) (us []*aghuser.User, err error) {\n\t\treturn slices.Collect(maps.Values(users)), nil\n\t}\n\n\tusersDB.onByLogin = func(_ context.Context, login aghuser.Login) (u *aghuser.User, err error) {\n\t\treturn users[login], nil\n\t}\n\n\tsessions := map[aghuser.SessionToken]*aghuser.Session{\n\t\ttoken: {\n\t\t\tUserLogin: login,\n\t\t},\n\t}\n\tts := newTestSessionStorage()\n\tts.onFindByToken = func(\n\t\t_ context.Context,\n\t\tt aghuser.SessionToken,\n\t) (s *aghuser.Session, err error) {\n\t\treturn sessions[t], nil\n\t}\n\n\tmw := newAuthMiddlewareDefault(&authMiddlewareDefaultConfig{\n\t\tlogger:      testLogger,\n\t\trateLimiter: emptyRateLimiter{},\n\t\tsessions:    ts,\n\t\tusers:       usersDB,\n\t})\n\n\tcookie := &http.Cookie{Name: sessionCookieName, Value: tokenHex}\n\tinvalidCookie := &http.Cookie{Name: sessionCookieName, Value: \"123\"}\n\n\ttestCases := []struct {\n\t\treq      *http.Request\n\t\twantUser *aghuser.User\n\t\tname     string\n\t\twantCode int\n\t}{{\n\t\treq:      httptest.NewRequest(http.MethodGet, \"/\", nil),\n\t\twantUser: nil,\n\t\tname:     \"no_auth_root\",\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\treq:      httptest.NewRequest(http.MethodGet, \"/index.html\", nil),\n\t\twantUser: nil,\n\t\tname:     \"no_auth\",\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\treq:      authRequest(\"/\", invalidCookie, \"\", \"\"),\n\t\twantUser: nil,\n\t\tname:     \"invalid_auth\",\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\treq:      authRequest(\"/\", cookie, \"\", \"\"),\n\t\twantUser: user,\n\t\tname:     \"cookie\",\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\treq:      authRequest(\"/login.html\", cookie, \"\", \"\"),\n\t\twantUser: nil,\n\t\tname:     \"redirect\",\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\treq:      authRequest(\"/control/profile\", cookie, \"\", \"\"),\n\t\twantUser: user,\n\t\tname:     \"protected\",\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\treq:      authRequest(\"/control/profile\", invalidCookie, \"\", \"\"),\n\t\twantUser: nil,\n\t\tname:     \"no_auth_protected\",\n\t\twantCode: http.StatusUnauthorized,\n\t}, {\n\t\treq:      httptest.NewRequest(http.MethodGet, \"/control/login\", nil),\n\t\twantUser: nil,\n\t\tname:     \"public\",\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\treq:      authRequest(\"/\", nil, loginStr, passwordStr),\n\t\twantUser: user,\n\t\tname:     \"basic_auth\",\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\treq:      authRequest(\"/\", invalidCookie, \"\", \"\"),\n\t\twantUser: nil,\n\t\tname:     \"invalid_cookie\",\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\treq:      authRequest(\"/\", nil, \"invalid\", \"creds\"),\n\t\twantUser: nil,\n\t\tname:     \"invalid_basic_auth\",\n\t\twantCode: http.StatusFound,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\th := &testAuthHandler{}\n\t\t\twrapped := mw.Wrap(h)\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\twrapped.ServeHTTP(w, tc.req)\n\n\t\t\tassert.Equal(t, tc.wantCode, w.Code)\n\t\t\tassert.Equal(t, tc.wantUser, h.user)\n\t\t})\n\t}\n}\n\n// authRequest is a test helper function that returns a GET request configured\n// with the provided credentials and path.\nfunc authRequest(path string, c *http.Cookie, user, pass string) (r *http.Request) {\n\tr = httptest.NewRequest(http.MethodGet, path, nil)\n\n\tif c != nil {\n\t\tr.AddCookie(c)\n\t}\n\n\tif user != \"\" {\n\t\tr.SetBasicAuth(user, pass)\n\t}\n\n\treturn r\n}\n\nfunc TestAuth_ServeHTTP_firstRun(t *testing.T) {\n\tstoreGlobals(t)\n\n\tmw := &webMw{}\n\tmux := http.NewServeMux()\n\thttpReg := aghhttp.NewDefaultRegistrar(mux, mw.wrap)\n\n\tweb := newTestWeb(t, &webConfig{\n\t\tmux:        mux,\n\t\thttpReg:    httpReg,\n\t\tisFirstRun: true,\n\t})\n\n\tglobalContext.web = web\n\tmw.set(web)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tpath     string\n\t\tmethod   string\n\t\twantCode int\n\t}{{\n\t\tname:     \"root\",\n\t\tpath:     \"/\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"doh_mobileconfig\",\n\t\tpath:     \"/apple/doh.mobileconfig\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"dot_mobileconfig\",\n\t\tpath:     \"/apple/dot.mobileconfig\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"change_language\",\n\t\tpath:     \"/control/i18n/change_language\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"current_language\",\n\t\tpath:     \"/control/i18n/current_language\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"check_config\",\n\t\tpath:     \"/control/install/check_config\",\n\t\tmethod:   http.MethodPost,\n\t\twantCode: http.StatusBadRequest,\n\t}, {\n\t\tname:     \"configure\",\n\t\tpath:     \"/control/install/configure\",\n\t\tmethod:   http.MethodPost,\n\t\twantCode: http.StatusBadRequest,\n\t}, {\n\t\tname:     \"get_addresses\",\n\t\tpath:     \"/control/install/get_addresses\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tname:     \"login\",\n\t\tpath:     \"/control/login\",\n\t\tmethod:   http.MethodPost,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"logout\",\n\t\tpath:     \"/control/logout\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"profile\",\n\t\tpath:     \"/control/profile\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"profile_update\",\n\t\tpath:     \"/control/profile/update\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"status\",\n\t\tpath:     \"/control/status\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"update\",\n\t\tpath:     \"/control/update\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}, {\n\t\tname:     \"version\",\n\t\tpath:     \"/control/version.json\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusFound,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := httptest.NewRequest(tc.method, tc.path, nil)\n\n\t\t\th, pattern := mux.Handler(r)\n\t\t\trequire.NotEmpty(t, pattern)\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\th.ServeHTTP(w, r)\n\n\t\t\tassert.Equal(t, tc.wantCode, w.Code)\n\t\t})\n\t}\n}\n\nfunc TestAuth_ServeHTTP_auth(t *testing.T) {\n\tstoreGlobals(t)\n\n\tconst (\n\t\ttestTTL = 60\n\n\t\tglTokenFileSuffix = \"test\"\n\n\t\tuserName     = \"name\"\n\t\tuserPassword = \"password\"\n\t)\n\n\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)\n\trequire.NoError(t, err)\n\n\ttempDir := t.TempDir()\n\tglFilePrefix = tempDir + \"/gl_token_\"\n\tglTokenFile := glFilePrefix + glTokenFileSuffix\n\n\tglFileData := make([]byte, 4)\n\tbinary.NativeEndian.PutUint32(glFileData, uint32(time.Now().Unix()+testTTL))\n\n\terr = os.WriteFile(glTokenFile, glFileData, 0o644)\n\trequire.NoError(t, err)\n\n\tsessionsDB := filepath.Join(tempDir, \"sessions.db\")\n\n\tusers := []webUser{{\n\t\tName:         userName,\n\t\tPasswordHash: string(passwordHash),\n\t}}\n\n\tauth, err := newAuth(testutil.ContextWithTimeout(t, testTimeout), &authConfig{\n\t\tbaseLogger:     testLogger,\n\t\trateLimiter:    emptyRateLimiter{},\n\t\ttrustedProxies: testTrustedProxies,\n\t\tdbFilename:     sessionsDB,\n\t\tusers:          users,\n\t\tsessionTTL:     testTTL * time.Second,\n\t\tisGLiNet:       false,\n\t})\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() { auth.close(testutil.ContextWithTimeout(t, testTimeout)) })\n\n\tmw := &webMw{}\n\tbaseMux := http.NewServeMux()\n\thttpReg := aghhttp.NewDefaultRegistrar(baseMux, mw.wrap)\n\n\ttlsMgr, err := newTLSManager(testutil.ContextWithTimeout(t, testTimeout), &tlsManagerConfig{\n\t\tlogger:       testLogger,\n\t\tconfModifier: agh.EmptyConfigModifier{},\n\t\tmanager:      aghtls.EmptyManager{},\n\t})\n\trequire.NoError(t, err)\n\n\tweb := newTestWeb(t, &webConfig{\n\t\ttlsManager: tlsMgr,\n\t\tauth:       auth,\n\t\tmux:        baseMux,\n\t\thttpReg:    httpReg,\n\t})\n\trequire.NoError(t, err)\n\n\tglobalContext.web = web\n\tmw.set(web)\n\n\tmux := auth.middleware().Wrap(baseMux)\n\n\tauth.isGLiNet = true\n\tgliNetMw := auth.middleware().Wrap(baseMux)\n\n\tloginCookie := generateAuthCookie(t, mux, userName, userPassword)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tpath     string\n\t\tmethod   string\n\t\twantCode int\n\t}{{\n\t\tname:     \"change_language\",\n\t\tpath:     \"/control/i18n/change_language\",\n\t\tmethod:   http.MethodPost,\n\t\twantCode: http.StatusInternalServerError,\n\t}, {\n\t\tname:     \"current_language\",\n\t\tpath:     \"/control/i18n/current_language\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tname:     \"profile\",\n\t\tpath:     \"/control/profile\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tname:     \"profile_update\",\n\t\tpath:     \"/control/profile/update\",\n\t\tmethod:   http.MethodPut,\n\t\twantCode: http.StatusBadRequest,\n\t}, {\n\t\tname:     \"status\",\n\t\tpath:     \"/control/status\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\tname:     \"version\",\n\t\tpath:     \"/control/version.json\",\n\t\tmethod:   http.MethodGet,\n\t\twantCode: http.StatusOK,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tr := httptest.NewRequest(tc.method, tc.path, nil)\n\t\t\tassertHandlerStatusCode(t, mux, r, http.StatusUnauthorized)\n\n\t\t\tr = httptest.NewRequest(tc.method, tc.path, nil)\n\t\t\tr.SetBasicAuth(userName, userPassword)\n\t\t\tassertHandlerStatusCode(t, mux, r, tc.wantCode)\n\n\t\t\tr = httptest.NewRequest(tc.method, tc.path, nil)\n\t\t\tr.AddCookie(loginCookie)\n\t\t\tassertHandlerStatusCode(t, mux, r, tc.wantCode)\n\n\t\t\tr.AddCookie(&http.Cookie{Name: glCookieName, Value: \"test\"})\n\t\t\tassertHandlerStatusCode(t, gliNetMw, r, tc.wantCode)\n\t\t})\n\t}\n}\n\n// generateAuthCookie is a helper function that logs in with the provided\n// credentials and returns the resulting authentication cookie.\nfunc generateAuthCookie(tb testing.TB, mux http.Handler, name, password string) (ac *http.Cookie) {\n\ttb.Helper()\n\n\tcreds, err := json.Marshal(&loginJSON{Name: name, Password: password})\n\trequire.NoError(tb, err)\n\n\tr := httptest.NewRequest(http.MethodPost, \"/control/login\", bytes.NewReader(creds))\n\tr.Header.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)\n\n\tw := httptest.NewRecorder()\n\tmux.ServeHTTP(w, r)\n\n\tfor _, c := range w.Result().Cookies() {\n\t\tif c.Name == sessionCookieName {\n\t\t\tac = c\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotNil(tb, ac)\n\n\treturn ac\n}\n\n// assertHandlerStatusCode is a helper function that asserts the response status\n// code of a HTTP handler.\nfunc assertHandlerStatusCode(tb testing.TB, h http.Handler, r *http.Request, wantCode int) {\n\ttb.Helper()\n\n\tw := httptest.NewRecorder()\n\th.ServeHTTP(w, r)\n\n\tassert.Equal(tb, wantCode, w.Code)\n}\n\nfunc TestAuth_ServeHTTP_logout(t *testing.T) {\n\tstoreGlobals(t)\n\n\tconst (\n\t\ttestTTL = 60\n\n\t\tuserName     = \"name\"\n\t\tuserPassword = \"password\"\n\t)\n\n\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)\n\trequire.NoError(t, err)\n\n\tsessionsDB := filepath.Join(t.TempDir(), \"sessions.db\")\n\n\tusers := []webUser{{\n\t\tName:         userName,\n\t\tPasswordHash: string(passwordHash),\n\t}}\n\n\tauth, err := newAuth(testutil.ContextWithTimeout(t, testTimeout), &authConfig{\n\t\tbaseLogger:     testLogger,\n\t\trateLimiter:    emptyRateLimiter{},\n\t\ttrustedProxies: testTrustedProxies,\n\t\tdbFilename:     sessionsDB,\n\t\tusers:          users,\n\t\tsessionTTL:     testTTL * time.Second,\n\t\tisGLiNet:       false,\n\t})\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() { auth.close(testutil.ContextWithTimeout(t, testTimeout)) })\n\n\tmw := &webMw{}\n\tbaseMux := http.NewServeMux()\n\thttpReg := aghhttp.NewDefaultRegistrar(baseMux, mw.wrap)\n\n\tweb := newTestWeb(t, &webConfig{\n\t\tauth:    auth,\n\t\tmux:     baseMux,\n\t\thttpReg: httpReg,\n\t})\n\trequire.NoError(t, err)\n\n\tglobalContext.web = web\n\tmw.set(web)\n\n\tmux := auth.middleware().Wrap(baseMux)\n\n\tloginCookie := generateAuthCookie(t, mux, userName, userPassword)\n\n\tr := httptest.NewRequest(http.MethodGet, \"/control/profile\", nil)\n\tr.AddCookie(loginCookie)\n\tassertHandlerStatusCode(t, mux, r, http.StatusOK)\n\n\tr = httptest.NewRequest(http.MethodGet, \"/control/logout\", nil)\n\tr.AddCookie(loginCookie)\n\tassertHandlerStatusCode(t, mux, r, http.StatusFound)\n\n\tr = httptest.NewRequest(http.MethodGet, \"/control/profile\", nil)\n\tr.AddCookie(loginCookie)\n\tassertHandlerStatusCode(t, mux, r, http.StatusUnauthorized)\n}\n\nfunc TestRealIP(t *testing.T) {\n\tconst remoteAddr = \"1.2.3.4:5678\"\n\n\ttestCases := []struct {\n\t\tname       string\n\t\theader     http.Header\n\t\tremoteAddr string\n\t\twantErrMsg string\n\t\twantIP     netip.Addr\n\t}{{\n\t\tname:       \"success_no_proxy\",\n\t\theader:     nil,\n\t\tremoteAddr: remoteAddr,\n\t\twantErrMsg: \"\",\n\t\twantIP:     netip.MustParseAddr(\"1.2.3.4\"),\n\t}, {\n\t\tname: \"success_proxy\",\n\t\theader: http.Header{\n\t\t\ttextproto.CanonicalMIMEHeaderKey(httphdr.XRealIP): []string{\"1.2.3.5\"},\n\t\t},\n\t\tremoteAddr: remoteAddr,\n\t\twantErrMsg: \"\",\n\t\twantIP:     netip.MustParseAddr(\"1.2.3.5\"),\n\t}, {\n\t\tname: \"success_proxy_multiple\",\n\t\theader: http.Header{\n\t\t\ttextproto.CanonicalMIMEHeaderKey(httphdr.XForwardedFor): []string{\n\t\t\t\t\"1.2.3.6, 1.2.3.5\",\n\t\t\t},\n\t\t},\n\t\tremoteAddr: remoteAddr,\n\t\twantErrMsg: \"\",\n\t\twantIP:     netip.MustParseAddr(\"1.2.3.6\"),\n\t}, {\n\t\tname:       \"error_no_proxy\",\n\t\theader:     nil,\n\t\tremoteAddr: \"1:::2\",\n\t\twantErrMsg: `getting ip from client addr: address 1:::2: ` +\n\t\t\t`too many colons in address`,\n\t\twantIP: netip.Addr{},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := &http.Request{\n\t\t\t\tHeader:     tc.header,\n\t\t\t\tRemoteAddr: tc.remoteAddr,\n\t\t\t}\n\n\t\t\tip, err := realIP(r)\n\t\t\tassert.Equal(t, tc.wantIP, ip)\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/home/authratelimiter.go",
    "content": "package home\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\n// failedAuthTTL is the period of time for which the failed attempt will stay in\n// cache.\nconst failedAuthTTL = 1 * time.Minute\n\n// loginRateLimiter is an interface for rate limiting login attempts.\ntype loginRateLimiter interface {\n\t// check returns the duration of time left until a user is unblocked.\n\t// A non-positive result indicates that the user is not blocked.\n\tcheck(usrID string) (left time.Duration)\n\n\t// inc records a failed login attempt for the specified user.\n\tinc(usrID string)\n\n\t// remove stops tracking and blocking of the specified user.\n\tremove(usrID string)\n}\n\n// emptyRateLimiter is the [loginRateLimiter] interface implementation that does\n// nothing.\ntype emptyRateLimiter struct{}\n\n// type check\nvar _ emptyRateLimiter = emptyRateLimiter{}\n\n// check implements the [loginRateLimiter] interface for emptyRateLimiter.  It\n// always returns zero.\nfunc (rl emptyRateLimiter) check(_ string) (left time.Duration) {\n\treturn 0\n}\n\n// inc implements the [loginRateLimiter] interface for emptyRateLimiter.\nfunc (rl emptyRateLimiter) inc(_ string) {}\n\n// remove implements the [loginRateLimiter] interface for emptyRateLimiter.\nfunc (rl emptyRateLimiter) remove(_ string) {}\n\n// failedAuth is an entry of authRateLimiter's cache.\ntype failedAuth struct {\n\tuntil time.Time\n\tnum   uint\n}\n\n// authRateLimiter used to cache failed authentication attempts.\ntype authRateLimiter struct {\n\tfailedAuths map[string]failedAuth\n\t// failedAuthsLock protects failedAuths.\n\tfailedAuthsLock sync.Mutex\n\tblockDur        time.Duration\n\tmaxAttempts     uint\n}\n\n// newAuthRateLimiter returns properly initialized *authRateLimiter.\nfunc newAuthRateLimiter(blockDur time.Duration, maxAttempts uint) (ab *authRateLimiter) {\n\treturn &authRateLimiter{\n\t\tfailedAuths: make(map[string]failedAuth),\n\t\tblockDur:    blockDur,\n\t\tmaxAttempts: maxAttempts,\n\t}\n}\n\n// type check\nvar _ loginRateLimiter = (*authRateLimiter)(nil)\n\n// cleanupLocked checks each blocked users removing ones with expired TTL.  For\n// internal use only.\nfunc (ab *authRateLimiter) cleanupLocked(now time.Time) {\n\tfor k, v := range ab.failedAuths {\n\t\tif now.After(v.until) {\n\t\t\tdelete(ab.failedAuths, k)\n\t\t}\n\t}\n}\n\n// checkLocked checks the attempter for it's state.  For internal use only.\nfunc (ab *authRateLimiter) checkLocked(usrID string, now time.Time) (left time.Duration) {\n\ta, ok := ab.failedAuths[usrID]\n\tif !ok {\n\t\treturn 0\n\t}\n\n\tif a.num < ab.maxAttempts {\n\t\treturn 0\n\t}\n\n\treturn a.until.Sub(now)\n}\n\n// check implements the [loginRateLimiter] interface for *authRateLimiter.\nfunc (ab *authRateLimiter) check(usrID string) (left time.Duration) {\n\tnow := time.Now()\n\n\tab.failedAuthsLock.Lock()\n\tdefer ab.failedAuthsLock.Unlock()\n\n\tab.cleanupLocked(now)\n\n\treturn ab.checkLocked(usrID, now)\n}\n\n// incLocked increments the number of unsuccessful attempts for attempter with\n// usrID and updates it's blocking moment if needed.  For internal use only.\nfunc (ab *authRateLimiter) incLocked(usrID string, now time.Time) {\n\tuntil := now.Add(failedAuthTTL)\n\tvar attNum uint = 1\n\n\ta, ok := ab.failedAuths[usrID]\n\tif ok {\n\t\tuntil = a.until\n\t\tattNum = a.num + 1\n\t}\n\tif attNum >= ab.maxAttempts {\n\t\tuntil = now.Add(ab.blockDur)\n\t}\n\n\tab.failedAuths[usrID] = failedAuth{\n\t\tnum:   attNum,\n\t\tuntil: until,\n\t}\n}\n\n// inc implements the [loginRateLimiter] interface for *authRateLimiter.\nfunc (ab *authRateLimiter) inc(usrID string) {\n\tnow := time.Now()\n\n\tab.failedAuthsLock.Lock()\n\tdefer ab.failedAuthsLock.Unlock()\n\n\tab.incLocked(usrID, now)\n}\n\n// remove implements the [loginRateLimiter] interface for *authRateLimiter.\nfunc (ab *authRateLimiter) remove(usrID string) {\n\tab.failedAuthsLock.Lock()\n\tdefer ab.failedAuthsLock.Unlock()\n\n\tdelete(ab.failedAuths, usrID)\n}\n"
  },
  {
    "path": "internal/home/authratelimiter_internal_test.go",
    "content": "package home\n\nimport (\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAuthRateLimiter_Cleanup(t *testing.T) {\n\tconst key = \"some-key\"\n\tnow := time.Now()\n\n\ttestCases := []struct {\n\t\tname    string\n\t\tatt     failedAuth\n\t\twantExp bool\n\t}{{\n\t\tname: \"expired\",\n\t\tatt: failedAuth{\n\t\t\tuntil: now.Add(-100 * time.Hour),\n\t\t},\n\t\twantExp: true,\n\t}, {\n\t\tname: \"nope_yet\",\n\t\tatt: failedAuth{\n\t\t\tuntil: now.Add(failedAuthTTL / 2),\n\t\t},\n\t\twantExp: false,\n\t}, {\n\t\tname: \"blocked\",\n\t\tatt: failedAuth{\n\t\t\tuntil: now.Add(100 * time.Hour),\n\t\t},\n\t\twantExp: false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tab := &authRateLimiter{\n\t\t\tfailedAuths: map[string]failedAuth{\n\t\t\t\tkey: tc.att,\n\t\t\t},\n\t\t}\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tab.cleanupLocked(now)\n\t\t\tif tc.wantExp {\n\t\t\t\tassert.Empty(t, ab.failedAuths)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Len(t, ab.failedAuths, 1)\n\n\t\t\t_, ok := ab.failedAuths[key]\n\t\t\trequire.True(t, ok)\n\t\t})\n\t}\n}\n\nfunc TestAuthRateLimiter_Check(t *testing.T) {\n\tkey := string(net.IP{127, 0, 0, 1})\n\tconst maxAtt = 1\n\tnow := time.Now()\n\n\ttestCases := []struct {\n\t\tuntil   time.Time\n\t\tname    string\n\t\tnum     uint\n\t\twantExp bool\n\t}{{\n\t\tuntil:   now.Add(-100 * time.Hour),\n\t\tname:    \"expired\",\n\t\tnum:     0,\n\t\twantExp: true,\n\t}, {\n\t\tuntil:   now.Add(failedAuthTTL),\n\t\tname:    \"not_blocked_but_tracked\",\n\t\tnum:     0,\n\t\twantExp: true,\n\t}, {\n\t\tuntil:   now,\n\t\tname:    \"expired_but_stayed\",\n\t\tnum:     2,\n\t\twantExp: true,\n\t}, {\n\t\tuntil:   now.Add(100 * time.Hour),\n\t\tname:    \"blocked\",\n\t\tnum:     2,\n\t\twantExp: false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tfailedAuths := map[string]failedAuth{\n\t\t\tkey: {\n\t\t\t\tnum:   tc.num,\n\t\t\t\tuntil: tc.until,\n\t\t\t},\n\t\t}\n\t\tab := &authRateLimiter{\n\t\t\tmaxAttempts: maxAtt,\n\t\t\tfailedAuths: failedAuths,\n\t\t}\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tuntil := ab.check(key)\n\n\t\t\tif tc.wantExp {\n\t\t\t\tassert.LessOrEqual(t, until, time.Duration(0))\n\t\t\t} else {\n\t\t\t\tassert.Greater(t, until, time.Duration(0))\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"non-existent\", func(t *testing.T) {\n\t\tab := &authRateLimiter{\n\t\t\tfailedAuths: map[string]failedAuth{\n\t\t\t\tkey + \"smthng\": {},\n\t\t\t},\n\t\t}\n\n\t\tuntil := ab.check(key)\n\n\t\tassert.Zero(t, until)\n\t})\n}\n\nfunc TestAuthRateLimiter_Inc(t *testing.T) {\n\tip := net.IP{127, 0, 0, 1}\n\tkey := string(ip)\n\tnow := time.Now()\n\tconst maxAtt = 2\n\tconst blockDur = 15 * time.Minute\n\n\ttestCases := []struct {\n\t\tuntil     time.Time\n\t\twantUntil time.Time\n\t\tname      string\n\t\tnum       uint\n\t\twantNum   uint\n\t}{{\n\t\tname:      \"only_inc\",\n\t\tuntil:     now,\n\t\twantUntil: now,\n\t\tnum:       maxAtt - 1,\n\t\twantNum:   maxAtt,\n\t}, {\n\t\tname:      \"inc_and_block\",\n\t\tuntil:     now,\n\t\twantUntil: now.Add(failedAuthTTL),\n\t\tnum:       maxAtt,\n\t\twantNum:   maxAtt + 1,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tfailedAuths := map[string]failedAuth{\n\t\t\tkey: {\n\t\t\t\tnum:   tc.num,\n\t\t\t\tuntil: tc.until,\n\t\t\t},\n\t\t}\n\t\tab := &authRateLimiter{\n\t\t\tblockDur:    blockDur,\n\t\t\tmaxAttempts: maxAtt,\n\t\t\tfailedAuths: failedAuths,\n\t\t}\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tab.inc(key)\n\n\t\t\ta, ok := ab.failedAuths[key]\n\t\t\trequire.True(t, ok)\n\n\t\t\tassert.Equal(t, tc.wantNum, a.num)\n\t\t\tassert.LessOrEqual(t, tc.wantUntil.Unix(), a.until.Unix())\n\t\t})\n\t}\n\n\tt.Run(\"non-existent\", func(t *testing.T) {\n\t\tab := &authRateLimiter{\n\t\t\tblockDur:    blockDur,\n\t\t\tmaxAttempts: maxAtt,\n\t\t\tfailedAuths: map[string]failedAuth{},\n\t\t}\n\n\t\tab.inc(key)\n\n\t\ta, ok := ab.failedAuths[key]\n\t\trequire.True(t, ok)\n\t\tassert.EqualValues(t, 1, a.num)\n\t})\n}\n\nfunc TestAuthRateLimiter_Remove(t *testing.T) {\n\tconst key = \"some-key\"\n\n\tfailedAuths := map[string]failedAuth{\n\t\tkey: {},\n\t}\n\tab := &authRateLimiter{\n\t\tfailedAuths: failedAuths,\n\t}\n\n\tab.remove(key)\n\n\tassert.Empty(t, ab.failedAuths)\n}\n"
  },
  {
    "path": "internal/home/clients.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/arpdb\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/querylog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/schedule\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n)\n\n// clientsContainer is the storage of all runtime and persistent clients.\ntype clientsContainer struct {\n\t// baseLogger is used to create loggers with custom prefixes for safe search\n\t// filter.  It must not be nil.\n\tbaseLogger *slog.Logger\n\n\t// logger is used for logging the operation of the client container.  It\n\t// must not be nil.\n\tlogger *slog.Logger\n\n\t// storage stores information about persistent clients.\n\tstorage *client.Storage\n\n\t// clientChecker checks if a client is blocked by the current access\n\t// settings.\n\tclientChecker BlockedClientChecker\n\n\t// confModifier is used to update the global configuration.  It must not be\n\t// nil.\n\tconfModifier agh.ConfigModifier\n\n\t// httpReg registers HTTP handlers.  It must not be nil.\n\thttpReg aghhttp.Registrar\n\n\t// lock protects all fields.\n\t//\n\t// TODO(a.garipov): Use a pointer and describe which fields are protected in\n\t// more detail.  Use sync.RWMutex.\n\tlock sync.Mutex\n\n\t// safeSearchCacheSize is the size of the safe search cache to use for\n\t// persistent clients.\n\tsafeSearchCacheSize uint\n\n\t// safeSearchCacheTTL is the TTL of the safe search cache to use for\n\t// persistent clients.\n\tsafeSearchCacheTTL time.Duration\n}\n\n// BlockedClientChecker checks if a client is blocked by the current access\n// settings.\ntype BlockedClientChecker interface {\n\t// TODO(s.chzhen):  Accept [client.FindParams].\n\tIsBlockedClient(ip netip.Addr, clientID string) (blocked bool, rule string)\n}\n\n// Init initializes the clients container.  All arguments must not be nil except\n// for objects.\n//\n// NOTE:  This function must be called only once.\n//\n// TODO(s.chzhen):  Use a configuration structure.\nfunc (clients *clientsContainer) Init(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tobjects []*clientObject,\n\tdhcpServer client.DHCP,\n\tetcHosts *aghnet.HostsContainer,\n\tarpDB arpdb.Interface,\n\tfilteringConf *filtering.Config,\n\tsigHdlr *signalHandler,\n\tconfModifier agh.ConfigModifier,\n\thttpReg aghhttp.Registrar,\n) (err error) {\n\t// TODO(s.chzhen):  Refactor it.\n\tif clients.storage != nil {\n\t\treturn errors.Error(\"clients container already initialized\")\n\t}\n\n\tclients.baseLogger = baseLogger\n\tclients.logger = baseLogger.With(slogutil.KeyPrefix, \"client_container\")\n\tclients.safeSearchCacheSize = filteringConf.SafeSearchCacheSize\n\tclients.safeSearchCacheTTL = time.Minute * time.Duration(filteringConf.CacheTime)\n\tclients.confModifier = confModifier\n\tclients.httpReg = httpReg\n\n\tconfClients := make([]*client.Persistent, 0, len(objects))\n\tfor i, o := range objects {\n\t\tvar p *client.Persistent\n\t\tp, err = o.toPersistent(ctx, baseLogger, clients.safeSearchCacheSize, clients.safeSearchCacheTTL)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"init persistent client at index %d: %w\", i, err)\n\t\t}\n\n\t\tconfClients = append(confClients, p)\n\t}\n\n\t// The clients.etcHosts may be nil even if config.Clients.Sources.HostsFile\n\t// is true, because of the deprecated option --no-etc-hosts.\n\t//\n\t// TODO(e.burkov):  The option should probably be returned, since hosts file\n\t// currently used not only for clients' information enrichment, but also in\n\t// the filtering module and upstream addresses resolution.\n\tvar hosts client.HostsContainer\n\tif config.Clients.Sources.HostsFile && etcHosts != nil {\n\t\thosts = etcHosts\n\t}\n\n\tclients.storage, err = client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger:             baseLogger,\n\t\tLogger:                 baseLogger.With(slogutil.KeyPrefix, \"client_storage\"),\n\t\tClock:                  timeutil.SystemClock{},\n\t\tInitialClients:         confClients,\n\t\tDHCP:                   dhcpServer,\n\t\tEtcHosts:               hosts,\n\t\tARPDB:                  arpDB,\n\t\tARPClientsUpdatePeriod: arpClientsUpdatePeriod,\n\t\tRuntimeSourceDHCP:      config.Clients.Sources.DHCP,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"init client storage: %w\", err)\n\t}\n\n\tsigHdlr.addClientStorage(clients.storage)\n\n\tfilteringConf.ApplyClientFiltering = clients.storage.ApplyClientFiltering\n\n\treturn nil\n}\n\n// webHandlersRegistered prevents a [clientsContainer] from registering its web\n// handlers more than once.\n//\n// TODO(a.garipov): Refactor HTTP handler registration logic.\nvar webHandlersRegistered = false\n\n// Start starts the clients container.\nfunc (clients *clientsContainer) Start(ctx context.Context) (err error) {\n\tif !webHandlersRegistered {\n\t\twebHandlersRegistered = true\n\t\tclients.registerWebHandlers()\n\t}\n\n\treturn clients.storage.Start(ctx)\n}\n\n// clientObject is the YAML representation of a persistent client.\ntype clientObject struct {\n\tSafeSearchConf filtering.SafeSearchConfig `yaml:\"safe_search\"`\n\n\t// BlockedServices is the configuration of blocked services of a client.\n\tBlockedServices *filtering.BlockedServices `yaml:\"blocked_services\"`\n\n\tName string `yaml:\"name\"`\n\n\tIDs       []string `yaml:\"ids\"`\n\tTags      []string `yaml:\"tags\"`\n\tUpstreams []string `yaml:\"upstreams\"`\n\n\t// UID is the unique identifier of the persistent client.\n\tUID client.UID `yaml:\"uid\"`\n\n\t// UpstreamsCacheSize is the DNS cache size (in bytes).\n\t//\n\t// TODO(d.kolyshev): Use [datasize.Bytesize].\n\tUpstreamsCacheSize uint32 `yaml:\"upstreams_cache_size\"`\n\n\t// UpstreamsCacheEnabled indicates if the DNS cache is enabled.\n\tUpstreamsCacheEnabled bool `yaml:\"upstreams_cache_enabled\"`\n\n\tUseGlobalSettings        bool `yaml:\"use_global_settings\"`\n\tFilteringEnabled         bool `yaml:\"filtering_enabled\"`\n\tParentalEnabled          bool `yaml:\"parental_enabled\"`\n\tSafeBrowsingEnabled      bool `yaml:\"safebrowsing_enabled\"`\n\tUseGlobalBlockedServices bool `yaml:\"use_global_blocked_services\"`\n\n\tIgnoreQueryLog   bool `yaml:\"ignore_querylog\"`\n\tIgnoreStatistics bool `yaml:\"ignore_statistics\"`\n}\n\n// toPersistent returns an initialized persistent client if there are no errors.\nfunc (o *clientObject) toPersistent(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tsafeSearchCacheSize uint,\n\tsafeSearchCacheTTL time.Duration,\n) (cli *client.Persistent, err error) {\n\tcli = &client.Persistent{\n\t\tName: o.Name,\n\n\t\tUpstreams: o.Upstreams,\n\n\t\tUID: o.UID,\n\n\t\tUseOwnSettings:        !o.UseGlobalSettings,\n\t\tFilteringEnabled:      o.FilteringEnabled,\n\t\tParentalEnabled:       o.ParentalEnabled,\n\t\tSafeSearchConf:        o.SafeSearchConf,\n\t\tSafeBrowsingEnabled:   o.SafeBrowsingEnabled,\n\t\tUseOwnBlockedServices: !o.UseGlobalBlockedServices,\n\t\tIgnoreQueryLog:        o.IgnoreQueryLog,\n\t\tIgnoreStatistics:      o.IgnoreStatistics,\n\t\tUpstreamsCacheEnabled: o.UpstreamsCacheEnabled,\n\t\tUpstreamsCacheSize:    o.UpstreamsCacheSize,\n\t}\n\n\terr = cli.SetIDs(o.IDs)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing ids: %w\", err)\n\t}\n\n\tif (cli.UID == client.UID{}) {\n\t\tcli.UID, err = client.NewUID()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"generating uid: %w\", err)\n\t\t}\n\t}\n\n\tif o.SafeSearchConf.Enabled {\n\t\tlogger := baseLogger.With(\n\t\t\tslogutil.KeyPrefix, safesearch.LogPrefix,\n\t\t\tsafesearch.LogKeyClient, cli.Name,\n\t\t)\n\t\tvar ss *safesearch.Default\n\t\tss, err = safesearch.NewDefault(ctx, &safesearch.DefaultConfig{\n\t\t\tLogger:         logger,\n\t\t\tServicesConfig: o.SafeSearchConf,\n\t\t\tClientName:     cli.Name,\n\t\t\tCacheSize:      safeSearchCacheSize,\n\t\t\tCacheTTL:       safeSearchCacheTTL,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"init safesearch %q: %w\", cli.Name, err)\n\t\t}\n\n\t\tcli.SafeSearch = ss\n\t}\n\n\tif o.BlockedServices == nil {\n\t\to.BlockedServices = &filtering.BlockedServices{\n\t\t\tSchedule: schedule.EmptyWeekly(),\n\t\t}\n\t}\n\n\to.BlockedServices.FilterUnknownIDs(ctx, baseLogger)\n\terr = o.BlockedServices.Validate()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"init blocked services %q: %w\", cli.Name, err)\n\t}\n\n\tcli.BlockedServices = o.BlockedServices.Clone()\n\n\tcli.Tags = slices.Clone(o.Tags)\n\n\treturn cli, nil\n}\n\n// forConfig returns all currently known persistent clients as objects for the\n// configuration file.\nfunc (clients *clientsContainer) forConfig() (objs []*clientObject) {\n\tclients.lock.Lock()\n\tdefer clients.lock.Unlock()\n\n\tobjs = make([]*clientObject, 0, clients.storage.Size())\n\tclients.storage.RangeByName(func(cli *client.Persistent) (cont bool) {\n\t\tobjs = append(objs, &clientObject{\n\t\t\tName: cli.Name,\n\n\t\t\tBlockedServices: cli.BlockedServices.Clone(),\n\n\t\t\tIDs:       cli.Identifiers(),\n\t\t\tTags:      slices.Clone(cli.Tags),\n\t\t\tUpstreams: slices.Clone(cli.Upstreams),\n\n\t\t\tUID: cli.UID,\n\n\t\t\tUseGlobalSettings:        !cli.UseOwnSettings,\n\t\t\tFilteringEnabled:         cli.FilteringEnabled,\n\t\t\tParentalEnabled:          cli.ParentalEnabled,\n\t\t\tSafeSearchConf:           cli.SafeSearchConf,\n\t\t\tSafeBrowsingEnabled:      cli.SafeBrowsingEnabled,\n\t\t\tUseGlobalBlockedServices: !cli.UseOwnBlockedServices,\n\t\t\tIgnoreQueryLog:           cli.IgnoreQueryLog,\n\t\t\tIgnoreStatistics:         cli.IgnoreStatistics,\n\t\t\tUpstreamsCacheEnabled:    cli.UpstreamsCacheEnabled,\n\t\t\tUpstreamsCacheSize:       cli.UpstreamsCacheSize,\n\t\t})\n\n\t\treturn true\n\t})\n\n\treturn objs\n}\n\n// arpClientsUpdatePeriod defines how often ARP clients are updated.\nconst arpClientsUpdatePeriod = 10 * time.Minute\n\n// findMultiple is a wrapper around [clientsContainer.find] to make it a valid\n// client finder for the query log.  c is never nil; if no information about the\n// client is found, it returns an artificial client record by only setting the\n// blocking-related fields.  err is always nil.\nfunc (clients *clientsContainer) findMultiple(ids []string) (c *querylog.Client, err error) {\n\tvar artClient *querylog.Client\n\tvar art bool\n\tfor _, id := range ids {\n\t\tip, _ := netip.ParseAddr(id)\n\t\tc, art = clients.clientOrArtificial(ip, id)\n\t\tif art {\n\t\t\tartClient = c\n\n\t\t\tcontinue\n\t\t}\n\n\t\treturn c, nil\n\t}\n\n\treturn artClient, nil\n}\n\n// clientOrArtificial returns information about one client.  If art is true,\n// this is an artificial client record, meaning that we currently don't have any\n// records about this client besides maybe whether or not it is blocked.  c is\n// never nil.\nfunc (clients *clientsContainer) clientOrArtificial(\n\tip netip.Addr,\n\tid string,\n) (c *querylog.Client, art bool) {\n\tdefer func() {\n\t\tc.Disallowed, c.DisallowedRule = clients.clientChecker.IsBlockedClient(ip, id)\n\t\tif c.WHOIS == nil {\n\t\t\tc.WHOIS = &whois.Info{}\n\t\t}\n\t}()\n\n\tcli, ok := clients.storage.FindLoose(ip, id)\n\tif ok {\n\t\treturn &querylog.Client{\n\t\t\tName:           cli.Name,\n\t\t\tIgnoreQueryLog: cli.IgnoreQueryLog,\n\t\t}, false\n\t}\n\n\trc := clients.storage.ClientRuntime(ip)\n\tif rc != nil {\n\t\t_, host := rc.Info()\n\n\t\treturn &querylog.Client{\n\t\t\tName:  host,\n\t\t\tWHOIS: rc.WHOIS(),\n\t\t}, false\n\t}\n\n\treturn &querylog.Client{\n\t\tName: \"\",\n\t}, true\n}\n\n// shouldCountClient is a wrapper around [client.Storage.Find] to make it a\n// valid client information finder for the statistics.  If no information about\n// the client is found, it returns true.  Values of ids must be either a valid\n// ClientID or a valid IP address.\n//\n// TODO(s.chzhen):  Accept [client.FindParams].\nfunc (clients *clientsContainer) shouldCountClient(ids []string) (y bool) {\n\tclients.lock.Lock()\n\tdefer clients.lock.Unlock()\n\n\tparams := &client.FindParams{}\n\tfor _, id := range ids {\n\t\terr := params.Set(id)\n\t\tif err != nil {\n\t\t\t// Should not happen.\n\t\t\tclients.logger.Warn(\"parsing find params\", slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tclient, ok := clients.storage.Find(params)\n\t\tif ok {\n\t\t\treturn !client.IgnoreStatistics\n\t\t}\n\t}\n\n\treturn true\n}\n\n// type check\nvar _ client.AddressUpdater = (*clientsContainer)(nil)\n\n// UpdateAddress implements the [client.AddressUpdater] interface for\n// *clientsContainer\nfunc (clients *clientsContainer) UpdateAddress(\n\tctx context.Context,\n\tip netip.Addr,\n\thost string,\n\tinfo *whois.Info,\n) {\n\tclients.storage.UpdateAddress(ctx, ip, host, info)\n}\n\n// close gracefully closes all the client-specific upstream configurations of\n// the persistent clients.\nfunc (clients *clientsContainer) close(ctx context.Context) (err error) {\n\treturn clients.storage.Shutdown(ctx)\n}\n"
  },
  {
    "path": "internal/home/clients_internal_test.go",
    "content": "package home\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// newClientsContainer is a helper that creates a new clients container for\n// tests.\nfunc newClientsContainer(tb testing.TB) (c *clientsContainer) {\n\ttb.Helper()\n\n\tc = &clientsContainer{}\n\n\tctx := testutil.ContextWithTimeout(tb, testTimeout)\n\terr := c.Init(\n\t\tctx,\n\t\ttestLogger,\n\t\tnil,\n\t\tclient.EmptyDHCP{},\n\t\tnil,\n\t\tnil,\n\t\t&filtering.Config{\n\t\t\tLogger: testLogger,\n\t\t},\n\t\tnewSignalHandler(testLogger, nil, nil),\n\t\tagh.EmptyConfigModifier{},\n\t\taghhttp.EmptyRegistrar{},\n\t)\n\n\trequire.NoError(tb, err)\n\n\treturn c\n}\n"
  },
  {
    "path": "internal/home/clientshttp.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/schedule\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// clientJSON is a common structure used by several handlers to deal with\n// clients.  Some of the fields are only necessary in one or two handlers and\n// are thus made pointers with an omitempty tag.\n//\n// TODO(a.garipov): Consider using nullbool and an optional string here?  Or\n// split into several structs?\ntype clientJSON struct {\n\t// Disallowed, if non-nil and false, means that the client's IP is\n\t// allowed.  Otherwise, the IP is blocked.\n\tDisallowed *bool `json:\"disallowed,omitempty\"`\n\n\t// DisallowedRule is the rule due to which the client is disallowed.\n\t// If Disallowed is true and this string is empty, the client IP is\n\t// disallowed by the \"allowed IP list\", that is it is not included in\n\t// the allowlist.\n\tDisallowedRule *string `json:\"disallowed_rule,omitempty\"`\n\n\t// WHOIS is the filtered WHOIS data of a client.\n\tWHOIS          *whois.Info                 `json:\"whois_info,omitempty\"`\n\tSafeSearchConf *filtering.SafeSearchConfig `json:\"safe_search\"`\n\n\t// Schedule is blocked services schedule for every day of the week.\n\tSchedule *schedule.Weekly `json:\"blocked_services_schedule\"`\n\n\tName string `json:\"name\"`\n\n\t// BlockedServices is the names of blocked services.\n\tBlockedServices []string `json:\"blocked_services\"`\n\tIDs             []string `json:\"ids\"`\n\tTags            []string `json:\"tags\"`\n\tUpstreams       []string `json:\"upstreams\"`\n\n\tFilteringEnabled    bool `json:\"filtering_enabled\"`\n\tParentalEnabled     bool `json:\"parental_enabled\"`\n\tSafeBrowsingEnabled bool `json:\"safebrowsing_enabled\"`\n\t// Deprecated: use safeSearchConf.\n\tSafeSearchEnabled        bool `json:\"safesearch_enabled\"`\n\tUseGlobalBlockedServices bool `json:\"use_global_blocked_services\"`\n\tUseGlobalSettings        bool `json:\"use_global_settings\"`\n\n\tIgnoreQueryLog   aghalg.NullBool `json:\"ignore_querylog\"`\n\tIgnoreStatistics aghalg.NullBool `json:\"ignore_statistics\"`\n\n\tUpstreamsCacheSize    uint32          `json:\"upstreams_cache_size\"`\n\tUpstreamsCacheEnabled aghalg.NullBool `json:\"upstreams_cache_enabled\"`\n}\n\n// runtimeClientJSON is a JSON representation of the [client.Runtime].\ntype runtimeClientJSON struct {\n\tWHOIS *whois.Info `json:\"whois_info\"`\n\n\tIP     netip.Addr    `json:\"ip\"`\n\tName   string        `json:\"name\"`\n\tSource client.Source `json:\"source\"`\n}\n\n// clientListJSON contains lists of persistent clients, runtime clients and also\n// supported tags.\ntype clientListJSON struct {\n\tClients        []*clientJSON       `json:\"clients\"`\n\tRuntimeClients []runtimeClientJSON `json:\"auto_clients\"`\n\tTags           []string            `json:\"supported_tags\"`\n}\n\n// whoisOrEmpty returns a WHOIS client information or a pointer to an empty\n// struct.  Frontend expects a non-nil value.\nfunc whoisOrEmpty(r *client.Runtime) (info *whois.Info) {\n\tinfo = r.WHOIS()\n\tif info != nil {\n\t\treturn info\n\t}\n\n\treturn &whois.Info{}\n}\n\n// handleGetClients is the handler for GET /control/clients HTTP API.\nfunc (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdata := clientListJSON{}\n\n\tclients.lock.Lock()\n\tdefer clients.lock.Unlock()\n\n\tclients.storage.RangeByName(func(c *client.Persistent) (cont bool) {\n\t\tcj := clientToJSON(c)\n\t\tdata.Clients = append(data.Clients, cj)\n\n\t\treturn true\n\t})\n\n\tclients.storage.UpdateDHCP(ctx)\n\n\tclients.storage.RangeRuntime(func(rc *client.Runtime) (cont bool) {\n\t\tsrc, host := rc.Info()\n\t\tcj := runtimeClientJSON{\n\t\t\tWHOIS:  whoisOrEmpty(rc),\n\t\t\tName:   host,\n\t\t\tSource: src,\n\t\t\tIP:     rc.Addr(),\n\t\t}\n\n\t\tdata.RuntimeClients = append(data.RuntimeClients, cj)\n\n\t\treturn true\n\t})\n\n\tdata.Tags = clients.storage.AllowedTags()\n\n\taghhttp.WriteJSONResponseOK(ctx, clients.logger, w, r, data)\n}\n\n// initPrev initializes the persistent client with the default or previous\n// client properties.\nfunc initPrev(cj clientJSON, prev *client.Persistent) (c *client.Persistent, err error) {\n\tvar (\n\t\tuid              client.UID\n\t\tignoreQueryLog   bool\n\t\tignoreStatistics bool\n\t\tupsCacheEnabled  bool\n\t\tupsCacheSize     uint32\n\t)\n\n\tif prev != nil {\n\t\tuid = prev.UID\n\t\tignoreQueryLog = prev.IgnoreQueryLog\n\t\tignoreStatistics = prev.IgnoreStatistics\n\t\tupsCacheEnabled = prev.UpstreamsCacheEnabled\n\t\tupsCacheSize = prev.UpstreamsCacheSize\n\t}\n\n\tif cj.IgnoreQueryLog != aghalg.NBNull {\n\t\tignoreQueryLog = cj.IgnoreQueryLog == aghalg.NBTrue\n\t}\n\n\tif cj.IgnoreStatistics != aghalg.NBNull {\n\t\tignoreStatistics = cj.IgnoreStatistics == aghalg.NBTrue\n\t}\n\n\tif cj.UpstreamsCacheEnabled != aghalg.NBNull {\n\t\tupsCacheEnabled = cj.UpstreamsCacheEnabled == aghalg.NBTrue\n\t\tupsCacheSize = cj.UpstreamsCacheSize\n\t}\n\n\tsvcs, err := copyBlockedServices(cj.Schedule, cj.BlockedServices, prev)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid blocked services: %w\", err)\n\t}\n\n\tif (uid == client.UID{}) {\n\t\tuid, err = client.NewUID()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"generating uid: %w\", err)\n\t\t}\n\t}\n\n\treturn &client.Persistent{\n\t\tBlockedServices:       svcs,\n\t\tUID:                   uid,\n\t\tIgnoreQueryLog:        ignoreQueryLog,\n\t\tIgnoreStatistics:      ignoreStatistics,\n\t\tUpstreamsCacheEnabled: upsCacheEnabled,\n\t\tUpstreamsCacheSize:    upsCacheSize,\n\t}, nil\n}\n\n// jsonToClient converts JSON object to persistent client object if there are no\n// errors.\nfunc (clients *clientsContainer) jsonToClient(\n\tctx context.Context,\n\tcj clientJSON,\n\tprev *client.Persistent,\n) (c *client.Persistent, err error) {\n\tc, err = initPrev(cj, prev)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\terr = c.SetIDs(cj.IDs)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tc.SafeSearchConf = copySafeSearch(cj.SafeSearchConf, cj.SafeSearchEnabled)\n\tc.Name = cj.Name\n\tc.Tags = cj.Tags\n\tc.Upstreams = cj.Upstreams\n\tc.UseOwnSettings = !cj.UseGlobalSettings\n\tc.FilteringEnabled = cj.FilteringEnabled\n\tc.ParentalEnabled = cj.ParentalEnabled\n\tc.SafeBrowsingEnabled = cj.SafeBrowsingEnabled\n\tc.UseOwnBlockedServices = !cj.UseGlobalBlockedServices\n\n\tif c.SafeSearchConf.Enabled {\n\t\tlogger := clients.baseLogger.With(\n\t\t\tslogutil.KeyPrefix, safesearch.LogPrefix,\n\t\t\tsafesearch.LogKeyClient, c.Name,\n\t\t)\n\t\tvar ss *safesearch.Default\n\t\tss, err = safesearch.NewDefault(ctx, &safesearch.DefaultConfig{\n\t\t\tLogger:         logger,\n\t\t\tServicesConfig: c.SafeSearchConf,\n\t\t\tClientName:     c.Name,\n\t\t\tCacheSize:      clients.safeSearchCacheSize,\n\t\t\tCacheTTL:       clients.safeSearchCacheTTL,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"creating safesearch for client %q: %w\", c.Name, err)\n\t\t}\n\n\t\tc.SafeSearch = ss\n\t}\n\n\treturn c, nil\n}\n\n// copySafeSearch returns safe search config created from provided parameters.\nfunc copySafeSearch(\n\tjsonConf *filtering.SafeSearchConfig,\n\tenabled bool,\n) (conf filtering.SafeSearchConfig) {\n\tif jsonConf != nil {\n\t\treturn *jsonConf\n\t}\n\n\t// TODO(d.kolyshev): Remove after cleaning the deprecated\n\t// [clientJSON.SafeSearchEnabled] field.\n\tconf = filtering.SafeSearchConfig{\n\t\tEnabled: enabled,\n\t}\n\n\t// Set default service flags for enabled safesearch.\n\tif conf.Enabled {\n\t\tconf.Bing = true\n\t\tconf.DuckDuckGo = true\n\t\tconf.Ecosia = true\n\t\tconf.Google = true\n\t\tconf.Pixabay = true\n\t\tconf.Yandex = true\n\t\tconf.YouTube = true\n\t}\n\n\treturn conf\n}\n\n// copyBlockedServices converts a json blocked services to an internal blocked\n// services.\nfunc copyBlockedServices(\n\tsch *schedule.Weekly,\n\tsvcStrs []string,\n\tprev *client.Persistent,\n) (svcs *filtering.BlockedServices, err error) {\n\tvar weekly *schedule.Weekly\n\tif sch != nil {\n\t\tweekly = sch.Clone()\n\t} else if prev != nil {\n\t\tweekly = prev.BlockedServices.Schedule.Clone()\n\t} else {\n\t\tweekly = schedule.EmptyWeekly()\n\t}\n\n\tsvcs = &filtering.BlockedServices{\n\t\tSchedule: weekly,\n\t\tIDs:      svcStrs,\n\t}\n\n\terr = svcs.Validate()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"validating blocked services: %w\", err)\n\t}\n\n\treturn svcs, nil\n}\n\n// clientToJSON converts persistent client object to JSON object.\nfunc clientToJSON(c *client.Persistent) (cj *clientJSON) {\n\t// TODO(d.kolyshev): Remove after cleaning the deprecated\n\t// [clientJSON.SafeSearchEnabled] field.\n\tcloneVal := c.SafeSearchConf\n\tsafeSearchConf := &cloneVal\n\n\treturn &clientJSON{\n\t\tName:                c.Name,\n\t\tIDs:                 c.Identifiers(),\n\t\tTags:                c.Tags,\n\t\tUseGlobalSettings:   !c.UseOwnSettings,\n\t\tFilteringEnabled:    c.FilteringEnabled,\n\t\tParentalEnabled:     c.ParentalEnabled,\n\t\tSafeSearchEnabled:   safeSearchConf.Enabled,\n\t\tSafeSearchConf:      safeSearchConf,\n\t\tSafeBrowsingEnabled: c.SafeBrowsingEnabled,\n\n\t\tUseGlobalBlockedServices: !c.UseOwnBlockedServices,\n\n\t\tSchedule:        c.BlockedServices.Schedule,\n\t\tBlockedServices: c.BlockedServices.IDs,\n\n\t\tUpstreams: c.Upstreams,\n\n\t\tIgnoreQueryLog:   aghalg.BoolToNullBool(c.IgnoreQueryLog),\n\t\tIgnoreStatistics: aghalg.BoolToNullBool(c.IgnoreStatistics),\n\n\t\tUpstreamsCacheSize:    c.UpstreamsCacheSize,\n\t\tUpstreamsCacheEnabled: aghalg.BoolToNullBool(c.UpstreamsCacheEnabled),\n\t}\n}\n\n// handleAddClient is the handler for POST /control/clients/add HTTP API.\nfunc (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := clients.logger\n\n\tcj := clientJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&cj)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"failed to process request body: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tc, err := clients.jsonToClient(ctx, cj, nil)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\terr = clients.storage.Add(ctx, c)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tclients.confModifier.Apply(ctx)\n}\n\n// handleDelClient is the handler for POST /control/clients/delete HTTP API.\nfunc (clients *clientsContainer) handleDelClient(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := clients.logger\n\n\tcj := clientJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&cj)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"failed to process request body: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tif len(cj.Name) == 0 {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"client's name must be non-empty\")\n\n\t\treturn\n\t}\n\n\tif !clients.storage.RemoveByName(ctx, cj.Name) {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"Client not found\")\n\n\t\treturn\n\t}\n\n\tclients.confModifier.Apply(ctx)\n}\n\n// updateJSON contains the name and data of the updated persistent client.\ntype updateJSON struct {\n\tName string     `json:\"name\"`\n\tData clientJSON `json:\"data\"`\n}\n\n// handleUpdateClient is the handler for POST /control/clients/update HTTP API.\n//\n// TODO(s.chzhen):  Accept updated parameters instead of whole structure.\nfunc (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := clients.logger\n\n\tdj := updateJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&dj)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"failed to process request body: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tif len(dj.Name) == 0 {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"Invalid request\")\n\n\t\treturn\n\t}\n\n\tc, err := clients.jsonToClient(ctx, dj.Data, nil)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\terr = clients.storage.Update(ctx, dj.Name, c)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tclients.confModifier.Apply(ctx)\n}\n\n// handleFindClient is the handler for GET /control/clients/find HTTP API.\n//\n// Deprecated:  Remove it when migration to the new API is over.\nfunc (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := clients.logger\n\n\tq := r.URL.Query()\n\tdata := make([]map[string]*clientJSON, 0, len(q))\n\tparams := &client.FindParams{}\n\tvar err error\n\n\tfor i := range len(q) {\n\t\tidStr := q.Get(fmt.Sprintf(\"ip%d\", i))\n\t\tif idStr == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\terr = params.Set(idStr)\n\t\tif err != nil {\n\t\t\tl.DebugContext(ctx, \"finding client\", \"id\", idStr, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tdata = append(data, map[string]*clientJSON{\n\t\t\tidStr: clients.findClient(idStr, params),\n\t\t})\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, data)\n}\n\n// findClient returns available information about a client by params from the\n// client's storage or access settings.  idStr is the string representation of\n// typed params.  params must not be nil.  cj is guaranteed to be non-nil.\nfunc (clients *clientsContainer) findClient(\n\tidStr string,\n\tparams *client.FindParams,\n) (cj *clientJSON) {\n\tc, ok := clients.storage.Find(params)\n\tif !ok {\n\t\treturn clients.findRuntime(idStr, params)\n\t}\n\n\tcj = clientToJSON(c)\n\tdisallowed, rule := clients.clientChecker.IsBlockedClient(\n\t\tparams.RemoteIP,\n\t\tstring(params.ClientID),\n\t)\n\tcj.Disallowed = &disallowed\n\n\tif disallowed && rule != \"\" {\n\t\t// Since \"disallowed_rule\" is omitted from JSON unless present, it\n\t\t// should only be set when the client is actually blocked.\n\t\tcj.DisallowedRule = &rule\n\t}\n\n\treturn cj\n}\n\n// searchQueryJSON is a request to the POST /control/clients/search HTTP API.\n//\n// TODO(s.chzhen):  Add UIDs.\ntype searchQueryJSON struct {\n\tClients []searchClientJSON `json:\"clients\"`\n}\n\n// searchClientJSON is a part of [searchQueryJSON] that contains a string\n// representation of the client's IP address, CIDR, MAC address, or ClientID.\ntype searchClientJSON struct {\n\tID string `json:\"id\"`\n}\n\n// handleSearchClient is the handler for the POST /control/clients/search HTTP\n// API.\nfunc (clients *clientsContainer) handleSearchClient(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := clients.logger\n\n\tq := searchQueryJSON{}\n\terr := json.NewDecoder(r.Body).Decode(&q)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"failed to process request body: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tdata := make([]map[string]*clientJSON, 0, len(q.Clients))\n\tparams := &client.FindParams{}\n\n\tfor _, c := range q.Clients {\n\t\tidStr := c.ID\n\t\terr = params.Set(idStr)\n\t\tif err != nil {\n\t\t\tl.DebugContext(ctx, \"searching client\", \"id\", idStr, slogutil.KeyError, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tdata = append(data, map[string]*clientJSON{\n\t\t\tidStr: clients.findClient(idStr, params),\n\t\t})\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, data)\n}\n\n// findRuntime looks up the IP in runtime and temporary storages, like\n// /etc/hosts tables, DHCP leases, or blocklists.  params must not be nil.  cj\n// is guaranteed to be non-nil.\nfunc (clients *clientsContainer) findRuntime(\n\tidStr string,\n\tparams *client.FindParams,\n) (cj *clientJSON) {\n\tvar host string\n\twhois := &whois.Info{}\n\n\tip := params.RemoteIP\n\trc := clients.storage.ClientRuntime(ip)\n\tif rc != nil {\n\t\t_, host = rc.Info()\n\t\twhois = whoisOrEmpty(rc)\n\t}\n\n\t// Check the DNS server's blocked IP list regardless of whether a runtime\n\t// client was found or not.  This is because it's still possible that the\n\t// runtime client associated with the IP address was stored previously, but\n\t// then the server was reloaded.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2428.\n\tdisallowed, rule := clients.clientChecker.IsBlockedClient(ip, string(params.ClientID))\n\n\tvar disallowedRule *string\n\tif disallowed && rule != \"\" {\n\t\t// Since \"disallowed_rule\" is omitted from JSON unless present, it\n\t\t// should only be set when the client is actually blocked.\n\t\tdisallowedRule = &rule\n\t}\n\n\treturn &clientJSON{\n\t\tName:           host,\n\t\tIDs:            []string{idStr},\n\t\tWHOIS:          whois,\n\t\tDisallowed:     &disallowed,\n\t\tDisallowedRule: disallowedRule,\n\t}\n}\n\n// registerWebHandlers registers HTTP handlers.\nfunc (clients *clientsContainer) registerWebHandlers() {\n\tclients.httpReg.Register(http.MethodGet, \"/control/clients\", clients.handleGetClients)\n\tclients.httpReg.Register(http.MethodPost, \"/control/clients/add\", clients.handleAddClient)\n\tclients.httpReg.Register(http.MethodPost, \"/control/clients/delete\", clients.handleDelClient)\n\tclients.httpReg.Register(http.MethodPost, \"/control/clients/update\", clients.handleUpdateClient)\n\tclients.httpReg.Register(http.MethodPost, \"/control/clients/search\", clients.handleSearchClient)\n\n\t// Deprecated handler.\n\tclients.httpReg.Register(http.MethodGet, \"/control/clients/find\", clients.handleFindClient)\n}\n"
  },
  {
    "path": "internal/home/clientshttp_internal_test.go",
    "content": "package home\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"slices\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/schedule\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is the common timeout for tests and contexts.\nconst testTimeout = 1 * time.Second\n\nconst (\n\ttestClientIP1 = \"1.1.1.1\"\n\ttestClientIP2 = \"2.2.2.2\"\n)\n\n// testBlockedClientChecker is a mock implementation of the\n// [BlockedClientChecker] interface.\ntype testBlockedClientChecker struct {\n\tonIsBlockedClient func(ip netip.Addr, clientiD string) (blocked bool, rule string)\n}\n\n// type check\nvar _ BlockedClientChecker = (*testBlockedClientChecker)(nil)\n\n// IsBlockedClient implements the [BlockedClientChecker] interface for\n// *testBlockedClientChecker.\nfunc (c *testBlockedClientChecker) IsBlockedClient(\n\tip netip.Addr,\n\tclientID string,\n) (blocked bool, rule string) {\n\treturn c.onIsBlockedClient(ip, clientID)\n}\n\n// newPersistentClient is a helper function that returns a persistent client\n// with the specified name and newly generated UID.\nfunc newPersistentClient(name string) (c *client.Persistent) {\n\treturn &client.Persistent{\n\t\tName: name,\n\t\tUID:  client.MustNewUID(),\n\t\tBlockedServices: &filtering.BlockedServices{\n\t\t\tSchedule: schedule.EmptyWeekly(),\n\t\t},\n\t}\n}\n\n// newPersistentClientWithIDs is a helper function that returns a persistent\n// client with the specified name and ids.\nfunc newPersistentClientWithIDs(tb testing.TB, name string, ids []string) (c *client.Persistent) {\n\ttb.Helper()\n\n\tc = newPersistentClient(name)\n\terr := c.SetIDs(ids)\n\trequire.NoError(tb, err)\n\n\treturn c\n}\n\n// assertClients is a helper function that compares lists of persistent clients.\nfunc assertClients(tb testing.TB, want, got []*client.Persistent) {\n\ttb.Helper()\n\n\trequire.Len(tb, got, len(want))\n\n\tsortFunc := func(a, b *client.Persistent) (n int) {\n\t\treturn cmp.Compare(a.Name, b.Name)\n\t}\n\n\tslices.SortFunc(want, sortFunc)\n\tslices.SortFunc(got, sortFunc)\n\n\tfor i, a := range want {\n\t\tb := got[i]\n\t\tassert.Truef(tb, a.EqualIDs(b), \"%q doesn't have the same ids as %q\", a.Name, b.Name)\n\t}\n}\n\n// assertPersistentClients is a helper function that uses HTTP API to check\n// whether want persistent clients are the same as the persistent clients stored\n// in the clients container.\nfunc assertPersistentClients(tb testing.TB, clients *clientsContainer, want []*client.Persistent) {\n\ttb.Helper()\n\n\trw := httptest.NewRecorder()\n\tclients.handleGetClients(rw, &http.Request{})\n\n\tclientList := &clientListJSON{}\n\terr := json.NewDecoder(rw.Body).Decode(clientList)\n\trequire.NoError(tb, err)\n\n\tvar got []*client.Persistent\n\tctx := testutil.ContextWithTimeout(tb, testTimeout)\n\tfor _, cj := range clientList.Clients {\n\t\tvar c *client.Persistent\n\t\tc, err = clients.jsonToClient(ctx, *cj, nil)\n\t\trequire.NoError(tb, err)\n\n\t\tgot = append(got, c)\n\t}\n\n\tassertClients(tb, want, got)\n}\n\n// assertPersistentClientsData is a helper function that checks whether want\n// persistent clients are the same as the persistent clients stored in data.\nfunc assertPersistentClientsData(\n\ttb testing.TB,\n\tclients *clientsContainer,\n\tdata []map[string]*clientJSON,\n\twant []*client.Persistent,\n) {\n\ttb.Helper()\n\n\tvar got []*client.Persistent\n\tctx := testutil.ContextWithTimeout(tb, testTimeout)\n\tfor _, cm := range data {\n\t\tfor _, cj := range cm {\n\t\t\tvar c *client.Persistent\n\t\t\tc, err := clients.jsonToClient(ctx, *cj, nil)\n\t\t\trequire.NoError(tb, err)\n\n\t\t\tgot = append(got, c)\n\t\t}\n\t}\n\n\tassertClients(tb, want, got)\n}\n\nfunc TestClientsContainer_HandleAddClient(t *testing.T) {\n\tclients := newClientsContainer(t)\n\n\tclientOne := newPersistentClientWithIDs(t, \"client1\", []string{testClientIP1})\n\tclientTwo := newPersistentClientWithIDs(t, \"client2\", []string{testClientIP2})\n\n\tclientEmptyID := newPersistentClient(\"empty_client_id\")\n\tclientEmptyID.ClientIDs = []client.ClientID{\"\"}\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tclient     *client.Persistent\n\t\twantCode   int\n\t\twantClient []*client.Persistent\n\t}{{\n\t\tname:       \"add_one\",\n\t\tclient:     clientOne,\n\t\twantCode:   http.StatusOK,\n\t\twantClient: []*client.Persistent{clientOne},\n\t}, {\n\t\tname:       \"add_two\",\n\t\tclient:     clientTwo,\n\t\twantCode:   http.StatusOK,\n\t\twantClient: []*client.Persistent{clientOne, clientTwo},\n\t}, {\n\t\tname:       \"duplicate_client\",\n\t\tclient:     clientTwo,\n\t\twantCode:   http.StatusBadRequest,\n\t\twantClient: []*client.Persistent{clientOne, clientTwo},\n\t}, {\n\t\tname:       \"empty_client_id\",\n\t\tclient:     clientEmptyID,\n\t\twantCode:   http.StatusBadRequest,\n\t\twantClient: []*client.Persistent{clientOne, clientTwo},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcj := clientToJSON(tc.client)\n\n\t\t\tbody, err := json.Marshal(cj)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tr, err := http.NewRequest(http.MethodPost, \"\", bytes.NewReader(body))\n\t\t\trequire.NoError(t, err)\n\n\t\t\trw := httptest.NewRecorder()\n\t\t\tclients.handleAddClient(rw, r)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.wantCode, rw.Code)\n\n\t\t\tassertPersistentClients(t, clients, tc.wantClient)\n\t\t})\n\t}\n}\n\nfunc TestClientsContainer_HandleDelClient(t *testing.T) {\n\tclients := newClientsContainer(t)\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tclientOne := newPersistentClientWithIDs(t, \"client1\", []string{testClientIP1})\n\terr := clients.storage.Add(ctx, clientOne)\n\trequire.NoError(t, err)\n\n\tclientTwo := newPersistentClientWithIDs(t, \"client2\", []string{testClientIP2})\n\terr = clients.storage.Add(ctx, clientTwo)\n\trequire.NoError(t, err)\n\n\tassertPersistentClients(t, clients, []*client.Persistent{clientOne, clientTwo})\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tclient     *client.Persistent\n\t\twantCode   int\n\t\twantClient []*client.Persistent\n\t}{{\n\t\tname:       \"remove_one\",\n\t\tclient:     clientOne,\n\t\twantCode:   http.StatusOK,\n\t\twantClient: []*client.Persistent{clientTwo},\n\t}, {\n\t\tname:       \"duplicate_client\",\n\t\tclient:     clientOne,\n\t\twantCode:   http.StatusBadRequest,\n\t\twantClient: []*client.Persistent{clientTwo},\n\t}, {\n\t\tname:       \"empty_client_name\",\n\t\tclient:     newPersistentClient(\"\"),\n\t\twantCode:   http.StatusBadRequest,\n\t\twantClient: []*client.Persistent{clientTwo},\n\t}, {\n\t\tname:       \"remove_two\",\n\t\tclient:     clientTwo,\n\t\twantCode:   http.StatusOK,\n\t\twantClient: []*client.Persistent{},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcj := clientToJSON(tc.client)\n\n\t\t\tvar body []byte\n\t\t\tbody, err = json.Marshal(cj)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar r *http.Request\n\t\t\tr, err = http.NewRequest(http.MethodPost, \"\", bytes.NewReader(body))\n\t\t\trequire.NoError(t, err)\n\n\t\t\trw := httptest.NewRecorder()\n\t\t\tclients.handleDelClient(rw, r)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.wantCode, rw.Code)\n\n\t\t\tassertPersistentClients(t, clients, tc.wantClient)\n\t\t})\n\t}\n}\n\nfunc TestClientsContainer_HandleUpdateClient(t *testing.T) {\n\tclients := newClientsContainer(t)\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tclientOne := newPersistentClientWithIDs(t, \"client1\", []string{testClientIP1})\n\terr := clients.storage.Add(ctx, clientOne)\n\trequire.NoError(t, err)\n\n\tassertPersistentClients(t, clients, []*client.Persistent{clientOne})\n\n\tclientModified := newPersistentClientWithIDs(t, \"client2\", []string{testClientIP2})\n\n\tclientEmptyID := newPersistentClient(\"empty_client_id\")\n\tclientEmptyID.ClientIDs = []client.ClientID{\"\"}\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tclientName string\n\t\tmodified   *client.Persistent\n\t\twantCode   int\n\t\twantClient []*client.Persistent\n\t}{{\n\t\tname:       \"update_one\",\n\t\tclientName: clientOne.Name,\n\t\tmodified:   clientModified,\n\t\twantCode:   http.StatusOK,\n\t\twantClient: []*client.Persistent{clientModified},\n\t}, {\n\t\tname:       \"empty_name\",\n\t\tclientName: \"\",\n\t\tmodified:   clientOne,\n\t\twantCode:   http.StatusBadRequest,\n\t\twantClient: []*client.Persistent{clientModified},\n\t}, {\n\t\tname:       \"client_not_found\",\n\t\tclientName: \"client_not_found\",\n\t\tmodified:   clientOne,\n\t\twantCode:   http.StatusBadRequest,\n\t\twantClient: []*client.Persistent{clientModified},\n\t}, {\n\t\tname:       \"empty_client_id\",\n\t\tclientName: clientModified.Name,\n\t\tmodified:   clientEmptyID,\n\t\twantCode:   http.StatusBadRequest,\n\t\twantClient: []*client.Persistent{clientModified},\n\t}, {\n\t\tname:       \"no_ids\",\n\t\tclientName: clientModified.Name,\n\t\tmodified:   newPersistentClient(\"no_ids\"),\n\t\twantCode:   http.StatusBadRequest,\n\t\twantClient: []*client.Persistent{clientModified},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tuj := updateJSON{\n\t\t\t\tName: tc.clientName,\n\t\t\t\tData: *clientToJSON(tc.modified),\n\t\t\t}\n\n\t\t\tvar body []byte\n\t\t\tbody, err = json.Marshal(uj)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar r *http.Request\n\t\t\tr, err = http.NewRequest(http.MethodPost, \"\", bytes.NewReader(body))\n\t\t\trequire.NoError(t, err)\n\n\t\t\trw := httptest.NewRecorder()\n\t\t\tclients.handleUpdateClient(rw, r)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.wantCode, rw.Code)\n\n\t\t\tassertPersistentClients(t, clients, tc.wantClient)\n\t\t})\n\t}\n}\n\nfunc TestClientsContainer_HandleFindClient(t *testing.T) {\n\tclients := newClientsContainer(t)\n\tclients.clientChecker = &testBlockedClientChecker{\n\t\tonIsBlockedClient: func(ip netip.Addr, clientID string) (ok bool, rule string) {\n\t\t\treturn false, \"\"\n\t\t},\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tclientOne := newPersistentClientWithIDs(t, \"client1\", []string{testClientIP1})\n\terr := clients.storage.Add(ctx, clientOne)\n\trequire.NoError(t, err)\n\n\tclientTwo := newPersistentClientWithIDs(t, \"client2\", []string{testClientIP2})\n\terr = clients.storage.Add(ctx, clientTwo)\n\trequire.NoError(t, err)\n\n\tassertPersistentClients(t, clients, []*client.Persistent{clientOne, clientTwo})\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tquery      url.Values\n\t\twantCode   int\n\t\twantClient []*client.Persistent\n\t}{{\n\t\tname: \"single\",\n\t\tquery: url.Values{\n\t\t\t\"ip0\": []string{testClientIP1},\n\t\t},\n\t\twantCode:   http.StatusOK,\n\t\twantClient: []*client.Persistent{clientOne},\n\t}, {\n\t\tname: \"multiple\",\n\t\tquery: url.Values{\n\t\t\t\"ip0\": []string{testClientIP1},\n\t\t\t\"ip1\": []string{testClientIP2},\n\t\t},\n\t\twantCode:   http.StatusOK,\n\t\twantClient: []*client.Persistent{clientOne, clientTwo},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar r *http.Request\n\t\t\tr, err = http.NewRequest(http.MethodGet, \"\", nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tr.URL.RawQuery = tc.query.Encode()\n\t\t\trw := httptest.NewRecorder()\n\t\t\tclients.handleFindClient(rw, r)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.wantCode, rw.Code)\n\n\t\t\tvar body []byte\n\t\t\tbody, err = io.ReadAll(rw.Body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclientData := []map[string]*clientJSON{}\n\t\t\terr = json.Unmarshal(body, &clientData)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassertPersistentClientsData(t, clients, clientData, tc.wantClient)\n\t\t})\n\t}\n}\n\nfunc TestClientsContainer_HandleSearchClient(t *testing.T) {\n\tvar (\n\t\truntimeCli = \"runtime_client1\"\n\n\t\truntimeCliIP     = \"3.3.3.3\"\n\t\tblockedCliIP     = \"4.4.4.4\"\n\t\tnonExistentCliIP = \"5.5.5.5\"\n\n\t\tallowed     = false\n\t\tdissallowed = true\n\n\t\tdisallowedRule = \"disallowed_rule\"\n\t)\n\n\tclients := newClientsContainer(t)\n\tclients.clientChecker = &testBlockedClientChecker{\n\t\tonIsBlockedClient: func(ip netip.Addr, _ string) (ok bool, rule string) {\n\t\t\tif ip == netip.MustParseAddr(blockedCliIP) {\n\t\t\t\treturn true, disallowedRule\n\t\t\t}\n\n\t\t\treturn false, \"\"\n\t\t},\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tclientOne := newPersistentClientWithIDs(t, \"client1\", []string{testClientIP1})\n\terr := clients.storage.Add(ctx, clientOne)\n\trequire.NoError(t, err)\n\n\tclientTwo := newPersistentClientWithIDs(t, \"client2\", []string{testClientIP2})\n\terr = clients.storage.Add(ctx, clientTwo)\n\trequire.NoError(t, err)\n\n\tassertPersistentClients(t, clients, []*client.Persistent{clientOne, clientTwo})\n\n\tclients.UpdateAddress(ctx, netip.MustParseAddr(runtimeCliIP), runtimeCli, nil)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tquery          *searchQueryJSON\n\t\twantPersistent []*client.Persistent\n\t\twantRuntime    *clientJSON\n\t}{{\n\t\tname: \"single\",\n\t\tquery: &searchQueryJSON{\n\t\t\tClients: []searchClientJSON{{\n\t\t\t\tID: testClientIP1,\n\t\t\t}},\n\t\t},\n\t\twantPersistent: []*client.Persistent{clientOne},\n\t}, {\n\t\tname: \"multiple\",\n\t\tquery: &searchQueryJSON{\n\t\t\tClients: []searchClientJSON{{\n\t\t\t\tID: testClientIP1,\n\t\t\t}, {\n\t\t\t\tID: testClientIP2,\n\t\t\t}},\n\t\t},\n\t\twantPersistent: []*client.Persistent{clientOne, clientTwo},\n\t}, {\n\t\tname: \"runtime\",\n\t\tquery: &searchQueryJSON{\n\t\t\tClients: []searchClientJSON{{\n\t\t\t\tID: runtimeCliIP,\n\t\t\t}},\n\t\t},\n\t\twantRuntime: &clientJSON{\n\t\t\tName:       runtimeCli,\n\t\t\tIDs:        []string{runtimeCliIP},\n\t\t\tDisallowed: &allowed,\n\t\t\tWHOIS:      &whois.Info{},\n\t\t},\n\t}, {\n\t\tname: \"blocked_access\",\n\t\tquery: &searchQueryJSON{\n\t\t\tClients: []searchClientJSON{{\n\t\t\t\tID: blockedCliIP,\n\t\t\t}},\n\t\t},\n\t\twantRuntime: &clientJSON{\n\t\t\tIDs:            []string{blockedCliIP},\n\t\t\tDisallowed:     &dissallowed,\n\t\t\tDisallowedRule: &disallowedRule,\n\t\t\tWHOIS:          &whois.Info{},\n\t\t},\n\t}, {\n\t\tname: \"non_existing_client\",\n\t\tquery: &searchQueryJSON{\n\t\t\tClients: []searchClientJSON{{\n\t\t\t\tID: nonExistentCliIP,\n\t\t\t}},\n\t\t},\n\t\twantRuntime: &clientJSON{\n\t\t\tIDs:        []string{nonExistentCliIP},\n\t\t\tDisallowed: &allowed,\n\t\t\tWHOIS:      &whois.Info{},\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar body []byte\n\t\t\tbody, err = json.Marshal(tc.query)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar r *http.Request\n\t\t\tr, err = http.NewRequest(http.MethodPost, \"\", bytes.NewReader(body))\n\t\t\trequire.NoError(t, err)\n\n\t\t\trw := httptest.NewRecorder()\n\t\t\tclients.handleSearchClient(rw, r)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, http.StatusOK, rw.Code)\n\n\t\t\tbody, err = io.ReadAll(rw.Body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclientData := []map[string]*clientJSON{}\n\t\t\terr = json.Unmarshal(body, &clientData)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.wantPersistent != nil {\n\t\t\t\tassertPersistentClientsData(t, clients, clientData, tc.wantPersistent)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Len(t, clientData, 1)\n\t\t\trequire.Len(t, clientData[0], 1)\n\n\t\t\trc := clientData[0][tc.wantRuntime.IDs[0]]\n\t\t\tassert.Equal(t, tc.wantRuntime, rc)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/home/config.go",
    "content": "package home\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/configmigrate\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpd\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dnsforward\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/querylog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/schedule\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/stats\"\n\t\"github.com/AdguardTeam/dnsproxy/fastip\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/renameio/v2/maybe\"\n\tyaml \"go.yaml.in/yaml/v4\"\n)\n\nconst (\n\t// dataDir is the name of a directory under the working one to store some\n\t// persistent data.\n\tdataDir = \"data\"\n\n\t// userFilterDataDir is the name of the directory used to store users'\n\t// FS-based rule lists.\n\tuserFilterDataDir = \"userfilters\"\n)\n\n// logSettings are the logging settings part of the configuration file.\ntype logSettings struct {\n\t// Enabled indicates whether logging is enabled.\n\tEnabled bool `yaml:\"enabled\"`\n\n\t// File is the path to the log file.  If empty, logs are written to stdout.\n\t// If \"syslog\", logs are written to syslog.\n\tFile string `yaml:\"file\"`\n\n\t// MaxBackups is the maximum number of old log files to retain.\n\t//\n\t// NOTE: MaxAge may still cause them to get deleted.\n\tMaxBackups int `yaml:\"max_backups\"`\n\n\t// MaxSize is the maximum size of the log file before it gets rotated, in\n\t// megabytes.  The default value is 100 MB.\n\tMaxSize int `yaml:\"max_size\"`\n\n\t// MaxAge is the maximum duration for retaining old log files, in days.\n\tMaxAge int `yaml:\"max_age\"`\n\n\t// Compress determines, if the rotated log files should be compressed using\n\t// gzip.\n\tCompress bool `yaml:\"compress\"`\n\n\t// LocalTime determines, if the time used for formatting the timestamps in\n\t// is the computer's local time.\n\tLocalTime bool `yaml:\"local_time\"`\n\n\t// Verbose determines, if verbose (aka debug) logging is enabled.\n\tVerbose bool `yaml:\"verbose\"`\n}\n\n// osConfig contains OS-related configuration.\ntype osConfig struct {\n\t// Group is the name of the group which AdGuard Home must switch to on\n\t// startup.  Empty string means no switching.\n\tGroup string `yaml:\"group\"`\n\t// User is the name of the user which AdGuard Home must switch to on\n\t// startup.  Empty string means no switching.\n\tUser string `yaml:\"user\"`\n\t// RlimitNoFile is the maximum number of opened fd's per process.  Zero\n\t// means use the default value.\n\tRlimitNoFile uint64 `yaml:\"rlimit_nofile\"`\n}\n\ntype clientsConfig struct {\n\t// Sources defines the set of sources to fetch the runtime clients from.\n\tSources *clientSourcesConfig `yaml:\"runtime_sources\"`\n\t// Persistent are the configured clients.\n\tPersistent []*clientObject `yaml:\"persistent\"`\n}\n\n// clientSourceConfig is used to configure where the runtime clients will be\n// obtained from.\ntype clientSourcesConfig struct {\n\tWHOIS     bool `yaml:\"whois\"`\n\tARP       bool `yaml:\"arp\"`\n\tRDNS      bool `yaml:\"rdns\"`\n\tDHCP      bool `yaml:\"dhcp\"`\n\tHostsFile bool `yaml:\"hosts\"`\n}\n\n// configuration is loaded from YAML.\n//\n// Field ordering is important, YAML fields better not to be reordered, if it's\n// not absolutely necessary.\ntype configuration struct {\n\t// Raw file data to avoid re-reading of configuration file\n\t// It's reset after config is parsed\n\tfileData []byte\n\n\t// HTTPConfig is the block with http conf.\n\tHTTPConfig httpConfig `yaml:\"http\"`\n\t// Users are the clients capable for accessing the web interface.\n\tUsers []webUser `yaml:\"users\"`\n\t// AuthAttempts is the maximum number of failed login attempts a user\n\t// can do before being blocked.\n\tAuthAttempts uint `yaml:\"auth_attempts\"`\n\t// AuthBlockMin is the duration, in minutes, of the block of new login\n\t// attempts after AuthAttempts unsuccessful login attempts.\n\tAuthBlockMin uint `yaml:\"block_auth_min\"`\n\t// ProxyURL is the address of proxy server for the internal HTTP client.\n\tProxyURL string `yaml:\"http_proxy\"`\n\t// Language is a two-letter ISO 639-1 language code.\n\tLanguage string `yaml:\"language\"`\n\t// Theme is a UI theme for current user.\n\tTheme Theme `yaml:\"theme\"`\n\n\t// TODO(a.garipov): Make DNS and the fields below pointers and validate\n\t// and/or reset on explicit nulling.\n\tDNS      dnsConfig         `yaml:\"dns\"`\n\tTLS      tlsConfigSettings `yaml:\"tls\"`\n\tQueryLog queryLogConfig    `yaml:\"querylog\"`\n\tStats    statsConfig       `yaml:\"statistics\"`\n\n\t// Filters reflects the filters from [filtering.Config].  It's cloned to the\n\t// config used in the filtering module at the startup.  Afterwards it's\n\t// cloned from the filtering module back here.\n\t//\n\t// TODO(e.burkov):  Move all the filtering configuration fields into the\n\t// only configuration subsection covering the changes with a single\n\t// migration.  Also keep the blocked services in mind.\n\tFilters          []filtering.FilterYAML `yaml:\"filters\"`\n\tWhitelistFilters []filtering.FilterYAML `yaml:\"whitelist_filters\"`\n\tUserRules        []string               `yaml:\"user_rules\"`\n\n\tDHCP      *dhcpd.ServerConfig `yaml:\"dhcp\"`\n\tFiltering *filtering.Config   `yaml:\"filtering\"`\n\n\t// Clients contains the YAML representations of the persistent clients.\n\t// This field is only used for reading and writing persistent client data.\n\t// Keep this field sorted to ensure consistent ordering.\n\tClients *clientsConfig `yaml:\"clients\"`\n\n\t// Log is a block with log configuration settings.\n\tLog logSettings `yaml:\"log\"`\n\n\tOSConfig *osConfig `yaml:\"os\"`\n\n\tsync.RWMutex `yaml:\"-\"`\n\n\t// SchemaVersion is the version of the configuration schema.  See\n\t// [configmigrate.LastSchemaVersion].\n\tSchemaVersion uint `yaml:\"schema_version\"`\n\n\t// UnsafeUseCustomUpdateIndexURL is the URL to the custom update index.\n\t//\n\t// NOTE: It's only exists for testing purposes and should not be used in\n\t// release.\n\tUnsafeUseCustomUpdateIndexURL bool `yaml:\"unsafe_use_custom_update_index_url,omitempty\"`\n}\n\n// httpConfig is a block with HTTP configuration params.\n//\n// Field ordering is important, YAML fields better not to be reordered, if it's\n// not absolutely necessary.\ntype httpConfig struct {\n\t// Pprof defines the profiling HTTP handler.\n\tPprof *httpPprofConfig `yaml:\"pprof\"`\n\n\t// Address is the address to serve the web UI on.\n\tAddress netip.AddrPort\n\n\t// SessionTTL for a web session.\n\t// An active session is automatically refreshed once a day.\n\tSessionTTL timeutil.Duration `yaml:\"session_ttl\"`\n}\n\n// httpPprofConfig is the block with pprof HTTP configuration.\ntype httpPprofConfig struct {\n\t// Port for the profiling handler.\n\tPort uint16 `yaml:\"port\"`\n\n\t// Enabled defines if the profiling handler is enabled.\n\tEnabled bool `yaml:\"enabled\"`\n}\n\n// dnsConfig is a block with DNS configuration params.\n//\n// Field ordering is important, YAML fields better not to be reordered, if it's\n// not absolutely necessary.\ntype dnsConfig struct {\n\tBindHosts []netip.Addr `yaml:\"bind_hosts\"`\n\tPort      uint16       `yaml:\"port\"`\n\n\t// AnonymizeClientIP defines if clients' IP addresses should be anonymized\n\t// in query log and statistics.\n\tAnonymizeClientIP bool `yaml:\"anonymize_client_ip\"`\n\n\t// Config is the embed configuration with DNS params.\n\t//\n\t// TODO(a.garipov): Remove embed.\n\tdnsforward.Config `yaml:\",inline\"`\n\n\t// UpstreamTimeout is the timeout for querying upstream servers.\n\tUpstreamTimeout timeutil.Duration `yaml:\"upstream_timeout\"`\n\n\t// PrivateNets is the set of IP networks for which the private reverse DNS\n\t// resolver should be used.\n\tPrivateNets []netutil.Prefix `yaml:\"private_networks\"`\n\n\t// UsePrivateRDNS enables resolving requests containing a private IP address\n\t// using private reverse DNS resolvers.  See PrivateRDNSResolvers.\n\t//\n\t// TODO(e.burkov):  Rename in YAML.\n\tUsePrivateRDNS bool `yaml:\"use_private_ptr_resolvers\"`\n\n\t// PrivateRDNSResolvers is the slice of addresses to be used as upstreams\n\t// for private requests.  It's only used for PTR, SOA, and NS queries,\n\t// containing an ARPA subdomain, came from the the client with private\n\t// address.  The address considered private according to PrivateNets.\n\t//\n\t// If empty, the OS-provided resolvers are used for private requests.\n\tPrivateRDNSResolvers []string `yaml:\"local_ptr_upstreams\"`\n\n\t// UseDNS64 defines if DNS64 should be used for incoming requests.  Requests\n\t// of type PTR for addresses within the configured prefixes will be resolved\n\t// via [PrivateRDNSResolvers], so those should be valid and UsePrivateRDNS\n\t// be set to true.\n\tUseDNS64 bool `yaml:\"use_dns64\"`\n\n\t// DNS64Prefixes is the list of NAT64 prefixes to be used for DNS64.\n\tDNS64Prefixes []netip.Prefix `yaml:\"dns64_prefixes\"`\n\n\t// ServeHTTP3 defines if HTTP/3 is allowed for incoming requests.\n\t//\n\t// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer\n\t// experimental.\n\tServeHTTP3 bool `yaml:\"serve_http3\"`\n\n\t// UseHTTP3Upstreams defines if HTTP/3 is allowed for DNS-over-HTTPS\n\t// upstreams.\n\t//\n\t// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer\n\t// experimental.\n\tUseHTTP3Upstreams bool `yaml:\"use_http3_upstreams\"`\n\n\t// ServePlainDNS defines if plain DNS is allowed for incoming requests.\n\tServePlainDNS bool `yaml:\"serve_plain_dns\"`\n\n\t// HostsFileEnabled defines whether to use information from the system hosts\n\t// file to resolve queries.\n\tHostsFileEnabled bool `yaml:\"hostsfile_enabled\"`\n\n\t// PendingRequests configures duplicate requests policy.\n\tPendingRequests *pendingRequests `yaml:\"pending_requests\"`\n}\n\n// pendingRequests is a block with pending requests configuration.\ntype pendingRequests struct {\n\t// Enabled controls if duplicate requests should be sent to the upstreams\n\t// along with the original one.\n\tEnabled bool `yaml:\"enabled\"`\n}\n\n// tlsConfigSettings is the TLS configuration for DNS-over-TLS, DNS-over-QUIC,\n// and HTTPS.  When adding new properties, update the [tlsConfigSettings.clone]\n// and [tlsConfigSettings.setPrivateFieldsAndCompare] methods as necessary.\ntype tlsConfigSettings struct {\n\t// Enabled indicates whether encryption (DoT/DoH/HTTPS) is enabled.\n\tEnabled bool `yaml:\"enabled\" json:\"enabled\"`\n\n\t// ServerName is the hostname of the HTTPS/TLS server.\n\tServerName string `yaml:\"server_name\" json:\"server_name,omitempty\"`\n\n\t// ForceHTTPS, if true, forces an HTTP to HTTPS redirect.\n\tForceHTTPS bool `yaml:\"force_https\" json:\"force_https\"`\n\n\t// PortHTTPS is the HTTPS port.  If 0, HTTPS will be disabled.\n\tPortHTTPS uint16 `yaml:\"port_https\" json:\"port_https,omitempty\"`\n\n\t// PortDNSOverTLS is the DNS-over-TLS port.  If 0, DoT will be disabled.\n\tPortDNSOverTLS uint16 `yaml:\"port_dns_over_tls\" json:\"port_dns_over_tls,omitempty\"`\n\n\t// PortDNSOverQUIC is the DNS-over-QUIC port.  If 0, DoQ will be disabled.\n\tPortDNSOverQUIC uint16 `yaml:\"port_dns_over_quic\" json:\"port_dns_over_quic,omitempty\"`\n\n\t// PortDNSCrypt is the port for DNSCrypt requests.  If it's zero, DNSCrypt\n\t// is disabled.\n\tPortDNSCrypt uint16 `yaml:\"port_dnscrypt\" json:\"port_dnscrypt\"`\n\n\t// DNSCryptConfigFile is the path to the DNSCrypt config file.  Must be set\n\t// if PortDNSCrypt is not zero.\n\t//\n\t// See https://github.com/AdguardTeam/dnsproxy and\n\t// https://github.com/ameshkov/dnscrypt.\n\tDNSCryptConfigFile string `yaml:\"dnscrypt_config_file\" json:\"dnscrypt_config_file\"`\n\n\t// AllowUnencryptedDoH allows DoH queries via unencrypted HTTP (e.g. for\n\t// reverse proxying).\n\t//\n\t// TODO(s.chzhen):  Add this option into the Web UI.\n\tAllowUnencryptedDoH bool `yaml:\"allow_unencrypted_doh\" json:\"allow_unencrypted_doh\"`\n\n\t// CertificateChain is the PEM-encoded certificate chain.  Must be empty if\n\t// [tlsConfigSettings.CertificatePath] is provided.\n\tCertificateChain string `yaml:\"certificate_chain\" json:\"certificate_chain\"`\n\n\t// PrivateKey is the PEM-encoded private key.  Must be empty if\n\t// [tlsConfigSettings.PrivateKeyPath] is provided.\n\tPrivateKey string `yaml:\"private_key\" json:\"private_key\"`\n\n\t// CertificatePath is the path to the certificate file.  Must be empty if\n\t// [tlsConfigSettings.CertificateChain] is provided.\n\tCertificatePath string `yaml:\"certificate_path\" json:\"certificate_path\"`\n\n\t// PrivateKeyPath is the path to the private key file.  Must be empty if\n\t// [tlsConfigSettings.PrivateKey] is provided.\n\tPrivateKeyPath string `yaml:\"private_key_path\" json:\"private_key_path\"`\n\n\t// OverrideTLSCiphers, when set, contains the names of the cipher suites to\n\t// use.  If the slice is empty, the default safe suites are used.\n\tOverrideTLSCiphers []string `yaml:\"override_tls_ciphers,omitempty\" json:\"-\"`\n\n\t// CertificateChainData is the PEM-encoded byte data for the certificate\n\t// chain.\n\tCertificateChainData []byte `yaml:\"-\" json:\"-\"`\n\n\t// PrivateKeyData is the PEM-encoded byte data for the private key.\n\tPrivateKeyData []byte `yaml:\"-\" json:\"-\"`\n\n\t// StrictSNICheck controls if the connections with SNI mismatching the\n\t// certificate's ones should be rejected.\n\tStrictSNICheck bool `yaml:\"strict_sni_check\" json:\"-\"`\n}\n\n// clone returns a deep copy of c.\nfunc (c *tlsConfigSettings) clone() (clone *tlsConfigSettings) {\n\tclone = &tlsConfigSettings{}\n\t*clone = *c\n\n\tclone.OverrideTLSCiphers = slices.Clone(c.OverrideTLSCiphers)\n\tclone.CertificateChainData = slices.Clone(c.CertificateChainData)\n\tclone.PrivateKeyData = slices.Clone(c.PrivateKeyData)\n\n\treturn clone\n}\n\n// setPrivateFieldsAndCompare sets any missing properties in conf to match those\n// in c and returns true if TLS configurations are equal.  conf must not be be\n// nil.\n// It sets the following properties because these are not accepted from the\n// frontend:\n//\n//\t[tlsConfigSettings.AllowUnencryptedDoH]\n//\t[tlsConfigSettings.DNSCryptConfigFile]\n//\t[tlsConfigSettings.OverrideTLSCiphers]\n//\t[tlsConfigSettings.PortDNSCrypt]\n//\n// The following properties are skipped as they are set by\n// [tlsManager.loadTLSConfig]:\n//\n//\t[tlsConfigSettings.CertificateChainData]\n//\t[tlsConfigSettings.PrivateKeyData]\nfunc (c *tlsConfigSettings) setPrivateFieldsAndCompare(conf *tlsConfigSettings) (equal bool) {\n\tconf.OverrideTLSCiphers = slices.Clone(c.OverrideTLSCiphers)\n\n\t// TODO(s.chzhen):  Remove this once the frontend supports it.\n\tconf.AllowUnencryptedDoH = c.AllowUnencryptedDoH\n\n\tconf.DNSCryptConfigFile = c.DNSCryptConfigFile\n\tconf.PortDNSCrypt = c.PortDNSCrypt\n\n\t// TODO(a.garipov): Define a custom comparer.\n\treturn cmp.Equal(c, conf)\n}\n\ntype queryLogConfig struct {\n\t// DirPath is the custom directory for logs.  If it's empty the default\n\t// directory will be used.  See [homeContext.getDataDir].\n\tDirPath string `yaml:\"dir_path\"`\n\n\t// Ignored is the list of host names, which should not be written to log.\n\t// \".\" is considered to be the root domain.\n\tIgnored []string `yaml:\"ignored\"`\n\n\t// Interval is the interval for query log's files rotation.\n\tInterval timeutil.Duration `yaml:\"interval\"`\n\n\t// MemSize is the number of entries kept in memory before they are flushed\n\t// to disk.\n\tMemSize uint `yaml:\"size_memory\"`\n\n\t// Enabled defines if the query log is enabled.\n\tEnabled bool `yaml:\"enabled\"`\n\n\t// IgnoredEnabled defines whether hosts from the ignored list should be\n\t// ignored.\n\tIgnoredEnabled bool `yaml:\"ignored_enabled\"`\n\n\t// FileEnabled defines, if the query log is written to the file.\n\tFileEnabled bool `yaml:\"file_enabled\"`\n}\n\ntype statsConfig struct {\n\t// DirPath is the custom directory for statistics.  If it's empty the\n\t// default directory is used.  See [homeContext.getDataDir].\n\tDirPath string `yaml:\"dir_path\"`\n\n\t// Ignored is the list of host names, which should not be counted.\n\tIgnored []string `yaml:\"ignored\"`\n\n\t// Interval is the retention interval for statistics.\n\tInterval timeutil.Duration `yaml:\"interval\"`\n\n\t// Enabled defines if the statistics are enabled.\n\tEnabled bool `yaml:\"enabled\"`\n\n\t// IgnoredEnabled defines whether hosts from the ignored list should be\n\t// ignored.\n\tIgnoredEnabled bool `yaml:\"ignored_enabled\"`\n}\n\n// Default block host constants.\nconst (\n\tdefaultSafeBrowsingBlockHost = \"standard-block.dns.adguard.com\"\n\tdefaultParentalBlockHost     = \"family-block.dns.adguard.com\"\n)\n\n// config is the global configuration structure.\n//\n// TODO(a.garipov, e.burkov): This global is awful and must be removed.\nvar config = &configuration{\n\tAuthAttempts: 5,\n\tAuthBlockMin: 15,\n\tHTTPConfig: httpConfig{\n\t\tAddress:    netip.AddrPortFrom(netip.IPv4Unspecified(), 3000),\n\t\tSessionTTL: timeutil.Duration(30 * timeutil.Day),\n\t\tPprof: &httpPprofConfig{\n\t\t\tEnabled: false,\n\t\t\tPort:    6060,\n\t\t},\n\t},\n\tDNS: dnsConfig{\n\t\tBindHosts: []netip.Addr{netip.IPv4Unspecified()},\n\t\tPort:      defaultPortDNS,\n\t\tConfig: dnsforward.Config{\n\t\t\tRatelimit:              20,\n\t\t\tRatelimitSubnetLenIPv4: 24,\n\t\t\tRatelimitSubnetLenIPv6: 56,\n\t\t\tRefuseAny:              true,\n\t\t\tUpstreamMode:           dnsforward.UpstreamModeLoadBalance,\n\t\t\tHandleDDR:              true,\n\t\t\tFastestTimeout:         timeutil.Duration(fastip.DefaultPingWaitTimeout),\n\n\t\t\tTrustedProxies: []netutil.Prefix{{\n\t\t\t\tPrefix: netip.MustParsePrefix(\"127.0.0.0/8\"),\n\t\t\t}, {\n\t\t\t\tPrefix: netip.MustParsePrefix(\"::1/128\"),\n\t\t\t}},\n\t\t\tCacheEnabled:             true,\n\t\t\tCacheSize:                4 * 1024 * 1024,\n\t\t\tCacheOptimisticAnswerTTL: timeutil.Duration(30 * time.Second),\n\t\t\tCacheOptimisticMaxAge:    timeutil.Duration(12 * time.Hour),\n\n\t\t\tEDNSClientSubnet: &dnsforward.EDNSClientSubnet{\n\t\t\t\tCustomIP:  netip.Addr{},\n\t\t\t\tEnabled:   false,\n\t\t\t\tUseCustom: false,\n\t\t\t},\n\n\t\t\t// set default maximum concurrent queries to 300\n\t\t\t// we introduced a default limit due to this:\n\t\t\t// https://github.com/AdguardTeam/AdGuardHome/issues/2015#issuecomment-674041912\n\t\t\t// was later increased to 300 due to https://github.com/AdguardTeam/AdGuardHome/issues/2257\n\t\t\tMaxGoroutines: 300,\n\t\t},\n\t\tUpstreamTimeout:  timeutil.Duration(dnsforward.DefaultTimeout),\n\t\tUsePrivateRDNS:   true,\n\t\tServePlainDNS:    true,\n\t\tHostsFileEnabled: true,\n\t\tPendingRequests: &pendingRequests{\n\t\t\tEnabled: true,\n\t\t},\n\t},\n\tTLS: tlsConfigSettings{\n\t\tPortHTTPS:       defaultPortHTTPS,\n\t\tPortDNSOverTLS:  defaultPortTLS, // needs to be passed through to dnsproxy\n\t\tPortDNSOverQUIC: defaultPortQUIC,\n\t},\n\tQueryLog: queryLogConfig{\n\t\tEnabled:        true,\n\t\tFileEnabled:    true,\n\t\tInterval:       timeutil.Duration(90 * timeutil.Day),\n\t\tMemSize:        1000,\n\t\tIgnored:        []string{},\n\t\tIgnoredEnabled: false,\n\t},\n\tStats: statsConfig{\n\t\tEnabled:        true,\n\t\tInterval:       timeutil.Duration(1 * timeutil.Day),\n\t\tIgnored:        []string{},\n\t\tIgnoredEnabled: false,\n\t},\n\t// NOTE: Keep these parameters in sync with the one put into\n\t// client/src/helpers/filters/filters.ts by scripts/vetted-filters.\n\t//\n\t// TODO(a.garipov): Think of a way to make scripts/vetted-filters update\n\t// these as well if necessary.\n\tFilters: []filtering.FilterYAML{{\n\t\tFilter:  filtering.Filter{ID: 1},\n\t\tEnabled: true,\n\t\tURL:     \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt\",\n\t\tName:    \"AdGuard DNS filter\",\n\t}, {\n\t\tFilter:  filtering.Filter{ID: 2},\n\t\tEnabled: false,\n\t\tURL:     \"https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt\",\n\t\tName:    \"AdAway Default Blocklist\",\n\t}},\n\tFiltering: &filtering.Config{\n\t\tProtectionEnabled:  true,\n\t\tBlockingMode:       filtering.BlockingModeDefault,\n\t\tBlockedResponseTTL: 10, // in seconds\n\n\t\tFilteringEnabled:           true,\n\t\tFiltersUpdateIntervalHours: 24,\n\n\t\tRewritesEnabled: true,\n\n\t\tParentalEnabled:     false,\n\t\tSafeBrowsingEnabled: false,\n\n\t\tSafeBrowsingCacheSize: 1 * 1024 * 1024,\n\t\tSafeSearchCacheSize:   1 * 1024 * 1024,\n\t\tParentalCacheSize:     1 * 1024 * 1024,\n\t\tCacheTime:             30,\n\n\t\tSafeSearchConf: filtering.SafeSearchConfig{\n\t\t\tEnabled:    false,\n\t\t\tBing:       true,\n\t\t\tDuckDuckGo: true,\n\t\t\tEcosia:     true,\n\t\t\tGoogle:     true,\n\t\t\tPixabay:    true,\n\t\t\tYandex:     true,\n\t\t\tYouTube:    true,\n\t\t},\n\n\t\tBlockedServices: &filtering.BlockedServices{\n\t\t\tSchedule: schedule.EmptyWeekly(),\n\t\t\tIDs:      []string{},\n\t\t},\n\n\t\tParentalBlockHost:     defaultParentalBlockHost,\n\t\tSafeBrowsingBlockHost: defaultSafeBrowsingBlockHost,\n\t},\n\tDHCP: &dhcpd.ServerConfig{\n\t\tLocalDomainName: \"lan\",\n\t\tConf4: dhcpd.V4ServerConf{\n\t\t\tLeaseDuration: dhcpd.DefaultDHCPLeaseTTL,\n\t\t\tICMPTimeout:   dhcpd.DefaultDHCPTimeoutICMP,\n\t\t},\n\t\tConf6: dhcpd.V6ServerConf{\n\t\t\tLeaseDuration: dhcpd.DefaultDHCPLeaseTTL,\n\t\t},\n\t},\n\tClients: &clientsConfig{\n\t\tSources: &clientSourcesConfig{\n\t\t\tWHOIS:     true,\n\t\t\tARP:       true,\n\t\t\tRDNS:      true,\n\t\t\tDHCP:      true,\n\t\t\tHostsFile: true,\n\t\t},\n\t},\n\tLog: logSettings{\n\t\tEnabled:    true,\n\t\tFile:       \"\",\n\t\tMaxBackups: 0,\n\t\tMaxSize:    100,\n\t\tMaxAge:     3,\n\t\tCompress:   false,\n\t\tLocalTime:  false,\n\t\tVerbose:    false,\n\t},\n\tOSConfig:      &osConfig{},\n\tSchemaVersion: configmigrate.LastSchemaVersion,\n\tTheme:         ThemeAuto,\n}\n\n// configFilePath returns the absolute, symlink-resolved path to the current\n// configuration file.  l must not be nil.\n//\n// TODO(s.chzhen):  Fix the bug where the wrong file may be resolved:\n// [filepath.EvalSymlinks] resolves a relative path against the current working\n// directory, not workDir.  Make the path absolute relative to workDir before\n// calling EvalSymlinks.\nfunc configFilePath(\n\tctx context.Context,\n\tl *slog.Logger,\n\tworkDir string,\n\tconfPath string,\n) (resolved string) {\n\tresolved, err := filepath.EvalSymlinks(confPath)\n\tif err != nil {\n\t\tl.DebugContext(\n\t\t\tctx,\n\t\t\t\"symlink resolve failed; using original path\",\n\t\t\t\"path\", confPath,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\n\t\tresolved = confPath\n\t}\n\n\tif !filepath.IsAbs(confPath) {\n\t\tresolved = filepath.Join(workDir, confPath)\n\t}\n\n\treturn resolved\n}\n\n// validateBindHosts returns error if any of binding hosts from configuration is\n// not a valid IP address.\nfunc validateBindHosts(\n\tctx context.Context,\n\tl *slog.Logger,\n\tconf *configuration,\n\tfileData []byte,\n) (err error) {\n\tif !conf.HTTPConfig.Address.IsValid() {\n\t\treturn errors.Error(\"http.address is not a valid ip address\")\n\t}\n\n\tfor i, addr := range conf.DNS.BindHosts {\n\t\tif !addr.IsValid() {\n\t\t\tlogIPHint(ctx, l, fileData)\n\n\t\t\treturn fmt.Errorf(\"dns.bind_hosts at index %d is not a valid ip address\", i)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// parseConfig loads configuration from the YAML file, upgrading it if\n// necessary.  l must not be nil.\nfunc parseConfig(ctx context.Context, l *slog.Logger, workDir, confPath string) (err error) {\n\t// Do the upgrade if necessary.\n\tconfig.fileData, err = readConfigFile(ctx, l, workDir, confPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmigrator := configmigrate.New(&configmigrate.Config{\n\t\tLogger:     l.With(slogutil.KeyPrefix, \"config_migrator\"),\n\t\tWorkingDir: workDir,\n\t\tDataDir:    filepath.Join(workDir, dataDir),\n\t})\n\n\tvar upgraded bool\n\tconfig.fileData, upgraded, err = migrator.Migrate(\n\t\tctx,\n\t\tconfig.fileData,\n\t\tconfigmigrate.LastSchemaVersion,\n\t)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t} else if upgraded {\n\t\tconfPath = configFilePath(ctx, l, workDir, confPath)\n\t\tl.DebugContext(ctx, \"writing config file after config upgrade\", \"path\", confPath)\n\n\t\terr = maybe.WriteFile(confPath, config.fileData, aghos.DefaultPermFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"writing new config: %w\", err)\n\t\t}\n\t}\n\n\terr = yaml.Unmarshal(config.fileData, &config)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = validateConfig(ctx, l, config.fileData)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif config.DNS.UpstreamTimeout == 0 {\n\t\tconfig.DNS.UpstreamTimeout = timeutil.Duration(dnsforward.DefaultTimeout)\n\t}\n\n\t// Do not wrap the error because it's informative enough as is.\n\treturn validateTLSCipherIDs(config.TLS.OverrideTLSCiphers)\n}\n\n// logIPHint logs an informational message when the config contains an unquoted\n// IP address with a trailing colon.  It's a best-effort check for a YAML\n// parsing behavior where a list item is decoded as {key: null}.  l must not be\n// nil.\nfunc logIPHint(ctx context.Context, l *slog.Logger, data []byte) {\n\tvar conf struct {\n\t\tDNS struct {\n\t\t\tBindHosts []any `yaml:\"bind_hosts\"`\n\t\t} `yaml:\"dns\"`\n\t}\n\n\terr := yaml.Unmarshal(data, &conf)\n\tif err != nil {\n\t\t// This should not happen since this is already the validation process.\n\t\tl.DebugContext(\n\t\t\tctx,\n\t\t\t\"failed to unmarshal config while logging ip hint\",\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\n\t\treturn\n\t}\n\n\tfor _, h := range conf.DNS.BindHosts {\n\t\tm, ok := h.(map[string]any)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !hasNilValue(m) {\n\t\t\tcontinue\n\t\t}\n\n\t\tl.WarnContext(ctx, \"quote addresses that end with a colon in 'dns.bind_hosts'\")\n\n\t\treturn\n\t}\n}\n\n// hasNilValue returns true if m contains a nil value.\nfunc hasNilValue(m map[string]any) (ok bool) {\n\tfor _, v := range m {\n\t\tif v == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// validateConfig returns error if the configuration is invalid.  l must not be\n// nil.\nfunc validateConfig(ctx context.Context, l *slog.Logger, fileData []byte) (err error) {\n\terr = validateBindHosts(ctx, l, config, fileData)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\ttcpPorts := aghalg.UniqChecker[tcpPort]{}\n\taddPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port()))\n\n\tudpPorts := aghalg.UniqChecker[udpPort]{}\n\taddPorts(udpPorts, udpPort(config.DNS.Port))\n\n\tif config.TLS.Enabled {\n\t\taddPorts(\n\t\t\ttcpPorts,\n\t\t\ttcpPort(config.TLS.PortHTTPS),\n\t\t\ttcpPort(config.TLS.PortDNSOverTLS),\n\t\t\ttcpPort(config.TLS.PortDNSCrypt),\n\t\t)\n\n\t\t// TODO(e.burkov):  Consider adding a udpPort with the same value when\n\t\t// we add support for HTTP/3 for web admin interface.\n\t\taddPorts(udpPorts, udpPort(config.TLS.PortDNSOverQUIC))\n\t}\n\n\tif err = tcpPorts.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"validating tcp ports: %w\", err)\n\t} else if err = udpPorts.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"validating udp ports: %w\", err)\n\t}\n\n\tif !filtering.ValidateUpdateIvl(config.Filtering.FiltersUpdateIntervalHours) {\n\t\tconfig.Filtering.FiltersUpdateIntervalHours = 24\n\t}\n\n\tif len(config.Users) == 0 {\n\t\tl.WarnContext(ctx, \"no users in the configuration file; authentication is disabled\")\n\t}\n\n\tif config.Language != \"\" && !allowedLanguages.Has(config.Language) {\n\t\tl.WarnContext(ctx, \"unsupported language\", \"lang\", config.Language)\n\n\t\t// Clear the language so the frontend can use the client's browser\n\t\t// language.\n\t\tconfig.Language = \"\"\n\t}\n\n\treturn nil\n}\n\n// udpPort is the port number for UDP protocol.\ntype udpPort uint16\n\n// tcpPort is the port number for TCP protocol.\ntype tcpPort uint16\n\n// addPorts is a helper for ports validation that skips zero ports.\nfunc addPorts[T tcpPort | udpPort](uc aghalg.UniqChecker[T], ports ...T) {\n\tfor _, p := range ports {\n\t\tif p != 0 {\n\t\t\tuc.Add(p)\n\t\t}\n\t}\n}\n\n// readConfigFile reads configuration file contents.  l must not be nil.\nfunc readConfigFile(\n\tctx context.Context,\n\tl *slog.Logger,\n\tworkDir string,\n\tconfPath string,\n) (fileData []byte, err error) {\n\tif len(config.fileData) > 0 {\n\t\treturn config.fileData, nil\n\t}\n\n\tconfPath = configFilePath(ctx, l, workDir, confPath)\n\tl.DebugContext(ctx, \"reading config file\", \"path\", confPath)\n\n\t// Do not wrap the error because it's informative enough as is.\n\treturn os.ReadFile(confPath)\n}\n\n// write saves configuration to the YAML file and also saves the user filter\n// contents to a file.  l must not be nil.\nfunc (c *configuration) write(\n\tctx context.Context,\n\tl *slog.Logger,\n\ttlsMgr *tlsManager,\n\tauth *auth,\n\tworkDir string,\n\tconfPath string,\n) (err error) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tif auth != nil {\n\t\tconfig.Users = auth.usersList(ctx)\n\t}\n\n\tif tlsMgr != nil {\n\t\ttlsConf := tlsMgr.config()\n\t\tconfig.TLS = *tlsConf\n\t}\n\n\tif globalContext.stats != nil {\n\t\tstatsConf := stats.Config{}\n\t\tglobalContext.stats.WriteDiskConfig(&statsConf)\n\t\tconfig.Stats.Interval = timeutil.Duration(statsConf.Limit)\n\t\tconfig.Stats.Enabled = statsConf.Enabled\n\t\tconfig.Stats.Ignored = statsConf.Ignored.Values()\n\t\tconfig.Stats.IgnoredEnabled = statsConf.Ignored.IsEnabled()\n\t}\n\n\tif globalContext.queryLog != nil {\n\t\tdc := querylog.Config{}\n\t\tglobalContext.queryLog.WriteDiskConfig(&dc)\n\t\tconfig.DNS.AnonymizeClientIP = dc.AnonymizeClientIP\n\t\tconfig.QueryLog.Enabled = dc.Enabled\n\t\tconfig.QueryLog.FileEnabled = dc.FileEnabled\n\t\tconfig.QueryLog.Interval = timeutil.Duration(dc.RotationIvl)\n\t\tconfig.QueryLog.MemSize = dc.MemSize\n\t\tconfig.QueryLog.Ignored = dc.Ignored.Values()\n\t\tconfig.QueryLog.IgnoredEnabled = dc.Ignored.IsEnabled()\n\t}\n\n\tif globalContext.filters != nil {\n\t\tglobalContext.filters.WriteDiskConfig(config.Filtering)\n\t\tconfig.Filters = config.Filtering.Filters\n\t\tconfig.WhitelistFilters = config.Filtering.WhitelistFilters\n\t\tconfig.UserRules = config.Filtering.UserRules\n\t}\n\n\tif s := globalContext.dnsServer; s != nil {\n\t\tc := dnsforward.Config{}\n\t\ts.WriteDiskConfig(&c)\n\t\tdns := &config.DNS\n\t\tdns.Config = c\n\n\t\tdns.PrivateRDNSResolvers = s.LocalPTRResolvers()\n\n\t\taddrProcConf := s.AddrProcConfig()\n\t\tconfig.Clients.Sources.RDNS = addrProcConf.UseRDNS\n\t\tconfig.Clients.Sources.WHOIS = addrProcConf.UseWHOIS\n\t\tdns.UsePrivateRDNS = addrProcConf.UsePrivateRDNS\n\t\tdns.UpstreamTimeout = timeutil.Duration(s.UpstreamTimeout())\n\t}\n\n\tif globalContext.dhcpServer != nil {\n\t\tglobalContext.dhcpServer.WriteDiskConfig(config.DHCP)\n\t}\n\n\tconfig.Clients.Persistent = globalContext.clients.forConfig()\n\n\tconfPath = configFilePath(ctx, l, workDir, confPath)\n\tl.DebugContext(ctx, \"writing config file\", \"path\", confPath)\n\n\tbuf := &bytes.Buffer{}\n\tenc := yaml.NewEncoder(buf)\n\tenc.SetIndent(2)\n\n\terr = enc.Encode(config)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"generating config file: %w\", err)\n\t}\n\n\terr = maybe.WriteFile(confPath, buf.Bytes(), aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"writing config file: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// validateTLSCipherIDs validates the custom TLS cipher suite IDs.\nfunc validateTLSCipherIDs(cipherIDs []string) (err error) {\n\tif len(cipherIDs) == 0 {\n\t\treturn nil\n\t}\n\n\t_, err = aghtls.ParseCiphers(cipherIDs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"override_tls_ciphers: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// defaultConfigModifier is a default [agh.ConfigModifier] implementation.\ntype defaultConfigModifier struct {\n\tauth     *auth\n\tconfig   *configuration\n\tlogger   *slog.Logger\n\ttlsMgr   *tlsManager\n\tworkDir  string\n\tconfPath string\n}\n\n// newDefaultConfigModifier returns the new properly initialized\n// *defaultConfigModifier.  All arguments must not be nil.\n//\n// TODO(s.chzhen):  Consider using configuration struct.\nfunc newDefaultConfigModifier(\n\tconf *configuration,\n\tl *slog.Logger,\n\tworkDir string,\n\tconfPath string,\n) (cm *defaultConfigModifier) {\n\treturn &defaultConfigModifier{\n\t\tconfig:   conf,\n\t\tlogger:   l,\n\t\tworkDir:  workDir,\n\t\tconfPath: confPath,\n\t}\n}\n\n// type check\nvar _ agh.ConfigModifier = (*defaultConfigModifier)(nil)\n\n// Apply implements the [agh.ConfigModifier] interface for\n// *defaultConfigModifier.\nfunc (cm *defaultConfigModifier) Apply(ctx context.Context) {\n\terr := cm.config.write(ctx, cm.logger, cm.tlsMgr, cm.auth, cm.workDir, cm.confPath)\n\tif err != nil {\n\t\tcm.logger.ErrorContext(ctx, \"writing config\", slogutil.KeyError, err)\n\t}\n}\n\n// setAuth sets the auth parameters used by Apply.\nfunc (cm *defaultConfigModifier) setAuth(a *auth) {\n\tcm.auth = a\n}\n\n// setTLSManager sets the TLS manager used by Apply.\nfunc (cm *defaultConfigModifier) setTLSManager(m *tlsManager) {\n\tcm.tlsMgr = m\n}\n"
  },
  {
    "path": "internal/home/config_internal_test.go",
    "content": "package home\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigFilePath(t *testing.T) {\n\tconst (\n\t\trealConf       = \"real.yaml\"\n\t\tlinkConf       = \"conf.link\"\n\t\tmissingConf    = \"missing.yaml\"\n\t\tbrokenLinkConf = \"broken.link\"\n\t)\n\n\tworkDir := t.TempDir()\n\ttargetPath := filepath.Join(workDir, realConf)\n\tlinkPath := filepath.Join(workDir, linkConf)\n\tmissingPath := filepath.Join(workDir, missingConf)\n\tbrokenLinkPath := filepath.Join(workDir, brokenLinkConf)\n\n\terr := os.Symlink(targetPath, linkPath)\n\trequire.NoError(t, err)\n\n\terr = os.Symlink(missingPath, brokenLinkPath)\n\trequire.NoError(t, err)\n\n\tf, err := os.Create(targetPath)\n\trequire.NoError(t, err)\n\n\ttestutil.CleanupAndRequireSuccess(t, f.Close)\n\n\totherDir := t.TempDir()\n\n\t// Canonicalize the absolute path (e.g., on macOS: /var -> /private/var; on\n\t// Windows: RUNNER~1 -> runneradmin).\n\twantAbs := targetPath\n\tp, err := filepath.EvalSymlinks(wantAbs)\n\tif err == nil {\n\t\twantAbs = p\n\t}\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tchDir    string\n\t\tconfPath string\n\t\twant     string\n\t}{{\n\t\tname:     \"absolute_path\",\n\t\tchDir:    \"\",\n\t\tconfPath: targetPath,\n\t\twant:     wantAbs,\n\t}, {\n\t\tname:     \"relative_path\",\n\t\tchDir:    \"\",\n\t\tconfPath: realConf,\n\t\twant:     targetPath,\n\t}, {\n\t\tname:     \"symlink\",\n\t\tchDir:    \"\",\n\t\tconfPath: linkConf,\n\t\twant:     linkPath,\n\t}, {\n\t\tname:     \"symlink_broken\",\n\t\tchDir:    \"\",\n\t\tconfPath: brokenLinkConf,\n\t\twant:     brokenLinkPath,\n\t}, {\n\t\tname:     \"symlink_before_join\",\n\t\tchDir:    otherDir,\n\t\tconfPath: linkConf,\n\t\twant:     linkPath,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.chDir != \"\" {\n\t\t\t\tt.Chdir(tc.chDir)\n\t\t\t}\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\tgot := configFilePath(ctx, testLogger, workDir, tc.confPath)\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/home/context.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghuser\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// ctxKey is the type for context keys within this package.\ntype ctxKey uint8\n\nconst (\n\tctxKeyWebUser ctxKey = iota\n)\n\n// type check\nvar _ fmt.Stringer = ctxKey(0)\n\n// String implements the [fmt.Stringer] interface for ctxKey.\nfunc (k ctxKey) String() (s string) {\n\tswitch k {\n\tcase ctxKeyWebUser:\n\t\treturn \"ctxKeyWebUser\"\n\tdefault:\n\t\tpanic(fmt.Errorf(\"ctx key: %w: %d\", errors.ErrBadEnumValue, k))\n\t}\n}\n\n// panicBadType is a helper that panics with a message about the context key and\n// the expected type.\nfunc panicBadType(key ctxKey, v any) {\n\tpanic(fmt.Errorf(\"bad type for %s: %T(%[2]v)\", key, v))\n}\n\n// withWebUser returns a copy of the parent context with the web user added.\nfunc withWebUser(ctx context.Context, u *aghuser.User) (withUser context.Context) {\n\treturn context.WithValue(ctx, ctxKeyWebUser, u)\n}\n\n// webUserFromContext returns the web user from the context, if any.\nfunc webUserFromContext(ctx context.Context) (u *aghuser.User, ok bool) {\n\tconst key = ctxKeyWebUser\n\tv := ctx.Value(key)\n\tif v == nil {\n\t\treturn nil, false\n\t}\n\n\tu, ok = v.(*aghuser.User)\n\tif !ok {\n\t\tpanicBadType(key, v)\n\t}\n\n\treturn u, true\n}\n"
  },
  {
    "path": "internal/home/control.go",
    "content": "package home\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dnsforward\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/NYTimes/gziphandler\"\n)\n\n// appendDNSAddrs is a convenient helper for appending a formatted form of DNS\n// addresses to a slice of strings.\nfunc appendDNSAddrs(dst []string, addrs ...netip.Addr) (res []string) {\n\tfor _, addr := range addrs {\n\t\thostport := addr.String()\n\t\tif p := config.DNS.Port; p != defaultPortDNS {\n\t\t\thostport = netutil.JoinHostPort(hostport, p)\n\t\t}\n\n\t\tdst = append(dst, hostport)\n\t}\n\n\treturn dst\n}\n\n// appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to\n// dst.  It also adds the IP addresses of all network interfaces if src contains\n// an unspecified IP address.\nfunc appendDNSAddrsWithIfaces(dst []string, src []netip.Addr) (res []string, err error) {\n\tifacesAdded := false\n\tfor _, h := range src {\n\t\tif !h.IsUnspecified() {\n\t\t\tdst = appendDNSAddrs(dst, h)\n\n\t\t\tcontinue\n\t\t} else if ifacesAdded {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Add addresses of all network interfaces for addresses like\n\t\t// \"0.0.0.0\" and \"::\".\n\t\tvar ifaces []*aghnet.NetInterface\n\t\tifaces, err = aghnet.GetValidNetInterfacesForWeb()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cannot get network interfaces: %w\", err)\n\t\t}\n\n\t\tfor _, iface := range ifaces {\n\t\t\tdst = appendDNSAddrs(dst, iface.Addresses...)\n\t\t}\n\n\t\tifacesAdded = true\n\t}\n\n\treturn dst, nil\n}\n\n// collectDNSAddresses returns the list of DNS addresses the server is listening\n// on, including the addresses on all interfaces in cases of unspecified IPs.\n// tlsMgr must not be nil.\nfunc collectDNSAddresses(tlsMgr *tlsManager) (addrs []string, err error) {\n\tif hosts := config.DNS.BindHosts; len(hosts) == 0 {\n\t\taddrs = appendDNSAddrs(addrs, netutil.IPv4Localhost())\n\t} else {\n\t\taddrs, err = appendDNSAddrsWithIfaces(addrs, hosts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"collecting dns addresses: %w\", err)\n\t\t}\n\t}\n\n\tde := getDNSEncryption(tlsMgr)\n\tif de.https != \"\" {\n\t\taddrs = append(addrs, de.https)\n\t}\n\n\tif de.tls != \"\" {\n\t\taddrs = append(addrs, de.tls)\n\t}\n\n\tif de.quic != \"\" {\n\t\taddrs = append(addrs, de.quic)\n\t}\n\n\treturn addrs, nil\n}\n\n// statusResponse is a response for /control/status endpoint.\ntype statusResponse struct {\n\tVersion  string   `json:\"version\"`\n\tLanguage string   `json:\"language\"`\n\tDNSAddrs []string `json:\"dns_addresses\"`\n\tDNSPort  uint16   `json:\"dns_port\"`\n\tHTTPPort uint16   `json:\"http_port\"`\n\n\t// ProtectionDisabledDuration is the duration of the protection pause in\n\t// milliseconds.\n\tProtectionDisabledDuration int64 `json:\"protection_disabled_duration\"`\n\n\t// StartTime is the start time of the web API server in Unix milliseconds.\n\tStartTime aghhttp.JSONTime `json:\"start_time\"`\n\n\tProtectionEnabled bool `json:\"protection_enabled\"`\n\t// TODO(e.burkov): Inspect if front-end doesn't requires this field as\n\t// openapi.yaml declares.\n\tIsDHCPAvailable bool `json:\"dhcp_available\"`\n\tIsRunning       bool `json:\"running\"`\n}\n\nfunc (web *webAPI) handleStatus(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\tdnsAddrs, err := collectDNSAddresses(web.tlsManager)\n\tif err != nil {\n\t\t// Don't add a lot of formatting, since the error is already\n\t\t// wrapped by collectDNSAddresses.\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tvar (\n\t\tfltConf           *dnsforward.Config\n\t\tprotDisabledUntil *time.Time\n\t\tprotEnabled       bool\n\t)\n\tif globalContext.dnsServer != nil {\n\t\tfltConf = &dnsforward.Config{}\n\t\tglobalContext.dnsServer.WriteDiskConfig(fltConf)\n\t\tprotEnabled, protDisabledUntil = globalContext.dnsServer.UpdatedProtectionStatus(ctx)\n\t}\n\n\tvar resp statusResponse\n\tfunc() {\n\t\tconfig.RLock()\n\t\tdefer config.RUnlock()\n\n\t\tvar protectionDisabledDuration int64\n\t\tif protDisabledUntil != nil {\n\t\t\t// Make sure that we don't send negative numbers to the frontend,\n\t\t\t// since enough time might have passed to make the difference less\n\t\t\t// than zero.\n\t\t\tprotectionDisabledDuration = max(0, time.Until(*protDisabledUntil).Milliseconds())\n\t\t}\n\n\t\tresp = statusResponse{\n\t\t\tVersion:                    version.Version(),\n\t\t\tLanguage:                   config.Language,\n\t\t\tDNSAddrs:                   dnsAddrs,\n\t\t\tDNSPort:                    config.DNS.Port,\n\t\t\tHTTPPort:                   config.HTTPConfig.Address.Port(),\n\t\t\tProtectionDisabledDuration: protectionDisabledDuration,\n\t\t\tStartTime:                  aghhttp.JSONTime(web.startTime),\n\t\t\tProtectionEnabled:          protEnabled,\n\t\t\tIsRunning:                  isRunning(),\n\t\t}\n\t}()\n\n\t// IsDHCPAvailable field is now false by default for Windows.\n\tif runtime.GOOS != \"windows\" {\n\t\tresp.IsDHCPAvailable = globalContext.dhcpServer != nil\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, resp)\n}\n\n// registerControlHandlers sets up HTTP handlers for various control endpoints.\nfunc (web *webAPI) registerControlHandlers() {\n\tmux := web.conf.mux\n\n\tmux.Handle(\n\t\t\"/control/version.json\",\n\t\tweb.postInstallHandler(http.HandlerFunc(web.handleVersionJSON)),\n\t)\n\tweb.httpReg.Register(http.MethodPost, \"/control/update\", web.handleUpdate)\n\n\tweb.httpReg.Register(http.MethodGet, \"/control/status\", web.handleStatus)\n\tweb.httpReg.Register(\n\t\thttp.MethodPost,\n\t\t\"/control/i18n/change_language\",\n\t\tweb.handleI18nChangeLanguage,\n\t)\n\tweb.httpReg.Register(\n\t\thttp.MethodGet,\n\t\t\"/control/i18n/current_language\",\n\t\tweb.handleI18nCurrentLanguage,\n\t)\n\tweb.httpReg.Register(http.MethodGet, \"/control/profile\", web.handleGetProfile)\n\tweb.httpReg.Register(http.MethodPut, \"/control/profile/update\", web.handlePutProfile)\n\n\t// No authentication is required for DoH/DoT configuration endpoints.\n\tmux.Handle(\n\t\t\"/apple/doh.mobileconfig\",\n\t\tweb.postInstallHandler(http.HandlerFunc(handleMobileConfigDoH)),\n\t)\n\tmux.Handle(\n\t\t\"/apple/dot.mobileconfig\",\n\t\tweb.postInstallHandler(http.HandlerFunc(handleMobileConfigDoT)),\n\t)\n\n\tweb.registerAuthHandlers()\n}\n\n// webMw provides middleware for route handlers.  The set method must be called\n// to initialize the middleware.\ntype webMw struct {\n\t// postInstallMw is middleware that verifies that AdGuard Home is not\n\t// running for the first time.\n\tpostInstallMw func(h http.Handler) (wrapped http.Handler)\n\n\t// ensureMw is like postInstallMw, but also applies gzip and enforces the\n\t// HTTP method.\n\tensureMw aghhttp.WrapFunc\n}\n\n// set sets the middleware functions used to build handler chains.\nfunc (mw *webMw) set(web *webAPI) {\n\tmw.postInstallMw = web.postInstallHandler\n\n\tmw.ensureMw = func(method string, h http.HandlerFunc) (wrapped http.Handler) {\n\t\treturn web.postInstallHandler(gziphandler.GzipHandler(web.ensure(method, h)))\n\t}\n}\n\n// wrap returns a wrapped HTTP handler for the given route.\n//\n// TODO(s.chzhen):  Implement [httputil.Middleware].\nfunc (mw *webMw) wrap(method string, h http.HandlerFunc) (wrapped http.Handler) {\n\tf := func(w http.ResponseWriter, r *http.Request) {\n\t\tvar handler http.Handler\n\t\tif method == \"\" {\n\t\t\t// The \"/dns-query\" handler doesn't require authentication or gzip,\n\t\t\t// and it isn't restricted to a single HTTP method.\n\t\t\thandler = mw.postInstallMw(h)\n\t\t} else {\n\t\t\thandler = mw.ensureMw(method, h)\n\t\t}\n\n\t\thandler.ServeHTTP(w, r)\n\t}\n\n\treturn http.HandlerFunc(f)\n}\n\n// ensure returns a wrapped handler that verifies the request method.  It also\n// performs additional method and header checks.\nfunc (web *webAPI) ensure(\n\tmethod string,\n\thandler func(http.ResponseWriter, *http.Request),\n) (wrapped http.HandlerFunc) {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tm := r.Method\n\t\tif m != method {\n\t\t\taghhttp.ErrorAndLog(\n\t\t\t\tr.Context(),\n\t\t\t\tweb.logger,\n\t\t\t\tr,\n\t\t\t\tw,\n\t\t\t\thttp.StatusMethodNotAllowed,\n\t\t\t\t\"only method %s is allowed\",\n\t\t\t\tmethod,\n\t\t\t)\n\n\t\t\treturn\n\t\t}\n\n\t\tif modifiesData(m) {\n\t\t\tif !web.ensureContentType(w, r) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tglobalContext.controlLock.Lock()\n\t\t\tdefer globalContext.controlLock.Unlock()\n\t\t}\n\n\t\thandler(w, r)\n\t}\n}\n\n// modifiesData returns true if m is an HTTP method that can modify data.\nfunc modifiesData(m string) (ok bool) {\n\treturn m == http.MethodPost || m == http.MethodPut || m == http.MethodDelete\n}\n\n// ensureContentType makes sure that the content type of a data-modifying\n// request is set correctly.  If it is not, ensureContentType writes a response\n// to w, and ok is false.\nfunc (web *webAPI) ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) {\n\tconst statusUnsup = http.StatusUnsupportedMediaType\n\n\tctx := r.Context()\n\n\tcType := r.Header.Get(httphdr.ContentType)\n\tif r.ContentLength == 0 {\n\t\tif cType == \"\" {\n\t\t\treturn true\n\t\t}\n\n\t\t// Assume that browsers always send a content type when submitting HTML\n\t\t// forms and require no content type for requests with no body to make\n\t\t// sure that the request comes from JavaScript.\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tweb.logger,\n\t\t\tr,\n\t\t\tw,\n\t\t\tstatusUnsup,\n\t\t\t\"empty body with content-type %q not allowed\",\n\t\t\tcType,\n\t\t)\n\n\t\treturn false\n\n\t}\n\n\tconst wantCType = aghhttp.HdrValApplicationJSON\n\tif cType == wantCType {\n\t\treturn true\n\t}\n\n\taghhttp.ErrorAndLog(\n\t\tctx,\n\t\tweb.logger,\n\t\tr,\n\t\tw,\n\t\tstatusUnsup,\n\t\t\"only content-type %s is allowed\",\n\t\twantCType,\n\t)\n\n\treturn false\n}\n\n// preInstallHandler lets the handler run only if firstRun is true; it does not\n// perform redirects.\nfunc (web *webAPI) preInstallHandler(handler http.Handler) (wrapped http.Handler) {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif !web.conf.firstRun {\n\t\t\t// If it's not first run, do not allow access to install-only routes\n\t\t\t// (for example, /install.html once configuration is complete).\n\t\t\thttp.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\n\t\t\treturn\n\t\t}\n\n\t\thandler.ServeHTTP(w, r)\n\t})\n}\n\n// handleHTTPSRedirect redirects the request to HTTPS, if needed, and adds some\n// HTTPS-related headers.  If proceed is true, the middleware must continue\n// handling the request.\nfunc (web *webAPI) handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (proceed bool) {\n\tif web.httpsServer.server == nil {\n\t\treturn true\n\t}\n\n\tctx := r.Context()\n\n\thost, err := netutil.SplitHost(r.Host)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, web.logger, r, w, http.StatusBadRequest, \"bad host: %s\", err)\n\n\t\treturn false\n\t}\n\n\tvar (\n\t\tforceHTTPS bool\n\t\tserveHTTP3 bool\n\t\tportHTTPS  uint16\n\t)\n\tfunc() {\n\t\tconfig.RLock()\n\t\tdefer config.RUnlock()\n\n\t\tserveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS\n\t\tforceHTTPS = config.TLS.ForceHTTPS && config.TLS.Enabled && config.TLS.PortHTTPS != 0\n\t}()\n\n\trespHdr := w.Header()\n\n\t// Let the browser know that server supports HTTP/3.\n\t//\n\t// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Alt-Svc.\n\t//\n\t// TODO(a.garipov): Consider adding a configurable max-age.  Currently, the\n\t// default is 24 hours.\n\tif serveHTTP3 {\n\t\taltSvc := fmt.Sprintf(`h3=\":%d\"`, portHTTPS)\n\t\trespHdr.Set(httphdr.AltSvc, altSvc)\n\t}\n\n\tif forceHTTPS {\n\t\tif r.TLS == nil {\n\t\t\tu := httpsURL(r.URL, host, portHTTPS)\n\t\t\thttp.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)\n\n\t\t\treturn false\n\t\t}\n\n\t\t// TODO(a.garipov): Consider adding a configurable max-age.  Currently,\n\t\t// the default is 365 days.\n\t\trespHdr.Set(httphdr.StrictTransportSecurity, aghhttp.HdrValStrictTransportSecurity)\n\t}\n\n\t// Allow the frontend from the HTTP origin to send requests to the HTTPS\n\t// server.  This can happen when the user has just set up HTTPS with\n\t// redirects.  Prevent cache-related errors by setting the Vary header.\n\t//\n\t// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin.\n\toriginURL := &url.URL{\n\t\tScheme: urlutil.SchemeHTTP,\n\t\tHost:   r.Host,\n\t}\n\n\trespHdr.Set(httphdr.AccessControlAllowOrigin, originURL.String())\n\trespHdr.Set(httphdr.Vary, httphdr.Origin)\n\n\treturn true\n}\n\n// httpsURL returns a copy of u for redirection to the HTTPS version, taking the\n// hostname and the HTTPS port into account.\nfunc httpsURL(u *url.URL, host string, portHTTPS uint16) (redirectURL *url.URL) {\n\thostPort := host\n\tif portHTTPS != defaultPortHTTPS {\n\t\thostPort = netutil.JoinHostPort(host, portHTTPS)\n\t}\n\n\treturn &url.URL{\n\t\tScheme:   urlutil.SchemeHTTPS,\n\t\tHost:     hostPort,\n\t\tPath:     u.Path,\n\t\tRawQuery: u.RawQuery,\n\t}\n}\n\n// postInstallHandler lets the handler to run only if firstRun is false.\n// Otherwise, it redirects to /install.html.  It also enforces HTTPS if it is\n// enabled and configured and sets appropriate access control headers.\nfunc (web *webAPI) postInstallHandler(handler http.Handler) (wrapped http.Handler) {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tpath := r.URL.Path\n\t\tif web.conf.firstRun &&\n\t\t\t!strings.HasPrefix(path, \"/install.\") &&\n\t\t\t!strings.HasPrefix(path, \"/assets/\") {\n\t\t\thttp.Redirect(w, r, \"install.html\", http.StatusFound)\n\n\t\t\treturn\n\t\t}\n\n\t\tif web.handleHTTPSRedirect(w, r) {\n\t\t\thandler.ServeHTTP(w, r)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/home/controlinstall.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/quic-go/quic-go/http3\"\n)\n\n// getAddrsResponse is the response for /install/get_addresses endpoint.\ntype getAddrsResponse struct {\n\tInterfaces map[string]*aghnet.NetInterface `json:\"interfaces\"`\n\n\t// Version is the version of AdGuard Home.\n\t//\n\t// TODO(a.garipov): In the new API, rename this endpoint to something more\n\t// general, since there will be more information here than just network\n\t// interfaces.\n\tVersion string `json:\"version\"`\n\n\tWebPort int `json:\"web_port\"`\n\tDNSPort int `json:\"dns_port\"`\n}\n\n// handleInstallGetAddresses is the handler for /install/get_addresses endpoint.\nfunc (web *webAPI) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\tdata := getAddrsResponse{\n\t\tVersion: version.Version(),\n\n\t\tWebPort: int(web.conf.defaultWebPort),\n\t\tDNSPort: int(defaultPortDNS),\n\t}\n\n\tifaces, err := aghnet.GetValidNetInterfacesForWeb()\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"Couldn't get interfaces: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tdata.Interfaces = make(map[string]*aghnet.NetInterface)\n\tfor _, iface := range ifaces {\n\t\tdata.Interfaces[iface.Name] = iface\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, data)\n}\n\ntype checkConfReqEnt struct {\n\tIP      netip.Addr `json:\"ip\"`\n\tPort    uint16     `json:\"port\"`\n\tAutofix bool       `json:\"autofix\"`\n}\n\ntype checkConfReq struct {\n\tWeb         checkConfReqEnt `json:\"web\"`\n\tDNS         checkConfReqEnt `json:\"dns\"`\n\tSetStaticIP bool            `json:\"set_static_ip\"`\n}\n\ntype checkConfRespEnt struct {\n\tStatus     string `json:\"status\"`\n\tCanAutofix bool   `json:\"can_autofix\"`\n}\n\ntype staticIPJSON struct {\n\tStatic string `json:\"static\"`\n\tIP     string `json:\"ip\"`\n\tError  string `json:\"error\"`\n}\n\ntype checkConfResp struct {\n\tStaticIP staticIPJSON     `json:\"static_ip\"`\n\tWeb      checkConfRespEnt `json:\"web\"`\n\tDNS      checkConfRespEnt `json:\"dns\"`\n}\n\n// validateWeb returns error is the web part if the initial configuration can't\n// be set.\nfunc (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"validating ports: %w\") }()\n\n\t// TODO(a.garipov): Declare all port variables anywhere as uint16.\n\treqPort := req.Web.Port\n\tport := tcpPort(reqPort)\n\taddPorts(tcpPorts, port)\n\tif err = tcpPorts.Validate(); err != nil {\n\t\t// Reset the value for the port to 1 to make sure that validateDNS\n\t\t// doesn't throw the same error, unless the same TCP port is set there\n\t\t// as well.\n\t\ttcpPorts[port] = 1\n\n\t\treturn err\n\t}\n\n\tswitch reqPort {\n\tcase 0, config.HTTPConfig.Address.Port():\n\t\treturn nil\n\tdefault:\n\t\t// Go on and check the port binding only if it's not zero or won't be\n\t\t// unbound after install.\n\t}\n\n\treturn aghnet.CheckPort(\"tcp\", netip.AddrPortFrom(req.Web.IP, reqPort))\n}\n\n// validateDNS returns error if the DNS part of the initial configuration can't\n// be set.  canAutofix is true if the port can be unbound by AdGuard Home\n// automatically.  cmdCons must not be nil.\nfunc (req *checkConfReq) validateDNS(\n\tctx context.Context,\n\tl *slog.Logger,\n\ttcpPorts aghalg.UniqChecker[tcpPort],\n\tcmdCons executil.CommandConstructor,\n) (canAutofix bool, err error) {\n\tdefer func() { err = errors.Annotate(err, \"validating ports: %w\") }()\n\n\tport := req.DNS.Port\n\tswitch port {\n\tcase 0:\n\t\treturn false, nil\n\tcase config.HTTPConfig.Address.Port():\n\t\t// Go on and only check the UDP port since the TCP one is already bound\n\t\t// by AdGuard Home for web interface.\n\tdefault:\n\t\t// Check TCP as well.\n\t\taddPorts(tcpPorts, tcpPort(port))\n\t\tif err = tcpPorts.Validate(); err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\terr = aghnet.CheckPort(\"tcp\", netip.AddrPortFrom(req.DNS.IP, port))\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\terr = aghnet.CheckPort(\"udp\", netip.AddrPortFrom(req.DNS.IP, port))\n\tif !aghnet.IsAddrInUse(err) {\n\t\treturn false, err\n\t}\n\n\t// Try to fix automatically.\n\tcanAutofix = checkDNSStubListener(ctx, l)\n\tif canAutofix && req.DNS.Autofix {\n\t\tif derr := disableDNSStubListener(ctx, l, cmdCons); derr != nil {\n\t\t\tl.ErrorContext(ctx, \"disabling DNSStubListener\", slogutil.KeyError, derr)\n\t\t}\n\n\t\terr = aghnet.CheckPort(\"udp\", netip.AddrPortFrom(req.DNS.IP, port))\n\t\tcanAutofix = false\n\t}\n\n\treturn canAutofix, err\n}\n\n// handleInstallCheckConfig handles the /check_config endpoint.\nfunc (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\treq := &checkConfReq{}\n\n\terr := json.NewDecoder(r.Body).Decode(req)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"decoding the request: %s\", err)\n\n\t\treturn\n\t}\n\n\tresp := &checkConfResp{}\n\ttcpPorts := aghalg.UniqChecker[tcpPort]{}\n\tif err = req.validateWeb(tcpPorts); err != nil {\n\t\tresp.Web.Status = err.Error()\n\t}\n\n\tresp.DNS.CanAutofix, err = req.validateDNS(ctx, l, tcpPorts, web.cmdCons)\n\tif err != nil {\n\t\tresp.DNS.Status = err.Error()\n\t} else if !req.DNS.IP.IsUnspecified() {\n\t\tresp.StaticIP = handleStaticIP(ctx, l, req.DNS.IP, req.SetStaticIP, web.cmdCons)\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, resp)\n}\n\n// handleStaticIP checks and optionally sets a static IP on the interface that\n// owns IP.  cmdCons must not be nil.\nfunc handleStaticIP(\n\tctx context.Context,\n\tl *slog.Logger,\n\tip netip.Addr,\n\tset bool,\n\tcmdCons executil.CommandConstructor,\n) (ipResp staticIPJSON) {\n\tinterfaceName := aghnet.InterfaceByIP(ip)\n\tipResp.Static = \"no\"\n\n\tif interfaceName == \"\" {\n\t\tipResp.Static = \"error\"\n\t\tipResp.Error = fmt.Sprintf(\"Couldn't find network interface by IP %s\", ip)\n\n\t\treturn ipResp\n\t}\n\n\tif set {\n\t\t// Try to set a static IP for the specified interface.\n\t\terr := aghnet.IfaceSetStaticIP(ctx, l, cmdCons, interfaceName)\n\t\tif err != nil {\n\t\t\tipResp.Static = \"error\"\n\t\t\tipResp.Error = err.Error()\n\n\t\t\treturn ipResp\n\t\t}\n\t}\n\n\t// Fall through even if we just set the static IP.  Check whether the\n\t// interface has a static IP and return the details.\n\tisStaticIP, err := aghnet.IfaceHasStaticIP(ctx, cmdCons, interfaceName)\n\tif err != nil {\n\t\tipResp.Static = \"error\"\n\t\tipResp.Error = err.Error()\n\n\t\treturn ipResp\n\t}\n\n\tif isStaticIP {\n\t\tipResp.Static = \"yes\"\n\t}\n\tipResp.IP = aghnet.GetSubnet(ctx, l, interfaceName).String()\n\n\treturn ipResp\n}\n\n// checkDNSStubListener returns true if DNSStubListener is active.\nfunc checkDNSStubListener(ctx context.Context, l *slog.Logger) (ok bool) {\n\tif runtime.GOOS != \"linux\" {\n\t\treturn false\n\t}\n\n\tcmds := container.KeyValues[string, []string]{{\n\t\tKey:   \"systemctl\",\n\t\tValue: []string{\"is-enabled\", \"systemd-resolved\"},\n\t}, {\n\t\tKey:   \"grep\",\n\t\tValue: []string{\"-E\", \"#?DNSStubListener=yes\", \"/etc/systemd/resolved.conf\"},\n\t}}\n\n\tfor _, cmd := range cmds {\n\t\tl.DebugContext(ctx, \"executing\", \"cmd\", cmd.Key, \"args\", cmd.Value)\n\n\t\terr := executil.RunWithPeek(\n\t\t\tctx,\n\t\t\texecutil.SystemCommandConstructor{},\n\t\t\tagh.DefaultOutputLimit,\n\t\t\tcmd.Key,\n\t\t\tcmd.Value...,\n\t\t)\n\t\tif err != nil {\n\t\t\tl.InfoContext(ctx, \"execution failed\", \"cmd\", cmd.Key, slogutil.KeyError, err)\n\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nconst (\n\tresolvedConfPath = \"/etc/systemd/resolved.conf.d/adguardhome.conf\"\n\tresolvedConfData = `[Resolve]\nDNS=127.0.0.1\nDNSStubListener=no\n`\n)\nconst resolvConfPath = \"/etc/resolv.conf\"\n\n// disableDNSStubListener deactivates DNSStubListerner and returns an error, if\n// any.  cmdCons must not be nil.\nfunc disableDNSStubListener(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n) (err error) {\n\tdir := filepath.Dir(resolvedConfPath)\n\terr = os.MkdirAll(dir, 0o755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"os.MkdirAll: %s: %w\", dir, err)\n\t}\n\n\terr = os.WriteFile(resolvedConfPath, []byte(resolvedConfData), 0o644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"os.WriteFile: %s: %w\", resolvedConfPath, err)\n\t}\n\n\t_ = os.Rename(resolvConfPath, resolvConfPath+\".backup\")\n\terr = os.Symlink(\"/run/systemd/resolve/resolv.conf\", resolvConfPath)\n\tif err != nil {\n\t\t_ = os.Remove(resolvedConfPath) // remove the file we've just created\n\t\treturn fmt.Errorf(\"os.Symlink: %s: %w\", resolvConfPath, err)\n\t}\n\n\tconst systemctlCmd = \"systemctl\"\n\n\tsystemctlArgs := []string{\"reload-or-restart\", \"systemd-resolved\"}\n\n\tl.DebugContext(ctx, \"executing\", \"cmd\", systemctlCmd, \"args\", systemctlArgs)\n\n\terr = executil.RunWithPeek(\n\t\tctx,\n\t\tcmdCons,\n\t\tagh.DefaultOutputLimit,\n\t\tsystemctlCmd,\n\t\tsystemctlArgs...,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"executing cmd: %w\", err)\n\t}\n\n\treturn nil\n}\n\ntype applyConfigReqEnt struct {\n\tIP   netip.Addr `json:\"ip\"`\n\tPort uint16     `json:\"port\"`\n}\n\ntype applyConfigReq struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n\n\tWeb applyConfigReqEnt `json:\"web\"`\n\tDNS applyConfigReqEnt `json:\"dns\"`\n}\n\n// copyInstallSettings copies the installation parameters between two\n// configuration structures.\nfunc copyInstallSettings(dst, src *configuration) {\n\tdst.HTTPConfig = src.HTTPConfig\n\tdst.DNS.BindHosts = src.DNS.BindHosts\n\tdst.DNS.Port = src.DNS.Port\n}\n\n// shutdownTimeout is the timeout for shutting HTTP server down operation.\nconst shutdownTimeout = 5 * time.Second\n\n// shutdownSrv shuts down srv and logs the error, if any.  l must not be nil.\nfunc shutdownSrv(ctx context.Context, l *slog.Logger, srv *http.Server) {\n\tdefer slogutil.RecoverAndLog(ctx, l)\n\n\tif srv == nil {\n\t\treturn\n\t}\n\n\terr := srv.Shutdown(ctx)\n\tif err == nil {\n\t\treturn\n\t}\n\n\tlvl := slog.LevelDebug\n\tif !errors.Is(err, context.Canceled) {\n\t\tlvl = slog.LevelError\n\t}\n\n\tl.Log(ctx, lvl, \"shutting down http server\", \"addr\", srv.Addr, slogutil.KeyError, err)\n}\n\n// shutdownSrv3 shuts down srv and logs the error, if any.  l must not be nil.\n//\n// TODO(a.garipov): Think of a good way to merge with [shutdownSrv].\nfunc shutdownSrv3(ctx context.Context, l *slog.Logger, srv *http3.Server) {\n\tdefer slogutil.RecoverAndLog(ctx, l)\n\n\tif srv == nil {\n\t\treturn\n\t}\n\n\terr := srv.Close()\n\tif err == nil {\n\t\treturn\n\t}\n\n\tlvl := slog.LevelDebug\n\tif !errors.Is(err, context.Canceled) {\n\t\tlvl = slog.LevelError\n\t}\n\n\tl.Log(ctx, lvl, \"shutting down http/3 server\", \"addr\", srv.Addr, slogutil.KeyError, err)\n}\n\n// PasswordMinRunes is the minimum length of user's password in runes.\nconst PasswordMinRunes = 8\n\n// Apply new configuration, start DNS server, restart Web server\nfunc (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\treq, restartHTTP, err := decodeApplyConfigReq(r.Body)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tif utf8.RuneCountInString(req.Password) < PasswordMinRunes {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusUnprocessableEntity,\n\t\t\t\"password must be at least %d symbols long\",\n\t\t\tPasswordMinRunes,\n\t\t)\n\n\t\treturn\n\t}\n\n\terr = aghnet.CheckPort(\"udp\", netip.AddrPortFrom(req.DNS.IP, req.DNS.Port))\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\terr = aghnet.CheckPort(\"tcp\", netip.AddrPortFrom(req.DNS.IP, req.DNS.Port))\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tweb.finalizeInstall(ctx, w, r, req, restartHTTP)\n}\n\n// finalizeInstall completes first-run setup by applying user-provided settings.\n// w, r, and req must not be nil.\nfunc (web *webAPI) finalizeInstall(\n\tctx context.Context,\n\tw http.ResponseWriter,\n\tr *http.Request,\n\treq *applyConfigReq,\n\trestartHTTP bool,\n) {\n\tl := web.logger\n\n\tvar err error\n\tcurConfig := &configuration{}\n\tcopyInstallSettings(curConfig, config)\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tcopyInstallSettings(config, curConfig)\n\t\t}\n\t}()\n\n\tconfig.DNS.BindHosts = []netip.Addr{req.DNS.IP}\n\tconfig.DNS.Port = req.DNS.Port\n\tconfig.Filtering.Logger = web.baseLogger.With(slogutil.KeyPrefix, \"filtering\")\n\tconfig.Filtering.SafeFSPatterns = []string{\n\t\tfilepath.Join(web.conf.workDir, userFilterDataDir, \"*\"),\n\t}\n\tconfig.HTTPConfig.Address = netip.AddrPortFrom(req.Web.IP, req.Web.Port)\n\n\tu := &webUser{\n\t\tName: req.Username,\n\t}\n\terr = web.auth.addUser(ctx, u, req.Password)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusUnprocessableEntity, \"%s\", err)\n\n\t\treturn\n\t}\n\n\t// TODO(e.burkov): StartMods() should be put in a separate goroutine at the\n\t// moment we'll allow setting up TLS in the initial configuration or the\n\t// configuration itself will use HTTPS protocol, because the underlying\n\t// functions potentially restart the HTTPS server.\n\terr = startMods(\n\t\tctx,\n\t\tweb.baseLogger,\n\t\tweb.tlsManager,\n\t\tweb.confModifier,\n\t\tweb.httpReg,\n\t\tweb.conf.workDir,\n\t)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"%s\", err)\n\n\t\treturn\n\t}\n\n\terr = config.write(\n\t\tctx,\n\t\tweb.logger,\n\t\tweb.tlsManager,\n\t\tweb.auth,\n\t\tweb.conf.workDir,\n\t\tweb.conf.confPath,\n\t)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"Couldn't write config: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tweb.conf.firstRun = false\n\tweb.conf.BindAddr = netip.AddrPortFrom(req.Web.IP, req.Web.Port)\n\n\tweb.registerControlHandlers()\n\n\taghhttp.OK(ctx, l, w)\n\n\trc := http.NewResponseController(w)\n\terr = rc.Flush()\n\tif err != nil {\n\t\tl.WarnContext(ctx, \"flushing response\", slogutil.KeyError, err)\n\t}\n\n\tif !restartHTTP {\n\t\treturn\n\t}\n\n\t// Method http.(*Server).Shutdown needs to be called in a separate goroutine\n\t// and with its own context, because it waits until all requests are handled\n\t// and will be blocked by it's own caller.\n\tgo func(timeout time.Duration) {\n\t\tshutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), timeout)\n\t\tdefer slogutil.RecoverAndLog(shutdownCtx, l)\n\t\tdefer cancel()\n\n\t\tshutdownSrv(shutdownCtx, l, web.httpServer)\n\t}(shutdownTimeout)\n}\n\n// decodeApplyConfigReq decodes the configuration, validates some parameters,\n// and returns it along with the boolean indicating whether or not the HTTP\n// server must be restarted.\nfunc decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, err error) {\n\treq = &applyConfigReq{}\n\terr = json.NewDecoder(r).Decode(&req)\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"parsing request: %w\", err)\n\t}\n\n\tif req.Web.Port == 0 || req.DNS.Port == 0 {\n\t\treturn nil, false, errors.Error(\"ports cannot be 0\")\n\t}\n\n\taddrPort := config.HTTPConfig.Address\n\trestartHTTP = addrPort.Addr() != req.Web.IP || addrPort.Port() != req.Web.Port\n\tif restartHTTP {\n\t\terr = aghnet.CheckPort(\"tcp\", netip.AddrPortFrom(req.Web.IP, req.Web.Port))\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\n\t\t\t\t\"checking address %s:%d: %w\",\n\t\t\t\treq.Web.IP.String(),\n\t\t\t\treq.Web.Port,\n\t\t\t\terr,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn req, restartHTTP, err\n}\n\n// startMods initializes and starts the DNS server after installation.\n// baseLogger, tlsMgr, confModifier, and httpReg must not be nil.\nfunc startMods(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\ttlsMgr *tlsManager,\n\tconfModifier agh.ConfigModifier,\n\thttpReg aghhttp.Registrar,\n\tworkDir string,\n) (err error) {\n\tstatsDir, querylogDir, err := checkStatsAndQuerylogDirs(config, workDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = initDNS(ctx, baseLogger, tlsMgr, confModifier, httpReg, statsDir, querylogDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttlsMgr.start(ctx)\n\n\terr = startDNSServer()\n\tif err != nil {\n\t\tcloseDNSServer(ctx)\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// registerInstallHandlers registers install handlers.\nfunc (web *webAPI) registerInstallHandlers() {\n\tmux := web.conf.mux\n\n\tmux.Handle(\n\t\thttp.MethodGet+\" \"+\"/control/install/get_addresses\",\n\t\tweb.preInstallHandler(http.HandlerFunc(web.handleInstallGetAddresses)),\n\t)\n\tmux.Handle(\n\t\t\"/control/install/check_config\",\n\t\tweb.preInstallHandler(web.ensure(http.MethodPost, web.handleInstallCheckConfig)),\n\t)\n\tmux.Handle(\n\t\t\"/control/install/configure\",\n\t\tweb.preInstallHandler(web.ensure(http.MethodPost, web.handleInstallConfigure)),\n\t)\n}\n"
  },
  {
    "path": "internal/home/controlupdate.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/updater\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\n// temporaryError is the interface for temporary errors from the Go standard\n// library.\ntype temporaryError interface {\n\terror\n\tTemporary() (ok bool)\n}\n\n// handleVersionJSON is the handler for the POST /control/version.json HTTP API.\n//\n// TODO(a.garipov): Find out if this API used with a GET method by anyone.\nfunc (web *webAPI) handleVersionJSON(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\tresp := &versionResponse{}\n\tif web.conf.disableUpdate {\n\t\tresp.Disabled = true\n\t\taghhttp.WriteJSONResponseOK(ctx, l, w, r, resp)\n\n\t\treturn\n\t}\n\n\treq := &struct {\n\t\tRecheck bool `json:\"recheck_now\"`\n\t}{}\n\n\tvar err error\n\tif r.ContentLength != 0 {\n\t\terr = json.NewDecoder(r.Body).Decode(req)\n\t\tif err != nil {\n\t\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"parsing request: %s\", err)\n\n\t\t\treturn\n\t\t}\n\t}\n\n\terr = web.requestVersionInfo(ctx, resp, req.Recheck)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadGateway, \"%s\", err)\n\n\t\treturn\n\t}\n\n\terr = resp.setAllowedToAutoUpdate(ctx, l, web.tlsManager)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"%s\", err)\n\n\t\treturn\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, resp)\n}\n\n// requestVersionInfo sets the VersionInfo field of resp if it can reach the\n// update server.\nfunc (web *webAPI) requestVersionInfo(\n\tctx context.Context,\n\tresp *versionResponse,\n\trecheck bool,\n) (err error) {\n\tupdater := web.conf.updater\n\tfor range 3 {\n\t\tresp.VersionInfo, err = updater.VersionInfo(ctx, recheck)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tvar terr temporaryError\n\t\tif errors.As(err, &terr) && terr.Temporary() {\n\t\t\t// Temporary network error.  This case may happen while we're\n\t\t\t// restarting our DNS server.  Log and sleep for some time.\n\t\t\t//\n\t\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/934.\n\t\t\tconst sleepTime = 2 * time.Second\n\n\t\t\terr = fmt.Errorf(\"temp net error: %w; sleeping for %s and retrying\", err, sleepTime)\n\t\t\tweb.logger.InfoContext(ctx, \"updating version info\", slogutil.KeyError, err)\n\n\t\t\ttime.Sleep(sleepTime)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tbreak\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting version info: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// handleUpdate performs an update to the latest available version procedure.\nfunc (web *webAPI) handleUpdate(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\tupdater := web.conf.updater\n\tif updater.NewVersion() == \"\" {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"/update request isn't allowed now\",\n\t\t)\n\n\t\treturn\n\t}\n\n\t// Retain the current absolute path of the executable, since the updater is\n\t// likely to change the position current one to the backup directory.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/4735.\n\texecPath, err := os.Executable()\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"getting path: %s\", err)\n\n\t\treturn\n\t}\n\n\terr = updater.Update(ctx, false)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"%s\", err)\n\n\t\treturn\n\t}\n\n\taghhttp.OK(ctx, web.logger, w)\n\n\trc := http.NewResponseController(w)\n\terr = rc.Flush()\n\tif err != nil {\n\t\tweb.logger.WarnContext(ctx, \"flushing response\", slogutil.KeyError, err)\n\t}\n\n\t// The background context is used because the underlying functions wrap it\n\t// with timeout and shut down the server, which handles current request.  It\n\t// also should be done in a separate goroutine for the same reason.\n\tgo finishUpdate(\n\t\tcontext.Background(),\n\t\tweb.logger,\n\t\tweb.cmdCons,\n\t\texecPath,\n\t\tweb.conf.runningAsService,\n\t)\n}\n\n// versionResponse is the response for /control/version.json endpoint.\ntype versionResponse struct {\n\tupdater.VersionInfo\n\tDisabled bool `json:\"disabled\"`\n}\n\n// setAllowedToAutoUpdate sets CanAutoUpdate to true if AdGuard Home is actually\n// allowed to perform an automatic update by the OS.  l and tlsMgr must not be\n// nil.\nfunc (vr *versionResponse) setAllowedToAutoUpdate(\n\tctx context.Context,\n\tl *slog.Logger,\n\ttlsMgr *tlsManager,\n) (err error) {\n\tif vr.CanAutoUpdate != aghalg.NBTrue {\n\t\treturn nil\n\t}\n\n\tcanUpdate := true\n\tif tlsConfUsesPrivilegedPorts(tlsMgr.config()) ||\n\t\tconfig.HTTPConfig.Address.Port() < 1024 ||\n\t\tconfig.DNS.Port < 1024 {\n\t\tcanUpdate, err = aghnet.CanBindPrivilegedPorts(ctx, l)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"checking ability to bind privileged ports: %w\", err)\n\t\t}\n\t}\n\n\tvr.CanAutoUpdate = aghalg.BoolToNullBool(canUpdate)\n\n\treturn nil\n}\n\n// tlsConfUsesPrivilegedPorts returns true if the provided TLS configuration\n// indicates that privileged ports are used.\nfunc tlsConfUsesPrivilegedPorts(c *tlsConfigSettings) (ok bool) {\n\treturn c.Enabled && (c.PortHTTPS < 1024 || c.PortDNSOverTLS < 1024 || c.PortDNSOverQUIC < 1024)\n}\n\n// finishUpdate completes an update procedure.  It is intended to be used as a\n// goroutine.  l and cmdCons must not be nil.\nfunc finishUpdate(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n\texecPath string,\n\trunningAsService bool,\n) {\n\tdefer slogutil.RecoverAndExit(ctx, l, osutil.ExitCodeFailure)\n\n\tl.InfoContext(ctx, \"stopping all tasks\")\n\n\tcleanup(ctx)\n\tcleanupAlways()\n\n\tif runtime.GOOS == \"windows\" {\n\t\tfinalizeWindowsUpdate(ctx, l, cmdCons, execPath, runningAsService)\n\n\t\tos.Exit(osutil.ExitCodeSuccess)\n\t}\n\n\tvar err error\n\tl.InfoContext(ctx, \"restarting\", \"exec_path\", execPath, \"args\", os.Args[1:])\n\terr = syscall.Exec(execPath, os.Args, os.Environ())\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"restarting: %w\", err))\n\t}\n}\n\n// finalizeWindowsUpdate completes an update procedure on windows.  l and\n// cmdCons must not be nil.\nfunc finalizeWindowsUpdate(ctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n\texecPath string,\n\trunningAsService bool,\n) {\n\tvar commandConf *executil.CommandConfig\n\n\tif runningAsService {\n\t\t// NOTE: We can't restart the service via \"kardianos/service\" package,\n\t\t// because it kills the process first we can't start a new instance,\n\t\t// because Windows doesn't allow it.\n\t\t//\n\t\t// TODO(a.garipov): Recheck the claim above.\n\t\tcommandConf = &executil.CommandConfig{\n\t\t\tPath: \"cmd\",\n\t\t\tArgs: []string{\"/c\", \"net stop AdGuardHome & net start AdGuardHome\"},\n\t\t}\n\t} else {\n\t\tcommandConf = &executil.CommandConfig{\n\t\t\tPath:   execPath,\n\t\t\tArgs:   os.Args[1:],\n\t\t\tStdin:  os.Stdin,\n\t\t\tStdout: os.Stdout,\n\t\t\tStderr: os.Stderr,\n\t\t}\n\t}\n\n\tl.InfoContext(ctx, \"restarting\", \"exec_path\", execPath, \"args\", os.Args[1:])\n\n\tvar cmd executil.Command\n\tcmd, err := cmdCons.New(ctx, commandConf)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"constructing cmd: %w\", err))\n\t}\n\n\terr = cmd.Start(ctx)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"restarting: %w\", err))\n\t}\n}\n"
  },
  {
    "path": "internal/home/dns.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dnsforward\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/querylog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/stats\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/ameshkov/dnscrypt/v2\"\n\tyaml \"go.yaml.in/yaml/v4\"\n)\n\n// Default listening ports.\nconst (\n\tdefaultPortDNS   uint16 = 53\n\tdefaultPortHTTP  uint16 = 80\n\tdefaultPortHTTPS uint16 = 443\n\tdefaultPortQUIC  uint16 = 853\n\tdefaultPortTLS   uint16 = 853\n)\n\n// initDNS updates all the fields of the [globalContext] needed to initialize\n// the DNS server and initializes it at last.  It also must not be called unless\n// [config] and [globalContext] are initialized.  baseLogger, tlsMgr,\n// confModifier, and httpReg must not be nil.\nfunc initDNS(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\ttlsMgr *tlsManager,\n\tconfModifier agh.ConfigModifier,\n\thttpReg aghhttp.Registrar,\n\tstatsDir string,\n\tquerylogDir string,\n) (err error) {\n\tanonymizer := config.anonymizer()\n\n\tstatsConf := stats.Config{\n\t\tLogger:            baseLogger.With(slogutil.KeyPrefix, \"stats\"),\n\t\tFilename:          filepath.Join(statsDir, \"stats.db\"),\n\t\tLimit:             time.Duration(config.Stats.Interval),\n\t\tConfigModifier:    confModifier,\n\t\tHTTPReg:           httpReg,\n\t\tEnabled:           config.Stats.Enabled,\n\t\tShouldCountClient: globalContext.clients.shouldCountClient,\n\t}\n\n\tengine, err := aghnet.NewIgnoreEngine(config.Stats.Ignored, config.Stats.IgnoredEnabled)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"statistics: ignored list: %w\", err)\n\t}\n\n\tstatsConf.Ignored = engine\n\tglobalContext.stats, err = stats.New(statsConf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"init stats: %w\", err)\n\t}\n\n\tconf := querylog.Config{\n\t\tLogger:            baseLogger.With(slogutil.KeyPrefix, \"querylog\"),\n\t\tAnonymizer:        anonymizer,\n\t\tConfigModifier:    confModifier,\n\t\tHTTPReg:           httpReg,\n\t\tFindClient:        globalContext.clients.findMultiple,\n\t\tBaseDir:           querylogDir,\n\t\tAnonymizeClientIP: config.DNS.AnonymizeClientIP,\n\t\tRotationIvl:       time.Duration(config.QueryLog.Interval),\n\t\tMemSize:           config.QueryLog.MemSize,\n\t\tEnabled:           config.QueryLog.Enabled,\n\t\tFileEnabled:       config.QueryLog.FileEnabled,\n\t}\n\n\tengine, err = aghnet.NewIgnoreEngine(config.QueryLog.Ignored, config.QueryLog.IgnoredEnabled)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"querylog: ignored list: %w\", err)\n\t}\n\n\tconf.Ignored = engine\n\tglobalContext.queryLog, err = querylog.New(conf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"init querylog: %w\", err)\n\t}\n\n\tglobalContext.filters, err = filtering.New(config.Filtering, nil)\n\tif err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn err\n\t}\n\n\treturn initDNSServer(\n\t\tctx,\n\t\tglobalContext.filters,\n\t\tglobalContext.stats,\n\t\tglobalContext.queryLog,\n\t\tglobalContext.dhcpServer,\n\t\tanonymizer,\n\t\thttpReg,\n\t\ttlsMgr,\n\t\tbaseLogger,\n\t\tconfModifier,\n\t)\n}\n\n// initDNSServer initializes the [context.dnsServer].  To only use the internal\n// proxy, none of the arguments are required, but tlsMgr and l still must not be\n// nil, in other cases all the arguments also must not be nil.  It also must not\n// be called unless [config] and [globalContext] are initialized.\n//\n// TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter.\nfunc initDNSServer(\n\tctx context.Context,\n\tfilters *filtering.DNSFilter,\n\tsts stats.Interface,\n\tqlog querylog.QueryLog,\n\tdhcpSrv dnsforward.DHCP,\n\tanonymizer *aghnet.IPMut,\n\thttpReg aghhttp.Registrar,\n\ttlsMgr *tlsManager,\n\tl *slog.Logger,\n\tconfModifier agh.ConfigModifier,\n) (err error) {\n\tglobalContext.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{\n\t\tLogger:      l,\n\t\tDNSFilter:   filters,\n\t\tStats:       sts,\n\t\tQueryLog:    qlog,\n\t\tPrivateNets: parseSubnetSet(config.DNS.PrivateNets),\n\t\tAnonymizer:  anonymizer,\n\t\tDHCPServer:  dhcpSrv,\n\t\tEtcHosts:    globalContext.etcHosts,\n\t\tLocalDomain: config.DHCP.LocalDomainName,\n\t})\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tcloseDNSServer(ctx)\n\t\t}\n\t}()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsforward.NewServer: %w\", err)\n\t}\n\n\tglobalContext.clients.clientChecker = globalContext.dnsServer\n\n\tdnsConf, err := newServerConfig(\n\t\t&config.DNS,\n\t\tconfig.Clients.Sources,\n\t\ttlsMgr.config(),\n\t\ttlsMgr,\n\t\thttpReg,\n\t\tglobalContext.clients.storage,\n\t\tconfModifier,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"newServerConfig: %w\", err)\n\t}\n\n\t// Try to prepare the server with disabled private RDNS resolution if it\n\t// failed to prepare as is.  See TODO on [dnsforward.PrivateRDNSError].\n\terr = globalContext.dnsServer.Prepare(ctx, dnsConf)\n\tif privRDNSErr := (&dnsforward.PrivateRDNSError{}); errors.As(err, &privRDNSErr) {\n\t\tl.WarnContext(ctx, \"private rdns resolution failed; disabling\", slogutil.KeyError, err)\n\n\t\tdnsConf.UsePrivateRDNS = false\n\t\terr = globalContext.dnsServer.Prepare(ctx, dnsConf)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsServer.Prepare: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// parseSubnetSet parses a slice of subnets.  If the slice is empty, it returns\n// a subnet set that matches all locally served networks, see\n// [netutil.IsLocallyServed].\nfunc parseSubnetSet(nets []netutil.Prefix) (s netutil.SubnetSet) {\n\tswitch len(nets) {\n\tcase 0:\n\t\t// Use an optimized function-based matcher.\n\t\treturn netutil.SubnetSetFunc(netutil.IsLocallyServed)\n\tcase 1:\n\t\treturn nets[0].Prefix\n\tdefault:\n\t\treturn netutil.SliceSubnetSet(netutil.UnembedPrefixes(nets))\n\t}\n}\n\nfunc isRunning() bool {\n\treturn globalContext.dnsServer != nil && globalContext.dnsServer.IsRunning()\n}\n\nfunc ipsToTCPAddrs(ips []netip.Addr, port uint16) (tcpAddrs []*net.TCPAddr) {\n\tif ips == nil {\n\t\treturn nil\n\t}\n\n\ttcpAddrs = make([]*net.TCPAddr, 0, len(ips))\n\tfor _, ip := range ips {\n\t\ttcpAddrs = append(tcpAddrs, net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, port)))\n\t}\n\n\treturn tcpAddrs\n}\n\n// ipsToAddrPorts converts a slice of [netip.Addr] into a slice of\n// [netip.AddrPort] with the given port.\nfunc ipsToAddrPorts(ips []netip.Addr, port uint16) (addrs []netip.AddrPort) {\n\tif ips == nil {\n\t\treturn nil\n\t}\n\n\taddrs = make([]netip.AddrPort, 0, len(ips))\n\tfor _, ip := range ips {\n\t\taddrs = append(addrs, netip.AddrPortFrom(ip, port))\n\t}\n\n\treturn addrs\n}\n\nfunc ipsToUDPAddrs(ips []netip.Addr, port uint16) (udpAddrs []*net.UDPAddr) {\n\tif ips == nil {\n\t\treturn nil\n\t}\n\n\tudpAddrs = make([]*net.UDPAddr, 0, len(ips))\n\tfor _, ip := range ips {\n\t\tudpAddrs = append(udpAddrs, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, port)))\n\t}\n\n\treturn udpAddrs\n}\n\n// newServerConfig converts values from the configuration file into the internal\n// DNS server configuration.  All arguments must not be nil.\nfunc newServerConfig(\n\tdnsConf *dnsConfig,\n\tclientSrcConf *clientSourcesConfig,\n\ttlsConf *tlsConfigSettings,\n\ttlsMgr *tlsManager,\n\thttpReg aghhttp.Registrar,\n\tclientsContainer dnsforward.ClientsContainer,\n\tconfModifier agh.ConfigModifier,\n) (newConf *dnsforward.ServerConfig, err error) {\n\thosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})\n\n\tfwdConf := dnsConf.Config\n\tfwdConf.ClientsContainer = clientsContainer\n\n\tintTLSConf, err := newDNSTLSConfig(tlsConf, hosts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"constructing tls config: %w\", err)\n\t}\n\n\tnewConf = &dnsforward.ServerConfig{\n\t\tUDPListenAddrs:         ipsToUDPAddrs(hosts, dnsConf.Port),\n\t\tTCPListenAddrs:         ipsToTCPAddrs(hosts, dnsConf.Port),\n\t\tConfig:                 fwdConf,\n\t\tTLSConf:                intTLSConf,\n\t\tTLSAllowUnencryptedDoH: tlsConf.AllowUnencryptedDoH,\n\t\tUpstreamTimeout:        time.Duration(dnsConf.UpstreamTimeout),\n\t\tTLSv12Roots:            tlsMgr.rootCerts,\n\t\tConfModifier:           confModifier,\n\t\tHTTPReg:                httpReg,\n\t\tLocalPTRResolvers:      dnsConf.PrivateRDNSResolvers,\n\t\tUseDNS64:               dnsConf.UseDNS64,\n\t\tDNS64Prefixes:          dnsConf.DNS64Prefixes,\n\t\tUsePrivateRDNS:         dnsConf.UsePrivateRDNS,\n\t\tServeHTTP3:             dnsConf.ServeHTTP3,\n\t\tUseHTTP3Upstreams:      dnsConf.UseHTTP3Upstreams,\n\t\tServePlainDNS:          dnsConf.ServePlainDNS,\n\t\tPendingRequestsEnabled: dnsConf.PendingRequests.Enabled,\n\t}\n\n\tvar initialAddresses []netip.Addr\n\t// Context.stats may be nil here if initDNSServer is called from\n\t// [cmdlineUpdate].\n\tif sts := globalContext.stats; sts != nil {\n\t\tconst initialClientsNum = 100\n\t\tinitialAddresses = globalContext.stats.TopClientsIP(initialClientsNum)\n\t}\n\n\t// Do not set DialContext, PrivateSubnets, and UsePrivateRDNS, because they\n\t// are set by [dnsforward.Server.Prepare].\n\tnewConf.AddrProcConf = &client.DefaultAddrProcConfig{\n\t\tExchanger:        globalContext.dnsServer,\n\t\tAddressUpdater:   &globalContext.clients,\n\t\tInitialAddresses: initialAddresses,\n\t\tCatchPanics:      true,\n\t\tUseRDNS:          clientSrcConf.RDNS,\n\t\tUseWHOIS:         clientSrcConf.WHOIS,\n\t}\n\n\treturn newConf, nil\n}\n\n// newDNSTLSConfig converts values from the configuration file into the internal\n// TLS settings for the DNS server.  conf must not be nil.\nfunc newDNSTLSConfig(\n\tconf *tlsConfigSettings,\n\taddrs []netip.Addr,\n) (dnsConf *dnsforward.TLSConfig, err error) {\n\tif !conf.Enabled {\n\t\treturn &dnsforward.TLSConfig{}, nil\n\t}\n\n\t// TODO(e.burkov):  Add tracking for DNSCrypt configuration file changes to\n\t// the [aghtls.Manager].\n\tdnsCryptConf, err := newDNSCryptConfig(conf, addrs)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tdnsConf = &dnsforward.TLSConfig{\n\t\tDNSCryptConf:   dnsCryptConf,\n\t\tServerName:     conf.ServerName,\n\t\tStrictSNICheck: conf.StrictSNICheck,\n\t}\n\n\tif conf.PortHTTPS != 0 {\n\t\tdnsConf.HTTPSListenAddrs = ipsToAddrPorts(addrs, conf.PortHTTPS)\n\t}\n\n\tif conf.PortDNSOverTLS != 0 {\n\t\tdnsConf.TLSListenAddrs = ipsToTCPAddrs(addrs, conf.PortDNSOverTLS)\n\t}\n\n\tif conf.PortDNSOverQUIC != 0 {\n\t\tdnsConf.QUICListenAddrs = ipsToUDPAddrs(addrs, conf.PortDNSOverQUIC)\n\t}\n\n\tcert, err := tls.X509KeyPair(conf.CertificateChainData, conf.PrivateKeyData)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"parsing tls key pair: %w\", err)\n\t\tif conf.AllowUnencryptedDoH || dnsCryptConf != nil {\n\t\t\t// TODO(s.chzhen):  Use [slog.Logger].\n\t\t\tlog.Info(\"warning: %s\", err)\n\n\t\t\treturn dnsConf, nil\n\t\t}\n\n\t\t// Don't wrap the error, because it's already annotated.\n\t\treturn nil, err\n\t}\n\n\tdnsConf.Cert = &cert\n\n\treturn dnsConf, nil\n}\n\n// newDNSCryptConfig converts values from the configuration file into the\n// internal DNSCrypt settings for the DNS server.  conf must not be nil.\nfunc newDNSCryptConfig(\n\tconf *tlsConfigSettings,\n\taddrs []netip.Addr,\n) (dnsCryptConf *dnsforward.DNSCryptConfig, err error) {\n\tif conf.PortDNSCrypt == 0 {\n\t\treturn nil, nil\n\t}\n\n\tif conf.DNSCryptConfigFile == \"\" {\n\t\treturn nil, fmt.Errorf(\"dnscrypt_config_file: %w\", errors.ErrEmptyValue)\n\t}\n\n\tf, err := os.Open(conf.DNSCryptConfigFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"opening dnscrypt config: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, f.Close()) }()\n\n\trc := &dnscrypt.ResolverConfig{}\n\terr = yaml.NewDecoder(f).Decode(rc)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding dnscrypt config: %w\", err)\n\t}\n\n\tcert, err := rc.CreateCert()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating dnscrypt cert: %w\", err)\n\t}\n\n\treturn &dnsforward.DNSCryptConfig{\n\t\tResolverCert:   cert,\n\t\tUDPListenAddrs: ipsToUDPAddrs(addrs, conf.PortDNSCrypt),\n\t\tTCPListenAddrs: ipsToTCPAddrs(addrs, conf.PortDNSCrypt),\n\t\tProviderName:   rc.ProviderName,\n\t}, nil\n}\n\n// dnsEncryption contains different types of TLS encryption addresses.\ntype dnsEncryption struct {\n\thttps string\n\ttls   string\n\tquic  string\n}\n\n// getDNSEncryption returns the TLS encryption addresses that AdGuard Home\n// listens on.  tlsMgr must not be nil.\nfunc getDNSEncryption(tlsMgr *tlsManager) (de dnsEncryption) {\n\ttlsConf := tlsMgr.config()\n\n\tif !tlsConf.Enabled || len(tlsConf.ServerName) == 0 {\n\t\treturn dnsEncryption{}\n\t}\n\n\thostname := tlsConf.ServerName\n\tif tlsConf.PortHTTPS != 0 {\n\t\taddr := hostname\n\t\tif p := tlsConf.PortHTTPS; p != defaultPortHTTPS {\n\t\t\taddr = netutil.JoinHostPort(addr, p)\n\t\t}\n\n\t\tde.https = (&url.URL{\n\t\t\tScheme: urlutil.SchemeHTTPS,\n\t\t\tHost:   addr,\n\t\t\tPath:   \"/dns-query\",\n\t\t}).String()\n\t}\n\n\tif p := tlsConf.PortDNSOverTLS; p != 0 {\n\t\tde.tls = (&url.URL{\n\t\t\tScheme: \"tls\",\n\t\t\tHost:   netutil.JoinHostPort(hostname, p),\n\t\t}).String()\n\t}\n\n\tif p := tlsConf.PortDNSOverQUIC; p != 0 {\n\t\tde.quic = (&url.URL{\n\t\t\tScheme: \"quic\",\n\t\t\tHost:   netutil.JoinHostPort(hostname, p),\n\t\t}).String()\n\t}\n\n\treturn de\n}\n\nfunc startDNSServer() error {\n\tconfig.RLock()\n\tdefer config.RUnlock()\n\n\tif isRunning() {\n\t\treturn fmt.Errorf(\"unable to start forwarding DNS server: Already running\")\n\t}\n\n\tglobalContext.filters.EnableFilters(false)\n\n\t// TODO(s.chzhen):  Pass context.\n\tctx := context.TODO()\n\terr := globalContext.clients.Start(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting clients container: %w\", err)\n\t}\n\n\terr = globalContext.dnsServer.Start(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting dns server: %w\", err)\n\t}\n\n\tglobalContext.filters.Start()\n\tglobalContext.stats.Start()\n\n\terr = globalContext.queryLog.Start(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting query log: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc stopDNSServer(ctx context.Context) (err error) {\n\tif !isRunning() {\n\t\treturn nil\n\t}\n\n\terr = globalContext.dnsServer.Stop(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"stopping forwarding dns server: %w\", err)\n\t}\n\n\terr = globalContext.clients.close(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"closing clients container: %w\", err)\n\t}\n\n\tcloseDNSServer(ctx)\n\n\treturn nil\n}\n\nfunc closeDNSServer(ctx context.Context) {\n\t// DNS forward module must be closed BEFORE stats or queryLog because it depends on them\n\tif globalContext.dnsServer != nil {\n\t\tglobalContext.dnsServer.Close(ctx)\n\t\tglobalContext.dnsServer = nil\n\t}\n\n\tif globalContext.filters != nil {\n\t\tglobalContext.filters.Close()\n\t}\n\n\tif globalContext.stats != nil {\n\t\terr := globalContext.stats.Close()\n\t\tif err != nil {\n\t\t\tlog.Error(\"closing stats: %s\", err)\n\t\t}\n\t}\n\n\tif globalContext.queryLog != nil {\n\t\terr := globalContext.queryLog.Shutdown(ctx)\n\t\tif err != nil {\n\t\t\tlog.Error(\"closing query log: %s\", err)\n\t\t}\n\t}\n\n\tlog.Debug(\"all dns modules are closed\")\n}\n\n// checkStatsAndQuerylogDirs checks and returns directory paths to store\n// statistics and query log.\nfunc checkStatsAndQuerylogDirs(\n\tconf *configuration,\n\tworkDir string,\n) (statsDir, querylogDir string, err error) {\n\tbaseDir := filepath.Join(workDir, dataDir)\n\n\tstatsDir = conf.Stats.DirPath\n\tif statsDir == \"\" {\n\t\tstatsDir = baseDir\n\t} else {\n\t\terr = checkDir(statsDir)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", fmt.Errorf(\"statistics: custom directory: %w\", err)\n\t\t}\n\t}\n\n\tquerylogDir = conf.QueryLog.DirPath\n\tif querylogDir == \"\" {\n\t\tquerylogDir = baseDir\n\t} else {\n\t\terr = checkDir(querylogDir)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", fmt.Errorf(\"querylog: custom directory: %w\", err)\n\t\t}\n\t}\n\n\treturn statsDir, querylogDir, nil\n}\n\n// checkDir checks if the path is a directory.  It's used to check for\n// misconfiguration at startup.\nfunc checkDir(path string) (err error) {\n\tvar fi os.FileInfo\n\tif fi, err = os.Stat(path); err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif !fi.IsDir() {\n\t\treturn fmt.Errorf(\"%q is not a directory\", path)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/home/home.go",
    "content": "// Package home contains AdGuard Home's HTTP API methods.\npackage home\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghslog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/arpdb\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dhcpd\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dnsforward\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/permcheck\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/querylog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/stats\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/updater\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/hostsfile\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\n// Global context\ntype homeContext struct {\n\t// Modules\n\t// --\n\n\tclients    clientsContainer   // per-client-settings module\n\tstats      stats.Interface    // statistics module\n\tqueryLog   querylog.QueryLog  // query log module\n\tdnsServer  *dnsforward.Server // DNS module\n\tdhcpServer dhcpd.Interface    // DHCP module\n\n\tfilters *filtering.DNSFilter // DNS filtering module\n\tweb     *webAPI              // Web (HTTP, HTTPS) module\n\n\t// etcHosts contains IP-hostname mappings taken from the OS-specific hosts\n\t// configuration files, for example /etc/hosts.\n\tetcHosts *aghnet.HostsContainer\n\n\t// Runtime properties\n\t// --\n\n\tpidFileName string // PID file name.  Empty if no PID file was created.\n\tcontrolLock sync.Mutex\n}\n\n// globalContext is a global context object.\n//\n// TODO(a.garipov): Refactor.\nvar globalContext homeContext\n\n// Main is the entry point\nfunc Main(clientBuildFS fs.FS) {\n\tctx := context.Background()\n\n\tinitCmdLineOpts()\n\n\t// The configuration file path can be overridden, but other command-line\n\t// options have to override config values.  Therefore, do it manually\n\t// instead of using package flag.\n\t//\n\t// TODO(a.garipov): The comment above is most likely false.  Replace with\n\t// package flag.\n\topts := loadCmdLineOpts()\n\n\t// TODO(s.chzhen):  Construct logger from command-line options.\n\tl := slog.Default()\n\tworkDir, err := initWorkingDir(opts)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"failed to init working directory\", slogutil.KeyError, err)\n\n\t\tos.Exit(osutil.ExitCodeFailure)\n\t}\n\n\tconfPath := initConfigFilename(ctx, l, opts, workDir)\n\n\tls := getLogSettings(ctx, l, opts, workDir, confPath)\n\n\t// TODO(a.garipov): Use slog everywhere.\n\tbaseLogger := newSlogLogger(ls)\n\n\t// Configure log level and output.\n\terr = configureLogger(ls, workDir)\n\tfatalOnError(err)\n\n\t// Print the first message after logger is configured.\n\tbaseLogger.InfoContext(ctx, \"starting adguard home\", \"version\", version.Full())\n\tbaseLogger.DebugContext(ctx, \"current working directory\", \"path\", workDir)\n\tif opts.runningAsService {\n\t\tbaseLogger.InfoContext(ctx, \"adguard home is running as a service\")\n\t}\n\n\tdone := make(chan struct{})\n\n\tsignals := make(chan os.Signal, 1)\n\tsignal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)\n\n\tsigHdlrLogger := baseLogger.With(slogutil.KeyPrefix, \"signalhdlr\")\n\tsigHdlr := newSignalHandler(sigHdlrLogger, signals, func(ctx context.Context) {\n\t\tcleanup(ctx)\n\t\tcleanupAlways()\n\t\tclose(done)\n\t})\n\n\tgo sigHdlr.handle(ctx)\n\n\tif opts.serviceControlAction != \"\" {\n\t\tsvcLogger := baseLogger.With(slogutil.KeyPrefix, \"service\")\n\t\terr = handleServiceControlAction(\n\t\t\tctx,\n\t\t\tbaseLogger,\n\t\t\tsvcLogger,\n\t\t\topts,\n\t\t\tclientBuildFS,\n\t\t\tsignals,\n\t\t\tdone,\n\t\t\tsigHdlr,\n\t\t\tworkDir,\n\t\t\tconfPath,\n\t\t)\n\t\tif err != nil {\n\t\t\tsvcLogger.ErrorContext(ctx, \"action failed\", slogutil.KeyError, err)\n\t\t\tos.Exit(osutil.ExitCodeFailure)\n\t\t}\n\n\t\treturn\n\t}\n\n\t// run the protection\n\trun(ctx, baseLogger, opts, clientBuildFS, done, sigHdlr, workDir, confPath)\n}\n\n// setupContext initializes [globalContext] fields.  It also reads and upgrades\n// config file if necessary.  baseLogger must not be nil.\nfunc setupContext(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\topts options,\n\tworkDir string,\n\tconfPath string,\n\tisFirstRun bool,\n) (err error) {\n\tif !opts.noEtcHosts {\n\t\terr = setupHostsContainer(ctx, baseLogger)\n\t\tif err != nil {\n\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif isFirstRun {\n\t\tbaseLogger.InfoContext(ctx, \"this is the first time adguard home has been launched\")\n\t\tcheckNetworkPermissions(ctx, baseLogger)\n\n\t\treturn nil\n\t}\n\n\t// TODO(s.chzhen):  Consider adding a key prefix.\n\terr = parseConfig(ctx, baseLogger, workDir, confPath)\n\tif err != nil {\n\t\tbaseLogger.ErrorContext(ctx, \"failed to parse configuration file\", slogutil.KeyError, err)\n\n\t\tos.Exit(osutil.ExitCodeFailure)\n\t}\n\n\tif opts.checkConfig {\n\t\tbaseLogger.InfoContext(ctx, \"configuration file is ok\")\n\n\t\tos.Exit(osutil.ExitCodeSuccess)\n\t}\n\n\treturn nil\n}\n\n// logIfUnsupported logs a formatted warning if the error is one of the\n// unsupported errors and returns nil.  If err is nil, logIfUnsupported returns\n// nil.  Otherwise, it returns err.\nfunc logIfUnsupported(msg string, err error) (outErr error) {\n\tif errors.Is(err, errors.ErrUnsupported) {\n\t\tlog.Debug(msg, err)\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\n// configureOS sets the OS-related configuration.\nfunc configureOS(conf *configuration) (err error) {\n\tosConf := conf.OSConfig\n\tif osConf == nil {\n\t\treturn nil\n\t}\n\n\tif osConf.Group != \"\" {\n\t\terr = aghos.SetGroup(osConf.Group)\n\t\terr = logIfUnsupported(\"warning: setting group\", err)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"setting group: %w\", err)\n\t\t}\n\n\t\tlog.Info(\"group set to %s\", osConf.Group)\n\t}\n\n\tif osConf.User != \"\" {\n\t\terr = aghos.SetUser(osConf.User)\n\t\terr = logIfUnsupported(\"warning: setting user\", err)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"setting user: %w\", err)\n\t\t}\n\n\t\tlog.Info(\"user set to %s\", osConf.User)\n\t}\n\n\tif osConf.RlimitNoFile != 0 {\n\t\terr = aghos.SetRlimit(osConf.RlimitNoFile)\n\t\terr = logIfUnsupported(\"warning: setting rlimit\", err)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"setting rlimit: %w\", err)\n\t\t}\n\n\t\tlog.Info(\"rlimit_nofile set to %d\", osConf.RlimitNoFile)\n\t}\n\n\treturn nil\n}\n\n// setupHostsContainer initializes the structures to keep up-to-date the hosts\n// provided by the OS.  baseLogger must not be nil.\nfunc setupHostsContainer(ctx context.Context, baseLogger *slog.Logger) (err error) {\n\tl := baseLogger.With(slogutil.KeyPrefix, \"hosts\")\n\n\tvar hostsWatcher aghos.FSWatcher\n\thostsWatcher, err = aghos.NewOSWatcher(&aghos.OSWatcherConfig{\n\t\tLogger: baseLogger.With(slogutil.KeyPrefix, \"hosts_watcher\"),\n\t})\n\tif err != nil {\n\t\tl.WarnContext(\n\t\t\tctx,\n\t\t\t\"initializing filesystem watcher; not watching for changes\",\n\t\t\tslogutil.KeyError,\n\t\t\terr,\n\t\t)\n\n\t\thostsWatcher = aghos.EmptyFSWatcher{}\n\t}\n\n\tpaths, err := hostsfile.DefaultHostsPaths()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting default system hosts paths: %w\", err)\n\t}\n\n\tglobalContext.etcHosts, err = aghnet.NewHostsContainer(\n\t\tctx,\n\t\tl,\n\t\tosutil.RootDirFS(),\n\t\thostsWatcher,\n\t\tpaths...,\n\t)\n\tif err != nil {\n\t\tcloseErr := hostsWatcher.Shutdown(ctx)\n\t\tif errors.Is(err, aghnet.ErrNoHostsPaths) {\n\t\t\tl.WarnContext(ctx, \"initializing hosts container\", slogutil.KeyError, err)\n\n\t\t\treturn closeErr\n\t\t}\n\n\t\treturn errors.Join(fmt.Errorf(\"initializing hosts container: %w\", err), closeErr)\n\t}\n\n\treturn hostsWatcher.Start(ctx)\n}\n\n// setupOpts sets up command-line options.\nfunc setupOpts(opts options) (err error) {\n\terr = setupBindOpts(opts)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {\n\t\tglobalContext.pidFileName = opts.pidFile\n\t}\n\n\treturn nil\n}\n\n// initContextClients initializes Context clients and related fields.  All\n// arguments must not be nil.\nfunc initContextClients(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tsigHdlr *signalHandler,\n\tconfModifier agh.ConfigModifier,\n\thttpReg aghhttp.Registrar,\n\tworkDir string,\n) (err error) {\n\t//lint:ignore SA1019 Migration is not over.\n\tconfig.DHCP.WorkDir = workDir\n\tconfig.DHCP.DataDir = filepath.Join(workDir, dataDir)\n\tconfig.DHCP.HTTPReg = httpReg\n\tconfig.DHCP.CommandConstructor = executil.SystemCommandConstructor{}\n\tconfig.DHCP.Logger = logger.With(slogutil.KeyPrefix, \"dhcpd\")\n\tconfig.DHCP.ConfModifier = confModifier\n\n\tglobalContext.dhcpServer, err = dhcpd.Create(ctx, config.DHCP)\n\tif globalContext.dhcpServer == nil || err != nil {\n\t\t// TODO(a.garipov): There are a lot of places in the code right\n\t\t// now which assume that the DHCP server can be nil despite this\n\t\t// condition.  Inspect them and perhaps rewrite them to use\n\t\t// Enabled() instead.\n\t\treturn fmt.Errorf(\"initing dhcp: %w\", err)\n\t}\n\n\tvar arpDB arpdb.Interface\n\tif config.Clients.Sources.ARP {\n\t\tarpDB = arpdb.New(logger.With(slogutil.KeyError, \"arpdb\"))\n\t}\n\n\treturn globalContext.clients.Init(\n\t\tctx,\n\t\tlogger,\n\t\tconfig.Clients.Persistent,\n\t\tglobalContext.dhcpServer,\n\t\tglobalContext.etcHosts,\n\t\tarpDB,\n\t\tconfig.Filtering,\n\t\tsigHdlr,\n\t\tconfModifier,\n\t\thttpReg,\n\t)\n}\n\n// setupBindOpts overrides bind host/port from the opts.\nfunc setupBindOpts(opts options) (err error) {\n\tbindAddr := opts.bindAddr\n\tif bindAddr != (netip.AddrPort{}) {\n\t\tconfig.HTTPConfig.Address = bindAddr\n\n\t\tif config.HTTPConfig.Address.Port() != 0 {\n\t\t\terr = checkPorts()\n\t\t\tif err != nil {\n\t\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif opts.bindPort != 0 {\n\t\tconfig.HTTPConfig.Address = netip.AddrPortFrom(\n\t\t\tconfig.HTTPConfig.Address.Addr(),\n\t\t\topts.bindPort,\n\t\t)\n\n\t\terr = checkPorts()\n\t\tif err != nil {\n\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif opts.bindHost.IsValid() {\n\t\tconfig.HTTPConfig.Address = netip.AddrPortFrom(\n\t\t\topts.bindHost,\n\t\t\tconfig.HTTPConfig.Address.Port(),\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// setupDNSFilteringConf sets up DNS filtering configuration settings.  All\n// arguments must not be nil.\nfunc setupDNSFilteringConf(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tconf *filtering.Config,\n\ttlsMgr *tlsManager,\n\tconfModifier agh.ConfigModifier,\n\thttpReg aghhttp.Registrar,\n\tworkDir string,\n) (err error) {\n\tconst (\n\t\tdnsTimeout = 3 * time.Second\n\n\t\tsbService                 = \"safe_browsing\"\n\t\tdefaultSafeBrowsingServer = `https://family.adguard-dns.com/dns-query`\n\t\tsbTXTSuffix               = `sb.dns.adguard.com.`\n\n\t\tpcService             = \"parental_control\"\n\t\tdefaultParentalServer = `https://family.adguard-dns.com/dns-query`\n\t\tpcTXTSuffix           = `pc.dns.adguard.com.`\n\t)\n\n\tconf.Logger = baseLogger.With(slogutil.KeyPrefix, \"filtering\")\n\n\tconf.EtcHosts = globalContext.etcHosts\n\t// TODO(s.chzhen):  Use empty interface.\n\tif globalContext.etcHosts == nil || !config.DNS.HostsFileEnabled {\n\t\tconf.EtcHosts = nil\n\t}\n\n\tconf.ConfModifier = confModifier\n\tconf.HTTPReg = httpReg\n\tconf.DataDir = filepath.Join(workDir, dataDir)\n\tconf.Filters = slices.Clone(config.Filters)\n\tconf.WhitelistFilters = slices.Clone(config.WhitelistFilters)\n\tconf.UserRules = slices.Clone(config.UserRules)\n\tconf.HTTPClient = httpClient(tlsMgr)\n\n\tcacheTime := time.Duration(conf.CacheTime) * time.Minute\n\n\tupsOpts := &upstream.Options{\n\t\tLogger:  aghslog.NewForUpstream(baseLogger, aghslog.UpstreamTypeService),\n\t\tTimeout: dnsTimeout,\n\t\tBootstrap: upstream.StaticResolver{\n\t\t\t// 94.140.14.15.\n\t\t\tnetip.AddrFrom4([4]byte{94, 140, 14, 15}),\n\t\t\t// 94.140.14.16.\n\t\t\tnetip.AddrFrom4([4]byte{94, 140, 14, 16}),\n\t\t\t// 2a10:50c0::bad1:ff.\n\t\t\tnetip.AddrFrom16([16]byte{42, 16, 80, 192, 12: 186, 209, 0, 255}),\n\t\t\t// 2a10:50c0::bad2:ff.\n\t\t\tnetip.AddrFrom16([16]byte{42, 16, 80, 192, 12: 186, 210, 0, 255}),\n\t\t},\n\t}\n\n\tsbUps, err := upstream.AddressToUpstream(defaultSafeBrowsingServer, upsOpts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"converting safe browsing server: %w\", err)\n\t}\n\n\tconf.SafeBrowsingChecker = hashprefix.New(&hashprefix.Config{\n\t\tLogger:    baseLogger.With(slogutil.KeyPrefix, sbService),\n\t\tUpstream:  sbUps,\n\t\tTXTSuffix: sbTXTSuffix,\n\t\tCacheTime: cacheTime,\n\t\tCacheSize: conf.SafeBrowsingCacheSize,\n\t})\n\n\t// Protect against invalid configuration, see #6181.\n\t//\n\t// TODO(a.garipov): Validate against an empty host instead of setting it to\n\t// default.\n\tif conf.SafeBrowsingBlockHost == \"\" {\n\t\thost := defaultSafeBrowsingBlockHost\n\t\tbaseLogger.WarnContext(ctx,\n\t\t\t\"empty blocking host; set default\",\n\t\t\t\"service\", sbService,\n\t\t\t\"host\", host,\n\t\t)\n\n\t\tconf.SafeBrowsingBlockHost = host\n\t}\n\n\tparUps, err := upstream.AddressToUpstream(defaultParentalServer, upsOpts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"converting parental server: %w\", err)\n\t}\n\n\tconf.ParentalControlChecker = hashprefix.New(&hashprefix.Config{\n\t\tLogger:    baseLogger.With(slogutil.KeyPrefix, pcService),\n\t\tUpstream:  parUps,\n\t\tTXTSuffix: pcTXTSuffix,\n\t\tCacheTime: cacheTime,\n\t\tCacheSize: conf.ParentalCacheSize,\n\t})\n\n\t// Protect against invalid configuration, see #6181.\n\t//\n\t// TODO(a.garipov): Validate against an empty host instead of setting it to\n\t// default.\n\tif conf.ParentalBlockHost == \"\" {\n\t\thost := defaultParentalBlockHost\n\t\tbaseLogger.WarnContext(ctx,\n\t\t\t\"empty blocking host; set default\",\n\t\t\t\"service\", pcService,\n\t\t\t\"host\", host,\n\t\t)\n\n\t\tconf.ParentalBlockHost = host\n\t}\n\n\tlogger := baseLogger.With(slogutil.KeyPrefix, safesearch.LogPrefix)\n\tconf.SafeSearch, err = safesearch.NewDefault(ctx, &safesearch.DefaultConfig{\n\t\tLogger:         logger,\n\t\tServicesConfig: conf.SafeSearchConf,\n\t\tCacheSize:      conf.SafeSearchCacheSize,\n\t\tCacheTTL:       cacheTime,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initializing safesearch: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// checkPorts is a helper for ports validation in config.\nfunc checkPorts() (err error) {\n\ttcpPorts := aghalg.UniqChecker[tcpPort]{}\n\taddPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port()))\n\n\tudpPorts := aghalg.UniqChecker[udpPort]{}\n\taddPorts(udpPorts, udpPort(config.DNS.Port))\n\n\tif config.TLS.Enabled {\n\t\taddPorts(\n\t\t\ttcpPorts,\n\t\t\ttcpPort(config.TLS.PortHTTPS),\n\t\t\ttcpPort(config.TLS.PortDNSOverTLS),\n\t\t\ttcpPort(config.TLS.PortDNSCrypt),\n\t\t)\n\n\t\taddPorts(udpPorts, udpPort(config.TLS.PortDNSOverQUIC))\n\t}\n\n\tif err = tcpPorts.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"validating tcp ports: %w\", err)\n\t} else if err = udpPorts.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"validating udp ports: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// isUpdateEnabled returns true if the update is enabled for current\n// configuration.  It also logs the decision.  isCustomURL should be true if the\n// updater is using a custom URL.\nfunc isUpdateEnabled(\n\tctx context.Context,\n\tl *slog.Logger,\n\topts *options,\n\tisCustomURL bool,\n) (ok bool) {\n\tif opts.disableUpdate {\n\t\tl.DebugContext(ctx, \"updates are disabled by command-line option\")\n\n\t\treturn false\n\t}\n\n\tswitch version.Channel() {\n\tcase\n\t\tversion.ChannelDevelopment,\n\t\tversion.ChannelCandidate:\n\t\tif isCustomURL {\n\t\t\tl.DebugContext(ctx, \"updates are enabled because custom url is used\")\n\t\t} else {\n\t\t\tl.DebugContext(ctx, \"updates are disabled for development and candidate builds\")\n\t\t}\n\n\t\treturn isCustomURL\n\tdefault:\n\t\tl.DebugContext(ctx, \"updates are enabled\")\n\n\t\treturn true\n\t}\n}\n\n// webConfig is a configuration structure for webAPI.\ntype webConfig struct {\n\t// opts are used to determine if update is enabled.\n\topts options\n\n\t// clientBuildFS is used for initializing client FS.  If opts.localFrontend\n\t// is false, then this field must not be nil.\n\tclientBuildFS fs.FS\n\n\t// updater is used for handling updates.  It must not be nil.\n\tupdater *updater.Updater\n\n\t// baseLogger is used for logging init process and for logging inside web\n\t// api.  It must not be nil.\n\tbaseLogger *slog.Logger\n\n\t// tlsManager contains the current configuration and state of TLS\n\t// encryption. It must not be nil.\n\ttlsManager *tlsManager\n\n\t// auth stores web user information and handles authentication.  It must not\n\t// be nil.\n\tauth *auth\n\n\t// mux is the default *http.ServeMux, the same as [globalContext.mux]. It\n\t// must not be nil.\n\tmux *http.ServeMux\n\n\t// configModifier is used to update the global configuration.\n\tconfigModifier agh.ConfigModifier\n\n\t// httpReg registers HTTP handlers. It must not be nil.\n\thttpReg aghhttp.Registrar\n\n\t// workDir is a base working directory.\n\tworkDir string\n\n\t// confPath is a config path.\n\tconfPath string\n\n\t// isCustomUpdURL defines if updater should use custom url.\n\tisCustomUpdURL bool\n\n\t// isFirstRun defines if current run is the first run.\n\tisFirstRun bool\n}\n\n// newWeb initializes the web module.  conf must not be nil.\nfunc newWeb(ctx context.Context, conf *webConfig) (web *webAPI, err error) {\n\tlogger := conf.baseLogger.With(slogutil.KeyPrefix, \"webapi\")\n\n\twebPort := suggestedWebPort(ctx, logger)\n\n\tvar clientFS fs.FS\n\tif conf.opts.localFrontend {\n\t\tlogger.WarnContext(ctx, \"using local frontend files\")\n\n\t\tclientFS = os.DirFS(\"build/static\")\n\t} else {\n\t\tclientFS, err = fs.Sub(conf.clientBuildFS, \"build/static\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"getting embedded client subdir: %w\", err)\n\t\t}\n\t}\n\n\tdisableUpdate := !isUpdateEnabled(ctx, conf.baseLogger, &conf.opts, conf.isCustomUpdURL)\n\n\twebConf := &webAPIConfig{\n\t\tCommandConstructor: executil.SystemCommandConstructor{},\n\t\tupdater:            conf.updater,\n\t\tlogger:             logger,\n\t\tbaseLogger:         conf.baseLogger,\n\t\tconfModifier:       conf.configModifier,\n\t\thttpReg:            conf.httpReg,\n\t\ttlsManager:         conf.tlsManager,\n\t\tauth:               conf.auth,\n\t\tmux:                conf.mux,\n\n\t\tclientFS: clientFS,\n\n\t\tBindAddr: config.HTTPConfig.Address,\n\n\t\tworkDir:  conf.workDir,\n\t\tconfPath: conf.confPath,\n\n\t\tReadTimeout:       readTimeout,\n\t\tReadHeaderTimeout: readHdrTimeout,\n\t\tWriteTimeout:      writeTimeout,\n\n\t\tdefaultWebPort: webPort,\n\n\t\tfirstRun:         conf.isFirstRun,\n\t\tdisableUpdate:    disableUpdate,\n\t\trunningAsService: conf.opts.runningAsService,\n\t\tserveHTTP3:       config.DNS.ServeHTTP3,\n\t}\n\n\tweb = newWebAPI(ctx, webConf)\n\tif web == nil {\n\t\treturn nil, errors.Error(\"can not initialize web\")\n\t}\n\n\treturn web, nil\n}\n\n// suggestedWebPort returns the suggested default HTTP port for the installation\n// wizard, using the port provided via an environment variable.  It falls back\n// to [defaultPortHTTP] on error.\nfunc suggestedWebPort(ctx context.Context, l *slog.Logger) (p uint16) {\n\tconst webPortEnv = \"ADGUARD_HOME_DEFAULT_WEB_PORT\"\n\n\ts := os.Getenv(webPortEnv)\n\tif s == \"\" {\n\t\treturn defaultPortHTTP\n\t}\n\n\tv, err := strconv.ParseUint(s, 10, 16)\n\tif err == nil && v == 0 {\n\t\terr = errors.ErrOutOfRange\n\t}\n\n\tif err != nil {\n\t\tl.WarnContext(\n\t\t\tctx,\n\t\t\t\"invalid web port; using default\",\n\t\t\t\"env\", webPortEnv,\n\t\t\t\"val\", s,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\n\t\treturn defaultPortHTTP\n\t}\n\n\treturn uint16(v)\n}\n\nfunc fatalOnError(err error) {\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\n// run configures and starts AdGuard Home.\n//\n// TODO(e.burkov):  Make opts a pointer.\nfunc run(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\topts options,\n\tclientBuildFS fs.FS,\n\tdone chan struct{},\n\tsigHdlr *signalHandler,\n\tworkDir string,\n\tconfPath string,\n) {\n\taghtls.Init(ctx, baseLogger.With(slogutil.KeyPrefix, \"aghtls\"))\n\n\tisFirstRun := detectFirstRun(ctx, baseLogger, workDir, confPath)\n\n\tmw := &webMw{}\n\tmux := http.NewServeMux()\n\thttpReg := aghhttp.NewDefaultRegistrar(mux, mw.wrap)\n\n\terr := setupContext(ctx, baseLogger, opts, workDir, confPath, isFirstRun)\n\tfatalOnError(err)\n\n\terr = configureOS(config)\n\tfatalOnError(err)\n\n\t// Clients package uses filtering package's static data\n\t// (filtering.BlockedSvcKnown()), so we have to initialize filtering static\n\t// data first, but also to avoid relying on automatic Go init() function.\n\tfiltering.InitModule(ctx, baseLogger)\n\n\tconfModifier := newDefaultConfigModifier(\n\t\tconfig,\n\t\tbaseLogger.With(slogutil.KeyPrefix, \"config_modifier\"),\n\t\tworkDir,\n\t\tconfPath,\n\t)\n\n\terr = initContextClients(ctx, baseLogger, sigHdlr, confModifier, httpReg, workDir)\n\tfatalOnError(err)\n\n\ttlsMgr, err := initTLS(ctx, baseLogger, sigHdlr, confModifier, httpReg)\n\tfatalOnError(err)\n\n\terr = setupDNSFilteringConf(\n\t\tctx,\n\t\tbaseLogger,\n\t\tconfig.Filtering,\n\t\ttlsMgr,\n\t\tconfModifier,\n\t\thttpReg,\n\t\tworkDir,\n\t)\n\tfatalOnError(err)\n\n\terr = setupOpts(opts)\n\tfatalOnError(err)\n\n\tupd, isCustomURL := initUpdate(ctx, baseLogger, opts, tlsMgr, isFirstRun, workDir, confPath)\n\n\tdataDirPath := filepath.Join(workDir, dataDir)\n\terr = os.MkdirAll(dataDirPath, aghos.DefaultPermDir)\n\tfatalOnError(errors.Annotate(err, \"creating DNS data dir at %s: %w\", dataDirPath))\n\n\tauth, err := initUsers(ctx, baseLogger, workDir, opts.glinetMode)\n\tfatalOnError(err)\n\n\tconfModifier.setAuth(auth)\n\n\tconf := &webConfig{\n\t\tclientBuildFS:  clientBuildFS,\n\t\tupdater:        upd,\n\t\topts:           opts,\n\t\tbaseLogger:     baseLogger,\n\t\ttlsManager:     tlsMgr,\n\t\tauth:           auth,\n\t\tmux:            mux,\n\t\tconfigModifier: confModifier,\n\t\thttpReg:        httpReg,\n\t\tworkDir:        workDir,\n\t\tconfPath:       confPath,\n\t\tisCustomUpdURL: isCustomURL,\n\t\tisFirstRun:     isFirstRun,\n\t}\n\n\tweb, err := newWeb(ctx, conf)\n\tfatalOnError(err)\n\n\tmw.set(web)\n\n\tglobalContext.web = web\n\n\ttlsMgr.setWebAPI(web)\n\n\tstatsDir, querylogDir, err := checkStatsAndQuerylogDirs(config, workDir)\n\tfatalOnError(err)\n\n\tif !isFirstRun {\n\t\trunDNSServer(ctx, baseLogger, tlsMgr, confModifier, statsDir, querylogDir, httpReg)\n\t}\n\n\tif !opts.noPermCheck {\n\t\tcheckPermissions(ctx, baseLogger, workDir, confPath, dataDirPath, statsDir, querylogDir)\n\t}\n\n\tweb.start(ctx)\n\n\t// Wait for other goroutines to complete their job.\n\t<-done\n}\n\n// runDNSServer initializes and starts DNS and DHCP servers if this is not the\n// first run.  httpReg, slogLogger, tlsMgr and confModifier must not be nil.\nfunc runDNSServer(\n\tctx context.Context,\n\tslogLogger *slog.Logger,\n\ttlsMgr *tlsManager,\n\tconfModifier *defaultConfigModifier,\n\tstatsDir string,\n\tquerylogDir string,\n\thttpReg *aghhttp.DefaultRegistrar,\n) {\n\terr := initDNS(ctx, slogLogger, tlsMgr, confModifier, httpReg, statsDir, querylogDir)\n\tfatalOnError(err)\n\n\ttlsMgr.start(ctx)\n\n\tgo func() {\n\t\tstartErr := startDNSServer()\n\t\tif startErr != nil {\n\t\t\tcloseDNSServer(ctx)\n\t\t\tfatalOnError(startErr)\n\t\t}\n\t}()\n\n\tif globalContext.dhcpServer != nil {\n\t\terr = globalContext.dhcpServer.Start(ctx)\n\t\tif err != nil {\n\t\t\tslogLogger.ErrorContext(ctx, \"starting dhcp server\", slogutil.KeyError, err)\n\t\t}\n\t}\n}\n\n// initTLS initializes TLS manager.  baseLogger, sigHdlr, confModifier, and\n// httpReg must not be nil.\nfunc initTLS(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tsigHdlr *signalHandler,\n\tconfModifier *defaultConfigModifier,\n\thttpReg *aghhttp.DefaultRegistrar,\n) (tlsMgr *tlsManager, err error) {\n\ttlsMgrLogger := baseLogger.With(slogutil.KeyPrefix, \"tls_manager\")\n\n\tvar watcher aghos.FSWatcher\n\twatcher, err = aghos.NewOSWatcher(&aghos.OSWatcherConfig{\n\t\tLogger: tlsMgrLogger.With(slogutil.KeyPrefix, \"cert_watcher\"),\n\t})\n\tif err != nil {\n\t\ttlsMgrLogger.ErrorContext(ctx, \"initializing watcher\", slogutil.KeyError, err)\n\t\twatcher = aghos.EmptyFSWatcher{}\n\t}\n\n\taghtlsMgr := aghtls.NewDefaultManager(&aghtls.DefaultManagerConfig{\n\t\tLogger:  baseLogger.With(slogutil.KeyPrefix, \"aghtls_manager\"),\n\t\tWatcher: watcher,\n\t})\n\terr = aghtlsMgr.Start(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"starting tls manager: %w\", err)\n\t}\n\n\tsigHdlr.addTLSManager(aghtlsMgr)\n\n\ttlsMgr, err = newTLSManager(ctx, &tlsManagerConfig{\n\t\tlogger:        tlsMgrLogger,\n\t\tconfModifier:  confModifier,\n\t\tmanager:       aghtlsMgr,\n\t\thttpReg:       httpReg,\n\t\ttlsSettings:   config.TLS,\n\t\tservePlainDNS: config.DNS.ServePlainDNS,\n\t})\n\tif err != nil {\n\t\ttlsMgrLogger.ErrorContext(ctx, \"initializing\", slogutil.KeyError, err)\n\t\tconfModifier.Apply(ctx)\n\t}\n\n\tconfModifier.setTLSManager(tlsMgr)\n\n\treturn tlsMgr, nil\n}\n\n// initUpdate configures and runs update of this application.  logger and tlsMgr\n// must not be nil.\nfunc initUpdate(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\topts options,\n\ttlsMgr *tlsManager,\n\tisFirstRun bool,\n\tworkDir string,\n\tconfPath string,\n) (upd *updater.Updater, isCustomURL bool) {\n\texecPath, err := os.Executable()\n\tfatalOnError(errors.Annotate(err, \"getting executable path: %w\"))\n\n\tupdLogger := baseLogger.With(slogutil.KeyPrefix, \"updater\")\n\tupd, isCustomURL = newUpdater(\n\t\tctx,\n\t\tupdLogger,\n\t\tconfig,\n\t\tworkDir,\n\t\tconfPath,\n\t\texecPath,\n\t)\n\n\t// TODO(e.burkov): This could be made earlier, probably as the option's\n\t// effect.\n\tcmdlineUpdate(ctx, baseLogger, opts, upd, tlsMgr, isFirstRun)\n\n\tif !isFirstRun {\n\t\t// Save the updated config.\n\t\terr = config.write(ctx, baseLogger, nil, nil, workDir, confPath)\n\t\tfatalOnError(err)\n\n\t\tif config.HTTPConfig.Pprof.Enabled {\n\t\t\tstartPprof(baseLogger, config.HTTPConfig.Pprof.Port)\n\t\t}\n\t}\n\n\treturn upd, isCustomURL\n}\n\n// newUpdater creates a new AdGuard Home updater.  l and conf must not be nil.\n// workDir, confPath, and execPath must not be empty.  isCustomURL is true if\n// the user has specified a custom version announcement URL.\nfunc newUpdater(\n\tctx context.Context,\n\tl *slog.Logger,\n\tconf *configuration,\n\tworkDir string,\n\tconfPath string,\n\texecPath string,\n) (upd *updater.Updater, isCustomURL bool) {\n\t// envName is the name of the environment variable that can be used to\n\t// override the default version check URL.\n\tconst envName = \"ADGUARD_HOME_TEST_UPDATE_VERSION_URL\"\n\n\tcustomURLStr := os.Getenv(envName)\n\n\tvar versionURL *url.URL\n\tswitch {\n\tcase version.Channel() == version.ChannelRelease:\n\t\t// Only enable custom version URL for development builds.\n\t\tl.DebugContext(ctx, \"custom version url is disabled for release builds\")\n\tcase !conf.UnsafeUseCustomUpdateIndexURL:\n\t\tl.DebugContext(ctx, \"custom version url is disabled in config\")\n\tdefault:\n\t\tversionURL, _ = url.Parse(customURLStr)\n\t}\n\n\terr := urlutil.ValidateHTTPURL(versionURL)\n\tif isCustomURL = err == nil; !isCustomURL {\n\t\tl.DebugContext(ctx, \"parsing custom version url\", slogutil.KeyError, err)\n\n\t\tversionURL = updater.DefaultVersionURL()\n\t}\n\n\tl.DebugContext(ctx, \"creating updater\", \"config_path\", confPath)\n\n\treturn updater.NewUpdater(&updater.Config{\n\t\tClient:             conf.Filtering.HTTPClient,\n\t\tLogger:             l,\n\t\tCommandConstructor: executil.SystemCommandConstructor{},\n\t\tVersion:            version.Version(),\n\t\tChannel:            version.Channel(),\n\t\tGOARCH:             runtime.GOARCH,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARM:              version.GOARM(),\n\t\tGOMIPS:             version.GOMIPS(),\n\t\tWorkDir:            workDir,\n\t\tConfName:           confPath,\n\t\tExecPath:           execPath,\n\t\tVersionCheckURL:    versionURL,\n\t}), isCustomURL\n}\n\n// checkPermissions checks and migrates permissions of the files and directories\n// used by AdGuard Home, if needed.\nfunc checkPermissions(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tworkDir string,\n\tconfPath string,\n\tdataDirPath string,\n\tstatsDir string,\n\tquerylogDir string,\n) {\n\tl := baseLogger.With(slogutil.KeyPrefix, \"permcheck\")\n\n\tif permcheck.NeedsMigration(ctx, l, workDir, confPath) {\n\t\tpermcheck.Migrate(ctx, l, workDir, dataDirPath, statsDir, querylogDir, confPath)\n\t}\n\n\tpermcheck.Check(ctx, l, workDir, dataDirPath, statsDir, querylogDir, confPath)\n}\n\n// initUsers initializes authentication module and clears the [config.Users]\n// field.\nfunc initUsers(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tworkDir string,\n\tisGLiNet bool,\n) (auth *auth, err error) {\n\tvar rateLimiter loginRateLimiter\n\tif config.AuthAttempts > 0 && config.AuthBlockMin > 0 {\n\t\tblockDur := time.Duration(config.AuthBlockMin) * time.Minute\n\t\trateLimiter = newAuthRateLimiter(blockDur, config.AuthAttempts)\n\t} else {\n\t\tbaseLogger.WarnContext(ctx, \"authratelimiter is disabled\")\n\t\trateLimiter = emptyRateLimiter{}\n\t}\n\n\tdataDirPath := filepath.Join(workDir, dataDir)\n\tauth, err = newAuth(ctx, &authConfig{\n\t\tbaseLogger:     baseLogger,\n\t\trateLimiter:    rateLimiter,\n\t\ttrustedProxies: netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies)),\n\t\tdbFilename:     filepath.Join(dataDirPath, sessionsDBName),\n\t\tusers:          config.Users,\n\t\tsessionTTL:     time.Duration(config.HTTPConfig.SessionTTL),\n\t\tisGLiNet:       isGLiNet,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initializing auth module: %w\", err)\n\t}\n\n\tconfig.Users = nil\n\n\treturn auth, nil\n}\n\nfunc (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {\n\tvar anonFunc aghnet.IPMutFunc\n\tif c.DNS.AnonymizeClientIP {\n\t\tanonFunc = querylog.AnonymizeIP\n\t}\n\n\treturn aghnet.NewIPMut(anonFunc)\n}\n\n// permCheckHelp is printed when binding to privileged ports is not permitted.\nconst permCheckHelp = `Permission check failed.\n\nAdGuard Home is not allowed to bind to privileged ports (for instance, port 53).\nPlease note that this is crucial for a server to be able to use privileged ports.\n\nYou have two options:\n1. Run AdGuard Home with root privileges.\n2. On Linux you can grant the CAP_NET_BIND_SERVICE capability:\nhttps://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser`\n\n// checkNetworkPermissions checks if the current user permissions are enough to\n// use the required networking functionality.  l must not be nil.\nfunc checkNetworkPermissions(ctx context.Context, l *slog.Logger) {\n\tl.InfoContext(ctx, \"checking if adguard home has the necessary permissions\")\n\n\tif ok, err := aghnet.CanBindPrivilegedPorts(ctx, l); !ok || err != nil {\n\t\tl.ErrorContext(\n\t\t\tctx,\n\t\t\t\"this is the first launch of adguard home; you must run it as administrator.\",\n\t\t)\n\n\t\tos.Exit(osutil.ExitCodeFailure)\n\t}\n\n\t// We should check if AdGuard Home is able to bind to port 53\n\terr := aghnet.CheckPort(\"tcp\", netip.AddrPortFrom(netutil.IPv4Localhost(), defaultPortDNS))\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrPermission) {\n\t\t\tslogutil.PrintLines(ctx, l, slog.LevelError, \"\", permCheckHelp)\n\n\t\t\tos.Exit(osutil.ExitCodeFailure)\n\t\t}\n\n\t\tl.ErrorContext(\n\t\t\tctx,\n\t\t\t\"failed to bind to port 53; binding to port 53 is required for a dns server\",\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\t}\n\n\tl.InfoContext(ctx, \"adguard home can bind to port 53\")\n}\n\n// Write PID to a file\nfunc writePIDFile(fn string) bool {\n\tdata := fmt.Sprintf(\"%d\", os.Getpid())\n\terr := os.WriteFile(fn, []byte(data), 0o644)\n\tif err != nil {\n\t\tlog.Error(\"Couldn't write PID to file %s: %v\", fn, err)\n\t\treturn false\n\t}\n\treturn true\n}\n\n// initConfigFilename returns the configuration file path.  If a path is\n// provided via command-line argument, it is used; otherwise a default within\n// workDir is returned.  l must not be nil.\nfunc initConfigFilename(\n\tctx context.Context,\n\tl *slog.Logger,\n\topts options,\n\tworkDir string,\n) (confPath string) {\n\tconfPath = opts.confFilename\n\tif confPath != \"\" {\n\t\tl.DebugContext(ctx, \"config path overridden from cmdline\", \"path\", confPath)\n\n\t\treturn confPath\n\t}\n\n\tconfPath = filepath.Join(workDir, \"AdGuardHome.yaml\")\n\n\treturn confPath\n}\n\n// initWorkingDir returns the working directory path.  If no command-line\n// argument is provided, it uses the executable's directory.\nfunc initWorkingDir(opts options) (workDir string, err error) {\n\tif opts.workDir != \"\" {\n\t\tworkDir = opts.workDir\n\t} else {\n\t\tvar execPath string\n\t\texecPath, err = os.Executable()\n\t\tif err != nil {\n\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tworkDir = filepath.Dir(execPath)\n\t}\n\n\tworkDir, err = filepath.EvalSymlinks(workDir)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn \"\", err\n\t}\n\n\treturn workDir, nil\n}\n\n// cleanup stops and resets all the modules.\nfunc cleanup(ctx context.Context) {\n\tlog.Info(\"stopping AdGuard Home\")\n\n\tif globalContext.web != nil {\n\t\tglobalContext.web.close(ctx)\n\t\tglobalContext.web = nil\n\t}\n\n\terr := stopDNSServer(ctx)\n\tif err != nil {\n\t\tlog.Error(\"stopping dns server: %s\", err)\n\t}\n\n\tif globalContext.dhcpServer != nil {\n\t\terr = globalContext.dhcpServer.Stop()\n\t\tif err != nil {\n\t\t\tlog.Error(\"stopping dhcp server: %s\", err)\n\t\t}\n\t}\n\n\tif globalContext.etcHosts != nil {\n\t\tif err = globalContext.etcHosts.Close(); err != nil {\n\t\t\tlog.Error(\"closing hosts container: %s\", err)\n\t\t}\n\t}\n}\n\n// This function is called before application exits\nfunc cleanupAlways() {\n\tif len(globalContext.pidFileName) != 0 {\n\t\t_ = os.Remove(globalContext.pidFileName)\n\t}\n\n\tlog.Info(\"stopped\")\n}\n\nfunc exitWithError() {\n\tos.Exit(64)\n}\n\n// loadCmdLineOpts reads command line arguments and initializes configuration\n// from them.  If there is an error or an effect, loadCmdLineOpts processes them\n// and exits.\nfunc loadCmdLineOpts() (opts options) {\n\topts, eff, err := parseCmdOpts(os.Args[0], os.Args[1:])\n\tif err != nil {\n\t\tlog.Error(\"%s\", err)\n\t\tprintHelp(os.Args[0])\n\n\t\texitWithError()\n\t}\n\n\tif eff != nil {\n\t\terr = eff()\n\t\tif err != nil {\n\t\t\tlog.Error(\"%s\", err)\n\t\t\texitWithError()\n\t\t}\n\n\t\tos.Exit(osutil.ExitCodeSuccess)\n\t}\n\n\treturn opts\n}\n\n// printWebAddrs prints addresses built from proto, addr, and an appropriate\n// port.  At least one address is printed with the value of port.  Output\n// example:\n//\n//\tgo to http://127.0.0.1:80\nfunc printWebAddrs(proto, addr string, port uint16) {\n\tlog.Printf(\"go to %s://%s\", proto, netutil.JoinHostPort(addr, port))\n}\n\n// printHTTPAddresses prints the IP addresses which user can use to access the\n// admin interface.  proto is either schemeHTTP or schemeHTTPS.\n//\n// TODO(s.chzhen):  Implement separate functions for HTTP and HTTPS.\nfunc printHTTPAddresses(proto string, tlsMgr *tlsManager) {\n\tvar tlsConf *tlsConfigSettings\n\tif tlsMgr != nil {\n\t\ttlsConf = tlsMgr.config()\n\t}\n\n\tport := config.HTTPConfig.Address.Port()\n\tif proto == urlutil.SchemeHTTPS {\n\t\tport = tlsConf.PortHTTPS\n\t}\n\n\tif proto == urlutil.SchemeHTTPS && tlsConf.ServerName != \"\" {\n\t\tprintWebAddrs(proto, tlsConf.ServerName, tlsConf.PortHTTPS)\n\n\t\treturn\n\t}\n\n\tbindHost := config.HTTPConfig.Address.Addr()\n\tif !bindHost.IsUnspecified() {\n\t\tprintWebAddrs(proto, bindHost.String(), port)\n\n\t\treturn\n\t}\n\n\tifaces, err := aghnet.GetValidNetInterfacesForWeb()\n\tif err != nil {\n\t\tlog.Error(\"web: getting iface ips: %s\", err)\n\t\t// That's weird, but we'll ignore it.\n\t\t//\n\t\t// TODO(e.burkov): Find out when it happens.\n\t\tprintWebAddrs(proto, bindHost.String(), port)\n\n\t\treturn\n\t}\n\n\tfor _, iface := range ifaces {\n\t\tfor _, addr := range iface.Addresses {\n\t\t\tprintWebAddrs(proto, addr.String(), port)\n\t\t}\n\t}\n}\n\n// detectFirstRun returns true if this is the first run of AdGuard Home.  l must\n// not be nil.\nfunc detectFirstRun(ctx context.Context, l *slog.Logger, workDir, confPath string) (ok bool) {\n\tif !filepath.IsAbs(confPath) {\n\t\tconfPath = filepath.Join(workDir, confPath)\n\t}\n\n\t_, err := os.Stat(confPath)\n\tif err == nil {\n\t\treturn false\n\t} else if errors.Is(err, os.ErrNotExist) {\n\t\treturn true\n\t}\n\n\tl.ErrorContext(ctx, \"failed to detect first run; considering first run\", slogutil.KeyError, err)\n\n\treturn true\n}\n\n// jsonError is a generic JSON error response.\n//\n// TODO(a.garipov): Merge together with the implementations in [dhcpd] and other\n// packages after refactoring the web handler registering.\ntype jsonError struct {\n\t// Message is the error message, an opaque string.\n\tMessage string `json:\"message\"`\n}\n\n// cmdlineUpdate updates current application and exits.  l, upd, and tlsMgr must\n// not be nil.\nfunc cmdlineUpdate(\n\tctx context.Context,\n\tl *slog.Logger,\n\topts options,\n\tupd *updater.Updater,\n\ttlsMgr *tlsManager,\n\tisFirstRun bool,\n) {\n\tif !opts.performUpdate {\n\t\treturn\n\t}\n\n\t// Initialize the DNS server to use the internal resolver which the updater\n\t// needs to be able to resolve the update source hostname.\n\t//\n\t// TODO(e.burkov):  We could probably initialize the internal resolver\n\t// separately.\n\terr := initDNSServer(ctx, nil, nil, nil, nil, nil, nil, tlsMgr, l, agh.EmptyConfigModifier{})\n\tfatalOnError(err)\n\n\tl.InfoContext(ctx, \"performing update via cli\")\n\n\tinfo, err := upd.VersionInfo(ctx, true)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"getting version info\", slogutil.KeyError, err)\n\n\t\tos.Exit(osutil.ExitCodeFailure)\n\t}\n\n\tif info.NewVersion == version.Version() {\n\t\tl.InfoContext(ctx, \"no updates available\")\n\n\t\tos.Exit(osutil.ExitCodeSuccess)\n\t}\n\n\terr = upd.Update(ctx, isFirstRun)\n\tfatalOnError(err)\n\n\terr = restartService(ctx, l)\n\tif err != nil {\n\t\tl.DebugContext(ctx, \"restarting service\", slogutil.KeyError, err)\n\t\tl.InfoContext(ctx, \"AdGuard Home was not installed as a service. \"+\n\t\t\t\"Please restart running instances of AdGuardHome manually.\")\n\t}\n\n\tos.Exit(osutil.ExitCodeSuccess)\n}\n"
  },
  {
    "path": "internal/home/home_internal_test.go",
    "content": "package home\n\nimport (\n\t\"cmp\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testLogger is a common logger for tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\n// testTrustedProxies is a common trusted proxies set for tests.\nvar testTrustedProxies = netutil.SliceSubnetSet([]netip.Prefix{})\n\n// newTestWeb is a helper that creates new webAPI and fills it's config with\n// given values.  If conf is nil, the default configuration will be used.\nfunc newTestWeb(\n\ttb testing.TB,\n\tconf *webConfig,\n) (web *webAPI) {\n\ttb.Helper()\n\n\tctx := testutil.ContextWithTimeout(tb, testTimeout)\n\tconf = cmp.Or(conf, &webConfig{})\n\n\tweb, err := newWeb(ctx, &webConfig{\n\t\tclientBuildFS:  conf.clientBuildFS,\n\t\tupdater:        conf.updater,\n\t\topts:           conf.opts,\n\t\tbaseLogger:     testLogger,\n\t\ttlsManager:     conf.tlsManager,\n\t\tauth:           conf.auth,\n\t\tmux:            cmp.Or(conf.mux, http.NewServeMux()),\n\t\tconfigModifier: cmp.Or[agh.ConfigModifier](conf.configModifier, &agh.EmptyConfigModifier{}),\n\t\thttpReg:        cmp.Or[aghhttp.Registrar](conf.httpReg, &aghhttp.EmptyRegistrar{}),\n\t\tworkDir:        conf.workDir,\n\t\tconfPath:       conf.confPath,\n\t\tisCustomUpdURL: conf.isCustomUpdURL,\n\t\tisFirstRun:     conf.isFirstRun,\n\t})\n\n\trequire.NoError(tb, err)\n\n\treturn web\n}\n\nfunc TestMain(m *testing.M) {\n\tinitCmdLineOpts()\n\ttestutil.DiscardLogOutput(m)\n}\n"
  },
  {
    "path": "internal/home/httpclient.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n)\n\n// customUserAgentTransport sets the User-Agent on requests when it is missing\n// to prevent Go from adding its default User-Agent.\ntype customUserAgentTransport struct {\n\t// transport is the underlying HTTP transport being wrapped.  It must not be\n\t// nil.\n\ttransport http.RoundTripper\n\n\t// userAgent is the custom User-Agent string for requests.  It must not be\n\t// empty.\n\tuserAgent string\n}\n\n// newCustomUserAgentTransport returns a properly initialized\n// *customUserAgentTransport.  rt must not be nil.  ua must not be empty.\nfunc newCustomUserAgentTransport(rt http.RoundTripper, ua string) (t *customUserAgentTransport) {\n\treturn &customUserAgentTransport{\n\t\ttransport: rt,\n\t\tuserAgent: ua,\n\t}\n}\n\n// type check\nvar _ http.RoundTripper = (*customUserAgentTransport)(nil)\n\n// RoundTrip implements the [http.RoundTripper] interface for\n// *customUserAgentTransport.\nfunc (t *customUserAgentTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {\n\tif req.Header.Get(httphdr.UserAgent) == \"\" {\n\t\treq = req.Clone(req.Context())\n\t\treq.Header.Set(httphdr.UserAgent, t.userAgent)\n\t}\n\n\treturn t.transport.RoundTrip(req)\n}\n\n// httpClient returns a new HTTP client that uses the AdGuard Home's own DNS\n// server for resolving hostnames.  The resulting client should not be used\n// until [Context.dnsServer] is initialized.  tlsMgr must not be nil.\n//\n// TODO(a.garipov, e.burkov): This is rather messy.  Refactor.\nfunc httpClient(tlsMgr *tlsManager) (c *http.Client) {\n\t// Do not use Context.dnsServer.DialContext directly in the struct literal\n\t// below, since Context.dnsServer may be nil when this function is called.\n\tdialContext := func(ctx context.Context, network, addr string) (conn net.Conn, err error) {\n\t\treturn globalContext.dnsServer.DialContext(ctx, network, addr)\n\t}\n\n\ttr := newCustomUserAgentTransport(&http.Transport{\n\t\tDialContext: dialContext,\n\t\tProxy:       httpProxy,\n\t\tTLSClientConfig: &tls.Config{\n\t\t\tRootCAs:      tlsMgr.rootCerts,\n\t\t\tCipherSuites: tlsMgr.customCipherIDs,\n\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t},\n\t}, aghhttp.UserAgent())\n\n\treturn &http.Client{\n\t\t// TODO(a.garipov): Make configurable.\n\t\tTimeout:   writeTimeout,\n\t\tTransport: tr,\n\t}\n}\n\n// httpProxy returns parses and returns an HTTP proxy URL from the config, if\n// any.\nfunc httpProxy(_ *http.Request) (u *url.URL, err error) {\n\tif config.ProxyURL == \"\" {\n\t\treturn nil, nil\n\t}\n\n\treturn url.Parse(config.ProxyURL)\n}\n"
  },
  {
    "path": "internal/home/httpclient_internal_test.go",
    "content": "package home\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCustomUserAgentTransport_RoundTrip(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tcustomUA  = \"Custom-user-agent/1.1\"\n\t\tpresentUA = \"Present-user-agent/1.1\"\n\t)\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tua := r.Header.Get(httphdr.UserAgent)\n\t\t_, err := io.WriteString(w, ua)\n\t\trequire.NoError(testutil.PanicT{}, err)\n\t}))\n\tt.Cleanup(srv.Close)\n\n\ttestCases := []struct {\n\t\tclient *http.Client\n\t\twantUA []byte\n\t\treqUA  string\n\t\tname   string\n\t}{{\n\t\tclient: &http.Client{Transport: http.DefaultTransport},\n\t\twantUA: []byte(\"Go-http-client/1.1\"),\n\t\treqUA:  \"\",\n\t\tname:   \"default\",\n\t}, {\n\t\tclient: &http.Client{\n\t\t\tTransport: newCustomUserAgentTransport(http.DefaultTransport, customUA),\n\t\t},\n\t\treqUA:  \"\",\n\t\twantUA: []byte(customUA),\n\t\tname:   \"custom\",\n\t}, {\n\t\tclient: &http.Client{\n\t\t\tTransport: newCustomUserAgentTransport(http.DefaultTransport, customUA),\n\t\t},\n\t\treqUA:  presentUA,\n\t\twantUA: []byte(presentUA),\n\t\tname:   \"present\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL, nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.reqUA != \"\" {\n\t\t\t\treq.Header.Set(httphdr.UserAgent, tc.reqUA)\n\t\t\t}\n\n\t\t\tresp, err := tc.client.Do(req)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttestutil.CleanupAndRequireSuccess(t, resp.Body.Close)\n\n\t\t\tgot, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.wantUA, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/home/i18n.go",
    "content": "package home\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/golibs/container\"\n)\n\n// TODO(a.garipov): Get rid of a global or generate from .twosky.json.\nvar allowedLanguages = container.NewMapSet(\n\t\"ar\",\n\t\"be\",\n\t\"bg\",\n\t\"cs\",\n\t\"da\",\n\t\"de\",\n\t\"en\",\n\t\"es\",\n\t\"fa\",\n\t\"fi\",\n\t\"fr\",\n\t\"hr\",\n\t\"hu\",\n\t\"id\",\n\t\"it\",\n\t\"ja\",\n\t\"ko\",\n\t\"nl\",\n\t\"no\",\n\t\"pl\",\n\t\"pt-br\",\n\t\"pt-pt\",\n\t\"ro\",\n\t\"ru\",\n\t\"si-lk\",\n\t\"sk\",\n\t\"sl\",\n\t\"sr-cs\",\n\t\"sv\",\n\t\"th\",\n\t\"tr\",\n\t\"uk\",\n\t\"vi\",\n\t\"zh-cn\",\n\t\"zh-hk\",\n\t\"zh-tw\",\n)\n\n// languageJSON is the JSON structure for language requests and responses.\ntype languageJSON struct {\n\tLanguage string `json:\"language\"`\n}\n\n// handleI18nCurrentLanguage is the handler for the GET\n// /control/i18n/current_language HTTP API.\n//\n// TODO(d.kolyshev): Deprecated, remove it later.\nfunc (web *webAPI) handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\tl.InfoContext(ctx, \"current language\", \"lang\", config.Language)\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, &languageJSON{\n\t\tLanguage: config.Language,\n\t})\n}\n\n// handleI18nChangeLanguage is the handler for the POST\n// /control/i18n/change_language HTTP API.\n//\n// TODO(d.kolyshev): Deprecated, remove it later.\nfunc (web *webAPI) handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\tif aghhttp.WriteTextPlainDeprecated(ctx, l, w, r) {\n\t\treturn\n\t}\n\n\tlangReq := &languageJSON{}\n\terr := json.NewDecoder(r.Body).Decode(langReq)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, \"reading req: %s\", err)\n\n\t\treturn\n\t}\n\n\tlang := langReq.Language\n\tif !allowedLanguages.Has(lang) {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"unknown language: %q\", lang)\n\n\t\treturn\n\t}\n\n\tfunc() {\n\t\tconfig.Lock()\n\t\tdefer config.Unlock()\n\n\t\tconfig.Language = lang\n\t\tl.InfoContext(ctx, \"language is updated\", \"lang\", lang)\n\t}()\n\n\tweb.confModifier.Apply(ctx)\n\n\taghhttp.OK(ctx, l, w)\n}\n"
  },
  {
    "path": "internal/home/log.go",
    "content": "package home\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\tyaml \"go.yaml.in/yaml/v4\"\n\t\"gopkg.in/natefinch/lumberjack.v2\"\n)\n\n// configSyslog is used to indicate that syslog or eventlog (win) should be used\n// for logger output.\nconst configSyslog = \"syslog\"\n\n// newSlogLogger returns new [*slog.Logger] configured with the given settings.\n// ls must not be nil.\nfunc newSlogLogger(ls *logSettings) (l *slog.Logger) {\n\tif !ls.Enabled {\n\t\treturn slogutil.NewDiscardLogger()\n\t}\n\n\tlvl := slog.LevelInfo\n\tif ls.Verbose {\n\t\tlvl = slog.LevelDebug\n\t}\n\n\tl = slogutil.New(&slogutil.Config{\n\t\tFormat:       slogutil.FormatAdGuardLegacy,\n\t\tLevel:        lvl,\n\t\tAddTimestamp: true,\n\t})\n\n\t// Configure logger level.\n\tif !ls.Enabled {\n\t\tlog.SetLevel(log.OFF)\n\t} else if ls.Verbose {\n\t\tlog.SetLevel(log.DEBUG)\n\t}\n\n\treturn l\n}\n\n// configureLogger configures logger output.  ls must not be nil.\nfunc configureLogger(ls *logSettings, workDir string) (err error) {\n\t// Make sure that we see the microseconds in logs, as networking stuff can\n\t// happen pretty quickly.\n\tlog.SetFlags(log.LstdFlags | log.Lmicroseconds)\n\n\t// Write logs to stdout by default.\n\tif ls.File == \"\" {\n\t\treturn nil\n\t}\n\n\tif ls.File == configSyslog {\n\t\t// Use syslog where it is possible and eventlog on Windows.\n\t\terr = aghos.ConfigureSyslog(serviceName)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cannot initialize syslog: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tlogFilePath := ls.File\n\tif !filepath.IsAbs(logFilePath) {\n\t\tlogFilePath = filepath.Join(workDir, logFilePath)\n\t}\n\n\tlog.SetOutput(&lumberjack.Logger{\n\t\tFilename:   logFilePath,\n\t\tCompress:   ls.Compress,\n\t\tLocalTime:  ls.LocalTime,\n\t\tMaxBackups: ls.MaxBackups,\n\t\tMaxSize:    ls.MaxSize,\n\t\tMaxAge:     ls.MaxAge,\n\t})\n\n\treturn err\n}\n\n// getLogSettings returns a log settings object properly initialized from opts.\n// l must not be nil.\nfunc getLogSettings(\n\tctx context.Context,\n\tl *slog.Logger,\n\topts options,\n\tworkDir string,\n\tconfPath string,\n) (ls *logSettings) {\n\tconfigLogSettings := config.Log\n\n\tls = readLogSettings(ctx, l, workDir, confPath)\n\tif ls == nil {\n\t\t// Use default log settings.\n\t\tls = &configLogSettings\n\t}\n\n\t// Command-line arguments can override config settings.\n\tif opts.verbose {\n\t\tls.Verbose = true\n\t}\n\n\tls.File = cmp.Or(opts.logFile, ls.File)\n\n\tif opts.runningAsService && ls.File == \"\" && runtime.GOOS == \"windows\" {\n\t\t// When running as a Windows service, use eventlog by default if\n\t\t// nothing else is configured.  Otherwise, we'll lose the log output.\n\t\tls.File = configSyslog\n\t}\n\n\treturn ls\n}\n\n// readLogSettings reads logging settings from the config file.  We do it in a\n// separate method in order to configure logger before the actual configuration\n// is parsed and applied.  l must not be nil.\nfunc readLogSettings(\n\tctx context.Context,\n\tl *slog.Logger,\n\tworkDir string,\n\tconfPath string,\n) (ls *logSettings) {\n\t// TODO(s.chzhen):  Add a helper function that returns default parameters\n\t// for this structure and for the global configuration structure [config].\n\tconf := &configuration{\n\t\tLog: logSettings{\n\t\t\t// By default, it is true if the property does not exist.\n\t\t\tEnabled: true,\n\t\t},\n\t}\n\n\tyamlFile, err := readConfigFile(ctx, l, workDir, confPath)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\terr = yaml.Unmarshal(yamlFile, conf)\n\tif err != nil {\n\t\tlog.Error(\"Couldn't get logging settings from the configuration: %s\", err)\n\t}\n\n\treturn &conf.Log\n}\n"
  },
  {
    "path": "internal/home/middlewares.go",
    "content": "package home\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\n// middlerware is a wrapper function signature.\ntype middleware func(http.Handler) http.Handler\n\n// withMiddlewares consequently wraps h with all the middlewares.\n//\n// TODO(e.burkov):  Use [httputil.Wrap].\nfunc withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Handler) {\n\twrapped = h\n\n\tfor _, mw := range middlewares {\n\t\twrapped = mw(wrapped)\n\t}\n\n\treturn wrapped\n}\n\nconst (\n\t// defaultReqBodySzLim is the default maximum request body size.\n\tdefaultReqBodySzLim datasize.ByteSize = 64 * datasize.KB\n\n\t// largerReqBodySzLim is the maximum request body size for APIs expecting\n\t// larger requests.\n\tlargerReqBodySzLim datasize.ByteSize = 4 * datasize.MB\n)\n\n// expectsLargerRequests shows if this request should use a larger body size\n// limit.  These are exceptions for poorly designed current APIs as well as APIs\n// that are designed to expect large files and requests.  Remove once the new,\n// better APIs are up.\n//\n// See https://github.com/AdguardTeam/AdGuardHome/issues/2666 and\n// https://github.com/AdguardTeam/AdGuardHome/issues/2675.\nfunc expectsLargerRequests(r *http.Request) (ok bool) {\n\tif r.Method != http.MethodPost {\n\t\treturn false\n\t}\n\n\tswitch r.URL.Path {\n\tcase \"/control/access/set\", \"/control/filtering/set_rules\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// limitRequestBody wraps underlying handler h, making it's request's body Read\n// method limited.\nfunc limitRequestBody(h http.Handler) (limited http.Handler) {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tszLim := defaultReqBodySzLim\n\t\tif expectsLargerRequests(r) {\n\t\t\tszLim = largerReqBodySzLim\n\t\t}\n\n\t\treader := ioutil.LimitReader(r.Body, szLim.Bytes())\n\n\t\t// HTTP handlers aren't supposed to call r.Body.Close(), so just\n\t\t// replace the body in a clone.\n\t\trr := r.Clone(r.Context())\n\t\trr.Body = io.NopCloser(reader)\n\n\t\th.ServeHTTP(w, rr)\n\t})\n}\n"
  },
  {
    "path": "internal/home/middlewares_internal_test.go",
    "content": "package home\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLimitRequestBody(t *testing.T) {\n\terrReqLimitReached := &ioutil.LimitError{\n\t\tLimit: defaultReqBodySzLim.Bytes(),\n\t}\n\n\ttestCases := []struct {\n\t\twantErr error\n\t\tname    string\n\t\tbody    string\n\t\twant    []byte\n\t}{{\n\t\twantErr: nil,\n\t\tname:    \"not_so_big\",\n\t\tbody:    \"somestr\",\n\t\twant:    []byte(\"somestr\"),\n\t}, {\n\t\twantErr: errReqLimitReached,\n\t\tname:    \"so_big\",\n\t\tbody:    string(make([]byte, defaultReqBodySzLim+1)),\n\t\twant:    make([]byte, defaultReqBodySzLim),\n\t}, {\n\t\twantErr: nil,\n\t\tname:    \"empty\",\n\t\tbody:    \"\",\n\t\twant:    []byte(nil),\n\t}}\n\n\tmakeHandler := func(tb testing.TB, err *error) http.HandlerFunc {\n\t\ttb.Helper()\n\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tvar b []byte\n\t\t\tb, *err = io.ReadAll(r.Body)\n\t\t\t_, werr := w.Write(b)\n\t\t\trequire.NoError(tb, werr)\n\t\t})\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar err error\n\t\t\thandler := makeHandler(t, &err)\n\t\t\tlim := limitRequestBody(handler)\n\n\t\t\treq := httptest.NewRequest(http.MethodPost, \"https://www.example.com\", strings.NewReader(tc.body))\n\t\t\tres := httptest.NewRecorder()\n\n\t\t\tlim.ServeHTTP(res, req)\n\n\t\t\tassert.Equal(t, tc.wantErr, err)\n\t\t\tassert.Equal(t, tc.want, res.Body.Bytes())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/home/mobileconfig.go",
    "content": "package home\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/google/uuid\"\n\t\"howett.net/plist\"\n)\n\n// dnsSettings is the DNSSetting.DNSSettings mobileconfig profile.\n//\n// See https://developer.apple.com/documentation/devicemanagement/dnssettings/dnssettings.\ntype dnsSettings struct {\n\t// DNSProtocol is the required protocol to be used.  The valid values\n\t// are \"HTTPS\" and \"TLS\".\n\tDNSProtocol string\n\n\t// ServerURL is the URI template of the DoH server.  It must be empty if\n\t// DNSProtocol is not \"HTTPS\".\n\tServerURL string `plist:\",omitempty\"`\n\n\t// ServerName is the hostname of the DoT server.  It must be empty if\n\t// DNSProtocol is not \"TLS\".\n\tServerName string `plist:\",omitempty\"`\n\n\t// ServerAddresses is a list IP addresses of the server.\n\t//\n\t// TODO(a.garipov): Allow users to set this.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/3607.\n\tServerAddresses []net.IP `plist:\",omitempty\"`\n}\n\n// payloadContent is a Device Management Profile payload.\n//\n// See https://developer.apple.com/documentation/devicemanagement/configuring_multiple_devices_using_profiles#3234127.\ntype payloadContent struct {\n\tDNSSettings *dnsSettings\n\n\tOnDemandEnabled int\n\tOnDemandRules   []*onDemandRule\n\n\tPayloadType        string\n\tPayloadIdentifier  string\n\tPayloadDisplayName string\n\tPayloadDescription string\n\tPayloadUUID        string\n\tPayloadVersion     int\n}\n\n// onDemandRule determines which queries use the DNS server.\n//\n// See https://developer.apple.com/documentation/devicemanagement/dnssettings/ondemandruleselement.\ntype onDemandRule struct {\n\tAction string\n}\n\n// dnsSettingsPayloadType is the payload type for a DNSSettings profile.\nconst dnsSettingsPayloadType = \"com.apple.dnsSettings.managed\"\n\n// mobileConfig contains the TopLevel properties for configuring Device\n// Management Profiles.\n//\n// See https://developer.apple.com/documentation/devicemanagement/toplevel.\ntype mobileConfig struct {\n\tPayloadDescription       string\n\tPayloadDisplayName       string\n\tPayloadType              string\n\tPayloadContent           []*payloadContent\n\tPayloadIdentifier        string\n\tPayloadUUID              string\n\tPayloadVersion           int\n\tPayloadRemovalDisallowed bool\n\tPayloadScope             string\n}\n\nconst (\n\tdnsProtoHTTPS = \"HTTPS\"\n\tdnsProtoTLS   = \"TLS\"\n)\n\nfunc encodeMobileConfig(d *dnsSettings, clientID string) ([]byte, error) {\n\tvar dspName string\n\tswitch proto := d.DNSProtocol; proto {\n\tcase dnsProtoHTTPS:\n\t\tdspName = fmt.Sprintf(\"%s DoH\", d.ServerName)\n\t\tu := &url.URL{\n\t\t\tScheme: urlutil.SchemeHTTPS,\n\t\t\tHost:   d.ServerName,\n\t\t\tPath:   path.Join(\"/dns-query\", clientID),\n\t\t}\n\t\td.ServerURL = u.String()\n\n\t\t// Empty the ServerName field since it is only must be presented\n\t\t// in DNS-over-TLS configuration.\n\t\td.ServerName = \"\"\n\tcase dnsProtoTLS:\n\t\tdspName = fmt.Sprintf(\"%s DoT\", d.ServerName)\n\t\tif clientID != \"\" {\n\t\t\td.ServerName = clientID + \".\" + d.ServerName\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"bad dns protocol %q\", proto)\n\t}\n\n\tpayloadID := fmt.Sprintf(\"%s.%s\", dnsSettingsPayloadType, uuid.New())\n\tdata := &mobileConfig{\n\t\tPayloadDescription: \"Adds AdGuard Home to macOS Big Sur and iOS 14 or newer systems\",\n\t\tPayloadDisplayName: dspName,\n\t\tPayloadType:        \"Configuration\",\n\t\tPayloadScope:       \"System\",\n\t\tPayloadContent: []*payloadContent{{\n\t\t\tDNSSettings:     d,\n\t\t\tOnDemandEnabled: 1,\n\t\t\tOnDemandRules: []*onDemandRule{{\n\t\t\t\tAction: \"Connect\",\n\t\t\t}},\n\t\t\tPayloadType:        dnsSettingsPayloadType,\n\t\t\tPayloadIdentifier:  payloadID,\n\t\t\tPayloadDisplayName: dspName,\n\t\t\tPayloadDescription: \"Configures device to use AdGuard Home\",\n\t\t\tPayloadUUID:        strings.ToUpper(uuid.New().String()),\n\t\t\tPayloadVersion:     1,\n\t\t}},\n\t\tPayloadIdentifier:        strings.ToUpper(uuid.New().String()),\n\t\tPayloadUUID:              strings.ToUpper(uuid.New().String()),\n\t\tPayloadVersion:           1,\n\t\tPayloadRemovalDisallowed: false,\n\t}\n\n\treturn plist.MarshalIndent(data, plist.XMLFormat, \"\\t\")\n}\n\nfunc respondJSONError(w http.ResponseWriter, status int, msg string) {\n\tw.WriteHeader(http.StatusInternalServerError)\n\terr := json.NewEncoder(w).Encode(&jsonError{\n\t\tMessage: msg,\n\t})\n\tif err != nil {\n\t\tlog.Debug(\"writing %d json response: %s\", status, err)\n\t}\n}\n\nconst errEmptyHost errors.Error = \"no host in query parameters and no server_name\"\n\nfunc handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {\n\tvar err error\n\n\tq := r.URL.Query()\n\thost := q.Get(\"host\")\n\tif host == \"\" {\n\t\trespondJSONError(w, http.StatusInternalServerError, string(errEmptyHost))\n\n\t\treturn\n\t}\n\n\tclientID := q.Get(\"client_id\")\n\tif clientID != \"\" {\n\t\terr = client.ValidateClientID(clientID)\n\t\tif err != nil {\n\t\t\trespondJSONError(w, http.StatusBadRequest, err.Error())\n\n\t\t\treturn\n\t\t}\n\t}\n\n\td := &dnsSettings{\n\t\tDNSProtocol: dnsp,\n\t\tServerName:  host,\n\t}\n\n\tmobileconfig, err := encodeMobileConfig(d, clientID)\n\tif err != nil {\n\t\trespondJSONError(w, http.StatusInternalServerError, err.Error())\n\n\t\treturn\n\t}\n\n\tw.Header().Set(httphdr.ContentType, \"application/xml\")\n\n\tconst (\n\t\tdohContDisp = `attachment; filename=doh.mobileconfig`\n\t\tdotContDisp = `attachment; filename=dot.mobileconfig`\n\t)\n\n\tcontDisp := dohContDisp\n\tif dnsp == dnsProtoTLS {\n\t\tcontDisp = dotContDisp\n\t}\n\n\tw.Header().Set(httphdr.ContentDisposition, contDisp)\n\n\t_, _ = w.Write(mobileconfig)\n}\n\nfunc handleMobileConfigDoH(w http.ResponseWriter, r *http.Request) {\n\thandleMobileConfig(w, r, dnsProtoHTTPS)\n}\n\nfunc handleMobileConfigDoT(w http.ResponseWriter, r *http.Request) {\n\thandleMobileConfig(w, r, dnsProtoTLS)\n}\n"
  },
  {
    "path": "internal/home/mobileconfig_internal_test.go",
    "content": "package home\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"howett.net/plist\"\n)\n\n// setupDNSIPs is a helper that sets up the server IP address configuration for\n// tests and also tears it down in a cleanup function.\nfunc setupDNSIPs(tb testing.TB) {\n\ttb.Helper()\n\n\tprevConfig := config\n\ttb.Cleanup(func() {\n\t\tconfig = prevConfig\n\t})\n\n\tconfig = &configuration{\n\t\tDNS: dnsConfig{\n\t\t\tBindHosts: []netip.Addr{netip.IPv4Unspecified()},\n\t\t\tPort:      defaultPortDNS,\n\t\t},\n\t}\n}\n\nfunc TestHandleMobileConfigDoH(t *testing.T) {\n\tsetupDNSIPs(t)\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tr, err := http.NewRequest(http.MethodGet, \"https://example.com:12345/apple/doh.mobileconfig?host=example.org\", nil)\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\n\t\thandleMobileConfigDoH(w, r)\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tvar mc mobileConfig\n\t\t_, err = plist.Unmarshal(w.Body.Bytes(), &mc)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, mc.PayloadContent, 1)\n\n\t\tassert.Equal(t, \"example.org DoH\", mc.PayloadContent[0].PayloadDisplayName)\n\n\t\ts := mc.PayloadContent[0].DNSSettings\n\t\trequire.NotNil(t, s)\n\n\t\tassert.Empty(t, s.ServerName)\n\t\tassert.Equal(t, \"https://example.org/dns-query\", s.ServerURL)\n\t})\n\n\tt.Run(\"error_no_host\", func(t *testing.T) {\n\t\tr, err := http.NewRequest(http.MethodGet, \"https://example.com:12345/apple/doh.mobileconfig\", nil)\n\t\trequire.NoError(t, err)\n\n\t\tb := &bytes.Buffer{}\n\t\terr = json.NewEncoder(b).Encode(&jsonError{\n\t\t\tMessage: errEmptyHost.Error(),\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\n\t\thandleMobileConfigDoH(w, r)\n\t\tassert.Equal(t, http.StatusInternalServerError, w.Code)\n\t\tassert.JSONEq(t, w.Body.String(), b.String())\n\t})\n\n\tt.Run(\"client_id\", func(t *testing.T) {\n\t\tr, err := http.NewRequest(http.MethodGet, \"https://example.com:12345/apple/doh.mobileconfig?host=example.org&client_id=cli42\", nil)\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\n\t\thandleMobileConfigDoH(w, r)\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tvar mc mobileConfig\n\t\t_, err = plist.Unmarshal(w.Body.Bytes(), &mc)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, mc.PayloadContent, 1)\n\n\t\tassert.Equal(t, \"example.org DoH\", mc.PayloadContent[0].PayloadDisplayName)\n\n\t\ts := mc.PayloadContent[0].DNSSettings\n\t\trequire.NotNil(t, s)\n\n\t\tassert.Empty(t, s.ServerName)\n\t\tassert.Equal(t, \"https://example.org/dns-query/cli42\", s.ServerURL)\n\t})\n}\n\nfunc TestHandleMobileConfigDoT(t *testing.T) {\n\tsetupDNSIPs(t)\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tr, err := http.NewRequest(http.MethodGet, \"https://example.com:12345/apple/dot.mobileconfig?host=example.org\", nil)\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\n\t\thandleMobileConfigDoT(w, r)\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tvar mc mobileConfig\n\t\t_, err = plist.Unmarshal(w.Body.Bytes(), &mc)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, mc.PayloadContent, 1)\n\n\t\tassert.Equal(t, \"example.org DoT\", mc.PayloadContent[0].PayloadDisplayName)\n\n\t\ts := mc.PayloadContent[0].DNSSettings\n\t\trequire.NotNil(t, s)\n\n\t\tassert.Equal(t, \"example.org\", s.ServerName)\n\t\tassert.Empty(t, s.ServerURL)\n\t})\n\n\tt.Run(\"error_no_host\", func(t *testing.T) {\n\t\tr, err := http.NewRequest(http.MethodGet, \"https://example.com:12345/apple/dot.mobileconfig\", nil)\n\t\trequire.NoError(t, err)\n\n\t\tb := &bytes.Buffer{}\n\t\terr = json.NewEncoder(b).Encode(&jsonError{\n\t\t\tMessage: errEmptyHost.Error(),\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\n\t\thandleMobileConfigDoT(w, r)\n\t\tassert.Equal(t, http.StatusInternalServerError, w.Code)\n\t\tassert.JSONEq(t, w.Body.String(), b.String())\n\t})\n\n\tt.Run(\"client_id\", func(t *testing.T) {\n\t\tr, err := http.NewRequest(http.MethodGet, \"https://example.com:12345/apple/dot.mobileconfig?host=example.org&client_id=cli42\", nil)\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\n\t\thandleMobileConfigDoT(w, r)\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tvar mc mobileConfig\n\t\t_, err = plist.Unmarshal(w.Body.Bytes(), &mc)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, mc.PayloadContent, 1)\n\n\t\tassert.Equal(t, \"example.org DoT\", mc.PayloadContent[0].PayloadDisplayName)\n\n\t\ts := mc.PayloadContent[0].DNSSettings\n\t\trequire.NotNil(t, s)\n\n\t\tassert.Equal(t, \"cli42.example.org\", s.ServerName)\n\t\tassert.Empty(t, s.ServerURL)\n\t})\n}\n"
  },
  {
    "path": "internal/home/options.go",
    "content": "package home\n\nimport (\n\t\"fmt\"\n\t\"iter\"\n\t\"net/netip\"\n\t\"os\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/configmigrate\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n)\n\n// TODO(a.garipov): Replace with package flag.\n\n// options represents the command-line options.\ntype options struct {\n\t// confFilename is the path to the configuration file.\n\tconfFilename string\n\n\t// workDir is the path to the working directory where AdGuard Home stores\n\t// filter data, the query log, and other data.\n\tworkDir string\n\n\t// logFile is the path to the log file.  If empty, AdGuard Home writes to\n\t// stdout; if \"syslog\", to syslog.\n\tlogFile string\n\n\t// pidFile is the file name for the file to which the PID is saved.\n\tpidFile string\n\n\t// serviceControlAction is the service action to perform.  See\n\t// [service.ControlAction] and [handleServiceControlAction].\n\t//\n\t// TODO(e.burkov):  Use [ossvc.ActionName].\n\tserviceControlAction string\n\n\t// bindHost is the address on which to serve the HTTP UI.\n\t//\n\t// Deprecated: Use bindAddr.\n\tbindHost netip.Addr\n\n\t// bindPort is the port on which to serve the HTTP UI.\n\t//\n\t// Deprecated: Use bindAddr.\n\tbindPort uint16\n\n\t// bindAddr is the address to serve the web UI on.\n\tbindAddr netip.AddrPort\n\n\t// checkConfig is true if the current invocation is only required to check\n\t// the configuration file and exit.\n\tcheckConfig bool\n\n\t// disableUpdate, if set, makes AdGuard Home not check for updates.\n\tdisableUpdate bool\n\n\t// performUpdate, if set, updates AdGuard Home without GUI and exits.\n\tperformUpdate bool\n\n\t// verbose shows if verbose logging is enabled.\n\tverbose bool\n\n\t// runningAsService flag is set to true when options are passed from the\n\t// service runner\n\t//\n\t// TODO(a.garipov): Perhaps this could be determined by a non-empty\n\t// serviceControlAction?\n\trunningAsService bool\n\n\t// glinetMode shows if the GL-Inet compatibility mode is enabled.\n\tglinetMode bool\n\n\t// noEtcHosts flag should be provided when /etc/hosts file shouldn't be\n\t// used.\n\tnoEtcHosts bool\n\n\t// localFrontend forces AdGuard Home to use the frontend files from disk\n\t// rather than the ones that have been compiled into the binary.\n\tlocalFrontend bool\n\n\t// noPermCheck disables checking and migration of permissions for the\n\t// security-sensitive files.\n\tnoPermCheck bool\n}\n\n// initCmdLineOpts completes initialization of the global command-line option\n// slice.  It must only be called once.\nfunc initCmdLineOpts() {\n\t// The --help option cannot be put directly into cmdLineOpts, because that\n\t// causes initialization cycle due to printHelp referencing cmdLineOpts.\n\tcmdLineOpts = append(cmdLineOpts, cmdLineOpt{\n\t\tupdateWithValue: nil,\n\t\tupdateNoValue:   nil,\n\t\teffect: func(o options, exec string) (effect, error) {\n\t\t\treturn func() error { printHelp(exec); exitWithError(); return nil }, nil\n\t\t},\n\t\tserialize:   func(o options) (val string, ok bool) { return \"\", false },\n\t\tdescription: \"Print this help.\",\n\t\tlongName:    \"help\",\n\t\tshortName:   \"\",\n\t})\n}\n\n// effect is the type for functions used for their side-effects.\ntype effect func() (err error)\n\n// cmdLineOpt contains the data for a single command-line option.  Only one of\n// updateWithValue, updateNoValue, and effect must be present.\ntype cmdLineOpt struct {\n\tupdateWithValue func(o options, v string) (updated options, err error)\n\tupdateNoValue   func(o options) (updated options, err error)\n\teffect          func(o options, exec string) (eff effect, err error)\n\n\t// serialize is a function that encodes the option into a slice of\n\t// command-line arguments, if necessary.  If ok is false, this option should\n\t// be skipped.\n\tserialize func(o options) (val string, ok bool)\n\n\tdescription string\n\tlongName    string\n\tshortName   string\n}\n\n// cmdLineOpts are all command-line options of AdGuard Home.\nvar cmdLineOpts = []cmdLineOpt{{\n\tupdateWithValue: func(o options, v string) (options, error) {\n\t\to.confFilename = v\n\t\treturn o, nil\n\t},\n\tupdateNoValue: nil,\n\teffect:        nil,\n\tserialize: func(o options) (val string, ok bool) {\n\t\treturn o.confFilename, o.confFilename != \"\"\n\t},\n\tdescription: \"Path to the config file.\",\n\tlongName:    \"config\",\n\tshortName:   \"c\",\n}, {\n\tupdateWithValue: func(o options, v string) (options, error) { o.workDir = v; return o, nil },\n\tupdateNoValue:   nil,\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return o.workDir, o.workDir != \"\" },\n\tdescription:     \"Path to the working directory.\",\n\tlongName:        \"work-dir\",\n\tshortName:       \"w\",\n}, {\n\tupdateWithValue: func(o options, v string) (oo options, err error) {\n\t\to.bindHost, err = netip.ParseAddr(v)\n\n\t\treturn o, err\n\t},\n\tupdateNoValue: nil,\n\teffect:        nil,\n\tserialize: func(o options) (val string, ok bool) {\n\t\tif !o.bindHost.IsValid() {\n\t\t\treturn \"\", false\n\t\t}\n\n\t\treturn o.bindHost.String(), true\n\t},\n\tdescription: \"Deprecated. Host address to bind HTTP server on. Use --web-addr. \" +\n\t\t\"The short -h will work as --help in the future.\",\n\tlongName:  \"host\",\n\tshortName: \"h\",\n}, {\n\tupdateWithValue: func(o options, v string) (options, error) {\n\t\tp, err := strconv.ParseUint(v, 10, 16)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"parsing port: %w\", err)\n\t\t} else {\n\t\t\to.bindPort = uint16(p)\n\t\t}\n\n\t\treturn o, err\n\t},\n\tupdateNoValue: nil,\n\teffect:        nil,\n\tserialize: func(o options) (val string, ok bool) {\n\t\tif o.bindPort == 0 {\n\t\t\treturn \"\", false\n\t\t}\n\n\t\treturn strconv.Itoa(int(o.bindPort)), true\n\t},\n\tdescription: \"Deprecated. Port to serve HTTP pages on. Use --web-addr.\",\n\tlongName:    \"port\",\n\tshortName:   \"p\",\n}, {\n\tupdateWithValue: func(o options, v string) (oo options, err error) {\n\t\to.bindAddr, err = netip.ParseAddrPort(v)\n\n\t\treturn o, err\n\t},\n\tupdateNoValue: nil,\n\teffect:        nil,\n\tserialize: func(o options) (val string, ok bool) {\n\t\treturn o.bindAddr.String(), o.bindAddr.IsValid()\n\t},\n\tdescription: \"Address to serve the web UI on, in the host:port format.\",\n\tlongName:    \"web-addr\",\n\tshortName:   \"\",\n}, {\n\tupdateWithValue: func(o options, v string) (options, error) {\n\t\to.serviceControlAction = v\n\t\treturn o, nil\n\t},\n\tupdateNoValue: nil,\n\teffect:        nil,\n\tserialize: func(o options) (val string, ok bool) {\n\t\treturn o.serviceControlAction, o.serviceControlAction != \"\"\n\t},\n\tdescription: `Service control action: status, install (as a service), ` +\n\t\t`uninstall (as a service), start, stop, restart, reload (configuration).`,\n\tlongName:  \"service\",\n\tshortName: \"s\",\n}, {\n\tupdateWithValue: func(o options, v string) (options, error) { o.logFile = v; return o, nil },\n\tupdateNoValue:   nil,\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return o.logFile, o.logFile != \"\" },\n\tdescription: `Path to log file.  If empty, write to stdout; ` +\n\t\t`if \"syslog\", write to system log.`,\n\tlongName:  \"logfile\",\n\tshortName: \"l\",\n}, {\n\tupdateWithValue: func(o options, v string) (options, error) { o.pidFile = v; return o, nil },\n\tupdateNoValue:   nil,\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return o.pidFile, o.pidFile != \"\" },\n\tdescription:     \"Path to a file where PID is stored.\",\n\tlongName:        \"pidfile\",\n\tshortName:       \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   func(o options) (options, error) { o.checkConfig = true; return o, nil },\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return \"\", o.checkConfig },\n\tdescription:     \"Check configuration and exit.\",\n\tlongName:        \"check-config\",\n\tshortName:       \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   func(o options) (options, error) { o.disableUpdate = true; return o, nil },\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return \"\", o.disableUpdate },\n\tdescription:     \"Don't check for updates.\",\n\tlongName:        \"no-check-update\",\n\tshortName:       \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   func(o options) (options, error) { o.performUpdate = true; return o, nil },\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return \"\", o.performUpdate },\n\tdescription:     \"Update the current binary and restart the service in case it's installed.\",\n\tlongName:        \"update\",\n\tshortName:       \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   nil,\n\teffect: func(_ options, _ string) (f effect, err error) {\n\t\tlog.Info(\"warning: using --no-mem-optimization flag has no effect and is deprecated\")\n\n\t\treturn nil, nil\n\t},\n\tserialize:   func(o options) (val string, ok bool) { return \"\", false },\n\tdescription: \"Deprecated.  Disable memory optimization.\",\n\tlongName:    \"no-mem-optimization\",\n\tshortName:   \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   func(o options) (options, error) { o.noEtcHosts = true; return o, nil },\n\teffect: func(_ options, _ string) (f effect, err error) {\n\t\tlog.Info(\n\t\t\t\"warning: --no-etc-hosts flag is deprecated \" +\n\t\t\t\t\"and will be removed in the future versions; \" +\n\t\t\t\t\"set clients.runtime_sources.hosts and dns.hostsfile_enabled \" +\n\t\t\t\t\"in the configuration file to false instead\",\n\t\t)\n\n\t\treturn nil, nil\n\t},\n\tserialize: func(o options) (val string, ok bool) { return \"\", o.noEtcHosts },\n\tdescription: \"Deprecated: use clients.runtime_sources.hosts and dns.hostsfile_enabled \" +\n\t\t\"instead.  Do not use the OS-provided hosts.\",\n\tlongName:  \"no-etc-hosts\",\n\tshortName: \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   func(o options) (options, error) { o.localFrontend = true; return o, nil },\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return \"\", o.localFrontend },\n\tdescription:     \"Use local frontend directories.\",\n\tlongName:        \"local-frontend\",\n\tshortName:       \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   func(o options) (options, error) { o.verbose = true; return o, nil },\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return \"\", o.verbose },\n\tdescription:     \"Enable verbose output.\",\n\tlongName:        \"verbose\",\n\tshortName:       \"v\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   func(o options) (options, error) { o.glinetMode = true; return o, nil },\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return \"\", o.glinetMode },\n\tdescription:     \"Run in GL-Inet compatibility mode.\",\n\tlongName:        \"glinet\",\n\tshortName:       \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   func(o options) (options, error) { o.noPermCheck = true; return o, nil },\n\teffect:          nil,\n\tserialize:       func(o options) (val string, ok bool) { return \"\", o.noPermCheck },\n\tdescription: \"Skip checking and migration of permissions \" +\n\t\t\"of security-sensitive files.\",\n\tlongName:  \"no-permcheck\",\n\tshortName: \"\",\n}, {\n\tupdateWithValue: nil,\n\tupdateNoValue:   nil,\n\teffect: func(o options, exec string) (effect, error) {\n\t\treturn func() error {\n\t\t\tif o.verbose {\n\t\t\t\tfmt.Print(version.Verbose(configmigrate.LastSchemaVersion))\n\t\t\t} else {\n\t\t\t\tfmt.Println(version.Full())\n\t\t\t}\n\n\t\t\tos.Exit(osutil.ExitCodeSuccess)\n\n\t\t\treturn nil\n\t\t}, nil\n\t},\n\tserialize:   func(o options) (val string, ok bool) { return \"\", false },\n\tdescription: \"Show the version and exit.  Show more detailed version description with -v.\",\n\tlongName:    \"version\",\n\tshortName:   \"\",\n}}\n\n// printHelp prints the entire help message.  It exits with an error code if\n// there are any I/O errors.\nfunc printHelp(exec string) {\n\tb := &strings.Builder{}\n\n\tstringutil.WriteToBuilder(\n\t\tb,\n\t\t\"Usage:\\n\\n\",\n\t\tfmt.Sprintf(\"%s [options]\\n\\n\", exec),\n\t\t\"Options:\\n\",\n\t)\n\n\tvar err error\n\tfor _, opt := range cmdLineOpts {\n\t\tval := \"\"\n\t\tif opt.updateWithValue != nil {\n\t\t\tval = \" VALUE\"\n\t\t}\n\n\t\tlongDesc := opt.longName + val\n\t\tif opt.shortName != \"\" {\n\t\t\t_, err = fmt.Fprintf(b, \"  -%s, --%-28s %s\\n\", opt.shortName, longDesc, opt.description)\n\t\t} else {\n\t\t\t_, err = fmt.Fprintf(b, \"  --%-32s %s\\n\", longDesc, opt.description)\n\t\t}\n\n\t\tif err != nil {\n\t\t\t// The only error here can be from incorrect Fprintf usage, which is\n\t\t\t// a programmer error.\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t_, err = fmt.Print(b)\n\tif err != nil {\n\t\t// Exit immediately, since not being able to print out a help message\n\t\t// essentially means that the I/O is very broken at the moment.\n\t\texitWithError()\n\t}\n}\n\n// parseCmdOpts parses the command-line arguments into options and effects.\nfunc parseCmdOpts(cmdName string, args []string) (o options, eff effect, err error) {\n\tnext, stop := iter.Pull2(slices.All(args))\n\tdefer stop()\n\n\tfor i, arg, ok := next(); ok; i, arg, ok = next() {\n\t\to, eff, err = parseArg(cmdName, next, o, eff, arg)\n\t\tif err != nil {\n\t\t\treturn o, eff, fmt.Errorf(\"parsing arg at index %d: %w\", i, err)\n\t\t}\n\t}\n\n\treturn o, eff, nil\n}\n\n// parseArg parses command-line argument into options and effects.  next and\n// eff must not be nil.\nfunc parseArg(\n\tcmdName string,\n\tnext func() (int, string, bool),\n\to options,\n\teff effect,\n\targ string,\n) (newOpt options, newEff effect, err error) {\n\topt, found := findMatchingOpt(arg)\n\tif !found {\n\t\treturn o, eff, fmt.Errorf(\"unknown option %s\", arg)\n\t}\n\n\tif opt.updateWithValue != nil {\n\t\treturn applyOptWithValue(opt, next, o, eff, arg)\n\t}\n\n\treturn applyOptNoValue(opt, cmdName, o, eff, arg)\n}\n\n// applyOptNoValue applies option with no value.  eff must not be\n// nil.\nfunc applyOptNoValue(\n\topt cmdLineOpt,\n\tcmdName string,\n\to options,\n\teff effect,\n\targ string,\n) (newOpt options, newEff effect, err error) {\n\tnewOpts, newEff, err := updateOptsNoValue(o, eff, opt, cmdName)\n\tif err != nil {\n\t\treturn o, eff, fmt.Errorf(\"applying option %s: %w\", arg, err)\n\t}\n\n\treturn newOpts, newEff, nil\n}\n\n// applyOptWithValue applies argument with value.   next and eff must not\n// be nil.\nfunc applyOptWithValue(\n\topt cmdLineOpt,\n\tnext func() (int, string, bool),\n\to options,\n\teff effect,\n\targ string,\n) (newOpt options, newEff effect, err error) {\n\t_, val, ok := next()\n\tif !ok {\n\t\treturn o, eff, fmt.Errorf(\"got %s without argument\", arg)\n\t}\n\n\tnewOpts, err := opt.updateWithValue(o, val)\n\tif err != nil {\n\t\treturn o, eff, fmt.Errorf(\"applying option %s: %w\", arg, err)\n\t}\n\n\treturn newOpts, eff, nil\n}\n\n// findMatchingOpt returns cmdLineOpt which matches the given arg.  ok indicates\n// whether the appropriate option was found.\nfunc findMatchingOpt(arg string) (opt cmdLineOpt, ok bool) {\n\tfor _, opt := range cmdLineOpts {\n\t\tif argMatches(opt, arg) {\n\t\t\treturn opt, true\n\t\t}\n\t}\n\n\treturn cmdLineOpt{}, false\n}\n\n// argMatches returns true if arg matches command-line option opt.\nfunc argMatches(opt cmdLineOpt, arg string) (ok bool) {\n\tif arg == \"\" || arg[0] != '-' {\n\t\treturn false\n\t}\n\n\targ = arg[1:]\n\tif arg == \"\" {\n\t\treturn false\n\t}\n\n\treturn (opt.shortName != \"\" && arg == opt.shortName) ||\n\t\t(arg[0] == '-' && arg[1:] == opt.longName)\n}\n\n// updateOptsNoValue sets values or effects from opt into o or prev.\nfunc updateOptsNoValue(\n\to options,\n\tprev effect,\n\topt cmdLineOpt,\n\tcmdName string,\n) (updated options, chained effect, err error) {\n\tif opt.updateNoValue != nil {\n\t\to, err = opt.updateNoValue(o)\n\t\tif err != nil {\n\t\t\treturn o, prev, err\n\t\t}\n\n\t\treturn o, prev, nil\n\t}\n\n\tnext, err := opt.effect(o, cmdName)\n\tif err != nil {\n\t\treturn o, prev, err\n\t}\n\n\tchained = chainEffect(prev, next)\n\n\treturn o, chained, nil\n}\n\n// chainEffect chans the next effect after the prev one.  If prev is nil, eff\n// only calls next.  If next is nil, eff is prev; if prev is nil, eff is next.\nfunc chainEffect(prev, next effect) (eff effect) {\n\tif prev == nil {\n\t\treturn next\n\t} else if next == nil {\n\t\treturn prev\n\t}\n\n\teff = func() (err error) {\n\t\terr = prev()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn next()\n\t}\n\n\treturn eff\n}\n\n// optsToArgs converts command line options into a list of arguments.\nfunc optsToArgs(o options) (args []string) {\n\tfor _, opt := range cmdLineOpts {\n\t\tval, ok := opt.serialize(o)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif opt.shortName != \"\" {\n\t\t\targs = append(args, \"-\"+opt.shortName)\n\t\t} else {\n\t\t\targs = append(args, \"--\"+opt.longName)\n\t\t}\n\n\t\tif val != \"\" {\n\t\t\targs = append(args, val)\n\t\t}\n\t}\n\n\treturn args\n}\n"
  },
  {
    "path": "internal/home/options_internal_test.go",
    "content": "package home\n\nimport (\n\t\"fmt\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testParseOK is a helper that parses the command-line options and returns the\n// parsed options.\nfunc testParseOK(tb testing.TB, ss ...string) (o options) {\n\ttb.Helper()\n\n\to, _, err := parseCmdOpts(\"\", ss)\n\trequire.NoError(tb, err)\n\n\treturn o\n}\n\n// testParseErr is a helper that asserts that parsing the command-line options\n// fails with error.\n//\n// TODO(a.garipov):  Search descr within an error.\nfunc testParseErr(tb testing.TB, descr string, ss ...string) {\n\ttb.Helper()\n\n\t_, _, err := parseCmdOpts(\"\", ss)\n\trequire.Errorf(tb, err, \"should have got error: %s\", descr)\n}\n\n// testParseParamMissing is a helper that asserts that parsing the command-line\n// options fails with error due to missing parameter.\nfunc testParseParamMissing(tb testing.TB, param string) {\n\ttb.Helper()\n\n\ttestParseErr(tb, fmt.Sprintf(\"%s parameter missing\", param), param)\n}\n\nfunc TestParseVerbose(t *testing.T) {\n\tassert.False(t, testParseOK(t).verbose, \"empty is not verbose\")\n\tassert.True(t, testParseOK(t, \"-v\").verbose, \"-v is verbose\")\n\tassert.True(t, testParseOK(t, \"--verbose\").verbose, \"--verbose is verbose\")\n}\n\nfunc TestParseConfigFilename(t *testing.T) {\n\tassert.Equal(t, \"\", testParseOK(t).confFilename, \"empty is no config filename\")\n\tassert.Equal(t, \"path\", testParseOK(t, \"-c\", \"path\").confFilename, \"-c is config filename\")\n\ttestParseParamMissing(t, \"-c\")\n\n\tassert.Equal(t, \"path\", testParseOK(t, \"--config\", \"path\").confFilename, \"--config is config filename\")\n\ttestParseParamMissing(t, \"--config\")\n}\n\nfunc TestParseWorkDir(t *testing.T) {\n\tassert.Equal(t, \"\", testParseOK(t).workDir, \"empty is no work dir\")\n\tassert.Equal(t, \"path\", testParseOK(t, \"-w\", \"path\").workDir, \"-w is work dir\")\n\ttestParseParamMissing(t, \"-w\")\n\n\tassert.Equal(t, \"path\", testParseOK(t, \"--work-dir\", \"path\").workDir, \"--work-dir is work dir\")\n\ttestParseParamMissing(t, \"--work-dir\")\n}\n\nfunc TestParseBindHost(t *testing.T) {\n\twantAddr := netip.MustParseAddr(\"1.2.3.4\")\n\n\tassert.Zero(t, testParseOK(t).bindHost, \"empty is not host\")\n\tassert.Equal(t, wantAddr, testParseOK(t, \"-h\", \"1.2.3.4\").bindHost, \"-h is host\")\n\ttestParseParamMissing(t, \"-h\")\n\n\tassert.Equal(t, wantAddr, testParseOK(t, \"--host\", \"1.2.3.4\").bindHost, \"--host is host\")\n\ttestParseParamMissing(t, \"--host\")\n}\n\nfunc TestParseBindPort(t *testing.T) {\n\tassert.Equal(t, uint16(0), testParseOK(t).bindPort, \"empty is port 0\")\n\tassert.Equal(t, uint16(65535), testParseOK(t, \"-p\", \"65535\").bindPort, \"-p is port\")\n\ttestParseParamMissing(t, \"-p\")\n\n\tassert.Equal(t, uint16(65535), testParseOK(t, \"--port\", \"65535\").bindPort, \"--port is port\")\n\ttestParseParamMissing(t, \"--port\")\n\n\ttestParseErr(t, \"not an int\", \"-p\", \"x\")\n\ttestParseErr(t, \"hex not supported\", \"-p\", \"0x100\")\n\ttestParseErr(t, \"port negative\", \"-p\", \"-1\")\n\ttestParseErr(t, \"port too high\", \"-p\", \"65536\")\n\ttestParseErr(t, \"port too high\", \"-p\", \"4294967297\")           // 2^32 + 1\n\ttestParseErr(t, \"port too high\", \"-p\", \"18446744073709551617\") // 2^64 + 1\n}\n\nfunc TestParseBindAddr(t *testing.T) {\n\twantAddrPort := netip.MustParseAddrPort(\"1.2.3.4:8089\")\n\n\tassert.Zero(t, testParseOK(t).bindAddr, \"empty is not web-addr\")\n\n\tassert.Equal(t, wantAddrPort, testParseOK(t, \"--web-addr\", \"1.2.3.4:8089\").bindAddr)\n\tassert.Equal(t, netip.MustParseAddrPort(\"1.2.3.4:0\"), testParseOK(t, \"--web-addr\", \"1.2.3.4:0\").bindAddr)\n\ttestParseParamMissing(t, \"-web-addr\")\n\n\ttestParseErr(t, \"not an int\", \"--web-addr\", \"1.2.3.4:x\")\n\ttestParseErr(t, \"hex not supported\", \"--web-addr\", \"1.2.3.4:0x100\")\n\ttestParseErr(t, \"port negative\", \"--web-addr\", \"1.2.3.4:-1\")\n\ttestParseErr(t, \"port too high\", \"--web-addr\", \"1.2.3.4:65536\")\n\ttestParseErr(t, \"port too high\", \"--web-addr\", \"1.2.3.4:4294967297\")           // 2^32 + 1\n\ttestParseErr(t, \"port too high\", \"--web-addr\", \"1.2.3.4:18446744073709551617\") // 2^64 + 1\n}\n\nfunc TestParseLogfile(t *testing.T) {\n\tassert.Equal(t, \"\", testParseOK(t).logFile, \"empty is no log file\")\n\tassert.Equal(t, \"path\", testParseOK(t, \"-l\", \"path\").logFile, \"-l is log file\")\n\tassert.Equal(t, \"path\", testParseOK(t, \"--logfile\", \"path\").logFile, \"--logfile is log file\")\n}\n\nfunc TestParsePidfile(t *testing.T) {\n\tassert.Equal(t, \"\", testParseOK(t).pidFile, \"empty is no pid file\")\n\tassert.Equal(t, \"path\", testParseOK(t, \"--pidfile\", \"path\").pidFile, \"--pidfile is pid file\")\n}\n\nfunc TestParseCheckConfig(t *testing.T) {\n\tassert.False(t, testParseOK(t).checkConfig, \"empty is not check config\")\n\tassert.True(t, testParseOK(t, \"--check-config\").checkConfig, \"--check-config is check config\")\n}\n\nfunc TestParseDisableUpdate(t *testing.T) {\n\tassert.False(t, testParseOK(t).disableUpdate, \"empty is not disable update\")\n\tassert.True(t, testParseOK(t, \"--no-check-update\").disableUpdate, \"--no-check-update is disable update\")\n}\n\nfunc TestParsePerformUpdate(t *testing.T) {\n\tassert.False(t, testParseOK(t).performUpdate, \"empty is not perform update\")\n\tassert.True(t, testParseOK(t, \"--update\").performUpdate, \"--update is perform update\")\n}\n\n// TODO(e.burkov):  Remove after v0.108.0.\nfunc TestParseDisableMemoryOptimization(t *testing.T) {\n\to, eff, err := parseCmdOpts(\"\", []string{\"--no-mem-optimization\"})\n\trequire.NoError(t, err)\n\n\tassert.Nil(t, eff)\n\tassert.Zero(t, o)\n}\n\nfunc TestParseService(t *testing.T) {\n\tassert.Equal(t, \"\", testParseOK(t).serviceControlAction, \"empty is not service cmd\")\n\tassert.Equal(t, \"cmd\", testParseOK(t, \"-s\", \"cmd\").serviceControlAction, \"-s is service cmd\")\n\tassert.Equal(t, \"cmd\", testParseOK(t, \"--service\", \"cmd\").serviceControlAction, \"--service is service cmd\")\n}\n\nfunc TestParseGLInet(t *testing.T) {\n\tassert.False(t, testParseOK(t).glinetMode, \"empty is not GL-Inet mode\")\n\tassert.True(t, testParseOK(t, \"--glinet\").glinetMode, \"--glinet is GL-Inet mode\")\n}\n\nfunc TestParseUnknown(t *testing.T) {\n\ttestParseErr(t, \"unknown word\", \"x\")\n\ttestParseErr(t, \"unknown short\", \"-x\")\n\ttestParseErr(t, \"unknown long\", \"--x\")\n\ttestParseErr(t, \"unknown triple\", \"---x\")\n\ttestParseErr(t, \"unknown plus\", \"+x\")\n\ttestParseErr(t, \"unknown dash\", \"-\")\n}\n\nfunc TestOptsToArgs(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\targs []string\n\t\topts options\n\t}{{\n\t\tname: \"empty\",\n\t\targs: []string{},\n\t\topts: options{},\n\t}, {\n\t\tname: \"config_filename\",\n\t\targs: []string{\"-c\", \"path\"},\n\t\topts: options{confFilename: \"path\"},\n\t}, {\n\t\tname: \"work_dir\",\n\t\targs: []string{\"-w\", \"path\"},\n\t\topts: options{workDir: \"path\"},\n\t}, {\n\t\tname: \"bind_host\",\n\t\topts: options{bindHost: netip.MustParseAddr(\"1.2.3.4\")},\n\t\targs: []string{\"-h\", \"1.2.3.4\"},\n\t}, {\n\t\tname: \"bind_port\",\n\t\targs: []string{\"-p\", \"666\"},\n\t\topts: options{bindPort: 666},\n\t}, {\n\t\tname: \"web-addr\",\n\t\targs: []string{\"--web-addr\", \"1.2.3.4:8080\"},\n\t\topts: options{bindAddr: netip.MustParseAddrPort(\"1.2.3.4:8080\")},\n\t}, {\n\t\tname: \"log_file\",\n\t\targs: []string{\"-l\", \"path\"},\n\t\topts: options{logFile: \"path\"},\n\t}, {\n\t\tname: \"pid_file\",\n\t\targs: []string{\"--pidfile\", \"path\"},\n\t\topts: options{pidFile: \"path\"},\n\t}, {\n\t\tname: \"disable_update\",\n\t\targs: []string{\"--no-check-update\"},\n\t\topts: options{disableUpdate: true},\n\t}, {\n\t\tname: \"perform_update\",\n\t\targs: []string{\"--update\"},\n\t\topts: options{performUpdate: true},\n\t}, {\n\t\tname: \"control_action\",\n\t\targs: []string{\"-s\", \"run\"},\n\t\topts: options{serviceControlAction: \"run\"},\n\t}, {\n\t\tname: \"glinet_mode\",\n\t\targs: []string{\"--glinet\"},\n\t\topts: options{glinetMode: true},\n\t}, {\n\t\tname: \"multiple\",\n\t\targs: []string{\n\t\t\t\"-c\", \"config\",\n\t\t\t\"-w\", \"work\",\n\t\t\t\"-s\", \"run\",\n\t\t\t\"--pidfile\", \"pid\",\n\t\t\t\"--no-check-update\",\n\t\t},\n\t\topts: options{\n\t\t\tserviceControlAction: \"run\",\n\t\t\tconfFilename:         \"config\",\n\t\t\tworkDir:              \"work\",\n\t\t\tpidFile:              \"pid\",\n\t\t\tdisableUpdate:        true,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := optsToArgs(tc.opts)\n\t\t\tassert.ElementsMatch(t, tc.args, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/home/profilehttp.go",
    "content": "package home\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n)\n\n// Theme is an enum of all allowed UI themes.\ntype Theme string\n\n// Allowed [Theme] values.\n//\n// Keep in sync with client/src/helpers/constants.ts.\nconst (\n\tThemeAuto  Theme = \"auto\"\n\tThemeLight Theme = \"light\"\n\tThemeDark  Theme = \"dark\"\n)\n\n// UnmarshalText implements [encoding.TextUnmarshaler] interface for *Theme.\nfunc (t *Theme) UnmarshalText(b []byte) (err error) {\n\tswitch string(b) {\n\tcase \"auto\":\n\t\t*t = ThemeAuto\n\tcase \"dark\":\n\t\t*t = ThemeDark\n\tcase \"light\":\n\t\t*t = ThemeLight\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid theme %q, supported: %q, %q, %q\", b, ThemeAuto, ThemeDark, ThemeLight)\n\t}\n\n\treturn nil\n}\n\n// profileJSON is an object for /control/profile and /control/profile/update\n// endpoints.\ntype profileJSON struct {\n\tName     string `json:\"name\"`\n\tLanguage string `json:\"language\"`\n\tTheme    Theme  `json:\"theme\"`\n}\n\n// handleGetProfile is the handler for GET /control/profile endpoint.\nfunc (web *webAPI) handleGetProfile(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tvar name string\n\n\tif !web.auth.isGLiNet && !web.auth.isUserless {\n\t\tu, ok := webUserFromContext(ctx)\n\t\tif !ok {\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\n\t\t\treturn\n\t\t}\n\n\t\tname = string(u.Login)\n\t}\n\n\tvar resp profileJSON\n\tfunc() {\n\t\tconfig.RLock()\n\t\tdefer config.RUnlock()\n\n\t\tresp = profileJSON{\n\t\t\tName:     name,\n\t\t\tLanguage: config.Language,\n\t\t\tTheme:    config.Theme,\n\t\t}\n\t}()\n\n\taghhttp.WriteJSONResponseOK(ctx, web.logger, w, r, resp)\n}\n\n// handlePutProfile is the handler for PUT /control/profile/update endpoint.\nfunc (web *webAPI) handlePutProfile(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := web.logger\n\n\tif aghhttp.WriteTextPlainDeprecated(ctx, l, w, r) {\n\t\treturn\n\t}\n\n\tprofileReq := &profileJSON{}\n\terr := json.NewDecoder(r.Body).Decode(profileReq)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"reading req: %s\", err)\n\n\t\treturn\n\t}\n\n\tlang := profileReq.Language\n\tif !allowedLanguages.Has(lang) {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"unknown language: %q\", lang)\n\n\t\treturn\n\t}\n\n\ttheme := profileReq.Theme\n\n\tchanged := false\n\tfunc() {\n\t\tconfig.Lock()\n\t\tdefer config.Unlock()\n\n\t\tif config.Language == lang && config.Theme == theme {\n\t\t\tl.DebugContext(ctx, \"updating profile; no changes\")\n\n\t\t\treturn\n\t\t}\n\n\t\tchanged = true\n\t\tconfig.Language = lang\n\t\tconfig.Theme = theme\n\t\tl.InfoContext(ctx, \"profile updated\", \"lang\", lang, \"theme\", theme)\n\t}()\n\n\tif changed {\n\t\tweb.confModifier.Apply(ctx)\n\t}\n\n\taghhttp.OK(ctx, l, w)\n}\n"
  },
  {
    "path": "internal/home/profilehttp_internal_test.go",
    "content": "package home\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nfunc TestWeb_HandleGetProfile(t *testing.T) {\n\tstoreGlobals(t)\n\n\tconst (\n\t\ttestTTL = 60\n\n\t\tglTokenFileSuffix = \"test\"\n\n\t\tuserName     = \"name\"\n\t\tuserPassword = \"password\"\n\n\t\tpath = \"/control/profile\"\n\t)\n\n\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)\n\trequire.NoError(t, err)\n\n\ttempDir := t.TempDir()\n\tglFilePrefix = tempDir + \"/gl_token_\"\n\tglTokenFile := glFilePrefix + glTokenFileSuffix\n\n\tglFileData := make([]byte, 4)\n\tbinary.NativeEndian.PutUint32(glFileData, uint32(time.Now().Unix()+testTTL))\n\n\terr = os.WriteFile(glTokenFile, glFileData, 0o644)\n\trequire.NoError(t, err)\n\n\tsessionsDB := filepath.Join(tempDir, \"sessions.db\")\n\n\tuser := &webUser{\n\t\tName:         userName,\n\t\tPasswordHash: string(passwordHash),\n\t}\n\n\tauth, err := newAuth(testutil.ContextWithTimeout(t, testTimeout), &authConfig{\n\t\tbaseLogger:     testLogger,\n\t\trateLimiter:    emptyRateLimiter{},\n\t\ttrustedProxies: testTrustedProxies,\n\t\tdbFilename:     sessionsDB,\n\t\tusers:          nil,\n\t\tsessionTTL:     testTTL * time.Second,\n\t\tisGLiNet:       false,\n\t})\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() { auth.close(testutil.ContextWithTimeout(t, testTimeout)) })\n\n\tbaseMux := http.NewServeMux()\n\n\ttlsMgr, err := newTLSManager(testutil.ContextWithTimeout(t, testTimeout), &tlsManagerConfig{\n\t\tlogger:       testLogger,\n\t\tconfModifier: agh.EmptyConfigModifier{},\n\t\tmanager:      aghtls.EmptyManager{},\n\t})\n\trequire.NoError(t, err)\n\n\tweb := newTestWeb(t, &webConfig{\n\t\ttlsManager: tlsMgr,\n\t\tauth:       auth,\n\t\tmux:        baseMux,\n\t})\n\trequire.NoError(t, err)\n\n\tglobalContext.web = web\n\n\tmux := auth.middleware().Wrap(baseMux)\n\n\trequire.True(t, t.Run(\"userless\", func(t *testing.T) {\n\t\tw := httptest.NewRecorder()\n\t\tr := httptest.NewRequest(http.MethodGet, path, nil)\n\n\t\tweb.handleGetProfile(w, r)\n\t\tassert.Equal(t, http.StatusOK, w.Code)\n\t}))\n\n\trequire.True(t, t.Run(\"add_user\", func(t *testing.T) {\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\terr = auth.addUser(ctx, user, userPassword)\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\t\tr := httptest.NewRequest(http.MethodGet, path, nil)\n\n\t\tweb.handleGetProfile(w, r)\n\t\tassert.Equal(t, http.StatusUnauthorized, w.Code)\n\n\t\tw = httptest.NewRecorder()\n\t\tr = httptest.NewRequest(http.MethodGet, path, nil)\n\n\t\tloginCookie := generateAuthCookie(t, mux, userName, userPassword)\n\t\tr.AddCookie(loginCookie)\n\n\t\tweb.handleGetProfile(w, r)\n\t\tassert.Equal(t, http.StatusUnauthorized, w.Code)\n\t}))\n}\n\nfunc TestWeb_HandlePutProfile(t *testing.T) {\n\tstoreGlobals(t)\n\n\tmw := &webMw{}\n\tmux := http.NewServeMux()\n\thttpReg := aghhttp.NewDefaultRegistrar(mux, mw.wrap)\n\n\tisConfigChanged := false\n\tconfModifier := &aghtest.ConfigModifier{\n\t\tOnApply: func(_ context.Context) { isConfigChanged = true },\n\t}\n\n\tweb := newTestWeb(t, &webConfig{\n\t\tmux:            mux,\n\t\tconfigModifier: confModifier,\n\t\thttpReg:        httpReg,\n\t})\n\n\tglobalContext.web = web\n\tmw.set(web)\n\n\tvar (\n\t\tdataValid = errors.Must(json.Marshal(&profileJSON{\n\t\t\tLanguage: \"en\",\n\t\t\tTheme:    \"auto\",\n\t\t}))\n\n\t\tdataInvalidLang = errors.Must(json.Marshal(&profileJSON{\n\t\t\tLanguage: \"invalid_lang\",\n\t\t\tTheme:    \"auto\",\n\t\t}))\n\n\t\tdataInvalidTheme = errors.Must(json.Marshal(&profileJSON{\n\t\t\tLanguage: \"en\",\n\t\t\tTheme:    \"invalid_theme\",\n\t\t}))\n\t)\n\n\ttestCases := []struct {\n\t\treq      *http.Request\n\t\tname     string\n\t\twantBody string\n\t\twantCode int\n\t}{{\n\t\treq:      newProfileUpdateRequest(http.MethodPut, dataValid, true),\n\t\tname:     \"basic\",\n\t\twantBody: \"OK\\n\",\n\t\twantCode: http.StatusOK,\n\t}, {\n\t\treq:      newProfileUpdateRequest(http.MethodGet, dataValid, true),\n\t\tname:     \"invalid_method\",\n\t\twantBody: \"only method PUT is allowed\\n\",\n\t\twantCode: http.StatusMethodNotAllowed,\n\t}, {\n\t\treq:      newProfileUpdateRequest(http.MethodPut, dataValid, false),\n\t\tname:     \"invalid_content_type\",\n\t\twantBody: \"only content-type application/json is allowed\\n\",\n\t\twantCode: http.StatusUnsupportedMediaType,\n\t}, {\n\t\treq:      newProfileUpdateRequest(http.MethodPut, nil, false),\n\t\tname:     \"empty_body\",\n\t\twantBody: \"reading req: EOF\\n\",\n\t\twantCode: http.StatusBadRequest,\n\t}, {\n\t\treq:      newProfileUpdateRequest(http.MethodPut, dataInvalidLang, true),\n\t\tname:     \"invalid_language\",\n\t\twantBody: `unknown language: \"invalid_lang\"` + \"\\n\",\n\t\twantCode: http.StatusBadRequest,\n\t}, {\n\t\treq:  newProfileUpdateRequest(http.MethodPut, dataInvalidTheme, true),\n\t\tname: \"invalid_theme\",\n\t\twantBody: `reading req: invalid theme \"invalid_theme\", ` +\n\t\t\t`supported: \"auto\", \"dark\", \"light\"` + \"\\n\",\n\t\twantCode: http.StatusBadRequest,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tmux.ServeHTTP(w, tc.req)\n\t\t\tassert.Equal(t, tc.wantCode, w.Code)\n\t\t\tassert.Equal(t, tc.wantBody, w.Body.String())\n\t\t})\n\t}\n\n\trequire.True(t, t.Run(\"single_config_update\", func(t *testing.T) {\n\t\tisConfigChanged = false\n\t\tconfig.Language = \"\"\n\t\tconfig.Theme = \"\"\n\n\t\tw := httptest.NewRecorder()\n\n\t\tmux.ServeHTTP(w, newProfileUpdateRequest(http.MethodPut, dataValid, true))\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tassert.True(t, isConfigChanged)\n\n\t\tisConfigChanged = false\n\n\t\tmux.ServeHTTP(w, newProfileUpdateRequest(http.MethodPut, dataValid, true))\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t\tassert.False(t, isConfigChanged)\n\t}))\n}\n\n// newProfileUpdateRequest builds an *http.Request for the profile update\n// endpoint.  If body is non-nil, it is used as the request body.  If setCT is\n// true, the Content-Type header is set to application/json.\nfunc newProfileUpdateRequest(method string, body []byte, setCT bool) (req *http.Request) {\n\tvar r io.Reader\n\tif body != nil {\n\t\tr = bytes.NewReader(body)\n\t}\n\n\treq = httptest.NewRequest(method, \"/control/profile/update\", r)\n\tif setCT {\n\t\treq.Header.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)\n\t}\n\n\treturn req\n}\n"
  },
  {
    "path": "internal/home/service.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/ossvc\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/kardianos/service\"\n)\n\nconst (\n\tserviceName        = \"AdGuardHome\"\n\tserviceDisplayName = \"AdGuard Home service\"\n\tserviceDescription = \"AdGuard Home: Network-level blocker\"\n)\n\n// svcLogPrefix is the prefix for logging from service manager.\nconst svcLogPrefix = \"service_manager\"\n\n// program represents the program that will be launched by as a service or a\n// daemon.\n//\n// TODO(e.burkov):  Handle the run action as a direct execution instead of\n// constructing a service instance and running it.  Perhaps, deprecate the\n// action.\ntype program struct {\n\tctx           context.Context\n\tclientBuildFS fs.FS\n\tsignals       chan os.Signal\n\tdone          chan struct{}\n\topts          options\n\tbaseLogger    *slog.Logger\n\tlogger        *slog.Logger\n\tsigHdlr       *signalHandler\n\tworkDir       string\n\tconfPath      string\n}\n\n// type check\nvar _ service.Interface = (*program)(nil)\n\n// Start implements service.Interface interface for *program.\nfunc (p *program) Start(_ service.Service) (err error) {\n\t// Start should not block.  Do the actual work async.\n\targs := p.opts\n\targs.runningAsService = true\n\n\tgo run(p.ctx, p.baseLogger, args, p.clientBuildFS, p.done, p.sigHdlr, p.workDir, p.confPath)\n\n\treturn nil\n}\n\n// Stop implements service.Interface interface for *program.\nfunc (p *program) Stop(_ service.Service) (err error) {\n\tp.logger.InfoContext(p.ctx, \"stopping: waiting for cleanup\")\n\n\taghos.SendShutdownSignal(p.signals)\n\n\t// Wait for other goroutines to complete their job.\n\t<-p.done\n\n\treturn nil\n}\n\n// handleRun runs p.\nfunc (p *program) handleRun(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\topts options,\n) (err error) {\n\tpwd, err := os.Getwd()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting current directory: %w\", err)\n\t}\n\n\targs := optsToArgs(opts)\n\tbaseLogger.DebugContext(ctx, \"using\", \"args\", args)\n\n\tsvcConfig := &service.Config{\n\t\tName:             serviceName,\n\t\tDisplayName:      serviceDisplayName,\n\t\tDescription:      serviceDescription,\n\t\tWorkingDirectory: pwd,\n\t\tArguments:        args,\n\t}\n\tossvc.ConfigureServiceOptions(svcConfig, version.Full())\n\n\ts, err := service.New(p, svcConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initializing service: %w\", err)\n\t}\n\n\treturn s.Run()\n}\n\n// restartService restarts the service.  It returns error if the service is not\n// running.  l must not be nil.\nfunc restartService(ctx context.Context, baseLogger *slog.Logger) (err error) {\n\tsvcMgr, err := ossvc.NewManager(ctx, &ossvc.ManagerConfig{\n\t\tLogger:             baseLogger.With(slogutil.KeyPrefix, svcLogPrefix),\n\t\tCommandConstructor: executil.SystemCommandConstructor{},\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initializing service manager: %w\", err)\n\t}\n\n\tact := &ossvc.ActionRestart{\n\t\tServiceName: serviceName,\n\t}\n\n\terr = svcMgr.Perform(ctx, act)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"restarting service: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// handleServiceControlAction one of the possible control actions:\n//\n//   - install:  Installs a service/daemon.\n//   - uninstall:  Uninstalls it.\n//   - status:  Prints the service status.\n//   - start:  Starts the previously installed service.\n//   - stop:  Stops the previously installed service.\n//   - restart:  Restarts the previously installed service.\n//   - run:  This is a special command that is not supposed to be used directly\n//     it is specified when we register a service, and it indicates to the app\n//     that it is being run as a service/daemon.\nfunc handleServiceControlAction(\n\tctx context.Context,\n\tbaseLogger *slog.Logger,\n\tl *slog.Logger,\n\topts options,\n\tclientBuildFS fs.FS,\n\tsignals chan os.Signal,\n\tdone chan struct{},\n\tsigHdlr *signalHandler,\n\tworkDir string,\n\tconfPath string,\n) (err error) {\n\tactionName := opts.serviceControlAction\n\tl.InfoContext(ctx, version.Full())\n\tl.InfoContext(ctx, \"control\", \"action\", actionName)\n\n\t// Create a service manager before even a run action, since it picks the\n\t// correct system implementation.\n\tsvcMgr, err := ossvc.NewManager(ctx, &ossvc.ManagerConfig{\n\t\tLogger:             baseLogger.With(slogutil.KeyPrefix, svcLogPrefix),\n\t\tCommandConstructor: executil.SystemCommandConstructor{},\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initializing service manager: %w\", err)\n\t}\n\n\tif actionName == \"run\" {\n\t\trunOpts := opts\n\t\trunOpts.serviceControlAction = \"run\"\n\n\t\tp := &program{\n\t\t\tctx:           ctx,\n\t\t\tclientBuildFS: clientBuildFS,\n\t\t\tsignals:       signals,\n\t\t\tdone:          done,\n\t\t\topts:          runOpts,\n\t\t\tbaseLogger:    baseLogger,\n\t\t\tlogger:        baseLogger.With(slogutil.KeyPrefix, \"service\"),\n\t\t\tsigHdlr:       sigHdlr,\n\t\t\tworkDir:       workDir,\n\t\t\tconfPath:      confPath,\n\t\t}\n\n\t\treturn p.handleRun(ctx, baseLogger, runOpts)\n\t}\n\n\tswitch actionName {\n\tcase \"reload\":\n\t\terr = handleServiceReloadCmd(ctx, l, svcMgr)\n\tcase \"status\":\n\t\terr = handleServiceStatusCmd(ctx, l, svcMgr)\n\tdefault:\n\t\terr = handleServiceCommand(ctx, baseLogger, svcMgr, opts, workDir, confPath)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"action %q: %w\", actionName, err)\n\t}\n\n\treturn nil\n}\n\n// handleServiceCommand handles service command.\nfunc handleServiceCommand(\n\tctx context.Context,\n\tl *slog.Logger,\n\tmgr ossvc.Manager,\n\topts options,\n\tworkDir string,\n\tconfPath string,\n) (err error) {\n\tvar action ossvc.Action\n\tswitch opts.serviceControlAction {\n\tcase \"install\":\n\t\treturn handleServiceInstallCmd(ctx, l, mgr, opts, workDir, confPath)\n\tcase \"uninstall\":\n\t\taction = &ossvc.ActionUninstall{\n\t\t\tServiceName: serviceName,\n\t\t}\n\tcase \"start\":\n\t\taction = &ossvc.ActionStart{\n\t\t\tServiceName: serviceName,\n\t\t}\n\tcase \"stop\":\n\t\taction = &ossvc.ActionStop{\n\t\t\tServiceName: serviceName,\n\t\t}\n\tcase \"restart\":\n\t\taction = &ossvc.ActionRestart{\n\t\t\tServiceName: serviceName,\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"%w: %q\", errors.ErrBadEnumValue, opts.serviceControlAction)\n\t}\n\n\treturn mgr.Perform(ctx, action)\n}\n\n// handleServiceStatusCmd logs the service's status.  l and mgr must not be\n// nil.\nfunc handleServiceStatusCmd(ctx context.Context, l *slog.Logger, mgr ossvc.Manager) (err error) {\n\tstatus, err := mgr.Status(ctx, serviceName)\n\tif err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tswitch status {\n\tcase ossvc.StatusNotInstalled:\n\t\tl.InfoContext(ctx, \"not installed\")\n\tcase ossvc.StatusStopped:\n\t\tl.InfoContext(ctx, \"stopped\")\n\tcase ossvc.StatusRunning:\n\t\tl.InfoContext(ctx, \"running\")\n\tcase ossvc.StatusRestartOnFail:\n\t\tl.InfoContext(ctx, \"restarting after failed start\")\n\t}\n\n\treturn nil\n}\n\n// handleServiceReloadCmd reloads the service, if it's running.  l must not be\n// nil, mgr must be a ReloadManager.\nfunc handleServiceReloadCmd(ctx context.Context, l *slog.Logger, mgr ossvc.Manager) (err error) {\n\trelSvcMgr, ok := mgr.(ossvc.ReloadManager)\n\tif !ok {\n\t\treturn fmt.Errorf(\"service manager can't reload: %w\", errors.ErrUnsupported)\n\t}\n\n\terr = relSvcMgr.Reload(ctx, serviceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"reloading service: %w\", err)\n\t}\n\n\tl.InfoContext(ctx, \"service reloaded successfully\")\n\n\treturn nil\n}\n\n// handleServiceInstallCmd handles the service \"install\" command.  l must\n// not be nil.\nfunc handleServiceInstallCmd(\n\tctx context.Context,\n\tl *slog.Logger,\n\tmgr ossvc.Manager,\n\topts options,\n\tworkDir string,\n\tconfPath string,\n) (err error) {\n\tpwd, err := os.Getwd()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting current directory: %w\", err)\n\t}\n\n\trunOpts := opts\n\trunOpts.serviceControlAction = \"run\"\n\n\targs := optsToArgs(runOpts)\n\tl.DebugContext(ctx, \"using\", \"args\", args)\n\n\terr = mgr.Perform(ctx, &ossvc.ActionInstall{\n\t\tServiceName:      serviceName,\n\t\tDisplayName:      serviceDisplayName,\n\t\tDescription:      serviceDescription,\n\t\tWorkingDirectory: pwd,\n\t\tVersion:          version.Full(),\n\t\tArguments:        args,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"installing service: %w\", err)\n\t}\n\n\terr = mgr.Perform(ctx, &ossvc.ActionStart{\n\t\tServiceName: serviceName,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting service: %w\", err)\n\t}\n\n\tif detectFirstRun(ctx, l, workDir, confPath) {\n\t\tslogutil.PrintLines(ctx, l, slog.LevelInfo, \"\", \"Almost ready!\\n\"+\n\t\t\t\"AdGuard Home is successfully installed and will automatically start on boot.\\n\"+\n\t\t\t\"There are a few more things that must be configured before you can use it.\\n\"+\n\t\t\t\"Click on the link below and follow the Installation Wizard steps to finish setup.\\n\"+\n\t\t\t\"AdGuard Home is now available at the following addresses:\")\n\t\tprintHTTPAddresses(urlutil.SchemeHTTP, nil)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/home/signal.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"os\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n)\n\n// signalHandler processes incoming signals.  It reloads configurations of\n// stored entities on SIGHUP and performs cleanup on all other signals.\n//\n// TODO(e.burkov):  Use [service.SignalHandler] instead.\ntype signalHandler struct {\n\t// logger is used to log the operation of the signal handler.\n\tlogger *slog.Logger\n\n\t// mu protects clientStorage and tlsManager.\n\tmu *sync.Mutex\n\n\t// clientStorage is used to reload information about runtime clients with an\n\t// ARP source.\n\tclientStorage *client.Storage\n\n\t// tlsManager is used to reload the TLS configuration.\n\ttlsManager aghtls.Manager\n\n\t// signals receives incoming signals.\n\tsignals <-chan os.Signal\n\n\t// cleanup is called to perform cleanup on all incoming signals, except\n\t// SIGHUP.\n\tcleanup func(ctx context.Context)\n}\n\n// newSignalHandler returns a new properly initialized *signalHandler.\nfunc newSignalHandler(\n\tl *slog.Logger,\n\tsignals <-chan os.Signal,\n\tcleanup func(ctx context.Context),\n) (h *signalHandler) {\n\treturn &signalHandler{\n\t\tlogger:  l,\n\t\tmu:      &sync.Mutex{},\n\t\tsignals: signals,\n\t\tcleanup: cleanup,\n\t}\n}\n\n// addClientStorage stores the client storage.\nfunc (h *signalHandler) addClientStorage(s *client.Storage) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\n\th.clientStorage = s\n}\n\n// addTLSManager stores the TLS manager.\nfunc (h *signalHandler) addTLSManager(m aghtls.Manager) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\n\th.tlsManager = m\n}\n\n// handle processes incoming signals.  It blocks until a signal is received.  It\n// reloads configurations of stored entities on SIGHUP, or performs cleanup on\n// all other signals.  It is intended to be used as a goroutine.\nfunc (h *signalHandler) handle(ctx context.Context) {\n\t// NOTE:  Avoid using [slogutil.RecoverAndExit] to prevent immediate\n\t// evaluation of the logger.\n\tdefer func() {\n\t\tv := recover()\n\t\tif v == nil {\n\t\t\treturn\n\t\t}\n\n\t\tslogutil.PrintRecovered(ctx, h.logger, v)\n\n\t\tos.Exit(osutil.ExitCodeFailure)\n\t}()\n\n\tfor {\n\t\tsig := <-h.signals\n\t\th.logger.InfoContext(ctx, \"received signal\", \"signal\", sig)\n\t\tswitch sig {\n\t\tcase syscall.SIGHUP:\n\t\t\th.reloadConfig(ctx)\n\t\tdefault:\n\t\t\th.shutdown(ctx)\n\t\t}\n\t}\n}\n\n// shutdown shuts the system down.\nfunc (h *signalHandler) shutdown(ctx context.Context) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\n\tif h.tlsManager != nil {\n\t\terr := h.tlsManager.Shutdown(ctx)\n\t\tif err != nil {\n\t\t\th.logger.ErrorContext(ctx, \"shutting down tls manager\", slogutil.KeyError, err)\n\t\t}\n\t}\n\n\th.cleanup(ctx)\n}\n\n// reloadConfig refreshes configurations of stored entities.\nfunc (h *signalHandler) reloadConfig(ctx context.Context) {\n\th.mu.Lock()\n\tdefer h.mu.Unlock()\n\n\tif h.clientStorage != nil {\n\t\th.clientStorage.ReloadARP(ctx)\n\t}\n\n\tif h.tlsManager != nil {\n\t\terr := h.tlsManager.Refresh(ctx)\n\t\tif err != nil {\n\t\t\th.logger.ErrorContext(ctx, \"refreshing tls manager\", slogutil.KeyError, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/home/tls.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\n// tlsManager contains the current configuration and state of AdGuard Home TLS\n// encryption.\ntype tlsManager struct {\n\t// logger is used for logging the operation of the TLS Manager.\n\tlogger *slog.Logger\n\n\t// mu protects status, certLastMod, conf, and servePlainDNS.\n\tmu *sync.Mutex\n\n\t// status is the current status of the configuration.  It is never nil.\n\tstatus *tlsConfigStatus\n\n\t// certLastMod is the last modification time of the certificate file.\n\tcertLastMod time.Time\n\n\t// rootCerts is a pool of root CAs for TLSv1.2.\n\trootCerts *x509.CertPool\n\n\t// web is the web UI and API server.  It must not be nil.\n\t//\n\t// TODO(s.chzhen):  Temporary cyclic dependency due to ongoing refactoring.\n\t// Resolve it.\n\tweb *webAPI\n\n\t// conf contains the TLS configuration settings.  It must not be nil.\n\tconf *tlsConfigSettings\n\n\t// confModifier is used to update the global configuration.\n\tconfModifier agh.ConfigModifier\n\n\t// httpReg registers HTTP handlers.  It must not be nil.\n\thttpReg aghhttp.Registrar\n\n\t// manager is used to manage the TLS certificate and key files.  It must not\n\t// be nil.\n\tmanager aghtls.Manager\n\n\t// customCipherIDs are the IDs of the cipher suites that AdGuard Home must\n\t// use.\n\tcustomCipherIDs []uint16\n\n\t// servePlainDNS defines if plain DNS is allowed for incoming requests.\n\tservePlainDNS bool\n}\n\n// tlsManagerConfig contains the settings for initializing the TLS manager.\ntype tlsManagerConfig struct {\n\t// logger is used for logging the operation of the TLS Manager.  It must not\n\t// be nil.\n\tlogger *slog.Logger\n\n\t// confModifier is used to update the global configuration.  It must not be\n\t// nil.\n\tconfModifier agh.ConfigModifier\n\n\t// manager is used to manage the TLS certificate and key files.  It must not\n\t// be nil.\n\tmanager aghtls.Manager\n\n\thttpReg aghhttp.Registrar\n\n\t// tlsSettings contains the TLS configuration settings.\n\ttlsSettings tlsConfigSettings\n\n\t// servePlainDNS defines if plain DNS is allowed for incoming requests.\n\tservePlainDNS bool\n}\n\n// newTLSManager initializes the manager of TLS configuration.  m is always\n// non-nil while any returned error indicates that the TLS configuration isn't\n// valid.  Thus TLS may be initialized later, e.g. via the web UI.  conf must\n// not be nil.  Note that [tlsManager.web] must be initialized later on by using\n// [tlsManager.setWebAPI].\nfunc newTLSManager(ctx context.Context, conf *tlsManagerConfig) (m *tlsManager, err error) {\n\tm = &tlsManager{\n\t\tlogger:        conf.logger,\n\t\tmu:            &sync.Mutex{},\n\t\tconfModifier:  conf.confModifier,\n\t\thttpReg:       conf.httpReg,\n\t\tmanager:       conf.manager,\n\t\tstatus:        &tlsConfigStatus{},\n\t\tconf:          &conf.tlsSettings,\n\t\tservePlainDNS: conf.servePlainDNS,\n\t}\n\n\tm.rootCerts = aghtls.SystemRootCAs(ctx, conf.logger)\n\n\tif len(conf.tlsSettings.OverrideTLSCiphers) > 0 {\n\t\tm.customCipherIDs, err = aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)\n\t\tif err != nil {\n\t\t\t// Should not happen because upstreams are already validated.  See\n\t\t\t// [validateTLSCipherIDs].\n\t\t\tpanic(err)\n\t\t}\n\n\t\tm.logger.InfoContext(ctx, \"overriding ciphers\", \"ciphers\", config.TLS.OverrideTLSCiphers)\n\t} else {\n\t\tm.logger.InfoContext(ctx, \"using default ciphers\")\n\t}\n\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tif !m.conf.Enabled {\n\t\treturn m, nil\n\t}\n\n\terr = m.manager.Set(ctx, aghtls.TLSPair{\n\t\tCertPath: m.conf.CertificatePath,\n\t\tKeyPath:  m.conf.PrivateKeyPath,\n\t})\n\tif err != nil {\n\t\tm.logger.ErrorContext(ctx, \"setting tls files\", slogutil.KeyError, err)\n\t}\n\n\terr = m.loadTLSConfig(ctx, m.conf, m.status)\n\tif err != nil {\n\t\tm.conf.Enabled = false\n\n\t\treturn m, err\n\t}\n\n\tm.setCertFileTime(ctx)\n\n\treturn m, nil\n}\n\n// setWebAPI stores the provided web API.  It must be called before\n// [tlsManager.start], [tlsManager.reload], [tlsManager.handleTLSConfigure], or\n// [tlsManager.validateTLSSettings].\n//\n// TODO(s.chzhen):  Remove it once cyclic dependency is resolved.\nfunc (m *tlsManager) setWebAPI(webAPI *webAPI) {\n\tm.web = webAPI\n}\n\n// config returns a deep copy of the stored TLS configuration.\nfunc (m *tlsManager) config() (conf *tlsConfigSettings) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\treturn m.conf.clone()\n}\n\n// setCertFileTime sets [tlsManager.certLastMod] from the certificate.  If there\n// are errors, setCertFileTime logs them.  m.mu is expected to be locked.\nfunc (m *tlsManager) setCertFileTime(ctx context.Context) {\n\tif len(m.conf.CertificatePath) == 0 {\n\t\treturn\n\t}\n\n\tfi, err := os.Stat(m.conf.CertificatePath)\n\tif err != nil {\n\t\tm.logger.ErrorContext(ctx, \"looking up certificate path\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tm.certLastMod = fi.ModTime().UTC()\n}\n\n// start updates the configuration of t and starts it.\n//\n// TODO(s.chzhen):  Use context.\nfunc (m *tlsManager) start(ctx context.Context) {\n\tm.registerWebHandlers()\n\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\t// The background context is used because the TLSConfigChanged wraps context\n\t// with timeout on its own and shuts down the server, which handles current\n\t// request.\n\tm.web.tlsConfigChanged(context.Background(), m.conf)\n\n\tgo m.handleCertFileChange(ctx)\n}\n\n// handleCertFileChange handles changes in the certificate file.  It's intended\n// to be run as a goroutine.\nfunc (m *tlsManager) handleCertFileChange(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, m.logger)\n\n\tupdates := m.manager.Updates(ctx)\n\tif updates == nil {\n\t\tm.logger.ErrorContext(ctx, \"no updates channel\")\n\n\t\treturn\n\t}\n\n\tfor range updates {\n\t\tm.logger.DebugContext(ctx, \"reloading\")\n\n\t\tm.reload(ctx)\n\t}\n}\n\n// reload updates the configuration and restarts the TLS manager.  It logs any\n// encountered errors.\n//\n// TODO(s.chzhen):  Consider returning an error.\nfunc (m *tlsManager) reload(ctx context.Context) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\ttlsConfPtr := m.conf\n\n\tif !tlsConfPtr.Enabled || len(tlsConfPtr.CertificatePath) == 0 {\n\t\treturn\n\t}\n\n\tcertPath := tlsConfPtr.CertificatePath\n\tfi, err := os.Stat(certPath)\n\tif err != nil {\n\t\tm.logger.ErrorContext(ctx, \"checking certificate file\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tif fi.ModTime().UTC().Equal(m.certLastMod) {\n\t\tm.logger.InfoContext(ctx, \"certificate file is not modified\")\n\n\t\treturn\n\t}\n\n\tm.logger.InfoContext(ctx, \"certificate file is modified\")\n\n\ttlsConf := *tlsConfPtr\n\tstatus := &tlsConfigStatus{}\n\n\terr = m.loadTLSConfig(ctx, &tlsConf, status)\n\tif err != nil {\n\t\tm.logger.WarnContext(ctx, \"reloading interrupted\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tm.conf = &tlsConf\n\tm.status = status\n\n\tm.certLastMod = fi.ModTime().UTC()\n\n\terr = m.reconfigureDNSServer(ctx)\n\tif err != nil {\n\t\tm.logger.ErrorContext(ctx, \"reconfiguring dns server\", slogutil.KeyError, err)\n\t}\n\n\t// The background context is used because the TLSConfigChanged wraps context\n\t// with timeout on its own and shuts down the server, which handles current\n\t// request.\n\tm.web.tlsConfigChanged(context.Background(), m.conf)\n}\n\n// reconfigureDNSServer updates the DNS server configuration using the stored\n// TLS settings.  m.mu is expected to be locked.\nfunc (m *tlsManager) reconfigureDNSServer(ctx context.Context) (err error) {\n\tnewConf, err := newServerConfig(\n\t\t&config.DNS,\n\t\tconfig.Clients.Sources,\n\t\tm.conf,\n\t\tm,\n\t\tm.httpReg,\n\t\tglobalContext.clients.storage,\n\t\tm.confModifier,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"generating forwarding dns server config: %w\", err)\n\t}\n\n\terr = globalContext.dnsServer.Reconfigure(ctx, newConf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting forwarding dns server: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// loadTLSConfig loads and validates the TLS configuration.  It also sets\n// [tlsConfigSettings.CertificateChainData] and\n// [tlsConfigSettings.PrivateKeyData] properties.  The returned error is also\n// set in status.WarningValidation.\nfunc (m *tlsManager) loadTLSConfig(\n\tctx context.Context,\n\ttlsConf *tlsConfigSettings,\n\tstatus *tlsConfigStatus,\n) (err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tstatus.WarningValidation = err.Error()\n\t\t\tif status.ValidCert && status.ValidKey && status.ValidPair {\n\t\t\t\t// Do not return warnings since those aren't critical.\n\t\t\t\terr = nil\n\t\t\t}\n\t\t}\n\t}()\n\n\terr = loadCertificateChainData(tlsConf)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = loadPrivateKeyData(tlsConf)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = m.validateCertificates(\n\t\tctx,\n\t\tstatus,\n\t\ttlsConf.CertificateChainData,\n\t\ttlsConf.PrivateKeyData,\n\t\ttlsConf.ServerName,\n\t)\n\n\treturn errors.Annotate(err, \"validating certificate pair: %w\")\n}\n\n// loadCertificateChainData loads PEM-encoded certificates chain data to the\n// TLS configuration. tlsConf must be not nil. tlsConf.CertificateChainData\n// struct field will be modified in case tlsConfig.CertificatePath is not an\n// empty string.\nfunc loadCertificateChainData(tlsConf *tlsConfigSettings) (err error) {\n\ttlsConf.CertificateChainData = []byte(tlsConf.CertificateChain)\n\tif tlsConf.CertificatePath != \"\" {\n\t\tif tlsConf.CertificateChain != \"\" {\n\t\t\treturn errors.Error(\"certificate data and file can't be set together\")\n\t\t}\n\n\t\ttlsConf.CertificateChainData, err = os.ReadFile(tlsConf.CertificatePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"reading cert file: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// loadPrivateKeyData loads PEM-encoded private key data to the TLS\n// configuration. tlsConf must be not nil. tlsConf.PrivateKeyData struct field\n// will be modified in case tlsConfig.PrivateKeyPath is not an empty string.\nfunc loadPrivateKeyData(tlsConf *tlsConfigSettings) (err error) {\n\ttlsConf.PrivateKeyData = []byte(tlsConf.PrivateKey)\n\tif tlsConf.PrivateKeyPath != \"\" {\n\t\tif tlsConf.PrivateKey != \"\" {\n\t\t\treturn errors.Error(\"private key data and file can't be set together\")\n\t\t}\n\n\t\ttlsConf.PrivateKeyData, err = os.ReadFile(tlsConf.PrivateKeyPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"reading key file: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// tlsConfigStatus contains the status of a certificate chain and key pair.\ntype tlsConfigStatus struct {\n\t// Subject is the subject of the first certificate in the chain.\n\tSubject string `json:\"subject,omitempty\"`\n\n\t// Issuer is the issuer of the first certificate in the chain.\n\tIssuer string `json:\"issuer,omitempty\"`\n\n\t// KeyType is the type of the private key.\n\tKeyType string `json:\"key_type,omitempty\"`\n\n\t// NotBefore is the NotBefore field of the first certificate in the chain.\n\tNotBefore time.Time `json:\"not_before\"`\n\n\t// NotAfter is the NotAfter field of the first certificate in the chain.\n\tNotAfter time.Time `json:\"not_after\"`\n\n\t// WarningValidation is a validation warning message with the issue\n\t// description.\n\tWarningValidation string `json:\"warning_validation,omitempty\"`\n\n\t// DNSNames is the value of SubjectAltNames field of the first certificate\n\t// in the chain.\n\tDNSNames []string `json:\"dns_names\"`\n\n\t// ValidCert is true if the specified certificate chain is a valid chain of\n\t// X509 certificates.\n\tValidCert bool `json:\"valid_cert\"`\n\n\t// ValidChain is true if the specified certificate chain is verified and\n\t// issued by a known CA.\n\tValidChain bool `json:\"valid_chain\"`\n\n\t// ValidKey is true if the key is a valid private key.\n\tValidKey bool `json:\"valid_key\"`\n\n\t// ValidPair is true if both certificate and private key are correct for\n\t// each other.\n\tValidPair bool `json:\"valid_pair\"`\n}\n\n// tlsConfig is the TLS configuration and status response.\ntype tlsConfig struct {\n\t*tlsConfigStatus     `json:\",inline\"`\n\ttlsConfigSettingsExt `json:\",inline\"`\n}\n\n// tlsConfigSettingsExt is used to (un)marshal PrivateKeySaved field and\n// ServePlainDNS field.\ntype tlsConfigSettingsExt struct {\n\ttlsConfigSettings `json:\",inline\"`\n\n\t// PrivateKeySaved is true if the private key is saved as a string and omit\n\t// key from answer.  It is used to ensure that clients don't send and\n\t// receive previously saved private keys.\n\tPrivateKeySaved bool `yaml:\"-\" json:\"private_key_saved\"`\n\n\t// ServePlainDNS defines if plain DNS is allowed for incoming requests.  It\n\t// is an [aghalg.NullBool] to be able to tell when it's set without using\n\t// pointers.\n\tServePlainDNS aghalg.NullBool `yaml:\"-\" json:\"serve_plain_dns\"`\n}\n\n// handleTLSStatus is the handler for the GET /control/tls/status HTTP API.\nfunc (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {\n\tvar tlsConf *tlsConfigSettings\n\tvar servePlainDNS bool\n\tfunc() {\n\t\tm.mu.Lock()\n\t\tdefer m.mu.Unlock()\n\n\t\ttlsConf = m.conf.clone()\n\t\tservePlainDNS = m.servePlainDNS\n\t}()\n\n\tdata := &tlsConfig{\n\t\ttlsConfigSettingsExt: tlsConfigSettingsExt{\n\t\t\ttlsConfigSettings: *tlsConf,\n\t\t\tServePlainDNS:     aghalg.BoolToNullBool(servePlainDNS),\n\t\t},\n\t\ttlsConfigStatus: m.status,\n\t}\n\n\tm.marshalTLS(r.Context(), w, r, data)\n}\n\n// handleTLSValidate is the handler for the POST /control/tls/validate HTTP API.\nfunc (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tsetts, err := unmarshalTLS(r)\n\tif err != nil {\n\t\t// errFmt does not follow error message guidelines because it is sent\n\t\t// directly to the frontend.\n\t\tconst errFmt = \"Failed to unmarshal TLS config: %s\"\n\n\t\taghhttp.ErrorAndLog(ctx, m.logger, r, w, http.StatusBadRequest, errFmt, err)\n\n\t\treturn\n\t}\n\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tif setts.PrivateKeySaved {\n\t\tsetts.PrivateKey = m.conf.PrivateKey\n\t}\n\n\tif err = m.validateTLSSettings(setts); err != nil {\n\t\tm.logger.InfoContext(ctx, \"validating tls settings\", slogutil.KeyError, err)\n\n\t\taghhttp.ErrorAndLog(ctx, m.logger, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\t// Skip the error check, since we are only interested in the value of\n\t// status.WarningValidation.\n\tstatus := &tlsConfigStatus{}\n\t_ = m.loadTLSConfig(ctx, &setts.tlsConfigSettings, status)\n\tresp := &tlsConfig{\n\t\ttlsConfigSettingsExt: setts,\n\t\ttlsConfigStatus:      status,\n\t}\n\n\tm.marshalTLS(ctx, w, r, resp)\n}\n\n// setConfig updates manager TLS configuration with the given one.  m.mu is\n// expected to be locked.\nfunc (m *tlsManager) setConfig(\n\tctx context.Context,\n\tnewConf tlsConfigSettings,\n\tstatus *tlsConfigStatus,\n\tservePlain aghalg.NullBool,\n) (restartHTTPS bool) {\n\tif !m.conf.setPrivateFieldsAndCompare(&newConf) {\n\t\tm.logger.InfoContext(ctx, \"config has changed, restarting https server\")\n\t\trestartHTTPS = true\n\t} else {\n\t\tm.logger.InfoContext(ctx, \"config has not changed\")\n\t}\n\n\tm.conf = &newConf\n\n\tm.status = status\n\n\tif servePlain != aghalg.NBNull {\n\t\tm.servePlainDNS = servePlain == aghalg.NBTrue\n\t}\n\n\tcertPath, keyPath := \"\", \"\"\n\tif newConf.Enabled {\n\t\tcertPath, keyPath = newConf.CertificatePath, newConf.PrivateKeyPath\n\t}\n\n\terr := m.manager.Set(ctx, aghtls.TLSPair{\n\t\tCertPath: certPath,\n\t\tKeyPath:  keyPath,\n\t})\n\tif err != nil {\n\t\tm.logger.ErrorContext(ctx, \"setting tls files\", slogutil.KeyError, err)\n\t}\n\n\treturn restartHTTPS\n}\n\n// handleTLSConfigure is the handler for the POST /control/tls/configure HTTP\n// API.\nfunc (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\treq, err := unmarshalTLS(r)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tm.logger,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Failed to unmarshal TLS config: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tvar restartHTTPS bool\n\tdefer func() {\n\t\tif restartHTTPS {\n\t\t\tm.confModifier.Apply(ctx)\n\t\t}\n\t}()\n\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tif req.PrivateKeySaved {\n\t\treq.PrivateKey = m.conf.PrivateKey\n\t}\n\n\tif err = m.validateTLSSettings(req); err != nil {\n\t\taghhttp.ErrorAndLog(ctx, m.logger, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tstatus := &tlsConfigStatus{}\n\terr = m.loadTLSConfig(ctx, &req.tlsConfigSettings, status)\n\tif err != nil {\n\t\tresp := &tlsConfig{\n\t\t\ttlsConfigSettingsExt: req,\n\t\t\ttlsConfigStatus:      status,\n\t\t}\n\n\t\tm.marshalTLS(ctx, w, r, resp)\n\n\t\treturn\n\t}\n\n\trestartHTTPS = m.setConfig(ctx, req.tlsConfigSettings, status, req.ServePlainDNS)\n\tm.setCertFileTime(ctx)\n\n\tif req.ServePlainDNS != aghalg.NBNull {\n\t\tfunc() {\n\t\t\tconfig.Lock()\n\t\t\tdefer config.Unlock()\n\n\t\t\tconfig.DNS.ServePlainDNS = req.ServePlainDNS == aghalg.NBTrue\n\t\t}()\n\t}\n\n\terr = m.reconfigureDNSServer(ctx)\n\tif err != nil {\n\t\tm.logger.ErrorContext(ctx, \"reconfiguring dns server\", slogutil.KeyError, err)\n\n\t\taghhttp.ErrorAndLog(ctx, m.logger, r, w, http.StatusInternalServerError, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tresp := &tlsConfig{\n\t\ttlsConfigSettingsExt: req,\n\t\ttlsConfigStatus:      m.status,\n\t}\n\n\tm.marshalTLS(ctx, w, r, resp)\n\trc := http.NewResponseController(w)\n\terr = rc.Flush()\n\tif err != nil {\n\t\tm.logger.ErrorContext(ctx, \"flushing response\", slogutil.KeyError, err)\n\t}\n\n\t// The background context is used because the TLSConfigChanged wraps context\n\t// with timeout on its own and shuts down the server, which handles current\n\t// request.  It is also should be done in a separate goroutine due to the\n\t// same reason.\n\tif restartHTTPS {\n\t\tgo m.web.tlsConfigChanged(context.Background(), &req.tlsConfigSettings)\n\t}\n}\n\n// validateTLSSettings returns error if the setts are not valid.\nfunc (m *tlsManager) validateTLSSettings(setts tlsConfigSettingsExt) (err error) {\n\tif !setts.Enabled {\n\t\tif setts.ServePlainDNS == aghalg.NBFalse {\n\t\t\t// TODO(a.garipov): Support full disabling of all DNS.\n\t\t\treturn errors.Error(\"plain DNS is required in case encryption protocols are disabled\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tvar (\n\t\ttlsConf      tlsConfigSettings\n\t\twebAPIAddr   netip.Addr\n\t\twebAPIPort   uint16\n\t\tplainDNSPort uint16\n\t)\n\n\tfunc() {\n\t\tconfig.Lock()\n\t\tdefer config.Unlock()\n\n\t\ttlsConf = config.TLS\n\t\twebAPIAddr = config.HTTPConfig.Address.Addr()\n\t\twebAPIPort = config.HTTPConfig.Address.Port()\n\t\tplainDNSPort = config.DNS.Port\n\t}()\n\n\terr = validatePorts(\n\t\ttcpPort(webAPIPort),\n\t\ttcpPort(setts.PortHTTPS),\n\t\ttcpPort(setts.PortDNSOverTLS),\n\t\ttcpPort(setts.PortDNSCrypt),\n\t\tudpPort(plainDNSPort),\n\t\tudpPort(setts.PortDNSOverQUIC),\n\t)\n\tif err != nil {\n\t\t// Don't wrap the error because it's informative enough as is.\n\t\treturn err\n\t}\n\n\t// Don't wrap the error because it's informative enough as is.\n\treturn m.checkPortAvailability(tlsConf, setts.tlsConfigSettings, webAPIAddr)\n}\n\n// validatePorts validates the uniqueness of TCP and UDP ports for AdGuard Home\n// DNS protocols.\nfunc validatePorts(\n\tbindPort, dohPort, dotPort, dnscryptTCPPort tcpPort,\n\tdnsPort, doqPort udpPort,\n) (err error) {\n\ttcpPorts := aghalg.UniqChecker[tcpPort]{}\n\taddPorts(\n\t\ttcpPorts,\n\t\tbindPort,\n\t\tdohPort,\n\t\tdotPort,\n\t\tdnscryptTCPPort,\n\t\ttcpPort(dnsPort),\n\t)\n\n\terr = tcpPorts.Validate()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating tcp ports: %w\", err)\n\t}\n\n\tudpPorts := aghalg.UniqChecker[udpPort]{}\n\taddPorts(udpPorts, dnsPort, doqPort)\n\n\terr = udpPorts.Validate()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating udp ports: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// validateCertChain verifies certs using the first as the main one and others\n// as intermediate.  srvName stands for the expected DNS name.  certs must not\n// be empty.\n//\n// TODO(e.burkov):  Pass logger and rootCerts through arguments and remove\n// dependency on tlsManager.\nfunc (m *tlsManager) validateCertChain(\n\tctx context.Context,\n\tcerts []*x509.Certificate,\n\tsrvName string,\n) (err error) {\n\tmain, others := certs[0], certs[1:]\n\n\tpool := x509.NewCertPool()\n\tfor _, cert := range others {\n\t\tpool.AddCert(cert)\n\t}\n\n\tothersLen := len(others)\n\tif othersLen > 0 {\n\t\tm.logger.InfoContext(\n\t\t\tctx,\n\t\t\t\"verifying certificate chain: got an intermediate cert\",\n\t\t\t\"num\", othersLen,\n\t\t)\n\t}\n\n\topts := x509.VerifyOptions{\n\t\tDNSName:       srvName,\n\t\tRoots:         m.rootCerts,\n\t\tIntermediates: pool,\n\t}\n\t_, err = main.Verify(opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"certificate does not verify: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// checkPortAvailability checks [tlsConfigSettings.PortHTTPS],\n// [tlsConfigSettings.PortDNSOverTLS], and [tlsConfigSettings.PortDNSOverQUIC]\n// are available for use.  It checks the current configuration and, if needed,\n// attempts to bind to the port.  The function returns human-readable error\n// messages for the frontend.  This is best-effort check to prevent an \"address\n// already in use\" error.\n//\n// TODO(a.garipov): Adapt for HTTP/3.\nfunc (m *tlsManager) checkPortAvailability(\n\tcurrConf tlsConfigSettings,\n\tnewConf tlsConfigSettings,\n\taddr netip.Addr,\n) (err error) {\n\tconst (\n\t\tnetworkTCP = \"tcp\"\n\t\tnetworkUDP = \"udp\"\n\n\t\tprotoHTTPS = \"HTTPS\"\n\t\tprotoDoT   = \"DNS-over-TLS\"\n\t\tprotoDoQ   = \"DNS-over-QUIC\"\n\t)\n\n\tneedBindingCheck := []struct {\n\t\tnetwork  string\n\t\tproto    string\n\t\tcurrPort uint16\n\t\tnewPort  uint16\n\t}{{\n\t\tnetwork:  networkTCP,\n\t\tproto:    protoHTTPS,\n\t\tcurrPort: currConf.PortHTTPS,\n\t\tnewPort:  newConf.PortHTTPS,\n\t}, {\n\t\tnetwork:  networkTCP,\n\t\tproto:    protoDoT,\n\t\tcurrPort: currConf.PortDNSOverTLS,\n\t\tnewPort:  newConf.PortDNSOverTLS,\n\t}, {\n\t\tnetwork:  networkUDP,\n\t\tproto:    protoDoQ,\n\t\tcurrPort: currConf.PortDNSOverQUIC,\n\t\tnewPort:  newConf.PortDNSOverQUIC,\n\t}}\n\n\tvar errs []error\n\tfor _, v := range needBindingCheck {\n\t\tport := v.newPort\n\t\tif v.currPort == port {\n\t\t\tcontinue\n\t\t}\n\n\t\taddrPort := netip.AddrPortFrom(addr, port)\n\t\terr = aghnet.CheckPort(v.network, addrPort)\n\t\tif err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"port %d for %s is not available\", port, v.proto))\n\t\t}\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// errNoIPInCert is the error that is returned from [tlsManager.parseCertChain]\n// if the leaf certificate doesn't contain IPs.\nconst errNoIPInCert errors.Error = `certificates has no IP addresses; ` +\n\t`DNS-over-TLS won't be advertised via DDR`\n\n// parseCertChain parses the certificate chain from raw data, and returns it.\n// If ok is true, the returned error, if any, is not critical.\n//\n// TODO(e.burkov):  Pass logger through arguments and remove dependency on\n// tlsManager.\nfunc (m *tlsManager) parseCertChain(\n\tctx context.Context,\n\tchain []byte,\n) (parsedCerts []*x509.Certificate, ok bool, err error) {\n\tm.logger.DebugContext(ctx, \"parsing certificate chain\", \"size\", datasize.ByteSize(len(chain)))\n\n\tvar certs []*pem.Block\n\tfor decoded, pemblock := pem.Decode(chain); decoded != nil; {\n\t\tif decoded.Type == \"CERTIFICATE\" {\n\t\t\tcerts = append(certs, decoded)\n\t\t}\n\n\t\tdecoded, pemblock = pem.Decode(pemblock)\n\t}\n\n\tparsedCerts, err = parsePEMCerts(certs)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\tm.logger.InfoContext(ctx, \"parsing multiple pem certificates\", \"num\", len(parsedCerts))\n\n\tif !aghtls.CertificateHasIP(parsedCerts[0]) {\n\t\terr = errNoIPInCert\n\t}\n\n\treturn parsedCerts, true, err\n}\n\n// parsePEMCerts parses multiple PEM-encoded certificates.\nfunc parsePEMCerts(certs []*pem.Block) (parsedCerts []*x509.Certificate, err error) {\n\tfor i, cert := range certs {\n\t\tvar parsed *x509.Certificate\n\t\tparsed, err = x509.ParseCertificate(cert.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing certificate at index %d: %w\", i, err)\n\t\t}\n\n\t\tparsedCerts = append(parsedCerts, parsed)\n\t}\n\n\tif len(parsedCerts) == 0 {\n\t\treturn nil, errors.Error(\"empty certificate\")\n\t}\n\n\treturn parsedCerts, nil\n}\n\n// validatePKey validates the private key, returning its type.  It returns an\n// empty string if error occurs.\nfunc validatePKey(pkey []byte) (keyType string, err error) {\n\tvar key *pem.Block\n\n\t// Go through all pem blocks, but take first valid pem block and drop the\n\t// rest.\n\tfor decoded, pemblock := pem.Decode([]byte(pkey)); decoded != nil; {\n\t\tif decoded.Type == \"PRIVATE KEY\" || strings.HasSuffix(decoded.Type, \" PRIVATE KEY\") {\n\t\t\tkey = decoded\n\n\t\t\tbreak\n\t\t}\n\n\t\tdecoded, pemblock = pem.Decode(pemblock)\n\t}\n\n\tif key == nil {\n\t\treturn \"\", errors.Error(\"no valid keys were found\")\n\t}\n\n\t_, keyType, err = parsePrivateKey(key.Bytes)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"parsing private key: %w\", err)\n\t}\n\n\tif keyType == keyTypeED25519 {\n\t\treturn \"\", errors.Error(\n\t\t\t\"ED25519 keys are not supported by browsers; \" +\n\t\t\t\t\"did you mean to use X25519 for key exchange?\",\n\t\t)\n\t}\n\n\treturn keyType, nil\n}\n\n// validateCertificates processes certificate data and its private key.  status\n// must not be nil, since it's used to accumulate the validation results.  Other\n// parameters are optional.\nfunc (m *tlsManager) validateCertificates(\n\tctx context.Context,\n\tstatus *tlsConfigStatus,\n\tcertChain []byte,\n\tpkey []byte,\n\tserverName string,\n) (err error) {\n\t// Check only the public certificate separately from the key.\n\tif len(certChain) > 0 {\n\t\tvar ok bool\n\t\tok, err = m.validateCertificate(ctx, status, certChain, serverName)\n\t\tif !ok {\n\t\t\t// Don't wrap the error, since it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Validate the private key by parsing it.\n\tif len(pkey) > 0 {\n\t\tvar keyErr error\n\t\tstatus.KeyType, keyErr = validatePKey(pkey)\n\t\tif keyErr != nil {\n\t\t\t// Don't wrap the error, since it's informative enough as is.\n\t\t\treturn keyErr\n\t\t}\n\n\t\t// Set status.ValidKey to true to signal the frontend that the\n\t\t// key is valid.\n\t\tstatus.ValidKey = true\n\t}\n\n\t// If both are set, validate together.\n\tif len(certChain) > 0 && len(pkey) > 0 {\n\t\t_, pairErr := tls.X509KeyPair(certChain, pkey)\n\t\tif pairErr != nil {\n\t\t\treturn fmt.Errorf(\"certificate-key pair: %w\", pairErr)\n\t\t}\n\n\t\tstatus.ValidPair = true\n\t}\n\n\treturn err\n}\n\n// validateCertificate processes certificate data.  status must not be nil, as\n// it is used to accumulate the validation results.  Other parameters are\n// optional.  If ok is true, the returned error, if any, is not critical.\nfunc (m *tlsManager) validateCertificate(\n\tctx context.Context,\n\tstatus *tlsConfigStatus,\n\tcertChain []byte,\n\tserverName string,\n) (ok bool, err error) {\n\t// parseErr is a non-critical parse warning.\n\tvar parseErr error\n\tvar certs []*x509.Certificate\n\n\t// Set status.ValidCert to true to signal the frontend that the\n\t// certificate opens successfully and certificate chain is valid.\n\tcerts, status.ValidCert, parseErr = m.parseCertChain(ctx, certChain)\n\tif !status.ValidCert {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn false, parseErr\n\t}\n\n\tmainCert := certs[0]\n\tstatus.Subject = mainCert.Subject.String()\n\tstatus.Issuer = mainCert.Issuer.String()\n\tstatus.NotAfter = mainCert.NotAfter\n\tstatus.NotBefore = mainCert.NotBefore\n\tstatus.DNSNames = mainCert.DNSNames\n\n\terr = m.validateCertChain(ctx, certs, serverName)\n\tif err != nil {\n\t\t// Let self-signed certs through and don't return this error to set\n\t\t// its message into the status.WarningValidation afterwards.\n\t\treturn true, err\n\t}\n\n\tstatus.ValidChain = true\n\n\t// Propagate the non-critical parse warning.\n\treturn true, parseErr\n}\n\n// Key types.\nconst (\n\tkeyTypeECDSA   = \"ECDSA\"\n\tkeyTypeED25519 = \"ED25519\"\n\tkeyTypeRSA     = \"RSA\"\n)\n\n// Attempt to parse the given private key DER block.  OpenSSL 0.9.8 generates\n// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.\n// OpenSSL ecparam generates SEC1 EC private keys for ECDSA.  We try all three.\n//\n// TODO(a.garipov): Find out if this version of parsePrivateKey from the stdlib\n// is actually necessary.\nfunc parsePrivateKey(der []byte) (key crypto.PrivateKey, typ string, err error) {\n\tif key, err = x509.ParsePKCS1PrivateKey(der); err == nil {\n\t\treturn key, keyTypeRSA, nil\n\t}\n\n\tif key, err = x509.ParsePKCS8PrivateKey(der); err == nil {\n\t\tswitch key := key.(type) {\n\t\tcase *rsa.PrivateKey:\n\t\t\treturn key, keyTypeRSA, nil\n\t\tcase *ecdsa.PrivateKey:\n\t\t\treturn key, keyTypeECDSA, nil\n\t\tcase ed25519.PrivateKey:\n\t\t\treturn key, keyTypeED25519, nil\n\t\tdefault:\n\t\t\treturn nil, \"\", fmt.Errorf(\n\t\t\t\t\"tls: found unknown private key type %T in PKCS#8 wrapping\",\n\t\t\t\tkey,\n\t\t\t)\n\t\t}\n\t}\n\n\tif key, err = x509.ParseECPrivateKey(der); err == nil {\n\t\treturn key, keyTypeECDSA, nil\n\t}\n\n\treturn nil, \"\", errors.Error(\"tls: failed to parse private key\")\n}\n\n// unmarshalTLS handles base64-encoded certificates transparently\nfunc unmarshalTLS(r *http.Request) (tlsConfigSettingsExt, error) {\n\tdata := tlsConfigSettingsExt{}\n\terr := json.NewDecoder(r.Body).Decode(&data)\n\tif err != nil {\n\t\treturn data, fmt.Errorf(\"failed to parse new TLS config json: %w\", err)\n\t}\n\n\tif data.CertificateChain != \"\" {\n\t\tvar cert []byte\n\t\tcert, err = base64.StdEncoding.DecodeString(data.CertificateChain)\n\t\tif err != nil {\n\t\t\treturn data, fmt.Errorf(\"failed to base64-decode certificate chain: %w\", err)\n\t\t}\n\n\t\tdata.CertificateChain = string(cert)\n\t\tif data.CertificatePath != \"\" {\n\t\t\treturn data, fmt.Errorf(\"certificate data and file can't be set together\")\n\t\t}\n\t}\n\n\tif data.PrivateKey == \"\" {\n\t\treturn data, nil\n\t}\n\n\tkey, err := base64.StdEncoding.DecodeString(data.PrivateKey)\n\tif err != nil {\n\t\treturn data, fmt.Errorf(\"failed to base64-decode private key: %w\", err)\n\t}\n\n\tdata.PrivateKey = string(key)\n\tif data.PrivateKeyPath != \"\" {\n\t\treturn data, fmt.Errorf(\"private key data and file can't be set together\")\n\t}\n\n\treturn data, nil\n}\n\n// marshalTLS encodes sensitive fields and writes data as JSON.  All arguments\n// must not be nil.\nfunc (m *tlsManager) marshalTLS(\n\tctx context.Context,\n\tw http.ResponseWriter,\n\tr *http.Request,\n\tdata *tlsConfig,\n) {\n\tif data.CertificateChain != \"\" {\n\t\tencoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))\n\t\tdata.CertificateChain = encoded\n\t}\n\n\tif data.PrivateKey != \"\" {\n\t\tdata.PrivateKeySaved = true\n\t\tdata.PrivateKey = \"\"\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, m.logger, w, r, *data)\n}\n\n// registerWebHandlers registers HTTP handlers for TLS configuration.\nfunc (m *tlsManager) registerWebHandlers() {\n\tm.httpReg.Register(http.MethodGet, \"/control/tls/status\", m.handleTLSStatus)\n\tm.httpReg.Register(http.MethodPost, \"/control/tls/configure\", m.handleTLSConfigure)\n\tm.httpReg.Register(http.MethodPost, \"/control/tls/validate\", m.handleTLSValidate)\n}\n"
  },
  {
    "path": "internal/home/tls_internal_test.go",
    "content": "package home\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtls\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/client\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/dnsforward\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TODO(s.chzhen):  Consider moving to testdata.\nvar testCertChainData = []byte(`-----BEGIN CERTIFICATE-----\nMIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV\nBAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3\nMDkyNDIzWhcNNDYwNzE0MDkyNDIzWjAtMRQwEgYDVQQKDAtBZEd1YXJkIEx0ZDEV\nMBMGA1UEAwwMQWRHdWFyZCBIb21lMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\ngQCwvwUnPJiOvLcOaWmGu6Y68ksFr13nrXBcsDlhxlXy8PaohVi3XxEmt2OrVjKW\nQFw/bdV4fZ9tdWFAVRRkgeGbIZzP7YBD1Ore/O5SQ+DbCCEafvjJCcXQIrTeKFE6\ni9G3aSMHs0Pwq2LgV8U5mYotLrvyFiE8QPInJbDDMpaFYwIDAQABo1MwUTAdBgNV\nHQ4EFgQUdLUmQpEqrhn4eKO029jYd2AAZEQwHwYDVR0jBBgwFoAUdLUmQpEqrhn4\neKO029jYd2AAZEQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQB8\nLwlXfbakf7qkVTlCNXgoY7RaJ8rJdPgOZPoCTVToEhT6u/cb1c2qp8QB0dNExDna\nb0Z+dnODTZqQOJo6z/wIXlcUrnR4cQVvytXt8lFn+26l6Y6EMI26twC/xWr+1swq\nMuj4FeWHVDerquH4yMr1jsYLD3ci+kc5sbIX6TfVxQ==\n-----END CERTIFICATE-----`)\n\nvar testPrivateKeyData = []byte(`-----BEGIN PRIVATE KEY-----\nMIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALC/BSc8mI68tw5p\naYa7pjrySwWvXeetcFywOWHGVfLw9qiFWLdfESa3Y6tWMpZAXD9t1Xh9n211YUBV\nFGSB4ZshnM/tgEPU6t787lJD4NsIIRp++MkJxdAitN4oUTqL0bdpIwezQ/CrYuBX\nxTmZii0uu/IWITxA8iclsMMyloVjAgMBAAECgYEAmjzoG1h27UDkIlB9BVWl95TP\nQVPLB81D267xNFDnWk1Lgr5zL/pnNjkdYjyjgpkBp1yKyE4gHV4skv5sAFWTcOCU\nQCgfPfUn/rDFcxVzAdJVWAa/CpJNaZgjTPR8NTGU+Ztod+wfBESNCP5tbnuw0GbL\nMuwdLQJGbzeJYpsNysECQQDfFHYoRNfgxHwMbX24GCoNZIgk12uDmGTA9CS5E+72\n9t3V1y4CfXxSkfhqNbd5RWrUBRLEw9BKofBS7L9NMDKDAkEAytQoIueE1vqEAaRg\na3A1YDUekKesU5wKfKfKlXvNgB7Hwh4HuvoQS9RCvVhf/60Dvq8KSu6hSjkFRquj\nFQ5roQJBAMwKwyiCD5MfJPeZDmzcbVpiocRQ5Z4wPbffl9dRTDnIA5AciZDthlFg\nAn/jMjZSMCxNl6UyFcqt5Et1EGVhuFECQQCZLXxaT+qcyHjlHJTMzuMgkz1QFbEp\nO5EX70gpeGQMPDK0QSWpaazg956njJSDbNCFM4BccrdQbJu1cW4qOsfBAkAMgZuG\nO88slmgTRHX4JGFmy3rrLiHNI2BbJSuJ++Yllz8beVzh6NfvuY+HKRCmPqoBPATU\nkXS9jgARhhiWXJrk\n-----END PRIVATE KEY-----`)\n\nfunc TestValidateCertificates(t *testing.T) {\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tm, err := newTLSManager(ctx, &tlsManagerConfig{\n\t\tlogger:        testLogger,\n\t\tconfModifier:  agh.EmptyConfigModifier{},\n\t\tmanager:       aghtls.EmptyManager{},\n\t\tservePlainDNS: false,\n\t})\n\trequire.NoError(t, err)\n\n\tt.Run(\"bad_certificate\", func(t *testing.T) {\n\t\tstatus := &tlsConfigStatus{}\n\t\terr = m.validateCertificates(ctx, status, []byte(\"bad cert\"), nil, \"\")\n\t\ttestutil.AssertErrorMsg(t, \"empty certificate\", err)\n\t\tassert.False(t, status.ValidCert)\n\t\tassert.False(t, status.ValidChain)\n\t})\n\n\tt.Run(\"bad_private_key\", func(t *testing.T) {\n\t\tstatus := &tlsConfigStatus{}\n\t\terr = m.validateCertificates(ctx, status, nil, []byte(\"bad priv key\"), \"\")\n\t\ttestutil.AssertErrorMsg(t, \"no valid keys were found\", err)\n\t\tassert.False(t, status.ValidKey)\n\t})\n\n\tt.Run(\"valid\", func(t *testing.T) {\n\t\tstatus := &tlsConfigStatus{}\n\t\terr = m.validateCertificates(ctx, status, testCertChainData, testPrivateKeyData, \"\")\n\t\tassert.Error(t, err)\n\n\t\tnotBefore := time.Date(2019, 2, 27, 9, 24, 23, 0, time.UTC)\n\t\tnotAfter := time.Date(2046, 7, 14, 9, 24, 23, 0, time.UTC)\n\n\t\tassert.True(t, status.ValidCert)\n\t\tassert.False(t, status.ValidChain)\n\t\tassert.True(t, status.ValidKey)\n\t\tassert.Equal(t, \"RSA\", status.KeyType)\n\t\tassert.Equal(t, \"CN=AdGuard Home,O=AdGuard Ltd\", status.Subject)\n\t\tassert.Equal(t, \"CN=AdGuard Home,O=AdGuard Ltd\", status.Issuer)\n\t\tassert.Equal(t, notBefore, status.NotBefore)\n\t\tassert.Equal(t, notAfter, status.NotAfter)\n\t\tassert.True(t, status.ValidPair)\n\t})\n\n\tt.Run(\"no_ip_in_cert\", func(t *testing.T) {\n\t\tcaCert, chainPEM, leafKeyPEM := newCertWithoutIP(t)\n\n\t\tm.rootCerts = x509.NewCertPool()\n\t\tm.rootCerts.AddCert(caCert)\n\n\t\tstatus := &tlsConfigStatus{}\n\t\tvar ok bool\n\t\tok, err = m.validateCertificate(ctx, status, chainPEM, \"\")\n\t\tassert.True(t, ok)\n\t\tassert.ErrorIs(t, err, errNoIPInCert)\n\t\tassert.True(t, status.ValidCert)\n\t\tassert.True(t, status.ValidChain)\n\n\t\tstatus = &tlsConfigStatus{}\n\t\terr = m.validateCertificates(ctx, status, chainPEM, leafKeyPEM, \"\")\n\t\tassert.ErrorIs(t, err, errNoIPInCert)\n\t\tassert.True(t, status.ValidCert)\n\t\tassert.True(t, status.ValidChain)\n\t\tassert.True(t, status.ValidKey)\n\t\tassert.True(t, status.ValidPair)\n\t})\n}\n\n// storeGlobals is a test helper function that saves global variables and\n// restores them once the test is complete.\n//\n// The global variables are:\n//   - [config]\n//   - [glFilePrefix]\n//   - [globalContext.auth]\n//   - [globalContext.clients.storage]\n//   - [globalContext.dnsServer]\n//   - [globalContext.firstRun]\n//   - [globalContext.mux]\n//   - [globalContext.web]\n//\n// TODO(s.chzhen):  Remove this once the TLS manager no longer accesses global\n// variables.  Make tests that use this helper concurrent.\nfunc storeGlobals(tb testing.TB) {\n\ttb.Helper()\n\n\tprevConfig := config\n\tprefGLFilePrefix := glFilePrefix\n\tstorage := globalContext.clients.storage\n\tdnsServer := globalContext.dnsServer\n\tweb := globalContext.web\n\n\ttb.Cleanup(func() {\n\t\tconfig = prevConfig\n\t\tglFilePrefix = prefGLFilePrefix\n\t\tglobalContext.clients.storage = storage\n\t\tglobalContext.dnsServer = dnsServer\n\t\tglobalContext.web = web\n\t})\n}\n\n// newCertWithoutIP generates a CA certificate, a leaf certificate without an IP\n// address, and the PEM-encoded leaf private key.\nfunc newCertWithoutIP(tb testing.TB) (\n\tcaCert *x509.Certificate,\n\tchainPEM []byte,\n\tleafKeyPEM []byte,\n) {\n\ttb.Helper()\n\n\tcaKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(tb, err)\n\n\tnow := time.Now()\n\tcaTmpl := &x509.Certificate{\n\t\tSerialNumber:          big.NewInt(1),\n\t\tNotBefore:             now.Add(-time.Hour),\n\t\tNotAfter:              now.Add(time.Hour),\n\t\tIsCA:                  true,\n\t\tBasicConstraintsValid: true,\n\t\tKeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,\n\t}\n\n\tcaDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey)\n\trequire.NoError(tb, err)\n\n\tcaCert, err = x509.ParseCertificate(caDER)\n\trequire.NoError(tb, err)\n\n\tleafKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(tb, err)\n\n\tleafTmpl := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(2),\n\t\tNotBefore:    now.Add(-time.Hour),\n\t\tNotAfter:     now.Add(time.Hour),\n\t\tKeyUsage:     x509.KeyUsageDigitalSignature,\n\t}\n\n\tleafDER, err := x509.CreateCertificate(\n\t\trand.Reader,\n\t\tleafTmpl,\n\t\tcaTmpl,\n\t\t&leafKey.PublicKey,\n\t\tcaKey,\n\t)\n\trequire.NoError(tb, err)\n\n\tbuf := bytes.Buffer{}\n\terr = pem.Encode(&buf, &pem.Block{Type: \"CERTIFICATE\", Bytes: leafDER})\n\trequire.NoError(tb, err)\n\n\terr = pem.Encode(&buf, &pem.Block{Type: \"CERTIFICATE\", Bytes: caDER})\n\trequire.NoError(tb, err)\n\n\tleafKeyPEM = pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(leafKey),\n\t})\n\n\treturn caCert, buf.Bytes(), leafKeyPEM\n}\n\n// newCertAndKey is a helper function that generates certificate and key.\nfunc newCertAndKey(tb testing.TB, n int64) (certDER []byte, key *rsa.PrivateKey) {\n\ttb.Helper()\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(tb, err)\n\n\tcertTmpl := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(n),\n\t}\n\n\tcertDER, err = x509.CreateCertificate(rand.Reader, certTmpl, certTmpl, &key.PublicKey, key)\n\trequire.NoError(tb, err)\n\n\treturn certDER, key\n}\n\n// writeCertAndKey is a helper function that writes certificate and key to\n// specified paths.\nfunc writeCertAndKey(\n\ttb testing.TB,\n\tcertDER []byte,\n\tcertPath string,\n\tkey *rsa.PrivateKey,\n\tkeyPath string,\n) {\n\ttb.Helper()\n\n\tcertFile, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE, 0o600)\n\trequire.NoError(tb, err)\n\n\tdefer func() {\n\t\terr = certFile.Close()\n\t\trequire.NoError(tb, err)\n\t}()\n\n\terr = pem.Encode(certFile, &pem.Block{Type: \"CERTIFICATE\", Bytes: certDER})\n\trequire.NoError(tb, err)\n\n\tkeyFile, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE, 0o600)\n\trequire.NoError(tb, err)\n\n\tdefer func() {\n\t\terr = keyFile.Close()\n\t\trequire.NoError(tb, err)\n\t}()\n\n\terr = pem.Encode(keyFile, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(key),\n\t})\n\trequire.NoError(tb, err)\n}\n\n// assertCertSerialNumber is a helper function that checks serial number of the\n// TLS certificate.\nfunc assertCertSerialNumber(tb testing.TB, conf *tlsConfigSettings, wantSN int64) {\n\ttb.Helper()\n\n\tcert, err := tls.X509KeyPair(conf.CertificateChainData, conf.PrivateKeyData)\n\trequire.NoError(tb, err)\n\n\tassert.Equal(tb, wantSN, cert.Leaf.SerialNumber.Int64())\n}\n\nfunc TestTLSManager_Reload(t *testing.T) {\n\tstoreGlobals(t)\n\n\tconfig.DNS.Port = 0\n\n\tvar (\n\t\tctx = testutil.ContextWithTimeout(t, testTimeout)\n\t\terr error\n\t)\n\n\tglobalContext.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{\n\t\tLogger: testLogger,\n\t})\n\trequire.NoError(t, err)\n\n\tglobalContext.clients.storage, err = client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger: testLogger,\n\t\tLogger:     testLogger,\n\t\tClock:      timeutil.SystemClock{},\n\t})\n\trequire.NoError(t, err)\n\n\tconst (\n\t\tsnBefore int64 = 1\n\t\tsnAfter  int64 = 2\n\t)\n\n\ttmpDir := t.TempDir()\n\tcertPath := filepath.Join(tmpDir, \"cert.pem\")\n\tkeyPath := filepath.Join(tmpDir, \"key.pem\")\n\n\tcertDER, key := newCertAndKey(t, snBefore)\n\twriteCertAndKey(t, certDER, certPath, key, keyPath)\n\n\tm, err := newTLSManager(ctx, &tlsManagerConfig{\n\t\tlogger:       testLogger,\n\t\tconfModifier: agh.EmptyConfigModifier{},\n\t\tmanager:      aghtls.EmptyManager{},\n\t\ttlsSettings: tlsConfigSettings{\n\t\t\tEnabled:         true,\n\t\t\tCertificatePath: certPath,\n\t\t\tPrivateKeyPath:  keyPath,\n\t\t},\n\t\tservePlainDNS: false,\n\t})\n\trequire.NoError(t, err)\n\n\tweb := newTestWeb(t, &webConfig{})\n\tm.setWebAPI(web)\n\n\tconf := m.config()\n\tassertCertSerialNumber(t, conf, snBefore)\n\n\tcertDER, key = newCertAndKey(t, snAfter)\n\twriteCertAndKey(t, certDER, certPath, key, keyPath)\n\n\tm.reload(ctx)\n\n\t// The [tlsManager.reload] method will start the DNS server and it should be\n\t// stopped after the test ends.\n\ttestutil.CleanupAndRequireSuccess(t, func() (err error) {\n\t\treturn globalContext.dnsServer.Stop(testutil.ContextWithTimeout(t, testTimeout))\n\t})\n\n\tconf = m.config()\n\tassertCertSerialNumber(t, conf, snAfter)\n}\n\nfunc TestTLSManager_HandleTLSStatus(t *testing.T) {\n\tvar (\n\t\tctx = testutil.ContextWithTimeout(t, testTimeout)\n\t\terr error\n\t)\n\n\tm, err := newTLSManager(ctx, &tlsManagerConfig{\n\t\tlogger:       testLogger,\n\t\tconfModifier: agh.EmptyConfigModifier{},\n\t\tmanager:      aghtls.EmptyManager{},\n\t\ttlsSettings: tlsConfigSettings{\n\t\t\tEnabled:          true,\n\t\t\tCertificateChain: string(testCertChainData),\n\t\t\tPrivateKey:       string(testPrivateKeyData),\n\t\t},\n\t\tservePlainDNS: false,\n\t})\n\trequire.NoError(t, err)\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(http.MethodGet, \"/control/tls/status\", nil)\n\tm.handleTLSStatus(w, r)\n\n\tres := &tlsConfigSettingsExt{}\n\terr = json.NewDecoder(w.Body).Decode(res)\n\trequire.NoError(t, err)\n\n\twantCertificateChain := base64.StdEncoding.EncodeToString(testCertChainData)\n\tassert.True(t, res.Enabled)\n\tassert.Equal(t, wantCertificateChain, res.CertificateChain)\n\tassert.True(t, res.PrivateKeySaved)\n}\n\nfunc TestValidateTLSSettings(t *testing.T) {\n\tstoreGlobals(t)\n\n\tvar (\n\t\tctx = testutil.ContextWithTimeout(t, testTimeout)\n\t\terr error\n\t)\n\n\tm, err := newTLSManager(ctx, &tlsManagerConfig{\n\t\tlogger:        testLogger,\n\t\tconfModifier:  agh.EmptyConfigModifier{},\n\t\tmanager:       aghtls.EmptyManager{},\n\t\tservePlainDNS: false,\n\t})\n\trequire.NoError(t, err)\n\n\tweb := newTestWeb(t, &webConfig{})\n\tm.setWebAPI(web)\n\n\ttcpLn, err := net.Listen(\"tcp\", \":0\")\n\trequire.NoError(t, err)\n\n\ttestutil.CleanupAndRequireSuccess(t, tcpLn.Close)\n\n\ttcpAddr := testutil.RequireTypeAssert[*net.TCPAddr](t, tcpLn.Addr())\n\tbusyTCPPort := tcpAddr.Port\n\n\tudpLn, err := net.ListenPacket(\"udp\", \":0\")\n\trequire.NoError(t, err)\n\n\ttestutil.CleanupAndRequireSuccess(t, udpLn.Close)\n\n\tudpAddr := testutil.RequireTypeAssert[*net.UDPAddr](t, udpLn.LocalAddr())\n\tbusyUDPPort := udpAddr.Port\n\n\ttestCases := []struct {\n\t\tname    string\n\t\twantErr string\n\t\tsetts   tlsConfigSettingsExt\n\t}{{\n\t\tname:    \"basic\",\n\t\twantErr: \"\",\n\t\tsetts:   tlsConfigSettingsExt{},\n\t}, {\n\t\tname:    \"disabled_all\",\n\t\twantErr: \"plain DNS is required in case encryption protocols are disabled\",\n\t\tsetts: tlsConfigSettingsExt{\n\t\t\tServePlainDNS: aghalg.NBFalse,\n\t\t},\n\t}, {\n\t\tname:    \"busy_https_port\",\n\t\twantErr: fmt.Sprintf(\"port %d for HTTPS is not available\", busyTCPPort),\n\t\tsetts: tlsConfigSettingsExt{\n\t\t\ttlsConfigSettings: tlsConfigSettings{\n\t\t\t\tEnabled:   true,\n\t\t\t\tPortHTTPS: uint16(busyTCPPort),\n\t\t\t},\n\t\t},\n\t}, {\n\t\tname:    \"busy_dot_port\",\n\t\twantErr: fmt.Sprintf(\"port %d for DNS-over-TLS is not available\", busyTCPPort),\n\t\tsetts: tlsConfigSettingsExt{\n\t\t\ttlsConfigSettings: tlsConfigSettings{\n\t\t\t\tEnabled:        true,\n\t\t\t\tPortDNSOverTLS: uint16(busyTCPPort),\n\t\t\t},\n\t\t},\n\t}, {\n\t\tname:    \"busy_doq_port\",\n\t\twantErr: fmt.Sprintf(\"port %d for DNS-over-QUIC is not available\", busyUDPPort),\n\t\tsetts: tlsConfigSettingsExt{\n\t\t\ttlsConfigSettings: tlsConfigSettings{\n\t\t\t\tEnabled:         true,\n\t\t\t\tPortDNSOverQUIC: uint16(busyUDPPort),\n\t\t\t},\n\t\t},\n\t}, {\n\t\tname:    \"duplicate_port\",\n\t\twantErr: \"validating tcp ports: duplicated values: [4433]\",\n\t\tsetts: tlsConfigSettingsExt{\n\t\t\ttlsConfigSettings: tlsConfigSettings{\n\t\t\t\tEnabled:        true,\n\t\t\t\tPortHTTPS:      4433,\n\t\t\t\tPortDNSOverTLS: 4433,\n\t\t\t},\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr = m.validateTLSSettings(tc.setts)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErr, err)\n\t\t})\n\t}\n}\n\nfunc TestTLSManager_HandleTLSValidate(t *testing.T) {\n\tstoreGlobals(t)\n\n\tvar (\n\t\tctx = testutil.ContextWithTimeout(t, testTimeout)\n\t\terr error\n\t)\n\n\tm, err := newTLSManager(ctx, &tlsManagerConfig{\n\t\tlogger:       testLogger,\n\t\tconfModifier: agh.EmptyConfigModifier{},\n\t\tmanager:      aghtls.EmptyManager{},\n\t\ttlsSettings: tlsConfigSettings{\n\t\t\tEnabled:          true,\n\t\t\tCertificateChain: string(testCertChainData),\n\t\t\tPrivateKey:       string(testPrivateKeyData),\n\t\t},\n\t\tservePlainDNS: false,\n\t})\n\trequire.NoError(t, err)\n\n\tweb := newTestWeb(t, &webConfig{})\n\tm.setWebAPI(web)\n\n\tsetts := &tlsConfigSettingsExt{\n\t\ttlsConfigSettings: tlsConfigSettings{\n\t\t\tEnabled:          true,\n\t\t\tCertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),\n\t\t\tPrivateKey:       base64.StdEncoding.EncodeToString(testPrivateKeyData),\n\t\t},\n\t}\n\n\treq, err := json.Marshal(setts)\n\trequire.NoError(t, err)\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(http.MethodPost, \"/control/tls/validate\", bytes.NewReader(req))\n\tm.handleTLSValidate(w, r)\n\n\tres := &tlsConfigStatus{}\n\terr = json.NewDecoder(w.Body).Decode(res)\n\trequire.NoError(t, err)\n\n\tcert, err := tls.X509KeyPair(testCertChainData, testPrivateKeyData)\n\trequire.NoError(t, err)\n\n\twantIssuer := cert.Leaf.Issuer.String()\n\tassert.Equal(t, wantIssuer, res.Issuer)\n}\n\nfunc TestTLSManager_HandleTLSConfigure(t *testing.T) {\n\t// Store the global state before making any changes.\n\tstoreGlobals(t)\n\n\tvar (\n\t\tctx = testutil.ContextWithTimeout(t, testTimeout)\n\t\terr error\n\t)\n\n\tglobalContext.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{\n\t\tLogger: testLogger,\n\t})\n\trequire.NoError(t, err)\n\n\terr = globalContext.dnsServer.Prepare(\n\t\ttestutil.ContextWithTimeout(t, testTimeout),\n\t\t&dnsforward.ServerConfig{\n\t\t\tTLSConf: &dnsforward.TLSConfig{},\n\t\t\tConfig: dnsforward.Config{\n\t\t\t\tUpstreamMode:     dnsforward.UpstreamModeLoadBalance,\n\t\t\t\tEDNSClientSubnet: &dnsforward.EDNSClientSubnet{Enabled: false},\n\t\t\t\tClientsContainer: dnsforward.EmptyClientsContainer{},\n\t\t\t},\n\t\t\tServePlainDNS: true,\n\t\t})\n\trequire.NoError(t, err)\n\n\tglobalContext.clients.storage, err = client.NewStorage(ctx, &client.StorageConfig{\n\t\tBaseLogger: testLogger,\n\t\tLogger:     testLogger,\n\t\tClock:      timeutil.SystemClock{},\n\t})\n\trequire.NoError(t, err)\n\n\tconfig.DNS.BindHosts = []netip.Addr{netip.MustParseAddr(\"127.0.0.1\")}\n\tconfig.DNS.Port = 0\n\n\tconst wantSerialNumber int64 = 1\n\n\t// Prepare the TLS manager configuration.\n\ttmpDir := t.TempDir()\n\tcertPath := filepath.Join(tmpDir, \"cert.pem\")\n\tkeyPath := filepath.Join(tmpDir, \"key.pem\")\n\n\tcertDER, key := newCertAndKey(t, wantSerialNumber)\n\twriteCertAndKey(t, certDER, certPath, key, keyPath)\n\n\t// Initialize the TLS manager and assert its configuration.\n\tm, err := newTLSManager(ctx, &tlsManagerConfig{\n\t\tlogger:       testLogger,\n\t\tconfModifier: agh.EmptyConfigModifier{},\n\t\tmanager:      aghtls.EmptyManager{},\n\t\ttlsSettings: tlsConfigSettings{\n\t\t\tEnabled:         true,\n\t\t\tCertificatePath: certPath,\n\t\t\tPrivateKeyPath:  keyPath,\n\t\t},\n\t\tservePlainDNS: true,\n\t})\n\trequire.NoError(t, err)\n\n\tweb := newTestWeb(t, &webConfig{})\n\tm.setWebAPI(web)\n\n\tconf := m.config()\n\tassertCertSerialNumber(t, conf, wantSerialNumber)\n\n\t// Prepare a request with the new TLS configuration.\n\tsetts := &tlsConfigSettingsExt{\n\t\ttlsConfigSettings: tlsConfigSettings{\n\t\t\tEnabled:          true,\n\t\t\tPortHTTPS:        4433,\n\t\t\tCertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),\n\t\t\tPrivateKey:       base64.StdEncoding.EncodeToString(testPrivateKeyData),\n\t\t},\n\t}\n\n\treq, err := json.Marshal(setts)\n\trequire.NoError(t, err)\n\n\tr := httptest.NewRequest(http.MethodPost, \"/control/tls/configure\", bytes.NewReader(req))\n\tw := httptest.NewRecorder()\n\n\t// Reconfigure the TLS manager.\n\tm.handleTLSConfigure(w, r)\n\n\t// The [tlsManager.handleTLSConfigure] method will start the DNS server and\n\t// it should be stopped after the test ends.\n\ttestutil.CleanupAndRequireSuccess(t, func() (err error) {\n\t\treturn globalContext.dnsServer.Stop(testutil.ContextWithTimeout(t, testTimeout))\n\t})\n\n\tres := &tlsConfig{\n\t\ttlsConfigStatus: &tlsConfigStatus{},\n\t}\n\n\terr = json.NewDecoder(w.Body).Decode(res)\n\trequire.NoError(t, err)\n\n\tcert, err := tls.X509KeyPair(testCertChainData, testPrivateKeyData)\n\trequire.NoError(t, err)\n\n\twantIssuer := cert.Leaf.Issuer.String()\n\tassert.Equal(t, wantIssuer, res.tlsConfigStatus.Issuer)\n\n\t// Assert that the Web API's TLS configuration has been updated.\n\t//\n\t// TODO(s.chzhen):  Remove when [httpsServer.cond] is removed.\n\tassert.Eventually(t, func() bool {\n\t\tweb.httpsServer.condLock.Lock()\n\t\tdefer web.httpsServer.condLock.Unlock()\n\n\t\tcert = web.httpsServer.cert\n\t\tif cert.Leaf == nil {\n\t\t\treturn false\n\t\t}\n\n\t\tassert.Equal(t, wantIssuer, cert.Leaf.Issuer.String())\n\n\t\treturn true\n\t}, testTimeout, testTimeout/10)\n}\n"
  },
  {
    "path": "internal/home/web.go",
    "content": "package home\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/updater\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/httputil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/NYTimes/gziphandler\"\n\t\"github.com/quic-go/quic-go/http3\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n)\n\n// TODO(a.garipov): Make configurable.\nconst (\n\t// readTimeout is the maximum duration for reading the entire request,\n\t// including the body.\n\treadTimeout = 60 * time.Second\n\t// readHdrTimeout is the amount of time allowed to read request headers.\n\treadHdrTimeout = 60 * time.Second\n\t// writeTimeout is the maximum duration before timing out writes of the\n\t// response.\n\twriteTimeout = 5 * time.Minute\n)\n\n// webAPIConfig is a configuration structure for webAPI.\ntype webAPIConfig struct {\n\t// CommandConstructor is used to run external commands.  It must not be nil.\n\tCommandConstructor executil.CommandConstructor\n\n\t// updater is used for updating AdGuard home.  If disableUpdate is set to\n\t// false, it must not be nil.\n\tupdater *updater.Updater\n\n\t// logger is a slog logger used in webAPI. It must not be nil.\n\tlogger *slog.Logger\n\n\t// baseLogger is used to create loggers for other entities.  It must not be\n\t// nil.\n\tbaseLogger *slog.Logger\n\n\t// confModifier is used to update the global configuration.\n\tconfModifier agh.ConfigModifier\n\n\t// httpReg registers HTTP handlers.  It must not be nil.\n\thttpReg aghhttp.Registrar\n\n\t// tlsManager contains the current configuration and state of TLS\n\t// encryption.  It must not be nil.\n\ttlsManager *tlsManager\n\n\t// auth stores web user information and handles authentication.  It must not\n\t// be nil.\n\tauth *auth\n\n\t// mux is the default *http.ServeMux, the same as [globalContext.mux].  It\n\t// must not be nil.\n\tmux *http.ServeMux\n\n\t// clientFS is used to initialize file server.  It must not be nil.\n\tclientFS fs.FS\n\n\t// BindAddr is the binding address with port for plain HTTP web interface.\n\tBindAddr netip.AddrPort\n\n\t// workDir is the base working directory.\n\tworkDir string\n\n\t// confPath is the configuration file path.\n\tconfPath string\n\n\t// ReadTimeout is an option to pass to http.Server for setting an\n\t// appropriate field.\n\tReadTimeout time.Duration\n\n\t// ReadHeaderTimeout is an option to pass to http.Server for setting an\n\t// appropriate field.\n\tReadHeaderTimeout time.Duration\n\n\t// WriteTimeout is an option to pass to http.Server for setting an\n\t// appropriate field.\n\tWriteTimeout time.Duration\n\n\t// defaultWebPort is the suggested default HTTP port for the install wizard.\n\tdefaultWebPort uint16\n\n\t// firstRun, if true, tells AdGuard Home to register install handlers.\n\tfirstRun bool\n\n\t// disableUpdate, if true, tells AdGuard Home to not check for updates.\n\tdisableUpdate bool\n\n\t// runningAsService flag is set to true when options are passed from the\n\t// service runner.\n\trunningAsService bool\n\n\t// serveHTTP3, if true, tells AdGuard Home to start HTTP3 server.\n\tserveHTTP3 bool\n}\n\n// httpsServer contains the data for the HTTPS server.\ntype httpsServer struct {\n\t// server is the pre-HTTP/3 HTTPS server.\n\tserver *http.Server\n\t// server3 is the HTTP/3 HTTPS server.  If it is not nil,\n\t// [httpsServer.server] must also be non-nil.\n\tserver3 *http3.Server\n\n\t// TODO(a.garipov): Why is there a *sync.Cond here?  Remove.\n\tcond       *sync.Cond\n\tcondLock   sync.Mutex\n\tcert       tls.Certificate\n\tinShutdown bool\n\tenabled    bool\n}\n\n// webAPI is the web UI and API server.\ntype webAPI struct {\n\tconf *webAPIConfig\n\n\t// confModifier is used to update the global configuration.\n\tconfModifier agh.ConfigModifier\n\n\t// cmdCons is used to run external commands.\n\tcmdCons executil.CommandConstructor\n\n\t// httpReg registers HTTP handlers.\n\thttpReg aghhttp.Registrar\n\n\t// TODO(a.garipov): Refactor all these servers.\n\thttpServer *http.Server\n\n\t// logger is a slog logger used in webAPI. It must not be nil.\n\tlogger *slog.Logger\n\n\t// baseLogger is used to create loggers for other entities.  It must not be\n\t// nil.\n\tbaseLogger *slog.Logger\n\n\t// tlsManager contains the current configuration and state of TLS\n\t// encryption.\n\ttlsManager *tlsManager\n\n\t// auth stores web user information and handles authentication.\n\tauth *auth\n\n\t// httpsServer is the server that handles HTTPS traffic.  If it is not nil,\n\t// [Web.http3Server] must also not be nil.\n\thttpsServer httpsServer\n\n\t// startTime is the start time of the web API server in Unix milliseconds.\n\tstartTime time.Time\n}\n\n// newWebAPI creates a new instance of the web UI and API server.  conf must be\n// valid.\n//\n// TODO(a.garipov):  Return a proper error.\nfunc newWebAPI(ctx context.Context, conf *webAPIConfig) (w *webAPI) {\n\tconf.logger.InfoContext(ctx, \"initializing\")\n\n\tw = &webAPI{\n\t\tconf:         conf,\n\t\tconfModifier: conf.confModifier,\n\t\thttpReg:      conf.httpReg,\n\t\tcmdCons:      conf.CommandConstructor,\n\t\tlogger:       conf.logger,\n\t\tbaseLogger:   conf.baseLogger,\n\t\ttlsManager:   conf.tlsManager,\n\t\tauth:         conf.auth,\n\t\tstartTime:    time.Now(),\n\t}\n\n\tclientFS := http.FileServer(http.FS(conf.clientFS))\n\n\tmux := conf.mux\n\t// if not configured, redirect / to /install.html, otherwise redirect /install.html to /\n\tmux.Handle(\"/\", withMiddlewares(clientFS, gziphandler.GzipHandler, w.postInstallHandler))\n\n\t// add handlers for /install paths, we only need them when we're not configured yet\n\tif conf.firstRun {\n\t\tconf.logger.InfoContext(\n\t\t\tctx,\n\t\t\t\"This is the first launch of AdGuard Home, redirecting everything to /install.html\",\n\t\t)\n\n\t\tmux.Handle(\"/install.html\", w.preInstallHandler(clientFS))\n\t\tw.registerInstallHandlers()\n\t} else {\n\t\tw.registerControlHandlers()\n\t}\n\n\tw.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock)\n\n\treturn w\n}\n\n// tlsConfigChanged updates the TLS configuration and restarts the HTTPS server\n// if necessary.  tlsConf must not be nil.\nfunc (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf *tlsConfigSettings) {\n\tdefer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)\n\n\tweb.logger.DebugContext(ctx, \"applying new tls configuration\")\n\n\tenabled := tlsConf.Enabled &&\n\t\ttlsConf.PortHTTPS != 0 &&\n\t\tlen(tlsConf.PrivateKeyData) != 0 &&\n\t\tlen(tlsConf.CertificateChainData) != 0\n\tvar cert tls.Certificate\n\tvar err error\n\tif enabled {\n\t\tcert, err = tls.X509KeyPair(tlsConf.CertificateChainData, tlsConf.PrivateKeyData)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tweb.httpsServer.cond.L.Lock()\n\tif web.httpsServer.server != nil {\n\t\tvar cancel context.CancelFunc\n\t\tctx, cancel = context.WithTimeout(ctx, shutdownTimeout)\n\t\tshutdownSrv(ctx, web.logger, web.httpsServer.server)\n\t\tshutdownSrv3(ctx, web.logger, web.httpsServer.server3)\n\n\t\tcancel()\n\t}\n\n\tweb.httpsServer.enabled = enabled\n\tweb.httpsServer.cert = cert\n\tweb.httpsServer.cond.Broadcast()\n\tweb.httpsServer.cond.L.Unlock()\n}\n\n// loggerKeyServer is the key used by [webAPI] to identify servers.\nconst loggerKeyServer = \"server\"\n\n// start - start serving HTTP requests\nfunc (web *webAPI) start(ctx context.Context) {\n\tdefer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)\n\n\tweb.logger.InfoContext(ctx, \"AdGuard Home is available at the following addresses:\")\n\n\t// for https, we have a separate goroutine loop\n\tgo web.tlsServerLoop(ctx)\n\n\t// this loop is used as an ability to change listening host and/or port\n\tfor !web.httpsServer.inShutdown {\n\t\tprintHTTPAddresses(urlutil.SchemeHTTP, web.tlsManager)\n\t\terrs := make(chan error, 2)\n\n\t\thdlr := withMiddlewares(web.conf.mux, limitRequestBody)\n\n\t\tlogger := web.baseLogger.With(loggerKeyServer, \"plain\")\n\n\t\t// TODO(a.garipov):  Remove other logs like this in other code.\n\t\tlogMw := httputil.NewLogMiddleware(logger, slog.LevelDebug)\n\t\thdlr = logMw.Wrap(hdlr)\n\n\t\thdlr = web.auth.middleware().Wrap(hdlr)\n\n\t\t// Use an h2c handler to support unencrypted HTTP/2, e.g. for proxies.\n\t\t//\n\t\t// NOTE:  The auth middleware must be inside the h2c handler to ensure\n\t\t// it applies to upgraded HTTP/2 connections as well.  See AG-51779.\n\t\thdlr = h2c.NewHandler(hdlr, &http2.Server{})\n\n\t\t// Create a new instance, because the Web is not usable after Shutdown.\n\t\tweb.httpServer = &http.Server{\n\t\t\tAddr:              web.conf.BindAddr.String(),\n\t\t\tHandler:           hdlr,\n\t\t\tReadTimeout:       web.conf.ReadTimeout,\n\t\t\tReadHeaderTimeout: web.conf.ReadHeaderTimeout,\n\t\t\tWriteTimeout:      web.conf.WriteTimeout,\n\t\t\tErrorLog:          slog.NewLogLogger(logger.Handler(), slog.LevelError),\n\t\t}\n\t\tgo func() {\n\t\t\tdefer slogutil.RecoverAndLog(ctx, logger)\n\n\t\t\tlogger.InfoContext(ctx, \"starting plain server\", \"addr\", web.httpServer.Addr)\n\n\t\t\terrs <- web.httpServer.ListenAndServe()\n\t\t}()\n\n\t\terr := <-errs\n\t\tif !errors.Is(err, http.ErrServerClosed) {\n\t\t\tcleanupAlways()\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// We use ErrServerClosed as a sign that we need to rebind on a new\n\t\t// address, so go back to the start of the loop.\n\t}\n}\n\n// close gracefully shuts down the HTTP servers.\nfunc (web *webAPI) close(ctx context.Context) {\n\tweb.logger.InfoContext(ctx, \"stopping http server\")\n\n\tweb.httpsServer.cond.L.Lock()\n\tweb.httpsServer.inShutdown = true\n\tweb.httpsServer.cond.L.Unlock()\n\n\tvar cancel context.CancelFunc\n\tctx, cancel = context.WithTimeout(ctx, shutdownTimeout)\n\tdefer cancel()\n\n\tshutdownSrv(ctx, web.logger, web.httpsServer.server)\n\tshutdownSrv3(ctx, web.logger, web.httpsServer.server3)\n\tshutdownSrv(ctx, web.logger, web.httpServer)\n\n\tif web.auth != nil {\n\t\tweb.auth.close(ctx)\n\t}\n\n\tweb.logger.InfoContext(ctx, \"stopped http server\")\n}\n\n// tlsServerLoop implements retry logic for http server start.\nfunc (web *webAPI) tlsServerLoop(ctx context.Context) {\n\tdefer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)\n\n\tfor {\n\t\tshouldContinue := web.serveTLS(ctx)\n\t\tif !shouldContinue {\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// serveTLS initializes and starts the HTTPS server.  Returns true when next\n// retry is necessary.\nfunc (web *webAPI) serveTLS(ctx context.Context) (next bool) {\n\tif !web.waitForTLSReady() {\n\t\treturn false\n\t}\n\n\tvar portHTTPS uint16\n\tfunc() {\n\t\tconfig.RLock()\n\t\tdefer config.RUnlock()\n\n\t\tportHTTPS = config.TLS.PortHTTPS\n\t}()\n\n\taddr := netip.AddrPortFrom(web.conf.BindAddr.Addr(), portHTTPS).String()\n\tlogger := web.baseLogger.With(loggerKeyServer, \"https\")\n\n\t// TODO(a.garipov):  Remove other logs like this in other code.\n\tlogMw := httputil.NewLogMiddleware(logger, slog.LevelDebug)\n\thdlr := logMw.Wrap(withMiddlewares(web.conf.mux, limitRequestBody))\n\n\tweb.httpsServer.server = &http.Server{\n\t\tAddr:    addr,\n\t\tHandler: web.auth.middleware().Wrap(hdlr),\n\t\tTLSConfig: &tls.Config{\n\t\t\tCertificates: []tls.Certificate{web.httpsServer.cert},\n\t\t\tRootCAs:      web.tlsManager.rootCerts,\n\t\t\tCipherSuites: web.tlsManager.customCipherIDs,\n\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t},\n\t\tReadTimeout:       web.conf.ReadTimeout,\n\t\tReadHeaderTimeout: web.conf.ReadHeaderTimeout,\n\t\tWriteTimeout:      web.conf.WriteTimeout,\n\t\tErrorLog:          slog.NewLogLogger(logger.Handler(), slog.LevelError),\n\t}\n\n\tprintHTTPAddresses(urlutil.SchemeHTTPS, web.tlsManager)\n\n\tif web.conf.serveHTTP3 {\n\t\tgo web.mustStartHTTP3(ctx, addr)\n\t}\n\n\tlogger.InfoContext(ctx, \"starting https server\")\n\terr := web.httpsServer.server.ListenAndServeTLS(\"\", \"\")\n\tif !errors.Is(err, http.ErrServerClosed) {\n\t\tcleanupAlways()\n\t\tpanic(fmt.Errorf(\"https: %w\", err))\n\t}\n\n\treturn true\n}\n\n// waitForTLSReady blocks until the HTTPS server is enabled or a shutdown signal\n// is received.  Returns true when server is ready.\nfunc (web *webAPI) waitForTLSReady() (ok bool) {\n\tweb.httpsServer.cond.L.Lock()\n\tdefer web.httpsServer.cond.L.Unlock()\n\n\tif web.httpsServer.inShutdown {\n\t\treturn false\n\t}\n\n\t// this mechanism doesn't let us through until all conditions are met\n\tfor !web.httpsServer.enabled { // sleep until necessary data is supplied\n\t\tweb.httpsServer.cond.Wait()\n\t\tif web.httpsServer.inShutdown {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// mustStartHTTP3 initializes and starts HTTP3 server.\nfunc (web *webAPI) mustStartHTTP3(ctx context.Context, address string) {\n\tdefer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)\n\n\tweb.httpsServer.server3 = &http3.Server{\n\t\t// TODO(a.garipov): See if there is a way to use the error log as\n\t\t// well as timeouts here.\n\t\tAddr: address,\n\t\tTLSConfig: &tls.Config{\n\t\t\tCertificates: []tls.Certificate{web.httpsServer.cert},\n\t\t\tRootCAs:      web.tlsManager.rootCerts,\n\t\t\tCipherSuites: web.tlsManager.customCipherIDs,\n\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t},\n\t\tHandler: web.auth.middleware().Wrap(withMiddlewares(web.conf.mux, limitRequestBody)),\n\t}\n\n\tweb.logger.DebugContext(ctx, \"starting http/3 server\")\n\terr := web.httpsServer.server3.ListenAndServe()\n\tif !errors.Is(err, http.ErrServerClosed) {\n\t\tcleanupAlways()\n\t\tpanic(fmt.Errorf(\"http3: %w\", err))\n\t}\n}\n\n// startPprof launches the debug and profiling server on the provided port.\nfunc startPprof(baseLogger *slog.Logger, port uint16) {\n\taddr := netip.AddrPortFrom(netutil.IPv4Localhost(), port)\n\n\truntime.SetBlockProfileRate(1)\n\truntime.SetMutexProfileFraction(1)\n\n\tmux := http.NewServeMux()\n\thttputil.RoutePprof(mux)\n\n\tctx := context.Background()\n\tlogger := baseLogger.With(slogutil.KeyPrefix, \"pprof\")\n\n\tgo func() {\n\t\tdefer slogutil.RecoverAndLog(ctx, logger)\n\n\t\tlogger.InfoContext(ctx, \"listening\", \"addr\", addr)\n\t\terr := http.ListenAndServe(addr.String(), mux)\n\t\tif !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlogger.ErrorContext(ctx, \"shutting down\", slogutil.KeyError, err)\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "internal/ipset/ipset.go",
    "content": "// Package ipset provides ipset functionality.\npackage ipset\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net\"\n)\n\n// Manager is the ipset manager interface.\n//\n// TODO(a.garipov): Perhaps generalize this into some kind of a NetFilter type,\n// since ipset is exclusive to Linux?\ntype Manager interface {\n\tAdd(ctx context.Context, host string, ip4s, ip6s []net.IP) (n int, err error)\n\tClose() (err error)\n}\n\n// Config is the configuration structure for the ipset manager.\ntype Config struct {\n\t// Logger is used for logging the operation of the ipset manager.  It must\n\t// not be nil.\n\tLogger *slog.Logger\n\n\t// Lines is the ipset configuration with the following syntax:\n\t//\n\t//\tDOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...\n\t//\n\t// Lines must not contain any blank lines or comments.\n\tLines []string\n}\n\n// NewManager returns a new ipset manager.  IPv4 addresses are added to an ipset\n// with an ipv4 family; IPv6 addresses, to an ipv6 ipset.  ipset must exist.\n//\n// If conf.Lines is empty, mgr and err are nil.  The error's chain contains\n// [errors.ErrUnsupported] if current OS is not supported.\nfunc NewManager(ctx context.Context, conf *Config) (mgr Manager, err error) {\n\tif len(conf.Lines) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treturn newManager(ctx, conf)\n}\n"
  },
  {
    "path": "internal/ipset/ipset_linux.go",
    "content": "//go:build linux\n\npackage ipset\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/digineo/go-ipset/v2\"\n\t\"github.com/mdlayher/netlink\"\n\t\"github.com/ti-mo/netfilter\"\n\t\"golang.org/x/sys/unix\"\n)\n\n// How to test on a real Linux machine:\n//\n//  1. Run \"sudo ipset create example_set hash:ip family ipv4\".\n//\n//  2. Run \"sudo ipset list example_set\".  The Members field should be empty.\n//\n//  3. Add the line \"example.com/example_set\" to your AdGuardHome.yaml.\n//\n//  4. Start AdGuardHome.\n//\n//  5. Make requests to example.com and its subdomains.\n//\n//  6. Run \"sudo ipset list example_set\".  The Members field should contain the\n//     resolved IP addresses.\n\n// newManager returns a new Linux ipset manager.\nfunc newManager(ctx context.Context, conf *Config) (set Manager, err error) {\n\treturn newManagerWithDialer(ctx, conf, defaultDial)\n}\n\n// defaultDial is the default netfilter dialing function.\nfunc defaultDial(pf netfilter.ProtoFamily, conf *netlink.Config) (conn ipsetConn, err error) {\n\tc, err := ipset.Dial(pf, conf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &queryConn{c}, nil\n}\n\n// queryConn is the [ipsetConn] implementation with listAll method, which\n// returns the list of properties of all available ipsets.\ntype queryConn struct {\n\t*ipset.Conn\n}\n\n// type check\nvar _ ipsetConn = (*queryConn)(nil)\n\n// listAll returns the list of properties of all available ipsets.\n//\n// TODO(s.chzhen):  Use https://github.com/vishvananda/netlink.\nfunc (qc *queryConn) listAll() (sets []props, err error) {\n\tmsg, err := netfilter.MarshalNetlink(\n\t\tnetfilter.Header{\n\t\t\t// The family doesn't seem to matter.  See TODO on parseIpsetConfig.\n\t\t\tFamily:      qc.Conn.Family,\n\t\t\tSubsystemID: netfilter.NFSubsysIPSet,\n\t\t\tMessageType: netfilter.MessageType(ipset.CmdList),\n\t\t\tFlags:       netlink.Request | netlink.Dump,\n\t\t},\n\t\t[]netfilter.Attribute{{\n\t\t\tType: uint16(ipset.AttrProtocol),\n\t\t\tData: []byte{ipset.Protocol},\n\t\t}},\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"marshaling netlink msg: %w\", err)\n\t}\n\n\t// We assume it's OK to call a method of an unexported type\n\t// [ipset.connector], since there is no negative effects.\n\tms, err := qc.Conn.Conn.Query(msg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"querying netlink msg: %w\", err)\n\t}\n\n\tfor i, s := range ms {\n\t\tp := props{}\n\t\terr = p.unmarshalMessage(s)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unmarshaling netlink msg at index %d: %w\", i, err)\n\t\t}\n\n\t\tsets = append(sets, p)\n\t}\n\n\treturn sets, nil\n}\n\n// ipsetConn is the ipset conn interface.\ntype ipsetConn interface {\n\tAdd(name string, entries ...*ipset.Entry) (err error)\n\tClose() (err error)\n\tHeader(name string) (p *ipset.HeaderPolicy, err error)\n\tlistAll() (sets []props, err error)\n}\n\n// dialer creates an ipsetConn.\ntype dialer func(pf netfilter.ProtoFamily, conf *netlink.Config) (conn ipsetConn, err error)\n\n// props contains one Linux Netfilter ipset properties.\ntype props struct {\n\t// name of the ipset.\n\tname string\n\n\t// typeName of the ipset.\n\ttypeName string\n\n\t// family of the IP addresses in the ipset.\n\tfamily netfilter.ProtoFamily\n\n\t// isPersistent indicates that ipset has no timeout parameter and all\n\t// entries are added permanently.\n\tisPersistent bool\n}\n\n// unmarshalMessage unmarshals netlink message and sets the properties of the\n// ipset.\nfunc (p *props) unmarshalMessage(msg netlink.Message) (err error) {\n\t_, attrs, err := netfilter.UnmarshalNetlink(msg)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\t// By default ipset has no timeout parameter.\n\tp.isPersistent = true\n\n\tfor _, a := range attrs {\n\t\tp.parseAttribute(a)\n\t}\n\n\treturn nil\n}\n\n// parseAttribute parses netfilter attribute and sets the name and family of\n// the ipset.\nfunc (p *props) parseAttribute(a netfilter.Attribute) {\n\tswitch ipset.AttributeType(a.Type) {\n\tcase ipset.AttrData:\n\t\tp.parseAttrData(a)\n\tcase ipset.AttrSetName:\n\t\t// Trim the null character.\n\t\tp.name = string(bytes.Trim(a.Data, \"\\x00\"))\n\tcase ipset.AttrTypeName:\n\t\tp.typeName = string(bytes.Trim(a.Data, \"\\x00\"))\n\tcase ipset.AttrFamily:\n\t\tp.family = netfilter.ProtoFamily(a.Data[0])\n\tdefault:\n\t\t// Go on.\n\t}\n}\n\n// parseAttrData parses attribute data and sets the timeout of the ipset.\nfunc (p *props) parseAttrData(a netfilter.Attribute) {\n\tfor _, a := range a.Children {\n\t\tswitch ipset.AttributeType(a.Type) {\n\t\tcase ipset.AttrTimeout:\n\t\t\ttimeout := a.Uint32()\n\t\t\tp.isPersistent = timeout == 0\n\t\tdefault:\n\t\t\t// Go on.\n\t\t}\n\t}\n}\n\n// manager is the Linux Netfilter ipset manager.\ntype manager struct {\n\tnameToIpset    map[string]props\n\tdomainToIpsets map[string][]props\n\n\tlogger *slog.Logger\n\n\tdial dialer\n\n\t// mu protects all properties below.\n\tmu *sync.Mutex\n\n\t// TODO(a.garipov): Currently, the ipset list is static, and we don't read\n\t// the IPs already in sets, so we can assume that all incoming IPs are\n\t// either added to all corresponding ipsets or not.  When that stops being\n\t// the case, for example if we add dynamic reconfiguration of ipsets, this\n\t// map will need to become a per-ipset-name one.\n\taddedIPs *container.MapSet[ipInIpsetEntry]\n\n\tipv4Conn ipsetConn\n\tipv6Conn ipsetConn\n}\n\n// ipInIpsetEntry is the type for entries in [manager.addIPs].\ntype ipInIpsetEntry struct {\n\tipsetName string\n\t// TODO(schzen):  Use netip.Addr.\n\tipArr [net.IPv6len]byte\n}\n\n// dialNetfilter establishes connections to Linux's netfilter module.\nfunc (m *manager) dialNetfilter(conf *netlink.Config) (err error) {\n\t// The kernel API does not actually require two sockets but package\n\t// github.com/digineo/go-ipset does.\n\t//\n\t// TODO(a.garipov): Perhaps we can ditch package ipset altogether and just\n\t// use packages netfilter and netlink.\n\tm.ipv4Conn, err = m.dial(netfilter.ProtoIPv4, conf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dialing v4: %w\", err)\n\t}\n\n\tm.ipv6Conn, err = m.dial(netfilter.ProtoIPv6, conf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dialing v6: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// parseIpsetConfigLine parses one ipset configuration line.\nfunc parseIpsetConfigLine(confStr string) (hosts, ipsetNames []string, err error) {\n\tconfStr = strings.TrimSpace(confStr)\n\thostsAndNames := strings.Split(confStr, \"/\")\n\tif len(hostsAndNames) != 2 {\n\t\treturn nil, nil, fmt.Errorf(\"invalid value %q: expected one slash\", confStr)\n\t}\n\n\thosts = strings.Split(hostsAndNames[0], \",\")\n\tipsetNames = strings.Split(hostsAndNames[1], \",\")\n\n\tif len(ipsetNames) == 0 {\n\t\treturn nil, nil, nil\n\t}\n\n\tfor i := range ipsetNames {\n\t\tipsetNames[i] = strings.TrimSpace(ipsetNames[i])\n\t\tif len(ipsetNames[i]) == 0 {\n\t\t\treturn nil, nil, fmt.Errorf(\"invalid value %q: empty ipset name\", confStr)\n\t\t}\n\t}\n\n\tfor i := range hosts {\n\t\thosts[i] = strings.ToLower(strings.TrimSpace(hosts[i]))\n\t}\n\n\treturn hosts, ipsetNames, nil\n}\n\n// parseIpsetConfig parses the ipset configuration and stores ipsets.  It\n// returns an error if the configuration can't be used.\nfunc (m *manager) parseIpsetConfig(ctx context.Context, ipsetConf []string) (err error) {\n\t// The family doesn't seem to matter when we use a header query, so query\n\t// only the IPv4 one.\n\t//\n\t// TODO(a.garipov): Find out if this is a bug or a feature.\n\tall, err := m.ipv4Conn.listAll()\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tcurrentlyKnown := map[string]props{}\n\tfor _, p := range all {\n\t\tcurrentlyKnown[p.name] = p\n\t}\n\n\tfor i, confStr := range ipsetConf {\n\t\tvar hosts, ipsetNames []string\n\t\thosts, ipsetNames, err = parseIpsetConfigLine(confStr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"config line at idx %d: %w\", i, err)\n\t\t}\n\n\t\tvar ipsets []props\n\t\tipsets, err = m.ipsets(ctx, ipsetNames, currentlyKnown)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"getting ipsets from config line at idx %d: %w\", i, err)\n\t\t}\n\n\t\tfor _, host := range hosts {\n\t\t\tm.domainToIpsets[host] = append(m.domainToIpsets[host], ipsets...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ipsetProps returns the properties of an ipset with the given name.\n//\n// Additional header data query.  See https://github.com/AdguardTeam/AdGuardHome/issues/6420.\n//\n// TODO(s.chzhen):  Use *props.\nfunc (m *manager) ipsetProps(name string) (p props, err error) {\n\t// The family doesn't seem to matter when we use a header query, so\n\t// query only the IPv4 one.\n\t//\n\t// TODO(a.garipov): Find out if this is a bug or a feature.\n\tvar res *ipset.HeaderPolicy\n\tres, err = m.ipv4Conn.Header(name)\n\tif err != nil {\n\t\treturn props{}, err\n\t}\n\n\tif res == nil || res.Family == nil {\n\t\treturn props{}, errors.Error(\"empty response or no family data\")\n\t}\n\n\tfamily := netfilter.ProtoFamily(res.Family.Value)\n\tif family != netfilter.ProtoIPv4 && family != netfilter.ProtoIPv6 {\n\t\treturn props{}, fmt.Errorf(\"unexpected ipset family %q\", family)\n\t}\n\n\ttypeName := res.TypeName.Get()\n\n\treturn props{\n\t\tname:         name,\n\t\ttypeName:     typeName,\n\t\tfamily:       family,\n\t\tisPersistent: false,\n\t}, nil\n}\n\n// ipsets returns ipset properties of currently known ipsets.  It also makes an\n// additional ipset header data query if needed.\nfunc (m *manager) ipsets(\n\tctx context.Context,\n\tnames []string,\n\tcurrentlyKnown map[string]props,\n) (sets []props, err error) {\n\tfor _, n := range names {\n\t\tp, ok := currentlyKnown[n]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"unknown ipset %q\", n)\n\t\t}\n\n\t\tif p.family != netfilter.ProtoIPv4 && p.family != netfilter.ProtoIPv6 {\n\t\t\tm.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"got unexpected ipset family while getting set properties\",\n\t\t\t\t\"set_name\", p.name,\n\t\t\t\t\"set_type\", p.typeName,\n\t\t\t\t\"set_family\", p.family,\n\t\t\t)\n\n\t\t\tp, err = m.ipsetProps(n)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"%q %q making header query: %w\", p.name, p.typeName, err)\n\t\t\t}\n\t\t}\n\n\t\tm.nameToIpset[n] = p\n\t\tsets = append(sets, p)\n\t}\n\n\treturn sets, nil\n}\n\n// newManagerWithDialer returns a new Linux ipset manager using the provided\n// dialer.\nfunc newManagerWithDialer(ctx context.Context, conf *Config, dial dialer) (mgr Manager, err error) {\n\tdefer func() { err = errors.Annotate(err, \"ipset: %w\") }()\n\n\tm := &manager{\n\t\tmu: &sync.Mutex{},\n\n\t\tnameToIpset:    make(map[string]props),\n\t\tdomainToIpsets: make(map[string][]props),\n\n\t\tlogger: conf.Logger,\n\n\t\tdial: dial,\n\n\t\taddedIPs: container.NewMapSet[ipInIpsetEntry](),\n\t}\n\n\terr = m.dialNetfilter(&netlink.Config{})\n\tif err != nil {\n\t\tif errors.Is(err, unix.EPROTONOSUPPORT) {\n\t\t\t// The implementation doesn't support this protocol version.  Just\n\t\t\t// issue a warning.\n\t\t\tm.logger.WarnContext(ctx, \"dialing netfilter\", slogutil.KeyError, err)\n\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"dialing netfilter: %w\", err)\n\t}\n\n\terr = m.parseIpsetConfig(ctx, conf.Lines)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"getting ipsets: %w\", err)\n\t}\n\n\tm.logger.DebugContext(ctx, \"initialized\")\n\n\treturn m, nil\n}\n\n// lookupHost find the ipsets for the host, taking subdomain wildcards into\n// account.\nfunc (m *manager) lookupHost(host string) (sets []props) {\n\t// Search for matching ipset hosts starting with most specific domain.\n\t// We could use a trie here but the simple, inefficient solution isn't\n\t// that expensive: ~10 ns for TLD + SLD vs. ~140 ns for 10 subdomains on\n\t// an AMD Ryzen 7 PRO 4750U CPU; ~120 ns vs. ~ 1500 ns on a Raspberry\n\t// Pi's ARMv7 rev 4 CPU.\n\tfor i := 0; ; i++ {\n\t\thost = host[i:]\n\t\tsets = m.domainToIpsets[host]\n\t\tif sets != nil {\n\t\t\treturn sets\n\t\t}\n\n\t\ti = strings.Index(host, \".\")\n\t\tif i == -1 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Check the root catch-all one.\n\treturn m.domainToIpsets[\"\"]\n}\n\n// addIPs adds the IP addresses for the host to the ipset.  set must be same\n// family as set's family.\nfunc (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error) {\n\tif len(ips) == 0 {\n\t\treturn 0, nil\n\t}\n\n\tvar entries []*ipset.Entry\n\tvar newAddedEntries []ipInIpsetEntry\n\tfor _, ip := range ips {\n\t\te := ipInIpsetEntry{\n\t\t\tipsetName: set.name,\n\t\t}\n\t\tcopy(e.ipArr[:], ip.To16())\n\n\t\tif m.addedIPs.Has(e) {\n\t\t\tcontinue\n\t\t}\n\n\t\tentries = append(entries, ipset.NewEntry(ipset.EntryIP(ip)))\n\t\tnewAddedEntries = append(newAddedEntries, e)\n\t}\n\n\tn = len(entries)\n\tif n == 0 {\n\t\treturn 0, nil\n\t}\n\n\tvar conn ipsetConn\n\tswitch set.family {\n\tcase netfilter.ProtoIPv4:\n\t\tconn = m.ipv4Conn\n\tcase netfilter.ProtoIPv6:\n\t\tconn = m.ipv6Conn\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"unexpected family %s for ipset %q\", set.family, set.name)\n\t}\n\n\terr = conn.Add(set.name, entries...)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"adding %q%s to %q %q: %w\", host, ips, set.name, set.typeName, err)\n\t}\n\n\t// Only add these to the cache once we're sure that all of them were\n\t// actually sent to the ipset.\n\tfor _, e := range newAddedEntries {\n\t\ts := m.nameToIpset[e.ipsetName]\n\t\tif s.isPersistent {\n\t\t\tm.addedIPs.Add(e)\n\t\t}\n\t}\n\n\treturn n, nil\n}\n\n// addToSets adds the IP addresses to the corresponding ipset.\nfunc (m *manager) addToSets(\n\tctx context.Context,\n\thost string,\n\tip4s []net.IP,\n\tip6s []net.IP,\n\tsets []props,\n) (n int, err error) {\n\tfor _, set := range sets {\n\t\tvar nn int\n\t\tswitch set.family {\n\t\tcase netfilter.ProtoIPv4:\n\t\t\tnn, err = m.addIPs(host, set, ip4s)\n\t\t\tif err != nil {\n\t\t\t\treturn n, err\n\t\t\t}\n\t\tcase netfilter.ProtoIPv6:\n\t\t\tnn, err = m.addIPs(host, set, ip6s)\n\t\t\tif err != nil {\n\t\t\t\treturn n, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn n, fmt.Errorf(\"%q %q unexpected family %q\", set.name, set.typeName, set.family)\n\t\t}\n\n\t\tm.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"added ips to set\",\n\t\t\t\"ips_num\", nn,\n\t\t\t\"set_name\", set.name,\n\t\t\t\"set_type\", set.typeName,\n\t\t)\n\n\t\tn += nn\n\t}\n\n\treturn n, nil\n}\n\n// Add implements the [Manager] interface for *manager.\nfunc (m *manager) Add(ctx context.Context, host string, ip4s, ip6s []net.IP) (n int, err error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tsets := m.lookupHost(host)\n\tif len(sets) == 0 {\n\t\treturn 0, nil\n\t}\n\n\tm.logger.DebugContext(ctx, \"found sets\", \"set_num\", len(sets))\n\n\treturn m.addToSets(ctx, host, ip4s, ip6s, sets)\n}\n\n// Close implements the [Manager] interface for *manager.\nfunc (m *manager) Close() (err error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tvar errs []error\n\n\t// Close both and collect errors so that the errors from closing one\n\t// don't interfere with closing the other.\n\terr = m.ipv4Conn.Close()\n\tif err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\terr = m.ipv6Conn.Close()\n\tif err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\treturn errors.Annotate(errors.Join(errs...), \"closing ipsets: %w\")\n}\n"
  },
  {
    "path": "internal/ipset/ipset_linux_internal_test.go",
    "content": "//go:build linux\n\npackage ipset\n\nimport (\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/digineo/go-ipset/v2\"\n\t\"github.com/mdlayher/netlink\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/ti-mo/netfilter\"\n)\n\n// testTimeout is a common timeout for tests and contexts.\nconst testTimeout = 1 * time.Second\n\n// fakeConn is a fake ipsetConn for tests.\ntype fakeConn struct {\n\tipv4Header  *ipset.HeaderPolicy\n\tipv4Entries *[]*ipset.Entry\n\tipv6Header  *ipset.HeaderPolicy\n\tipv6Entries *[]*ipset.Entry\n\tsets        []props\n}\n\n// type check\nvar _ ipsetConn = (*fakeConn)(nil)\n\n// Add implements the [ipsetConn] interface for *fakeConn.\nfunc (c *fakeConn) Add(name string, entries ...*ipset.Entry) (err error) {\n\tif strings.Contains(name, \"ipv4\") {\n\t\t*c.ipv4Entries = append(*c.ipv4Entries, entries...)\n\n\t\treturn nil\n\t} else if strings.Contains(name, \"ipv6\") {\n\t\t*c.ipv6Entries = append(*c.ipv6Entries, entries...)\n\n\t\treturn nil\n\t}\n\n\treturn errors.Error(\"test: ipset not found\")\n}\n\n// Close implements the [ipsetConn] interface for *fakeConn.\nfunc (c *fakeConn) Close() (err error) {\n\treturn nil\n}\n\n// Header implements the [ipsetConn] interface for *fakeConn.\nfunc (c *fakeConn) Header(_ string) (_ *ipset.HeaderPolicy, _ error) {\n\treturn nil, nil\n}\n\n// listAll implements the [ipsetConn] interface for *fakeConn.\nfunc (c *fakeConn) listAll() (sets []props, err error) {\n\treturn c.sets, nil\n}\n\nfunc TestManager_Add(t *testing.T) {\n\tipsetList := []string{\n\t\t\"example.com,example.net/ipv4set\",\n\t\t\"example.org,example.biz/ipv6set\",\n\t}\n\n\tvar ipv4Entries []*ipset.Entry\n\tvar ipv6Entries []*ipset.Entry\n\n\tfakeDial := func(\n\t\tpf netfilter.ProtoFamily,\n\t\tconf *netlink.Config,\n\t) (conn ipsetConn, err error) {\n\t\treturn &fakeConn{\n\t\t\tipv4Header: &ipset.HeaderPolicy{\n\t\t\t\tFamily: ipset.NewUInt8Box(uint8(netfilter.ProtoIPv4)),\n\t\t\t},\n\t\t\tipv4Entries: &ipv4Entries,\n\t\t\tipv6Header: &ipset.HeaderPolicy{\n\t\t\t\tFamily: ipset.NewUInt8Box(uint8(netfilter.ProtoIPv6)),\n\t\t\t},\n\t\t\tipv6Entries: &ipv6Entries,\n\t\t\tsets: []props{{\n\t\t\t\tname:   \"ipv4set\",\n\t\t\t\tfamily: netfilter.ProtoIPv4,\n\t\t\t}, {\n\t\t\t\tname:   \"ipv6set\",\n\t\t\t\tfamily: netfilter.ProtoIPv6,\n\t\t\t}},\n\t\t}, nil\n\t}\n\n\tconf := &Config{\n\t\tLogger: slogutil.NewDiscardLogger(),\n\t\tLines:  ipsetList,\n\t}\n\tm, err := newManagerWithDialer(testutil.ContextWithTimeout(t, testTimeout), conf, fakeDial)\n\trequire.NoError(t, err)\n\n\tip4 := net.IP{1, 2, 3, 4}\n\tip6 := net.IP{\n\t\t0x12, 0x34, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x00, 0x00,\n\t\t0x00, 0x00, 0x56, 0x78,\n\t}\n\n\tn, err := m.Add(testutil.ContextWithTimeout(t, testTimeout), \"example.net\", []net.IP{ip4}, nil)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, n)\n\n\trequire.Len(t, ipv4Entries, 1)\n\n\tgotIP4 := ipv4Entries[0].IP.Value\n\tassert.Equal(t, ip4, gotIP4)\n\n\tn, err = m.Add(testutil.ContextWithTimeout(t, testTimeout), \"example.biz\", nil, []net.IP{ip6})\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, n)\n\n\trequire.Len(t, ipv6Entries, 1)\n\n\tgotIP6 := ipv6Entries[0].IP.Value\n\tassert.Equal(t, ip6, gotIP6)\n\n\terr = m.Close()\n\tassert.NoError(t, err)\n}\n\nfunc BenchmarkManager_LookupHost(b *testing.B) {\n\tpropsLong := []props{{\n\t\tname:   \"example.com\",\n\t\tfamily: netfilter.ProtoIPv4,\n\t}}\n\n\tpropsShort := []props{{\n\t\tname:   \"example.net\",\n\t\tfamily: netfilter.ProtoIPv4,\n\t}}\n\n\tm := &manager{\n\t\tdomainToIpsets: map[string][]props{\n\t\t\t\"\":            propsLong,\n\t\t\t\"example.net\": propsShort,\n\t\t},\n\t}\n\n\tvar ipsetPropsSink []props\n\n\tb.Run(\"long\", func(b *testing.B) {\n\t\tconst name = \"a.very.long.domain.name.inside.the.domain.example.com\"\n\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tipsetPropsSink = m.lookupHost(name)\n\t\t}\n\n\t\trequire.Equal(b, propsLong, ipsetPropsSink)\n\t})\n\n\tb.Run(\"short\", func(b *testing.B) {\n\t\tconst name = \"example.net\"\n\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tipsetPropsSink = m.lookupHost(name)\n\t\t}\n\n\t\trequire.Equal(b, propsShort, ipsetPropsSink)\n\t})\n\n\t// Most recent results:\n\t//\n\t//\tgoos: linux\n\t//\tgoarch: amd64\n\t//\tpkg: github.com/AdguardTeam/AdGuardHome/internal/ipset\n\t//\tcpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz\n\t//\tBenchmarkManager_LookupHost/long-8         \t 6562424\t       174.8 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkManager_LookupHost/short-8        \t100000000\t        10.72 ns/op\t       0 B/op\t       0 allocs/op\n}\n"
  },
  {
    "path": "internal/ipset/ipset_others.go",
    "content": "//go:build !linux\n\npackage ipset\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n)\n\nfunc newManager(_ context.Context, _ *Config) (mgr Manager, err error) {\n\treturn nil, aghos.Unsupported(\"ipset\")\n}\n"
  },
  {
    "path": "internal/next/AdGuardHome.example.yaml",
    "content": "# This is a file showing example configuration for AdGuard Home.\n#\n# TODO(a.garipov): Move to the top level once the rewrite is over.\n\ndns:\n    upstream_mode: parallel\n    addresses:\n        - '0.0.0.0:53'\n    bootstrap_dns:\n        - '9.9.9.10'\n        - '149.112.112.10'\n        - '2620:fe::10'\n        - '2620:fe::fe:10'\n    upstream_dns:\n        - '8.8.8.8'\n    dns64_prefixes:\n        - '1234::/64'\n    upstream_timeout: 1s\n    cache_size: 1048576\n    ratelimit: 100\n    bootstrap_prefer_ipv6: true\n    refuse_any: true\n    use_dns64: true\nhttp:\n    pprof:\n        port: 6060\n        enabled: true\n    addresses:\n        - '0.0.0.0:3000'\n    secure_addresses: []\n    timeout: 5s\n    force_https: true\nlog:\n    verbose: true\nschema_version: 100\n"
  },
  {
    "path": "internal/next/agh/agh.go",
    "content": "// Package agh contains common entities and interfaces of AdGuard Home.\npackage agh\n\nimport (\n\t\"github.com/AdguardTeam/golibs/service\"\n)\n\n// ServiceWithConfig is an extension of the [Service] interface for services\n// that can return their configuration.\n//\n// TODO(a.garipov): Consider removing this generic interface if we figure out\n// how to make it testable in a better way.\ntype ServiceWithConfig[ConfigType any] interface {\n\tservice.Interface\n\n\t// Config returns a deep clone of the configuration of the service.\n\tConfig() (c ConfigType)\n}\n\n// type check\nvar _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)\n\n// EmptyServiceWithConfig is a ServiceWithConfig that does nothing.  Its Config\n// method returns Conf.\n//\n// TODO(a.garipov): Remove if unnecessary.\ntype EmptyServiceWithConfig[ConfigType any] struct {\n\tservice.Empty\n\n\tConf ConfigType\n}\n\n// Config implements the [ServiceWithConfig] interface for\n// *EmptyServiceWithConfig.\nfunc (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {\n\treturn s.Conf\n}\n"
  },
  {
    "path": "internal/next/changelog.md",
    "content": "# AdGuard Home v0.108.0 Changelog DRAFT\n\nThis changelog should be merged into the main one once the next API matures enough.\n\n## [v0.108.0] - TODO\n\n### Added\n\n- The ability to change the port of the pprof debug API.\n\n- The ability to log to stderr using `--logFile=stderr`.\n\n- The new `--web-addr` flag to set the Web UI address in a `host:port` form.\n\n- `SIGHUP` now reloads all configuration from the configuration file ([#5676]).\n\n### Changed\n\n#### New HTTP API\n\n**TODO(a.garipov):** Describe the new API and add a link to the new OpenAPI doc.\n\n#### Other changes\n\n- `-h` is now an alias for `--help` instead of the removed `--host`, see below. Use `--web-addr=host:port` to set an address on which to serve the Web UI.\n\n### Fixed\n\n- `--check-config` breaking the configuration file ([#4067]).\n\n- Inconsistent application of `--work-dir/-w` ([#2598], [#2902]).\n\n- The order of `-v/--verbose` and `--version` being significant ([#2893]).\n\n### Removed\n\n- The deprecated `--no-mem-optimization` and `--no-etc-hosts` flags.\n\n- `--host` and `-p/--port` flags.  Use `--web-addr=host:port` to set an address on which to serve the Web UI.  `-h` is now an alias for `--help`, see above.\n\n[#2598]: https://github.com/AdguardTeam/AdGuardHome/issues/2598\n[#2893]: https://github.com/AdguardTeam/AdGuardHome/issues/2893\n[#2902]: https://github.com/AdguardTeam/AdGuardHome/issues/2902\n[#4067]: https://github.com/AdguardTeam/AdGuardHome/issues/4067\n[#5676]: https://github.com/AdguardTeam/AdGuardHome/issues/5676\n"
  },
  {
    "path": "internal/next/cmd/cmd.go",
    "content": "// Package cmd is the AdGuard Home entry point.  It assembles the configuration\n// file manager, sets up signal processing logic, and so on.\n//\n// TODO(a.garipov): Move to the upper-level internal/.\npackage cmd\n\nimport (\n\t\"context\"\n\t\"io/fs\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/service\"\n)\n\n// Main is the entry point of AdGuard Home.\nfunc Main(embeddedFrontend fs.FS) {\n\tctx := context.Background()\n\n\tstart := time.Now()\n\n\tcmdName := os.Args[0]\n\topts, err := parseOptions(cmdName, os.Args[1:])\n\texitCode, needExit := processOptions(opts, cmdName, err)\n\tif needExit {\n\t\tos.Exit(exitCode)\n\t}\n\n\tbaseLogger := newBaseLogger(opts)\n\n\tbaseLogger.InfoContext(\n\t\tctx,\n\t\t\"starting adguard home\",\n\t\t\"version\", version.Version(),\n\t\t\"pid\", os.Getpid(),\n\t)\n\n\tif opts.workDir != \"\" {\n\t\tbaseLogger.InfoContext(ctx, \"changing working directory\", \"dir\", opts.workDir)\n\n\t\terr = os.Chdir(opts.workDir)\n\t\terrors.Check(err)\n\t}\n\n\tfrontend, err := frontendFromOpts(ctx, baseLogger, opts, embeddedFrontend)\n\terrors.Check(err)\n\n\tstartCtx, startCancel := context.WithTimeout(ctx, defaultTimeoutStart)\n\tdefer startCancel()\n\n\tconfMgrConf := &configmgr.Config{\n\t\tBaseLogger: baseLogger,\n\t\tLogger:     baseLogger.With(slogutil.KeyPrefix, \"configmgr\"),\n\t\tFrontend:   frontend,\n\t\tWebAddr:    opts.webAddr,\n\t\tStart:      start,\n\t\tFileName:   opts.confFile,\n\t}\n\n\tsvc, err := newServiceMgr(ctx, &serviceMgrConfig{\n\t\tconfMgrConf: confMgrConf,\n\t\tlogger:      baseLogger.With(slogutil.KeyPrefix, \"svc\"),\n\t\tpidFilePath: opts.pidFile,\n\t})\n\terrors.Check(err)\n\n\terrors.Check(svc.Start(startCtx))\n\n\tsigHdlr := service.NewSignalHandler(&service.SignalHandlerConfig{\n\t\tLogger: baseLogger.With(slogutil.KeyPrefix, service.SignalHandlerPrefix),\n\t})\n\n\tsigHdlr.AddService(svc)\n\tsigHdlr.AddRefresher(svc)\n\n\tos.Exit(sigHdlr.Handle(ctx))\n}\n\n// Default timeouts.\n//\n// TODO(a.garipov):  Make configurable.\nconst (\n\tdefaultTimeoutStart = 1 * time.Minute\n)\n"
  },
  {
    "path": "internal/next/cmd/log.go",
    "content": "package cmd\n\nimport (\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// newBaseLogger constructs a base logger based on the command-line options.\n// opts must not be nil.\nfunc newBaseLogger(opts *options) (baseLogger *slog.Logger) {\n\tvar output io.Writer\n\tswitch opts.confFile {\n\tcase \"stdout\":\n\t\toutput = os.Stdout\n\tcase \"stderr\":\n\t\toutput = os.Stderr\n\tcase \"syslog\":\n\t\t// TODO(a.garipov):  Add a syslog handler to golibs.\n\tdefault:\n\t\t// TODO(a.garipov):  Use the path.\n\t}\n\n\tlvl := slog.LevelInfo\n\tif opts.verbose {\n\t\tlvl = slog.LevelDebug\n\t}\n\n\treturn slogutil.New(&slogutil.Config{\n\t\tOutput: output,\n\t\t// TODO(a.garipov):  Get from config?\n\t\tFormat: slogutil.FormatText,\n\t\tLevel:  lvl,\n\t\t// TODO(a.garipov):  Get from config.\n\t\tAddTimestamp: true,\n\t})\n}\n"
  },
  {
    "path": "internal/next/cmd/opt.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/configmigrate\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n)\n\n// options contains all command-line options for the AdGuardHome(.exe) binary.\ntype options struct {\n\t// confFile is the path to the configuration file.\n\tconfFile string\n\n\t// logFile is the path to the log file.  Special values:\n\t//\n\t//   - \"stdout\":  Write to stdout (the default).\n\t//   - \"stderr\":  Write to stderr.\n\t//   - \"syslog\":  Write to the system log.\n\tlogFile string\n\n\t// pidFile is the path to the file where to store the PID.\n\tpidFile string\n\n\t// serviceAction is the service control action to perform:\n\t//\n\t//   - \"install\":  Installs AdGuard Home as a system service.\n\t//   - \"uninstall\":  Uninstalls it.\n\t//   - \"status\":  Prints the service status.\n\t//   - \"start\":  Starts the previously installed service.\n\t//   - \"stop\":  Stops the previously installed service.\n\t//   - \"restart\":  Restarts the previously installed service.\n\t//   - \"reload\":  Reloads the configuration.\n\t//   - \"run\":  This is a special command that is not supposed to be used\n\t//     directly it is specified when we register a service, and it indicates\n\t//     to the app that it is being run as a service.\n\t//\n\t// TODO(a.garipov): Use.\n\tserviceAction string\n\n\t// workDir is the path to the working directory.  It is applied before all\n\t// other configuration is read, so all relative paths are relative to it.\n\tworkDir string\n\n\t// webAddr contains the address on which to serve the web UI.\n\twebAddr netip.AddrPort\n\n\t// checkConfig, if true, instructs AdGuard Home to check the configuration\n\t// file, optionally print an error message to stdout, and exit with a\n\t// corresponding exit code.\n\tcheckConfig bool\n\n\t// disableUpdate, if true, prevents AdGuard Home from automatically checking\n\t// for updates.\n\t//\n\t// TODO(a.garipov): Use.\n\tdisableUpdate bool\n\n\t// glinetMode enables the GL-Inet compatibility mode.\n\t//\n\t// TODO(a.garipov): Use.\n\tglinetMode bool\n\n\t// help, if true, instructs AdGuard Home to print the command-line option\n\t// help message and quit with a successful exit-code.\n\thelp bool\n\n\t// localFrontend, if true, instructs AdGuard Home to use the local frontend\n\t// directory instead of the files compiled into the binary.\n\t//\n\t// TODO(a.garipov): Use.\n\tlocalFrontend bool\n\n\t// performUpdate, if true, instructs AdGuard Home to update the current\n\t// binary and restart the service in case it's installed.\n\t//\n\t// TODO(a.garipov): Use.\n\tperformUpdate bool\n\n\t// noPermCheck, if true, instructs AdGuard Home to skip checking and\n\t// migrating the permissions of its security-sensitive files.\n\t//\n\t// TODO(e.burkov):  Use.\n\tnoPermCheck bool\n\n\t// verbose, if true, instructs AdGuard Home to enable verbose logging.\n\tverbose bool\n\n\t// version, if true, instructs AdGuard Home to print the version to stdout\n\t// and quit with a successful exit-code.  If verbose is also true, print a\n\t// more detailed version description.\n\tversion bool\n}\n\n// Indexes to help with the [commandLineOptions] initialization.\nconst (\n\tconfFileIdx = iota\n\tlogFileIdx\n\tpidFileIdx\n\tserviceActionIdx\n\tworkDirIdx\n\twebAddrIdx\n\tcheckConfigIdx\n\tdisableUpdateIdx\n\tglinetModeIdx\n\thelpIdx\n\tlocalFrontendIdx\n\tnoPermCheckIdx\n\tperformUpdateIdx\n\tverboseIdx\n\tversionIdx\n)\n\n// commandLineOption contains information about a command-line option: its long\n// and, if there is one, short forms, the value type, the description, and the\n// default value.\ntype commandLineOption struct {\n\tdefaultValue any\n\tdescription  string\n\tlong         string\n\tshort        string\n\tvalueType    string\n}\n\n// commandLineOptions are all command-line options currently supported by\n// AdGuard Home.\nvar commandLineOptions = []*commandLineOption{\n\tconfFileIdx: {\n\t\t// TODO(a.garipov): Remove the directory when the new code is ready.\n\t\tdefaultValue: \"internal/next/AdGuardHome.yaml\",\n\t\tdescription:  \"Path to the config file.\",\n\t\tlong:         \"config\",\n\t\tshort:        \"c\",\n\t\tvalueType:    \"path\",\n\t},\n\n\tlogFileIdx: {\n\t\tdefaultValue: \"stdout\",\n\t\tdescription:  `Path to log file.  Special values include \"stdout\", \"stderr\", and \"syslog\".`,\n\t\tlong:         \"logfile\",\n\t\tshort:        \"l\",\n\t\tvalueType:    \"path\",\n\t},\n\n\tpidFileIdx: {\n\t\tdefaultValue: \"\",\n\t\tdescription:  \"Path to the file where to store the PID.\",\n\t\tlong:         \"pidfile\",\n\t\tshort:        \"\",\n\t\tvalueType:    \"path\",\n\t},\n\n\tserviceActionIdx: {\n\t\tdefaultValue: \"\",\n\t\tdescription: `Service control action: \"status\", \"install\" (as a service), ` +\n\t\t\t`\"uninstall\" (as a service), \"start\", \"stop\", \"restart\", \"reload\" (configuration).`,\n\t\tlong:      \"service\",\n\t\tshort:     \"s\",\n\t\tvalueType: \"action\",\n\t},\n\n\tworkDirIdx: {\n\t\tdefaultValue: \"\",\n\t\tdescription: `Path to the working directory.  ` +\n\t\t\t`It is applied before all other configuration is read, ` +\n\t\t\t`so all relative paths are relative to it.`,\n\t\tlong:      \"work-dir\",\n\t\tshort:     \"w\",\n\t\tvalueType: \"path\",\n\t},\n\n\twebAddrIdx: {\n\t\tdefaultValue: netip.AddrPort{},\n\t\tdescription:  `Address to serve the web UI on, in the host:port format.`,\n\t\tlong:         \"web-addr\",\n\t\tshort:        \"\",\n\t\tvalueType:    \"host:port\",\n\t},\n\n\tcheckConfigIdx: {\n\t\tdefaultValue: false,\n\t\tdescription:  \"Check configuration, print errors to stdout, and quit.\",\n\t\tlong:         \"check-config\",\n\t\tshort:        \"\",\n\t\tvalueType:    \"\",\n\t},\n\n\tdisableUpdateIdx: {\n\t\tdefaultValue: false,\n\t\tdescription:  \"Disable automatic update checking.\",\n\t\tlong:         \"no-check-update\",\n\t\tshort:        \"\",\n\t\tvalueType:    \"\",\n\t},\n\n\tglinetModeIdx: {\n\t\tdefaultValue: false,\n\t\tdescription:  \"Run in GL-Inet compatibility mode.\",\n\t\tlong:         \"glinet\",\n\t\tshort:        \"\",\n\t\tvalueType:    \"\",\n\t},\n\n\thelpIdx: {\n\t\tdefaultValue: false,\n\t\tdescription:  \"Print this help message and quit.\",\n\t\tlong:         \"help\",\n\t\tshort:        \"h\",\n\t\tvalueType:    \"\",\n\t},\n\n\tlocalFrontendIdx: {\n\t\tdefaultValue: false,\n\t\tdescription:  \"Use local frontend directories.\",\n\t\tlong:         \"local-frontend\",\n\t\tshort:        \"\",\n\t\tvalueType:    \"\",\n\t},\n\n\tnoPermCheckIdx: {\n\t\tdefaultValue: false,\n\t\tdescription:  \"Skip checking the permissions of security-sensitive files.\",\n\t\tlong:         \"no-permcheck\",\n\t\tshort:        \"\",\n\t\tvalueType:    \"\",\n\t},\n\n\tperformUpdateIdx: {\n\t\tdefaultValue: false,\n\t\tdescription:  \"Update the current binary and restart the service in case it's installed.\",\n\t\tlong:         \"update\",\n\t\tshort:        \"\",\n\t\tvalueType:    \"\",\n\t},\n\n\tverboseIdx: {\n\t\tdefaultValue: false,\n\t\tdescription:  \"Enable verbose logging.\",\n\t\tlong:         \"verbose\",\n\t\tshort:        \"v\",\n\t\tvalueType:    \"\",\n\t},\n\n\tversionIdx: {\n\t\tdefaultValue: false,\n\t\tdescription: `Print the version to stdout and quit.  ` +\n\t\t\t`Print a more detailed version description with -v.`,\n\t\tlong:      \"version\",\n\t\tshort:     \"\",\n\t\tvalueType: \"\",\n\t},\n}\n\n// parseOptions parses the command-line options for AdGuardHome.\nfunc parseOptions(cmdName string, args []string) (opts *options, err error) {\n\tflags := flag.NewFlagSet(cmdName, flag.ContinueOnError)\n\n\topts = &options{}\n\tfor i, fieldPtr := range []any{\n\t\tconfFileIdx:      &opts.confFile,\n\t\tlogFileIdx:       &opts.logFile,\n\t\tpidFileIdx:       &opts.pidFile,\n\t\tserviceActionIdx: &opts.serviceAction,\n\t\tworkDirIdx:       &opts.workDir,\n\t\twebAddrIdx:       &opts.webAddr,\n\t\tcheckConfigIdx:   &opts.checkConfig,\n\t\tdisableUpdateIdx: &opts.disableUpdate,\n\t\tglinetModeIdx:    &opts.glinetMode,\n\t\thelpIdx:          &opts.help,\n\t\tlocalFrontendIdx: &opts.localFrontend,\n\t\tnoPermCheckIdx:   &opts.noPermCheck,\n\t\tperformUpdateIdx: &opts.performUpdate,\n\t\tverboseIdx:       &opts.verbose,\n\t\tversionIdx:       &opts.version,\n\t} {\n\t\taddOption(flags, fieldPtr, commandLineOptions[i])\n\t}\n\n\tflags.Usage = func() { usage(cmdName, os.Stderr) }\n\n\terr = flags.Parse(args)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\treturn opts, nil\n}\n\n// addOption adds the command-line option described by o to flags using fieldPtr\n// as the pointer to the value.\nfunc addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) {\n\tswitch fieldPtr := fieldPtr.(type) {\n\tcase *string:\n\t\tflags.StringVar(fieldPtr, o.long, o.defaultValue.(string), o.description)\n\t\tif o.short != \"\" {\n\t\t\tflags.StringVar(fieldPtr, o.short, o.defaultValue.(string), o.description)\n\t\t}\n\tcase *bool:\n\t\tflags.BoolVar(fieldPtr, o.long, o.defaultValue.(bool), o.description)\n\t\tif o.short != \"\" {\n\t\t\tflags.BoolVar(fieldPtr, o.short, o.defaultValue.(bool), o.description)\n\t\t}\n\tcase encoding.TextUnmarshaler:\n\t\tflags.TextVar(fieldPtr, o.long, o.defaultValue.(encoding.TextMarshaler), o.description)\n\t\tif o.short != \"\" {\n\t\t\tflags.TextVar(fieldPtr, o.short, o.defaultValue.(encoding.TextMarshaler), o.description)\n\t\t}\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unexpected field pointer type %T\", fieldPtr))\n\t}\n}\n\n// usage prints a usage message similar to the one printed by package flag but\n// taking long vs. short versions into account as well as using more informative\n// value hints.\nfunc usage(cmdName string, output io.Writer) {\n\toptions := slices.Clone(commandLineOptions)\n\tslices.SortStableFunc(options, func(a, b *commandLineOption) (res int) {\n\t\treturn strings.Compare(a.long, b.long)\n\t})\n\n\tb := &strings.Builder{}\n\t_, _ = fmt.Fprintf(b, \"Usage of %s:\\n\", cmdName)\n\n\tfor _, o := range options {\n\t\twriteUsageLine(b, o)\n\n\t\t// Use four spaces before the tab to trigger good alignment for both 4-\n\t\t// and 8-space tab stops.\n\t\tif shouldIncludeDefault(o.defaultValue) {\n\t\t\t_, _ = fmt.Fprintf(b, \"    \\t%s  (Default value: %q)\\n\", o.description, o.defaultValue)\n\t\t} else {\n\t\t\t_, _ = fmt.Fprintf(b, \"    \\t%s\\n\", o.description)\n\t\t}\n\t}\n\n\t_, _ = io.WriteString(output, b.String())\n}\n\n// shouldIncludeDefault returns true if this default value should be printed.\nfunc shouldIncludeDefault(v any) (ok bool) {\n\tswitch v := v.(type) {\n\tcase bool:\n\t\treturn v\n\tcase string:\n\t\treturn v != \"\"\n\tdefault:\n\t\treturn v == nil\n\t}\n}\n\n// writeUsageLine writes the usage line for the provided command-line option.\nfunc writeUsageLine(b *strings.Builder, o *commandLineOption) {\n\tif o.short == \"\" {\n\t\tif o.valueType == \"\" {\n\t\t\t_, _ = fmt.Fprintf(b, \"  --%s\\n\", o.long)\n\t\t} else {\n\t\t\t_, _ = fmt.Fprintf(b, \"  --%s=%s\\n\", o.long, o.valueType)\n\t\t}\n\n\t\treturn\n\t}\n\n\tif o.valueType == \"\" {\n\t\t_, _ = fmt.Fprintf(b, \"  --%s/-%s\\n\", o.long, o.short)\n\t} else {\n\t\t_, _ = fmt.Fprintf(b, \"  --%[1]s=%[3]s/-%[2]s %[3]s\\n\", o.long, o.short, o.valueType)\n\t}\n}\n\n// processOptions decides if AdGuard Home should exit depending on the results\n// of command-line option parsing.\nfunc processOptions(\n\topts *options,\n\tcmdName string,\n\tparseErr error,\n) (exitCode int, needExit bool) {\n\tif parseErr != nil {\n\t\t// Assume that usage has already been printed.\n\t\treturn osutil.ExitCodeArgumentError, true\n\t}\n\n\tif opts.help {\n\t\tusage(cmdName, os.Stdout)\n\n\t\treturn osutil.ExitCodeSuccess, true\n\t}\n\n\tif opts.version {\n\t\tif opts.verbose {\n\t\t\tfmt.Print(version.Verbose(configmigrate.LastSchemaVersion))\n\t\t} else {\n\t\t\tfmt.Printf(\"AdGuard Home %s\\n\", version.Version())\n\t\t}\n\n\t\treturn osutil.ExitCodeSuccess, true\n\t}\n\n\tif opts.checkConfig {\n\t\terr := configmgr.Validate(opts.confFile)\n\t\tif err != nil {\n\t\t\t_, _ = io.WriteString(os.Stdout, err.Error()+\"\\n\")\n\n\t\t\treturn osutil.ExitCodeFailure, true\n\t\t}\n\n\t\treturn osutil.ExitCodeSuccess, true\n\t}\n\n\treturn 0, false\n}\n\n// frontendFromOpts returns the frontend to use based on the options.\nfunc frontendFromOpts(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\topts *options,\n\tembeddedFrontend fs.FS,\n) (frontend fs.FS, err error) {\n\tconst frontendSubdir = \"build/static\"\n\n\tif opts.localFrontend {\n\t\tlogger.WarnContext(ctx, \"using local frontend files\")\n\n\t\treturn os.DirFS(frontendSubdir), nil\n\t}\n\n\treturn fs.Sub(embeddedFrontend, frontendSubdir)\n}\n"
  },
  {
    "path": "internal/next/cmd/service.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/service\"\n\t\"github.com/google/renameio/v2/maybe\"\n)\n\n// serviceMgr manages AdGuard Home services.\ntype serviceMgr struct {\n\t// confMgrMu protects confMgr.\n\tconfMgrMu *sync.RWMutex\n\n\tconfMgr     *configmgr.Manager\n\tconfMgrConf *configmgr.Config\n\tlogger      *slog.Logger\n\tpidFilePath string\n}\n\n// serviceMgrConfig contains service manager configuration parameters.\ntype serviceMgrConfig struct {\n\t// confMgrConf is the configuration manager config, it must not be nil.\n\tconfMgrConf *configmgr.Config\n\n\t// logger is the logger used to log services activity, it must not be nil.\n\tlogger *slog.Logger\n\n\t// pidFilePath is the path to the file where to store the PID, if any.\n\tpidFilePath string\n}\n\n// newServiceMgr creates a new *serviceMgr.\nfunc newServiceMgr(ctx context.Context, conf *serviceMgrConfig) (s *serviceMgr, err error) {\n\tconfMgr, err := configmgr.New(ctx, conf.confMgrConf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating config manager: %w\", err)\n\t}\n\n\treturn &serviceMgr{\n\t\tconfMgr:     confMgr,\n\t\tconfMgrMu:   &sync.RWMutex{},\n\t\tconfMgrConf: conf.confMgrConf,\n\t\tlogger:      conf.logger,\n\t\tpidFilePath: conf.pidFilePath,\n\t}, nil\n}\n\n// type check\nvar _ service.Interface = (*serviceMgr)(nil)\n\n// Start implements the [service.Interface] interface for *serviceMgr.\nfunc (s *serviceMgr) Start(ctx context.Context) (err error) {\n\ts.writePID(ctx)\n\n\ts.confMgrMu.RLock()\n\tdefer s.confMgrMu.RUnlock()\n\n\tvar errs []error\n\n\terr = s.confMgr.Web().Start(ctx)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"starting web: %w\", err))\n\t}\n\n\terr = s.confMgr.DNS().Start(ctx)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"starting dnssvc: %w\", err))\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// writePID writes the PID to the file.  Any errors are reported to log.\nfunc (s *serviceMgr) writePID(ctx context.Context) {\n\tif s.pidFilePath == \"\" {\n\t\treturn\n\t}\n\n\tpid := os.Getpid()\n\tdata := strconv.AppendInt(nil, int64(pid), 10)\n\tdata = append(data, '\\n')\n\n\terr := maybe.WriteFile(s.pidFilePath, data, 0o644)\n\tif err != nil {\n\t\ts.logger.ErrorContext(ctx, \"writing pidfile\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\ts.logger.DebugContext(ctx, \"wrote pid\", \"file\", s.pidFilePath, \"pid\", pid)\n}\n\n// Shutdown implements the [service.Interface] interface for *serviceMgr.\nfunc (s *serviceMgr) Shutdown(ctx context.Context) (err error) {\n\ts.confMgrMu.RLock()\n\tdefer s.confMgrMu.RUnlock()\n\n\tvar errs []error\n\n\terr = s.confMgr.Web().Shutdown(ctx)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"shutting down web: %w\", err))\n\t}\n\n\terr = s.confMgr.DNS().Shutdown(ctx)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"shutting down dnssvc: %w\", err))\n\t}\n\n\ts.removePID(ctx)\n\n\treturn errors.Join(errs...)\n}\n\n// removePID removes the PID file.  Any errors are reported to log.\nfunc (s *serviceMgr) removePID(ctx context.Context) {\n\tif s.pidFilePath == \"\" {\n\t\treturn\n\t}\n\n\terr := os.Remove(s.pidFilePath)\n\tif err != nil {\n\t\ts.logger.ErrorContext(ctx, \"removing pidfile\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\ts.logger.DebugContext(ctx, \"removed pidfile\", \"file\", s.pidFilePath)\n}\n\n// type check\nvar _ service.Refresher = (*serviceMgr)(nil)\n\n// Refresh implements the [service.Refresher] interface for *serviceMgr.\nfunc (s *serviceMgr) Refresh(ctx context.Context) (err error) {\n\ts.logger.InfoContext(ctx, \"reconfiguring started\")\n\n\terr = s.Shutdown(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"shutdown failed: %w\", err)\n\t}\n\n\t// TODO(a.garipov):  This is a very rough way to do it.  Some services can\n\t// be reconfigured without the full shutdown, and the error handling is\n\t// currently not the best.\n\n\tctx, cancel := context.WithTimeout(ctx, defaultTimeoutStart)\n\tdefer cancel()\n\n\terr = s.updConfMgr(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"updating configuration manager: %w\", err)\n\t}\n\n\terr = s.Start(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"restarting services: %w\", err)\n\t}\n\n\ts.logger.InfoContext(ctx, \"reconfiguring finished\")\n\n\treturn nil\n}\n\n// updConfMgr updates the configuration manager.\nfunc (s *serviceMgr) updConfMgr(ctx context.Context) (err error) {\n\tconfMgr, err := configmgr.New(ctx, s.confMgrConf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating config manager: %w\", err)\n\t}\n\n\ts.confMgrMu.Lock()\n\tdefer s.confMgrMu.Unlock()\n\n\ts.confMgr = confMgr\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/next/configmgr/config.go",
    "content": "package configmgr\n\nimport (\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n)\n\n// config is the top-level on-disk configuration structure.\ntype config struct {\n\tDNS  *dnsConfig  `yaml:\"dns\"`\n\tHTTP *httpConfig `yaml:\"http\"`\n\tLog  *logConfig  `yaml:\"log\"`\n\t// TODO(a.garipov): Use.\n\tSchemaVersion int `yaml:\"schema_version\"`\n}\n\n// type check\nvar _ validate.Interface = (*config)(nil)\n\n// Validate implements the [validate.Interface] interface for *config.\nfunc (c *config) Validate() (err error) {\n\tif c == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\t// TODO(a.garipov): Add more validations.\n\n\t// Keep this in the same order as the fields in the config.\n\tvalidators := container.KeyValues[string, validate.Interface]{{\n\t\tKey:   \"dns\",\n\t\tValue: c.DNS,\n\t}, {\n\t\tKey:   \"http\",\n\t\tValue: c.HTTP,\n\t}, {\n\t\tKey:   \"log\",\n\t\tValue: c.Log,\n\t}}\n\n\tvar errs []error\n\tfor _, kv := range validators {\n\t\terrs = validate.Append(errs, kv.Key, kv.Value)\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// dnsConfig is the on-disk DNS configuration.\ntype dnsConfig struct {\n\tUpstreamMode        proxy.UpstreamMode `yaml:\"upstream_mode\"`\n\tAddresses           []netip.AddrPort   `yaml:\"addresses\"`\n\tBootstrapDNS        []string           `yaml:\"bootstrap_dns\"`\n\tUpstreamDNS         []string           `yaml:\"upstream_dns\"`\n\tDNS64Prefixes       []netip.Prefix     `yaml:\"dns64_prefixes\"`\n\tUpstreamTimeout     timeutil.Duration  `yaml:\"upstream_timeout\"`\n\tRatelimit           int                `yaml:\"ratelimit\"`\n\tCacheSize           int                `yaml:\"cache_size\"`\n\tBootstrapPreferIPv6 bool               `yaml:\"bootstrap_prefer_ipv6\"`\n\tRefuseAny           bool               `yaml:\"refuse_any\"`\n\tUseDNS64            bool               `yaml:\"use_dns64\"`\n}\n\n// type check\nvar _ validate.Interface = (*dnsConfig)(nil)\n\n// Validate implements the [validate.Interface] interface for *dnsConfig.\n//\n// TODO(a.garipov): Add more validations.\nfunc (c *dnsConfig) Validate() (err error) {\n\tif c == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\t// TODO(a.garipov): Add more validations.\n\n\treturn validate.Positive(\"upstream_timeout\", c.UpstreamTimeout)\n}\n\n// httpConfig is the on-disk web API configuration.\ntype httpConfig struct {\n\tPprof *httpPprofConfig `yaml:\"pprof\"`\n\n\t// TODO(a.garipov): Document the configuration change.\n\tAddresses       []netip.AddrPort  `yaml:\"addresses\"`\n\tSecureAddresses []netip.AddrPort  `yaml:\"secure_addresses\"`\n\tTimeout         timeutil.Duration `yaml:\"timeout\"`\n\tForceHTTPS      bool              `yaml:\"force_https\"`\n}\n\n// type check\nvar _ validate.Interface = (*httpConfig)(nil)\n\n// Validate implements the [validate.Interface] interface for *httpConfig.\n//\n// TODO(a.garipov): Add more validations.\nfunc (c *httpConfig) Validate() (err error) {\n\tif c == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\terrs := []error{\n\t\tvalidate.Positive(\"timeout\", c.Timeout),\n\t}\n\n\terrs = validate.Append(errs, \"pprof\", c.Pprof)\n\n\treturn errors.Join(errs...)\n}\n\n// httpPprofConfig is the on-disk pprof configuration.\ntype httpPprofConfig struct {\n\tPort    uint16 `yaml:\"port\"`\n\tEnabled bool   `yaml:\"enabled\"`\n}\n\n// type check\nvar _ validate.Interface = (*httpPprofConfig)(nil)\n\n// Validate implements the [validate.Interface] interface for *httpPprofConfig.\nfunc (c *httpPprofConfig) Validate() (err error) {\n\tif c == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\treturn nil\n}\n\n// logConfig is the on-disk web API configuration.\ntype logConfig struct {\n\t// TODO(a.garipov):  Use.\n\tVerbose bool `yaml:\"verbose\"`\n}\n\n// type check\nvar _ validate.Interface = (*logConfig)(nil)\n\n// Validate implements the [validate.Interface] interface for *logConfig.\n//\n// TODO(a.garipov): Add more validations.\nfunc (c *logConfig) Validate() (err error) {\n\tif c == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/next/configmgr/configmgr.go",
    "content": "// Package configmgr defines the AdGuard Home on-disk configuration entities and\n// configuration manager.\n//\n// TODO(a.garipov): Add tests.\npackage configmgr\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"os\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/websvc\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/google/renameio/v2/maybe\"\n\tyaml \"go.yaml.in/yaml/v4\"\n)\n\n// Manager handles full and partial changes in the configuration, persisting\n// them to disk if necessary.\n//\n// TODO(a.garipov): Support missing configs and default values.\ntype Manager struct {\n\t// baseLogger is used to create loggers for other entities.\n\tbaseLogger *slog.Logger\n\n\t// logger is used for logging the operation of the configuration manager.\n\tlogger *slog.Logger\n\n\t// updMu makes sure that at most one reconfiguration is performed at a time.\n\t// updMu protects all fields below.\n\tupdMu *sync.RWMutex\n\n\t// dns is the DNS service.\n\tdns *dnssvc.Service\n\n\t// Web is the Web API service.\n\tweb *websvc.Service\n\n\t// current is the current configuration.\n\tcurrent *config\n\n\t// fileName is the name of the configuration file.\n\tfileName string\n}\n\n// Validate returns an error if the configuration file with the given name does\n// not exist or is invalid.\nfunc Validate(fileName string) (err error) {\n\tconf, err := read(fileName)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = conf.Validate()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"validating config: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Config contains the configuration parameters for the configuration manager.\ntype Config struct {\n\t// BaseLogger is used to create loggers for other entities.  It must not be\n\t// nil.\n\tBaseLogger *slog.Logger\n\n\t// Logger is used for logging the operation of the configuration manager.\n\t// It must not be nil.\n\tLogger *slog.Logger\n\n\t// Frontend is the filesystem with the frontend files.\n\tFrontend fs.FS\n\n\t// WebAddr is the initial or override address for the Web UI.  It is not\n\t// written to the configuration file.\n\tWebAddr netip.AddrPort\n\n\t// Start is the time of start of AdGuard Home.\n\tStart time.Time\n\n\t// FileName is the path to the configuration file.\n\tFileName string\n}\n\n// New creates a new *Manager that persists changes to the file pointed to by\n// c.FileName.  It reads the configuration file and populates the service\n// fields.  c must not be nil.\nfunc New(ctx context.Context, c *Config) (m *Manager, err error) {\n\tconf, err := read(c.FileName)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\terr = conf.Validate()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"validating config: %w\", err)\n\t}\n\n\tm = &Manager{\n\t\tbaseLogger: c.BaseLogger,\n\t\tlogger:     c.Logger,\n\t\tupdMu:      &sync.RWMutex{},\n\t\tcurrent:    conf,\n\t\tfileName:   c.FileName,\n\t}\n\n\terr = m.assemble(ctx, conf, c.Frontend, c.WebAddr, c.Start)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating config manager: %w\", err)\n\t}\n\n\treturn m, nil\n}\n\n// read reads and decodes configuration from the provided filename.\nfunc read(fileName string) (conf *config, err error) {\n\tdefer func() { err = errors.Annotate(err, \"reading config: %w\") }()\n\n\tconf = &config{}\n\tf, err := os.Open(fileName)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\tdefer func() { err = errors.WithDeferred(err, f.Close()) }()\n\n\terr = yaml.NewDecoder(f).Decode(conf)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\treturn conf, nil\n}\n\n// assemble creates all services and puts them into the corresponding fields.\n// The fields of conf must not be modified after calling assemble.\nfunc (m *Manager) assemble(\n\tctx context.Context,\n\tconf *config,\n\tfrontend fs.FS,\n\twebAddr netip.AddrPort,\n\tstart time.Time,\n) (err error) {\n\tdnsConf := &dnssvc.Config{\n\t\tLogger:              m.baseLogger.With(slogutil.KeyPrefix, \"dnssvc\"),\n\t\tUpstreamMode:        conf.DNS.UpstreamMode,\n\t\tAddresses:           conf.DNS.Addresses,\n\t\tBootstrapServers:    conf.DNS.BootstrapDNS,\n\t\tUpstreamServers:     conf.DNS.UpstreamDNS,\n\t\tDNS64Prefixes:       conf.DNS.DNS64Prefixes,\n\t\tUpstreamTimeout:     time.Duration(conf.DNS.UpstreamTimeout),\n\t\tCacheSize:           conf.DNS.CacheSize,\n\t\tRatelimit:           conf.DNS.Ratelimit,\n\t\tBootstrapPreferIPv6: conf.DNS.BootstrapPreferIPv6,\n\t\tCacheEnabled:        conf.DNS.CacheSize > 0,\n\t\tRefuseAny:           conf.DNS.RefuseAny,\n\t\tUseDNS64:            conf.DNS.UseDNS64,\n\t}\n\terr = m.updateDNS(ctx, dnsConf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"assembling dnssvc: %w\", err)\n\t}\n\n\twebSvcConf := &websvc.Config{\n\t\tLogger: m.baseLogger.With(slogutil.KeyPrefix, \"websvc\"),\n\t\tPprof: &websvc.PprofConfig{\n\t\t\tPort:    conf.HTTP.Pprof.Port,\n\t\t\tEnabled: conf.HTTP.Pprof.Enabled,\n\t\t},\n\t\tConfigManager: m,\n\t\tFrontend:      frontend,\n\t\t// TODO(a.garipov): Fill from config file.\n\t\tTLS:             nil,\n\t\tStart:           start,\n\t\tAddresses:       conf.HTTP.Addresses,\n\t\tSecureAddresses: conf.HTTP.SecureAddresses,\n\t\tOverrideAddress: webAddr,\n\t\tTimeout:         time.Duration(conf.HTTP.Timeout),\n\t\tForceHTTPS:      conf.HTTP.ForceHTTPS,\n\t}\n\n\terr = m.updateWeb(ctx, webSvcConf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"assembling websvc: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// write writes the current configuration to disk.\nfunc (m *Manager) write(ctx context.Context) (err error) {\n\tb, err := yaml.Marshal(m.current)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"encoding: %w\", err)\n\t}\n\n\terr = maybe.WriteFile(m.fileName, b, aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"writing: %w\", err)\n\t}\n\n\tm.logger.InfoContext(ctx, \"config file written\", \"path\", m.fileName)\n\n\treturn nil\n}\n\n// DNS returns the current DNS service.  It is safe for concurrent use.\nfunc (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {\n\tm.updMu.RLock()\n\tdefer m.updMu.RUnlock()\n\n\treturn m.dns\n}\n\n// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager.  The\n// fields of c must not be modified after calling UpdateDNS.\nfunc (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {\n\tm.updMu.Lock()\n\tdefer m.updMu.Unlock()\n\n\t// TODO(a.garipov): Update and write the configuration file.  Return an\n\t// error if something went wrong.\n\n\terr = m.updateDNS(ctx, c)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"reassembling dnssvc: %w\", err)\n\t}\n\n\tm.updateCurrentDNS(c)\n\n\treturn m.write(ctx)\n}\n\n// updateDNS recreates the DNS service.  m.updMu is expected to be locked.\nfunc (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {\n\tif prev := m.dns; prev != nil {\n\t\terr = prev.Shutdown(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"shutting down dns svc: %w\", err)\n\t\t}\n\t}\n\n\tsvc, err := dnssvc.New(c)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating dns svc: %w\", err)\n\t}\n\n\tm.dns = svc\n\n\treturn nil\n}\n\n// updateCurrentDNS updates the DNS configuration in the current config.\nfunc (m *Manager) updateCurrentDNS(c *dnssvc.Config) {\n\tm.current.DNS.Addresses = slices.Clone(c.Addresses)\n\tm.current.DNS.UpstreamMode = c.UpstreamMode\n\tm.current.DNS.BootstrapDNS = slices.Clone(c.BootstrapServers)\n\tm.current.DNS.UpstreamDNS = slices.Clone(c.UpstreamServers)\n\tm.current.DNS.DNS64Prefixes = slices.Clone(c.DNS64Prefixes)\n\tm.current.DNS.UpstreamTimeout = timeutil.Duration(c.UpstreamTimeout)\n\tm.current.DNS.Ratelimit = c.Ratelimit\n\tm.current.DNS.CacheSize = c.CacheSize\n\tm.current.DNS.BootstrapPreferIPv6 = c.BootstrapPreferIPv6\n\tm.current.DNS.RefuseAny = c.RefuseAny\n\tm.current.DNS.UseDNS64 = c.UseDNS64\n}\n\n// Web returns the current web service.  It is safe for concurrent use.\nfunc (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {\n\tm.updMu.RLock()\n\tdefer m.updMu.RUnlock()\n\n\treturn m.web\n}\n\n// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager.  The\n// fields of c must not be modified after calling UpdateWeb.\nfunc (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {\n\tm.updMu.Lock()\n\tdefer m.updMu.Unlock()\n\n\terr = m.updateWeb(ctx, c)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"reassembling websvc: %w\", err)\n\t}\n\n\tm.updateCurrentWeb(c)\n\n\treturn m.write(ctx)\n}\n\n// updateWeb recreates the web service.  m.upd is expected to be locked.\nfunc (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {\n\tif prev := m.web; prev != nil {\n\t\terr = prev.Shutdown(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"shutting down web svc: %w\", err)\n\t\t}\n\t}\n\n\tm.web, err = websvc.New(c)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating web svc: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// updateCurrentWeb updates the web configuration in the current config.\nfunc (m *Manager) updateCurrentWeb(c *websvc.Config) {\n\t// TODO(a.garipov): Update pprof from API?\n\n\tm.current.HTTP.Addresses = slices.Clone(c.Addresses)\n\tm.current.HTTP.SecureAddresses = slices.Clone(c.SecureAddresses)\n\tm.current.HTTP.Timeout = timeutil.Duration(c.Timeout)\n\tm.current.HTTP.ForceHTTPS = c.ForceHTTPS\n}\n"
  },
  {
    "path": "internal/next/dnssvc/config.go",
    "content": "package dnssvc\n\nimport (\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n)\n\n// Config is the AdGuard Home DNS service configuration structure.\n//\n// TODO(a.garipov): Add timeout for incoming requests.\ntype Config struct {\n\t// Logger is used for logging the operation of the DNS service.  It must not\n\t// be nil.\n\tLogger *slog.Logger\n\n\t// UpstreamMode defines how upstreams are used.\n\tUpstreamMode proxy.UpstreamMode\n\n\t// Addresses are the addresses on which to serve plain DNS queries.\n\tAddresses []netip.AddrPort\n\n\t// BootstrapServers are the addresses of DNS servers used for bootstrapping\n\t// the upstream DNS server addresses.\n\tBootstrapServers []string\n\n\t// UpstreamServers are the upstream DNS server addresses to use.\n\tUpstreamServers []string\n\n\t// DNS64Prefixes is a slice of NAT64 prefixes to be used for DNS64.  See\n\t// also [Config.UseDNS64].\n\tDNS64Prefixes []netip.Prefix\n\n\t// UpstreamTimeout is the timeout for upstream requests.\n\tUpstreamTimeout time.Duration\n\n\t// CacheSize is the maximum cache size in bytes.\n\t//\n\t// TODO(a.garipov):  Use bytesize.Bytes everywhere.\n\tCacheSize int\n\n\t// Ratelimit is the maximum number of requests per second from a given IP or\n\t// subnet.  If it is zero, rate limiting is disabled.\n\tRatelimit int\n\n\t// BootstrapPreferIPv6, if true, instructs the bootstrapper to prefer IPv6\n\t// addresses to IPv4 ones when bootstrapping.\n\tBootstrapPreferIPv6 bool\n\n\t// CacheEnabled defines if the response cache should be used.\n\tCacheEnabled bool\n\n\t// RefuseAny, if true, refuses DNS queries with the type ANY.\n\tRefuseAny bool\n\n\t// UseDNS64, if true, enables DNS64 protection for incoming requests.\n\tUseDNS64 bool\n}\n"
  },
  {
    "path": "internal/next/dnssvc/dnssvc.go",
    "content": "// Package dnssvc contains the AdGuard Home DNS service.\n//\n// TODO(a.garipov): Define, if all methods of a *Service should work with a nil\n// receiver.\npackage dnssvc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghslog\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/dnsproxy/ratelimit\"\n\t\"github.com/AdguardTeam/dnsproxy/upstream\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n)\n\n// Service is the AdGuard Home DNS service.  A nil *Service is a valid\n// [agh.Service] that does nothing.\n//\n// TODO(a.garipov): Consider saving a [*proxy.Config] instance for those\n// fields that are only used in [New] and [Service.Config].\ntype Service struct {\n\t// logger is used for logging the operation of the DNS service.\n\tlogger *slog.Logger\n\n\t// proxy is the current DNS proxy.\n\tproxy *proxy.Proxy\n\n\t// proxyConf contains the fields that have been used to create proxy to\n\t// return them in [Service.Config].\n\tproxyConf *proxy.Config\n\n\t// The fields below have been used to create proxy and are saved to return\n\t// them in [Service.Config].\n\n\tbootstraps          []string\n\tbootstrapResolvers  []*upstream.UpstreamResolver\n\tupstreams           []string\n\tupstreamTimeout     time.Duration\n\tbootstrapPreferIPv6 bool\n\n\t// The fields above have been used to create proxy and are saved to return\n\t// them in [Service.Config].\n\n\t// running is true when the service has started.\n\trunning atomic.Bool\n}\n\n// New returns a new properly initialized *Service.  If c is nil, svc is a nil\n// *Service that does nothing.  The fields of c must not be modified after\n// calling New.\nfunc New(c *Config) (svc *Service, err error) {\n\tif c == nil {\n\t\treturn nil, nil\n\t}\n\n\trlMw, err := newRatelimitMw(c.Logger, c.Ratelimit)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ratelimit middleware: %w\", err)\n\t}\n\n\tsvc = &Service{\n\t\tlogger: c.Logger,\n\t\tproxyConf: &proxy.Config{\n\t\t\tUpstreamMode:   c.UpstreamMode,\n\t\t\tDNS64Prefs:     c.DNS64Prefixes,\n\t\t\tCacheSizeBytes: c.CacheSize,\n\t\t\tCacheEnabled:   c.CacheEnabled,\n\t\t\tRefuseAny:      c.RefuseAny,\n\t\t\tUseDNS64:       c.UseDNS64,\n\t\t},\n\t\tbootstraps:          c.BootstrapServers,\n\t\tupstreams:           c.UpstreamServers,\n\t\tupstreamTimeout:     c.UpstreamTimeout,\n\t\tbootstrapPreferIPv6: c.BootstrapPreferIPv6,\n\t}\n\n\tupstreams, resolvers, err := addressesToUpstreams(\n\t\tsvc.logger.With(slogutil.KeyPrefix, aghslog.PrefixDNSProxy),\n\t\tc.UpstreamServers,\n\t\tc.BootstrapServers,\n\t\tc.UpstreamTimeout,\n\t\tc.BootstrapPreferIPv6,\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"converting upstreams: %w\", err)\n\t}\n\n\tsvc.bootstrapResolvers = resolvers\n\n\tsvc.proxy, err = proxy.New(&proxy.Config{\n\t\tLogger: svc.logger,\n\t\tUpstreamConfig: &proxy.UpstreamConfig{\n\t\t\tUpstreams: upstreams,\n\t\t},\n\t\tUDPListenAddr:  udpAddrs(c.Addresses),\n\t\tTCPListenAddr:  tcpAddrs(c.Addresses),\n\t\tUpstreamMode:   svc.proxyConf.UpstreamMode,\n\t\tRequestHandler: rlMw.Wrap(proxy.DefaultHandler{}),\n\t\tDNS64Prefs:     svc.proxyConf.DNS64Prefs,\n\t\tCacheEnabled:   svc.proxyConf.CacheEnabled,\n\t\tRefuseAny:      svc.proxyConf.RefuseAny,\n\t\tUseDNS64:       svc.proxyConf.UseDNS64,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"proxy: %w\", err)\n\t}\n\n\treturn svc, nil\n}\n\n// newRatelimitMw returns the ratelimit middleware.  In case of invalid\n// ratelimit configuration returns an error. l must not be nil.\nfunc newRatelimitMw(l *slog.Logger, limit int) (mw proxy.Middleware, err error) {\n\tif limit == 0 {\n\t\treturn proxy.MiddlewareFunc(proxy.PassThrough), nil\n\t}\n\n\trlConf := &ratelimit.Config{\n\t\tLogger:        l.With(slogutil.KeyPrefix, \"ratelimit\"),\n\t\tRatelimit:     uint(limit),\n\t\tSubnetLenIPv4: netutil.IPv4BitLen,\n\t\tSubnetLenIPv6: netutil.IPv6BitLen,\n\t}\n\tif err = rlConf.Validate(); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid configuration: %w\", err)\n\t}\n\n\treturn ratelimit.NewMiddleware(rlConf), nil\n}\n\n// addressesToUpstreams is a wrapper around [upstream.AddressToUpstream].  It\n// accepts a slice of addresses and other upstream parameters, and returns a\n// slice of upstreams.  logger must not be nil.\nfunc addressesToUpstreams(\n\tlogger *slog.Logger,\n\tupsStrs []string,\n\tbootstraps []string,\n\ttimeout time.Duration,\n\tpreferIPv6 bool,\n) (upstreams []upstream.Upstream, boots []*upstream.UpstreamResolver, err error) {\n\tboots, err = aghnet.ParseBootstraps(bootstraps, &upstream.Options{\n\t\tLogger:     logger.With(aghslog.KeyUpstreamType, aghslog.UpstreamTypeBootstrap),\n\t\tTimeout:    timeout,\n\t\tPreferIPv6: preferIPv6,\n\t})\n\tif err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn nil, nil, err\n\t}\n\n\t// TODO(e.burkov):  Add system hosts resolver here.\n\tvar bootstrap upstream.ParallelResolver\n\tfor _, r := range boots {\n\t\tbootstrap = append(bootstrap, upstream.NewCachingResolver(r))\n\t}\n\n\tupstreams = make([]upstream.Upstream, len(upsStrs))\n\tfor i, upsStr := range upsStrs {\n\t\tupstreams[i], err = upstream.AddressToUpstream(upsStr, &upstream.Options{\n\t\t\tLogger:     logger.With(aghslog.KeyUpstreamType, aghslog.UpstreamTypeMain),\n\t\t\tBootstrap:  bootstrap,\n\t\t\tTimeout:    timeout,\n\t\t\tPreferIPv6: preferIPv6,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, boots, fmt.Errorf(\"upstream at index %d: %w\", i, err)\n\t\t}\n\t}\n\n\treturn upstreams, boots, nil\n}\n\n// tcpAddrs converts []netip.AddrPort into []*net.TCPAddr.\nfunc tcpAddrs(addrPorts []netip.AddrPort) (tcpAddrs []*net.TCPAddr) {\n\tif addrPorts == nil {\n\t\treturn nil\n\t}\n\n\ttcpAddrs = make([]*net.TCPAddr, len(addrPorts))\n\tfor i, a := range addrPorts {\n\t\ttcpAddrs[i] = net.TCPAddrFromAddrPort(a)\n\t}\n\n\treturn tcpAddrs\n}\n\n// udpAddrs converts []netip.AddrPort into []*net.UDPAddr.\nfunc udpAddrs(addrPorts []netip.AddrPort) (udpAddrs []*net.UDPAddr) {\n\tif addrPorts == nil {\n\t\treturn nil\n\t}\n\n\tudpAddrs = make([]*net.UDPAddr, len(addrPorts))\n\tfor i, a := range addrPorts {\n\t\tudpAddrs[i] = net.UDPAddrFromAddrPort(a)\n\t}\n\n\treturn udpAddrs\n}\n\n// type check\nvar _ agh.ServiceWithConfig[*Config] = (*Service)(nil)\n\n// Start implements the [agh.Service] interface for *Service.  svc may be nil.\n// After Start exits, all DNS servers have tried to start, but there is no\n// guarantee that they did.  Errors from the servers are written to the log.\nfunc (svc *Service) Start(ctx context.Context) (err error) {\n\tif svc == nil {\n\t\treturn nil\n\t}\n\n\tdefer func() {\n\t\t// TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to\n\t\t// tell when all servers are actually up, so at best this is merely an\n\t\t// assumption.\n\t\tsvc.running.Store(err == nil)\n\t}()\n\n\treturn svc.proxy.Start(ctx)\n}\n\n// Shutdown implements the [agh.Service] interface for *Service.  svc may be\n// nil.\nfunc (svc *Service) Shutdown(ctx context.Context) (err error) {\n\tif svc == nil {\n\t\treturn nil\n\t}\n\n\terrs := []error{\n\t\tsvc.proxy.Shutdown(ctx),\n\t}\n\n\tfor _, b := range svc.bootstrapResolvers {\n\t\terrs = append(errs, errors.Annotate(b.Close(), \"closing bootstrap %s: %w\", b.Address()))\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// Config returns the current configuration of the web service.  Config must not\n// be called simultaneously with Start.  If svc was initialized with \":0\"\n// addresses, addrs will not return the actual bound ports until Start is\n// finished.\nfunc (svc *Service) Config() (c *Config) {\n\t// TODO(a.garipov): Do we need to get the TCP addresses separately?\n\n\tvar addrs []netip.AddrPort\n\tif svc.running.Load() {\n\t\tudpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)\n\t\taddrs = make([]netip.AddrPort, len(udpAddrs))\n\t\tfor i, a := range udpAddrs {\n\t\t\taddrs[i] = a.(*net.UDPAddr).AddrPort()\n\t\t}\n\t} else {\n\t\tconf := svc.proxy.Config\n\t\tudpAddrs := conf.UDPListenAddr\n\t\taddrs = make([]netip.AddrPort, len(udpAddrs))\n\t\tfor i, a := range udpAddrs {\n\t\t\taddrs[i] = a.AddrPort()\n\t\t}\n\t}\n\n\t// TODO(d.kolyshev): Fill ratelimit.\n\tc = &Config{\n\t\tLogger:              svc.logger,\n\t\tUpstreamMode:        svc.proxyConf.UpstreamMode,\n\t\tAddresses:           addrs,\n\t\tBootstrapServers:    svc.bootstraps,\n\t\tUpstreamServers:     svc.upstreams,\n\t\tDNS64Prefixes:       svc.proxyConf.DNS64Prefs,\n\t\tUpstreamTimeout:     svc.upstreamTimeout,\n\t\tCacheSize:           svc.proxyConf.CacheSizeBytes,\n\t\tBootstrapPreferIPv6: svc.bootstrapPreferIPv6,\n\t\tCacheEnabled:        svc.proxyConf.CacheEnabled,\n\t\tRefuseAny:           svc.proxyConf.RefuseAny,\n\t\tUseDNS64:            svc.proxyConf.UseDNS64,\n\t}\n\n\treturn c\n}\n"
  },
  {
    "path": "internal/next/dnssvc/dnssvc_test.go",
    "content": "package dnssvc_test\n\nimport (\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n\nfunc TestService(t *testing.T) {\n\tconst (\n\t\tlistenAddr    = \"127.0.0.1:0\"\n\t\tbootstrapAddr = \"127.0.0.1:0\"\n\t\tupstreamAddr  = \"upstream.example\"\n\t)\n\n\tupstreamErrCh := make(chan error, 1)\n\tupstreamStartedCh := make(chan struct{})\n\tupstreamSrv := &dns.Server{\n\t\tAddr: bootstrapAddr,\n\t\tNet:  \"udp\",\n\t\tHandler: dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {\n\t\t\tpt := testutil.PanicT{}\n\n\t\t\tresp := (&dns.Msg{}).SetReply(req)\n\t\t\tresp.Answer = append(resp.Answer, &dns.A{\n\t\t\t\tHdr: dns.RR_Header{},\n\t\t\t\tA:   netip.MustParseAddrPort(bootstrapAddr).Addr().AsSlice(),\n\t\t\t})\n\n\t\t\twriteErr := w.WriteMsg(resp)\n\t\t\trequire.NoError(pt, writeErr)\n\t\t}),\n\t\tNotifyStartedFunc: func() { close(upstreamStartedCh) },\n\t}\n\n\tgo func() {\n\t\tlistenErr := upstreamSrv.ListenAndServe()\n\t\tif listenErr != nil {\n\t\t\t// Log these immediately to see what happens.\n\t\t\tt.Logf(\"upstream listen error: %s\", listenErr)\n\t\t}\n\n\t\tupstreamErrCh <- listenErr\n\t}()\n\n\t_, _ = testutil.RequireReceive(t, upstreamStartedCh, testTimeout)\n\n\tc := &dnssvc.Config{\n\t\tLogger:           slogutil.NewDiscardLogger(),\n\t\tUpstreamMode:     proxy.UpstreamModeParallel,\n\t\tAddresses:        []netip.AddrPort{netip.MustParseAddrPort(listenAddr)},\n\t\tBootstrapServers: []string{upstreamSrv.PacketConn.LocalAddr().String()},\n\t\tUpstreamServers:  []string{upstreamAddr},\n\t\tUpstreamTimeout:  testTimeout,\n\t}\n\n\tsvc, err := dnssvc.New(c)\n\trequire.NoError(t, err)\n\n\terr = svc.Start(testutil.ContextWithTimeout(t, testTimeout))\n\trequire.NoError(t, err)\n\n\tgotConf := svc.Config()\n\trequire.NotNil(t, gotConf)\n\trequire.Len(t, gotConf.Addresses, 1)\n\n\taddr := gotConf.Addresses[0]\n\n\tt.Run(\"dns\", func(t *testing.T) {\n\t\treq := &dns.Msg{\n\t\t\tMsgHdr: dns.MsgHdr{\n\t\t\t\tId:               dns.Id(),\n\t\t\t\tRecursionDesired: true,\n\t\t\t},\n\t\t\tQuestion: []dns.Question{{\n\t\t\t\tName:   \"example.com.\",\n\t\t\t\tQtype:  dns.TypeA,\n\t\t\t\tQclass: dns.ClassINET,\n\t\t\t}},\n\t\t}\n\n\t\tcli := &dns.Client{}\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\t\tvar resp *dns.Msg\n\t\trequire.Eventually(t, func() (ok bool) {\n\t\t\tvar excErr error\n\t\t\tresp, _, excErr = cli.ExchangeContext(ctx, req, addr.String())\n\n\t\t\treturn excErr == nil\n\t\t}, testTimeout, testTimeout/10)\n\n\t\tassert.NotNil(t, resp)\n\t})\n\n\terr = svc.Shutdown(testutil.ContextWithTimeout(t, testTimeout))\n\n\trequire.NoError(t, err)\n\n\terr = upstreamSrv.Shutdown()\n\trequire.NoError(t, err)\n\n\terr, ok := testutil.RequireReceive(t, upstreamErrCh, testTimeout)\n\trequire.True(t, ok)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/next/jsonpatch/jsonpatch.go",
    "content": "// Package jsonpatch contains utilities for JSON Merge Patch APIs.\n//\n// See https://www.rfc-editor.org/rfc/rfc7396.\npackage jsonpatch\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// NonRemovable is a type that prevents JSON null from being used to try and\n// remove a value.\ntype NonRemovable[T any] struct {\n\tValue T\n\tIsSet bool\n}\n\n// type check\nvar _ json.Unmarshaler = (*NonRemovable[struct{}])(nil)\n\n// UnmarshalJSON implements the [json.Unmarshaler] interface for *NonRemovable.\nfunc (v *NonRemovable[T]) UnmarshalJSON(b []byte) (err error) {\n\tif v == nil {\n\t\treturn errors.Error(\"jsonpatch.NonRemovable is nil\")\n\t}\n\n\tif bytes.Equal(b, []byte(\"null\")) {\n\t\treturn errors.Error(\"property cannot be removed\")\n\t}\n\n\tv.IsSet = true\n\n\treturn json.Unmarshal(b, &v.Value)\n}\n\n// Set sets ptr if the value has been provided.\nfunc (v NonRemovable[T]) Set(ptr *T) {\n\tif v.IsSet {\n\t\t*ptr = v.Value\n\t}\n}\n"
  },
  {
    "path": "internal/next/jsonpatch/jsonpatch_test.go",
    "content": "package jsonpatch_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNonRemovable(t *testing.T) {\n\ttype T struct {\n\t\tValue jsonpatch.NonRemovable[int] `json:\"value\"`\n\t}\n\n\tvar v T\n\n\terr := json.Unmarshal([]byte(`{\"value\":null}`), &v)\n\ttestutil.AssertErrorMsg(t, \"property cannot be removed\", err)\n\n\terr = json.Unmarshal([]byte(`{\"value\":42}`), &v)\n\tassert.NoError(t, err)\n\n\tvar got int\n\tv.Value.Set(&got)\n\n\tassert.Equal(t, 42, got)\n}\n"
  },
  {
    "path": "internal/next/websvc/config.go",
    "content": "package websvc\n\nimport (\n\t\"crypto/tls\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"time\"\n)\n\n// Config is the AdGuard Home web service configuration structure.\ntype Config struct {\n\t// Logger is used for logging the operation of the web API service.  It must\n\t// not be nil.\n\tLogger *slog.Logger\n\n\t// Pprof is the configuration for the pprof debug API.  It must not be nil.\n\tPprof *PprofConfig\n\n\t// ConfigManager is used to show information about services as well as\n\t// dynamically reconfigure them.\n\tConfigManager ConfigManager\n\n\t// Frontend is the filesystem with the frontend and other statically\n\t// compiled files.\n\tFrontend fs.FS\n\n\t// TLS is the optional TLS configuration.  If TLS is not nil,\n\t// SecureAddresses must not be empty.\n\tTLS *tls.Config\n\n\t// Start is the time of start of AdGuard Home.\n\tStart time.Time\n\n\t// OverrideAddress is the initial or override address for the HTTP API.  If\n\t// set, it is used instead of [Addresses] and [SecureAddresses].\n\tOverrideAddress netip.AddrPort\n\n\t// Addresses are the addresses on which to serve the plain HTTP API.\n\tAddresses []netip.AddrPort\n\n\t// SecureAddresses are the addresses on which to serve the HTTPS API.  If\n\t// SecureAddresses is not empty, TLS must not be nil.\n\tSecureAddresses []netip.AddrPort\n\n\t// Timeout is the timeout for all server operations.\n\tTimeout time.Duration\n\n\t// ForceHTTPS tells if all requests to Addresses should be redirected to a\n\t// secure address instead.\n\t//\n\t// TODO(a.garipov): Use; define rules, which address to redirect to.\n\tForceHTTPS bool\n}\n\n// PprofConfig is the configuration for the pprof debug API.\ntype PprofConfig struct {\n\tPort    uint16 `yaml:\"port\"`\n\tEnabled bool   `yaml:\"enabled\"`\n}\n\n// Config returns the current configuration of the web service.  Config must not\n// be called simultaneously with Start.  If svc was initialized with \":0\"\n// addresses, addrs will not return the actual bound ports until Start is\n// finished.\nfunc (svc *Service) Config() (c *Config) {\n\tc = &Config{\n\t\tLogger: svc.logger,\n\t\tPprof: &PprofConfig{\n\t\t\tPort:    svc.pprofPort,\n\t\t\tEnabled: svc.pprof != nil,\n\t\t},\n\t\tConfigManager: svc.confMgr,\n\t\tFrontend:      svc.frontend,\n\t\tTLS:           svc.tls,\n\t\t// Leave Addresses and SecureAddresses empty and get the actual\n\t\t// addresses that include the :0 ones later.\n\t\tStart:           svc.start,\n\t\tOverrideAddress: svc.overrideAddr,\n\t\tTimeout:         svc.timeout,\n\t\tForceHTTPS:      svc.forceHTTPS,\n\t}\n\n\tc.Addresses, c.SecureAddresses = svc.addrs()\n\n\treturn c\n}\n"
  },
  {
    "path": "internal/next/websvc/dns.go",
    "content": "package websvc\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/netip\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n)\n\n// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns\n// HTTP API.\ntype ReqPatchSettingsDNS struct {\n\t// TODO(a.garipov): Add more as we go.\n\n\tUpstreamMode jsonpatch.NonRemovable[proxy.UpstreamMode] `json:\"upstream_mode\"`\n\n\tAddresses        jsonpatch.NonRemovable[[]netip.AddrPort] `json:\"addresses\"`\n\tBootstrapServers jsonpatch.NonRemovable[[]string]         `json:\"bootstrap_servers\"`\n\tUpstreamServers  jsonpatch.NonRemovable[[]string]         `json:\"upstream_servers\"`\n\tDNS64Prefixes    jsonpatch.NonRemovable[[]netip.Prefix]   `json:\"dns64_prefixes\"`\n\n\tUpstreamTimeout jsonpatch.NonRemovable[aghhttp.JSONDuration] `json:\"upstream_timeout\"`\n\n\tCacheSize jsonpatch.NonRemovable[int] `json:\"cache_size\"`\n\tRatelimit jsonpatch.NonRemovable[int] `json:\"ratelimit\"`\n\n\tBootstrapPreferIPv6 jsonpatch.NonRemovable[bool] `json:\"bootstrap_prefer_ipv6\"`\n\tRefuseAny           jsonpatch.NonRemovable[bool] `json:\"refuse_any\"`\n\tUseDNS64            jsonpatch.NonRemovable[bool] `json:\"use_dns64\"`\n}\n\n// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API.  See the\n// DnsSettings object in the OpenAPI specification.\ntype HTTPAPIDNSSettings struct {\n\t// TODO(a.garipov): Add more as we go.\n\n\tUpstreamMode proxy.UpstreamMode `json:\"upstream_mode\"`\n\n\tAddresses []netip.AddrPort `json:\"addresses\"`\n\n\tBootstrapServers []string `json:\"bootstrap_servers\"`\n\tUpstreamServers  []string `json:\"upstream_servers\"`\n\n\tDNS64Prefixes []netip.Prefix `json:\"dns64_prefixes\"`\n\n\tUpstreamTimeout aghhttp.JSONDuration `json:\"upstream_timeout\"`\n\n\tRatelimit int `json:\"ratelimit\"`\n\tCacheSize int `json:\"cache_size\"`\n\n\tBootstrapPreferIPv6 bool `json:\"bootstrap_prefer_ipv6\"`\n\tRefuseAny           bool `json:\"refuse_any\"`\n\tUseDNS64            bool `json:\"use_dns64\"`\n}\n\n// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP\n// API.\nfunc (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tl := svc.logger\n\treq := &ReqPatchSettingsDNS{}\n\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\taghhttp.WriteJSONResponseError(ctx, l, w, r, fmt.Errorf(\"decoding: %w\", err))\n\n\t\treturn\n\t}\n\n\tdnsSvc := svc.confMgr.DNS()\n\tnewConf := dnsSvc.Config()\n\n\t// TODO(a.garipov): Add more as we go.\n\n\treq.UpstreamMode.Set(&newConf.UpstreamMode)\n\n\treq.Addresses.Set(&newConf.Addresses)\n\treq.BootstrapServers.Set(&newConf.BootstrapServers)\n\treq.UpstreamServers.Set(&newConf.UpstreamServers)\n\treq.DNS64Prefixes.Set(&newConf.DNS64Prefixes)\n\n\treq.UpstreamTimeout.Set((*aghhttp.JSONDuration)(&newConf.UpstreamTimeout))\n\n\tif req.CacheSize.IsSet {\n\t\tnewConf.CacheSize = req.CacheSize.Value\n\t\tnewConf.CacheEnabled = req.CacheSize.Value > 0\n\t}\n\treq.Ratelimit.Set(&newConf.Ratelimit)\n\n\treq.BootstrapPreferIPv6.Set(&newConf.BootstrapPreferIPv6)\n\treq.RefuseAny.Set(&newConf.RefuseAny)\n\treq.UseDNS64.Set(&newConf.UseDNS64)\n\n\terr = svc.confMgr.UpdateDNS(ctx, newConf)\n\tif err != nil {\n\t\taghhttp.WriteJSONResponseError(ctx, l, w, r, fmt.Errorf(\"updating: %w\", err))\n\n\t\treturn\n\t}\n\n\tnewSvc := svc.confMgr.DNS()\n\terr = newSvc.Start(ctx)\n\tif err != nil {\n\t\taghhttp.WriteJSONResponseError(ctx, l, w, r, fmt.Errorf(\"starting new service: %w\", err))\n\n\t\treturn\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, &HTTPAPIDNSSettings{\n\t\tUpstreamMode:        newConf.UpstreamMode,\n\t\tAddresses:           newConf.Addresses,\n\t\tBootstrapServers:    newConf.BootstrapServers,\n\t\tUpstreamServers:     newConf.UpstreamServers,\n\t\tDNS64Prefixes:       newConf.DNS64Prefixes,\n\t\tUpstreamTimeout:     aghhttp.JSONDuration(newConf.UpstreamTimeout),\n\t\tRatelimit:           newConf.Ratelimit,\n\t\tBootstrapPreferIPv6: newConf.BootstrapPreferIPv6,\n\t\tCacheSize:           newConf.CacheSize,\n\t\tRefuseAny:           newConf.RefuseAny,\n\t\tUseDNS64:            newConf.UseDNS64,\n\t})\n}\n"
  },
  {
    "path": "internal/next/websvc/dns_test.go",
    "content": "package websvc_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/websvc\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestService_HandlePatchSettingsDNS(t *testing.T) {\n\twantDNS := &websvc.HTTPAPIDNSSettings{\n\t\tUpstreamMode:        proxy.UpstreamModeParallel,\n\t\tAddresses:           []netip.AddrPort{netip.MustParseAddrPort(\"127.0.1.1:53\")},\n\t\tBootstrapServers:    []string{\"1.0.0.1\"},\n\t\tUpstreamServers:     []string{\"1.1.1.1\"},\n\t\tDNS64Prefixes:       []netip.Prefix{netip.MustParsePrefix(\"1234::/64\")},\n\t\tUpstreamTimeout:     aghhttp.JSONDuration(2 * time.Second),\n\t\tRatelimit:           100,\n\t\tCacheSize:           1048576,\n\t\tBootstrapPreferIPv6: true,\n\t\tRefuseAny:           true,\n\t\tUseDNS64:            true,\n\t}\n\n\tvar started atomic.Bool\n\tconfMgr := newConfigManager()\n\tconfMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {\n\t\treturn &aghtest.ServiceWithConfig[*dnssvc.Config]{\n\t\t\tOnStart: func(_ context.Context) (err error) {\n\t\t\t\tstarted.Store(true)\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tOnShutdown: func(ctx context.Context) (_ error) { panic(testutil.UnexpectedCall(ctx)) },\n\t\t\tOnConfig:   func() (c *dnssvc.Config) { return &dnssvc.Config{} },\n\t\t}\n\t}\n\tconfMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {\n\t\treturn nil\n\t}\n\n\t_, addr := newTestServer(t, confMgr)\n\tu := &url.URL{\n\t\tScheme: urlutil.SchemeHTTP,\n\t\tHost:   addr.String(),\n\t\tPath:   websvc.PathPatternV1SettingsDNS,\n\t}\n\n\treq := jobj{\n\t\t\"upstream_mode\":         wantDNS.UpstreamMode,\n\t\t\"addresses\":             wantDNS.Addresses,\n\t\t\"bootstrap_servers\":     wantDNS.BootstrapServers,\n\t\t\"upstream_servers\":      wantDNS.UpstreamServers,\n\t\t\"dns64_prefixes\":        wantDNS.DNS64Prefixes,\n\t\t\"upstream_timeout\":      wantDNS.UpstreamTimeout,\n\t\t\"cache_size\":            wantDNS.CacheSize,\n\t\t\"ratelimit\":             wantDNS.Ratelimit,\n\t\t\"bootstrap_prefer_ipv6\": wantDNS.BootstrapPreferIPv6,\n\t\t\"refuse_any\":            wantDNS.RefuseAny,\n\t\t\"use_dns64\":             wantDNS.UseDNS64,\n\t}\n\n\trespBody := httpPatch(t, u, req, http.StatusOK)\n\tresp := &websvc.HTTPAPIDNSSettings{}\n\terr := json.Unmarshal(respBody, resp)\n\trequire.NoError(t, err)\n\n\tassert.True(t, started.Load())\n\tassert.Equal(t, wantDNS, resp)\n\tassert.Equal(t, wantDNS, resp)\n}\n"
  },
  {
    "path": "internal/next/websvc/http.go",
    "content": "package websvc\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/jsonpatch\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http\n// HTTP API.\ntype ReqPatchSettingsHTTP struct {\n\t// TODO(a.garipov): Add more as we go.\n\t//\n\t// TODO(a.garipov): Add wait time.\n\n\tAddresses       jsonpatch.NonRemovable[[]netip.AddrPort] `json:\"addresses\"`\n\tSecureAddresses jsonpatch.NonRemovable[[]netip.AddrPort] `json:\"secure_addresses\"`\n\n\tTimeout jsonpatch.NonRemovable[aghhttp.JSONDuration] `json:\"timeout\"`\n\n\tForceHTTPS jsonpatch.NonRemovable[bool] `json:\"force_https\"`\n}\n\n// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API.  See the\n// HttpSettings object in the OpenAPI specification.\ntype HTTPAPIHTTPSettings struct {\n\t// TODO(a.garipov): Add more as we go.\n\n\tAddresses       []netip.AddrPort     `json:\"addresses\"`\n\tSecureAddresses []netip.AddrPort     `json:\"secure_addresses\"`\n\tTimeout         aghhttp.JSONDuration `json:\"timeout\"`\n\tForceHTTPS      bool                 `json:\"force_https\"`\n}\n\n// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http\n// HTTP API.\nfunc (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\treq := &ReqPatchSettingsHTTP{}\n\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\taghhttp.WriteJSONResponseError(ctx, svc.logger, w, r, fmt.Errorf(\"decoding: %w\", err))\n\n\t\treturn\n\t}\n\n\tnewConf := svc.Config()\n\n\t// TODO(a.garipov): Add more as we go.\n\n\treq.Addresses.Set(&newConf.Addresses)\n\treq.SecureAddresses.Set(&newConf.SecureAddresses)\n\treq.Timeout.Set((*aghhttp.JSONDuration)(&newConf.Timeout))\n\treq.ForceHTTPS.Set(&newConf.ForceHTTPS)\n\n\taghhttp.WriteJSONResponseOK(ctx, svc.logger, w, r, &HTTPAPIHTTPSettings{\n\t\tAddresses:       newConf.Addresses,\n\t\tSecureAddresses: newConf.SecureAddresses,\n\t\tTimeout:         aghhttp.JSONDuration(newConf.Timeout),\n\t\tForceHTTPS:      newConf.ForceHTTPS,\n\t})\n\n\tcancelUpd := func() {}\n\tupdCtx := context.Background()\n\n\tif deadline, ok := ctx.Deadline(); ok {\n\t\tupdCtx, cancelUpd = context.WithDeadline(updCtx, deadline)\n\t}\n\n\t// Launch the new HTTP service in a separate goroutine to let this handler\n\t// finish and thus, this server to shutdown.\n\tgo svc.relaunch(updCtx, cancelUpd, newConf)\n}\n\n// relaunch updates the web service in the configuration manager and starts it.\n// It is intended to be used as a goroutine.\nfunc (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, newConf *Config) {\n\tdefer slogutil.RecoverAndLog(ctx, svc.logger)\n\n\tdefer cancel()\n\n\terr := svc.confMgr.UpdateWeb(ctx, newConf)\n\tif err != nil {\n\t\tsvc.logger.ErrorContext(ctx, \"updating web\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\t// TODO(a.garipov): Consider better ways to do this.\n\tconst maxUpdDur = 5 * time.Second\n\tupdStart := time.Now()\n\tvar newSvc agh.ServiceWithConfig[*Config]\n\tfor newSvc = svc.confMgr.Web(); newSvc == svc; {\n\t\tif time.Since(updStart) >= maxUpdDur {\n\t\t\tsvc.logger.ErrorContext(ctx, \"failed to update service on time\", \"duration\", maxUpdDur)\n\n\t\t\treturn\n\t\t}\n\n\t\tsvc.logger.DebugContext(ctx, \"waiting for new service\")\n\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\n\terr = newSvc.Start(ctx)\n\tif err != nil {\n\t\tsvc.logger.ErrorContext(ctx, \"new service failed\", slogutil.KeyError, err)\n\t}\n}\n"
  },
  {
    "path": "internal/next/websvc/http_test.go",
    "content": "package websvc_test\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/websvc\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestService_HandlePatchSettingsHTTP(t *testing.T) {\n\twantWeb := &websvc.HTTPAPIHTTPSettings{\n\t\tAddresses:       []netip.AddrPort{netip.MustParseAddrPort(\"127.0.1.1:80\")},\n\t\tSecureAddresses: []netip.AddrPort{netip.MustParseAddrPort(\"127.0.1.1:443\")},\n\t\tTimeout:         aghhttp.JSONDuration(10 * time.Second),\n\t\tForceHTTPS:      false,\n\t}\n\n\tsvc, err := websvc.New(&websvc.Config{\n\t\tLogger: slogutil.NewDiscardLogger(),\n\t\tPprof: &websvc.PprofConfig{\n\t\t\tEnabled: false,\n\t\t},\n\t\tTLS: &tls.Config{\n\t\t\tCertificates: []tls.Certificate{{}},\n\t\t},\n\t\tAddresses:       []netip.AddrPort{netip.MustParseAddrPort(\"127.0.0.1:0\")},\n\t\tSecureAddresses: []netip.AddrPort{netip.MustParseAddrPort(\"127.0.0.1:0\")},\n\t\tTimeout:         5 * time.Second,\n\t\tForceHTTPS:      true,\n\t})\n\trequire.NoError(t, err)\n\n\tconfMgr := newConfigManager()\n\tconfMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) { return svc }\n\tconfMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) { return nil }\n\n\t_, addr := newTestServer(t, confMgr)\n\tu := &url.URL{\n\t\tScheme: urlutil.SchemeHTTP,\n\t\tHost:   addr.String(),\n\t\tPath:   websvc.PathPatternV1SettingsHTTP,\n\t}\n\n\treq := jobj{\n\t\t\"addresses\":        wantWeb.Addresses,\n\t\t\"secure_addresses\": wantWeb.SecureAddresses,\n\t\t\"timeout\":          wantWeb.Timeout,\n\t\t\"force_https\":      wantWeb.ForceHTTPS,\n\t}\n\n\trespBody := httpPatch(t, u, req, http.StatusOK)\n\tresp := &websvc.HTTPAPIHTTPSettings{}\n\terr = json.Unmarshal(respBody, resp)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, wantWeb, resp)\n}\n"
  },
  {
    "path": "internal/next/websvc/middleware.go",
    "content": "package websvc\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n)\n\n// jsonMw sets the content type of the response to application/json.\nfunc jsonMw(h http.Handler) (wrapped http.HandlerFunc) {\n\tf := func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)\n\n\t\th.ServeHTTP(w, r)\n\t}\n\n\treturn http.HandlerFunc(f)\n}\n"
  },
  {
    "path": "internal/next/websvc/route.go",
    "content": "package websvc\n\nimport (\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/golibs/netutil/httputil\"\n)\n\n// Path pattern constants.\nconst (\n\tPathPatternFrontend       = \"/\"\n\tPathPatternHealthCheck    = \"/health-check\"\n\tPathPatternV1SettingsAll  = \"/api/v1/settings/all\"\n\tPathPatternV1SettingsDNS  = \"/api/v1/settings/dns\"\n\tPathPatternV1SettingsHTTP = \"/api/v1/settings/http\"\n\tPathPatternV1SystemInfo   = \"/api/v1/system/info\"\n)\n\n// Route pattern constants.\nconst (\n\troutePatternFrontend            = http.MethodGet + \" \" + PathPatternFrontend\n\troutePatternGetV1SettingsAll    = http.MethodGet + \" \" + PathPatternV1SettingsAll\n\troutePatternGetV1SystemInfo     = http.MethodGet + \" \" + PathPatternV1SystemInfo\n\troutePatternHealthCheck         = http.MethodGet + \" \" + PathPatternHealthCheck\n\troutePatternPatchV1SettingsDNS  = http.MethodPatch + \" \" + PathPatternV1SettingsDNS\n\troutePatternPatchV1SettingsHTTP = http.MethodPatch + \" \" + PathPatternV1SettingsHTTP\n)\n\n// route registers all necessary handlers in mux.\nfunc (svc *Service) route(mux *http.ServeMux) {\n\troutes := []struct {\n\t\thandler http.Handler\n\t\tpattern string\n\t\tisJSON  bool\n\t}{{\n\t\thandler: httputil.HealthCheckHandler,\n\t\tpattern: routePatternHealthCheck,\n\t\tisJSON:  false,\n\t}, {\n\t\thandler: http.FileServer(http.FS(svc.frontend)),\n\t\tpattern: routePatternFrontend,\n\t\tisJSON:  false,\n\t}, {\n\t\thandler: http.HandlerFunc(svc.handleGetSettingsAll),\n\t\tpattern: routePatternGetV1SettingsAll,\n\t\tisJSON:  true,\n\t}, {\n\t\thandler: http.HandlerFunc(svc.handlePatchSettingsDNS),\n\t\tpattern: routePatternPatchV1SettingsDNS,\n\t\tisJSON:  true,\n\t}, {\n\t\thandler: http.HandlerFunc(svc.handlePatchSettingsHTTP),\n\t\tpattern: routePatternPatchV1SettingsHTTP,\n\t\tisJSON:  true,\n\t}, {\n\t\thandler: http.HandlerFunc(svc.handleGetV1SystemInfo),\n\t\tpattern: routePatternGetV1SystemInfo,\n\t\tisJSON:  true,\n\t}}\n\n\tlogMw := httputil.NewLogMiddleware(svc.logger, slog.LevelDebug)\n\tfor _, r := range routes {\n\t\tvar hdlr http.Handler\n\t\tif r.isJSON {\n\t\t\thdlr = jsonMw(r.handler)\n\t\t} else {\n\t\t\thdlr = r.handler\n\t\t}\n\n\t\tmux.Handle(r.pattern, logMw.Wrap(hdlr))\n\t}\n}\n"
  },
  {
    "path": "internal/next/websvc/server.go",
    "content": "package websvc\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n)\n\n// server contains an *http.Server as well as entities and data associated with\n// it.\n//\n// TODO(a.garipov):  Join with similar structs in other projects and move to\n// golibs/netutil/httputil.\n//\n// TODO(a.garipov):  Once the above standardization is complete, consider\n// merging debugsvc and websvc into a single httpsvc.\ntype server struct {\n\t// mu protects http, logger, tcpListener, and url.\n\tmu          *sync.Mutex\n\thttp        *http.Server\n\tlogger      *slog.Logger\n\ttcpListener *net.TCPListener\n\turl         *url.URL\n\n\ttlsConf     *tls.Config\n\tinitialAddr netip.AddrPort\n}\n\n// loggerKeyServer is the key used by [server] to identify itself.\nconst loggerKeyServer = \"server\"\n\n// newServer returns a *server that is ready to serve HTTP queries.  The TCP\n// listener is not started.  handler must not be nil.\nfunc newServer(\n\tbaseLogger *slog.Logger,\n\tinitialAddr netip.AddrPort,\n\ttlsConf *tls.Config,\n\thandler http.Handler,\n\ttimeout time.Duration,\n) (s *server) {\n\tu := &url.URL{\n\t\tScheme: urlutil.SchemeHTTP,\n\t\tHost:   initialAddr.String(),\n\t}\n\n\tif tlsConf != nil {\n\t\tu.Scheme = urlutil.SchemeHTTPS\n\t}\n\n\tlogger := baseLogger.With(loggerKeyServer, u)\n\n\treturn &server{\n\t\tmu: &sync.Mutex{},\n\t\thttp: &http.Server{\n\t\t\tHandler:           handler,\n\t\t\tReadTimeout:       timeout,\n\t\t\tReadHeaderTimeout: timeout,\n\t\t\tWriteTimeout:      timeout,\n\t\t\tIdleTimeout:       timeout,\n\t\t\tErrorLog:          slog.NewLogLogger(logger.Handler(), slog.LevelError),\n\t\t},\n\t\tlogger: logger,\n\t\turl:    u,\n\n\t\ttlsConf:     tlsConf,\n\t\tinitialAddr: initialAddr,\n\t}\n}\n\n// localAddr returns the local address of the server if the server has started\n// listening; otherwise, it returns nil.\nfunc (s *server) localAddr() (addr net.Addr) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif l := s.tcpListener; l != nil {\n\t\treturn l.Addr()\n\t}\n\n\treturn nil\n}\n\n// serve starts s.  baseLogger is used as a base logger for s.  If s fails to\n// serve with anything other than [http.ErrServerClosed], it causes an unhandled\n// panic.  It is intended to be used as a goroutine.\n//\n// TODO(a.garipov):  Improve error handling.\nfunc (s *server) serve(ctx context.Context, baseLogger *slog.Logger) {\n\tl, err := net.ListenTCP(\"tcp\", net.TCPAddrFromAddrPort(s.initialAddr))\n\tif err != nil {\n\t\ts.logger.ErrorContext(ctx, \"listening tcp\", slogutil.KeyError, err)\n\n\t\tpanic(fmt.Errorf(\"websvc: listening tcp: %w\", err))\n\t}\n\n\tfunc() {\n\t\ts.mu.Lock()\n\t\tdefer s.mu.Unlock()\n\n\t\ts.tcpListener = l\n\n\t\t// Reassign the address in case the port was zero.\n\t\ts.url.Host = l.Addr().String()\n\t\ts.logger = baseLogger.With(loggerKeyServer, s.url)\n\t\ts.http.ErrorLog = slog.NewLogLogger(s.logger.Handler(), slog.LevelError)\n\t}()\n\n\ts.logger.InfoContext(ctx, \"starting\")\n\tdefer s.logger.InfoContext(ctx, \"started\")\n\n\terr = s.http.Serve(l)\n\tif err == nil || errors.Is(err, http.ErrServerClosed) {\n\t\treturn\n\t}\n\n\ts.logger.ErrorContext(ctx, \"serving\", slogutil.KeyError, err)\n\n\tpanic(fmt.Errorf(\"websvc: serving: %w\", err))\n}\n\n// shutdown shuts s down.\nfunc (s *server) shutdown(ctx context.Context) (err error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tvar errs []error\n\terr = s.http.Shutdown(ctx)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"shutting down server %s: %w\", s.url, err))\n\t}\n\n\t// Close the listener separately, as it might not have been closed if the\n\t// context has been canceled.\n\t//\n\t// NOTE:  The listener could remain uninitialized if [net.ListenTCP] failed\n\t// in [s.serve].\n\tif l := s.tcpListener; l != nil {\n\t\terr = l.Close()\n\t\tif err != nil && !errors.Is(err, net.ErrClosed) {\n\t\t\terrs = append(errs, fmt.Errorf(\"closing listener for server %s: %w\", s.url, err))\n\t\t}\n\t}\n\n\treturn errors.Join(errs...)\n}\n"
  },
  {
    "path": "internal/next/websvc/settings.go",
    "content": "package websvc\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n)\n\n// All Settings Handlers\n\n// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all\n// HTTP API.\ntype RespGetV1SettingsAll struct {\n\t// TODO(a.garipov): Add more as we go.\n\n\tDNS  *HTTPAPIDNSSettings  `json:\"dns\"`\n\tHTTP *HTTPAPIHTTPSettings `json:\"http\"`\n}\n\n// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP\n// API.\nfunc (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {\n\tdnsSvc := svc.confMgr.DNS()\n\tdnsConf := dnsSvc.Config()\n\n\twebSvc := svc.confMgr.Web()\n\thttpConf := webSvc.Config()\n\n\t// TODO(a.garipov): Add all currently supported parameters.\n\taghhttp.WriteJSONResponseOK(r.Context(), svc.logger, w, r, &RespGetV1SettingsAll{\n\t\tDNS: &HTTPAPIDNSSettings{\n\t\t\tUpstreamMode:        dnsConf.UpstreamMode,\n\t\t\tAddresses:           dnsConf.Addresses,\n\t\t\tBootstrapServers:    dnsConf.BootstrapServers,\n\t\t\tUpstreamServers:     dnsConf.UpstreamServers,\n\t\t\tDNS64Prefixes:       dnsConf.DNS64Prefixes,\n\t\t\tUpstreamTimeout:     aghhttp.JSONDuration(dnsConf.UpstreamTimeout),\n\t\t\tRatelimit:           dnsConf.Ratelimit,\n\t\t\tBootstrapPreferIPv6: dnsConf.BootstrapPreferIPv6,\n\t\t\tCacheSize:           dnsConf.CacheSize,\n\t\t\tRefuseAny:           dnsConf.RefuseAny,\n\t\t\tUseDNS64:            dnsConf.UseDNS64,\n\t\t},\n\t\tHTTP: &HTTPAPIHTTPSettings{\n\t\t\tAddresses:       httpConf.Addresses,\n\t\t\tSecureAddresses: httpConf.SecureAddresses,\n\t\t\tTimeout:         aghhttp.JSONDuration(httpConf.Timeout),\n\t\t\tForceHTTPS:      httpConf.ForceHTTPS,\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "internal/next/websvc/settings_test.go",
    "content": "package websvc_test\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/websvc\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestService_HandleGetSettingsAll(t *testing.T) {\n\t// TODO(a.garipov): Add all currently supported parameters.\n\n\twantDNS := &websvc.HTTPAPIDNSSettings{\n\t\tUpstreamMode:        proxy.UpstreamModeParallel,\n\t\tAddresses:           []netip.AddrPort{netip.MustParseAddrPort(\"127.0.0.1:53\")},\n\t\tBootstrapServers:    []string{\"94.140.14.140\", \"94.140.14.141\"},\n\t\tUpstreamServers:     []string{\"94.140.14.14\", \"1.1.1.1\"},\n\t\tUpstreamTimeout:     aghhttp.JSONDuration(1 * time.Second),\n\t\tCacheSize:           1048576,\n\t\tBootstrapPreferIPv6: true,\n\t\tRefuseAny:           true,\n\t\tUseDNS64:            true,\n\t}\n\n\tconfMgr := newConfigManager()\n\tconfMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {\n\t\tc, err := dnssvc.New(&dnssvc.Config{\n\t\t\tLogger:              slogutil.NewDiscardLogger(),\n\t\t\tUpstreamMode:        proxy.UpstreamModeParallel,\n\t\t\tAddresses:           wantDNS.Addresses,\n\t\t\tUpstreamServers:     wantDNS.UpstreamServers,\n\t\t\tBootstrapServers:    wantDNS.BootstrapServers,\n\t\t\tUpstreamTimeout:     time.Duration(wantDNS.UpstreamTimeout),\n\t\t\tCacheSize:           1048576,\n\t\t\tBootstrapPreferIPv6: true,\n\t\t\tRefuseAny:           true,\n\t\t\tUseDNS64:            true,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\treturn c\n\t}\n\n\tsvc, addr := newTestServer(t, confMgr)\n\tu := &url.URL{\n\t\tScheme: urlutil.SchemeHTTP,\n\t\tHost:   addr.String(),\n\t\tPath:   websvc.PathPatternV1SettingsAll,\n\t}\n\n\tconfMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {\n\t\treturn svc\n\t}\n\n\twantWeb := &websvc.HTTPAPIHTTPSettings{\n\t\tAddresses:       []netip.AddrPort{addr},\n\t\tSecureAddresses: nil,\n\t\tTimeout:         aghhttp.JSONDuration(testTimeout),\n\t\tForceHTTPS:      false,\n\t}\n\n\tbody := httpGet(t, u, http.StatusOK)\n\tresp := &websvc.RespGetV1SettingsAll{}\n\terr := json.Unmarshal(body, resp)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, wantDNS, resp.DNS)\n\tassert.Equal(t, wantWeb, resp.HTTP)\n}\n"
  },
  {
    "path": "internal/next/websvc/system.go",
    "content": "package websvc\n\nimport (\n\t\"net/http\"\n\t\"runtime\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n)\n\n// System Handlers\n\n// RespGetV1SystemInfo describes the response of the GET /api/v1/system/info\n// HTTP API.\ntype RespGetV1SystemInfo struct {\n\tArch       string           `json:\"arch\"`\n\tChannel    string           `json:\"channel\"`\n\tOS         string           `json:\"os\"`\n\tNewVersion string           `json:\"new_version,omitempty\"`\n\tStart      aghhttp.JSONTime `json:\"start\"`\n\tVersion    string           `json:\"version\"`\n}\n\n// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP\n// API.\nfunc (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {\n\taghhttp.WriteJSONResponseOK(r.Context(), svc.logger, w, r, &RespGetV1SystemInfo{\n\t\tArch:    runtime.GOARCH,\n\t\tChannel: version.Channel(),\n\t\tOS:      runtime.GOOS,\n\t\t// TODO(a.garipov): Fill this when we have an updater.\n\t\tNewVersion: \"\",\n\t\tStart:      aghhttp.JSONTime(svc.start),\n\t\tVersion:    version.Version(),\n\t})\n}\n"
  },
  {
    "path": "internal/next/websvc/system_test.go",
    "content": "package websvc_test\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/websvc\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestService_handleGetV1SystemInfo(t *testing.T) {\n\tconfMgr := newConfigManager()\n\t_, addr := newTestServer(t, confMgr)\n\tu := &url.URL{\n\t\tScheme: urlutil.SchemeHTTP,\n\t\tHost:   addr.String(),\n\t\tPath:   websvc.PathPatternV1SystemInfo,\n\t}\n\n\tbody := httpGet(t, u, http.StatusOK)\n\tresp := &websvc.RespGetV1SystemInfo{}\n\terr := json.Unmarshal(body, resp)\n\trequire.NoError(t, err)\n\n\t// TODO(a.garipov): Consider making version.Channel and version.Version\n\t// testable and test these better.\n\tassert.NotEmpty(t, resp.Channel)\n\n\tassert.Equal(t, resp.Arch, runtime.GOARCH)\n\tassert.Equal(t, resp.OS, runtime.GOOS)\n\tassert.Equal(t, testStart, time.Time(resp.Start))\n}\n"
  },
  {
    "path": "internal/next/websvc/websvc.go",
    "content": "// Package websvc contains the AdGuard Home HTTP API service.\n//\n// NOTE: Packages other than cmd must not import this package, as it imports\n// most other packages.\n//\n// TODO(a.garipov):  Add tests.\n//\n// TODO(a.garipov):  Split into subpackages for groups of handlers?\npackage websvc\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/httputil\"\n)\n\n// ConfigManager is the configuration manager interface.\n//\n// TODO(a.garipov): Add docs.\ntype ConfigManager interface {\n\tDNS() (svc agh.ServiceWithConfig[*dnssvc.Config])\n\tWeb() (svc agh.ServiceWithConfig[*Config])\n\n\tUpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)\n\tUpdateWeb(ctx context.Context, c *Config) (err error)\n}\n\n// Service is the AdGuard Home web service.  A nil *Service is a valid\n// [agh.Service] that does nothing.\ntype Service struct {\n\tlogger       *slog.Logger\n\tconfMgr      ConfigManager\n\tfrontend     fs.FS\n\ttls          *tls.Config\n\tpprof        *server\n\tstart        time.Time\n\toverrideAddr netip.AddrPort\n\tservers      []*server\n\ttimeout      time.Duration\n\tpprofPort    uint16\n\tforceHTTPS   bool\n}\n\n// New returns a new properly initialized *Service.  If c is nil, svc is a nil\n// *Service that does nothing.  The fields of c must not be modified after\n// calling New.\n//\n// TODO(a.garipov): Get rid of this special handling of nil or explain it\n// better.\nfunc New(c *Config) (svc *Service, err error) {\n\tif c == nil {\n\t\treturn nil, nil\n\t}\n\n\tsvc = &Service{\n\t\tlogger:       c.Logger,\n\t\tconfMgr:      c.ConfigManager,\n\t\tfrontend:     c.Frontend,\n\t\ttls:          c.TLS,\n\t\tstart:        c.Start,\n\t\toverrideAddr: c.OverrideAddress,\n\t\ttimeout:      c.Timeout,\n\t\tforceHTTPS:   c.ForceHTTPS,\n\t}\n\n\tmux := http.NewServeMux()\n\tsvc.route(mux)\n\n\tif svc.overrideAddr != (netip.AddrPort{}) {\n\t\tsvc.servers = []*server{newServer(svc.logger, svc.overrideAddr, nil, mux, c.Timeout)}\n\t} else {\n\t\tfor _, a := range c.Addresses {\n\t\t\tsvc.servers = append(svc.servers, newServer(svc.logger, a, nil, mux, c.Timeout))\n\t\t}\n\n\t\tfor _, a := range c.SecureAddresses {\n\t\t\tsvc.servers = append(svc.servers, newServer(svc.logger, a, c.TLS, mux, c.Timeout))\n\t\t}\n\t}\n\n\tsvc.setupPprof(c.Pprof)\n\n\treturn svc, nil\n}\n\n// setupPprof sets the pprof properties of svc.\nfunc (svc *Service) setupPprof(c *PprofConfig) {\n\tif !c.Enabled {\n\t\t// Set to zero explicitly in case pprof used to be enabled before a\n\t\t// reconfiguration took place.\n\t\truntime.SetBlockProfileRate(0)\n\t\truntime.SetMutexProfileFraction(0)\n\n\t\treturn\n\t}\n\n\truntime.SetBlockProfileRate(1)\n\truntime.SetMutexProfileFraction(1)\n\n\tpprofMux := http.NewServeMux()\n\thttputil.RoutePprof(pprofMux)\n\n\tsvc.pprofPort = c.Port\n\taddr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), c.Port)\n\n\tsvc.pprof = newServer(svc.logger, addr, nil, pprofMux, 10*time.Minute)\n}\n\n// addrs returns all addresses on which this server serves the HTTP API.  addrs\n// must not be called simultaneously with Start.  If svc was initialized with\n// \":0\" addresses, addrs will not return the actual bound ports until Start is\n// finished.\nfunc (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {\n\tif svc.overrideAddr != (netip.AddrPort{}) {\n\t\treturn []netip.AddrPort{svc.overrideAddr}, nil\n\t}\n\n\tfor _, srv := range svc.servers {\n\t\taddrPort := netutil.NetAddrToAddrPort(srv.localAddr())\n\t\tif addrPort == (netip.AddrPort{}) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif srv.tlsConf == nil {\n\t\t\taddrs = append(addrs, addrPort)\n\t\t} else {\n\t\t\tsecureAddrs = append(secureAddrs, addrPort)\n\t\t}\n\t}\n\n\treturn addrs, secureAddrs\n}\n\n// type check\nvar _ agh.ServiceWithConfig[*Config] = (*Service)(nil)\n\n// Start implements the [agh.Service] interface for *Service.  svc may be nil.\n// After Start exits, all HTTP servers have tried to start, possibly failing and\n// writing error messages to the log.\n//\n// TODO(a.garipov):  Use the context for cancelation as well.\nfunc (svc *Service) Start(ctx context.Context) (err error) {\n\tif svc == nil {\n\t\treturn nil\n\t}\n\n\tsvc.logger.InfoContext(ctx, \"starting\")\n\tdefer svc.logger.InfoContext(ctx, \"started\")\n\n\tfor _, srv := range svc.servers {\n\t\tgo srv.serve(ctx, svc.logger)\n\t}\n\n\tif svc.pprof != nil {\n\t\tgo svc.pprof.serve(ctx, svc.logger)\n\t}\n\n\treturn svc.wait(ctx)\n}\n\n// wait waits until either the context is canceled or all servers have started.\nfunc (svc *Service) wait(ctx context.Context) (err error) {\n\tfor !svc.serversHaveStarted() {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t\t// Wait and let the other goroutines do their job.\n\t\t\truntime.Gosched()\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// serversHaveStarted returns true if all servers have started serving.\nfunc (svc *Service) serversHaveStarted() (started bool) {\n\tstarted = len(svc.servers) != 0\n\tfor _, srv := range svc.servers {\n\t\tstarted = started && srv.localAddr() != nil\n\t}\n\n\tif svc.pprof != nil {\n\t\tstarted = started && svc.pprof.localAddr() != nil\n\t}\n\n\treturn started\n}\n\n// Shutdown implements the [agh.Service] interface for *Service.  svc may be\n// nil.\nfunc (svc *Service) Shutdown(ctx context.Context) (err error) {\n\tif svc == nil {\n\t\treturn nil\n\t}\n\n\tsvc.logger.InfoContext(ctx, \"shutting down\")\n\tdefer svc.logger.InfoContext(ctx, \"shut down\")\n\n\tdefer func() { err = errors.Annotate(err, \"shutting down: %w\") }()\n\n\tvar errs []error\n\tfor _, srv := range svc.servers {\n\t\tshutdownErr := srv.shutdown(ctx)\n\t\tif shutdownErr != nil {\n\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif svc.pprof != nil {\n\t\tshutdownErr := svc.pprof.shutdown(ctx)\n\t\tif shutdownErr != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"pprof: %w\", shutdownErr))\n\t\t}\n\t}\n\n\treturn errors.Join(errs...)\n}\n"
  },
  {
    "path": "internal/next/websvc/websvc_test.go",
    "content": "package websvc_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/websvc\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/httputil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/fakeio/fakefs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testStart is the server start value for tests.\nvar testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)\n\n// type check\nvar _ websvc.ConfigManager = (*configManager)(nil)\n\n// configManager is a [websvc.ConfigManager] for tests.\ntype configManager struct {\n\tonDNS func() (svc agh.ServiceWithConfig[*dnssvc.Config])\n\tonWeb func() (svc agh.ServiceWithConfig[*websvc.Config])\n\n\tonUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)\n\tonUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)\n}\n\n// DNS implements the [websvc.ConfigManager] interface for *configManager.\nfunc (m *configManager) DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) {\n\treturn m.onDNS()\n}\n\n// Web implements the [websvc.ConfigManager] interface for *configManager.\nfunc (m *configManager) Web() (svc agh.ServiceWithConfig[*websvc.Config]) {\n\treturn m.onWeb()\n}\n\n// UpdateDNS implements the [websvc.ConfigManager] interface for *configManager.\nfunc (m *configManager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {\n\treturn m.onUpdateDNS(ctx, c)\n}\n\n// UpdateWeb implements the [websvc.ConfigManager] interface for *configManager.\nfunc (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {\n\treturn m.onUpdateWeb(ctx, c)\n}\n\n// newConfigManager returns a *configManager all methods of which panic.\nfunc newConfigManager() (m *configManager) {\n\treturn &configManager{\n\t\tonDNS: func() (_ agh.ServiceWithConfig[*dnssvc.Config]) {\n\t\t\tpanic(testutil.UnexpectedCall())\n\t\t},\n\t\tonWeb: func() (_ agh.ServiceWithConfig[*websvc.Config]) {\n\t\t\tpanic(testutil.UnexpectedCall())\n\t\t},\n\t\tonUpdateDNS: func(ctx context.Context, c *dnssvc.Config) (_ error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx, c))\n\t\t},\n\t\tonUpdateWeb: func(ctx context.Context, c *websvc.Config) (_ error) {\n\t\t\tpanic(testutil.UnexpectedCall(ctx, c))\n\t\t},\n\t}\n}\n\n// newTestServer creates and starts a new web service instance as well as its\n// sole address.  It also registers a cleanup procedure, which shuts the\n// instance down.\nfunc newTestServer(\n\ttb testing.TB,\n\tconfMgr websvc.ConfigManager,\n) (svc *websvc.Service, addr netip.AddrPort) {\n\ttb.Helper()\n\n\tc := &websvc.Config{\n\t\tLogger: slogutil.NewDiscardLogger(),\n\t\tPprof: &websvc.PprofConfig{\n\t\t\tEnabled: false,\n\t\t},\n\t\tConfigManager: confMgr,\n\t\tFrontend: &fakefs.FS{\n\t\t\tOnOpen: func(_ string) (_ fs.File, _ error) { return nil, fs.ErrNotExist },\n\t\t},\n\t\tTLS:             nil,\n\t\tAddresses:       []netip.AddrPort{netip.MustParseAddrPort(\"127.0.0.1:0\")},\n\t\tSecureAddresses: nil,\n\t\tTimeout:         testTimeout,\n\t\tStart:           testStart,\n\t\tForceHTTPS:      false,\n\t}\n\n\tsvc, err := websvc.New(c)\n\trequire.NoError(tb, err)\n\n\terr = svc.Start(testutil.ContextWithTimeout(tb, testTimeout))\n\trequire.NoError(tb, err)\n\ttestutil.CleanupAndRequireSuccess(tb, func() (err error) {\n\t\treturn svc.Shutdown(testutil.ContextWithTimeout(tb, testTimeout))\n\t})\n\n\tc = svc.Config()\n\trequire.NotNil(tb, c)\n\trequire.Len(tb, c.Addresses, 1)\n\n\treturn svc, c.Addresses[0]\n}\n\n// jobj is a utility alias for JSON objects.\ntype jobj map[string]any\n\n// httpGet is a helper that performs an HTTP GET request and returns the body of\n// the response as well as checks that the status code is correct.\n//\n// TODO(a.garipov): Add helpers for other methods.\nfunc httpGet(tb testing.TB, u *url.URL, wantCode int) (body []byte) {\n\ttb.Helper()\n\n\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\trequire.NoErrorf(tb, err, \"creating req\")\n\n\thttpCli := &http.Client{\n\t\tTimeout: testTimeout,\n\t}\n\tresp, err := httpCli.Do(req)\n\trequire.NoErrorf(tb, err, \"performing req\")\n\trequire.Equal(tb, wantCode, resp.StatusCode)\n\n\ttestutil.CleanupAndRequireSuccess(tb, resp.Body.Close)\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoErrorf(tb, err, \"reading body\")\n\n\treturn body\n}\n\n// httpPatch is a helper that performs an HTTP PATCH request with JSON-encoded\n// reqBody as the request body and returns the body of the response as well as\n// checks that the status code is correct.\n//\n// TODO(a.garipov): Add helpers for other methods.\nfunc httpPatch(tb testing.TB, u *url.URL, reqBody any, wantCode int) (body []byte) {\n\ttb.Helper()\n\n\tb, err := json.Marshal(reqBody)\n\trequire.NoErrorf(tb, err, \"marshaling reqBody\")\n\n\treq, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(b))\n\trequire.NoErrorf(tb, err, \"creating req\")\n\n\thttpCli := &http.Client{\n\t\tTimeout: testTimeout,\n\t}\n\tresp, err := httpCli.Do(req)\n\trequire.NoErrorf(tb, err, \"performing req\")\n\trequire.Equal(tb, wantCode, resp.StatusCode)\n\n\ttestutil.CleanupAndRequireSuccess(tb, resp.Body.Close)\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoErrorf(tb, err, \"reading body\")\n\n\treturn body\n}\n\nfunc TestService_Start_getHealthCheck(t *testing.T) {\n\tconfMgr := newConfigManager()\n\t_, addr := newTestServer(t, confMgr)\n\tu := &url.URL{\n\t\tScheme: urlutil.SchemeHTTP,\n\t\tHost:   addr.String(),\n\t\tPath:   websvc.PathPatternHealthCheck,\n\t}\n\n\tbody := httpGet(t, u, http.StatusOK)\n\n\tassert.Equal(t, []byte(httputil.HealthCheckHandler), body)\n}\n"
  },
  {
    "path": "internal/ossvc/action.go",
    "content": "package ossvc\n\n// ActionName is the type for actions' names.  It has the following valid\n// values:\n//   - [ActionNameInstall]\n//   - [ActionNameRestart]\n//   - [ActionNameStart]\n//   - [ActionNameStop]\n//   - [ActionNameUninstall]\ntype ActionName string\n\nconst (\n\tActionNameInstall   ActionName = \"install\"\n\tActionNameRestart   ActionName = \"restart\"\n\tActionNameStart     ActionName = \"start\"\n\tActionNameStop      ActionName = \"stop\"\n\tActionNameUninstall ActionName = \"uninstall\"\n)\n\n// Action is the interface for actions that can be performed by [Manager].\ntype Action interface {\n\t// Name returns the name of the action.\n\tName() (name ActionName)\n\n\t// isAction is a marker method to prevent types from other packages from\n\t// implementing this interface.\n\tisAction()\n}\n"
  },
  {
    "path": "internal/ossvc/config.go",
    "content": "package ossvc\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/kardianos/service\"\n)\n\n// ConfigureServiceOptions defines additional settings of the service\n// configuration.  conf must not be nil.\n//\n// TODO(e.burkov):  Use [timeutil.Clock].\nfunc ConfigureServiceOptions(conf *service.Config, versionInfo string) {\n\tif conf.Option == nil {\n\t\tconf.Option = map[string]any{}\n\t}\n\n\tconf.Option[\"SvcInfo\"] = fmt.Sprintf(\"%s %s\", versionInfo, time.Now())\n\n\tconfigureOSOptions(conf)\n}\n"
  },
  {
    "path": "internal/ossvc/config_darwin.go",
    "content": "//go:build darwin\n\npackage ossvc\n\nimport (\n\t\"github.com/kardianos/service\"\n)\n\n// configureServiceOptions defines additional settings of the service\n// configuration on macOS.  conf must not be nil.\nfunc configureOSOptions(conf *service.Config) {\n\tconf.Option[\"LaunchdConfig\"] = launchdConfig\n}\n\n// launchdConfig is the template for Launchd service file.  Basically the same\n// template as the one defined in github.com/kardianos/service but with two\n// additional keys:\n//   - StandardOutPath\n//   - StandardErrorPath\nconst launchdConfig = `<?xml version='1.0' encoding='UTF-8'?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\"\n\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\" >\n<plist version='1.0'>\n<dict>\n<key>Label</key><string>{{html .Name}}</string>\n<key>ProgramArguments</key>\n<array>\n        <string>{{html .Path}}</string>\n{{range .Config.Arguments}}\n        <string>{{html .}}</string>\n{{end}}\n</array>\n{{if .UserName}}<key>UserName</key><string>{{html .UserName}}</string>{{end}}\n{{if .ChRoot}}<key>RootDirectory</key><string>{{html .ChRoot}}</string>{{end}}\n{{if .WorkingDirectory}}<key>WorkingDirectory</key><string>{{html .WorkingDirectory}}</string>{{end}}\n<key>SessionCreate</key><{{bool .SessionCreate}}/>\n<key>KeepAlive</key><{{bool .KeepAlive}}/>\n<key>RunAtLoad</key><{{bool .RunAtLoad}}/>\n<key>Disabled</key><false/>\n<key>StandardOutPath</key>\n<string>` + launchdStdoutPath + `</string>\n<key>StandardErrorPath</key>\n<string>` + launchdStderrPath + `</string>\n</dict>\n</plist>\n`\n"
  },
  {
    "path": "internal/ossvc/config_freebsd.go",
    "content": "//go:build freebsd\n\npackage ossvc\n\nimport (\n\t\"github.com/kardianos/service\"\n)\n\n// configureServiceOptions defines additional settings of the service\n// configuration on FreeBSD.  conf must not be nil.\nfunc configureOSOptions(conf *service.Config) {\n\tconf.Option[\"SysvScript\"] = freeBSDScript\n}\n\n// freeBSDScript is the source of the daemon script for FreeBSD.  Keep as close\n// as possible to the https://github.com/kardianos/service/blob/18c957a3dc1120a2efe77beb401d476bade9e577/service_freebsd.go#L204.\nconst freeBSDScript = `#!/bin/sh\n# PROVIDE: {{.Name}}\n# REQUIRE: networking\n# KEYWORD: shutdown\n\n. /etc/rc.subr\n\nname=\"{{.Name}}\"\n{{.Name}}_env=\"IS_DAEMON=1\"\n{{.Name}}_user=\"root\"\npidfile_child=\"/var/run/${name}.pid\"\npidfile=\"/var/run/${name}_daemon.pid\"\ncommand=\"/usr/sbin/daemon\"\ndaemon_args=\"-P ${pidfile} -p ${pidfile_child} -r -t ${name}\"\ncommand_args=\"${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}\"\n\nrun_rc_command \"$1\"\n`\n"
  },
  {
    "path": "internal/ossvc/config_linux.go",
    "content": "//go:build linux\n\npackage ossvc\n\nimport (\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/kardianos/service\"\n)\n\n// configureServiceOptions defines additional settings of the service\n// configuration on Linux.  conf must not be nil.\nfunc configureOSOptions(conf *service.Config) {\n\tconf.Option[\"LogOutput\"] = true\n\tconf.Option[\"Restart\"] = \"always\"\n\n\tconf.Dependencies = []string{\n\t\t\"After=syslog.target network-online.target\",\n\t}\n\n\tconf.Option[\"SystemdScript\"] = systemdScript\n\n\t// Use different scripts on OpenWrt.\n\tif aghos.IsOpenWrt() {\n\t\tconf.Option[\"SysvScript\"] = openWrtScript\n\t} else {\n\t\tconf.Option[\"SysvScript\"] = sysvScript\n\t}\n}\n\n// systemdScript is an improved version of the systemd script originally from\n// the systemdScript constant in file service_systemd_linux.go in module\n// github.com/kardianos/service.  The following changes have been made:\n//\n//  1. The RestartSec setting is set to a lower value of 10 to make sure we\n//     always restart quickly.\n//\n//  2. The StandardOutput and StandardError settings are set to redirect the\n//     output to the systemd journal, see\n//     https://man7.org/linux/man-pages/man5/systemd.exec.5.html#LOGGING_AND_STANDARD_INPUT/OUTPUT.\nconst systemdScript = `[Unit]\nDescription={{.Description}}\nConditionFileIsExecutable={{.Path|cmdEscape}}\n{{range $i, $dep := .Dependencies}}\n{{$dep}} {{end}}\n\n[Service]\nStartLimitInterval=5\nStartLimitBurst=10\nExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}}\n{{if .ChRoot}}RootDirectory={{.ChRoot|cmd}}{{end}}\n{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}}\n{{if .UserName}}User={{.UserName}}{{end}}\n{{if .ReloadSignal}}ExecReload=/bin/kill -{{.ReloadSignal}} \"$MAINPID\"{{end}}\n{{if .PIDFile}}PIDFile={{.PIDFile|cmd}}{{end}}\n{{if and .LogOutput .HasOutputFileSupport -}}\nStandardOutput=journal\nStandardError=journal\n{{- end}}\n{{if gt .LimitNOFILE -1 }}LimitNOFILE={{.LimitNOFILE}}{{end}}\n{{if .Restart}}Restart={{.Restart}}{{end}}\n{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}}\nRestartSec=10\nEnvironmentFile=-/etc/sysconfig/{{.Name}}\n\n[Install]\nWantedBy=multi-user.target\n`\n\n// sysvScript is the source of the daemon script for SysV-based Linux systems.\n// Keep as close as possible to the https://github.com/kardianos/service/blob/29f8c79c511bc18422bb99992779f96e6bc33921/service_sysv_linux.go#L187.\n//\n// Use ps command instead of reading the procfs since it's a more\n// implementation-independent approach.\nconst sysvScript = `#!/bin/sh\n# For RedHat and cousins:\n# chkconfig: - 99 01\n# description: {{.Description}}\n# processname: {{.Path}}\n\n### BEGIN INIT INFO\n# Provides:          {{.Path}}\n# Required-Start:\n# Required-Stop:\n# Default-Start:     2 3 4 5\n# Default-Stop:      0 1 6\n# Short-Description: {{.DisplayName}}\n# Description:       {{.Description}}\n### END INIT INFO\n\ncmd=\"{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}\"\n\nname=$(basename $(readlink -f $0))\npid_file=\"/var/run/$name.pid\"\nstdout_log=\"/var/log/$name.log\"\nstderr_log=\"/var/log/$name.err\"\n\n[ -e /etc/sysconfig/$name ] && . /etc/sysconfig/$name\n\nget_pid() {\n    cat \"$pid_file\"\n}\n\nis_running() {\n    [ -f \"$pid_file\" ] && ps -p \"$(get_pid)\" > /dev/null 2>&1\n}\n\ncase \"$1\" in\n    start)\n        if is_running; then\n            echo \"Already started\"\n        else\n            echo \"Starting $name\"\n            {{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}}\n            $cmd >> \"$stdout_log\" 2>> \"$stderr_log\" &\n            echo $! > \"$pid_file\"\n            if ! is_running; then\n                echo \"Unable to start, see $stdout_log and $stderr_log\"\n                exit 1\n            fi\n        fi\n    ;;\n    stop)\n        if is_running; then\n            echo -n \"Stopping $name..\"\n            kill $(get_pid)\n            for i in $(seq 1 10)\n            do\n                if ! is_running; then\n                    break\n                fi\n                echo -n \".\"\n                sleep 1\n            done\n            echo\n            if is_running; then\n                echo \"Not stopped; may still be shutting down or shutdown may have failed\"\n                exit 1\n            else\n                echo \"Stopped\"\n                if [ -f \"$pid_file\" ]; then\n                    rm \"$pid_file\"\n                fi\n            fi\n        else\n            echo \"Not running\"\n        fi\n    ;;\n    restart)\n        $0 stop\n        if is_running; then\n            echo \"Unable to stop, will not attempt to start\"\n            exit 1\n        fi\n        $0 start\n    ;;\n    status)\n        if is_running; then\n            echo \"Running\"\n        else\n            echo \"Stopped\"\n            exit 1\n        fi\n    ;;\n    *)\n    echo \"Usage: $0 {start|stop|restart|status}\"\n    exit 1\n    ;;\nesac\nexit 0\n`\n\n// OpenWrt procd init script\n// https://github.com/AdguardTeam/AdGuardHome/internal/issues/1386\nconst openWrtScript = `#!/bin/sh /etc/rc.common\n\nUSE_PROCD=1\n\nSTART=95\nSTOP=01\n\ncmd=\"{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}\"\nname=\"{{.Name}}\"\npid_file=\"/var/run/${name}.pid\"\n\nstart_service() {\n    echo \"Starting ${name}\"\n\n    procd_open_instance\n    procd_set_param command ${cmd}\n    procd_set_param respawn             # respawn automatically if something died\n    procd_set_param stdout 1            # forward stdout of the command to logd\n    procd_set_param stderr 1            # same for stderr\n    procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop\n\n    procd_close_instance\n    echo \"${name} has been started\"\n}\n\nstop_service() {\n    echo \"Stopping ${name}\"\n}\n\nEXTRA_COMMANDS=\"status\"\nEXTRA_HELP=\"        status  Print the service status\"\n\nget_pid() {\n    cat \"${pid_file}\"\n}\n\nis_running() {\n    [ -f \"${pid_file}\" ] && ps | grep -v grep | grep $(get_pid) >/dev/null 2>&1\n}\n\nstatus() {\n    if is_running; then\n        echo \"Running\"\n    else\n        echo \"Stopped\"\n        exit 1\n    fi\n}\n`\n"
  },
  {
    "path": "internal/ossvc/config_openbsd.go",
    "content": "//go:build openbsd\n\npackage ossvc\n\nimport (\n\t\"github.com/kardianos/service\"\n)\n\n// configureServiceOptions defines additional settings of the service\n// configuration on OpenBSD.  conf must not be nil.\nfunc configureOSOptions(conf *service.Config) {\n\tconf.Option[\"RunComScript\"] = openBSDScript\n}\n\n// openBSDScript is the source of the daemon script for OpenBSD.\nconst openBSDScript = `#!/bin/ksh\n#\n# $OpenBSD: {{ .SvcInfo }}\n\ndaemon=\"{{.Path}}\"\ndaemon_flags={{ .Arguments | args }}\ndaemon_logger=\"daemon.info\"\n\n. /etc/rc.d/rc.subr\n\nrc_bg=YES\n\nrc_cmd $1\n`\n"
  },
  {
    "path": "internal/ossvc/config_windows.go",
    "content": "//go:build windows\n\npackage ossvc\n\nimport \"github.com/kardianos/service\"\n\n// configureOSOptions defines additional settings of the service\n// configuration on Windows.\nfunc configureOSOptions(_ *service.Config) {}\n"
  },
  {
    "path": "internal/ossvc/defaultaction.go",
    "content": "package ossvc\n\n// TODO(e.burkov):  Declare actions for each OS.\n\n// ActionInstall is the implementation of the [Action] interface.\ntype ActionInstall struct {\n\tServiceName      ServiceName\n\tDisplayName      string\n\tDescription      string\n\tWorkingDirectory string\n\tVersion          string\n\tArguments        []string\n}\n\n// Name implements the [Action] interface for *ActionInstall.\nfunc (a *ActionInstall) Name() (name ActionName) { return ActionNameInstall }\n\n// isAction implements the [Action] interface for *ActionInstall.\nfunc (a *ActionInstall) isAction() {}\n\n// ActionRestart is the implementation of the [Action] interface.\ntype ActionRestart struct {\n\tServiceName ServiceName\n}\n\n// Name implements the [Action] interface for *ActionRestart.\nfunc (a *ActionRestart) Name() (name ActionName) { return ActionNameRestart }\n\n// isAction implements the [Action] interface for *ActionRestart.\nfunc (a *ActionRestart) isAction() {}\n\n// ActionStart is the implementation of the [Action] interface.\ntype ActionStart struct {\n\tServiceName ServiceName\n}\n\n// Name implements the [Action] interface for *ActionStart.\nfunc (a *ActionStart) Name() (name ActionName) { return ActionNameStart }\n\n// isAction implements the [Action] interface for *ActionStart.\nfunc (a *ActionStart) isAction() {}\n\n// ActionStop is the implementation of the [Action] interface.\ntype ActionStop struct {\n\tServiceName ServiceName\n}\n\n// Name implements the [Action] interface for *ActionStop.\nfunc (a *ActionStop) Name() (name ActionName) { return ActionNameStop }\n\n// isAction implements the [Action] interface for *ActionStop.\nfunc (a *ActionStop) isAction() {}\n\n// ActionUninstall is the implementation of the [Action] interface.\ntype ActionUninstall struct {\n\tServiceName ServiceName\n}\n\n// Name implements the [Action] interface for *ActionUninstall.\nfunc (a *ActionUninstall) Name() (name ActionName) { return ActionNameUninstall }\n\n// isAction implements the [Action] interface for *ActionUninstall.\nfunc (a *ActionUninstall) isAction() {}\n"
  },
  {
    "path": "internal/ossvc/defaultmanager.go",
    "content": "package ossvc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/kardianos/service\"\n)\n\n// TODO(e.burkov):  Declare managers for each OS.\n\n// manager is the implementation of [Manager] that uses [service.Service].\ntype manager struct {\n\tlogger        *slog.Logger\n\tcmdCons       executil.CommandConstructor\n\tisOpenWrt     bool\n\tisUnixSystemV bool\n}\n\n// newManager creates a new [Manager] that uses [service.Service].\n//\n// TODO(e.burkov):  Return error.\nfunc newManager(_ context.Context, conf *ManagerConfig) (mgr *manager) {\n\t// Call chooseSystem explicitly to introduce platform-specific support for\n\t// service package.  It's a noop for other GOOS values.\n\tchooseSystem()\n\n\treturn &manager{\n\t\tlogger:        conf.Logger,\n\t\tcmdCons:       conf.CommandConstructor,\n\t\tisOpenWrt:     aghos.IsOpenWrt(),\n\t\tisUnixSystemV: service.Platform() == \"unix-systemv\",\n\t}\n}\n\n// type check\nvar _ Manager = (*manager)(nil)\n\n// Perform implements the [Manager] interface for *manager.\nfunc (m *manager) Perform(ctx context.Context, action Action) (err error) {\n\tswitch action := action.(type) {\n\tcase *ActionInstall:\n\t\terr = m.install(ctx, action)\n\tcase *ActionRestart:\n\t\terr = m.restart(ctx, action)\n\tcase *ActionStart:\n\t\terr = m.start(ctx, action)\n\tcase *ActionStop:\n\t\terr = m.stop(ctx, action)\n\tcase *ActionUninstall:\n\t\terr = m.uninstall(ctx, action)\n\tdefault:\n\t\tpanic(fmt.Errorf(\"action: %w: %T(%[2]v)\", errors.ErrBadEnumValue, action))\n\t}\n\tif err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tm.logger.DebugContext(\n\t\tctx,\n\t\t\"performed service action\",\n\t\t\"action\", action.Name(),\n\t\t\"system\", service.ChosenSystem(),\n\t)\n\n\treturn nil\n}\n\n// statusRestartOnFail is a custom status value used to indicate the service's\n// state of restarting after failed start.\nconst statusRestartOnFail = service.StatusStopped + 1\n\n// Status implements the [Manager] interface for *manager.\nfunc (m *manager) Status(ctx context.Context, name ServiceName) (status Status, err error) {\n\tm.logger.InfoContext(ctx, \"getting service status\", \"name\", name)\n\n\ts, err := service.New(nil, &service.Config{\n\t\tName: string(name),\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"creating service: %w\", err)\n\t}\n\n\tsvcStatus, err := s.Status()\n\tif err != nil && m.isUnixSystemV {\n\t\tvar code int\n\t\tcode, err = m.runInitdCommand(ctx, string(name), \"status\")\n\t\tif err != nil || code != 0 {\n\t\t\t// Treat an error or non-zero exit code as stopped status on Unix\n\t\t\t// System V.\n\t\t\t//\n\t\t\t// TODO(e.burkov):  Investigate if it's a valid assumption, and\n\t\t\t// properly handle errors in similar cases.\n\t\t\treturn StatusStopped, nil\n\t\t}\n\n\t\treturn StatusRunning, nil\n\t}\n\n\tif err != nil {\n\t\tif errors.Is(err, service.ErrNotInstalled) {\n\t\t\treturn StatusNotInstalled, nil\n\t\t}\n\n\t\treturn \"\", fmt.Errorf(\"getting service status: %w\", err)\n\t}\n\n\treturn statusToInternal(svcStatus)\n}\n\n// type check\nvar _ ReloadManager = (*manager)(nil)\n\n// Reload implements the [ReloadManager] interface for *manager.\nfunc (m *manager) Reload(ctx context.Context, name ServiceName) (err error) {\n\treturn m.reload(ctx, name)\n}\n\n// install installs the service in the service manager.\nfunc (m *manager) install(ctx context.Context, action *ActionInstall) (err error) {\n\tm.logger.InfoContext(ctx, \"installing service\", \"name\", action.ServiceName)\n\n\tconf := &service.Config{\n\t\tName:             string(action.ServiceName),\n\t\tDisplayName:      action.DisplayName,\n\t\tDescription:      action.Description,\n\t\tWorkingDirectory: action.WorkingDirectory,\n\t\tArguments:        action.Arguments,\n\t}\n\tConfigureServiceOptions(conf, action.Version)\n\n\ts, err := service.New(emptyInterface{}, conf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating service: %w\", err)\n\t}\n\n\terr = s.Install()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"installing service: %w\", err)\n\t}\n\n\tif m.isOpenWrt {\n\t\t// On OpenWrt it is important to run enable after the service\n\t\t// installation.  Otherwise, the service won't start on the system\n\t\t// startup.\n\t\t_, err = m.runInitdCommand(ctx, string(action.ServiceName), \"enable\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"enabling service on openwrt: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// restart stops, if not yet, and starts the configured service in the service\n// manager.\nfunc (m *manager) restart(ctx context.Context, action *ActionRestart) (err error) {\n\tm.logger.InfoContext(ctx, \"restarting service\", \"name\", action.ServiceName)\n\n\ts, err := service.New(emptyInterface{}, &service.Config{\n\t\tName: string(action.ServiceName),\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating service: %w\", err)\n\t}\n\n\terr = s.Restart()\n\tif err != nil && m.isUnixSystemV {\n\t\t_, initdErr := m.runInitdCommand(ctx, string(action.ServiceName), \"restart\")\n\t\tif initdErr != nil {\n\t\t\treturn fmt.Errorf(\"%w (restarting via init.d: %w)\", err, initdErr)\n\t\t}\n\t}\n\n\treturn err\n}\n\n// start starts the configured service in the service manager.\nfunc (m *manager) start(ctx context.Context, action *ActionStart) (err error) {\n\tm.logger.InfoContext(ctx, \"starting service\", \"name\", action.ServiceName)\n\n\t// Perform pre-check before starting service.\n\tif err = aghos.PreCheckActionStart(); err != nil {\n\t\tm.logger.ErrorContext(ctx, \"pre-check failed\", \"err\", err)\n\t}\n\n\ts, err := service.New(emptyInterface{}, &service.Config{\n\t\tName: string(action.ServiceName),\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating service: %w\", err)\n\t}\n\n\terr = s.Start()\n\tif err != nil && m.isUnixSystemV {\n\t\t_, initdErr := m.runInitdCommand(ctx, string(action.ServiceName), \"start\")\n\t\tif initdErr != nil {\n\t\t\treturn fmt.Errorf(\"%w (starting via init.d: %w)\", err, initdErr)\n\t\t}\n\t}\n\n\treturn err\n}\n\n// stop stops the service in the service manager.\nfunc (m *manager) stop(ctx context.Context, action *ActionStop) (err error) {\n\tm.logger.InfoContext(ctx, \"stopping service\", \"name\", action.ServiceName)\n\n\tconf := &service.Config{\n\t\tName: string(action.ServiceName),\n\t}\n\n\ts, err := service.New(emptyInterface{}, conf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating service: %w\", err)\n\t}\n\n\terr = s.Stop()\n\tif err != nil && m.isUnixSystemV {\n\t\t_, initdErr := m.runInitdCommand(ctx, string(action.ServiceName), \"stop\")\n\t\tif initdErr != nil {\n\t\t\treturn fmt.Errorf(\"%w (stopping via init.d: %w)\", err, initdErr)\n\t\t}\n\t}\n\n\treturn err\n}\n\n// uninstall uninstalls the service from the service manager.\nfunc (m *manager) uninstall(ctx context.Context, action *ActionUninstall) (err error) {\n\tm.logger.InfoContext(ctx, \"uninstalling service\", \"name\", action.ServiceName)\n\n\tif m.isOpenWrt {\n\t\t// On OpenWrt it is important to run disable command first as it will\n\t\t// remove the symlink.\n\t\t_, err = m.runInitdCommand(ctx, string(action.ServiceName), \"disable\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"disabling service on openwrt: %w\", err)\n\t\t}\n\t}\n\n\ts, err := service.New(emptyInterface{}, &service.Config{\n\t\tName: string(action.ServiceName),\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating service: %w\", err)\n\t}\n\n\terr = s.Stop()\n\tif err != nil {\n\t\tm.logger.DebugContext(ctx, \"stopping service\", \"err\", err)\n\t}\n\n\terr = s.Uninstall()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"uninstalling service: %w\", err)\n\t}\n\n\tif runtime.GOOS == \"darwin\" {\n\t\tremoveLaunchdStdLogs(ctx, m.logger)\n\t}\n\n\treturn nil\n}\n\n// Paths to stdout and stderr logs for Darwin service manager.\n//\n// TODO(e.burkov):  Move to config_darwin.go.\nconst (\n\tlaunchdStdoutPath = \"/var/log/AdGuardHome.stdout.log\"\n\tlaunchdStderrPath = \"/var/log/AdGuardHome.stderr.log\"\n)\n\n// removeLaunchdStdLogs removes launchd stdout and stderr log files, if needed,\n// and logs errors at warning level.\n//\n// TODO(e.burkov):  Move to manager_darwin.go.\nfunc removeLaunchdStdLogs(ctx context.Context, logger *slog.Logger) {\n\tif runtime.GOOS != \"darwin\" {\n\t\treturn\n\t}\n\n\terr := os.Remove(launchdStdoutPath)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tlogger.WarnContext(ctx, \"removing stdout file\", \"err\", err)\n\t}\n\n\terr = os.Remove(launchdStderrPath)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tlogger.WarnContext(ctx, \"removing stderr file\", \"err\", err)\n\t}\n}\n\n// runInitdCommand runs init.d service command.  It returns command code or\n// error if any.\n//\n// TODO(e.burkov):  Move to manager_linux.go.\nfunc (m *manager) runInitdCommand(\n\tctx context.Context,\n\tserviceName string,\n\taction string,\n) (code int, err error) {\n\tconfPath := filepath.Join(\"/etc\", \"init.d\", serviceName)\n\t// Pass the script and action as a single string argument.\n\t//\n\t// TODO(e.burkov):  Use CommandConstructor.\n\tcode, _, err = aghos.RunCommand(ctx, m.cmdCons, \"sh\", \"-c\", confPath, action)\n\n\treturn code, err\n}\n\n// emptyInterface is an empty implementation of the [service.Interface], as the\n// actual implementation is only needed for the [service.Service.Run] method.\ntype emptyInterface struct{}\n\n// type check\nvar _ service.Interface = emptyInterface{}\n\n// Start implements the [service.Interface] interface for emptyInterface.\nfunc (emptyInterface) Start(_ service.Service) (err error) { return nil }\n\n// Stop implements the [service.Interface] interface for emptyInterface.\nfunc (emptyInterface) Stop(_ service.Service) (err error) { return nil }\n"
  },
  {
    "path": "internal/ossvc/manager.go",
    "content": "package ossvc\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\n// Manager is the interface for communication with the OS service manager.\n//\n// TODO(e.burkov):  Move to golibs.\n//\n// TODO(e.burkov):  Use.\ntype Manager interface {\n\t// Perform performs the specified action.\n\tPerform(ctx context.Context, action Action) (err error)\n\n\t// Status returns the status of the service with the given name.\n\tStatus(ctx context.Context, name ServiceName) (status Status, err error)\n}\n\n// ManagerConfig contains the configuration for [Manager].\ntype ManagerConfig struct {\n\t// Logger is the logger to use.\n\tLogger *slog.Logger\n\n\t// CommandConstructor is the constructor to use for creating commands.\n\tCommandConstructor executil.CommandConstructor\n}\n\n// NewManager returns a new properly initialized [Manager], appropriate for the\n// current platform.\nfunc NewManager(ctx context.Context, conf *ManagerConfig) (mgr Manager, err error) {\n\treturn newManager(ctx, conf), nil\n}\n\n// EmptyManager is an empty implementation of [Manager] that does nothing.\ntype EmptyManager struct{}\n\n// type check\nvar _ Manager = EmptyManager{}\n\n// Perform implements the [Manager] interface for EmptyManager.\nfunc (EmptyManager) Perform(_ context.Context, _ Action) (err error) {\n\treturn nil\n}\n\n// Status implements the [Manager] interface for EmptyManager.\nfunc (EmptyManager) Status(_ context.Context, _ ServiceName) (status Status, err error) {\n\treturn StatusNotInstalled, nil\n}\n"
  },
  {
    "path": "internal/ossvc/manager_unix.go",
    "content": "//go:build unix\n\npackage ossvc\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"syscall\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// reload is a UNIX platform implementation for the Reload method of\n// [ReloadManager] interface for *manager.\nfunc (m *manager) reload(ctx context.Context, name ServiceName) (err error) {\n\tnameStr := string(name)\n\n\tvar pid int\n\tpidFile := filepath.Join(\"/var\", \"run\", nameStr+\".pid\")\n\t// #nosec CWE-22 -- The name of the variable is always predictable, it is a\n\t// constant.\n\tdata, err := os.ReadFile(pidFile)\n\tif err != nil {\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"reading service pid file: %w\", err)\n\t\t}\n\n\t\tpid, err = aghos.PIDByCommand(ctx, m.logger, nameStr, os.Getpid())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"finding process: %w\", err)\n\t\t}\n\t} else {\n\t\tparts := bytes.SplitN(data, []byte(\"\\n\"), 2)\n\t\tif len(parts) == 0 {\n\t\t\treturn fmt.Errorf(\"parsing %q: %w\", pidFile, errors.ErrEmptyValue)\n\t\t}\n\n\t\tpidStr := string(bytes.TrimSpace(parts[0]))\n\t\tpid, err = strconv.Atoi(pidStr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"parsing pid from %q: %w\", pidFile, err)\n\t\t}\n\t}\n\n\tproc, err := os.FindProcess(pid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"finding process with pid %d: %w\", pid, err)\n\t}\n\n\terr = proc.Signal(syscall.SIGHUP)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sending sighup to process with pid %d: %w\", pid, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/ossvc/manager_windows.go",
    "content": "//go:build windows\n\npackage ossvc\n\nimport (\n\t\"context\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n)\n\n// reload is a Windows platform implementation of the Reload method of the\n// [ReloadManager] interface for *manager.\nfunc (*manager) reload(context.Context, ServiceName) error {\n\treturn errors.ErrUnsupported\n}\n"
  },
  {
    "path": "internal/ossvc/ossvc.go",
    "content": "// Package ossvc contains abstractions and utilities for platform-independent\n// service management.\n//\n// TODO(e.burkov):  Add tests.\npackage ossvc\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/kardianos/service\"\n)\n\n// ServiceName is the name of a service.\n//\n// TODO(e.burkov):  Validate for each platform.\ntype ServiceName string\n\n// Status represents the status of a service.\ntype Status string\n\nconst (\n\t// StatusNotInstalled means that the service is not installed.\n\tStatusNotInstalled Status = \"not installed\"\n\n\t// StatusStopped means that the service is stopped.\n\tStatusStopped Status = \"stopped\"\n\n\t// StatusRunning means that the service is running.\n\tStatusRunning Status = \"running\"\n\n\t// StatusRestartOnFail means that the service is restarting after failed\n\t// start.\n\tStatusRestartOnFail Status = \"restart on fail\"\n)\n\n// statusToInternal converts a service.Status to a Status.\n//\n// TODO(e.burkov):  Get rid of [service] package dependency and remove this\n// function.\nfunc statusToInternal(status service.Status) (s Status, err error) {\n\tswitch status {\n\tcase service.StatusRunning:\n\t\treturn StatusRunning, nil\n\tcase service.StatusStopped:\n\t\treturn StatusStopped, nil\n\tcase statusRestartOnFail:\n\t\treturn StatusRestartOnFail, nil\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"service status: %w: %v\", errors.ErrBadEnumValue, status)\n\t}\n}\n"
  },
  {
    "path": "internal/ossvc/reloadmanager.go",
    "content": "package ossvc\n\nimport \"context\"\n\n// ReloadManager is the extension interface for [Manager] that provides an\n// ability to reload a service.\ntype ReloadManager interface {\n\tManager\n\n\t// Reload reloads the service with the given name.  As opposed to\n\t// [ActionRestart], this method does not stop the service.\n\tReload(ctx context.Context, name ServiceName) (err error)\n}\n"
  },
  {
    "path": "internal/ossvc/service_linux.go",
    "content": "//go:build linux\n\npackage ossvc\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/kardianos/service\"\n)\n\n// chooseSystem checks the current system detected and substitutes it with local\n// implementation if needed.\nfunc chooseSystem() {\n\tsys := service.ChosenSystem()\n\tswitch sys.String() {\n\tcase \"unix-systemv\":\n\t\t// By default, package service uses the SysV system if it cannot detect\n\t\t// anything other, but the update-rc.d fix should not be applied on\n\t\t// OpenWrt, so exclude it explicitly.\n\t\t//\n\t\t// See https://github.com/AdguardTeam/AdGuardHome/issues/4480 and\n\t\t// https://github.com/AdguardTeam/AdGuardHome/issues/4677.\n\t\tif !aghos.IsOpenWrt() {\n\t\t\tservice.ChooseSystem(&sysvSystem{System: sys})\n\t\t}\n\tcase \"linux-systemd\":\n\t\tservice.ChooseSystem(&systemdSystem{System: sys})\n\tdefault:\n\t\t// Do nothing.\n\t}\n}\n\n// sysvSystem is a wrapper for a [service.System] that returns the custom\n// implementation of the [service.Service] interface.\n//\n// TODO(e.burkov):  File a PR to github.com/kardianos/service.\ntype sysvSystem struct {\n\t// System must have an unexported type *service.linuxSystemService.\n\tservice.System\n}\n\n// type check\nvar _ service.System = (*sysvSystem)(nil)\n\n// New implements the [service.System] interface for *sysvSystem.  i and c must\n// not be nil.\nfunc (sys *sysvSystem) New(i service.Interface, c *service.Config) (s service.Service, err error) {\n\ts, err = sys.System.New(i, c)\n\tif err != nil {\n\t\t// Don't wrap the error to keep it as close to the original one as\n\t\t// possible.\n\t\treturn s, err\n\t}\n\n\treturn &sysvService{\n\t\tcmdCons: executil.SystemCommandConstructor{},\n\t\tService: s,\n\t\tname:    c.Name,\n\t}, nil\n}\n\n// sysvService is a wrapper for a SysV [service.Service] that supplements the\n// installation and uninstallation.\ntype sysvService struct {\n\t// cmdCons is used to run external commands.  It must not be nil.\n\tcmdCons executil.CommandConstructor\n\n\t// Service must have an unexported type *service.sysv.\n\tservice.Service\n\n\t// name stores the name of the service to call updating script with it.\n\tname string\n}\n\n// type check\nvar _ service.Service = (*sysvService)(nil)\n\n// Install implements the [service.Service] interface for *sysvService.\nfunc (svc *sysvService) Install() (err error) {\n\terr = svc.Service.Install()\n\tif err != nil {\n\t\t// Don't wrap the error to keep it as close to the original one as\n\t\t// possible.\n\t\treturn err\n\t}\n\n\t// TODO(s.chzhen):  Pass context.\n\t_, _, err = aghos.RunCommand(context.TODO(), svc.cmdCons, \"update-rc.d\", svc.name, \"defaults\")\n\n\t// Don't wrap an error since it's informative enough as is.\n\treturn err\n}\n\n// Uninstall implements the [service.Service] interface for *sysvService.\nfunc (svc *sysvService) Uninstall() (err error) {\n\terr = svc.Service.Uninstall()\n\tif err != nil {\n\t\t// Don't wrap the error to keep it as close to the original one as\n\t\t// possible.\n\t\treturn err\n\t}\n\n\t// TODO(s.chzhen):  Pass context.\n\t_, _, err = aghos.RunCommand(context.TODO(), svc.cmdCons, \"update-rc.d\", svc.name, \"remove\")\n\n\t// Don't wrap an error since it's informative enough as is.\n\treturn err\n}\n\n// systemdSystem is a wrapper for a [service.System] that returns the custom\n// implementation of the [service.Service] interface.\ntype systemdSystem struct {\n\t// System must have an unexported type *service.linuxSystemService.\n\tservice.System\n}\n\n// type check\nvar _ service.System = (*systemdSystem)(nil)\n\n// New implements the [service.System] interface for *systemdSystem.  i and c\n// must not be nil.\nfunc (sys *systemdSystem) New(i service.Interface, c *service.Config) (s service.Service, err error) {\n\ts, err = sys.System.New(i, c)\n\tif err != nil {\n\t\t// Don't wrap the error to keep it as close to the original one as\n\t\t// possible.\n\t\treturn s, err\n\t}\n\n\treturn &systemdService{\n\t\tcmdCons:  executil.SystemCommandConstructor{},\n\t\tService:  s,\n\t\tunitName: fmt.Sprintf(\"%s.service\", c.Name),\n\t}, nil\n}\n\n// type check\nvar _ service.Service = (*systemdService)(nil)\n\n// systemdService is a wrapper for a systemd [service.Service] that enriches the\n// service status information.\ntype systemdService struct {\n\t// cmdCons is used to run external commands.  It must not be nil.\n\tcmdCons executil.CommandConstructor\n\n\t// Service is expected to have an unexported type *service.systemd.\n\tservice.Service\n\n\t// unitName stores the name of the systemd daemon.\n\tunitName string\n}\n\n// type check\nvar _ service.Service = (*systemdService)(nil)\n\n// Status implements the [service.Service] interface for *systemdService.\nfunc (s *systemdService) Status() (status service.Status, err error) {\n\tconst systemctlCmd = \"systemctl\"\n\n\tvar (\n\t\tsystemctlArgs   = []string{\"show\", s.unitName}\n\t\tsystemctlStdout bytes.Buffer\n\t)\n\n\t// TODO(s.chzhen):  Consider streaming the output if needed.  Using\n\t// [io.Pipe] here is unnecessary; it complicates lifecycle management\n\t// because the output must be read concurrently, and the PipeWriter must be\n\t// explicitly closed to signal EOF.  Since this command's output is small, a\n\t// bytes.Buffer via executil.Run is sufficient.\n\terr = executil.Run(\n\t\t// TODO(s.chzhen):  Pass context.\n\t\tcontext.TODO(),\n\t\ts.cmdCons,\n\t\t&executil.CommandConfig{\n\t\t\tPath:   systemctlCmd,\n\t\t\tArgs:   systemctlArgs,\n\t\t\tStdout: &systemctlStdout,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn service.StatusUnknown, fmt.Errorf(\"executing command: %w\", err)\n\t}\n\n\tstatus, err = parseSystemctlShow(&systemctlStdout)\n\tif err != nil {\n\t\treturn service.StatusUnknown, fmt.Errorf(\"parsing command output: %w\", err)\n\t}\n\n\treturn status, nil\n}\n\n// Searched property names.  See man systemctl(1).\nconst (\n\tpropNameLoadState   = \"LoadState\"\n\tpropNameActiveState = \"ActiveState\"\n\tpropNameSubState    = \"SubState\"\n)\n\n// parseSystemctlShow parses the output of the systemctl show command.  It\n// expects the key=value pairs separated by newlines.\nfunc parseSystemctlShow(output io.Reader) (status service.Status, err error) {\n\tvar loadState, activeState, subState string\n\n\tscanner := bufio.NewScanner(output)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tpropName, propValue, ok := strings.Cut(line, \"=\")\n\t\tif !ok {\n\t\t\treturn service.StatusUnknown, fmt.Errorf(\"unexpected line format: %q\", line)\n\t\t}\n\n\t\tswitch propName {\n\t\tcase propNameLoadState:\n\t\t\tloadState = propValue\n\t\tcase propNameActiveState:\n\t\t\tactiveState = propValue\n\t\tcase propNameSubState:\n\t\t\tsubState = propValue\n\t\tdefault:\n\t\t\t// Go on.\n\t\t}\n\t}\n\tif err = scanner.Err(); err != nil {\n\t\treturn service.StatusUnknown, err\n\t}\n\n\treturn statusFromState(loadState, activeState, subState)\n}\n\n// statusFromState returns the service status based on the systemctl state\n// property values.\nfunc statusFromState(loadState, activeState, subState string) (status service.Status, err error) {\n\t// Desired property values.  See man systemctl(1).\n\tconst (\n\t\tpropValueLoadStateNotFound   = \"not-found\"\n\t\tpropValueActiveStateActive   = \"active\"\n\t\tpropValueActiveStateInactive = \"inactive\"\n\t\tpropValueSubStateAutoRestart = \"auto-restart\"\n\t)\n\n\tswitch {\n\tcase loadState == propValueLoadStateNotFound:\n\t\treturn service.StatusUnknown, service.ErrNotInstalled\n\tcase activeState == propValueActiveStateActive:\n\t\treturn service.StatusRunning, nil\n\tcase activeState == propValueActiveStateInactive:\n\t\treturn service.StatusStopped, nil\n\tcase subState == propValueSubStateAutoRestart:\n\t\treturn statusRestartOnFail, nil\n\tdefault:\n\t\treturn service.StatusUnknown, fmt.Errorf(\n\t\t\t\"unexpected state: %s=%q, %s=%q, %s=%q\",\n\t\t\tpropNameLoadState, loadState,\n\t\t\tpropNameActiveState, activeState,\n\t\t\tpropNameSubState, subState,\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "internal/ossvc/service_openbsd.go",
    "content": "//go:build openbsd\n\npackage ossvc\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"text/template\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/log\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/kardianos/service\"\n)\n\n// OpenBSD Service Implementation\n//\n// The file contains OpenBSD implementations for service.System and\n// service.Service interfaces.  It uses the default approach for RunCom-based\n// services systems, e.g. rc.d script.  It's written as if it was in a separate\n// package and has only one internal dependency.\n//\n// TODO(e.burkov):  Perhaps, file a PR to github.com/kardianos/service.\n\n// sysVersion is the version of local service.System interface implementation.\nconst sysVersion = \"openbsd-runcom\"\n\n// chooseSystem checks the current system detected and substitutes it with local\n// implementation if needed.\nfunc chooseSystem() {\n\tservice.ChooseSystem(openbsdSystem{})\n}\n\n// openbsdSystem is the service.System to be used on the OpenBSD.\ntype openbsdSystem struct{}\n\n// String implements service.System interface for openbsdSystem.\nfunc (openbsdSystem) String() string {\n\treturn sysVersion\n}\n\n// Detect implements service.System interface for openbsdSystem.\nfunc (openbsdSystem) Detect() (ok bool) {\n\treturn true\n}\n\n// Interactive implements service.System interface for openbsdSystem.\nfunc (openbsdSystem) Interactive() (ok bool) {\n\treturn os.Getppid() != 1\n}\n\n// New implements service.System interface for openbsdSystem.\nfunc (openbsdSystem) New(i service.Interface, c *service.Config) (s service.Service, err error) {\n\treturn &openbsdRunComService{\n\t\tcmdCons: executil.SystemCommandConstructor{},\n\t\ti:       i,\n\t\tcfg:     c,\n\t}, nil\n}\n\n// openbsdRunComService is the RunCom-based service.Service to be used on the\n// OpenBSD.\ntype openbsdRunComService struct {\n\tcmdCons executil.CommandConstructor\n\ti       service.Interface\n\tcfg     *service.Config\n}\n\n// Platform implements service.Service interface for *openbsdRunComService.\nfunc (*openbsdRunComService) Platform() (p string) {\n\treturn \"openbsd\"\n}\n\n// String implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) String() string {\n\treturn cmp.Or(s.cfg.DisplayName, s.cfg.Name)\n}\n\n// getBool returns the value of the given name from kv, assuming the value is a\n// boolean.  If the value isn't found or is not of the type, the defaultValue is\n// returned.\nfunc getBool(kv service.KeyValue, name string, defaultValue bool) (val bool) {\n\tvar ok bool\n\tif val, ok = kv[name].(bool); ok {\n\t\treturn val\n\t}\n\n\treturn defaultValue\n}\n\n// getString returns the value of the given name from kv, assuming the value is\n// a string.  If the value isn't found or is not of the type, the defaultValue\n// is returned.\nfunc getString(kv service.KeyValue, name, defaultValue string) (val string) {\n\tvar ok bool\n\tif val, ok = kv[name].(string); ok {\n\t\treturn val\n\t}\n\n\treturn defaultValue\n}\n\n// getFuncNiladic returns the value of the given name from kv, assuming the\n// value is a func().  If the value isn't found or is not of the type, the\n// defaultValue is returned.\nfunc getFuncNiladic(kv service.KeyValue, name string, defaultValue func()) (val func()) {\n\tvar ok bool\n\tif val, ok = kv[name].(func()); ok {\n\t\treturn val\n\t}\n\n\treturn defaultValue\n}\n\nconst (\n\t// optionUserService is the UserService option name.\n\toptionUserService = \"UserService\"\n\n\t// optionUserServiceDefault is the UserService option default value.\n\toptionUserServiceDefault = false\n\n\t// errNoUserServiceRunCom is returned when the service uses some custom\n\t// path to script.\n\terrNoUserServiceRunCom errors.Error = \"user services are not supported on \" + sysVersion\n)\n\n// scriptPath returns the absolute path to the script.  It's commonly used to\n// send commands to the service.\nfunc (s *openbsdRunComService) scriptPath() (cp string, err error) {\n\tif getBool(s.cfg.Option, optionUserService, optionUserServiceDefault) {\n\t\treturn \"\", errNoUserServiceRunCom\n\t}\n\n\tconst scriptPathPref = \"/etc/rc.d\"\n\n\treturn filepath.Join(scriptPathPref, s.cfg.Name), nil\n}\n\nconst (\n\t// optionRunComScript is the RunCom script option name.\n\toptionRunComScript = \"RunComScript\"\n\n\t// runComScript is the default RunCom script.\n\trunComScript = `#!/bin/sh\n#\n# $OpenBSD: {{ .SvcInfo }}\n\ndaemon=\"{{.Path}}\"\ndaemon_flags={{ .Arguments | args }}\n\n. /etc/rc.d/rc.subr\n\nrc_bg=YES\n\nrc_cmd $1\n`\n)\n\n// template returns the script template to put into rc.d.\nfunc (s *openbsdRunComService) template() (t *template.Template) {\n\ttf := map[string]any{\n\t\t\"args\": func(sl []string) string {\n\t\t\treturn `\"` + strings.Join(sl, \" \") + `\"`\n\t\t},\n\t}\n\n\treturn template.Must(template.New(\"\").Funcs(tf).Parse(getString(\n\t\ts.cfg.Option,\n\t\toptionRunComScript,\n\t\trunComScript,\n\t)))\n}\n\n// execPath returns the absolute path to the executable to be run as a service.\nfunc (s *openbsdRunComService) execPath() (path string, err error) {\n\tif c := s.cfg; c != nil && len(c.Executable) != 0 {\n\t\treturn filepath.Abs(c.Executable)\n\t}\n\n\tif path, err = os.Executable(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filepath.Abs(path)\n}\n\n// annotate wraps errors.Annotate applying a common error format.\nfunc (s *openbsdRunComService) annotate(action string, err error) (annotated error) {\n\treturn errors.Annotate(err, \"%s %s %s service: %w\", action, sysVersion, s.cfg.Name)\n}\n\n// Install implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) Install() (err error) {\n\tdefer func() { err = s.annotate(\"installing\", err) }()\n\n\tif err = s.writeScript(); err != nil {\n\t\treturn err\n\t}\n\n\treturn s.configureSysStartup(true)\n}\n\n// configureSysStartup adds s into the group of packages started with system.\nfunc (s *openbsdRunComService) configureSysStartup(enable bool) (err error) {\n\tcmd := \"enable\"\n\tif !enable {\n\t\tcmd = \"disable\"\n\t}\n\n\t// TODO(s.chzhen):  Pass context.\n\tctx := context.TODO()\n\tvar code int\n\tcode, _, err = aghos.RunCommand(ctx, s.cmdCons, \"rcctl\", cmd, s.cfg.Name)\n\tif err != nil {\n\t\treturn err\n\t} else if code != 0 {\n\t\treturn fmt.Errorf(\"rcctl finished with code %d\", code)\n\t}\n\n\treturn nil\n}\n\n// writeScript tries to write the script for the service.\nfunc (s *openbsdRunComService) writeScript() (err error) {\n\tvar scriptPath string\n\tif scriptPath, err = s.scriptPath(); err != nil {\n\t\treturn err\n\t}\n\n\tif _, err = os.Stat(scriptPath); !errors.Is(err, os.ErrNotExist) {\n\t\treturn fmt.Errorf(\"script already exists at %s\", scriptPath)\n\t}\n\n\tvar execPath string\n\tif execPath, err = s.execPath(); err != nil {\n\t\treturn err\n\t}\n\n\tt := s.template()\n\tf, err := os.Create(scriptPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating rc.d script file: %w\", err)\n\t}\n\tdefer f.Close()\n\n\terr = t.Execute(f, &struct {\n\t\t*service.Config\n\t\tPath    string\n\t\tSvcInfo string\n\t}{\n\t\tConfig:  s.cfg,\n\t\tPath:    execPath,\n\t\tSvcInfo: getString(s.cfg.Option, \"SvcInfo\", s.String()),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn errors.Annotate(\n\t\tos.Chmod(scriptPath, 0o755),\n\t\t\"changing rc.d script file permissions: %w\",\n\t)\n}\n\n// Uninstall implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) Uninstall() (err error) {\n\tdefer func() { err = s.annotate(\"uninstalling\", err) }()\n\n\tif err = s.configureSysStartup(false); err != nil {\n\t\treturn err\n\t}\n\n\tvar scriptPath string\n\tif scriptPath, err = s.scriptPath(); err != nil {\n\t\treturn err\n\t}\n\n\tif err = os.Remove(scriptPath); errors.Is(err, os.ErrNotExist) {\n\t\treturn service.ErrNotInstalled\n\t}\n\n\treturn errors.Annotate(err, \"removing rc.d script: %w\")\n}\n\n// optionRunWait is the name of the option associated with function which waits\n// for the service to be stopped.\nconst optionRunWait = \"RunWait\"\n\n// runWait is the default function to wait for service to be stopped.\nfunc runWait() {\n\tsigChan := make(chan os.Signal, 3)\n\tsignal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)\n\t<-sigChan\n}\n\n// Run implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) Run() (err error) {\n\tif err = s.i.Start(s); err != nil {\n\t\treturn err\n\t}\n\n\tgetFuncNiladic(s.cfg.Option, optionRunWait, runWait)()\n\n\treturn s.i.Stop(s)\n}\n\n// runCom calls the script with the specified cmd.\nfunc (s *openbsdRunComService) runCom(cmd string) (out string, err error) {\n\tvar scriptPath string\n\tif scriptPath, err = s.scriptPath(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// TODO(s.chzhen):  Pass context.\n\tctx := context.TODO()\n\n\t// TODO(e.burkov):  It's possible that os.ErrNotExist is caused by\n\t// something different than the service script's non-existence.  Keep it\n\t// in mind, when replace the aghos.RunCommand.\n\tvar outData []byte\n\t_, outData, err = aghos.RunCommand(ctx, s.cmdCons, scriptPath, cmd)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn \"\", service.ErrNotInstalled\n\t}\n\n\treturn string(outData), err\n}\n\n// Status implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) Status() (status service.Status, err error) {\n\tdefer func() { err = s.annotate(\"getting status of\", err) }()\n\n\tvar out string\n\tif out, err = s.runCom(\"check\"); err != nil {\n\t\treturn service.StatusUnknown, err\n\t}\n\n\tname := s.cfg.Name\n\tswitch out {\n\tcase fmt.Sprintf(\"%s(ok)\\n\", name):\n\t\treturn service.StatusRunning, nil\n\tcase fmt.Sprintf(\"%s(failed)\\n\", name):\n\t\treturn service.StatusStopped, nil\n\tdefault:\n\t\treturn service.StatusUnknown, service.ErrNotInstalled\n\t}\n}\n\n// Start implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) Start() (err error) {\n\t_, err = s.runCom(\"start\")\n\n\treturn s.annotate(\"starting\", err)\n}\n\n// Stop implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) Stop() (err error) {\n\t_, err = s.runCom(\"stop\")\n\n\treturn s.annotate(\"stopping\", err)\n}\n\n// Restart implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) Restart() (err error) {\n\tif err = s.Stop(); err != nil {\n\t\treturn err\n\t}\n\n\treturn s.Start()\n}\n\n// Logger implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) Logger(errs chan<- error) (l service.Logger, err error) {\n\tif service.ChosenSystem().Interactive() {\n\t\treturn service.ConsoleLogger, nil\n\t}\n\n\treturn s.SystemLogger(errs)\n}\n\n// SystemLogger implements service.Service interface for *openbsdRunComService.\nfunc (s *openbsdRunComService) SystemLogger(errs chan<- error) (l service.Logger, err error) {\n\treturn newSysLogger(s.cfg.Name, errs)\n}\n\n// newSysLogger returns a stub service.Logger implementation.\nfunc newSysLogger(_ string, _ chan<- error) (service.Logger, error) {\n\treturn sysLogger{}, nil\n}\n\n// sysLogger wraps calls of the logging functions understandable for service\n// interfaces.\ntype sysLogger struct{}\n\n// Error implements service.Logger interface for sysLogger.\nfunc (sysLogger) Error(v ...any) error {\n\tlog.Error(\"%s\", fmt.Sprint(v...))\n\n\treturn nil\n}\n\n// Warning implements service.Logger interface for sysLogger.\nfunc (sysLogger) Warning(v ...any) error {\n\tlog.Info(\"warning: %s\", fmt.Sprint(v...))\n\n\treturn nil\n}\n\n// Info implements service.Logger interface for sysLogger.\nfunc (sysLogger) Info(v ...any) error {\n\tlog.Info(\"%s\", fmt.Sprint(v...))\n\n\treturn nil\n}\n\n// Errorf implements service.Logger interface for sysLogger.\nfunc (sysLogger) Errorf(format string, a ...any) error {\n\tlog.Error(format, a...)\n\n\treturn nil\n}\n\n// Warningf implements service.Logger interface for sysLogger.\nfunc (sysLogger) Warningf(format string, a ...any) error {\n\tlog.Info(\"warning: %s\", fmt.Sprintf(format, a...))\n\n\treturn nil\n}\n\n// Infof implements service.Logger interface for sysLogger.\nfunc (sysLogger) Infof(format string, a ...any) error {\n\tlog.Info(format, a...)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/ossvc/service_others.go",
    "content": "//go:build !openbsd && !linux\n\npackage ossvc\n\n// chooseSystem checks the current system detected and substitutes it with local\n// implementation if needed.\nfunc chooseSystem() {}\n"
  },
  {
    "path": "internal/permcheck/check_unix.go",
    "content": "//go:build unix\n\npackage permcheck\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n)\n\n// check is the Unix-specific implementation of [Check].\nfunc check(\n\tctx context.Context,\n\tl *slog.Logger,\n\tworkDir string,\n\tdataDir string,\n\tstatsDir string,\n\tquerylogDir string,\n\tconfFilePath string,\n) {\n\tdirLoggger, fileLogger := l.With(\"type\", typeDir), l.With(\"type\", typeFile)\n\n\tfor _, ent := range entities(workDir, dataDir, statsDir, querylogDir, confFilePath) {\n\t\tif ent.Value {\n\t\t\tcheckDir(ctx, dirLoggger, ent.Key)\n\t\t} else {\n\t\t\tcheckFile(ctx, fileLogger, ent.Key)\n\t\t}\n\t}\n}\n\n// checkDir checks the permissions of a single directory.  The results are\n// logged at the appropriate level.\nfunc checkDir(ctx context.Context, l *slog.Logger, dirPath string) {\n\tcheckPath(ctx, l, dirPath, aghos.DefaultPermDir)\n}\n\n// checkFile checks the permissions of a single file.  The results are logged at\n// the appropriate level.\nfunc checkFile(ctx context.Context, l *slog.Logger, filePath string) {\n\tcheckPath(ctx, l, filePath, aghos.DefaultPermFile)\n}\n"
  },
  {
    "path": "internal/permcheck/check_windows.go",
    "content": "//go:build windows\n\npackage permcheck\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// check is the Windows-specific implementation of [Check].\n//\n// Note, that it only checks the owner and the ACEs of the working directory.\n// This is due to the assumption that the working directory ACEs are inherited\n// by the underlying files and directories, since at least [migrate] sets this\n// inheritance mode.\nfunc check(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {\n\tl = l.With(\"type\", typeDir, \"path\", workDir)\n\n\tdacl, owner, err := getSecurityInfo(workDir)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"getting security info\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tif !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {\n\t\tl.WarnContext(ctx, \"owner is not in administrators group\")\n\t}\n\n\terr = rangeACEs(dacl, func(\n\t\thdr windows.ACE_HEADER,\n\t\tmask windows.ACCESS_MASK,\n\t\tsid *windows.SID,\n\t) (cont bool) {\n\t\tl.DebugContext(ctx, \"checking access control entry\", \"mask\", mask, \"sid\", sid)\n\n\t\twarn := false\n\t\tswitch {\n\t\tcase hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:\n\t\t\t// Skip non-allowed ACEs.\n\t\tcase !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):\n\t\t\t// Non-administrator ACEs should not have any access rights.\n\t\t\twarn = mask > 0\n\t\tdefault:\n\t\t\t// Administrators should full control access rights.\n\t\t\twarn = mask&fullControlMask != fullControlMask\n\t\t}\n\t\tif warn {\n\t\t\tl.WarnContext(ctx, \"unexpected access control entry\", \"mask\", mask, \"sid\", sid)\n\t\t}\n\n\t\treturn true\n\t})\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"checking access control entries\", slogutil.KeyError, err)\n\t}\n}\n"
  },
  {
    "path": "internal/permcheck/migrate_unix.go",
    "content": "//go:build unix\n\npackage permcheck\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"os\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// needsMigration is a Unix-specific implementation of [NeedsMigration].\n//\n// TODO(a.garipov):  Consider ways to detect this better.\nfunc needsMigration(ctx context.Context, l *slog.Logger, _, confFilePath string) (ok bool) {\n\ts, err := os.Stat(confFilePath)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t// Likely a first run.  Don't check.\n\t\t\treturn false\n\t\t}\n\n\t\tl.ErrorContext(ctx, \"checking a need for permission migration\", slogutil.KeyError, err)\n\n\t\t// Unexpected error.  Try to migrate just in case.\n\t\treturn true\n\t}\n\n\treturn s.Mode().Perm() != aghos.DefaultPermFile\n}\n\n// migrate is a Unix-specific implementation of [Migrate].\nfunc migrate(\n\tctx context.Context,\n\tl *slog.Logger,\n\tworkDir string,\n\tdataDir string,\n\tstatsDir string,\n\tquerylogDir string,\n\tconfFilePath string,\n) {\n\tdirLoggger, fileLogger := l.With(\"type\", typeDir), l.With(\"type\", typeFile)\n\n\tfor _, ent := range entities(workDir, dataDir, statsDir, querylogDir, confFilePath) {\n\t\tif ent.Value {\n\t\t\tchmodDir(ctx, dirLoggger, ent.Key)\n\t\t} else {\n\t\t\tchmodFile(ctx, fileLogger, ent.Key)\n\t\t}\n\t}\n}\n\n// chmodDir changes the permissions of a single directory.  The results are\n// logged at the appropriate level.\nfunc chmodDir(ctx context.Context, l *slog.Logger, dirPath string) {\n\tchmodPath(ctx, l, dirPath, aghos.DefaultPermDir)\n}\n\n// chmodFile changes the permissions of a single file.  The results are logged\n// at the appropriate level.\nfunc chmodFile(ctx context.Context, l *slog.Logger, filePath string) {\n\tchmodPath(ctx, l, filePath, aghos.DefaultPermFile)\n}\n"
  },
  {
    "path": "internal/permcheck/migrate_windows.go",
    "content": "//go:build windows\n\npackage permcheck\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// needsMigration is the Windows-specific implementation of [NeedsMigration].\nfunc needsMigration(ctx context.Context, l *slog.Logger, workDir, _ string) (ok bool) {\n\tl = l.With(\"type\", typeDir, \"path\", workDir)\n\n\tdacl, owner, err := getSecurityInfo(workDir)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"getting security info\", slogutil.KeyError, err)\n\n\t\treturn true\n\t}\n\n\tif !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {\n\t\treturn true\n\t}\n\n\terr = rangeACEs(dacl, func(\n\t\thdr windows.ACE_HEADER,\n\t\tmask windows.ACCESS_MASK,\n\t\tsid *windows.SID,\n\t) (cont bool) {\n\t\tswitch {\n\t\tcase hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:\n\t\t\t// Skip non-allowed access control entries.\n\t\t\tl.DebugContext(ctx, \"skipping deny access control entry\", \"sid\", sid)\n\t\tcase !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):\n\t\t\t// Non-administrator access control entries should not have any\n\t\t\t// access rights.\n\t\t\tok = mask > 0\n\t\tdefault:\n\t\t\t// Administrators should have full control.\n\t\t\tok = mask&fullControlMask != fullControlMask\n\t\t}\n\n\t\t// Stop ranging if the access control entry is unexpected.\n\t\treturn !ok\n\t})\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"checking access control entries\", slogutil.KeyError, err)\n\n\t\treturn true\n\t}\n\n\treturn ok\n}\n\n// migrate is the Windows-specific implementation of [Migrate].\n//\n// It sets the owner to administrators and adds a full control access control\n// entry for the account.  It also removes all non-administrator access control\n// entries, and keeps deny access control entries.  For any created or modified\n// entry it sets the propagation flags to be inherited by child objects.\nfunc migrate(ctx context.Context, logger *slog.Logger, workDir, _, _, _, _ string) {\n\tl := logger.With(\"type\", typeDir, \"path\", workDir)\n\n\tdacl, owner, err := getSecurityInfo(workDir)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"getting security info\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tadmins, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"creating administrators sid\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\t// TODO(e.burkov):  Check for duplicates?\n\tvar accessEntries []windows.EXPLICIT_ACCESS\n\tvar setACL bool\n\t// Iterate over the access control entries in DACL to determine if its\n\t// migration is needed.\n\terr = rangeACEs(dacl, func(\n\t\thdr windows.ACE_HEADER,\n\t\tmask windows.ACCESS_MASK,\n\t\tsid *windows.SID,\n\t) (cont bool) {\n\t\tswitch {\n\t\tcase hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:\n\t\t\t// Add non-allowed access control entries as is, since they specify\n\t\t\t// the access restrictions, which shouldn't be lost.\n\t\t\tl.InfoContext(ctx, \"migrating deny access control entry\", \"sid\", sid)\n\t\t\taccessEntries = append(accessEntries, newDenyExplicitAccess(sid, mask))\n\t\t\tsetACL = true\n\t\tcase !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):\n\t\t\t// Remove non-administrator ACEs, since such accounts should not\n\t\t\t// have any access rights.\n\t\t\tl.InfoContext(ctx, \"removing access control entry\", \"sid\", sid)\n\t\t\tsetACL = true\n\t\tdefault:\n\t\t\t// Administrators should have full control.  Don't add a new entry\n\t\t\t// here since it will be added later in case there are other\n\t\t\t// required entries.\n\t\t\tl.InfoContext(ctx, \"migrating access control entry\", \"sid\", sid, \"mask\", mask)\n\t\t\tsetACL = setACL || mask&fullControlMask != fullControlMask\n\t\t}\n\n\t\treturn true\n\t})\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"ranging through access control entries\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tif setACL {\n\t\taccessEntries = append(accessEntries, newFullExplicitAccess(admins))\n\t}\n\n\tif !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {\n\t\tl.InfoContext(ctx, \"migrating owner\", \"sid\", owner)\n\t\towner = admins\n\t} else {\n\t\tl.DebugContext(ctx, \"owner is already an administrator\")\n\t\towner = nil\n\t}\n\n\terr = setSecurityInfo(workDir, owner, accessEntries)\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"setting security info\", slogutil.KeyError, err)\n\t}\n}\n"
  },
  {
    "path": "internal/permcheck/permcheck.go",
    "content": "// Package permcheck contains code for simplifying permissions checks on files\n// and directories.\npackage permcheck\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n)\n\n// File type constants for logging.\nconst (\n\ttypeDir  = \"directory\"\n\ttypeFile = \"file\"\n)\n\n// Check checks the permissions on important files.  It logs the results at\n// appropriate levels.\nfunc Check(\n\tctx context.Context,\n\tl *slog.Logger,\n\tworkDir string,\n\tdataDir string,\n\tstatsDir string,\n\tquerylogDir string,\n\tconfFilePath string,\n) {\n\tcheck(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)\n}\n\n// NeedsMigration returns true if AdGuard Home files need permission migration.\nfunc NeedsMigration(ctx context.Context, l *slog.Logger, workDir, confFilePath string) (ok bool) {\n\treturn needsMigration(ctx, l, workDir, confFilePath)\n}\n\n// Migrate attempts to change the permissions of AdGuard Home's files.  It logs\n// the results at an appropriate level.\nfunc Migrate(\n\tctx context.Context,\n\tl *slog.Logger,\n\tworkDir string,\n\tdataDir string,\n\tstatsDir string,\n\tquerylogDir string,\n\tconfFilePath string,\n) {\n\tmigrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)\n}\n"
  },
  {
    "path": "internal/permcheck/security_unix.go",
    "content": "//go:build unix\n\npackage permcheck\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// entity is a filesystem entity with a path and a flag indicating whether it is\n// a directory.\ntype entity = container.KeyValue[string, bool]\n\n// entities returns a list of filesystem entities that need to be ranged over.\n//\n// TODO(a.garipov): Put all paths in one place and remove this duplication.\nfunc entities(workDir, dataDir, statsDir, querylogDir, confFilePath string) (ents []entity) {\n\tents = []entity{{\n\t\tKey:   workDir,\n\t\tValue: true,\n\t}, {\n\t\tKey:   confFilePath,\n\t\tValue: false,\n\t}, {\n\t\tKey:   dataDir,\n\t\tValue: true,\n\t}, {\n\t\tKey:   filepath.Join(dataDir, \"filters\"),\n\t\tValue: true,\n\t}, {\n\t\tKey:   filepath.Join(dataDir, \"sessions.db\"),\n\t\tValue: false,\n\t}, {\n\t\tKey:   filepath.Join(dataDir, \"leases.json\"),\n\t\tValue: false,\n\t}}\n\n\tif dataDir != querylogDir {\n\t\tents = append(ents, entity{\n\t\t\tKey:   querylogDir,\n\t\t\tValue: true,\n\t\t})\n\t}\n\tents = append(ents, entity{\n\t\tKey:   filepath.Join(querylogDir, \"querylog.json\"),\n\t\tValue: false,\n\t}, entity{\n\t\tKey:   filepath.Join(querylogDir, \"querylog.json.1\"),\n\t\tValue: false,\n\t})\n\n\tif dataDir != statsDir {\n\t\tents = append(ents, entity{\n\t\t\tKey:   statsDir,\n\t\t\tValue: true,\n\t\t})\n\t}\n\tents = append(ents, entity{\n\t\tKey: filepath.Join(statsDir, \"stats.db\"),\n\t})\n\n\treturn ents\n}\n\n// checkPath checks the permissions of a single filesystem entity.  The results\n// are logged at the appropriate level.\nfunc checkPath(ctx context.Context, l *slog.Logger, entPath string, want fs.FileMode) {\n\tl = l.With(\"path\", entPath)\n\n\ts, err := os.Stat(entPath)\n\tif err != nil {\n\t\tlvl := slog.LevelError\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tlvl = slog.LevelDebug\n\t\t}\n\n\t\tl.Log(ctx, lvl, \"checking permissions\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\t// TODO(a.garipov): Add a more fine-grained check and result reporting.\n\tperm := s.Mode().Perm()\n\tif perm == want {\n\t\treturn\n\t}\n\n\tpermOct, wantOct := fmt.Sprintf(\"%#o\", perm), fmt.Sprintf(\"%#o\", want)\n\tl.WarnContext(ctx, \"found unexpected permissions\", \"perm\", permOct, \"want\", wantOct)\n}\n\n// chmodPath changes the permissions of a single filesystem entity.  The results\n// are logged at the appropriate level.\nfunc chmodPath(ctx context.Context, l *slog.Logger, entPath string, fm fs.FileMode) {\n\tvar lvl slog.Level\n\tvar msg string\n\targs := []any{\"path\", entPath}\n\n\tswitch err := os.Chmod(entPath, fm); {\n\tcase err == nil:\n\t\tlvl = slog.LevelInfo\n\t\tmsg = \"changed permissions\"\n\tcase errors.Is(err, os.ErrNotExist):\n\t\tlvl = slog.LevelDebug\n\t\tmsg = \"checking permissions\"\n\t\targs = append(args, slogutil.KeyError, err)\n\tdefault:\n\t\tlvl = slog.LevelError\n\t\tmsg = \"cannot change permissions; this can leave your system vulnerable, see \" +\n\t\t\t\"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns\"\n\t\targs = append(args, \"target_perm\", fmt.Sprintf(\"%#o\", fm), slogutil.KeyError, err)\n\t}\n\n\tl.Log(ctx, lvl, msg, args...)\n}\n"
  },
  {
    "path": "internal/permcheck/security_windows.go",
    "content": "//go:build windows\n\npackage permcheck\n\nimport (\n\t\"fmt\"\n\t\"unsafe\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// objectType is the type of the object for directories in context of security\n// API.\nconst objectType windows.SE_OBJECT_TYPE = windows.SE_FILE_OBJECT\n\n// fileDeleteChildRight is the mask bit for the right to delete a child object.\n// It seems to be missing from the [windows] package.\n//\n// See https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/access-mask.\nconst fileDeleteChildRight windows.ACCESS_MASK = 0b0100_0000\n\n// fullControlMask is the mask for full control access rights.\nconst fullControlMask windows.ACCESS_MASK = windows.FILE_LIST_DIRECTORY |\n\twindows.FILE_WRITE_DATA |\n\twindows.FILE_APPEND_DATA |\n\twindows.FILE_READ_EA |\n\twindows.FILE_WRITE_EA |\n\twindows.FILE_TRAVERSE |\n\tfileDeleteChildRight |\n\twindows.FILE_READ_ATTRIBUTES |\n\twindows.FILE_WRITE_ATTRIBUTES |\n\twindows.DELETE |\n\twindows.READ_CONTROL |\n\twindows.WRITE_DAC |\n\twindows.WRITE_OWNER |\n\twindows.SYNCHRONIZE\n\n// aceFunc is a function that handles access control entries in the\n// discretionary access control list.  It should return true to continue\n// iterating over the entries, or false to stop.\ntype aceFunc = func(\n\thdr windows.ACE_HEADER,\n\tmask windows.ACCESS_MASK,\n\tsid *windows.SID,\n) (cont bool)\n\n// rangeACEs ranges over the access control entries in the discretionary access\n// control list of the specified security descriptor and calls f for each one.\nfunc rangeACEs(dacl *windows.ACL, f aceFunc) (err error) {\n\tvar errs []error\n\tfor i := range uint32(dacl.AceCount) {\n\t\tvar ace *windows.ACCESS_ALLOWED_ACE\n\t\terr = windows.GetAce(dacl, i, &ace)\n\t\tif err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"getting entry at index %d: %w\", i, err))\n\n\t\t\tcontinue\n\t\t}\n\n\t\tsid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))\n\t\tif !f(ace.Header, ace.Mask, sid) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err = errors.Join(errs...); err != nil {\n\t\treturn fmt.Errorf(\"checking access control entries: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// setSecurityInfo sets the security information on the specified file, using\n// ents to create a discretionary access control list.  Either owner or ents can\n// be nil, in which case the corresponding information is not set, but at least\n// one of them should be specified.\nfunc setSecurityInfo(fname string, owner *windows.SID, ents []windows.EXPLICIT_ACCESS) (err error) {\n\tvar secInfo windows.SECURITY_INFORMATION\n\n\tvar acl *windows.ACL\n\tif len(ents) > 0 {\n\t\t// TODO(e.burkov):  Investigate if this whole set is necessary.\n\t\tsecInfo |= windows.DACL_SECURITY_INFORMATION |\n\t\t\twindows.PROTECTED_DACL_SECURITY_INFORMATION |\n\t\t\twindows.UNPROTECTED_DACL_SECURITY_INFORMATION\n\n\t\tacl, err = windows.ACLFromEntries(ents, nil)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"creating access control list: %w\", err)\n\t\t}\n\t}\n\n\tif owner != nil {\n\t\tsecInfo |= windows.OWNER_SECURITY_INFORMATION\n\t}\n\n\tif secInfo == 0 {\n\t\treturn errors.Error(\"no security information to set\")\n\t}\n\n\terr = windows.SetNamedSecurityInfo(fname, objectType, secInfo, owner, nil, acl, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"setting security info: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// getSecurityInfo retrieves the security information for the specified file.\nfunc getSecurityInfo(fname string) (dacl *windows.ACL, owner *windows.SID, err error) {\n\t// desiredSecInfo defines the parts of a security descriptor to retrieve.\n\tconst desiredSecInfo windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION |\n\t\twindows.DACL_SECURITY_INFORMATION |\n\t\twindows.PROTECTED_DACL_SECURITY_INFORMATION |\n\t\twindows.UNPROTECTED_DACL_SECURITY_INFORMATION\n\n\tsd, err := windows.GetNamedSecurityInfo(fname, objectType, desiredSecInfo)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"getting security descriptor: %w\", err)\n\t}\n\n\towner, _, err = sd.Owner()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"getting owner sid: %w\", err)\n\t}\n\n\tdacl, _, err = sd.DACL()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"getting discretionary access control list: %w\", err)\n\t}\n\n\treturn dacl, owner, nil\n}\n\n// newFullExplicitAccess creates a new explicit access entry with full control\n// permissions.\nfunc newFullExplicitAccess(sid *windows.SID) (accEnt windows.EXPLICIT_ACCESS) {\n\treturn windows.EXPLICIT_ACCESS{\n\t\tAccessPermissions: fullControlMask,\n\t\tAccessMode:        windows.GRANT_ACCESS,\n\t\tInheritance:       windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,\n\t\tTrustee: windows.TRUSTEE{\n\t\t\tTrusteeForm:  windows.TRUSTEE_IS_SID,\n\t\t\tTrusteeType:  windows.TRUSTEE_IS_UNKNOWN,\n\t\t\tTrusteeValue: windows.TrusteeValueFromSID(sid),\n\t\t},\n\t}\n}\n\n// newDenyExplicitAccess creates a new explicit access entry with specified deny\n// permissions.\nfunc newDenyExplicitAccess(\n\tsid *windows.SID,\n\tmask windows.ACCESS_MASK,\n) (accEnt windows.EXPLICIT_ACCESS) {\n\treturn windows.EXPLICIT_ACCESS{\n\t\tAccessPermissions: mask,\n\t\tAccessMode:        windows.DENY_ACCESS,\n\t\tInheritance:       windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,\n\t\tTrustee: windows.TRUSTEE{\n\t\t\tTrusteeForm:  windows.TRUSTEE_IS_SID,\n\t\t\tTrusteeType:  windows.TRUSTEE_IS_UNKNOWN,\n\t\t\tTrusteeValue: windows.TrusteeValueFromSID(sid),\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/querylog/client.go",
    "content": "package querylog\n\nimport \"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\n// Client is the information required by the query log to match against clients\n// during searches.\ntype Client struct {\n\tWHOIS          *whois.Info `json:\"whois,omitempty\"`\n\tName           string      `json:\"name\"`\n\tDisallowedRule string      `json:\"disallowed_rule\"`\n\tDisallowed     bool        `json:\"disallowed\"`\n\tIgnoreQueryLog bool        `json:\"-\"`\n}\n\n// clientCacheKey is the key by which a cached client information is found.\ntype clientCacheKey struct {\n\tclientID string\n\tip       string\n}\n\n// clientCache is the cache of client information found throughout a request to\n// the query log API.  It is used both to speed up the lookup, as well as to\n// make sure that changes in client data between two lookups don't create\n// discrepancies in our response.\ntype clientCache map[clientCacheKey]*Client\n"
  },
  {
    "path": "internal/querylog/decode.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n)\n\n// logEntryHandler represents a handler for decoding json token to the logEntry\n// struct.\ntype logEntryHandler func(t json.Token, ent *logEntry) error\n\n// logEntryHandlers is the map of log entry decode handlers for various keys.\nvar logEntryHandlers = map[string]logEntryHandler{\n\t\"CID\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.ClientID = v\n\n\t\treturn nil\n\t},\n\t\"IP\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tif ent.IP == nil {\n\t\t\tent.IP = net.ParseIP(v)\n\t\t}\n\n\t\treturn nil\n\t},\n\t\"T\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tvar err error\n\t\tent.Time, err = time.Parse(time.RFC3339, v)\n\n\t\treturn err\n\t},\n\t\"QH\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\tent.QHost = v\n\t\treturn nil\n\t},\n\t\"QT\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\tent.QType = v\n\t\treturn nil\n\t},\n\t\"QC\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.QClass = v\n\n\t\treturn nil\n\t},\n\t\"CP\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tvar err error\n\t\tent.ClientProto, err = NewClientProto(v)\n\n\t\treturn err\n\t},\n\t\"Answer\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tvar err error\n\t\tent.Answer, err = base64.StdEncoding.DecodeString(v)\n\n\t\treturn err\n\t},\n\t\"OrigAnswer\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tvar err error\n\t\tent.OrigAnswer, err = base64.StdEncoding.DecodeString(v)\n\n\t\treturn err\n\t},\n\t\"ECS\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.ReqECS = v\n\n\t\treturn nil\n\t},\n\t\"Cached\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(bool)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.Cached = v\n\n\t\treturn nil\n\t},\n\t\"AD\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(bool)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.AuthenticatedData = v\n\n\t\treturn nil\n\t},\n\t\"Upstream\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.Upstream = v\n\n\t\treturn nil\n\t},\n\t\"Elapsed\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(json.Number)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\ti, err := v.Int64()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tent.Elapsed = time.Duration(i)\n\n\t\treturn nil\n\t},\n}\n\n// decodeResultRuleKey decodes the token of \"Rules\" type to logEntry struct.\nfunc (l *queryLog) decodeResultRuleKey(\n\tctx context.Context,\n\tkey string,\n\ti int,\n\tdec *json.Decoder,\n\tent *logEntry,\n) {\n\tvar vToken json.Token\n\tswitch key {\n\tcase \"FilterListID\":\n\t\tent.Result.Rules, vToken = l.decodeVTokenAndAddRule(ctx, key, i, dec, ent.Result.Rules)\n\t\tif n, ok := vToken.(json.Number); ok {\n\t\t\tid, _ := n.Int64()\n\t\t\tent.Result.Rules[i].FilterListID = rulelist.APIID(id)\n\t\t}\n\tcase \"IP\":\n\t\tent.Result.Rules, vToken = l.decodeVTokenAndAddRule(ctx, key, i, dec, ent.Result.Rules)\n\t\tif ipStr, ok := vToken.(string); ok {\n\t\t\tif ip, err := netip.ParseAddr(ipStr); err == nil {\n\t\t\t\tent.Result.Rules[i].IP = ip\n\t\t\t} else {\n\t\t\t\tl.logger.DebugContext(ctx, \"decoding ip\", \"value\", ipStr, slogutil.KeyError, err)\n\t\t\t}\n\t\t}\n\tcase \"Text\":\n\t\tent.Result.Rules, vToken = l.decodeVTokenAndAddRule(ctx, key, i, dec, ent.Result.Rules)\n\t\tif s, ok := vToken.(string); ok {\n\t\t\tent.Result.Rules[i].Text = s\n\t\t}\n\tdefault:\n\t\t// Go on.\n\t}\n}\n\n// decodeVTokenAndAddRule decodes the \"Rules\" toke as [filtering.ResultRule]\n// and then adds the decoded object to the slice of result rules.\nfunc (l *queryLog) decodeVTokenAndAddRule(\n\tctx context.Context,\n\tkey string,\n\ti int,\n\tdec *json.Decoder,\n\trules []*filtering.ResultRule,\n) (newRules []*filtering.ResultRule, vToken json.Token) {\n\tnewRules = rules\n\n\tvToken, err := dec.Token()\n\tif err != nil {\n\t\tif err != io.EOF {\n\t\t\tl.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"decoding result rule key\",\n\t\t\t\t\"key\", key,\n\t\t\t\tslogutil.KeyError, err,\n\t\t\t)\n\t\t}\n\n\t\treturn newRules, nil\n\t}\n\n\tif len(rules) < i+1 {\n\t\tnewRules = append(newRules, &filtering.ResultRule{})\n\t}\n\n\treturn newRules, vToken\n}\n\n// decodeResultRules parses the dec's tokens into logEntry ent interpreting it\n// as a slice of the result rules.\nfunc (l *queryLog) decodeResultRules(ctx context.Context, dec *json.Decoder, ent *logEntry) {\n\tconst msgPrefix = \"decoding result rules\"\n\n\tfor {\n\t\tdelimToken, err := dec.Token()\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; token\", slogutil.KeyError, err)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif d, ok := delimToken.(json.Delim); !ok {\n\t\t\treturn\n\t\t} else if d != '[' {\n\t\t\tl.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\tmsgPrefix,\n\t\t\t\tslogutil.KeyError, newUnexpectedDelimiterError(d),\n\t\t\t)\n\t\t}\n\n\t\terr = l.decodeResultRuleToken(ctx, dec, ent)\n\t\tif err != nil {\n\t\t\tif err != io.EOF && !errors.Is(err, ErrEndOfToken) {\n\t\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; rule token\", slogutil.KeyError, err)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// decodeResultRuleToken decodes the tokens of \"Rules\" type to the logEntry ent.\nfunc (l *queryLog) decodeResultRuleToken(\n\tctx context.Context,\n\tdec *json.Decoder,\n\tent *logEntry,\n) (err error) {\n\ti := 0\n\tfor {\n\t\tvar keyToken json.Token\n\t\tkeyToken, err = dec.Token()\n\t\tif err != nil {\n\t\t\t// Don't wrap the error, because it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\n\t\tif d, ok := keyToken.(json.Delim); ok {\n\t\t\tswitch d {\n\t\t\tcase '}':\n\t\t\t\ti++\n\t\t\tcase ']':\n\t\t\t\treturn ErrEndOfToken\n\t\t\tdefault:\n\t\t\t\t// Go on.\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tkey, ok := keyToken.(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"keyToken is %T (%[1]v) and not string\", keyToken)\n\t\t}\n\n\t\tl.decodeResultRuleKey(ctx, key, i, dec, ent)\n\t}\n}\n\n// decodeResultReverseHosts parses the dec's tokens into ent interpreting it as\n// the result of hosts container's $dnsrewrite rule.  It assumes there are no\n// other occurrences of DNSRewriteResult in the entry since hosts container's\n// rewrites currently has the highest priority along the entire filtering\n// pipeline.\nfunc (l *queryLog) decodeResultReverseHosts(ctx context.Context, dec *json.Decoder, ent *logEntry) {\n\tconst msgPrefix = \"decoding result reverse hosts\"\n\n\tfor {\n\t\titemToken, err := dec.Token()\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; token\", slogutil.KeyError, err)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tswitch v := itemToken.(type) {\n\t\tcase json.Delim:\n\t\t\tif v == '[' {\n\t\t\t\tcontinue\n\t\t\t} else if v == ']' {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tl.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\tmsgPrefix,\n\t\t\t\tslogutil.KeyError, newUnexpectedDelimiterError(v),\n\t\t\t)\n\n\t\t\treturn\n\t\tcase string:\n\t\t\tv = dns.Fqdn(v)\n\t\t\tif res := &ent.Result; res.DNSRewriteResult == nil {\n\t\t\t\tres.DNSRewriteResult = &filtering.DNSRewriteResult{\n\t\t\t\t\tRCode: dns.RcodeSuccess,\n\t\t\t\t\tResponse: filtering.DNSRewriteResultResponse{\n\t\t\t\t\t\tdns.TypePTR: []rules.RRValue{v},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\tres.DNSRewriteResult.RCode = dns.RcodeSuccess\n\t\t\t}\n\n\t\t\tif rres := ent.Result.DNSRewriteResult; rres.Response == nil {\n\t\t\t\trres.Response = filtering.DNSRewriteResultResponse{dns.TypePTR: []rules.RRValue{v}}\n\t\t\t} else {\n\t\t\t\trres.Response[dns.TypePTR] = append(rres.Response[dns.TypePTR], v)\n\t\t\t}\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\n// decodeResultIPList parses the dec's tokens into logEntry ent interpreting it\n// as the result IP addresses list.\nfunc (l *queryLog) decodeResultIPList(ctx context.Context, dec *json.Decoder, ent *logEntry) {\n\tconst msgPrefix = \"decoding result ip list\"\n\n\tfor {\n\t\titemToken, err := dec.Token()\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; token\", slogutil.KeyError, err)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tswitch v := itemToken.(type) {\n\t\tcase json.Delim:\n\t\t\tif v == '[' {\n\t\t\t\tcontinue\n\t\t\t} else if v == ']' {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tl.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\tmsgPrefix,\n\t\t\t\tslogutil.KeyError, newUnexpectedDelimiterError(v),\n\t\t\t)\n\n\t\t\treturn\n\t\tcase string:\n\t\t\tvar ip netip.Addr\n\t\t\tip, err = netip.ParseAddr(v)\n\t\t\tif err == nil {\n\t\t\t\tent.Result.IPList = append(ent.Result.IPList, ip)\n\t\t\t}\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\n// decodeResultDNSRewriteResultKey decodes the token of \"DNSRewriteResult\" type\n// to the logEntry struct.\nfunc (l *queryLog) decodeResultDNSRewriteResultKey(\n\tctx context.Context,\n\tkey string,\n\tdec *json.Decoder,\n\tent *logEntry,\n) {\n\tconst msgPrefix = \"decoding result dns rewrite result key\"\n\n\tvar err error\n\n\tswitch key {\n\tcase \"RCode\":\n\t\tvar vToken json.Token\n\t\tvToken, err = dec.Token()\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; token\", slogutil.KeyError, err)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif ent.Result.DNSRewriteResult == nil {\n\t\t\tent.Result.DNSRewriteResult = &filtering.DNSRewriteResult{}\n\t\t}\n\n\t\tif n, ok := vToken.(json.Number); ok {\n\t\t\trcode64, _ := n.Int64()\n\t\t\tent.Result.DNSRewriteResult.RCode = rules.RCode(rcode64)\n\t\t}\n\tcase \"Response\":\n\t\tif ent.Result.DNSRewriteResult == nil {\n\t\t\tent.Result.DNSRewriteResult = &filtering.DNSRewriteResult{}\n\t\t}\n\n\t\tif ent.Result.DNSRewriteResult.Response == nil {\n\t\t\tent.Result.DNSRewriteResult.Response = filtering.DNSRewriteResultResponse{}\n\t\t}\n\n\t\t// TODO(a.garipov): I give up.  This whole file is a mess.  Luckily, we\n\t\t// can assume that this field is relatively rare and just use the normal\n\t\t// decoding and correct the values.\n\t\terr = dec.Decode(&ent.Result.DNSRewriteResult.Response)\n\t\tif err != nil {\n\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; response\", slogutil.KeyError, err)\n\t\t}\n\n\t\tent.parseDNSRewriteResultIPs()\n\tdefault:\n\t\t// Go on.\n\t}\n}\n\n// decodeResultDNSRewriteResult parses the dec's tokens into logEntry ent\n// interpreting it as the result DNSRewriteResult.\nfunc (l *queryLog) decodeResultDNSRewriteResult(\n\tctx context.Context,\n\tdec *json.Decoder,\n\tent *logEntry,\n) {\n\tconst msgPrefix = \"decoding result dns rewrite result\"\n\n\tfor {\n\t\tkey, err := parseKeyToken(dec)\n\t\tif err != nil {\n\t\t\tif err != io.EOF && !errors.Is(err, ErrEndOfToken) {\n\t\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; token\", slogutil.KeyError, err)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif key == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tl.decodeResultDNSRewriteResultKey(ctx, key, dec, ent)\n\t}\n}\n\n// translateResult converts some fields of the ent.Result to the format\n// consistent with current implementation.\nfunc translateResult(ent *logEntry) {\n\tres := &ent.Result\n\tif res.Reason != filtering.RewrittenAutoHosts || len(res.IPList) == 0 {\n\t\treturn\n\t}\n\n\tif res.DNSRewriteResult == nil {\n\t\tres.DNSRewriteResult = &filtering.DNSRewriteResult{\n\t\t\tRCode: dns.RcodeSuccess,\n\t\t}\n\t}\n\n\tif res.DNSRewriteResult.Response == nil {\n\t\tres.DNSRewriteResult.Response = filtering.DNSRewriteResultResponse{}\n\t}\n\n\tresp := res.DNSRewriteResult.Response\n\tfor _, ip := range res.IPList {\n\t\tqType := dns.TypeAAAA\n\t\tif ip.Is4() {\n\t\t\tqType = dns.TypeA\n\t\t}\n\n\t\tresp[qType] = append(resp[qType], ip)\n\t}\n\n\tres.IPList = nil\n}\n\n// ErrEndOfToken is an error returned by parse key token when the closing\n// bracket is found.\nconst ErrEndOfToken errors.Error = \"end of token\"\n\n// parseKeyToken parses the dec's token key.\nfunc parseKeyToken(dec *json.Decoder) (key string, err error) {\n\tkeyToken, err := dec.Token()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif d, ok := keyToken.(json.Delim); ok {\n\t\tif d == '}' {\n\t\t\treturn \"\", ErrEndOfToken\n\t\t}\n\n\t\treturn \"\", nil\n\t}\n\n\tkey, ok := keyToken.(string)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"keyToken is %T (%[1]v) and not string\", keyToken)\n\t}\n\n\treturn key, nil\n}\n\n// decodeResult decodes a token of \"Result\" type to logEntry struct.\nfunc (l *queryLog) decodeResult(ctx context.Context, dec *json.Decoder, ent *logEntry) {\n\tconst msgPrefix = \"decoding result\"\n\n\tdefer translateResult(ent)\n\n\tfor {\n\t\tkey, err := parseKeyToken(dec)\n\t\tif err != nil {\n\t\t\tif err != io.EOF && !errors.Is(err, ErrEndOfToken) {\n\t\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; token\", slogutil.KeyError, err)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif key == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tok := l.resultDecHandler(ctx, key, dec, ent)\n\t\tif ok {\n\t\t\tcontinue\n\t\t}\n\n\t\thandler, ok := resultHandlers[key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tval, err := dec.Token()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif err = handler(val, ent); err != nil {\n\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; handler\", slogutil.KeyError, err)\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// resultHandlers is the map of log entry decode handlers for various keys.\nvar resultHandlers = map[string]logEntryHandler{\n\t\"IsFiltered\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(bool)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.Result.IsFiltered = v\n\n\t\treturn nil\n\t},\n\t\"Rule\": func(t json.Token, ent *logEntry) error {\n\t\ts, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tl := len(ent.Result.Rules)\n\t\tif l == 0 {\n\t\t\tent.Result.Rules = []*filtering.ResultRule{{}}\n\t\t\tl++\n\t\t}\n\n\t\tent.Result.Rules[l-1].Text = s\n\n\t\treturn nil\n\t},\n\t\"FilterID\": func(t json.Token, ent *logEntry) error {\n\t\tn, ok := t.(json.Number)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tid, err := n.Int64()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tl := len(ent.Result.Rules)\n\t\tif l == 0 {\n\t\t\tent.Result.Rules = []*filtering.ResultRule{{}}\n\t\t\tl++\n\t\t}\n\n\t\tent.Result.Rules[l-1].FilterListID = rulelist.APIID(id)\n\n\t\treturn nil\n\t},\n\t\"Reason\": func(t json.Token, ent *logEntry) error {\n\t\tv, ok := t.(json.Number)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\ti, err := v.Int64()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tent.Result.Reason = filtering.Reason(i)\n\n\t\treturn nil\n\t},\n\t\"ServiceName\": func(t json.Token, ent *logEntry) error {\n\t\ts, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.Result.ServiceName = s\n\n\t\treturn nil\n\t},\n\t\"CanonName\": func(t json.Token, ent *logEntry) error {\n\t\ts, ok := t.(string)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tent.Result.CanonName = s\n\n\t\treturn nil\n\t},\n}\n\n// resultDecHandlers calls a decode handler for key if there is one.\nfunc (l *queryLog) resultDecHandler(\n\tctx context.Context,\n\tname string,\n\tdec *json.Decoder,\n\tent *logEntry,\n) (ok bool) {\n\tok = true\n\tswitch name {\n\tcase \"ReverseHosts\":\n\t\tl.decodeResultReverseHosts(ctx, dec, ent)\n\tcase \"IPList\":\n\t\tl.decodeResultIPList(ctx, dec, ent)\n\tcase \"Rules\":\n\t\tl.decodeResultRules(ctx, dec, ent)\n\tcase \"DNSRewriteResult\":\n\t\tl.decodeResultDNSRewriteResult(ctx, dec, ent)\n\tdefault:\n\t\tok = false\n\t}\n\n\treturn ok\n}\n\n// decodeLogEntry decodes string str to logEntry ent.\nfunc (l *queryLog) decodeLogEntry(ctx context.Context, ent *logEntry, str string) {\n\tconst msgPrefix = \"decoding log entry\"\n\n\tdec := json.NewDecoder(strings.NewReader(str))\n\tdec.UseNumber()\n\n\tfor {\n\t\tkeyToken, err := dec.Token()\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; token\", slogutil.KeyError, err)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif _, ok := keyToken.(json.Delim); ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tkey, ok := keyToken.(string)\n\t\tif !ok {\n\t\t\terr = fmt.Errorf(\"%s: keyToken is %T (%[2]v) and not string\", msgPrefix, keyToken)\n\t\t\tl.logger.DebugContext(ctx, msgPrefix, slogutil.KeyError, err)\n\n\t\t\treturn\n\t\t}\n\n\t\tif key == \"Result\" {\n\t\t\tl.decodeResult(ctx, dec, ent)\n\n\t\t\tcontinue\n\t\t}\n\n\t\thandler, ok := logEntryHandlers[key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tval, err := dec.Token()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif err = handler(val, ent); err != nil {\n\t\t\tl.logger.DebugContext(ctx, msgPrefix+\"; handler\", slogutil.KeyError, err)\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// newUnexpectedDelimiterError is a helper for creating informative errors.\nfunc newUnexpectedDelimiterError(d json.Delim) (err error) {\n\treturn fmt.Errorf(\"unexpected delimiter: %q\", d)\n}\n"
  },
  {
    "path": "internal/querylog/decode_internal_test.go",
    "content": "package querylog\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/urlfilter/rules\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Common constants for tests.\nconst testTimeout = 1 * time.Second\n\nfunc TestQueryLog_DecodeLogEntry_success(t *testing.T) {\n\tlogOutput := &bytes.Buffer{}\n\tl := &queryLog{\n\t\tlogger: slog.New(slog.NewTextHandler(logOutput, &slog.HandlerOptions{\n\t\t\tLevel:       slog.LevelDebug,\n\t\t\tReplaceAttr: slogutil.RemoveTime,\n\t\t})),\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tconst ansStr = `Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==`\n\tconst data = `{\"IP\":\"127.0.0.1\",` +\n\t\t`\"CID\":\"cli42\",` +\n\t\t`\"T\":\"2020-11-25T18:55:56.519796+03:00\",` +\n\t\t`\"QH\":\"an.yandex.ru\",` +\n\t\t`\"QT\":\"A\",` +\n\t\t`\"QC\":\"IN\",` +\n\t\t`\"CP\":\"\",` +\n\t\t`\"ECS\":\"1.2.3.0/24\",` +\n\t\t`\"Answer\":\"` + ansStr + `\",` +\n\t\t`\"Cached\":true,` +\n\t\t`\"AD\":true,` +\n\t\t`\"Result\":{` +\n\t\t`\"IsFiltered\":true,` +\n\t\t`\"Reason\":3,` +\n\t\t`\"IPList\":[\"127.0.0.2\"],` +\n\t\t`\"Rules\":[{\"FilterListID\":42,\"Text\":\"||an.yandex.ru\",\"IP\":\"127.0.0.2\"},` +\n\t\t`{\"FilterListID\":43,\"Text\":\"||an2.yandex.ru\",\"IP\":\"127.0.0.3\"}],` +\n\t\t`\"CanonName\":\"example.com\",` +\n\t\t`\"ServiceName\":\"example.org\",` +\n\t\t`\"DNSRewriteResult\":{\"RCode\":0,\"Response\":{\"1\":[\"127.0.0.2\"]}}},` +\n\t\t`\"Upstream\":\"https://some.upstream\",` +\n\t\t`\"Elapsed\":837429}`\n\n\tans, err := base64.StdEncoding.DecodeString(ansStr)\n\trequire.NoError(t, err)\n\n\tresult := filtering.Result{\n\t\tDNSRewriteResult: &filtering.DNSRewriteResult{\n\t\t\tRCode: dns.RcodeSuccess,\n\t\t\tResponse: filtering.DNSRewriteResultResponse{\n\t\t\t\tdns.TypeA: []rules.RRValue{net.IPv4(127, 0, 0, 2)},\n\t\t\t},\n\t\t},\n\t\tCanonName:   \"example.com\",\n\t\tServiceName: \"example.org\",\n\t\tIPList:      []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 2})},\n\t\tRules: []*filtering.ResultRule{{\n\t\t\tFilterListID: 42,\n\t\t\tText:         \"||an.yandex.ru\",\n\t\t\tIP:           netip.AddrFrom4([4]byte{127, 0, 0, 2}),\n\t\t}, {\n\t\t\tFilterListID: 43,\n\t\t\tText:         \"||an2.yandex.ru\",\n\t\t\tIP:           netip.AddrFrom4([4]byte{127, 0, 0, 3}),\n\t\t}},\n\t\tReason:     filtering.FilteredBlockList,\n\t\tIsFiltered: true,\n\t}\n\n\twant := &logEntry{\n\t\tIP:                net.IPv4(127, 0, 0, 1),\n\t\tTime:              time.Date(2020, 11, 25, 15, 55, 56, 519796000, time.UTC),\n\t\tQHost:             \"an.yandex.ru\",\n\t\tQType:             \"A\",\n\t\tQClass:            \"IN\",\n\t\tClientID:          \"cli42\",\n\t\tClientProto:       \"\",\n\t\tReqECS:            \"1.2.3.0/24\",\n\t\tAnswer:            ans,\n\t\tCached:            true,\n\t\tResult:            result,\n\t\tUpstream:          \"https://some.upstream\",\n\t\tElapsed:           837429,\n\t\tAuthenticatedData: true,\n\t}\n\n\tgot := &logEntry{}\n\tl.decodeLogEntry(ctx, got, data)\n\n\ts := logOutput.String()\n\tassert.Empty(t, s)\n\n\t// Correct for time zones.\n\tgot.Time = got.Time.UTC()\n\tassert.Equal(t, want, got)\n}\n\nfunc TestQueryLog_DecodeLogEntry(t *testing.T) {\n\tlogOutput := &bytes.Buffer{}\n\tl := &queryLog{\n\t\tlogger: slog.New(slog.NewTextHandler(logOutput, &slog.HandlerOptions{\n\t\t\tLevel:       slog.LevelDebug,\n\t\t\tReplaceAttr: slogutil.RemoveTime,\n\t\t})),\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\ttestCases := []struct {\n\t\tname string\n\t\tlog  string\n\t\twant string\n\t}{{\n\t\tname: \"all_right_old_rule\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3,\"Rule\":\"||an.yandex.\",\"FilterID\":1,\"ReverseHosts\":[\"example.com\"],\"IPList\":[\"127.0.0.1\"]},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"bad_filter_id_old_rule\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3,\"FilterID\":1.5},\"Elapsed\":837429}`,\n\t\twant: `level=DEBUG msg=\"decoding result; handler\" err=\"strconv.ParseInt: parsing \\\"1.5\\\": invalid syntax\"`,\n\t}, {\n\t\tname: \"bad_is_filtered\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":trooe,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: `level=DEBUG msg=\"decoding log entry; token\" err=\"invalid character 'o' in literal true (expecting 'u')\"`,\n\t}, {\n\t\tname: \"bad_elapsed\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":-1}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"bad_ip\",\n\t\tlog:  `{\"IP\":127001,\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"bad_time\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"12/09/1998T15:00:00.000000+05:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: `level=DEBUG msg=\"decoding log entry; handler\" err=\"parsing time \\\"12/09/1998T15:00:00.000000+05:00\\\" as \\\"2006-01-02T15:04:05Z07:00\\\": cannot parse \\\"12/09/1998T15:00:00.000000+05:00\\\" as \\\"2006\\\"\"`,\n\t}, {\n\t\tname: \"bad_host\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":6,\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"bad_type\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":true,\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"bad_class\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":false,\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"bad_client_proto\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":8,\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"very_bad_client_proto\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"dog\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: `level=DEBUG msg=\"decoding log entry; handler\" err=\"invalid client proto: \\\"dog\\\"\"`,\n\t}, {\n\t\tname: \"bad_answer\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":0.9,\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"very_bad_answer\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3},\"Elapsed\":837429}`,\n\t\twant: `level=DEBUG msg=\"decoding log entry; handler\" err=\"illegal base64 data at input byte 61\"`,\n\t}, {\n\t\tname: \"bad_rule\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3,\"Rule\":false},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"bad_reason\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":true},\"Elapsed\":837429}`,\n\t\twant: \"\",\n\t}, {\n\t\tname: \"bad_reverse_hosts\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3,\"ReverseHosts\":[{}]},\"Elapsed\":837429}`,\n\t\twant: `level=DEBUG msg=\"decoding result reverse hosts\" err=\"unexpected delimiter: \\\"{\\\"\"`,\n\t}, {\n\t\tname: \"bad_ip_list\",\n\t\tlog:  `{\"IP\":\"127.0.0.1\",\"T\":\"2020-11-25T18:55:56.519796+03:00\",\"QH\":\"an.yandex.ru\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==\",\"Result\":{\"IsFiltered\":true,\"Reason\":3,\"ReverseHosts\":[\"example.net\"],\"IPList\":[{}]},\"Elapsed\":837429}`,\n\t\twant: `level=DEBUG msg=\"decoding result ip list\" err=\"unexpected delimiter: \\\"{\\\"\"`,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tl.decodeLogEntry(ctx, new(logEntry), tc.log)\n\t\t\tgot := logOutput.String()\n\t\t\tif tc.want == \"\" {\n\t\t\t\tassert.Empty(t, got)\n\t\t\t} else {\n\t\t\t\trequire.NotEmpty(t, got)\n\n\t\t\t\t// Remove newline.\n\t\t\t\tgot = got[:len(got)-1]\n\t\t\t\tassert.Equal(t, tc.want, got)\n\t\t\t}\n\n\t\t\tlogOutput.Reset()\n\t\t})\n\t}\n}\n\nfunc TestDecodeLogEntry_backwardCompatability(t *testing.T) {\n\tvar (\n\t\ta1    = netutil.IPv4Localhost()\n\t\ta2    = a1.Next()\n\t\taaaa1 = netutil.IPv6Localhost()\n\t\taaaa2 = aaaa1.Next()\n\t)\n\n\tl := &queryLog{\n\t\tlogger: slogutil.NewDiscardLogger(),\n\t}\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\ttestCases := []struct {\n\t\twant  *logEntry\n\t\tentry string\n\t\tname  string\n\t}{{\n\t\tentry: `{\"Result\":{\"ReverseHosts\":[\"example.net\",\"example.org\"]}`,\n\t\twant: &logEntry{\n\t\t\tResult: filtering.Result{DNSRewriteResult: &filtering.DNSRewriteResult{\n\t\t\t\tRCode: dns.RcodeSuccess,\n\t\t\t\tResponse: filtering.DNSRewriteResultResponse{\n\t\t\t\t\tdns.TypePTR: []rules.RRValue{\"example.net.\", \"example.org.\"},\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\tname: \"reverse_hosts\",\n\t}, {\n\t\tentry: `{\"Result\":{\"IPList\":[\"127.0.0.1\",\"127.0.0.2\",\"::1\",\"::2\"],\"Reason\":10}}`,\n\t\twant: &logEntry{\n\t\t\tResult: filtering.Result{\n\t\t\t\tDNSRewriteResult: &filtering.DNSRewriteResult{\n\t\t\t\t\tRCode: dns.RcodeSuccess,\n\t\t\t\t\tResponse: filtering.DNSRewriteResultResponse{\n\t\t\t\t\t\tdns.TypeA:    []rules.RRValue{a1, a2},\n\t\t\t\t\t\tdns.TypeAAAA: []rules.RRValue{aaaa1, aaaa2},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tReason: filtering.RewrittenAutoHosts,\n\t\t\t},\n\t\t},\n\t\tname: \"iplist_autohosts\",\n\t}, {\n\t\tentry: `{\"Result\":{\"IPList\":[\"127.0.0.1\",\"127.0.0.2\",\"::1\",\"::2\"],\"Reason\":9}}`,\n\t\twant: &logEntry{\n\t\t\tResult: filtering.Result{\n\t\t\t\tIPList: []netip.Addr{\n\t\t\t\t\ta1,\n\t\t\t\t\ta2,\n\t\t\t\t\taaaa1,\n\t\t\t\t\taaaa2,\n\t\t\t\t},\n\t\t\t\tReason: filtering.Rewritten,\n\t\t\t},\n\t\t},\n\t\tname: \"iplist_rewritten\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := &logEntry{}\n\t\t\tl.decodeLogEntry(ctx, e, tc.entry)\n\n\t\t\tassert.Equal(t, tc.want, e)\n\t\t})\n\t}\n}\n\n// anonymizeIPSlow masks ip to anonymize the client if the ip is a valid one.\n// It only exists in purposes of benchmark comparison, see BenchmarkAnonymizeIP.\nfunc anonymizeIPSlow(ip net.IP) {\n\tif ip4 := ip.To4(); ip4 != nil {\n\t\tcopy(ip4[net.IPv4len-2:net.IPv4len], []byte{0, 0})\n\t} else if len(ip) == net.IPv6len {\n\t\tcopy(ip[net.IPv6len-10:net.IPv6len], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0})\n\t}\n}\n\n// TODO(e.burkov):  Investigate the results, it seems that the slow version\n// isn't that slow.\nfunc BenchmarkAnonymizeIP(b *testing.B) {\n\tbenchCases := []struct {\n\t\tname string\n\t\tip   net.IP\n\t\twant net.IP\n\t}{{\n\t\tname: \"v4\",\n\t\tip:   net.IP{1, 2, 3, 4},\n\t\twant: net.IP{1, 2, 0, 0},\n\t}, {\n\t\tname: \"v4_mapped\",\n\t\tip:   net.IP{1, 2, 3, 4}.To16(),\n\t\twant: net.IP{1, 2, 0, 0}.To16(),\n\t}, {\n\t\tname: \"v6\",\n\t\tip: net.IP{\n\t\t\t0xa, 0xb, 0x0, 0x0,\n\t\t\t0x0, 0xb, 0xa, 0x9,\n\t\t\t0x8, 0x7, 0x6, 0x5,\n\t\t\t0x4, 0x3, 0x2, 0x1,\n\t\t},\n\t\twant: net.IP{\n\t\t\t0xa, 0xb, 0x0, 0x0,\n\t\t\t0x0, 0xb, 0x0, 0x0,\n\t\t\t0x0, 0x0, 0x0, 0x0,\n\t\t\t0x0, 0x0, 0x0, 0x0,\n\t\t},\n\t}, {\n\t\tname: \"invalid\",\n\t\tip:   net.IP{1, 2, 3},\n\t\twant: net.IP{1, 2, 3},\n\t}}\n\n\tfor _, bc := range benchCases {\n\t\tb.Run(bc.name, func(b *testing.B) {\n\t\t\tb.ReportAllocs()\n\n\t\t\tfor b.Loop() {\n\t\t\t\tAnonymizeIP(bc.ip)\n\t\t\t}\n\n\t\t\tassert.Equal(b, bc.want, bc.ip)\n\t\t})\n\n\t\tb.Run(bc.name+\"_slow\", func(b *testing.B) {\n\t\t\tb.ReportAllocs()\n\n\t\t\tfor b.Loop() {\n\t\t\t\tanonymizeIPSlow(bc.ip)\n\t\t\t}\n\n\t\t\tassert.Equal(b, bc.want, bc.ip)\n\t\t})\n\t}\n\n\t// Most recent results:\n\t//\n\t//\tgoos: darwin\n\t//\tgoarch: amd64\n\t//\tpkg: github.com/AdguardTeam/AdGuardHome/internal/querylog\n\t//\tcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz\n\t//\tBenchmarkAnonymizeIP/v4-12              \t426499675\t         2.687 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkAnonymizeIP/v4_slow-12         \t510082938\t         2.412 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkAnonymizeIP/v4_mapped-12       \t149121745\t         7.992 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkAnonymizeIP/v4_mapped_slow-12  \t178441804\t         6.698 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkAnonymizeIP/v6-12              \t346746447\t         3.436 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkAnonymizeIP/v6_slow-12         \t419062732\t         2.966 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkAnonymizeIP/invalid-12         \t316385232\t         3.941 ns/op\t       0 B/op\t       0 allocs/op\n\t//\tBenchmarkAnonymizeIP/invalid_slow-12    \t456531592\t         2.760 ns/op\t       0 B/op\t       0 allocs/op\n}\n"
  },
  {
    "path": "internal/querylog/entry.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// logEntry represents a single entry in the file.\ntype logEntry struct {\n\t// client is the found client information, if any.\n\tclient *Client\n\n\tTime time.Time `json:\"T\"`\n\n\tQHost  string `json:\"QH\"`\n\tQType  string `json:\"QT\"`\n\tQClass string `json:\"QC\"`\n\n\tReqECS string `json:\"ECS,omitempty\"`\n\n\tClientID    string      `json:\"CID,omitempty\"`\n\tClientProto ClientProto `json:\"CP\"`\n\n\tUpstream string `json:\",omitempty\"`\n\n\tAnswer     []byte `json:\",omitempty\"`\n\tOrigAnswer []byte `json:\",omitempty\"`\n\n\t// TODO(s.chzhen):  Use netip.Addr.\n\tIP net.IP `json:\"IP\"`\n\n\tResult filtering.Result\n\n\tElapsed time.Duration\n\n\tCached            bool `json:\",omitempty\"`\n\tAuthenticatedData bool `json:\"AD,omitempty\"`\n}\n\n// shallowClone returns a shallow clone of e.\nfunc (e *logEntry) shallowClone() (clone *logEntry) {\n\tcloneVal := *e\n\n\treturn &cloneVal\n}\n\n// addResponse adds data from resp to e.Answer if resp is not nil.  If isOrig is\n// true, addResponse sets the e.OrigAnswer field instead of e.Answer.  Any\n// errors are logged.\nfunc (e *logEntry) addResponse(ctx context.Context, l *slog.Logger, resp *dns.Msg, isOrig bool) {\n\tif resp == nil {\n\t\treturn\n\t}\n\n\tvar err error\n\tif isOrig {\n\t\te.OrigAnswer, err = resp.Pack()\n\t\terr = errors.Annotate(err, \"packing orig answer: %w\")\n\t} else {\n\t\te.Answer, err = resp.Pack()\n\t\terr = errors.Annotate(err, \"packing answer: %w\")\n\t}\n\n\tif err != nil {\n\t\tl.ErrorContext(ctx, \"adding data from response\", slogutil.KeyError, err)\n\t}\n}\n\n// parseDNSRewriteResultIPs fills logEntry's DNSRewriteResult response records\n// with the IP addresses parsed from the raw strings.\nfunc (e *logEntry) parseDNSRewriteResultIPs() {\n\tfor rrType, rrValues := range e.Result.DNSRewriteResult.Response {\n\t\tswitch rrType {\n\t\tcase dns.TypeA, dns.TypeAAAA:\n\t\t\tfor i, v := range rrValues {\n\t\t\t\ts, _ := v.(string)\n\t\t\t\trrValues[i] = net.ParseIP(s)\n\t\t\t}\n\t\tdefault:\n\t\t\t// Go on.\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/querylog/http.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"golang.org/x/net/idna\"\n)\n\n// configJSON is the JSON structure for the querylog configuration.\ntype configJSON struct {\n\t// Interval is the querylog rotation interval.  Use float64 here to support\n\t// fractional numbers and not mess the API users by changing the units.\n\tInterval float64 `json:\"interval\"`\n\n\t// Enabled shows if the querylog is enabled.  It is an aghalg.NullBool to\n\t// be able to tell when it's set without using pointers.\n\tEnabled aghalg.NullBool `json:\"enabled\"`\n\n\t// AnonymizeClientIP shows if the clients' IP addresses must be anonymized.\n\t// It is an [aghalg.NullBool] to be able to tell when it's set without using\n\t// pointers.\n\tAnonymizeClientIP aghalg.NullBool `json:\"anonymize_client_ip\"`\n}\n\n// getConfigResp is the JSON structure for the querylog configuration.\ntype getConfigResp struct {\n\t// Ignored is the list of host names, which should not be written to log.\n\tIgnored []string `json:\"ignored\"`\n\n\t// Interval is the querylog rotation interval in milliseconds.\n\tInterval float64 `json:\"interval\"`\n\n\t// Enabled shows if the querylog is enabled.  It is an aghalg.NullBool to\n\t// be able to tell when it's set without using pointers.\n\tEnabled aghalg.NullBool `json:\"enabled\"`\n\n\tIgnoredEnabled aghalg.NullBool `json:\"ignored_enabled\"`\n\n\t// AnonymizeClientIP shows if the clients' IP addresses must be anonymized.\n\t// It is an aghalg.NullBool to be able to tell when it's set without using\n\t// pointers.\n\t//\n\t// TODO(a.garipov): Consider using separate setting for statistics.\n\tAnonymizeClientIP aghalg.NullBool `json:\"anonymize_client_ip\"`\n}\n\n// Register web handlers\nfunc (l *queryLog) initWeb() {\n\tl.conf.HTTPReg.Register(http.MethodGet, \"/control/querylog\", l.handleQueryLog)\n\tl.conf.HTTPReg.Register(http.MethodPost, \"/control/querylog_clear\", l.handleQueryLogClear)\n\tl.conf.HTTPReg.Register(http.MethodGet, \"/control/querylog/config\", l.handleGetQueryLogConfig)\n\tl.conf.HTTPReg.Register(\n\t\thttp.MethodPut,\n\t\t\"/control/querylog/config/update\",\n\t\tl.handlePutQueryLogConfig,\n\t)\n\n\t// Deprecated handlers.\n\tl.conf.HTTPReg.Register(http.MethodGet, \"/control/querylog_info\", l.handleQueryLogInfo)\n\tl.conf.HTTPReg.Register(http.MethodPost, \"/control/querylog_config\", l.handleQueryLogConfig)\n}\n\n// handleQueryLog is the handler for the GET /control/querylog HTTP API.\nfunc (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tparams, err := l.parseSearchParams(ctx, r)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l.logger, r, w, http.StatusBadRequest, \"parsing params: %s\", err)\n\n\t\treturn\n\t}\n\n\tvar entries []*logEntry\n\tvar oldest time.Time\n\tfunc() {\n\t\tl.confMu.RLock()\n\t\tdefer l.confMu.RUnlock()\n\n\t\tentries, oldest = l.search(ctx, params)\n\t}()\n\n\tresp := l.entriesToJSON(ctx, entries, oldest, l.anonymizer.Load())\n\n\taghhttp.WriteJSONResponseOK(ctx, l.logger, w, r, resp)\n}\n\n// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP\n// API.\nfunc (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, r *http.Request) {\n\tl.clear(r.Context())\n}\n\n// handleQueryLogInfo is the handler for the GET /control/querylog_info HTTP\n// API.\n//\n// Deprecated:  Remove it when migration to the new API is over.\nfunc (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {\n\tl.confMu.RLock()\n\tdefer l.confMu.RUnlock()\n\n\tivl := l.conf.RotationIvl\n\n\tif !checkInterval(ivl) {\n\t\t// NOTE: If interval is custom we set it to 90 days for compatibility\n\t\t// with old API.\n\t\tivl = timeutil.Day * 90\n\t}\n\n\taghhttp.WriteJSONResponseOK(r.Context(), l.logger, w, r, configJSON{\n\t\tEnabled:           aghalg.BoolToNullBool(l.conf.Enabled),\n\t\tInterval:          ivl.Hours() / 24,\n\t\tAnonymizeClientIP: aghalg.BoolToNullBool(l.conf.AnonymizeClientIP),\n\t})\n}\n\n// handleGetQueryLogConfig is the handler for the GET /control/querylog/config\n// HTTP API.\nfunc (l *queryLog) handleGetQueryLogConfig(w http.ResponseWriter, r *http.Request) {\n\tvar resp *getConfigResp\n\tfunc() {\n\t\tl.confMu.RLock()\n\t\tdefer l.confMu.RUnlock()\n\n\t\tresp = &getConfigResp{\n\t\t\tInterval:          float64(l.conf.RotationIvl.Milliseconds()),\n\t\t\tEnabled:           aghalg.BoolToNullBool(l.conf.Enabled),\n\t\t\tAnonymizeClientIP: aghalg.BoolToNullBool(l.conf.AnonymizeClientIP),\n\t\t\tIgnored:           l.conf.Ignored.Values(),\n\t\t\tIgnoredEnabled:    aghalg.BoolToNullBool(l.conf.Ignored.IsEnabled()),\n\t\t}\n\t}()\n\n\taghhttp.WriteJSONResponseOK(r.Context(), l.logger, w, r, resp)\n}\n\n// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.\nfunc AnonymizeIP(ip net.IP) {\n\t// zeroes is a slice of zero bytes from which the IP address tail is copied.\n\t// Using constant string as source of copying is more efficient than byte\n\t// slice, see https://github.com/golang/go/issues/49997.\n\tconst zeroes = \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n\n\tif ip4 := ip.To4(); ip4 != nil {\n\t\tcopy(ip4[net.IPv4len-2:net.IPv4len], zeroes)\n\t} else if len(ip) == net.IPv6len {\n\t\tcopy(ip[net.IPv6len-10:net.IPv6len], zeroes)\n\t}\n}\n\n// handleQueryLogConfig is the handler for the POST /control/querylog_config\n// HTTP API.\n//\n// Deprecated:  Remove it when migration to the new API is over.\nfunc (l *queryLog) handleQueryLogConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\t// Set NaN as initial value to be able to know if it changed later by\n\t// comparing it to NaN.\n\tnewConf := &configJSON{\n\t\tInterval: math.NaN(),\n\t}\n\n\terr := json.NewDecoder(r.Body).Decode(newConf)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l.logger, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tivl := time.Duration(float64(timeutil.Day) * newConf.Interval)\n\n\thasIvl := !math.IsNaN(newConf.Interval)\n\tif hasIvl && !checkInterval(ivl) {\n\t\taghhttp.ErrorAndLog(ctx, l.logger, r, w, http.StatusBadRequest, \"unsupported interval\")\n\n\t\treturn\n\t}\n\n\tdefer l.conf.ConfigModifier.Apply(ctx)\n\n\tl.confMu.Lock()\n\tdefer l.confMu.Unlock()\n\n\tconf := *l.conf\n\tif newConf.Enabled != aghalg.NBNull {\n\t\tconf.Enabled = newConf.Enabled == aghalg.NBTrue\n\t}\n\n\tif hasIvl {\n\t\tconf.RotationIvl = ivl\n\t}\n\n\tif newConf.AnonymizeClientIP != aghalg.NBNull {\n\t\tconf.AnonymizeClientIP = newConf.AnonymizeClientIP == aghalg.NBTrue\n\t\tif conf.AnonymizeClientIP {\n\t\t\tl.anonymizer.Store(AnonymizeIP)\n\t\t} else {\n\t\t\tl.anonymizer.Store(nil)\n\t\t}\n\t}\n\n\tl.conf = &conf\n}\n\n// handlePutQueryLogConfig is the handler for the PUT\n// /control/querylog/config/update HTTP API.\nfunc (l *queryLog) handlePutQueryLogConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tnewConf, err := readConfigResp(r)\n\tif err != nil {\n\t\tcode := http.StatusBadRequest\n\t\tif errors.Is(err, ErrNullConfEnabled) || errors.Is(err, ErrNullAnonymizeIP) {\n\t\t\tcode = http.StatusUnprocessableEntity\n\t\t}\n\n\t\taghhttp.ErrorAndLog(ctx, l.logger, r, w, code, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tvar ignoredEnabled bool\n\tif newConf.IgnoredEnabled == aghalg.NBNull {\n\t\tignoredEnabled = len(newConf.Ignored) > 0\n\t} else {\n\t\tignoredEnabled = newConf.IgnoredEnabled == aghalg.NBTrue\n\t}\n\n\tengine, err := aghnet.NewIgnoreEngine(newConf.Ignored, ignoredEnabled)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl.logger,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusUnprocessableEntity,\n\t\t\t\"ignored: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tivl := time.Duration(newConf.Interval) * time.Millisecond\n\terr = validateIvl(ivl)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\tl.logger,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusUnprocessableEntity,\n\t\t\t\"unsupported interval: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tl.applyQueryLogConfig(ctx, engine, ivl, newConf)\n}\n\nconst (\n\t// ErrNullConfEnabled is returned when [getConfigResp.Enabled] is not set.\n\tErrNullConfEnabled errors.Error = \"enabled is null\"\n\n\t// ErrNullAnonymizeIP is returned when [getConfigResp.AnonymizeClientIP] is\n\t// not set.\n\tErrNullAnonymizeIP errors.Error = \"anonymize_client_ip is null\"\n)\n\n// readConfigResp decodes and minimally validates the request body.  r must not\n// be nil.\nfunc readConfigResp(r *http.Request) (conf *getConfigResp, err error) {\n\tconf = &getConfigResp{}\n\terr = json.NewDecoder(r.Body).Decode(conf)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tif conf.Enabled == aghalg.NBNull {\n\t\treturn nil, ErrNullConfEnabled\n\t}\n\n\tif conf.AnonymizeClientIP == aghalg.NBNull {\n\t\treturn nil, ErrNullAnonymizeIP\n\t}\n\n\treturn conf, nil\n}\n\n// applyQueryLogConfig applies the validated config to queryLog.  engine must\n// not be nil.  ivl must pass [validateIvl], and newConf must be produced by\n// [readConfigResp].\nfunc (l *queryLog) applyQueryLogConfig(\n\tctx context.Context,\n\tengine *aghnet.IgnoreEngine,\n\tivl time.Duration,\n\tnewConf *getConfigResp,\n) {\n\tdefer l.conf.ConfigModifier.Apply(ctx)\n\n\tl.confMu.Lock()\n\tdefer l.confMu.Unlock()\n\n\tconf := *l.conf\n\n\tconf.Ignored = engine\n\tconf.RotationIvl = ivl\n\tconf.Enabled = newConf.Enabled == aghalg.NBTrue\n\tconf.AnonymizeClientIP = newConf.AnonymizeClientIP == aghalg.NBTrue\n\n\tif conf.AnonymizeClientIP {\n\t\tl.anonymizer.Store(AnonymizeIP)\n\t} else {\n\t\tl.anonymizer.Store(nil)\n\t}\n\n\tl.conf = &conf\n}\n\n// \"value\" -> value, return TRUE\nfunc getDoubleQuotesEnclosedValue(s *string) bool {\n\tt := *s\n\tif len(t) >= 2 && t[0] == '\"' && t[len(t)-1] == '\"' {\n\t\t*s = t[1 : len(t)-1]\n\t\treturn true\n\t}\n\treturn false\n}\n\n// parseSearchCriterion parses a search criterion from the query parameter.\nfunc (l *queryLog) parseSearchCriterion(\n\tctx context.Context,\n\tq url.Values,\n\tname string,\n\tct criterionType,\n) (ok bool, sc searchCriterion, err error) {\n\tval := q.Get(name)\n\tif val == \"\" {\n\t\treturn false, sc, nil\n\t}\n\n\tstrict := getDoubleQuotesEnclosedValue(&val)\n\n\tvar asciiVal string\n\tswitch ct {\n\tcase ctTerm:\n\t\t// Decode lowercased value from punycode to make EqualFold and\n\t\t// friends work properly with IDNAs.\n\t\t//\n\t\t// TODO(e.burkov):  Make it work with parts of IDNAs somehow.\n\t\tloweredVal := strings.ToLower(val)\n\t\tif asciiVal, err = idna.ToASCII(loweredVal); err != nil {\n\t\t\tl.logger.DebugContext(ctx, \"converting  to ascii\", \"value\", val, slogutil.KeyError, err)\n\t\t} else if asciiVal == loweredVal {\n\t\t\t// Purge asciiVal to prevent checking the same value\n\t\t\t// twice.\n\t\t\tasciiVal = \"\"\n\t\t}\n\tcase ctFilteringStatus:\n\t\tif !slices.Contains(filteringStatusValues, val) {\n\t\t\treturn false, sc, fmt.Errorf(\"invalid value %s\", val)\n\t\t}\n\tdefault:\n\t\treturn false, sc, fmt.Errorf(\n\t\t\t\"invalid criterion type %v: should be one of %v\",\n\t\t\tct,\n\t\t\t[]criterionType{ctTerm, ctFilteringStatus},\n\t\t)\n\t}\n\n\tsc = searchCriterion{\n\t\tcriterionType: ct,\n\t\tvalue:         val,\n\t\tasciiVal:      asciiVal,\n\t\tstrict:        strict,\n\t}\n\n\treturn true, sc, nil\n}\n\n// parseSearchParams parses search parameters from the HTTP request's query\n// string.\nfunc (l *queryLog) parseSearchParams(\n\tctx context.Context,\n\tr *http.Request,\n) (p *searchParams, err error) {\n\tp = newSearchParams()\n\n\tq := r.URL.Query()\n\tolderThan := q.Get(\"older_than\")\n\tif len(olderThan) != 0 {\n\t\tp.olderThan, err = time.Parse(time.RFC3339Nano, olderThan)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tvar limit64 int64\n\tif limit64, err = strconv.ParseInt(q.Get(\"limit\"), 10, 64); err == nil {\n\t\tp.limit = int(limit64)\n\t}\n\n\tvar offset64 int64\n\tif offset64, err = strconv.ParseInt(q.Get(\"offset\"), 10, 64); err == nil {\n\t\tp.offset = int(offset64)\n\n\t\t// If we don't use \"olderThan\" and use offset/limit instead, we should change the default behavior\n\t\t// and scan all log records until we found enough log entries\n\t\tp.maxFileScanEntries = 0\n\t}\n\n\tfor _, v := range []struct {\n\t\turlField string\n\t\tct       criterionType\n\t}{{\n\t\turlField: \"search\",\n\t\tct:       ctTerm,\n\t}, {\n\t\turlField: \"response_status\",\n\t\tct:       ctFilteringStatus,\n\t}} {\n\t\tvar ok bool\n\t\tvar c searchCriterion\n\t\tok, c, err = l.parseSearchCriterion(ctx, q, v.urlField, v.ct)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif ok {\n\t\t\tp.searchCriteria = append(p.searchCriteria, c)\n\t\t}\n\t}\n\n\treturn p, nil\n}\n"
  },
  {
    "path": "internal/querylog/json.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/miekg/dns\"\n\t\"golang.org/x/net/idna\"\n)\n\n// TODO(a.garipov): Use a proper structured approach here.\n\n// jobject is a JSON object alias.\ntype jobject = map[string]any\n\n// entriesToJSON converts query log entries to JSON.\nfunc (l *queryLog) entriesToJSON(\n\tctx context.Context,\n\tentries []*logEntry,\n\toldest time.Time,\n\tanonFunc aghnet.IPMutFunc,\n) (res jobject) {\n\tdata := make([]jobject, 0, len(entries))\n\n\t// The elements order is already reversed to be from newer to older.\n\tfor _, entry := range entries {\n\t\tjsonEntry := l.entryToJSON(ctx, entry, anonFunc)\n\t\tdata = append(data, jsonEntry)\n\t}\n\n\tres = jobject{\n\t\t\"data\":   data,\n\t\t\"oldest\": \"\",\n\t}\n\tif !oldest.IsZero() {\n\t\tres[\"oldest\"] = oldest.Format(time.RFC3339Nano)\n\t}\n\n\treturn res\n}\n\n// entryToJSON converts a log entry's data into an entry for the JSON API.\nfunc (l *queryLog) entryToJSON(\n\tctx context.Context,\n\tentry *logEntry,\n\tanonFunc aghnet.IPMutFunc,\n) (jsonEntry jobject) {\n\tquestion := questionPayload(ctx, l.logger, entry)\n\tentIP := slices.Clone(entry.IP)\n\tanonFunc(entIP)\n\n\tjsonEntry = jobject{\n\t\t\"reason\":       entry.Result.Reason.String(),\n\t\t\"elapsedMs\":    strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),\n\t\t\"time\":         entry.Time.Format(time.RFC3339Nano),\n\t\t\"client\":       entIP,\n\t\t\"client_proto\": entry.ClientProto,\n\t\t\"cached\":       entry.Cached,\n\t\t\"upstream\":     entry.Upstream,\n\t\t\"question\":     question,\n\t\t\"rules\":        resultRulesToJSONRules(entry.Result.Rules),\n\t}\n\n\tif entIP.Equal(entry.IP) {\n\t\tjsonEntry[\"client_info\"] = entry.client\n\t}\n\n\tif entry.ClientID != \"\" {\n\t\tjsonEntry[\"client_id\"] = entry.ClientID\n\t}\n\n\tif entry.ReqECS != \"\" {\n\t\tjsonEntry[\"ecs\"] = entry.ReqECS\n\t}\n\n\tif len(entry.Result.Rules) > 0 {\n\t\tif r := entry.Result.Rules[0]; len(r.Text) > 0 {\n\t\t\tjsonEntry[\"rule\"] = r.Text\n\t\t\tjsonEntry[\"filterId\"] = r.FilterListID\n\t\t}\n\t}\n\n\tif len(entry.Result.ServiceName) != 0 {\n\t\tjsonEntry[\"service_name\"] = entry.Result.ServiceName\n\t}\n\n\tl.setMsgData(ctx, entry, jsonEntry)\n\tl.setOrigAns(ctx, entry, jsonEntry)\n\n\treturn jsonEntry\n}\n\n// questionPayload builds the \"question\" field for a logEntry.  l and entry must\n// not be nil.\nfunc questionPayload(ctx context.Context, l *slog.Logger, entry *logEntry) (q jobject) {\n\thostname := entry.QHost\n\tq = jobject{\n\t\t\"type\":  entry.QType,\n\t\t\"class\": entry.QClass,\n\t\t\"name\":  hostname,\n\t}\n\n\tif qhost, err := idna.ToUnicode(hostname); err != nil {\n\t\tl.DebugContext(\n\t\t\tctx,\n\t\t\t\"translating into unicode\",\n\t\t\t\"hostname\", hostname,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\t} else if qhost != hostname && qhost != \"\" {\n\t\tq[\"unicode_name\"] = qhost\n\t}\n\n\treturn q\n}\n\n// setMsgData sets the message data in jsonEntry.\nfunc (l *queryLog) setMsgData(ctx context.Context, entry *logEntry, jsonEntry jobject) {\n\tif len(entry.Answer) == 0 {\n\t\treturn\n\t}\n\n\tmsg := &dns.Msg{}\n\tif err := msg.Unpack(entry.Answer); err != nil {\n\t\tl.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"unpacking dns message\",\n\t\t\t\"answer\", entry.Answer,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\n\t\treturn\n\t}\n\n\tjsonEntry[\"status\"] = dns.RcodeToString[msg.Rcode]\n\t// Old query logs may still keep AD flag value in the message.  Try to get\n\t// it from there as well.\n\tjsonEntry[\"answer_dnssec\"] = entry.AuthenticatedData || msg.AuthenticatedData\n\n\tif a := answerToJSON(msg); a != nil {\n\t\tjsonEntry[\"answer\"] = a\n\t}\n}\n\n// setOrigAns sets the original answer data in jsonEntry.\nfunc (l *queryLog) setOrigAns(ctx context.Context, entry *logEntry, jsonEntry jobject) {\n\tif len(entry.OrigAnswer) == 0 {\n\t\treturn\n\t}\n\n\torig := &dns.Msg{}\n\terr := orig.Unpack(entry.OrigAnswer)\n\tif err != nil {\n\t\tl.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"setting original answer\",\n\t\t\t\"answer\", entry.OrigAnswer,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\n\t\treturn\n\t}\n\n\tif a := answerToJSON(orig); a != nil {\n\t\tjsonEntry[\"original_answer\"] = a\n\t}\n}\n\nfunc resultRulesToJSONRules(rules []*filtering.ResultRule) (jsonRules []jobject) {\n\tjsonRules = make([]jobject, len(rules))\n\tfor i, r := range rules {\n\t\tjsonRules[i] = jobject{\n\t\t\t\"filter_list_id\": r.FilterListID,\n\t\t\t\"text\":           r.Text,\n\t\t}\n\t}\n\n\treturn jsonRules\n}\n\ntype dnsAnswer struct {\n\tType  string `json:\"type\"`\n\tValue string `json:\"value\"`\n\tTTL   uint32 `json:\"ttl\"`\n}\n\n// answerToJSON converts the answer records of msg, if any, to their JSON form.\nfunc answerToJSON(msg *dns.Msg) (answers []*dnsAnswer) {\n\tif msg == nil || len(msg.Answer) == 0 {\n\t\treturn nil\n\t}\n\n\tanswers = make([]*dnsAnswer, 0, len(msg.Answer))\n\tfor _, rr := range msg.Answer {\n\t\theader := rr.Header()\n\t\ta := &dnsAnswer{\n\t\t\tType: dns.TypeToString[header.Rrtype],\n\t\t\t// Remove the header string from the answer value since it's mostly\n\t\t\t// unnecessary in the log.\n\t\t\tValue: strings.TrimPrefix(rr.String(), header.String()),\n\t\t\tTTL:   header.Ttl,\n\t\t}\n\n\t\tanswers = append(answers, a)\n\t}\n\n\treturn answers\n}\n"
  },
  {
    "path": "internal/querylog/qlog.go",
    "content": "// Package querylog provides query log functions and interfaces.\npackage querylog\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/miekg/dns\"\n)\n\n// queryLogFileName is a name of the log file.  \".gz\" extension is added later\n// during compression.\nconst queryLogFileName = \"querylog.json\"\n\n// queryLog is a structure that writes and reads the DNS query log.\ntype queryLog struct {\n\t// logger is used for logging the operation of the query log.  It must not\n\t// be nil.\n\tlogger *slog.Logger\n\n\t// confMu protects conf.\n\tconfMu *sync.RWMutex\n\n\tconf       *Config\n\tanonymizer *aghnet.IPMut\n\n\tfindClient func(ids []string) (c *Client, err error)\n\n\t// buffer contains recent log entries.  The entries in this buffer must not\n\t// be modified.\n\tbuffer *container.RingBuffer[*logEntry]\n\n\t// logFile is the path to the log file.\n\tlogFile string\n\n\t// bufferLock protects buffer.\n\tbufferLock sync.RWMutex\n\n\t// fileFlushLock synchronizes a file-flushing goroutine and main thread.\n\tfileFlushLock sync.Mutex\n\tfileWriteLock sync.Mutex\n\n\tflushPending bool\n}\n\n// ClientProto values are names of the client protocols.\ntype ClientProto string\n\n// Client protocol names.\nconst (\n\tClientProtoDoH      ClientProto = \"doh\"\n\tClientProtoDoQ      ClientProto = \"doq\"\n\tClientProtoDoT      ClientProto = \"dot\"\n\tClientProtoDNSCrypt ClientProto = \"dnscrypt\"\n\tClientProtoPlain    ClientProto = \"\"\n)\n\n// NewClientProto validates that the client protocol name is valid and returns\n// the name as a ClientProto.\nfunc NewClientProto(s string) (cp ClientProto, err error) {\n\tswitch cp = ClientProto(s); cp {\n\tcase\n\t\tClientProtoDoH,\n\t\tClientProtoDoQ,\n\t\tClientProtoDoT,\n\t\tClientProtoDNSCrypt,\n\t\tClientProtoPlain:\n\n\t\treturn cp, nil\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"invalid client proto: %q\", s)\n\t}\n}\n\n// type check\nvar _ QueryLog = (*queryLog)(nil)\n\n// Start implements the [QueryLog] interface for *queryLog.\nfunc (l *queryLog) Start(ctx context.Context) (err error) {\n\tif l.conf.HTTPReg != nil {\n\t\tl.initWeb()\n\t}\n\n\tgo l.periodicRotate(ctx)\n\n\treturn nil\n}\n\n// Shutdown implements the [QueryLog] interface for *queryLog.\nfunc (l *queryLog) Shutdown(ctx context.Context) (err error) {\n\tl.confMu.RLock()\n\tdefer l.confMu.RUnlock()\n\n\tif l.conf.FileEnabled {\n\t\terr = l.flushLogBuffer(ctx)\n\t\tif err != nil {\n\t\t\t// Don't wrap the error because it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc checkInterval(ivl time.Duration) (ok bool) {\n\t// The constants for possible values of query log's rotation interval.\n\tconst (\n\t\tquarterDay  = timeutil.Day / 4\n\t\tday         = timeutil.Day\n\t\tweek        = timeutil.Day * 7\n\t\tmonth       = timeutil.Day * 30\n\t\tthreeMonths = timeutil.Day * 90\n\t)\n\n\treturn ivl == quarterDay || ivl == day || ivl == week || ivl == month || ivl == threeMonths\n}\n\n// validateIvl returns an error if ivl is less than an hour or more than a\n// year.\nfunc validateIvl(ivl time.Duration) (err error) {\n\tif ivl < time.Hour {\n\t\treturn errors.Error(\"less than an hour\")\n\t}\n\n\tif ivl > timeutil.Day*365 {\n\t\treturn errors.Error(\"more than a year\")\n\t}\n\n\treturn nil\n}\n\n// WriteDiskConfig implements the [QueryLog] interface for *queryLog.\nfunc (l *queryLog) WriteDiskConfig(c *Config) {\n\tl.confMu.RLock()\n\tdefer l.confMu.RUnlock()\n\n\t*c = *l.conf\n}\n\n// Clear memory buffer and remove log files\nfunc (l *queryLog) clear(ctx context.Context) {\n\tl.fileFlushLock.Lock()\n\tdefer l.fileFlushLock.Unlock()\n\n\tfunc() {\n\t\tl.bufferLock.Lock()\n\t\tdefer l.bufferLock.Unlock()\n\n\t\tl.buffer.Clear()\n\t\tl.flushPending = false\n\t}()\n\n\toldLogFile := l.logFile + \".1\"\n\terr := os.Remove(oldLogFile)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tl.logger.ErrorContext(\n\t\t\tctx,\n\t\t\t\"removing old log file\",\n\t\t\t\"file\", oldLogFile,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\t}\n\n\terr = os.Remove(l.logFile)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tl.logger.ErrorContext(ctx, \"removing log file\", \"file\", l.logFile, slogutil.KeyError, err)\n\t}\n\n\tl.logger.DebugContext(ctx, \"cleared\")\n}\n\n// newLogEntry creates an instance of logEntry from parameters.\nfunc newLogEntry(ctx context.Context, logger *slog.Logger, params *AddParams) (entry *logEntry) {\n\tq := params.Question.Question[0]\n\tqHost := aghnet.NormalizeDomain(q.Name)\n\n\tentry = &logEntry{\n\t\t// TODO(d.kolyshev): Export this timestamp to func params.\n\t\tTime:   time.Now(),\n\t\tQHost:  qHost,\n\t\tQType:  dns.Type(q.Qtype).String(),\n\t\tQClass: dns.Class(q.Qclass).String(),\n\n\t\tClientID:    params.ClientID,\n\t\tClientProto: params.ClientProto,\n\n\t\tResult:   *params.Result,\n\t\tUpstream: params.Upstream,\n\n\t\tIP: params.ClientIP,\n\n\t\tElapsed: params.Elapsed,\n\n\t\tCached:            params.Cached,\n\t\tAuthenticatedData: params.AuthenticatedData,\n\t}\n\n\tif params.ReqECS != nil {\n\t\tentry.ReqECS = params.ReqECS.String()\n\t}\n\n\tentry.addResponse(ctx, logger, params.Answer, false)\n\tentry.addResponse(ctx, logger, params.OrigAnswer, true)\n\n\treturn entry\n}\n\n// Add implements the [QueryLog] interface for *queryLog.\nfunc (l *queryLog) Add(params *AddParams) {\n\tvar isEnabled, fileIsEnabled bool\n\tvar memSize uint\n\tfunc() {\n\t\tl.confMu.RLock()\n\t\tdefer l.confMu.RUnlock()\n\n\t\tisEnabled, fileIsEnabled = l.conf.Enabled, l.conf.FileEnabled\n\t\tmemSize = l.conf.MemSize\n\t}()\n\n\tif !isEnabled {\n\t\treturn\n\t}\n\n\t// TODO(s.chzhen):  Pass context.\n\tctx := context.TODO()\n\n\terr := params.validate()\n\tif err != nil {\n\t\tl.logger.ErrorContext(ctx, \"adding record\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tif params.Result == nil {\n\t\tparams.Result = &filtering.Result{}\n\t}\n\n\tentry := newLogEntry(ctx, l.logger, params)\n\n\tl.bufferLock.Lock()\n\tdefer l.bufferLock.Unlock()\n\n\tl.buffer.Push(entry)\n\n\tif !l.flushPending && fileIsEnabled && l.buffer.Len() >= memSize {\n\t\tl.flushPending = true\n\n\t\t// TODO(s.chzhen):  Fix occasional rewrite of entires.\n\t\tgo func() {\n\t\t\tflushErr := l.flushLogBuffer(ctx)\n\t\t\tif flushErr != nil {\n\t\t\t\tl.logger.ErrorContext(ctx, \"flushing after adding\", slogutil.KeyError, flushErr)\n\t\t\t}\n\t\t}()\n\t}\n}\n\n// ShouldLog returns true if request for the host should be logged.\nfunc (l *queryLog) ShouldLog(host string, _, _ uint16, ids []string) bool {\n\tl.confMu.RLock()\n\tdefer l.confMu.RUnlock()\n\n\tc, err := l.findClient(ids)\n\tif err != nil {\n\t\t// TODO(s.chzhen):  Pass context.\n\t\tl.logger.ErrorContext(context.TODO(), \"finding client\", slogutil.KeyError, err)\n\t}\n\n\tif c != nil && c.IgnoreQueryLog {\n\t\treturn false\n\t}\n\n\treturn !l.isIgnored(host)\n}\n\n// isIgnored returns true if the host is in the ignored domains list.  It\n// assumes that l.confMu is locked for reading.\nfunc (l *queryLog) isIgnored(host string) bool {\n\treturn l.conf.Ignored.Has(host)\n}\n"
  },
  {
    "path": "internal/querylog/qlog_internal_test.go",
    "content": "package querylog\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// searchCriTerm is a helper function for tests that constructs a search\n// criterion of type [ctTerm].\nfunc searchCriTerm(val string, isStrict bool) (c searchCriterion) {\n\treturn searchCriterion{\n\t\tvalue:         val,\n\t\tcriterionType: ctTerm,\n\t\tstrict:        isStrict,\n\t}\n}\n\n// TestQueryLog tests adding and loading (with filtering) entries from disk and\n// memory.\nfunc TestQueryLog(t *testing.T) {\n\thosts := []string{\n\t\t\"example.org\",\n\t\t\"example.org\",\n\t\t\"test.example.org\",\n\t\t\"example.com\",\n\t\t\"\",\n\t}\n\n\tentries := make([]*logEntry, len(hosts))\n\tfor i, h := range hosts {\n\t\tn := byte(i + 1)\n\t\tentries[i] = &logEntry{\n\t\t\tQHost:  h,\n\t\t\tAnswer: net.IPv4(192, 0, 2, n),\n\t\t\tIP:     net.IPv4(203, 0, 113, n),\n\t\t}\n\t}\n\n\tentry1, entry2, entry3, entry4 := entries[0], entries[1], entries[2], entries[3]\n\n\tentryRoot := entries[4]\n\tentryRootWant := &logEntry{QHost: \".\", Answer: entryRoot.Answer, IP: entryRoot.IP}\n\n\tl, err := newQueryLog(Config{\n\t\tLogger:      slogutil.NewDiscardLogger(),\n\t\tEnabled:     true,\n\t\tFileEnabled: true,\n\t\tRotationIvl: timeutil.Day,\n\t\tMemSize:     100,\n\t\tBaseDir:     t.TempDir(),\n\t})\n\trequire.NoError(t, err)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\t// Add disk entries.\n\taddEntry(l, entry1.QHost, entry1.Answer, entry1.IP)\n\t// Write to disk (first file).\n\trequire.NoError(t, l.flushLogBuffer(ctx))\n\n\t// Start writing to the second file.\n\trequire.NoError(t, l.rotate(ctx))\n\n\t// Add disk entries.\n\taddEntry(l, entry2.QHost, entry2.Answer, entry2.IP)\n\t// Write to disk.\n\trequire.NoError(t, l.flushLogBuffer(ctx))\n\n\t// Add memory entries.\n\taddEntry(l, entry3.QHost, entry3.Answer, entry3.IP)\n\taddEntry(l, entry4.QHost, entry4.Answer, entry4.IP)\n\taddEntry(l, entryRoot.QHost, entryRoot.Answer, entryRoot.IP)\n\n\ttestCases := []struct {\n\t\tname string\n\t\tsCr  []searchCriterion\n\t\twant []*logEntry\n\t}{{\n\t\tname: \"all\",\n\t\tsCr:  []searchCriterion{},\n\t\twant: []*logEntry{entryRootWant, entry4, entry3, entry2, entry1},\n\t}, {\n\t\tname: \"by_domain_strict\",\n\t\tsCr:  []searchCriterion{searchCriTerm(\"TEST.example.org\", true)},\n\t\twant: []*logEntry{entry3},\n\t}, {\n\t\tname: \"by_domain_non-strict\",\n\t\tsCr:  []searchCriterion{searchCriTerm(\"example.ORG\", false)},\n\t\twant: []*logEntry{entry3, entry2, entry1},\n\t}, {\n\t\tname: \"by_client_ip_strict\",\n\t\tsCr:  []searchCriterion{searchCriTerm(entry2.IP.String(), true)},\n\t\twant: []*logEntry{entry2},\n\t}, {\n\t\tname: \"by_client_ip_non-strict\",\n\t\tsCr:  []searchCriterion{searchCriTerm(\"203.0.113\", false)},\n\t\twant: []*logEntry{entryRootWant, entry4, entry3, entry2, entry1},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tparams := newSearchParams()\n\t\t\tparams.searchCriteria = tc.sCr\n\n\t\t\tentries, _ = l.search(ctx, params)\n\t\t\trequire.Len(t, entries, len(tc.want))\n\n\t\t\tfor i, want := range tc.want {\n\t\t\t\tassertLogEntry(t, entries[i], want.QHost, want.Answer, want.IP)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestQueryLogOffsetLimit(t *testing.T) {\n\tl, err := newQueryLog(Config{\n\t\tLogger:      slogutil.NewDiscardLogger(),\n\t\tEnabled:     true,\n\t\tRotationIvl: timeutil.Day,\n\t\tMemSize:     100,\n\t\tBaseDir:     t.TempDir(),\n\t})\n\trequire.NoError(t, err)\n\n\tconst (\n\t\tentNum           = 10\n\t\tfirstPageDomain  = \"first.example.org\"\n\t\tsecondPageDomain = \"second.example.org\"\n\t)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\t// Add entries to the log.\n\tfor range entNum {\n\t\taddEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))\n\t}\n\t// Write them to the first file.\n\trequire.NoError(t, l.flushLogBuffer(ctx))\n\n\t// Add more to the in-memory part of log.\n\tfor range entNum {\n\t\taddEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))\n\t}\n\n\tparams := newSearchParams()\n\n\ttestCases := []struct {\n\t\tname    string\n\t\twant    string\n\t\twantLen int\n\t\toffset  int\n\t\tlimit   int\n\t}{{\n\t\tname:    \"page_1\",\n\t\twant:    firstPageDomain,\n\t\twantLen: 10,\n\t\toffset:  0,\n\t\tlimit:   10,\n\t}, {\n\t\tname:    \"page_2\",\n\t\twant:    secondPageDomain,\n\t\twantLen: 10,\n\t\toffset:  10,\n\t\tlimit:   10,\n\t}, {\n\t\tname:    \"page_2.5\",\n\t\twant:    secondPageDomain,\n\t\twantLen: 5,\n\t\toffset:  15,\n\t\tlimit:   10,\n\t}, {\n\t\tname:    \"page_3\",\n\t\twant:    \"\",\n\t\twantLen: 0,\n\t\toffset:  20,\n\t\tlimit:   10,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tparams.offset = tc.offset\n\t\t\tparams.limit = tc.limit\n\t\t\tentries, _ := l.search(ctx, params)\n\t\t\trequire.Len(t, entries, tc.wantLen)\n\n\t\t\tif tc.wantLen > 0 {\n\t\t\t\tassert.Equal(t, entries[0].QHost, tc.want)\n\t\t\t\tassert.Equal(t, entries[tc.wantLen-1].QHost, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestQueryLogMaxFileScanEntries(t *testing.T) {\n\tl, err := newQueryLog(Config{\n\t\tLogger:      slogutil.NewDiscardLogger(),\n\t\tEnabled:     true,\n\t\tFileEnabled: true,\n\t\tRotationIvl: timeutil.Day,\n\t\tMemSize:     100,\n\t\tBaseDir:     t.TempDir(),\n\t})\n\trequire.NoError(t, err)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tconst entNum = 10\n\t// Add entries to the log.\n\tfor range entNum {\n\t\taddEntry(l, \"example.org\", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))\n\t}\n\t// Write them to disk.\n\trequire.NoError(t, l.flushLogBuffer(ctx))\n\n\tparams := newSearchParams()\n\tfor _, maxFileScanEntries := range []int{5, 0} {\n\t\tt.Run(fmt.Sprintf(\"limit_%d\", maxFileScanEntries), func(t *testing.T) {\n\t\t\tparams.maxFileScanEntries = maxFileScanEntries\n\t\t\tentries, _ := l.search(ctx, params)\n\t\t\tassert.Len(t, entries, entNum-maxFileScanEntries)\n\t\t})\n\t}\n}\n\nfunc TestQueryLogFileDisabled(t *testing.T) {\n\tl, err := newQueryLog(Config{\n\t\tLogger:      slogutil.NewDiscardLogger(),\n\t\tEnabled:     true,\n\t\tFileEnabled: false,\n\t\tRotationIvl: timeutil.Day,\n\t\tMemSize:     2,\n\t\tBaseDir:     t.TempDir(),\n\t})\n\trequire.NoError(t, err)\n\n\taddEntry(l, \"example1.org\", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))\n\taddEntry(l, \"example2.org\", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))\n\t// The oldest entry is going to be removed from memory buffer.\n\taddEntry(l, \"example3.org\", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))\n\n\tparams := newSearchParams()\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tll, _ := l.search(ctx, params)\n\trequire.Len(t, ll, 2)\n\n\tassert.Equal(t, \"example3.org\", ll[0].QHost)\n\tassert.Equal(t, \"example2.org\", ll[1].QHost)\n}\n\nfunc TestQueryLogShouldLog(t *testing.T) {\n\tconst (\n\t\tignored1        = \"ignor.ed\"\n\t\tignored2        = \"ignored.to\"\n\t\tignoredWildcard = \"*.ignored.com\"\n\t\tignoredRoot     = \"|.^\"\n\t)\n\n\tignored := []string{\n\t\tignored1,\n\t\tignored2,\n\t\tignoredWildcard,\n\t\tignoredRoot,\n\t}\n\n\tengine, err := aghnet.NewIgnoreEngine(ignored, true)\n\trequire.NoError(t, err)\n\n\tfindClient := func(ids []string) (c *Client, err error) {\n\t\tlog := ids[0] == \"no_log\"\n\n\t\treturn &Client{IgnoreQueryLog: log}, nil\n\t}\n\n\tl, err := newQueryLog(Config{\n\t\tIgnored:     engine,\n\t\tEnabled:     true,\n\t\tRotationIvl: timeutil.Day,\n\t\tMemSize:     100,\n\t\tBaseDir:     t.TempDir(),\n\t\tFindClient:  findClient,\n\t})\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname    string\n\t\thost    string\n\t\tids     []string\n\t\twantLog bool\n\t}{{\n\t\tname:    \"log\",\n\t\thost:    \"example.com\",\n\t\tids:     []string{\"whatever\"},\n\t\twantLog: true,\n\t}, {\n\t\tname:    \"no_log_ignored_1\",\n\t\thost:    ignored1,\n\t\tids:     []string{\"whatever\"},\n\t\twantLog: false,\n\t}, {\n\t\tname:    \"no_log_ignored_2\",\n\t\thost:    ignored2,\n\t\tids:     []string{\"whatever\"},\n\t\twantLog: false,\n\t}, {\n\t\tname:    \"no_log_ignored_wildcard\",\n\t\thost:    \"www.ignored.com\",\n\t\tids:     []string{\"whatever\"},\n\t\twantLog: false,\n\t}, {\n\t\tname:    \"no_log_ignored_root\",\n\t\thost:    \".\",\n\t\tids:     []string{\"whatever\"},\n\t\twantLog: false,\n\t}, {\n\t\tname:    \"no_log_client_ignore\",\n\t\thost:    \"example.com\",\n\t\tids:     []string{\"no_log\"},\n\t\twantLog: false,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tres := l.ShouldLog(tc.host, dns.TypeA, dns.ClassINET, tc.ids)\n\n\t\t\tassert.Equal(t, tc.wantLog, res)\n\t\t})\n\t}\n}\n\nfunc addEntry(l *queryLog, host string, answerStr, client net.IP) {\n\tq := dns.Msg{\n\t\tQuestion: []dns.Question{{\n\t\t\tName:   host + \".\",\n\t\t\tQtype:  dns.TypeA,\n\t\t\tQclass: dns.ClassINET,\n\t\t}},\n\t}\n\n\ta := dns.Msg{\n\t\tQuestion: q.Question,\n\t\tAnswer: []dns.RR{&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   q.Question[0].Name,\n\t\t\t\tRrtype: dns.TypeA,\n\t\t\t\tClass:  dns.ClassINET,\n\t\t\t},\n\t\t\tA: answerStr,\n\t\t}},\n\t}\n\n\tres := filtering.Result{\n\t\tServiceName: \"SomeService\",\n\t\tRules: []*filtering.ResultRule{{\n\t\t\tFilterListID: 1,\n\t\t\tText:         \"SomeRule\",\n\t\t}},\n\t\tReason:     filtering.Rewritten,\n\t\tIsFiltered: true,\n\t}\n\n\tparams := &AddParams{\n\t\tQuestion:   &q,\n\t\tAnswer:     &a,\n\t\tOrigAnswer: &a,\n\t\tResult:     &res,\n\t\tUpstream:   \"upstream\",\n\t\tClientIP:   client,\n\t}\n\n\tl.Add(params)\n}\n\nfunc assertLogEntry(tb testing.TB, entry *logEntry, host string, answer, client net.IP) {\n\ttb.Helper()\n\n\trequire.NotNil(tb, entry)\n\n\tassert.Equal(tb, host, entry.QHost)\n\tassert.Equal(tb, client, entry.IP)\n\tassert.Equal(tb, \"A\", entry.QType)\n\tassert.Equal(tb, \"IN\", entry.QClass)\n\n\tmsg := &dns.Msg{}\n\trequire.NoError(tb, msg.Unpack(entry.Answer))\n\trequire.Len(tb, msg.Answer, 1)\n\n\ta := testutil.RequireTypeAssert[*dns.A](tb, msg.Answer[0])\n\tassert.Equal(tb, answer, a.A.To16())\n}\n"
  },
  {
    "path": "internal/querylog/qlogfile.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\nconst (\n\t// Timestamp not found errors.\n\terrTSNotFound errors.Error = \"ts not found\"\n\terrTSTooLate  errors.Error = \"ts too late\"\n\terrTSTooEarly errors.Error = \"ts too early\"\n\n\t// maxEntrySize is a maximum size of the entry.\n\t//\n\t// TODO: Find a way to grow buffer instead of relying on this value when\n\t// reading strings.\n\tmaxEntrySize = 16 * 1024\n\n\t// bufferSize should be enough for at least this number of entries.\n\tbufferSize = 100 * maxEntrySize\n)\n\n// qLogFile represents a single query log file.  It allows reading from the\n// file in the reverse order.\n//\n// Please note, that this is a stateful object.  Internally, it contains a\n// pointer to a specific position in the file, and it reads lines in reverse\n// order starting from that position.\ntype qLogFile struct {\n\t// file is the query log file.\n\tfile *os.File\n\n\t// buffer that we've read from the file.\n\tbuffer []byte\n\n\t// lock is a mutex to make it thread-safe.\n\tlock sync.Mutex\n\n\t// position is the position in the file.\n\tposition int64\n\n\t// bufferStart is the start of the buffer (in the file).\n\tbufferStart int64\n\n\t// bufferLen is the length of the buffer.\n\tbufferLen int\n}\n\n// newQLogFile initializes a new instance of the qLogFile.\nfunc newQLogFile(path string) (qf *qLogFile, err error) {\n\tf, err := os.OpenFile(path, os.O_RDONLY, aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &qLogFile{file: f}, nil\n}\n\n// validateQLogLineIdx returns error if the line index is not valid to continue\n// search.\nfunc (q *qLogFile) validateQLogLineIdx(lineIdx, lastProbeLineIdx, ts, fSize int64) (err error) {\n\tif lineIdx == lastProbeLineIdx {\n\t\tif lineIdx == 0 {\n\t\t\treturn errTSTooEarly\n\t\t}\n\n\t\t// If we're testing the same line twice then most likely the scope is\n\t\t// too narrow and we won't find anything anymore in any other file.\n\t\treturn fmt.Errorf(\"looking up timestamp %d in %q: %w\", ts, q.file.Name(), errTSNotFound)\n\t} else if lineIdx == fSize {\n\t\treturn errTSTooLate\n\t}\n\n\treturn nil\n}\n\n// seekTS performs binary search in the query log file looking for a record\n// with the specified timestamp.  Once the record is found, it sets \"position\"\n// so that the next ReadNext call returned that record.\n//\n// The algorithm is rather simple:\n//  1. It starts with the position in the middle of a file.\n//  2. Shifts back to the beginning of the line.\n//  3. Checks the log record timestamp.\n//  4. If it is lower than the timestamp we are looking for, it shifts seek\n//     position to 3/4 of the file. Otherwise, to 1/4 of the file.\n//  5. It performs the search again, every time the search scope is narrowed\n//     twice.\n//\n// Returns:\n//   - It returns the position of the line with the timestamp we were looking\n//     for so that when we call \"ReadNext\" this line was returned.\n//   - Depth of the search (how many times we compared timestamps).\n//   - If we could not find it, it returns one of the errors described above.\nfunc (q *qLogFile) seekTS(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\ttimestamp int64,\n) (pos int64, depth int, err error) {\n\tq.lock.Lock()\n\tdefer q.lock.Unlock()\n\n\t// Empty the buffer.\n\tq.buffer = nil\n\n\t// First of all, check the file size.\n\tfileInfo, err := q.file.Stat()\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\n\tfileSize := fileInfo.Size()\n\tst := &tsSearchState{\n\t\tstart:            0,\n\t\tend:              fileSize,\n\t\tprobe:            fileSize / 2,\n\t\tlastProbeLineIdx: -1,\n\t\tfileSize:         fileSize,\n\t}\n\n\t// Count seek depth in order to detect mistakes.  If depth is too large,\n\t// we should stop the search.\n\tfor {\n\t\tvar found bool\n\t\tfound, err = q.seekTSStep(ctx, logger, timestamp, st)\n\t\tif err != nil {\n\t\t\treturn 0, st.depth, err\n\t\t}\n\n\t\tif found {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tq.position = st.lineIdx + int64(len(st.line))\n\n\treturn q.position, st.depth, nil\n}\n\n// tsSearchState contains the state of the binary-search loop.\ntype tsSearchState struct {\n\t// line holds the contents of the matched line.\n\tline string\n\n\t// end is the end of the current search interval.\n\tend int64\n\n\t// fileSize is the total file size in bytes.\n\tfileSize int64\n\n\t// lastProbeLineIdx is the byte offset of the last probed line start.\n\tlastProbeLineIdx int64\n\n\t// lineIdx is the byte offset of the current line start.\n\tlineIdx int64\n\n\t// probe is the byte offset to probe.\n\tprobe int64\n\n\t// start is the start of current search interval.\n\tstart int64\n\n\t// depth is the number of search iterations performed.\n\tdepth int\n}\n\n// maxSearchDepth is the maximum number of search iterations.\nconst maxSearchDepth = 100\n\n// seekTSStep performs one iteration of the binary search over the qlog file.  l\n// and st must not be nil.\nfunc (q *qLogFile) seekTSStep(\n\tctx context.Context,\n\tl *slog.Logger,\n\ttimestamp int64,\n\tst *tsSearchState,\n) (found bool, err error) {\n\tline, lineIdx, lineEndIdx, err := q.readAndValidateProbe(st, timestamp)\n\tif err != nil {\n\t\t// Don't wrap the error, because it's informative enough as is.\n\t\treturn false, err\n\t}\n\n\t// Get the timestamp from the query log record.\n\tts := readQLogTimestamp(ctx, l, line)\n\tif ts == 0 {\n\t\treturn false, fmt.Errorf(\n\t\t\t\"looking up timestamp %d in %q: record %q has empty timestamp\",\n\t\t\ttimestamp,\n\t\t\tq.file.Name(),\n\t\t\tline,\n\t\t)\n\t}\n\n\tif ts == timestamp {\n\t\t// Found the target record.\n\t\tst.line = line\n\t\tst.lineIdx = lineIdx\n\n\t\treturn true, nil\n\t}\n\n\t// Narrow the scope and repeat the search.\n\tif ts > timestamp {\n\t\t// If the timestamp we're looking for is OLDER than what we found, then\n\t\t// the line is somewhere on the LEFT side from the current probe\n\t\t// position.\n\t\tst.end = lineIdx\n\t} else {\n\t\t// If the timestamp we're looking for is NEWER than what we found, then\n\t\t// the line is somewhere on the RIGHT side from the current probe\n\t\t// position.\n\t\tst.start = lineEndIdx\n\t}\n\tst.probe = st.start + (st.end-st.start)/2\n\n\tst.depth++\n\tif st.depth >= maxSearchDepth {\n\t\treturn false, fmt.Errorf(\n\t\t\t\"looking up timestamp %d in %q: depth %d too high: %w\",\n\t\t\ttimestamp,\n\t\t\tq.file.Name(),\n\t\t\tst.depth,\n\t\t\terrTSNotFound,\n\t\t)\n\t}\n\n\treturn false, nil\n}\n\n// readAndValidateProbe reads the probe line and validates the line index.\nfunc (q *qLogFile) readAndValidateProbe(\n\tst *tsSearchState,\n\ttimestamp int64,\n) (line string, lineIdx, lineEndIdx int64, err error) {\n\t// Get the line at the specified position.\n\tline, lineIdx, lineEndIdx, err = q.readProbeLine(st.probe)\n\tif err != nil {\n\t\treturn \"\", 0, 0, fmt.Errorf(\"reading probe line: %w\", err)\n\t}\n\n\terr = q.validateQLogLineIdx(lineIdx, st.lastProbeLineIdx, timestamp, st.fileSize)\n\tif err != nil {\n\t\treturn \"\", 0, 0, fmt.Errorf(\"validating line index: %w\", err)\n\t}\n\n\tst.lastProbeLineIdx = lineIdx\n\n\treturn line, lineIdx, lineEndIdx, nil\n}\n\n// SeekStart changes the current position to the end of the file.  Please note,\n// that we're reading query log in the reverse order and that's why log start\n// is actually the end of file.\n//\n// Returns nil if we were able to change the current position.  Returns error\n// in any other case.\nfunc (q *qLogFile) SeekStart() (int64, error) {\n\tq.lock.Lock()\n\tdefer q.lock.Unlock()\n\n\t// Empty the buffer.\n\tq.buffer = nil\n\n\t// First of all, check the file size.\n\tfileInfo, err := q.file.Stat()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Place the position to the very end of file.\n\tq.position = fileInfo.Size() - 1\n\tif q.position < 0 {\n\t\tq.position = 0\n\t}\n\n\treturn q.position, nil\n}\n\n// ReadNext reads the next line (in the reverse order) from the file and shifts\n// the current position left to the next (actually prev) line.\n//\n// Returns io.EOF if there's nothing more to read.\nfunc (q *qLogFile) ReadNext() (string, error) {\n\tq.lock.Lock()\n\tdefer q.lock.Unlock()\n\n\tif q.position == 0 {\n\t\treturn \"\", io.EOF\n\t}\n\n\tline, lineIdx, err := q.readNextLine(q.position)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Shift position.\n\tif lineIdx == 0 {\n\t\tq.position = 0\n\t} else {\n\t\t// There's usually a line break before the line, so we should shift one\n\t\t// more char left from the line \"\\nline\".\n\t\tq.position = lineIdx - 1\n\t}\n\treturn line, err\n}\n\n// Close frees the underlying resources.\nfunc (q *qLogFile) Close() error {\n\treturn q.file.Close()\n}\n\n// readNextLine reads the next line from the specified position.  This line\n// actually have to END on that position.\n//\n// The algorithm is:\n//  1. Check if we have the buffer initialized.\n//  2. If it is so, scan it and look for the line there.\n//  3. If we cannot find the line there, read the prev chunk into the buffer.\n//  4. Read the line from the buffer.\nfunc (q *qLogFile) readNextLine(position int64) (string, int64, error) {\n\trelativePos := position - q.bufferStart\n\tif q.buffer == nil || (relativePos < maxEntrySize && q.bufferStart != 0) {\n\t\t// Time to re-init the buffer.\n\t\terr := q.initBuffer(position)\n\t\tif err != nil {\n\t\t\treturn \"\", 0, err\n\t\t}\n\t\trelativePos = position - q.bufferStart\n\t}\n\n\t// Look for the end of the prev line, this is where we'll read from.\n\tstartLine := int64(0)\n\tfor i := relativePos - 1; i >= 0; i-- {\n\t\tif q.buffer[i] == '\\n' {\n\t\t\tstartLine = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tline := string(q.buffer[startLine:relativePos])\n\tlineIdx := q.bufferStart + startLine\n\n\treturn line, lineIdx, nil\n}\n\n// initBuffer initializes the qLogFile buffer.  The goal is to read a chunk of\n// file that includes the line with the specified position.\nfunc (q *qLogFile) initBuffer(position int64) error {\n\tq.bufferStart = int64(0)\n\tif position > bufferSize {\n\t\tq.bufferStart = position - bufferSize\n\t}\n\n\t// Seek to this position.\n\t_, err := q.file.Seek(q.bufferStart, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif q.buffer == nil {\n\t\tq.buffer = make([]byte, bufferSize)\n\t}\n\n\tq.bufferLen, err = q.file.Read(q.buffer)\n\n\treturn err\n}\n\n// readProbeLine reads a line that includes the specified position.  This\n// method is supposed to be used when we use binary search in the Seek method.\n// In the case of consecutive reads, use readNext, cause it uses better buffer.\nfunc (q *qLogFile) readProbeLine(position int64) (string, int64, int64, error) {\n\t// First of all, we should read a buffer that will include the query log\n\t// line.  In order to do this, we'll define the boundaries.\n\tseekPosition := int64(0)\n\t// Position relative to the buffer we're going to read.\n\trelativePos := position\n\tif position > maxEntrySize {\n\t\tseekPosition = position - maxEntrySize\n\t\trelativePos = maxEntrySize\n\t}\n\n\t// Seek to this position.\n\t_, err := q.file.Seek(seekPosition, io.SeekStart)\n\tif err != nil {\n\t\treturn \"\", 0, 0, err\n\t}\n\n\t// The buffer size is 2*maxEntrySize.\n\tbuffer := make([]byte, maxEntrySize*2)\n\tbufferLen, err := q.file.Read(buffer)\n\tif err != nil {\n\t\treturn \"\", 0, 0, err\n\t}\n\n\t// Now start looking for the new line character starting from the\n\t// relativePos and going left.\n\tstartLine := int64(0)\n\tfor i := relativePos - 1; i >= 0; i-- {\n\t\tif buffer[i] == '\\n' {\n\t\t\tstartLine = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\t// Looking for the end of line now.\n\tendLine := int64(bufferLen)\n\tlineEndIdx := endLine + seekPosition\n\tfor i := relativePos; i < int64(bufferLen); i++ {\n\t\tif buffer[i] == '\\n' {\n\t\t\tendLine = i\n\t\t\tlineEndIdx = endLine + seekPosition + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Finally we can return the string we were looking for.\n\tlineIdx := startLine + seekPosition\n\treturn string(buffer[startLine:endLine]), lineIdx, lineEndIdx, nil\n}\n\n// readJSONValue reads a JSON string in form of '\"key\":\"value\"'.  prefix must\n// be of the form '\"key\":\"' to generate less garbage.\nfunc readJSONValue(s, prefix string) string {\n\ti := strings.Index(s, prefix)\n\tif i == -1 {\n\t\treturn \"\"\n\t}\n\n\tstart := i + len(prefix)\n\ti = strings.IndexByte(s[start:], '\"')\n\tif i == -1 {\n\t\treturn \"\"\n\t}\n\n\tend := start + i\n\treturn s[start:end]\n}\n\n// readQLogTimestamp reads the timestamp field from the query log line.\nfunc readQLogTimestamp(ctx context.Context, logger *slog.Logger, str string) int64 {\n\tval := readJSONValue(str, `\"T\":\"`)\n\tif len(val) == 0 {\n\t\tval = readJSONValue(str, `\"Time\":\"`)\n\t}\n\n\tif len(val) == 0 {\n\t\tlogger.ErrorContext(ctx, \"couldn't find timestamp\", \"line\", str)\n\n\t\treturn 0\n\t}\n\n\ttm, err := time.Parse(time.RFC3339Nano, val)\n\tif err != nil {\n\t\tlogger.ErrorContext(ctx, \"couldn't parse timestamp\", \"value\", val, slogutil.KeyError, err)\n\n\t\treturn 0\n\t}\n\n\treturn tm.UnixNano()\n}\n"
  },
  {
    "path": "internal/querylog/qlogfile_internal_test.go",
    "content": "package querylog\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// prepareTestFile prepares one test query log file with the specified lines\n// count.\nfunc prepareTestFile(tb testing.TB, dir string, linesNum int) (name string) {\n\ttb.Helper()\n\n\tf, err := os.CreateTemp(dir, \"*.txt\")\n\trequire.NoError(tb, err)\n\n\t// Use defer and not t.Cleanup to make sure that the file is closed\n\t// after this function is done.\n\tdefer func() {\n\t\tderr := f.Close()\n\t\trequire.NoError(tb, derr)\n\t}()\n\n\tconst ans = `\"AAAAAAABAAEAAAAAB2V4YW1wbGUDb3JnAAABAAEHZXhhbXBsZQNvcmcAAAEAAQAAAAAABAECAwQ=\"`\n\tconst format = `{\"IP\":%q,\"T\":%q,\"QH\":\"example.org\",\"QT\":\"A\",\"QC\":\"IN\",` +\n\t\t`\"Answer\":` + ans + `,\"Result\":{},\"Elapsed\":0,\"Upstream\":\"upstream\"}` + \"\\n\"\n\n\tvar lineIP uint32\n\tlineTime := time.Date(2020, 2, 18, 19, 36, 35, 920973000, time.UTC)\n\tfor range linesNum {\n\t\tlineIP++\n\t\tlineTime = lineTime.Add(time.Second)\n\n\t\tip := make(net.IP, 4)\n\t\tbinary.BigEndian.PutUint32(ip, lineIP)\n\n\t\tline := fmt.Sprintf(format, ip, lineTime.Format(time.RFC3339Nano))\n\n\t\t_, err = f.WriteString(line)\n\t\trequire.NoError(tb, err)\n\t}\n\n\treturn f.Name()\n}\n\n// prepareTestFiles prepares several test query log files, each with the\n// specified lines count.\nfunc prepareTestFiles(tb testing.TB, filesNum, linesNum int) []string {\n\ttb.Helper()\n\n\tif filesNum == 0 {\n\t\treturn []string{}\n\t}\n\n\tdir := tb.TempDir()\n\n\tfiles := make([]string, filesNum)\n\tfor i := range files {\n\t\tfiles[filesNum-i-1] = prepareTestFile(tb, dir, linesNum)\n\t}\n\n\treturn files\n}\n\n// newTestQLogFile creates new *qLogFile for tests and registers the required\n// cleanup functions.\nfunc newTestQLogFile(tb testing.TB, linesNum int) (file *qLogFile) {\n\ttb.Helper()\n\n\ttestFile := prepareTestFiles(tb, 1, linesNum)[0]\n\n\t// Create the new qLogFile instance.\n\tfile, err := newQLogFile(testFile)\n\trequire.NoError(tb, err)\n\n\tassert.NotNil(tb, file)\n\ttestutil.CleanupAndRequireSuccess(tb, file.Close)\n\n\treturn file\n}\n\nfunc TestQLogFile_ReadNext(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tlinesNum int\n\t}{{\n\t\tname:     \"empty\",\n\t\tlinesNum: 0,\n\t}, {\n\t\tname:     \"large\",\n\t\tlinesNum: 50000,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tq := newTestQLogFile(t, tc.linesNum)\n\n\t\t\t// Calculate the expected position.\n\t\t\tfileInfo, err := q.file.Stat()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar expPos int64\n\t\t\tif expPos = fileInfo.Size(); expPos > 0 {\n\t\t\t\texpPos--\n\t\t\t}\n\n\t\t\t// Seek to the start.\n\t\t\tpos, err := q.SeekStart()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.EqualValues(t, expPos, pos)\n\n\t\t\tread := readAllLines(t, q)\n\t\t\tassert.Equal(t, tc.linesNum, read)\n\t\t})\n\t}\n}\n\n// readAllLines is a helper function that reads entries until EOF and returns\n// the number of lines read.\nfunc readAllLines(tb testing.TB, q *qLogFile) (n int) {\n\ttb.Helper()\n\n\tfor {\n\t\tline, err := q.ReadNext()\n\t\tif err == io.EOF {\n\t\t\treturn n\n\t\t}\n\n\t\trequire.NoError(tb, err)\n\n\t\tassert.NotEmpty(tb, line)\n\n\t\tn++\n\t}\n}\n\nfunc TestQLogFile_SeekTS_good(t *testing.T) {\n\tlinesCases := []struct {\n\t\tname string\n\t\tnum  int\n\t}{{\n\t\tname: \"large\",\n\t\tnum:  10000,\n\t}, {\n\t\tname: \"small\",\n\t\tnum:  10,\n\t}}\n\n\tlogger := slogutil.NewDiscardLogger()\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tfor _, l := range linesCases {\n\t\ttestCases := []struct {\n\t\t\tname     string\n\t\t\tlinesNum int\n\t\t\tline     int\n\t\t}{{\n\t\t\tname: \"not_too_old\",\n\t\t\tline: 2,\n\t\t}, {\n\t\t\tname: \"old\",\n\t\t\tline: l.num - 2,\n\t\t}, {\n\t\t\tname: \"first\",\n\t\t\tline: 0,\n\t\t}, {\n\t\t\tname: \"last\",\n\t\t\tline: l.num,\n\t\t}}\n\n\t\tq := newTestQLogFile(t, l.num)\n\n\t\tfor _, tc := range testCases {\n\t\t\tt.Run(l.name+\"_\"+tc.name, func(t *testing.T) {\n\t\t\t\tline, err := getQLogFileLine(q, tc.line)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tts := readQLogTimestamp(ctx, logger, line)\n\t\t\t\tassert.NotEqualValues(t, 0, ts)\n\n\t\t\t\t// Try seeking to that line now.\n\t\t\t\tpos, _, err := q.seekTS(ctx, logger, ts)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.NotEqualValues(t, 0, pos)\n\n\t\t\t\ttestLine, err := q.ReadNext()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, line, testLine)\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestQLogFile_SeekTS_bad(t *testing.T) {\n\tlinesCases := []struct {\n\t\tname string\n\t\tnum  int\n\t}{{\n\t\tname: \"large\",\n\t\tnum:  10000,\n\t}, {\n\t\tname: \"small\",\n\t\tnum:  10,\n\t}}\n\n\tlogger := slogutil.NewDiscardLogger()\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\tfor _, l := range linesCases {\n\t\ttestCases := []struct {\n\t\t\tname string\n\t\t\tts   int64\n\t\t\tleq  bool\n\t\t}{{\n\t\t\tname: \"non-existent_long_ago\",\n\t\t}, {\n\t\t\tname: \"non-existent_far_ahead\",\n\t\t}, {\n\t\t\tname: \"almost\",\n\t\t\tleq:  true,\n\t\t}}\n\n\t\tq := newTestQLogFile(t, l.num)\n\t\ttestCases[0].ts = 123\n\n\t\tlateTS, _ := time.Parse(time.RFC3339, \"2100-01-02T15:04:05Z07:00\")\n\t\ttestCases[1].ts = lateTS.UnixNano()\n\n\t\tline, err := getQLogFileLine(q, l.num/2)\n\t\trequire.NoError(t, err)\n\n\t\ttestCases[2].ts = readQLogTimestamp(ctx, logger, line) - 1\n\t\tfor _, tc := range testCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tassert.NotEqualValues(t, 0, tc.ts)\n\n\t\t\t\tvar depth int\n\t\t\t\t_, depth, err = q.seekTS(ctx, logger, tc.ts)\n\t\t\t\tassert.NotEmpty(t, l.num)\n\t\t\t\trequire.Error(t, err)\n\n\t\t\t\tif tc.leq {\n\t\t\t\t\tassert.LessOrEqual(t, depth, int(math.Log2(float64(l.num))+3))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc getQLogFileLine(q *qLogFile, lineNumber int) (line string, err error) {\n\tif _, err = q.SeekStart(); err != nil {\n\t\treturn line, err\n\t}\n\n\tfor i := 1; i < lineNumber; i++ {\n\t\tif _, err = q.ReadNext(); err != nil {\n\t\t\treturn line, err\n\t\t}\n\t}\n\n\treturn q.ReadNext()\n}\n\n// Check adding and loading (with filtering) entries from disk and memory.\nfunc TestQLogFile(t *testing.T) {\n\t// Create the new qLogFile instance.\n\tq := newTestQLogFile(t, 2)\n\n\t// Seek to the start.\n\tpos, err := q.SeekStart()\n\trequire.NoError(t, err)\n\n\tassert.Greater(t, pos, int64(0))\n\n\t// Read first line.\n\tline, err := q.ReadNext()\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, line, \"0.0.0.2\")\n\tassert.True(t, strings.HasPrefix(line, \"{\"), line)\n\tassert.True(t, strings.HasSuffix(line, \"}\"), line)\n\n\t// Read second line.\n\tline, err = q.ReadNext()\n\trequire.NoError(t, err)\n\n\tassert.EqualValues(t, 0, q.position)\n\tassert.Contains(t, line, \"0.0.0.1\")\n\tassert.True(t, strings.HasPrefix(line, \"{\"), line)\n\tassert.True(t, strings.HasSuffix(line, \"}\"), line)\n\n\t// Try reading again (there's nothing to read anymore).\n\tline, err = q.ReadNext()\n\trequire.Equal(t, io.EOF, err)\n\n\tassert.Empty(t, line)\n}\n\nfunc newTestQLogFileData(t *testing.T, data string) (file *qLogFile) {\n\tf, err := os.CreateTemp(t.TempDir(), \"*.txt\")\n\trequire.NoError(t, err)\n\n\ttestutil.CleanupAndRequireSuccess(t, f.Close)\n\n\t_, err = f.WriteString(data)\n\trequire.NoError(t, err)\n\n\tfile, err = newQLogFile(f.Name())\n\trequire.NoError(t, err)\n\n\ttestutil.CleanupAndRequireSuccess(t, file.Close)\n\n\treturn file\n}\n\nfunc TestQLog_Seek(t *testing.T) {\n\tconst nl = \"\\n\"\n\tconst strV = \"%s\"\n\tconst recs = `{\"T\":\"` + strV + `\",\"QH\":\"wfqvjymurpwegyv\",\"QT\":\"A\",\"QC\":\"IN\",\"CP\":\"\",\"Answer\":\"\",\"Result\":{},\"Elapsed\":66286385,\"Upstream\":\"tls://unfiltered.adguard-dns.com:853\"}` + nl +\n\t\t`{\"T\":\"` + strV + `\"}` + nl +\n\t\t`{\"T\":\"` + strV + `\"}` + nl\n\ttimestamp, _ := time.Parse(time.RFC3339Nano, \"2020-08-31T18:44:25.376690873+03:00\")\n\n\tlogger := slogutil.NewDiscardLogger()\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\ttestCases := []struct {\n\t\twantErr   error\n\t\tname      string\n\t\tdelta     int\n\t\twantDepth int\n\t}{{\n\t\tname:      \"ok\",\n\t\tdelta:     0,\n\t\twantErr:   nil,\n\t\twantDepth: 2,\n\t}, {\n\t\tname:      \"too_late\",\n\t\tdelta:     2,\n\t\twantErr:   errTSTooLate,\n\t\twantDepth: 2,\n\t}, {\n\t\tname:      \"too_early\",\n\t\tdelta:     -2,\n\t\twantErr:   errTSTooEarly,\n\t\twantDepth: 1,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdata := fmt.Sprintf(recs,\n\t\t\t\ttimestamp.Add(-time.Second).Format(time.RFC3339Nano),\n\t\t\t\ttimestamp.Format(time.RFC3339Nano),\n\t\t\t\ttimestamp.Add(time.Second).Format(time.RFC3339Nano),\n\t\t\t)\n\n\t\t\tq := newTestQLogFileData(t, data)\n\n\t\t\tts := timestamp.Add(time.Second * time.Duration(tc.delta)).UnixNano()\n\t\t\t_, depth, err := q.seekTS(ctx, logger, ts)\n\t\t\trequire.Truef(t, errors.Is(err, tc.wantErr), \"%v\", err)\n\n\t\t\tassert.Equal(t, tc.wantDepth, depth)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/querylog/qlogreader.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// qLogReader allows reading from multiple query log files in the reverse\n// order.\n//\n// Please note that this is a stateful object.  Internally, it contains a\n// pointer to a particular query log file, and to a specific position in this\n// file, and it reads lines in reverse order starting from that position.\ntype qLogReader struct {\n\t// logger is used for logging the operation of the query log reader.  It\n\t// must not be nil.\n\tlogger *slog.Logger\n\n\t// qFiles is an array with the query log files.  The order is from oldest\n\t// to newest.\n\tqFiles []*qLogFile\n\n\t// currentFile is the index of the current file.\n\tcurrentFile int\n}\n\n// newQLogReader initializes a qLogReader instance with the specified files.\nfunc newQLogReader(ctx context.Context, logger *slog.Logger, files []string) (*qLogReader, error) {\n\tqFiles := make([]*qLogFile, 0)\n\n\tfor _, f := range files {\n\t\tq, err := newQLogFile(f)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Close what we've already opened.\n\t\t\tcErr := closeQFiles(qFiles)\n\t\t\tif cErr != nil {\n\t\t\t\tlogger.DebugContext(ctx, \"closing files\", slogutil.KeyError, cErr)\n\t\t\t}\n\n\t\t\treturn nil, err\n\t\t}\n\n\t\tqFiles = append(qFiles, q)\n\t}\n\n\treturn &qLogReader{\n\t\tlogger:      logger,\n\t\tqFiles:      qFiles,\n\t\tcurrentFile: len(qFiles) - 1,\n\t}, nil\n}\n\n// seekTS performs binary search of a query log record with the specified\n// timestamp.  If the record is found, it sets qLogReader's position to point\n// to that line, so that the next ReadNext call returned this line.\nfunc (r *qLogReader) seekTS(ctx context.Context, timestamp int64) (err error) {\n\tfor i := len(r.qFiles) - 1; i >= 0; i-- {\n\t\tq := r.qFiles[i]\n\t\t_, _, err = q.seekTS(ctx, r.logger, timestamp)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, errTSTooEarly) {\n\t\t\t\t// Look at the next file, since we've reached the end of this\n\t\t\t\t// one.  If there is no next file, it's not found.\n\t\t\t\terr = errTSNotFound\n\n\t\t\t\tcontinue\n\t\t\t} else if errors.Is(err, errTSTooLate) {\n\t\t\t\t// Just seek to the start then.  timestamp is probably between\n\t\t\t\t// the end of the previous one and the start of this one.\n\t\t\t\treturn r.SeekStart()\n\t\t\t} else if errors.Is(err, errTSNotFound) {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"seekts: file at index %d: %w\", i, err)\n\t\t\t}\n\t\t}\n\n\t\t// The search is finished, and the searched element has been found.\n\t\t// Update currentFile only, position is already set properly in\n\t\t// qLogFile.\n\t\tr.currentFile = i\n\n\t\treturn nil\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"seekts: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// SeekStart changes the current position to the end of the newest file.\n// Please note that we're reading query log in the reverse order and that's why\n// the log starts actually at the end of file.\n//\n// Returns nil if we were able to change the current position.  Returns error\n// in any other cases.\nfunc (r *qLogReader) SeekStart() error {\n\tif len(r.qFiles) == 0 {\n\t\treturn nil\n\t}\n\n\tr.currentFile = len(r.qFiles) - 1\n\t_, err := r.qFiles[r.currentFile].SeekStart()\n\n\treturn err\n}\n\n// ReadNext reads the next line (in the reverse order) from the query log\n// files.  Then shifts the current position left to the next (actually prev)\n// line (or the next file).\n//\n// Returns io.EOF if there is nothing more to read.\nfunc (r *qLogReader) ReadNext() (string, error) {\n\tif len(r.qFiles) == 0 {\n\t\treturn \"\", io.EOF\n\t}\n\n\tfor r.currentFile >= 0 {\n\t\tq := r.qFiles[r.currentFile]\n\t\tline, err := q.ReadNext()\n\t\tif err == nil {\n\t\t\treturn line, nil\n\t\t}\n\n\t\t// Shift to the older file.\n\t\tr.currentFile--\n\t\tif r.currentFile < 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tq = r.qFiles[r.currentFile]\n\n\t\t// Set its position to the start right away.\n\t\t_, err = q.SeekStart()\n\t\t// This is unexpected, return an error right away.\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\t// Nothing to read anymore.\n\treturn \"\", io.EOF\n}\n\n// Close closes the qLogReader.\nfunc (r *qLogReader) Close() error {\n\treturn closeQFiles(r.qFiles)\n}\n\n// closeQFiles is a helper method to close multiple qLogFile instances.\nfunc closeQFiles(qFiles []*qLogFile) (err error) {\n\tvar errs []error\n\n\tfor _, q := range qFiles {\n\t\terr = q.Close()\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\treturn errors.Annotate(errors.Join(errs...), \"closing qLogReader: %w\")\n}\n"
  },
  {
    "path": "internal/querylog/qlogreader_internal_test.go",
    "content": "package querylog\n\nimport (\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// newTestQLogReader creates new *qLogReader for tests and registers the\n// required cleanup functions.\nfunc newTestQLogReader(tb testing.TB, filesNum, linesNum int) (reader *qLogReader) {\n\ttb.Helper()\n\n\ttestFiles := prepareTestFiles(tb, filesNum, linesNum)\n\n\tlogger := slogutil.NewDiscardLogger()\n\tctx := testutil.ContextWithTimeout(tb, testTimeout)\n\n\t// Create the new qLogReader instance.\n\treader, err := newQLogReader(ctx, logger, testFiles)\n\trequire.NoError(tb, err)\n\n\tassert.NotNil(tb, reader)\n\ttestutil.CleanupAndRequireSuccess(tb, reader.Close)\n\n\treturn reader\n}\n\nfunc TestQLogReader(t *testing.T) {\n\ttestCases := []struct {\n\t\tname     string\n\t\tfilesNum int\n\t\tlinesNum int\n\t}{{\n\t\tname:     \"empty\",\n\t\tfilesNum: 0,\n\t\tlinesNum: 0,\n\t}, {\n\t\tname:     \"one_file\",\n\t\tfilesNum: 1,\n\t\tlinesNum: 10,\n\t}, {\n\t\tname:     \"multiple_files\",\n\t\tfilesNum: 5,\n\t\tlinesNum: 10000,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := newTestQLogReader(t, tc.filesNum, tc.linesNum)\n\n\t\t\t// Seek to the start.\n\t\t\terr := r.SeekStart()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Read everything.\n\t\t\tvar read int\n\t\t\tvar line string\n\t\t\tfor err == nil {\n\t\t\t\tline, err = r.ReadNext()\n\t\t\t\tif err == nil {\n\t\t\t\t\tassert.NotEmpty(t, line)\n\t\t\t\t\tread++\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.Equal(t, io.EOF, err)\n\t\t\tassert.Equal(t, tc.filesNum*tc.linesNum, read)\n\t\t})\n\t}\n}\n\nfunc TestQLogReader_Seek(t *testing.T) {\n\tr := newTestQLogReader(t, 2, 10000)\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\n\ttestCases := []struct {\n\t\twant error\n\t\tname string\n\t\ttime string\n\t}{{\n\t\tname: \"not_too_old\",\n\t\ttime: \"2020-02-18T22:39:35.920973+03:00\",\n\t\twant: nil,\n\t}, {\n\t\tname: \"old\",\n\t\ttime: \"2020-02-19T01:28:16.920973+03:00\",\n\t\twant: nil,\n\t}, {\n\t\tname: \"first\",\n\t\ttime: \"2020-02-18T22:36:36.920973+03:00\",\n\t\twant: nil,\n\t}, {\n\t\tname: \"last\",\n\t\ttime: \"2020-02-19T01:23:16.920973+03:00\",\n\t\twant: nil,\n\t}, {\n\t\tname: \"non-existent_long_ago\",\n\t\ttime: \"2000-02-19T01:23:16.920973+03:00\",\n\t\twant: errTSNotFound,\n\t}, {\n\t\tname: \"non-existent_far_ahead\",\n\t\ttime: \"2100-02-19T01:23:16.920973+03:00\",\n\t\twant: nil,\n\t}, {\n\t\tname: \"non-existent_but_could\",\n\t\ttime: \"2020-02-18T22:36:37.000000+03:00\",\n\t\twant: errTSNotFound,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tts, err := time.Parse(time.RFC3339Nano, tc.time)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = r.seekTS(ctx, ts.UnixNano())\n\t\t\tassert.ErrorIs(t, err, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestQLogReader_ReadNext(t *testing.T) {\n\tconst linesNum = 10\n\tconst filesNum = 1\n\tr := newTestQLogReader(t, filesNum, linesNum)\n\n\ttestCases := []struct {\n\t\twant  error\n\t\tname  string\n\t\tstart int\n\t}{{\n\t\tname:  \"ok\",\n\t\tstart: 0,\n\t\twant:  nil,\n\t}, {\n\t\tname:  \"too_big\",\n\t\tstart: linesNum + 1,\n\t\twant:  io.EOF,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := r.SeekStart()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor i := 1; i < tc.start; i++ {\n\t\t\t\t_, err = r.ReadNext()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t_, err = r.ReadNext()\n\t\t\tassert.Equal(t, tc.want, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/querylog/querylog.go",
    "content": "package querylog\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/container\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/service\"\n\t\"github.com/miekg/dns\"\n)\n\n// QueryLog is the query log interface for use by other packages.\ntype QueryLog interface {\n\t// Interface starts and stops the query log.\n\tservice.Interface\n\n\t// Add adds a log entry.\n\tAdd(params *AddParams)\n\n\t// WriteDiskConfig writes the query log configuration to c.\n\tWriteDiskConfig(c *Config)\n\n\t// ShouldLog returns true if request for the host should be logged.\n\tShouldLog(host string, qType, qClass uint16, ids []string) bool\n}\n\n// Config is the query log configuration structure.\n//\n// Do not alter any fields of this structure after using it.\ntype Config struct {\n\t// Logger is used for logging the operation of the query log.  It must not\n\t// be nil.\n\tLogger *slog.Logger\n\n\t// Ignored contains the list of host names, which should not be written to\n\t// log, and matches them.\n\tIgnored *aghnet.IgnoreEngine\n\n\t// Anonymizer processes the IP addresses to anonymize those if needed.\n\tAnonymizer *aghnet.IPMut\n\n\t// ConfigModifier is used to update the global configuration.  It must not\n\t// be nil.\n\tConfigModifier agh.ConfigModifier\n\n\t// HTTPRegister registers an HTTP handler.\n\tHTTPReg aghhttp.Registrar\n\n\t// FindClient returns client information by their IDs.\n\tFindClient func(ids []string) (c *Client, err error)\n\n\t// BaseDir is the base directory for log files.\n\tBaseDir string\n\n\t// RotationIvl is the interval for log rotation.  After that period, the old\n\t// log file will be renamed, NOT deleted, so the actual log retention time\n\t// is twice the interval.\n\tRotationIvl time.Duration\n\n\t// MemSize is the number of entries kept in a memory buffer before they are\n\t// flushed to disk.\n\tMemSize uint\n\n\t// Enabled tells if the query log is enabled.\n\tEnabled bool\n\n\t// FileEnabled tells if the query log writes logs to files.\n\tFileEnabled bool\n\n\t// AnonymizeClientIP tells if the query log should anonymize clients' IP\n\t// addresses.\n\tAnonymizeClientIP bool\n}\n\n// AddParams is the parameters for adding an entry.\ntype AddParams struct {\n\tQuestion *dns.Msg\n\n\t// ReqECS is the IP network extracted from EDNS Client-Subnet option of a\n\t// request.\n\tReqECS *net.IPNet\n\n\t// Answer is the response which is sent to the client, if any.\n\tAnswer *dns.Msg\n\n\t// OrigAnswer is the response from an upstream server.  It's only set if the\n\t// answer has been modified by filtering.\n\tOrigAnswer *dns.Msg\n\n\t// Result is the filtering result (optional).\n\tResult *filtering.Result\n\n\tClientID string\n\n\t// Upstream is the URL of the upstream DNS server.\n\tUpstream string\n\n\tClientProto ClientProto\n\n\tClientIP net.IP\n\n\t// Elapsed is the time spent for processing the request.\n\tElapsed time.Duration\n\n\t// Cached indicates if the response is served from cache.\n\tCached bool\n\n\t// AuthenticatedData shows if the response had the AD bit set.\n\tAuthenticatedData bool\n}\n\n// validate returns an error if the parameters aren't valid.\nfunc (p *AddParams) validate() (err error) {\n\tswitch {\n\tcase p.Question == nil:\n\t\treturn errors.Error(\"question is nil\")\n\tcase len(p.Question.Question) != 1:\n\t\treturn errors.Error(\"more than one question\")\n\tcase len(p.Question.Question[0].Name) == 0:\n\t\treturn errors.Error(\"no host in question\")\n\tcase p.ClientIP == nil:\n\t\treturn errors.Error(\"no client ip\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// New creates a new instance of the query log.\nfunc New(conf Config) (ql QueryLog, err error) {\n\treturn newQueryLog(conf)\n}\n\n// newQueryLog crates a new queryLog.\nfunc newQueryLog(conf Config) (l *queryLog, err error) {\n\tfindClient := conf.FindClient\n\tif findClient == nil {\n\t\tfindClient = func(_ []string) (_ *Client, _ error) {\n\t\t\treturn nil, nil\n\t\t}\n\t}\n\n\tmemSize := conf.MemSize\n\tif memSize == 0 {\n\t\t// If query log is enabled, we still need to write entries to a file.\n\t\t// And all writing goes through a buffer.\n\t\tmemSize = 1\n\t}\n\n\tl = &queryLog{\n\t\tlogger:     conf.Logger,\n\t\tfindClient: findClient,\n\n\t\tbuffer: container.NewRingBuffer[*logEntry](memSize),\n\n\t\tconf:    &Config{},\n\t\tconfMu:  &sync.RWMutex{},\n\t\tlogFile: filepath.Join(conf.BaseDir, queryLogFileName),\n\n\t\tanonymizer: conf.Anonymizer,\n\t}\n\n\t*l.conf = conf\n\n\terr = validateIvl(conf.RotationIvl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unsupported interval: %w\", err)\n\t}\n\n\treturn l, nil\n}\n"
  },
  {
    "path": "internal/querylog/querylogfile.go",
    "content": "package querylog\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\n// flushLogBuffer flushes the current buffer to file and resets the current\n// buffer.\nfunc (l *queryLog) flushLogBuffer(ctx context.Context) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"flushing log buffer: %w\") }()\n\n\tl.fileFlushLock.Lock()\n\tdefer l.fileFlushLock.Unlock()\n\n\tb, err := l.encodeEntries(ctx)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\treturn l.flushToFile(ctx, b)\n}\n\n// encodeEntries returns JSON encoded log entries, logs estimated time, clears\n// the log buffer.\nfunc (l *queryLog) encodeEntries(ctx context.Context) (b *bytes.Buffer, err error) {\n\tl.bufferLock.Lock()\n\tdefer l.bufferLock.Unlock()\n\n\tbufLen := l.buffer.Len()\n\tif bufLen == 0 {\n\t\treturn nil, errors.Error(\"nothing to write to a file\")\n\t}\n\n\tstart := time.Now()\n\n\tb = &bytes.Buffer{}\n\te := json.NewEncoder(b)\n\n\tl.buffer.Range(func(entry *logEntry) (cont bool) {\n\t\terr = e.Encode(entry)\n\n\t\treturn err == nil\n\t})\n\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tsize := b.Len()\n\telapsed := time.Since(start)\n\tl.logger.DebugContext(\n\t\tctx,\n\t\t\"serialized elements via json\",\n\t\t\"count\", bufLen,\n\t\t\"elapsed\", elapsed,\n\t\t\"size\", datasize.ByteSize(size),\n\t\t\"size_per_entry\", datasize.ByteSize(float64(size)/float64(bufLen)),\n\t\t\"time_per_entry\", elapsed/time.Duration(bufLen),\n\t)\n\n\tl.buffer.Clear()\n\tl.flushPending = false\n\n\treturn b, nil\n}\n\n// flushToFile saves the encoded log entries to the query log file.\nfunc (l *queryLog) flushToFile(ctx context.Context, b *bytes.Buffer) (err error) {\n\tl.fileWriteLock.Lock()\n\tdefer l.fileWriteLock.Unlock()\n\n\tfilename := l.logFile\n\n\tf, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating file %q: %w\", filename, err)\n\t}\n\n\tdefer func() { err = errors.WithDeferred(err, f.Close()) }()\n\n\tn, err := f.Write(b.Bytes())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"writing to file %q: %w\", filename, err)\n\t}\n\n\tl.logger.DebugContext(ctx, \"flushed to file\", \"file\", filename, \"size\", datasize.ByteSize(n))\n\n\treturn nil\n}\n\nfunc (l *queryLog) rotate(ctx context.Context) error {\n\tfrom := l.logFile\n\tto := l.logFile + \".1\"\n\n\terr := os.Rename(from, to)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tl.logger.DebugContext(ctx, \"no log to rotate\")\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to rename old file: %w\", err)\n\t}\n\n\tl.logger.DebugContext(ctx, \"renamed log file\", \"from\", from, \"to\", to)\n\n\treturn nil\n}\n\nfunc (l *queryLog) readFileFirstTimeValue(ctx context.Context) (first time.Time, err error) {\n\tvar f *os.File\n\tf, err = os.Open(l.logFile)\n\tif err != nil {\n\t\treturn time.Time{}, err\n\t}\n\n\tdefer func() { err = errors.WithDeferred(err, f.Close()) }()\n\n\tbuf := make([]byte, 512)\n\tvar r int\n\tr, err = f.Read(buf)\n\tif err != nil {\n\t\treturn time.Time{}, err\n\t}\n\n\tval := readJSONValue(string(buf[:r]), `\"T\":\"`)\n\tt, err := time.Parse(time.RFC3339Nano, val)\n\tif err != nil {\n\t\treturn time.Time{}, err\n\t}\n\n\tl.logger.DebugContext(ctx, \"oldest log entry\", \"entry_time\", val)\n\n\treturn t, nil\n}\n\nfunc (l *queryLog) periodicRotate(ctx context.Context) {\n\tdefer slogutil.RecoverAndLog(ctx, l.logger)\n\n\tl.checkAndRotate(ctx)\n\n\t// rotationCheckIvl is the period of time between checking the need for\n\t// rotating log files.  It's smaller of any available rotation interval to\n\t// increase time accuracy.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/3823.\n\tconst rotationCheckIvl = 1 * time.Hour\n\n\trotations := time.NewTicker(rotationCheckIvl)\n\tdefer rotations.Stop()\n\n\tfor range rotations.C {\n\t\tl.checkAndRotate(ctx)\n\t}\n}\n\n// checkAndRotate rotates log files if those are older than the specified\n// rotation interval.\nfunc (l *queryLog) checkAndRotate(ctx context.Context) {\n\tvar rotationIvl time.Duration\n\tfunc() {\n\t\tl.confMu.RLock()\n\t\tdefer l.confMu.RUnlock()\n\n\t\trotationIvl = l.conf.RotationIvl\n\t}()\n\n\toldest, err := l.readFileFirstTimeValue(ctx)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tl.logger.ErrorContext(ctx, \"reading oldest record for rotation\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tif rotTime, now := oldest.Add(rotationIvl), time.Now(); rotTime.After(now) {\n\t\tl.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"not rotating\",\n\t\t\t\"now\", now.Format(time.RFC3339),\n\t\t\t\"rotate_time\", rotTime.Format(time.RFC3339),\n\t\t)\n\n\t\treturn\n\t}\n\n\terr = l.rotate(ctx)\n\tif err != nil {\n\t\tl.logger.ErrorContext(ctx, \"rotating\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tl.logger.DebugContext(ctx, \"rotated successfully\")\n}\n"
  },
  {
    "path": "internal/querylog/search.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\n// client finds the client info, if any, by its ClientID and IP address,\n// optionally checking the provided cache.  It will use the IP address\n// regardless of if the IP anonymization is enabled now, because the\n// anonymization could have been disabled in the past, and client will try to\n// find those records as well.\nfunc (l *queryLog) client(clientID, ip string, cache clientCache) (c *Client, err error) {\n\tcck := clientCacheKey{clientID: clientID, ip: ip}\n\n\tvar ok bool\n\tif c, ok = cache[cck]; ok {\n\t\treturn c, nil\n\t}\n\n\tvar ids []string\n\tif clientID != \"\" {\n\t\tids = append(ids, clientID)\n\t}\n\n\tif ip != \"\" {\n\t\tids = append(ids, ip)\n\t}\n\n\tc, err = l.findClient(ids)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Cache all results, including negative ones, to prevent excessive and\n\t// expensive client searching.\n\tcache[cck] = c\n\n\treturn c, nil\n}\n\n// searchMemory looks up log records which are currently in the in-memory\n// buffer.  It optionally uses the client cache, if provided.  It also returns\n// the total amount of records in the buffer at the moment of searching.\n// l.confMu is expected to be locked.\nfunc (l *queryLog) searchMemory(\n\tctx context.Context,\n\tparams *searchParams,\n\tcache clientCache,\n) (entries []*logEntry, total int) {\n\t// Check memory size, as the buffer can contain a single log record.  See\n\t// [newQueryLog].\n\tif l.conf.MemSize == 0 {\n\t\treturn nil, 0\n\t}\n\n\tl.bufferLock.Lock()\n\tdefer l.bufferLock.Unlock()\n\n\tl.buffer.ReverseRange(func(entry *logEntry) (cont bool) {\n\t\t// A shallow clone is enough, since the only thing that this loop\n\t\t// modifies is the client field.\n\t\te := entry.shallowClone()\n\n\t\tvar err error\n\t\te.client, err = l.client(e.ClientID, e.IP.String(), cache)\n\t\tif err != nil {\n\t\t\tl.logger.ErrorContext(\n\t\t\t\tctx,\n\t\t\t\t\"enriching memory record\",\n\t\t\t\t\"at\", e.Time,\n\t\t\t\t\"client_ip\", e.IP,\n\t\t\t\t\"client_id\", e.ClientID,\n\t\t\t\tslogutil.KeyError, err,\n\t\t\t)\n\n\t\t\t// Go on and try to match anyway.\n\t\t}\n\n\t\tif params.match(e) {\n\t\t\tentries = append(entries, e)\n\t\t}\n\n\t\treturn true\n\t})\n\n\treturn entries, int(l.buffer.Len())\n}\n\n// search searches log entries in memory buffer and log file using specified\n// parameters and returns the list of entries found and the time of the oldest\n// entry.  l.confMu is expected to be locked.\nfunc (l *queryLog) search(\n\tctx context.Context,\n\tparams *searchParams,\n) (entries []*logEntry, oldest time.Time) {\n\tstart := time.Now()\n\n\tif params.limit == 0 {\n\t\treturn []*logEntry{}, time.Time{}\n\t}\n\n\tcache := clientCache{}\n\n\tmemoryEntries, bufLen := l.searchMemory(ctx, params, cache)\n\tl.logger.DebugContext(ctx, \"got entries from memory\", \"count\", len(memoryEntries))\n\n\tfileEntries, oldest, total := l.searchFiles(ctx, params, cache)\n\tl.logger.DebugContext(ctx, \"got entries from files\", \"count\", len(fileEntries))\n\n\ttotal += bufLen\n\n\ttotalLimit := params.offset + params.limit\n\n\t// now let's get a unified collection\n\tentries = append(memoryEntries, fileEntries...)\n\tif len(entries) > totalLimit {\n\t\t// remove extra records\n\t\tentries = entries[:totalLimit]\n\t}\n\n\tentries, oldest = l.finalizeSearchResults(entries, params, oldest)\n\n\tl.logger.DebugContext(\n\t\tctx,\n\t\t\"prepared data\",\n\t\t\"count\", len(entries),\n\t\t\"total\", total,\n\t\t\"older_than\", params.olderThan,\n\t\t\"elapsed\", time.Since(start),\n\t)\n\n\treturn entries, oldest\n}\n\n// finalizeSearchResults sorts entries and applies offset trimming, and updates\n// the oldest timestamp.  params must not be nil.\nfunc (l *queryLog) finalizeSearchResults(\n\tentries []*logEntry,\n\tparams *searchParams,\n\toldest time.Time,\n) (res []*logEntry, t time.Time) {\n\t// Resort entries on start time to partially mitigate query log looking\n\t// weird on the frontend.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/2293.\n\tslices.SortStableFunc(entries, func(a, b *logEntry) (res int) {\n\t\treturn -a.Time.Compare(b.Time)\n\t})\n\n\tif params.offset > 0 {\n\t\tif len(entries) > params.offset {\n\t\t\tentries = entries[params.offset:]\n\t\t} else {\n\t\t\treturn nil, time.Time{}\n\t\t}\n\t}\n\n\tif len(entries) > 0 {\n\t\t// Update oldest after merging in the memory buffer.\n\t\toldest = entries[len(entries)-1].Time\n\t}\n\n\treturn entries, oldest\n}\n\n// seekRecord changes the current position to the next record older than the\n// provided parameter.\nfunc (r *qLogReader) seekRecord(ctx context.Context, olderThan time.Time) (err error) {\n\tif olderThan.IsZero() {\n\t\treturn r.SeekStart()\n\t}\n\n\terr = r.seekTS(ctx, olderThan.UnixNano())\n\tif err == nil {\n\t\t// Read to the next record, because we only need the one that goes\n\t\t// after it.\n\t\t_, err = r.ReadNext()\n\t}\n\n\treturn err\n}\n\n// setQLogReader creates a reader with the specified files and sets the\n// position to the next record older than the provided parameter.\nfunc (l *queryLog) setQLogReader(\n\tctx context.Context,\n\tolderThan time.Time,\n) (qr *qLogReader, err error) {\n\tfiles := []string{\n\t\tl.logFile + \".1\",\n\t\tl.logFile,\n\t}\n\n\tr, err := newQLogReader(ctx, l.logger, files)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"opening qlog reader: %w\", err)\n\t}\n\n\terr = r.seekRecord(ctx, olderThan)\n\tif err != nil {\n\t\tdefer func() { err = errors.WithDeferred(err, r.Close()) }()\n\t\tl.logger.DebugContext(ctx, \"cannot seek\", \"older_than\", olderThan, slogutil.KeyError, err)\n\n\t\treturn nil, nil\n\t}\n\n\treturn r, nil\n}\n\n// readEntries reads entries from the reader to totalLimit.  By default, we do\n// not scan more than maxFileScanEntries at once.  The idea is to make search\n// calls faster so that the UI could handle it and show something quicker.\n// This behavior can be overridden if maxFileScanEntries is set to 0.\nfunc (l *queryLog) readEntries(\n\tctx context.Context,\n\tr *qLogReader,\n\tparams *searchParams,\n\tcache clientCache,\n\ttotalLimit int,\n) (entries []*logEntry, oldestNano int64, total int) {\n\tfor total < params.maxFileScanEntries || params.maxFileScanEntries <= 0 {\n\t\tent, ts, rErr := l.readNextEntry(ctx, r, params, cache)\n\t\tif rErr == io.EOF {\n\t\t\toldestNano = 0\n\n\t\t\tbreak\n\t\t} else if rErr != nil {\n\t\t\tl.logger.ErrorContext(ctx, \"reading next entry\", slogutil.KeyError, rErr)\n\t\t}\n\n\t\toldestNano = ts\n\t\ttotal++\n\n\t\tif ent == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tentries = append(entries, ent)\n\t\tif len(entries) == totalLimit {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn entries, oldestNano, total\n}\n\n// searchFiles looks up log records from all log files.  It optionally uses the\n// client cache, if provided.  searchFiles does not scan more than\n// maxFileScanEntries so callers may need to call it several times to get all\n// the results.  oldest and total are the time of the oldest processed entry\n// and the total number of processed entries, including discarded ones,\n// correspondingly.\nfunc (l *queryLog) searchFiles(\n\tctx context.Context,\n\tparams *searchParams,\n\tcache clientCache,\n) (entries []*logEntry, oldest time.Time, total int) {\n\tr, err := l.setQLogReader(ctx, params.olderThan)\n\tif err != nil {\n\t\tl.logger.ErrorContext(ctx, \"searching files\", slogutil.KeyError, err)\n\t}\n\n\tif r == nil {\n\t\treturn entries, oldest, 0\n\t}\n\n\tdefer func() {\n\t\tif closeErr := r.Close(); closeErr != nil {\n\t\t\tl.logger.ErrorContext(ctx, \"closing files\", slogutil.KeyError, closeErr)\n\t\t}\n\t}()\n\n\ttotalLimit := params.offset + params.limit\n\tentries, oldestNano, total := l.readEntries(ctx, r, params, cache, totalLimit)\n\tif oldestNano != 0 {\n\t\toldest = time.Unix(0, oldestNano)\n\t}\n\n\treturn entries, oldest, total\n}\n\n// quickMatchClientFinder is a wrapper around the usual client finding function\n// to make it easier to use with quick matches.\ntype quickMatchClientFinder struct {\n\tclient func(clientID, ip string, cache clientCache) (c *Client, err error)\n\tcache  clientCache\n}\n\n// findClient is a method that can be used as a quickMatchClientFinder.\nfunc (f quickMatchClientFinder) findClient(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tclientID string,\n\tip string,\n) (c *Client) {\n\tvar err error\n\tc, err = f.client(clientID, ip, f.cache)\n\tif err != nil {\n\t\tlogger.ErrorContext(\n\t\t\tctx,\n\t\t\t\"enriching file record for quick search\",\n\t\t\t\"client_ip\", ip,\n\t\t\t\"client_id\", clientID,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\t}\n\n\treturn c\n}\n\n// readNextEntry reads the next log entry and checks if it matches the search\n// criteria.  It optionally uses the client cache, if provided.  e is nil if\n// the entry doesn't match the search criteria.  ts is the timestamp of the\n// processed entry.\nfunc (l *queryLog) readNextEntry(\n\tctx context.Context,\n\tr *qLogReader,\n\tparams *searchParams,\n\tcache clientCache,\n) (e *logEntry, ts int64, err error) {\n\tvar line string\n\tline, err = r.ReadNext()\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tclientFinder := quickMatchClientFinder{\n\t\tclient: l.client,\n\t\tcache:  cache,\n\t}\n\n\tif !params.quickMatch(ctx, l.logger, line, clientFinder.findClient) {\n\t\tts = readQLogTimestamp(ctx, l.logger, line)\n\n\t\treturn nil, ts, nil\n\t}\n\n\te = &logEntry{}\n\tl.decodeLogEntry(ctx, e, line)\n\n\tif l.isIgnored(e.QHost) {\n\t\treturn nil, ts, nil\n\t}\n\n\te.client, err = l.client(e.ClientID, e.IP.String(), cache)\n\tif err != nil {\n\t\tl.logger.ErrorContext(\n\t\t\tctx,\n\t\t\t\"enriching file record\",\n\t\t\t\"at\", e.Time,\n\t\t\t\"client_ip\", e.IP,\n\t\t\t\"client_id\", e.ClientID,\n\t\t\tslogutil.KeyError, err,\n\t\t)\n\n\t\t// Go on and try to match anyway.\n\t}\n\n\tif e.client != nil && e.client.IgnoreQueryLog {\n\t\treturn nil, ts, nil\n\t}\n\n\tts = e.Time.UnixNano()\n\tif !params.match(e) {\n\t\treturn nil, ts, nil\n\t}\n\n\treturn e, ts, nil\n}\n"
  },
  {
    "path": "internal/querylog/search_internal_test.go",
    "content": "package querylog\n\nimport (\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestQueryLog_Search_findClient(t *testing.T) {\n\tconst knownClientID = \"client-1\"\n\tconst knownClientName = \"Known Client 1\"\n\tconst unknownClientID = \"client-2\"\n\n\tknownClient := &Client{\n\t\tName: knownClientName,\n\t}\n\n\tfindClientCalls := 0\n\tfindClient := func(ids []string) (c *Client, _ error) {\n\t\tdefer func() { findClientCalls++ }()\n\n\t\tif len(ids) == 0 {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tif ids[0] == knownClientID {\n\t\t\treturn knownClient, nil\n\t\t}\n\n\t\treturn nil, nil\n\t}\n\n\tl, err := newQueryLog(Config{\n\t\tLogger:            slogutil.NewDiscardLogger(),\n\t\tFindClient:        findClient,\n\t\tBaseDir:           t.TempDir(),\n\t\tRotationIvl:       timeutil.Day,\n\t\tMemSize:           100,\n\t\tEnabled:           true,\n\t\tFileEnabled:       true,\n\t\tAnonymizeClientIP: false,\n\t})\n\trequire.NoError(t, err)\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\ttestutil.CleanupAndRequireSuccess(t, func() (err error) {\n\t\treturn l.Shutdown(ctx)\n\t})\n\n\tq := &dns.Msg{\n\t\tQuestion: []dns.Question{{\n\t\t\tName: \"example.com\",\n\t\t}},\n\t}\n\n\tl.Add(&AddParams{\n\t\tQuestion: q,\n\t\tClientID: knownClientID,\n\t\tClientIP: net.IP{1, 2, 3, 4},\n\t})\n\n\t// Add the same thing again to test the cache.\n\tl.Add(&AddParams{\n\t\tQuestion: q,\n\t\tClientID: knownClientID,\n\t\tClientIP: net.IP{1, 2, 3, 4},\n\t})\n\n\tl.Add(&AddParams{\n\t\tQuestion: q,\n\t\tClientID: unknownClientID,\n\t\tClientIP: net.IP{1, 2, 3, 5},\n\t})\n\n\tsp := &searchParams{\n\t\t// Add some time to the \"current\" one to protect against\n\t\t// low-resolution timers on some Windows machines.\n\t\t//\n\t\t// TODO(a.garipov): Use some kind of timeSource interface\n\t\t// instead of relying on time.Now() in tests.\n\t\tolderThan: time.Now().Add(10 * time.Second),\n\t\tlimit:     3,\n\t}\n\tentries, _ := l.search(ctx, sp)\n\tassert.Equal(t, 2, findClientCalls)\n\n\trequire.Len(t, entries, 3)\n\n\tassert.Nil(t, entries[0].client)\n\n\tgotClient := entries[2].client\n\trequire.NotNil(t, gotClient)\n\n\tassert.Equal(t, knownClientName, gotClient.Name)\n}\n"
  },
  {
    "path": "internal/querylog/searchcriterion.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/filtering\"\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n)\n\ntype criterionType int\n\nconst (\n\t// ctTerm is for searching by the domain name, the client's IP address,\n\t// the client's ID or the client's name.  The domain name search\n\t// supports IDNAs.\n\tctTerm criterionType = iota\n\t// ctFilteringStatus is for searching by the filtering status.\n\t//\n\t// See (*searchCriterion).ctFilteringStatusCase for details.\n\tctFilteringStatus\n)\n\nconst (\n\tfilteringStatusAll      = \"all\"\n\tfilteringStatusFiltered = \"filtered\" // all kinds of filtering\n\n\tfilteringStatusBlocked             = \"blocked\"              // blocked or blocked services\n\tfilteringStatusBlockedService      = \"blocked_services\"     // blocked\n\tfilteringStatusBlockedSafebrowsing = \"blocked_safebrowsing\" // blocked by safebrowsing\n\tfilteringStatusBlockedParental     = \"blocked_parental\"     // blocked by parental control\n\tfilteringStatusWhitelisted         = \"whitelisted\"          // whitelisted\n\tfilteringStatusRewritten           = \"rewritten\"            // all kinds of rewrites\n\tfilteringStatusSafeSearch          = \"safe_search\"          // enforced safe search\n\tfilteringStatusProcessed           = \"processed\"            // not blocked, not white-listed entries\n)\n\n// filteringStatusValues -- array with all possible filteringStatus values\nvar filteringStatusValues = []string{\n\tfilteringStatusAll, filteringStatusFiltered, filteringStatusBlocked,\n\tfilteringStatusBlockedService, filteringStatusBlockedSafebrowsing, filteringStatusBlockedParental,\n\tfilteringStatusWhitelisted, filteringStatusRewritten, filteringStatusSafeSearch,\n\tfilteringStatusProcessed,\n}\n\n// searchCriterion is a search criterion that is used to match a record.\ntype searchCriterion struct {\n\tvalue         string\n\tasciiVal      string\n\tcriterionType criterionType\n\t// strict, if true, means that the criterion must be applied to the\n\t// whole value rather than the part of it.  That is, equality and not\n\t// containment.\n\tstrict bool\n}\n\nfunc ctDomainOrClientCaseStrict(\n\tterm string,\n\tasciiTerm string,\n\tclientID string,\n\tname string,\n\thost string,\n\tip string,\n) (ok bool) {\n\treturn strings.EqualFold(host, term) ||\n\t\t(asciiTerm != \"\" && strings.EqualFold(host, asciiTerm)) ||\n\t\tstrings.EqualFold(clientID, term) ||\n\t\tstrings.EqualFold(ip, term) ||\n\t\tstrings.EqualFold(name, term)\n}\n\nfunc ctDomainOrClientCaseNonStrict(\n\tterm string,\n\tasciiTerm string,\n\tclientID string,\n\tname string,\n\thost string,\n\tip string,\n) (ok bool) {\n\treturn stringutil.ContainsFold(clientID, term) ||\n\t\tstringutil.ContainsFold(host, term) ||\n\t\t(asciiTerm != \"\" && stringutil.ContainsFold(host, asciiTerm)) ||\n\t\tstringutil.ContainsFold(ip, term) ||\n\t\tstringutil.ContainsFold(name, term)\n}\n\n// quickMatch quickly checks if the line matches the given search criterion.\n// It returns false if the like doesn't match.  This method is only here for\n// optimization purposes.\nfunc (c *searchCriterion) quickMatch(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tline string,\n\tfindClient quickMatchClientFunc,\n) (ok bool) {\n\tswitch c.criterionType {\n\tcase ctTerm:\n\t\thost := readJSONValue(line, `\"QH\":\"`)\n\t\tip := readJSONValue(line, `\"IP\":\"`)\n\t\tclientID := readJSONValue(line, `\"CID\":\"`)\n\n\t\tvar name string\n\t\tif cli := findClient(ctx, logger, clientID, ip); cli != nil {\n\t\t\tname = cli.Name\n\t\t}\n\n\t\tif c.strict {\n\t\t\treturn ctDomainOrClientCaseStrict(c.value, c.asciiVal, clientID, name, host, ip)\n\t\t}\n\n\t\treturn ctDomainOrClientCaseNonStrict(c.value, c.asciiVal, clientID, name, host, ip)\n\tcase ctFilteringStatus:\n\t\t// Go on, as we currently don't do quick matches against\n\t\t// filtering statuses.\n\t\treturn true\n\tdefault:\n\t\treturn true\n\t}\n}\n\n// match checks if the log entry matches this search criterion.\nfunc (c *searchCriterion) match(entry *logEntry) bool {\n\tswitch c.criterionType {\n\tcase ctTerm:\n\t\treturn c.ctDomainOrClientCase(entry)\n\tcase ctFilteringStatus:\n\t\treturn c.ctFilteringStatusCase(entry.Result.Reason, entry.Result.IsFiltered)\n\t}\n\n\treturn false\n}\n\nfunc (c *searchCriterion) ctDomainOrClientCase(e *logEntry) bool {\n\tclientID := e.ClientID\n\thost := e.QHost\n\n\tvar name string\n\tif e.client != nil {\n\t\tname = e.client.Name\n\t}\n\n\tip := e.IP.String()\n\tif c.strict {\n\t\treturn ctDomainOrClientCaseStrict(c.value, c.asciiVal, clientID, name, host, ip)\n\t}\n\n\treturn ctDomainOrClientCaseNonStrict(c.value, c.asciiVal, clientID, name, host, ip)\n}\n\n// ctFilteringStatusCase returns true if the result matches the value.\nfunc (c *searchCriterion) ctFilteringStatusCase(\n\treason filtering.Reason,\n\tisFiltered bool,\n) (matched bool) {\n\tswitch c.value {\n\tcase filteringStatusAll:\n\t\treturn true\n\tcase filteringStatusFiltered:\n\t\treturn isFiltered || reason.In(\n\t\t\tfiltering.NotFilteredAllowList,\n\t\t\tfiltering.Rewritten,\n\t\t\tfiltering.RewrittenAutoHosts,\n\t\t\tfiltering.RewrittenRule,\n\t\t)\n\tcase\n\t\tfilteringStatusBlocked,\n\t\tfilteringStatusBlockedParental,\n\t\tfilteringStatusBlockedSafebrowsing,\n\t\tfilteringStatusBlockedService,\n\t\tfilteringStatusSafeSearch:\n\t\treturn isFiltered && c.isFilteredWithReason(reason)\n\tcase filteringStatusWhitelisted:\n\t\treturn reason == filtering.NotFilteredAllowList\n\tcase filteringStatusRewritten:\n\t\treturn reason.In(\n\t\t\tfiltering.Rewritten,\n\t\t\tfiltering.RewrittenAutoHosts,\n\t\t\tfiltering.RewrittenRule,\n\t\t)\n\tcase filteringStatusProcessed:\n\t\treturn !reason.In(\n\t\t\tfiltering.FilteredBlockList,\n\t\t\tfiltering.FilteredBlockedService,\n\t\t\tfiltering.NotFilteredAllowList,\n\t\t)\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// isFilteredWithReason returns true if reason matches the criterion value.\n// c.value must be one of:\n//\n//   - filteringStatusBlocked\n//   - filteringStatusBlockedParental\n//   - filteringStatusBlockedSafebrowsing\n//   - filteringStatusBlockedService\n//   - filteringStatusSafeSearch\nfunc (c *searchCriterion) isFilteredWithReason(reason filtering.Reason) (matched bool) {\n\tswitch c.value {\n\tcase filteringStatusBlocked:\n\t\treturn reason.In(filtering.FilteredBlockList, filtering.FilteredBlockedService)\n\tcase filteringStatusBlockedParental:\n\t\treturn reason == filtering.FilteredParental\n\tcase filteringStatusBlockedSafebrowsing:\n\t\treturn reason == filtering.FilteredSafeBrowsing\n\tcase filteringStatusBlockedService:\n\t\treturn reason == filtering.FilteredBlockedService\n\tcase filteringStatusSafeSearch:\n\t\treturn reason == filtering.FilteredSafeSearch\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unexpected value %q\", c.value))\n\t}\n}\n"
  },
  {
    "path": "internal/querylog/searchparams.go",
    "content": "package querylog\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"time\"\n)\n\n// searchParams represent the search query sent by the client.\ntype searchParams struct {\n\t// olderThen represents a parameter for entries that are older than this\n\t// parameter value.  If not set, disregard it and return any value.\n\tolderThan time.Time\n\n\t// searchCriteria is a list of search criteria that we use to get filter\n\t// results.\n\tsearchCriteria []searchCriterion\n\n\t// offset for the search.\n\toffset int\n\n\t// limit the number of records returned.\n\tlimit int\n\n\t// maxFileScanEntries is a maximum of log entries to scan in query log\n\t// files.  If not set, then no limit.\n\tmaxFileScanEntries int\n}\n\n// newSearchParams - creates an empty instance of searchParams\nfunc newSearchParams() *searchParams {\n\treturn &searchParams{\n\t\t// default max log entries to return\n\t\tlimit: 500,\n\n\t\t// by default, we scan up to 50k entries at once\n\t\tmaxFileScanEntries: 50000,\n\t}\n}\n\n// quickMatchClientFunc is a simplified client finder for quick matches.\ntype quickMatchClientFunc = func(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tclientID, ip string,\n) (c *Client)\n\n// quickMatch quickly checks if the line matches the given search parameters.\n// It returns false if the line doesn't match.  This method is only here for\n// optimization purposes.\nfunc (s *searchParams) quickMatch(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\tline string,\n\tfindClient quickMatchClientFunc,\n) (ok bool) {\n\tfor _, c := range s.searchCriteria {\n\t\tif !c.quickMatch(ctx, logger, line, findClient) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// match - checks if the logEntry matches the searchParams\nfunc (s *searchParams) match(entry *logEntry) bool {\n\tif !s.olderThan.IsZero() && !entry.Time.Before(s.olderThan) {\n\t\t// Ignore entries newer than what was requested\n\t\treturn false\n\t}\n\n\tfor _, c := range s.searchCriteria {\n\t\tif !c.match(entry) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "internal/rdns/rdns.go",
    "content": "// Package rdns processes reverse DNS lookup queries.\npackage rdns\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/bluele/gcache\"\n)\n\n// Interface processes rDNS queries.\ntype Interface interface {\n\t// Process makes rDNS request and returns domain name.  changed indicates\n\t// that domain name was updated since last request.\n\tProcess(ctx context.Context, ip netip.Addr) (host string, changed bool)\n}\n\n// Empty is an empty [Interface] implementation which does nothing.\ntype Empty struct{}\n\n// type check\nvar _ Interface = (*Empty)(nil)\n\n// Process implements the [Interface] interface for Empty.\nfunc (Empty) Process(_ context.Context, _ netip.Addr) (host string, changed bool) {\n\treturn \"\", false\n}\n\n// Exchanger is a resolver for clients' addresses.\ntype Exchanger interface {\n\t// Exchange tries to resolve the ip in a suitable way, i.e. either as local\n\t// or as external.\n\tExchange(ctx context.Context, ip netip.Addr) (host string, ttl time.Duration, err error)\n}\n\n// Config is the configuration structure for Default.\ntype Config struct {\n\t// Logger is used for logging the operation of the reverse DNS lookup\n\t// queries.  It must not be nil.\n\tLogger *slog.Logger\n\n\t// Exchanger resolves IP addresses to domain names.\n\tExchanger Exchanger\n\n\t// CacheSize is the maximum size of the cache.  It must be greater than\n\t// zero.\n\tCacheSize int\n\n\t// CacheTTL is the Time to Live duration for cached IP addresses.\n\tCacheTTL time.Duration\n}\n\n// Default is the default rDNS query processor.\ntype Default struct {\n\t// logger is used for logging the operation of the reverse DNS lookup\n\t// queries.  It must not be nil.\n\tlogger *slog.Logger\n\n\t// cache is the cache containing IP addresses of clients.  An active IP\n\t// address is resolved once again after it expires.  If IP address couldn't\n\t// be resolved, it stays here for some time to prevent further attempts to\n\t// resolve the same IP.\n\tcache gcache.Cache\n\n\t// exchanger resolves IP addresses to domain names.\n\texchanger Exchanger\n\n\t// cacheTTL is the Time to Live duration for cached IP addresses.\n\tcacheTTL time.Duration\n}\n\n// New returns a new default rDNS query processor.  conf must not be nil.\nfunc New(conf *Config) (r *Default) {\n\treturn &Default{\n\t\tlogger:    conf.Logger,\n\t\tcache:     gcache.New(conf.CacheSize).LRU().Build(),\n\t\texchanger: conf.Exchanger,\n\t\tcacheTTL:  conf.CacheTTL,\n\t}\n}\n\n// type check\nvar _ Interface = (*Default)(nil)\n\n// Process implements the [Interface] interface for Default.\nfunc (r *Default) Process(ctx context.Context, ip netip.Addr) (host string, changed bool) {\n\tfromCache, expired := r.findInCache(ctx, ip)\n\tif !expired {\n\t\treturn fromCache, false\n\t}\n\n\thost, ttl, err := r.exchanger.Exchange(ctx, ip)\n\tif err != nil {\n\t\tr.logger.DebugContext(ctx, \"resolving\", \"ip\", ip, slogutil.KeyError, err)\n\t}\n\n\tttl = max(ttl, r.cacheTTL)\n\n\titem := &cacheItem{\n\t\texpiry: time.Now().Add(ttl),\n\t\thost:   host,\n\t}\n\n\terr = r.cache.Set(ip, item)\n\tif err != nil {\n\t\tr.logger.DebugContext(ctx, \"adding item to cache\", \"key\", ip, slogutil.KeyError, err)\n\t}\n\n\t// TODO(e.burkov):  The name doesn't change if it's neither stored in cache\n\t// nor resolved successfully.  Is it correct?\n\treturn host, fromCache == \"\" || host != fromCache\n}\n\n// findInCache finds domain name in the cache.  expired is true if host is not\n// valid anymore.\nfunc (r *Default) findInCache(ctx context.Context, ip netip.Addr) (host string, expired bool) {\n\tval, err := r.cache.Get(ip)\n\tif err != nil {\n\t\tif !errors.Is(err, gcache.KeyNotFoundError) {\n\t\t\tr.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"retrieving item from cache\",\n\t\t\t\t\"key\", ip,\n\t\t\t\tslogutil.KeyError, err,\n\t\t\t)\n\t\t}\n\n\t\treturn \"\", true\n\t}\n\n\titem := val.(*cacheItem)\n\n\treturn item.host, time.Now().After(item.expiry)\n}\n\n// cacheItem represents an item that we will store in the cache.\ntype cacheItem struct {\n\t// expiry is the time when cacheItem will expire.\n\texpiry time.Time\n\n\t// host is the domain name of a runtime client.\n\thost string\n}\n"
  },
  {
    "path": "internal/rdns/rdns_test.go",
    "content": "package rdns_test\n\nimport (\n\t\"context\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/rdns\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is a common timeout for tests and contexts.\nconst testTimeout = 1 * time.Second\n\nfunc TestDefault_Process(t *testing.T) {\n\tip1 := netip.MustParseAddr(\"1.2.3.4\")\n\trevAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())\n\trequire.NoError(t, err)\n\n\tip2 := netip.MustParseAddr(\"4.3.2.1\")\n\trevAddr2, err := netutil.IPToReversedAddr(ip2.AsSlice())\n\trequire.NoError(t, err)\n\n\tlocalIP := netip.MustParseAddr(\"192.168.0.1\")\n\tlocalRevAddr1, err := netutil.IPToReversedAddr(localIP.AsSlice())\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname string\n\t\taddr netip.Addr\n\t\twant string\n\t}{{\n\t\tname: \"first\",\n\t\taddr: ip1,\n\t\twant: revAddr1,\n\t}, {\n\t\tname: \"second\",\n\t\taddr: ip2,\n\t\twant: revAddr2,\n\t}, {\n\t\tname: \"empty\",\n\t\taddr: netip.MustParseAddr(\"0.0.0.0\"),\n\t\twant: \"\",\n\t}, {\n\t\tname: \"private\",\n\t\taddr: localIP,\n\t\twant: localRevAddr1,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\thit := 0\n\t\t\tonExchange := func(\n\t\t\t\t_ context.Context,\n\t\t\t\tip netip.Addr,\n\t\t\t) (host string, ttl time.Duration, err error) {\n\t\t\t\thit++\n\n\t\t\t\tswitch ip {\n\t\t\t\tcase ip1:\n\t\t\t\t\treturn revAddr1, time.Hour, nil\n\t\t\t\tcase ip2:\n\t\t\t\t\treturn revAddr2, time.Hour, nil\n\t\t\t\tcase localIP:\n\t\t\t\t\treturn localRevAddr1, time.Hour, nil\n\t\t\t\tdefault:\n\t\t\t\t\treturn \"\", time.Hour, nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tr := rdns.New(&rdns.Config{\n\t\t\t\tCacheSize: 100,\n\t\t\t\tCacheTTL:  time.Hour,\n\t\t\t\tExchanger: &aghtest.Exchanger{OnExchange: onExchange},\n\t\t\t})\n\n\t\t\tgot, changed := r.Process(testutil.ContextWithTimeout(t, testTimeout), tc.addr)\n\t\t\trequire.True(t, changed)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t\tassert.Equal(t, 1, hit)\n\n\t\t\t// From cache.\n\t\t\tgot, changed = r.Process(testutil.ContextWithTimeout(t, testTimeout), tc.addr)\n\t\t\trequire.False(t, changed)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t\tassert.Equal(t, 1, hit)\n\t\t})\n\t}\n\n\tt.Run(\"zero_ttl\", func(t *testing.T) {\n\t\tconst cacheTTL = time.Second / 2\n\n\t\tzeroTTLExchanger := &aghtest.Exchanger{\n\t\t\tOnExchange: func(\n\t\t\t\t_ context.Context,\n\t\t\t\tip netip.Addr,\n\t\t\t) (host string, ttl time.Duration, err error) {\n\t\t\t\treturn revAddr1, 0, nil\n\t\t\t},\n\t\t}\n\n\t\tr := rdns.New(&rdns.Config{\n\t\t\tCacheSize: 1,\n\t\t\tCacheTTL:  cacheTTL,\n\t\t\tExchanger: zeroTTLExchanger,\n\t\t})\n\n\t\tgot, changed := r.Process(testutil.ContextWithTimeout(t, testTimeout), ip1)\n\t\trequire.True(t, changed)\n\t\tassert.Equal(t, revAddr1, got)\n\n\t\tzeroTTLExchanger.OnExchange = func(\n\t\t\t_ context.Context,\n\t\t\tip netip.Addr,\n\t\t) (host string, ttl time.Duration, err error) {\n\t\t\treturn revAddr2, time.Hour, nil\n\t\t}\n\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\trequire.EventuallyWithT(t, func(t *assert.CollectT) {\n\t\t\tgot, changed = r.Process(ctx, ip1)\n\t\t\tassert.True(t, changed)\n\t\t\tassert.Equal(t, revAddr2, got)\n\t\t}, 2*cacheTTL, time.Millisecond*100)\n\n\t\tassert.Never(t, func() (changed bool) {\n\t\t\t_, changed = r.Process(testutil.ContextWithTimeout(t, testTimeout), ip1)\n\n\t\t\treturn changed\n\t\t}, 2*cacheTTL, time.Millisecond*100)\n\t})\n}\n"
  },
  {
    "path": "internal/schedule/schedule.go",
    "content": "// Package schedule provides types for scheduling.\npackage schedule\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\tyaml \"go.yaml.in/yaml/v4\"\n)\n\n// Weekly is a schedule for one week.  Each day of the week has one range with\n// a beginning and an end.\ntype Weekly struct {\n\t// location is used to calculate the offsets of the day ranges.\n\tlocation *time.Location\n\n\t// days are the day ranges of this schedule.  The indexes of this array are\n\t// the [time.Weekday] values.\n\tdays [7]dayRange\n}\n\n// EmptyWeekly creates empty weekly schedule with local time zone.\nfunc EmptyWeekly() (w *Weekly) {\n\treturn &Weekly{\n\t\tlocation: time.Local,\n\t}\n}\n\n// FullWeekly creates full weekly schedule with local time zone.\n//\n// TODO(s.chzhen):  Consider moving into tests.\nfunc FullWeekly() (w *Weekly) {\n\tfullDay := dayRange{start: 0, end: maxDayRange}\n\n\treturn &Weekly{\n\t\tlocation: time.Local,\n\t\tdays: [7]dayRange{\n\t\t\ttime.Sunday:    fullDay,\n\t\t\ttime.Monday:    fullDay,\n\t\t\ttime.Tuesday:   fullDay,\n\t\t\ttime.Wednesday: fullDay,\n\t\t\ttime.Thursday:  fullDay,\n\t\t\ttime.Friday:    fullDay,\n\t\t\ttime.Saturday:  fullDay,\n\t\t},\n\t}\n}\n\n// Clone returns a deep copy of a weekly.\nfunc (w *Weekly) Clone() (c *Weekly) {\n\tif w == nil {\n\t\treturn nil\n\t}\n\n\t// NOTE:  Do not use time.LoadLocation, because the results will be\n\t// different on time zone database update.\n\treturn &Weekly{\n\t\tlocation: w.location,\n\t\tdays:     w.days,\n\t}\n}\n\n// Contains returns true if t is within the corresponding day range of the\n// schedule in the schedule's time zone.\nfunc (w *Weekly) Contains(t time.Time) (ok bool) {\n\tt = t.In(w.location)\n\twd := t.Weekday()\n\tdr := w.days[wd]\n\n\t// Calculate the offset of the day range.\n\t//\n\t// NOTE: Do not use [time.Truncate] since it requires UTC time zone.\n\ty, m, d := t.Date()\n\tday := time.Date(y, m, d, 0, 0, 0, 0, w.location)\n\toffset := t.Sub(day)\n\n\treturn dr.contains(offset)\n}\n\n// type check\nvar _ json.Unmarshaler = (*Weekly)(nil)\n\n// UnmarshalJSON implements the [json.Unmarshaler] interface for *Weekly.\nfunc (w *Weekly) UnmarshalJSON(data []byte) (err error) {\n\tconf := &weeklyConfigJSON{}\n\terr = json.Unmarshal(data, conf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tweekly := Weekly{}\n\n\tweekly.location, err = time.LoadLocation(conf.TimeZone)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdays := []*dayConfigJSON{\n\t\ttime.Sunday:    conf.Sunday,\n\t\ttime.Monday:    conf.Monday,\n\t\ttime.Tuesday:   conf.Tuesday,\n\t\ttime.Wednesday: conf.Wednesday,\n\t\ttime.Thursday:  conf.Thursday,\n\t\ttime.Friday:    conf.Friday,\n\t\ttime.Saturday:  conf.Saturday,\n\t}\n\tfor i, d := range days {\n\t\tvar r dayRange\n\n\t\tif d != nil {\n\t\t\tr = dayRange{\n\t\t\t\tstart: time.Duration(d.Start),\n\t\t\t\tend:   time.Duration(d.End),\n\t\t\t}\n\t\t}\n\n\t\terr = w.validate(r)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"weekday %s: %w\", time.Weekday(i), err)\n\t\t}\n\n\t\tweekly.days[i] = r\n\t}\n\n\t*w = weekly\n\n\treturn nil\n}\n\n// type check\nvar _ yaml.Unmarshaler = (*Weekly)(nil)\n\n// UnmarshalYAML implements the [yaml.Unmarshaler] interface for *Weekly.\nfunc (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {\n\tconf := &weeklyConfigYAML{}\n\n\terr = value.Decode(conf)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tweekly := Weekly{}\n\n\tweekly.location, err = time.LoadLocation(conf.TimeZone)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tdays := []dayConfigYAML{\n\t\ttime.Sunday:    conf.Sunday,\n\t\ttime.Monday:    conf.Monday,\n\t\ttime.Tuesday:   conf.Tuesday,\n\t\ttime.Wednesday: conf.Wednesday,\n\t\ttime.Thursday:  conf.Thursday,\n\t\ttime.Friday:    conf.Friday,\n\t\ttime.Saturday:  conf.Saturday,\n\t}\n\tfor i, d := range days {\n\t\tr := dayRange{\n\t\t\tstart: time.Duration(d.Start),\n\t\t\tend:   time.Duration(d.End),\n\t\t}\n\n\t\terr = w.validate(r)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"weekday %s: %w\", time.Weekday(i), err)\n\t\t}\n\n\t\tweekly.days[i] = r\n\t}\n\n\t*w = weekly\n\n\treturn nil\n}\n\n// weeklyConfigYAML is the YAML configuration structure of Weekly.\ntype weeklyConfigYAML struct {\n\t// TimeZone is the local time zone.\n\tTimeZone string `yaml:\"time_zone\"`\n\n\t// Days of the week.\n\n\tSunday    dayConfigYAML `yaml:\"sun,omitempty\"`\n\tMonday    dayConfigYAML `yaml:\"mon,omitempty\"`\n\tTuesday   dayConfigYAML `yaml:\"tue,omitempty\"`\n\tWednesday dayConfigYAML `yaml:\"wed,omitempty\"`\n\tThursday  dayConfigYAML `yaml:\"thu,omitempty\"`\n\tFriday    dayConfigYAML `yaml:\"fri,omitempty\"`\n\tSaturday  dayConfigYAML `yaml:\"sat,omitempty\"`\n}\n\n// dayConfigYAML is the YAML configuration structure of dayRange.\ntype dayConfigYAML struct {\n\tStart timeutil.Duration `yaml:\"start\"`\n\tEnd   timeutil.Duration `yaml:\"end\"`\n}\n\n// maxDayRange is the maximum value for day range end.\nconst maxDayRange = 24 * time.Hour\n\n// validate returns the day range rounding errors, if any.\nfunc (w *Weekly) validate(r dayRange) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"bad day range: %w\") }()\n\n\terr = r.validate()\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tstart := r.start.Truncate(time.Minute)\n\tend := r.end.Truncate(time.Minute)\n\n\tswitch {\n\tcase start != r.start:\n\t\treturn fmt.Errorf(\"start %s isn't rounded to minutes\", r.start)\n\tcase end != r.end:\n\t\treturn fmt.Errorf(\"end %s isn't rounded to minutes\", r.end)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// type check\nvar _ json.Marshaler = (*Weekly)(nil)\n\n// MarshalJSON implements the [json.Marshaler] interface for *Weekly.\nfunc (w *Weekly) MarshalJSON() (data []byte, err error) {\n\tc := &weeklyConfigJSON{\n\t\tTimeZone:  w.location.String(),\n\t\tSunday:    w.days[time.Sunday].toDayConfigJSON(),\n\t\tMonday:    w.days[time.Monday].toDayConfigJSON(),\n\t\tTuesday:   w.days[time.Tuesday].toDayConfigJSON(),\n\t\tWednesday: w.days[time.Wednesday].toDayConfigJSON(),\n\t\tThursday:  w.days[time.Thursday].toDayConfigJSON(),\n\t\tFriday:    w.days[time.Friday].toDayConfigJSON(),\n\t\tSaturday:  w.days[time.Saturday].toDayConfigJSON(),\n\t}\n\n\treturn json.Marshal(c)\n}\n\n// type check\nvar _ yaml.Marshaler = (*Weekly)(nil)\n\n// MarshalYAML implements the [yaml.Marshaler] interface for *Weekly.\nfunc (w *Weekly) MarshalYAML() (v any, err error) {\n\treturn weeklyConfigYAML{\n\t\tTimeZone: w.location.String(),\n\t\tSunday: dayConfigYAML{\n\t\t\tStart: timeutil.Duration(w.days[time.Sunday].start),\n\t\t\tEnd:   timeutil.Duration(w.days[time.Sunday].end),\n\t\t},\n\t\tMonday: dayConfigYAML{\n\t\t\tStart: timeutil.Duration(w.days[time.Monday].start),\n\t\t\tEnd:   timeutil.Duration(w.days[time.Monday].end),\n\t\t},\n\t\tTuesday: dayConfigYAML{\n\t\t\tStart: timeutil.Duration(w.days[time.Tuesday].start),\n\t\t\tEnd:   timeutil.Duration(w.days[time.Tuesday].end),\n\t\t},\n\t\tWednesday: dayConfigYAML{\n\t\t\tStart: timeutil.Duration(w.days[time.Wednesday].start),\n\t\t\tEnd:   timeutil.Duration(w.days[time.Wednesday].end),\n\t\t},\n\t\tThursday: dayConfigYAML{\n\t\t\tStart: timeutil.Duration(w.days[time.Thursday].start),\n\t\t\tEnd:   timeutil.Duration(w.days[time.Thursday].end),\n\t\t},\n\t\tFriday: dayConfigYAML{\n\t\t\tStart: timeutil.Duration(w.days[time.Friday].start),\n\t\t\tEnd:   timeutil.Duration(w.days[time.Friday].end),\n\t\t},\n\t\tSaturday: dayConfigYAML{\n\t\t\tStart: timeutil.Duration(w.days[time.Saturday].start),\n\t\t\tEnd:   timeutil.Duration(w.days[time.Saturday].end),\n\t\t},\n\t}, nil\n}\n\n// dayRange represents a single interval within a day.  The interval begins at\n// start and ends before end.  That is, it contains a time point T if start <=\n// T < end.\ntype dayRange struct {\n\t// start is an offset from the beginning of the day.  It must be greater\n\t// than or equal to zero and less than 24h.\n\tstart time.Duration\n\n\t// end is an offset from the beginning of the day.  It must be greater than\n\t// or equal to zero and less than or equal to 24h.\n\tend time.Duration\n}\n\n// validate returns the day range validation errors, if any.\nfunc (r dayRange) validate() (err error) {\n\tswitch {\n\tcase r == dayRange{}:\n\t\treturn nil\n\tcase r.start < 0:\n\t\treturn fmt.Errorf(\"start %s is negative\", r.start)\n\tcase r.end < 0:\n\t\treturn fmt.Errorf(\"end %s is negative\", r.end)\n\tcase r.start >= r.end:\n\t\treturn fmt.Errorf(\"start %s is greater or equal to end %s\", r.start, r.end)\n\tcase r.start >= maxDayRange:\n\t\treturn fmt.Errorf(\"start %s is greater or equal to %s\", r.start, maxDayRange)\n\tcase r.end > maxDayRange:\n\t\treturn fmt.Errorf(\"end %s is greater than %s\", r.end, maxDayRange)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// contains returns true if start <= offset < end, where offset is the time\n// duration from the beginning of the day.\nfunc (r *dayRange) contains(offset time.Duration) (ok bool) {\n\treturn r.start <= offset && offset < r.end\n}\n\n// toDayConfigJSON returns nil if the day range is empty, otherwise returns\n// initialized JSON configuration of the day range.\nfunc (r dayRange) toDayConfigJSON() (j *dayConfigJSON) {\n\tif (r == dayRange{}) {\n\t\treturn nil\n\t}\n\n\treturn &dayConfigJSON{\n\t\tStart: aghhttp.JSONDuration(r.start),\n\t\tEnd:   aghhttp.JSONDuration(r.end),\n\t}\n}\n\n// weeklyConfigJSON is the JSON configuration structure of Weekly.\ntype weeklyConfigJSON struct {\n\t// Days of the week.\n\n\tSunday    *dayConfigJSON `json:\"sun,omitempty\"`\n\tMonday    *dayConfigJSON `json:\"mon,omitempty\"`\n\tTuesday   *dayConfigJSON `json:\"tue,omitempty\"`\n\tWednesday *dayConfigJSON `json:\"wed,omitempty\"`\n\tThursday  *dayConfigJSON `json:\"thu,omitempty\"`\n\tFriday    *dayConfigJSON `json:\"fri,omitempty\"`\n\tSaturday  *dayConfigJSON `json:\"sat,omitempty\"`\n\n\t// TimeZone is the local time zone.\n\tTimeZone string `json:\"time_zone\"`\n}\n\n// dayConfigJSON is the JSON configuration structure of dayRange.\ntype dayConfigJSON struct {\n\tStart aghhttp.JSONDuration `json:\"start\"`\n\tEnd   aghhttp.JSONDuration `json:\"end\"`\n}\n"
  },
  {
    "path": "internal/schedule/schedule_internal_test.go",
    "content": "package schedule\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tyaml \"go.yaml.in/yaml/v4\"\n)\n\nfunc TestWeekly_Contains(t *testing.T) {\n\tbaseTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)\n\totherTime := baseTime.Add(1 * timeutil.Day)\n\n\t// NOTE: In the Etc area the sign of the offsets is flipped.  So, Etc/GMT-3\n\t// is actually UTC+03:00.\n\totherTZ := time.FixedZone(\"Etc/GMT-3\", 3*60*60)\n\n\t// baseSchedule, 12:00 to 14:00.\n\tbaseSchedule := &Weekly{\n\t\tdays: [7]dayRange{\n\t\t\ttime.Friday: {start: 12 * time.Hour, end: 14 * time.Hour},\n\t\t},\n\t\tlocation: time.UTC,\n\t}\n\n\t// allDaySchedule, 00:00 to 24:00.\n\tallDaySchedule := &Weekly{\n\t\tdays: [7]dayRange{\n\t\t\ttime.Friday: {start: 0, end: 24 * time.Hour},\n\t\t},\n\t\tlocation: time.UTC,\n\t}\n\n\t// oneMinSchedule, 00:00 to 00:01.\n\toneMinSchedule := &Weekly{\n\t\tdays: [7]dayRange{\n\t\t\ttime.Friday: {start: 0, end: 1 * time.Minute},\n\t\t},\n\t\tlocation: time.UTC,\n\t}\n\n\ttestCases := []struct {\n\t\tschedule *Weekly\n\t\tassert   assert.BoolAssertionFunc\n\t\tt        time.Time\n\t\tname     string\n\t}{{\n\t\tschedule: EmptyWeekly(),\n\t\tassert:   assert.False,\n\t\tt:        baseTime,\n\t\tname:     \"empty\",\n\t}, {\n\t\tschedule: allDaySchedule,\n\t\tassert:   assert.True,\n\t\tt:        baseTime,\n\t\tname:     \"same_day_all_day\",\n\t}, {\n\t\tschedule: baseSchedule,\n\t\tassert:   assert.True,\n\t\tt:        baseTime.Add(13 * time.Hour),\n\t\tname:     \"same_day_inside\",\n\t}, {\n\t\tschedule: baseSchedule,\n\t\tassert:   assert.False,\n\t\tt:        baseTime.Add(11 * time.Hour),\n\t\tname:     \"same_day_outside\",\n\t}, {\n\t\tschedule: allDaySchedule,\n\t\tassert:   assert.True,\n\t\tt:        baseTime.Add(24*time.Hour - time.Second),\n\t\tname:     \"same_day_last_second\",\n\t}, {\n\t\tschedule: allDaySchedule,\n\t\tassert:   assert.False,\n\t\tt:        otherTime,\n\t\tname:     \"other_day_all_day\",\n\t}, {\n\t\tschedule: baseSchedule,\n\t\tassert:   assert.False,\n\t\tt:        otherTime.Add(13 * time.Hour),\n\t\tname:     \"other_day_inside\",\n\t}, {\n\t\tschedule: baseSchedule,\n\t\tassert:   assert.False,\n\t\tt:        otherTime.Add(11 * time.Hour),\n\t\tname:     \"other_day_outside\",\n\t}, {\n\t\tschedule: baseSchedule,\n\t\tassert:   assert.True,\n\t\tt:        baseTime.Add(13 * time.Hour).In(otherTZ),\n\t\tname:     \"same_day_inside_other_tz\",\n\t}, {\n\t\tschedule: baseSchedule,\n\t\tassert:   assert.False,\n\t\tt:        baseTime.Add(11 * time.Hour).In(otherTZ),\n\t\tname:     \"same_day_outside_other_tz\",\n\t}, {\n\t\tschedule: oneMinSchedule,\n\t\tassert:   assert.True,\n\t\tt:        baseTime,\n\t\tname:     \"one_minute_beginning\",\n\t}, {\n\t\tschedule: oneMinSchedule,\n\t\tassert:   assert.True,\n\t\tt:        baseTime.Add(1*time.Minute - 1),\n\t\tname:     \"one_minute_end\",\n\t}, {\n\t\tschedule: oneMinSchedule,\n\t\tassert:   assert.False,\n\t\tt:        baseTime.Add(1 * time.Minute),\n\t\tname:     \"one_minute_past_end\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttc.assert(t, tc.schedule.Contains(tc.t))\n\t\t})\n\t}\n}\n\nconst brusselsSundayYAML = `\nsun:\n    start: 12h\n    end: 14h\ntime_zone: Europe/Brussels\n`\n\nfunc TestWeekly_UnmarshalYAML(t *testing.T) {\n\tconst (\n\t\tsameTime = `\nsun:\n    start: 9h\n    end: 9h\n`\n\t\tnegativeStart = `\nsun:\n    start: -1h\n    end: 1h\n`\n\t\tbadTZ = `\ntime_zone: \"bad_timezone\"\n`\n\t\tbadYAML = `\nyaml: \"bad\"\nyaml: \"bad\"\n`\n\t)\n\n\tbrusseltsTZ, err := time.LoadLocation(\"Europe/Brussels\")\n\trequire.NoError(t, err)\n\n\tbrusselsWeekly := &Weekly{\n\t\tdays: [7]dayRange{{\n\t\t\tstart: time.Hour * 12,\n\t\t\tend:   time.Hour * 14,\n\t\t}},\n\t\tlocation: brusseltsTZ,\n\t}\n\n\ttestCases := []struct {\n\t\twant       *Weekly\n\t\tname       string\n\t\twantErrMsg string\n\t\tdata       []byte\n\t}{{\n\t\tname:       \"empty\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte(\"\"),\n\t\twant:       &Weekly{},\n\t}, {\n\t\tname:       \"null\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte(\"null\"),\n\t\twant:       &Weekly{},\n\t}, {\n\t\tname:       \"brussels_sunday\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte(brusselsSundayYAML),\n\t\twant:       brusselsWeekly,\n\t}, {\n\t\tname: \"start_equal_end\",\n\t\twantErrMsg: \"yaml: unmarshal errors:\\n\" +\n\t\t\t\"  line 2: weekday Sunday: bad day range: \" +\n\t\t\t\"start 9h0m0s is greater or equal to end 9h0m0s\",\n\t\tdata: []byte(sameTime),\n\t\twant: &Weekly{},\n\t}, {\n\t\tname: \"start_negative\",\n\t\twantErrMsg: \"yaml: unmarshal errors:\\n\" +\n\t\t\t\"  line 2: weekday Sunday: bad day range: start -1h0m0s is negative\",\n\t\tdata: []byte(negativeStart),\n\t\twant: &Weekly{},\n\t}, {\n\t\tname: \"bad_time_zone\",\n\t\twantErrMsg: \"yaml: unmarshal errors:\\n\" +\n\t\t\t\"  line 2: unknown time zone bad_timezone\",\n\t\tdata: []byte(badTZ),\n\t\twant: &Weekly{},\n\t}, {\n\t\tname: \"bad_yaml\",\n\t\twantErrMsg: \"yaml: unmarshal errors:\\n\" +\n\t\t\t\"  line 3: mapping key \\\"yaml\\\" already defined at line 2\",\n\t\tdata: []byte(badYAML),\n\t\twant: &Weekly{},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tw := &Weekly{}\n\t\t\terr = yaml.Unmarshal(tc.data, w)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, w)\n\t\t})\n\t}\n}\n\nfunc TestWeekly_MarshalYAML(t *testing.T) {\n\tbrusselsTZ, err := time.LoadLocation(\"Europe/Brussels\")\n\trequire.NoError(t, err)\n\n\tbrusselsWeekly := &Weekly{\n\t\tdays: [7]dayRange{time.Sunday: {\n\t\t\tstart: time.Hour * 12,\n\t\t\tend:   time.Hour * 14,\n\t\t}},\n\t\tlocation: brusselsTZ,\n\t}\n\n\ttestCases := []struct {\n\t\twant *Weekly\n\t\tname string\n\t\tdata []byte\n\t}{{\n\t\tname: \"empty\",\n\t\tdata: []byte(\"\"),\n\t\twant: &Weekly{},\n\t}, {\n\t\tname: \"null\",\n\t\tdata: []byte(\"null\"),\n\t\twant: &Weekly{},\n\t}, {\n\t\tname: \"brussels_sunday\",\n\t\tdata: []byte(brusselsSundayYAML),\n\t\twant: brusselsWeekly,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar data []byte\n\t\t\tdata, err = yaml.Marshal(brusselsWeekly)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tw := &Weekly{}\n\t\t\terr = yaml.Unmarshal(data, w)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, brusselsWeekly, w)\n\t\t})\n\t}\n}\n\nfunc TestWeekly_Validate(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\tin         dayRange\n\t}{{\n\t\tname:       \"empty\",\n\t\twantErrMsg: \"\",\n\t\tin:         dayRange{},\n\t}, {\n\t\tname:       \"start_seconds\",\n\t\twantErrMsg: \"bad day range: start 1s isn't rounded to minutes\",\n\t\tin: dayRange{\n\t\t\tstart: time.Second,\n\t\t\tend:   time.Hour,\n\t\t},\n\t}, {\n\t\tname:       \"end_seconds\",\n\t\twantErrMsg: \"bad day range: end 1s isn't rounded to minutes\",\n\t\tin: dayRange{\n\t\t\tstart: 0,\n\t\t\tend:   time.Second,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tw := &Weekly{}\n\t\t\terr := w.validate(tc.in)\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\nfunc TestDayRange_Validate(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\twantErrMsg string\n\t\tin         dayRange\n\t}{{\n\t\tname:       \"empty\",\n\t\twantErrMsg: \"\",\n\t\tin:         dayRange{},\n\t}, {\n\t\tname:       \"valid\",\n\t\twantErrMsg: \"\",\n\t\tin: dayRange{\n\t\t\tstart: time.Hour,\n\t\t\tend:   time.Hour * 2,\n\t\t},\n\t}, {\n\t\tname:       \"valid_end_max\",\n\t\twantErrMsg: \"\",\n\t\tin: dayRange{\n\t\t\tstart: 0,\n\t\t\tend:   time.Hour * 24,\n\t\t},\n\t}, {\n\t\tname:       \"start_negative\",\n\t\twantErrMsg: \"start -1h0m0s is negative\",\n\t\tin: dayRange{\n\t\t\tstart: time.Hour * -1,\n\t\t\tend:   time.Hour * 2,\n\t\t},\n\t}, {\n\t\tname:       \"end_negative\",\n\t\twantErrMsg: \"end -1h0m0s is negative\",\n\t\tin: dayRange{\n\t\t\tstart: 0,\n\t\t\tend:   time.Hour * -1,\n\t\t},\n\t}, {\n\t\tname:       \"start_equal_end\",\n\t\twantErrMsg: \"start 1h0m0s is greater or equal to end 1h0m0s\",\n\t\tin: dayRange{\n\t\t\tstart: time.Hour,\n\t\t\tend:   time.Hour,\n\t\t},\n\t}, {\n\t\tname:       \"start_greater_end\",\n\t\twantErrMsg: \"start 2h0m0s is greater or equal to end 1h0m0s\",\n\t\tin: dayRange{\n\t\t\tstart: time.Hour * 2,\n\t\t\tend:   time.Hour,\n\t\t},\n\t}, {\n\t\tname:       \"start_equal_max\",\n\t\twantErrMsg: \"start 24h0m0s is greater or equal to 24h0m0s\",\n\t\tin: dayRange{\n\t\t\tstart: time.Hour * 24,\n\t\t\tend:   time.Hour * 48,\n\t\t},\n\t}, {\n\t\tname:       \"end_greater_max\",\n\t\twantErrMsg: \"end 48h0m0s is greater than 24h0m0s\",\n\t\tin: dayRange{\n\t\t\tstart: 0,\n\t\t\tend:   time.Hour * 48,\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.in.validate()\n\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\t\t})\n\t}\n}\n\nconst brusselsSundayJSON = `{\n  \"sun\": {\n    \"end\": 50400000,\n    \"start\": 43200000\n  },\n  \"time_zone\": \"Europe/Brussels\"\n}`\n\nfunc TestWeekly_UnmarshalJSON(t *testing.T) {\n\tconst (\n\t\tsameTime = `{\n  \"sun\": {\n    \"end\": 32400000,\n    \"start\": 32400000\n  }\n}`\n\t\tnegativeStart = `{\n  \"sun\": {\n    \"end\": 3600000,\n    \"start\": -3600000\n  }\n}`\n\t\tbadTZ = `{\n  \"time_zone\": \"bad_timezone\"\n}`\n\t\tbadJSON = `{\n  \"bad\": \"json\",\n}`\n\t)\n\n\tbrusseltsTZ, err := time.LoadLocation(\"Europe/Brussels\")\n\trequire.NoError(t, err)\n\n\tbrusselsWeekly := &Weekly{\n\t\tdays: [7]dayRange{{\n\t\t\tstart: time.Hour * 12,\n\t\t\tend:   time.Hour * 14,\n\t\t}},\n\t\tlocation: brusseltsTZ,\n\t}\n\n\ttestCases := []struct {\n\t\twant       *Weekly\n\t\tname       string\n\t\twantErrMsg string\n\t\tdata       []byte\n\t}{{\n\t\tname:       \"empty\",\n\t\twantErrMsg: \"unexpected end of JSON input\",\n\t\tdata:       []byte(\"\"),\n\t\twant:       &Weekly{},\n\t}, {\n\t\tname:       \"null\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte(\"null\"),\n\t\twant:       &Weekly{location: time.UTC},\n\t}, {\n\t\tname:       \"brussels_sunday\",\n\t\twantErrMsg: \"\",\n\t\tdata:       []byte(brusselsSundayJSON),\n\t\twant:       brusselsWeekly,\n\t}, {\n\t\tname:       \"start_equal_end\",\n\t\twantErrMsg: \"weekday Sunday: bad day range: start 9h0m0s is greater or equal to end 9h0m0s\",\n\t\tdata:       []byte(sameTime),\n\t\twant:       &Weekly{},\n\t}, {\n\t\tname:       \"start_negative\",\n\t\twantErrMsg: \"weekday Sunday: bad day range: start -1h0m0s is negative\",\n\t\tdata:       []byte(negativeStart),\n\t\twant:       &Weekly{},\n\t}, {\n\t\tname:       \"bad_time_zone\",\n\t\twantErrMsg: \"unknown time zone bad_timezone\",\n\t\tdata:       []byte(badTZ),\n\t\twant:       &Weekly{},\n\t}, {\n\t\tname:       \"bad_json\",\n\t\twantErrMsg: \"invalid character '}' looking for beginning of object key string\",\n\t\tdata:       []byte(badJSON),\n\t\twant:       &Weekly{},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tw := &Weekly{}\n\t\t\terr = json.Unmarshal(tc.data, w)\n\t\t\ttestutil.AssertErrorMsg(t, tc.wantErrMsg, err)\n\n\t\t\tassert.Equal(t, tc.want, w)\n\t\t})\n\t}\n}\n\nfunc TestWeekly_MarshalJSON(t *testing.T) {\n\tbrusselsTZ, err := time.LoadLocation(\"Europe/Brussels\")\n\trequire.NoError(t, err)\n\n\tbrusselsWeekly := &Weekly{\n\t\tdays: [7]dayRange{time.Sunday: {\n\t\t\tstart: time.Hour * 12,\n\t\t\tend:   time.Hour * 14,\n\t\t}},\n\t\tlocation: brusselsTZ,\n\t}\n\n\ttestCases := []struct {\n\t\twant *Weekly\n\t\tname string\n\t\tdata []byte\n\t}{{\n\t\tname: \"empty\",\n\t\tdata: []byte(\"\"),\n\t\twant: &Weekly{},\n\t}, {\n\t\tname: \"null\",\n\t\tdata: []byte(\"null\"),\n\t\twant: &Weekly{},\n\t}, {\n\t\tname: \"brussels_sunday\",\n\t\tdata: []byte(brusselsSundayJSON),\n\t\twant: brusselsWeekly,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar data []byte\n\t\t\tdata, err = json.Marshal(brusselsWeekly)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tw := &Weekly{}\n\t\t\terr = json.Unmarshal(data, w)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, brusselsWeekly, w)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stats/http.go",
    "content": "// HTTP request handlers for accessing statistics data and configuration settings\n\npackage stats\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n)\n\n// queryKeyRecent is the key of the query parameter that contains the lookback\n// interval for statistics.\nconst queryKeyRecent = \"recent\"\n\n// millisecondsInHour contains number of milliseconds in one hour.\nconst millisecondsInHour = int64(time.Hour / time.Millisecond)\n\n// topAddrs is an alias for the types of the TopFoo fields of statsResponse.\n// The key is either a client's address or a requested address.\ntype topAddrs = map[string]uint64\n\n// topAddrsFloat is like [topAddrs] but the value is float64 number.\ntype topAddrsFloat = map[string]float64\n\n// StatsResp is a response to the GET /control/stats.\ntype StatsResp struct {\n\tTimeUnits string `json:\"time_units\"`\n\n\tTopQueried []topAddrs `json:\"top_queried_domains\"`\n\tTopClients []topAddrs `json:\"top_clients\"`\n\tTopBlocked []topAddrs `json:\"top_blocked_domains\"`\n\n\tTopUpstreamsResponses []topAddrs      `json:\"top_upstreams_responses\"`\n\tTopUpstreamsAvgTime   []topAddrsFloat `json:\"top_upstreams_avg_time\"`\n\n\tDNSQueries []uint64 `json:\"dns_queries\"`\n\n\tBlockedFiltering     []uint64 `json:\"blocked_filtering\"`\n\tReplacedSafebrowsing []uint64 `json:\"replaced_safebrowsing\"`\n\tReplacedParental     []uint64 `json:\"replaced_parental\"`\n\n\tNumDNSQueries           uint64 `json:\"num_dns_queries\"`\n\tNumBlockedFiltering     uint64 `json:\"num_blocked_filtering\"`\n\tNumReplacedSafebrowsing uint64 `json:\"num_replaced_safebrowsing\"`\n\tNumReplacedSafesearch   uint64 `json:\"num_replaced_safesearch\"`\n\tNumReplacedParental     uint64 `json:\"num_replaced_parental\"`\n\n\tAvgProcessingTime float64 `json:\"avg_processing_time\"`\n}\n\n// handleStats is the handler for the GET /control/stats HTTP API.\nfunc (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {\n\tstart := time.Now()\n\n\tctx := r.Context()\n\tl := s.logger\n\n\tvar limit time.Duration\n\tfunc() {\n\t\ts.confMu.RLock()\n\t\tdefer s.confMu.RUnlock()\n\n\t\tlimit = s.limit\n\t}()\n\n\trecent := r.URL.Query().Get(queryKeyRecent)\n\n\tlimit, err := parseRecent(recent, limit)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, \"%s\", err)\n\n\t\treturn\n\t}\n\n\tresp, ok := s.getData(uint32(limit.Hours()))\n\n\tl.DebugContext(ctx, \"prepared data\", \"elapsed\", time.Since(start))\n\n\tif !ok {\n\t\t// Don't bring the message to the lower case since it's a part of UI\n\t\t// text for the moment.\n\t\tconst msg = \"Couldn't get statistics data\"\n\t\taghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, msg)\n\n\t\treturn\n\t}\n\n\taghhttp.WriteJSONResponseOK(ctx, l, w, r, resp)\n}\n\n// parseRecent parses and validates the value of the recent URL parameter.  If\n// the parameter is empty, the original limit is returned.\nfunc parseRecent(recent string, limit time.Duration) (parsedLimit time.Duration, err error) {\n\tif recent == \"\" {\n\t\treturn limit, nil\n\t}\n\n\trecentMs, err := strconv.ParseInt(recent, 10, 64)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"%s: parsing interval: %s\", queryKeyRecent, err)\n\t}\n\n\terr = validate.InRange(queryKeyRecent, recentMs, millisecondsInHour, limit.Milliseconds())\n\tif err != nil {\n\t\t// Don't wrap the error since it's already informative enough as is.\n\t\treturn 0, err\n\t}\n\n\tif recentMs%millisecondsInHour != 0 {\n\t\treturn 0, fmt.Errorf(\"%s: must be a multiple of 1 hour\", queryKeyRecent)\n\t}\n\n\treturn time.Duration(recentMs) * time.Millisecond, nil\n}\n\n// configResp is the response to the GET /control/stats_info.\ntype configResp struct {\n\tIntervalDays uint32 `json:\"interval\"`\n}\n\n// getConfigResp is the response to the GET /control/stats_info.\ntype getConfigResp struct {\n\t// Ignored is the list of host names, which should not be counted.\n\tIgnored []string `json:\"ignored\"`\n\n\t// Interval is the statistics rotation interval in milliseconds.\n\tInterval float64 `json:\"interval\"`\n\n\t// Enabled shows if statistics are enabled.  It is an aghalg.NullBool to be\n\t// able to tell when it's set without using pointers.\n\tEnabled aghalg.NullBool `json:\"enabled\"`\n\n\t// IgnoredEnabled defines if ignored list is enabled.\n\tIgnoredEnabled aghalg.NullBool `json:\"ignored_enabled\"`\n}\n\n// handleStatsInfo is the handler for the GET /control/stats_info HTTP API.\n//\n// Deprecated:  Remove it when migration to the new API is over.\nfunc (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {\n\tvar (\n\t\tenabled bool\n\t\tlimit   time.Duration\n\t)\n\tfunc() {\n\t\ts.confMu.RLock()\n\t\tdefer s.confMu.RUnlock()\n\n\t\tenabled, limit = s.enabled, s.limit\n\t}()\n\n\tdays := uint32(limit / timeutil.Day)\n\tok := checkInterval(days)\n\tif !ok || (enabled && days == 0) {\n\t\t// NOTE: If interval is custom we set it to 90 days for compatibility\n\t\t// with old API.\n\t\tdays = 90\n\t}\n\n\tresp := configResp{IntervalDays: days}\n\tif !enabled {\n\t\tresp.IntervalDays = 0\n\t}\n\n\taghhttp.WriteJSONResponseOK(r.Context(), s.logger, w, r, resp)\n}\n\n// handleGetStatsConfig is the handler for the GET /control/stats/config HTTP\n// API.\nfunc (s *StatsCtx) handleGetStatsConfig(w http.ResponseWriter, r *http.Request) {\n\tvar resp *getConfigResp\n\tfunc() {\n\t\ts.confMu.RLock()\n\t\tdefer s.confMu.RUnlock()\n\n\t\tresp = &getConfigResp{\n\t\t\tIgnored:        s.ignored.Values(),\n\t\t\tIgnoredEnabled: aghalg.BoolToNullBool(s.ignored.IsEnabled()),\n\t\t\tInterval:       float64(s.limit.Milliseconds()),\n\t\t\tEnabled:        aghalg.BoolToNullBool(s.enabled),\n\t\t}\n\t}()\n\n\taghhttp.WriteJSONResponseOK(r.Context(), s.logger, w, r, resp)\n}\n\n// handleStatsConfig is the handler for the POST /control/stats_config HTTP API.\n//\n// Deprecated:  Remove it when migration to the new API is over.\nfunc (s *StatsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\treqData := configResp{}\n\terr := json.NewDecoder(r.Body).Decode(&reqData)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, \"json decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tif !checkInterval(reqData.IntervalDays) {\n\t\taghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, \"Unsupported interval\")\n\n\t\treturn\n\t}\n\n\tlimit := time.Duration(reqData.IntervalDays) * timeutil.Day\n\n\tdefer s.configModifier.Apply(ctx)\n\n\ts.confMu.Lock()\n\tdefer s.confMu.Unlock()\n\n\ts.setLimit(limit)\n}\n\n// handlePutStatsConfig is the handler for the PUT /control/stats/config/update\n// HTTP API.\nfunc (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\treqData := getConfigResp{}\n\terr := json.NewDecoder(r.Body).Decode(&reqData)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, \"json decode: %s\", err)\n\n\t\treturn\n\t}\n\n\tvar ignoredEnabled bool\n\tif reqData.IgnoredEnabled == aghalg.NBNull {\n\t\tignoredEnabled = len(reqData.Ignored) > 0\n\t} else {\n\t\tignoredEnabled = reqData.IgnoredEnabled == aghalg.NBTrue\n\t}\n\n\tengine, err := aghnet.NewIgnoreEngine(reqData.Ignored, ignoredEnabled)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusUnprocessableEntity, \"ignored: %s\", err)\n\n\t\treturn\n\t}\n\n\tivl := time.Duration(reqData.Interval) * time.Millisecond\n\terr = validateIvl(ivl)\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tctx,\n\t\t\ts.logger,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusUnprocessableEntity,\n\t\t\t\"unsupported interval: %s\",\n\t\t\terr,\n\t\t)\n\n\t\treturn\n\t}\n\n\tif reqData.Enabled == aghalg.NBNull {\n\t\taghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusUnprocessableEntity, \"enabled is null\")\n\n\t\treturn\n\t}\n\n\tdefer s.configModifier.Apply(ctx)\n\n\ts.confMu.Lock()\n\tdefer s.confMu.Unlock()\n\n\ts.ignored = engine\n\ts.limit = ivl\n\ts.enabled = reqData.Enabled == aghalg.NBTrue\n}\n\n// handleStatsReset is the handler for the POST /control/stats_reset HTTP API.\nfunc (s *StatsCtx) handleStatsReset(w http.ResponseWriter, r *http.Request) {\n\terr := s.clear()\n\tif err != nil {\n\t\taghhttp.ErrorAndLog(\n\t\t\tr.Context(),\n\t\t\ts.logger,\n\t\t\tr,\n\t\t\tw,\n\t\t\thttp.StatusInternalServerError,\n\t\t\t\"stats: %s\",\n\t\t\terr,\n\t\t)\n\t}\n}\n\n// initWeb registers the handlers for web endpoints of statistics module.\nfunc (s *StatsCtx) initWeb() {\n\ts.httpReg.Register(http.MethodGet, \"/control/stats\", s.handleStats)\n\ts.httpReg.Register(http.MethodPost, \"/control/stats_reset\", s.handleStatsReset)\n\ts.httpReg.Register(http.MethodGet, \"/control/stats/config\", s.handleGetStatsConfig)\n\ts.httpReg.Register(http.MethodPut, \"/control/stats/config/update\", s.handlePutStatsConfig)\n\n\t// Deprecated handlers.\n\ts.httpReg.Register(http.MethodGet, \"/control/stats_info\", s.handleStatsInfo)\n\ts.httpReg.Register(http.MethodPost, \"/control/stats_config\", s.handleStatsConfig)\n}\n"
  },
  {
    "path": "internal/stats/http_internal_test.go",
    "content": "package stats\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Common domain values for tests.\nconst (\n\tTestDomain1 = \"example.com\"\n\tTestDomain2 = \"example.org\"\n)\n\nfunc TestHandleStatsConfig(t *testing.T) {\n\tconst (\n\t\tsmallIvl = 1 * time.Minute\n\t\tminIvl   = 1 * time.Hour\n\t\tmaxIvl   = 365 * timeutil.Day\n\t)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\twantErr  string\n\t\tbody     getConfigResp\n\t\twantCode int\n\t}{{\n\t\tname: \"set_ivl_1_minIvl\",\n\t\tbody: getConfigResp{\n\t\t\tEnabled:        aghalg.NBTrue,\n\t\t\tInterval:       float64(minIvl.Milliseconds()),\n\t\t\tIgnored:        []string{},\n\t\t\tIgnoredEnabled: aghalg.NBFalse,\n\t\t},\n\t\twantCode: http.StatusOK,\n\t\twantErr:  \"\",\n\t}, {\n\t\tname: \"small_interval\",\n\t\tbody: getConfigResp{\n\t\t\tEnabled:        aghalg.NBTrue,\n\t\t\tInterval:       float64(smallIvl.Milliseconds()),\n\t\t\tIgnored:        []string{},\n\t\t\tIgnoredEnabled: aghalg.NBFalse,\n\t\t},\n\t\twantCode: http.StatusUnprocessableEntity,\n\t\twantErr:  \"unsupported interval: less than an hour\\n\",\n\t}, {\n\t\tname: \"big_interval\",\n\t\tbody: getConfigResp{\n\t\t\tEnabled:        aghalg.NBTrue,\n\t\t\tInterval:       float64(maxIvl.Milliseconds() + minIvl.Milliseconds()),\n\t\t\tIgnored:        []string{},\n\t\t\tIgnoredEnabled: aghalg.NBFalse,\n\t\t},\n\t\twantCode: http.StatusUnprocessableEntity,\n\t\twantErr:  \"unsupported interval: more than a year\\n\",\n\t}, {\n\t\tname: \"set_ignored_ivl_1_maxIvl\",\n\t\tbody: getConfigResp{\n\t\t\tEnabled:  aghalg.NBTrue,\n\t\t\tInterval: float64(maxIvl.Milliseconds()),\n\t\t\tIgnored: []string{\n\t\t\t\t\"ignor.ed\",\n\t\t\t},\n\t\t\tIgnoredEnabled: aghalg.NBTrue,\n\t\t},\n\t\twantCode: http.StatusOK,\n\t\twantErr:  \"\",\n\t}, {\n\t\tname: \"enabled_is_null\",\n\t\tbody: getConfigResp{\n\t\t\tEnabled:        aghalg.NBNull,\n\t\t\tInterval:       float64(minIvl.Milliseconds()),\n\t\t\tIgnored:        []string{},\n\t\t\tIgnoredEnabled: aghalg.NBFalse,\n\t\t},\n\t\twantCode: http.StatusUnprocessableEntity,\n\t\twantErr:  \"enabled is null\\n\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := newTestStatsCtx(t, Config{Enabled: true})\n\n\t\t\ts.Start()\n\t\t\ttestutil.CleanupAndRequireSuccess(t, s.Close)\n\n\t\t\tbuf, err := json.Marshal(tc.body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tconst (\n\t\t\t\tconfigGet = \"/control/stats/config\"\n\t\t\t\tconfigPut = \"/control/stats/config/update\"\n\t\t\t)\n\n\t\t\treq := httptest.NewRequest(http.MethodPut, configPut, bytes.NewReader(buf))\n\t\t\trw := httptest.NewRecorder()\n\n\t\t\ts.handlePutStatsConfig(rw, req)\n\t\t\trequire.Equal(t, tc.wantCode, rw.Code)\n\n\t\t\tif tc.wantCode != http.StatusOK {\n\t\t\t\tassert.Equal(t, tc.wantErr, rw.Body.String())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresp := httptest.NewRequest(http.MethodGet, configGet, nil)\n\t\t\trw = httptest.NewRecorder()\n\n\t\t\ts.handleGetStatsConfig(rw, resp)\n\t\t\trequire.Equal(t, http.StatusOK, rw.Code)\n\n\t\t\tans := getConfigResp{}\n\t\t\terr = json.Unmarshal(rw.Body.Bytes(), &ans)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.body, ans)\n\t\t})\n\t}\n}\n\n// populateTestData is a helper that creates test entries in db.  s must not be\n// nil.\nfunc populateTestData(tb testing.TB, s *StatsCtx) {\n\ttb.Helper()\n\n\toldUnitID := newUnitID() - 1\n\toldUnit := &unitDB{\n\t\tNResult: make([]uint64, resultLast),\n\t\tDomains: []countPair{{Name: TestDomain1, Count: 1}},\n\t\tNTotal:  1,\n\t}\n\n\tdb := s.db.Load()\n\ttx, err := db.Begin(true)\n\trequire.NoError(tb, err)\n\n\terr = s.flushUnitToDB(oldUnit, tx, uint32(oldUnitID))\n\trequire.NoError(tb, err)\n\n\terr = finishTxn(tx, true)\n\trequire.NoError(tb, err)\n\n\ts.Update(&Entry{\n\t\tClient:         netutil.IPv4Localhost().String(),\n\t\tDomain:         TestDomain2,\n\t\tProcessingTime: 3 * time.Minute,\n\t\tResult:         RNotFiltered,\n\t})\n}\n\nfunc TestStatsCtx_handleStats(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                  string\n\t\twantErr               string\n\t\twantTopQueriedDomains []topAddrs\n\t\twantDNSQueries        uint64\n\t\twantCode              int\n\t\trecent                int64\n\t}{{\n\t\tname:     \"short_interval\",\n\t\twantErr:  \"recent: out of range: must be no less than 3600000, got 240000\\n\",\n\t\twantCode: http.StatusBadRequest,\n\t\trecent:   4 * time.Minute.Milliseconds(),\n\t}, {\n\t\tname:     \"long_interval\",\n\t\twantErr:  \"recent: out of range: must be no greater than 86400000, got 259200000\\n\",\n\t\twantCode: http.StatusBadRequest,\n\t\trecent:   72 * time.Hour.Milliseconds(),\n\t}, {\n\t\tname:     \"interval_is_not_multiple_of_hour\",\n\t\twantErr:  \"recent: must be a multiple of 1 hour\\n\",\n\t\twantCode: http.StatusBadRequest,\n\t\trecent:   time.Hour.Milliseconds() + 1,\n\t}, {\n\t\tname:           \"no_interval\",\n\t\twantCode:       http.StatusOK,\n\t\twantDNSQueries: 2,\n\t\twantTopQueriedDomains: []topAddrs{{\n\t\t\tTestDomain1: 1,\n\t\t}, {\n\t\t\tTestDomain2: 1,\n\t\t}},\n\t}, {\n\t\tname:           \"valid_interval\",\n\t\twantCode:       http.StatusOK,\n\t\twantDNSQueries: 1,\n\t\twantTopQueriedDomains: []topAddrs{{\n\t\t\tTestDomain2: 1,\n\t\t}},\n\t\trecent: time.Hour.Milliseconds(),\n\t}}\n\n\ts := newTestStatsCtx(t, Config{\n\t\tEnabled: true,\n\t})\n\n\ts.Start()\n\tdefer testutil.CleanupAndRequireSuccess(t, s.Close)\n\n\tpopulateTestData(t, s)\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\turl := \"/control/stats\"\n\t\t\tif tc.recent != 0 {\n\t\t\t\turl += fmt.Sprintf(\"?recent=%d\", tc.recent)\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, url, nil)\n\t\t\trw := httptest.NewRecorder()\n\n\t\t\ts.handleStats(rw, req)\n\t\t\trequire.Equal(t, tc.wantCode, rw.Code)\n\n\t\t\tif rw.Code != http.StatusOK {\n\t\t\t\trequire.Equal(t, tc.wantErr, rw.Body.String())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tans := StatsResp{}\n\t\t\terr := json.Unmarshal(rw.Body.Bytes(), &ans)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.wantDNSQueries, ans.NumDNSQueries)\n\t\t\tassert.ElementsMatch(t, tc.wantTopQueriedDomains, ans.TopQueried)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stats/stats.go",
    "content": "// Package stats provides units for managing statistics of the filtering DNS\n// server.\npackage stats\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/netip\"\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"go.etcd.io/bbolt\"\n\tbbolterrors \"go.etcd.io/bbolt/errors\"\n)\n\n// checkInterval returns true if days is valid to be used as statistics\n// retention interval.  The valid values are 0, 1, 7, 30 and 90.\nfunc checkInterval(days uint32) (ok bool) {\n\treturn days == 0 || days == 1 || days == 7 || days == 30 || days == 90\n}\n\n// validateIvl returns an error if ivl is less than an hour or more than a\n// year.\nfunc validateIvl(ivl time.Duration) (err error) {\n\tif ivl < time.Hour {\n\t\treturn errors.Error(\"less than an hour\")\n\t}\n\n\tif ivl > timeutil.Day*365 {\n\t\treturn errors.Error(\"more than a year\")\n\t}\n\n\treturn nil\n}\n\n// Config is the configuration structure for the statistics collecting.\n//\n// Do not alter any fields of this structure after using it.\ntype Config struct {\n\t// Logger is used for logging the operation of the statistics management.\n\t// It must not be nil.\n\tLogger *slog.Logger\n\n\t// UnitID is the function to generate the identifier for current unit.  If\n\t// nil, the default function is used, see newUnitID.\n\tUnitID UnitIDGenFunc\n\n\t// ConfigModifier is used to update the global configuration.  It must not\n\t// be nil.\n\tConfigModifier agh.ConfigModifier\n\n\t// ShouldCountClient returns client's ignore setting.\n\tShouldCountClient func([]string) bool\n\n\t// HTTPRegister is the function that registers handlers for the stats\n\t// endpoints.\n\tHTTPReg aghhttp.Registrar\n\n\t// Ignored contains the list of host names, which should not be counted,\n\t// and matches them.\n\tIgnored *aghnet.IgnoreEngine\n\n\t// Filename is the name of the database file.\n\t//\n\t// TODO(f.setrakov): Move the work with DB into a separate entity with\n\t// interface.\n\tFilename string\n\n\t// Limit is an upper limit for collecting statistics.\n\tLimit time.Duration\n\n\t// Enabled tells if the statistics are enabled.\n\tEnabled bool\n}\n\n// Interface is the statistics interface to be used by other packages.\ntype Interface interface {\n\t// Start begins the statistics collecting.\n\tStart()\n\n\tio.Closer\n\n\t// Update collects the incoming statistics data.\n\tUpdate(e *Entry)\n\n\t// GetTopClientIP returns at most limit IP addresses corresponding to the\n\t// clients with the most number of requests.\n\tTopClientsIP(limit uint) []netip.Addr\n\n\t// WriteDiskConfig puts the Interface's configuration to the dc.\n\tWriteDiskConfig(dc *Config)\n\n\t// ShouldCount returns true if request for the host should be counted.\n\tShouldCount(host string, qType, qClass uint16, ids []string) bool\n}\n\n// StatsCtx collects the statistics and flushes it to the database.  Its default\n// flushing interval is one hour.\ntype StatsCtx struct {\n\t// logger is used for logging the operation of the statistics management.\n\t// It must not be nil.\n\tlogger *slog.Logger\n\n\t// currMu protects curr.\n\tcurrMu *sync.RWMutex\n\t// curr is the actual statistics collection result.\n\tcurr *unit\n\n\t// db is the opened statistics database, if any.\n\tdb atomic.Pointer[bbolt.DB]\n\n\t// unitIDGen is the function that generates an identifier for the current\n\t// unit.  It's here for only testing purposes.\n\tunitIDGen UnitIDGenFunc\n\n\t// httpReg registers HTTP handlers.  It must not be nil.\n\thttpReg aghhttp.Registrar\n\n\t// configModifier is used to update the global configuration.\n\tconfigModifier agh.ConfigModifier\n\n\t// confMu protects ignored, limit, and enabled.\n\tconfMu *sync.RWMutex\n\n\t// ignored contains the list of host names, which should not be counted,\n\t// and matches them.\n\tignored *aghnet.IgnoreEngine\n\n\t// shouldCountClient returns client's ignore setting.\n\tshouldCountClient func([]string) bool\n\n\t// filename is the name of database file.\n\tfilename string\n\n\t// limit is an upper limit for collecting statistics.\n\tlimit time.Duration\n\n\t// enabled tells if the statistics are enabled.\n\tenabled bool\n}\n\n// New creates s from conf and properly initializes it.  Don't use s before\n// calling it's Start method.\nfunc New(conf Config) (s *StatsCtx, err error) {\n\tdefer withRecovered(&err)\n\n\terr = validateIvl(conf.Limit)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unsupported interval: %w\", err)\n\t}\n\n\tif conf.ShouldCountClient == nil {\n\t\treturn nil, errors.Error(\"should count client is unspecified\")\n\t}\n\n\ts = &StatsCtx{\n\t\tlogger:         conf.Logger,\n\t\tcurrMu:         &sync.RWMutex{},\n\t\thttpReg:        conf.HTTPReg,\n\t\tconfigModifier: conf.ConfigModifier,\n\t\tfilename:       conf.Filename,\n\n\t\tconfMu:            &sync.RWMutex{},\n\t\tignored:           conf.Ignored,\n\t\tshouldCountClient: conf.ShouldCountClient,\n\t\tlimit:             conf.Limit,\n\t\tenabled:           conf.Enabled,\n\t}\n\n\tif s.unitIDGen = newUnitID; conf.UnitID != nil {\n\t\ts.unitIDGen = conf.UnitID\n\t}\n\n\t// TODO(e.burkov):  Move the code below to the Start method.\n\n\terr = s.openDB()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"opening database: %w\", err)\n\t}\n\n\tvar udb *unitDB\n\tid := s.unitIDGen()\n\n\ttx, err := s.db.Load().Begin(true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"opening a transaction: %w\", err)\n\t}\n\n\tdeleted := s.deleteOldUnits(tx, id-uint32(s.limit.Hours())-1)\n\tudb = s.loadUnitFromDB(tx, id)\n\n\terr = finishTxn(tx, deleted > 0)\n\tif err != nil {\n\t\ts.logger.Error(\"finishing transacation\", slogutil.KeyError, err)\n\t}\n\n\ts.curr = newUnit(id)\n\ts.curr.deserialize(udb)\n\n\ts.logger.Debug(\"initialized\")\n\n\treturn s, nil\n}\n\n// withRecovered turns the value recovered from panic if any into an error and\n// combines it with the one pointed by orig.  orig must be non-nil.\nfunc withRecovered(orig *error) {\n\tp := recover()\n\tif p == nil {\n\t\treturn\n\t}\n\n\tvar err error\n\tswitch p := p.(type) {\n\tcase error:\n\t\terr = fmt.Errorf(\"panic: %w\", p)\n\tdefault:\n\t\terr = fmt.Errorf(\"panic: recovered value of type %[1]T: %[1]v\", p)\n\t}\n\n\t*orig = errors.WithDeferred(*orig, err)\n}\n\n// type check\nvar _ Interface = (*StatsCtx)(nil)\n\n// Start implements the [Interface] interface for *StatsCtx.\nfunc (s *StatsCtx) Start() {\n\ts.initWeb()\n\n\tgo s.periodicFlush()\n}\n\n// Close implements the [io.Closer] interface for *StatsCtx.\nfunc (s *StatsCtx) Close() (err error) {\n\tdb := s.db.Swap(nil)\n\tif db == nil {\n\t\treturn nil\n\t}\n\tdefer func() {\n\t\tcerr := db.Close()\n\t\tif cerr == nil {\n\t\t\ts.logger.Debug(\"database closed\")\n\t\t}\n\n\t\terr = errors.WithDeferred(err, cerr)\n\t}()\n\n\ttx, err := db.Begin(true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"opening transaction: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, finishTxn(tx, err == nil)) }()\n\n\ts.currMu.RLock()\n\tdefer s.currMu.RUnlock()\n\n\tudb := s.curr.serialize()\n\n\treturn s.flushUnitToDB(udb, tx, s.curr.id)\n}\n\n// Update implements the [Interface] interface for *StatsCtx.  e must not be\n// nil.\nfunc (s *StatsCtx) Update(e *Entry) {\n\ts.confMu.Lock()\n\tdefer s.confMu.Unlock()\n\n\tif !s.enabled || s.limit == 0 {\n\t\treturn\n\t}\n\n\terr := e.validate()\n\tif err != nil {\n\t\ts.logger.Debug(\"validating entry\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\ts.currMu.Lock()\n\tdefer s.currMu.Unlock()\n\n\tif s.curr == nil {\n\t\ts.logger.Error(\"current unit is nil\")\n\n\t\treturn\n\t}\n\n\ts.curr.add(e)\n}\n\n// WriteDiskConfig implements the [Interface] interface for *StatsCtx.\nfunc (s *StatsCtx) WriteDiskConfig(dc *Config) {\n\ts.confMu.RLock()\n\tdefer s.confMu.RUnlock()\n\n\tdc.Ignored = s.ignored\n\tdc.Limit = s.limit\n\tdc.Enabled = s.enabled\n}\n\n// TopClientsIP implements the [Interface] interface for *StatsCtx.\nfunc (s *StatsCtx) TopClientsIP(maxCount uint) (ips []netip.Addr) {\n\ts.confMu.RLock()\n\tdefer s.confMu.RUnlock()\n\n\tlimit := uint32(s.limit.Hours())\n\tif !s.enabled || limit == 0 {\n\t\treturn nil\n\t}\n\n\tunits, _ := s.loadUnits(limit)\n\tif units == nil {\n\t\treturn nil\n\t}\n\n\t// Collect data for all the clients to sort and crop it afterwards.\n\tm := map[string]uint64{}\n\tfor _, u := range units {\n\t\tfor _, it := range u.Clients {\n\t\t\tm[it.Name] += it.Count\n\t\t}\n\t}\n\n\ta := convertMapToSlice(m, int(maxCount))\n\tips = []netip.Addr{}\n\tfor _, it := range a {\n\t\tip, err := netip.ParseAddr(it.Name)\n\t\tif err == nil {\n\t\t\tips = append(ips, ip)\n\t\t}\n\t}\n\n\treturn ips\n}\n\n// deleteOldUnits walks the buckets available to tx and deletes old units.  It\n// returns the number of deletions performed.\nfunc (s *StatsCtx) deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {\n\ts.logger.Debug(\"deleting old units up to\", \"unit\", firstID)\n\n\t// TODO(a.garipov): See if this is actually necessary.  Looks like a rather\n\t// bizarre solution.\n\tconst errStop errors.Error = \"stop iteration\"\n\n\twalk := func(name []byte, _ *bbolt.Bucket) (err error) {\n\t\tnameID, ok := unitNameToID(name)\n\t\tif ok && nameID >= firstID {\n\t\t\treturn errStop\n\t\t}\n\n\t\terr = tx.DeleteBucket(name)\n\t\tif err != nil {\n\t\t\ts.logger.Debug(\"deleting bucket\", slogutil.KeyError, err)\n\n\t\t\treturn nil\n\t\t}\n\n\t\ts.logger.Debug(\"deleted unit\", \"name_id\", nameID, \"name\", fmt.Sprintf(\"%x\", name))\n\n\t\tdeleted++\n\n\t\treturn nil\n\t}\n\n\terr := tx.ForEach(walk)\n\tif err != nil && !errors.Is(err, errStop) {\n\t\ts.logger.Debug(\"deleting units\", slogutil.KeyError, err)\n\t}\n\n\treturn deleted\n}\n\n// openDB returns an error if the database can't be opened from the specified\n// file.  It's safe for concurrent use.\nfunc (s *StatsCtx) openDB() (err error) {\n\ts.logger.Debug(\"opening database\")\n\n\tvar db *bbolt.DB\n\n\tdb, err = bbolt.Open(s.filename, aghos.DefaultPermFile, nil)\n\tif err != nil {\n\t\tif err.Error() == \"invalid argument\" {\n\t\t\tconst lines = `AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations`\n\n\t\t\t// TODO(s.chzhen):  Use passed context.\n\t\t\tslogutil.PrintLines(\n\t\t\t\tcontext.TODO(),\n\t\t\t\ts.logger,\n\t\t\t\tslog.LevelError,\n\t\t\t\t\"opening database\",\n\t\t\t\tlines,\n\t\t\t)\n\t\t}\n\n\t\treturn err\n\t}\n\n\tdefer s.logger.Debug(\"database opened\")\n\n\ts.db.Store(db)\n\n\treturn nil\n}\n\nfunc (s *StatsCtx) flush() (cont bool, sleepFor time.Duration) {\n\tid := s.unitIDGen()\n\n\ts.confMu.Lock()\n\tdefer s.confMu.Unlock()\n\n\ts.currMu.Lock()\n\tdefer s.currMu.Unlock()\n\n\tptr := s.curr\n\tif ptr == nil {\n\t\treturn false, 0\n\t}\n\n\tlimit := uint32(s.limit.Hours())\n\tif limit == 0 || ptr.id == id {\n\t\treturn true, time.Second\n\t}\n\n\treturn s.flushDB(id, limit, ptr)\n}\n\n// flushDB flushes the unit to the database.  confMu and currMu are expected to\n// be locked.\nfunc (s *StatsCtx) flushDB(id, limit uint32, ptr *unit) (cont bool, sleepFor time.Duration) {\n\tdb := s.db.Load()\n\tif db == nil {\n\t\treturn true, 0\n\t}\n\n\tisCommitable := true\n\ttx, err := db.Begin(true)\n\tif err != nil {\n\t\ts.logger.Error(\"opening transaction\", slogutil.KeyError, err)\n\n\t\treturn true, 0\n\t}\n\tdefer func() {\n\t\tif err = finishTxn(tx, isCommitable); err != nil {\n\t\t\ts.logger.Error(\"finishing transaction\", slogutil.KeyError, err)\n\t\t}\n\t}()\n\n\ts.curr = newUnit(id)\n\n\tudb := ptr.serialize()\n\tflushErr := s.flushUnitToDB(udb, tx, ptr.id)\n\tif flushErr != nil {\n\t\ts.logger.Error(\"flushing unit\", slogutil.KeyError, flushErr)\n\t\tisCommitable = false\n\t}\n\n\tdelErr := tx.DeleteBucket(idToUnitName(id - limit))\n\n\tif delErr != nil {\n\t\t// TODO(e.burkov):  Improve the algorithm of deleting the oldest bucket\n\t\t// to avoid the error.\n\t\tlvl := slog.LevelDebug\n\t\tif !errors.Is(delErr, bbolterrors.ErrBucketNotFound) {\n\t\t\tisCommitable = false\n\t\t\tlvl = slog.LevelError\n\t\t}\n\n\t\ts.logger.Log(context.TODO(), lvl, \"deleting bucket\", slogutil.KeyError, delErr)\n\t}\n\n\treturn true, 0\n}\n\n// periodicFlush checks and flushes the unit to the database if the freshly\n// generated unit ID differs from the current's ID.  Flushing process includes:\n//   - swapping the current unit with the new empty one;\n//   - writing the current unit to the database;\n//   - removing the stale unit from the database.\nfunc (s *StatsCtx) periodicFlush() {\n\tfor cont, sleepFor := true, time.Duration(0); cont; time.Sleep(sleepFor) {\n\t\tcont, sleepFor = s.flush()\n\t}\n\n\ts.logger.Debug(\"periodic flushing finished\")\n}\n\n// setLimit sets the limit.  s.lock is expected to be locked.\n//\n// TODO(s.chzhen):  Remove it when migration to the new API is over.\nfunc (s *StatsCtx) setLimit(limit time.Duration) {\n\tif limit != 0 {\n\t\ts.enabled = true\n\t\ts.limit = limit\n\t\ts.logger.Debug(\"setting limit in days\", \"num\", limit/timeutil.Day)\n\n\t\treturn\n\t}\n\n\ts.enabled = false\n\ts.logger.Debug(\"disabled\")\n\n\tif err := s.clear(); err != nil {\n\t\ts.logger.Error(\"clearing\", slogutil.KeyError, err)\n\t}\n}\n\n// Reset counters and clear database\nfunc (s *StatsCtx) clear() (err error) {\n\tdefer func() { err = errors.Annotate(err, \"clearing: %w\") }()\n\n\tdb := s.db.Swap(nil)\n\tif db != nil {\n\t\tvar tx *bbolt.Tx\n\t\ttx, err = db.Begin(true)\n\t\tif err != nil {\n\t\t\ts.logger.Error(\"opening transaction\", slogutil.KeyError, err)\n\t\t} else if err = finishTxn(tx, false); err != nil {\n\t\t\t// Don't wrap the error since it's informative enough as is.\n\t\t\treturn err\n\t\t}\n\n\t\t// Active transactions will continue using database, but new ones won't\n\t\t// be created.\n\t\terr = db.Close()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"closing database: %w\", err)\n\t\t}\n\n\t\t// All active transactions are now closed.\n\t\ts.logger.Debug(\"database closed\")\n\t}\n\n\terr = os.Remove(s.filename)\n\tif err != nil {\n\t\ts.logger.Error(\"removing\", slogutil.KeyError, err)\n\t}\n\n\terr = s.openDB()\n\tif err != nil {\n\t\ts.logger.Error(\"opening database\", slogutil.KeyError, err)\n\t}\n\n\t// Use defer to unlock the mutex as soon as possible.\n\tdefer s.logger.Debug(\"cleared\")\n\n\ts.currMu.Lock()\n\tdefer s.currMu.Unlock()\n\n\ts.curr = newUnit(s.unitIDGen())\n\n\treturn nil\n}\n\n// loadUnits returns stored units from the database and current unit ID.\nfunc (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {\n\tdb := s.db.Load()\n\tif db == nil {\n\t\treturn nil, 0\n\t}\n\n\t// Use writable transaction to ensure any ongoing writable transaction is\n\t// taken into account.\n\ttx, err := db.Begin(true)\n\tif err != nil {\n\t\ts.logger.Error(\"opening transaction\", slogutil.KeyError, err)\n\n\t\treturn nil, 0\n\t}\n\n\ts.currMu.RLock()\n\tdefer s.currMu.RUnlock()\n\n\tcur := s.curr\n\n\tif cur != nil {\n\t\tcurID = cur.id\n\t} else {\n\t\tcurID = s.unitIDGen()\n\t}\n\n\t// Per-hour units.\n\tunits = make([]*unitDB, 0, limit)\n\tfirstID := curID - limit + 1\n\tfor i := firstID; i != curID; i++ {\n\t\tu := s.loadUnitFromDB(tx, i)\n\t\tif u == nil {\n\t\t\tu = &unitDB{NResult: make([]uint64, resultLast)}\n\t\t}\n\t\tunits = append(units, u)\n\t}\n\n\terr = finishTxn(tx, false)\n\tif err != nil {\n\t\ts.logger.Error(\"finishing transaction\", slogutil.KeyError, err)\n\t}\n\n\tif cur != nil {\n\t\tunits = append(units, cur.serialize())\n\t}\n\n\tif unitsLen := len(units); unitsLen != int(limit) {\n\t\t// Should not happen.\n\t\tpanic(fmt.Errorf(\"loaded %d units when the desired number is %d\", unitsLen, limit))\n\t}\n\n\treturn units, curID\n}\n\n// ShouldCount returns true if request for the host should be counted.\nfunc (s *StatsCtx) ShouldCount(host string, _, _ uint16, ids []string) bool {\n\ts.confMu.RLock()\n\tdefer s.confMu.RUnlock()\n\n\tif !s.shouldCountClient(ids) {\n\t\treturn false\n\t}\n\n\treturn !s.isIgnored(host)\n}\n\n// isIgnored returns true if the host is in the ignored domains list.  It\n// assumes that s.confMu is locked for reading.\nfunc (s *StatsCtx) isIgnored(host string) bool {\n\treturn s.ignored.Has(host)\n}\n"
  },
  {
    "path": "internal/stats/stats_internal_test.go",
    "content": "package stats\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/agh\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testLogger is the common logger for tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\n// newTestStatsCtx returns StatsCtx initialised with given values.  All empty\n// values from c will be replaced with defaults.\nfunc newTestStatsCtx(tb testing.TB, c Config) (s *StatsCtx) {\n\tc.Logger = cmp.Or(c.Logger, testLogger)\n\tc.ConfigModifier = cmp.Or[agh.ConfigModifier](c.ConfigModifier, agh.EmptyConfigModifier{})\n\tc.HTTPReg = cmp.Or[aghhttp.Registrar](c.HTTPReg, aghhttp.EmptyRegistrar{})\n\tc.Filename = cmp.Or(c.Filename, filepath.Join(tb.TempDir(), \"./stats.db\"))\n\tc.Limit = cmp.Or(c.Limit, timeutil.Day)\n\tif c.ShouldCountClient == nil {\n\t\tc.ShouldCountClient = func([]string) bool { return true }\n\t}\n\n\tif c.UnitID == nil {\n\t\tc.UnitID = newUnitID\n\t}\n\n\tvar err error\n\ts, err = New(c)\n\trequire.NoError(tb, err)\n\n\treturn s\n}\n\nfunc TestStats_races(t *testing.T) {\n\tvar r uint32\n\tidGen := func() (id uint32) { return atomic.LoadUint32(&r) }\n\ts := newTestStatsCtx(t, Config{\n\t\tUnitID:  idGen,\n\t\tEnabled: true,\n\t})\n\n\ts.Start()\n\tstartTime := time.Now()\n\ttestutil.CleanupAndRequireSuccess(t, s.Close)\n\n\twriteFunc := func(start, fin *sync.WaitGroup, waitCh <-chan unit, i int) {\n\t\te := &Entry{\n\t\t\tDomain:         fmt.Sprintf(\"example-%d.org\", i),\n\t\t\tClient:         fmt.Sprintf(\"client_%d\", i),\n\t\t\tResult:         Result(i)%(resultLast-1) + 1,\n\t\t\tProcessingTime: time.Since(startTime),\n\t\t}\n\n\t\tstart.Done()\n\t\tdefer fin.Done()\n\n\t\t<-waitCh\n\n\t\ts.Update(e)\n\t}\n\treadFunc := func(start, fin *sync.WaitGroup, waitCh <-chan unit) {\n\t\tstart.Done()\n\t\tdefer fin.Done()\n\n\t\t<-waitCh\n\n\t\t_, _ = s.getData(24)\n\t}\n\n\tconst (\n\t\troundsNum = 3\n\n\t\twritersNum = 10\n\t\treadersNum = 5\n\t)\n\n\tfor round := range roundsNum {\n\t\tatomic.StoreUint32(&r, uint32(round))\n\n\t\tstartWG, finWG := &sync.WaitGroup{}, &sync.WaitGroup{}\n\t\twaitCh := make(chan unit)\n\n\t\tfor i := range writersNum {\n\t\t\tstartWG.Add(1)\n\t\t\tfinWG.Add(1)\n\t\t\tgo writeFunc(startWG, finWG, waitCh, i)\n\t\t}\n\n\t\tfor range readersNum {\n\t\t\tstartWG.Add(1)\n\t\t\tfinWG.Add(1)\n\t\t\tgo readFunc(startWG, finWG, waitCh)\n\t\t}\n\n\t\tstartWG.Wait()\n\t\tclose(waitCh)\n\t\tfinWG.Wait()\n\t}\n}\n\nfunc TestStatsCtx_FillCollectedStats_daily(t *testing.T) {\n\tconst (\n\t\tdaysCount = 10\n\n\t\ttimeUnits = \"days\"\n\t)\n\n\ts := newTestStatsCtx(t, Config{\n\t\tLimit:   time.Hour,\n\t\tEnabled: true,\n\t})\n\n\ttestutil.CleanupAndRequireSuccess(t, s.Close)\n\n\tsum := make([][]uint64, resultLast)\n\tsum[RFiltered] = make([]uint64, daysCount)\n\tsum[RSafeBrowsing] = make([]uint64, daysCount)\n\tsum[RParental] = make([]uint64, daysCount)\n\n\ttotal := make([]uint64, daysCount)\n\n\tdailyData := []*unitDB{}\n\n\tfor i := range daysCount * 24 {\n\t\tn := uint64(i)\n\t\tnResult := make([]uint64, resultLast)\n\t\tnResult[RFiltered] = n\n\t\tnResult[RSafeBrowsing] = n\n\t\tnResult[RParental] = n\n\n\t\tday := i / 24\n\t\tsum[RFiltered][day] += n\n\t\tsum[RSafeBrowsing][day] += n\n\t\tsum[RParental][day] += n\n\n\t\tt := n * 3\n\n\t\ttotal[day] += t\n\n\t\tdailyData = append(dailyData, &unitDB{\n\t\t\tNTotal:  t,\n\t\t\tNResult: nResult,\n\t\t})\n\t}\n\n\tdata := &StatsResp{}\n\n\t// In this way we will not skip first hours.\n\tcurID := uint32(daysCount * 24)\n\n\ts.fillCollectedStats(data, dailyData, curID)\n\n\tassert.Equal(t, timeUnits, data.TimeUnits)\n\tassert.Equal(t, sum[RFiltered], data.BlockedFiltering)\n\tassert.Equal(t, sum[RSafeBrowsing], data.ReplacedSafebrowsing)\n\tassert.Equal(t, sum[RParental], data.ReplacedParental)\n\tassert.Equal(t, total, data.DNSQueries)\n}\n\nfunc TestStatsCtx_DataFromUnits_month(t *testing.T) {\n\tconst hoursInMonth = 720\n\n\ts := newTestStatsCtx(t, Config{\n\t\tLimit:   time.Hour,\n\t\tEnabled: true,\n\t})\n\n\ttestutil.CleanupAndRequireSuccess(t, s.Close)\n\n\tunits, curID := s.loadUnits(hoursInMonth)\n\trequire.Len(t, units, hoursInMonth)\n\n\tvar h uint32\n\tfor h = 1; h <= hoursInMonth; h++ {\n\t\tdata := s.dataFromUnits(units[:h], curID)\n\t\trequire.NotNil(t, data)\n\t}\n}\n"
  },
  {
    "path": "internal/stats/stats_test.go",
    "content": "package stats_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"path/filepath\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/stats\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/AdguardTeam/golibs/timeutil\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// constUnitID is the UnitIDGenFunc which always return 0.\nfunc constUnitID() (id uint32) { return 0 }\n\nfunc assertSuccessAndUnmarshal(tb testing.TB, to any, handler http.Handler, req *http.Request) {\n\ttb.Helper()\n\n\trequire.NotNil(tb, handler)\n\n\trw := httptest.NewRecorder()\n\n\thandler.ServeHTTP(rw, req)\n\trequire.Equal(tb, http.StatusOK, rw.Code)\n\n\tdata := rw.Body.Bytes()\n\tif to == nil {\n\t\tassert.Empty(tb, data)\n\n\t\treturn\n\t}\n\n\terr := json.Unmarshal(data, to)\n\trequire.NoError(tb, err)\n}\n\nfunc TestStats(t *testing.T) {\n\tcliIP := netutil.IPv4Localhost()\n\tcliIPStr := cliIP.String()\n\n\thandlers := map[string]http.Handler{}\n\tconf := stats.Config{\n\t\tLogger:            slogutil.NewDiscardLogger(),\n\t\tShouldCountClient: func([]string) bool { return true },\n\t\tFilename:          filepath.Join(t.TempDir(), \"stats.db\"),\n\t\tLimit:             timeutil.Day,\n\t\tEnabled:           true,\n\t\tUnitID:            constUnitID,\n\t\tHTTPReg: &aghtest.Registrar{\n\t\t\tOnRegister: func(_, url string, handler http.HandlerFunc) {\n\t\t\t\thandlers[url] = handler\n\t\t\t},\n\t\t},\n\t}\n\n\ts, err := stats.New(conf)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\ttestutil.CleanupAndRequireSuccess(t, s.Close)\n\n\tt.Run(\"data\", func(t *testing.T) {\n\t\tconst reqDomain = \"domain\"\n\t\tconst respUpstream = \"upstream\"\n\n\t\tentries := []*stats.Entry{{\n\t\t\tDomain:         reqDomain,\n\t\t\tClient:         cliIPStr,\n\t\t\tResult:         stats.RFiltered,\n\t\t\tProcessingTime: time.Microsecond * 123456,\n\t\t\tUpstreamStats: []*proxy.UpstreamStatistics{{\n\t\t\t\tAddress:       respUpstream,\n\t\t\t\tQueryDuration: time.Microsecond * 222222,\n\t\t\t}},\n\t\t}, {\n\t\t\tDomain:         reqDomain,\n\t\t\tClient:         cliIPStr,\n\t\t\tResult:         stats.RNotFiltered,\n\t\t\tProcessingTime: time.Microsecond * 123456,\n\t\t\tUpstreamStats: []*proxy.UpstreamStatistics{{\n\t\t\t\tAddress:       respUpstream,\n\t\t\t\tQueryDuration: time.Microsecond * 222222,\n\t\t\t}},\n\t\t}}\n\n\t\twantData := &stats.StatsResp{\n\t\t\tTimeUnits:             \"hours\",\n\t\t\tTopQueried:            []map[string]uint64{0: {reqDomain: 1}},\n\t\t\tTopClients:            []map[string]uint64{0: {cliIPStr: 2}},\n\t\t\tTopBlocked:            []map[string]uint64{0: {reqDomain: 1}},\n\t\t\tTopUpstreamsResponses: []map[string]uint64{0: {respUpstream: 2}},\n\t\t\tTopUpstreamsAvgTime:   []map[string]float64{0: {respUpstream: 0.222222}},\n\t\t\tDNSQueries: []uint64{\n\t\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,\n\t\t\t},\n\t\t\tBlockedFiltering: []uint64{\n\t\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,\n\t\t\t},\n\t\t\tReplacedSafebrowsing: []uint64{\n\t\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t\t},\n\t\t\tReplacedParental: []uint64{\n\t\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t\t\t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t\t},\n\t\t\tNumDNSQueries:           2,\n\t\t\tNumBlockedFiltering:     1,\n\t\t\tNumReplacedSafebrowsing: 0,\n\t\t\tNumReplacedSafesearch:   0,\n\t\t\tNumReplacedParental:     0,\n\t\t\tAvgProcessingTime:       0.123456,\n\t\t}\n\n\t\tfor _, e := range entries {\n\t\t\ts.Update(e)\n\t\t}\n\n\t\tdata := &stats.StatsResp{}\n\t\treq := httptest.NewRequest(http.MethodGet, \"/control/stats\", nil)\n\t\tassertSuccessAndUnmarshal(t, data, handlers[\"/control/stats\"], req)\n\n\t\tassert.Equal(t, wantData, data)\n\t})\n\n\tt.Run(\"tops\", func(t *testing.T) {\n\t\ttopClients := s.TopClientsIP(2)\n\t\trequire.NotEmpty(t, topClients)\n\n\t\tassert.Equal(t, cliIP, topClients[0])\n\t})\n\n\tt.Run(\"reset\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodPost, \"/control/stats_reset\", nil)\n\t\tassertSuccessAndUnmarshal(t, nil, handlers[\"/control/stats_reset\"], req)\n\n\t\t_24zeroes := [24]uint64{}\n\t\temptyData := &stats.StatsResp{\n\t\t\tTimeUnits:             \"hours\",\n\t\t\tTopQueried:            []map[string]uint64{},\n\t\t\tTopClients:            []map[string]uint64{},\n\t\t\tTopBlocked:            []map[string]uint64{},\n\t\t\tTopUpstreamsResponses: []map[string]uint64{},\n\t\t\tTopUpstreamsAvgTime:   []map[string]float64{},\n\t\t\tDNSQueries:            _24zeroes[:],\n\t\t\tBlockedFiltering:      _24zeroes[:],\n\t\t\tReplacedSafebrowsing:  _24zeroes[:],\n\t\t\tReplacedParental:      _24zeroes[:],\n\t\t}\n\n\t\treq = httptest.NewRequest(http.MethodGet, \"/control/stats\", nil)\n\t\tdata := &stats.StatsResp{}\n\n\t\tassertSuccessAndUnmarshal(t, data, handlers[\"/control/stats\"], req)\n\t\tassert.Equal(t, emptyData, data)\n\t})\n}\n\nfunc TestLargeNumbers(t *testing.T) {\n\tvar curHour uint32 = 1\n\thandlers := map[string]http.Handler{}\n\n\tconf := stats.Config{\n\t\tLogger:            slogutil.NewDiscardLogger(),\n\t\tShouldCountClient: func([]string) bool { return true },\n\t\tFilename:          filepath.Join(t.TempDir(), \"stats.db\"),\n\t\tLimit:             timeutil.Day,\n\t\tEnabled:           true,\n\t\tUnitID:            func() (id uint32) { return atomic.LoadUint32(&curHour) },\n\t\tHTTPReg: &aghtest.Registrar{\n\t\t\tOnRegister: func(_, url string, handler http.HandlerFunc) {\n\t\t\t\thandlers[url] = handler\n\t\t\t},\n\t\t},\n\t}\n\n\ts, err := stats.New(conf)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\ttestutil.CleanupAndRequireSuccess(t, s.Close)\n\n\tconst (\n\t\thoursNum      = 12\n\t\tcliNumPerHour = 1000\n\t)\n\n\treq := httptest.NewRequest(http.MethodGet, \"/control/stats\", nil)\n\n\tfor h := 0; h < hoursNum; h++ {\n\t\tatomic.AddUint32(&curHour, 1)\n\n\t\tfor i := range cliNumPerHour {\n\t\t\tip := net.IP{127, 0, byte((i & 0xff00) >> 8), byte(i & 0xff)}\n\t\t\te := &stats.Entry{\n\t\t\t\tDomain:         fmt.Sprintf(\"domain%d.hour%d\", i, h),\n\t\t\t\tClient:         ip.String(),\n\t\t\t\tResult:         stats.RNotFiltered,\n\t\t\t\tProcessingTime: 123456,\n\t\t\t}\n\t\t\ts.Update(e)\n\t\t}\n\t}\n\n\tdata := &stats.StatsResp{}\n\tassertSuccessAndUnmarshal(t, data, handlers[\"/control/stats\"], req)\n\tassert.Equal(t, hoursNum*cliNumPerHour, int(data.NumDNSQueries))\n}\n\nfunc TestShouldCount(t *testing.T) {\n\tconst (\n\t\tignored1 = \"ignor.ed\"\n\t\tignored2 = \"ignored.to\"\n\t)\n\tignored := []string{ignored1, ignored2}\n\tengine, err := aghnet.NewIgnoreEngine(ignored, true)\n\trequire.NoError(t, err)\n\n\ts, err := stats.New(stats.Config{\n\t\tLogger:   slogutil.NewDiscardLogger(),\n\t\tEnabled:  true,\n\t\tFilename: filepath.Join(t.TempDir(), \"stats.db\"),\n\t\tLimit:    timeutil.Day,\n\t\tIgnored:  engine,\n\t\tShouldCountClient: func(ids []string) (a bool) {\n\t\t\treturn ids[0] != \"no_count\"\n\t\t},\n\t\tHTTPReg: aghhttp.EmptyRegistrar{},\n\t})\n\trequire.NoError(t, err)\n\n\ts.Start()\n\ttestutil.CleanupAndRequireSuccess(t, s.Close)\n\n\ttestCases := []struct {\n\t\twantCount assert.BoolAssertionFunc\n\t\tname      string\n\t\thost      string\n\t\tids       []string\n\t}{{\n\t\tname:      \"count\",\n\t\thost:      \"example.com\",\n\t\tids:       []string{\"whatever\"},\n\t\twantCount: assert.True,\n\t}, {\n\t\tname:      \"no_count_ignored_1\",\n\t\thost:      ignored1,\n\t\tids:       []string{\"whatever\"},\n\t\twantCount: assert.False,\n\t}, {\n\t\tname:      \"no_count_ignored_2\",\n\t\thost:      ignored2,\n\t\tids:       []string{\"whatever\"},\n\t\twantCount: assert.False,\n\t}, {\n\t\tname:      \"no_count_client_ignore\",\n\t\thost:      \"example.com\",\n\t\tids:       []string{\"no_count\"},\n\t\twantCount: assert.False,\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tres := s.ShouldCount(tc.host, dns.TypeA, dns.ClassINET, tc.ids)\n\n\t\t\ttc.wantCount(t, res)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stats/unit.go",
    "content": "package stats\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/gob\"\n\t\"fmt\"\n\t\"maps\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/dnsproxy/proxy\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"go.etcd.io/bbolt\"\n)\n\nconst (\n\t// maxDomains is the max number of top domains to return.\n\tmaxDomains = 100\n\n\t// maxClients is the max number of top clients to return.\n\tmaxClients = 100\n\n\t// maxUpstreams is the max number of top upstreams to return.\n\tmaxUpstreams = 100\n)\n\n// UnitIDGenFunc is the signature of a function that generates a unique ID for\n// the statistics unit.\ntype UnitIDGenFunc func() (id uint32)\n\n// Supported values of [StatsResp.TimeUnits].\nconst (\n\ttimeUnitsHours = \"hours\"\n\ttimeUnitsDays  = \"days\"\n)\n\n// Result is the resulting code of processing the DNS request.\ntype Result int\n\n// Supported Result values.\n//\n// TODO(e.burkov):  Think about better naming.\nconst (\n\tRNotFiltered Result = iota + 1\n\tRFiltered\n\tRSafeBrowsing\n\tRSafeSearch\n\tRParental\n\n\tresultLast = RParental + 1\n)\n\n// Entry is a statistics data entry.\ntype Entry struct {\n\t// Clients is the client's primary ID.\n\t//\n\t// TODO(a.garipov): Make this a {net.IP, string} enum?\n\tClient string\n\n\t// Domain is the domain name requested.\n\tDomain string\n\n\t// UpstreamStats contains the DNS query statistics for both the upstream and\n\t// fallback DNS servers.  Don't modify items in the slice.\n\tUpstreamStats []*proxy.UpstreamStatistics\n\n\t// Result is the result of processing the request.\n\tResult Result\n\n\t// ProcessingTime is the duration of the request processing from the start\n\t// of the request including timeouts.\n\tProcessingTime time.Duration\n}\n\n// validate returns an error if entry is not valid.\nfunc (e *Entry) validate() (err error) {\n\tswitch {\n\tcase e.Result == 0:\n\t\treturn errors.Error(\"result code is not set\")\n\tcase e.Result >= resultLast:\n\t\treturn fmt.Errorf(\"unknown result code %d\", e.Result)\n\tcase e.Domain == \"\":\n\t\treturn errors.Error(\"domain is empty\")\n\tcase e.Client == \"\":\n\t\treturn errors.Error(\"client is empty\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// unit collects the statistics data for a specific period of time.\ntype unit struct {\n\t// domains stores the number of requests for each domain.\n\tdomains map[string]uint64\n\n\t// blockedDomains stores the number of requests for each domain that has\n\t// been blocked.\n\tblockedDomains map[string]uint64\n\n\t// clients stores the number of requests from each client.\n\tclients map[string]uint64\n\n\t// upstreamsResponses stores the number of responses from each upstream.\n\tupstreamsResponses map[string]uint64\n\n\t// upstreamsTimeSum stores the sum of durations of successful queries in\n\t// microseconds to each upstream.\n\tupstreamsTimeSum map[string]uint64\n\n\t// nResult stores the number of requests grouped by it's result.\n\tnResult []uint64\n\n\t// id is the unique unit's identifier.  It's set to an absolute hour number\n\t// since the beginning of UNIX time by the default ID generating function.\n\t//\n\t// Must not be rewritten after creating to be accessed concurrently without\n\t// using mu.\n\tid uint32\n\n\t// nTotal stores the total number of requests.\n\tnTotal uint64\n\n\t// timeSum stores the sum of processing time in microseconds of each request\n\t// written by the unit.\n\ttimeSum uint64\n}\n\n// newUnit allocates the new *unit.\nfunc newUnit(id uint32) (u *unit) {\n\treturn &unit{\n\t\tdomains:            map[string]uint64{},\n\t\tblockedDomains:     map[string]uint64{},\n\t\tclients:            map[string]uint64{},\n\t\tupstreamsResponses: map[string]uint64{},\n\t\tupstreamsTimeSum:   map[string]uint64{},\n\t\tnResult:            make([]uint64, resultLast),\n\t\tid:                 id,\n\t}\n}\n\n// countPair is a single name-number pair for deserializing statistics data into\n// the database.\ntype countPair struct {\n\tName  string\n\tCount uint64\n}\n\n// unitDB is the structure for serializing statistics data into the database.\n//\n// NOTE: Do not change the names or types of fields, as this structure is used\n// for GOB encoding.\ntype unitDB struct {\n\t// NResult is the number of requests by the result's kind.\n\tNResult []uint64\n\n\t// Domains is the number of requests for each domain name.\n\tDomains []countPair\n\n\t// BlockedDomains is the number of requests blocked for each domain name.\n\tBlockedDomains []countPair\n\n\t// Clients is the number of requests from each client.\n\tClients []countPair\n\n\t// UpstreamsResponses is the number of responses from each upstream.\n\tUpstreamsResponses []countPair\n\n\t// UpstreamsTimeSum is the sum of processing time in microseconds of\n\t// responses from each upstream.\n\tUpstreamsTimeSum []countPair\n\n\t// NTotal is the total number of requests.\n\tNTotal uint64\n\n\t// TimeAvg is the average of processing times in microseconds of all the\n\t// requests in the unit.\n\tTimeAvg uint32\n}\n\n// newUnitID is the default UnitIDGenFunc that generates the unique id hourly.\nfunc newUnitID() (id uint32) {\n\tconst secsInHour = int64(time.Hour / time.Second)\n\n\treturn uint32(time.Now().Unix() / secsInHour)\n}\n\nfunc finishTxn(tx *bbolt.Tx, commit bool) (err error) {\n\tif commit {\n\t\terr = errors.Annotate(tx.Commit(), \"committing: %w\")\n\t} else {\n\t\terr = errors.Annotate(tx.Rollback(), \"rolling back: %w\")\n\t}\n\n\treturn err\n}\n\n// bucketNameLen is the length of a bucket, a 64-bit unsigned integer.\n//\n// TODO(a.garipov): Find out why a 64-bit integer is used when IDs seem to\n// always be 32 bits.\nconst bucketNameLen = 8\n\n// idToUnitName converts a numerical ID into a database unit name.\nfunc idToUnitName(id uint32) (name []byte) {\n\tn := [bucketNameLen]byte{}\n\tbinary.BigEndian.PutUint64(n[:], uint64(id))\n\n\treturn n[:]\n}\n\n// unitNameToID converts a database unit name into a numerical ID.  ok is false\n// if name is not a valid database unit name.\nfunc unitNameToID(name []byte) (id uint32, ok bool) {\n\tif len(name) < bucketNameLen {\n\t\treturn 0, false\n\t}\n\n\treturn uint32(binary.BigEndian.Uint64(name)), true\n}\n\n// compareCount used to sort countPair by Count in descending order.\nfunc (a countPair) compareCount(b countPair) (res int) {\n\tswitch x, y := a.Count, b.Count; {\n\tcase x > y:\n\t\treturn -1\n\tcase x < y:\n\t\treturn +1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc convertMapToSlice(m map[string]uint64, maxVal int) (s []countPair) {\n\ts = make([]countPair, 0, len(m))\n\tfor k, v := range m {\n\t\ts = append(s, countPair{Name: k, Count: v})\n\t}\n\n\tslices.SortFunc(s, countPair.compareCount)\n\n\treturn s[:min(maxVal, len(s))]\n}\n\nfunc convertSliceToMap(a []countPair) (m map[string]uint64) {\n\tm = map[string]uint64{}\n\tfor _, it := range a {\n\t\tm[it.Name] = it.Count\n\t}\n\n\treturn m\n}\n\n// serialize converts u to the *unitDB.  It's safe for concurrent use.  u must\n// not be nil.\nfunc (u *unit) serialize() (udb *unitDB) {\n\tvar timeAvg uint32 = 0\n\tif u.nTotal != 0 {\n\t\ttimeAvg = uint32(u.timeSum / u.nTotal)\n\t}\n\n\treturn &unitDB{\n\t\tNTotal:             u.nTotal,\n\t\tNResult:            append([]uint64{}, u.nResult...),\n\t\tDomains:            convertMapToSlice(u.domains, maxDomains),\n\t\tBlockedDomains:     convertMapToSlice(u.blockedDomains, maxDomains),\n\t\tClients:            convertMapToSlice(u.clients, maxClients),\n\t\tUpstreamsResponses: convertMapToSlice(u.upstreamsResponses, maxUpstreams),\n\t\tUpstreamsTimeSum:   convertMapToSlice(u.upstreamsTimeSum, maxUpstreams),\n\t\tTimeAvg:            timeAvg,\n\t}\n}\n\n// loadUnitFromDB loads unit by id from the database.\nfunc (s *StatsCtx) loadUnitFromDB(tx *bbolt.Tx, id uint32) (udb *unitDB) {\n\tbkt := tx.Bucket(idToUnitName(id))\n\tif bkt == nil {\n\t\treturn nil\n\t}\n\n\ts.logger.Debug(\"loading unit\", \"id\", id)\n\n\tvar buf bytes.Buffer\n\tbuf.Write(bkt.Get([]byte{0}))\n\tudb = &unitDB{}\n\n\terr := gob.NewDecoder(&buf).Decode(udb)\n\tif err != nil {\n\t\ts.logger.Error(\"gob decode\", slogutil.KeyError, err)\n\n\t\treturn nil\n\t}\n\n\treturn udb\n}\n\n// deserialize assigns the appropriate values from udb to u.  u must not be nil.\n// It's safe for concurrent use.\nfunc (u *unit) deserialize(udb *unitDB) {\n\tif udb == nil {\n\t\treturn\n\t}\n\n\tu.nTotal = udb.NTotal\n\tu.nResult = make([]uint64, resultLast)\n\tcopy(u.nResult, udb.NResult)\n\tu.domains = convertSliceToMap(udb.Domains)\n\tu.blockedDomains = convertSliceToMap(udb.BlockedDomains)\n\tu.clients = convertSliceToMap(udb.Clients)\n\tu.upstreamsResponses = convertSliceToMap(udb.UpstreamsResponses)\n\tu.upstreamsTimeSum = convertSliceToMap(udb.UpstreamsTimeSum)\n\tu.timeSum = uint64(udb.TimeAvg) * udb.NTotal\n}\n\n// add adds new data to u.  It's safe for concurrent use.\nfunc (u *unit) add(e *Entry) {\n\tu.nResult[e.Result]++\n\tif e.Result == RNotFiltered {\n\t\tu.domains[e.Domain]++\n\t} else {\n\t\tu.blockedDomains[e.Domain]++\n\t}\n\n\tu.clients[e.Client]++\n\tpt := uint64(e.ProcessingTime.Microseconds())\n\tu.timeSum += pt\n\tu.nTotal++\n\n\tfor _, s := range e.UpstreamStats {\n\t\tif s.IsCached || s.Error != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\taddr := s.Address\n\t\tu.upstreamsResponses[addr]++\n\t\tu.upstreamsTimeSum[addr] += uint64(s.QueryDuration.Microseconds())\n\t}\n}\n\n// flushUnitToDB puts udb to the database at id.\nfunc (s *StatsCtx) flushUnitToDB(udb *unitDB, tx *bbolt.Tx, id uint32) (err error) {\n\ts.logger.Debug(\"flushing unit\", \"id\", id, \"req_num\", udb.NTotal)\n\n\tbkt, err := tx.CreateBucketIfNotExists(idToUnitName(id))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating bucket: %w\", err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\terr = gob.NewEncoder(buf).Encode(udb)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"encoding unit: %w\", err)\n\t}\n\n\terr = bkt.Put([]byte{0}, buf.Bytes())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"putting unit to database: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc convertTopSlice(a []countPair) (m []map[string]uint64) {\n\tm = make([]map[string]uint64, 0, len(a))\n\tfor _, it := range a {\n\t\tm = append(m, map[string]uint64{it.Name: it.Count})\n\t}\n\n\treturn m\n}\n\n// pairsGetter is a signature for topsCollector argument.\ntype pairsGetter func(u *unitDB) (pairs []countPair)\n\n// topsCollector collects statistics about highest values from the given *unitDB\n// slice using pg to retrieve data.\nfunc topsCollector(units []*unitDB, max int, ignored *aghnet.IgnoreEngine, pg pairsGetter) []map[string]uint64 {\n\tm := map[string]uint64{}\n\tfor _, u := range units {\n\t\tfor _, cp := range pg(u) {\n\t\t\tif !ignored.Has(cp.Name) {\n\t\t\t\tm[cp.Name] += cp.Count\n\t\t\t}\n\t\t}\n\t}\n\ta2 := convertMapToSlice(m, max)\n\n\treturn convertTopSlice(a2)\n}\n\n// getData returns the statistics data using the following algorithm:\n//\n//  1. Prepare a slice of N units, where N is the value of \"limit\" configuration\n//     setting.  Load data for the most recent units from the file.  If a unit\n//     with required ID doesn't exist, just add an empty unit.  Get data for the\n//     current unit.\n//\n//  2. Process data from the units and prepare an output map object, including\n//     per time unit counters (DNS queries per time-unit, blocked queries per\n//     time unit, etc.).  If the time unit is hour, just add values from each\n//     unit to the slice; otherwise, the time unit is day, so aggregate per-hour\n//     data into days.\n//\n//     To get the top counters (queries per domain, queries per blocked domain,\n//     etc.), first sum up data for all units into a single map.  Then,  get the\n//     pairs with the highest numbers.\n//\n//     The total counters (DNS queries, blocked, etc.) are just the sum of data\n//     for all units.\nfunc (s *StatsCtx) getData(limit uint32) (resp *StatsResp, ok bool) {\n\tif limit == 0 {\n\t\treturn &StatsResp{\n\t\t\tTimeUnits: \"days\",\n\n\t\t\tTopBlocked:            []topAddrs{},\n\t\t\tTopClients:            []topAddrs{},\n\t\t\tTopQueried:            []topAddrs{},\n\t\t\tTopUpstreamsResponses: []topAddrs{},\n\t\t\tTopUpstreamsAvgTime:   []topAddrsFloat{},\n\n\t\t\tBlockedFiltering:     []uint64{},\n\t\t\tDNSQueries:           []uint64{},\n\t\t\tReplacedParental:     []uint64{},\n\t\t\tReplacedSafebrowsing: []uint64{},\n\t\t}, true\n\t}\n\n\tunits, curID := s.loadUnits(limit)\n\tif units == nil {\n\t\treturn &StatsResp{}, false\n\t}\n\n\treturn s.dataFromUnits(units, curID), true\n}\n\n// dataFromUnits collects and returns the statistics data.\nfunc (s *StatsCtx) dataFromUnits(units []*unitDB, curID uint32) (resp *StatsResp) {\n\ttopUpstreamsResponses, topUpstreamsAvgTime := topUpstreamsPairs(units)\n\n\tresp = &StatsResp{\n\t\tTopQueried:            topsCollector(units, maxDomains, s.ignored, func(u *unitDB) (pairs []countPair) { return u.Domains }),\n\t\tTopBlocked:            topsCollector(units, maxDomains, s.ignored, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }),\n\t\tTopUpstreamsResponses: topUpstreamsResponses,\n\t\tTopUpstreamsAvgTime:   topUpstreamsAvgTime,\n\t\tTopClients:            topsCollector(units, maxClients, nil, topClientPairs(s)),\n\t}\n\n\ts.fillCollectedStats(resp, units, curID)\n\n\t// Total counters:\n\tsum := unitDB{\n\t\tNResult: make([]uint64, resultLast),\n\t}\n\tvar timeN uint32\n\tfor _, u := range units {\n\t\tsum.NTotal += u.NTotal\n\t\tsum.TimeAvg += u.TimeAvg\n\t\tif u.TimeAvg != 0 {\n\t\t\ttimeN++\n\t\t}\n\t\tsum.NResult[RFiltered] += u.NResult[RFiltered]\n\t\tsum.NResult[RSafeBrowsing] += u.NResult[RSafeBrowsing]\n\t\tsum.NResult[RSafeSearch] += u.NResult[RSafeSearch]\n\t\tsum.NResult[RParental] += u.NResult[RParental]\n\t}\n\n\tresp.NumDNSQueries = sum.NTotal\n\tresp.NumBlockedFiltering = sum.NResult[RFiltered]\n\tresp.NumReplacedSafebrowsing = sum.NResult[RSafeBrowsing]\n\tresp.NumReplacedSafesearch = sum.NResult[RSafeSearch]\n\tresp.NumReplacedParental = sum.NResult[RParental]\n\n\tif timeN != 0 {\n\t\tresp.AvgProcessingTime = microsecondsToSeconds(float64(sum.TimeAvg / timeN))\n\t}\n\n\treturn resp\n}\n\n// fillCollectedStats fills data with collected statistics.\nfunc (s *StatsCtx) fillCollectedStats(data *StatsResp, units []*unitDB, curID uint32) {\n\tsize := len(units)\n\tdata.TimeUnits = timeUnitsHours\n\n\tdaysCount := size / 24\n\tif daysCount > 7 {\n\t\tsize = daysCount\n\t\tdata.TimeUnits = timeUnitsDays\n\t}\n\n\tdata.DNSQueries = make([]uint64, size)\n\tdata.BlockedFiltering = make([]uint64, size)\n\tdata.ReplacedSafebrowsing = make([]uint64, size)\n\tdata.ReplacedParental = make([]uint64, size)\n\n\tif data.TimeUnits == timeUnitsDays {\n\t\ts.fillCollectedStatsDaily(data, units, curID, size)\n\n\t\treturn\n\t}\n\n\tfor i, u := range units {\n\t\tdata.DNSQueries[i] += u.NTotal\n\t\tdata.BlockedFiltering[i] += u.NResult[RFiltered]\n\t\tdata.ReplacedSafebrowsing[i] += u.NResult[RSafeBrowsing]\n\t\tdata.ReplacedParental[i] += u.NResult[RParental]\n\t}\n}\n\n// fillCollectedStatsDaily fills data with collected daily statistics.  units\n// must contain data for the count of days.\n//\n// TODO(s.chzhen):  Improve collection of statistics for frontend.  Dashboard\n// cards should contain statistics for the whole interval without rounding to\n// days.\nfunc (s *StatsCtx) fillCollectedStatsDaily(\n\tdata *StatsResp,\n\tunits []*unitDB,\n\tcurHour uint32,\n\tdays int,\n) {\n\t// Per time unit counters: 720 hours may span 31 days, so we skip data for\n\t// the first hours in this case.  align_ceil(24)\n\thours := countHours(curHour, days)\n\tunits = units[len(units)-hours:]\n\n\tfor i, u := range units {\n\t\tday := i / 24\n\n\t\tdata.DNSQueries[day] += u.NTotal\n\t\tdata.BlockedFiltering[day] += u.NResult[RFiltered]\n\t\tdata.ReplacedSafebrowsing[day] += u.NResult[RSafeBrowsing]\n\t\tdata.ReplacedParental[day] += u.NResult[RParental]\n\t}\n}\n\n// countHours returns the number of hours in the last days.\nfunc countHours(curHour uint32, days int) (n int) {\n\thoursInCurDay := int(curHour % 24)\n\tif hoursInCurDay == 0 {\n\t\thoursInCurDay = 24\n\t}\n\n\thoursInRestDays := (days - 1) * 24\n\n\treturn hoursInRestDays + hoursInCurDay\n}\n\nfunc topClientPairs(s *StatsCtx) (pg pairsGetter) {\n\treturn func(u *unitDB) (clients []countPair) {\n\t\tfor _, c := range u.Clients {\n\t\t\tif c.Name != \"\" && !s.shouldCountClient([]string{c.Name}) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tclients = append(clients, c)\n\t\t}\n\n\t\treturn clients\n\t}\n}\n\n// topUpstreamsPairs returns sorted lists of number of total responses and the\n// average of processing time for each upstream.\nfunc topUpstreamsPairs(\n\tunits []*unitDB,\n) (topUpstreamsResponses []topAddrs, topUpstreamsAvgTime []topAddrsFloat) {\n\tupstreamsResponses := topAddrs{}\n\tupstreamsTimeSum := topAddrsFloat{}\n\n\tfor _, u := range units {\n\t\tfor _, cp := range u.UpstreamsResponses {\n\t\t\tupstreamsResponses[cp.Name] += cp.Count\n\t\t}\n\n\t\tfor _, cp := range u.UpstreamsTimeSum {\n\t\t\tupstreamsTimeSum[cp.Name] += float64(cp.Count)\n\t\t}\n\t}\n\n\tupstreamsAvgTime := topAddrsFloat{}\n\n\tfor u, n := range upstreamsResponses {\n\t\ttotal := upstreamsTimeSum[u]\n\n\t\tif total != 0 {\n\t\t\tupstreamsAvgTime[u] = microsecondsToSeconds(total / float64(n))\n\t\t}\n\t}\n\n\tupstreamsPairs := convertMapToSlice(upstreamsResponses, maxUpstreams)\n\ttopUpstreamsResponses = convertTopSlice(upstreamsPairs)\n\n\treturn topUpstreamsResponses, prepareTopUpstreamsAvgTime(upstreamsAvgTime)\n}\n\n// microsecondsToSeconds converts microseconds to seconds.\n//\n// NOTE:  Frontend expects time duration in seconds as floating-point number\n// with double precision.\nfunc microsecondsToSeconds(n float64) (r float64) {\n\tconst micro = 1e-6\n\n\treturn n * micro\n}\n\n// prepareTopUpstreamsAvgTime returns sorted list of average processing times\n// of the DNS requests from each upstream.\nfunc prepareTopUpstreamsAvgTime(\n\tupstreamsAvgTime topAddrsFloat,\n) (topUpstreamsAvgTime []topAddrsFloat) {\n\tkeys := slices.SortedStableFunc(maps.Keys(upstreamsAvgTime), func(a, b string) (res int) {\n\t\tswitch x, y := upstreamsAvgTime[a], upstreamsAvgTime[b]; {\n\t\tcase x > y:\n\t\t\treturn -1\n\t\tcase x < y:\n\t\t\treturn +1\n\t\tdefault:\n\t\t\treturn 0\n\t\t}\n\t})\n\n\ttopUpstreamsAvgTime = make([]topAddrsFloat, 0, len(upstreamsAvgTime))\n\tfor _, k := range keys {\n\t\ttopUpstreamsAvgTime = append(topUpstreamsAvgTime, topAddrsFloat{k: upstreamsAvgTime[k]})\n\t}\n\n\treturn topUpstreamsAvgTime\n}\n"
  },
  {
    "path": "internal/stats/unit_internal_test.go",
    "content": "package stats\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUnit_Deserialize(t *testing.T) {\n\ttestCases := []struct {\n\t\tdb   *unitDB\n\t\tname string\n\t\twant unit\n\t}{{\n\t\tname: \"empty\",\n\t\twant: unit{\n\t\t\tdomains:            map[string]uint64{},\n\t\t\tblockedDomains:     map[string]uint64{},\n\t\t\tclients:            map[string]uint64{},\n\t\t\tnResult:            []uint64{0, 0, 0, 0, 0, 0},\n\t\t\tid:                 0,\n\t\t\tnTotal:             0,\n\t\t\ttimeSum:            0,\n\t\t\tupstreamsResponses: map[string]uint64{},\n\t\t\tupstreamsTimeSum:   map[string]uint64{},\n\t\t},\n\t\tdb: &unitDB{\n\t\t\tNResult:            []uint64{0, 0, 0, 0, 0, 0},\n\t\t\tDomains:            []countPair{},\n\t\t\tBlockedDomains:     []countPair{},\n\t\t\tClients:            []countPair{},\n\t\t\tNTotal:             0,\n\t\t\tTimeAvg:            0,\n\t\t\tUpstreamsResponses: []countPair{},\n\t\t\tUpstreamsTimeSum:   []countPair{},\n\t\t},\n\t}, {\n\t\tname: \"basic\",\n\t\twant: unit{\n\t\t\tdomains: map[string]uint64{\n\t\t\t\t\"example.com\": 1,\n\t\t\t},\n\t\t\tblockedDomains: map[string]uint64{\n\t\t\t\t\"example.net\": 1,\n\t\t\t},\n\t\t\tclients: map[string]uint64{\n\t\t\t\t\"127.0.0.1\": 2,\n\t\t\t},\n\t\t\tnResult: []uint64{0, 1, 1, 0, 0, 0},\n\t\t\tid:      0,\n\t\t\tnTotal:  2,\n\t\t\ttimeSum: 246912,\n\t\t\tupstreamsResponses: map[string]uint64{\n\t\t\t\t\"1.2.3.4\": 2,\n\t\t\t},\n\t\t\tupstreamsTimeSum: map[string]uint64{\n\t\t\t\t\"1.2.3.4\": 246912,\n\t\t\t},\n\t\t},\n\t\tdb: &unitDB{\n\t\t\tNResult: []uint64{0, 1, 1, 0, 0, 0},\n\t\t\tDomains: []countPair{{\n\t\t\t\t\"example.com\", 1,\n\t\t\t}},\n\t\t\tBlockedDomains: []countPair{{\n\t\t\t\t\"example.net\", 1,\n\t\t\t}},\n\t\t\tClients: []countPair{{\n\t\t\t\t\"127.0.0.1\", 2,\n\t\t\t}},\n\t\t\tNTotal:  2,\n\t\t\tTimeAvg: 123456,\n\t\t\tUpstreamsResponses: []countPair{{\n\t\t\t\t\"1.2.3.4\", 2,\n\t\t\t}},\n\t\t\tUpstreamsTimeSum: []countPair{{\n\t\t\t\t\"1.2.3.4\", 246912,\n\t\t\t}},\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := unit{}\n\t\t\tgot.deserialize(tc.db)\n\t\t\trequire.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc TestTopUpstreamsPairs(t *testing.T) {\n\ttestCases := []struct {\n\t\tdb            *unitDB\n\t\tname          string\n\t\twantResponses []topAddrs\n\t\twantAvgTime   []topAddrsFloat\n\t}{{\n\t\tname: \"empty\",\n\t\tdb: &unitDB{\n\t\t\tNResult:            []uint64{0, 0, 0, 0, 0, 0},\n\t\t\tDomains:            []countPair{},\n\t\t\tBlockedDomains:     []countPair{},\n\t\t\tClients:            []countPair{},\n\t\t\tNTotal:             0,\n\t\t\tTimeAvg:            0,\n\t\t\tUpstreamsResponses: []countPair{},\n\t\t\tUpstreamsTimeSum:   []countPair{},\n\t\t},\n\t\twantResponses: []topAddrs{},\n\t\twantAvgTime:   []topAddrsFloat{},\n\t}, {\n\t\tname: \"basic\",\n\t\tdb: &unitDB{\n\t\t\tNResult:        []uint64{0, 0, 0, 0, 0, 0},\n\t\t\tDomains:        []countPair{},\n\t\t\tBlockedDomains: []countPair{},\n\t\t\tClients:        []countPair{},\n\t\t\tNTotal:         0,\n\t\t\tTimeAvg:        0,\n\t\t\tUpstreamsResponses: []countPair{{\n\t\t\t\t\"1.2.3.4\", 2,\n\t\t\t}},\n\t\t\tUpstreamsTimeSum: []countPair{{\n\t\t\t\t\"1.2.3.4\", 246912,\n\t\t\t}},\n\t\t},\n\t\twantResponses: []topAddrs{{\n\t\t\t\"1.2.3.4\": 2,\n\t\t}},\n\t\twantAvgTime: []topAddrsFloat{{\n\t\t\t\"1.2.3.4\": 0.123456,\n\t\t}},\n\t}, {\n\t\tname: \"sorted\",\n\t\tdb: &unitDB{\n\t\t\tNResult:        []uint64{0, 0, 0, 0, 0, 0},\n\t\t\tDomains:        []countPair{},\n\t\t\tBlockedDomains: []countPair{},\n\t\t\tClients:        []countPair{},\n\t\t\tNTotal:         0,\n\t\t\tTimeAvg:        0,\n\t\t\tUpstreamsResponses: []countPair{\n\t\t\t\t{\"3.3.3.3\", 8},\n\t\t\t\t{\"2.2.2.2\", 4},\n\t\t\t\t{\"4.4.4.4\", 16},\n\t\t\t\t{\"1.1.1.1\", 2},\n\t\t\t},\n\t\t\tUpstreamsTimeSum: []countPair{\n\t\t\t\t{\"3.3.3.3\", 800_000_000},\n\t\t\t\t{\"2.2.2.2\", 40_000_000},\n\t\t\t\t{\"4.4.4.4\", 16_000_000_000},\n\t\t\t\t{\"1.1.1.1\", 2_000_000},\n\t\t\t},\n\t\t},\n\t\twantResponses: []topAddrs{\n\t\t\t{\"4.4.4.4\": 16},\n\t\t\t{\"3.3.3.3\": 8},\n\t\t\t{\"2.2.2.2\": 4},\n\t\t\t{\"1.1.1.1\": 2},\n\t\t},\n\t\twantAvgTime: []topAddrsFloat{\n\t\t\t{\"4.4.4.4\": 1000},\n\t\t\t{\"3.3.3.3\": 100},\n\t\t\t{\"2.2.2.2\": 10},\n\t\t\t{\"1.1.1.1\": 1},\n\t\t},\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgotResponses, gotAvgTime := topUpstreamsPairs([]*unitDB{tc.db})\n\t\t\tassert.Equal(t, tc.wantResponses, gotResponses)\n\t\t\tassert.Equal(t, tc.wantAvgTime, gotAvgTime)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/updater/check.go",
    "content": "package updater\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net/http\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\n// TODO(a.garipov): Make configurable.\nconst versionCheckPeriod = 8 * time.Hour\n\n// VersionInfo contains information about a new version.\ntype VersionInfo struct {\n\tNewVersion      string `json:\"new_version,omitempty\"`\n\tAnnouncement    string `json:\"announcement,omitempty\"`\n\tAnnouncementURL string `json:\"announcement_url,omitempty\"`\n\t// TODO(a.garipov): See if the frontend actually still cares about\n\t// nullability.\n\tCanAutoUpdate aghalg.NullBool `json:\"can_autoupdate,omitempty\"`\n}\n\n// maxVersionRespSize is the maximum length in bytes for version information\n// response.\nconst maxVersionRespSize datasize.ByteSize = 64 * datasize.KB\n\n// VersionInfo downloads the latest version information.  If forceRecheck is\n// false and there are cached results, those results are returned.\nfunc (u *Updater) VersionInfo(ctx context.Context, forceRecheck bool) (vi VersionInfo, err error) {\n\tu.mu.Lock()\n\tdefer u.mu.Unlock()\n\n\tnow := time.Now()\n\trecheckTime := u.prevCheckTime.Add(versionCheckPeriod)\n\tif !forceRecheck && now.Before(recheckTime) {\n\t\treturn u.prevCheckResult, u.prevCheckError\n\t}\n\n\tvcu := u.versionCheckURL\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, vcu, nil)\n\tif err != nil {\n\t\treturn VersionInfo{}, fmt.Errorf(\"constructing request to %s: %w\", vcu, err)\n\t}\n\n\tu.logger.DebugContext(ctx, \"requesting version data\", \"url\", vcu)\n\n\tresp, err := u.client.Do(req)\n\tif err != nil {\n\t\treturn VersionInfo{}, fmt.Errorf(\"requesting %s: %w\", vcu, err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()\n\n\tr := ioutil.LimitReader(resp.Body, maxVersionRespSize.Bytes())\n\n\t// This use of ReadAll is safe, because we just limited the appropriate\n\t// ReadCloser.\n\tbody, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn VersionInfo{}, fmt.Errorf(\"reading response from %s: %w\", vcu, err)\n\t}\n\n\tu.prevCheckTime = now\n\tu.prevCheckResult, u.prevCheckError = u.parseVersionResponse(ctx, body)\n\n\treturn u.prevCheckResult, u.prevCheckError\n}\n\nfunc (u *Updater) parseVersionResponse(ctx context.Context, data []byte) (VersionInfo, error) {\n\tinfo := VersionInfo{\n\t\tCanAutoUpdate: aghalg.NBFalse,\n\t}\n\tversionJSON := map[string]string{\n\t\t\"version\":          \"\",\n\t\t\"announcement\":     \"\",\n\t\t\"announcement_url\": \"\",\n\t}\n\terr := json.Unmarshal(data, &versionJSON)\n\tif err != nil {\n\t\treturn info, fmt.Errorf(\"version.json: %w\", err)\n\t}\n\n\tfor k, v := range versionJSON {\n\t\tif v == \"\" {\n\t\t\treturn info, fmt.Errorf(\"version.json: bad data: value for key %q is empty\", k)\n\t\t}\n\t}\n\n\tinfo.NewVersion = versionJSON[\"version\"]\n\tinfo.Announcement = versionJSON[\"announcement\"]\n\tinfo.AnnouncementURL = versionJSON[\"announcement_url\"]\n\n\tpackageURL, key, found := u.downloadURL(ctx, versionJSON)\n\tif !found {\n\t\treturn info, fmt.Errorf(\"version.json: no package URL: key %q not found in object\", key)\n\t}\n\n\tinfo.CanAutoUpdate = aghalg.BoolToNullBool(info.NewVersion != u.version)\n\n\tu.newVersion = info.NewVersion\n\tu.packageURL = packageURL\n\n\treturn info, nil\n}\n\n// downloadURL returns the download URL for current build as well as its key in\n// versionObj.  If the key is not found, it additionally prints an informative\n// log message.\nfunc (u *Updater) downloadURL(\n\tctx context.Context,\n\tversionObj map[string]string,\n) (dlURL, key string, ok bool) {\n\tif u.goarch == \"arm\" && u.goarm != \"\" {\n\t\tkey = fmt.Sprintf(\"download_%s_%sv%s\", u.goos, u.goarch, u.goarm)\n\t} else if isMIPS(u.goarch) && u.gomips != \"\" {\n\t\tkey = fmt.Sprintf(\"download_%s_%s_%s\", u.goos, u.goarch, u.gomips)\n\t} else {\n\t\tkey = fmt.Sprintf(\"download_%s_%s\", u.goos, u.goarch)\n\t}\n\n\tdlURL, ok = versionObj[key]\n\tif ok {\n\t\treturn dlURL, key, true\n\t}\n\n\tkeys := slices.Sorted(maps.Keys(versionObj))\n\n\tu.logger.ErrorContext(ctx, \"key not found\", \"missing\", key, \"got\", keys)\n\n\treturn \"\", key, false\n}\n\n// isMIPS returns true if arch is any MIPS architecture.\nfunc isMIPS(arch string) (ok bool) {\n\tswitch arch {\n\tcase\n\t\t\"mips\",\n\t\t\"mips64\",\n\t\t\"mips64le\",\n\t\t\"mipsle\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/updater/check_test.go",
    "content": "package updater_test\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghalg\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/updater\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUpdater_VersionInfo(t *testing.T) {\n\tconst jsonData = `{\n  \"version\": \"v0.103.0-beta.2\",\n  \"announcement\": \"AdGuard Home v0.103.0-beta.2 is now available!\",\n  \"announcement_url\": \"https://github.com/AdguardTeam/AdGuardHome/internal/releases\",\n  \"selfupdate_min_version\": \"v0.0\",\n  \"download_windows_amd64\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_windows_amd64.zip\",\n  \"download_windows_386\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_windows_386.zip\",\n  \"download_darwin_amd64\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_darwin_amd64.zip\",\n  \"download_darwin_386\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_darwin_386.zip\",\n  \"download_linux_amd64\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz\",\n  \"download_linux_386\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_386.tar.gz\",\n  \"download_linux_arm\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz\",\n  \"download_linux_armv5\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz\",\n  \"download_linux_armv6\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz\",\n  \"download_linux_armv7\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz\",\n  \"download_linux_arm64\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz\",\n  \"download_linux_mips\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz\",\n  \"download_linux_mipsle\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz\",\n  \"download_linux_mips64\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz\",\n  \"download_linux_mips64le\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz\",\n  \"download_freebsd_386\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz\",\n  \"download_freebsd_amd64\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz\",\n  \"download_freebsd_arm\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz\",\n  \"download_freebsd_armv5\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz\",\n  \"download_freebsd_armv6\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz\",\n  \"download_freebsd_armv7\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz\",\n  \"download_freebsd_arm64\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz\"\n}`\n\n\tcounter := 0\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tcounter++\n\t\t_, _ = w.Write([]byte(jsonData))\n\t}))\n\tt.Cleanup(srv.Close)\n\n\tsrvURL, err := url.Parse(srv.URL)\n\trequire.NoError(t, err)\n\n\tfakeURL := srvURL.JoinPath(\"adguardhome\", version.ChannelBeta, \"version.json\")\n\n\tu := updater.NewUpdater(&updater.Config{\n\t\tClient:             srv.Client(),\n\t\tLogger:             testLogger,\n\t\tCommandConstructor: testCmdCons,\n\t\tVersion:            \"v0.103.0-beta.1\",\n\t\tChannel:            version.ChannelBeta,\n\t\tGOARCH:             \"arm\",\n\t\tGOOS:               \"linux\",\n\t\tVersionCheckURL:    fakeURL,\n\t})\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\tinfo, err := u.VersionInfo(ctx, false)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, counter, 1)\n\tassert.Equal(t, \"v0.103.0-beta.2\", info.NewVersion)\n\tassert.Equal(t, \"AdGuard Home v0.103.0-beta.2 is now available!\", info.Announcement)\n\tassert.Equal(t, \"https://github.com/AdguardTeam/AdGuardHome/internal/releases\", info.AnnouncementURL)\n\tassert.Equal(t, aghalg.NBTrue, info.CanAutoUpdate)\n\n\tt.Run(\"cache_check\", func(t *testing.T) {\n\t\t_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), false)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, counter, 1)\n\t})\n\n\tt.Run(\"force_check\", func(t *testing.T) {\n\t\t_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), true)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, counter, 2)\n\t})\n\n\tt.Run(\"api_fail\", func(t *testing.T) {\n\t\tsrv.Close()\n\n\t\t_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), true)\n\t\tvar urlErr *url.Error\n\t\tassert.ErrorAs(t, err, &urlErr)\n\t})\n}\n\nfunc TestUpdater_VersionInfo_others(t *testing.T) {\n\tconst jsonData = `{\n  \"version\": \"v0.103.0-beta.2\",\n  \"announcement\": \"AdGuard Home v0.103.0-beta.2 is now available!\",\n  \"announcement_url\": \"https://github.com/AdguardTeam/AdGuardHome/internal/releases\",\n  \"selfupdate_min_version\": \"v0.0\",\n  \"download_linux_armv7\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz\",\n  \"download_linux_mips_softfloat\": \"https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz\"\n}`\n\n\tfakeClient, fakeURL := aghtest.StartHTTPServer(t, []byte(jsonData))\n\tfakeURL = fakeURL.JoinPath(\"adguardhome\", version.ChannelBeta, \"version.json\")\n\n\ttestCases := []struct {\n\t\tname string\n\t\tarch string\n\t\tarm  string\n\t\tmips string\n\t}{{\n\t\tname: \"ARM\",\n\t\tarch: \"arm\",\n\t\tarm:  \"7\",\n\t\tmips: \"\",\n\t}, {\n\t\tname: \"MIPS\",\n\t\tarch: \"mips\",\n\t\tmips: \"softfloat\",\n\t\tarm:  \"\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tu := updater.NewUpdater(&updater.Config{\n\t\t\tClient:             fakeClient,\n\t\t\tLogger:             testLogger,\n\t\t\tCommandConstructor: testCmdCons,\n\t\t\tVersion:            \"v0.103.0-beta.1\",\n\t\t\tChannel:            version.ChannelBeta,\n\t\t\tGOOS:               \"linux\",\n\t\t\tGOARCH:             tc.arch,\n\t\t\tGOARM:              tc.arm,\n\t\t\tGOMIPS:             tc.mips,\n\t\t\tVersionCheckURL:    fakeURL,\n\t\t})\n\n\t\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t\tinfo, err := u.VersionInfo(ctx, false)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, \"v0.103.0-beta.2\", info.NewVersion)\n\t\tassert.Equal(t, \"AdGuard Home v0.103.0-beta.2 is now available!\", info.Announcement)\n\t\tassert.Equal(t, \"https://github.com/AdguardTeam/AdGuardHome/internal/releases\", info.AnnouncementURL)\n\t\tassert.Equal(t, aghalg.NBTrue, info.CanAutoUpdate)\n\t}\n}\n"
  },
  {
    "path": "internal/updater/updater.go",
    "content": "// Package updater provides an updater for AdGuardHome.\npackage updater\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil/urlutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n)\n\n// Updater is the AdGuard Home updater.\ntype Updater struct {\n\tclient *http.Client\n\tlogger *slog.Logger\n\n\tcmdCons executil.CommandConstructor\n\n\tversion string\n\tchannel string\n\tgoarch  string\n\tgoos    string\n\tgoarm   string\n\tgomips  string\n\n\tworkDir         string\n\tconfName        string\n\texecPath        string\n\tversionCheckURL string\n\n\t// mu protects all fields below.\n\tmu *sync.RWMutex\n\n\t// TODO(a.garipov): See if all of these fields actually have to be in\n\t// this struct.\n\tcurrentExeName string // current binary executable\n\tupdateDir      string // \"workDir/agh-update-v0.103.0\"\n\tpackageName    string // \"workDir/agh-update-v0.103.0/pkg_name.tar.gz\"\n\tbackupDir      string // \"workDir/agh-backup\"\n\tbackupExeName  string // \"workDir/agh-backup/AdGuardHome[.exe]\"\n\tupdateExeName  string // \"workDir/agh-update-v0.103.0/AdGuardHome[.exe]\"\n\tunpackedFiles  []string\n\n\tnewVersion string\n\tpackageURL string\n\n\t// Cached fields to prevent too many API requests.\n\tprevCheckError  error\n\tprevCheckTime   time.Time\n\tprevCheckResult VersionInfo\n}\n\n// DefaultVersionURL returns the default URL for the version announcement.\nfunc DefaultVersionURL() *url.URL {\n\treturn &url.URL{\n\t\tScheme: urlutil.SchemeHTTPS,\n\t\tHost:   \"static.adtidy.org\",\n\t\tPath:   path.Join(\"adguardhome\", version.Channel(), \"version.json\"),\n\t}\n}\n\n// Config is the AdGuard Home updater configuration.\ntype Config struct {\n\t// Client is used to perform HTTP requests.  It must not be nil.\n\tClient *http.Client\n\n\t// Logger is used for logging the update process.  It must not be nil.\n\tLogger *slog.Logger\n\n\t// VersionCheckURL is URL to the latest version announcement.  It must not\n\t// be nil, see [DefaultVersionURL].\n\tVersionCheckURL *url.URL\n\n\t// CommandConstructor is used to run external commands.  It must not be nil.\n\tCommandConstructor executil.CommandConstructor\n\n\t// Version is the current AdGuard Home version.  It must not be empty.\n\tVersion string\n\n\t// Channel is the current AdGuard Home update channel.  It must be a valid\n\t// channel, see [version.ChannelBeta] and the related constants.\n\tChannel string\n\n\t// GOARCH is the current CPU architecture.  It must not be empty and must be\n\t// one of the supported architectures.\n\tGOARCH string\n\n\t// GOOS is the current operating system.  It must not be empty and must be\n\t// one of the supported OSs.\n\tGOOS string\n\n\t// GOARM is the current ARM variant, if any.  It must either be empty or be\n\t// a valid and supported GOARM value.\n\tGOARM string\n\n\t// GOMIPS is the current MIPS variant, if any.  It must either be empty or\n\t// be a valid and supported GOMIPS value.\n\tGOMIPS string\n\n\t// ConfName is the name of the current configuration file.  It must not be\n\t// empty.\n\tConfName string\n\n\t// WorkDir is the working directory that is used for temporary files.  It\n\t// must not be empty.\n\tWorkDir string\n\n\t// ExecPath is path to the executable file.  It must not be empty.\n\tExecPath string\n}\n\n// NewUpdater creates a new Updater.  conf must not be nil.\nfunc NewUpdater(conf *Config) *Updater {\n\treturn &Updater{\n\t\tclient: conf.Client,\n\t\tlogger: conf.Logger,\n\n\t\tcmdCons: conf.CommandConstructor,\n\n\t\tversion: conf.Version,\n\t\tchannel: conf.Channel,\n\t\tgoarch:  conf.GOARCH,\n\t\tgoos:    conf.GOOS,\n\t\tgoarm:   conf.GOARM,\n\t\tgomips:  conf.GOMIPS,\n\n\t\tconfName:        conf.ConfName,\n\t\tworkDir:         conf.WorkDir,\n\t\texecPath:        conf.ExecPath,\n\t\tversionCheckURL: conf.VersionCheckURL.String(),\n\n\t\tmu: &sync.RWMutex{},\n\t}\n}\n\n// Update performs the auto-update.  It returns an error if the update fails.\n// If firstRun is true, it assumes the configuration file doesn't exist.\nfunc (u *Updater) Update(ctx context.Context, firstRun bool) (err error) {\n\tu.mu.Lock()\n\tdefer u.mu.Unlock()\n\n\tu.logger.InfoContext(ctx, \"starting update\", \"first_run\", firstRun)\n\tdefer func() {\n\t\tu.logUpdateResult(ctx, err)\n\t}()\n\n\terr = u.prepare(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"preparing: %w\", err)\n\t}\n\n\tdefer u.clean(ctx)\n\n\terr = u.downloadPackageFile(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"downloading package file: %w\", err)\n\t}\n\n\terr = u.unpack(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unpacking: %w\", err)\n\t}\n\n\tif !firstRun {\n\t\terr = u.check(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"checking config: %w\", err)\n\t\t}\n\t}\n\n\terr = u.backup(ctx, firstRun)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"making backup: %w\", err)\n\t}\n\n\terr = u.replace(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"replacing: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// logUpdateResult logs the result of the update operation.\nfunc (u *Updater) logUpdateResult(ctx context.Context, err error) {\n\tif err != nil {\n\t\tu.logger.ErrorContext(ctx, \"update failed\", slogutil.KeyError, err)\n\n\t\treturn\n\t}\n\n\tu.logger.InfoContext(ctx, \"update finished\")\n}\n\n// NewVersion returns the available new version.\nfunc (u *Updater) NewVersion() (nv string) {\n\tu.mu.RLock()\n\tdefer u.mu.RUnlock()\n\n\treturn u.newVersion\n}\n\n// prepare fills all necessary fields in Updater object.\nfunc (u *Updater) prepare(ctx context.Context) (err error) {\n\tu.updateDir = filepath.Join(u.workDir, fmt.Sprintf(\"agh-update-%s\", u.newVersion))\n\n\t_, pkgNameOnly := filepath.Split(u.packageURL)\n\tif pkgNameOnly == \"\" {\n\t\treturn fmt.Errorf(\"invalid PackageURL: %q\", u.packageURL)\n\t}\n\n\tu.packageName = filepath.Join(u.updateDir, pkgNameOnly)\n\tu.backupDir = filepath.Join(u.workDir, \"agh-backup\")\n\n\tupdateExeName := \"AdGuardHome\"\n\tif u.goos == \"windows\" {\n\t\tupdateExeName = \"AdGuardHome.exe\"\n\t}\n\n\tu.backupExeName = filepath.Join(u.backupDir, filepath.Base(u.execPath))\n\tu.updateExeName = filepath.Join(u.updateDir, updateExeName)\n\n\tu.logger.InfoContext(\n\t\tctx,\n\t\t\"updating\",\n\t\t\"from\", version.Version(),\n\t\t\"to\", u.newVersion,\n\t\t\"package_url\", u.packageURL,\n\t)\n\n\tu.currentExeName = u.execPath\n\t_, err = os.Stat(u.currentExeName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checking %q: %w\", u.currentExeName, err)\n\t}\n\n\treturn nil\n}\n\n// unpack extracts the files from the downloaded archive.\nfunc (u *Updater) unpack(ctx context.Context) (err error) {\n\t_, pkgNameOnly := filepath.Split(u.packageURL)\n\n\tu.logger.InfoContext(ctx, \"unpacking package\", \"package_name\", pkgNameOnly)\n\tif strings.HasSuffix(pkgNameOnly, \".zip\") {\n\t\tu.unpackedFiles, err = u.unpackZip(ctx, u.packageName, u.updateDir)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\".zip unpack failed: %w\", err)\n\t\t}\n\t} else if strings.HasSuffix(pkgNameOnly, \".tar.gz\") {\n\t\tu.unpackedFiles, err = u.unpackTarGz(ctx, u.packageName, u.updateDir)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\".tar.gz unpack failed: %w\", err)\n\t\t}\n\t} else {\n\t\treturn fmt.Errorf(\"unknown package extension\")\n\t}\n\n\treturn nil\n}\n\n// check returns an error if the configuration file couldn't be used with the\n// version of AdGuard Home just downloaded.\nfunc (u *Updater) check(ctx context.Context) (err error) {\n\tu.logger.InfoContext(ctx, \"checking configuration\")\n\n\terr = copyFile(u.confName, filepath.Join(u.updateDir, \"AdGuardHome.yaml\"), aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copyFile() failed: %w\", err)\n\t}\n\n\tconst format = \"executing configuration check command: %w %d:\\n\" +\n\t\t\"below is the output of configuration check:\\n\" +\n\t\t\"%s\" +\n\t\t\"end of the output\"\n\n\tvar (\n\t\targs = []string{\"--check-config\"}\n\t\tbuf  bytes.Buffer\n\t)\n\n\tu.logger.DebugContext(ctx, \"executing\", \"cmd\", u.updateExeName, \"args\", args)\n\n\terr = executil.Run(\n\t\tctx,\n\t\tu.cmdCons,\n\t\t&executil.CommandConfig{\n\t\t\tPath:   u.updateExeName,\n\t\t\tArgs:   args,\n\t\t\tStdout: &buf,\n\t\t\tStderr: &buf,\n\t\t},\n\t)\n\tif err != nil {\n\t\tcode, _ := executil.ExitCodeFromError(err)\n\n\t\treturn fmt.Errorf(format, err, code, buf.Bytes())\n\t}\n\n\treturn nil\n}\n\n// backup makes a backup of the current configuration and supporting files.  It\n// ignores the configuration file if firstRun is true.\nfunc (u *Updater) backup(ctx context.Context, firstRun bool) (err error) {\n\tu.logger.InfoContext(ctx, \"backing up current configuration\")\n\n\t_ = os.Mkdir(u.backupDir, aghos.DefaultPermDir)\n\tif !firstRun {\n\t\terr = copyFile(u.confName, filepath.Join(u.backupDir, \"AdGuardHome.yaml\"), aghos.DefaultPermFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"copyFile() failed: %w\", err)\n\t\t}\n\t}\n\n\twd := u.workDir\n\terr = u.copySupportingFiles(ctx, u.unpackedFiles, wd, u.backupDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copySupportingFiles(%s, %s) failed: %w\", wd, u.backupDir, err)\n\t}\n\n\treturn nil\n}\n\n// replace moves the current executable with the updated one and also copies the\n// supporting files.\nfunc (u *Updater) replace(ctx context.Context) (err error) {\n\terr = u.copySupportingFiles(ctx, u.unpackedFiles, u.updateDir, u.workDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copySupportingFiles(%s, %s) failed: %w\", u.updateDir, u.workDir, err)\n\t}\n\n\tu.logger.InfoContext(\n\t\tctx,\n\t\t\"backing up current executable\",\n\t\t\"from\", u.currentExeName,\n\t\t\"to\", u.backupExeName,\n\t)\n\terr = os.Rename(u.currentExeName, u.backupExeName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif u.goos == \"windows\" {\n\t\t// Use copy, since renaming fails with \"File in use\" error.\n\t\terr = copyFile(u.updateExeName, u.currentExeName, aghos.DefaultPermExe)\n\t} else {\n\t\terr = os.Rename(u.updateExeName, u.currentExeName)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu.logger.InfoContext(\n\t\tctx,\n\t\t\"replacing current executable\",\n\t\t\"from\", u.updateExeName,\n\t\t\"to\", u.currentExeName,\n\t)\n\n\treturn nil\n}\n\n// clean removes the temporary directory itself and all it's contents.\nfunc (u *Updater) clean(ctx context.Context) {\n\terr := os.RemoveAll(u.updateDir)\n\tif err != nil {\n\t\tu.logger.WarnContext(ctx, \"removing update dir\", slogutil.KeyError, err)\n\t}\n}\n\n// MaxPackageFileSize is a maximum package file length in bytes.  The largest\n// package whose size is limited by this constant currently has the size of\n// approximately 9 MiB.\nconst MaxPackageFileSize = 32 * 1024 * 1024\n\n// Download package file and save it to disk\nfunc (u *Updater) downloadPackageFile(ctx context.Context) (err error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, u.packageURL, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constructing package request: %w\", err)\n\t}\n\n\tresp, err := u.client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"requesting package: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()\n\n\tr := ioutil.LimitReader(resp.Body, MaxPackageFileSize)\n\n\tu.logger.InfoContext(ctx, \"reading http body\")\n\n\t// This use of ReadAll is now safe, because we limited body's Reader.\n\tbody, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"io.ReadAll() failed: %w\", err)\n\t}\n\n\terr = os.Mkdir(u.updateDir, aghos.DefaultPermDir)\n\tif err != nil {\n\t\t// TODO(a.garipov):  Consider returning this error.\n\t\tu.logger.WarnContext(ctx, \"creating update dir\", slogutil.KeyError, err)\n\t}\n\n\tu.logger.InfoContext(ctx, \"saving package\", \"to\", u.packageName)\n\n\terr = os.WriteFile(u.packageName, body, aghos.DefaultPermFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"writing package file: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// unpackTarGzFile unpacks one file from a .tar.gz archive into outDir.  All\n// arguments must not be empty.\nfunc (u *Updater) unpackTarGzFile(\n\tctx context.Context,\n\toutDir string,\n\ttr *tar.Reader,\n\thdr *tar.Header,\n) (name string, err error) {\n\tname = filepath.Base(hdr.Name)\n\tif name == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\toutName := filepath.Join(outDir, name)\n\n\tif hdr.Typeflag == tar.TypeDir {\n\t\tif name == \"AdGuardHome\" {\n\t\t\t// Top-level AdGuardHome/.  Skip it.\n\t\t\t//\n\t\t\t// TODO(a.garipov): This whole package needs to be rewritten and\n\t\t\t// covered in more integration tests.  It has weird assumptions and\n\t\t\t// file mode issues.\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\terr = os.Mkdir(outName, os.FileMode(hdr.Mode&0o755))\n\t\tif err != nil && !errors.Is(err, os.ErrExist) {\n\t\t\treturn \"\", fmt.Errorf(\"creating directory %q: %w\", outName, err)\n\t\t}\n\n\t\tu.logger.InfoContext(ctx, \"created directory\", \"name\", outName)\n\n\t\treturn \"\", nil\n\t}\n\n\tif hdr.Typeflag != tar.TypeReg {\n\t\tu.logger.WarnContext(\n\t\t\tctx,\n\t\t\t\"unknown file type; skipping\",\n\t\t\t\"file_name\", name,\n\t\t\t\"type\", hdr.Typeflag,\n\t\t)\n\n\t\treturn \"\", nil\n\t}\n\n\tvar wc io.WriteCloser\n\twc, err = os.OpenFile(outName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode)&0o755)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"os.OpenFile(%s): %w\", outName, err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, wc.Close()) }()\n\n\t_, err = io.Copy(wc, tr)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"io.Copy(): %w\", err)\n\t}\n\n\tu.logger.InfoContext(ctx, \"created file\", \"name\", outName)\n\n\treturn name, nil\n}\n\n// unpackTarGz unpack all files from a .tar.gz archive to outDir.  Existing\n// files are overwritten.  All files are created inside outDir.  files are the\n// list of created files.\nfunc (u *Updater) unpackTarGz(\n\tctx context.Context,\n\ttarfile string,\n\toutDir string,\n) (files []string, err error) {\n\tf, err := os.Open(tarfile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"os.Open(): %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, f.Close()) }()\n\n\tgzReader, err := gzip.NewReader(f)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gzip.NewReader(): %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, gzReader.Close()) }()\n\n\ttarReader := tar.NewReader(gzReader)\n\tfor {\n\t\tvar hdr *tar.Header\n\t\thdr, err = tarReader.Next()\n\t\tif errors.Is(err, io.EOF) {\n\t\t\terr = nil\n\n\t\t\tbreak\n\t\t} else if err != nil {\n\t\t\terr = fmt.Errorf(\"tarReader.Next(): %w\", err)\n\n\t\t\tbreak\n\t\t}\n\n\t\tvar name string\n\t\tname, err = u.unpackTarGzFile(ctx, outDir, tarReader, hdr)\n\n\t\tif name != \"\" {\n\t\t\tfiles = append(files, name)\n\t\t}\n\t}\n\n\treturn files, err\n}\n\n// unpackZipFile unpacks one file from a .zip archive into outDir.  All\n// arguments must not be empty.\nfunc (u *Updater) unpackZipFile(\n\tctx context.Context,\n\toutDir string,\n\tzf *zip.File,\n) (name string, err error) {\n\tvar rc io.ReadCloser\n\trc, err = zf.Open()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"zip file Open(): %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, rc.Close()) }()\n\n\tfi := zf.FileInfo()\n\tname = fi.Name()\n\tif name == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\toutputName := filepath.Join(outDir, name)\n\tif fi.IsDir() {\n\t\tif name == \"AdGuardHome\" {\n\t\t\t// Top-level AdGuardHome/.  Skip it.\n\t\t\t//\n\t\t\t// TODO(a.garipov): See the similar TODO in\n\t\t\t// [Updater.unpackTarGzFile].\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\terr = os.Mkdir(outputName, fi.Mode())\n\t\tif err != nil && !errors.Is(err, os.ErrExist) {\n\t\t\treturn \"\", fmt.Errorf(\"creating directory %q: %w\", outputName, err)\n\t\t}\n\n\t\tu.logger.InfoContext(ctx, \"created directory\", \"name\", outputName)\n\n\t\treturn \"\", nil\n\t}\n\n\tvar wc io.WriteCloser\n\twc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"os.OpenFile(): %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, wc.Close()) }()\n\n\t_, err = io.Copy(wc, rc)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"io.Copy(): %w\", err)\n\t}\n\n\tu.logger.InfoContext(ctx, \"created file\", \"name\", outputName)\n\n\treturn name, nil\n}\n\n// unpackZip unpack all files from a .zip archive to outDir.  Existing files are\n// overwritten.  All files are created inside outDir.  files are the list of\n// created files.\nfunc (u *Updater) unpackZip(\n\tctx context.Context,\n\tzipfile string,\n\toutDir string,\n) (files []string, err error) {\n\tzrc, err := zip.OpenReader(zipfile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"zip.OpenReader(): %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, zrc.Close()) }()\n\n\tfor _, zf := range zrc.File {\n\t\tvar name string\n\t\tname, err = u.unpackZipFile(ctx, outDir, zf)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif name != \"\" {\n\t\t\tfiles = append(files, name)\n\t\t}\n\t}\n\n\treturn files, err\n}\n\n// copyFile copies a file from src to dst with the specified permissions.\nfunc copyFile(src, dst string, perm fs.FileMode) (err error) {\n\td, err := os.ReadFile(src)\n\tif err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn err\n\t}\n\n\terr = os.WriteFile(dst, d, perm)\n\tif err != nil {\n\t\t// Don't wrap the error, since it's informative enough as is.\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// copySupportingFiles copies each file specified in files from srcdir to\n// dstdir.  If a file specified as a path, only the name of the file is used.\n// It skips AdGuardHome, AdGuardHome.exe, and AdGuardHome.yaml.\nfunc (u *Updater) copySupportingFiles(\n\tctx context.Context,\n\tfiles []string,\n\tsrcdir string,\n\tdstdir string,\n) (err error) {\n\tfor _, f := range files {\n\t\t_, name := filepath.Split(f)\n\t\tif name == \"AdGuardHome\" || name == \"AdGuardHome.exe\" || name == \"AdGuardHome.yaml\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tsrc := filepath.Join(srcdir, name)\n\t\tdst := filepath.Join(dstdir, name)\n\n\t\terr = copyFile(src, dst, aghos.DefaultPermFile)\n\t\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn err\n\t\t}\n\n\t\tu.logger.InfoContext(ctx, \"copied\", \"from\", src, \"to\", dst)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/updater/updater_internal_test.go",
    "content": "package updater\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghtest\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUpdater_internal(t *testing.T) {\n\twd := t.TempDir()\n\n\texePathUnix := filepath.Join(wd, \"AdGuardHome.exe\")\n\texePathWindows := filepath.Join(wd, \"AdGuardHome\")\n\tyamlPath := filepath.Join(wd, \"AdGuardHome.yaml\")\n\treadmePath := filepath.Join(wd, \"README.md\")\n\tlicensePath := filepath.Join(wd, \"LICENSE.txt\")\n\n\trequire.NoError(t, os.WriteFile(exePathUnix, []byte(\"AdGuardHome.exe\"), 0o755))\n\trequire.NoError(t, os.WriteFile(exePathWindows, []byte(\"AdGuardHome\"), 0o755))\n\trequire.NoError(t, os.WriteFile(yamlPath, []byte(\"AdGuardHome.yaml\"), 0o644))\n\trequire.NoError(t, os.WriteFile(readmePath, []byte(\"README.md\"), 0o644))\n\trequire.NoError(t, os.WriteFile(licensePath, []byte(\"LICENSE.txt\"), 0o644))\n\n\ttestCases := []struct {\n\t\tname        string\n\t\texeName     string\n\t\tos          string\n\t\tarchiveName string\n\t}{{\n\t\tname:        \"unix\",\n\t\tos:          \"linux\",\n\t\texeName:     \"AdGuardHome\",\n\t\tarchiveName: \"AdGuardHome.tar.gz\",\n\t}, {\n\t\tname:        \"windows\",\n\t\tos:          \"windows\",\n\t\texeName:     \"AdGuardHome.exe\",\n\t\tarchiveName: \"AdGuardHome.zip\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\texePath := filepath.Join(wd, tc.exeName)\n\n\t\t// Start server for returning package file.\n\t\tpkgData, err := os.ReadFile(filepath.Join(\"testdata\", tc.archiveName))\n\t\trequire.NoError(t, err)\n\n\t\tfakeClient, fakeURL := aghtest.StartHTTPServer(t, pkgData)\n\t\tfakeURL = fakeURL.JoinPath(tc.archiveName)\n\n\t\tu := NewUpdater(&Config{\n\t\t\tClient:             fakeClient,\n\t\t\tLogger:             slogutil.NewDiscardLogger(),\n\t\t\tCommandConstructor: executil.EmptyCommandConstructor{},\n\t\t\tGOOS:               tc.os,\n\t\t\tVersion:            \"v0.103.0\",\n\t\t\tExecPath:           exePath,\n\t\t\tWorkDir:            wd,\n\t\t\tConfName:           yamlPath,\n\t\t\t// TODO(e.burkov):  Rewrite the test to use a fake version check\n\t\t\t// URL with a fake URLs for the package files.\n\t\t\tVersionCheckURL: &url.URL{},\n\t\t})\n\n\t\tu.newVersion = \"v0.103.1\"\n\t\tu.packageURL = fakeURL.String()\n\n\t\trequire.NoError(t, u.prepare(newCtx(t)))\n\t\trequire.NoError(t, u.downloadPackageFile(newCtx(t)))\n\t\trequire.NoError(t, u.unpack(newCtx(t)))\n\t\trequire.NoError(t, u.backup(newCtx(t), false))\n\t\trequire.NoError(t, u.replace(newCtx(t)))\n\n\t\tu.clean(newCtx(t))\n\n\t\trequire.True(t, t.Run(\"backup\", func(t *testing.T) {\n\t\t\tvar d []byte\n\t\t\td, err = os.ReadFile(filepath.Join(wd, \"agh-backup\", \"AdGuardHome.yaml\"))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, \"AdGuardHome.yaml\", string(d))\n\n\t\t\td, err = os.ReadFile(filepath.Join(wd, \"agh-backup\", tc.exeName))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.exeName, string(d))\n\t\t}))\n\n\t\trequire.True(t, t.Run(\"updated\", func(t *testing.T) {\n\t\t\tvar d []byte\n\t\t\td, err = os.ReadFile(exePath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, \"1\", string(d))\n\n\t\t\td, err = os.ReadFile(readmePath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, \"2\", string(d))\n\n\t\t\td, err = os.ReadFile(licensePath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, \"3\", string(d))\n\n\t\t\td, err = os.ReadFile(yamlPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, \"AdGuardHome.yaml\", string(d))\n\t\t}))\n\t}\n}\n\n// newCtx is a helper that returns a new context with a timeout.\nfunc newCtx(tb testing.TB) (ctx context.Context) {\n\treturn testutil.ContextWithTimeout(tb, 1*time.Second)\n}\n"
  },
  {
    "path": "internal/updater/updater_test.go",
    "content": "package updater_test\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/updater\"\n\t\"github.com/AdguardTeam/AdGuardHome/internal/version\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/AdguardTeam/golibs/testutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testTimeout is the common timeout for tests.\nconst testTimeout = 1 * time.Second\n\n// testLogger is the common logger for tests.\nvar testLogger = slogutil.NewDiscardLogger()\n\n// testCmdCons is the common command constructor for tests.\nvar testCmdCons = executil.EmptyCommandConstructor{}\n\nfunc TestUpdater_Update(t *testing.T) {\n\tconst jsonData = `{\n  \"version\": \"v0.103.0-beta.2\",\n  \"announcement\": \"AdGuard Home v0.103.0-beta.2 is now available!\",\n  \"announcement_url\": \"https://github.com/AdguardTeam/AdGuardHome/internal/releases\",\n  \"selfupdate_min_version\": \"v0.0\",\n  \"download_linux_amd64\": \"%s\"\n}`\n\n\tconst packagePath = \"/AdGuardHome.tar.gz\"\n\n\twd := t.TempDir()\n\n\texePath := filepath.Join(wd, \"AdGuardHome\")\n\tyamlPath := filepath.Join(wd, \"AdGuardHome.yaml\")\n\treadmePath := filepath.Join(wd, \"README.md\")\n\tlicensePath := filepath.Join(wd, \"LICENSE.txt\")\n\n\trequire.NoError(t, os.WriteFile(exePath, []byte(\"AdGuardHome\"), 0o755))\n\trequire.NoError(t, os.WriteFile(yamlPath, []byte(\"AdGuardHome.yaml\"), 0o644))\n\trequire.NoError(t, os.WriteFile(readmePath, []byte(\"README.md\"), 0o644))\n\trequire.NoError(t, os.WriteFile(licensePath, []byte(\"LICENSE.txt\"), 0o644))\n\n\tpkgData, err := os.ReadFile(\"testdata/AdGuardHome_unix.tar.gz\")\n\trequire.NoError(t, err)\n\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(packagePath, func(w http.ResponseWriter, _ *http.Request) {\n\t\t_, _ = w.Write(pkgData)\n\t})\n\n\tversionPath := path.Join(\"/adguardhome\", version.ChannelBeta, \"version.json\")\n\tmux.HandleFunc(versionPath, func(w http.ResponseWriter, r *http.Request) {\n\t\tvar u string\n\t\tu, err = url.JoinPath(\"http://\", r.Host, packagePath)\n\t\trequire.NoError(t, err)\n\n\t\t_, _ = fmt.Fprintf(w, jsonData, u)\n\t})\n\n\tsrv := httptest.NewServer(mux)\n\tt.Cleanup(srv.Close)\n\n\tsrvURL, err := url.Parse(srv.URL)\n\trequire.NoError(t, err)\n\n\tversionCheckURL := srvURL.JoinPath(versionPath)\n\trequire.NoError(t, err)\n\n\tu := updater.NewUpdater(&updater.Config{\n\t\tClient:             srv.Client(),\n\t\tLogger:             testLogger,\n\t\tCommandConstructor: testCmdCons,\n\t\tGOARCH:             \"amd64\",\n\t\tGOOS:               \"linux\",\n\t\tVersion:            \"v0.103.0\",\n\t\tConfName:           yamlPath,\n\t\tWorkDir:            wd,\n\t\tExecPath:           exePath,\n\t\tVersionCheckURL:    versionCheckURL,\n\t})\n\n\tctx := testutil.ContextWithTimeout(t, testTimeout)\n\t_, err = u.VersionInfo(ctx, false)\n\trequire.NoError(t, err)\n\n\tctx = testutil.ContextWithTimeout(t, testTimeout)\n\terr = u.Update(ctx, true)\n\trequire.NoError(t, err)\n\n\t// check backup files\n\td, err := os.ReadFile(filepath.Join(wd, \"agh-backup\", \"LICENSE.txt\"))\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"LICENSE.txt\", string(d))\n\n\td, err = os.ReadFile(filepath.Join(wd, \"agh-backup\", \"README.md\"))\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"README.md\", string(d))\n\n\t// check updated files\n\t_, err = os.Stat(exePath)\n\trequire.NoError(t, err)\n\n\td, err = os.ReadFile(readmePath)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"2\", string(d))\n\n\td, err = os.ReadFile(licensePath)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"3\", string(d))\n\n\td, err = os.ReadFile(yamlPath)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"AdGuardHome.yaml\", string(d))\n\n\tt.Run(\"config_check\", func(t *testing.T) {\n\t\t// TODO(s.chzhen):  Test on Windows also.\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tt.Skip(\"skipping config check test on windows\")\n\t\t}\n\n\t\terr = u.Update(testutil.ContextWithTimeout(t, testTimeout), false)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"api_fail\", func(t *testing.T) {\n\t\tsrv.Close()\n\n\t\terr = u.Update(testutil.ContextWithTimeout(t, testTimeout), true)\n\n\t\tvar urlErr *url.Error\n\t\tassert.ErrorAs(t, err, &urlErr)\n\t})\n}\n"
  },
  {
    "path": "internal/version/norace.go",
    "content": "//go:build !race\n\npackage version\n\nconst isRace = false\n"
  },
  {
    "path": "internal/version/race.go",
    "content": "//go:build race\n\npackage version\n\nconst isRace = true\n"
  },
  {
    "path": "internal/version/version.go",
    "content": "// Package version contains AdGuard Home version information.\npackage version\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/stringutil\"\n)\n\n// Channel constants.\nconst (\n\tChannelBeta        = \"beta\"\n\tChannelCandidate   = \"candidate\"\n\tChannelDevelopment = \"development\"\n\tChannelEdge        = \"edge\"\n\tChannelRelease     = \"release\"\n)\n\n// These are set by the linker.  Unfortunately we cannot set constants during\n// linking, and Go doesn't have a concept of immutable variables, so to be\n// thorough we have to only export them through getters.\n//\n// TODO(a.garipov): Find out if we can get GOARM and GOMIPS values the same way\n// we can GOARCH and GOOS.\nvar (\n\tchannel    string = ChannelDevelopment\n\tgoarm      string\n\tgomips     string\n\tversion    string\n\tcommittime string\n)\n\n// Channel returns the current AdGuard Home release channel.\nfunc Channel() (v string) {\n\treturn channel\n}\n\n// vFmtFull defines the format of full version output.\nconst vFmtFull = \"AdGuard Home, version %s\"\n\n// Full returns the full current version of AdGuard Home.\nfunc Full() (v string) {\n\treturn fmt.Sprintf(vFmtFull, version)\n}\n\n// GOARM returns the GOARM value used to build the current AdGuard Home release.\nfunc GOARM() (v string) {\n\treturn goarm\n}\n\n// GOMIPS returns the GOMIPS value used to build the current AdGuard Home\n// release.\nfunc GOMIPS() (v string) {\n\treturn gomips\n}\n\n// Version returns the AdGuard Home build version.\nfunc Version() (v string) {\n\treturn version\n}\n\n// fmtModule returns formatted information about module.  The result looks like:\n//\n//\tgithub.com/Username/module@v1.2.3 (sum: someHASHSUM=)\nfunc fmtModule(m *debug.Module) (formatted string) {\n\tif m == nil {\n\t\treturn \"\"\n\t}\n\n\tif repl := m.Replace; repl != nil {\n\t\treturn fmtModule(repl)\n\t}\n\n\tb := &strings.Builder{}\n\n\tstringutil.WriteToBuilder(b, m.Path)\n\tif ver := m.Version; ver != \"\" {\n\t\tsep := \"@\"\n\t\tif ver == \"(devel)\" {\n\t\t\tsep = \" \"\n\t\t}\n\n\t\tstringutil.WriteToBuilder(b, sep, ver)\n\t}\n\n\tif sum := m.Sum; sum != \"\" {\n\t\tstringutil.WriteToBuilder(b, \"(sum: \", sum, \")\")\n\t}\n\n\treturn b.String()\n}\n\n// Constants defining the headers of build information message.\nconst (\n\tvFmtAGHHdr       = \"AdGuard Home\"\n\tvFmtVerHdr       = \"Version: \"\n\tvFmtSchemaVerHdr = \"Schema version: \"\n\tvFmtChanHdr      = \"Channel: \"\n\tvFmtGoHdr        = \"Go version: \"\n\tvFmtTimeHdr      = \"Commit time: \"\n\tvFmtRaceHdr      = \"Race: \"\n\tvFmtGOOSHdr      = \"GOOS: \" + runtime.GOOS\n\tvFmtGOARCHHdr    = \"GOARCH: \" + runtime.GOARCH\n\tvFmtGOARMHdr     = \"GOARM: \"\n\tvFmtGOMIPSHdr    = \"GOMIPS: \"\n\tvFmtDepsHdr      = \"Dependencies:\"\n)\n\n// Verbose returns formatted build information.  Output example:\n//\n//\tAdGuard Home\n//\tVersion: v0.105.3\n//\tSchema version: 27\n//\tChannel: development\n//\tGo version: go1.15.3\n//\tBuild time: 2021-03-30T16:26:08Z+0300\n//\tGOOS: darwin\n//\tGOARCH: amd64\n//\tRace: false\n//\tMain module:\n//\t        ...\n//\tDependencies:\n//\t        ...\n//\n// TODO(e.burkov): Make it write into passed io.Writer.\nfunc Verbose(schemaVersion uint) (v string) {\n\tb := &strings.Builder{}\n\n\tconst nl = \"\\n\"\n\tstringutil.WriteToBuilder(b, vFmtAGHHdr, nl)\n\tstringutil.WriteToBuilder(b, vFmtVerHdr, version, nl)\n\n\tschemaVerStr := strconv.FormatUint(uint64(schemaVersion), 10)\n\tstringutil.WriteToBuilder(b, vFmtSchemaVerHdr, schemaVerStr, nl)\n\n\tstringutil.WriteToBuilder(b, vFmtChanHdr, channel, nl)\n\tstringutil.WriteToBuilder(b, vFmtGoHdr, runtime.Version(), nl)\n\n\twriteCommitTime(b)\n\n\tstringutil.WriteToBuilder(b, vFmtGOOSHdr, nl)\n\tstringutil.WriteToBuilder(b, vFmtGOARCHHdr, nl)\n\n\tif goarm != \"\" {\n\t\tstringutil.WriteToBuilder(b, vFmtGOARMHdr, \"v\", goarm, nl)\n\t} else if gomips != \"\" {\n\t\tstringutil.WriteToBuilder(b, vFmtGOMIPSHdr, gomips, nl)\n\t}\n\n\tstringutil.WriteToBuilder(b, vFmtRaceHdr, strconv.FormatBool(isRace), nl)\n\n\tinfo, ok := debug.ReadBuildInfo()\n\tif !ok {\n\t\treturn b.String()\n\t}\n\n\tif len(info.Deps) == 0 {\n\t\treturn b.String()\n\t}\n\n\tstringutil.WriteToBuilder(b, vFmtDepsHdr, nl)\n\tfor _, dep := range info.Deps {\n\t\tif depStr := fmtModule(dep); depStr != \"\" {\n\t\t\tstringutil.WriteToBuilder(b, \"\\t\", depStr, nl)\n\t\t}\n\t}\n\n\treturn b.String()\n}\n\nfunc writeCommitTime(b *strings.Builder) {\n\tif committime == \"\" {\n\t\treturn\n\t}\n\n\tcommitTimeUnix, err := strconv.ParseInt(committime, 10, 64)\n\tif err != nil {\n\t\tstringutil.WriteToBuilder(b, vFmtTimeHdr, fmt.Sprintf(\"parse error: %s\", err), \"\\n\")\n\t} else {\n\t\tstringutil.WriteToBuilder(b, vFmtTimeHdr, time.Unix(commitTimeUnix, 0).String(), \"\\n\")\n\t}\n}\n"
  },
  {
    "path": "internal/whois/whois.go",
    "content": "// Package whois provides WHOIS functionality.\npackage whois\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghnet\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/bluele/gcache\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\nconst (\n\t// DefaultServer is the default WHOIS server.\n\tDefaultServer = \"whois.arin.net\"\n\n\t// DefaultPort is the default port for WHOIS requests.\n\tDefaultPort = 43\n)\n\n// Interface provides WHOIS functionality.\ntype Interface interface {\n\t// Process makes WHOIS request and returns WHOIS information or nil.\n\t// changed indicates that Info was updated since last request.\n\tProcess(ctx context.Context, ip netip.Addr) (info *Info, changed bool)\n}\n\n// Empty is an empty [Interface] implementation which does nothing.\ntype Empty struct{}\n\n// type check\nvar _ Interface = (*Empty)(nil)\n\n// Process implements the [Interface] interface for Empty.\nfunc (Empty) Process(_ context.Context, _ netip.Addr) (info *Info, changed bool) {\n\treturn nil, false\n}\n\n// Config is the configuration structure for Default.\ntype Config struct {\n\t// Logger is used for logging the operation of the WHOIS lookup queries.  It\n\t// must not be nil.\n\tLogger *slog.Logger\n\n\t// DialContext is used to create TCP connections to WHOIS servers.\n\tDialContext aghnet.DialContextFunc\n\n\t// ServerAddr is the address of the WHOIS server.\n\tServerAddr string\n\n\t// Timeout is the timeout for WHOIS requests.\n\tTimeout time.Duration\n\n\t// CacheTTL is the Time to Live duration for cached IP addresses.\n\tCacheTTL time.Duration\n\n\t// MaxConnReadSize is an upper limit in bytes for reading from net.Conn.\n\tMaxConnReadSize uint64\n\n\t// MaxRedirects is the maximum redirects count.\n\tMaxRedirects int\n\n\t// MaxInfoLen is the maximum length of Info fields returned by Process.\n\tMaxInfoLen int\n\n\t// CacheSize is the maximum size of the cache.  It must be greater than\n\t// zero.\n\tCacheSize int\n\n\t// Port is the port for WHOIS requests.\n\tPort uint16\n}\n\n// Default is the default WHOIS information processor.\ntype Default struct {\n\t// logger is used for logging the operation of the WHOIS lookup queries.  It\n\t// must not be nil.\n\tlogger *slog.Logger\n\n\t// cache is the cache containing IP addresses of clients.  An active IP\n\t// address is resolved once again after it expires.  If IP address couldn't\n\t// be resolved, it stays here for some time to prevent further attempts to\n\t// resolve the same IP.\n\tcache gcache.Cache\n\n\t// dialContext is used to create TCP connections to WHOIS servers.\n\tdialContext aghnet.DialContextFunc\n\n\t// serverAddr is the address of the WHOIS server.\n\tserverAddr string\n\n\t// portStr is the port for WHOIS requests.\n\tportStr string\n\n\t// timeout is the timeout for WHOIS requests.\n\ttimeout time.Duration\n\n\t// cacheTTL is the Time to Live duration for cached IP addresses.\n\tcacheTTL time.Duration\n\n\t// maxConnReadSize is an upper limit in bytes for reading from net.Conn.\n\tmaxConnReadSize uint64\n\n\t// maxRedirects is the maximum redirects count.\n\tmaxRedirects int\n\n\t// maxInfoLen is the maximum length of Info fields returned by Process.\n\tmaxInfoLen int\n}\n\n// New returns a new default WHOIS information processor.  conf must not be\n// nil.\nfunc New(conf *Config) (w *Default) {\n\treturn &Default{\n\t\tlogger:          conf.Logger,\n\t\tserverAddr:      conf.ServerAddr,\n\t\tdialContext:     conf.DialContext,\n\t\ttimeout:         conf.Timeout,\n\t\tcache:           gcache.New(conf.CacheSize).LRU().Build(),\n\t\tmaxConnReadSize: conf.MaxConnReadSize,\n\t\tmaxRedirects:    conf.MaxRedirects,\n\t\tportStr:         strconv.Itoa(int(conf.Port)),\n\t\tmaxInfoLen:      conf.MaxInfoLen,\n\t\tcacheTTL:        conf.CacheTTL,\n\t}\n}\n\n// trimValue trims s and replaces the last 3 characters of the cut with \"...\"\n// to fit into max.  max must be greater than 3.\nfunc trimValue(s string, max int) string {\n\tif len(s) <= max {\n\t\treturn s\n\t}\n\n\treturn s[:max-3] + \"...\"\n}\n\n// isWHOISComment returns true if the data is empty or is a WHOIS comment.\nfunc isWHOISComment(data []byte) (ok bool) {\n\treturn len(data) == 0 || data[0] == '#' || data[0] == '%'\n}\n\n// whoisParse parses a subset of plain-text data from the WHOIS response into a\n// string map.  It trims values of the returned map to maxLen.\nfunc whoisParse(data []byte, maxLen int) (info map[string]string) {\n\tinfo = map[string]string{}\n\n\tvar orgname string\n\tlines := bytes.Split(data, []byte(\"\\n\"))\n\tfor _, l := range lines {\n\t\tif isWHOISComment(l) {\n\t\t\tcontinue\n\t\t}\n\n\t\tbefore, after, found := bytes.Cut(l, []byte(\":\"))\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tkey := strings.ToLower(string(before))\n\t\tval := strings.TrimSpace(string(after))\n\t\tif val == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"orgname\", \"org-name\":\n\t\t\tkey = \"orgname\"\n\t\t\tval = trimValue(val, maxLen)\n\t\t\torgname = val\n\t\tcase \"city\", \"country\":\n\t\t\tval = trimValue(val, maxLen)\n\t\tcase \"descr\", \"netname\":\n\t\t\tkey = \"orgname\"\n\t\t\tval = cmp.Or(orgname, val)\n\t\t\torgname = val\n\t\tcase \"whois\":\n\t\t\tkey = \"whois\"\n\t\tcase \"referralserver\":\n\t\t\tkey = \"whois\"\n\t\t\tval = strings.TrimPrefix(val, \"whois://\")\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tinfo[key] = val\n\t}\n\n\treturn info\n}\n\n// query sends request to a server and returns the response or error.\nfunc (w *Default) query(ctx context.Context, target, serverAddr string) (data []byte, err error) {\n\taddr, _, _ := net.SplitHostPort(serverAddr)\n\tif addr == DefaultServer {\n\t\t// Display type flags for query.\n\t\t//\n\t\t// See https://www.arin.net/resources/registry/whois/rws/api/#nicname-whois-queries.\n\t\ttarget = \"n + \" + target\n\t}\n\n\tconn, err := w.dialContext(ctx, \"tcp\", serverAddr)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\tdefer func() { err = errors.WithDeferred(err, conn.Close()) }()\n\n\tr := ioutil.LimitReader(conn, w.maxConnReadSize)\n\n\t_ = conn.SetDeadline(time.Now().Add(w.timeout))\n\t_, err = io.WriteString(conn, target+\"\\r\\n\")\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\t// This use of ReadAll is now safe, because we limited the conn Reader.\n\tdata, err = io.ReadAll(r)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\treturn data, nil\n}\n\n// queryAll queries WHOIS server and handles redirects.\nfunc (w *Default) queryAll(ctx context.Context, target string) (info map[string]string, err error) {\n\tserver := net.JoinHostPort(w.serverAddr, w.portStr)\n\n\tfor range w.maxRedirects {\n\t\tvar data []byte\n\t\tdata, err = w.query(ctx, target, server)\n\t\tif err != nil {\n\t\t\t// Don't wrap the error since it's informative enough as is.\n\t\t\treturn nil, err\n\t\t}\n\n\t\tw.logger.DebugContext(\n\t\t\tctx,\n\t\t\t\"received response\",\n\t\t\t\"size\", datasize.ByteSize(len(data)),\n\t\t\t\"source\", server,\n\t\t\t\"target\", target,\n\t\t)\n\n\t\tinfo = whoisParse(data, w.maxInfoLen)\n\t\tredir, ok := info[\"whois\"]\n\t\tif !ok {\n\t\t\treturn info, nil\n\t\t}\n\n\t\tredir = strings.ToLower(redir)\n\n\t\t_, _, err = net.SplitHostPort(redir)\n\t\tif err != nil {\n\t\t\tserver = net.JoinHostPort(redir, w.portStr)\n\t\t} else {\n\t\t\tserver = redir\n\t\t}\n\n\t\tw.logger.DebugContext(ctx, \"redirected\", \"destination\", redir, \"target\", target)\n\t}\n\n\treturn nil, fmt.Errorf(\"whois: redirect loop\")\n}\n\n// type check\nvar _ Interface = (*Default)(nil)\n\n// Process makes WHOIS request and returns WHOIS information or nil.  changed\n// indicates that Info was updated since last request.\nfunc (w *Default) Process(ctx context.Context, ip netip.Addr) (wi *Info, changed bool) {\n\tif netutil.IsSpecialPurpose(ip) {\n\t\treturn nil, false\n\t}\n\n\twi, expired := w.findInCache(ctx, ip)\n\tif wi != nil && !expired {\n\t\t// Don't return an empty struct so that the frontend doesn't get\n\t\t// confused.\n\t\tif (*wi == Info{}) {\n\t\t\treturn nil, false\n\t\t}\n\n\t\treturn wi, false\n\t}\n\n\treturn w.requestInfo(ctx, ip, wi)\n}\n\n// requestInfo makes WHOIS request and returns WHOIS info.  changed is false if\n// received information is equal to cached.\nfunc (w *Default) requestInfo(\n\tctx context.Context,\n\tip netip.Addr,\n\tcached *Info,\n) (wi *Info, changed bool) {\n\tvar info Info\n\n\tdefer func() {\n\t\titem := toCacheItem(info, w.cacheTTL)\n\t\terr := w.cache.Set(ip, item)\n\t\tif err != nil {\n\t\t\tw.logger.DebugContext(ctx, \"adding item to cache\", \"key\", ip, slogutil.KeyError, err)\n\t\t}\n\t}()\n\n\tkv, err := w.queryAll(ctx, ip.String())\n\tif err != nil {\n\t\tw.logger.DebugContext(ctx, \"querying\", \"target\", ip, slogutil.KeyError, err)\n\n\t\treturn nil, true\n\t}\n\n\tinfo = Info{\n\t\tCity:    kv[\"city\"],\n\t\tCountry: kv[\"country\"],\n\t\tOrgname: kv[\"orgname\"],\n\t}\n\n\tchanged = cached == nil || info != *cached\n\n\t// Don't return an empty struct so that the frontend doesn't get confused.\n\tif (info == Info{}) {\n\t\treturn nil, changed\n\t}\n\n\treturn &info, changed\n}\n\n// findInCache finds Info in the cache.  expired indicates that Info is valid.\nfunc (w *Default) findInCache(ctx context.Context, ip netip.Addr) (wi *Info, expired bool) {\n\tval, err := w.cache.Get(ip)\n\tif err != nil {\n\t\tif !errors.Is(err, gcache.KeyNotFoundError) {\n\t\t\tw.logger.DebugContext(\n\t\t\t\tctx,\n\t\t\t\t\"retrieving item from cache\",\n\t\t\t\t\"key\", ip,\n\t\t\t\tslogutil.KeyError, err,\n\t\t\t)\n\t\t}\n\n\t\treturn nil, false\n\t}\n\n\treturn fromCacheItem(val.(*cacheItem))\n}\n\n// Info is the filtered WHOIS data for a runtime client.\ntype Info struct {\n\tCity    string `json:\"city,omitempty\"`\n\tCountry string `json:\"country,omitempty\"`\n\tOrgname string `json:\"orgname,omitempty\"`\n}\n\n// Clone returns a deep copy of the WHOIS info.\nfunc (i *Info) Clone() (c *Info) {\n\tif i == nil {\n\t\treturn nil\n\t}\n\n\treturn &Info{\n\t\tCity:    i.City,\n\t\tCountry: i.Country,\n\t\tOrgname: i.Orgname,\n\t}\n}\n\n// cacheItem represents an item that we will store in the cache.\ntype cacheItem struct {\n\t// expiry is the time when cacheItem will expire.\n\texpiry time.Time\n\n\t// info is the WHOIS data for a runtime client.\n\tinfo *Info\n}\n\n// toCacheItem creates a cached item from a WHOIS info and Time to Live\n// duration.\nfunc toCacheItem(info Info, ttl time.Duration) (item *cacheItem) {\n\treturn &cacheItem{\n\t\texpiry: time.Now().Add(ttl),\n\t\tinfo:   &info,\n\t}\n}\n\n// fromCacheItem creates a WHOIS info from the cached item.  expired indicates\n// that WHOIS info is valid.  item must not be nil.\nfunc fromCacheItem(item *cacheItem) (info *Info, expired bool) {\n\tif time.Now().After(item.expiry) {\n\t\treturn item.info, true\n\t}\n\n\treturn item.info, false\n}\n"
  },
  {
    "path": "internal/whois/whois_test.go",
    "content": "package whois_test\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/whois\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/testutil/fakenet\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDefault_Process(t *testing.T) {\n\tconst (\n\t\tnl             = \"\\n\"\n\t\tcity           = \"Nonreal\"\n\t\tcountry        = \"Imagiland\"\n\t\torgname        = \"FakeOrgLLC\"\n\t\treferralserver = \"whois.example.net\"\n\t)\n\n\tip := netip.MustParseAddr(\"1.2.3.4\")\n\n\ttestCases := []struct {\n\t\twant *whois.Info\n\t\tname string\n\t\tdata string\n\t}{{\n\t\twant: nil,\n\t\tname: \"empty\",\n\t\tdata: \"\",\n\t}, {\n\t\twant: nil,\n\t\tname: \"comments\",\n\t\tdata: \"%\\n#\",\n\t}, {\n\t\twant: nil,\n\t\tname: \"no_colon\",\n\t\tdata: \"city\",\n\t}, {\n\t\twant: nil,\n\t\tname: \"no_value\",\n\t\tdata: \"city:\",\n\t}, {\n\t\twant: &whois.Info{\n\t\t\tCity: city,\n\t\t},\n\t\tname: \"city\",\n\t\tdata: \"city: \" + city,\n\t}, {\n\t\twant: &whois.Info{\n\t\t\tCountry: country,\n\t\t},\n\t\tname: \"country\",\n\t\tdata: \"country: \" + country,\n\t}, {\n\t\twant: &whois.Info{\n\t\t\tOrgname: orgname,\n\t\t},\n\t\tname: \"orgname\",\n\t\tdata: \"orgname: \" + orgname,\n\t}, {\n\t\twant: &whois.Info{\n\t\t\tOrgname: orgname,\n\t\t},\n\t\tname: \"orgname_hyphen\",\n\t\tdata: \"org-name: \" + orgname,\n\t}, {\n\t\twant: &whois.Info{\n\t\t\tOrgname: orgname,\n\t\t},\n\t\tname: \"orgname_descr\",\n\t\tdata: \"descr: \" + orgname,\n\t}, {\n\t\twant: &whois.Info{\n\t\t\tOrgname: orgname,\n\t\t},\n\t\tname: \"orgname_netname\",\n\t\tdata: \"netname: \" + orgname,\n\t}, {\n\t\twant: &whois.Info{\n\t\t\tCity:    city,\n\t\t\tCountry: country,\n\t\t\tOrgname: orgname,\n\t\t},\n\t\tname: \"full\",\n\t\tdata: \"OrgName: \" + orgname + nl + \"City: \" + city + nl + \"Country: \" + country,\n\t}, {\n\t\twant: nil,\n\t\tname: \"whois\",\n\t\tdata: \"whois: \" + referralserver,\n\t}, {\n\t\twant: nil,\n\t\tname: \"referralserver\",\n\t\tdata: \"referralserver: whois://\" + referralserver,\n\t}, {\n\t\twant: nil,\n\t\tname: \"other\",\n\t\tdata: \"other: value\",\n\t}}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\thit := 0\n\n\t\t\tfakeConn := &fakenet.Conn{\n\t\t\t\tOnRead: func(b []byte) (n int, err error) {\n\t\t\t\t\thit++\n\n\t\t\t\t\treturn copy(b, tc.data), io.EOF\n\t\t\t\t},\n\t\t\t\tOnWrite:       func(b []byte) (n int, err error) { return len(b), nil },\n\t\t\t\tOnClose:       func() (err error) { return nil },\n\t\t\t\tOnSetDeadline: func(t time.Time) (err error) { return nil },\n\t\t\t}\n\n\t\t\tw := whois.New(&whois.Config{\n\t\t\t\tLogger:  slogutil.NewDiscardLogger(),\n\t\t\t\tTimeout: 5 * time.Second,\n\t\t\t\tDialContext: func(_ context.Context, _, _ string) (_ net.Conn, _ error) {\n\t\t\t\t\thit = 0\n\n\t\t\t\t\treturn fakeConn, nil\n\t\t\t\t},\n\t\t\t\tMaxConnReadSize: 1024,\n\t\t\t\tMaxRedirects:    3,\n\t\t\t\tMaxInfoLen:      250,\n\t\t\t\tCacheSize:       100,\n\t\t\t\tCacheTTL:        time.Hour,\n\t\t\t})\n\n\t\t\tgot, changed := w.Process(context.Background(), ip)\n\t\t\trequire.True(t, changed)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t\tassert.Equal(t, 1, hit)\n\n\t\t\t// From cache.\n\t\t\tgot, changed = w.Process(context.Background(), ip)\n\t\t\trequire.False(t, changed)\n\n\t\t\tassert.Equal(t, tc.want, got)\n\t\t\tassert.Equal(t, 1, hit)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "main.go",
    "content": "//go:build !next\n\npackage main\n\nimport (\n\t\"embed\"\n\t// Embed tzdata in binary.\n\t//\n\t// See https://github.com/AdguardTeam/AdGuardHome/issues/6758\n\t_ \"time/tzdata\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/home\"\n)\n\n// Embed the prebuilt client here since we strive to keep .go files inside the\n// internal directory and the embed package is unable to embed files located\n// outside of the same or underlying directory.\n\n//go:embed build\nvar clientBuildFS embed.FS\n\nfunc main() {\n\thome.Main(clientBuildFS)\n}\n"
  },
  {
    "path": "main_next.go",
    "content": "//go:build next\n\npackage main\n\nimport (\n\t\"embed\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/next/cmd\"\n)\n\n// Embed the prebuilt client here since we strive to keep .go files inside the\n// internal directory and the embed package is unable to embed files located\n// outside of the same or underlying directory.\n\n//go:embed build\nvar frontend embed.FS\n\nfunc main() {\n\tcmd.Main(frontend)\n}\n"
  },
  {
    "path": "openapi/CHANGELOG.md",
    "content": "# AdGuard Home API Change Log\n\n<!-- TODO(a.garipov): Reformat in accordance with the KeepAChangelog spec. -->\n\n## v0.107.72: API changes\n\n## New `recent` query parameter in 'GET /control/stats/'\n\n- New query parameter `recent` defines the statistics lookback period in millieseconds.\n\n### New `ignored_enabled` field in `GetStatsConfigResponse` and  `GetQueryLogConfigResponse`\n\n- The new field `ignored_enabled` indicates whether the host names in the ignored array should be ignored.  This field has been added for the following endpoints:\n    - `GET /control/querylog/config`\n    - `PUT /control/querylog/config/update`\n    - `GET /control/stats/config`\n    - `PUT /control/stats/config/update`\n\n## v0.107.70: API changes\n\n### New `\"start_time\"` field in 'GET /control/status'\n\n- New field `\"start_time\"` indicates the start time of the web API server (Unix time in milliseconds).\n\n## v0.107.68: API changes\n\n### New HTTP APIs 'GET /control/rewrite/settings' and 'PUT /control/rewrite/settings/update'\n\n- New HTTP APIs to manage global DNS rewrites.\n\n    ```json\n    {\n      \"enabled\": true\n    }\n    ```\n\n### New `\"enabled\"` field in 'POST /control/rewrite/add' and 'PUT /control/rewrite/update'\n\n- New optional field `\"enabled\"` indicates whether the rewrite is active.\n\n### The blocked services groups\n\n- The new field `\"groups\"` in `GET /control/blocked_services/all` is a list of service group.  Groups make it possible to block multiple services with equal `\"group_id\"` at once.\n\n- The new field `\"group_id\"` for each `BlockedService` object in `GET /control/blocked_services/all` indicates which group the service belongs to.\n\n## v0.107.64: API changes\n\n- The new field `\"cache_enabled\"` in `GET /control/dns_info` and `POST /control/dns_config`.  Setting this flag to true turns the DNS-response cache on and requires a positive `cache_size` value (or a positive `dns.cache_size` in the configuration file).\n\n## v0.107.58: API changes\n\n### The ability to check rules for query types and/or clients: GET /control/check_host\n\n- Added optional `client` and `qtype` URL query parameters.\n\n## v0.107.57: API changes\n\n- The new field `\"upstream_timeout\"` in `GET /control/dns_info` and `POST /control/dns_config` is the number of seconds to wait for a response from the upstream server.\n\n## v0.107.56: API changes\n\n### Documentation fix of `NetInterface`\n\n- The `NetInterface` object schema has been updated to reflect the actual structure of the response.  It has included and required the `ipv4_addresses` and `ipv6_addresses` fields, required the `gateway_ip` field, and excluded the `mtu` field.\n\n### Deprecated client APIs\n\n- The `GET /control/clients/find` HTTP API; use the new `POST /control/clients/search` API instead.\n\n### New client APIs\n\n- The new `POST /control/clients/search` HTTP API allows config updates.  It accepts a JSON object with the following format:\n\n    ```json\n    {\n      \"clients\": [\n        {\n          \"id\": \"192.0.2.1\"\n        },\n        {\n          \"id\": \"test\"\n        }\n      ]\n    }\n    ```\n\n## v0.107.53: API changes\n\n### The new field `\"ecosia\"` in `SafeSearchConfig`\n\n- The new field `\"ecosia\"` in `PUT /control/safesearch/settings` and `GET /control/safesearch/status` is true if safe search is enforced for Ecosia search engine.\n\n## v0.107.44: API changes\n\n### The field `\"upstream_mode\"` in `DNSConfig`\n\n- The field `\"upstream_mode\"` in `POST /control/dns_config` and `GET /control/dns_info` now accepts `load_balance` value. Note that, the usage of an empty string or field absence is considered to as deprecated and is not recommended. Use `load_balance` instead.\n\n### Type correction in `Client`\n\n- Field `upstreams_cache_size` of object `Client` now correctly has type `integer` instead of the previous incorrect type `boolean`.\n\n## v0.107.42: API changes\n\n### The new field `\"serve_plain_dns\"` in `TlsConfig`\n\n- The new field `\"serve_plain_dns\"` in `POST /control/tls/configure`, `POST /control/tls/validate` and `GET /control/tls/status` is true if plain DNS is allowed for incoming requests.\n\n### The new fields `\"upstreams_cache_enabled\"` and `\"upstreams_cache_size\"` in `Client` object\n\n- The new field `\"upstreams_cache_enabled\"` in `GET /control/clients`, `GET /control/clients/find`, `POST /control/clients/add`, and `POST /control/clients/update` methods shows if client’s DNS cache is enabled for the client.  If not set AdGuard Home will use default value (false).\n\n- The new field `\"upstreams_cache_size\"` in `GET /control/clients`, `GET /control/clients/find`, `POST /control/clients/add`, and `POST /control/clients/update` methods is the size of client’s DNS cache in bytes.\n\n### The new field `\"ratelimit_subnet_len_ipv4\"` in `DNSConfig` object\n\n- The new field `\"ratelimit_subnet_len_ipv4\"` in `GET /control/dns_info` and `POST /control/dns_config` is the length of the subnet mask for IPv4 addresses.\n\n### The new field `\"ratelimit_subnet_len_ipv6\"` in `DNSConfig` object\n\n- The new field `\"ratelimit_subnet_len_ipv6\"` in `GET /control/dns_info` and `POST /control/dns_config` is the length of the subnet mask for IPv6 addresses.\n\n### The new field `\"ratelimit_whitelist\"` in `DNSConfig` object\n\n- The new field `\"blocked_response_ttl\"` in `GET /control/dns_info` and `POST /control/dns_config` is the list of IP addresses excluded from rate limiting.\n\n## v0.107.39: API changes\n\n### New HTTP API 'POST /control/dhcp/update_static_lease'\n\n- The new `POST /control/dhcp/update_static_lease` HTTP API allows modifying IP address, hostname of the static DHCP lease.  IP version must be the same as previous.\n\n### The new field `\"blocked_response_ttl\"` in `DNSConfig` object\n\n- The new field `\"blocked_response_ttl\"` in `GET /control/dns_info` and `POST /control/dns_config` is the TTL for blocked responses.\n\n## v0.107.37: API changes\n\n### The new field `\"fallback_dns\"` in `UpstreamsConfig` object\n\n- The new field `\"fallback_dns\"` in `POST /control/test_upstream_dns` is the list of fallback DNS servers to test.\n\n### The new field `\"fallback_dns\"` in `DNSConfig` object\n\n- The new field `\"fallback_dns\"` in `GET /control/dns_info` and `POST /control/dns_config` is the list of fallback DNS servers used when upstream DNS servers are not responding.\n\n### Deprecated blocked services APIs\n\n- The `GET /control/blocked_services/list` HTTP API; use the new `GET /control/blocked_services/get` API instead.\n\n- The `POST /control/blocked_services/set` HTTP API; use the new `PUT /control/blocked_services/update` API instead.\n\n### New blocked services APIs\n\n- The new `GET /control/blocked_services/get` HTTP API.\n\n- The new `PUT /control/blocked_services/update` HTTP API allows config updates.\n\nThese APIs accept and return a JSON object with the following format:\n\n```json\n{\n  \"schedule\": {\n    \"time_zone\": \"Local\",\n    \"sun\": {\n      \"start\": 46800000,\n      \"end\": 82800000\n    }\n  },\n  \"ids\": [\n    \"vk\"\n  ]\n}\n```\n\n### `/control/clients` HTTP APIs\n\nThe following HTTP APIs have been changed:\n\n- `GET /control/clients`;\n- `GET /control/clients/find?ip0=...&ip1=...&ip2=...`;\n- `POST /control/clients/add`;\n- `POST /control/clients/update`;\n\nThe new field `blocked_services_schedule` has been added to JSON objects.  It has the following format:\n\n```json\n{\n  \"time_zone\": \"Local\",\n  \"sun\": {\n    \"start\": 0,\n    \"end\": 86400000\n  },\n  \"mon\": {\n    \"start\": 60000,\n    \"end\": 82800000\n  },\n  \"thu\": {\n    \"start\": 120000,\n    \"end\": 79200000\n  },\n  \"tue\": {\n    \"start\": 180000,\n    \"end\": 75600000\n  },\n  \"wed\": {\n    \"start\": 240000,\n    \"end\": 72000000\n  },\n  \"fri\": {\n    \"start\": 300000,\n    \"end\": 68400000\n  },\n  \"sat\": {\n    \"start\": 360000,\n    \"end\": 64800000\n  }\n}\n```\n\n## v0.107.36: API changes\n\n### The new fields `\"top_upstreams_responses\"` and `\"top_upstreams_avg_time\"` in `Stats` object\n\n- The new field `\"top_upstreams_responses\"` in `GET /control/stats` method shows the total number of responses from each upstream.\n\n- The new field `\"top_upstreams_avg_time\"` in `GET /control/stats` method shows the average processing time in seconds of requests from each upstream.\n\n## v0.107.30: API changes\n\n### `POST /control/version.json` and `GET /control/dhcp/interfaces` content type\n\n- The value of the `Content-Type` header in the `POST /control/version.json` and `GET /control/dhcp/interfaces` HTTP APIs is now correctly set to `application/json` as opposed to `text/plain`.\n\n### New HTTP API 'PUT /control/rewrite/update'\n\n- The new `PUT /control/rewrite/update` HTTP API allows rewrite rule updates.  It accepts a JSON object with the following format:\n\n    ```json\n    {\n      \"target\": {\n        \"domain\": \"example.com\",\n        \"answer\": \"answer-to-update\"\n      },\n      \"update\": {\n        \"domain\": \"example.com\",\n        \"answer\": \"new-answer\"\n      }\n    }\n    ```\n\n## v0.107.29: API changes\n\n### `GET /control/clients` And `GET /control/clients/find`\n\n- The new optional fields `\"ignore_querylog\"` and `\"ignore_statistics\"` are set if AdGuard Home excludes client activity from query log or statistics.\n\n### `POST /control/clients/add` And `POST /control/clients/update`\n\n- The new optional fields `\"ignore_querylog\"` and `\"ignore_statistics\"` make AdGuard Home exclude client activity from query log or statistics.  If not set AdGuard Home will use default value (false).  It can be changed in the future versions.\n\n## v0.107.27: API changes\n\n### The new optional fields `\"edns_cs_use_custom\"` and `\"edns_cs_custom_ip\"` in `DNSConfig`\n\n- The new optional fields `\"edns_cs_use_custom\"` and `\"edns_cs_custom_ip\"` in `POST /control/dns_config` method makes AdGuard Home use or not use the custom IP for EDNS Client Subnet.\n\n- The new optional fields `\"edns_cs_use_custom\"` and `\"edns_cs_custom_ip\"` in `GET /control/dns_info` method are set if AdGuard Home uses custom IP for EDNS Client Subnet.\n\n### Deprecated statistics APIs\n\n- The `GET /control/stats_info` HTTP API; use the new `GET /control/stats/config` API instead.\n\n    **NOTE:** If `interval` was configured by editing configuration file or new HTTP API call `PUT /control/stats/config/update` and it’s not equal to previous allowed enum values then it will be equal to `90` days for compatibility reasons.\n\n- The `POST /control/stats_config` HTTP API; use the new `PUT /control/stats/config/update` API instead.\n\n### New statistics APIs\n\n- The new `GET /control/stats/config` HTTP API.\n\n- The new `PUT /control/stats/config/update` HTTP API allows config updates.\n\nThese `control/stats/config/update` and `control/stats/config` APIs accept and return a JSON object with the following format:\n\n```json\n{\n  \"enabled\": true,\n  \"interval\": 3600,\n  \"ignored\": [\n    \"example.com\"\n  ]\n}\n```\n\n### Deprecated query log APIs\n\n- The `GET /control/querylog_info` HTTP API; use the new `GET /control/querylog/config` API instead.\n\n    **NOTE:** If `interval` was configured by editing configuration file or new HTTP API call `PUT /control/querylog/config/update` and it’s not equal to previous allowed enum values then it will be equal to `90` days for compatibility reasons.\n\n- The `POST /control/querylog_config` HTTP API; use the new `PUT /control/querylog/config/update` API instead.\n\n### New query log APIs\n\n- The new `GET /control/querylog/config` HTTP API.\n\n- The new `PUT /control/querylog/config/update` HTTP API allows config updates.\n\nThese `control/querylog/config/update` and `control/querylog/config` APIs accept and return a JSON object with the following format:\n\n```json\n{\n  \"enabled\": true,\n  \"anonymize_client_ip\": false,\n  \"interval\": 3600,\n  \"ignored\": [\n    \"example.com\"\n  ]\n}\n```\n\n### New `\"protection_disabled_until\"` field in `GET /control/dns_info` response\n\n- The new field `\"protection_disabled_until\"` in `GET /control/dns_info` is the timestamp until when the protection is disabled.\n\n### New `\"protection_disabled_duration\"` field in `GET /control/status` response\n\n- The new field `\"protection_disabled_duration\"` is the duration of protection pause in milliseconds.\n\n### `POST /control/protection`\n\n- The new `POST /control/protection` HTTP API allows to pause protection for specified duration in milliseconds.\n\nThis API accepts a JSON object with the following format:\n\n```json\n{\n  \"enabled\": false,\n  \"duration\": 10000\n}\n```\n\n### Deprecated HTTP APIs\n\nThe following HTTP APIs are deprecated:\n\n- `POST /control/safesearch/enable` is deprecated.  Use the new `PUT /control/safesearch/settings`.\n\n- `POST /control/safesearch/disable` is deprecated.  Use the new `PUT /control/safesearch/settings`.\n\n### New HTTP API `PUT /control/safesearch/settings`\n\n- The new `PUT /control/safesearch/settings` HTTP API allows safesearch settings updates. It accepts a JSON object with the following format:\n\n    ```json\n    {\n      \"enabled\": true,\n      \"bing\": false,\n      \"duckduckgo\": true,\n      \"google\": false,\n      \"pixabay\": false,\n      \"yandex\": true,\n      \"youtube\": false\n    }\n    ```\n\n### `GET /control/safesearch/status`\n\n- The `control/safesearch/status` HTTP API has been changed.  It now returns a JSON object with the following format:\n\n    ```json\n    {\n      \"enabled\": true,\n      \"bing\": false,\n      \"duckduckgo\": true,\n      \"google\": false,\n      \"pixabay\": false,\n      \"yandex\": true,\n      \"youtube\": false\n    }\n    ```\n\n### `/control/clients` HTTP APIs\n\nThe following HTTP APIs have been changed:\n\n- `GET /control/clients`;\n- `GET /control/clients/find?ip0=...&ip1=...&ip2=...`;\n- `POST /control/clients/add`;\n- `POST /control/clients/update`;\n\nThe `safesearch_enabled` field is deprecated.  The new field `safe_search` has been added to JSON objects.  It has the following format:\n\n```json\n{\n  \"enabled\": true,\n  \"bing\": false,\n  \"duckduckgo\": true,\n  \"google\": false,\n  \"pixabay\": false,\n  \"yandex\": true,\n  \"youtube\": false\n}\n```\n\n## v0.107.23: API changes\n\n### Experimental “beta” APIs removed\n\nThe following experimental beta APIs have been removed:\n\n- `GET  /control/install/get_addresses_beta`;\n- `POST /control/install/check_config_beta`;\n- `POST /control/install/configure_beta`.\n\nThey never quite worked properly, and the future new version of AdGuard Home API will probably be different.\n\n## v0.107.22: API changes\n\n### `POST /control/i18n/change_language` is deprecated\n\nUse `PUT /control/profile/update`.\n\n### `GET /control/i18n/current_language` is deprecated\n\nUse `GET /control/profile`.\n\n- The `/control/profile` HTTP API has been changed.\n\n- The new `PUT /control/profile/update` HTTP API allows user info updates.\n\nThese `control/profile/update` and `control/profile` APIs accept and return a JSON object with the following format:\n\n```json\n{\n  \"name\": \"user name\",\n  \"language\": \"en\",\n  \"theme\": \"auto\"\n}\n```\n\n## v0.107.20: API Changes\n\n### `POST /control/cache_clear`\n\n- The new `POST /control/cache_clear` HTTP API allows clearing the DNS cache.\n\n## v0.107.17: API Changes\n\n### `GET /control/blocked_services/services` is deprecated\n\nUse `GET /control/blocked_services/all`.\n\n### `GET /control/blocked_services/all`\n\n- The new `GET /control/blocked_services/all` HTTP API allows inspecting all available services and their data, such as SVG icons and human-readable names.\n\n## v0.107.15: `POST` Requests Without Bodies\n\nAs an additional CSRF protection measure, AdGuard Home now ensures that requests that change its state but have no body do not have a `Content-Type` header set on them.\n\nThis concerns the following APIs:\n\n- `POST /control/dhcp/reset_leases`;\n- `POST /control/dhcp/reset`;\n- `POST /control/parental/disable`;\n- `POST /control/parental/enable`;\n- `POST /control/querylog_clear`;\n- `POST /control/safebrowsing/disable`;\n- `POST /control/safebrowsing/enable`;\n- `POST /control/safesearch/disable`;\n- `POST /control/safesearch/enable`;\n- `POST /control/stats_reset`;\n- `POST /control/update`.\n\n## v0.107.14: BREAKING API CHANGES\n\nA Cross-Site Request Forgery (CSRF) vulnerability has been discovered.  We have implemented several measures to prevent such vulnerabilities in the future, but some of these measures break backwards compatibility for the sake of better protection.\n\nAll JSON APIs that expect a body now check if the request actually has `Content-Type` set to `application/json`.\n\nAll new formats for the request and response bodies are documented in `openapi.yaml`.\n\n### `POST /control/filtering/set_rules` And Other Plain-Text APIs\n\nThe following APIs, which previously accepted or returned `text/plain` data, now accept or return data as JSON.\n\n#### `POST /control/filtering/set_rules`\n\nPreviously, the API accepted a raw list of filters as a plain-text file.  Now, the filters must be presented in a JSON object with the following format:\n\n```json\n{\n  \"rules\": [\n    \"||example.com^\",\n    \"# comment\",\n    \"@@||www.example.com^\"\n  ]\n}\n```\n\n#### `GET /control/i18n/current_language` And `POST /control/i18n/change_language`\n\nPreviously, these APIs accepted and returned the language code in plain text.  Now, they accept and return them in a JSON object with the following format:\n\n```json\n{\n  \"language\": \"en\"\n}\n```\n\n#### `POST /control/dhcp/find_active_dhcp`\n\nPreviously, the API accepted the name of the network interface as a plain-text string.  Now, it must be contained within a JSON object with the following format:\n\n```json\n{\n  \"interface\": \"eth0\"\n}\n```\n\n## v0.107.12: API changes\n\n### `GET /control/blocked_services/services`\n\n- The new `GET /control/blocked_services/services` HTTP API allows inspecting all available services.\n\n## v0.107.7: API changes\n\n### The new optional field `\"ecs\"` in `QueryLogItem`\n\n- The new optional field `\"ecs\"` in `GET /control/querylog` contains the IP network from an EDNS Client-Subnet option from the request message if any.\n\n### The new possible status code in `/install/configure` response\n\n- The new status code `422 Unprocessable Entity` in the response for `POST /install/configure` which means that the specified password does not meet the strength requirements.\n\n## v0.107.3: API changes\n\n### The new field `\"version\"` in `AddressesInfo`\n\n- The new field `\"version\"` in `GET /install/get_addresses` is the version of the AdGuard Home instance.\n\n## v0.107.0: API changes\n\n### The new field `\"cached\"` in `QueryLogItem`\n\n- The new field `\"cached\"` in `GET /control/querylog` is true if the response is served from cache instead of being resolved by an upstream server.\n\n### New constant values for `filter_list_id` field in `ResultRule`\n\n- Value of `0` is now used for custom filtering rules list.\n\n- Value of `-1` is now used for rules generated from the operating system hosts files.\n\n- Value of `-2` is now used for blocked services’ rules.\n\n- Value of `-3` is now used for rules generated by parental control web service.\n\n- Value of `-4` is now used for rules generated by safe browsing web service.\n\n- Value of `-5` is now used for rules generated by safe search web service.\n\n### New possible value of `\"name\"` field in `QueryLogItemClient`\n\n- The value of `\"name\"` field in `GET /control/querylog` method is never empty, either persistent client’s name or runtime client’s hostname.\n\n### Lists in `AccessList`\n\n- Fields `\"allowed_clients\"`, `\"disallowed_clients\"` and `\"blocked_hosts\"` in `POST /access/set` now should contain only unique elements.\n\n- Fields `\"allowed_clients\"` and `\"disallowed_clients\"` cannot contain the same elements.\n\n### The new field `\"private_key_saved\"` in `TlsConfig`\n\n- The new field `\"private_key_saved\"` in `POST /control/tls/configure`, `POST /control/tls/validate` and `GET /control/tls/status` is true if the private key was previously saved as a string and now the private key omitted from communication between server and client due to security issues.\n\n### The new field `\"cache_optimistic\"` in DNS configuration\n\n- The new optional field `\"cache_optimistic\"` in `POST /control/dns_config` method makes AdGuard Home use or not use the optimistic cache mechanism.\n\n- The new field `\"cache_optimistic\"` in `GET /control/dns_info` method is true if AdGuard Home uses the optimistic cache mechanism.\n\n### New possible value of `\"interval\"` field in `QueryLogConfig`\n\n- The value of `\"interval\"` field in `POST /control/querylog_config` and `GET /control/querylog_info` methods could now take the value of `0.25`.  It’s equal to 6 hours.\n\n- All the possible values of `\"interval\"` field are enumerated.\n\n- The type of `\"interval\"` field is now `number` instead of `integer`.\n\n### ClientIDs in Access Settings\n\n- The `POST /control/access/set` HTTP API now accepts ClientIDs in `\"allowed_clients\"` and `\"disallowed_clients\"` fields.\n\n### The new field `\"unicode_name\"` in `DNSQuestion`\n\n- The new optional field `\"unicode_name\"` is the Unicode representation of question’s domain name.  It is only presented if the original question’s domain name is an IDN.\n\n### Documentation fix of `DNSQuestion`\n\n- Previously incorrectly named field `\"host\"` in `DNSQuestion` is now named `\"name\"`.\n\n### Disabling Statistics\n\n- The `POST /control/stats_config` HTTP API allows disabling statistics by setting `\"interval\"` to `0`.\n\n### `POST /control/dhcp/reset_leases`\n\n- The new `POST /control/dhcp/reset_leases` HTTP API allows removing all leases from the DHCP server’s database without erasing its configuration.\n\n### The parameter `\"host\"` in `GET /apple/*.mobileconfig` is now required\n\n- The parameter `\"host\"` in `GET` requests for `/apple/doh.mobileconfig` and `/apple/doh.mobileconfig` is now required to prevent unexpected server name’s value.\n\n### The new field `\"default_local_ptr_upstreams\"` in `GET /control/dns_info`\n\n- The new optional field `\"default_local_ptr_upstreams\"` is the list of IP addresses AdGuard Home would use by default to resolve PTR request for addresses from locally-served networks.\n\n### The field `\"use_private_ptr_resolvers\"` in DNS configuration\n\n- The new optional field  `\"use_private_ptr_resolvers\"` of `\"DNSConfig\"` specifies if the DNS server should use `\"local_ptr_upstreams\"` at all.\n\n## v0.106: API changes\n\n### The field `\"supported_tags\"` in `GET /control/clients`\n\n- Previously undocumented field `\"supported_tags\"` in the response is now documented.\n\n### The field `\"whois_info\"` in `GET /control/clients`\n\n- Objects in the `\"auto_clients\"` array now have the `\"whois_info\"` field.\n\n### New response code in `POST /control/login`\n\n- `429` is returned when user is out of login attempts.  It adds the `Retry-After` header with the number of seconds of block left in it.\n\n### New `\"private_upstream\"` field in `POST /test_upstream_dns`\n\n- The new optional field `\"private_upstream\"` of `UpstreamConfig` contains the upstream servers for resolving locally-served ip addresses to be checked.\n\n### New fields `\"resolve_clients\"` and `\"local_ptr_upstreams\"` in DNS configuration\n\n- The new optional field `\"resolve_clients\"` of `DNSConfig` is used to turn resolving clients’ addresses on and off.\n\n- The new optional field `\"local_ptr_upstreams\"` of `\"DNSConfig\"` contains the upstream servers for resolving addresses from locally-served networks.  The empty `\"local_ptr_resolvers\"` states that AGH should use resolvers provided by the operating system.\n\n### New `\"client_info\"` field in `GET /querylog` response\n\n- The new optional field `\"client_info\"` of `QueryLogItem` objects contains a more full information about the client.\n\n## v0.105: API changes\n\n### New `\"client_id\"` field in `GET /querylog` response\n\n- The new field `\"client_id\"` of `QueryLogItem` objects is the ID sent by the client for encrypted requests, if there was any.  See the \"[Identifying clients]\" section of our wiki.\n\n### New `\"dnscrypt\"` `\"client_proto\"` value in `GET /querylog` response\n\n- The field `\"client_proto\"` can now have the value `\"dnscrypt\"` when the request was sent over a DNSCrypt connection.\n\n### New `\"reason\"` in `GET /filtering/check_host` and `GET /querylog`\n\n- The new `RewriteRule` reason is added to `GET /filtering/check_host` and `GET /querylog`.\n\n- Also, the reason which was incorrectly documented as `\"ReasonRewrite\"` is now correctly documented as `\"Rewrite\"`, and the previously undocumented `\"RewriteEtcHosts\"` is now documented as well.\n\n### Multiple matched rules in `GET /filtering/check_host` and `GET /querylog`\n\n- The properties `rule` and `filter_id` are now deprecated.  API users should inspect the newly-added `rules` object array instead.  For most rules, it’s either empty or contains one object, which contains the same things as the old two properties did, but under more correct names:\n\n    ```js\n    {\n      // …\n\n      // Deprecated.\n      \"rule\": \"||example.com^\",\n      // Deprecated.\n      \"filter_id\": 42,\n      // Newly-added.\n      \"rules\": [{\n        \"text\": \"||example.com^\",\n        \"filter_list_id\": 42\n      }]\n    }\n    ```\n\n  For `$dnsrewrite` rules, they contain all rules that contributed to the result.  For example, if you have the following filtering rules:\n\n    ```adblock\n    ||example.com^$dnsrewrite=127.0.0.1\n    ||example.com^$dnsrewrite=127.0.0.2\n    ```\n\n  The `\"rules\"` will be something like:\n\n    ```js\n    {\n      // …\n\n      \"rules\": [{\n        \"text\": \"||example.com^$dnsrewrite=127.0.0.1\",\n        \"filter_list_id\": 0\n      }, {\n        \"text\": \"||example.com^$dnsrewrite=127.0.0.2\",\n        \"filter_list_id\": 0\n      }]\n    }\n    ```\n\n  The old fields will be removed in v0.106.0.\n\n  As well as other documentation fixes.\n\n[Identifying clients]: https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#idclient\n\n## v0.103: API changes\n\n### API: replace settings in GET /control/dns_info & POST /control/dns_config\n\n- Added `\"upstream_mode\"`:\n\n    ```none\n    \"upstream_mode\": \"\" | \"parallel\" | \"fastest_addr\"\n    ```\n\n- Removed `\"fastest_addr\"`, `\"parallel_requests\"`.\n\n### API: Get querylog: GET /control/querylog\n\n- Added optional \"offset\" and \"limit\" parameters.\n\n  We are still using \"older_than\" approach in AdGuard Home UI, but we realize that it’s easier to use offset/limit so here is this option now.\n\n## v0.102: API changes\n\n### API: Get general status: GET /control/status\n\n- Removed `\"upstream_dns\"`, `\"bootstrap_dns\"`, `\"all_servers\"` parameters.\n\n### API: Get DNS general settings: GET /control/dns_info\n\n- Added `\"parallel_requests\"`, `\"upstream_dns\"`, `\"bootstrap_dns\"` parameters or `GET /control/dns_info` API.  An example of `200 OK` response:\n\n    ```json\n    {\n      \"upstream_dns\": [\"tls://...\", ...],\n      \"bootstrap_dns\": [\"1.2.3.4\", ...],\n      \"protection_enabled\": true | false,\n      \"ratelimit\": 1234,\n      \"blocking_mode\": \"default\" | \"nxdomain\" | \"null_ip\" | \"custom_ip\",\n      \"blocking_ipv4\": \"1.2.3.4\",\n      \"blocking_ipv6\": \"1:2:3::4\",\n      \"edns_cs_enabled\": true | false,\n      \"dnssec_enabled\": true | false\n      \"disable_ipv6\": true | false,\n      \"fastest_addr\": true | false, // use Fastest Address algorithm\n      \"parallel_requests\": true | false, // send DNS requests to all upstream servers at once\n    }\n    ```\n\n### API: Set DNS general settings: POST /control/dns_config\n\n- Added `\"parallel_requests\"`, `\"upstream_dns\"`, `\"bootstrap_dns\"` parameters.\n- Removed `/control/set_upstreams_config` method.\n\nExample of a `POST /control/dns_config` request:\n\n  ```json\n  {\n    \"upstream_dns\": [\"tls://...\", ...],\n    \"bootstrap_dns\": [\"1.2.3.4\", ...],\n    \"protection_enabled\": true | false,\n    \"ratelimit\": 1234,\n    \"blocking_mode\": \"default\" | \"nxdomain\" | \"null_ip\" | \"custom_ip\",\n    \"blocking_ipv4\": \"1.2.3.4\",\n    \"blocking_ipv6\": \"1:2:3::4\",\n    \"edns_cs_enabled\": true | false,\n    \"dnssec_enabled\": true | false\n    \"disable_ipv6\": true | false,\n    \"fastest_addr\": true | false, // use Fastest Address algorithm\n    \"parallel_requests\": true | false, // send DNS requests to all upstream servers at once\n  }\n  ```\n\n## v0.101: API changes\n\n### API: Refresh filters: POST /control/filtering/refresh\n\n- Added `\"whitelist\"` boolean parameter.\n- Response is in JSON format.\n\nExample of a `POST /control/filtering/refresh` request and `200 OK` response:\n\n  ```json\n  {\n    \"whitelist\": true\n  }\n  ```\n\n  ```json\n  {\n    \"updated\": 123 // number of filters updated\n  }\n  ```\n\n## v0.100: API changes\n\n### API: Get list of clients: GET /control/clients\n\n- `\"ip\"` and `\"mac\"` fields are removed.\n- `\"ids\"` and `\"ip_addrs\"` fields are added.\n\nExample of a `200 OK` response:\n\n  ```json\n  {\n    \"clients\": [\n      {\n        \"name\": \"client1\",\n        \"ids\": [\"...\", /* ... */], // IP or MAC\n        \"ip_addrs\": [\"...\", /* ... */], // all IP addresses (set by user and resolved by MAC)\n        \"use_global_settings\": true,\n        \"filtering_enabled\": false,\n        \"parental_enabled\": false,\n        \"safebrowsing_enabled\": false,\n        \"safesearch_enabled\": false,\n        \"use_global_blocked_services\": true,\n        \"blocked_services\": [ \"name1\", /* ... */  ],\n        \"whois_info\": {\n          \"key\": \"value\",\n          // ...\n        }\n      }\n    ]\n    \"auto_clients\": [\n      {\n        \"name\": \"host\",\n        \"ip\": \"...\",\n        \"source\": \"etc/hosts\" || \"rDNS\",\n        \"whois_info\": {\n          \"key\": \"value\",\n          // ...\n        }\n      }\n    ]\n  }\n  ```\n\n### API: Add client: POST /control/clients/add\n\n- `\"ip\"` and `\"mac\"` fields are removed.\n- `\"ids\"` field is added.\n\nExample of a `POST /control/clients/add` request:\n\n  ```json\n  {\n    \"name\": \"client1\",\n    \"ids\": [\"...\", /* ... */], // IP or MAC\n    \"use_global_settings\": true,\n    \"filtering_enabled\": false,\n    \"parental_enabled\": false,\n    \"safebrowsing_enabled\": false,\n    \"safesearch_enabled\": false,\n    \"use_global_blocked_services\": true,\n    \"blocked_services\": [ \"name1\", /* ... */  ]\n  }\n  ```\n\n### API: Update client: POST /control/clients/update\n\n- `\"ip\"` and `\"mac\"` fields are removed.\n- `\"ids\"` field is added.\n\nExample of a `POST /control/clients/update` request:\n\n  ```json\n  {\n    \"name\": \"client1\",\n    \"data\": {\n      \"name\": \"client1\",\n      \"ids\": [\"...\", /* ... */], // IP or MAC\n      \"use_global_settings\": true,\n      \"filtering_enabled\": false,\n      \"parental_enabled\": false,\n      \"safebrowsing_enabled\": false,\n      \"safesearch_enabled\": false,\n      \"use_global_blocked_services\": true,\n      \"blocked_services\": [ \"name1\", /* ... */  ]\n    }\n  }\n  ```\n\n## v0.99.3: API changes\n\n### API: Get query log: GET /control/querylog\n\nThe response data is now a JSON object, not an array.\n\nExample of a `200 OK` response:\n\n  ```json\n  {\n    \"oldest\": \"2006-01-02T15:04:05.999999999Z07:00\",\n    \"data\": [\n      {\n        \"answer\": [\n          {\n            \"ttl\": 10,\n            \"type\": \"AAAA\",\n            \"value\": \"::\"\n          }\n        ],\n        \"client\": \"127.0.0.1\",\n        \"elapsedMs\":\"0.098403\",\n        \"filterId\":1,\n        \"question\": {\n          \"class\":\"IN\",\n          \"host\":\"doubleclick.net\",\n          \"type\":\"AAAA\"\n        },\n        \"reason\":\"FilteredBlackList\",\n        \"rule\":\"||doubleclick.net^\",\n        \"status\":\"NOERROR\",\n        \"time\":\"2006-01-02T15:04:05.999999999Z07:00\"\n      }\n    // ...\n    ]\n  }\n  ```\n\n## v0.99.1: API changes\n\n### API: Get current user info: GET /control/profile\n\nExample of a `200 OK` response:\n\n  ```json\n  {\n    \"name\": \"...\"\n  }\n  ```\n\n### Set DNS general settings: POST /control/dns_config\n\nReplaces the `POST /control/enable_protection` and `POST /control/disable_protection` API methods.  Example of a `POST /control/dns_config` request:\n\n  ```json\n  {\n    \"protection_enabled\": true | false,\n    \"ratelimit\": 1234,\n    \"blocking_mode\": \"nxdomain\" | \"null_ip\" | \"custom_ip\",\n    \"blocking_ipv4\": \"1.2.3.4\",\n    \"blocking_ipv6\": \"1:2:3::4\",\n  }\n  ```\n\n## v0.99: incompatible API changes\n\n- A note about web user authentication.\n- Set filtering parameters: `POST /control/filtering/config`.\n- Set filter parameters: `POST /control/filtering/set_url`.\n- Set querylog parameters: `POST /control/querylog_config`.\n- Get statistics data: `GET /control/stats`.\n\n### A note about web user authentication\n\nIf AdGuard Home’s web user is password-protected, a web client must use authentication mechanism when sending requests to server.  Basic access authentication is the most simple method - a client must pass `Authorization` HTTP header along with all requests:\n\n  ```http\n  Authorization: Basic BASE64_DATA\n  ```\n\nwhere `BASE64_DATA` is base64-encoded data for `username:password` string.\n\n### Set filtering parameters: POST /control/filtering/config\n\nReplaces the `POST /control/filtering/enable` and `POST /control/filtering/disable` API methods.  Example of a `POST /control/filtering/config` request:\n\n  ```json\n  {\n    \"enabled\": true | false,\n    \"interval\": 0 | 1 | 12 | 1*24 | 3*24 | 7*24\n  }\n  ```\n\n### Set filter parameters: POST /control/filtering/set_url\n\nReplaces the `POST /control/filtering/enable_url` and `POST /control/filtering/disable_url` API methods.\n\nExample of a `POST /control/filtering/set_url` request:\n\n  ```json\n  {\n    \"url\": \"...\",\n    \"enabled\": true | false\n  }\n  ```\n\n### Set querylog parameters: POST /control/querylog_config\n\nReplaces the `POST /querylog_enable` and `POST /querylog_disable` API methods.\n\nExample of a `POST /control/querylog_config` request:\n\n  ```json\n  {\n    \"enabled\": true | false,\n    \"interval\": 0 | 1 | 12 | 1*24 | 3*24 | 7*24\n  }\n  ```\n\n### Get statistics data: GET /control/stats\n\nReplaces the `GET /control/stats_top` and `GET /control/stats_history` API methods.  Example of a `200 OK` response:\n\n  ```json\n  {\n    \"time_units\": \"hours\" | \"days\",\n    \"num_dns_queries\": 123,\n    \"num_blocked_filtering\": 123,\n    \"num_replaced_safebrowsing\": 123,\n    \"num_replaced_safesearch\": 123,\n    \"num_replaced_parental\": 123,\n    \"avg_processing_time\": 123.123,\n    \"dns_queries\": [123, ...],\n    \"blocked_filtering\": [123, ...],\n    \"replaced_parental\": [123, ...],\n    \"replaced_safebrowsing\": [123, ...],\n    \"top_queried_domains\": [\n      {\"host\": 123},\n      ...\n    ],\n    \"top_blocked_domains\": [\n      {\"host\": 123},\n      ...\n    ],\n    \"top_clients\": [\n      {\"IP\": 123},\n      ...\n    ]\n  }\n  ```\n"
  },
  {
    "path": "openapi/README.md",
    "content": "# AdGuard Home OpenAPI\n\nWe are using [OpenAPI specification](https://swagger.io/docs/specification/about/) to generate AdGuard Home API specification.\n\n## How to edit the API spec\n\nThe easiest way would be to use [Swagger Editor](http://editor.swagger.io/) and just copy/paste the YAML file there.\n\n## Changelog\n\nSee [`CHANGELOG.md`](CHANGELOG.md) where we keep track of all non-compatible changes that are being made.\n\n## Authentication\n\nIf AdGuard Home’s web user is password-protected, a web client must use authentication mechanism when sending requests to server. Basic access authentication is the most simple method: a client must pass `Authorization` HTTP header along with all requests:\n\n```http\nAuthorization: Basic BASE64_DATA\n```\n\nWhere `BASE64_DATA` is base64-encoded data for `username:password` string.\n"
  },
  {
    "path": "openapi/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>AdGuard Home API</title>\n    <!-- needed for adaptive design -->\n    <meta charset=\"utf-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700\" rel=\"stylesheet\">\n\n    <!--\n    ReDoc doesn't change outer page styles\n    -->\n    <style>\n      body {\n        margin: 0;\n        padding: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <redoc spec-url='https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/openapi/openapi.yaml'></redoc>\n    <script src=\"https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js\"> </script>\n  </body>\n</html>\n"
  },
  {
    "path": "openapi/next.yaml",
    "content": "'openapi': '3.0.3'\n'info':\n  'contact':\n    'email': 'devteam@adguard.com'\n    'name': 'AdGuard Home'\n    'url': 'https://github.com/AdguardTeam/AdGuardHome'\n  'description': |\n    **!! WARNING!  API IS AT THE DRAFT STAGE! THINGS WILL BREAK! !!**\n\n    AdGuard Home REST API, V1 **DRAFT**.  Our administration web interface is built on top of this REST API.\n\n    This API is currently a **DRAFT** and is not covered by any stability guarantees.  Once this API reaches maturity, the old `/control/` API will mostly be removed.\n\n    ## Information for API users\n\n    - Empty arrays are always sent by the backend, unless documented otherwise.  If the backend doesn't, it's a backend error.\n\n    - `PATCH` requests with JSON bodies use RFC 7396 JSON Merge Patch unless documented otherwise.\n\n    - The property `x-error-class` on plain text error responses suggests, which class of error should be used if the API user wants to wrap it into an object.  The property `x-error-code` suggest the error code for the error object.  The code usually goes into the `code` property, and the content, into `msg`.\n\n    - The property `x-skip-web-api` on operations suggests API clients for web, like our frontend, to skip this operation in their generated code.\n\n    - The header `Server` will be set to `AdGuardHome/<<full-version>>`.  For example: `AdGuardHome/v0.107.0-a.42+abcd1234`.\n\n    ## Conventions for API authors\n\n    ### Naming\n\n    - `CapitalCamelCase` for entities.\n\n    - Initialisms are spelled like `DhcpSettings` and not `DHCPSettings`.\n\n    - `lower_snake_case` for path and query parameters.\n\n    - No unit suffices.\n\n    - Path parameters' names start with `Path`; query, with `Query`.\n\n    - Requests end with `Req`; responses, with `Resp`.\n\n    ### Structure\n\n    - Add `400` and `422` responses to requests that accept data.\n\n    - Add `401` responses, unless the method can work without authorization.\n\n    - Add `500` responses.\n\n    - Descriptions are always on their own lines.\n\n    - Don't add a description if there is already one a level above.\n\n    - Five levels of indentation max, except for descriptions and array items.\n\n    - Keep things in alphabetical order.\n\n    - Mark required things as such.  Document possibly-absent fields **both** in `required` **and** in `description`, if there is one.\n\n    - Prefer flat objects.  Example: `resp.top_user`, not `resp.top.user.val`.\n\n    - Prefer to make it easier for the frontend where possible.\n\n    - Provide examples for requests and responses.  If examples are provided elsewhere, document that.\n\n    - Summaries and descriptions with dots.\n\n    - Top-level value in a JSON request or response must be an object.\n\n    ### Types\n\n    - Add `'maximum': 65535` for 16-bit unsigned integers (for example, port numbers).\n\n    - Add `'minimum': 0` for unsigned integers.\n\n    - Duration in milliseconds.  Time in milliseconds in the Unix epoch.  Both of type `double`, because that is easier for the JS frontend.\n\n    - Integers are always `int64`, numbers are always `double.\n\n  'license':\n    'name': 'GNU General Public License v3.0'\n    'url': 'https://www.gnu.org/licenses/gpl-3.0.txt'\n  'title': 'AdGuard Home V1 DRAFT API'\n  'version': '0.108'\n\n'servers':\n- 'description': >\n    The V1 HTTP API namespace.\n  'url': '/api/v1'\n\n'security':\n- 'basicAuth': []\n\n'tags':\n- 'description': >\n    Authorization and account management.\n  'name': 'accounts'\n- 'description': >\n    Configuration and settings for Apple products.\n  'name': 'apple'\n- 'description': >\n    Runtime and persistent client information.\n  'name': 'clients'\n- 'description': >\n    DHCP server methods.\n  'name': 'dhcp'\n- 'description': >\n    First-time install configuration handlers.  Will not be available once the\n    installation is done.\n  'name': 'install'\n- 'description': >\n    Query logs.\n  'name': 'log'\n- 'description': >\n    Filter lists, blocked services, and custom filtering rules.\n  'name': 'protection'\n- 'description': >\n    Settings management.\n  'name': 'settings'\n- 'description': >\n    Query, filtering, system, and other statistics.\n  'name': 'stats'\n- 'description': >\n    Information about the AdGuard Home server and the host system.\n  'name': 'system'\n\n'paths':\n  '/health-check':\n    'get':\n      'operationId': 'HealthCheck'\n      'responses':\n        '200':\n          'description': >\n            An OK response.\n          'content':\n            'text/plain':\n              'example': 'OK'\n      'servers':\n        - 'url': '/'\n      'summary': 'Check if the server is up.'\n      'tags':\n      - 'system'\n\n  '/accounts/profile':\n    'get':\n      'operationId': 'GetV1AccountsProfile'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1AccountsProfileResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get the profile of the current user.'\n      'tags':\n      - 'accounts'\n    'patch':\n      'operationId': 'PatchV1AccountsProfile'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1AccountsProfileReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1AccountsProfileResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update the profile of the current user.'\n      'tags':\n      - 'accounts'\n\n  '/accounts/session':\n    'delete':\n      'operationId': 'DeleteV1AccountsSession'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Log out of the system.'\n      'tags':\n      - 'accounts'\n    'post':\n      'operationId': 'PostV1AccountsSession'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1AccountsSessionReq'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Log into the system.'\n      'tags':\n      - 'accounts'\n\n  '/apple/doh.mobileconfig':\n    'get':\n      'operationId': 'GetV1AppleDohMobileconfig'\n      'parameters':\n      - '$ref': '#/components/parameters/QueryClientId'\n      - '$ref': '#/components/parameters/QueryHost'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1AppleDohMobileconfigResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get a DNS-over-HTTPS .mobileconfig.'\n      'tags':\n      - 'apple'\n      'x-skip-web-api': true\n\n  '/apple/dot.mobileconfig':\n    'get':\n      'operationId': 'GetV1AppleDotMobileconfig'\n      'parameters':\n      - '$ref': '#/components/parameters/QueryHost'\n      - '$ref': '#/components/parameters/QueryClientId'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1AppleDotMobileconfigResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get a DNS-over-HTTPS .mobileconfig.'\n      'tags':\n      - 'apple'\n      'x-skip-web-api': true\n\n  '/clients/persistent':\n    'get':\n      'operationId': 'GetV1ClientsPersistent'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1ClientsPersistentResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get all persistent clients.'\n      'tags':\n      - 'clients'\n    'post':\n      'operationId': 'PostV1ClientsPersistent'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1ClientsPersistentReq'\n      'responses':\n        '201':\n          '$ref': '#/components/responses/PostV1ClientsPersistentResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Create a new persistent client.'\n      'tags':\n      - 'clients'\n\n  '/clients/persistent/{client_uid}':\n    'delete':\n      'operationId': 'DeleteV1ClientPersistent'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '404':\n          '$ref': '#/components/responses/NotFoundResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Delete a persistent client.'\n      'tags':\n      - 'clients'\n    'parameters':\n    - '$ref': '#/components/parameters/PathClientUid'\n    'patch':\n      'operationId': 'PatchV1ClientPersistent'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1ClientPersistentReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1ClientPersistentResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '404':\n          '$ref': '#/components/responses/NotFoundResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update a persistent client.'\n      'tags':\n      - 'clients'\n\n  '/clients/runtime':\n    'get':\n      'operationId': 'GetV1ClientsRuntime'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1ClientsRuntimeResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get all runtime clients.'\n      'tags':\n      - 'clients'\n\n  '/dhcp/leases':\n    'get':\n      'operationId': 'GetV1DhcpLeases'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1DhcpLeasesResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get all dynamic and static DHCP leases.'\n      'tags':\n      - 'dhcp'\n    'post':\n      'operationId': 'PostV1DhcpLeases'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1DhcpLeasesReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PostV1DhcpLeasesResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Create a new static DHCP lease.'\n      'tags':\n      - 'dhcp'\n\n  '/dhcp/leases/{lease_uid}':\n    'delete':\n      'operationId': 'DeleteV1DhcpLease'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '404':\n          '$ref': '#/components/responses/NotFoundResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Delete a static DHCP lease.'\n      'tags':\n      - 'dhcp'\n    'parameters':\n    - '$ref': '#/components/parameters/PathLeaseUid'\n    'patch':\n      'operationId': 'PatchV1DhcpLease'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1DhcpLeaseReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1DhcpLeaseResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '404':\n          '$ref': '#/components/responses/NotFoundResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update a static DHCP lease.'\n      'tags':\n      - 'dhcp'\n\n  '/dhcp/status':\n    'get':\n      'operationId': 'GetV1DhcpStatus'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1DhcpStatusResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get DHCP server status.'\n      'tags':\n      - 'dhcp'\n\n  '/install/check':\n    'post':\n      'operationId': 'PostV1InstallCheck'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1InstallCheckReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PostV1InstallCheckResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Check initial configuration.'\n      'tags':\n      - 'install'\n\n  '/install/configure':\n    'post':\n      'operationId': 'PostV1InstallConfigure'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1InstallConfigureReq'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Apply initial configuration.'\n      'tags':\n      - 'install'\n\n  '/install/info':\n    'get':\n      'operationId': 'GetV1InstallInfo'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1InstallInfoResp'\n        '404':\n          '$ref': '#/components/responses/NotFoundResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get initial configuration information.'\n      'tags':\n      - 'install'\n\n  '/log/clear':\n    'post':\n      'operationId': 'PostV1LogClear'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1LogClearReq'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Clear the whole query log.'\n      'tags':\n      - 'log'\n\n  '/log/search':\n    'get':\n      'operationId': 'GetV1LogSearch'\n      'parameters':\n      - '$ref': '#/components/parameters/QueryBefore'\n      - '$ref': '#/components/parameters/QueryLimit'\n      - '$ref': '#/components/parameters/QueryReason'\n      - '$ref': '#/components/parameters/QueryTerm'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1LogSearchResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Search query logs.'\n      'tags':\n      - 'log'\n\n  '/protection/blocked_services':\n    'get':\n      'operationId': 'GetV1ProtectionBlockedServices'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1ProtectionBlockedServicesResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get blocked services.'\n      'tags':\n      - 'protection'\n    'put':\n      'operationId': 'PutV1ProtectionBlockedServices'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PutV1ProtectionBlockedServicesReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1ProtectionBlockedServicesResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Replace blocked services.'\n      'tags':\n      - 'protection'\n\n  '/protection/check_custom_rules':\n    'post':\n      'operationId': 'PostV1ProtectionCheckCustomRules'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1ProtectionCheckCustomRulesReq'\n      'responses':\n        '201':\n          '$ref': '#/components/responses/PostV1ProtectionCheckCustomRulesResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Check custom filtering rules.'\n      'tags':\n      - 'protection'\n\n  '/protection/custom_rules':\n    'get':\n      'operationId': 'GetV1ProtectionCustomRules'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1ProtectionCustomRulesResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get custom rules.'\n      'tags':\n      - 'protection'\n    'put':\n      'operationId': 'PutV1ProtectionCustomRules'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PutV1ProtectionCustomRulesReq'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Replace custom rules.'\n      'tags':\n      - 'protection'\n\n  '/protection/dns_rewrites':\n    'get':\n      'operationId': 'GetV1ProtectionDnsRewrites'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1ProtectionDnsRewritesResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get all classic DNS rewrites.'\n      'tags':\n      - 'protection'\n    'post':\n      'operationId': 'PostV1ProtectionDnsRewrites'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1ProtectionDnsRewritesReq'\n      'responses':\n        '201':\n          '$ref': '#/components/responses/PostV1ProtectionDnsRewritesResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Add a new classic DNS rewrite.'\n      'tags':\n      - 'protection'\n\n  '/protection/dns_rewrites/{dns_rewrite_uid}':\n    'delete':\n      'operationId': 'DeleteV1ProtectionDnsRewrite'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '404':\n          '$ref': '#/components/responses/NotFoundResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Delete a classic DNS rewrite.'\n      'tags':\n      - 'protection'\n    'parameters':\n    - '$ref': '#/components/parameters/PathDnsRewriteUid'\n\n  '/protection/filters':\n    'get':\n      'operationId': 'GetV1ProtectionFilters'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1ProtectionFiltersResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get all filters.'\n      'tags':\n      - 'protection'\n    'post':\n      'operationId': 'PostV1ProtectionFilters'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1ProtectionFiltersReq'\n      'responses':\n        '201':\n          '$ref': '#/components/responses/PostV1ProtectionFiltersResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Add a new filter.'\n      'tags':\n      - 'protection'\n\n  '/protection/filters/{filter_uid}':\n    'delete':\n      'operationId': 'DeleteV1ProtectionFilter'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '404':\n          '$ref': '#/components/responses/NotFoundResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Delete a filter.'\n      'tags':\n      - 'protection'\n    'parameters':\n    - '$ref': '#/components/parameters/PathFilterUid'\n    'patch':\n      'operationId': 'PatchV1ProtectionFilter'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1ProtectionFilterReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1ProtectionFilterResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '404':\n          '$ref': '#/components/responses/NotFoundResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': >\n        Update a filter's settings.\n      'tags':\n      - 'protection'\n\n  '/protection/refresh_filters':\n    'post':\n      'operationId': 'PostV1ProtectionRefreshFilters'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1ProtectionRefreshFiltersReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PostV1ProtectionRefreshFiltersResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': >\n        Refresh all filters.\n      'tags':\n      - 'protection'\n\n  '/protection/refresh_filters/{filter_uid}':\n    'parameters':\n    - '$ref': '#/components/parameters/PathFilterUid'\n    'post':\n      'operationId': 'PostV1ProtectionRefreshFilter'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1ProtectionRefreshFilterReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PostV1ProtectionRefreshFilterResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': >\n        Refresh a filter.\n      'tags':\n      - 'protection'\n\n  '/settings/all':\n    'get':\n      'operationId': 'GetV1SettingsAll'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1SettingsAllResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get all settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/dhcp':\n    'patch':\n      'operationId': 'PatchV1SettingsDhcp'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1SettingsDhcpReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1SettingsDhcpResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update DHCP server settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/dns':\n    'patch':\n      'operationId': 'PatchV1SettingsDns'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1SettingsDnsReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1SettingsDnsResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update DNS server settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/dns/access':\n    'get':\n      'description': >\n        Get DNS access settings.  This is a separate API, because these lists\n        can become quite big.\n      'operationId': 'GetV1SettingsDnsAccess'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1SettingsDnsAccessResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get DNS access settings.'\n      'tags':\n      - 'settings'\n    'put':\n      'description': >\n        Update DNS access settings.  This is a separate API, because these lists\n        can become quite big.\n      'operationId': 'PutV1SettingsDnsAccess'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PutV1SettingsDnsAccessReq'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update DNS access settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/dns/check':\n    'post':\n      'operationId': 'PostV1SettingsDnsCheck'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1SettingsDnsCheckReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PostV1SettingsDnsCheckResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Check DNS upstream settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/http':\n    'patch':\n      'operationId': 'PatchV1SettingsHttp'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1SettingsHttpReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1SettingsHttpResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update web interface settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/log':\n    'patch':\n      'operationId': 'PatchV1SettingsLog'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1SettingsLogReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1SettingsLogResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update query logging settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/protection':\n    'patch':\n      'operationId': 'PatchV1SettingsProtection'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1SettingsProtectionReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1SettingsProtectionResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update protection settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/stats':\n    'patch':\n      'operationId': 'PatchV1SettingsStats'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1SettingsStatsReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1SettingsStatsResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update statistics settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/tls':\n    'patch':\n      'operationId': 'PatchV1SettingsTls'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PatchV1SettingsTlsReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PatchV1SettingsTlsResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update TLS and encryption settings.'\n      'tags':\n      - 'settings'\n\n  '/settings/tls/check':\n    'post':\n      'operationId': 'PostV1SettingsTlsCheck'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1SettingsTlsCheckReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PostV1SettingsTlsCheckResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Check TLS and encryption settings.'\n      'tags':\n      - 'settings'\n\n  '/stats/all':\n    'get':\n      'operationId': 'GetV1StatsAll'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1StatsAllResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get all statistics.'\n      'tags':\n      - 'stats'\n\n  '/stats/clear':\n    'post':\n      'operationId': 'PostV1StatsClear'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1StatsClearReq'\n      'responses':\n        '204':\n          '$ref': '#/components/responses/NoContentResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Clear all statistics.'\n      'tags':\n      - 'stats'\n\n  '/system/info':\n    'get':\n      'operationId': 'GetV1SystemInfo'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/GetV1SystemInfoResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Get server information.'\n      'tags':\n      - 'system'\n\n  '/system/reset':\n    'post':\n      'operationId': 'PostV1SystemReset'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1SystemResetReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PostV1SystemResetResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Reset all settings to defaults.'\n      'tags':\n      - 'system'\n\n  '/system/update':\n    'post':\n      'operationId': 'PostV1SystemUpdate'\n      'requestBody':\n        '$ref': '#/components/requestBodies/PostV1SystemUpdateReq'\n      'responses':\n        '200':\n          '$ref': '#/components/responses/PostV1SystemUpdateResp'\n        '400':\n          '$ref': '#/components/responses/BadRequestResp'\n        '401':\n          '$ref': '#/components/responses/UnauthorizedResp'\n        '422':\n          '$ref': '#/components/responses/UnprocessableEntityResp'\n        '500':\n          '$ref': '#/components/responses/InternalServerErrorResp'\n      'summary': 'Update AdGuard Home.'\n      'tags':\n      - 'system'\n\n'components':\n  'parameters':\n    'PathDnsRewriteUid':\n      'description': >\n        DNS rewrite ID.\n      'example': 'abcd1234'\n      'in': 'path'\n      'name': 'dns_rewrite_uid'\n      'required': true\n      'schema':\n        '$ref': '#/components/schemas/Uid'\n\n    'PathClientUid':\n      'description': >\n        The unique ID of a client.\n      'example': 'abcd1234'\n      'in': 'path'\n      'name': 'client_uid'\n      'required': true\n      'schema':\n        '$ref': '#/components/schemas/Uid'\n\n    'PathFilterUid':\n      'description': >\n        The ID of a filter.\n      'example': 'abcd1234'\n      'in': 'path'\n      'name': 'filter_uid'\n      'required': true\n      'schema':\n        '$ref': '#/components/schemas/Uid'\n\n    'PathLeaseUid':\n      'description': >\n        The ID of a static lease.\n      'example': 'abcd1234'\n      'in': 'path'\n      'name': 'lease_uid'\n      'required': true\n      'schema':\n        '$ref': '#/components/schemas/Uid'\n\n    'QueryBefore':\n      'description': >\n        Unix time, before which to show the search results, in milliseconds.\n      'example': 1614345496000\n      'in': 'query'\n      'name': 'before'\n      'required': false\n      'schema':\n        'format': 'double'\n        'type': 'number'\n\n    'QueryClientId':\n      'description': >\n        ClientID, **not** its UID.\n      'example': 'client-1'\n      'in': 'query'\n      'name': 'client_id'\n      'required': false\n      'schema':\n        '$ref': '#/components/schemas/ClientId'\n\n    'QueryHost':\n      'description': >\n        The host for which the Configuration is generated.\n      'example': 'example.org'\n      'in': 'query'\n      'name': 'host'\n      'required': true\n      'schema':\n        'type': 'string'\n\n    'QueryLimit':\n      'description': >\n        Maximum amount of records to return.\n      'example': 100\n      'in': 'query'\n      'name': 'limit'\n      'required': false\n      'schema':\n        'format': 'int64'\n        'type': 'integer'\n\n    'QueryReason':\n      'description': >\n        Filter query log results by filtering reason.\n      'example': 'not_filtered_notfound'\n      'in': 'query'\n      'name': 'reason'\n      'required': false\n      'schema':\n        '$ref': '#/components/schemas/FilteringReason'\n\n    'QueryTerm':\n      'description': >\n        Search term.\n      'example': '127.0.0.1'\n      'in': 'query'\n      'name': 'term'\n      'required': false\n      'schema':\n        'type': 'string'\n\n  'requestBodies':\n    'PatchV1AccountsProfileReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1AccountsProfileReq'\n      'required': true\n\n    'PatchV1ClientPersistentReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1ClientPersistentReq'\n      'required': true\n\n    'PatchV1DhcpLeaseReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1DhcpLeaseReq'\n      'required': true\n\n    'PatchV1ProtectionFilterReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1ProtectionFilterReq'\n      'required': true\n\n    'PatchV1SettingsDhcpReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsDhcpReq'\n      'required': true\n\n    'PatchV1SettingsDnsReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsDnsReq'\n      'required': true\n\n    'PatchV1SettingsHttpReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsHttpReq'\n      'required': true\n\n    'PatchV1SettingsLogReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsLogReq'\n      'required': true\n\n    'PatchV1SettingsProtectionReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsProtectionReq'\n      'required': true\n\n    'PatchV1SettingsStatsReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsStatsReq'\n      'required': true\n\n    'PatchV1SettingsTlsReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsTlsReq'\n      'required': true\n\n    'PostV1AccountsSessionReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1AccountsSessionReq'\n      'required': true\n\n    'PostV1ClientsPersistentReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ClientsPersistentReq'\n      'required': true\n\n    'PostV1DhcpLeasesReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1DhcpLeasesReq'\n      'required': true\n\n    'PostV1InstallCheckReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1InstallCheckReq'\n      'required': true\n\n    'PostV1InstallConfigureReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1InstallConfigureReq'\n      'required': true\n\n    'PostV1LogClearReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1LogClearReq'\n      'required': true\n\n    'PostV1ProtectionCheckCustomRulesReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionCheckCustomRulesReq'\n      'required': true\n\n    'PostV1ProtectionDnsRewritesReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionDnsRewritesReq'\n      'required': true\n\n    'PostV1ProtectionFiltersReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionFiltersReq'\n      'required': true\n\n    'PostV1ProtectionRefreshFilterReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionRefreshFilterReq'\n      'required': true\n\n    'PostV1ProtectionRefreshFiltersReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionRefreshFiltersReq'\n      'required': true\n\n    'PostV1SettingsDnsCheckReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1SettingsDnsCheckReq'\n      'required': true\n\n    'PostV1SettingsTlsCheckReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1SettingsTlsCheckReq'\n      'required': true\n\n    'PostV1StatsClearReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1StatsClearReq'\n      'required': true\n\n    'PostV1SystemResetReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1SystemResetReq'\n      'required': true\n\n    'PostV1SystemUpdateReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1SystemUpdateReq'\n      'required': true\n\n    'PutV1ProtectionBlockedServicesReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PutV1ProtectionBlockedServicesReq'\n      'required': true\n\n    'PutV1ProtectionCustomRulesReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PutV1ProtectionCustomRulesReq'\n      'required': true\n\n    'PutV1SettingsDnsAccessReq':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PutV1SettingsDnsAccessReq'\n      'required': true\n\n  'responses':\n    'BadRequestResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/BadRequestResp'\n        'text/plain':\n          'example': >-\n            invalid character '{' looking for beginning of object key string\n          'x-error-class': '#/components/schemas/BadRequestResp'\n          'x-error-code': 'TXT400'\n      'description': >\n        Generic bad request response.  Sent when the request data is malformed\n        (for example, invalid JSON).\n\n    'GetV1AccountsProfileResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1AccountsProfileResp'\n      'description': >\n        A successful response to a `GET /api/v1/accounts/profile` request.\n\n    'GetV1AppleDohMobileconfigResp':\n      'content':\n        'application/xml':\n          'schema':\n            '$ref': '#/components/schemas/GetV1AppleDohMobileconfigResp'\n      'description': >\n        A successful response to a `GET /api/v1/apple/doh.mobileconfig` request.\n\n    'GetV1AppleDotMobileconfigResp':\n      'content':\n        'application/xml':\n          'schema':\n            '$ref': '#/components/schemas/GetV1AppleDotMobileconfigResp'\n      'description': >\n        A successful response to a `GET /api/v1/apple/dot.mobileconfig` request.\n\n    'GetV1ClientsPersistentResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1ClientsPersistentResp'\n      'description': >\n        A successful response to a `GET /api/v1/clients/persistent` request.\n\n    'GetV1ClientsRuntimeResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1ClientsRuntimeResp'\n      'description': >\n        A successful response to a `GET /api/v1/clients/runtime` request.\n\n    'GetV1DhcpLeasesResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1DhcpLeasesResp'\n      'description': >\n        A successful response to a `GET /api/v1/dhcp/leases` request.\n\n    'GetV1DhcpStatusResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1DhcpStatusResp'\n      'description': >\n        A successful response to a `GET /api/v1/dhcp/status` request.\n\n    'GetV1InstallInfoResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1InstallInfoResp'\n      'description': >\n        A successful response to a `GET /api/v1/install/info` request.\n\n    'GetV1LogSearchResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1LogSearchResp'\n      'description': >\n        A successful response to a `GET /api/v1/log/search` request.\n\n    'GetV1ProtectionBlockedServicesResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1ProtectionBlockedServicesResp'\n      'description': >\n        A successful response to a `GET /api/v1/protection/blocked_services`\n        or a `PUT /api/v1/protection/blocked_services` request.\n\n    'GetV1ProtectionCustomRulesResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1ProtectionCustomRulesResp'\n      'description': >\n        A successful response to a `GET /api/v1/protection/custom_rules`\n        request.\n\n    'GetV1ProtectionDnsRewritesResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1ProtectionDnsRewritesResp'\n      'description': >\n        A successful response to a `GET /api/v1/protection/dns_rewrites`\n        request.\n\n    'GetV1ProtectionFiltersResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1ProtectionFiltersResp'\n      'description': >\n        A successful response to a `GET /api/v1/protection/filters` request.\n\n    'GetV1SettingsAllResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1SettingsAllResp'\n      'description': >\n        A successful response to a `GET /api/v1/settings/all` request.\n\n    'GetV1SettingsDnsAccessResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1SettingsDnsAccessResp'\n      'description': >\n        A successful response to a `GET /api/v1/settings/dns/access` request.\n\n    'GetV1StatsAllResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1StatsAllResp'\n      'description': >\n        A successful response to a `GET /api/v1/stats/all` request.\n\n    'GetV1SystemInfoResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/GetV1SystemInfoResp'\n      'description': >\n        A successful response to a `GET /api/v1/server/info` request.\n\n    'InternalServerErrorResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/InternalServerErrorResp'\n        'text/plain':\n          'example': >-\n            runtime error: invalid memory address or nil pointer dereference\n          'x-error-class': '#/components/schemas/InternalServerErrorResp'\n          'x-error-code': 'TXT500'\n      'description': >\n        Generic internal server error.\n\n    'NoContentResp':\n      'description': >\n        Generic no-error no-content response.\n\n    'NotFoundResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/NotFoundResp'\n        'text/plain':\n          'example': >-\n            Not found.\n          'x-error-class': '#/components/schemas/NotFoundResp'\n          'x-error-code': 'TXT404'\n      'description': >\n        Generic not found response.\n\n    'PatchV1AccountsProfileResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1AccountsProfileResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/accounts/profile` request.\n\n    'PatchV1ClientPersistentResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1ClientPersistentResp'\n      'description': >\n        A successful response to\n        a `PATCH /api/v1/clients/persistent/{client_uid}` request.\n\n    'PatchV1DhcpLeaseResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1DhcpLeaseResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/dhcp/leases/{lease_uid}`\n        request.\n\n    'PatchV1ProtectionFilterResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1ProtectionFilterResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/filters/{filter_uid}` request.\n\n    'PatchV1SettingsDhcpResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsDhcpResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/settings/dhcp` request.\n\n    'PatchV1SettingsDnsResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsDnsResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/settings/dns` request.\n\n    'PatchV1SettingsHttpResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsHttpResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/settings/http` request.\n\n    'PatchV1SettingsLogResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsLogResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/settings/log` request.\n\n    'PatchV1SettingsProtectionResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsProtectionResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/settings/protection` request.\n\n    'PatchV1SettingsStatsResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsStatsResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/settings/stats` request.\n\n    'PatchV1SettingsTlsResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PatchV1SettingsTlsResp'\n      'description': >\n        A successful response to a `PATCH /api/v1/settings/tls` request.\n\n    'PostV1ClientsPersistentResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ClientsPersistentResp'\n      'description': >\n        A successful response to a `POST /api/v1/clients/persistent` request.\n\n    'PostV1DhcpLeasesResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1DhcpLeasesResp'\n      'description': >\n        A successful response to a `POST /api/v1/dhcp/leases` request.\n\n    'PostV1InstallCheckResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1InstallCheckResp'\n      'description': >\n        A successful response to a `POST /api/v1/install/check` request.\n\n    'PostV1ProtectionCheckCustomRulesResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionCheckCustomRulesResp'\n      'description': >\n        A successful response to a `POST /api/v1/protection/check_custom_rules`\n        request.\n\n    'PostV1ProtectionDnsRewritesResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionDnsRewritesResp'\n      'description': >\n        A successful response to a `POST /api/v1/protection/dns_rewrites`\n        request.\n\n    'PostV1ProtectionFiltersResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionFiltersResp'\n      'description': >\n        A successful response to a `POST /api/v1/protection/filters` request.\n\n    'PostV1ProtectionRefreshFilterResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionRefreshFilterResp'\n      'description': >\n        A successful response to\n        a `POST /api/v1/protection/refresh_filters/{filter_uid}` request.\n\n    'PostV1ProtectionRefreshFiltersResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1ProtectionRefreshFiltersResp'\n      'description': >\n        A successful response to a `POST /api/v1/protection/refresh_filters`\n        request.\n\n    'PostV1SettingsDnsCheckResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1SettingsDnsCheckResp'\n      'description': >\n        A successful response to a `POST /api/v1/settings/dns/check` request.\n\n    'PostV1SettingsTlsCheckResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1SettingsTlsCheckResp'\n      'description': >\n        A successful response to a `POST /api/v1/settings/tls/check` request.\n\n    'PostV1SystemResetResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1SystemResetResp'\n      'description': >\n        A successful response to a `POST /api/v1/system/reset` request.\n\n    'PostV1SystemUpdateResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/PostV1SystemUpdateResp'\n      'description': >\n        A successful response to a `POST /api/v1/system/update` request.\n\n    'UnauthorizedResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/UnauthorizedResp'\n        'text/plain':\n          'example': 'no or bad authorization provided'\n          'x-error-class': '#/components/schemas/UnauthorizedResp'\n          'x-error-code': 'TXT401'\n      'description': >\n        This API requires authorization.\n      'headers':\n        'WWW-Authenticate':\n          'description': >\n            The required WWW-Authenticate header.\n          'example': 'Basic realm=\"AdGuard Home\", charset=\"UTF-8\"'\n          'required': true\n          'schema':\n            'type': 'string'\n\n    'UnprocessableEntityResp':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/UnprocessableEntityResp'\n      'description': >\n        Generic bad request data response.  Sent when the request data is\n        well-formed but is invalid for this request.\n\n  'schemas':\n    'BadRequestResp':\n      'example':\n        'code': 'JSN000'\n        'msg': >-\n          invalid character '{' looking for beginning of object key string\n      'properties':\n        'code':\n          '$ref': '#/components/schemas/ErrorCode'\n        'msg':\n          'description': >\n            Error message string.\n          'type': 'string'\n      'required':\n      - 'code'\n      - 'msg'\n      'type': 'object'\n\n    'BlockedServiceId':\n      'description': >\n        ID of a blocked service.\n      'enum':\n      - '9gag'\n      - 'amazon'\n      - 'cloudflare'\n      - 'dailymotion'\n      - 'discord'\n      - 'disneyplus'\n      - 'ebay'\n      - 'epic_games'\n      - 'facebook'\n      - 'hulu'\n      - 'imgur'\n      - 'instagram'\n      - 'mail_ru'\n      - 'netflix'\n      - 'ok'\n      - 'origin'\n      - 'pinterest'\n      - 'qq'\n      - 'reddit'\n      - 'skype'\n      - 'snapchat'\n      - 'spotify'\n      - 'steam'\n      - 'telegram'\n      - 'tiktok'\n      - 'tinder'\n      - 'twitch'\n      - 'twitter'\n      - 'viber'\n      - 'vimeo'\n      - 'vk'\n      - 'wechat'\n      - 'weibo'\n      - 'whatsapp'\n      - 'youtube'\n      'type': 'string'\n\n    'BlockedServices':\n      'description': >\n        Blocked services.\n      'example':\n        'services':\n        - '9gag'\n        - 'dailymotion'\n      'properties':\n        'services':\n          'description': >\n            All blocked services.\n          'items':\n            '$ref': '#/components/schemas/BlockedServiceId'\n          'type': 'array'\n      'required':\n      - 'services'\n      'type': 'object'\n\n    'Channel':\n      'description': >\n        AdGuard Home release channel.\n      'enum':\n      - 'beta'\n      - 'development'\n      - 'edge'\n      - 'release'\n      'type': 'string'\n\n    'ClientId':\n      'pattern': '[0-9a-z-]{1,64}'\n      'type': 'string'\n\n    'ClientInfo':\n      'description': >\n        A shorter information about a client.  If the `uid` field is present,\n        this is a persistent client.  Otherwise, this is a runtime client.\n      'properties':\n        'blocked':\n          'description': >\n            If `true`, client is blocked.\n          'type': 'boolean'\n        'ids':\n          'description': |\n            Client identifiers.  That includes ClientIDs set by users as well as\n            IP addresses.  There must be at least one identifier.\n\n            Not to be confused with the `uid` field.\n          'example':\n          - '1.2.3.4'\n          - 'user-1'\n          'items':\n            'type': 'string'\n          'minItems': 1\n          'type': 'array'\n        'name':\n          'description': >\n            The name of the client, if any.  If there are none, this field is\n            absent.\n          'example': 'User 1'\n          'type': 'string'\n        'num':\n          'description': >\n            Total number of requests for this client.\n          'example': 1000\n          'format': 'int64'\n          'type': 'integer'\n        'num_blocked':\n          'description': >\n            Total number of blocked requests for this client.\n          'example': 1000\n          'format': 'int64'\n          'type': 'integer'\n        'uid':\n          '$ref': '#/components/schemas/Uid'\n        'whois':\n          '$ref': '#/components/schemas/Whois'\n      'required':\n      - 'blocked'\n      - 'ids'\n      - 'num'\n      - 'num_blocked'\n      'type': 'object'\n\n    'CustomRules':\n      'description': >\n        Custom filtering rules.\n      'example':\n        'rules':\n        - '||example.com'\n        - '# Some comment'\n      'properties':\n        'rules':\n          'description': >\n            All custom filtering rules\n          'items':\n            'type': 'string'\n          'type': 'array'\n      'required':\n      - 'rules'\n      'type': 'object'\n\n    'DhcpLease':\n      'allOf':\n      - '$ref': '#/components/schemas/DhcpLeasePost'\n      - 'description': >\n          A dynamic or static DHCP lease.  If the `uid` field is present, this is\n          a static lease.  Otherwise, this is a dynamic lease.\n        'example':\n          'expires': 1614345496000\n          'hostname': 'my-mobile'\n          'ip': '192.168.1.2'\n          'mac': '01:23:45:67:89:ab'\n          'uid': 'abcd1234'\n        'properties':\n          'uid':\n            '$ref': '#/components/schemas/Uid'\n\n    'DhcpLeasePatch':\n      'description': >\n        A static DHCP lease update object.\n      'example':\n        'expires': 1614345496000\n      'properties':\n        'expires':\n          'description': >\n            The Unix time of the lease's expiry time, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n        'hostname':\n          'description': >\n            Client's hostname.\n          'type': 'string'\n        'ip':\n          'description': >\n            IP address leased to the client.\n          'type': 'string'\n        'mac':\n          'description': >\n            Hardware address of the lease client.\n          'type': 'string'\n      'type': 'object'\n\n    'DhcpLeasePost':\n      'allOf':\n      - '$ref': '#/components/schemas/DhcpLeasePatch'\n      - 'description': >\n          A static DHCP lease create object.\n        'example':\n          'expires': 1614345496000\n          'hostname': 'my-mobile'\n          'ip': '192.168.1.2'\n          'mac': '01:23:45:67:89:ab'\n        'required':\n        - 'expires'\n        - 'hostname'\n        - 'ip'\n        - 'mac'\n\n    'DhcpSettings':\n      'allOf':\n      - '$ref': '#/components/schemas/DhcpSettingsPatch'\n      - 'description': >\n          DHCP server settings.\n        'example':\n          'enabled': true\n          'interface_name': 'wlan0'\n          'ipv4_gateway_ip': '192.168.1.1'\n          'ipv4_lease_duration': 86400000\n          'ipv4_range_end': '192.168.1.101'\n          'ipv4_range_start': '192.168.1.2'\n          'ipv4_subnet_mask': '255.255.255.0'\n          'ipv6_range_start': '2001:db8::1'\n          'ipv6_lease_duration': 86400000\n        'required':\n        - 'enabled'\n\n    'DhcpSettingsPatch':\n      'description': >\n        DHCP server settings update object.\n      'example':\n        'enabled': true\n        'interface_name': 'wlan0'\n        'ipv4_gateway_ip': '192.168.1.1'\n        'ipv4_lease_duration': 86400000\n        'ipv4_range_end': '192.168.1.101'\n        'ipv4_range_start': '192.168.1.2'\n        'ipv4_subnet_mask': '255.255.255.0'\n      'properties':\n        'enabled':\n          'description': >\n            If `true`, the DHCP server is enabled.\n          'type': 'boolean'\n        'interface_name':\n          'description': >\n            The name of network interface to serve on.\n          'type': 'string'\n        'ipv4_gateway_ip':\n          'description': >\n            The IP address of the gateway.\n          'type': 'string'\n        'ipv4_lease_duration':\n          'description': >\n            The duration of the IPv4 lease, in milliseconds.\n          'type': 'number'\n        'ipv4_range_end':\n          'description': >\n            The end of the IPv4 addresses to serve to clients.\n          'type': 'string'\n        'ipv4_range_start':\n          'description': >\n            The start of the IPv4 addresses to serve to clients.\n          'type': 'string'\n        'ipv4_subnet_mask':\n          'description': >\n            The IP subnet mask.\n          'type': 'string'\n        'ipv6_lease_duration':\n          'description': >\n            The duration of the IPv6 lease, in milliseconds.\n          'type': 'number'\n        'ipv6_range_start':\n          'description': >\n            The start of the IPv6 addresses to serve to clients.\n          'type': 'string'\n      'type': 'object'\n\n    'DnsAccessSettings':\n      'description': >\n        DNS server access settings.\n      'example':\n        'allowed_clients': []\n        'blocked_clients':\n        - '1.2.3.4'\n        - '5.6.7.8/16'\n        'blocked_domain_rules':\n        - 'id.server'\n        - '*.example.org'\n        - '||example.com^'\n      'properties':\n        'allowed_clients':\n          'description': >\n            CIDR or IP addresses of clients in the allowlist.  If non-empty,\n            AdGuard Home will accept requests from these IP addresses only.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'blocked_clients':\n          'description': >\n            CIDR or IP addresses of clients in the blocklist.  If non-empty,\n            AdGuard Home will drop requests from these IP addresses.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'blocked_domain_rules':\n          'description': >\n            AdGuard Home will drop DNS queries, if the domains in their queries\n            match these rules.  Here you can specify the exact domain\n            names, wildcards, and `urlfilter` rules.  Examples:\n\n             *  `example.org`\n\n             *  `*.example.org`\n\n             *  `||example.org^`\n          'items':\n            'type': 'string'\n          'type': 'array'\n      'required':\n      - 'allowed_clients'\n      - 'blocked_clients'\n      - 'blocked_domain_rules'\n      'type': 'object'\n\n    'DnsBlockingMode':\n      'description': |\n        DNS blocking mode.\n\n         *  `custom_ip`: Respond with a custom IP address.  If this mode is\n             selected, both `blocking_ipv4` and `blocking_ipv6` parameters must\n             be set.\n\n         *  `default`: Same as `null_ip` for Adblock-style rules, but respond\n             with the IP address specified in the rule when blocked by an\n            `/etc/hosts`-style rule.\n\n         *  `null_ip`: Respond with a zero IP address: `0.0.0.0` for `A`\n             requests and `::` for `AAAA` ones.\n\n         *  `nxdomain`: Respond with the `NXDOMAIN` code.\n\n         *  `refused`: Respond with the `REFUSET` code.\n\n      'enum':\n      - 'custom_ip'\n      - 'default'\n      - 'null_ip'\n      - 'nxdomain'\n      - 'refused'\n      'type': 'string'\n\n    'DnsClass':\n      'description': >\n        DNS resource record class, aka `CLASS`.\n      'enum':\n      - 'any'\n      - 'ch'\n      - 'cs'\n      - 'hs'\n      - 'in'\n      'type': 'string'\n\n    'DnsProto':\n      'description': >\n        DNS protocol.\n      'enum':\n      - 'dot'\n      - 'doh'\n      - 'doq'\n      - 'dnscrypt'\n      - 'udp'\n      'type': 'string'\n\n    'DnsResponseCode':\n      'description': >\n        DNS response code, aka `RCODE`.\n      'enum':\n      - 'badalg'\n      - 'badcookie'\n      - 'badkey'\n      - 'badmode'\n      - 'badname'\n      - 'badsig'\n      - 'badtime'\n      - 'badtrunc'\n      - 'badvers'\n      - 'formerr'\n      - 'noerror'\n      - 'notauth'\n      - 'notimp'\n      - 'notzone'\n      - 'nxdomain'\n      - 'nxrrset'\n      - 'refused'\n      - 'servfail'\n      - 'yxdomain'\n      - 'yxrrset'\n      'type': 'string'\n\n    'DnsRewrite':\n      'allOf':\n      - '$ref': '#/components/schemas/DnsRewritePost'\n      - 'description': >\n          A classic DNS rewrite.\n        'example':\n          'answer': 'A'\n          'domain': 'example.com'\n          'id': 'abcd1234'\n        'properties':\n          'id':\n            '$ref': '#/components/schemas/Uid'\n        'required':\n        - 'answer'\n        - 'domain'\n        - 'id'\n        'type': 'object'\n\n    'DnsRewritePost':\n      'description': >\n        A classic DNS rewrite create object.\n      'example':\n        'answer': 'A'\n        'domain': 'example.com'\n      'properties':\n        'answer':\n          'description': >\n            The value of an `A`, `AAAA`, or `CNAME` DNS record in the response.\n            Acceptable formats:\n\n             *   Domain name: add a `CNAME` record with this domain name.\n\n             *   IPv4 address: use this IP in an `A` response.\n\n             *   IPv6 address: use this IP in an `AAAA` response.\n\n             *   The literal `A`: keep only `A` records from the upstream\n                 response.\n\n             *   The literal `AAAA`: keep only `AAAA` records from the upstream\n                 response.\n          'type': 'string'\n        'domain':\n          'description': >\n            Domain name or wildcard.\n          'type': 'string'\n      'required':\n      - 'answer'\n      - 'domain'\n      'type': 'object'\n\n    'DnsSettings':\n      'allOf':\n      - '$ref': '#/components/schemas/DnsSettingsPatch'\n      - 'description': >\n          DNS server settings.\n        'example':\n          'addresses':\n          - '127.0.0.1:53'\n          - '192.168.1.1:53'\n          'blocking_mode': 'default'\n          'bootstrap_servers':\n          - '9.9.9.10'\n          - '149.112.112.10'\n          'cache_size': 4194304\n          'cache_ttl_max': 0\n          'cache_ttl_min': 0\n          'dnssec': false\n          'edns_client_subnet': false\n          'ipv6': true\n          'ratelimit': 20\n          'refuse_any': true\n          'upstream_mode': 'load_balancing'\n          'upstream_servers':\n          - '1.1.1.1'\n          - '8.8.8.8'\n          'upstream_timeout': 1000\n        'required':\n        - 'addresses'\n        - 'blocking_mode'\n        - 'bootstrap_servers'\n        - 'cache_size'\n        - 'cache_ttl_max'\n        - 'cache_ttl_min'\n        - 'dnssec'\n        - 'edns_client_subnet'\n        - 'ipv6'\n        - 'ratelimit'\n        - 'refuse_any'\n        - 'upstream_mode'\n        - 'upstream_servers'\n        - 'upstream_timeout'\n\n    'DnsSettingsPatch':\n      'description': >\n        DNS server settings update object.\n      'example':\n        'cache_size': 4194304\n        'upstream_servers':\n        - '1.1.1.1'\n      'properties':\n        'addresses':\n          'description': >\n            Addresses on which to serve plain DNS, in ip:port format.  Empty\n            array disables plain DNS.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'blocking_ipv4':\n          'description': >\n            IPv4 address to respond with when `blocking_mode` is `custom_ip`.\n            See the documentation for the `DnsBlockingMode` schema.  If\n            `blocking_mode` is different from `custom_ip`, this property is not\n            included.\n          'type': 'string'\n        'blocking_ipv6':\n          'description': >\n            IPv6 address to respond with when `blocking_mode` is `custom_ip`.\n            See the documentation for the `DnsBlockingMode` schema.  If\n            `blocking_mode` is different from `custom_ip`, this property is not\n            included.\n          'type': 'string'\n        'blocking_mode':\n          '$ref': '#/components/schemas/DnsBlockingMode'\n        'bootstrap_servers':\n          'description': |\n            Bootstrap DNS servers' IP addresses to resolve the hostnames of the\n            encrypted DNS server providers.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'cache_size':\n          'description': >\n            DNS cache size in bytes.\n          'format': 'int64'\n          'minimum': 0\n          'type': 'integer'\n        'cache_ttl_max':\n          'description': >\n            Set a maximum time-to-live value for entries in the DNS cache.  `0`\n            means no override.  The value is in **seconds**, like in DNS record\n            headers.\n          'format': 'int64'\n          'minimum': 0\n          'type': 'integer'\n        'cache_ttl_min':\n          'description': >\n            Extend short time-to-live values received from the upstream server\n            when caching DNS responses.  `0` means no override.  TThe value is\n            in **seconds**, like in DNS record headers.\n          'format': 'int64'\n          'minimum': 0\n          'type': 'integer'\n        'dnssec':\n          'description': >\n            If `true`, set DNSSEC flag in outcoming DNS queries and check the\n            result.  A DNSSEC-enabled resolver is required.\n          'type': 'boolean'\n        'edns_client_subnet':\n          'description': >\n            If `true`, enable EDNS Client Subnet support and send clients'\n            subnets to DNS servers.\n          'type': 'boolean'\n        'ipv6':\n          'description': >\n            If `true`, accept `AAAA` DNS queries.  If `false`, respond to them\n            with an empty answer.\n          'type': 'boolean'\n        'ratelimit':\n          'description': >\n            The number of requests per second that a single client is allowed to\n            make.  `0` means no limit.\n          'format': 'int64'\n          'minimum': 0\n          'type': 'integer'\n        'refuse_any':\n          'description': >\n            If `true`, reject `ANY` DNS queries.\n          'type': 'boolean'\n        'upstream_mode':\n          '$ref': '#/components/schemas/DnsUpstreamMode'\n        'upstream_servers':\n          'description': >\n            Upstream DNS servers.\n          'items':\n            '$ref': '#/components/schemas/UpstreamServerAddr'\n          'type': 'array'\n        'upstream_timeout':\n          'description': >\n            Upstream request timeout, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n      'type': 'object'\n\n    'DnsType':\n      'description': >\n        DNS resource record type, aka `TYPE`.\n      'enum':\n      - 'a'\n      - 'aaaa'\n      - 'afsdb'\n      - 'any'\n      - 'apl'\n      - 'atma'\n      - 'avc'\n      - 'axfr'\n      - 'caa'\n      - 'cdnskey'\n      - 'cds'\n      - 'cert'\n      - 'cname'\n      - 'csync'\n      - 'dhcid'\n      - 'dlv'\n      - 'dname'\n      - 'dnskey'\n      - 'ds'\n      - 'eid'\n      - 'eui48'\n      - 'eui64'\n      - 'gid'\n      - 'gpos'\n      - 'hinfo'\n      - 'hip'\n      - 'https'\n      - 'isdn'\n      - 'ixfr'\n      - 'key'\n      - 'kx'\n      - 'l32'\n      - 'l64'\n      - 'loc'\n      - 'lp'\n      - 'maila'\n      - 'mailb'\n      - 'mb'\n      - 'md'\n      - 'mf'\n      - 'mg'\n      - 'minfo'\n      - 'mr'\n      - 'mx'\n      - 'naptr'\n      - 'nid'\n      - 'nimloc'\n      - 'ninfo'\n      - 'ns'\n      - 'nsap-ptr'\n      - 'nsec'\n      - 'nsec3'\n      - 'nsec3param'\n      - 'null'\n      - 'nxt'\n      - 'openpgpkey'\n      - 'opt'\n      - 'ptr'\n      - 'px'\n      - 'rkey'\n      - 'rp'\n      - 'rrsig'\n      - 'rt'\n      - 'sig'\n      - 'smimea'\n      - 'soa'\n      - 'spf'\n      - 'srv'\n      - 'sshfp'\n      - 'svcb'\n      - 'ta'\n      - 'talink'\n      - 'tkey'\n      - 'tlsa'\n      - 'tsig'\n      - 'txt'\n      - 'uid'\n      - 'uinfo'\n      - 'unspec'\n      - 'uri'\n      - 'x25'\n      'type': 'string'\n\n    'DnsUpstreamMode':\n      'description': |\n        Upstream request mode.\n\n         *  `fastest`: Query all DNS servers and return the IP address that was\n             returned by the fastest response.  Slows down DNS responses, since\n             it waits for responses from all upstreams, but improves the overall\n             connectivity.\n\n         *  `load_balancing`: Query one server at a time using a weighted random\n             algorithm picking the server so that the fastest server is used\n             more often.\n\n         *  `parallel`: Use parallel requests to speed up resolving by\n             simultaneously querying all upstream servers.\n      'enum':\n      - 'fastest'\n      - 'load_balancing'\n      - 'parallel'\n      'type': 'string'\n\n    'ErrorCode':\n      'description': |\n        An error code.\n\n         *  `AUT000`:  No or bad authorization credentials provided.\n\n         *  `ENT404`:  Entity not found; as opposed to path not found.\n\n         *  `JSN000`:  A JSON syntax error.\n\n         *  `JSN001`:  A JSON type error.\n\n         *  `OSS000`:  The server's operating system doesn't support the\n             requested functionality.\n\n         *  `PTH404`:  Path not found; as opposed to entity not found.\n\n         *  `RNT000`:  A server runtime error.\n\n         *  `TXT400`:  A plaintext bad request error.  Used when a plaintext\n             error is wrapped.\n\n         *  `TXT401`:  A plaintext unauthorized error.  Used when a plaintext\n             error is wrapped.\n\n         *  `TXT404`:  A plaintext not found error.  Used when a plaintext error\n             is wrapped.\n\n         *  `TXT500`:  A plaintext internal server error.  Used when a plaintext\n             error is wrapped.\n\n        TODO(a.garipov): Expand with TLS validation errors, DHCP errors, filter\n        URL reaching errors, OS and I/O errors, and so on.\n      'enum':\n      - 'AUT000'\n      - 'ENT404'\n      - 'JSN000'\n      - 'JSN001'\n      - 'OSS000'\n      - 'PTH404'\n      - 'RNT000'\n      - 'TXT400'\n      - 'TXT401'\n      - 'TXT404'\n      - 'TXT500'\n      'type': 'string'\n\n    'Filter':\n      'allOf':\n      - '$ref': '#/components/schemas/FilterPatch'\n      - 'description': >\n          A single filter list of rules.\n        'example':\n          'allowlist': false\n          'enabled': true\n          'name': 'AdMaster 5000 Super List v2.0 Final'\n          'num_rules': 36766\n          'refreshed': 1614345496000\n          'uid': 'abcd1234'\n          'url': 'https://admaster.example.com/list.txt'\n        'properties':\n          'num_rules':\n            'description': >\n              Number of rules in this filter.\n            'format': 'int64'\n            'minimum': 0\n            'type': 'integer'\n          'refreshed':\n            'description': >\n              Unix time of last refresh for this filter, in milliseconds.\n            'format': 'double'\n            'type': 'number'\n          'uid':\n            '$ref': '#/components/schemas/Uid'\n        'required':\n        - 'allowlist'\n        - 'enabled'\n        - 'name'\n        - 'num_rules'\n        - 'refreshed'\n        - 'uid'\n        - 'url'\n\n    'FilterPatch':\n      'description': >\n        A filter update object.\n      'example':\n        'enabled': true\n      'properties':\n        'allowlist':\n          'description': >\n            If `true`, this filter works as an allowlist filters.\n          'type': 'boolean'\n        'enabled':\n          'description': >\n            If `true`, this filter is applied.\n          'type': 'boolean'\n        'name':\n          'description': >\n            The name of this filter.\n          'type': 'string'\n        'url':\n          'description': |\n            A URL of the file containing filtering rules.\n\n            Examples of allowed schemes:\n\n             *  `file:///home/user/ads/rules.txt`: A local file.\n\n             *  `http://example.com/ads/rules.txt`: Remote list, fetched over\n                 plain HTTP.\n\n             *  `https://example.com/ads/rules.txt`: Remote list, fetched over\n                 HTTPS.\n          'type': 'string'\n      'type': 'object'\n\n    'FilterPost':\n      'allOf':\n      - '$ref': '#/components/schemas/FilterPatch'\n      - 'description': >\n          A filter create object.\n        'example':\n          'allowlist': false\n          'enabled': true\n          'name': 'AdMaster 5000 Super List v2.0 Final'\n          'url': 'https://admaster.example.com/list.txt'\n        'required':\n        - 'allowlist'\n        - 'enabled'\n        - 'name'\n        - 'url'\n\n    'FilteringReason':\n      'description': >\n        Request filtering status.\n      'enum':\n      - 'filtered_blocked_service'\n      - 'filtered_blocklist'\n      - 'filtered_invalid'\n      - 'filtered_parental'\n      - 'filtered_safe_browsing'\n      - 'filtered_safe_search'\n      - 'not_filtered_allowlist'\n      - 'not_filtered_error'\n      - 'not_filtered_notfound'\n      - 'rewrite'\n      - 'rewrite_etc_hosts'\n      - 'rewrite_rule'\n      'type': 'string'\n\n    'FilteringResultRule':\n      'description': >\n        Applied filtering rule.\n      'properties':\n        'filter_list_uid':\n          '$ref': '#/components/schemas/Uid'\n        'text':\n          'description': >\n            The text of the filtering rule applied to the request, if any.\n          'type': 'string'\n      'required':\n      - 'filter_list_uid'\n      - 'text'\n      'type': 'object'\n\n    'GetV1AccountsProfileResp':\n      '$ref': '#/components/schemas/Profile'\n\n    # TODO(a.garipov): Find a way to describe such XML documents using OpenAPI.\n    # If that is even possible.\n    'GetV1AppleDohMobileconfigResp':\n      'example': |\n        <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n        <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n        <plist version=\"1.0\">\n          <dict>\n            <key>PayloadContent</key>\n            <array>\n              <dict>\n                <key>DNSSettings</key>\n                <dict>\n                  <key>DNSProtocol</key>\n                  <string>HTTPS</string>\n                  <key>ServerName</key>\n                  <string>example.com</string>\n                  <key>ServerURL</key>\n                  <string>https://example.com/dns-query/123</string>\n                </dict>\n                <key>Name</key>\n                <string>myexample.local DoH</string>\n                <key>PayloadDescription</key>\n                <string>Configures device to use AdGuard Home</string>\n                <key>PayloadDisplayName</key>\n                <string>myexample.local DoH</string>\n                <key>PayloadIdentifier</key>\n                <string>com.apple.dnsSettings.managed.b6928468-ae3a-4368-a70d-cb7122275013</string>\n                <key>PayloadType</key>\n                <string>com.apple.dnsSettings.managed</string>\n                <key>PayloadUUID</key>\n                <string>18526b8c-6065-4b96-b635-9cde769ac0f2</string>\n                <key>PayloadVersion</key>\n                <integer>1</integer>\n              </dict>\n            </array>\n            <key>PayloadDescription</key>\n            <string>Adds AdGuard Home to Big Sur and iOS 14 or newer systems</string>\n            <key>PayloadDisplayName</key>\n            <string>myexample.local DoH</string>\n            <key>PayloadIdentifier</key>\n            <string>9a37b659-7541-4f9e-8b4d-6e2a59a123c8</string>\n            <key>PayloadRemovalDisallowed</key>\n            <false/>\n            <key>PayloadType</key>\n            <string>Configuration</string>\n            <key>PayloadUUID</key>\n            <string>255dbaf7-0c52-4855-9b22-ad8209690197</string>\n            <key>PayloadVersion</key>\n            <integer>1</integer>\n          </dict>\n        </plist>\n      'type': 'object'\n\n    # TODO(a.garipov): See the comment on GetV1AppleDohMobileconfigResp.\n    'GetV1AppleDotMobileconfigResp':\n      'example': |\n        <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n        <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n        <plist version=\"1.0\">\n          <dict>\n            <key>PayloadContent</key>\n            <array>\n              <dict>\n                <key>DNSSettings</key>\n                <dict>\n                  <key>DNSProtocol</key>\n                  <string>TLS</string>\n                  <key>ServerName</key>\n                  <string>123.example.com</string>\n                </dict>\n                <key>Name</key>\n                <string>example.com DoT</string>\n                <key>PayloadDescription</key>\n                <string>Configures device to use AdGuard Home</string>\n                <key>PayloadDisplayName</key>\n                <string>example.com DoT</string>\n                <key>PayloadIdentifier</key>\n                <string>com.apple.dnsSettings.managed.7807cb66-c6ec-4c78-be29-d8ffcb3321ee</string>\n                <key>PayloadType</key>\n                <string>com.apple.dnsSettings.managed</string>\n                <key>PayloadUUID</key>\n                <string>b0fb9137-e27a-4f95-abc3-556103ad9ac1</string>\n                <key>PayloadVersion</key>\n                <integer>1</integer>\n              </dict>\n            </array>\n            <key>PayloadDescription</key>\n            <string>Adds AdGuard Home to Big Sur and iOS 14 or newer systems</string>\n            <key>PayloadDisplayName</key>\n            <string>myexample.local DoT</string>\n            <key>PayloadIdentifier</key>\n            <string>f1095036-406e-4243-8210-cf0ffa52b3f6</string>\n            <key>PayloadRemovalDisallowed</key>\n            <false/>\n            <key>PayloadType</key>\n            <string>Configuration</string>\n            <key>PayloadUUID</key>\n            <string>21cd3597-0769-486a-86d0-7b5e32d24305</string>\n            <key>PayloadVersion</key>\n            <integer>1</integer>\n          </dict>\n        </plist>\n      'type': 'object'\n\n    'GetV1ClientsPersistentResp':\n      'description': >\n        Persistent clients.\n      'example':\n        'clients':\n        - 'blocked': false\n          'blocked_services': []\n          'filtering': false\n          'ids': ['client-1']\n          'name': 'Client 1'\n          'parental': false\n          'safe_browsing': false\n          'safe_search': false\n          'tags': ['user_admin']\n          'use_global_blocked_services': true\n          'use_global_settings': true\n          'uid': 'abcd1234'\n          'upstream_servers': []\n        - 'blocked': false\n          'blocked_services': []\n          'filtering': true\n          'ids': ['client-2']\n          'name': 'Client 2'\n          'parental': true\n          'safe_browsing': true\n          'safe_search': true\n          'tags': ['user_child']\n          'use_global_blocked_services': false\n          'use_global_settings': false\n          'uid': 'efgh5678'\n          'upstream_servers': []\n      'properties':\n        'clients':\n          'description': >\n            All persistent clients.\n          'items':\n            '$ref': '#/components/schemas/PersistentClient'\n          'type': 'array'\n      'required':\n      - 'clients'\n      'type': 'object'\n\n    'GetV1ClientsRuntimeResp':\n      'description': >\n        Runtime clients.\n      'example':\n        'clients':\n        - 'host': 'my-box'\n          'ip': '1.2.3.4'\n          'num_blocked_requests': 0\n          'num_requests': 100\n          'sources':\n          - 'arp'\n        - 'ip': '5.6.7.8'\n          'num_blocked_requests': 100\n          'num_requests': 100\n          'sources':\n          - 'whois'\n          'whois':\n            'city': 'Minsk'\n            'country': 'BY'\n      'properties':\n        'clients':\n          'description': >\n            All runtime clients.\n          'items':\n            '$ref': '#/components/schemas/RuntimeClient'\n          'type': 'array'\n      'required':\n      - 'clients'\n      'type': 'object'\n\n    'GetV1DhcpLeasesResp':\n      'description': >\n        All dynamic and static DHCP leases.\n      'example':\n        'leases':\n        - 'expires': 1614345496000\n          'hostname': 'my-mobile'\n          'ip': '192.168.1.2'\n          'mac': '01:23:45:67:89:ab'\n          'uid': 'abcd1234'\n        - 'expires': 1614345497000\n          'hostname': ''\n          'ip': '192.168.1.3'\n          'mac': '01:23:45:67:89:cd'\n      'properties':\n        'leases':\n          'description': >\n            Dynamic and static DHCP leases.\n          'items':\n            '$ref': '#/components/schemas/DhcpLease'\n          'type': 'array'\n      'required':\n      - 'leases'\n      'type': 'object'\n\n    'GetV1DhcpStatusResp':\n      'description': >\n        Current DHCP server status and data for enabling it.\n      'example':\n        'interfaces':\n        - 'ips':\n          - '192.168.1.1'\n          'mac': '01:23:45:67:89:ab'\n          'mtu': 1500\n          'name': 'lan0'\n          'up': true\n        'ipv4_other_servers':\n          'ips':\n          - '192.169.1.1'\n        'ipv4_static_ip':\n          'ip': '192.168.1.1'\n          'static': true\n          'supported': true\n        'ipv6_other_servers':\n          'ips': []\n          'error': 'permission denied'\n        'ipv6_static_ip':\n          'ip': '200f::1'\n          'static': true\n          'supported': true\n      'properties':\n        'interfaces':\n          'description': >\n            Available network interfaces.\n          'items':\n            '$ref': '#/components/schemas/NetworkInterface'\n          'type': 'array'\n        'ipv4_other_servers':\n          '$ref': '#/components/schemas/GetV1DhcpStatusRespOtherServer'\n        'ipv4_static_ip':\n          '$ref': '#/components/schemas/StaticIpCheckResult'\n        'ipv6_other_servers':\n          '$ref': '#/components/schemas/GetV1DhcpStatusRespOtherServer'\n        'ipv6_static_ip':\n          '$ref': '#/components/schemas/StaticIpCheckResult'\n      'required':\n      - 'interfaces'\n      - 'ipv4_other_servers'\n      - 'ipv4_static_ip'\n      - 'ipv6_other_servers'\n      - 'ipv6_static_ip'\n      'type': 'object'\n\n    'GetV1DhcpStatusRespOtherServer':\n      'properties':\n        'error':\n          'description': >\n            Error, if any.  If there is no error, this field is absent.\n          'type': 'string'\n        'ips':\n          'description': >\n            IP addresses of other DHCP servers, if found.\n      'required':\n      - 'ips'\n      'type': 'object'\n\n    'GetV1InstallInfoResp':\n      'description': >\n        AdGuard Home addresses configuration.\n      'example':\n        'dns_port': 53\n        'interfaces':\n        - 'ips':\n          - '192.168.1.1'\n          'mac': '01:23:45:67:89:ab'\n          'mtu': 1500\n          'name': 'lan0'\n          'up': true\n        'web_port': 80\n      'properties':\n        'dns_port':\n          'description': >\n            Recommended DNS port.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n        'interfaces':\n          'description': >\n            Available network interfaces.\n          'items':\n            '$ref': '#/components/schemas/NetworkInterface'\n          'type': 'array'\n        'web_port':\n          'description': >\n            Recommended web interface port.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n      'required':\n      - 'dns_port'\n      - 'interfaces'\n      - 'web_port'\n      'type': 'object'\n\n    'GetV1LogSearchResp':\n      'description': >\n        Query log search results.\n      'example':\n        'results':\n        - 'answer':\n          - 'ttl': 60\n            'type': 'a'\n            'value': '5.6.7.8'\n          'answer_dnssec': false\n          'client':\n            'blocked': false\n            'ids':\n            - '1.2.3.4'\n            - 'user-1'\n            'name': 'User 1'\n            'num': 100\n            'num_blocked': 50\n            'uid': 'abcd1234'\n            'whois':\n              'city': 'Minsk'\n              'country': 'BY'\n          'elapsed': 3.2\n          'proto': 'udp'\n          'question':\n            'class': 'in'\n            'host': 'example.com'\n            'type': 'a'\n          'rcode': 'noerror'\n          'reason': 'not_filtered_notfound'\n          'rules': []\n          'start': 1614345496000\n          'upstream': '8.8.8.8'\n      'properties':\n        'results':\n          'description': >\n            The query log.\n          'items':\n            '$ref': '#/components/schemas/LogRecord'\n          'type': 'array'\n      'required':\n      - 'results'\n      'type': 'object'\n\n    'GetV1ProtectionBlockedServicesResp':\n      '$ref': '#/components/schemas/BlockedServices'\n\n    'GetV1ProtectionCustomRulesResp':\n      '$ref': '#/components/schemas/CustomRules'\n\n    'GetV1ProtectionDnsRewritesResp':\n      'description': >\n        Classic DNS rewrites.\n      'example':\n        'rules':\n        - 'answer': 'A'\n          'domain': 'example.com'\n          'id': 'abcd1234'\n        - 'answer': '0.0.0.0'\n          'domain': '*.example.org'\n          'id': 'efgh5678'\n        - 'answer': 'my.example.net'\n          'domain': 'example.net'\n          'id': 'ijkl9012'\n      'properties':\n        'rules':\n          'description': >\n            All classic DNS rewrites.\n          'items':\n            '$ref': '#/components/schemas/DnsRewrite'\n          'type': 'array'\n      'required':\n      - 'rules'\n      'type': 'object'\n\n    'GetV1ProtectionFiltersResp':\n      'description': >\n        Filters.\n      'example':\n        'filters':\n        - 'allowlist': false\n          'enabled': true\n          'name': 'AdMaster 5000 Super List v2.0 Final'\n          'num_rules': 36766\n          'refreshed': 1614345496000\n          'uid': 'abcd1234'\n          'url': 'https://admaster.example.com/list.txt'\n        - 'allowlist': false\n          'enabled': true\n          'name': 'My personal list'\n          'num_rules': 0\n          'refreshed': 1614345497000\n          'uid': 'efgh5678'\n          'url': 'file:///home/user/Documents/ad_list.txt'\n      'properties':\n        'filters':\n          'description': >\n            All current filters.\n          'items':\n            '$ref': '#/components/schemas/Filter'\n          'type': 'array'\n      'required':\n      - 'filters'\n      'type': 'object'\n\n    # Perhaps a lot of these belong in separate APIs, but our colleagues asked\n    # to pack as much data into every request as reasonably possible.\n    'GetV1SettingsAllResp':\n      'description': >\n        Most settings.\n      # Don't add examples, as are provided by the subclasses.\n      'properties':\n        'dhcp':\n          '$ref': '#/components/schemas/DhcpSettings'\n        'dns':\n          '$ref': '#/components/schemas/DnsSettings'\n        'http':\n          '$ref': '#/components/schemas/HttpSettings'\n        'log':\n          '$ref': '#/components/schemas/LogSettings'\n        'protection':\n          '$ref': '#/components/schemas/ProtectionSettings'\n        'stats':\n          '$ref': '#/components/schemas/StatsSettings'\n        'tls':\n          '$ref': '#/components/schemas/TlsSettings'\n      'required':\n      - 'dhcp'\n      - 'dns'\n      - 'http'\n      - 'log'\n      - 'protection'\n      - 'stats'\n      - 'tls'\n      'type': 'object'\n\n    'GetV1SettingsDnsAccessResp':\n      '$ref': '#/components/schemas/DnsAccessSettings'\n\n    # See the comment on the GetV1SettingsAllResp schema.\n    'GetV1StatsAllResp':\n      'description': >\n        All statistics.\n      'example':\n        'dns_cache_hit_rate': 56.7\n        'dns_cache_records': 123\n        'graph_avg_processing':\n        - 3.0\n        - 0.4\n        'graph_blocked_ad_queries':\n        - 10\n        - 20\n        'graph_blocked_custom_rule_queries':\n        - 10\n        - 20\n        'graph_blocked_domains':\n        - 10\n        - 20\n        'graph_blocked_parental_control_queries':\n        - 10\n        - 20\n        'graph_blocked_safe_browsing_queries':\n        - 10\n        - 20\n        'graph_blocked_safe_search_queries':\n        - 10\n        - 20\n        'graph_blocked_service_queries':\n        - 10\n        - 20\n        'graph_blocked_tracker_queries':\n        - 10\n        - 20\n        'graph_cpu_percent':\n        - 50\n        - 75\n        'graph_domains':\n        - 20\n        - 30\n        'graph_queries':\n        - 1000\n        - 2002\n        'graph_ram_resident':\n        - 1048576\n        - 2097152\n        'time_unit': 'hour'\n        'top_blocked_domains':\n        - 'name': 'example.net'\n          'num': 100\n        'top_clients':\n        - 'blocked': false\n          'ids':\n          - '1.2.3.4'\n          - 'user-1'\n          'name': 'User 1'\n          'num': 100\n          'num_blocked': 50\n          'uid': 'abcd1234'\n          'whois':\n            'city': 'Minsk'\n            'country': 'BY'\n        - 'blocked': true\n          'ids':\n          - '5.6.7.8'\n          'num': 100\n          'num_blocked': 100\n        'top_domains':\n        - 'name': 'example.com'\n          'num': 1000\n        - 'name': 'example.net'\n          'num': 100\n        'total_blocked_ad_queries': 100\n        'total_blocked_custom_rule_queries': 10\n        'total_blocked_domains': 500\n        'total_blocked_parental_control_queries': 10\n        'total_blocked_safe_browsing_queries': 10\n        'total_blocked_safe_search_queries': 10\n        'total_blocked_service_queries': 10\n        'total_blocked_tracker_queries': 10\n        'total_domains': 1000\n        'total_queries': 10000\n      'properties':\n        'dns_cache_hit_rate':\n          'description': >\n            DNS cache hit rate, in percent.\n          'maximum': 100.0\n          'minimum': 0.0\n          'format': 'double'\n          'type': 'number'\n        'dns_cache_records':\n          'description': >\n            Number of DNS responses currently in cache.\n          'minimum': 0\n          'format': 'int64'\n          'type': 'integer'\n        'graph_avg_processing':\n          'description': >\n            Average DNS query processing duration graph information.  Each item\n            is one `time_unit` long.  The duration is in milliseconds.  Sorted\n            by time in descending order.\n          'items':\n            'format': 'double'\n            'type': 'number'\n          'type': 'array'\n        'graph_blocked_ad_queries':\n          'description': >\n            Number of queries blocked by advertising filters graph information.\n            Each item is one `time_unit` long.  Sorted by time in descending\n            order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_blocked_custom_rule_queries':\n          'description': >\n            Number of queries blocked by custom filtering rules graph\n            information.  Each item is one `time_unit` long.  Sorted by time in\n            descending order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_blocked_domains':\n          'description': >\n            Blocked queried domains graph information.  Each item is one\n            `time_unit` long.  Sorted by time in descending order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_blocked_parental_control_queries':\n          'description': >\n            Number of queries blocked by parental control services graph\n            information.  Each item is one `time_unit` long.  Sorted by time in\n            descending order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_blocked_safe_browsing_queries':\n          'description': >\n            Number of queries blocked by safe browsing services graph\n            information.  Each item is one `time_unit` long.  Sorted by time in\n            descending order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_blocked_safe_search_queries':\n          'description': >\n            Number of queries blocked by safe search services graph information.\n            Each item is one `time_unit` long.  Sorted by time in descending\n            order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_blocked_service_queries':\n          'description': >\n            Number of queries blocked by blocked service settings graph\n            information.  Each item is one `time_unit` long.  Sorted by time in\n            descending order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_blocked_tracker_queries':\n          'description': >\n            Number of queries blocked by tracker filters graph information.\n            Each item is one `time_unit` long.  Sorted by time in descending\n            order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_cpu_percent':\n          'description': >\n            CPU usage percentage graph information.  Each item is one\n            `time_unit` long.  Sorted by time in descending order.\n          'items':\n            'format': 'double'\n            'type': 'number'\n          'type': 'array'\n        'graph_domains':\n          'description': >\n            Queried domains graph information.  Each item is one `time_unit`\n            long.  Sorted by time in descending order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_queries':\n          'description': >\n            Number of served DNS queries graph information.  Each item is one\n            `time_unit` long.  Sorted by time in descending order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'graph_ram_resident':\n          'description': >\n            AdGuard Home's resident memory usage graph information.  The size is\n            in bytes.  Each item is one `time_unit` long.  Sorted by time in\n            descending order.\n          'items':\n            'format': 'int64'\n            'type': 'integer'\n          'type': 'array'\n        'time_unit':\n          '$ref': '#/components/schemas/TimeUnit'\n        'top_blocked_domains':\n          'description': >\n            Top blocked queried domains.  Sorted by number in descending order.\n          'items':\n            '$ref': '#/components/schemas/GetV1StatsAllRespTopsItem'\n          'type': 'array'\n        'top_clients':\n          'description': >\n            Top clients.  Sorted by number in descending order.\n          'items':\n            '$ref': '#/components/schemas/ClientInfo'\n          'type': 'array'\n        'top_domains':\n          'description': >\n            Top queried domains.  Sorted by number in descending order.\n          'items':\n            '$ref': '#/components/schemas/GetV1StatsAllRespTopsItem'\n          'type': 'array'\n        'total_blocked_ad_queries':\n          'description': >\n            Total number of queries blocked by advertising filters.\n          'format': 'int64'\n          'type': 'integer'\n        'total_blocked_custom_rule_queries':\n          'description': >\n            Total number of queries blocked by custom filtering rules.\n          'format': 'int64'\n          'type': 'integer'\n        'total_blocked_domains':\n          'description': >\n            Total number of blocked queried domains.\n          'format': 'int64'\n          'type': 'integer'\n        'total_blocked_parental_control_queries':\n          'description': >\n            Total number of queries blocked by parental control services.\n          'format': 'int64'\n          'type': 'integer'\n        'total_blocked_safe_browsing_queries':\n          'description': >\n            Total number of queries blocked by safe browsing services.\n          'format': 'int64'\n          'type': 'integer'\n        'total_blocked_safe_search_queries':\n          'description': >\n            Total number of queries blocked by safe search services.\n          'format': 'int64'\n          'type': 'integer'\n        'total_blocked_service_queries':\n          'description': >\n            Total number of queries blocked by blocked service settings.\n          'format': 'int64'\n          'type': 'integer'\n        'total_blocked_tracker_queries':\n          'description': >\n            Total number of queries blocked by tracker filters.\n          'format': 'int64'\n          'type': 'integer'\n        'total_domains':\n          'description': >\n            Total number of queried domains.\n          'format': 'int64'\n          'type': 'integer'\n        'total_queries':\n          'description': >\n            Total number of served DNS queries.\n          'format': 'int64'\n          'type': 'integer'\n      'required':\n      - 'dns_cache_hit_rate'\n      - 'dns_cache_records'\n      - 'graph_avg_processing'\n      - 'graph_blocked_ad_queries'\n      - 'graph_blocked_custom_rule_queries'\n      - 'graph_blocked_domains'\n      - 'graph_blocked_parental_control_queries'\n      - 'graph_blocked_safe_browsing_queries'\n      - 'graph_blocked_safe_search_queries'\n      - 'graph_blocked_service_queries'\n      - 'graph_blocked_tracker_queries'\n      - 'graph_cpu_percent'\n      - 'graph_domains'\n      - 'graph_queries'\n      - 'graph_ram_resident'\n      - 'time_unit'\n      - 'top_blocked_domains'\n      - 'top_clients'\n      - 'top_domains'\n      - 'total_blocked_ad_queries'\n      - 'total_blocked_custom_rule_queries'\n      - 'total_blocked_domains'\n      - 'total_blocked_parental_control_queries'\n      - 'total_blocked_safe_browsing_queries'\n      - 'total_blocked_safe_search_queries'\n      - 'total_blocked_service_queries'\n      - 'total_blocked_tracker_queries'\n      - 'total_domains'\n      - 'total_queries'\n      'type': 'object'\n\n    'GetV1StatsAllRespTopsItem':\n      'description': >\n        A top array item.\n      'properties':\n        'name':\n          'description': >\n            The name of the entity.  Mostly domain names.\n          'example': 'example.com'\n          'type': 'string'\n        'num':\n          'description': >\n            The value of the statistic.\n          'example': 1000\n          'format': 'int64'\n          'type': 'integer'\n      'required':\n      - 'name'\n      - 'num'\n      'type': 'object'\n\n    'GetV1SystemInfoResp':\n      'description': >\n        Information about the AdGuard Home server.\n      'example':\n        'arch': 'amd64'\n        'channel': 'release'\n        'new_version': 'v0.108.1'\n        'os': 'linux'\n        'start': 1614345496000\n        'version': 'v0.108.0'\n      'properties':\n        'arch':\n          'description': >\n            CPU architecture.\n          'type': 'string'\n        'channel':\n          '$ref': '#/components/schemas/Channel'\n        'new_version':\n          'description': >\n            New available version of AdGuard Home to which the server can be\n            updated, if any.  If there are none, this field is absent.\n          'type': 'string'\n        'os':\n          'description': >\n            Operating system type.\n          'type': 'string'\n        'start':\n          'description': >\n            Unix time at which AdGuard Home started working, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n        'version':\n          'description': >\n            Current AdGuard Home version.\n          'type': 'string'\n      'required':\n      - 'arch'\n      - 'channel'\n      - 'os'\n      - 'start'\n      - 'version'\n      'type': 'object'\n\n    'HttpSettings':\n      'allOf':\n      - '$ref': '#/components/schemas/HttpSettingsPatch'\n      - 'description': >\n          HTTP interface server settings.\n\n          **TODO(a.garipov): Finish, split from TLS settings.**\n        'example':\n          'addresses':\n          - '127.0.0.1:80'\n          - '192.168.1.1:80'\n          'force_https': true\n          'secure_addresses':\n          - '127.0.0.1:443'\n          - '192.168.1.1:443'\n          'timeout': 10000\n        'required':\n        - 'addresses'\n        - 'force_https'\n        - 'secure_addresses'\n        - 'timeout'\n\n    'HttpSettingsPatch':\n      'description': >\n        HTTP server settings update object.\n      'example':\n        'force_https': false\n      'properties':\n        'addresses':\n          'description': >\n            Addresses on which to serve the plain-HTTP web interface and API, in\n            ip:port format.  Empty array disables the web interface over plain\n            HTTP.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'force_https':\n          'description': >\n            If `true`, enabled the HTTP-to-HTTPS redirect.\n          'type': 'boolean'\n        'secure_addresses':\n          'description': >\n            Addresses on which to serve the HTTPS web interface and API, in\n            ip:port format.  Empty array disables the web interface over HTTPS.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'timeout':\n          'description': >\n            HTTP request timeout, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n      'type': 'object'\n\n    'InternalServerErrorResp':\n      'example':\n        'code': 'RNT000'\n        'msg': >-\n          runtime error: invalid memory address or nil pointer dereference\n      'properties':\n        'code':\n          '$ref': '#/components/schemas/ErrorCode'\n        'msg':\n          'description': >\n            Error message string.\n          'type': 'string'\n      'required':\n      - 'code'\n      - 'msg'\n      'type': 'object'\n\n    'Lang':\n      'description': >\n        Language code.\n      # Hold the enum in sync with .twosky.json.\n      'enum':\n      - 'be'\n      - 'bg'\n      - 'cs'\n      - 'da'\n      - 'de'\n      - 'en'\n      - 'es'\n      - 'fa'\n      - 'fr'\n      - 'hr'\n      - 'hu'\n      - 'id'\n      - 'it'\n      - 'ja'\n      - 'ko'\n      - 'nl'\n      - 'no'\n      - 'pl'\n      - 'pt-br'\n      - 'pt-pt'\n      - 'ro'\n      - 'ru'\n      - 'si-lk'\n      - 'sk'\n      - 'sl'\n      - 'sr-cs'\n      - 'sv'\n      - 'th'\n      - 'tr'\n      - 'vi'\n      - 'zh-cn'\n      - 'zh-hk'\n      - 'zh-tw'\n      'type': 'string'\n\n    'LogRecord':\n      'description': >\n        Query log record.\n      'properties':\n        'answer':\n          'description': >\n            The answer given to the user.\n          'items':\n            '$ref': '#/components/schemas/LogRecordDnsAnswer'\n          'type': 'array'\n        'answer_dnssec':\n          'description': >\n            If `true`, DNSSEC was used.\n          'type': 'boolean'\n        'blocked_service':\n          'description': >\n            Set if `reason` is `filtered_blocked_service`.  Otherwise, this\n            field is absent.\n          'type': 'string'\n        'client':\n          '$ref': '#/components/schemas/ClientInfo'\n        'elapsed':\n          'description': >\n            Time it took to process the request, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n        'original_answer':\n          'description': >\n            Original answer from the upstream server, if the answer was\n            rewritten.\n          'items':\n            '$ref': '#/components/schemas/LogRecordDnsAnswer'\n          'type': 'array'\n        'proto':\n          '$ref': '#/components/schemas/DnsProto'\n        'question':\n          '$ref': '#/components/schemas/LogRecordDnsQuestion'\n        'rcode':\n          '$ref': '#/components/schemas/DnsResponseCode'\n        'reason':\n          '$ref': '#/components/schemas/FilteringReason'\n        'rules':\n          'description': >\n            Applied rules.\n          'items':\n            '$ref': '#/components/schemas/FilteringResultRule'\n          'type': 'array'\n        'start':\n          'description': >\n            Request processing start Unix time, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n        'upstream':\n          '$ref': '#/components/schemas/UpstreamServerAddr'\n      'required':\n      - 'answer'\n      - 'answer_dnssec'\n      - 'client'\n      - 'elapsed'\n      - 'proto'\n      - 'question'\n      - 'rcode'\n      - 'reason'\n      - 'rules'\n      - 'start'\n      - 'upstream'\n      'type': 'object'\n\n    'LogRecordDnsAnswer':\n      'description': >\n        DNS answer section.\n      'properties':\n        'ttl':\n          'description': >\n            TTL of a record.  This value is in **seconds**, like in DNS record\n            headers.\n          'format': 'int64'\n          'minimum': 0\n          'type': 'integer'\n        'type':\n          '$ref': '#/components/schemas/DnsType'\n        'value':\n          'description': >\n            An opaque string describing the result value.\n          'type': 'string'\n      'required':\n      - 'ttl'\n      - 'type'\n      - 'value'\n      'type': 'object'\n\n    'LogRecordDnsQuestion':\n      'description': >\n        DNS question section.\n      'properties':\n        'class':\n          '$ref': '#/components/schemas/DnsClass'\n        'host':\n          'description': >\n            Host from the query.\n          'type': 'string'\n        'type':\n          '$ref': '#/components/schemas/DnsType'\n      'required':\n      - 'class'\n      - 'host'\n      - 'type'\n      'type': 'object'\n\n    'LogSettings':\n      'allOf':\n      - '$ref': '#/components/schemas/LogSettingsPatch'\n      - 'description': >\n          Query logging settings.\n        'example':\n          'anonymize': true\n          'enabled': true\n          'rotation': 604800000\n        'required':\n        - 'anonymize'\n        - 'enabled'\n        - 'rotation'\n\n    'LogSettingsPatch':\n      'description': >\n        Query logging settings update object.\n      'properties':\n        'anonymize':\n          'description': >\n            If `true`, client IP address anonymization is enabled.\n          'type': 'boolean'\n        'enabled':\n          'description': >\n            If `true`, query logging is enabled.\n          'type': 'boolean'\n        'rotation':\n          'description': >\n            Log rotation interval, in milliseconds.  After that time, the log\n            file will be replaced by a new one, while the old one gets renamed.\n          'format': 'double'\n          'minimum': 86400000\n          'maximum': 7776000000\n          'type': 'number'\n      'type': 'object'\n\n    'NetworkInterface':\n      'properties':\n        'ips':\n          'description': >\n            The IP addresses of the interface, if any.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'mac':\n          'description': >\n            The MAC address of the interface.\n          'type': 'string'\n        'mtu':\n          'description': >\n            The interface's MTU, the maximum transmission unit.\n          'format': 'int64'\n          'type': 'integer'\n        'name':\n          'description': >\n            The name of the interface.\n          'type': 'string'\n        'up':\n          'description': >\n            If `true`, the interface is up.\n          'type': 'boolean'\n      'required':\n      - 'ips'\n      - 'mac'\n      - 'mtu'\n      - 'name'\n      - 'up'\n      'type': 'object'\n\n    'NotFoundResp':\n      'example':\n        'code': 'ENT404'\n        'msg': >-\n          entity not found\n      'properties':\n        'code':\n          '$ref': '#/components/schemas/ErrorCode'\n        'msg':\n          'description': >\n            Error message string.\n          'type': 'string'\n      'required':\n      - 'code'\n      - 'msg'\n      'type': 'object'\n\n    'PatchV1AccountsProfileReq':\n      'example':\n        'lang': 'ru'\n      'properties':\n        'lang':\n          '$ref': '#/components/schemas/Lang'\n      'type': 'object'\n\n    'PatchV1AccountsProfileResp':\n      '$ref': '#/components/schemas/Profile'\n\n    'PatchV1ClientPersistentReq':\n      '$ref': '#/components/schemas/PersistentClientPatch'\n\n    'PatchV1ClientPersistentResp':\n      '$ref': '#/components/schemas/PersistentClient'\n\n    'PatchV1DhcpLeaseReq':\n      '$ref': '#/components/schemas/DhcpLeasePatch'\n\n    'PatchV1DhcpLeaseResp':\n      '$ref': '#/components/schemas/DhcpLease'\n\n    'PatchV1ProtectionFilterReq':\n      '$ref': '#/components/schemas/FilterPatch'\n\n    'PatchV1ProtectionFilterResp':\n      '$ref': '#/components/schemas/Filter'\n\n    'PatchV1SettingsDhcpReq':\n      '$ref': '#/components/schemas/DhcpSettingsPatch'\n\n    'PatchV1SettingsDhcpResp':\n      '$ref': '#/components/schemas/DhcpSettings'\n\n    'PatchV1SettingsDnsReq':\n      '$ref': '#/components/schemas/DnsSettingsPatch'\n\n    'PatchV1SettingsDnsResp':\n      '$ref': '#/components/schemas/DnsSettings'\n\n    'PatchV1SettingsHttpReq':\n      '$ref': '#/components/schemas/HttpSettingsPatch'\n\n    'PatchV1SettingsHttpResp':\n      '$ref': '#/components/schemas/HttpSettings'\n\n    'PatchV1SettingsLogReq':\n      '$ref': '#/components/schemas/LogSettingsPatch'\n\n    'PatchV1SettingsLogResp':\n      '$ref': '#/components/schemas/LogSettings'\n\n    'PatchV1SettingsProtectionReq':\n      '$ref': '#/components/schemas/ProtectionSettingsPatch'\n\n    'PatchV1SettingsProtectionResp':\n      '$ref': '#/components/schemas/ProtectionSettings'\n\n    'PatchV1SettingsStatsReq':\n      '$ref': '#/components/schemas/StatsSettingsPatch'\n\n    'PatchV1SettingsStatsResp':\n      '$ref': '#/components/schemas/StatsSettings'\n\n    'PatchV1SettingsTlsReq':\n      '$ref': '#/components/schemas/TlsSettingsPatch'\n\n    'PatchV1SettingsTlsResp':\n      '$ref': '#/components/schemas/TlsSettings'\n\n    'PersistentClient':\n      'allOf':\n      - '$ref': '#/components/schemas/PersistentClientPatch'\n      - 'description': >\n          Persistent client.\n        'example':\n          'blocked': false\n          'blocked_services': []\n          'filtering': false\n          'ids': ['client-1']\n          'name': 'Client 1'\n          'num_blocked_requests': 50\n          'num_requests': 100\n          'parental': false\n          'safe_browsing': false\n          'safe_search': false\n          'tags': ['user_admin']\n          'use_global_blocked_services': true\n          'use_global_settings': true\n          'uid': 'abcd1234'\n          'upstream_servers': []\n        'properties':\n          'num_blocked_requests':\n            'description': >\n              Total number of blocked requests for this runtime client.\n            'format': 'int64'\n            'minimum': 0\n            'type': 'integer'\n          'num_requests':\n            'description': >\n              Total number of requests for this runtime client.\n            'format': 'int64'\n            'minimum': 0\n            'type': 'integer'\n          'uid':\n            '$ref': '#/components/schemas/Uid'\n        'required':\n        - 'blocked'\n        - 'blocked_services'\n        - 'filtering'\n        - 'ids'\n        - 'name'\n        - 'parental'\n        - 'safe_browsing'\n        - 'safe_search'\n        - 'tags'\n        - 'uid'\n        - 'upstream_servers'\n        - 'use_global_blocked_services'\n        - 'use_global_settings'\n\n    'PersistentClientPatch':\n      'description': >\n        Persistent client update object.\n      'example':\n        'filtering': false\n        'parental': false\n        'safe_browsing': false\n        'safe_search': false\n        'tags': ['user_admin']\n      'properties':\n        'blocked':\n          'description': >\n            If `true`, the client is blocked.\n          'type': 'boolean'\n        'blocked_services':\n          'description': >\n            Custom blocked services for this client.\n          'items':\n            '$ref': '#/components/schemas/BlockedServiceId'\n          'type': 'array'\n        'filtering':\n          'description': >\n            If `true`, filtering based on filter rule lists is enabled for this\n            client.\n          'type': 'boolean'\n        'ids':\n          'description': >\n            IP, CIDR, MAC, or ClientID (not to be confused with the `uid` field)\n            for client identification.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'name':\n          'description': >\n            The name of this client.\n          'type': 'string'\n        'parental':\n          'description': >\n            If `true`, parental protection is enabled for this client.\n          'type': 'boolean'\n        'safe_browsing':\n          'description': >\n            If `true`, safe browsing protection is enabled for this client.\n          'type': 'boolean'\n        'safe_search':\n          'description': >\n            If `true`, safe search protection is enabled for this client.\n          'type': 'boolean'\n        'tags':\n          'description': >\n            Client tags.\n          'items':\n            '$ref': '#/components/schemas/PersistentClientTag'\n          'type': 'array'\n        'use_global_blocked_services':\n          'description': >\n            If `true`, use global blocked services for this client instead of\n            the custom ones.\n          'type': 'boolean'\n        'use_global_settings':\n          'description': >\n            If `true`, use global protection settings for this client instead of\n            the custom ones.\n          'type': 'boolean'\n        'upstream_servers':\n          'description': >\n            Custom upstream DNS servers for this client.\n          'items':\n            '$ref': '#/components/schemas/UpstreamServerAddr'\n          'type': 'array'\n      'type': 'object'\n\n    'PersistentClientPost':\n      'allOf':\n      - '$ref': '#/components/schemas/PersistentClientPatch'\n      - 'description': >\n          Persistent client create object.\n        'example':\n          'blocked': false\n          'blocked_services': []\n          'filtering': false\n          'ids': ['client-1']\n          'name': 'Client 1'\n          'parental': false\n          'safe_browsing': false\n          'safe_search': false\n          'tags': ['user_admin']\n          'use_global_blocked_services': true\n          'use_global_settings': true\n          'upstream_servers': []\n        'required':\n        - 'blocked'\n        - 'blocked_services'\n        - 'filtering'\n        - 'ids'\n        - 'name'\n        - 'parental'\n        - 'safe_browsing'\n        - 'safe_search'\n        - 'tags'\n        - 'upstream_servers'\n        - 'use_global_blocked_services'\n        - 'use_global_settings'\n\n    'PersistentClientTag':\n      'description': >\n        Tags can be included in filtering rules to allow you to apply them more\n        accurately.\n      'enum':\n      - 'device_audio'\n      - 'device_camera'\n      - 'device_gameconsole'\n      - 'device_laptop'\n      - 'device_nas'\n      - 'device_other'\n      - 'device_pc'\n      - 'device_phone'\n      - 'device_printer'\n      - 'device_securityalarm'\n      - 'device_tablet'\n      - 'device_tv'\n      - 'os_android'\n      - 'os_ios'\n      - 'os_linux'\n      - 'os_macos'\n      - 'os_other'\n      - 'os_windows'\n      - 'user_admin'\n      - 'user_child'\n      - 'user_regular'\n      'type': 'string'\n\n    'PostV1AccountsSessionReq':\n      'example':\n        'password': 'G00dp455word!'\n        'username': 'admin'\n      'properties':\n        'password':\n          'description': >\n            Password.\n          'format': 'password'\n          'type': 'string'\n        'username':\n          'description': >\n            Username.\n          'type': 'string'\n      'required':\n      - 'password'\n      - 'username'\n      'type': 'object'\n\n    'PostV1ClientsPersistentReq':\n      '$ref': '#/components/schemas/PersistentClientPost'\n\n    'PostV1ClientsPersistentResp':\n      '$ref': '#/components/schemas/PersistentClient'\n\n    'PostV1DhcpLeasesReq':\n      '$ref': '#/components/schemas/DhcpLeasePost'\n\n    'PostV1DhcpLeasesResp':\n      '$ref': '#/components/schemas/DhcpLease'\n\n    'PostV1InstallCheckReq':\n      'description': >\n        Configuration for checking.\n      'example':\n        'dns':\n          'ip':\n          - '0.0.0.0'\n          'port': 53\n        'static_ip': false\n        'web':\n          'ip':\n          - '0.0.0.0'\n          'port': 80\n      'properties':\n        'dns':\n          '$ref': '#/components/schemas/PostV1InstallCheckReqServer'\n        'static_ip':\n          'description': >\n            If `true`, check if a static IP is set or can be set.\n          'type': 'boolean'\n        'web':\n          '$ref': '#/components/schemas/PostV1InstallCheckReqServer'\n      'required':\n      - 'dns'\n      - 'static_ip'\n      - 'web'\n      'type': 'object'\n\n    'PostV1InstallCheckReqServer':\n      'description': >\n        A configuration for a server check.\n      'properties':\n        'ip':\n          'description': >\n            IP addresses to check for availability.\n          'items':\n            'type': 'string'\n          'minItems': 1\n          'type': 'array'\n        'port':\n          'description': >\n            Port to check for availability.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n      'required':\n      - 'ip'\n      - 'port'\n      'type': 'object'\n\n    'PostV1InstallCheckResp':\n      'description': >\n        Configuration checking response.\n      'example':\n        'dns':\n          'error': 'permission denied'\n        'static_ip':\n          'ip': '192.168.1.1'\n          'static': true\n          'supported': true\n        'web': {}\n      'properties':\n        'dns':\n          '$ref': '#/components/schemas/PostV1InstallCheckRespNetwork'\n        'static_ip':\n          '$ref': '#/components/schemas/StaticIpCheckResult'\n        'web':\n          '$ref': '#/components/schemas/PostV1InstallCheckRespNetwork'\n      'required':\n      - 'dns'\n      - 'static_ip'\n      - 'web'\n      'type': 'object'\n\n    'PostV1InstallCheckRespNetwork':\n      'properties':\n        'error':\n          'description': >\n            Error, if any.  If there is no error, this field is absent.\n          'type': 'string'\n      'type': 'object'\n\n    'PostV1InstallConfigureReq':\n      'description': >\n        AdGuard Home initial configuration.\n      'example':\n        'dns_ip': '0.0.0.0'\n        'dns_port': 53\n        'password': 'G00dp455word!'\n        'username': 'admin'\n        'set_static_ip': true\n        'web_ip': '0.0.0.0'\n        'web_port': 80\n      'properties':\n        'dns_ip':\n          'description': >\n            The IP address to serve DNS queries on.\n          'type': 'string'\n        'dns_port':\n          'description': >\n            The port to serve DNS queries on.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n        'password':\n          'description': >\n            Password.\n          'type': 'string'\n        'username':\n          'description': >\n            Username.\n          'type': 'string'\n        'set_static_ip':\n          'description': >\n            If `true`, set the server's IP address to static.\n          'type': 'boolean'\n        'web_ip':\n          'description': >\n            The IP address to serve the web interface on.\n          'type': 'string'\n        'web_port':\n          'description': >\n            The port to serve the web interface on.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n      'required':\n      - 'dns_ip'\n      - 'dns_port'\n      - 'password'\n      - 'username'\n      - 'set_static_ip'\n      - 'web_ip'\n      - 'web_port'\n      'type': 'object'\n\n    'PostV1LogClearReq':\n      'description': >\n        Currently empty, may get more fields in the future.\n      'type': 'object'\n\n    'PostV1ProtectionCheckCustomRulesReq':\n      'description': >\n        Data to check using custom filtering rules.\n      'example':\n        'host': 'example.com'\n      'properties':\n        'host':\n          'description': >\n            The hostname to check.\n          'type': 'string'\n      'required':\n      - 'host'\n      'type': 'object'\n\n    'PostV1ProtectionCheckCustomRulesResp':\n      'description': >\n        Custom filtering rules check results.\n      'example':\n        'reason': 'filtered_blocklist'\n        'rules':\n        - 'filter_list_uid': 'abcd1234'\n          'text': '||example.com^'\n      'properties':\n        'cname':\n          'description': >\n            Set if `reason` is `Rewrite`.  Otherwise, this field is absent.\n          'type': 'string'\n        'ip_addrs':\n          'description': >\n            Set if `reason` is `Rewrite`.  Otherwise, this field is absent.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'reason':\n          '$ref': '#/components/schemas/FilteringReason'\n        'rules':\n          'description': >\n            Applied rules.\n          'items':\n            '$ref': '#/components/schemas/FilteringResultRule'\n          'type': 'array'\n        'service_name':\n          'description': >\n            Set if `reason` is `FilteredBlockedService`.  Otherwise, this field\n            is absent.\n          'type': 'string'\n      'required':\n      - 'reason'\n      - 'rules'\n      'type': 'object'\n\n    'PostV1ProtectionDnsRewritesReq':\n      '$ref': '#/components/schemas/DnsRewritePost'\n\n    'PostV1ProtectionDnsRewritesResp':\n      '$ref': '#/components/schemas/DnsRewrite'\n\n    'PostV1ProtectionFiltersReq':\n      '$ref': '#/components/schemas/FilterPost'\n\n    'PostV1ProtectionFiltersResp':\n      '$ref': '#/components/schemas/Filter'\n\n    'PostV1ProtectionRefreshFilterReq':\n      'description': >\n        Currently empty, may get more fields in the future.\n      'type': 'object'\n\n    'PostV1ProtectionRefreshFilterResp':\n      '$ref': '#/components/schemas/Filter'\n\n    'PostV1ProtectionRefreshFiltersReq':\n      'description': >\n        Filters refresh parameters.\n      'example':\n        'allowlist': false\n        'blocklist': true\n      'properties':\n        'allowlist':\n          'description': >\n            If `true`, refresh all allowlist filters.\n          'type': 'boolean'\n        'blocklist':\n          'description': >\n            If `true`, refresh all blocklist filters.\n          'type': 'boolean'\n      'required':\n      - 'allowlist'\n      - 'blocklist'\n      'type': 'object'\n\n    'PostV1ProtectionRefreshFiltersResp':\n      'description': >\n        Refresh results.\n      'example':\n        'errors':\n        - 'msg': 'context deadline exceeded'\n          'uid': 'efgh5678'\n        'refreshed':\n        - 'allowlist': false\n          'enabled': true\n          'name': 'AdMaster 5000 Super List v2.0 Final'\n          'num_rules': 36766\n          'refreshed': 1614345496000\n          'uid': 'abcd1234'\n          'url': 'https://admaster.example.com/list.txt'\n      'properties':\n        'errors':\n          'description': >\n            All encountered errors.\n          'items':\n            '$ref': '#/components/schemas/RefreshFilterError'\n          'type': 'array'\n        'refreshed':\n          'description': >\n            Refreshed filters.\n          'items':\n            '$ref': '#/components/schemas/Filter'\n          'type': 'array'\n      'required':\n      - 'errors'\n      - 'refreshed'\n      'type': 'object'\n\n    'PostV1SettingsDnsCheckReq':\n      'description': >\n        Validatable DNS settings.\n      'example':\n        'bootstrap_servers':\n        - '9.9.9.10'\n        - '149.112.112.10'\n        'upstream_servers':\n        - '1.1.1.1'\n        - '8.8.8.8'\n      'properties':\n        'bootstrap_servers':\n          'description': |\n            Bootstrap DNS servers' IP addresses to check.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'upstream_servers':\n          'description': >\n            Upstream DNS servers to check.\n          'items':\n            '$ref': '#/components/schemas/UpstreamServerAddr'\n          'type': 'array'\n      'required':\n      - 'bootstrap_servers'\n      - 'upstream_servers'\n      'type': 'object'\n\n    'PostV1SettingsDnsCheckResp':\n      'description': >\n        DNS settings validation results.\n      'example':\n        'bootstrap_servers':\n          '9.9.9.10': 'network is unreachable'\n        'upstream_servers':\n          '8.8.8.8': 'network is unreachable'\n      'properties':\n        'bootstrap_servers':\n          'additionalProperties':\n            'minLength': 1\n            'type': 'string'\n          'description': >\n            An IP-address-to-error mapping.  If an address is not in this\n            object, the check for that address is successful.  If there were no\n            errors, this field is absent.\n        'upstream_servers':\n          'additionalProperties':\n            'type': 'string'\n          'description': >\n            An upstream-address-to-error mapping.  If an address is not in this\n            object, the check for that address is successful.  If there were no\n            errors, this field is absent.\n      'type': 'object'\n\n    'PostV1SettingsTlsCheckReq':\n      'description': >\n        Validatable TLS settings.\n      'example':\n        'certificate_path': '/etc/ssl/example.com.cert'\n        'port_dns_over_quic': 853\n        'port_dns_over_tls': 853\n        'port_https': 443\n        'private_key_path': '/etc/ssl/example.com.key'\n        'server_name': 'dns.example.com'\n      'properties':\n        'certificate':\n          'description': |\n            Base64-encoded string with PEM-encoded certificate chain.\n\n            Should not be sent if `certificate_path` is sent.  Otherwise, must\n            be sent.\n          'format': 'byte'\n          'type': 'string'\n        'certificate_path':\n          'description': |\n            Path to the certificate file.\n\n            Should not be sent if `certificate` is sent.  Otherwise, must be\n            sent.\n          'type': 'string'\n        'port_dns_over_quic':\n          'default': 853\n          'description': >\n            The DNS-over-QUIC port.  If `0`, DNS-over-QUIC is disabled.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n        'port_dns_over_tls':\n          'default': 853\n          'description': >\n            The DNS-over-TLS port.  If `0`, DNS-over-TLS is disabled.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n        'port_https':\n          'default': 443\n          'description': >\n            The HTTPS port.  If `0`, HTTPS is disabled.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n        'private_key':\n          'description': |\n            Base64-encoded string with PEM-encoded private key.\n\n            Should not be sent if `private_key_path` is sent.  Otherwise, must\n            be sent.\n          'format': 'byte'\n          'type': 'string'\n        'private_key_path':\n          'description': |\n            Path to the private key file.\n\n            Should not be sent if `private_key` is sent.  Otherwise, must be\n            sent.\n          'type': 'string'\n        'server_name':\n          'description': >\n            The name of the server.  Used to validate the certificates as well\n            as to check ClientIDs in DNS-over-HTTP and DNS-over-TLS.\n          'type': 'string'\n      'required':\n      - 'port_dns_over_quic'\n      - 'port_dns_over_tls'\n      - 'port_https'\n      - 'server_name'\n      'type': 'object'\n\n    'PostV1SettingsTlsCheckResp':\n      'description': >\n        TLS settings validation results.\n      'example':\n        'dns_names':\n          - '*.example.com'\n          - 'example.com'\n        'issuer': 'CN=Example CA,OU=Development,O=Example CA,L=Canberra,ST=Canberra,C=AU'\n        'key_type': 'RSA'\n        'not_after': 1614345497000\n        'not_before': 1614345496000\n        'port_https_error': 'address already in use'\n        'subject': 'CN=Example CA,OU=Development,O=Example CA,L=Canberra,ST=Canberra,C=AU'\n        'warnings': []\n      'properties':\n        'cert_error':\n          'description': >\n            Certificate validation error, if any.  If the certificate is valid,\n            this field is absent.\n          'type': 'string'\n        'chain_error':\n          'description': >\n            Certificate chain validation error, if any.  If the certificate\n            chain is valid, this field is absent.\n          'type': 'string'\n        'dns_names':\n          'description': >\n            The value of the `SubjectAltNames` field of the first certificate in\n            the chain.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'issuer':\n          'description': >\n            The issuer of the first certificate in the chain.\n          'type': 'string'\n        'key_error':\n          'description': >\n            Private key pair error, if any.  If the key is valid, this field is\n            absent.\n          'type': 'string'\n        'key_type':\n          '$ref': '#/components/schemas/TlsKeyType'\n        'not_after':\n          'description': >\n            The value of the `NotAfter` field of the first certificate in the\n            chain, as a Unix time, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n        'not_before':\n          'description': >\n            The value of the `NotBefore` field of the first certificate in the\n            chain, as a Unix time, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n        'port_dns_over_quic_error':\n          'description': >\n            DNS-over-QUIC port checking error, if any.  If the port is\n            available, this field is absent.\n          'type': 'string'\n        'port_dns_over_tls_error':\n          'description': >\n            DNS-over-TLS port checking error, if any.  If the port is available,\n            this field is absent.\n          'type': 'string'\n        'port_https_error':\n          'description': >\n            DNS-over-HTTPS port checking error, if any.  If the port is\n            available, this field is absent.\n          'type': 'string'\n        'pair_error':\n          'description': >\n            Certificate and key pair error, if any.  If the pair is valid, this\n            field is absent.\n          'type': 'string'\n        'subject':\n          'description': >\n            The subject of the first certificate in the chain.\n          'type': 'string'\n        'warnings':\n          'description': >\n            Validation warnings, if any.\n          'items':\n            'type': 'string'\n          'type': 'array'\n      'required':\n      - 'dns_names'\n      - 'issuer'\n      - 'key_type'\n      - 'not_after'\n      - 'not_before'\n      - 'subject'\n      - 'warnings'\n      'type': 'object'\n\n    'PostV1StatsClearReq':\n      'description': >\n        Currently empty, may get more fields in the future.\n      'type': 'object'\n\n    'PostV1SystemResetReq':\n      'description': >\n        Currently empty, may get more fields in the future.\n      'type': 'object'\n\n    'PostV1SystemResetResp':\n      'description': >\n        Currently empty, may get more fields in the future.\n      'type': 'object'\n\n    'PostV1SystemUpdateReq':\n      'description': >\n        Currently empty, may get more fields in the future.\n      'type': 'object'\n\n    'PostV1SystemUpdateResp':\n      'example':\n        'reload': 10000\n      'properties':\n        'reload':\n          'description': >\n            Time, after which the frontend must reload the page, in\n            milliseconds.\n          'format': 'double'\n          'type': 'number'\n      'type': 'object'\n\n    'Profile':\n      'description': >\n        Current user's profile.\n      'example':\n        'lang': 'en'\n        'username': 'admin'\n      'properties':\n        'lang':\n          '$ref': '#/components/schemas/Lang'\n        'username':\n          'description': >\n            Current user's name.\n          'type': 'string'\n      'required':\n      - 'lang'\n      - 'username'\n      'type': 'object'\n\n    'ProtectionSettings':\n      'allOf':\n      - '$ref': '#/components/schemas/ProtectionSettingsPatch'\n      - 'description': >\n          Protection settings.\n        'example':\n          'autoupdate': 86400000\n          'filtering': true\n          'parental': true\n          'safe_browsing': false\n          'safe_search': false\n        'required':\n        - 'autoupdate'\n        - 'filtering'\n        - 'parental'\n        - 'safe_browsing'\n        - 'safe_search'\n\n    'ProtectionSettingsPatch':\n      'description': >\n        Protection settings update object.\n      'example':\n        'autoupdate': 0\n      'properties':\n        'autoupdate':\n          'description': >\n            Filter automatic update interval, in milliseconds.  Set to `0` to\n            disable automatic updates.\n          'format': 'double'\n          'minimum': 0\n          'maximum': 604800000\n          'type': 'number'\n        'filtering':\n          'description': >\n            If `true`, filtering based on filter rule lists is enabled.\n          'type': 'boolean'\n        'parental':\n          'description': >\n            If `true`, parental protection is enabled.\n          'type': 'boolean'\n        'pause_end':\n          'description': |\n            If `state` is `paused`, `pause_end` will show the Unix time until\n            which the protection is disabled in milliseconds.  Otherwise, the\n            property won't be set.\n\n            When updating, if `state` is set to `paused`, `pause_end` must be\n            set to a timestamp in the future.\n          'format': 'double'\n          'type': 'number'\n        'safe_browsing':\n          'description': >\n            If `true`, safe browsing protection is enabled.\n          'type': 'boolean'\n        'safe_search':\n          'description': >\n            If `true`, safe search protection is enabled.\n          'type': 'boolean'\n        'state':\n          '$ref': '#/components/schemas/ProtectionSettingsState'\n      'type': 'object'\n\n    'ProtectionSettingsState':\n      'description': |\n        State of protection.\n\n         *  `off`: Protection is disabled.\n\n         *  `on`: Protection is enabled.\n\n         *  `paused`: Protection is paused.  See the `pause_end` property to get\n             or set the end of the pause.\n      'enum':\n      - 'off'\n      - 'on'\n      - 'paused'\n      'type': 'string'\n\n    'PutV1ProtectionBlockedServicesReq':\n      '$ref': '#/components/schemas/BlockedServices'\n\n    'PutV1ProtectionCustomRulesReq':\n      '$ref': '#/components/schemas/CustomRules'\n\n    'PutV1SettingsDnsAccessReq':\n      '$ref': '#/components/schemas/DnsAccessSettings'\n\n    'RefreshFilterError':\n      'description': >\n        Filter refresh error.\n      'properties':\n        'msg':\n          'description': >\n            Error message.\n          'type': 'string'\n        'uid':\n          '$ref': '#/components/schemas/Uid'\n      'required':\n      - 'msg'\n      - 'uid'\n      'type': 'object'\n\n    'RuntimeClient':\n      'description': >\n        A runtime client's information.\n      'properties':\n        'host':\n          'description': >\n            The RDNS host of the runtime, if any.  If there is none, this field\n            is absent.\n          'type': 'string'\n        'ip':\n          'description': >\n            The IP-address of the runtime client.\n          'type': 'string'\n        'sources':\n          'description': >\n            The sources from which the information about this runtime client was\n            collected.\n          'items':\n            '$ref': '#/components/schemas/RuntimeClientSource'\n          'minItems': 1\n          'type': 'array'\n        'num_blocked_requests':\n          'description': >\n            Total number of blocked requests for this runtime client.\n          'format': 'int64'\n          'minimum': 0\n          'type': 'integer'\n        'num_requests':\n          'description': >\n            Total number of requests for this runtime client.\n          'format': 'int64'\n          'minimum': 0\n          'type': 'integer'\n        'whois':\n          '$ref': '#/components/schemas/Whois'\n      'required':\n      - 'ip'\n      - 'num_blocked_requests'\n      - 'num_requests'\n      - 'sources'\n      'type': 'object'\n\n    'RuntimeClientSource':\n      'description': >\n        The source from which the information about this runtime client was\n        collected.\n\n         *  `arp`: The information was collected from the `arp -a` output.\n\n         *  `dhcp`: The information was collected from our DHCP server.\n\n         *  `hosts_file`: The information was collected from the `/etc/hosts`\n             file.\n\n         *  `rdns`: The information was collected by performing a reverse DNS\n             lookup.\n\n         *  `whois`: The information was collected by performing a WHOIS lookup.\n      'enum':\n      - 'arp'\n      - 'dhcp'\n      - 'hosts_file'\n      - 'rdns'\n      - 'whois'\n      'type': 'string'\n\n    'StaticIpCheckResult':\n      'properties':\n        'error':\n          'description': >\n            Error, if any.  If there is no error, this field is absent.\n          'type': 'string'\n        'ip':\n          'description': >\n            The IP address.\n          'type': 'string'\n        'static':\n          'description': >\n            If `true`, the interface has a static IP address.\n          'type': 'boolean'\n        'supported':\n          'description': >\n            If `true`, setting a static IP on this system is supported.\n          'type': 'boolean'\n      'required':\n      - 'ip'\n      - 'static'\n      - 'supported'\n      'type': 'object'\n\n    'StatsSettings':\n      'allOf':\n      - '$ref': '#/components/schemas/StatsSettingsPatch'\n      - 'description': >\n          Statistics settings.\n        'required':\n        - 'autorefresh'\n        - 'retention'\n\n    'StatsSettingsPatch':\n      'description': >\n        Statistics settings update object.\n      'properties':\n        'autorefresh':\n          'description': >\n            Statistics UI autorefresh time in milliseconds.  `0` means\n            autorefresh is disabled.\n          'format': 'double'\n          'type': 'number'\n        'retention':\n          'description': >\n            Statistics retention interval, in milliseconds.\n          'format': 'double'\n          'type': 'number'\n      'type': 'object'\n\n    'TimeUnit':\n      'description': >\n        Time units used for statistics.  See the documentation for the\n        `GET /api/v1/stats/all` request.\n      'enum':\n      - 'hour'\n      - 'day'\n      'type': 'string'\n\n    'TlsKeyType':\n      'description': >\n        TLS key type.\n      'enum':\n      - 'ECDSA'\n      - 'RSA'\n      'type': 'string'\n\n    'TlsSettings':\n      'allOf':\n      - '$ref': '#/components/schemas/TlsSettingsPatch'\n      - 'description': >\n          TLS and encryption settings.\n        'example':\n          'certificate_path': '/etc/ssl/example.com.cert'\n          'enabled': true\n          'port_dns_over_quic': 853\n          'port_dns_over_tls': 853\n          'port_https': 443\n          'private_key_path': '/etc/ssl/example.com.key'\n          'server_name': 'dns.example.com'\n        'required':\n        - 'enabled'\n        - 'port_dns_over_quic'\n        - 'port_dns_over_tls'\n        - 'port_https'\n        - 'server_name'\n\n    'TlsSettingsPatch':\n      'description': >\n        TLS and encryption settings update object.\n      'example':\n        'certificate': 'Base64KeyDatAA=='\n        'enabled': true\n        'private_key': 'Base64CertDatA=='\n      'properties':\n        'certificate':\n          'description': |\n            Base64-encoded string with PEM-encoded certificate chain.\n\n            Should not be sent if `certificate_path` is sent.  Otherwise, must\n            be sent.\n          'format': 'byte'\n          'type': 'string'\n        'certificate_path':\n          'description': |\n            Path to the certificate file.\n\n            Should not be sent if `certificate` is sent.  Otherwise, must be\n            sent.\n          'type': 'string'\n        'enabled':\n          'description': >\n            If `true`,  AdGuard Home the administration interface will be served\n            over HTTPS, and the DNS server will listen requests over\n            DNS-over-TLS and other protocols.\n          'type': 'boolean'\n        'port_dns_over_quic':\n          'default': 853\n          'description': >\n            The DNS-over-QUIC port.  If `0`, DNS-over-QUIC is disabled.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n        'port_dns_over_tls':\n          'default': 853\n          'description': >\n            The DNS-over-TLS port.  If `0`, DNS-over-TLS is disabled.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n        'port_https':\n          'default': 443\n          'description': >\n            The HTTPS port.  If `0`, HTTPS is disabled.\n          'format': 'int64'\n          'maximum': 65535\n          'minimum': 0\n          'type': 'integer'\n        'private_key':\n          'description': |\n            Base64-encoded string with PEM-encoded private key.\n\n            Should not be sent if `private_key_path` is sent.  Otherwise, must\n            be sent.\n          'format': 'byte'\n          'type': 'string'\n        'private_key_path':\n          'description': |\n            Path to the private key file.\n\n            Should not be sent if `private_key` is sent.  Otherwise, must be\n            sent.\n          'type': 'string'\n        'server_name':\n          'description': >\n            The name of the server.  Used to validate the certificates as well\n            as to check ClientIDs in DNS-over-HTTP and DNS-over-TLS.\n          'type': 'string'\n      'type': 'object'\n\n    'Uid':\n      'description': >\n        A unique ID of an entity, an opaque string.\n      'pattern': '[0-9a-zA-Z_-]{1,64}'\n      'type': 'string'\n\n    'UnauthorizedResp':\n      'example':\n        'code': 'AUT000'\n        'msg': 'no or bad authorization provided'\n      'properties':\n        'code':\n          '$ref': '#/components/schemas/ErrorCode'\n        'msg':\n          'description': >\n            Error message string.\n          'type': 'string'\n      'required':\n      - 'code'\n      - 'msg'\n      'type': 'object'\n\n    'UnprocessableEntityResp':\n      'example':\n        'code': 'JSN001'\n        'msg': >-\n          json: cannot unmarshal string into Go struct field T.A of type int\n      'properties':\n        'code':\n          '$ref': '#/components/schemas/ErrorCode'\n        'msg':\n          'description': >\n            Error message string.\n          'type': 'string'\n      'required':\n      - 'code'\n      - 'msg'\n      'type': 'object'\n\n    'UpstreamServerAddr':\n      'description': |\n        Upstream DNS server address.  Supported item formats:\n\n         *  `94.140.14.140`: plain DNS-over-UDP.\n\n         *  `tls://unfiltered.adguard-dns.com`: encrypted DNS-over-TLS.\n\n         *  `https://unfiltered.adguard-dns.com/dns-query`: encrypted\n             DNS-over-HTTPS.\n\n         *  `quic://unfiltered.adguard-dns.com`: encrypted DNS-over-QUIC.\n\n         *  `tcp://94.140.14.140`: plain DNS-over-TCP.\n\n         *  `sdns://...`: DNS Stamps for DNSCrypt or DNS-over-HTTPS\n             resolvers.\n\n         *  `[/example.local/]94.140.14.140`: DNS upstream for specific\n             domain(s).\n\n         *  `# comment`: A comment.\n      'type': 'string'\n\n    'Whois':\n      'additionalProperties':\n        'type': 'string'\n      'description': >\n        WHOIS information, if any.  If there are none, this field is usually\n        absent.\n      'minProperties': 1\n      'type': 'object'\n\n  # TODO(a.garipov): Find a way to specify a cookie authorization.\n  'securitySchemes':\n    'basicAuth':\n      'description': >\n        Basic HTTP authorization.\n      'scheme': 'basic'\n      'type': 'http'\n"
  },
  {
    "path": "openapi/openapi.yaml",
    "content": "'openapi': '3.0.3'\n'info':\n  'title': 'AdGuard Home'\n  'description': >\n    AdGuard Home REST-ish API.  Our admin web interface is built on top of this\n    REST-ish API.\n  'version': '0.107'\n  'contact':\n    'name': 'AdGuard Home'\n    'url': 'https://github.com/AdguardTeam/AdGuardHome'\n\n'servers':\n- 'url': '/control'\n\n'security':\n- 'basicAuth': []\n\n'tags':\n- 'name': 'blocked_services'\n  'description': 'Blocked services controls'\n- 'name': 'clients'\n  'description': 'Clients list operations'\n- 'name': 'dhcp'\n  'description': 'Built-in DHCP server controls'\n- 'name': 'filtering'\n  'description': 'Rule-based filtering'\n- 'name': 'global'\n  'description': 'AdGuard Home server general settings and controls'\n- 'name': 'i18n'\n  'description': 'Application localization'\n- 'name': 'install'\n  'description': 'First-time install configuration handlers'\n- 'name': 'log'\n  'description': 'AdGuard Home query log'\n- 'name': 'mobileconfig'\n  'description': 'Apple .mobileconfig'\n- 'name': 'parental'\n  'description': 'Blocking adult and explicit materials'\n- 'name': 'rewrite'\n  'description': 'DNS rewrites'\n- 'name': 'safebrowsing'\n  'description': 'Blocking malware/phishing sites'\n- 'name': 'safesearch'\n  'description': 'Enforce family-friendly results in search engines'\n- 'name': 'stats'\n  'description': 'AdGuard Home statistics'\n- 'name': 'tls'\n  'description': 'AdGuard Home HTTPS/DoH/DoQ/DoT settings'\n\n'paths':\n  '/status':\n    'get':\n      'tags':\n      - 'global'\n      'operationId': 'status'\n      'summary': 'Get DNS server current status and general settings'\n      'responses':\n        '200':\n          'description': 'OK'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/ServerStatus'\n  '/dns_info':\n    'get':\n      'tags':\n      - 'global'\n      'operationId': 'dnsInfo'\n      'summary': 'Get general DNS parameters'\n      'responses':\n        '200':\n          'description': 'OK'\n          'content':\n            'application/json':\n              'schema':\n                'allOf':\n                - '$ref': '#/components/schemas/DNSConfig'\n                - 'type': 'object'\n                  'properties':\n                    'default_local_ptr_upstreams':\n                      'type': 'array'\n                      'items':\n                        'type': 'string'\n                      'example':\n                      - '192.168.168.192'\n                      - '10.0.0.10'\n  '/dns_config':\n    'post':\n      'tags':\n      - 'global'\n      'operationId': 'dnsConfig'\n      'summary': 'Set general DNS parameters'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/DNSConfig'\n      'responses':\n        '200':\n          'description': 'OK'\n  '/protection':\n    'post':\n      'tags':\n        - 'global'\n      'operationId': 'setProtection'\n      'summary': 'Set protection state and duration'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/SetProtectionRequest'\n      'responses':\n        '200':\n          'description': 'OK'\n  '/cache_clear':\n    'post':\n      'tags':\n      - 'global'\n      'operationId': 'cacheClear'\n      'summary': 'Clear DNS cache'\n      'responses':\n        '200':\n          'description': 'OK'\n  '/test_upstream_dns':\n    'post':\n      'tags':\n      - 'global'\n      'operationId': 'testUpstreamDNS'\n      'summary': 'Test upstream configuration'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/UpstreamsConfig'\n        'description': 'Upstream configuration to be tested'\n      'responses':\n        '200':\n          'description': >\n            Status of testing each requested server, with \"OK\" meaning that\n            server works, any other text means an error.\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/UpstreamsConfigResponse'\n              'examples':\n                'response':\n                  'value':\n                    '1.1.1.1': 'OK'\n                    '1.0.0.1': 'OK'\n                    '8.8.8.8': 'OK'\n                    '8.8.4.4': 'OK'\n                    '192.168.1.104:53535': >\n                      upstream \"192.168.1.104:1234\" fails to exchange: couldn't\n                      communicate with upstream: read udp\n                      192.168.1.100:60675->8.8.8.8:1234: i/o timeout\n  '/version.json':\n    'post':\n      'tags':\n      - 'global'\n      'operationId': 'getVersionJson'\n      'summary': >\n        Gets information about the latest available version of AdGuard\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/GetVersionRequest'\n        'required': true\n      'responses':\n        '200':\n          'description': >\n            Version info.  If response message is empty, UI does not show\n            a version update message.\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/VersionInfo'\n        '500':\n          'description': 'Cannot write answer'\n        '502':\n          'description': 'Cannot retrieve the version.json file contents'\n  '/update':\n    'post':\n      'tags':\n      - 'global'\n      'operationId': 'beginUpdate'\n      'summary': 'Begin auto-upgrade procedure'\n      'responses':\n        '200':\n          'description': 'OK.'\n        '500':\n          'description': 'Failed'\n  '/querylog':\n    'get':\n      'tags':\n      - 'log'\n      'operationId': 'queryLog'\n      'summary': 'Get DNS server query log.'\n      'parameters':\n      - 'name': 'older_than'\n        'in': 'query'\n        'description': 'Filter by older than'\n        'schema':\n          'type': 'string'\n      - 'name': 'offset'\n        'in': 'query'\n        'description': >\n          Specify the ranking number of the first item on the page.  Even\n          though it is possible to use \"offset\" and \"older_than\", we recommend\n          choosing one of them and sticking to it.\n        'schema':\n          'type': 'integer'\n      - 'name': 'limit'\n        'in': 'query'\n        'description': 'Limit the number of records to be returned'\n        'schema':\n          'type': 'integer'\n      - 'name': 'search'\n        'in': 'query'\n        'description': 'Filter by domain name or client IP'\n        'schema':\n          'type': 'string'\n      - 'name': 'response_status'\n        'in': 'query'\n        'description': 'Filter by response status'\n        'schema':\n          'type': 'string'\n          'enum':\n          - 'all'\n          - 'filtered'\n          - 'blocked'\n          - 'blocked_safebrowsing'\n          - 'blocked_parental'\n          - 'whitelisted'\n          - 'rewritten'\n          - 'safe_search'\n          - 'processed'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/QueryLog'\n  '/querylog_info':\n    'get':\n      'deprecated': true\n      'description': |\n        Deprecated: Use `GET /querylog/config` instead.\n\n        NOTE: If `interval` was configured by editing configuration file or new\n        HTTP API call `PUT /querylog/config/update` and it's not equal to\n        previous allowed enum values then it will be equal to `90` days for\n        compatibility reasons.\n      'tags':\n      - 'log'\n      'operationId': 'queryLogInfo'\n      'summary': 'Get query log parameters'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/QueryLogConfig'\n  '/querylog_config':\n    'post':\n      'deprecated': true\n      'description': >\n        Deprecated: Use `PUT /querylog/config/update` instead.\n      'tags':\n      - 'log'\n      'operationId': 'queryLogConfig'\n      'summary': 'Set query log parameters'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/QueryLogConfig'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/querylog_clear':\n    'post':\n      'tags':\n      - 'log'\n      'operationId': 'querylogClear'\n      'summary': 'Clear query log'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/querylog/config':\n    'get':\n      'tags':\n      - 'log'\n      'operationId': 'getQueryLogConfig'\n      'summary': 'Get query log parameters'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/GetQueryLogConfigResponse'\n  '/querylog/config/update':\n    'put':\n      'tags':\n      - 'log'\n      'operationId': 'putQueryLogConfig'\n      'summary': 'Set query log parameters'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/PutQueryLogConfigUpdateRequest'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/stats':\n    'get':\n      'tags':\n      - 'stats'\n      'operationId': 'stats'\n      'summary': 'Get DNS server statistics'\n      'parameters':\n      - 'name': 'recent'\n        'in': 'query'\n        'description': |\n          The lookback period for statistics in milliseconds.  The interval must\n          be a multiple of one hour and must not be greater than the value of\n          `statistics.interval`.\n        'required': false\n        'example': 604800000\n        'schema':\n          'type': 'integer'\n      'responses':\n        '200':\n          'description': 'Returns statistics data'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Stats'\n        '400':\n          'description': 'Invalid value of parameter `recent`'\n  '/stats_reset':\n    'post':\n      'tags':\n      - 'stats'\n      'operationId': 'statsReset'\n      'summary': 'Reset all statistics to zeroes'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/stats_info':\n    'get':\n      'deprecated': true\n      'description': |\n        Deprecated: Use `GET /stats/config` instead.\n\n        NOTE: If `interval` was configured by editing configuration file or new\n        HTTP API call `PUT /stats/config/update` and it's not equal to\n        previous allowed enum values then it will be equal to `90` days for\n        compatibility reasons.\n      'tags':\n      - 'stats'\n      'operationId': 'statsInfo'\n      'summary': 'Get statistics parameters'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/StatsConfig'\n  '/stats_config':\n    'post':\n      'deprecated': true\n      'description': >\n        Deprecated: Use `PUT /stats/config/update` instead.\n      'tags':\n      - 'stats'\n      'operationId': 'statsConfig'\n      'summary': 'Set statistics parameters'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/StatsConfig'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/stats/config':\n    'get':\n      'tags':\n      - 'stats'\n      'operationId': 'getStatsConfig'\n      'summary': 'Get statistics parameters'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/GetStatsConfigResponse'\n  '/stats/config/update':\n    'put':\n      'tags':\n      - 'stats'\n      'operationId': 'putStatsConfig'\n      'summary': 'Set statistics parameters'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/PutStatsConfigUpdateRequest'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/tls/status':\n    'get':\n      'tags':\n      - 'tls'\n      'operationId': 'tlsStatus'\n      'summary': 'Returns TLS configuration and its status'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/TlsConfig'\n  '/tls/configure':\n    'post':\n      'tags':\n      - 'tls'\n      'operationId': 'tlsConfigure'\n      'summary': 'Updates current TLS configuration'\n      'requestBody':\n        '$ref': '#/components/requestBodies/TlsConfig'\n      'responses':\n        '200':\n          'description': 'TLS configuration and its status'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/TlsConfig'\n        '400':\n          'description': 'Invalid configuration or unavailable port'\n        '500':\n          'description': 'Error occurred while applying configuration'\n  '/tls/validate':\n    'post':\n      'tags':\n      - 'tls'\n      'operationId': 'tlsValidate'\n      'summary': 'Checks if the current TLS configuration is valid'\n      'requestBody':\n        '$ref': '#/components/requestBodies/TlsConfig'\n      'responses':\n        '200':\n          'description': 'TLS configuration and its status'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/TlsConfig'\n        '400':\n          'description': 'Invalid configuration or unavailable port'\n  '/dhcp/status':\n    'get':\n      'tags':\n      - 'dhcp'\n      'operationId': 'dhcpStatus'\n      'summary': 'Gets the current DHCP settings and status'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/DhcpStatus'\n        '500':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/dhcp/interfaces':\n    'get':\n      'tags':\n      - 'dhcp'\n      'operationId': 'dhcpInterfaces'\n      'summary': 'Gets the available interfaces'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/NetInterfaces'\n        '500':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/dhcp/set_config':\n    'post':\n      'tags':\n      - 'dhcp'\n      'operationId': 'dhcpSetConfig'\n      'summary': 'Updates the current DHCP server configuration'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/DhcpConfig'\n      'responses':\n        '200':\n          'description': 'OK.'\n        '501':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/dhcp/find_active_dhcp':\n    'post':\n      'tags':\n      - 'dhcp'\n      'operationId': 'checkActiveDhcp'\n      'summary': 'Searches for an active DHCP server on the network'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/DhcpFindActiveReq'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/DhcpSearchResult'\n        '501':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/dhcp/add_static_lease':\n    'post':\n      'tags':\n      - 'dhcp'\n      'operationId': 'dhcpAddStaticLease'\n      'summary': 'Adds a static lease'\n      'requestBody':\n        '$ref': '#/components/requestBodies/DhcpStaticLease'\n      'responses':\n        '200':\n          'description': 'OK.'\n        '501':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/dhcp/remove_static_lease':\n    'post':\n      'tags':\n      - 'dhcp'\n      'operationId': 'dhcpRemoveStaticLease'\n      'summary': 'Removes a static lease'\n      'requestBody':\n        '$ref': '#/components/requestBodies/DhcpStaticLease'\n      'responses':\n        '200':\n          'description': 'OK.'\n        '501':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/dhcp/update_static_lease':\n    'post':\n      'tags':\n      - 'dhcp'\n      'operationId': 'dhcpUpdateStaticLease'\n      'description': >\n        Updates IP address, hostname of the static lease.  IP version must be\n        the same as previous.\n      'summary': 'Updates a static lease'\n      'requestBody':\n        '$ref': '#/components/requestBodies/DhcpStaticLease'\n      'responses':\n        '200':\n          'description': 'OK.'\n        '501':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/dhcp/reset':\n    'post':\n      'tags':\n      - 'dhcp'\n      'operationId': 'dhcpReset'\n      'summary': 'Reset DHCP configuration'\n      'responses':\n        '200':\n          'description': 'OK.'\n        '501':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/dhcp/reset_leases':\n    'post':\n      'tags':\n      - 'dhcp'\n      'operationId': 'dhcpResetLeases'\n      'summary': 'Reset DHCP leases'\n      'responses':\n        '200':\n          'description': 'OK.'\n        '501':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Not implemented (for example, on Windows).'\n  '/filtering/status':\n    'get':\n      'tags':\n      - 'filtering'\n      'operationId': 'filteringStatus'\n      'summary': 'Get filtering parameters'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/FilterStatus'\n  '/filtering/config':\n    'post':\n      'tags':\n      - 'filtering'\n      'operationId': 'filteringConfig'\n      'summary': 'Set filtering parameters'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/FilterConfig'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/filtering/add_url':\n    'post':\n      'tags':\n      - 'filtering'\n      'operationId': 'filteringAddURL'\n      'summary': 'Add filter URL or an absolute file path'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/AddUrlRequest'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/filtering/remove_url':\n    'post':\n      'tags':\n      - 'filtering'\n      'operationId': 'filteringRemoveURL'\n      'summary': 'Remove filter URL'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/RemoveUrlRequest'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/filtering/set_url':\n    'post':\n      'tags':\n      - 'filtering'\n      'operationId': 'filteringSetURL'\n      'summary': 'Set URL parameters'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/FilterSetUrl'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/filtering/refresh':\n    'post':\n      'tags':\n      - 'filtering'\n      'operationId': 'filteringRefresh'\n      'summary': >\n        Reload filtering rules from URLs.  This might be needed if new URL was\n        just added and you don't want to wait for automatic refresh to kick in.\n        This API request is ratelimited, so you can call it freely as often as\n        you like, it wont create unnecessary burden on servers that host the\n        URL.  This should work as intended, a `force` parameter is offered as\n        last-resort attempt to make filter lists fresh.  If you ever find\n        yourself using `force` to make something work that otherwise wont, this\n        is a bug and report it accordingly.\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/FilterRefreshRequest'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/FilterRefreshResponse'\n  '/filtering/set_rules':\n    'post':\n      'tags':\n      - 'filtering'\n      'operationId': 'filteringSetRules'\n      'summary': 'Set user-defined filter rules'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/SetRulesRequest'\n        'description': 'Custom filtering rules.'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/filtering/check_host':\n    'get':\n      'tags':\n      - 'filtering'\n      'operationId': 'filteringCheckHost'\n      'summary': 'Check if host name is filtered'\n      'parameters':\n      - 'name': 'name'\n        'in': 'query'\n        'description': 'Filter by host name'\n        'required': true\n        'example': 'google.com'\n        'schema':\n          'type': 'string'\n      - 'name': 'client'\n        'in': 'query'\n        'description': 'Optional ClientID or client IP address'\n        'example': '192.0.2.1'\n        'schema':\n          'type': 'string'\n      - 'name': 'qtype'\n        'in': 'query'\n        'description': 'Optional DNS type'\n        'example': 'AAAA'\n        'schema':\n          'type': 'string'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/FilterCheckHostResponse'\n  '/safebrowsing/enable':\n    'post':\n      'tags':\n      - 'safebrowsing'\n      'operationId': 'safebrowsingEnable'\n      'summary': 'Enable safebrowsing'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/safebrowsing/disable':\n    'post':\n      'tags':\n      - 'safebrowsing'\n      'operationId': 'safebrowsingDisable'\n      'summary': 'Disable safebrowsing'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/safebrowsing/status':\n    'get':\n      'tags':\n      - 'safebrowsing'\n      'operationId': 'safebrowsingStatus'\n      'summary': 'Get safebrowsing status'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                'type': 'object'\n                'properties':\n                  'enabled':\n                    'type': 'boolean'\n              'examples':\n                'response':\n                  'value':\n                    'enabled': false\n  '/parental/enable':\n    'post':\n      'tags':\n      - 'parental'\n      'operationId': 'parentalEnable'\n      'summary': 'Enable parental filtering'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/parental/disable':\n    'post':\n      'tags':\n      - 'parental'\n      'operationId': 'parentalDisable'\n      'summary': 'Disable parental filtering'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/parental/status':\n    'get':\n      'tags':\n      - 'parental'\n      'operationId': 'parentalStatus'\n      'summary': 'Get parental filtering status'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                'type': 'object'\n                'properties':\n                  'enable':\n                    'type': 'boolean'\n                  'sensitivity':\n                    'type': 'integer'\n              'examples':\n                'response':\n                  'value':\n                    'enabled': true\n                    'sensitivity': 13\n  '/safesearch/enable':\n    'post':\n      'deprecated': true\n      'tags':\n      - 'safesearch'\n      'operationId': 'safesearchEnable'\n      'summary': 'Enable safesearch'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/safesearch/disable':\n    'post':\n      'deprecated': true\n      'tags':\n      - 'safesearch'\n      'operationId': 'safesearchDisable'\n      'summary': 'Disable safesearch'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/safesearch/settings':\n    'put':\n      'tags':\n        - 'safesearch'\n      'operationId': 'safesearchSettings'\n      'summary': 'Update safesearch settings'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/SafeSearchConfig'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/safesearch/status':\n    'get':\n      'tags':\n      - 'safesearch'\n      'operationId': 'safesearchStatus'\n      'summary': 'Get safesearch status'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/SafeSearchConfig'\n  '/clients':\n    'get':\n      'tags':\n      - 'clients'\n      'operationId': 'clientsStatus'\n      'summary': 'Get information about configured clients'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Clients'\n  '/clients/add':\n    'post':\n      'tags':\n      - 'clients'\n      'operationId': 'clientsAdd'\n      'summary': 'Add a new client'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/Client'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/clients/delete':\n    'post':\n      'tags':\n      - 'clients'\n      'operationId': 'clientsDelete'\n      'summary': 'Remove a client'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/ClientDelete'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/clients/update':\n    'post':\n      'tags':\n      - 'clients'\n      'operationId': 'clientsUpdate'\n      'summary': 'Update client information'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/ClientUpdate'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/clients/find':\n    'get':\n      'deprecated': true\n      'description': >\n        Deprecated: Use `POST /clients/search` instead.\n      'tags':\n      - 'clients'\n      'operationId': 'clientsFind'\n      'summary': >\n        Get information about clients by their IP addresses or ClientIDs.\n      'parameters':\n      - 'name': 'ip0'\n        'in': 'query'\n        'description': >\n          Filter by IP address or ClientIDs.  Parameters with names `ip1`,\n          `ip2`, and so on are also accepted and interpreted as \"ip0 OR ip1 OR\n          ip2\".\n\n          TODO(a.garipov): Replace with a better query API.\n        'schema':\n          'type': 'string'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/ClientsFindResponse'\n  '/clients/search':\n    'post':\n      'tags':\n      - 'clients'\n      'operationId': 'clientsSearch'\n      'summary': >\n        Retrieve information about clients by performing an exact match search\n        using IP addresses, CIDRs, MAC addresses, or ClientIDs.\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/ClientsSearchRequest'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/ClientsFindResponse'\n  '/access/list':\n    'get':\n      'operationId': 'accessList'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/AccessListResponse'\n      'summary': 'List (dis)allowed clients, blocked hosts, etc.'\n      'tags':\n      - 'clients'\n  '/access/set':\n    'post':\n      'operationId': 'accessSet'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/AccessSetRequest'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n        '400':\n          'description': >\n            Failed to parse JSON or cannot save the list.\n        '500':\n          'description': 'Internal error.'\n      'summary': 'Set (dis)allowed clients, blocked hosts, etc.'\n      'tags':\n      - 'clients'\n  '/blocked_services/services':\n    'get':\n      'deprecated': true\n      'description': >\n        Deprecated: Use `GET /blocked_services/all` instead.\n      'tags':\n      - 'blocked_services'\n      'operationId': 'blockedServicesAvailableServices'\n      'summary': 'Get available services to use for blocking'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/BlockedServicesArray'\n  '/blocked_services/all':\n    'get':\n      'tags':\n      - 'blocked_services'\n      'operationId': 'blockedServicesAll'\n      'summary': 'Get available services to use for blocking'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/BlockedServicesAll'\n  '/blocked_services/list':\n    'get':\n      'deprecated': true\n      'description': >\n        Deprecated: Use `GET /blocked_services/get` instead.\n      'tags':\n      - 'blocked_services'\n      'operationId': 'blockedServicesList'\n      'summary': 'Get blocked services list'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/BlockedServicesArray'\n  '/blocked_services/set':\n    'post':\n      'deprecated': true\n      'description': >\n        Deprecated: Use `PUT /blocked_services/update` instead.\n      'tags':\n      - 'blocked_services'\n      'operationId': 'blockedServicesSet'\n      'summary': 'Set blocked services list'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/BlockedServicesArray'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/blocked_services/get':\n    'get':\n      'tags':\n      - 'blocked_services'\n      'operationId': 'blockedServicesSchedule'\n      'summary': 'Get blocked services'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/BlockedServicesSchedule'\n  '/blocked_services/update':\n    'put':\n      'tags':\n      - 'blocked_services'\n      'operationId': 'blockedServicesScheduleUpdate'\n      'summary': 'Update blocked services'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/BlockedServicesSchedule'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/rewrite/list':\n    'get':\n      'tags':\n      - 'rewrite'\n      'operationId': 'rewriteList'\n      'summary': 'Get list of Rewrite rules'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/RewriteList'\n  '/rewrite/add':\n    'post':\n      'tags':\n      - 'rewrite'\n      'operationId': 'rewriteAdd'\n      'summary': 'Add a new Rewrite rule'\n      'requestBody':\n        '$ref': '#/components/requestBodies/RewriteEntry'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/rewrite/delete':\n    'post':\n      'tags':\n      - 'rewrite'\n      'operationId': 'rewriteDelete'\n      'summary': 'Remove a Rewrite rule'\n      'requestBody':\n        '$ref': '#/components/requestBodies/RewriteEntry'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/rewrite/settings':\n    'get':\n      'tags':\n        - 'rewrite'\n      'operationId': 'rewriteSettingsGet'\n      'summary': 'Get rewrite settings'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/RewriteSettings'\n  '/rewrite/settings/update':\n    'put':\n      'tags':\n        - 'rewrite'\n      'operationId': 'rewriteSettingsUpdate'\n      'summary': 'Update rewrite settings'\n      'requestBody':\n        '$ref': '#/components/requestBodies/RewriteSettings'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/rewrite/update':\n    'put':\n      'tags':\n        - 'rewrite'\n      'operationId': 'rewriteUpdate'\n      'summary': 'Update a Rewrite rule'\n      'requestBody':\n        '$ref': '#/components/requestBodies/RewriteUpdate'\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/i18n/change_language':\n    'post':\n      'deprecated': true\n      'description': >\n        Deprecated: Use `PUT /control/profile` instead.\n      'tags':\n      - 'i18n'\n      'operationId': 'changeLanguage'\n      'summary': >\n        Change current language.  Argument must be an ISO 639-1 two-letter code.\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/LanguageSettings'\n        'description': >\n          New language.  It must be known to the server and must be an ISO 639-1\n          two-letter code.\n      'responses':\n        '200':\n          'description': 'OK.'\n  '/i18n/current_language':\n    'get':\n      'deprecated': true\n      'description': >\n        Deprecated: Use `GET /control/profile` instead.\n      'tags':\n      - 'i18n'\n      'operationId': 'currentLanguage'\n      'summary': >\n        Get currently set language.  Result is ISO 639-1 two-letter code.  Empty\n        result means default language.\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/LanguageSettings'\n  '/install/get_addresses':\n    'get':\n      'tags':\n      - 'install'\n      'operationId': 'installGetAddresses'\n      'summary': 'Gets the network interfaces information.'\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/AddressesInfo'\n  '/install/check_config':\n    'post':\n      'tags':\n      - 'install'\n      'operationId': 'installCheckConfig'\n      'summary': 'Checks configuration'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/CheckConfigRequest'\n        'description': 'Configuration to be checked'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/CheckConfigResponse'\n        '400':\n          'description': >\n            Failed to parse JSON or cannot listen on the specified address.\n  '/install/configure':\n    'post':\n      'tags':\n      - 'install'\n      'operationId': 'installConfigure'\n      'summary': 'Applies the initial configuration.'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/InitialConfiguration'\n        'description': 'Initial configuration JSON'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n        '400':\n          'description': >\n            Failed to parse initial configuration or cannot listen to the\n            specified addresses.\n        '422':\n          'description': >\n            The specified password does not meet the strength requirements.\n        '500':\n          'description': 'Cannot start the DNS server'\n  '/login':\n    'post':\n      'tags':\n      - 'global'\n      'operationId': 'login'\n      'summary': 'Perform administrator log-in'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/Login'\n        'required': true\n      'responses':\n        '200':\n          'description': 'OK.'\n        '400':\n          'description': >\n            Invalid username or password.\n        '429':\n          'description': >\n            Out of login attempts.\n  '/logout':\n    'get':\n      'tags':\n      - 'global'\n      'operationId': 'logout'\n      'summary': 'Perform administrator log-out'\n      'responses':\n        '302':\n          'description': 'OK.'\n  '/profile/update':\n    'put':\n      'tags':\n        - 'global'\n      'operationId': 'updateProfile'\n      'summary': 'Updates current user info'\n      'requestBody':\n        'content':\n          'application/json':\n            'schema':\n              '$ref': '#/components/schemas/ProfileInfo'\n      'responses':\n        '200':\n          'description': 'OK'\n  '/profile':\n    'get':\n      'tags':\n      - 'global'\n      'operationId': 'getProfile'\n      'summary': ''\n      'responses':\n        '200':\n          'description': 'OK.'\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/ProfileInfo'\n\n  '/apple/doh.mobileconfig':\n    'get':\n      'operationId': 'mobileConfigDoH'\n      'parameters':\n      - 'description': >\n          Host for which the config is generated.  If no host is provided,\n          `tls.server_name` from the configuration file is used.  If\n          `tls.server_name` is not set, the API returns an error with a 500\n          status.\n        'example': 'example.org'\n        'in': 'query'\n        'name': 'host'\n        'required': true\n        'schema':\n          'type': 'string'\n      - 'description': >\n          ClientID.\n        'example': 'client-1'\n        'in': 'query'\n        'name': 'client_id'\n        'schema':\n          'type': 'string'\n      'responses':\n        '200':\n          'description': 'DNS over HTTPS plist file.'\n        '500':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Server configuration error.'\n      'summary': 'Get DNS over HTTPS .mobileconfig.'\n      'tags':\n      - 'mobileconfig'\n      - 'global'\n  '/apple/dot.mobileconfig':\n    'get':\n      'operationId': 'mobileConfigDoT'\n      'parameters':\n      - 'description': >\n          Host for which the config is generated.  If no host is provided,\n          `tls.server_name` from the configuration file is used.  If\n          `tls.server_name` is not set, the API returns an error with a 500\n          status.\n        'example': 'example.org'\n        'in': 'query'\n        'name': 'host'\n        'required': true\n        'schema':\n          'type': 'string'\n      - 'description': >\n          ClientID.\n        'example': 'client-1'\n        'in': 'query'\n        'name': 'client_id'\n        'schema':\n          'type': 'string'\n      'responses':\n        '200':\n          'description': 'DNS over TLS plist file'\n        '500':\n          'content':\n            'application/json':\n              'schema':\n                '$ref': '#/components/schemas/Error'\n          'description': 'Server configuration error.'\n      'summary': 'Get DNS over TLS .mobileconfig.'\n      'tags':\n      - 'mobileconfig'\n      - 'global'\n\n'components':\n  'requestBodies':\n    'TlsConfig':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/TlsConfig'\n      'description': 'TLS configuration JSON'\n      'required': true\n    'DhcpStaticLease':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/DhcpStaticLease'\n      'required': true\n    'RewriteEntry':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/RewriteEntry'\n      'required': true\n    'RewriteSettings':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/RewriteSettings'\n      'required': true\n    'RewriteUpdate':\n      'content':\n        'application/json':\n          'schema':\n            '$ref': '#/components/schemas/RewriteUpdate'\n      'required': true\n  'schemas':\n    'ServerStatus':\n      'type': 'object'\n      'description': 'AdGuard Home server status and configuration'\n      'required':\n      - 'dns_addresses'\n      - 'dns_port'\n      - 'http_port'\n      - 'protection_enabled'\n      - 'protection_disabled_until'\n      - 'running'\n      - 'version'\n      - 'language'\n      'properties':\n        'dns_addresses':\n          'example': ['127.0.0.1']\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'dns_port':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 53\n          'minimum': 1\n          'maximum': 65535\n        'http_port':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 80\n          'minimum': 1\n          'maximum': 65535\n        'protection_enabled':\n          'type': 'boolean'\n        'protection_disabled_duration':\n          'type': 'integer'\n          'format': 'int64'\n        'dhcp_available':\n          'type': 'boolean'\n        'running':\n          'type': 'boolean'\n        'version':\n          'type': 'string'\n          'example': 'v0.123.4'\n        'language':\n          'type': 'string'\n          'example': 'en'\n        'start_time':\n          'type': 'number'\n          'format': 'double'\n          'example': 1700000000000\n          'description': 'Start time of the web API server (Unix time in milliseconds).'\n    'DNSConfig':\n      'type': 'object'\n      'description': 'DNS server configuration'\n      'properties':\n        'bootstrap_dns':\n          'type': 'array'\n          'description': >\n            Bootstrap servers, port is optional after colon.  Empty value will\n            reset it to default values.\n          'items':\n            'type': 'string'\n          'example':\n          - '8.8.8.8:53'\n          - '1.1.1.1:53'\n        'upstream_dns':\n          'type': 'array'\n          'description': >\n            Upstream servers, port is optional after colon.  Empty value will\n            reset it to default values.\n          'items':\n            'type': 'string'\n          'example':\n          - 'tls://1.1.1.1'\n          - 'tls://1.0.0.1'\n        'fallback_dns':\n          'type': 'array'\n          'description': >\n            List of fallback DNS servers used when upstream DNS servers are not\n            responding.  Empty value will clear the list.\n          'items':\n            'type': 'string'\n          'example':\n          - '8.8.8.8'\n          - '1.1.1.1:53'\n        'upstream_dns_file':\n          'type': 'string'\n        'protection_enabled':\n          'type': 'boolean'\n        'ratelimit':\n          'type': 'integer'\n        'ratelimit_subnet_subnet_len_ipv4':\n          'description': 'Length of the subnet mask for IPv4 addresses.'\n          'type': 'integer'\n          'default': 24\n          'minimum': 0\n          'maximum': 32\n        'ratelimit_subnet_subnet_len_ipv6':\n          'description': 'Length of the subnet mask for IPv6 addresses.'\n          'type': 'integer'\n          'default': 56\n          'minimum': 0\n          'maximum': 128\n        'ratelimit_whitelist':\n          'type': 'array'\n          'description': 'List of IP addresses excluded from rate limiting.'\n          'items':\n            'type': 'string'\n        'blocking_mode':\n          'type': 'string'\n          'enum':\n          - 'default'\n          - 'refused'\n          - 'nxdomain'\n          - 'null_ip'\n          - 'custom_ip'\n        'blocking_ipv4':\n          'type': 'string'\n        'blocking_ipv6':\n          'type': 'string'\n        'blocked_response_ttl':\n          'type': 'integer'\n          'minimum': 0\n          'description': 'TTL for blocked responses.'\n        'protection_disabled_until':\n          'type': 'string'\n          'description': 'Protection is pause until this time.  Nullable.'\n          'example': '2018-11-26T00:02:41+03:00'\n        'edns_cs_enabled':\n          'type': 'boolean'\n        'edns_cs_use_custom':\n          'type': 'boolean'\n        'edns_cs_custom_ip':\n          'type': 'string'\n        'disable_ipv6':\n          'type': 'boolean'\n        'dnssec_enabled':\n          'type': 'boolean'\n        'cache_size':\n          'type': 'integer'\n        'cache_ttl_min':\n          'type': 'integer'\n        'cache_ttl_max':\n          'type': 'integer'\n        'cache_enabled':\n          'type': 'boolean'\n          'description': |\n            Enables or disables the DNS response cache.\n\n            If `cache_enabled` is `true`, the companion field `cache_size` must\n            be present and greater than 0, or the `dns.cache_size` setting in\n            the configuration file must already be greater than 0.\n        'cache_optimistic':\n          'type': 'boolean'\n        'upstream_mode':\n          'type': 'string'\n          'enum':\n          - const: ''\n            deprecated: true\n            description: Use `load_balance` instead.\n          - const: 'fastest_addr'\n          - const: 'load_balance'\n          - const: 'parallel'\n          'description': Upstream modes enumeration.\n        'use_private_ptr_resolvers':\n          'type': 'boolean'\n        'resolve_clients':\n          'type': 'boolean'\n        'local_ptr_upstreams':\n          'type': 'array'\n          'description': >\n            Upstream servers, port is optional after colon.  Empty value will\n            reset it to default values.\n          'items':\n            'type': 'string'\n          'example':\n          - 'tls://1.1.1.1'\n          - 'tls://1.0.0.1'\n        'upstream_timeout':\n          'type': 'integer'\n          'minimum': 1\n          'description': 'The number of seconds to wait for a response from the upstream server'\n    'UpstreamsConfig':\n      'type': 'object'\n      'description': 'Upstream configuration to be tested'\n      'required':\n      - 'bootstrap_dns'\n      - 'upstream_dns'\n      'properties':\n        'bootstrap_dns':\n          'type': 'array'\n          'description': >\n            Bootstrap DNS servers, port is optional after colon.\n          'items':\n            'type': 'string'\n          'example':\n          - '8.8.8.8:53'\n          - '1.1.1.1:53'\n        'upstream_dns':\n          'type': 'array'\n          'description': >\n            Upstream DNS servers, port is optional after colon.\n          'items':\n            'type': 'string'\n          'example':\n          - 'tls://1.1.1.1'\n          - 'tls://1.0.0.1'\n        'fallback_dns':\n          'type': 'array'\n          'description': >\n            Fallback DNS servers, port is optional after colon.\n          'items':\n            'type': 'string'\n          'example':\n          - '8.8.8.8'\n          - '1.1.1.1:53'\n        'private_upstream':\n          'type': 'array'\n          'description': >\n            Local PTR resolvers, port is optional after colon.\n          'items':\n            'type': 'string'\n          'example':\n          - 'tls://1.1.1.1'\n          - 'tls://1.0.0.1'\n    'UpstreamsConfigResponse':\n      'type': 'object'\n      'description': 'Upstreams configuration response'\n      'additionalProperties':\n        'type': 'string'\n    'Filter':\n      'type': 'object'\n      'description': 'Filter subscription info'\n      'required':\n      - 'enabled'\n      - 'id'\n      - 'name'\n      - 'rules_count'\n      - 'url'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n        'id':\n          'example': 1234\n          'format': 'int64'\n          'type': 'integer'\n        'last_updated':\n          'example': '2018-10-30T12:18:57+03:00'\n          'format': 'date-time'\n          'type': 'string'\n        'name':\n          'example': 'AdGuard Simplified Domain Names filter'\n          'type': 'string'\n        'rules_count':\n          'example': 5912\n          'format': 'uint32'\n          'type': 'integer'\n        'url':\n          'type': 'string'\n          'example': >\n            https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n    'FilterStatus':\n      'type': 'object'\n      'description': 'Filtering settings'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n        'interval':\n          'type': 'integer'\n        'filters':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/Filter'\n        'whitelist_filters':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/Filter'\n        'user_rules':\n          'type': 'array'\n          'items':\n            'type': 'string'\n    'FilterConfig':\n      'type': 'object'\n      'description': 'Filtering settings'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n        'interval':\n          'type': 'integer'\n    'FilterSetUrl':\n      'type': 'object'\n      'description': 'Filtering URL settings'\n      'properties':\n        'data':\n          '$ref': '#/components/schemas/FilterSetUrlData'\n        'url':\n          'type': 'string'\n        'whitelist':\n          'type': 'boolean'\n    'FilterSetUrlData':\n      'type': 'object'\n      'description': 'Filter update data'\n      'required':\n      - 'enabled'\n      - 'name'\n      - 'url'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n        'name':\n          'example': 'AdGuard Simplified Domain Names filter'\n          'type': 'string'\n        'url':\n          'type': 'string'\n          'example': >\n            https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n    'FilterRefreshRequest':\n      'type': 'object'\n      'description': 'Refresh Filters request data'\n      'properties':\n        'whitelist':\n          'type': 'boolean'\n    'FilterCheckHostResponse':\n      'type': 'object'\n      'description': 'Check Host Result'\n      'properties':\n        'reason':\n          'type': 'string'\n          'description': 'Request filtering status.'\n          'enum':\n          - 'NotFilteredNotFound'\n          - 'NotFilteredWhiteList'\n          - 'NotFilteredError'\n          - 'FilteredBlackList'\n          - 'FilteredSafeBrowsing'\n          - 'FilteredParental'\n          - 'FilteredInvalid'\n          - 'FilteredSafeSearch'\n          - 'FilteredBlockedService'\n          - 'Rewrite'\n          - 'RewriteEtcHosts'\n          - 'RewriteRule'\n        'filter_id':\n          'deprecated': true\n          'description': >\n            In case if there's a rule applied to this DNS request, this is ID of\n            the filter list that the rule belongs to.\n\n            Deprecated: use `rules[*].filter_list_id` instead.\n          'type': 'integer'\n        'rule':\n          'deprecated': true\n          'type': 'string'\n          'example': '||example.org^'\n          'description': >\n            Filtering rule applied to the request (if any).\n\n            Deprecated: use `rules[*].text` instead.\n        'rules':\n          'description': 'Applied rules.'\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/ResultRule'\n        'service_name':\n          'type': 'string'\n          'description': 'Set if reason=FilteredBlockedService'\n        'cname':\n          'type': 'string'\n          'description': 'Set if reason=Rewrite'\n        'ip_addrs':\n          'type': 'array'\n          'items':\n            'type': 'string'\n          'description': 'Set if reason=Rewrite'\n    'FilterRefreshResponse':\n      'type': 'object'\n      'description': '/filtering/refresh response data'\n      'properties':\n        'updated':\n          'type': 'integer'\n    'SetRulesRequest':\n      'description': 'Custom filtering rules setting request.'\n      'example':\n        'rules':\n        - '||example.com^'\n        - '# comment'\n        - '@@||www.example.com^'\n      'properties':\n        'rules':\n          'items':\n            'type': 'string'\n          'type': 'array'\n      'type': 'object'\n    'GetVersionRequest':\n      'type': 'object'\n      'description': '/version.json request data'\n      'properties':\n        'recheck_now':\n          'description': >\n            If false, server will check for a new version data only once in\n            several hours.\n          'type': 'boolean'\n    'VersionInfo':\n      'type': 'object'\n      'description': >\n        Information about the latest available version of AdGuard Home.\n      'required':\n      - 'disabled'\n      'properties':\n        'disabled':\n          'type': 'boolean'\n          'description': >\n            If true then other fields doesn't appear.\n        'new_version':\n          'type': 'string'\n          'example': 'v0.9'\n        'announcement':\n          'type': 'string'\n          'example': 'AdGuard Home v0.9 is now available!'\n        'announcement_url':\n          'type': 'string'\n          'example': >\n            https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9\n        'can_autoupdate':\n          'type': 'boolean'\n    'Stats':\n      'type': 'object'\n      'description': 'Server statistics data'\n      'properties':\n        'time_units':\n          'type': 'string'\n          'enum':\n          - 'hours'\n          - 'days'\n          'description': 'Time units'\n          'example': 'hours'\n        'num_dns_queries':\n          'type': 'integer'\n          'description': 'Total number of DNS queries'\n          'example': 123\n        'num_blocked_filtering':\n          'type': 'integer'\n          'description': 'Number of requests blocked by filtering rules'\n          'example': 50\n        'num_replaced_safebrowsing':\n          'type': 'integer'\n          'description': 'Number of requests blocked by safebrowsing module'\n          'example': 5\n        'num_replaced_safesearch':\n          'type': 'integer'\n          'description': 'Number of requests blocked by safesearch module'\n          'example': 5\n        'num_replaced_parental':\n          'type': 'integer'\n          'description': 'Number of blocked adult websites'\n          'example': 15\n        'avg_processing_time':\n          'type': 'number'\n          'format': 'float'\n          'description': 'Average time in seconds on processing a DNS request'\n          'example': 0.34\n        'top_queried_domains':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/TopArrayEntry'\n        'top_clients':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/TopArrayEntry'\n        'top_blocked_domains':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/TopArrayEntry'\n        'top_upstreams_responses':\n          'type': 'array'\n          'description': 'Total number of responses from each upstream.'\n          'items':\n            '$ref': '#/components/schemas/TopArrayEntry'\n          'maxItems': 100\n        'top_upstreams_avg_time':\n          'type': 'array'\n          'description': >\n            Average processing time in seconds of requests from each upstream.\n          'items':\n            '$ref': '#/components/schemas/TopArrayEntry'\n          'maxItems': 100\n        'dns_queries':\n          'type': 'array'\n          'items':\n            'type': 'integer'\n        'blocked_filtering':\n          'type': 'array'\n          'items':\n            'type': 'integer'\n        'replaced_safebrowsing':\n          'type': 'array'\n          'items':\n            'type': 'integer'\n        'replaced_parental':\n          'type': 'array'\n          'items':\n            'type': 'integer'\n    'TopArrayEntry':\n      'type': 'object'\n      'description': >\n        Represent the number of hits or time duration per key (url, domain, or\n        client IP).\n      'properties':\n        'domain_or_ip':\n          'type': 'number'\n      'additionalProperties':\n          'type': 'number'\n    'StatsConfig':\n      'type': 'object'\n      'description': 'Statistics configuration'\n      'properties':\n        'interval':\n          'description': >\n            Time period to keep the data.  `0` means that the statistics is\n            disabled.\n          'enum':\n          - 0\n          - 1\n          - 7\n          - 30\n          - 90\n          'type': 'integer'\n    'GetStatsConfigResponse':\n      'type': 'object'\n      'description': 'Statistics configuration'\n      'required':\n      - 'enabled'\n      - 'interval'\n      - 'ignored'\n      'properties':\n        'enabled':\n          'description': 'Are statistics enabled'\n          'type': 'boolean'\n        'interval':\n          'description': 'Statistics rotation interval in milliseconds'\n          'type': 'number'\n        'ignored':\n          'description': 'List of host names, which should not be counted'\n          'type': 'array'\n          'items':\n            'type': 'string'\n        'ignored_enabled':\n          'type': 'boolean'\n          'description': >\n            If true, the host names in the `ignored` array are excluded from the\n            statistics.\n    'PutStatsConfigUpdateRequest':\n      '$ref': '#/components/schemas/GetStatsConfigResponse'\n    'DhcpConfig':\n      'type': 'object'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n        'interface_name':\n          'type': 'string'\n        'v4':\n          '$ref': '#/components/schemas/DhcpConfigV4'\n        'v6':\n          '$ref': '#/components/schemas/DhcpConfigV6'\n    'DhcpConfigV4':\n      'type': 'object'\n      'properties':\n        'gateway_ip':\n          'type': 'string'\n          'example': '192.168.1.1'\n        'subnet_mask':\n          'type': 'string'\n          'example': '255.255.255.0'\n        'range_start':\n          'type': 'string'\n          'example': '192.168.1.2'\n        'range_end':\n          'type': 'string'\n          'example': '192.168.10.50'\n        'lease_duration':\n          'type': 'integer'\n    'DhcpConfigV6':\n      'type': 'object'\n      'properties':\n        'range_start':\n          'type': 'string'\n        'lease_duration':\n          'type': 'integer'\n    'DhcpLease':\n      'type': 'object'\n      'description': 'DHCP lease information'\n      'required':\n      - 'mac'\n      - 'ip'\n      - 'hostname'\n      - 'expires'\n      'properties':\n        'mac':\n          'type': 'string'\n          'example': '00:11:09:b3:b3:b8'\n        'ip':\n          'type': 'string'\n          'example': '192.168.1.22'\n        'hostname':\n          'type': 'string'\n          'example': 'dell'\n        'expires':\n          'type': 'string'\n          'example': '2017-07-21T17:32:28Z'\n    'DhcpStaticLease':\n      'type': 'object'\n      'description': 'DHCP static lease information'\n      'required':\n      - 'mac'\n      - 'ip'\n      - 'hostname'\n      'properties':\n        'mac':\n          'type': 'string'\n          'example': '00:11:09:b3:b3:b8'\n        'ip':\n          'type': 'string'\n          'example': '192.168.1.22'\n        'hostname':\n          'type': 'string'\n          'example': 'dell'\n    'DhcpStatus':\n      'type': 'object'\n      'description': 'Built-in DHCP server configuration and status'\n      'required':\n      - 'config'\n      - 'leases'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n        'interface_name':\n          'type': 'string'\n        'v4':\n          '$ref': '#/components/schemas/DhcpConfigV4'\n        'v6':\n          '$ref': '#/components/schemas/DhcpConfigV6'\n        'leases':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/DhcpLease'\n        'static_leases':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/DhcpStaticLease'\n    'NetInterfaces':\n      'type': 'object'\n      'description': >\n        Network interfaces dictionary, keys are interface names.\n      'additionalProperties':\n        '$ref': '#/components/schemas/NetInterface'\n\n    'DhcpFindActiveReq':\n      'description': >\n        Request for checking for other DHCP servers in the network.\n      'properties':\n        'interface':\n          'description': 'The name of the network interface'\n          'example': 'eth0'\n          'type': 'string'\n      'type': 'object'\n\n    'DhcpSearchResult':\n      'type': 'object'\n      'description': >\n        Information about a DHCP server discovered in the current network.\n      'properties':\n        'v4':\n          '$ref': '#/components/schemas/DhcpSearchV4'\n        'v6':\n          '$ref': '#/components/schemas/DhcpSearchV6'\n\n    'DhcpSearchV4':\n      'type': 'object'\n      'properties':\n        'other_server':\n          '$ref': '#/components/schemas/DhcpSearchResultOtherServer'\n        'static_ip':\n          '$ref': '#/components/schemas/DhcpSearchResultStaticIP'\n\n    'DhcpSearchV6':\n      'type': 'object'\n      'properties':\n        'other_server':\n          '$ref': '#/components/schemas/DhcpSearchResultOtherServer'\n\n    'DhcpSearchResultOtherServer':\n      'type': 'object'\n      'properties':\n        'found':\n          'type': 'string'\n          'enum':\n          - 'yes'\n          - 'no'\n          - 'error'\n          'description': >\n            The result of searching the other DHCP server.\n          'example': 'no'\n        'error':\n          'type': 'string'\n          'description': 'Set if found=error'\n          'example': ''\n\n    'DhcpSearchResultStaticIP':\n      'type': 'object'\n      'properties':\n        'static':\n          'type': 'string'\n          'enum':\n          - 'yes'\n          - 'no'\n          - 'error'\n          'description': >\n            The result of determining static IP address.\n          'example': 'yes'\n        'ip':\n          'type': 'string'\n          'description': 'Set if static=no'\n          'example': ''\n\n    'DnsAnswer':\n      'type': 'object'\n      'description': 'DNS answer section'\n      'properties':\n        'ttl':\n          'example': 55\n          'format': 'uint32'\n          'type': 'integer'\n        'type':\n          'type': 'string'\n          'example': 'A'\n        'value':\n          'type': 'string'\n          'example': '217.69.139.201'\n    'DnsQuestion':\n      'type': 'object'\n      'description': 'DNS question section'\n      'properties':\n        'class':\n          'type': 'string'\n          'example': 'IN'\n        'name':\n          'type': 'string'\n          'example': 'xn--d1abbgf6aiiy.xn--p1ai'\n        'unicode_name':\n          'type': 'string'\n          'example': 'президент.рф'\n        'type':\n          'type': 'string'\n          'example': 'A'\n    'AddUrlRequest':\n      'type': 'object'\n      'description': '/add_url request data'\n      'properties':\n        'name':\n          'type': 'string'\n        'url':\n          'description': >\n            URL or an absolute path to the file containing filtering rules.\n          'type': 'string'\n          'example': 'https://filters.adtidy.org/windows/filters/15.txt'\n        'whitelist':\n          'type': 'boolean'\n    'RemoveUrlRequest':\n      'type': 'object'\n      'description': '/remove_url request data'\n      'properties':\n        'url':\n          'description': 'Previously added URL containing filtering rules'\n          'type': 'string'\n          'example': 'https://filters.adtidy.org/windows/filters/15.txt'\n        'whitelist':\n          'type': 'boolean'\n    'QueryLogItem':\n      'type': 'object'\n      'description': 'Query log item'\n      'properties':\n        'answer':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/DnsAnswer'\n        'original_answer':\n          'type': 'array'\n          'description': 'Answer from upstream server (optional)'\n          'items':\n            '$ref': '#/components/schemas/DnsAnswer'\n        'cached':\n          'type': 'boolean'\n          'description': >\n            Defines if the response has been served from cache.\n        'upstream':\n          'type': 'string'\n          'description': >\n            Upstream URL starting with tcp://, tls://, https://, or with an IP\n            address.\n        'answer_dnssec':\n          'description': >\n            If true, the response had the Authenticated Data (AD) flag set.\n          'type': 'boolean'\n        'client':\n          'description': >\n            The client's IP address.\n          'example': '192.168.0.1'\n          'type': 'string'\n        'client_id':\n          'description': >\n            The ClientID, if provided in DoH, DoQ, or DoT.\n          'example': 'cli123'\n          'type': 'string'\n        'client_info':\n          '$ref': '#/components/schemas/QueryLogItemClient'\n        'client_proto':\n          'enum':\n          - 'dot'\n          - 'doh'\n          - 'doq'\n          - 'dnscrypt'\n          - ''\n          'type': 'string'\n        'ecs':\n          'type': 'string'\n          'example': '192.168.0.0/16'\n          'description': >\n            The IP network defined by an EDNS Client-Subnet option in the\n            request message if any.\n        'elapsedMs':\n          'type': 'string'\n          'example': '54.023928'\n        'question':\n          '$ref': '#/components/schemas/DnsQuestion'\n        'filterId':\n          'deprecated': true\n          'type': 'integer'\n          'example': 123123\n          'description': >\n            In case if there's a rule applied to this DNS request, this is ID of\n            the filter list that the rule belongs to.\n\n            Deprecated: use `rules[*].filter_list_id` instead.\n        'rule':\n          'deprecated': true\n          'type': 'string'\n          'example': '||example.org^'\n          'description': >\n            Filtering rule applied to the request (if any).\n\n            Deprecated: use `rules[*].text` instead.\n        'rules':\n          'description': 'Applied rules.'\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/ResultRule'\n        'reason':\n          'type': 'string'\n          'description': 'Request filtering status.'\n          'enum':\n          - 'NotFilteredNotFound'\n          - 'NotFilteredWhiteList'\n          - 'NotFilteredError'\n          - 'FilteredBlackList'\n          - 'FilteredSafeBrowsing'\n          - 'FilteredParental'\n          - 'FilteredInvalid'\n          - 'FilteredSafeSearch'\n          - 'FilteredBlockedService'\n          - 'Rewrite'\n          - 'RewriteEtcHosts'\n          - 'RewriteRule'\n        'service_name':\n          'type': 'string'\n          'description': 'Set if reason=FilteredBlockedService'\n        'status':\n          'type': 'string'\n          'description': 'DNS response status'\n          'example': 'NOERROR'\n        'time':\n          'type': 'string'\n          'description': 'DNS request processing start time'\n          'example': '2018-11-26T00:02:41+03:00'\n    'QueryLogItemClient':\n      'description': >\n        Client information for a query log item.\n      'properties':\n        'disallowed':\n          'type': 'boolean'\n          'description': >\n            Whether the client's IP is blocked or not.\n        'disallowed_rule':\n          'type': 'string'\n          'description': >\n            The rule due to which the client is allowed or blocked.\n        'name':\n          'description': >\n            Persistent client's name or runtime client's hostname.  May be\n            empty.\n          'type': 'string'\n        'whois':\n          '$ref': '#/components/schemas/QueryLogItemClientWhois'\n      'required':\n      - 'disallowed'\n      - 'disallowed_rule'\n      - 'name'\n      - 'whois'\n      'type': 'object'\n    'QueryLogItemClientWhois':\n      'description': >\n        Client WHOIS information, if any.\n      'properties':\n        'city':\n          'description': >\n            City, if any.\n          'type': 'string'\n        'country':\n          'description': >\n            Country, if any.\n          'type': 'string'\n        'orgname':\n          'description': >\n            Organization name, if any.\n          'type': 'string'\n      'type': 'object'\n    'QueryLog':\n      'type': 'object'\n      'description': 'Query log'\n      'properties':\n        'oldest':\n          'type': 'string'\n          'example': '2018-11-26T00:02:41+03:00'\n        'data':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/QueryLogItem'\n    'QueryLogConfig':\n      'type': 'object'\n      'description': 'Query log configuration'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n          'description': 'Is query log enabled'\n        'interval':\n          'description': >\n            Time period for query log rotation.\n          'type': 'number'\n          'enum':\n          - 0.25\n          - 1\n          - 7\n          - 30\n          - 90\n        'anonymize_client_ip':\n          'type': 'boolean'\n          'description': \"Anonymize clients' IP addresses\"\n    'GetQueryLogConfigResponse':\n      'type': 'object'\n      'description': 'Query log configuration'\n      'required':\n      - 'enabled'\n      - 'interval'\n      - 'anonymize_client_ip'\n      - 'ignored'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n          'description': 'Is query log enabled'\n        'interval':\n          'description': >\n            Time period for query log rotation in milliseconds.\n          'type': 'number'\n        'anonymize_client_ip':\n          'type': 'boolean'\n          'description': \"Anonymize clients' IP addresses\"\n        'ignored':\n          'description': 'List of host names, which should not be written to log'\n          'type': 'array'\n          'items':\n            'type': 'string'\n        'ignored_enabled':\n          'type': 'boolean'\n          'description': >\n            If true, the host names in the `ignored` array are excluded from the\n            query log.\n    'PutQueryLogConfigUpdateRequest':\n      '$ref': '#/components/schemas/GetQueryLogConfigResponse'\n    'ResultRule':\n      'description': 'Applied rule.'\n      'properties':\n        'filter_list_id':\n          'description': >\n            In case if there's a rule applied to this DNS request, this is ID of\n            the filter list that the rule belongs to.\n          'example': 123123\n          'format': 'int64'\n          'type': 'integer'\n        'text':\n          'description': >\n            The text of the filtering rule applied to the request (if any).\n          'example': '||example.org^'\n          'type': 'string'\n      'type': 'object'\n    'TlsConfig':\n      'type': 'object'\n      'description': 'TLS configuration settings and status'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n          'example': true\n          'description': 'enabled is the encryption (DoT/DoH/HTTPS) status'\n        'server_name':\n          'type': 'string'\n          'example': 'example.org'\n          'description': 'server_name is the hostname of your HTTPS/TLS server'\n        'force_https':\n          'type': 'boolean'\n          'example': true\n          'description': 'if true, forces HTTP->HTTPS redirect'\n        'port_https':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 443\n          'description': 'HTTPS port. If 0, HTTPS will be disabled.'\n        'port_dns_over_tls':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 853\n          'description': 'DNS-over-TLS port. If 0, DoT will be disabled.'\n        'port_dns_over_quic':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 784\n          'description': 'DNS-over-QUIC port. If 0, DoQ will be disabled.'\n        'certificate_chain':\n          'type': 'string'\n          'description': 'Base64 string with PEM-encoded certificates chain'\n        'private_key':\n          'type': 'string'\n          'description': 'Base64 string with PEM-encoded private key'\n        'private_key_saved':\n          'type': 'boolean'\n          'example': true\n          'description': >\n            Set to true if the user has previously saved a private key as\n            a string.  This is used so that the server and the client don't\n            have to send the private key between each other every time,\n            which might lead to security issues.\n        'certificate_path':\n          'type': 'string'\n          'description': 'Path to certificate file'\n        'private_key_path':\n          'type': 'string'\n          'description': 'Path to private key file'\n        'valid_cert':\n          'type': 'boolean'\n          'example': true\n          'description': >\n            Set to true if the specified certificates chain is a valid chain of\n            X509 certificates.\n        'valid_chain':\n          'type': 'boolean'\n          'example': true\n          'description': >\n            Set to true if the specified certificates chain is verified and\n            issued by a known CA.\n        'subject':\n          'type': 'string'\n          'example': 'CN=example.org'\n          'description': 'The subject of the first certificate in the chain.'\n        'issuer':\n          'type': 'string'\n          'example': \"CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US\"\n          'description': 'The issuer of the first certificate in the chain.'\n        'not_before':\n          'type': 'string'\n          'example': '2019-01-31T10:47:32Z'\n          'description': >\n            The NotBefore field of the first certificate in the chain.\n        'not_after':\n          'type': 'string'\n          'example': '2019-05-01T10:47:32Z'\n          'description': >\n            The NotAfter field of the first certificate in the chain.\n        'dns_names':\n          'type': 'array'\n          'items':\n            'type': 'string'\n          'description': >\n            The value of SubjectAltNames field of the first certificate in the\n            chain.\n          'example':\n          - '*.example.org'\n        'valid_key':\n          'type': 'boolean'\n          'example': true\n          'description': 'Set to true if the key is a valid private key.'\n        'key_type':\n          'type': 'string'\n          'example': 'RSA'\n          'enum':\n          - 'RSA'\n          - 'ECDSA'\n          'description': 'Key type.'\n        'warning_validation':\n          'type': 'string'\n          'example': 'You have specified an empty certificate'\n          'description': >\n            A validation warning message with the issue description.\n        'valid_pair':\n          'type': 'boolean'\n          'example': true\n          'description': >\n            Set to true if both certificate and private key are correct.\n        'serve_plain_dns':\n          'type': 'boolean'\n          'example': true\n          'description': >\n            Set to true if plain DNS is allowed for incoming requests.\n    'NetInterface':\n      'type': 'object'\n      'description': 'Network interface info'\n      'required':\n      - 'flags'\n      - 'gateway_ip'\n      - 'hardware_address'\n      - 'ipv4_addresses'\n      - 'ipv6_addresses'\n      - 'name'\n      'properties':\n        'flags':\n          'type': 'string'\n          'description': >\n            Flags could be any combination of the following values, divided by\n            the \"|\" character: \"up\", \"broadcast\", \"loopback\", \"pointtopoint\" and\n            \"multicast\".\n          'example': 'up|broadcast|multicast'\n        'gateway_ip':\n          'type': 'string'\n          'description': 'The IP address of the gateway.'\n          'example': '192.0.2.0'\n        'hardware_address':\n          'type': 'string'\n          'example': '52:54:00:11:09:ba'\n        'ipv4_addresses':\n          'type': 'array'\n          'description': >\n            The addresses of the interface of v4 family.\n          'items':\n            'type': 'string'\n        'ipv6_addresses':\n          'type': 'array'\n          'description': >\n            The addresses of the interface of v6 family.\n          'items':\n            'type': 'string'\n        'name':\n          'type': 'string'\n          'example': 'eth0'\n    'AddressInfo':\n      'type': 'object'\n      'description': 'Port information'\n      'required':\n      - 'ip'\n      - 'port'\n      'properties':\n        'ip':\n          'type': 'string'\n          'example': '127.0.0.1'\n        'port':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 53\n    'AddressesInfo':\n      'type': 'object'\n      'description': 'AdGuard Home addresses configuration'\n      'required':\n      - 'dns_port'\n      - 'interfaces'\n      - 'version'\n      - 'web_port'\n      'properties':\n        'dns_port':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 53\n        'interfaces':\n          '$ref': '#/components/schemas/NetInterfaces'\n        'version':\n          'type': 'string'\n          'example': 'v0.123.4'\n        'web_port':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 80\n    'SetProtectionRequest':\n      'type': 'object'\n      'description': 'Protection state configuration'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n        'duration':\n          'type': 'integer'\n          'format': 'uint64'\n          'description': 'Duration of a pause, in milliseconds.  Enabled should be false.'\n      'required':\n        - 'enabled'\n    'ProfileInfo':\n      'type': 'object'\n      'description': 'Information about the current user'\n      'properties':\n        'name':\n          'type': 'string'\n        'language':\n          'type': 'string'\n        'theme':\n          'type': 'string'\n          'description': 'Interface theme'\n          'enum':\n            - 'auto'\n            - 'dark'\n            - 'light'\n      'required':\n        - 'name'\n        - 'language'\n        - 'theme'\n    'SafeSearchConfig':\n      'type': 'object'\n      'description': 'Safe search settings.'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n        'bing':\n          'type': 'boolean'\n        'duckduckgo':\n          'type': 'boolean'\n        'ecosia':\n          'type': 'boolean'\n        'google':\n          'type': 'boolean'\n        'pixabay':\n          'type': 'boolean'\n        'yandex':\n          'type': 'boolean'\n        'youtube':\n          'type': 'boolean'\n    'Schedule':\n      'type': 'object'\n      'description': >\n        Sets periods of inactivity for filtering blocked services.  The\n        schedule contains 7 days (Sunday to Saturday) and a time zone.\n      'properties':\n        'time_zone':\n          'description': >\n            Time zone name according to IANA time zone database.  For example\n            `Europe/Brussels`.  `Local` represents the system's local time\n            zone.\n          'type': 'string'\n        'sun':\n          '$ref': '#/components/schemas/DayRange'\n        'mon':\n          '$ref': '#/components/schemas/DayRange'\n        'tue':\n          '$ref': '#/components/schemas/DayRange'\n        'wed':\n          '$ref': '#/components/schemas/DayRange'\n        'thu':\n          '$ref': '#/components/schemas/DayRange'\n        'fri':\n          '$ref': '#/components/schemas/DayRange'\n        'sat':\n          '$ref': '#/components/schemas/DayRange'\n    'DayRange':\n      'type': 'object'\n      'description': >\n        The single interval within a day.  It begins at the `start` and ends\n        before the `end`.\n      'properties':\n        'start':\n          'type': 'number'\n          'description': >\n            The number of milliseconds elapsed from the start of a day.  It\n            must be less than `end` and is expected to be rounded to minutes.\n            So the maximum value is `86340000` (23 hours and 59 minutes).\n          'minimum': 0\n          'maximum': 86340000\n        'end':\n          'type': 'number'\n          'description': >\n            The number of milliseconds elapsed from the start of a day.  It is\n            expected to be rounded to minutes.  The maximum value is `86400000`\n            (24 hours).\n          'minimum': 0\n          'maximum': 86400000\n    'Client':\n      'type': 'object'\n      'description': 'Client information.'\n      'properties':\n        'name':\n          'type': 'string'\n          'description': 'Name'\n          'example': 'localhost'\n        'ids':\n          'type': 'array'\n          'description': 'IP, CIDR, MAC, or ClientID.'\n          'items':\n            'type': 'string'\n        'use_global_settings':\n          'type': 'boolean'\n        'filtering_enabled':\n          'type': 'boolean'\n        'parental_enabled':\n          'type': 'boolean'\n        'safebrowsing_enabled':\n          'type': 'boolean'\n        'safesearch_enabled':\n          'deprecated': true\n          'type': 'boolean'\n        'safe_search':\n          '$ref': '#/components/schemas/SafeSearchConfig'\n        'use_global_blocked_services':\n          'type': 'boolean'\n        'blocked_services_schedule':\n          '$ref': '#/components/schemas/Schedule'\n        'blocked_services':\n          'type': 'array'\n          'items':\n            'type': 'string'\n        'upstreams':\n          'type': 'array'\n          'items':\n            'type': 'string'\n        'tags':\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'ignore_querylog':\n          'description': |\n            NOTE: If `ignore_querylog` is not set in HTTP API `GET /clients/add`\n            request then default value (false) will be used.\n\n            If `ignore_querylog` is not set in HTTP API `GET /clients/update`\n            request then the existing value will not be changed.\n\n            This behaviour can be changed in the future versions.\n          'type': 'boolean'\n        'ignore_statistics':\n          'description': |\n            NOTE: If `ignore_statistics` is not set in HTTP API `GET\n            /clients/add` request then default value (false) will be used.\n\n            If `ignore_statistics` is not set in HTTP API `GET /clients/update`\n            request then the existing value will not be changed.\n\n            This behaviour can be changed in the future versions.\n          'type': 'boolean'\n        'upstreams_cache_enabled':\n          'description': |\n            NOTE: If `upstreams_cache_enabled` is not set in HTTP API\n            `GET /clients/add` request then default value (false) will be used.\n\n            If `upstreams_cache_enabled` is not set in HTTP API\n            `GET /clients/update` request then the existing value will not be\n            changed.\n\n            This behaviour can be changed in the future versions.\n          'type': 'boolean'\n        'upstreams_cache_size':\n          'description': |\n            NOTE: If `upstreams_cache_enabled` is not set in HTTP API\n            `GET /clients/update` request then the existing value will not be\n            changed.\n\n            This behaviour can be changed in the future versions.\n          'type': 'integer'\n    'ClientAuto':\n      'type': 'object'\n      'description': 'Auto-Client information'\n      'properties':\n        'ip':\n          'type': 'string'\n          'description': 'IP address'\n          'example': '127.0.0.1'\n        'name':\n          'type': 'string'\n          'description': 'Name'\n          'example': 'localhost'\n        'source':\n          'type': 'string'\n          'description': 'The source of this information'\n          'example': 'etc/hosts'\n        'whois_info':\n          '$ref': '#/components/schemas/WhoisInfo'\n    'ClientUpdate':\n      'type': 'object'\n      'description': 'Client update request'\n      'properties':\n        'name':\n          'type': 'string'\n        'data':\n          '$ref': '#/components/schemas/Client'\n    'ClientDelete':\n      'type': 'object'\n      'description': 'Client delete request'\n      'properties':\n        'name':\n          'type': 'string'\n    'ClientsSearchRequest':\n      'type': 'object'\n      'description': 'Client search request'\n      'properties':\n        'clients':\n          'type': 'array'\n          'items':\n            '$ref': '#/components/schemas/ClientsSearchRequestItem'\n    'ClientsSearchRequestItem':\n      'type': 'object'\n      'properties':\n        'id':\n          'type': 'string'\n          'description': 'Client IP address, CIDR, MAC address, or ClientID'\n    'ClientsFindResponse':\n      'description': 'Client search results.'\n      'items':\n        '$ref': '#/components/schemas/ClientsFindEntry'\n      'example':\n      - 'cli42':\n          'name': 'Client 42'\n          'ids': ['cli42']\n          'use_global_settings': true\n          'filtering_enabled': true\n          'parental_enabled': true\n          'safebrowsing_enabled': true\n          'safesearch_enabled': true\n          'safe_search': {}\n          'use_global_blocked_services': true\n          'blocked_services': []\n          'upstreams': []\n          'whois_info': {}\n          'disallowed': false\n          'disallowed_rule': ''\n          'ignore_querylog': false\n          'ignore_statistics': false\n      - '1.2.3.4':\n          'name': 'Client 1-2-3-4'\n          'ids': ['1.2.3.4']\n          'use_global_settings': true\n          'filtering_enabled': true\n          'parental_enabled': true\n          'safebrowsing_enabled': true\n          'safesearch_enabled': true\n          'safe_search': {}\n          'use_global_blocked_services': true\n          'blocked_services': []\n          'upstreams': []\n          'whois_info': {}\n          'disallowed': false\n          'disallowed_rule': ''\n          'ignore_querylog': false\n          'ignore_statistics': false\n      'type': 'array'\n    'AccessListResponse':\n      '$ref': '#/components/schemas/AccessList'\n    'AccessSetRequest':\n      '$ref': '#/components/schemas/AccessList'\n    'AccessList':\n      'description': >\n        Client and host access list.  Each of the lists should contain only\n        unique elements.  In addition, allowed and disallowed lists cannot\n        contain the same elements.\n      'properties':\n        'allowed_clients':\n          'description': >\n            The allowlist of clients: IP addresses, CIDRs, or ClientIDs.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'disallowed_clients':\n          'description': >\n            The blocklist of clients: IP addresses, CIDRs, or ClientIDs.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'blocked_hosts':\n          'description': 'The blocklist of hosts.'\n          'items':\n            'type': 'string'\n          'type': 'array'\n      'type': 'object'\n    'ClientsFindEntry':\n      'type': 'object'\n      'additionalProperties':\n        '$ref': '#/components/schemas/ClientFindSubEntry'\n    'ClientFindSubEntry':\n      'type': 'object'\n      'description': 'Client information.'\n      'properties':\n        'name':\n          'type': 'string'\n          'description': 'Name'\n          'example': 'localhost'\n        'ids':\n          'type': 'array'\n          'description': 'IP, CIDR, MAC, or ClientID.'\n          'items':\n            'type': 'string'\n        'use_global_settings':\n          'type': 'boolean'\n        'filtering_enabled':\n          'type': 'boolean'\n        'parental_enabled':\n          'type': 'boolean'\n        'safebrowsing_enabled':\n          'type': 'boolean'\n        'safesearch_enabled':\n          'deprecated': true\n          'type': 'boolean'\n        'safe_search':\n          '$ref': '#/components/schemas/SafeSearchConfig'\n        'use_global_blocked_services':\n          'type': 'boolean'\n        'blocked_services':\n          'type': 'array'\n          'items':\n            'type': 'string'\n        'upstreams':\n          'type': 'array'\n          'items':\n            'type': 'string'\n        'whois_info':\n          '$ref': '#/components/schemas/WhoisInfo'\n        'disallowed':\n          'type': 'boolean'\n          'description': >\n            Whether the client's IP is blocked or not.\n        'disallowed_rule':\n          'type': 'string'\n          'description': >\n            The rule due to which the client is disallowed.  If disallowed is\n            set to true, and this string is empty, then the client IP is\n            disallowed by the \"allowed IP list\", that is it is not included in\n            the allowed list.\n        'ignore_querylog':\n          'type': 'boolean'\n        'ignore_statistics':\n          'type': 'boolean'\n    'WhoisInfo':\n      'type': 'object'\n      'additionalProperties':\n        'type': 'string'\n\n    'Clients':\n      'type': 'object'\n      'properties':\n        'clients':\n          '$ref': '#/components/schemas/ClientsArray'\n        'auto_clients':\n          '$ref': '#/components/schemas/ClientsAutoArray'\n        'supported_tags':\n          'items':\n            'type': 'string'\n          'type': 'array'\n    'ClientsArray':\n      'type': 'array'\n      'items':\n        '$ref': '#/components/schemas/Client'\n      'description': 'Clients array'\n    'ClientsAutoArray':\n      'type': 'array'\n      'items':\n        '$ref': '#/components/schemas/ClientAuto'\n      'description': 'Auto-Clients array'\n    'RewriteList':\n      'type': 'array'\n      'items':\n        '$ref': '#/components/schemas/RewriteEntry'\n      'description': 'Rewrite rules array'\n    'RewriteUpdate':\n      'type': 'object'\n      'description': 'Rewrite rule update object'\n      'properties':\n        'target':\n          '$ref': '#/components/schemas/RewriteEntry'\n        'update':\n          '$ref': '#/components/schemas/RewriteEntry'\n    'RewriteEntry':\n      'type': 'object'\n      'description': 'Rewrite rule'\n      'properties':\n        'domain':\n          'type': 'string'\n          'description': 'Domain name'\n          'example': 'example.org'\n        'answer':\n          'type': 'string'\n          'description': 'value of A, AAAA or CNAME DNS record'\n          'example': '127.0.0.1'\n        'enabled':\n          'type': 'boolean'\n          'description': >\n            Optional. If omitted on add, defaults to `true`. On update, omitted\n            preserves previous value.\n          'example': true\n          'default': true\n    'RewriteSettings':\n      'type': 'object'\n      'description': 'DNS rewrite settings'\n      'required':\n      - 'enabled'\n      'properties':\n        'enabled':\n          'type': 'boolean'\n          'description': 'indicates whether rewrites are applied'\n          'example': true\n    'BlockedServicesArray':\n      'type': 'array'\n      'items':\n        'type': 'string'\n    'BlockedServicesAll':\n      'properties':\n        'blocked_services':\n          'items':\n            '$ref': '#/components/schemas/BlockedService'\n          'type': 'array'\n        'groups':\n          'items':\n            '$ref': '#/components/schemas/ServiceGroup'\n      'required':\n      - 'blocked_services'\n      - 'groups'\n      'type': 'object'\n    'BlockedService':\n      'properties':\n        'icon_svg':\n          'description': >\n            The SVG icon as a Base64-encoded string to make it easier to embed\n            it into a data URL.\n          'type': 'string'\n        'id':\n          'description': >\n            The ID of this service.\n          'type': 'string'\n        'name':\n          'description': >\n            The human-readable name of this service.\n          'type': 'string'\n        'rules':\n          'description': >\n            The array of the filtering rules.\n          'items':\n            'type': 'string'\n          'type': 'array'\n        'group_id':\n          'description': >\n            The ID of the group, that the service belongs to.\n          'type': 'string'\n      'required':\n      - 'icon_svg'\n      - 'id'\n      - 'name'\n      - 'rules'\n      'type': 'object'\n    'ServiceGroup':\n      'properties':\n        'id':\n          'description': >\n            The ID of this group.\n          'type': 'string'\n      'required':\n      -  'id'\n      'type': 'object'\n    'BlockedServicesSchedule':\n      'type': 'object'\n      'properties':\n        'schedule':\n          '$ref': '#/components/schemas/Schedule'\n        'ids':\n          'description': >\n            The names of the blocked services.\n          'type': 'array'\n          'items':\n            'type': 'string'\n    'CheckConfigRequest':\n      'type': 'object'\n      'description': 'Configuration to be checked'\n      'properties':\n        'dns':\n          '$ref': '#/components/schemas/CheckConfigRequestInfo'\n        'web':\n          '$ref': '#/components/schemas/CheckConfigRequestInfo'\n        'set_static_ip':\n          'type': 'boolean'\n          'example': false\n    'CheckConfigRequestInfo':\n      'type': 'object'\n      'properties':\n        'ip':\n          'type': 'string'\n          'example': '127.0.0.1'\n        'port':\n          'type': 'integer'\n          'format': 'uint16'\n          'example': 53\n        'autofix':\n          'type': 'boolean'\n          'example': false\n    'CheckConfigResponse':\n      'type': 'object'\n      'required':\n      - 'dns'\n      - 'web'\n      - 'static_ip'\n      'properties':\n        'dns':\n          '$ref': '#/components/schemas/CheckConfigResponseInfo'\n        'web':\n          '$ref': '#/components/schemas/CheckConfigResponseInfo'\n        'static_ip':\n          '$ref': '#/components/schemas/CheckConfigStaticIpInfo'\n    'CheckConfigResponseInfo':\n      'type': 'object'\n      'required':\n      - 'status'\n      - 'can_autofix'\n      'properties':\n        'status':\n          'type': 'string'\n          'default': ''\n        'can_autofix':\n          'type': 'boolean'\n          'example': false\n    'CheckConfigStaticIpInfoStatic':\n      'type': 'string'\n      'example': 'no'\n      'enum':\n      - 'yes'\n      - 'no'\n      - 'error'\n      'description': 'Can be: yes, no, error'\n    'CheckConfigStaticIpInfo':\n      'type': 'object'\n      'properties':\n        'static':\n          '$ref': '#/components/schemas/CheckConfigStaticIpInfoStatic'\n        'ip':\n          'type': 'string'\n          'default': ''\n          'example': '192.168.1.1'\n          'description': 'Current dynamic IP address. Set if static=no'\n        'error':\n          'type': 'string'\n          'default': ''\n          'description': 'Error text. Set if static=error'\n    'InitialConfiguration':\n      'type': 'object'\n      'description': >\n        AdGuard Home initial configuration for the first-install wizard.\n      'required':\n      - 'dns'\n      - 'web'\n      - 'username'\n      - 'password'\n      'properties':\n        'dns':\n          '$ref': '#/components/schemas/AddressInfo'\n        'web':\n          '$ref': '#/components/schemas/AddressInfo'\n        'username':\n          'type': 'string'\n          'description': 'Basic auth username'\n          'example': 'admin'\n        'password':\n          'type': 'string'\n          'description': 'Basic auth password'\n          'example': 'password'\n    'Login':\n      'type': 'object'\n      'description': 'Login request data'\n      'properties':\n        'name':\n          'type': 'string'\n          'description': 'User name'\n        'password':\n          'type': 'string'\n          'description': 'Password'\n    'Error':\n      'description': 'A generic JSON error response.'\n      'properties':\n        'message':\n          'description': 'The error message, an opaque string.'\n          'type': 'string'\n      'type': 'object'\n    'LanguageSettings':\n      'description': 'Language settings object.'\n      'properties':\n        'language':\n          'description': 'The current language or the language to set.'\n          'type': 'string'\n      'required':\n      - 'language'\n      'type': 'object'\n  'securitySchemes':\n    'basicAuth':\n      'type': 'http'\n      'scheme': 'basic'\n"
  },
  {
    "path": "scripts/README.md",
    "content": "# AdGuard Home scripts\n\n## `hooks/`: Git hooks\n\n### Usage\n\nRun `make init` from the project root.\n\n## `make/`: Makefile scripts\n\nThe release channels are: `development` (the default), `edge`, `beta`, and `release`. If verbosity levels aren’t documented here, there are only two: `0`, don’t print anything, and `1`, be verbose.\n\n### `build-docker.sh`: Build a multi-architecture Docker image\n\nRequired environment:\n\n- `CHANNEL`: release channel, see above.\n\n- `DIST_DIR`: the directory where a release has previously been built.\n\n- `REVISION`: current Git revision.\n\n- `VERSION`: release version.\n\nOptional environment:\n\n- `DOCKER_IMAGE_NAME`: the name of the resulting Docker container. By default it’s `adguardhome-dev`.\n\n- `DOCKER_PUSH`: `1` to push the image to DockerHub, `0` to not push. By default it’s `0`.\n\n- `SUDO`: allow users to use `sudo` or `doas` with `docker`. By default none is used.\n\n### `build-release.sh`: Build a release for all platforms\n\nRequired environment:\n\n- `CHANNEL`: release channel, see above.\n\n- `GPG_KEY` and `GPG_KEY_PASSPHRASE`: data for `gpg`. Only required if `SIGN` is `1`.\n\nOptional environment:\n\n- `ARCH` and `OS`: space-separated list of architectures and operating systems for which to build a release. For example, to build only for 64-bit ARM and AMD on Linux and Darwin:\n\n    ```sh\n    make ARCH='amd64 arm64' OS='darwin linux' … build-release\n    ```\n\n    The default value is `''`, which means build everything.\n\n- `DIST_DIR`: the directory to build a release into. The default value is `dist`.\n\n- `GO`: set an alternative name for the Go compiler.\n\n- `SIGN`: `0` to not sign the resulting packages, `1` to sign. The default value is `1`.\n\n- `VERBOSE`: `1` to be verbose, `2` to also print environment. This script calls `go-build.sh` with the verbosity level one level lower, so to get verbosity level `2` in `go-build.sh`, set this to `3` when calling `build-release.sh`.\n\n- `VERSION`: release version. Will be set by `version.sh` if it is unset or if it has the default `Makefile` value of `v0.0.0`.\n\nWe’re using Go’s [forward compatibility mechanism][go-toolchain] for updating the Go version. This means that if your `go` version is 1.21+ but is different from the one required by AdGuard Home, the `go` tool will automatically download the required version.\n\nIf you want to use the version installed on your builder, run:\n\n```sh\ngo get go@$YOUR_VERSION\ngo mod tidy\n```\n\nand call `make` with `GOTOOLCHAIN=local`.\n\n[go-toolchain]: https://go.dev/blog/toolchain\n\n### `go-bench.sh`: Run backend benchmarks\n\nOptional environment:\n\n- `GO`: set an alternative name for the Go compiler.\n\n- `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is `--timeout=30s`.\n\n- `VERBOSE`: verbosity level. `1` shows every command that is run and every Go package that is processed. `2` also shows subcommands and environment. The default value is `0`, don’t be verbose.\n\n### `go-build.sh`: Build the backend\n\nOptional environment:\n\n- `GOAMD64`: architectural level for [AMD64][amd64]. The default value is `v1`.\n\n- `GOARM`: ARM processor options for the Go compiler.\n\n- `GOMIPS`: ARM processor options for the Go compiler.\n\n- `GO`: set an alternative name for the Go compiler.\n\n- `OUT`: output binary name.\n\n- `PARALLELISM`: set the maximum number of concurrently run build commands (that is, compiler, linker, etc.).\n\n- `SOURCE_DATE_EPOCH`: the [standardized][repr] environment variable for the Unix epoch time of the latest commit in the repository. If set, overrides the default obtained from Git. Useful for reproducible builds.\n\n- `VERBOSE`: verbosity level. `1` shows every command that is run and every Go package that is processed. `2` also shows subcommands and environment. The default value is `0`, don’t be verbose.\n\n- `VERSION`: release version. Will be set by `version.sh` if it is unset or if it has the default `Makefile` value of `v0.0.0`.\n\nRequired environment:\n\n- `CHANNEL`: release channel, see above.\n\n[amd64]: https://github.com/golang/go/wiki/MinimumRequirements#amd64\n[repr]:  https://reproducible-builds.org/docs/source-date-epoch/\n\n### `go-deps.sh`: Install backend dependencies\n\nOptional environment:\n\n- `GO`: set an alternative name for the Go compiler.\n\n- `VERBOSE`: verbosity level. `1` shows every command that is run and every Go package that is processed. `2` also shows subcommands and environment. The default value is `0`, don’t be verbose.\n\n### `go-fuzz.sh`: Run backend fuzz tests\n\nOptional environment:\n\n- `GO`: set an alternative name for the Go compiler.\n\n- `FUZZTIME_FLAGS`: set fuss flags for tests. The default value is `--fuzztime=20s`.\n\n- `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is `--timeout=30s`.\n\n- `VERBOSE`: verbosity level. `1` shows every command that is run and every Go package that is processed. `2` also shows subcommands and environment. The default value is `0`, don’t be verbose.\n\n### `go-lint.sh`: Run backend static analyzers\n\nOptional environment:\n\n- `EXIT_ON_ERROR`: if set to `0`, don’t exit the script after the first encountered error. The default value is `1`.\n\n- `GO`: set an alternative name for the Go compiler.\n\n- `VERBOSE`: verbosity level. `1` shows every command that is run. `2` also shows subcommands. The default value is `0`, don’t be verbose.\n\n### `go-test.sh`: Run backend tests\n\nOptional environment:\n\n- `GO`: set an alternative name for the Go compiler.\n\n- `RACE`: set to `0` to not use the Go race detector. The default value is `1`, use the race detector.\n\n- `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is `--timeout=30s`.\n\n- `VERBOSE`: verbosity level. `1` shows every command that is run and every Go package that is processed. `2` also shows subcommands. The default value is `0`, don’t be verbose.\n\n### `version.sh`: Generate And Print The Current Version\n\nRequired environment:\n\n- `CHANNEL`: release channel, see above.\n\n## `snap/`: Snapcraft scripts\n\n### `build.sh`\n\nBuilds the Snapcraft packages from the binaries created by `download.sh`.\n\nOptional environment:\n\n- `SNAPCRAFT_CMD`: Overrides the Snapcraft command. Default: `snapcraft`.\n\n### `download.sh`\n\nDownloads the binaries to pack them into Snapcraft packages.\n\nRequired environment:\n\n- `CHANNEL`: release channel, see above.\n\n### `upload.sh`\n\nUploads the Snapcraft packages created by `build.sh`.\n\nRequired environment:\n\n- `SNAPCRAFT_CHANNEL`: Snapcraft release channel: `edge`, `beta`, or `candidate`.\n\n- `SNAPCRAFT_STORE_CREDENTIALS`: Credentials for Snapcraft store.\n\nOptional environment:\n\n- `SNAPCRAFT_CMD`: Overrides the Snapcraft command. Default: `snapcraft`.\n\n## `translations/`: Twosky Integration Script\n\n### Usage\n\n- `go run ./scripts/translations help`: print usage.\n\n- `go run ./scripts/translations download [-n <count>]`: download and save all translations. `n` is optional flag where count is a number of concurrent downloads. Note, that it downloads locales for all configurations in the `.twosky.json` file.\n\n- `go run ./scripts/translations upload`: upload the base `en` locale.\n\n- `go run ./scripts/translations summary`: show the current locales summary.\n\n- `go run ./scripts/translations unused`: show the list of unused strings.\n\n- `go run ./scripts/translations auto-add`: add locales with additions to the git and restore locales with deletions.\n\nAfter the download you’ll find the output locales in the `client/src/__locales/` directory.\n\nOptional environment:\n\n- `DOWNLOAD_LANGUAGES`: set a list of specific languages to `download`. For example `ar be bg`. If it set to `blocker` then script will download only those languages, which need to be fully translated (`de en es fr it ja ko pt-br pt-pt ru zh-cn zh-tw`).\n\n- `UPLOAD_LANGUAGE`: set an alternative language for `upload`.\n\n- `TWOSKY_URI`: set an alternative URL for `download` or `upload`.\n\n- `TWOSKY_PROJECT_ID`: set an alternative project ID for `download` or `upload`.\n\n    Deprectated: This environment variable should not be used since the script began supporting multiple configurations.\n\n## `companiesdb/`: Whotracks.me database converter\n\nA simple script that downloads and updates the companies DB in the `client` code from [the repo][companiesrepo].\n\n### Usage\n\n```sh\nsh ./scripts/companiesdb/download.sh\n```\n\n[companiesrepo]: https://github.com/AdguardTeam/companiesdb\n\n## `blocked-services/`: Blocked-services updater\n\nA simple script that downloads and updates the blocked services index from AdGuard’s [Hostlists Registry][reg].\n\nOptional environment:\n\n- `URL`: the URL of the index file. By default it’s `https://adguardteam.github.io/HostlistsRegistry/assets/services.json`.\n\n### Usage\n\n```sh\ngo run ./scripts/blocked-services/main.go\n```\n\n[reg]: https://github.com/AdguardTeam/HostlistsRegistry\n\n## `vetted-filters/`: Vetted-filters updater\n\nSimilar to the one above, a script that downloads and updates the vetted filtering list data from AdGuard’s [Hostlists Registry][reg].\n\nOptional environment:\n\n- `URL`: the URL of the index file. By default it’s `https://adguardteam.github.io/HostlistsRegistry/assets/filters.json`.\n\n### Usage\n\n```sh\ngo run ./scripts/vetted-filters/main.go\n```\n"
  },
  {
    "path": "scripts/blocked-services/main.go",
    "content": "// blocked services fetches the most recent Hostlists Registry blocked service\n// index and transforms the filters from it to AdGuard Home's data and code\n// formats.\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\tl := slogutil.New(nil)\n\n\turlStr := \"https://adguardteam.github.io/HostlistsRegistry/assets/services.json\"\n\tif v, ok := os.LookupEnv(\"URL\"); ok {\n\t\turlStr = v\n\t}\n\n\t// Validate the URL.\n\t_, err := url.Parse(urlStr)\n\terrors.Check(err)\n\n\tc := &http.Client{\n\t\tTimeout: 10 * time.Second,\n\t}\n\n\tresp := errors.Must(c.Get(urlStr))\n\tdefer slogutil.CloseAndLog(ctx, l, resp.Body, slog.LevelError)\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tpanic(fmt.Errorf(\"expected code %d, got %d\", http.StatusOK, resp.StatusCode))\n\t}\n\n\thlSvcs := &hlServices{}\n\terr = json.NewDecoder(resp.Body).Decode(hlSvcs)\n\terrors.Check(err)\n\n\t// Sort all services and rules to make the output more predictable.\n\tslices.SortStableFunc(hlSvcs.BlockedServices, func(a, b *hlServicesService) (res int) {\n\t\treturn strings.Compare(a.ID, b.ID)\n\t})\n\tfor _, s := range hlSvcs.BlockedServices {\n\t\tslices.Sort(s.Rules)\n\t}\n\n\t// Use another set of delimiters to prevent them interfering with the Go\n\t// code.\n\ttmpl, err := template.New(\"main\").Delims(\"<%\", \"%>\").Funcs(template.FuncMap{\n\t\t\"isnotlast\": func(idx, sliceLen int) (ok bool) {\n\t\t\treturn idx != sliceLen-1\n\t\t},\n\t}).Parse(tmplStr)\n\terrors.Check(err)\n\n\tf := errors.Must(os.OpenFile(\n\t\t\"./internal/filtering/servicelist.go\",\n\t\tos.O_CREATE|os.O_TRUNC|os.O_WRONLY,\n\t\t0o644,\n\t))\n\tdefer slogutil.CloseAndLog(ctx, l, f, slog.LevelError)\n\n\terrors.Check(tmpl.Execute(f, hlSvcs))\n}\n\n// tmplStr is the template for the Go source file with the services.\nconst tmplStr = `// Code generated by go run ./scripts/blocked-services/main.go; DO NOT EDIT.\n\npackage filtering\n\n// blockedService represents a single blocked service.\ntype blockedService struct {\n\tID      string   ` + \"`\" + `json:\"id\"` + \"`\" + `\n\tName    string   ` + \"`\" + `json:\"name\"` + \"`\" + `\n\tIconSVG []byte   ` + \"`\" + `json:\"icon_svg\"` + \"`\" + `\n\tRules   []string ` + \"`\" + `json:\"rules\"` + \"`\" + `\n\tGroupID string   ` + \"`\" + `json:\"group_id\"` + \"`\" + `\n}\n\n// serviceGroup represents single group of services.\ntype serviceGroup struct {\n\tID string ` + \"`\" + `json:\"id\"` + \"`\" + `\n}\n\n// blockedServices contains raw blocked service data.\nvar blockedServices = []blockedService{<% $l := len .BlockedServices %>\n\t<%- range $i, $s := .BlockedServices %>{\n\tID:      <% printf \"%q\" $s.ID %>,\n\tName:    <% printf \"%q\" $s.Name %>,\n\tIconSVG: []byte(<% printf \"%q\" $s.IconSVG %>),\n\tRules: []string{<% range $s.Rules %>\n\t\t<% printf \"%q\" . %>,<% end %>\n\t},\n\tGroupID: <% printf \"%q\" $s.Group %>,\n}<% if isnotlast $i $l %>, <% end %><% end %>}\n\n// serviceGroups contains raw service group data.\nvar serviceGroups = []serviceGroup{<% $l := len .ServiceGroups %>\n\t<%- range $i, $s := .ServiceGroups %>{\n\tID: <% printf \"%q\" $s.ID %>,\n}<% if isnotlast $i $l %>, <% end %><% end %>}\n`\n\n// hlServices is the JSON structure for the Hostlists Registry blocked service\n// index.\ntype hlServices struct {\n\tBlockedServices []*hlServicesService `json:\"blocked_services\"`\n\tServiceGroups   []*hlServicesGroup   `json:\"groups\"`\n}\n\n// hlServicesService is the JSON structure for a service in the Hostlists\n// Registry.\ntype hlServicesService struct {\n\tID      string   `json:\"id\"`\n\tName    string   `json:\"name\"`\n\tIconSVG string   `json:\"icon_svg\"`\n\tRules   []string `json:\"rules\"`\n\tGroup   string   `json:\"group\"`\n}\n\n// hlServicesGroup is the JSON structure for a service group in the Hostlists\n// Registry.\ntype hlServicesGroup struct {\n\tID string `json:\"id\"`\n}\n"
  },
  {
    "path": "scripts/companiesdb/download.sh",
    "content": "#!/bin/sh\n\nset -e -f -u -x\n\n# This script syncs companies DB that we bundle with AdGuard Home.  The source\n# for this database is https://github.com/AdguardTeam/companiesdb.\ntrackers_url='https://raw.githubusercontent.com/AdguardTeam/companiesdb/main/dist/trackers.json'\noutput='./client/src/helpers/trackers/trackers.json'\nreadonly trackers_url output\n\ncurl -v \"$trackers_url\" | jq . >\"$output\"\n"
  },
  {
    "path": "scripts/hooks/helper.sh",
    "content": "#!/bin/sh\n\n# Common git hook script helpers\n#\n# This file contains common script helpers for git hooks.  It should be sourced\n# in scripts right after the initial environment processing.\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 3\n\n# Only show interactive prompts if there a terminal is attached to stdout.\n# While this technically doesn't guarantee that reading from /dev/tty works,\n# this should work reasonably well on all of our supported development systems\n# and in most terminal emulators.\nis_tty='0'\nif [ -t '1' ]; then\n\tis_tty='1'\nfi\nreadonly is_tty\n\n# Helpers\n\n# prompt is a helper that prompts the user for interactive input if that can be\n# done.  If there is no terminal attached, it sleeps for two seconds, giving the\n# programmer some time to react, and returns with a zero exit code.\nprompt() {\n\tif [ \"$is_tty\" -eq '0' ]; then\n\t\tsleep 2\n\n\t\treturn 0\n\tfi\n\n\twhile true; do\n\t\tprintf 'commit anyway? y/[n]: '\n\t\tread -r ans </dev/tty\n\n\t\tcase \"$ans\" in\n\t\t'y' | 'Y')\n\t\t\tbreak\n\t\t\t;;\n\t\t'' | 'n' | 'N')\n\t\t\texit 1\n\t\t\t;;\n\t\t*)\n\t\t\tcontinue\n\t\t\t;;\n\t\tesac\n\tdone\n}\n\n# check_unstaged_changes helper checks for unstaged changes and untracked files.\n# If any are found, the programmer will be warned, but the commit will not fail.\ncheck_unstaged_changes() {\n\t# shellcheck disable=SC2016\n\tawk_prog='substr($2, 2, 1) != \".\" { print $9; } $1 == \"?\" { print $2; }'\n\treadonly awk_prog\n\n\tunstaged=\"$(git status --porcelain=2 | awk \"$awk_prog\")\"\n\treadonly unstaged\n\n\tif [ \"$unstaged\" != '' ]; then\n\t\tprintf 'WARNING: you have unstaged changes:\\n\\n%s\\n\\n' \"$unstaged\"\n\t\tprompt\n\tfi\n}\n\n# lint_staged_changes is a helper that runs all necessary linters, tests, etc.,\n# based on the types of files that have been modified.\nlint_staged_changes() {\n\tverbose=\"${VERBOSE:-0}\"\n\treadonly verbose\n\n\tif [ \"$(git diff --cached --name-only -- '*.md' || :)\" != '' ]; then\n\t\tmake VERBOSE=\"$verbose\" md-lint\n\tfi\n\n\tif [ \"$(git diff --cached --name-only -- '*.sh' || :)\" != '' ]; then\n\t\tmake VERBOSE=\"$verbose\" sh-lint\n\tfi\n\n\ttxt_diff=\"$(\n\t\tgit \\\n\t\t\tdiff \\\n\t\t\t--cached \\\n\t\t\t--name-only \\\n\t\t\t-- \\\n\t\t\t'*.json' \\\n\t\t\t'*.md' \\\n\t\t\t'*.yaml' \\\n\t\t\t'*.yml' \\\n\t\t\t'*.dockerignore' \\\n\t\t\t'.gitignore' \\\n\t\t\t'Makefile' || :\n\t)\"\n\treadonly txt_diff\n\n\tif [ \"$txt_diff\" != '' ]; then\n\t\tmake VERBOSE=\"$verbose\" txt-lint\n\tfi\n\n\tif [ \"$(git diff --cached --name-only -- '*.go' '*.mod' 'Makefile' || :)\" != '' ]; then\n\t\tmake VERBOSE=\"$verbose\" go-os-check go-lint go-test\n\tfi\n}\n"
  },
  {
    "path": "scripts/hooks/pre-commit",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 6\n\nset -e -f -u\n\n. ./scripts/hooks/helper.sh\n\n# Warn the programmer about temporary todos and skel FIXMEs, but do not fail the\n# commit, because the commit could be in a temporary branch.\ntemp_todos=\"$(\n\tgit grep -e 'FIXME' -e 'TODO.*!!' -- \\\n\t\t':!./scripts/hooks/pre-commit' \\\n\t\t':!./client' \\\n\t\t|| :\n)\"\nreadonly temp_todos\n\nif [ \"$temp_todos\" != '' ]; then\n\tprintf 'WARNING: you have temporary todos:\\n\\n%s\\n\\n' \"$temp_todos\"\n\tprompt\nfi\n\ncheck_unstaged_changes\n\nlint_staged_changes\n"
  },
  {
    "path": "scripts/hooks/pre-merge-commit",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 1\n\nset -e -f -u\n\n. ./scripts/hooks/helper.sh\n\ncheck_unstaged_changes\n\nlint_staged_changes\n"
  },
  {
    "path": "scripts/install.sh",
    "content": "#!/bin/sh\n\n# AdGuard Home Installation Script\n\n# Exit the script if a pipeline fails (-e), prevent accidental filename\n# expansion (-f), and consider undefined variables as errors (-u).\nset -e -f -u\n\n# Function log is an echo wrapper that writes to stderr if the caller\n# requested verbosity level greater than 0.  Otherwise, it does nothing.\nlog() {\n\tif [ \"$verbose\" -gt '0' ]; then\n\t\techo \"$1\" 1>&2\n\tfi\n}\n\n# Function error_exit is an echo wrapper that writes to stderr and stops the\n# script execution with code 1.\nerror_exit() {\n\techo \"$1\" 1>&2\n\n\texit 1\n}\n\n# Function usage prints the note about how to use the script.\n#\n# TODO(e.burkov): Document each option.\nusage() {\n\techo 'install.sh: usage: [-c channel] [-C cpu_type] [-h] [-O os] [-o output_dir]' \\\n\t\t'[-r|-R] [-u|-U] [-v|-V]' 1>&2\n\n\texit 2\n}\n\n# Function maybe_sudo runs passed command with root privileges if use_sudo isn't\n# equal to 0.\n#\n# TODO(e.burkov):  Use everywhere the sudo_cmd isn't quoted.\nmaybe_sudo() {\n\tif [ \"$use_sudo\" -eq 0 ]; then\n\t\t\"$@\"\n\telse\n\t\t\"$sudo_cmd\" \"$@\"\n\tfi\n}\n\n# Function is_command checks if the command exists on the machine.\nis_command() {\n\tcommand -v \"$1\" >/dev/null 2>&1\n}\n\n# Function is_little_endian checks if the CPU is little-endian.\n#\n# See https://serverfault.com/a/163493/267530.\nis_little_endian() {\n\t# The ASCII character \"I\" has the octal code of 111.  In the two-byte octal\n\t# display mode (-o), hexdump will print it either as \"000111\" on a little\n\t# endian system or as a \"111000\" on a big endian one.  Return the sixth\n\t# character to compare it against the number '1'.\n\t#\n\t# Do not use echo -n, because its behavior in the presence of the -n flag is\n\t# explicitly implementation-defined in POSIX.  Use hexdump instead of od,\n\t# because OpenWrt and its derivatives have the former but not the latter.\n\tis_little_endian_result=\"$(\n\t\tprintf 'I' \\\n\t\t\t| hexdump -o \\\n\t\t\t| awk '{ print substr($2, 6, 1); exit; }'\n\t)\"\n\treadonly is_little_endian_result\n\n\t[ \"$is_little_endian_result\" -eq '1' ]\n}\n\n# Function check_required checks if the required software is available on the\n# machine.  The required software:\n#\n#   unzip (macOS) / tar (other unixes)\n#\n# curl/wget are checked in function configure.\ncheck_required() {\n\trequired_darwin=\"unzip\"\n\trequired_unix=\"tar\"\n\treadonly required_darwin required_unix\n\n\tcase \"$os\" in\n\t'freebsd' | 'linux' | 'openbsd')\n\t\trequired=\"$required_unix\"\n\t\t;;\n\t'darwin')\n\t\trequired=\"$required_darwin\"\n\t\t;;\n\t*)\n\t\t# Generally shouldn't happen, since the OS has already been validated.\n\t\terror_exit \"unsupported operating system: '$os'\"\n\t\t;;\n\tesac\n\treadonly required\n\n\t# Don't use quotes to get word splitting.\n\tfor cmd in $required; do\n\t\tlog \"checking $cmd\"\n\t\tif ! is_command \"$cmd\"; then\n\t\t\tlog \"the full list of required software: [$required]\"\n\n\t\t\terror_exit \"$cmd is required to install AdGuard Home via this script\"\n\t\tfi\n\tdone\n}\n\n# Function check_out_dir requires the output directory to be set and exist.\ncheck_out_dir() {\n\tif [ \"$out_dir\" = '' ]; then\n\t\terror_exit 'output directory should be presented'\n\tfi\n\n\tif ! [ -d \"$out_dir\" ]; then\n\t\tlog \"$out_dir directory will be created\"\n\tfi\n}\n\n# Function parse_opts parses the options list and validates it's combinations.\nparse_opts() {\n\twhile getopts \"C:c:hO:o:rRuUvV\" opt \"$@\"; do\n\t\tcase \"$opt\" in\n\t\tC)\n\t\t\tcpu=\"$OPTARG\"\n\t\t\t;;\n\t\tc)\n\t\t\tchannel=\"$OPTARG\"\n\t\t\t;;\n\t\th)\n\t\t\tusage\n\t\t\t;;\n\t\tO)\n\t\t\tos=\"$OPTARG\"\n\t\t\t;;\n\t\to)\n\t\t\tout_dir=\"$OPTARG\"\n\t\t\t;;\n\t\tR)\n\t\t\treinstall='0'\n\t\t\t;;\n\t\tU)\n\t\t\tuninstall='0'\n\t\t\t;;\n\t\tr)\n\t\t\treinstall='1'\n\t\t\t;;\n\t\tu)\n\t\t\tuninstall='1'\n\t\t\t;;\n\t\tV)\n\t\t\tverbose='0'\n\t\t\t;;\n\t\tv)\n\t\t\tverbose='1'\n\t\t\t;;\n\t\t*)\n\t\t\tlog \"bad option $OPTARG\"\n\n\t\t\tusage\n\t\t\t;;\n\t\tesac\n\tdone\n\n\tif [ \"$uninstall\" -eq '1' ] && [ \"$reinstall\" -eq '1' ]; then\n\t\terror_exit 'the -r and -u options are mutually exclusive'\n\tfi\n}\n\n# Function set_channel sets the channel if needed and validates the value.\nset_channel() {\n\t# Validate.\n\tcase \"$channel\" in\n\t'development' | 'edge' | 'beta' | 'release')\n\t\t# All is well, go on.\n\t\t;;\n\t*)\n\t\terror_exit \"invalid channel '$channel'\nsupported values are 'development', 'edge', 'beta', and 'release'\"\n\t\t;;\n\tesac\n\n\t# Log.\n\tlog \"channel: $channel\"\n}\n\n# Function set_os sets the os if needed and validates the value.\nset_os() {\n\t# Set if needed.\n\tif [ \"$os\" = '' ]; then\n\t\tos=\"$(uname -s)\"\n\t\tcase \"$os\" in\n\t\t'Darwin')\n\t\t\tos='darwin'\n\t\t\t;;\n\t\t'FreeBSD')\n\t\t\tos='freebsd'\n\t\t\t;;\n\t\t'Linux')\n\t\t\tos='linux'\n\t\t\t;;\n\t\t'OpenBSD')\n\t\t\tos='openbsd'\n\t\t\t;;\n\t\t*)\n\t\t\terror_exit \"unsupported operating system: '$os'\"\n\t\t\t;;\n\t\tesac\n\tfi\n\n\t# Validate.\n\tcase \"$os\" in\n\t'darwin' | 'freebsd' | 'linux' | 'openbsd')\n\t\t# All right, go on.\n\t\t;;\n\t*)\n\t\terror_exit \"unsupported operating system: '$os'\"\n\t\t;;\n\tesac\n\n\t# Log.\n\tlog \"operating system: $os\"\n}\n\n# Function set_cpu sets the cpu if needed and validates the value.\nset_cpu() {\n\t# Set if needed.\n\tif [ \"$cpu\" = '' ]; then\n\t\tcpu=\"$(uname -m)\"\n\t\tcase \"$cpu\" in\n\t\t'x86_64' | 'x86-64' | 'x64' | 'amd64')\n\t\t\tcpu='amd64'\n\t\t\t;;\n\t\t'i386' | 'i486' | 'i686' | 'i786' | 'x86')\n\t\t\tcpu='386'\n\t\t\t;;\n\t\t'armv5l')\n\t\t\tcpu='armv5'\n\t\t\t;;\n\t\t'armv6l')\n\t\t\tcpu='armv6'\n\t\t\t;;\n\t\t'armv7l' | 'armv8l')\n\t\t\tcpu='armv7'\n\t\t\t;;\n\t\t'aarch64' | 'arm64')\n\t\t\tcpu='arm64'\n\t\t\t;;\n\t\t'mips' | 'mips64')\n\t\t\tif is_little_endian; then\n\t\t\t\tcpu=\"${cpu}le\"\n\t\t\tfi\n\n\t\t\tcpu=\"${cpu}_softfloat\"\n\t\t\t;;\n\t\t'riscv64')\n\t\t\tcpu='riscv64'\n\t\t\t;;\n\t\t*)\n\t\t\terror_exit \"unsupported cpu type: $cpu\"\n\t\t\t;;\n\t\tesac\n\tfi\n\n\t# Validate.\n\tcase \"$cpu\" in\n\t'amd64' | '386' | 'armv5' | 'armv6' | 'armv7' | 'arm64' | 'riscv64')\n\t\t# All right, go on.\n\t\t;;\n\t'mips64le_softfloat' | 'mips64_softfloat' | 'mipsle_softfloat' | 'mips_softfloat')\n\t\t# That's right too.\n\t\t;;\n\t*)\n\t\terror_exit \"unsupported cpu type: $cpu\"\n\t\t;;\n\tesac\n\n\t# Log.\n\tlog \"cpu type: $cpu\"\n}\n\n# Function fix_darwin performs some configuration changes for macOS if needed.\n#\n# TODO(a.garipov): Remove after the final v0.107.0 release.\n#\n# See https://github.com/AdguardTeam/AdGuardHome/issues/2443.\nfix_darwin() {\n\tif [ \"$os\" != 'darwin' ]; then\n\t\treturn 0\n\tfi\n\n\t# Set the package extension.\n\tpkg_ext='zip'\n\n\t# It is important to install AdGuard Home into the /Applications directory\n\t# on macOS.  Otherwise, it may grant not enough privileges to the AdGuard\n\t# Home.\n\tout_dir='/Applications'\n}\n\n# Function fix_freebsd performs some fixes to make it work on FreeBSD.\nfix_freebsd() {\n\tif [ \"$os\" != 'freebsd' ]; then\n\t\treturn 0\n\tfi\n\n\trcd='/usr/local/etc/rc.d'\n\treadonly rcd\n\n\tif ! [ -d \"$rcd\" ]; then\n\t\tmkdir \"$rcd\"\n\tfi\n}\n\n# download_curl uses curl(1) to download a file.  The first argument is the URL.\n# The second argument is optional and is the output file.\ndownload_curl() {\n\tcurl_output=\"${2:-}\"\n\tif [ \"$curl_output\" = '' ]; then\n\t\tcurl -L -S -s \"$1\"\n\telse\n\t\tcurl -L -S -o \"$curl_output\" -s \"$1\"\n\tfi\n}\n\n# download_wget uses wget(1) to download a file.  The first argument is the URL.\n# The second argument is optional and is the output file.\ndownload_wget() {\n\twget_output=\"${2:--}\"\n\n\twget --no-verbose -O \"$wget_output\" \"$1\"\n}\n\n# download_fetch uses fetch(1) to download a file.  The first argument is the\n# URL.  The second argument is optional and is the output file.\ndownload_fetch() {\n\tfetch_output=\"${2:-}\"\n\tif [ \"$fetch_output\" = '' ]; then\n\t\tfetch -o '-' \"$1\"\n\telse\n\t\tfetch -o \"$fetch_output\" \"$1\"\n\tfi\n}\n\n# Function set_download_func sets the appropriate function for downloading\n# files.\nset_download_func() {\n\tif is_command 'curl'; then\n\t\t# Go on and use the default, download_curl.\n\t\treturn 0\n\telif is_command 'wget'; then\n\t\tdownload_func='download_wget'\n\telif is_command 'fetch'; then\n\t\tdownload_func='download_fetch'\n\telse\n\t\terror_exit \"either curl or wget is required to install AdGuard Home via this script\"\n\tfi\n}\n\n# Function set_sudo_cmd sets the appropriate command to run a command under\n# superuser privileges.\nset_sudo_cmd() {\n\tcase \"$os\" in\n\t'openbsd')\n\t\tsudo_cmd='doas'\n\t\t;;\n\t'darwin' | 'freebsd' | 'linux')\n\t\t# Go on and use the default, sudo.\n\t\t;;\n\t*)\n\t\terror_exit \"unsupported operating system: '$os'\"\n\t\t;;\n\tesac\n}\n\n# Function configure sets the script's configuration.\nconfigure() {\n\tset_channel\n\tset_os\n\tset_cpu\n\tfix_darwin\n\tset_download_func\n\tset_sudo_cmd\n\tcheck_out_dir\n\n\tpkg_name=\"AdGuardHome_${os}_${cpu}.${pkg_ext}\"\n\turl=\"https://static.adtidy.org/adguardhome/${channel}/${pkg_name}\"\n\tagh_dir=\"${out_dir}/AdGuardHome\"\n\treadonly pkg_name url agh_dir\n\n\tlog \"AdGuard Home will be installed into $agh_dir\"\n}\n\n# Function is_root checks for root privileges to be granted.\nis_root() {\n\tuser_id=\"$(id -u)\"\n\tif [ \"$user_id\" -eq '0' ]; then\n\t\tlog 'script is executed with root privileges'\n\n\t\treturn 0\n\tfi\n\n\tif is_command \"$sudo_cmd\"; then\n\t\tlog 'note that AdGuard Home requires root privileges to install using this script'\n\n\t\treturn 1\n\tfi\n\n\terror_exit 'root privileges are required to install AdGuard Home using this script\nplease, restart it with root privileges'\n}\n\n# Function rerun_with_root downloads the script, runs it with root privileges,\n# and exits the current script.  It passes the necessary configuration of the\n# current script to the child script.\n#\n# TODO(e.burkov): Try to avoid restarting.\nrerun_with_root() {\n\tscript_url='https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh'\n\treadonly script_url\n\n\tr='-R'\n\tif [ \"$reinstall\" -eq '1' ]; then\n\t\tr='-r'\n\tfi\n\n\tu='-U'\n\tif [ \"$uninstall\" -eq '1' ]; then\n\t\tu='-u'\n\tfi\n\n\tv='-V'\n\tif [ \"$verbose\" -eq '1' ]; then\n\t\tv='-v'\n\tfi\n\n\treadonly r u v\n\n\tlog 'restarting with root privileges'\n\n\t# Group curl/wget together with an echo, so that if the former fails before\n\t# producing any output, the latter prints an exit command for the following\n\t# shell to execute to prevent it from getting an empty input and exiting\n\t# with a zero code in that case.\n\t{ \"$download_func\" \"$script_url\" || echo 'exit 1'; } \\\n\t\t| $sudo_cmd sh -s -- -c \"$channel\" -C \"$cpu\" -O \"$os\" -o \"$out_dir\" \"$r\" \"$u\" \"$v\"\n\n\t# Exit the script.  Since if the code of the previous pipeline is non-zero,\n\t# the execution won't reach this point thanks to set -e, exit with zero.\n\texit 0\n}\n\n# Function download downloads the file from the URL and saves it to the\n# specified filepath.\ndownload() {\n\tlog \"downloading package from $url to $pkg_name\"\n\n\tif ! \"$download_func\" \"$url\" \"$pkg_name\"; then\n\t\terror_exit \"cannot download the package from $url into $pkg_name\"\n\tfi\n\n\tlog \"successfully downloaded $pkg_name\"\n}\n\n# Function unpack unpacks the passed archive depending on it's extension.\nunpack() {\n\tlog \"unpacking package from $pkg_name into $out_dir\"\n\n\t# shellcheck disable=SC2174\n\tif ! mkdir -m 0700 -p \"$out_dir\"; then\n\t\terror_exit \"cannot create directory $out_dir\"\n\tfi\n\n\tcase \"$pkg_ext\" in\n\t'zip')\n\t\tunzip \"$pkg_name\" -d \"$out_dir\"\n\t\t;;\n\t'tar.gz')\n\t\ttar -C \"$out_dir\" -f \"$pkg_name\" -x -z\n\t\t;;\n\t*)\n\t\terror_exit \"unexpected package extension: '$pkg_ext'\"\n\t\t;;\n\tesac\n\n\tunpacked_contents=\"$(\n\t\techo\n\t\tls -l -A \"$agh_dir\"\n\t)\"\n\tlog \"successfully unpacked, contents: $unpacked_contents\"\n\n\trm \"$pkg_name\"\n}\n\n# Function handle_existing detects the existing AGH installation and takes care\n# of removing it if needed.\nhandle_existing() {\n\tif ! [ -d \"$agh_dir\" ]; then\n\t\tlog 'no need to uninstall'\n\n\t\tif [ \"$uninstall\" -eq '1' ]; then\n\t\t\texit 0\n\t\tfi\n\n\t\treturn 0\n\tfi\n\n\texisting_adguard_home=\"$(ls -1 -A \"$agh_dir\")\"\n\tif [ \"$existing_adguard_home\" != '' ]; then\n\t\tlog 'the existing AdGuard Home installation is detected'\n\n\t\tif [ \"$reinstall\" -ne '1' ] && [ \"$uninstall\" -ne '1' ]; then\n\t\t\terror_exit \\\n\t\t\t\t\"to reinstall/uninstall the AdGuard Home using this script specify one of the '-r' or '-u' flags\"\n\t\tfi\n\n\t\t# TODO(e.burkov):  Remove the stop once v0.107.1 released.\n\t\tif (cd \"$agh_dir\" && ! ./AdGuardHome -s stop || ! ./AdGuardHome -s uninstall); then\n\t\t\t# It doesn't terminate the script since it is possible that AGH just\n\t\t\t# not installed as service but appearing in the directory.\n\t\t\tlog \"cannot uninstall AdGuard Home from $agh_dir\"\n\t\tfi\n\n\t\trm -r \"$agh_dir\"\n\n\t\tlog 'AdGuard Home was successfully uninstalled'\n\tfi\n\n\tif [ \"$uninstall\" -eq '1' ]; then\n\t\texit 0\n\tfi\n}\n\n# Function install_service tries to install AGH as service.\ninstall_service() {\n\t# Installing the service as root is required at least on FreeBSD.\n\tuse_sudo='0'\n\tif [ \"$os\" = 'freebsd' ]; then\n\t\tuse_sudo='1'\n\tfi\n\n\tif (cd \"$agh_dir\" && maybe_sudo ./AdGuardHome -s install); then\n\t\treturn 0\n\tfi\n\n\tlog \"installation failed, removing $agh_dir\"\n\n\trm -r \"$agh_dir\"\n\n\t# Some devices detected to have armv7 CPU face the compatibility issues with\n\t# actual armv7 builds.  We should try to install the armv5 binary instead.\n\t#\n\t# See https://github.com/AdguardTeam/AdGuardHome/issues/2542.\n\tif [ \"$cpu\" = 'armv7' ]; then\n\t\tcpu='armv5'\n\t\treinstall='1'\n\n\t\tlog \"trying to use $cpu cpu\"\n\n\t\trerun_with_root\n\tfi\n\n\terror_exit 'cannot install AdGuardHome as a service'\n}\n\n# Entrypoint\n\n# Set default values of configuration variables.\nchannel='release'\nreinstall='0'\nuninstall='0'\nverbose='0'\ncpu=''\nos=''\nout_dir='/opt'\npkg_ext='tar.gz'\ndownload_func='download_curl'\nsudo_cmd='sudo'\n\nparse_opts \"$@\"\n\necho 'starting AdGuard Home installation script'\n\nconfigure\ncheck_required\n\nif ! is_root; then\n\trerun_with_root\nfi\n# Needs rights.\nfix_freebsd\n\nhandle_existing\n\ndownload\nunpack\n\ninstall_service\n\nprintf '%s\\n' \\\n\t'AdGuard Home is now installed and running' \\\n\t'you can control the service status with the following commands:' \\\n\t\"$sudo_cmd ${agh_dir}/AdGuardHome -s start|stop|restart|status|install|uninstall\"\n"
  },
  {
    "path": "scripts/make/build-docker.sh",
    "content": "#!/bin/sh\n\nverbose=\"${VERBOSE:-0}\"\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nelse\n\tset +x\nfi\n\nset -e -f -u\n\n# Require these to be set.  The channel value is validated later.\nchannel=\"${CHANNEL:?please set CHANNEL}\"\ncommit=\"${REVISION:?please set REVISION}\"\ndist_dir=\"${DIST_DIR:?please set DIST_DIR}\"\nreadonly channel commit dist_dir\n\nif [ \"${VERSION:-}\" = 'v0.0.0' ] || [ \"${VERSION:-}\" = '' ]; then\n\tversion=\"$(sh ./scripts/make/version.sh)\"\nelse\n\tversion=\"$VERSION\"\nfi\nreadonly version\n\n# Allow users to use sudo.\nsudo_cmd=\"${SUDO:-}\"\nreadonly sudo_cmd\n\n# Make sure that those are built using something like:\n#\tmake ARCH='386 amd64 arm arm64 ppc64le' OS=linux VERBOSE=1 build-release\ndocker_platforms=\"\\\nlinux/386,\\\nlinux/amd64,\\\nlinux/arm/v6,\\\nlinux/arm/v7,\\\nlinux/arm64,\\\nlinux/ppc64le\"\nreadonly docker_platforms\n\nbuild_date=\"$(date -u +'%Y-%m-%dT%H:%M:%SZ')\"\nreadonly build_date\n\n# Set DOCKER_IMAGE_NAME to 'adguard/adguard-home' if you want (and are allowed)\n# to push to DockerHub.\ndocker_image_name=\"${DOCKER_IMAGE_NAME:-adguardhome-dev}\"\nreadonly docker_image_name\n\n# Set DOCKER_PUSH to '1' if you want (and are allowed) to push to DockerHub.\ndocker_push=\"${DOCKER_PUSH:-0}\"\nreadonly docker_push\n\ncase \"$channel\" in\n'release')\n\tdocker_version_tag=\"--tag=${docker_image_name}:${version}\"\n\tdocker_channel_tag=\"--tag=${docker_image_name}:latest\"\n\t;;\n'beta')\n\tdocker_version_tag=\"--tag=${docker_image_name}:${version}\"\n\tdocker_channel_tag=\"--tag=${docker_image_name}:beta\"\n\t;;\n'edge')\n\t# Set the version tag to an empty string when pushing to the edge channel.\n\tdocker_version_tag=''\n\tdocker_channel_tag=\"--tag=${docker_image_name}:edge\"\n\t;;\n'development')\n\t# Set both tags to an empty string for development builds.\n\tdocker_version_tag=''\n\tdocker_channel_tag=''\n\t;;\n*)\n\techo \"invalid channel '$channel', supported values are\\\n\t\t'development', 'edge', 'beta', and 'release'\" 1>&2\n\texit 1\n\t;;\nesac\nreadonly docker_version_tag docker_channel_tag\n\n# Copy the binaries into a new directory under new names, so that it's easier to\n# COPY them later.  DO NOT remove the trailing underscores.  See file\n# docker/Dockerfile.\ndist_docker=\"${dist_dir}/docker\"\nreadonly dist_docker\n\nmkdir -p \"$dist_docker\"\ncp \"${dist_dir}/AdGuardHome_linux_386/AdGuardHome/AdGuardHome\" \\\n\t\"${dist_docker}/AdGuardHome_linux_386_\"\ncp \"${dist_dir}/AdGuardHome_linux_amd64/AdGuardHome/AdGuardHome\" \\\n\t\"${dist_docker}/AdGuardHome_linux_amd64_\"\ncp \"${dist_dir}/AdGuardHome_linux_arm64/AdGuardHome/AdGuardHome\" \\\n\t\"${dist_docker}/AdGuardHome_linux_arm64_\"\ncp \"${dist_dir}/AdGuardHome_linux_arm_6/AdGuardHome/AdGuardHome\" \\\n\t\"${dist_docker}/AdGuardHome_linux_arm_v6\"\ncp \"${dist_dir}/AdGuardHome_linux_arm_7/AdGuardHome/AdGuardHome\" \\\n\t\"${dist_docker}/AdGuardHome_linux_arm_v7\"\ncp \"${dist_dir}/AdGuardHome_linux_ppc64le/AdGuardHome/AdGuardHome\" \\\n\t\"${dist_docker}/AdGuardHome_linux_ppc64le_\"\n\n# docker_build_opt_tag is a function that wraps the call of docker build command\n# with optionally --tag flags.\ndocker_build_opt_tag() {\n\tif [ \"$sudo_cmd\" != '' ]; then\n\t\tset -- \"$sudo_cmd\"\n\tfi\n\n\t# Set the initial parameters.\n\tset -- \\\n\t\t\"$@\" \\\n\t\tdocker \\\n\t\tbuildx \\\n\t\tbuild \\\n\t\t--build-arg BUILD_DATE=\"$build_date\" \\\n\t\t--build-arg DIST_DIR=\"$dist_dir\" \\\n\t\t--build-arg VCS_REF=\"$commit\" \\\n\t\t--build-arg VERSION=\"$version\" \\\n\t\t--platform \"$docker_platforms\" \\\n\t\t--progress 'plain' \\\n\t\t;\n\n\t# Append the channel tag, if any.\n\tif [ \"$docker_channel_tag\" != '' ]; then\n\t\tset -- \"$@\" \"$docker_channel_tag\"\n\tfi\n\n\t# Append the version tag, if any.\n\tif [ \"$docker_version_tag\" != '' ]; then\n\t\tset -- \"$@\" \"$docker_version_tag\"\n\tfi\n\n\t# Push to DockerHub, if requested.\n\tif [ \"$docker_push\" -eq 1 ]; then\n\t\tset -- \"$@\" '--push'\n\tfi\n\n\t# Append the rest.\n\tset -- \\\n\t\t\"$@\" \\\n\t\t-f \\\n\t\t./docker/build.Dockerfile \\\n\t\t. \\\n\t\t;\n\n\t# Call the command with the assembled parameters.\n\t\"$@\"\n}\n\ndocker_build_opt_tag\n"
  },
  {
    "path": "scripts/make/build-release.sh",
    "content": "#!/bin/sh\n\n# AdGuard Home Release Script\n#\n# The commentary in this file is written with the assumption that the reader\n# only has superficial knowledge of the POSIX shell language and alike.\n# Experienced readers may find it overly verbose.\n\n# The default verbosity level is 0.  Show log messages if the caller requested\n# verbosity level greater than 0.  Show the environment and every command that\n# is run if the verbosity level is greater than 1.  Otherwise, print nothing.\n#\n# The level of verbosity for the build script is the same minus one level.  See\n# below in build().\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\nif [ \"$verbose\" -gt '1' ]; then\n\tenv\n\tset -x\nfi\n\n# By default, sign the packages, but allow users to skip that step.\nsign=\"${SIGN:-1}\"\nreadonly sign\n\n# Exit the script if a pipeline fails (-e), prevent accidental filename\n# expansion (-f), and consider undefined variables as errors (-u).\nset -e -f -u\n\n# Function log is an echo wrapper that writes to stderr if the caller requested\n# verbosity level greater than 0.  Otherwise, it does nothing.\nlog() {\n\tif [ \"$verbose\" -gt '0' ]; then\n\t\t# Don't use quotes to get word splitting.\n\t\techo \"$1\" 1>&2\n\tfi\n}\n\nlog 'starting to build AdGuard Home release'\n\n# Require the channel to be set.  Additional validation is performed later by\n# go-build.sh.\nchannel=\"${CHANNEL:?please set CHANNEL}\"\nreadonly channel\n\n# Check VERSION against the default value from the Makefile.  If it is that, use\n# the version calculation script.\nversion=\"${VERSION:-}\"\nif [ \"$version\" = 'v0.0.0' ] || [ \"$version\" = '' ]; then\n\tversion=\"$(sh ./scripts/make/version.sh)\"\nfi\nreadonly version\n\nlog \"channel '$channel'\"\nlog \"version '$version'\"\n\n# Check architecture and OS limiters.  Add spaces to the local versions for\n# better pattern matching.\nif [ \"${ARCH:-}\" != '' ]; then\n\tlog \"arches: '$ARCH'\"\n\tarches=\" $ARCH \"\nelse\n\tarches=''\nfi\nreadonly arches\n\nif [ \"${OS:-}\" != '' ]; then\n\tlog \"oses: '$OS'\"\n\toses=\" $OS \"\nelse\n\toses=''\nfi\nreadonly oses\n\n# Require the gpg key and passphrase to be set if the signing is required.\nif [ \"$sign\" -eq '1' ]; then\n\tgpg_key_passphrase=\"${GPG_KEY_PASSPHRASE:?please set GPG_KEY_PASSPHRASE or unset SIGN}\"\n\tgpg_key=\"${GPG_KEY:?please set GPG_KEY or unset SIGN}\"\n\tsigner_api_key=\"${SIGNER_API_KEY:?please set SIGNER_API_KEY or unset SIGN}\"\n\tdeploy_script_path=\"${DEPLOY_SCRIPT_PATH:?please set DEPLOY_SCRIPT_PATH or unset SIGN}\"\nelse\n\tgpg_key_passphrase=''\n\tgpg_key=''\n\tsigner_api_key=''\n\tdeploy_script_path=''\nfi\nreadonly gpg_key_passphrase gpg_key signer_api_key deploy_script_path\n\n# The default distribution files directory is dist.\ndist=\"${DIST_DIR:-dist}\"\nreadonly dist\n\nlog \"checking tools\"\n\n# Make sure we fail gracefully if one of the tools we need is missing.  Use\n# alternatives when available.\nuse_shasum='0'\nfor tool in gpg gzip sed sha256sum tar zip; do\n\tif ! command -v \"$tool\" >/dev/null; then\n\t\tif [ \"$tool\" = 'sha256sum' ] && command -v 'shasum' >/dev/null; then\n\t\t\t# macOS doesn't have sha256sum installed by default, but it does\n\t\t\t# have shasum.\n\t\t\tlog 'replacing sha256sum with shasum -a 256'\n\t\t\tuse_shasum='1'\n\t\telse\n\t\t\tlog \"pieces don't fit, '$tool' not found\"\n\n\t\t\texit 1\n\t\tfi\n\tfi\ndone\nreadonly use_shasum\n\n# Data section.  Arrange data into space-separated tables for read -r to read.\n# Use a hyphen for missing values.\n\n#    os  arch      arm mips\nplatforms=\"\\\ndarwin   amd64     -   -\ndarwin   arm64     -   -\nfreebsd  386       -   -\nfreebsd  amd64     -   -\nfreebsd  arm       5   -\nfreebsd  arm       6   -\nfreebsd  arm       7   -\nfreebsd  arm64     -   -\nlinux    386       -   -\nlinux    amd64     -   -\nlinux    arm       5   -\nlinux    arm       6   -\nlinux    arm       7   -\nlinux    arm64     -   -\nlinux    mips      -   softfloat\nlinux    mips64    -   softfloat\nlinux    mips64le  -   softfloat\nlinux    mipsle    -   softfloat\nlinux    ppc64le   -   -\nlinux    riscv64   -   -\nopenbsd  amd64     -   -\nopenbsd  arm64     -   -\nwindows  386       -   -\nwindows  amd64     -   -\nwindows  arm64     -   -\"\nreadonly platforms\n\n# Function sign signs the specified build as intended by the target operating\n# system.\nsign() {\n\t# Only sign if needed.\n\tif [ \"$sign\" -ne '1' ]; then\n\t\treturn\n\tfi\n\n\t# Get the arguments.  Here and below, use the \"sign_\" prefix for all\n\t# variables local to function sign.\n\tsign_os=\"$1\"\n\tsign_bin_path=\"$2\"\n\n\tif [ \"$sign_os\" != 'windows' ]; then\n\t\tgpg \\\n\t\t\t--default-key \"$gpg_key\" \\\n\t\t\t--detach-sig \\\n\t\t\t--passphrase \"$gpg_key_passphrase\" \\\n\t\t\t--pinentry-mode loopback -q \"$sign_bin_path\" \\\n\t\t\t;\n\n\t\treturn\n\telif [ \"$channel\" = 'beta' ] || [ \"$channel\" = 'release' ]; then\n\t\tsigned_bin_path=\"${sign_bin_path}.signed\"\n\n\t\tenv INPUT_FILE=\"$sign_bin_path\" \\\n\t\t\tOUTPUT_FILE=\"$signed_bin_path\" \\\n\t\t\tSIGNER_API_KEY=\"$signer_api_key\" \\\n\t\t\t\"$deploy_script_path\" sign-executable\n\n\t\tmv \"$signed_bin_path\" \"$sign_bin_path\"\n\tfi\n}\n\n# Function build builds the release for one platform.  It builds a binary and an\n# archive.\nbuild() {\n\t# Get the arguments.  Here and below, use the \"build_\" prefix for all\n\t# variables local to function build.\n\tbuild_dir=\"${dist}/${1}/AdGuardHome\" \\\n\t\tbuild_ar=\"$2\" \\\n\t\tbuild_os=\"$3\" \\\n\t\tbuild_arch=\"$4\" \\\n\t\tbuild_arm=\"$5\" \\\n\t\tbuild_mips=\"$6\" \\\n\t\t;\n\n\t# Use the \".exe\" filename extension if we build a Windows release.\n\tif [ \"$build_os\" = 'windows' ]; then\n\t\tbuild_output=\"./${build_dir}/AdGuardHome.exe\"\n\telse\n\t\tbuild_output=\"./${build_dir}/AdGuardHome\"\n\tfi\n\n\tmkdir -p \"./${build_dir}\"\n\n\t# Build the binary.\n\t#\n\t# Set GOARM and GOMIPS to an empty string if $build_arm and $build_mips are\n\t# the zero value by removing the hyphen as if it's a prefix.\n\tenv GOARCH=\"$build_arch\" \\\n\t\tGOARM=\"${build_arm#-}\" \\\n\t\tGOMIPS=\"${build_mips#-}\" \\\n\t\tGOOS=\"$os\" \\\n\t\tVERBOSE=\"$((verbose - 1))\" \\\n\t\tVERSION=\"$version\" \\\n\t\tOUT=\"$build_output\" \\\n\t\tsh ./scripts/make/go-build.sh\n\n\tlog \"$build_output\"\n\n\tsign \"$os\" \"$build_output\"\n\n\t# Prepare the build directory for archiving.\n\tcp ./CHANGELOG.md ./LICENSE.txt ./README.md \"$build_dir\"\n\n\t# Make archives.  Windows and macOS prefer ZIP archives; the rest,\n\t# gzipped tarballs.\n\tcase \"$build_os\" in\n\t'darwin' | 'windows')\n\t\tbuild_archive=\"./${dist}/${build_ar}.zip\"\n\t\t# TODO(a.garipov): Find an option similar to the -C option of tar for\n\t\t# zip.\n\t\t(cd \"${dist}/${1}\" && zip -9 -q -r \"../../${build_archive}\" \"./AdGuardHome\")\n\t\t;;\n\t*)\n\t\tbuild_archive=\"./${dist}/${build_ar}.tar.gz\"\n\t\ttar -C \"./${dist}/${1}\" -c -f - \"./AdGuardHome\" | gzip -9 - >\"$build_archive\"\n\t\t;;\n\tesac\n\n\tlog \"$build_archive\"\n}\n\nlog \"starting builds\"\n\n# Go over all platforms defined in the space-separated table above, tweak the\n# values where necessary, and feed to build.\necho \"$platforms\" | while read -r os arch arm mips; do\n\t# See if the architecture or the OS is in the allowlist.  To do so, try\n\t# removing everything that matches the pattern (well, a prefix, but that\n\t# doesn't matter here) containing the arch or the OS.\n\t#\n\t# For example, when $arches is \" amd64 arm64 \" and $arch is \"amd64\",\n\t# then the pattern to remove is \"* amd64 *\", so the whole string becomes\n\t# empty.  On the other hand, if $arch is \"windows\", then the pattern is\n\t# \"* windows *\", which doesn't match, so nothing is removed.\n\t#\n\t# See https://stackoverflow.com/a/43912605/1892060.\n\t#\n\t# shellcheck disable=SC2295\n\tif [ \"${arches##* $arch *}\" != '' ]; then\n\t\tlog \"$arch excluded, continuing\"\n\n\t\tcontinue\n\telif [ \"${oses##* $os *}\" != '' ]; then\n\t\tlog \"$os excluded, continuing\"\n\n\t\tcontinue\n\tfi\n\n\tcase \"$arch\" in\n\tarm)\n\t\tdir=\"AdGuardHome_${os}_${arch}_${arm}\"\n\t\tar=\"AdGuardHome_${os}_${arch}v${arm}\"\n\t\t;;\n\tmips*)\n\t\tdir=\"AdGuardHome_${os}_${arch}_${mips}\"\n\t\tar=\"$dir\"\n\t\t;;\n\t*)\n\t\tdir=\"AdGuardHome_${os}_${arch}\"\n\t\tar=\"$dir\"\n\t\t;;\n\tesac\n\n\tbuild \"$dir\" \"$ar\" \"$os\" \"$arch\" \"$arm\" \"$mips\"\ndone\n\nlog \"packing frontend\"\n\nbuild_archive=\"./${dist}/AdGuardHome_frontend.tar.gz\"\ntar -c -f - ./build | gzip -9 - >\"$build_archive\"\nlog \"$build_archive\"\n\nlog \"calculating checksums\"\n\n# calculate_checksums uses the previously detected SHA-256 tool to calculate\n# checksums.  Do not use find with -exec, since shasum requires arguments.\ncalculate_checksums() {\n\tif [ \"$use_shasum\" -eq '0' ]; then\n\t\tsha256sum \"$@\"\n\telse\n\t\tshasum -a 256 \"$@\"\n\tfi\n}\n\n# Calculate the checksums of the files in a subshell with a different working\n# directory.  Don't use ls, because files matching one of the patterns may be\n# absent, which will make ls return with a non-zero status code.\n#\n# TODO(a.garipov): Consider calculating these as the build goes.\n(\n\tset +f\n\n\tcd \"./${dist}\"\n\n\t: >./checksums.txt\n\n\tfor archive in ./*.zip ./*.tar.gz; do\n\t\t# Make sure that we don't try to calculate a checksum for a glob pattern\n\t\t# that matched no files.\n\t\tif [ ! -f \"$archive\" ]; then\n\t\t\tcontinue\n\t\tfi\n\n\t\tcalculate_checksums \"$archive\" >>./checksums.txt\n\tdone\n)\n\nlog \"writing versions\"\n\necho \"version=$version\" >\"./${dist}/version.txt\"\n\n# Create the version.json file.\n\nversion_download_url=\"https://static.adtidy.org/adguardhome/${channel}\"\nversion_json=\"./${dist}/version.json\"\nreadonly version_download_url version_json\n\n# If the channel is edge, point users to the \"Platforms\" page on the Wiki,\n# because the direct links to the edge packages are listed there.\nif [ \"$channel\" = 'edge' ]; then\n\tannouncement_url='https://github.com/AdguardTeam/AdGuardHome/wiki/Platforms'\nelse\n\tannouncement_url=\"https://github.com/AdguardTeam/AdGuardHome/releases/tag/${version}\"\nfi\nreadonly announcement_url\n\n# TODO(a.garipov): Remove \"selfupdate_min_version\" in future versions.\nrm -f \"$version_json\"\necho \"{\n  \\\"version\\\": \\\"${version}\\\",\n  \\\"announcement\\\": \\\"AdGuard Home ${version} is now available!\\\",\n  \\\"announcement_url\\\": \\\"${announcement_url}\\\",\n  \\\"selfupdate_min_version\\\": \\\"0.0\\\",\n\" >>\"$version_json\"\n\n# Add the MIPS* object keys without the \"softfloat\" part to mitigate the\n# consequences of #5373.\n#\n# TODO(a.garipov): Remove this around fall 2023.\necho \"\n  \\\"download_linux_mips64\\\": \\\"${version_download_url}/AdGuardHome_linux_mips64_softfloat.tar.gz\\\",\n  \\\"download_linux_mips64le\\\": \\\"${version_download_url}/AdGuardHome_linux_mips64le_softfloat.tar.gz\\\",\n  \\\"download_linux_mipsle\\\": \\\"${version_download_url}/AdGuardHome_linux_mipsle_softfloat.tar.gz\\\",\n\" >>\"$version_json\"\n\n# Same as with checksums above, don't use ls, because files matching one of the\n# patterns may be absent.\nar_files=\"$(find \"./${dist}\" ! -name \"${dist}\" -prune \\( -name '*.tar.gz' -o -name '*.zip' \\))\"\nar_files_len=\"$(echo \"$ar_files\" | wc -l)\"\nreadonly ar_files ar_files_len\n\ni='1'\n# Don't use quotes to get word splitting.\nfor f in $ar_files; do\n\tplatform=\"$f\"\n\n\t# Remove the prefix.\n\tplatform=\"${platform#\"./${dist}/AdGuardHome_\"}\"\n\n\t# Remove the filename extensions.\n\tplatform=\"${platform%.zip}\"\n\tplatform=\"${platform%.tar.gz}\"\n\n\t# Use the filename's base path.\n\tfilename=\"${f#\"./${dist}/\"}\"\n\n\tif [ \"$i\" -eq \"$ar_files_len\" ]; then\n\t\techo \"  \\\"download_${platform}\\\": \\\"${version_download_url}/${filename}\\\"\" >>\"$version_json\"\n\telse\n\t\techo \"  \\\"download_${platform}\\\": \\\"${version_download_url}/${filename}\\\",\" >>\"$version_json\"\n\tfi\n\n\ti=\"$((i + 1))\"\ndone\n\necho '}' >>\"$version_json\"\n\nlog \"finished\"\n"
  },
  {
    "path": "scripts/make/go-bench.sh",
    "content": "#!/bin/sh\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\n# Verbosity levels:\n#   0 = Don't print anything except for errors.\n#   1 = Print commands, but not nested commands.\n#   2 = Print everything.\nif [ \"$verbose\" -gt '1' ]; then\n\tset -x\n\tv_flags='-v=1'\n\tx_flags='-x=1'\nelif [ \"$verbose\" -gt '0' ]; then\n\tset -x\n\tv_flags='-v=1'\n\tx_flags='-x=0'\nelse\n\tset +x\n\tv_flags='-v=0'\n\tx_flags='-x=0'\nfi\nreadonly v_flags x_flags\n\nset -e -f -u\n\nif [ \"${RACE:-1}\" -eq '0' ]; then\n\trace_flags='--race=0'\nelse\n\trace_flags='--race=1'\nfi\nreadonly race_flags\n\nbenchtime_flags=\"${BENCHTIME_FLAGS:---benchtime=1x}\"\ncount_flags='--count=2'\ngo=\"${GO:-go}\"\nshuffle_flags='--shuffle=on'\ntimeout_flags=\"${TIMEOUT_FLAGS:---timeout=30s}\"\nreadonly benchtime_flags count_flags go shuffle_flags timeout_flags\n\nenv \\\n\tGOMAXPROCS=\"${GOMAXPROCS:-2}\" \\\n\t\"$go\" test \\\n\t\"$benchtime_flags\" \\\n\t\"$count_flags\" \\\n\t\"$race_flags\" \\\n\t\"$shuffle_flags\" \\\n\t\"$timeout_flags\" \\\n\t\"$v_flags\" \\\n\t\"$x_flags\" \\\n\t--bench='.' \\\n\t--benchmem \\\n\t--run='^$' \\\n\twork \\\n\t;\n"
  },
  {
    "path": "scripts/make/go-build.sh",
    "content": "#!/bin/sh\n\n# AdGuard Home Build Script\n#\n# The commentary in this file is written with the assumption that the reader\n# only has superficial knowledge of the POSIX shell language and alike.\n# Experienced readers may find it overly verbose.\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 2\n\n# The default verbosity level is 0.  Show every command that is run and every\n# package that is processed if the caller requested verbosity level greater than\n# 0.  Also show subcommands if the requested verbosity level is greater than 1.\n# Otherwise, do nothing.\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\nif [ \"$verbose\" -gt '1' ]; then\n\tenv\n\tset -x\n\tv_flags='-v=1'\n\tx_flags='-x=1'\nelif [ \"$verbose\" -gt '0' ]; then\n\tset -x\n\tv_flags='-v=1'\n\tx_flags='-x=0'\nelse\n\tset +x\n\tv_flags='-v=0'\n\tx_flags='-x=0'\nfi\nreadonly x_flags v_flags\n\n# Exit the script if a pipeline fails (-e), prevent accidental filename\n# expansion (-f), and consider undefined variables as errors (-u).\nset -e -f -u\n\n# Allow users to override the go command from environment.  For example, to\n# build two releases with two different Go versions and test the difference.\ngo=\"${GO:-go}\"\nreadonly go\n\n# Require the channel to be set and validate the value.\nchannel=\"${CHANNEL:?please set CHANNEL}\"\nreadonly channel\n\ncase \"$channel\" in\n'development' | 'edge' | 'beta' | 'release' | 'candidate')\n\t# All is well, go on.\n\t;;\n*)\n\techo \"invalid channel '$channel', supported values are \\\n\t\t'development', 'edge', 'beta', 'release', and 'candidate'\" 1>&2\n\texit 1\n\t;;\nesac\n\n# Check VERSION against the default value from the Makefile.  If it is that, use\n# the version calculation script.\nversion=\"${VERSION:-}\"\nif [ \"$version\" = 'v0.0.0' ] || [ \"$version\" = '' ]; then\n\tversion=\"$(sh ./scripts/make/version.sh)\"\nfi\nreadonly version\n\n# Set date and time of the latest commit unless already set.\ncommittime=\"${SOURCE_DATE_EPOCH:-$(git log -1 --pretty=%ct)}\"\nreadonly committime\n\n# Set the linker flags accordingly: set the release channel and the current\n# version as well as goarm and gomips variable values, if the variables are set\n# and are not empty.\nversion_pkg='github.com/AdguardTeam/AdGuardHome/internal/version'\nreadonly version_pkg\n\nldflags=\"-s -w\"\nldflags=\"${ldflags} -X ${version_pkg}.version=${version}\"\nldflags=\"${ldflags} -X ${version_pkg}.channel=${channel}\"\nldflags=\"${ldflags} -X ${version_pkg}.committime=${committime}\"\nif [ \"${GOARM:-}\" != '' ]; then\n\tldflags=\"${ldflags} -X ${version_pkg}.goarm=${GOARM}\"\nelif [ \"${GOMIPS:-}\" != '' ]; then\n\tldflags=\"${ldflags} -X ${version_pkg}.gomips=${GOMIPS}\"\nfi\nreadonly ldflags\n\n# Allow users to limit the build's parallelism.\nparallelism=\"${PARALLELISM:-}\"\nreadonly parallelism\n\n# Use GOFLAGS for -p, because -p=0 simply disables the build instead of leaving\n# the default value.\nif [ \"${parallelism}\" != '' ]; then\n\tGOFLAGS=\"${GOFLAGS:-} -p=${parallelism}\"\nfi\nreadonly GOFLAGS\nexport GOFLAGS\n\n# Allow users to specify a different output name.\nout=\"${OUT:-AdGuardHome}\"\nreadonly out\n\no_flags=\"-o=${out}\"\nreadonly o_flags\n\n# Allow users to enable the race detector.  Unfortunately, that means that cgo\n# must be enabled.\nif [ \"${RACE:-0}\" -eq '0' ]; then\n\tCGO_ENABLED='0'\n\trace_flags='--race=0'\nelse\n\tCGO_ENABLED='1'\n\trace_flags='--race=1'\nfi\nreadonly CGO_ENABLED race_flags\nexport CGO_ENABLED\n\n# Build the new binary if requested.\nif [ \"${NEXTAPI:-0}\" -eq '0' ]; then\n\ttags_flags='--tags='\nelse\n\ttags_flags='--tags=next'\nfi\nreadonly tags_flags\n\nif [ \"$verbose\" -gt '0' ]; then\n\t\"$go\" env\nfi\n\nif [ \"${COVER:-0}\" -eq '1' ]; then\n\tcover_flags='--cover=1'\nelse\n\tcover_flags='--cover=0'\nfi\n\n\"$go\" build \\\n\t\"$cover_flags\" \\\n\t--ldflags=\"$ldflags\" \\\n\t\"$race_flags\" \\\n\t\"$tags_flags\" \\\n\t--trimpath \\\n\t\"$o_flags\" \\\n\t\"$v_flags\" \\\n\t\"$x_flags\" \\\n\t;\n"
  },
  {
    "path": "scripts/make/go-deps.sh",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 2\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\nif [ \"$verbose\" -gt '1' ]; then\n\tenv\n\tset -x\n\tx_flags='-x=1'\nelif [ \"$verbose\" -gt '0' ]; then\n\tset -x\n\tx_flags='-x=0'\nelse\n\tset +x\n\tx_flags='-x=0'\nfi\nreadonly x_flags\n\nset -e -f -u\n\ngo=\"${GO:-go}\"\nreadonly go\n\n\"$go\" mod download \"$x_flags\"\n"
  },
  {
    "path": "scripts/make/go-fuzz.sh",
    "content": "#!/bin/sh\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\n# Verbosity levels:\n#   0 = Don't print anything except for errors.\n#   1 = Print commands, but not nested commands.\n#   2 = Print everything.\nif [ \"$verbose\" -gt '1' ]; then\n\tset -x\n\tv_flags='-v=1'\n\tx_flags='-x=1'\nelif [ \"$verbose\" -gt '0' ]; then\n\tset -x\n\tv_flags='-v=1'\n\tx_flags='-x=0'\nelse\n\tset +x\n\tv_flags='-v=0'\n\tx_flags='-x=0'\nfi\nreadonly v_flags x_flags\n\nset -e -f -u\n\nif [ \"${RACE:-1}\" -eq '0' ]; then\n\trace_flags='--race=0'\nelse\n\trace_flags='--race=1'\nfi\nreadonly race_flags\n\ngo=\"${GO:-go}\"\n\ncount_flags='--count=2'\nshuffle_flags='--shuffle=on'\ntimeout_flags=\"${TIMEOUT_FLAGS:---timeout=30s}\"\nfuzztime_flags=\"${FUZZTIME_FLAGS:---fuzztime=20s}\"\n\nreadonly go count_flags shuffle_flags timeout_flags fuzztime_flags\n\n# TODO(a.garipov): File an issue about using --fuzz with multiple packages.\n\"$go\" test \\\n\t\"$count_flags\" \\\n\t\"$shuffle_flags\" \\\n\t\"$race_flags\" \\\n\t\"$timeout_flags\" \\\n\t\"$x_flags\" \\\n\t\"$v_flags\" \\\n\t\"$fuzztime_flags\" \\\n\t--fuzz='.' \\\n\t--run='^$' \\\n\t./internal/filtering/rulelist/ \\\n\t;\n"
  },
  {
    "path": "scripts/make/go-lint.sh",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 18\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nfi\n\n# Set $EXIT_ON_ERROR to zero to see all errors.\nif [ \"${EXIT_ON_ERROR:-1}\" -eq '0' ]; then\n\tset +e\nelse\n\tset -e\nfi\n\nset -f -u\n\n# Source the common helpers, including not_found and run_linter.\n. ./scripts/make/helper.sh\n\n# Simple analyzers\n\n# blocklist_imports is a simple best-effort check against unwanted packages.\n# The following packages are banned:\n#\n#   *  Package errors is replaced by our own package in the\n#      github.com/AdguardTeam/golibs module.\n#\n#   *  Packages log and github.com/AdguardTeam/golibs/log are replaced by\n#      stdlib's new package log/slog and AdGuard's new utilities package\n#      github.com/AdguardTeam/golibs/logutil/slogutil.\n#\n#   *  Package github.com/prometheus/client_golang/prometheus/promauto is not\n#      recommended, as it encourages reliance on global state.\n#\n#   *  Packages golang.org/x/exp/maps, golang.org/x/exp/slices, and\n#      golang.org/x/net/context have been moved into stdlib.\n#\n#   *  Package io/ioutil is soft-deprecated.\n#\n#   *  Package reflect is often an overkill, and for deep comparisons there are\n#      much better functions in module github.com/google/go-cmp.  Which is\n#      already our indirect dependency and which may or may not enter the stdlib\n#      at some point.\n#\n#      See https://github.com/golang/go/issues/45200.\n#\n#   *  Package sort is replaced by package slices.\n#\n#   *  Package unsafe is… unsafe.\n#\n# Currently, the only standard exception are files generated from protobuf\n# schemas, which use package reflect.  If your project needs more exceptions,\n# add and document them.\n#\n# NOTE:  Flag -H for grep is non-POSIX but all of Busybox, GNU, macOS, and\n# OpenBSD support it.\n#\n# NOTE:  Exclude the security_windows.go, because it requires unsafe for the OS\n# APIs.\n#\n# TODO(a.garipov): Add golibs/log.\nblocklist_imports() {\n\timport_or_tab=\"$(printf '^\\\\(import \\\\|\\t\\\\)')\"\n\treadonly import_or_tab\n\n\tfind_with_ignore \\\n\t\t-type 'f' \\\n\t\t-name '*.go' \\\n\t\t'!' '(' \\\n\t\t-name '*.pb.go' \\\n\t\t-o -path './internal/permcheck/security_windows.go' \\\n\t\t')' \\\n\t\t-exec \\\n\t\t'grep' \\\n\t\t'-H' \\\n\t\t'-e' \"$import_or_tab\"'\"errors\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"github.com/prometheus/client_golang/prometheus/promauto\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"golang.org/x/exp/maps\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"golang.org/x/exp/slices\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"golang.org/x/net/context\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"io/ioutil\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"log\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"reflect\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"sort\"$' \\\n\t\t'-e' \"$import_or_tab\"'\"unsafe\"$' \\\n\t\t'-n' \\\n\t\t'{}' \\\n\t\t';'\n}\n\n# method_const is a simple check against the usage of some raw strings and\n# numbers where one should use named constants.\n#\n# NOTE:  Flag -H for grep is non-POSIX but all of Busybox, GNU, macOS, and\n# OpenBSD support it.\nmethod_const() {\n\tfind_with_ignore \\\n\t\t-type 'f' \\\n\t\t-name '*.go' \\\n\t\t-exec \\\n\t\t'grep' \\\n\t\t'-H' \\\n\t\t'-e' '\"DELETE\"' \\\n\t\t'-e' '\"GET\"' \\\n\t\t'-e' '\"PATCH\"' \\\n\t\t'-e' '\"POST\"' \\\n\t\t'-e' '\"PUT\"' \\\n\t\t'-n' \\\n\t\t'{}' \\\n\t\t';'\n}\n\n# underscores is a simple check against Go filenames with underscores.  Add new\n# build tags and OS as you go.  The main goal of this check is to discourage the\n# use of filenames like client_manager.go.\nunderscores() {\n\tunderscore_files=\"$(\n\t\tfind_with_ignore \\\n\t\t\t-type 'f' \\\n\t\t\t-name '*_*.go' \\\n\t\t\t'!' '(' \\\n\t\t\t-name '*_bsd.go' \\\n\t\t\t-o -name '*_darwin.go' \\\n\t\t\t-o -name '*_freebsd.go' \\\n\t\t\t-o -name '*_generate.go' \\\n\t\t\t-o -name '*_linux.go' \\\n\t\t\t-o -name '*_next.go' \\\n\t\t\t-o -name '*_openbsd.go' \\\n\t\t\t-o -name '*_others.go' \\\n\t\t\t-o -name '*_test.go' \\\n\t\t\t-o -name '*_unix.go' \\\n\t\t\t-o -name '*_windows.go' \\\n\t\t\t')' \\\n\t\t\t-exec 'printf' '\\t%s\\n' '{}' ';'\n\t)\"\n\treadonly underscore_files\n\n\tif [ \"$underscore_files\" != '' ]; then\n\t\tprintf \\\n\t\t\t'found file names with underscores:\\n%s\\n' \\\n\t\t\t\"$underscore_files\"\n\tfi\n}\n\ngo=\"${GO:-go}\"\nreadonly go\n\n# TODO(a.garipov): Add an analyzer to look for `fallthrough`, `goto`, and `new`?\n\n# Checks\n\nrun_linter -e blocklist_imports\n\nrun_linter -e method_const\n\nrun_linter -e underscores\n\nrun_linter -e \"$go\" tool gofumpt --extra -e -l .\n\nrun_linter \"${GO:-go}\" vet work\n\n# govulncheck is not stricly reproducible, because it queries the VulnDB, which\n# is updated constantly.  If a stricly reproducible lint is desired, for example\n# for Docker lint stages, set IGNORE_NON_REPRODUCIBLE to 1 to ignore the exit\n# code from govulncheck.\n#\n# TODO(a.garipov):  Return the default to 0 and update the Go version once\n# https://github.com/quic-go/quic-go/issues/5543 is fixed.\nif [ \"${IGNORE_NON_REPRODUCIBLE:-1}\" -gt '0' ]; then\n\t# run_linter calls set +e, so don't mind the cancelling effect of ||.\n\t# shellcheck disable=SC2310\n\trun_linter \"$go\" tool govulncheck work || :\nelse\n\trun_linter \"$go\" tool govulncheck work\nfi\n\nrun_linter \"$go\" tool gocyclo --over 10 .\n\n# TODO(a.garipov): Enable 10 for all.\nrun_linter \"$go\" tool gocognit --over='20' \\\n\t./internal/querylog/ \\\n\t;\n\nrun_linter \"$go\" tool gocognit --over='14' \\\n\t./internal/dhcpd \\\n\t;\n\nrun_linter \"$go\" tool gocognit --over='10' \\\n\t./internal/aghalg/ \\\n\t./internal/aghhttp/ \\\n\t./internal/aghnet/ \\\n\t./internal/aghos/ \\\n\t./internal/aghrenameio/ \\\n\t./internal/aghtest/ \\\n\t./internal/aghtls/ \\\n\t./internal/aghuser/ \\\n\t./internal/arpdb/ \\\n\t./internal/client/ \\\n\t./internal/configmigrate/ \\\n\t./internal/dhcpsvc \\\n\t./internal/dnsforward/ \\\n\t./internal/filtering/ \\\n\t./internal/home/ \\\n\t./internal/ipset \\\n\t./internal/next/ \\\n\t./internal/ossvc/ \\\n\t./internal/rdns/ \\\n\t./internal/schedule/ \\\n\t./internal/stats/ \\\n\t./internal/updater/ \\\n\t./internal/version/ \\\n\t./internal/whois/ \\\n\t./scripts/ \\\n\t;\n\nrun_linter \"$go\" tool ineffassign work\n\nrun_linter \"$go\" tool unparam work\n\nfind_with_ignore \\\n\t-type 'f' \\\n\t'(' \\\n\t-name 'Makefile' \\\n\t-o -name '*.conf' \\\n\t-o -name '*.go' \\\n\t-o -name '*.mod' \\\n\t-o -name '*.sh' \\\n\t-o -name '*.yaml' \\\n\t-o -name '*.yml' \\\n\t')' \\\n\t-exec \"$go\" 'tool' 'misspell' '--error' '{}' '+'\n\nrun_linter \"$go\" tool nilness work\n\n# TODO(a.garipov): Enable for all.\nrun_linter \"$go\" tool fieldalignment \\\n\t./internal/aghalg/ \\\n\t./internal/aghhttp/ \\\n\t./internal/aghos/ \\\n\t./internal/aghrenameio/ \\\n\t./internal/aghtest/ \\\n\t./internal/aghtls/ \\\n\t./internal/aghuser/ \\\n\t./internal/arpdb/ \\\n\t./internal/client/ \\\n\t./internal/configmigrate/ \\\n\t./internal/dhcpsvc/ \\\n\t./internal/filtering/hashprefix/ \\\n\t./internal/filtering/rewrite/ \\\n\t./internal/filtering/rulelist/ \\\n\t./internal/filtering/safesearch/ \\\n\t./internal/ipset/ \\\n\t./internal/next/... \\\n\t./internal/ossvc/ \\\n\t./internal/querylog/ \\\n\t./internal/rdns/ \\\n\t./internal/schedule/ \\\n\t./internal/stats/ \\\n\t./internal/updater/ \\\n\t./internal/version/ \\\n\t./internal/whois/ \\\n\t;\n\nrun_linter -e \"$go\" tool shadow --strict work\n\n# TODO(a.garipov): Enable for all.\n# TODO(e.burkov):  Re-enable G115.\nrun_linter \"$go\" tool gosec --exclude=G115 --fmt=golint --quiet \\\n\t./internal/aghalg/ \\\n\t./internal/aghhttp/ \\\n\t./internal/aghnet/ \\\n\t./internal/aghos/ \\\n\t./internal/aghrenameio/ \\\n\t./internal/aghtest/ \\\n\t./internal/aghuser/ \\\n\t./internal/arpdb/ \\\n\t./internal/client/ \\\n\t./internal/configmigrate/ \\\n\t./internal/dhcpd/ \\\n\t./internal/dhcpsvc/ \\\n\t./internal/dnsforward/ \\\n\t./internal/filtering/hashprefix/ \\\n\t./internal/filtering/rewrite/ \\\n\t./internal/filtering/rulelist/ \\\n\t./internal/filtering/safesearch/ \\\n\t./internal/ipset/ \\\n\t./internal/next/ \\\n\t./internal/ossvc/ \\\n\t./internal/rdns/ \\\n\t./internal/schedule/ \\\n\t./internal/stats/ \\\n\t./internal/version/ \\\n\t./internal/whois/ \\\n\t;\n\nrun_linter \"$go\" tool errcheck work\n\nrun_linter \"$go\" tool staticcheck --matrix work <<-'EOF'\n\tdarwin:  GOOS=darwin\n\tfreebsd: GOOS=freebsd\n\tlinux:   GOOS=linux\n\topenbsd: GOOS=openbsd\n\twindows: GOOS=windows\nEOF\n"
  },
  {
    "path": "scripts/make/go-test.sh",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 8\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\n# Verbosity levels:\n#   0 = Don't print anything except for errors.\n#   1 = Print commands, but not nested commands.\n#   2 = Print everything.\nif [ \"$verbose\" -gt '1' ]; then\n\tset -x\n\tv_flags='-v=1'\n\tx_flags='-x=1'\nelif [ \"$verbose\" -gt '0' ]; then\n\tset -x\n\tv_flags='-v=1'\n\tx_flags='-x=0'\nelse\n\tset +x\n\tv_flags='-v=0'\n\tx_flags='-x=0'\nfi\nreadonly v_flags x_flags\n\nset -e -f -u\n\nif [ \"${RACE:-1}\" -eq '0' ]; then\n\trace_flags='--race=0'\nelse\n\trace_flags='--race=1'\nfi\nreadonly race_flags\n\ncount_flags='--count=2'\ncover_flags='--coverprofile=./cover.out'\ngo=\"${GO:-go}\"\nshuffle_flags='--shuffle=on'\ntimeout_flags=\"${TIMEOUT_FLAGS:---timeout=90s}\"\nreadonly count_flags cover_flags go shuffle_flags timeout_flags\n\ngo_test() {\n\t\"$go\" test \\\n\t\t\"$count_flags\" \\\n\t\t\"$cover_flags\" \\\n\t\t\"$race_flags\" \\\n\t\t\"$shuffle_flags\" \\\n\t\t\"$timeout_flags\" \\\n\t\t\"$v_flags\" \\\n\t\t\"$x_flags\" \\\n\t\twork \\\n\t\t;\n}\n\ntest_reports_dir=\"${TEST_REPORTS_DIR:-}\"\nreadonly test_reports_dir\n\nif [ \"$test_reports_dir\" = '' ]; then\n\tgo_test\n\n\texit \"$?\"\nfi\n\nmkdir -p \"$test_reports_dir\"\n\n# NOTE:  The pipe ignoring the exit code here is intentional, as go-junit-report\n# will set the exit code to be saved.\ngo_test 2>&1 \\\n\t| tee \"${test_reports_dir}/test-output.txt\"\n\n# Don't fail on errors in exporting, because TEST_REPORTS_DIR is generally only\n# not empty in CI, and so the exit code must be preserved to exit with it later.\nset +e\n\"${GO:-go}\" tool go-junit-report \\\n\t--in \"${test_reports_dir}/test-output.txt\" \\\n\t--set-exit-code \\\n\t>\"${test_reports_dir}/test-report.xml\"\nprintf '%s\\n' \"$?\" \\\n\t>\"${test_reports_dir}/test-exit-code.txt\"\n"
  },
  {
    "path": "scripts/make/go-upd-tools.sh",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 4\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\nif [ \"$verbose\" -gt '1' ]; then\n\tenv\n\tset -x\n\tx_flags='-x=1'\nelif [ \"$verbose\" -gt '0' ]; then\n\tset -x\n\tx_flags='-x=0'\nelse\n\tset +x\n\tx_flags='-x=0'\nfi\nreadonly x_flags\n\nset -e -f -u\n\ngo=\"${GO:-go}\"\nreadonly go\n\n\"$go\" get -u \"$x_flags\" tool\n\"$go\" mod tidy \"$x_flags\"\n"
  },
  {
    "path": "scripts/make/helper.sh",
    "content": "#!/bin/sh\n\n# Common script helpers\n#\n# This file contains common script helpers.  It should be sourced in scripts\n# right after the initial environment processing.\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 7\n\n# Deferred helpers\n\n# TODO(f.setrakov): Consider removing.\nnot_found_msg='\nlooks like a binary not found error.\nmake sure you have installed the linter binaries.\n'\nreadonly not_found_msg\n\nnot_found() {\n\tif [ \"$?\" -eq '127' ]; then\n\t\t# Code 127 is the exit status a shell uses when a command or a file is\n\t\t# not found, according to the Bash Hackers wiki.\n\t\t#\n\t\t# See https://wiki.bash-hackers.org/dict/terms/exit_status.\n\t\techo \"$not_found_msg\" 1>&2\n\tfi\n}\ntrap not_found EXIT\n\n# Helpers\n\n# run_linter runs the given linter with two additions:\n#\n# 1.  If the first argument is \"-e\", run_linter exits with a nonzero exit code\n#     if there is anything in the command's combined output.\n#\n# 2.  In any case, run_linter adds the program's name to its combined output.\nrun_linter() (\n\tset +e\n\n\tif [ \"${VERBOSE:-0}\" -lt '2' ]; then\n\t\tset +x\n\tfi\n\n\tcmd=\"${1:?run_linter: provide a command}\"\n\tshift\n\n\texit_on_output='0'\n\tif [ \"$cmd\" = '-e' ]; then\n\t\texit_on_output='1'\n\t\tcmd=\"${1:?run_linter: provide a command}\"\n\t\tshift\n\tfi\n\n\treadonly cmd\n\n\toutput=\"$(\"$cmd\" \"$@\" 2>&1)\"\n\texitcode=\"$?\"\n\n\treadonly output\n\n\tif [ \"$output\" != '' ]; then\n\t\t# Print the correct prefix for linter output.  For example, print the\n\t\t# tool name for a \"go tool\" call, or \"vet\" for a \"go vet\" call.\n\t\tprefix=\"$cmd\"\n\t\tif [ \"$#\" -ge '3' ] && [ \"$1\" = 'tool' ]; then\n\t\t\tprefix=\"$2\"\n\t\telif [ \"$#\" -ge '2' ] && [ \"$1\" = 'vet' ]; then\n\t\t\tprefix=\"$1\"\n\t\tfi\n\n\t\treadonly prefix\n\n\t\techo \"$output\" | sed -e \"s/^/${prefix}: /\"\n\n\t\tif [ \"$exitcode\" -eq '0' ] && [ \"$exit_on_output\" -eq '1' ]; then\n\t\t\texitcode='1'\n\t\tfi\n\tfi\n\n\treturn \"$exitcode\"\n)\n\n# find_with_ignore is a wrapper around find that does not descend into ignored\n# directories, such as ./tmp/.\n#\n# NOTE:  The arguments must contain one of -exec, -ok, or -print; see\n# https://pubs.opengroup.org/onlinepubs/9799919799/utilities/find.html.\n#\n# TODO(a.garipov):  Find a way to integrate the entire gitignore, including the\n# global one, without using git, as .git is not copied into the build container.\n#\n# Keep in sync with .gitignore.\nfind_with_ignore() {\n\tfind . \\\n\t\t'(' \\\n\t\t-type 'd' \\\n\t\t'(' \\\n\t\t-name '.git' \\\n\t\t-o -path '/agh-backup' \\\n\t\t-o -path './bin' \\\n\t\t-o -path './build' \\\n\t\t-o -path './client/blob-report' \\\n\t\t-o -path './client/playwright-report' \\\n\t\t-o -path './client/playwright/.cache' \\\n\t\t-o -path './client/test-results' \\\n\t\t-o -path './data' \\\n\t\t-o -path './dist' \\\n\t\t-o -path './launchpad_credentials' \\\n\t\t-o -path './snapcraft_login' \\\n\t\t-o -name 'node_modules' \\\n\t\t-o -name 'test-reports' \\\n\t\t-o -name 'tmp' \\\n\t\t')' \\\n\t\t-prune \\\n\t\t')' \\\n\t\t-o \\\n\t\t\"$@\" \\\n\t\t;\n}\n"
  },
  {
    "path": "scripts/make/md-lint.sh",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 3\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\nset -e -f -u\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nfi\n\n# TODO(e.burkov):  Add README.md and possibly AGHTechDoc.md.\nmarkdownlint \\\n\t./CHANGELOG.md \\\n\t./CONTRIBUTING.md \\\n\t./HACKING.md \\\n\t./SECURITY.md \\\n\t./internal/next/changelog.md \\\n\t./internal/dhcpd/*.md \\\n\t./openapi/*.md \\\n\t./scripts/*.md \\\n\t;\n"
  },
  {
    "path": "scripts/make/sh-lint.sh",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 4\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\n# Don't use -f, because we use globs in this script.\nset -e -u\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nfi\n\n# Source the common helpers, including not_found and run_linter.\n. ./scripts/make/helper.sh\n\ngo=\"${GO:-go}\"\nreadonly go\n\nrun_linter -e \"$go\" tool shfmt --binary-next-line -d -p -s \\\n\t./scripts/hooks/* \\\n\t./scripts/install.sh \\\n\t./scripts/make/*.sh \\\n\t./scripts/snap/*.sh \\\n\t./snap/local/*.sh \\\n\t;\n\nshellcheck -e 'SC2250' -e 'SC2310' -f 'gcc' -o 'all' -x -- \\\n\t./scripts/hooks/* \\\n\t./scripts/install.sh \\\n\t./scripts/make/*.sh \\\n\t./scripts/snap/*.sh \\\n\t./snap/local/*.sh \\\n\t;\n"
  },
  {
    "path": "scripts/make/txt-lint.sh",
    "content": "#!/bin/sh\n\n# This comment is used to simplify checking local copies of the script.  Bump\n# this number every time a significant change is made to this script.\n#\n# AdGuard-Project-Version: 12\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nfi\n\n# Set $EXIT_ON_ERROR to zero to see all errors.\nif [ \"${EXIT_ON_ERROR:-1}\" -eq '0' ]; then\n\tset +e\nelse\n\tset -e\nfi\n\n# We don't need glob expansions and we want to see errors about unset variables.\nset -f -u\n\n# Source the common helpers, including not_found.\n. ./scripts/make/helper.sh\n\n# Simple analyzers\n\n# trailing_newlines is a simple check that makes sure that all plain-text files\n# have a trailing newlines to make sure that all tools work correctly with them.\ntrailing_newlines() (\n\tnl=\"$(printf '\\n')\"\n\treadonly nl\n\n\tfind_with_ignore \\\n\t\t-type 'f' \\\n\t\t'!' '(' \\\n\t\t-name '*.db' \\\n\t\t-o -name '*.exe' \\\n\t\t-o -name '*.out' \\\n\t\t-o -name '*.png' \\\n\t\t-o -name '*.svg' \\\n\t\t-o -name '*.tar.gz' \\\n\t\t-o -name '*.test' \\\n\t\t-o -name '*.zip' \\\n\t\t-o -name 'AdGuardHome' \\\n\t\t-o -name 'adguard-home' \\\n\t\t')' \\\n\t\t-print \\\n\t\t| while read -r f; do\n\t\t\tfinal_byte=\"$(tail -c -1 \"$f\")\"\n\t\t\tif [ \"$final_byte\" != \"$nl\" ]; then\n\t\t\t\tprintf '%s: must have a trailing newline\\n' \"$f\"\n\t\t\tfi\n\t\tdone\n)\n\n# trailing_whitespace is a simple check that makes sure that there are no\n# trailing whitespace in plain-text files.\ntrailing_whitespace() {\n\tfind_with_ignore \\\n\t\t-type 'f' \\\n\t\t'!' '(' \\\n\t\t-name '*.db' \\\n\t\t-o -name '*.exe' \\\n\t\t-o -name '*.out' \\\n\t\t-o -name '*.png' \\\n\t\t-o -name '*.svg' \\\n\t\t-o -name '*.tar.gz' \\\n\t\t-o -name '*.test' \\\n\t\t-o -name '*.zip' \\\n\t\t-o -name 'AdGuardHome' \\\n\t\t-o -name 'adguard-home' \\\n\t\t')' \\\n\t\t-print \\\n\t\t| while read -r f; do\n\t\t\tgrep -e '[[:space:]]$' -n -- \"$f\" \\\n\t\t\t\t| sed -e \"s:^:${f}\\::\" -e 's/ \\+$/>>>&<<</'\n\t\tdone\n}\n\n# valid_json check ensures that all the .json files in the project are valid and\n# well-formatted according to the jq.\n#\n# TODO(e.burkov):  Include tsconfig.json when it stop containing comments.\nvalid_json() {\n\tfind_with_ignore \\\n\t\t-type 'f' \\\n\t\t-name '*.json' \\\n\t\t'!' '(' \\\n\t\t-name 'tsconfig.json' \\\n\t\t')' \\\n\t\t-print \\\n\t\t| while read -r f; do\n\t\t\tvalidation_msg=\"$(jq empty \"$f\" 2>&1)\"\n\t\t\texitcode=\"$?\"\n\n\t\t\tif [ \"$exitcode\" -ne '0' ]; then\n\t\t\t\tprintf 'file %s: %s\\n' \"$f\" \"$validation_msg\"\n\n\t\t\t\tcontinue\n\t\t\tfi\n\n\t\t\tif ! jq . \"$f\" | diff -u \"$f\" - >/dev/null 2>&1; then\n\t\t\t\tprintf 'file %s has formatting issues\\n' \"$f\"\n\t\t\tfi\n\t\tdone\n}\n\nrun_linter -e trailing_newlines\n\nrun_linter -e trailing_whitespace\n\nrun_linter -e valid_json\n\ngo=\"${GO:-go}\"\nreadonly go\n\nfind_with_ignore \\\n\t-type 'f' \\\n\t'(' \\\n\t-name 'Makefile' \\\n\t-o -name '*.conf' \\\n\t-o -name '*.md' \\\n\t-o -name '*.txt' \\\n\t-o -name '*.yaml' \\\n\t-o -name '*.yml' \\\n\t')' \\\n\t-exec \"$go\" 'tool' 'misspell' '--error' '{}' '+' \\\n\t;\n"
  },
  {
    "path": "scripts/make/version.sh",
    "content": "#!/bin/sh\n\n# AdGuard Home Version Generation Script\n#\n# This script generates versions based on the current git tree state.  The valid\n# output formats are:\n#\n#  *  For release versions, \"v0.123.4\".  This version should be the one in the\n#     current tag, and the script merely checks, that the current commit is\n#     properly tagged.\n#\n#  *  For prerelease beta versions, \"v0.123.4-b.5\".  This version should be the\n#     one in the current tag, and the script merely checks, that the current\n#     commit is properly tagged.\n#\n#  *  For prerelease alpha versions (aka snapshots), \"v0.123.4-a.6+a1b2c3d4\".\n#\n# BUG(a.garipov): The script currently can't differentiate between beta tags and\n# release tags if they are on the same commit, so the beta tag **must** be\n# pushed and built **before** the release tag is pushed.\n#\n# TODO(a.garipov): The script currently doesn't handle release branches, so it\n# must be modified once we have those.\n\nverbose=\"${VERBOSE:-0}\"\nreadonly verbose\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nfi\n\nset -e -f -u\n\n# bump_minor is an awk program that reads a minor release version, increments\n# the minor part of it, and prints the next version.\n#\n# shellcheck disable=SC2016\nbump_minor='/^v[0-9]+\\.[0-9]+\\.0$/ {\n\tprint($1 \".\" $2 + 1 \".0\");\n\n\tnext;\n}\n\n{\n\tprintf(\"invalid minor release version: \\\"%s\\\"\\n\", $0);\n\n\texit 1;\n}'\nreadonly bump_minor\n\n# get_last_minor_zero returns the last new minor release.\nget_last_minor_zero() {\n\t# List all tags.  Then, select those that fit the pattern of a new minor\n\t# release: a semver version with the patch part set to zero.\n\t#\n\t# Then, sort them first by the first field (\"1\"), starting with the\n\t# second character to skip the \"v\" prefix (\".2\"), and only spanning the\n\t# first field (\",1\").  The sort is numeric and reverse (\"nr\").\n\t#\n\t# Then, sort them by the second field (\"2\"), and only spanning the\n\t# second field (\",2\").  The sort is also numeric and reverse (\"nr\").\n\t#\n\t# Finally, get the top (that is, most recent) version.\n\tgit tag \\\n\t\t| grep -e 'v[0-9]\\+\\.[0-9]\\+\\.0$' \\\n\t\t| sort -k 1.2,1nr -k 2,2nr -t '.' \\\n\t\t| head -n 1 \\\n\t\t;\n}\n\nchannel=\"${CHANNEL:?please set CHANNEL}\"\nreadonly channel\n\ncase \"$channel\" in\n'development')\n\t# commit_number is the number of current commit within the branch.\n\tcommit_number=\"$(git rev-list --count master..HEAD)\"\n\treadonly commit_number\n\n\t# The development builds are described with a combination of unset semantic\n\t# version, the commit's number within the branch, and the commit hash, e.g.:\n\t#\n\t#   v0.0.0-dev.5-a1b2c3d4\n\t#\n\tversion=\"v0.0.0-dev.${commit_number}+$(git rev-parse --short HEAD)\"\n\t;;\n'edge')\n\t# last_minor_zero is the last new minor release.\n\tlast_minor_zero=\"$(get_last_minor_zero)\"\n\treadonly last_minor_zero\n\n\t# num_commits_since_minor is the number of commits since the last new\n\t# minor release.  If the current commit is the new minor release,\n\t# num_commits_since_minor is zero.\n\tnum_commits_since_minor=\"$(git rev-list --count \"${last_minor_zero}..HEAD\")\"\n\treadonly num_commits_since_minor\n\n\t# next_minor is the next minor release version.\n\tnext_minor=\"$(echo \"$last_minor_zero\" | awk -F '.' \"$bump_minor\")\"\n\treadonly next_minor\n\n\t# Make this commit a prerelease version for the next minor release.  For\n\t# example, if the last minor release was v0.123.0, and the current\n\t# commit is the fifth since then, the version will look something like:\n\t#\n\t#   v0.124.0-a.5+a1b2c3d4\n\t#\n\tversion=\"${next_minor}-a.${num_commits_since_minor}+$(git rev-parse --short HEAD)\"\n\t;;\n'beta' | 'release')\n\t# current_desc is the description of the current git commit.  If the\n\t# current commit is tagged, git describe will show the tag.\n\tcurrent_desc=\"$(git describe)\"\n\treadonly current_desc\n\n\t# last_tag is the most recent git tag.\n\tlast_tag=\"$(git describe --abbrev=0)\"\n\treadonly last_tag\n\n\t# Require an actual tag for the beta and final releases.\n\tif [ \"$current_desc\" != \"$last_tag\" ]; then\n\t\techo 'need a tag' 1>&2\n\n\t\texit 1\n\tfi\n\n\tversion=\"$last_tag\"\n\t;;\n'candidate')\n\t# This pseudo-channel is used to set a proper versions into release\n\t# candidate builds.\n\n\t# last_tag is expected to be the latest release tag.\n\tlast_tag=\"$(git describe --abbrev=0)\"\n\treadonly last_tag\n\n\t# current_branch is the name of the branch currently checked out.\n\tcurrent_branch=\"$(git rev-parse --abbrev-ref HEAD)\"\n\treadonly current_branch\n\n\t# The branch should be named like:\n\t#\n\t#   rc-v12.34.56\n\t#\n\tif ! echo \"$current_branch\" | grep -E -e '^rc-v[0-9]+\\.[0-9]+\\.[0-9]+$' -q; then\n\t\techo \"invalid release candidate branch name '$current_branch'\" 1>&2\n\n\t\texit 1\n\tfi\n\n\tversion=\"${current_branch#rc-}-rc.$(git rev-list --count \"$last_tag\"..HEAD)\"\n\t;;\n*)\n\techo \"invalid channel '$channel', supported values are \\\n\t\t'development', 'edge', 'beta', 'release' and 'candidate'\" 1>&2\n\texit 1\n\t;;\nesac\n\n# Finally, make sure that we don't output invalid versions.\nif ! echo \"$version\" | grep -E -e '^v[0-9]+\\.[0-9]+\\.[0-9]+(-(a|b|dev|rc)\\.[0-9]+)?(\\+[[:xdigit:]]+)?$' -q; then\n\techo \"generated an invalid version '$version'\" 1>&2\n\n\texit 1\nfi\n\necho \"$version\"\n"
  },
  {
    "path": "scripts/snap/build.sh",
    "content": "#!/bin/sh\n\nverbose=\"${VERBOSE:-0}\"\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nfi\n\nset -e -f -u\n\n# Function log is an echo wrapper that writes to stderr if the caller requested\n# verbosity level greater than 0.  Otherwise, it does nothing.\n#\n# TODO(a.garipov): Add to helpers.sh and use more actively in scripts.\nlog() {\n\tif [ \"$verbose\" -gt '0' ]; then\n\t\t# Don't use quotes to get word splitting.\n\t\techo \"$1\" 1>&2\n\tfi\n}\n\n# Allow developers to overwrite the command, e.g. for testing.\nsnapcraft_cmd=\"${SNAPCRAFT_CMD:-snapcraft}\"\nreadonly snapcraft_cmd\n\nversion=\"$(./AdGuardHome_amd64 --version | cut -d ' ' -f 4)\"\nif [ \"$version\" = '' ]; then\n\tlog 'empty version from ./AdGuardHome_amd64'\n\n\texit 1\nfi\nreadonly version\n\nlog \"version '$version'\"\n\nfor arch in \\\n\t'amd64' \\\n\t'arm64' \\\n\t'armhf' \\\n\t'i386'; do\n\tbuild_output=\"./AdGuardHome_${arch}\"\n\tsnap_output=\"./AdGuardHome_${arch}.snap\"\n\tsnap_dir=\"${snap_output}.dir\"\n\n\t# Create the meta subdirectory and copy files there.\n\tmkdir -p \"${snap_dir}/meta\"\n\tcp \"$build_output\" \"${snap_dir}/AdGuardHome\"\n\tcp './snap/local/adguard-home-web.sh' \"$snap_dir\"\n\tcp -r './snap/gui' \"${snap_dir}/meta/\"\n\n\t# Set required permissions.\n\tchmod o+rx \"${snap_dir}/adguard-home-web.sh\"\n\tchmod o+rx \"${snap_dir}/meta/gui\"\n\tchmod o+r \"${snap_dir}/meta/gui/adguard-home-web.desktop\"\n\tchmod o+r \"${snap_dir}/meta/gui/adguard-home-web.png\"\n\n\t# Create a snap.yaml file, setting the values.\n\tsed \\\n\t\t-e 's/%VERSION%/'\"$version\"'/' \\\n\t\t-e 's/%ARCH%/'\"$arch\"'/' \\\n\t\t./snap/snap.tmpl.yaml \\\n\t\t>\"${snap_dir}/meta/snap.yaml\"\n\n\t\"$snapcraft_cmd\" pack \"$snap_dir\" --output \"$snap_output\"\n\n\tlog \"$snap_output\"\n\n\trm -f -r \"$snap_dir\"\ndone\n"
  },
  {
    "path": "scripts/snap/download.sh",
    "content": "#!/bin/sh\n\nverbose=\"${VERBOSE:-0}\"\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nfi\n\nset -e -f -u\n\nchannel=\"${CHANNEL:?please set CHANNEL}\"\nreadonly channel\n\nwhile read -r arch snap_arch; do\n\trelease_url=\"https://static.adtidy.org/adguardhome/${channel}/AdGuardHome_linux_${arch}.tar.gz\"\n\toutput=\"./AdGuardHome_linux_${arch}.tar.gz\"\n\n\tcurl -o \"$output\" -v \"$release_url\"\n\ttar -f \"$output\" -v -x -z\n\tcp ./AdGuardHome/AdGuardHome \"./AdGuardHome_${snap_arch}\"\n\trm -f -r \"$output\" ./AdGuardHome\ndone <<-'EOF'\n\t386   i386\n\tamd64 amd64\n\tarmv7 armhf\n\tarm64 arm64\nEOF\n"
  },
  {
    "path": "scripts/snap/upload.sh",
    "content": "#!/bin/sh\n\nverbose=\"${VERBOSE:-0}\"\n\nif [ \"$verbose\" -gt '0' ]; then\n\tset -x\nfi\n\nset -e -f -u\n\n# Function log is an echo wrapper that writes to stderr if the caller requested\n# verbosity level greater than 0.  Otherwise, it does nothing.\nlog() {\n\tif [ \"$verbose\" -gt '0' ]; then\n\t\t# Don't use quotes to get word splitting.\n\t\techo \"$1\" 1>&2\n\tfi\n}\n\n# Do not set a new lowercase variable, because the snapcraft tool expects the\n# uppercase form.\nif [ \"${SNAPCRAFT_STORE_CREDENTIALS:-}\" = '' ]; then\n\tlog 'please set SNAPCRAFT_STORE_CREDENTIALS'\n\n\texit 1\nfi\nexport SNAPCRAFT_STORE_CREDENTIALS\n\nsnapcraft_channel=\"${SNAPCRAFT_CHANNEL:?please set SNAPCRAFT_CHANNEL}\"\nreadonly snapcraft_channel\n\n# Allow developers to overwrite the command, e.g. for testing.\nsnapcraft_cmd=\"${SNAPCRAFT_CMD:-snapcraft}\"\nreadonly snapcraft_cmd\n\ndefault_timeout='90s'\nkill_timeout='120s'\nreadonly default_timeout kill_timeout\n\nfor arch in \\\n\t'amd64' \\\n\t'arm64' \\\n\t'armhf' \\\n\t'i386'; do\n\tsnap_file=\"./AdGuardHome_${arch}.snap\"\n\n\t# Catch the exit code and the combined output to later inspect it.\n\tset +e\n\tsnapcraft_output=\"$(\n\t\t# Use timeout(1) to force snapcraft to quit after a certain time.  There\n\t\t# seems to be no environment variable or flag to force this behavior.\n\t\ttimeout \\\n\t\t\t--preserve-status \\\n\t\t\t-k \"$kill_timeout\" \\\n\t\t\t-v \"$default_timeout\" \\\n\t\t\t\"$snapcraft_cmd\" upload \\\n\t\t\t--release=\"${snapcraft_channel}\" \\\n\t\t\t--quiet \\\n\t\t\t\"${snap_file}\" \\\n\t\t\t2>&1\n\t)\"\n\tsnapcraft_exit_code=\"$?\"\n\tset -e\n\n\tif [ \"$snapcraft_exit_code\" -eq '0' ]; then\n\t\tlog \"successful upload: ${snapcraft_output}\"\n\n\t\tcontinue\n\tfi\n\n\t# Skip the ones that were failed by a duplicate upload error.\n\tcase \"$snapcraft_output\" in\n\t*'A file with this exact same content has already been uploaded'* | \\\n\t\t*'Error checking upload uniqueness'*)\n\n\t\tlog \"warning: duplicate upload, skipping\"\n\t\tlog \"snapcraft upload error: ${snapcraft_output}\"\n\n\t\tcontinue\n\t\t;;\n\t*)\n\t\techo \"unexpected snapcraft upload error: ${snapcraft_output}\"\n\n\t\treturn \"$snapcraft_exit_code\"\n\t\t;;\n\tesac\ndone\n"
  },
  {
    "path": "scripts/translations/download.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/ioutil\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/netutil\"\n\t\"github.com/AdguardTeam/golibs/syncutil\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n)\n\n// download and save all translations.\nfunc (c *twoskyClient) download(ctx context.Context, l *slog.Logger) {\n\tnumWorker, err := parseDownloadArgs()\n\tif err != nil {\n\t\tusage(err.Error())\n\t}\n\n\tdownloadURI := c.uri.JoinPath(\"download\")\n\n\twg := &sync.WaitGroup{}\n\treqCh := make(chan downloadRequest, numWorker)\n\n\tdw := &downloadWorker{\n\t\tctx:    ctx,\n\t\tl:      l,\n\t\tfailed: syncutil.NewMap[string, struct{}](),\n\t\tclient: &http.Client{\n\t\t\tTimeout: 10 * time.Second,\n\t\t},\n\t\treqCh: reqCh,\n\t}\n\n\tfor range numWorker {\n\t\twg.Go(dw.run)\n\t}\n\n\tfor _, baseFile := range c.localizableFiles {\n\t\tdir, file := filepath.Split(baseFile)\n\n\t\tfor _, lang := range c.langs {\n\t\t\turi := translationURL(downloadURI, file, c.projectID, lang)\n\n\t\t\treqCh <- downloadRequest{\n\t\t\t\turi: uri,\n\t\t\t\tdir: dir,\n\t\t\t}\n\t\t}\n\t}\n\n\tclose(reqCh)\n\twg.Wait()\n\n\tprintFailedLocales(ctx, l, dw.failed)\n}\n\n// parseDownloadArgs parses command-line arguments for the download command.\nfunc parseDownloadArgs() (numWorker int, err error) {\n\tflagSet := flag.NewFlagSet(\"download\", flag.ExitOnError)\n\tflagSet.IntVar(&numWorker, \"n\", 1, \"number of concurrent downloads\")\n\n\terr = flagSet.Parse(os.Args[2:])\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn 0, err\n\t}\n\n\treturn numWorker, validate.Positive(\"count\", numWorker)\n}\n\n// printFailedLocales prints sorted list of failed downloads, if any.  l and\n// failed must not be nil.\nfunc printFailedLocales(\n\tctx context.Context,\n\tl *slog.Logger,\n\tfailed *syncutil.Map[string, struct{}],\n) {\n\tvar keys []string\n\tfor k := range failed.Range {\n\t\tkeys = append(keys, k)\n\t}\n\n\tif len(keys) == 0 {\n\t\treturn\n\t}\n\n\tslices.Sort(keys)\n\n\tl.InfoContext(ctx, \"failed\", \"locales\", keys)\n}\n\n// downloadWorker is a worker for downloading translations.  It uses URLs\n// received from the channel to download translations and save them to files.\n// Failures are stored in the failed map.  All fields must not be nil.\ntype downloadWorker struct {\n\tctx    context.Context\n\tl      *slog.Logger\n\tfailed *syncutil.Map[string, struct{}]\n\tclient *http.Client\n\treqCh  <-chan downloadRequest\n}\n\n// downloadRequest is a request to download a translation.  All fields must not\n// be empty.\ntype downloadRequest struct {\n\turi *url.URL\n\tdir string\n}\n\n// run handles the channel of URLs, one by one.  It returns when the channel is\n// closed.  It's used to be run in a separate goroutine.\nfunc (w *downloadWorker) run() {\n\tfor req := range w.reqCh {\n\t\tq := req.uri.Query()\n\t\tcode := q.Get(\"language\")\n\n\t\terr := saveToFile(w.ctx, w.l, w.client, req.uri, code, req.dir)\n\t\tif err != nil {\n\t\t\tw.l.ErrorContext(w.ctx, \"download worker\", slogutil.KeyError, err)\n\t\t\tw.failed.Store(code, struct{}{})\n\t\t}\n\t}\n}\n\n// saveToFile downloads translation by url and saves it to a file, or returns\n// error.\nfunc saveToFile(\n\tctx context.Context,\n\tl *slog.Logger,\n\tclient *http.Client,\n\turi *url.URL,\n\tcode string,\n\tlocalesDir string,\n) (err error) {\n\tdata, err := getTranslation(ctx, l, client, uri.String())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting translation %q: %s\", code, err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tenc := json.NewEncoder(buf)\n\tenc.SetIndent(\"\", \"  \")\n\tenc.SetEscapeHTML(false)\n\n\terr = enc.Encode(data)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"encoding translation %q: %w\", code, err)\n\t}\n\n\tname := filepath.Join(localesDir, code+\".json\")\n\terr = os.WriteFile(name, buf.Bytes(), 0o664)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"writing file: %w\", err)\n\t}\n\n\tfmt.Println(name)\n\n\treturn nil\n}\n\n// getTranslation returns received translation data and error.  If err is not\n// nil, data may contain a response from server for inspection.  Otherwise, the\n// data is guaranteed to be non-empty.\nfunc getTranslation(\n\tctx context.Context,\n\tl *slog.Logger,\n\tclient *http.Client,\n\turl string,\n) (data map[string]any, err error) {\n\tresp, err := client.Get(url)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"requesting: %w\", err)\n\t}\n\n\tdefer slogutil.CloseAndLog(ctx, l, resp.Body, slog.LevelError)\n\n\tif resp.StatusCode != http.StatusOK {\n\t\terr = fmt.Errorf(\"url: %q; status code: %s\", url, http.StatusText(resp.StatusCode))\n\n\t\t// Go on and download the body for inspection.\n\t}\n\n\tlimitReader := ioutil.LimitReader(resp.Body, readLimit.Bytes())\n\n\tdec := json.NewDecoder(limitReader)\n\n\tdecodeErr := dec.Decode(&data)\n\tif decodeErr != nil {\n\t\treturn nil, errors.WithDeferred(err, decodeErr)\n\t}\n\n\treturn data, validate.NotEmpty(\"response\", len(data))\n}\n\n// translationURL returns a new url.URL with provided query parameters.\nfunc translationURL(baseURL *url.URL, baseFile, projectID string, lang langCode) (uri *url.URL) {\n\turi = netutil.CloneURL(baseURL)\n\n\tq := uri.Query()\n\tq.Set(\"format\", \"json\")\n\tq.Set(\"filename\", baseFile)\n\tq.Set(\"project\", projectID)\n\tq.Set(\"language\", string(lang))\n\n\turi.RawQuery = q.Encode()\n\n\treturn uri\n}\n"
  },
  {
    "path": "scripts/translations/main.go",
    "content": "// translations downloads translations, uploads translations, prints summary\n// for translations, prints unused strings.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghos\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/AdguardTeam/golibs/osutil\"\n\t\"github.com/AdguardTeam/golibs/osutil/executil\"\n\t\"github.com/c2h5oh/datasize\"\n)\n\n// TODO(e.burkov):  Remove the default as they should be set by configuration.\nconst (\n\ttwoskyConfFile  = \"./.twosky.json\"\n\tlocalesDirHome  = \"./client/src/__locales\"\n\tdefaultBaseFile = \"en.json\"\n\tsrcDir          = \"./client/src\"\n\ttwoskyURI       = \"https://twosky.int.agrd.dev/api/v1\"\n\n\treadLimit     = 1 * datasize.MB\n\tuploadTimeout = 20 * time.Second\n)\n\n// blockerLangCodes is the codes of languages which need to be fully translated.\nvar blockerLangCodes = []langCode{\n\t\"de\",\n\t\"en\",\n\t\"es\",\n\t\"fr\",\n\t\"it\",\n\t\"ja\",\n\t\"ko\",\n\t\"pt-br\",\n\t\"pt-pt\",\n\t\"ru\",\n\t\"zh-cn\",\n\t\"zh-tw\",\n}\n\n// langCode is a language code.\ntype langCode string\n\n// languages is a map, where key is language code and value is display name.\ntype languages map[langCode]string\n\n// textlabel is a text label of localization.\ntype textLabel string\n\n// locales is a map, where key is text label and value is translation.\ntype locales map[textLabel]string\n\nfunc main() {\n\tctx := context.Background()\n\tl := slogutil.New(nil)\n\n\tif len(os.Args) == 1 {\n\t\tusage(\"need a command\")\n\t}\n\n\tif os.Args[1] == \"help\" {\n\t\tusage(\"\")\n\t}\n\n\thomeConf, servicesConf, err := readTwoskyConfig()\n\terrors.Check(err)\n\n\tvar cli *twoskyClient\n\n\tswitch os.Args[1] {\n\tcase \"summary\":\n\t\terrors.Check(summary(homeConf.Languages))\n\tcase \"download\":\n\t\tcli = errors.Must(newTwoskyClient(homeConf))\n\t\tcli.download(ctx, l)\n\n\t\tcli = errors.Must(newTwoskyClient(servicesConf))\n\t\tcli.download(ctx, l)\n\tcase \"unused\":\n\t\terrors.Check(unused(ctx, l, homeConf.LocalizableFiles[0]))\n\tcase \"upload\":\n\t\tcli = errors.Must(newTwoskyClient(homeConf))\n\t\terrors.Check(cli.upload())\n\tcase \"auto-add\":\n\t\terrors.Check(autoAdd(ctx, l, homeConf.LocalizableFiles[0]))\n\tdefault:\n\t\tusage(\"unknown command\")\n\t}\n}\n\n// usage prints usage.  If addStr is not empty print addStr and exit with code\n// 1, otherwise exit with code 0.\nfunc usage(addStr string) {\n\tconst usageStr = `Usage: go run main.go <command> [<args>]\nCommands:\n  help\n        Print usage.\n  summary\n        Print summary.\n  download [-n <count>]\n        Download translations.  count is a number of concurrent downloads.\n  unused\n        Print unused strings.\n  upload\n        Upload translations.\n  auto-add\n\t\tAdd locales with additions to the git and restore locales with\n\t\tdeletions.`\n\n\tif addStr != \"\" {\n\t\tfmt.Printf(\"%s\\n%s\\n\", addStr, usageStr)\n\n\t\tos.Exit(osutil.ExitCodeFailure)\n\t}\n\n\tfmt.Println(usageStr)\n\n\tos.Exit(osutil.ExitCodeSuccess)\n}\n\n// validateLanguageStr validates languages codes that contain in the str and\n// returns them or error.\nfunc validateLanguageStr(str string, all languages) (langs []langCode, err error) {\n\tcodes := strings.Fields(str)\n\tlangs = make([]langCode, 0, len(codes))\n\n\tfor _, k := range codes {\n\t\tlc := langCode(k)\n\t\t_, ok := all[lc]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"validating languages: unexpected language code %q\", k)\n\t\t}\n\n\t\tlangs = append(langs, lc)\n\t}\n\n\treturn langs, nil\n}\n\n// readLocales reads file with name fn and returns a map, where key is text\n// label and value is localization.\nfunc readLocales(fn string) (loc locales, err error) {\n\tb, err := os.ReadFile(fn)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, err\n\t}\n\n\tloc = make(locales)\n\terr = json.Unmarshal(b, &loc)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"unmarshalling %q: %w\", fn, err)\n\n\t\treturn nil, err\n\t}\n\n\treturn loc, nil\n}\n\n// summary prints summary for translations.\n//\n// TODO(e.burkov):  Consider making it a method of [twoskyClient] and\n// calculating summary for all configurations.\nfunc summary(langs languages) (err error) {\n\tbasePath := filepath.Join(localesDirHome, defaultBaseFile)\n\tbaseLoc, err := readLocales(basePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"summary: %w\", err)\n\t}\n\n\tsize := float64(len(baseLoc))\n\n\tfor _, lang := range slices.Sorted(maps.Keys(langs)) {\n\t\tname := filepath.Join(localesDirHome, string(lang)+\".json\")\n\t\tif name == basePath {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar loc locales\n\t\tloc, err = readLocales(name)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"summary: reading locales: %w\", err)\n\t\t}\n\n\t\tf := float64(len(loc)) * 100 / size\n\n\t\tblocker := \"\"\n\n\t\t// N is small enough to not raise performance questions.\n\t\tok := slices.Contains(blockerLangCodes, lang)\n\t\tif ok {\n\t\t\tblocker = \" (blocker)\"\n\t\t}\n\n\t\tfmt.Printf(\"%s\\t %6.2f %%%s\\n\", lang, f, blocker)\n\t}\n\n\treturn nil\n}\n\n// unused prints unused text labels.\n//\n// TODO(e.burkov):  Consider making it a method of [twoskyClient] and searching\n// unused strings for all configurations.\nfunc unused(ctx context.Context, l *slog.Logger, basePath string) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"unused: %w\") }()\n\n\tbaseLoc, err := readLocales(basePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlocDir := filepath.Clean(localesDirHome)\n\tjs, err := findJS(ctx, l, locDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn findUnused(js, baseLoc)\n}\n\n// findJS returns list of JavaScript and JSON files or error.\nfunc findJS(ctx context.Context, l *slog.Logger, locDir string) (fileNames []string, err error) {\n\twalkFn := func(name string, _ os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\tl.WarnContext(ctx, \"accessing a path\", slogutil.KeyError, err)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tif strings.HasPrefix(name, locDir) {\n\t\t\treturn nil\n\t\t}\n\n\t\text := filepath.Ext(name)\n\t\tif ext == \".js\" || ext == \".json\" {\n\t\t\tfileNames = append(fileNames, name)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr = filepath.Walk(srcDir, walkFn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"filepath walking %q: %w\", srcDir, err)\n\t}\n\n\treturn fileNames, nil\n}\n\n// findUnused prints unused text labels from fileNames.\nfunc findUnused(fileNames []string, loc locales) (err error) {\n\tknownUsed := []textLabel{\n\t\t\"blocking_mode_refused\",\n\t\t\"blocking_mode_nxdomain\",\n\t\t\"blocking_mode_custom_ip\",\n\t}\n\n\tfor _, v := range knownUsed {\n\t\tdelete(loc, v)\n\t}\n\n\tfor _, fn := range fileNames {\n\t\tvar buf []byte\n\t\tbuf, err = os.ReadFile(fn)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"finding unused: %w\", err)\n\t\t}\n\n\t\tfor k := range loc {\n\t\t\tif bytes.Contains(buf, []byte(k)) {\n\t\t\t\tdelete(loc, k)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, v := range slices.Sorted(maps.Keys(loc)) {\n\t\tfmt.Println(v)\n\t}\n\n\treturn nil\n}\n\n// autoAdd adds locales with additions to the git and restores locales with\n// deletions.\nfunc autoAdd(ctx context.Context, l *slog.Logger, basePath string) (err error) {\n\tdefer func() { err = errors.Annotate(err, \"auto add: %w\") }()\n\n\tcmdCons := executil.SystemCommandConstructor{}\n\n\tadds, dels, err := changedLocales(ctx, l, cmdCons)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn err\n\t}\n\n\tif slices.Contains(dels, basePath) {\n\t\treturn errors.Error(\"base locale contains deletions\")\n\t}\n\n\terr = handleAdds(ctx, l, cmdCons, adds)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil\n\t}\n\n\terr = handleDels(ctx, l, cmdCons, dels)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n\n// gitCmd is the shell command for Git.\nconst gitCmd = \"git\"\n\n// handleAdds adds locales with additions to the git.\nfunc handleAdds(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n\tlocales []string,\n) (err error) {\n\tif len(locales) == 0 {\n\t\treturn nil\n\t}\n\n\tgitArgs := append([]string{\"add\"}, locales...)\n\tl.DebugContext(ctx, \"executing\", \"cmd\", gitCmd, \"args\", gitArgs)\n\n\tcode, out, err := aghos.RunCommand(ctx, cmdCons, gitCmd, gitArgs...)\n\n\tif err != nil || code != 0 {\n\t\treturn fmt.Errorf(\"git add exited with code %d output %q: %w\", code, out, err)\n\t}\n\n\treturn nil\n}\n\n// handleDels restores locales with deletions.\nfunc handleDels(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n\tlocales []string,\n) (err error) {\n\tif len(locales) == 0 {\n\t\treturn nil\n\t}\n\n\tgitArgs := append([]string{\"restore\"}, locales...)\n\tl.DebugContext(ctx, \"executing\", \"cmd\", gitCmd, \"args\", gitArgs)\n\n\tcode, out, err := aghos.RunCommand(ctx, cmdCons, gitCmd, gitArgs...)\n\n\tif err != nil || code != 0 {\n\t\treturn fmt.Errorf(\"git restore exited with code %d output %q: %w\", code, out, err)\n\t}\n\n\treturn nil\n}\n\n// changedLocales returns cleaned paths of locales with changes or error.  adds\n// is the list of locales with only additions.  dels is the list of locales\n// with only deletions.\nfunc changedLocales(\n\tctx context.Context,\n\tl *slog.Logger,\n\tcmdCons executil.CommandConstructor,\n) (adds, dels []string, err error) {\n\tdefer func() { err = errors.Annotate(err, \"getting changes: %w\") }()\n\n\tgitArgs := []string{\"diff\", \"--numstat\", localesDirHome}\n\tl.DebugContext(ctx, \"executing\", \"cmd\", gitCmd, \"args\", gitArgs)\n\n\t// TODO(s.chzhen):  Consider streaming the output if needed.  Using\n\t// [io.Pipe] here is unnecessary; it complicates lifecycle management\n\t// because the output must be read concurrently, and the PipeWriter must be\n\t// explicitly closed to signal EOF.  Since this command's output is small, a\n\t// bytes.Buffer via executil.Run is sufficient.\n\tvar out bytes.Buffer\n\terr = executil.Run(ctx, cmdCons, &executil.CommandConfig{\n\t\tPath:   gitCmd,\n\t\tArgs:   gitArgs,\n\t\tStdout: &out,\n\t})\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"executing cmd: %w\", err)\n\t}\n\n\tscanner := bufio.NewScanner(&out)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tfields := strings.Fields(line)\n\t\tif len(fields) < 3 {\n\t\t\treturn nil, nil, fmt.Errorf(\"invalid input: %q\", line)\n\t\t}\n\n\t\tpath := fields[2]\n\n\t\tif fields[0] == \"0\" {\n\t\t\tdels = append(dels, path)\n\t\t} else if fields[1] == \"0\" {\n\t\t\tadds = append(adds, path)\n\t\t}\n\t}\n\n\terr = scanner.Err()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"scanning: %w\", err)\n\t}\n\n\treturn adds, dels, nil\n}\n"
  },
  {
    "path": "scripts/translations/twosky.go",
    "content": "package main\n\nimport (\n\t\"cmp\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"maps\"\n\t\"net/url\"\n\t\"os\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/validate\"\n)\n\n// Constants for mapping the twosky configurations.\n//\n// Keep in sync with the .twosky.json file.\nconst (\n\t// twoskyProjectIdxHome is the index of the Home project in the localization\n\t// configuration.\n\ttwoskyProjectIdxHome = iota\n\n\t// twoskyProjectIdxServices is the index of the Services project in the\n\t// localization configuration.\n\ttwoskyProjectIdxServices\n\n\t// twoskyProjectCount is the number of projects in the localization\n\t// configuration.\n\ttwoskyProjectCount\n)\n\n// twoskyConfig is the configuration structure for localization of a single\n// project.\ntype twoskyConfig struct {\n\tLanguages        languages `json:\"languages\"`\n\tProjectID        string    `json:\"project_id\"`\n\tBaseLangcode     langCode  `json:\"base_locale\"`\n\tLocalizableFiles []string  `json:\"localizable_files\"`\n}\n\n// type check\nvar _ validate.Interface = (*twoskyConfig)(nil)\n\n// Validate implements the [validate.Interface] interface for *twoskyConfig.\nfunc (t *twoskyConfig) Validate() (err error) {\n\tif t == nil {\n\t\treturn errors.ErrNoValue\n\t}\n\n\terrs := []error{\n\t\tvalidate.NotEmpty(\"project_id\", t.ProjectID),\n\t\tvalidate.NotEmpty(\"base_locale\", t.BaseLangcode),\n\t\tvalidate.NotEmptySlice(\"localizable_files\", t.LocalizableFiles),\n\t}\n\n\tif len(t.Languages) == 0 {\n\t\terrs = append(errs, fmt.Errorf(\"languages: %w\", errors.ErrEmptyValue))\n\t}\n\n\tfor code, lang := range t.Languages {\n\t\terr = validate.NotEmpty(\"languages: \"+string(code), lang)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\treturn errors.Join(errs...)\n}\n\n// readTwoskyConfig returns twosky configuration.\nfunc readTwoskyConfig() (home, services *twoskyConfig, err error) {\n\tdefer func() { err = errors.Annotate(err, \"parsing twosky config: %w\") }()\n\n\tb, err := os.ReadFile(twoskyConfFile)\n\tif err != nil {\n\t\t// Don't wrap the error since it's informative enough as is.\n\t\treturn nil, nil, err\n\t}\n\n\tvar tsc []*twoskyConfig\n\terr = json.Unmarshal(b, &tsc)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"unmarshalling %q: %w\", twoskyConfFile, err)\n\t}\n\n\terr = validate.Equal(\"projects count\", len(tsc), twoskyProjectCount)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\terr = errors.Join(validate.AppendSlice(nil, \"projects\", tsc)...)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn tsc[twoskyProjectIdxHome], tsc[twoskyProjectIdxServices], nil\n}\n\n// twoskyClient is the twosky client with methods for download and upload\n// translations.\ntype twoskyClient struct {\n\t// uri is the base URL.\n\turi *url.URL\n\n\t// projectID is the name of the project.\n\tprojectID string\n\n\t// baseLang is the base language code.\n\tbaseLang langCode\n\n\t// langs is the list of codes of languages to download.\n\tlangs []langCode\n\n\t// localizableFiles are the files to localize.\n\tlocalizableFiles []string\n}\n\n// newTwoskyClient reads values from environment variables or defaults,\n// validates them, and returns the twosky client.\nfunc newTwoskyClient(conf *twoskyConfig) (cli *twoskyClient, err error) {\n\tdefer func() { err = errors.Annotate(err, \"filling config: %w\") }()\n\n\turiStr := cmp.Or(os.Getenv(\"TWOSKY_URI\"), twoskyURI)\n\turi, err := url.Parse(uriStr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// TODO(e.burkov):  Don't use env.\n\tprojectID := conf.ProjectID\n\tenvProjectID := os.Getenv(\"PROJECT_ID\")\n\tif envProjectID != \"\" {\n\t\tprojectID = envProjectID\n\t}\n\n\tbaseLang := conf.BaseLangcode\n\tuLangStr := os.Getenv(\"UPLOAD_LANGUAGE\")\n\tif uLangStr != \"\" {\n\t\tbaseLang = langCode(uLangStr)\n\t}\n\n\tlangs := slices.Sorted(maps.Keys(conf.Languages))\n\n\tdlLangStr := os.Getenv(\"DOWNLOAD_LANGUAGES\")\n\tif dlLangStr == \"blocker\" {\n\t\tlangs = blockerLangCodes\n\t} else if dlLangStr != \"\" {\n\t\tvar dlLangs []langCode\n\t\tdlLangs, err = validateLanguageStr(dlLangStr, conf.Languages)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tlangs = dlLangs\n\t}\n\n\treturn &twoskyClient{\n\t\turi:              uri,\n\t\tprojectID:        projectID,\n\t\tbaseLang:         baseLang,\n\t\tlangs:            langs,\n\t\tlocalizableFiles: conf.LocalizableFiles,\n\t}, nil\n}\n"
  },
  {
    "path": "scripts/translations/upload.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/AdguardTeam/AdGuardHome/internal/aghhttp\"\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/httphdr\"\n)\n\n// upload uploads the base locale file.\nfunc (c *twoskyClient) upload() (err error) {\n\tdefer func() { err = errors.Annotate(err, \"upload: %w\") }()\n\n\tuploadURI := c.uri.JoinPath(\"upload\")\n\tbasePath := filepath.Join(localesDirHome, defaultBaseFile)\n\n\tformData := map[string]string{\n\t\t\"format\":   \"json\",\n\t\t\"language\": string(c.baseLang),\n\t\t\"filename\": defaultBaseFile,\n\t\t\"project\":  c.projectID,\n\t}\n\n\tbuf, cType, err := prepareMultipartMsg(formData, basePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"preparing multipart msg: %w\", err)\n\t}\n\n\terr = send(uploadURI.String(), cType, buf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sending multipart msg: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// prepareMultipartMsg prepares translation data for upload.\nfunc prepareMultipartMsg(\n\tformData map[string]string,\n\tbasePath string,\n) (buf *bytes.Buffer, cType string, err error) {\n\tbuf = &bytes.Buffer{}\n\tw := multipart.NewWriter(buf)\n\tvar fw io.Writer\n\n\tfor _, k := range slices.Sorted(maps.Keys(formData)) {\n\t\terr = w.WriteField(k, formData[k])\n\t\tif err != nil {\n\t\t\treturn nil, \"\", fmt.Errorf(\"writing field %q: %w\", k, err)\n\t\t}\n\t}\n\n\tfile, err := os.Open(basePath)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"opening file: %w\", err)\n\t}\n\tdefer func() { err = errors.WithDeferred(err, file.Close()) }()\n\n\th := make(textproto.MIMEHeader)\n\th.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)\n\n\td := fmt.Sprintf(\"form-data; name=%q; filename=%q\", \"file\", defaultBaseFile)\n\th.Set(httphdr.ContentDisposition, d)\n\n\tfw, err = w.CreatePart(h)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"creating part: %w\", err)\n\t}\n\n\t_, err = io.Copy(fw, file)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"copying: %w\", err)\n\t}\n\n\terr = w.Close()\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"closing writer: %w\", err)\n\t}\n\n\treturn buf, w.FormDataContentType(), nil\n}\n\n// send POST request to uriStr.\nfunc send(uriStr, cType string, buf *bytes.Buffer) (err error) {\n\tclient := http.Client{\n\t\tTimeout: uploadTimeout,\n\t}\n\n\treq, err := http.NewRequest(http.MethodPost, uriStr, buf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bad request: %w\", err)\n\t}\n\n\treq.Header.Set(httphdr.ContentType, cType)\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"client post form: %w\", err)\n\t}\n\n\tdefer func() {\n\t\terr = errors.WithDeferred(err, resp.Body.Close())\n\t}()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"status code is not ok: %q\", http.StatusText(resp.StatusCode))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "scripts/vetted-filters/main.go",
    "content": "// vetted-filters fetches the most recent Hostlists Registry filtering rule list\n// index and transforms the filters from it to AdGuard Home's format.\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/AdguardTeam/golibs/errors\"\n\t\"github.com/AdguardTeam/golibs/logutil/slogutil\"\n\t\"github.com/google/renameio/v2/maybe\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\tl := slogutil.New(nil)\n\n\turlStr := \"https://adguardteam.github.io/HostlistsRegistry/assets/filters.json\"\n\tif s := os.Getenv(\"URL\"); s != \"\" {\n\t\turlStr = s\n\t}\n\n\t// Validate the URL.\n\t_, err := url.Parse(urlStr)\n\terrors.Check(err)\n\n\tc := &http.Client{\n\t\tTimeout: 10 * time.Second,\n\t}\n\n\tresp := errors.Must(c.Get(urlStr))\n\tdefer slogutil.CloseAndLog(ctx, l, resp.Body, slog.LevelError)\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tpanic(fmt.Errorf(\"expected code %d, got %d\", http.StatusOK, resp.StatusCode))\n\t}\n\n\thlFlt := &hlFilters{}\n\terr = json.NewDecoder(resp.Body).Decode(hlFlt)\n\terrors.Check(err)\n\n\taghFlt := &aghFilters{\n\t\tCategories: map[string]*aghFiltersCategory{\n\t\t\t\"general\": {\n\t\t\t\tName:        \"filter_category_general\",\n\t\t\t\tDescription: \"filter_category_general_desc\",\n\t\t\t},\n\t\t\t\"other\": {\n\t\t\t\tName:        \"filter_category_other\",\n\t\t\t\tDescription: \"filter_category_other_desc\",\n\t\t\t},\n\t\t\t\"regional\": {\n\t\t\t\tName:        \"filter_category_regional\",\n\t\t\t\tDescription: \"filter_category_regional_desc\",\n\t\t\t},\n\t\t\t\"security\": {\n\t\t\t\tName:        \"filter_category_security\",\n\t\t\t\tDescription: \"filter_category_security_desc\",\n\t\t\t},\n\t\t},\n\t\tFilters: map[string]*aghFiltersFilter{},\n\t}\n\n\tfor i, f := range hlFlt.Filters {\n\t\tkey := f.FilterKey\n\t\tcat := f.category()\n\t\tif cat == \"\" {\n\t\t\tl.WarnContext(ctx, \"no fitting category for filter\", \"key\", key, \"idx\", i)\n\t\t}\n\n\t\taghFlt.Filters[key] = &aghFiltersFilter{\n\t\t\tName:       f.Name,\n\t\t\tCategoryID: cat,\n\t\t\tHomepage:   f.Homepage,\n\t\t\t// NOTE: The source URL in filters.json is not guaranteed to contain\n\t\t\t// the URL of the filtering rule list.  So, use our mirror for the\n\t\t\t// vetted blocklists, which are mostly guaranteed to be valid and\n\t\t\t// available lists.\n\t\t\tSource: f.DownloadURL,\n\t\t}\n\t}\n\n\tbuf := &bytes.Buffer{}\n\t_, _ = buf.WriteString(jsHeader)\n\n\tenc := json.NewEncoder(buf)\n\tenc.SetIndent(\"\", \"    \")\n\n\terrors.Check(enc.Encode(aghFlt))\n\n\terr = maybe.WriteFile(\"client/src/helpers/filters/filters.ts\", buf.Bytes(), 0o644)\n\terrors.Check(err)\n}\n\n// jsHeader is the header for the generated JavaScript file.  It informs the\n// reader that the file is generated and disables some style-related eslint\n// checks.\nconst jsHeader = `// Code generated by go run ./scripts/vetted-filters/main.go; DO NOT EDIT.\n\n/* eslint quote-props: 'off', quotes: 'off', comma-dangle: 'off', semi: 'off' */\n\nexport default `\n\n// hlFilters is the JSON structure for the Hostlists Registry rule list index.\ntype hlFilters struct {\n\tFilters []*hlFiltersFilter `json:\"filters\"`\n}\n\n// hlFiltersFilter is the JSON structure for a filter in the Hostlists Registry.\ntype hlFiltersFilter struct {\n\tDownloadURL string `json:\"downloadUrl\"`\n\tFilterKey   string `json:\"filterKey\"`\n\tHomepage    string `json:\"homepage\"`\n\tName        string `json:\"name\"`\n\tTags        []int  `json:\"tags\"`\n}\n\n// Known tag IDs.  Keep in sync with tags/metadata.json in the source repo.\nconst (\n\ttagIDGeneral  = 1\n\ttagIDSecurity = 2\n\ttagIDRegional = 3\n\ttagIDOther    = 4\n)\n\n// category returns the AdGuard Home category for this filter.  If there is no\n// fitting category, cat is empty.\nfunc (f *hlFiltersFilter) category() (cat string) {\n\tfor _, t := range f.Tags {\n\t\tswitch t {\n\t\tcase tagIDGeneral:\n\t\t\treturn \"general\"\n\t\tcase tagIDSecurity:\n\t\t\treturn \"security\"\n\t\tcase tagIDRegional:\n\t\t\treturn \"regional\"\n\t\tcase tagIDOther:\n\t\t\treturn \"other\"\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// aghFilters is the JSON structure for AdGuard Home's list of vetted filtering\n// rule list in file client/src/helpers/filters/filters.ts.\ntype aghFilters struct {\n\tCategories map[string]*aghFiltersCategory `json:\"categories\"`\n\tFilters    map[string]*aghFiltersFilter   `json:\"filters\"`\n}\n\n// aghFiltersCategory is the JSON structure for a category in the vetted\n// filtering rule list file.\ntype aghFiltersCategory struct {\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n}\n\n// aghFiltersFilter is the JSON structure for a filter in the vetted filtering\n// rule list file.\ntype aghFiltersFilter struct {\n\tName       string `json:\"name\"`\n\tCategoryID string `json:\"categoryId\"`\n\tHomepage   string `json:\"homepage\"`\n\tSource     string `json:\"source\"`\n}\n"
  },
  {
    "path": "snap/gui/adguard-home-web.desktop",
    "content": "[Desktop Entry]\nType=Application\nEncoding=UTF-8\nName=AdGuard Home\nComment=Network-wide ads & trackers blocking DNS server\nExec=adguard-home.adguard-home-web\nIcon=${SNAP}/meta/gui/adguard-home-web.png\nTerminal=false\n"
  },
  {
    "path": "snap/local/adguard-home-web.sh",
    "content": "#!/bin/sh\n\n# shellcheck disable=SC2154\nconf_file=\"${SNAP_DATA}/AdGuardHome.yaml\"\nreadonly conf_file\n\nif ! [ -f \"$conf_file\" ]; then\n\txdg-open 'http://localhost:3000'\n\n\texit\nfi\n\n# Get the admin interface port from the configuration.\n#\n# shellcheck disable=SC2016\nawk_prog='/^[^[:space:]]/ { is_http = /^http:/ };/^[[:space:]]+address:/ { if (is_http) print $2 }'\nreadonly awk_prog\n\nbind_port=\"$(awk \"$awk_prog\" \"$conf_file\" | awk -F ':' '{print $NF}')\"\nreadonly bind_port\n\nif [ \"$bind_port\" = '' ]; then\n\txdg-open 'http://localhost:3000'\nelse\n\txdg-open \"http://localhost:${bind_port}\"\nfi\n"
  },
  {
    "path": "snap/snap.tmpl.yaml",
    "content": "# The %VARIABLES% are be replaced by actual values by the build script.\n\n'name': 'adguard-home'\n'base': 'core22'\n'version': '%VERSION%'\n'summary': Network-wide ads & trackers blocking DNS server\n'description': |\n  AdGuard Home is a network-wide software for blocking ads & tracking. After\n  you set it up, it'll cover ALL your home devices, and you don't need any\n  client-side software for that.\n\n  It operates as a DNS server that re-routes tracking domains to a \"black hole,\"\n  thus preventing your devices from connecting to those servers. It's based\n  on software we use for our public AdGuard DNS servers -- both share a lot\n  of common code.\n'grade': 'stable'\n'confinement': 'strict'\n\n'architectures':\n- '%ARCH%'\n\n'apps':\n  'adguard-home':\n    'command': 'AdGuardHome --no-check-update -w $SNAP_DATA'\n    'plugs':\n    # Add the \"network-bind\" plug to bind to interfaces.\n    - 'network-bind'\n    # Add the \"network-observe\" plug to be able to bind to ports below 1024\n    # (cap_net_bind_service) and also to bind to a particular interface using\n    # SO_BINDTODEVICE (cap_net_raw).\n    - 'network-observe'\n    # Add the \"network-control\" plug to be able to use raw sockets in the DHCP\n    # server.\n    #\n    # TODO(a.garipov): If this works, request auto-connect of this plug.\n    - 'network-control'\n    'daemon': 'simple'\n    'restart-condition': 'always'\n  'adguard-home-web':\n    'command': 'adguard-home-web.sh'\n    'plugs':\n    - 'desktop'\n"
  },
  {
    "path": "staticcheck.conf",
    "content": "checks = [\"all\"]\ninitialisms = [\n  # See https://github.com/dominikh/go-tools/blob/master/config/config.go.\n  #\n  # Do not add \"PTR\" since we use \"Ptr\" as a suffix.\n  \"inherit\"\n, \"ASN\"\n, \"DHCP\"\n, \"DNSSEC\"\n  # E.g. SentryDSN.\n, \"DSN\"\n, \"ECS\"\n, \"EDNS\"\n, \"MX\"\n, \"QUIC\"\n, \"RA\"\n, \"RRSIG\"\n, \"SDNS\"\n, \"SLAAC\"\n, \"SOA\"\n, \"SVCB\"\n, \"TLD\"\n, \"WHOIS\"\n]\ndot_import_whitelist = []\nhttp_status_code_whitelist = []\n"
  }
]